LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
LineEvaluator.m
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.
6 %
7 % A copied model carries a cached NetworkStruct that scalar setters do not
8 % invalidate, so evaluateValues forces refreshStruct after applying
9 % variables.
10
11 properties
12 baseModel
13 variables % cell of opt.DecisionVariable
14 fixedVariables % cell of {var, value}
15 evaluationCount = 0;
16 totalDimension
17 varOffsets
18 isLayered = false;
19 end
20
21 methods
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;
28 else
29 obj.fixedVariables = {};
30 end
31 dim = 0;
32 obj.varOffsets = zeros(1, numel(variables));
33 for i = 1:numel(variables)
34 obj.varOffsets(i) = dim;
35 dim = dim + variables{i}.getDimension();
36 end
37 obj.totalDimension = dim;
38 end
39
40 function n = getEvaluationCount(obj), n = obj.evaluationCount; end
41
42 function b = getBounds(obj)
43 b = [];
44 for i = 1:numel(obj.variables)
45 b = [b; obj.variables{i}.getBounds()]; %#ok<AGROW>
46 end
47 end
48
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);
57 end
58 end
59
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()));
65 end
66 end
67 end
68
69 function m = copyModel(obj)
70 m = obj.baseModel.copy();
71 end
72
73 function result = evaluateValues(obj, values)
74 obj.evaluationCount = obj.evaluationCount + 1;
75 result = opt.EvaluationResult();
76 try
77 model = obj.copyModel();
78 for k = 1:numel(obj.fixedVariables)
79 fv = obj.fixedVariables{k};
80 fv{1}.apply(model, fv{2});
81 end
82 obj.applyVariables(model, values);
83
84 if obj.isLayered
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).
89 model.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.
97 else
98 model.refreshStruct();
99
100 solver = SolverAuto(model);
101 [QN, UN, RN, ~, ~, TN] = solver.getAvg();
102 result.feasible = true;
103 try
104 result.solverUsed = solver.getSelectedSolverName();
105 catch
106 result.solverUsed = '';
107 end
108 obj.extractMetrics(model, QN, UN, RN, TN, result);
109 obj.extractSystemMetrics(solver, model, result);
110 result.sensitivities = opt.sens.computeModelSensitivities(model);
111 end
112 catch ME %#ok<NASGU>
113 result.feasible = false;
114 end
115 end
116
117 function extractMetrics(~, model, QN, UN, RN, TN, result)
118 sn = model.getStruct();
119 R = sn.nclasses;
120 for ist = 1:sn.nstations
121 nodeIdx = sn.stationToNode(ist);
122 st = sn.nodenames{nodeIdx};
123 for r = 1:R
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
128 if ~isempty(UN)
129 if isKey(result.utilizations, st)
130 result.utilizations(st) = result.utilizations(st) + UN(ist, r);
131 else
132 result.utilizations(st) = UN(ist, r);
133 end
134 end
135 end
136 end
137 end
138
139 function extractSystemMetrics(~, solver, model, result)
140 try
141 [SysRespT, SysTput] = solver.getAvgSys();
142 catch
143 return;
144 end
145 SysRespT = SysRespT(:); SysTput = SysTput(:);
146 sn = model.getStruct();
147 chains = sn.chains; classnames = sn.classnames;
148 njobs = sn.njobs(:);
149 nchains = size(chains, 1); nclasses = size(chains, 2);
150 for ci = 1:nchains
151 members = {};
152 isOpen = false;
153 for k = 1:nclasses
154 if chains(ci, k) > 0
155 members{end+1} = classnames{k}; %#ok<AGROW>
156 if isinf(njobs(k)), isOpen = true; end
157 end
158 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
163 jobsInSystem = 0.0;
164 ks = keys(result.queueLengths);
165 for kk = 1:numel(ks)
166 parts = strsplit(ks{kk}, '||');
167 if any(strcmp(parts{2}, members))
168 jobsInSystem = jobsInSystem + result.queueLengths(ks{kk});
169 end
170 end
171 respt = jobsInSystem / tput;
172 end
173 for mi = 1:numel(members)
174 if ~isnan(respt), result.systemResponseTimes(members{mi}) = respt; end
175 result.systemThroughputs(members{mi}) = tput;
176 end
177 end
178 end
179
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
187 return;
188 end
189 nodes = cellstr(avgTable.Node);
190 for r = 1:numel(nodes)
191 node = nodes{r};
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
200 end
201 end
202
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)
212 task = tasks{i};
213 if ~opt.Layered.isRefTask(task)
214 continue;
215 end
216 tname = task.getName();
217 tputKey = [tname '||' tname];
218 if isKey(result.throughputs, tputKey)
219 tput = result.throughputs(tputKey);
220 if tput > 0
221 result.systemThroughputs(tname) = tput;
222 end
223 end
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);
231 haveRt = true;
232 end
233 end
234 if haveRt
235 result.systemResponseTimes(tname) = totalRt;
236 end
237 end
238 end
239
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).
247 sens = [];
248 if ~obj.isLayered
249 return;
250 end
251 try
252 model = obj.copyModel();
253 for k = 1:numel(obj.fixedVariables)
254 fv = obj.fixedVariables{k};
255 fv{1}.apply(model, fv{2});
256 end
257 obj.applyVariables(model, values);
258 model.lsn = [];
259 solver = opt.Layered.makeSolver(model);
260 sens = opt.Layered.computeSensitivities(solver);
261 catch
262 sens = [];
263 end
264 end
265
266 function result = evaluateValuesWithCache(obj, values, cache)
267 key = opt.LineEvaluator.valuesKey(values);
268 if isKey(cache, key)
269 result = cache(key);
270 else
271 result = obj.evaluateValues(values);
272 cache(key) = result; %#ok<NASGU>
273 end
274 end
275 end
276
277 methods (Static)
278 function key = valuesKey(values)
279 ks = sort(keys(values));
280 parts = cell(1, numel(ks));
281 for i = 1:numel(ks)
282 v = values(ks{i});
283 if isnumeric(v)
284 parts{i} = [ks{i} '=' mat2str(round(v(:).' * 1e9) / 1e9)];
285 else
286 parts{i} = [ks{i} '=' num2str(v)];
287 end
288 end
289 key = strjoin(parts, ';');
290 end
291 end
292end