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