LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_chain.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_MVA_SN_CHAIN_H
6#define LINE_SOLVERS_MVA_SN_CHAIN_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Chain aggregation and de-aggregation.
12 *
13 * Ports of matlab/src/api/sn/sn_get_demands_chain.m,
14 * sn_get_product_form_chain_params.m and sn_deaggregate_chain_results.m. These
15 * are the bridge between the class-level layer struct and the chain-level
16 * product-form solvers: a chain is a set of classes a job moves between while
17 * circulating, so the solvers work on chains and the results are pushed back to
18 * classes with the visit-share weights `alpha`.
19 *
20 * NON-FINITE VALUES. MATLAB reaches this code with NaN and Inf in `sn.rates`
21 * (NaN for a station-class pair the class never visits, Inf for an immediate
22 * service) and clears them afterwards with `X(~isfinite(X)) = 0`. The exact
23 * backend has neither value, so the two are carried as explicit flags -- the
24 * layer's `disabled` mask, and a local `st_inf` mask for a zero rate -- and the
25 * same entries end up zero. Every place a MATLAB `isfinite` guard fires, the
26 * corresponding flag test fires here.
27 */
28
29#include <algorithm>
30#include <cmath>
31#include <vector>
32
34#include "line/num/number.h"
35#include "line/util/error.h"
36#include "line/util/matrix.h"
37
38namespace line {
39namespace mva {
40
41using lang::GlobalConstants;
43
44/** The chain-level view of a layer, as sn_get_demands_chain returns it. */
45template <class T>
47 Matrix<T> Lchain; ///< (M x C) demand
48 Matrix<T> STchain; ///< (M x C) mean service time
49 Matrix<T> Vchain; ///< (M x C) visits
50 Matrix<T> alpha; ///< (M x K) class share of its chain's visits at a station
51 std::vector<double> Nchain; ///< (C) population, infinite for an open chain
52 Matrix<T> SCVchain; ///< (M x C)
53 std::vector<std::size_t> refstatchain; ///< (C) 1-based reference station
54 Matrix<T> ST; ///< (M x K) class-level mean service time, 0 where disabled
55};
56
57/**
58 * Port of sn_get_demands_chain.
59 *
60 * @param L the refreshed layer
61 */
62template <class T>
64 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
65 const T zero = num_traits<T>::from_int(0);
66 const T one = num_traits<T>::from_int(1);
67
69 d.ST = Matrix<T>(M, K, zero);
70 std::vector<std::vector<bool>> st_inf(M, std::vector<bool>(K, false));
71 Matrix<T> SCV(M, K, one);
72 std::vector<std::vector<bool>> scv_known(M, std::vector<bool>(K, true));
73 for (std::size_t i = 0; i < M; ++i)
74 for (std::size_t r = 0; r < K; ++r) {
75 if (L.disabled[i][r]) {
76 // MATLAB: ST = 1./rates gives NaN, then ST(isnan(ST)) = 0;
77 // SCV(isnan(SCV)) = 1
78 d.ST(i, r) = zero;
79 SCV(i, r) = one;
80 continue;
81 }
82 if (L.rates(i, r) == zero) {
83 // 1/0 = Inf in MATLAB; every use is guarded by isfinite and
84 // ends up zeroed, so the flag records it and the value stays 0
85 st_inf[i][r] = true;
86 d.ST(i, r) = zero;
87 } else {
88 d.ST(i, r) = T(one / L.rates(i, r));
89 }
90 SCV(i, r) = L.scv(i, r);
91 }
92
93 d.alpha = Matrix<T>(M, K, zero);
94 d.Vchain = Matrix<T>(M, C, zero);
95 std::vector<std::vector<bool>> v_bad(M, std::vector<bool>(C, false));
96
97 for (std::size_t c = 0; c < C; ++c) {
98 const std::vector<std::size_t>& ic = L.inchain[c];
99 const std::size_t rstat = L.classes[ic[0] - 1].refstat;
100 // denominator: the reference class's visits when the chain has one,
101 // otherwise the whole chain's visits at the reference station
102 T den = zero;
103 const std::size_t rsf = L.stateful_of_station(rstat) - 1;
104 if (L.refclass[c] > 0) {
105 den = L.visits[c](rsf, L.refclass[c] - 1);
106 } else {
107 for (std::size_t k : ic) den += L.visits[c](rsf, k - 1);
108 }
109 for (std::size_t i = 0; i < M; ++i) {
110 const std::size_t sf = L.stateful_of_station(i + 1) - 1;
111 T num = zero;
112 for (std::size_t k : ic) num += L.visits[c](sf, k - 1);
113 if (den == zero)
114 v_bad[i][c] = true; // MATLAB: 0/0 or x/0 -> NaN or Inf, then zeroed
115 else
116 d.Vchain(i, c) = T(num / den);
117 if (num != zero)
118 for (std::size_t k : ic)
119 d.alpha(i, k - 1) = T(d.alpha(i, k - 1) + L.visits[c](sf, k - 1) / num);
120 }
121 }
122 for (std::size_t i = 0; i < M; ++i)
123 for (std::size_t c = 0; c < C; ++c)
124 if (v_bad[i][c]) d.Vchain(i, c) = zero;
125
126 // renormalise each chain's visits by the value at its reference station
127 for (std::size_t c = 0; c < C; ++c) {
128 const std::size_t rstat = L.classes[L.inchain[c][0] - 1].refstat;
129 const T vref = d.Vchain(rstat - 1, c);
130 if (vref == zero) continue; // MATLAB divides by 0 and clears below
131 for (std::size_t i = 0; i < M; ++i) d.Vchain(i, c) = T(d.Vchain(i, c) / vref);
132 }
133 for (std::size_t i = 0; i < M; ++i)
134 for (std::size_t k = 0; k < K; ++k)
136 d.alpha(i, k) = zero;
137
138 d.Lchain = Matrix<T>(M, C, zero);
139 d.STchain = Matrix<T>(M, C, zero);
140 d.SCVchain = Matrix<T>(M, C, zero);
141 d.Nchain.assign(C, 0.0);
142 d.refstatchain.assign(C, 1);
143
144 for (std::size_t c = 0; c < C; ++c) {
145 const std::vector<std::size_t>& ic = L.inchain[c];
146 bool open = false;
147 double n = 0.0;
148 for (std::size_t k : ic) {
149 const double p = L.classes[k - 1].population;
150 if (std::isinf(p)) open = true;
151 n += p;
152 }
153 d.Nchain[c] = open ? std::numeric_limits<double>::infinity() : n;
154 const std::size_t rstat = L.classes[ic[0] - 1].refstat;
155 d.refstatchain[c] = rstat;
156
157 for (std::size_t i = 0; i < M; ++i) {
158 bool bad = false;
159 T st = zero;
160 for (std::size_t k : ic) {
161 if (st_inf[i][k - 1] && d.alpha(i, k - 1) != zero) bad = true;
162 st += d.ST(i, k - 1) * d.alpha(i, k - 1);
163 }
164 if (open && i + 1 == rstat) {
165 // a source station: the chain service time is 1 / total arrival rate
166 T lam = zero;
167 for (std::size_t k : ic)
168 if (!L.disabled[i][k - 1]) lam += L.rates(i, k - 1);
169 if (lam == zero)
170 bad = true;
171 else
172 st = T(num_traits<T>::from_int(1) / lam);
173 }
174 d.STchain(i, c) = bad ? zero : st;
175 d.Lchain(i, c) = T(d.Vchain(i, c) * d.STchain(i, c));
176
177 T ach = zero;
178 for (std::size_t k : ic) ach += d.alpha(i, k - 1);
179 if (ach > zero) {
180 T s = zero;
181 for (std::size_t k : ic) s += SCV(i, k - 1) * d.alpha(i, k - 1);
182 d.SCVchain(i, c) = T(s / ach);
183 }
184 }
185 }
186 return d;
187}
188
189/** Class-level results, as sn_deaggregate_chain_results returns them. */
190template <class T>
193 std::vector<T> C, X;
194};
195
196/**
197 * Port of sn_deaggregate_chain_results.
198 *
199 * OPEN CHAINS. A closed class's share of its chain's throughput is its share of
200 * the visits at the reference station; an open class's is its share of the
201 * DEPARTURES, i.e. the chain's visit count at the Sink, because an open job
202 * completes by leaving rather than by returning. Only X and C are computed this
203 * way; Q, U, R and T follow the same path for both.
204 *
205 * The Sink visits come from this layer's own `nodevisits`, normalised by the
206 * chain's reference NODE. MATLAB normalises the node-level visits by
207 * `statefulToNode(refstat(...))`, which feeds a STATION index to a STATEFUL ->
208 * node map and lands on an unrelated node, so its Sink visits carry an
209 * arbitrary scale factor (3 rather than 1 on the fork layer of lqn_workflows).
210 * The correct normalisation is used here. It reaches X and C only, which
211 * SolverLN does not read back, so no layered metric depends on the choice.
212 */
213template <class T>
215 const Matrix<T>& Qchain, const Matrix<T>& Uchain,
216 const Matrix<T>& Rchain, const Matrix<T>& Tchain,
217 const std::vector<T>& Xchain) {
218 const std::size_t M = L.nstations, K = L.nclasses;
219 const T zero = num_traits<T>::from_int(0);
221 r.Q = Matrix<T>(M, K, zero);
222 r.U = Matrix<T>(M, K, zero);
223 r.R = Matrix<T>(M, K, zero);
224 r.Tp = Matrix<T>(M, K, zero);
225 r.C.assign(K, zero);
226 r.X.assign(K, zero);
227
228 std::vector<T> Vsink(K, zero);
229 if (L.sinkNode > 0)
230 for (std::size_t c = 0; c < L.nchains; ++c)
231 for (std::size_t k = 0; k < K; ++k) Vsink[k] += L.nodevisits[c](L.sinkNode - 1, k);
232
233 for (std::size_t c = 0; c < L.nchains; ++c) {
234 const std::vector<std::size_t>& ic = L.inchain[c];
235 const bool open = std::isinf(d.Nchain[c]);
236 if (open && L.sinkNode == 0)
237 throw UnsupportedError(
238 "sn_deaggregate_chain_results: an open chain needs the Sink node visits, and this "
239 "layer has no Sink node to read them from");
240 for (std::size_t kk : ic) {
241 const std::size_t k = kk - 1;
242 const std::size_t rstat = L.classes[k].refstat;
243 r.X[k] = open ? T(Xchain[c] * Vsink[k]) : T(Xchain[c] * d.alpha(rstat - 1, k));
244 const T vref = d.Vchain(rstat - 1, c);
245 for (std::size_t i = 0; i < M; ++i) {
246 const double S = L.stations[i].nservers;
247 if (vref != zero) {
248 const T base = T(d.ST(i, k) * (Xchain[c] * d.Vchain(i, c) / vref) *
249 d.alpha(i, k));
250 if (std::isinf(S)) {
251 r.U(i, k) = base;
252 } else if (Uchain.rows() == 0) {
253 r.U(i, k) = T(base / num_traits<T>::from_double(S));
254 } else {
255 r.U(i, k) = T(Uchain(i, c) * d.alpha(i, k));
256 }
257 }
258 if (d.Lchain(i, c) > zero) {
259 if (Qchain.rows() > 0) {
260 r.Q(i, k) = T(Qchain(i, c) * d.alpha(i, k));
261 } else if (d.STchain(i, c) != zero && vref != zero) {
262 r.Q(i, k) = T(Rchain(i, c) * d.ST(i, k) / d.STchain(i, c) * Xchain[c] *
263 d.Vchain(i, c) / vref * d.alpha(i, k));
264 }
265 r.Tp(i, k) = T(Tchain(i, c) * d.alpha(i, k));
266 r.R(i, k) = r.Tp(i, k) == zero ? zero : T(r.Q(i, k) / r.Tp(i, k));
267 }
268 }
269 const double njobs = L.classes[k].population;
270 r.C[k] = r.X[k] == zero ? zero
271 : T(num_traits<T>::from_double(njobs) / r.X[k]);
272 }
273 }
274 auto absify = [&](Matrix<T>& A) {
275 for (std::size_t i = 0; i < A.rows(); ++i)
276 for (std::size_t j = 0; j < A.cols(); ++j)
277 if (A(i, j) < zero) A(i, j) = T(-A(i, j));
278 };
279 absify(r.Q);
280 absify(r.U);
281 absify(r.R);
282 absify(r.Tp);
283 for (T& x : r.C)
284 if (x < zero) x = T(-x);
285 for (T& x : r.X)
286 if (x < zero) x = T(-x);
287 return r;
288}
289
290/** Product-form chain parameters, the split into queueing and delay stations. */
291template <class T>
293 Matrix<T> D; ///< (Mq x C) demands at queueing stations
294 Matrix<T> Z; ///< (Md x C) demands at delay stations
295 std::vector<T> lambda; ///< (C) chain arrival rates, zero on a closed chain
296 std::vector<double> N; ///< (C) populations
297 std::vector<double> S; ///< (Mq) server counts
298 std::vector<std::size_t> queue_stations; ///< 1-based
299 std::vector<std::size_t> delay_stations; ///< 1-based
300};
301
302/**
303 * Port of sn_get_product_form_chain_params.
304 *
305 * `lambda` is the chain-level arrival rate the reference builds by summing the class
306 * rates of `sn_get_product_form_params` over `sn.inchain{c}`: the Source's rate for an
307 * open class, nothing for a closed one. Only the mixed `lin` branch of solver_amva
308 * reads it, so a closed model leaves the vector at zero.
309 */
310template <class T>
313 for (std::size_t i = 0; i < L.nstations; ++i) {
314 if (L.stations[i].nodetype == qn::NodeType::Queue) p.queue_stations.push_back(i + 1);
315 else if (L.stations[i].nodetype == qn::NodeType::Delay) p.delay_stations.push_back(i + 1);
316 }
317 const T zero = num_traits<T>::from_int(0);
318 p.D = Matrix<T>(p.queue_stations.size(), L.nchains, zero);
319 p.Z = Matrix<T>(p.delay_stations.size(), L.nchains, zero);
320 for (std::size_t c = 0; c < L.nchains; ++c) {
321 for (std::size_t a = 0; a < p.queue_stations.size(); ++a)
322 p.D(a, c) = d.Lchain(p.queue_stations[a] - 1, c);
323 for (std::size_t a = 0; a < p.delay_stations.size(); ++a)
324 p.Z(a, c) = d.Lchain(p.delay_stations[a] - 1, c);
325 }
326 p.N = d.Nchain;
327
328 // An open class contributes the Source's rate for it, a closed one nothing. A
329 // disabled pair contributes nothing either, which is where MATLAB's 'omitnan' sum
330 // lands: sn.rates is NaN there.
331 std::size_t source_station = 0;
332 for (std::size_t i = 0; i < L.nstations; ++i)
333 if (L.stations[i].nodetype == qn::NodeType::Source) source_station = i + 1;
334 p.lambda.assign(L.nchains, zero);
335 if (source_station > 0)
336 for (std::size_t c = 0; c < L.nchains; ++c)
337 for (std::size_t r = 0; r < L.nclasses; ++r) {
338 if (!L.chains[c][r]) continue;
339 if (!std::isinf(L.classes[r].population)) continue;
340 if (L.disabled[source_station - 1][r]) continue;
341 p.lambda[c] += L.rates(source_station - 1, r);
342 }
343
344 for (std::size_t a = 0; a < p.queue_stations.size(); ++a)
345 p.S.push_back(L.stations[p.queue_stations[a] - 1].nservers);
346 return p;
347}
348
349/**
350 * Aggregate a class-indexed interlock matrix to the chain basis the MVA analyzers
351 * work in. IL[r][s] is the share of the class-s queue that a class-r arrival must
352 * not see, the interlocked flow of Franks (1999), Eq. (4.7). Two classes of the same
353 * chain belong to the same client, so the diagonal blocks carry no information and
354 * the chain diagonal stays zero: an arrival always sees its own chain in full.
355 * Returns an empty matrix when nothing is interlocked.
356 */
357template <class T>
359 const std::vector<std::vector<double>>& ILclass) {
360 if (ILclass.empty()) return Matrix<T>();
361 const std::size_t R = L.nclasses, K = L.nchains;
362 if (ILclass.size() != R)
363 throw InputError("sn_interlock_chain: the interlock matrix disagrees with the class count");
364 const T zero = num_traits<T>::from_int(0);
365 Matrix<T> ILchain(K, K, zero);
366 bool any = false;
367 for (std::size_t cr = 0; cr < K; ++cr)
368 for (std::size_t cs = 0; cs < K; ++cs) {
369 if (cr == cs) continue;
370 double best = 0.0;
371 for (std::size_t r = 0; r < R; ++r) {
372 if (!L.chains[cr][r]) continue;
373 if (ILclass[r].size() != R)
374 throw InputError("sn_interlock_chain: the interlock matrix is not square");
375 for (std::size_t sIl = 0; sIl < R; ++sIl) {
376 if (!L.chains[cs][sIl]) continue;
377 if (ILclass[r][sIl] > best) best = ILclass[r][sIl];
378 }
379 }
380 if (best > 0.0) {
381 ILchain(cr, cs) = num_traits<T>::from_double(best);
382 any = true;
383 }
384 }
385 return any ? ILchain : Matrix<T>();
386}
387
388} // namespace mva
389} // namespace line
390
391#endif // LINE_SOLVERS_MVA_SN_CHAIN_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< Matrix< T > > nodevisits
(nchains) each (nnodes x nclasses)
std::size_t stateful_of_station(std::size_t st) const
std::vector< std::vector< bool > > chains
(nchains x nclasses)
std::vector< std::size_t > refclass
(nchains) 1-based class, 0 = none
std::vector< std::vector< bool > > disabled
std::vector< JobClass > classes
std::size_t sinkNode
1-based NODE index of the Sink, 0 = none (it is not a station)
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
The exception types the port throws.
Dense matrix and non-owning view.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
PfChainParams< T > sn_get_product_form_chain_params(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d)
Port of sn_get_product_form_chain_params.
Definition sn_chain.h:311
Matrix< T > sn_interlock_chain(const qn::NetworkStruct< T > &L, const std::vector< std::vector< double > > &ILclass)
Aggregate a class-indexed interlock matrix to the chain basis the MVA analyzers work in.
Definition sn_chain.h:358
ClassResults< T > sn_deaggregate_chain_results(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, const Matrix< T > &Qchain, const Matrix< T > &Uchain, const Matrix< T > &Rchain, const Matrix< T > &Tchain, const std::vector< T > &Xchain)
Port of sn_deaggregate_chain_results.
Definition sn_chain.h:214
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
Matrix< T > ST
(M x K) class-level mean service time, 0 where disabled
Definition sn_chain.h:54
std::vector< std::size_t > refstatchain
(C) 1-based reference station
Definition sn_chain.h:53
std::vector< double > Nchain
(C) population, infinite for an open chain
Definition sn_chain.h:51
Matrix< T > alpha
(M x K) class share of its chain's visits at a station
Definition sn_chain.h:50
Matrix< T > STchain
(M x C) mean service time
Definition sn_chain.h:48
Matrix< T > Lchain
(M x C) demand
Definition sn_chain.h:47
Matrix< T > Vchain
(M x C) visits
Definition sn_chain.h:49
Matrix< T > SCVchain
(M x C)
Definition sn_chain.h:52
Class-level results, as sn_deaggregate_chain_results returns them.
Definition sn_chain.h:191
std::vector< T > C
Definition sn_chain.h:193
std::vector< T > X
Definition sn_chain.h:193
static constexpr double Zero
Definition lang_types.h:670
Product-form chain parameters, the split into queueing and delay stations.
Definition sn_chain.h:292
Matrix< T > D
(Mq x C) demands at queueing stations
Definition sn_chain.h:293
std::vector< T > lambda
(C) chain arrival rates, zero on a closed chain
Definition sn_chain.h:295
Matrix< T > Z
(Md x C) demands at delay stations
Definition sn_chain.h:294
std::vector< std::size_t > delay_stations
1-based
Definition sn_chain.h:299
std::vector< double > N
(C) populations
Definition sn_chain.h:296
std::vector< std::size_t > queue_stations
1-based
Definition sn_chain.h:298
std::vector< double > S
(Mq) server counts
Definition sn_chain.h:297