LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_chain_tables.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_SOLVER_CHAIN_TABLES_H
6#define LINE_SOLVERS_SOLVER_CHAIN_TABLES_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The CHAIN-level and SYSTEM-level views of a solved model.
12 *
13 * Ports of `@@NetworkSolver/getAvgSys.m`, `getAvgChain.m` and
14 * `getAvgNodeChain.m` with the tables built on top of them
15 * (`getAvgSysTable`, `getAvgChainTable`, `getAvgNodeChainTable`).
16 *
17 * A CHAIN IS NOT A CLASS AND THE AGGREGATION IS NOT A SUM FOR EVERY METRIC,
18 * which is the whole reason these live apart from the AvgTable. Queue lengths,
19 * utilizations, arrival rates, throughputs and residence times are ADDITIVE
20 * over the classes of a chain, so their chain value is the row sum. Response
21 * time is NOT: a chain's response time at a station is the per-visit time
22 * averaged over the classes with the VISIT SHARE alpha as the weight, because a
23 * job of the chain arrives as one class or another in proportion to how often
24 * that class visits. Summing it instead would report the total time a job would
25 * spend if it were every class at once.
26 *
27 * The system view is a third thing again. `getAvgSys` returns one response time
28 * and one throughput PER CHAIN, both measured at the chain's reference station:
29 * the throughput is the completing flow INTO that station, read off the routing
30 * matrix, and the response time is the cycle time -- Little's law on a closed
31 * chain, the visit-weighted sum of the per-class system times on an open one.
32 *
33 * INDEXING: these functions take a solved `mva::AvgResult` (station x class)
34 * and the struct it was solved from, and never re-solve. That keeps them usable
35 * from any solver whose runner returns an AvgResult, which is what the reference
36 * means by putting them on @@NetworkSolver rather than on one solver.
37 *
38 * ONE DEVIATION FROM THE REFERENCE, STATED: `getAvgSys.m` indexes `sn.rt` and,
39 * in one branch, `sn.visits{c}` with a STATION index, while both matrices are
40 * indexed by STATEFUL node. The two orders coincide on every model whose
41 * stateful nodes are all stations -- which is every model the reference is
42 * exercised on -- and differ as soon as one is not (a Cache, a Logger). This
43 * port uses `stateful_of_station`, so it agrees with the reference wherever the
44 * reference is self-consistent and is correct where it is not.
45 */
46
47#include <cmath>
48#include <limits>
49#include <cstddef>
50#include <string>
51#include <vector>
52
54#include "line/num/number.h"
57#include "line/util/error.h"
58#include "line/util/matrix.h"
59
60namespace line {
61namespace solvers {
62
63namespace chain_detail {
64
65/**
66 * Mean of the maximum of independent exponentials, by inclusion-exclusion.
67 *
68 * With one exponential of rate lambda_i per parallel branch, the expected time
69 * until ALL have finished is
70 *
71 * E[max] = sum_{k=1..n} (-1)^(k-1) sum_{|S|=k} 1 / (sum_{i in S} lambda_i),
72 *
73 * which is what `getAvgSys.m` and `pathsCS.m` both spell out with `nchoosek`.
74 * The rates come from the branch response times as lambda_i = 1/r_i, so the
75 * whole construction reads the parallel section as a race between exponentials
76 * whose means are the measured branch times. That is an ASSUMPTION about the
77 * branch laws, not a measurement of them, and it is the reference's -- a branch
78 * whose time is far from exponential is the one case where this quantity is not
79 * the synchronization delay it is reported as.
80 *
81 * Enumerated over bitmasks rather than by `nchoosek`, which materializes every
82 * combination as a matrix row. The cap is on the branch count, not on the
83 * subset count: 2^n terms alternate in sign and cancel catastrophically well
84 * before memory becomes the issue, and a fork with more than 20 parallel
85 * branches is a modelling question, not a numerical one.
86 */
87template <class T>
88T exp_max_mean(const std::vector<T>& branch_times) {
89 const std::size_t n = branch_times.size();
90 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
91 if (n == 0) return zero;
92 if (n > 20)
93 throw UnsupportedError(ErrorKind{"Fork"},
94 "getAvgSys: the fork-join section has " + std::to_string(n) +
95 " parallel paths, and the reference's synchronization delay is an "
96 "inclusion-exclusion sum of 2^n alternating terms; past 20 paths that sum has lost "
97 "every significant digit to cancellation, so it is refused rather than reported");
98
99 std::vector<T> lambda(n);
100 for (std::size_t i = 0; i < n; ++i) {
101 const double ri = num_traits<T>::to_double(branch_times[i]);
102 if (!(ri > 0.0))
103 throw NumericError(ErrorKind{"Degenerate"},
104 "getAvgSys: a path from the fork to the join has non-positive total response "
105 "time, so its exponential rate is not defined; the branch carries no station "
106 "with a finite response time");
107 lambda[i] = T(one / branch_times[i]);
108 }
109
110 T d0 = zero;
111 const unsigned long long total = 1ull << n;
112 for (unsigned long long mask = 1; mask < total; ++mask) {
113 T rate = zero;
114 std::size_t bits = 0;
115 for (std::size_t i = 0; i < n; ++i)
116 if (mask & (1ull << i)) {
117 rate = T(rate + lambda[i]);
118 ++bits;
119 }
120 const T term = T(one / rate);
121 if (bits % 2 == 1)
122 d0 = T(d0 + term);
123 else
124 d0 = T(d0 - term);
125 }
126 return d0;
127}
128
129/** What one `pathsCS` walk returns. */
130template <class T>
131struct PathsResult {
132 std::vector<T> times; ///< total response time of each fork-to-join path
133 std::vector<std::size_t> stations; ///< 1-based stations lying on those paths
134};
135
136/**
137 * Port of `ModelAdapter.pathsCS`: enumerate every path from `cur` to `stop` and
138 * total the response time along each.
139 *
140 * THE ROUTING MATRIX IS `rtnodes` HERE AND `rtorig` IN THE REFERENCE, and the
141 * indexing differs with it: `cell2mat(getLinkedRoutingMatrix)` is CLASS-major,
142 * addressed as `(class-1)*nnodes + node`, while `rtnodes` is NODE-major,
143 * `(node-1)*nclasses + class`. Only the SUPPORT of the matrix is read -- the
144 * walk follows nonzero successors and never multiplies by a probability -- and
145 * the two describe the same graph once the refresh has resolved class
146 * switching, so the enumerated path SET is the same. The order in which the
147 * paths come out differs, and nothing downstream depends on it: `exp_max_mean`
148 * is symmetric in its argument.
149 *
150 * A NESTED FORK IS COLLAPSED BEFORE THE WALK CONTINUES, which is why this
151 * mutates `RN`: the inner join's response time is set to the inner section's
152 * own synchronization delay and the inner branch stations are zeroed, so the
153 * outer walk crosses the inner section as a single station. The `RN(join) == 0`
154 * test is what makes that happen once rather than once per outer path.
155 *
156 * A CYCLE IS REFUSED, NOT WALKED. The reference has no guard and recurses until
157 * MATLAB runs out of stack; a routing loop inside a fork-join section has no
158 * finite path set, so there is nothing to enumerate and saying so is the only
159 * available answer.
160 */
161template <class T>
162PathsResult<T> paths_cs(const qn::NetworkStruct<T>& sn, const Matrix<T>& P, std::size_t cur,
163 std::size_t stop, std::size_t cls, Matrix<T>& RN, const T& elapsed,
164 const std::vector<std::size_t>& acc_stations,
165 std::vector<std::pair<std::size_t, std::size_t>>& on_path) {
166 const std::size_t K = sn.nclasses, N = sn.nodes.size();
167 const T zero = num_traits<T>::from_int(0);
168
169 PathsResult<T> out;
170 if (cur == stop) {
171 out.times.push_back(elapsed);
172 out.stations = acc_stations;
173 return out;
174 }
175 for (std::size_t i = 0; i < on_path.size(); ++i)
176 if (on_path[i].first == cur && on_path[i].second == cls)
177 throw UnsupportedError(ErrorKind{"Fork"},
178 "getAvgSys: the fork-join section of node '" + sn.nodes[cur - 1].name +
179 "' contains a routing cycle, so the set of paths from the fork to the join is "
180 "infinite and the synchronization delay has no inclusion-exclusion form");
181 on_path.push_back(std::make_pair(cur, cls));
182
183 T here = zero;
184 std::vector<std::size_t> stations = acc_stations;
185 const std::size_t ist = sn.nodes[cur - 1].station;
186 if (ist != 0) {
187 here = RN(ist - 1, cls - 1);
188 stations.push_back(ist);
189 }
190
191 const std::size_t row = (cur - 1) * K + (cls - 1);
192 for (std::size_t nxt = 1; nxt <= N; ++nxt) {
193 for (std::size_t s = 1; s <= K; ++s) {
194 if (row >= P.rows()) break;
195 const std::size_t col = (nxt - 1) * K + (s - 1);
196 if (col >= P.cols()) continue;
197 if (num_traits<T>::to_double(P(row, col)) == 0.0) continue;
198
199 std::size_t hop = nxt;
200 T entry = T(elapsed + here);
201 if (sn.nodes[nxt - 1].nodetype == lang::NodeType::Fork) {
202 std::size_t inner_join = 0;
203 for (std::size_t k = 0; k < sn.fj.size(); ++k)
204 if (sn.fj[k].first == nxt) inner_join = sn.fj[k].second;
205 if (inner_join != 0) {
206 const std::size_t jst = sn.nodes[inner_join - 1].station;
207 if (jst != 0 && num_traits<T>::to_double(RN(jst - 1, s - 1)) == 0.0) {
208 std::vector<std::pair<std::size_t, std::size_t>> inner_path;
209 const PathsResult<T> in =
210 paths_cs(sn, P, nxt, inner_join, s, RN, zero,
211 std::vector<std::size_t>(), inner_path);
212 RN(jst - 1, s - 1) = exp_max_mean(in.times);
213 for (std::size_t q = 0; q < in.stations.size(); ++q)
214 RN(in.stations[q] - 1, s - 1) = zero;
215 }
216 hop = inner_join;
217 }
218 }
219 const PathsResult<T> sub = paths_cs(sn, P, hop, stop, s, RN, entry, stations, on_path);
220 out.times.insert(out.times.end(), sub.times.begin(), sub.times.end());
221 out.stations.insert(out.stations.end(), sub.stations.begin(), sub.stations.end());
222 }
223 }
224 on_path.pop_back();
225 return out;
226}
227
228} // namespace chain_detail
229
230/** `@@NetworkSolver/getAvgSys`: one response time and one throughput per chain. */
231template <class T>
232struct SysResult {
233 std::vector<T> CN; ///< (nchains) system response time, i.e. the cycle time
234 std::vector<T> XN; ///< (nchains) system throughput at the reference station
235};
236
237/** The station- or node-level table aggregated by chain. */
238template <class T>
240 Matrix<T> QN, UN, RN, WN, AN, TN; ///< (rows x nchains), rows = stations or nodes
241};
242
243/**
244 * Port of `@@NetworkSolver/getAvgSys.m`.
245 *
246 * FORK-JOIN WITH AN OPEN CHAIN IS SERVED (2026-08-15). The reference fills the
247 * join station's response time with the order statistic of the parallel branch
248 * times -- `d0`, an inclusion-exclusion sum over every path from the fork to
249 * the join, enumerated by `ModelAdapter.pathsCS` -- and both halves of that are
250 * ported above as `chain_detail::paths_cs` and `chain_detail::exp_max_mean`.
251 * A CLOSED fork-join chain needs neither: its cycle time comes from Little's
252 * law, which reads the population and the throughput and never touches the
253 * join's response time, and the reference skips the walk there too. The JAR
254 * still refuses this case and then fills RN with NaN.
255 */
256template <class T>
258 const std::size_t M = sn.nstations, K = sn.nclasses, C = sn.nchains;
259 const T zero = num_traits<T>::from_int(0);
260 const std::vector<double> njobs = sn.njobs();
261
262 bool has_join = false, has_fork = false;
263 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
264 if (sn.nodes[i].nodetype == lang::NodeType::Join) has_join = true;
265 if (sn.nodes[i].nodetype == lang::NodeType::Fork) has_fork = true;
266 }
267
268 // `RN(join, :) = 0` of the reference, applied before anything reads RN: the
269 // join holds no service, and whatever the solver reported there is the
270 // synchronization wait, which the cycle time accounts for at the fork.
271 Matrix<T> RN = r.RN;
272 if (has_join)
273 for (std::size_t i = 0; i < M; ++i)
274 if (sn.stations[i].nodetype == lang::NodeType::Join)
275 for (std::size_t c = 0; c < K; ++c) RN(i, c) = zero;
276
277 // ---- The synchronization delay of each fork-join section, on OPEN chains
278 // only. A closed chain gets its cycle time from Little's law, which never
279 // reads the join's response time, so the reference skips the whole walk
280 // there and so does this.
281 //
282 // ORDER MATTERS: this rewrites RN -- the join gets the delay, the branch
283 // stations get zero -- and CNclass below reads RN. Computing CNclass first
284 // would total the branch times ALONG the paths, which double-counts the
285 // parallel section instead of taking its maximum.
286 if (has_fork && has_join) {
287 for (std::size_t f = 0; f < sn.fj.size(); ++f) {
288 const std::size_t fork = sn.fj[f].first, join = sn.fj[f].second;
289 if (fork == 0 || join == 0) continue;
290 const std::size_t jst = sn.nodes[join - 1].station;
291 if (jst == 0) continue;
292 for (std::size_t c = 0; c < C; ++c) {
293 double nJobsChain = 0.0;
294 for (std::size_t k = 0; k < K; ++k)
295 if (sn.chains[c][k]) nJobsChain += njobs[k];
296 if (!std::isinf(nJobsChain)) continue;
297 for (std::size_t q = 0; q < sn.inchain[c].size(); ++q) {
298 const std::size_t rr = sn.inchain[c][q];
299 if (num_traits<T>::to_double(RN(jst - 1, rr - 1)) != 0.0) continue;
300 std::vector<std::pair<std::size_t, std::size_t>> on_path;
301 const chain_detail::PathsResult<T> paths =
302 chain_detail::paths_cs(sn, sn.rtnodes, fork, join, rr, RN, zero,
303 std::vector<std::size_t>(), on_path);
304 if (paths.times.empty()) continue;
305 // d0 already accounts for the time spent on the branches,
306 // which is why they are zeroed rather than left to be added.
307 RN(jst - 1, rr - 1) = chain_detail::exp_max_mean(paths.times);
308 for (std::size_t j = 0; j < paths.stations.size(); ++j)
309 RN(paths.stations[j] - 1, rr - 1) = zero;
310 }
311 }
312 }
313 }
314
315 // ---- CNclass: the per-class system time, visits-weighted to the reference
316 // station. Computed for every model, used only by the open branch below,
317 // exactly as the reference computes it.
318 std::vector<T> CNclass(K, zero);
319 for (std::size_t c = 0; c < C; ++c) {
320 for (std::size_t j = 0; j < sn.inchain[c].size(); ++j) {
321 const std::size_t rr = sn.inchain[c][j]; // 1-based class
322 const std::size_t refst = sn.classes[rr - 1].refstat;
323 if (refst == 0) continue;
324 const std::size_t refsf = sn.stateful_of_station(refst);
325 const T vref = sn.visits[c](refsf - 1, rr - 1);
326 if (num_traits<T>::to_double(vref) == 0.0) continue;
327 for (std::size_t i = 0; i < M; ++i) {
328 // The source of an open class is not part of its system time.
329 if (std::isinf(njobs[rr - 1]) && i + 1 == refst) continue;
330 const std::size_t isf = sn.stateful_of_station(i + 1);
331 CNclass[rr - 1] =
332 T(CNclass[rr - 1] + T(T(sn.visits[c](isf - 1, rr - 1) * RN(i, rr - 1)) / vref));
333 }
334 }
335 }
336
337 // ---- alpha: the share of its chain's reference-station completions that a
338 // class accounts for, per station. `refclass` is indexed by CHAIN and
339 // `inchain` holds CLASS indices; the reference intersects the two anyway,
340 // and this port reproduces that intersection rather than repairing it,
341 // because the weights it produces are the ones every reference number was
342 // computed with.
343 Matrix<T> alpha(M, K, zero);
344 std::vector<std::size_t> refclass_nz;
345 for (std::size_t c = 0; c < C; ++c)
346 if (sn.refclass[c] != 0) refclass_nz.push_back(c + 1);
347 for (std::size_t c = 0; c < C; ++c) {
348 // The classes of the chain that COMPLETE at the reference station: a
349 // non-completing class passes through without ending a cycle, so it
350 // contributes visits but no completions to divide by.
351 std::vector<std::size_t> completing;
352 for (std::size_t k = 0; k < K; ++k)
353 if (sn.chains[c][k] && sn.classes[k].completes) completing.push_back(k + 1);
354
355 std::vector<std::size_t> ks;
356 for (std::size_t j = 0; j < refclass_nz.size(); ++j)
357 for (std::size_t q = 0; q < sn.inchain[c].size(); ++q)
358 if (refclass_nz[j] == sn.inchain[c][q]) ks.push_back(refclass_nz[j]);
359 if (sn.refclass[c] == 0) ks = sn.inchain[c];
360
361 for (std::size_t i = 0; i < M; ++i) {
362 for (std::size_t j = 0; j < ks.size(); ++j) {
363 const std::size_t k = ks[j];
364 const std::size_t refst = sn.classes[k - 1].refstat;
365 if (refst == 0) continue;
366 const std::size_t refsf = sn.stateful_of_station(refst);
367 T denom = zero;
368 for (std::size_t q = 0; q < completing.size(); ++q)
369 denom = T(denom + sn.visits[c](refsf - 1, completing[q] - 1));
370 if (num_traits<T>::to_double(denom) == 0.0) continue;
371 const std::size_t isf = sn.stateful_of_station(i + 1);
372 alpha(i, k - 1) = T(alpha(i, k - 1) + T(sn.visits[c](isf - 1, k - 1) / denom));
373 }
374 }
375 }
376 // `alpha(~isfinite(alpha)) = 0`: a class that never reaches the reference
377 // station has no share, and a non-finite weight would poison the sum.
378 for (std::size_t i = 0; i < M; ++i)
379 for (std::size_t k = 0; k < K; ++k)
380 if (!std::isfinite(num_traits<T>::to_double(alpha(i, k)))) alpha(i, k) = zero;
381
382 SysResult<T> out;
383 out.CN.assign(C, zero);
384 out.XN.assign(C, zero);
385 const std::size_t nsf = sn.nof_stateful();
386
387 for (std::size_t c = 0; c < C; ++c) {
388 const std::vector<std::size_t>& inchain = sn.inchain[c];
389 if (inchain.empty()) continue;
390 std::vector<std::size_t> completing;
391 for (std::size_t k = 0; k < K; ++k)
392 if (sn.chains[c][k] && sn.classes[k].completes) completing.push_back(k + 1);
393
394 // ---- XN: the completing flow INTO the chain's reference station.
395 // Read off the routing matrix rather than from any one station's
396 // throughput, because a chain completes wherever its routing returns to
397 // the reference station and that can be several places at once.
398 const std::size_t ref = sn.classes[inchain[0] - 1].refstat;
399 if (ref != 0 && r.TN.rows() == M) {
400 const std::size_t refsf = sn.stateful_of_station(ref);
401 std::vector<std::size_t> ss;
402 for (std::size_t j = 0; j < refclass_nz.size(); ++j)
403 for (std::size_t q = 0; q < inchain.size(); ++q)
404 if (refclass_nz[j] == inchain[q]) ss.push_back(refclass_nz[j]);
405 if (ss.empty()) ss = inchain;
406 for (std::size_t i = 0; i < M; ++i) {
407 const std::size_t isf = sn.stateful_of_station(i + 1);
408 if (isf == 0 || isf > nsf) continue;
409 for (std::size_t q = 0; q < completing.size(); ++q) {
410 const std::size_t rr = completing[q];
411 const double tn = num_traits<T>::to_double(r.TN(i, rr - 1));
412 if (std::isnan(tn)) continue;
413 for (std::size_t j = 0; j < ss.size(); ++j) {
414 const std::size_t s = ss[j];
415 out.XN[c] = T(out.XN[c] + T(sn.rt((isf - 1) * K + (rr - 1),
416 (refsf - 1) * K + (s - 1)) *
417 r.TN(i, rr - 1)));
418 }
419 }
420 }
421 }
422
423 // ---- CN: Little's law on a closed chain, the alpha-weighted sum of the
424 // per-class system times on an open one.
425 double nJobsChain = 0.0;
426 for (std::size_t k = 0; k < K; ++k)
427 if (sn.chains[c][k]) nJobsChain += njobs[k];
428
429 if (std::isinf(nJobsChain)) {
430 if (inchain.size() != completing.size())
431 throw UnsupportedError(ErrorKind{"OpenClass"},
432 "getAvgSys: edge-based chain definition is not supported for open queueing "
433 "networks -- the chain holds a non-completing class, so there is no single "
434 "flow whose reciprocal is the cycle time");
435 const std::size_t refst = sn.classes[inchain[0] - 1].refstat;
436 T acc = zero;
437 for (std::size_t j = 0; j < inchain.size(); ++j) {
438 const T v = T(alpha(refst - 1, inchain[j] - 1) * CNclass[inchain[j] - 1]);
439 // `sumfinite`: a class the solver reported no finite time for is
440 // skipped, not propagated as Inf over the whole chain.
441 if (std::isfinite(num_traits<T>::to_double(v))) acc = T(acc + v);
442 }
443 out.CN[c] = acc;
444 } else {
445 const double x = num_traits<T>::to_double(out.XN[c]);
446 if (x != 0.0) {
447 out.CN[c] = T(num_traits<T>::from_double(nJobsChain) / out.XN[c]);
448 } else if (nJobsChain > 0.0) {
449 // N/0 of the reference: a populated chain that never completes (trapped at a
450 // zero-capacity station) has an infinite cycle time, not a zero one.
451 if constexpr (num_traits<T>::is_exact)
452 throw UnsupportedError(ErrorKind{"Arithmetic"},
453 "getAvgSys: the chain holds jobs but completes none, so its cycle time is "
454 "infinite, which exact arithmetic cannot represent; rerun in double");
455 else
456 out.CN[c] = num_traits<T>::from_double(std::numeric_limits<double>::infinity());
457 }
458 }
459 }
460 return out;
461}
462
463/**
464 * Port of `@@NetworkSolver/getAvgChain.m`: the station table aggregated by chain.
465 *
466 * QLen, Util, ArvR, Tput and ResidT are row sums over the chain's classes;
467 * RespT is the alpha-weighted average, alpha being `sn_get_demands_chain`'s
468 * visit share. See the file header for why the two rules differ.
469 */
470template <class T>
472 const mva::AvgResult<T>& r) {
473 const std::size_t M = sn.nstations, C = sn.nchains;
474 const T zero = num_traits<T>::from_int(0);
476
477 ChainResult<T> out;
478 out.QN = Matrix<T>(M, C, zero);
479 out.UN = Matrix<T>(M, C, zero);
480 out.RN = Matrix<T>(M, C, zero);
481 out.WN = Matrix<T>(M, C, zero);
482 out.AN = Matrix<T>(M, C, zero);
483 out.TN = Matrix<T>(M, C, zero);
484 for (std::size_t c = 0; c < C; ++c) {
485 for (std::size_t j = 0; j < sn.inchain[c].size(); ++j) {
486 const std::size_t k = sn.inchain[c][j] - 1;
487 for (std::size_t i = 0; i < M; ++i) {
488 out.QN(i, c) = T(out.QN(i, c) + r.QN(i, k));
489 out.UN(i, c) = T(out.UN(i, c) + r.UN(i, k));
490 out.WN(i, c) = T(out.WN(i, c) + r.WN(i, k));
491 out.AN(i, c) = T(out.AN(i, c) + r.AN(i, k));
492 out.TN(i, c) = T(out.TN(i, c) + r.TN(i, k));
493 out.RN(i, c) = T(out.RN(i, c) + T(r.RN(i, k) * dem.alpha(i, k)));
494 }
495 }
496 }
497 return out;
498}
499
500/**
501 * Port of `@@NetworkSolver/getAvgNodeChain.m`: the NODE table aggregated by chain.
502 *
503 * The node-level class matrices are the caller's, because the scatter from
504 * stations to nodes and the recomputation of ArvR and Tput per node is
505 * `getAvgNodeTable`'s work and is not repeated here. Rows that are not stations
506 * carry zero response and residence time, which is the reference's construction
507 * and not a gap: a node that is not a station holds no jobs.
508 */
509template <class T>
511 const Matrix<T>& UNn, const Matrix<T>& RNn,
512 const Matrix<T>& WNn, const Matrix<T>& ANn,
513 const Matrix<T>& TNn) {
514 const std::size_t I = sn.nodes.size(), C = sn.nchains;
515 const T zero = num_traits<T>::from_int(0);
517
518 ChainResult<T> out;
519 out.QN = Matrix<T>(I, C, zero);
520 out.UN = Matrix<T>(I, C, zero);
521 out.RN = Matrix<T>(I, C, zero);
522 out.WN = Matrix<T>(I, C, zero);
523 out.AN = Matrix<T>(I, C, zero);
524 out.TN = Matrix<T>(I, C, zero);
525 // Node -> station, so the alpha weights (which are indexed by station) can
526 // be applied to the response time column.
527 std::vector<std::size_t> node_to_station(I, 0);
528 for (std::size_t ist = 0; ist < sn.nstations; ++ist) {
529 const std::size_t ind = sn.station_to_node[ist];
530 if (ind) node_to_station[ind - 1] = ist + 1;
531 }
532 for (std::size_t c = 0; c < C; ++c) {
533 for (std::size_t j = 0; j < sn.inchain[c].size(); ++j) {
534 const std::size_t k = sn.inchain[c][j] - 1;
535 for (std::size_t i = 0; i < I; ++i) {
536 out.QN(i, c) = T(out.QN(i, c) + QNn(i, k));
537 out.UN(i, c) = T(out.UN(i, c) + UNn(i, k));
538 out.AN(i, c) = T(out.AN(i, c) + ANn(i, k));
539 out.TN(i, c) = T(out.TN(i, c) + TNn(i, k));
540 const std::size_t ist = node_to_station[i];
541 if (!ist) continue;
542 out.RN(i, c) = T(out.RN(i, c) + T(RNn(i, k) * dem.alpha(ist - 1, k)));
543 // a residence time already carries the visits: the chain's is the plain sum, as in
544 // solver_get_avg_chain; weighting it by alpha counted the visits twice
545 out.WN(i, c) = T(out.WN(i, c) + WNn(i, k));
546 }
547 }
548 }
549 return out;
550}
551
552/** `Chain1`, `Chain2`, ... -- the reference's own chain labels. */
553inline std::vector<std::string> chain_names(std::size_t nchains) {
554 std::vector<std::string> out;
555 for (std::size_t c = 0; c < nchains; ++c) out.push_back("Chain" + std::to_string(c + 1));
556 return out;
557}
558
559/** `(ClassA ClassB)`, the JobClasses column: which classes a chain holds. */
560template <class T>
561std::vector<std::string> chain_class_labels(const qn::NetworkStruct<T>& sn) {
562 std::vector<std::string> out;
563 for (std::size_t c = 0; c < sn.nchains; ++c) {
564 std::string s = "(";
565 for (std::size_t j = 0; j < sn.inchain[c].size(); ++j) {
566 if (j) s += " ";
567 s += sn.classes[sn.inchain[c][j] - 1].name;
568 }
569 out.push_back(s + ")");
570 }
571 return out;
572}
573
574} // namespace solvers
575} // namespace line
576
577#endif // LINE_SOLVERS_SOLVER_CHAIN_TABLES_H
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
bool in(const std::string &s, std::initializer_list< const char * > set)
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
std::vector< std::string > chain_class_labels(const qn::NetworkStruct< T > &sn)
(ClassA ClassB), the JobClasses column: which classes a chain holds.
SysResult< T > solver_get_avg_sys(const qn::NetworkStruct< T > &sn, const mva::AvgResult< T > &r)
Port of @@NetworkSolver/getAvgSys.m.
ChainResult< T > solver_get_avg_node_chain(const qn::NetworkStruct< T > &sn, const Matrix< T > &QNn, const Matrix< T > &UNn, const Matrix< T > &RNn, const Matrix< T > &WNn, const Matrix< T > &ANn, const Matrix< T > &TNn)
Port of @@NetworkSolver/getAvgNodeChain.m: the NODE table aggregated by chain.
ChainResult< T > solver_get_avg_chain(const qn::NetworkStruct< T > &sn, const mva::AvgResult< T > &r)
Port of @@NetworkSolver/getAvgChain.m: the station table aggregated by chain.
std::vector< std::string > chain_names(std::size_t nchains)
Chain1, Chain2, ... – the reference's own chain labels.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Chain aggregation and de-aggregation.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
Matrix< T > UN
utilization
Matrix< T > WN
residence time, per job
Matrix< T > QN
queue length
Matrix< T > AN
arrival rate
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
Matrix< T > alpha
(M x K) class share of its chain's visits at a station
Definition sn_chain.h:50
The station- or node-level table aggregated by chain.
Matrix< T > TN
(rows x nchains), rows = stations or nodes
@@NetworkSolver/getAvgSys: one response time and one throughput per chain.
std::vector< T > XN
(nchains) system throughput at the reference station
std::vector< T > CN
(nchains) system response time, i.e. the cycle time