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.
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'};
19 dependencyGraph % Map toNode -> cell of fromNodes
24 function obj = DecompositionWorkflow(problem)
25 obj.problem = problem;
26 obj.dependencyGraph = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
27 obj.solverOptions = opt.LineOptSolverOptions();
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
34 function obj = autoDecompose(obj)
35 byType = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
36 vars = obj.problem.getVariables();
38 t = vars{i}.getVariableType();
39 if isKey(byType, t), lst = byType(t);
else, lst = {}; end
40 lst{end+1} = vars{i}; %#ok<AGROW>
44 used = containers.Map(
'KeyType',
'char',
'ValueType',
'logical');
45 for k = 1:numel(obj.DEFAULT_ORDER)
46 t = obj.DEFAULT_ORDER{k};
48 obj.subproblems{end+1} = opt.SubProblem(t, t, byType(t)); %#ok<AGROW>
54 if ~isKey(used, bk{k})
55 obj.subproblems{end+1} = opt.SubProblem(bk{k}, bk{k}, byType(bk{k})); %#ok<AGROW>
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;
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
74 function ordered = getExecutionOrder(obj)
75 if obj.dependencyGraph.Count == 0
76 ordered = obj.subproblems;
return;
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);
84 to = tks{i}; froms = obj.dependencyGraph(to);
85 for j = 1:numel(froms)
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;
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>
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>
105 if numel(orderNames) ~= numel(obj.subproblems)
106 ordered = obj.subproblems; return; % cycle: fall back
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
113 function result = solveSequential(obj, maxCycles, tolerance)
114 if nargin < 2, maxCycles = 10; end
115 if nargin < 3, tolerance = 0.01; end
117 result = opt.WorkflowResult();
118 if isempty(obj.subproblems)
119 result.converged = true; return;
121 ordered = obj.getExecutionOrder();
122 fixedValues = containers.Map('KeyType','
char','ValueType','any');
124 for cycle = 1:maxCycles
125 for si = 1:numel(ordered)
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
136 currentObjective = obj.evaluateFullObjective(fixedValues);
137 result.objectiveHistory(end+1) = currentObjective;
138 if abs(currentObjective - prevObjective) < tolerance
139 result.converged =
true;
break;
141 prevObjective = currentObjective;
142 result.cyclesCompleted = cycle;
144 if isempty(result.objectiveHistory)
145 result.finalObjective = inf;
147 result.finalObjective = result.objectiveHistory(end);
149 fk = keys(fixedValues);
150 for i = 1:numel(fk), result.finalVariableValues(fk{i}) = fixedValues(fk{i}); end
151 result.totalSolveTime = toc(t0);
154 function result = solveHierarchical(obj)
155 result = obj.solveSequential(1, 0.0);
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();
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>
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
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;
191 % ---- LayeredNetwork (LQN) layer-wise decomposition ---------------
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.
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
218 model = obj.problem.getModel();
219 if ~opt.Layered.isLayered(model)
220 result = obj.solveSequential(maxCycles, tolerance);
224 % Group variables by their primary (first) layer.
225 groups = containers.Map('KeyType
', 'char', 'ValueType
', 'any
');
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)
234 lst = {}; groupOrder{end+1} = lkey; %#ok<AGROW>
236 lst{end+1} = vars{i}; %#ok<AGROW>
240 objective = obj.problem.getObjective();
241 penaltyWeight = obj.solverOptions.penaltyWeight;
242 evaluator = opt.LineEvaluator(model, vars, {});
244 frozen = frozenLayers;
245 fixedValues = containers.Map('KeyType
', 'char', 'ValueType
', 'any
');
246 prevSig = containers.Map('KeyType
', 'char', 'ValueType
', 'any
');
249 modelEvaluations = 0;
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);
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>
267 partial.setFixedVariables(fixedPairs);
268 partial.setObjective(objective);
269 cons = obj.problem.getConstraints();
270 for i = 1:numel(cons), partial.addConstraint(cons{i}); end
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
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
');
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;
296 sig = opt.DecompositionWorkflow.layerSignatures(evalResult, groupOrder);
299 if autoFreeze && havePrevSig
300 moved = containers.Map('KeyType
', 'char', 'ValueType
', 'double');
301 for gi = 1:numel(groupOrder)
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);
309 for gi = 1:numel(groupOrder)
311 if ~any(strcmp(L, frozen)) && moved(L) > freezeTol
312 activeMoved = true; break;
315 for gi = 1:numel(groupOrder)
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>
325 prevSig = sig; havePrevSig = true;
326 result.objectiveHistory(end+1) = currentObjective;
327 if abs(currentObjective - prevObjective) < tolerance
328 result.converged = true; break;
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;
338 if isempty(result.objectiveHistory)
339 result.finalObjective = inf;
341 result.finalObjective = result.objectiveHistory(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;
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)
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];
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));