LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
DecompositionWorkflow.m
1classdef DecompositionWorkflow < handle
2 % DecompositionWorkflow Decomposes a joint problem into per-variable-type
3 % subproblems solved via Gauss-Seidel cycling with fixed-value propagation.
4 % An internal topological sort orders subproblems when dependencies are set.
5 % Mirrors native-Python DecompositionWorkflow.
6
7 properties (Constant)
8 % Flat-network variable types first, then LayeredNetwork (LQN) types;
9 % only types present in a given problem produce subproblems.
10 DEFAULT_ORDER = {'server_allocation','station_replicas','service_rate', ...
11 'job_population','class_priority','routing','class_mapping', ...
12 'processor_multiplicity','task_multiplicity','task_replication', ...
13 'host_demand','think_time'};
14 end
15
16 properties
17 problem
18 subproblems = {};
19 dependencyGraph % Map toNode -> cell of fromNodes
20 solverOptions
21 end
22
23 methods
24 function obj = DecompositionWorkflow(problem)
25 obj.problem = problem;
26 obj.dependencyGraph = containers.Map('KeyType','char','ValueType','any');
27 obj.solverOptions = opt.LineOptSolverOptions();
28 end
29
30 function p = getProblem(obj), p = obj.problem; end
31 function s = getSubProblems(obj), s = obj.subproblems; end
32 function obj = setSolverOptions(obj, options), obj.solverOptions = options; end
33
34 function obj = autoDecompose(obj)
35 byType = containers.Map('KeyType','char','ValueType','any');
36 vars = obj.problem.getVariables();
37 for i = 1:numel(vars)
38 t = vars{i}.getVariableType();
39 if isKey(byType, t), lst = byType(t); else, lst = {}; end
40 lst{end+1} = vars{i}; %#ok<AGROW>
41 byType(t) = lst;
42 end
43 obj.subproblems = {};
44 used = containers.Map('KeyType','char','ValueType','logical');
45 for k = 1:numel(obj.DEFAULT_ORDER)
46 t = obj.DEFAULT_ORDER{k};
47 if isKey(byType, t)
48 obj.subproblems{end+1} = opt.SubProblem(t, t, byType(t)); %#ok<AGROW>
49 used(t) = true;
50 end
51 end
52 bk = keys(byType);
53 for k = 1:numel(bk)
54 if ~isKey(used, bk{k})
55 obj.subproblems{end+1} = opt.SubProblem(bk{k}, bk{k}, byType(bk{k})); %#ok<AGROW>
56 end
57 end
58 end
59
60 function obj = setDependency(obj, fromProblem, toProblem)
61 if isKey(obj.dependencyGraph, toProblem), s = obj.dependencyGraph(toProblem); else, s = {}; end
62 s{end+1} = fromProblem;
63 obj.dependencyGraph(toProblem) = s;
64 end
65
66 function obj = addSubProblem(obj, name, variables, after)
67 if isempty(variables), vt = 'custom'; else, vt = variables{1}.getVariableType(); end
68 obj.subproblems{end+1} = opt.SubProblem(name, vt, variables);
69 if nargin >= 4 && ~isempty(after)
70 for i = 1:numel(after), obj.setDependency(after{i}, name); end
71 end
72 end
73
74 function ordered = getExecutionOrder(obj)
75 if obj.dependencyGraph.Count == 0
76 ordered = obj.subproblems; return;
77 end
78 names = cellfun(@(sp) sp.name, obj.subproblems, 'UniformOutput', false);
79 indeg = containers.Map(names, num2cell(zeros(1, numel(names))));
80 adj = containers.Map('KeyType','char','ValueType','any');
81 for i = 1:numel(names), adj(names{i}) = {}; end
82 tks = keys(obj.dependencyGraph);
83 for i = 1:numel(tks)
84 to = tks{i}; froms = obj.dependencyGraph(to);
85 for j = 1:numel(froms)
86 from = froms{j};
87 if isKey(adj, from) && isKey(indeg, to)
88 a = adj(from); a{end+1} = to; adj(from) = a; %#ok<AGROW>
89 indeg(to) = indeg(to) + 1;
90 end
91 end
92 end
93 q = {};
94 for i = 1:numel(names), if indeg(names{i}) == 0, q{end+1} = names{i}; end; end %#ok<AGROW>
95 orderNames = {}; head = 1;
96 while head <= numel(q)
97 n = q{head}; head = head + 1;
98 orderNames{end+1} = n; %#ok<AGROW>
99 succ = adj(n);
100 for j = 1:numel(succ)
101 m = succ{j}; indeg(m) = indeg(m) - 1;
102 if indeg(m) == 0, q{end+1} = m; end %#ok<AGROW>
103 end
104 end
105 if numel(orderNames) ~= numel(obj.subproblems)
106 ordered = obj.subproblems; return; % cycle: fall back
107 end
108 byName = containers.Map(names, obj.subproblems);
109 ordered = cell(1, numel(orderNames));
110 for i = 1:numel(orderNames), ordered{i} = byName(orderNames{i}); end
111 end
112
113 function result = solveSequential(obj, maxCycles, tolerance)
114 if nargin < 2, maxCycles = 10; end
115 if nargin < 3, tolerance = 0.01; end
116 t0 = tic;
117 result = opt.WorkflowResult();
118 if isempty(obj.subproblems)
119 result.converged = true; return;
120 end
121 ordered = obj.getExecutionOrder();
122 fixedValues = containers.Map('KeyType','char','ValueType','any');
123 prevObjective = inf;
124 for cycle = 1:maxCycles
125 for si = 1:numel(ordered)
126 sp = ordered{si};
127 partial = obj.createPartialProblem(sp, fixedValues);
128 spResult = opt.LineOptSolver(partial, obj.solverOptions).solve();
129 spr = opt.SubProblemResult(sp.name, spResult);
130 fk = keys(fixedValues);
131 for i = 1:numel(fk), spr.variablesFixed(fk{i}) = fixedValues(fk{i}); end
132 result.subproblemResults(sp.name) = spr;
133 vk = keys(spResult.variableValues);
134 for i = 1:numel(vk), fixedValues(vk{i}) = spResult.variableValues(vk{i}); end
135 end
136 currentObjective = obj.evaluateFullObjective(fixedValues);
137 result.objectiveHistory(end+1) = currentObjective;
138 if abs(currentObjective - prevObjective) < tolerance
139 result.converged = true; break;
140 end
141 prevObjective = currentObjective;
142 result.cyclesCompleted = cycle;
143 end
144 if isempty(result.objectiveHistory)
145 result.finalObjective = inf;
146 else
147 result.finalObjective = result.objectiveHistory(end);
148 end
149 fk = keys(fixedValues);
150 for i = 1:numel(fk), result.finalVariableValues(fk{i}) = fixedValues(fk{i}); end
151 result.totalSolveTime = toc(t0);
152 end
153
154 function result = solveHierarchical(obj)
155 result = obj.solveSequential(1, 0.0);
156 end
157
158 function partial = createPartialProblem(obj, subproblem, fixedValues)
159 partial = opt.OptimizationProblem(obj.problem.getModel());
160 for i = 1:numel(subproblem.variables), partial.addVariable(subproblem.variables{i}); end
161 subNames = subproblem.getVariableNames();
162 fixedPairs = {};
163 vars = obj.problem.getVariables();
164 for i = 1:numel(vars)
165 nm = vars{i}.getName();
166 if ~any(strcmp(nm, subNames)) && isKey(fixedValues, nm)
167 fixedPairs{end+1} = {vars{i}, fixedValues(nm)}; %#ok<AGROW>
168 end
169 end
170 partial.setFixedVariables(fixedPairs);
171 partial.setObjective(obj.problem.getObjective());
172 cons = obj.problem.getConstraints();
173 for i = 1:numel(cons), partial.addConstraint(cons{i}); end
174 scen = obj.problem.getScenarios();
175 for i = 1:numel(scen), partial.addScenario(scen{i}{1}, scen{i}{2}); end
176 end
177
178 function value = evaluateFullObjective(obj, variableValues)
179 penaltyWeight = obj.solverOptions.penaltyWeight;
180 evaluator = opt.LineEvaluator(obj.problem.getModel(), obj.problem.getVariables(), {});
181 res = evaluator.evaluateValues(variableValues);
182 if ~res.feasible, value = inf; return; end
183 objective = obj.problem.getObjective();
184 value = objective.evaluateWithPenalty(res, variableValues, penaltyWeight);
185 cons = obj.problem.getConstraints();
186 for i = 1:numel(cons)
187 value = value + cons{i}.evaluate(res, variableValues) * penaltyWeight;
188 end
189 end
190
191 % ---- LayeredNetwork (LQN) layer-wise decomposition ---------------
192
193 function result = solveLayered(obj, maxCycles, tolerance, autoFreeze, ...
194 freezeTol, frozenLayers)
195 % Solve an LQN by layer, optionally freezing converged layers.
196 % Groups decision variables by the LQN layer they perturb (host or
197 % task layer) and cycles Gauss-Seidel over the layer groups, fixing
198 % every other layer's variables at their current values while one
199 % layer is optimized. The LQN analogue of solveSequential, but the
200 % subproblems are LAYERS rather than variable types.
201 %
202 % Freezing has two composable sources: frozenLayers (an explicit
203 % seed set held fixed throughout) and autoFreeze (adaptive: after
204 % each cycle a layer whose representative node metrics moved less
205 % than freezeTol relative is frozen and skipped; unfrozen again if
206 % any still-active layer later moves by more than freezeTol).
207 % Convergence is on the full penalized objective delta, or when
208 % every layer is frozen. Falls back to solveSequential for a flat
209 % network. The WorkflowResult carries frozenLayers (final frozen
210 % set) and modelEvaluations (total LINE solves).
211 if nargin < 2 || isempty(maxCycles), maxCycles = 10; end
212 if nargin < 3 || isempty(tolerance), tolerance = 0.01; end
213 if nargin < 4 || isempty(autoFreeze), autoFreeze = true; end
214 if nargin < 5 || isempty(freezeTol), freezeTol = 1e-3; end
215 if nargin < 6 || isempty(frozenLayers), frozenLayers = {}; end
216
217 t0 = tic;
218 model = obj.problem.getModel();
219 if ~opt.Layered.isLayered(model)
220 result = obj.solveSequential(maxCycles, tolerance);
221 return;
222 end
223
224 % Group variables by their primary (first) layer.
225 groups = containers.Map('KeyType', 'char', 'ValueType', 'any');
226 groupOrder = {};
227 vars = obj.problem.getVariables();
228 for i = 1:numel(vars)
229 layers = vars{i}.getLayer(model);
230 if isempty(layers), lkey = '_nolayer'; else, lkey = layers{1}; end
231 if isKey(groups, lkey)
232 lst = groups(lkey);
233 else
234 lst = {}; groupOrder{end+1} = lkey; %#ok<AGROW>
235 end
236 lst{end+1} = vars{i}; %#ok<AGROW>
237 groups(lkey) = lst;
238 end
239
240 objective = obj.problem.getObjective();
241 penaltyWeight = obj.solverOptions.penaltyWeight;
242 evaluator = opt.LineEvaluator(model, vars, {});
243
244 frozen = frozenLayers;
245 fixedValues = containers.Map('KeyType', 'char', 'ValueType', 'any');
246 prevSig = containers.Map('KeyType', 'char', 'ValueType', 'any');
247 havePrevSig = false;
248 prevObjective = inf;
249 modelEvaluations = 0;
250
251 result = opt.WorkflowResult();
252 for cycle = 1:maxCycles
253 for gi = 1:numel(groupOrder)
254 layer = groupOrder{gi};
255 if any(strcmp(layer, frozen)), continue; end
256 layerVars = groups(layer);
257 partial = opt.OptimizationProblem(model);
258 for i = 1:numel(layerVars), partial.addVariable(layerVars{i}); end
259 subNames = cellfun(@(v) v.getName(), layerVars, 'UniformOutput', false);
260 fixedPairs = {};
261 for i = 1:numel(vars)
262 nm = vars{i}.getName();
263 if ~any(strcmp(nm, subNames)) && isKey(fixedValues, nm)
264 fixedPairs{end+1} = {vars{i}, fixedValues(nm)}; %#ok<AGROW>
265 end
266 end
267 partial.setFixedVariables(fixedPairs);
268 partial.setObjective(objective);
269 cons = obj.problem.getConstraints();
270 for i = 1:numel(cons), partial.addConstraint(cons{i}); end
271
272 spResult = opt.LineOptSolver(partial, obj.solverOptions).solve();
273 modelEvaluations = modelEvaluations + spResult.modelEvaluations;
274 spr = opt.SubProblemResult(layer, spResult);
275 fk = keys(fixedValues);
276 for i = 1:numel(fk), spr.variablesFixed(fk{i}) = fixedValues(fk{i}); end
277 result.subproblemResults(layer) = spr;
278 vk = keys(spResult.variableValues);
279 for i = 1:numel(vk), fixedValues(vk{i}) = spResult.variableValues(vk{i}); end
280 end
281
282 % Full evaluation: objective + per-layer signatures for freezing.
283 evalResult = evaluator.evaluateValues(fixedValues);
284 modelEvaluations = modelEvaluations + 1;
285 if ~evalResult.feasible
286 currentObjective = inf;
287 sig = containers.Map('KeyType', 'char', 'ValueType', 'any');
288 else
289 currentObjective = objective.evaluateWithPenalty( ...
290 evalResult, fixedValues, penaltyWeight);
291 cons = obj.problem.getConstraints();
292 for i = 1:numel(cons)
293 currentObjective = currentObjective + ...
294 cons{i}.evaluate(evalResult, fixedValues) * penaltyWeight;
295 end
296 sig = opt.DecompositionWorkflow.layerSignatures(evalResult, groupOrder);
297 end
298
299 if autoFreeze && havePrevSig
300 moved = containers.Map('KeyType', 'char', 'ValueType', 'double');
301 for gi = 1:numel(groupOrder)
302 L = groupOrder{gi};
303 a = []; b = [];
304 if isKey(prevSig, L), a = prevSig(L); end
305 if isKey(sig, L), b = sig(L); end
306 moved(L) = opt.DecompositionWorkflow.sigDelta(a, b);
307 end
308 activeMoved = false;
309 for gi = 1:numel(groupOrder)
310 L = groupOrder{gi};
311 if ~any(strcmp(L, frozen)) && moved(L) > freezeTol
312 activeMoved = true; break;
313 end
314 end
315 for gi = 1:numel(groupOrder)
316 L = groupOrder{gi};
317 if any(strcmp(L, frozen))
318 if activeMoved, frozen(strcmp(frozen, L)) = []; end
319 elseif moved(L) < freezeTol
320 frozen{end+1} = L; %#ok<AGROW>
321 end
322 end
323 end
324
325 prevSig = sig; havePrevSig = true;
326 result.objectiveHistory(end+1) = currentObjective;
327 if abs(currentObjective - prevObjective) < tolerance
328 result.converged = true; break;
329 end
330 prevObjective = currentObjective;
331 result.cyclesCompleted = cycle;
332 % All layers frozen: nothing left to optimize.
333 if numel(frozen) >= numel(groupOrder)
334 result.converged = true; break;
335 end
336 end
337
338 if isempty(result.objectiveHistory)
339 result.finalObjective = inf;
340 else
341 result.finalObjective = result.objectiveHistory(end);
342 end
343 fk = keys(fixedValues);
344 for i = 1:numel(fk), result.finalVariableValues(fk{i}) = fixedValues(fk{i}); end
345 result.totalSolveTime = toc(t0);
346 result.frozenLayers = sort(frozen);
347 result.modelEvaluations = modelEvaluations;
348 end
349 end
350
351 methods (Static)
352 function sig = layerSignatures(evalResult, layers)
353 % Representative [Util, QLen, Tput, RespT] per layer, keyed by its
354 % node. A layer named after a processor or task has a same-named
355 % node in the LQN average table; its metrics are the layer's
356 % convergence signature. Layers without a matching node (e.g. the
357 % '_nolayer' bucket) get an empty signature so they never auto-freeze.
358 sig = containers.Map('KeyType', 'char', 'ValueType', 'any');
359 for i = 1:numel(layers)
360 L = layers{i};
361 util = evalResult.getUtilization(L);
362 qlen = evalResult.getQueueLength(L);
363 tput = evalResult.getThroughput(L);
364 respt = evalResult.getResponseTime(L);
365 probe = [util, qlen, tput];
366 if any(probe ~= 0 & ~isinf(probe))
367 sig(L) = [util, qlen, tput, respt];
368 else
369 sig(L) = [];
370 end
371 end
372 end
373
374 function d = sigDelta(a, b)
375 % Max relative change between two layer signatures (inf if unknown).
376 if isempty(a) || isempty(b), d = inf; return; end
377 eps0 = 1e-12; d = 0.0;
378 for i = 1:min(numel(a), numel(b))
379 ai = a(i); bi = b(i);
380 if ~(isfinite(ai) && isfinite(bi)), continue; end
381 d = max(d, abs(bi - ai) / (abs(ai) + eps0));
382 end
383 end
384 end
385end