1classdef LineEvaluator < handle
2 % LineEvaluator Interface between the optimizer and SolverAuto. Applies
3 % decision variable values to a per-evaluation model copy, solves, and
4 % extracts per-(station,
class) and system metrics. Mirrors native-Python
5 % line_solver.opt.evaluator.LineEvaluator.
7 % A copied model carries a cached NetworkStruct that scalar setters
do not
8 % invalidate, so evaluateValues forces refreshStruct after applying
13 variables % cell of opt.DecisionVariable
14 fixedVariables % cell of {var, value}
22 function obj = LineEvaluator(model, variables, fixedVariables)
23 obj.baseModel = model;
24 obj.variables = variables;
25 obj.isLayered = opt.Layered.isLayered(model);
26 if nargin >= 3 && ~isempty(fixedVariables)
27 obj.fixedVariables = fixedVariables;
29 obj.fixedVariables = {};
32 obj.varOffsets = zeros(1, numel(variables));
33 for i = 1:numel(variables)
34 obj.varOffsets(i) = dim;
35 dim = dim + variables{i}.getDimension();
37 obj.totalDimension = dim;
40 function n = getEvaluationCount(obj), n = obj.evaluationCount; end
42 function b = getBounds(obj)
44 for i = 1:numel(obj.variables)
45 b = [b; obj.variables{i}.getBounds()]; %#ok<AGROW>
49 function values = decodeVariables(obj, x)
50 values = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
51 for i = 1:numel(obj.variables)
52 var = obj.variables{i};
53 offset = obj.varOffsets(i);
54 dim = var.getDimension();
55 slice = x(offset+1 : offset+dim);
56 values(var.getName()) = var.decode(slice);
60 function applyVariables(obj, model, values)
61 for i = 1:numel(obj.variables)
62 var = obj.variables{i};
63 if isKey(values, var.getName())
64 var.apply(model, values(var.getName()));
69 function m = copyModel(obj)
70 m = obj.baseModel.copy();
73 function result = evaluateValues(obj, values)
74 obj.evaluationCount = obj.evaluationCount + 1;
75 result = opt.EvaluationResult();
77 model = obj.copyModel();
78 for k = 1:numel(obj.fixedVariables)
79 fv = obj.fixedVariables{k};
80 fv{1}.apply(model, fv{2});
82 obj.applyVariables(model, values);
85 % LQN path: solve with SolverLN, read the per-node LQN
86 % table. Force a fresh
struct so the mutated LQN elements
87 % are read (the base
is never solved, so a cached
struct
88 % would only appear via a deep-copied lsn).
90 [~, avgTable] = opt.Layered.solveAvg(model);
91 result.feasible =
true;
92 result.solverUsed =
'SolverLN';
93 obj.extractLayeredMetrics(avgTable, result);
94 obj.extractLayeredSystemMetrics(model, result);
95 % LQN sensitivities are expensive (they re-solve every
96 % layer), so the gradient path computes them lazily.
98 model.refreshStruct();
100 solver = SolverAuto(model);
101 [QN, UN, RN, ~, ~, TN] = solver.getAvg();
102 result.feasible =
true;
104 result.solverUsed = solver.getSelectedSolverName();
106 result.solverUsed =
'';
108 obj.extractMetrics(model, QN, UN, RN, TN, result);
109 obj.extractSystemMetrics(solver, model, result);
110 result.sensitivities = opt.sens.computeModelSensitivities(model);
113 result.feasible =
false;
117 function extractMetrics(~, model, QN, UN, RN, TN, result)
118 sn = model.getStruct();
120 for ist = 1:sn.nstations
121 nodeIdx = sn.stationToNode(ist);
122 st = sn.nodenames{nodeIdx};
124 cl = sn.classnames{r};
125 if ~isempty(RN), result.setResponseTime(st, cl, RN(ist, r)); end
126 if ~isempty(TN), result.setThroughput(st, cl, TN(ist, r)); end
127 if ~isempty(QN), result.setQueueLength(st, cl, QN(ist, r)); end
129 if isKey(result.utilizations, st)
130 result.utilizations(st) = result.utilizations(st) + UN(ist, r);
132 result.utilizations(st) = UN(ist, r);
139 function extractSystemMetrics(~, solver, model, result)
141 [SysRespT, SysTput] = solver.getAvgSys();
145 SysRespT = SysRespT(:); SysTput = SysTput(:);
146 sn = model.getStruct();
147 chains = sn.chains; classnames = sn.classnames;
149 nchains = size(chains, 1); nclasses = size(chains, 2);
155 members{end+1} = classnames{k}; %#ok<AGROW>
156 if isinf(njobs(k)), isOpen =
true; end
159 if isempty(members),
continue; end
160 tput = 0.0;
if ci <= numel(SysTput), tput = SysTput(ci); end
161 respt = NaN;
if ci <= numel(SysRespT), respt = SysRespT(ci); end
162 if isOpen && tput > 0 && result.queueLengths.Count > 0
164 ks = keys(result.queueLengths);
166 parts = strsplit(ks{kk},
'||');
167 if any(strcmp(parts{2}, members))
168 jobsInSystem = jobsInSystem + result.queueLengths(ks{kk});
171 respt = jobsInSystem / tput;
173 for mi = 1:numel(members)
174 if ~isnan(respt), result.systemResponseTimes(members{mi}) = respt; end
175 result.systemThroughputs(members{mi}) = tput;
180 function extractLayeredMetrics(~, avgTable, result)
181 % Extract per-LQN-node metrics from SolverLN
's average table (one
182 % row per Processor/Task/Entry/Activity; columns Node, NodeType,
183 % QLen, Util, RespT, ResidT, ArvR, Tput). Throughput/queue-length/
184 % response-time keyed by (node,node); utilization by node, matching
185 % the flat EvaluationResult convention. Non-finite cells skipped.
186 if isempty(avgTable) || height(avgTable) == 0
189 nodes = cellstr(avgTable.Node);
190 for r = 1:numel(nodes)
192 v = avgTable.RespT(r);
193 if isfinite(v), result.setResponseTime(node, node, v); end
194 v = avgTable.Tput(r);
195 if isfinite(v), result.setThroughput(node, node, v); end
196 v = avgTable.QLen(r);
197 if isfinite(v), result.setQueueLength(node, node, v); end
198 v = avgTable.Util(r);
199 if isfinite(v), result.utilizations(node) = v; end
203 function extractLayeredSystemMetrics(~, model, result)
204 % Derive end-to-end (system) metrics from the reference task(s): a
205 % closed LQN's system throughput
is the reference task
's
206 % throughput; its end-to-end response time is the sum of response
207 % times over the reference task's entries. Keyed by the reference
208 % task
's name so MinimizeSystemResponseTime /
209 % SystemResponseTimeConstraint resolve without a chain concept.
210 tasks = model.getTasks();
211 for i = 1:numel(tasks)
213 if ~opt.Layered.isRefTask(task)
216 tname = task.getName();
217 tputKey = [tname '||
' tname];
218 if isKey(result.throughputs, tputKey)
219 tput = result.throughputs(tputKey);
221 result.systemThroughputs(tname) = tput;
224 entries = task.entries;
225 totalRt = 0.0; haveRt = false;
226 for j = 1:numel(entries)
227 ename = entries(j).getName();
228 ekey = [ename '||
' ename];
229 if isKey(result.responseTimes, ekey)
230 totalRt = totalRt + result.responseTimes(ekey);
235 result.systemResponseTimes(tname) = totalRt;
240 function sens = evaluateLayeredSensitivities(obj, values)
241 % Compute LQN per-layer service-rate partial sensitivities on
242 % demand: rebuild the configured model copy, solve it with
243 % SolverLN, and reshape SolverLN.getSensitivityTable into a
244 % containers.Map 'Station||JobClass
' -> struct(Tput,RespT,QLen,
245 % Util). Called only by the partial-sensitivity gradient path.
246 % Returns [] on any failure (the caller then finite-differences).
252 model = obj.copyModel();
253 for k = 1:numel(obj.fixedVariables)
254 fv = obj.fixedVariables{k};
255 fv{1}.apply(model, fv{2});
257 obj.applyVariables(model, values);
259 solver = opt.Layered.makeSolver(model);
260 sens = opt.Layered.computeSensitivities(solver);
266 function result = evaluateValuesWithCache(obj, values, cache)
267 key = opt.LineEvaluator.valuesKey(values);
271 result = obj.evaluateValues(values);
272 cache(key) = result; %#ok<NASGU>
278 function key = valuesKey(values)
279 ks = sort(keys(values));
280 parts = cell(1, numel(ks));
284 parts{i} = [ks{i} '=
' mat2str(round(v(:).' * 1e9) / 1e9)];
286 parts{i} = [ks{i}
'=' num2str(v)];
289 key = strjoin(parts,
';');