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.
12 evaluators % cell of opt.LineEvaluator
14 caches % cell of containers.Map
18 convergenceHistory = [];
24 function obj = LineOptSolver(problem, options)
25 obj.problem = problem;
27 obj.fixedValueMap = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
28 fixed = problem.getFixedVariables();
29 for k = 1:numel(fixed)
30 obj.fixedValueMap(fixed{k}{1}.getName()) = fixed{k}{2};
32 obj.evaluators = {opt.LineEvaluator(problem.getModel(), problem.getVariables(), fixed)};
33 scen = problem.getScenarios();
34 w = zeros(1, 1 + numel(scen));
37 obj.evaluators{end+1} = opt.LineEvaluator(scen{i}{1}, problem.getVariables(), fixed);
40 obj.scenarioWeights = w;
43 function result = solve(obj)
48 obj.convergenceHistory = [];
49 obj.caches = cell(1, numel(obj.evaluators));
50 for i = 1:numel(obj.evaluators)
51 obj.caches{i} = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
53 obj.deadline = obj.opt.timeLimit;
55 bounds = obj.evaluators{1}.getBounds();
57 result = obj.buildEmptyResult();
60 if obj.shouldUseGradient()
61 result = obj.solveGradient(bounds);
63 result = obj.solveEvolution(bounds);
67 function result = solveEvolution(obj, bounds)
69 high = bounds(:, 2).';
70 if isempty(obj.opt.seed)
71 seed = mod(int64(feature('timing','cpucount')), 2^31);
75 de = opt.de.DifferentialEvolution(@(x) obj.objectiveFunction(x), low, high, ...
76 obj.opt.strategy, obj.opt.popsize, obj.opt.maxIterations, ...
77 obj.opt.mutationLow, obj.opt.mutationHigh, obj.opt.recombination, obj.opt.tol, seed);
78 de.callback = @(bx, nit) obj.deCallback(nit);
83 resX = obj.engineBestVector(r);
86 if strcmp(ME.identifier, 'LineOpt:TimeLimit')
89 result = obj.buildEmptyResult(); return;
91 resX = obj.bestX; resFun = obj.bestValue;
96 solveTime = toc(obj.startTime);
97 result = obj.buildResult(resX, resFun, solveTime);
98 if timedOut, result.terminatedBy = 'time_limit'; end
101 function stop = deCallback(obj, nit)
102 obj.iterations = nit;
103 obj.convergenceHistory(end+1) = obj.bestValue;
104 stop = toc(obj.startTime) >= obj.deadline;
107 function x = engineBestVector(obj, r)
108 if ~isempty(obj.bestX) && obj.bestValue <= r.fun
115 function total = objectiveFunction(obj, x)
116 if toc(obj.startTime) >= obj.deadline
117 error('LineOpt:TimeLimit', 'time limit reached');
119 values = obj.evaluators{1}.decodeVariables(x);
120 allValues = obj.mergeValues(values);
121 objective = obj.problem.getObjective();
122 pw = obj.opt.penaltyWeight;
123 cons = obj.problem.getConstraints();
124 scenarioValues = zeros(1, numel(obj.evaluators));
125 for i = 1:numel(obj.evaluators)
126 res = obj.evaluators{i}.evaluateValuesWithCache(values, obj.caches{i});
130 value = objective.evaluateWithPenalty(res, allValues, pw);
131 for c = 1:numel(cons)
132 value = value + cons{c}.evaluate(res, allValues) * pw;
134 scenarioValues(i) = value;
136 total = obj.aggregateScenarios(scenarioValues);
137 if total < obj.bestValue
138 obj.bestValue = total;
143 function av = mergeValues(obj, values)
144 % merge: start from fixed, overlay values
145 av = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
146 fk = keys(obj.fixedValueMap);
147 for i = 1:numel(fk), av(fk{i}) = obj.fixedValueMap(fk{i}); end
149 for i = 1:numel(vk), av(vk{i}) = values(vk{i}); end
152 function total = aggregateScenarios(obj, values)
153 if numel(values) == 1
154 total = values(1);
return;
156 if strcmp(obj.opt.scenarioAggregation,
'mean')
157 total = sum(obj.scenarioWeights .* values) / sum(obj.scenarioWeights);
163 function tf = shouldUseGradient(obj)
164 if strcmp(obj.opt.optimizer, 'gradient'), tf = true; return; end
165 if strcmp(obj.opt.optimizer, 'evolution'), tf = false; return; end
166 tf = obj.allContinuous();
169 function tf = allContinuous(obj)
170 vars = obj.problem.getVariables();
171 if isempty(vars), tf = false; return; end
173 for i = 1:numel(vars)
174 vt = vars{i}.getVariableType();
175 if ~any(strcmp(vt, {
'service_rate',
'routing'}))
181 function result = solveGradient(obj, bounds)
182 dim = size(bounds, 1);
183 nStart = max(1, obj.opt.gradientRestarts);
184 if isempty(obj.opt.seed), sd = 0; else, sd = obj.opt.seed; end
185 rs = RandStream('mt19937ar', 'Seed', sd);
186 starts = {0.5 * ones(1, dim)};
187 for s = 2:nStart, starts{end+1} = rand(rs, 1, dim); end %#ok<AGROW>
188 best = []; bestFun = inf; timedOut =
false;
189 for si = 1:numel(starts)
190 if toc(obj.startTime) >= obj.deadline, timedOut =
true;
break; end
192 xx = obj.projectedGradientDescent(starts{si});
193 fx = obj.objectiveFunction(xx);
194 if isfinite(fx) && fx < bestFun, bestFun = fx; best = xx; end
196 if strcmp(ME.identifier,
'LineOpt:TimeLimit'), timedOut =
true;
break;
else, rethrow(ME); end
199 if isempty(best) || (~isempty(obj.bestX) && obj.bestValue < bestFun)
200 if ~isempty(obj.bestX), best = obj.bestX; bestFun = obj.bestValue;
201 elseif isempty(best), result = obj.buildEmptyResult();
return; end
203 solveTime = toc(obj.startTime);
204 result = obj.buildResult(best, bestFun, solveTime);
205 if timedOut, result.terminatedBy =
'time_limit'; end
208 function x = projectedGradientDescent(obj, x0)
209 x = min(max(x0, 0), 1);
210 f = obj.objectiveFunction(x);
211 for iter = 1:obj.opt.maxIterations
212 g = obj.objectiveGradient(x);
213 if norm(g) < 1e-9,
break; end
214 step = 1.0; improved =
false;
216 xn = min(max(x - step * g, 0), 1);
217 fn = obj.objectiveFunction(xn);
218 if isfinite(fn) && fn < f - 1e-12
219 x = xn; f = fn; improved =
true;
break;
223 if ~improved,
break; end
227 function g = objectiveGradient(obj, x)
234 xp(i) = min(1.0, x(i) + h);
235 xm(i) = max(0.0, x(i) - h);
236 fp = obj.objectiveFunction(xp);
237 fm = obj.objectiveFunction(xm);
238 if isfinite(fp) && isfinite(fm) && xp(i) > xm(i)
239 g(i) = (fp - fm) / (xp(i) - xm(i));
continue;
241 if isempty(f0), f0 = obj.objectiveFunction(x); end
242 if isfinite(fp) && isfinite(f0) && xp(i) > x(i)
243 g(i) = (fp - f0) / (xp(i) - x(i));
244 elseif isfinite(fm) && isfinite(f0) && x(i) > xm(i)
245 g(i) = (f0 - fm) / (x(i) - xm(i));
252 function result = buildResult(obj, x, objectiveValue, solveTime)
253 result = opt.OptimizationResult();
254 result.objectiveValue = objectiveValue;
255 vals = obj.evaluators{1}.decodeVariables(x);
257 for i = 1:numel(vk), result.variableValues(vk{i}) = vals(vk{i}); end
258 result.iterations = obj.iterations;
259 result.solveTime = solveTime;
261 for i = 1:numel(obj.evaluators), evals = evals + obj.evaluators{i}.getEvaluationCount(); end
262 result.modelEvaluations = evals;
263 result.convergenceHistory = obj.convergenceHistory;
265 allValues = obj.mergeValues(result.variableValues);
266 objective = obj.problem.getObjective();
267 allCons = [objective.getConstraints(), obj.problem.getConstraints()];
268 result.feasible =
true;
269 for i = 1:numel(obj.evaluators)
270 er = obj.evaluators{i}.evaluateValuesWithCache(result.variableValues, obj.caches{i});
271 if ~er.feasible, result.feasible =
false;
continue; end
272 for c = 1:numel(allCons)
273 viol = allCons{c}.evaluate(er, allValues);
275 nm = allCons{c}.getName();
276 prev = 0.0;
if isKey(result.constraintViolations, nm), prev = result.constraintViolations(nm); end
277 result.constraintViolations(nm) = max(viol, prev);
278 result.feasible =
false;
282 elapsed = toc(obj.startTime);
283 if elapsed >= obj.opt.timeLimit
284 result.terminatedBy =
'time_limit';
285 elseif obj.iterations >= obj.opt.maxIterations
286 result.terminatedBy =
'iterations';
288 result.terminatedBy =
'convergence';
292 function result = buildEmptyResult(obj) %#ok<MANU>
293 result = opt.OptimizationResult();
294 result.objectiveValue = 0.0;
295 result.feasible =
true;
296 result.terminatedBy =
'empty';