LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
LineOptSolver.m
1classdef LineOptSolver < handle
2 % LineOptSolver Main line-opt solver. Minimizes a penalized scalar
3 % objective (constraints as penalties) over the decision variables,
4 % aggregating across scenarios, using the self-contained
5 % opt.de.DifferentialEvolution engine (numpy-exact RNG) or an analytic/FD
6 % projected-gradient path. Mirrors native-Python LineOptSolver.
7
8 properties
9 problem
10 opt
11 fixedValueMap
12 freeVariables % cell of opt.DecisionVariable (post-freeze)
13 evaluators % cell of opt.LineEvaluator
14 scenarioWeights
15 caches % cell of containers.Map
16 iterations = 0;
17 bestValue = inf;
18 bestX = [];
19 convergenceHistory = [];
20 startTime
21 deadline = inf;
22 lqnSensCache % Map valuesKey -> sensitivity Map (or [])
23 gradCalls = 0;
24 end
25
26 methods
27 function obj = LineOptSolver(problem, options)
28 obj.problem = problem;
29 obj.opt = options;
30 obj.fixedValueMap = containers.Map('KeyType', 'char', 'ValueType', 'any');
31 fixed = problem.getFixedVariables();
32 for k = 1:numel(fixed)
33 obj.fixedValueMap(fixed{k}{1}.getName()) = fixed{k}{2};
34 end
35
36 % Explicit layer freezing: variables whose layer is in frozenLayers
37 % are held at the model's current parameter value (moved from free
38 % to fixed) instead of being optimized. See freezeLayers.
39 freeVars = problem.getVariables();
40 [freeVars, fixed] = obj.freezeLayers(freeVars, fixed);
41 obj.freeVariables = freeVars;
42
43 obj.evaluators = {opt.LineEvaluator(problem.getModel(), freeVars, fixed)};
44 scen = problem.getScenarios();
45 w = zeros(1, 1 + numel(scen));
46 w(1) = 1.0;
47 for i = 1:numel(scen)
48 obj.evaluators{end+1} = opt.LineEvaluator(scen{i}{1}, freeVars, fixed);
49 w(i+1) = scen{i}{2};
50 end
51 obj.scenarioWeights = w;
52 obj.lqnSensCache = containers.Map('KeyType', 'char', 'ValueType', 'any');
53 end
54
55 function [freeVars, fixed] = freezeLayers(obj, freeVars, fixed)
56 % Partition variables by the frozenLayers option (LQN only). A
57 % variable whose layer set (DecisionVariable.getLayer) intersects
58 % the frozen set is moved from free to fixed, held at its current
59 % model parameter value (DecisionVariable.currentValue). If the
60 % current value cannot be read the variable is dropped from the
61 % optimization, leaving the model's built-in value untouched.
62 % No-op for a flat model or when no layers are frozen.
63 frozen = obj.opt.frozenLayers;
64 if isempty(frozen), return; end
65 model = obj.problem.getModel();
66 if ~opt.Layered.isLayered(model), return; end
67
68 already = {};
69 for k = 1:numel(fixed), already{end+1} = fixed{k}{1}.getName(); end %#ok<AGROW>
70 kept = {};
71 for i = 1:numel(freeVars)
72 var = freeVars{i};
73 layers = var.getLayer(model);
74 if ~isempty(layers) && any(ismember(layers, frozen))
75 if any(strcmp(var.getName(), already))
76 continue;
77 end
78 value = var.currentValue(model);
79 if ~isempty(value)
80 fixed{end+1} = {var, value}; %#ok<AGROW>
81 obj.fixedValueMap(var.getName()) = value;
82 end
83 % else: drop; the model keeps its built-in value
84 else
85 kept{end+1} = var; %#ok<AGROW>
86 end
87 end
88 freeVars = kept;
89 end
90
91 function result = solve(obj)
92 obj.startTime = tic;
93 obj.iterations = 0;
94 obj.bestValue = inf;
95 obj.bestX = [];
96 obj.convergenceHistory = [];
97 obj.caches = cell(1, numel(obj.evaluators));
98 for i = 1:numel(obj.evaluators)
99 obj.caches{i} = containers.Map('KeyType', 'char', 'ValueType', 'any');
100 end
101 obj.lqnSensCache = containers.Map('KeyType', 'char', 'ValueType', 'any');
102 obj.gradCalls = 0;
103 obj.deadline = obj.opt.timeLimit;
104
105 bounds = obj.evaluators{1}.getBounds();
106 if isempty(bounds)
107 result = obj.buildEmptyResult();
108 return;
109 end
110 if obj.shouldUseGradient()
111 result = obj.solveGradient(bounds);
112 else
113 result = obj.solveEvolution(bounds);
114 end
115 end
116
117 function result = solveEvolution(obj, bounds)
118 low = bounds(:, 1).';
119 high = bounds(:, 2).';
120 if isempty(obj.opt.seed)
121 seed = mod(int64(feature('timing','cpucount')), 2^31);
122 else
123 seed = obj.opt.seed;
124 end
125 de = opt.de.DifferentialEvolution(@(x) obj.objectiveFunction(x), low, high, ...
126 obj.opt.strategy, obj.opt.popsize, obj.opt.maxIterations, ...
127 obj.opt.mutationLow, obj.opt.mutationHigh, obj.opt.recombination, obj.opt.tol, seed);
128 de.callback = @(bx, nit) obj.deCallback(nit);
129
130 timedOut = false;
131 try
132 r = de.solve();
133 resX = obj.engineBestVector(r);
134 resFun = r.fun;
135 catch ME
136 if strcmp(ME.identifier, 'LineOpt:TimeLimit')
137 timedOut = true;
138 if isempty(obj.bestX)
139 result = obj.buildEmptyResult(); return;
140 end
141 resX = obj.bestX; resFun = obj.bestValue;
142 else
143 rethrow(ME);
144 end
145 end
146 solveTime = toc(obj.startTime);
147 result = obj.buildResult(resX, resFun, solveTime);
148 if timedOut, result.terminatedBy = 'time_limit'; end
149 end
150
151 function stop = deCallback(obj, nit)
152 obj.iterations = nit;
153 obj.convergenceHistory(end+1) = obj.bestValue;
154 stop = toc(obj.startTime) >= obj.deadline;
155 end
156
157 function x = engineBestVector(obj, r)
158 if ~isempty(obj.bestX) && obj.bestValue <= r.fun
159 x = obj.bestX;
160 else
161 x = r.x;
162 end
163 end
164
165 function total = objectiveFunction(obj, x)
166 if toc(obj.startTime) >= obj.deadline
167 error('LineOpt:TimeLimit', 'time limit reached');
168 end
169 values = obj.evaluators{1}.decodeVariables(x);
170 allValues = obj.mergeValues(values);
171 objective = obj.problem.getObjective();
172 pw = obj.opt.penaltyWeight;
173 cons = obj.problem.getConstraints();
174 scenarioValues = zeros(1, numel(obj.evaluators));
175 for i = 1:numel(obj.evaluators)
176 res = obj.evaluators{i}.evaluateValuesWithCache(values, obj.caches{i});
177 if ~res.feasible
178 total = inf; return;
179 end
180 value = objective.evaluateWithPenalty(res, allValues, pw);
181 for c = 1:numel(cons)
182 value = value + cons{c}.evaluate(res, allValues) * pw;
183 end
184 scenarioValues(i) = value;
185 end
186 total = obj.aggregateScenarios(scenarioValues);
187 if total < obj.bestValue
188 obj.bestValue = total;
189 obj.bestX = x;
190 end
191 end
192
193 function av = mergeValues(obj, values)
194 % merge: start from fixed, overlay values
195 av = containers.Map('KeyType', 'char', 'ValueType', 'any');
196 fk = keys(obj.fixedValueMap);
197 for i = 1:numel(fk), av(fk{i}) = obj.fixedValueMap(fk{i}); end
198 vk = keys(values);
199 for i = 1:numel(vk), av(vk{i}) = values(vk{i}); end
200 end
201
202 function total = aggregateScenarios(obj, values)
203 if numel(values) == 1
204 total = values(1); return;
205 end
206 if strcmp(obj.opt.scenarioAggregation, 'mean')
207 total = sum(obj.scenarioWeights .* values) / sum(obj.scenarioWeights);
208 else
209 total = max(values);
210 end
211 end
212
213 function tf = shouldUseGradient(obj)
214 if strcmp(obj.opt.optimizer, 'gradient'), tf = true; return; end
215 if strcmp(obj.opt.optimizer, 'evolution'), tf = false; return; end
216 tf = obj.allContinuous();
217 end
218
219 function tf = allContinuous(obj)
220 % Continuous (differentiable) types, incl. the continuous LQN knobs
221 % host demand and think time.
222 vars = obj.freeVariables;
223 if isempty(vars), tf = false; return; end
224 tf = true;
225 for i = 1:numel(vars)
226 vt = vars{i}.getVariableType();
227 if ~any(strcmp(vt, {'service_rate', 'routing', ...
228 'host_demand', 'think_time'}))
229 tf = false; return;
230 end
231 end
232 end
233
234 function result = solveGradient(obj, bounds)
235 dim = size(bounds, 1);
236 nStart = max(1, obj.opt.gradientRestarts);
237 if isempty(obj.opt.seed), sd = 0; else, sd = obj.opt.seed; end
238 rs = RandStream('mt19937ar', 'Seed', sd);
239 starts = {0.5 * ones(1, dim)};
240 for s = 2:nStart, starts{end+1} = rand(rs, 1, dim); end %#ok<AGROW>
241 best = []; bestFun = inf; timedOut = false;
242 for si = 1:numel(starts)
243 if toc(obj.startTime) >= obj.deadline, timedOut = true; break; end
244 try
245 xx = obj.projectedGradientDescent(starts{si});
246 fx = obj.objectiveFunction(xx);
247 if isfinite(fx) && fx < bestFun, bestFun = fx; best = xx; end
248 catch ME
249 if strcmp(ME.identifier, 'LineOpt:TimeLimit'), timedOut = true; break; else, rethrow(ME); end
250 end
251 end
252 if isempty(best) || (~isempty(obj.bestX) && obj.bestValue < bestFun)
253 if ~isempty(obj.bestX), best = obj.bestX; bestFun = obj.bestValue;
254 elseif isempty(best), result = obj.buildEmptyResult(); return; end
255 end
256 solveTime = toc(obj.startTime);
257 result = obj.buildResult(best, bestFun, solveTime);
258 if timedOut, result.terminatedBy = 'time_limit'; end
259 end
260
261 function x = projectedGradientDescent(obj, x0)
262 x = min(max(x0, 0), 1);
263 f = obj.objectiveFunction(x);
264 for iter = 1:obj.opt.maxIterations
265 g = obj.objectiveGradient(x);
266 if norm(g) < 1e-9, break; end
267 step = 1.0; improved = false;
268 for ls = 1:30
269 xn = min(max(x - step * g, 0), 1);
270 fn = obj.objectiveFunction(xn);
271 if isfinite(fn) && fn < f - 1e-12
272 x = xn; f = fn; improved = true; break;
273 end
274 step = step * 0.5;
275 end
276 if ~improved, break; end
277 end
278 end
279
280 function g = objectiveGradient(obj, x)
281 % Gradient of the penalized objective in encoded space. For a
282 % LayeredNetwork the source is selected by lqnGradient: 'fd'
283 % (whole-model finite difference, robust default), 'partial_sens'
284 % (SolverLN per-layer partial derivatives, cheap/biased), or
285 % 'partial_plus_fd' (partial with a periodic full-FD correction).
286 % A flat network always finite-differences. All paths fall back to
287 % finite differences, which always work.
288 if obj.evaluators{1}.isLayered
289 mode = obj.opt.lqnGradient;
290 if any(strcmp(mode, {'partial_sens', 'partial_plus_fd'}))
291 obj.gradCalls = obj.gradCalls + 1;
292 refresh = max(1, obj.opt.fdRefresh);
293 % partial_plus_fd periodically replaces the biased partial
294 % direction with the correct whole-model finite difference.
295 if ~(strcmp(mode, 'partial_plus_fd') && mod(obj.gradCalls, refresh) == 0)
296 g = obj.lqnAnalyticGradient(x);
297 if ~isempty(g), return; end
298 end
299 end
300 g = obj.finiteDifferenceGradient(x);
301 return;
302 end
303 g = obj.finiteDifferenceGradient(x);
304 end
305
306 function g = finiteDifferenceGradient(obj, x)
307 % Central finite-difference gradient of the penalized scalar
308 % objective. Works for any model or solver (for a LayeredNetwork
309 % each perturbed evaluation re-solves the whole ensemble, giving
310 % the correct total derivative). One-sided differences near an
311 % infeasible/unstable boundary where a two-sided value is non-finite.
312 h = obj.opt.fdStep;
313 dim = numel(x);
314 g = zeros(1, dim);
315 f0 = [];
316 for i = 1:dim
317 xp = x; xm = x;
318 xp(i) = min(1.0, x(i) + h);
319 xm(i) = max(0.0, x(i) - h);
320 fp = obj.objectiveFunction(xp);
321 fm = obj.objectiveFunction(xm);
322 if isfinite(fp) && isfinite(fm) && xp(i) > xm(i)
323 g(i) = (fp - fm) / (xp(i) - xm(i)); continue;
324 end
325 if isempty(f0), f0 = obj.objectiveFunction(x); end
326 if isfinite(fp) && isfinite(f0) && xp(i) > x(i)
327 g(i) = (fp - f0) / (xp(i) - x(i));
328 elseif isfinite(fm) && isfinite(f0) && x(i) > xm(i)
329 g(i) = (f0 - fm) / (x(i) - xm(i));
330 else
331 g(i) = 0.0;
332 end
333 end
334 end
335
336 function g = lqnAnalyticGradient(obj, x)
337 % Partial-sensitivity gradient for a LayeredNetwork ([] to fall back
338 % to the whole-model finite difference). Assembles d(objective)/dx
339 % from SolverLN's per-layer WITHIN-LAYER service-rate derivatives,
340 % WITHOUT re-solving per parameter: (i) read d(layer metric)/d(rate)
341 % at the variable's host-layer row, (ii) map each layer metric to
342 % the LQN node metric it approximates and take d(penalized scalar)/
343 % d(that node metric) by cheap metric-space finite differences,
344 % (iii) chain through d(rate)/d(demand) = -1/D^2 and the linear
345 % decode. Returns [] when multi-scenario, any variable is not a
346 % host-demand variable, or the sensitivity table is unavailable.
347 % BIASED (omits cross-layer coupling); fd/partial_plus_fd correct it.
348 g = [];
349 vars = obj.freeVariables;
350 if numel(obj.evaluators) ~= 1 || isempty(vars), return; end
351 for i = 1:numel(vars)
352 if ~strcmp(vars{i}.getVariableType(), 'host_demand') ...
353 || ~ismethod(vars{i}, 'sensKey')
354 return;
355 end
356 end
357
358 values = obj.evaluators{1}.decodeVariables(x);
359 allValues = obj.mergeValues(values);
360 res = obj.evaluators{1}.evaluateValuesWithCache(values, obj.caches{1});
361 if ~res.feasible, return; end
362
363 skey = opt.LineEvaluator.valuesKey(values);
364 if isKey(obj.lqnSensCache, skey)
365 sens = obj.lqnSensCache(skey);
366 else
367 sens = obj.evaluators{1}.evaluateLayeredSensitivities(values);
368 obj.lqnSensCache(skey) = sens;
369 end
370 if isempty(sens), return; end
371
372 objective = obj.problem.getObjective();
373 cons = obj.problem.getConstraints();
374 pw = obj.opt.penaltyWeight;
375 h = obj.opt.fdStep;
376 model = obj.problem.getModel();
377 kinds = {'Tput', 'RespT', 'QLen', 'Util'};
378
379 grad = zeros(1, numel(x));
380 for i = 1:numel(vars)
381 var = vars{i};
382 kv = var.sensKey(model);
383 if isempty(kv) || ~isKey(sens, kv)
384 % No sensitivity row: leave 0 (other components still make
385 % progress; the FD modes cover it fully).
386 continue;
387 end
388 row = sens(kv);
389 targets = var.sensMetricTargets(model);
390 dS_drate = 0.0;
391 for kk = 1:numel(kinds)
392 kind = kinds{kk};
393 dmetric_drate = row.(kind);
394 if dmetric_drate == 0, continue; end
395 mkey = targets.(kind);
396 dS_dmetric = obj.scalarMetricDerivative(res, allValues, ...
397 kind, mkey, objective, cons, pw, h);
398 dS_drate = dS_drate + dS_dmetric * dmetric_drate;
399 end
400 demand = allValues(var.getName());
401 if isnumeric(demand)
402 rateJac = var.rateJacobian(demand);
403 else
404 rateJac = 0.0;
405 end
406 grad(i) = dS_drate * rateJac * var.decodeJacobian(x(i));
407 end
408 g = grad;
409 end
410
411 function d = scalarMetricDerivative(obj, res, allValues, kind, mkey, ...
412 objective, cons, pw, h)
413 % d(penalized scalar)/d(node metric[kind][mkey]) by central FD in
414 % metric space (pure arithmetic, no solving).
415 d = 0.0;
416 if isempty(mkey), return; end
417 m = obj.metricMapFor(res, kind);
418 if isempty(m) || ~isKey(m, mkey), return; end
419 base = m(mkey);
420 m(mkey) = base + h;
421 fp = obj.scalarObjective(res, allValues, objective, cons, pw);
422 m(mkey) = base - h;
423 fm = obj.scalarObjective(res, allValues, objective, cons, pw);
424 m(mkey) = base;
425 d = (fp - fm) / (2.0 * h);
426 end
427
428 function v = scalarObjective(~, res, allValues, objective, cons, pw)
429 v = objective.evaluateWithPenalty(res, allValues, pw);
430 for c = 1:numel(cons)
431 v = v + cons{c}.evaluate(res, allValues) * pw;
432 end
433 end
434
435 function m = metricMapFor(~, res, kind)
436 switch kind
437 case 'RespT', m = res.responseTimes;
438 case 'QLen', m = res.queueLengths;
439 case 'Tput', m = res.throughputs;
440 case 'Util', m = res.utilizations;
441 otherwise, m = [];
442 end
443 end
444
445 function result = buildResult(obj, x, objectiveValue, solveTime)
446 result = opt.OptimizationResult();
447 result.objectiveValue = objectiveValue;
448 vals = obj.evaluators{1}.decodeVariables(x);
449 vk = keys(vals);
450 for i = 1:numel(vk), result.variableValues(vk{i}) = vals(vk{i}); end
451 result.iterations = obj.iterations;
452 result.solveTime = solveTime;
453 evals = 0;
454 for i = 1:numel(obj.evaluators), evals = evals + obj.evaluators{i}.getEvaluationCount(); end
455 result.modelEvaluations = evals;
456 result.convergenceHistory = obj.convergenceHistory;
457
458 allValues = obj.mergeValues(result.variableValues);
459 objective = obj.problem.getObjective();
460 allCons = [objective.getConstraints(), obj.problem.getConstraints()];
461 result.feasible = true;
462 for i = 1:numel(obj.evaluators)
463 er = obj.evaluators{i}.evaluateValuesWithCache(result.variableValues, obj.caches{i});
464 if ~er.feasible, result.feasible = false; continue; end
465 for c = 1:numel(allCons)
466 viol = allCons{c}.evaluate(er, allValues);
467 if viol > 0
468 nm = allCons{c}.getName();
469 prev = 0.0; if isKey(result.constraintViolations, nm), prev = result.constraintViolations(nm); end
470 result.constraintViolations(nm) = max(viol, prev);
471 result.feasible = false;
472 end
473 end
474 end
475 elapsed = toc(obj.startTime);
476 if elapsed >= obj.opt.timeLimit
477 result.terminatedBy = 'time_limit';
478 elseif obj.iterations >= obj.opt.maxIterations
479 result.terminatedBy = 'iterations';
480 else
481 result.terminatedBy = 'convergence';
482 end
483 end
484
485 function result = buildEmptyResult(obj) %#ok<MANU>
486 result = opt.OptimizationResult();
487 result.objectiveValue = 0.0;
488 result.feasible = true;
489 result.terminatedBy = 'empty';
490 end
491 end
492end