LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_bgchain.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_MAM_SOLVER_MAM_BGCHAIN_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_BGCHAIN_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_bgchain.m` and its three helpers: the analyzer that
12 * treats the CLOSED classes as a background modulating chain and the OPEN
13 * classes as matrix-analytic queues driven by it. A purely CLOSED model is the
14 * degenerate case of the same construction -- with no open work to take a share
15 * of the servers the chain alone answers, and it answers with the EXACT closed
16 * CTMC at chain granularity -- so only a purely OPEN model is refused.
17 *
18 * WHY IT EXISTS. `dec.source` replaces a closed chain by a Poisson surrogate at
19 * the current throughput iterate, which is exactly the part of a mixed model it
20 * has least information about; measured against SolverCTMC that costs it 10-24%
21 * on the queue lengths. But the closed population vector is the one part of a
22 * mixed model whose state space is BOUNDED, so it can be solved exactly. This
23 * method does that, and hands each open station a station-local Markovian
24 * environment read off the chain, turning it into a level-dependent QBD whose
25 * phase carries the number of closed jobs competing for its server.
26 *
27 * 1 background chain the closed population vector over the stations the
28 * closed classes visit (mam_bgchain_ctmc)
29 * 2 environment that chain lumped onto the closed occupancy of ONE
30 * station (mam_bgchain_env)
31 * 3 open station a MAP/PH/c queue modulated by that environment,
32 * solved as a level-dependent QBD (mam_bgchain_station)
33 * 4 fixed point the capacity share feeds step 1 and closes
34 *
35 * TAGGED-CLASS ITERATION. Step 1 is a population process of dimension (closed
36 * chains) x (stations), so its state space is exponential in the number of
37 * closed chains R. The method therefore keeps ONE chain free at a time: the
38 * tagged chain r is carried exactly, the other R-1 collapse into flow-equivalent
39 * aggregate classes whose population is their total and whose service time and
40 * routing at each station are their throughput-weighted means
41 * (Chandy-Herzog-Woo). Each chain takes its turn as the tagged one and reads its
42 * own metrics off the chain it is exact in; the open results are averaged over
43 * the passes.
44 *
45 * HOW MUCH TO AGGREGATE is options.config.bgaggr, the number G of aggregate
46 * classes; the background chain then carries 1 + G. G = 1 is the classic
47 * tagged/aggregate pair and the default, so the chain stays two-class whatever R
48 * is; G >= R-1 aggregates nothing, carries every closed chain exactly, and answers
49 * in ONE pass instead of solving the same chain R times. Passing R reaches that, so
50 * asking for no aggregation needs no magic value. The cost is the state space, the
51 * product over the 1 + G classes of nchoosek(N_b + Mc - 1, Mc - 1), capped by
52 * bgstates_max.
53 *
54 * WHICH CHAINS SHARE A GROUP is decided by similarity of per-station SERVICE
55 * DEMAND. An aggregate carries the flow-weighted mean of its members' service times
56 * and routing, so it is exact when they place the same demand at every station and
57 * distorts in proportion to how far apart they are; grouping the demand-similar
58 * chains together keeps the aggregation where it is harmless and away from the
59 * chains it would misrepresent.
60 *
61 * THE EXCHANGED QUANTITY IS THE SHARE, NOT THE MEAN OCCUPANCY. The two halves
62 * iterate on `cshare(i,e) = E[min(e+k,c) e/(e+k)]`, the mean number of servers
63 * of station i that its e closed jobs hold, averaged over the open occupancy k.
64 * Exchanging the mean open occupancy instead and rebuilding the share from it is
65 * what a first cut does and it is WRONG: e/(e+k) is convex in k, so Jensen
66 * biases the closed service rate down and the closed throughput with it. The
67 * environment generator inside the QBD is level-dependent for the same reason.
68 *
69 * EXACTNESS, measured against SolverCTMC and exact MVA on mixed models of two to
70 * four stations: PS or INF with ANY service law (exponential, Erlang, HyperExp,
71 * Coxian), any number of servers, Poisson or MAP arrivals and one to four closed
72 * chains agree to 4-5 significant digits, as does FCFS with class-INDEPENDENT
73 * rates. FCFS with class-DEPENDENT rates keeps the closed queue lengths within
74 * ~1% while the open queue length reads 14-20% low, the server being held here
75 * in random order rather than head-of-line.
76 *
77 * PS IS INSENSITIVE to the service law beyond its mean, and the method honours
78 * that rather than approximating it: at a PS station the open service is
79 * replaced by the exponential of the same mean before the QBD is built. This QBD
80 * tracks ONE service phase for the whole station, so carrying the phase-type
81 * there makes the open queue length inherit the SCV-sensitivity of an M/PH/1
82 * FCFS queue -- measured, a HyperExp of SCV 4 read 21% high where the exact
83 * answer is the exponential one to five digits. At an FCFS station the service
84 * law IS carried, collapsed into one phase-type process scaled by the share (the
85 * same collapse `solver_mam_ldqbd` documents), and the background chain reads
86 * only the MEAN closed service time.
87 *
88 * ARITHMETIC. The path runs `ctmc_solve` on the background chain and the
89 * level-dependent QBD recursion on each station, the latter falling back to a
90 * pseudo-inverse on a singular level, so it is gated on transcendental
91 * arithmetic exactly as `solver_mam_ldqbd` is.
92 */
93
94#include <algorithm>
95#include <cmath>
96#include <cstddef>
97#include <limits>
98#include <map>
99#include <string>
100#include <vector>
101
102#include "line/api/mam/ldqbd.h"
111#include "line/lang/qn/state.h"
115#include "line/util/error.h"
116#include "line/util/linalg.h"
117#include "line/util/matrix.h"
118
119namespace line {
120namespace mam {
121
122namespace bgchain_detail {
123
124/**
125 * Block of one background class: the ways to place N jobs over the m stations
126 * that class visits.
127 *
128 * This is `qn::space_closed_single`, the lattice primitive the CTMC solver
129 * enumerates a closed population over, so the row order and the row count are
130 * the reference's rather than this file's; only the support differs, being the
131 * class's own stations rather than all of them.
132 */
133inline std::vector<std::vector<int>> closed_block(std::size_t m, int N) {
134 // The primitive is templated on the numeric type of a state row and
135 // `num_traits` is specialised for the field types only, so it is taken at
136 // double and rounded back, as the JAR twin does off its Matrix.
137 const std::vector<std::vector<double>> rows =
138 qn::space_closed_single<double>(m, static_cast<std::size_t>(N));
139 std::vector<std::vector<int>> out(rows.size(), std::vector<int>(m, 0));
140 for (std::size_t r = 0; r < rows.size(); ++r)
141 for (std::size_t t = 0; t < m; ++t)
142 out[r][t] = static_cast<int>(std::lround(rows[r][t]));
143 return out;
144}
145
146/**
147 * Share of the server capacity that k open jobs hold when e closed jobs are also
148 * present: min(k+e,c) busy servers times the open fraction k/(k+e).
149 */
150inline double open_share(double k, double e, double c) {
151 const double tot = k + e;
152 if (!(tot > 0.0)) return 0.0;
153 return std::min(tot, c) * (k / tot);
154}
155
156/** The mirror image of open_share, for the closed jobs. */
157inline double closed_share(double e, double k, double c) {
158 const double tot = e + k;
159 if (!(tot > 0.0)) return 0.0;
160 return std::min(tot, c) * (e / tot);
161}
162
163/**
164 * Group the columns of D into G clusters by per-station SERVICE DEMAND
165 * (mam_bgchain_groups.m).
166 *
167 * WHY DEMAND IS THE RIGHT CRITERION. The aggregate that replaces a group carries
168 * the flow-weighted mean of its members' service times and routing, so the group
169 * aggregates EXACTLY when its members place the same demand at every station and
170 * distorts both quantities in proportion to how far apart they are. The distance
171 * is the symmetric relative L1 gap between the demand vectors,
172 *
173 * dist(a,b) = sum_i |D[i][a] - D[i][b]| / ((sum_i D[i][a] + sum_i D[i][b])/2),
174 *
175 * scale-relative rather than absolute: it separates two chains whose demand
176 * PROFILE across the stations differs and two whose profile agrees but whose
177 * magnitude does not, and being dimensionless it groups a model the same way
178 * whatever its time unit.
179 *
180 * WHY COMPLETE LINKAGE. Agglomerative from singletons, merging the pair of
181 * clusters whose WORST member-to-member distance is smallest. The aggregation
182 * error inside a group is driven by its worst mismatch, not its average one.
183 *
184 * DETERMINISM. Ties break on the lexicographically smallest pair of cluster
185 * indices and the groups are relabelled by their smallest member, so the same
186 * input gives the same grouping in MATLAB, the JAR, Python and C++.
187 *
188 * @param D (Mc x n) per-station demand, one column per chain
189 * @param G number of groups wanted, clamped to [1, n]
190 * @return group index in 0..G-1 of each chain
191 */
192inline std::vector<std::size_t> mam_bgchain_groups(const std::vector<std::vector<double>>& D,
193 std::size_t G) {
194 const std::size_t Mc = D.size();
195 const std::size_t n = (Mc == 0) ? 0 : D[0].size();
196 std::vector<std::size_t> grp(n, 0);
197 if (n == 0) return grp;
198 if (G < 1) G = 1;
199 if (G > n) G = n;
200
201 std::vector<double> tot(n, 0.0);
202 for (std::size_t c = 0; c < n; ++c)
203 for (std::size_t i = 0; i < Mc; ++i) tot[c] += D[i][c];
204 std::vector<std::vector<double>> dist(n, std::vector<double>(n, 0.0));
205 for (std::size_t a = 0; a < n; ++a) {
206 for (std::size_t b = a + 1; b < n; ++b) {
207 const double den = (tot[a] + tot[b]) / 2.0;
208 double d = 0.0;
209 if (den > 1e-14) {
210 double acc = 0.0;
211 for (std::size_t i = 0; i < Mc; ++i) acc += std::fabs(D[i][a] - D[i][b]);
212 d = acc / den;
213 }
214 dist[a][b] = d;
215 dist[b][a] = d;
216 }
217 }
218
219 std::vector<std::vector<std::size_t>> clusters(n);
220 for (std::size_t a = 0; a < n; ++a) clusters[a].push_back(a);
221 std::vector<bool> active(n, true);
222 std::size_t nactive = n;
223 while (nactive > G) {
224 double best = std::numeric_limits<double>::infinity();
225 long bp = -1, bq = -1;
226 for (std::size_t p = 0; p < n; ++p) {
227 if (!active[p]) continue;
228 for (std::size_t q = p + 1; q < n; ++q) {
229 if (!active[q]) continue;
230 double d = 0.0;
231 for (std::size_t x : clusters[p])
232 for (std::size_t y : clusters[q])
233 if (dist[x][y] > d) d = dist[x][y];
234 if (d < best - 1e-14) {
235 best = d;
236 bp = static_cast<long>(p);
237 bq = static_cast<long>(q);
238 }
239 }
240 }
241 if (bp < 0) break;
242 std::vector<std::size_t>& cp = clusters[static_cast<std::size_t>(bp)];
243 std::vector<std::size_t>& cq = clusters[static_cast<std::size_t>(bq)];
244 cp.insert(cp.end(), cq.begin(), cq.end());
245 std::sort(cp.begin(), cp.end());
246 cq.clear();
247 active[static_cast<std::size_t>(bq)] = false;
248 --nactive;
249 }
250
251 // Relabel by smallest member, so the group numbering is canonical
252 std::vector<std::size_t> live;
253 for (std::size_t p = 0; p < n; ++p)
254 if (active[p]) live.push_back(p);
255 std::sort(live.begin(), live.end(), [&](std::size_t a, std::size_t b) {
256 return clusters[a][0] < clusters[b][0];
257 });
258 for (std::size_t g = 0; g < live.size(); ++g)
259 for (std::size_t x : clusters[live[g]]) grp[x] = g;
260 return grp;
261}
262
263} // namespace bgchain_detail
264
265/** The solved background modulating chain. */
266template <class T>
268 /** space[s][i][b]: class-b jobs held by station i in state s. */
269 std::vector<std::vector<std::vector<int>>> space;
270 /** totocc[s][i]: closed jobs of any background class held by station i. */
271 std::vector<std::vector<int>> totocc;
272 std::vector<T> pi;
274 std::vector<std::vector<T>> QLen, Tput, Ubusy; ///< (Mc x B)
275 std::size_t nstates = 0;
276};
277
278/**
279 * Build and solve the background chain (mam_bgchain_ctmc.m).
280 *
281 * A station holding e closed jobs serves background class b at rate
282 * `n[i][b] / STb[i][b]` when it is an infinite server, and
283 * `cshare[i][e] * (n[i][b]/e) / STb[i][b]` otherwise, splitting the capacity the
284 * closed jobs hold over the background classes in proportion to their counts.
285 * That is exact under PS and is the random-order surrogate under FCFS.
286 *
287 * @param Nb population of each background class, size B in {1,2}
288 * @param STb (Mc x B) mean service time per station per background class
289 * @param Pb B row-stochastic (Mc x Mc) routing matrices
290 * @param isinf_i whether each station of the support is an infinite server
291 * @param nsrv servers of each station of the support
292 * @param cshare (Mc x (Nmax+1)) mean servers the e closed jobs hold
293 * @param supp supp[i][b]: station i is on the route of background class b.
294 * NOT an optimization -- a chain that never visits a station
295 * cannot hold jobs there, and enumerating the union of every
296 * chain's stations puts probability on unreachable configurations
297 * that also ABSORB, because the chain's routing matrix has a zero
298 * row at an unvisited station which row-normalizes to a self-loop.
299 * The generator turns reducible and population conservation
300 * silently fails
301 * @param states_max cap on the number of chain states
302 */
303template <class T>
304BgchainCtmc<T> mam_bgchain_ctmc(const std::vector<int>& Nb,
305 const std::vector<std::vector<T>>& STb,
306 const std::vector<Matrix<T>>& Pb,
307 const std::vector<bool>& isinf_i,
308 const std::vector<double>& nsrv,
309 const std::vector<std::vector<double>>& cshare,
310 const std::vector<std::vector<bool>>& supp,
311 std::size_t states_max) {
312 const T zero = num_traits<T>::from_int(0);
313 const std::size_t Mc = STb.size();
314 const std::size_t B = Nb.size();
315
316 // Each class is enumerated over ITS OWN stations only; see the supp param.
317 std::vector<std::vector<std::vector<int>>> sp(B); // expanded to Mc columns
318 std::vector<std::vector<std::vector<int>>> compb(B); // reduced to the class's stations
319 std::vector<std::vector<std::size_t>> idxb(B);
320 std::vector<std::size_t> nst(B);
321 std::size_t nstates = 1;
322 for (std::size_t b = 0; b < B; ++b) {
323 for (std::size_t i = 0; i < Mc; ++i)
324 if (supp.empty() || supp[i][b]) idxb[b].push_back(i);
325 if (idxb[b].empty()) {
326 if (Nb[b] > 0)
327 throw UnsupportedError("mam_bgchain_ctmc: background class " + std::to_string(b + 1) +
328 " holds " + std::to_string(Nb[b]) +
329 " jobs but visits no station");
330 idxb[b].push_back(0); // an empty class still needs one slot
331 }
332 compb[b] = bgchain_detail::closed_block(idxb[b].size(), Nb[b]);
333 sp[b].assign(compb[b].size(), std::vector<int>(Mc, 0));
334 for (std::size_t r = 0; r < compb[b].size(); ++r)
335 for (std::size_t t = 0; t < idxb[b].size(); ++t)
336 sp[b][r][idxb[b][t]] = compb[b][r][t];
337 nst[b] = compb[b].size();
338 nstates *= nst[b];
339 }
340 if (nstates > states_max)
341 throw UnsupportedError(
342 "mam_bgchain_ctmc: the background chain of this model has " + std::to_string(nstates) +
343 " states, above the limit of " + std::to_string(states_max) +
344 ". The chain enumerates the closed-class population vector over the " +
345 std::to_string(Mc) +
346 " stations the closed classes visit, so its size grows as nchoosek(N+Mc-1,Mc-1) per "
347 "class, and it carries " + std::to_string(B) +
348 " classes. Lower options.config.bgaggr to aggregate more of the closed chains into "
349 "fewer classes, raise options.config.bgstates_max to solve it anyway, or reduce the "
350 "closed populations.");
351
352 // Per-class transition targets: tgt[b][s][i*Mc+j] is the class-b state
353 // reached from s when one job moves from station i to station j, or -1.
354 std::vector<std::vector<std::vector<long>>> tgt(B);
355 for (std::size_t b = 0; b < B; ++b) {
356 const std::size_t mb = idxb[b].size();
357 std::map<std::vector<int>, long> index;
358 for (std::size_t s = 0; s < nst[b]; ++s) index[compb[b][s]] = static_cast<long>(s);
359 tgt[b].assign(nst[b], std::vector<long>(Mc * Mc, -1));
360 for (std::size_t s = 0; s < nst[b]; ++s) {
361 for (std::size_t ii = 0; ii < mb; ++ii) {
362 if (compb[b][s][ii] == 0) continue;
363 for (std::size_t jj = 0; jj < mb; ++jj) {
364 if (jj == ii) continue;
365 std::vector<int> cand = compb[b][s];
366 --cand[ii];
367 ++cand[jj];
368 const auto it = index.find(cand);
369 if (it != index.end())
370 tgt[b][s][idxb[b][ii] * Mc + idxb[b][jj]] = it->second;
371 }
372 }
373 }
374 }
375
376 // Joint space, class 0 outermost.
377 std::vector<std::size_t> strideb(B, 1);
378 for (std::size_t b = 0; b < B; ++b) {
379 std::size_t s = 1;
380 for (std::size_t b2 = b + 1; b2 < B; ++b2) s *= nst[b2];
381 strideb[b] = s;
382 }
383 BgchainCtmc<T> out;
384 out.nstates = nstates;
385 out.space.assign(nstates, std::vector<std::vector<int>>(Mc, std::vector<int>(B, 0)));
386 out.totocc.assign(nstates, std::vector<int>(Mc, 0));
387 std::vector<std::vector<std::size_t>> subidx(nstates, std::vector<std::size_t>(B, 0));
388 for (std::size_t s = 0; s < nstates; ++s) {
389 std::size_t rem = s;
390 for (std::size_t bb = B; bb-- > 0;) {
391 subidx[s][bb] = rem % nst[bb];
392 rem /= nst[bb];
393 }
394 for (std::size_t b = 0; b < B; ++b) {
395 const std::vector<int>& vec = sp[b][subidx[s][b]];
396 for (std::size_t i = 0; i < Mc; ++i) {
397 out.space[s][i][b] = vec[i];
398 out.totocc[s][i] += vec[i];
399 }
400 }
401 }
402
403 std::vector<std::vector<T>> mu(Mc, std::vector<T>(B, zero));
404 for (std::size_t b = 0; b < B; ++b)
405 for (std::size_t i = 0; i < Mc; ++i)
406 if (num_traits<T>::to_double(STb[i][b]) > 0.0)
407 mu[i][b] = T(num_traits<T>::from_int(1) / STb[i][b]);
408
409 Matrix<T> Q(nstates, nstates, zero);
410 std::vector<std::vector<std::vector<T>>> rate_full(
411 nstates, std::vector<std::vector<T>>(Mc, std::vector<T>(B, zero)));
412 std::vector<std::vector<T>> cap_busy(nstates, std::vector<T>(Mc, zero));
413 for (std::size_t s = 0; s < nstates; ++s) {
414 for (std::size_t i = 0; i < Mc; ++i) {
415 const int eclosed = out.totocc[s][i];
416 if (eclosed == 0) continue;
417 double held_d;
418 if (isinf_i[i]) {
419 held_d = static_cast<double>(eclosed);
420 } else {
421 const std::size_t ecap =
422 std::min<std::size_t>(static_cast<std::size_t>(eclosed), cshare[i].size() - 1);
423 held_d = cshare[i][ecap];
424 }
425 if (!(held_d > 0.0)) continue;
426 const T held = num_traits<T>::from_double(held_d);
427 cap_busy[s][i] = held;
428 for (std::size_t b = 0; b < B; ++b) {
429 if (out.space[s][i][b] == 0 || mu[i][b] == zero) continue;
430 const T frac = num_traits<T>::from_double(static_cast<double>(out.space[s][i][b]) /
431 static_cast<double>(eclosed));
432 const T r = T(held * frac * mu[i][b]);
433 rate_full[s][i][b] = r;
434 for (std::size_t j = 0; j < Mc; ++j) {
435 if (j == i) continue;
436 if (!(num_traits<T>::to_double(Pb[b](i, j)) > 0.0)) continue;
437 const long tsub = tgt[b][subidx[s][b]][i * Mc + j];
438 if (tsub < 0) continue;
439 const std::size_t sdest =
440 s + (static_cast<std::size_t>(tsub) - subidx[s][b]) * strideb[b];
441 Q(s, sdest) += T(r * Pb[b](i, j));
442 }
443 }
444 }
445 }
446
447 out.Q = mc::ctmc_makeinfgen(Q);
448 if (nstates == 1) {
449 out.pi.assign(1, num_traits<T>::from_int(1));
450 } else {
451 out.pi = mc::ctmc_solve(out.Q);
452 }
453 T tot = zero;
454 for (T& v : out.pi) {
455 if (num_traits<T>::to_double(v) < 0.0) v = zero;
456 tot += v;
457 }
458 if (tot != zero)
459 for (T& v : out.pi) v /= tot;
460
461 out.QLen.assign(Mc, std::vector<T>(B, zero));
462 out.Tput.assign(Mc, std::vector<T>(B, zero));
463 out.Ubusy.assign(Mc, std::vector<T>(B, zero));
464 for (std::size_t s = 0; s < nstates; ++s) {
465 const T p = out.pi[s];
466 if (p == zero) continue;
467 for (std::size_t i = 0; i < Mc; ++i)
468 for (std::size_t b = 0; b < B; ++b) {
469 out.QLen[i][b] += T(p * num_traits<T>::from_int(out.space[s][i][b]));
470 out.Tput[i][b] += T(p * rate_full[s][i][b]);
471 }
472 }
473 for (std::size_t i = 0; i < Mc; ++i) {
474 if (isinf_i[i]) {
475 out.Ubusy[i] = out.QLen[i];
476 continue;
477 }
478 for (std::size_t s = 0; s < nstates; ++s) {
479 const T p = out.pi[s];
480 const int occ = out.totocc[s][i];
481 if (p == zero || occ == 0) continue;
482 for (std::size_t b = 0; b < B; ++b) {
483 const T share = num_traits<T>::from_double(
484 static_cast<double>(out.space[s][i][b]) / static_cast<double>(occ));
485 out.Ubusy[i][b] +=
486 T(p * cap_busy[s][i] * share / num_traits<T>::from_double(nsrv[i]));
487 }
488 }
489 }
490 return out;
491}
492
493/** The environment one station sees: the background chain lumped onto its occupancy. */
494template <class T>
497 std::vector<T> phi;
498 std::vector<int> esup; ///< ascending
499};
500
501/**
502 * Lump the background chain onto the closed occupancy of station i
503 * (mam_bgchain_env.m).
504 *
505 * Station i does not observe the whole closed population vector, only how many
506 * closed jobs compete with the open ones for its server. The lumped generator is
507 * the stationary-weighted aggregation of Q over the level sets
508 * {s : totocc(s,i) = e}, exact when the partition is lumpable in the
509 * Kemeny-Snell sense and the standard exact-aggregation approximation otherwise.
510 * The diagonal is rebuilt from the off-diagonal row sums, so the result is a
511 * proper generator whatever the lumping error is. Environment states of zero
512 * stationary probability are unreachable and are dropped.
513 */
514template <class T>
516 const T zero = num_traits<T>::from_int(0);
517 const std::size_t n = bg.nstates;
518
519 std::vector<int> levels;
520 for (std::size_t s = 0; s < n; ++s) levels.push_back(bg.totocc[s][i]);
521 std::sort(levels.begin(), levels.end());
522 levels.erase(std::unique(levels.begin(), levels.end()), levels.end());
523
524 std::vector<T> wAll(levels.size(), zero);
525 for (std::size_t s = 0; s < n; ++s) {
526 const std::size_t pos = static_cast<std::size_t>(
527 std::lower_bound(levels.begin(), levels.end(), bg.totocc[s][i]) - levels.begin());
528 wAll[pos] += bg.pi[s];
529 }
530
532 std::vector<T> w;
533 for (std::size_t e = 0; e < levels.size(); ++e) {
534 if (num_traits<T>::to_double(wAll[e]) > 1e-14) {
535 env.esup.push_back(levels[e]);
536 w.push_back(wAll[e]);
537 }
538 }
539 if (env.esup.empty()) {
540 // degenerate chain: the station never holds a closed job
541 env.A = Matrix<T>(1, 1, zero);
542 env.phi.assign(1, num_traits<T>::from_int(1));
543 env.esup.assign(1, 0);
544 return env;
545 }
546
547 const std::size_t me = env.esup.size();
548 std::vector<long> lvl(n, -1);
549 for (std::size_t s = 0; s < n; ++s) {
550 const auto it = std::lower_bound(env.esup.begin(), env.esup.end(), bg.totocc[s][i]);
551 if (it != env.esup.end() && *it == bg.totocc[s][i])
552 lvl[s] = static_cast<long>(it - env.esup.begin());
553 }
554
555 env.A = Matrix<T>(me, me, zero);
556 if (me > 1) {
557 for (std::size_t s = 0; s < n; ++s) {
558 if (lvl[s] < 0 || bg.pi[s] == zero) continue;
559 for (std::size_t sp = 0; sp < n; ++sp) {
560 if (sp == s || lvl[sp] < 0 || lvl[sp] == lvl[s]) continue;
561 const T q = bg.Q(s, sp);
562 if (q == zero) continue;
563 env.A(static_cast<std::size_t>(lvl[s]), static_cast<std::size_t>(lvl[sp])) +=
564 T(bg.pi[s] * q);
565 }
566 }
567 for (std::size_t e = 0; e < me; ++e) {
568 T diag = zero;
569 for (std::size_t ep = 0; ep < me; ++ep) {
570 if (ep == e) continue;
571 env.A(e, ep) /= w[e];
572 diag += env.A(e, ep);
573 }
574 env.A(e, e) = T(-diag);
575 }
576 }
577 T wsum = zero;
578 for (const T& v : w) wsum += v;
579 env.phi.assign(me, zero);
580 for (std::size_t e = 0; e < me; ++e) env.phi[e] = T(w[e] / wsum);
581 return env;
582}
583
584/** What one modulated station QBD returns. */
585template <class T>
588 std::vector<T> penv;
589 std::vector<double> cshare; ///< E[min(e+k,c) e/(e+k) | e], indexed by esup
590 std::vector<int> esup;
591};
592
593/**
594 * Solve the open classes of one station as a modulated level-dependent QBD
595 * (mam_bgchain_station.m).
596 *
597 * Level = number of open jobs held by the station, phase = (arrival MAP phase,
598 * environment state, service phase). With k open and e closed jobs present the
599 * open aggregate completes at rate `min(k+e,c) k/(k+e)` times the phase-type
600 * completion rate of one busy server: the dependence on k makes the QBD
601 * level-dependent, the dependence on e makes it modulated.
602 *
603 * The environment is level-dependent too. A lumped transition that LOWERS the
604 * closed occupancy is a closed completion here, so it carries the closed share
605 * and level k rescales it by the ratio to the averaged share `gref` the chain
606 * was built at; a transition that RAISES the occupancy is an arrival from
607 * elsewhere and is left alone. Without this the closed jobs would drain at their
608 * mean-field rate however long the open queue is, and the positive correlation
609 * between the two occupancies would be lost.
610 *
611 * The level space is truncated at Kmax. An arrival at the top level is lost but
612 * still advances the arrival phase, so the arrival process keeps its exact
613 * marginal and autocorrelation and only the queue tail is cut.
614 */
615template <class T>
617 const std::vector<T>& alpha_s, const Matrix<T>& Tsvc,
618 const Matrix<T>& Ain, const std::vector<int>& esup_in,
619 double nservers, const std::vector<double>& gref_in,
620 std::size_t Kmax) {
621 const T zero = num_traits<T>::from_int(0);
622 const T one = num_traits<T>::from_int(1);
623 const std::size_t ma = Da0.rows();
624 const std::size_t me = esup_in.size();
625 const std::size_t ms = alpha_s.size();
626 if (Kmax < 1) Kmax = 1;
627
628 // esup arrives ascending from mam_bgchain_env; gref is indexed to match.
629 const std::vector<int>& esup = esup_in;
630 const std::vector<double>& gref = gref_in;
631
632 Matrix<T> tvec(ms, 1, zero);
633 for (std::size_t i = 0; i < ms; ++i) {
634 T s = zero;
635 for (std::size_t j = 0; j < ms; ++j) s += Tsvc(i, j);
636 tvec(i, 0) = T(-s);
637 }
638 Matrix<T> alphaRow(1, ms, zero);
639 for (std::size_t j = 0; j < ms; ++j) alphaRow(0, j) = alpha_s[j];
640
641 Matrix<T> Ime(me, me, zero), Ima(ma, ma, zero), Ims(ms, ms, zero);
642 for (std::size_t i = 0; i < me; ++i) Ime(i, i) = one;
643 for (std::size_t i = 0; i < ma; ++i) Ima(i, i) = one;
644 for (std::size_t i = 0; i < ms; ++i) Ims(i, i) = one;
645
646 // Split the environment into the closed departures from this station (which
647 // the open level throttles) and the arrivals to it (which it does not).
648 Matrix<T> Adown(me, me, zero), Aup(me, me, zero);
649 for (std::size_t e = 0; e < me; ++e)
650 for (std::size_t ep = 0; ep < me; ++ep) {
651 if (ep < e) Adown(e, ep) = Ain(e, ep);
652 else if (ep > e) Aup(e, ep) = Ain(e, ep);
653 }
654
655 auto env_at_level = [&](std::size_t k) {
656 Matrix<T> Ak(me, me, zero);
657 for (std::size_t e = 0; e < me; ++e) {
658 const double g = bgchain_detail::closed_share(static_cast<double>(esup[e]),
659 static_cast<double>(k), nservers);
660 const double ratio = (gref[e] > 0.0) ? g / gref[e] : 1.0;
661 const T rt = num_traits<T>::from_double(ratio);
662 T diag = zero;
663 for (std::size_t ep = 0; ep < me; ++ep) {
664 if (ep == e) continue;
665 const T v = T(Aup(e, ep) + rt * Adown(e, ep));
666 Ak(e, ep) = v;
667 diag += v;
668 }
669 Ak(e, e) = T(-diag);
670 }
671 return Ak;
672 };
673 auto diagm = [&](const std::vector<T>& v) {
674 Matrix<T> D(v.size(), v.size(), zero);
675 for (std::size_t i = 0; i < v.size(); ++i) D(i, i) = v[i];
676 return D;
677 };
678
679 // The C++ ldqbd indexes q0/q1/q2 BY LEVEL, with q2[0] unused, unlike the
680 // reference's 1-based cell arrays.
681 std::vector<Matrix<T>> Q0(Kmax), Q1(Kmax + 1), Q2(Kmax + 1);
682 std::vector<std::vector<T>> phiae(Kmax);
683
684 Q1[0] = qbd_detail::madd(kron(Da0, Ime), kron(Ima, env_at_level(0)));
685 Q0[0] = kron(kron(Da1, Ime), alphaRow);
686
687 const Matrix<T> Da0kron = kron(kron(Da0, Ime), Ims);
688 const Matrix<T> Da1kron = kron(kron(Da1, Ime), Ims);
689 Q2[0] = Matrix<T>(1, 1, zero); // unused
690 for (std::size_t k = 1; k <= Kmax; ++k) {
691 std::vector<T> rep(ma * me, zero);
692 for (std::size_t a = 0; a < ma; ++a)
693 for (std::size_t e = 0; e < me; ++e)
694 rep[a * me + e] = num_traits<T>::from_double(bgchain_detail::open_share(
695 static_cast<double>(k), static_cast<double>(esup[e]), nservers));
696 phiae[k - 1] = rep;
697 const Matrix<T> Ak = env_at_level(k);
698 Q1[k] = qbd_detail::madd(
699 qbd_detail::madd(Da0kron, kron(kron(Ima, Ak), Ims)), kron(diagm(rep), Tsvc));
700 if (k < Kmax) Q0[k] = Da1kron;
701 if (k == 1) {
702 Q2[1] = kron(diagm(rep), tvec);
703 } else {
704 Q2[k] = kron(diagm(rep), matmul(tvec, alphaRow));
705 }
706 }
707 // truncation: an arrival at the top level is lost, its phase transition is kept
708 Q1[Kmax] = qbd_detail::madd(Q1[Kmax], Da1kron);
709
710 const LdqbdResult<T> res = ldqbd(Q0, Q1, Q2);
711 const std::vector<T>& plev = res.pi.pi;
712
714 out.esup = esup;
715 out.QLen = zero;
716 for (std::size_t k = 0; k <= Kmax; ++k)
717 out.QLen += T(num_traits<T>::from_int(static_cast<int>(k)) * plev[k]);
718 out.ploss = plev[Kmax];
719
720 std::vector<T> penv(me, zero), gacc(me, zero);
721 T util = zero, tput = zero;
722 for (std::size_t k = 0; k <= Kmax; ++k) {
723 const std::vector<T>& pk = res.pi.pi_level[k];
724 std::vector<T> marg(me, zero);
725 if (k == 0) {
726 for (std::size_t a = 0; a < ma; ++a)
727 for (std::size_t e = 0; e < me; ++e) marg[e] += pk[a * me + e];
728 } else {
729 for (std::size_t a = 0; a < ma; ++a)
730 for (std::size_t e = 0; e < me; ++e) {
731 T block = zero, dep = zero;
732 for (std::size_t s = 0; s < ms; ++s) {
733 const T v = pk[(a * me + e) * ms + s];
734 block += v;
735 dep += T(v * tvec(s, 0));
736 }
737 marg[e] += block;
738 util += T(block * phiae[k - 1][a * me + e]);
739 tput += T(dep * phiae[k - 1][a * me + e]);
740 }
741 }
742 for (std::size_t e = 0; e < me; ++e) {
743 penv[e] += marg[e];
744 gacc[e] += T(marg[e] * num_traits<T>::from_double(bgchain_detail::closed_share(
745 static_cast<double>(esup[e]), static_cast<double>(k),
746 nservers)));
747 }
748 }
749 T psum = zero;
750 for (const T& v : penv) psum += v;
751 if (psum != zero)
752 for (std::size_t e = 0; e < me; ++e) {
753 penv[e] /= psum;
754 gacc[e] /= psum;
755 }
756 out.penv = penv;
757 out.cshare.assign(me, 0.0);
758 for (std::size_t e = 0; e < me; ++e) {
759 const double pe = num_traits<T>::to_double(penv[e]);
760 out.cshare[e] = (pe > 1e-14) ? num_traits<T>::to_double(gacc[e]) / pe : 0.0;
761 }
762 out.Util = T(util / num_traits<T>::from_double(nservers));
763 out.Tput = tput;
764 return out;
765}
766
767/**
768 * Port of `solver_mam_bgchain.m`.
769 *
770 * @param L the refreshed struct; must be mixed (at least one open and one
771 * closed chain)
772 * @param opt the MAM options; `cutoff` bounds the open level truncation,
773 * `bgstates_max` the background chain, `qbdphases_max` each station
774 */
775/**
776 * Number of states of the background-chain CTMC solver_mam_bgchain would build
777 * on this model, WITHOUT building it. Mirrors `mam_bgchain_states.m`.
778 *
779 * The size is what decides whether bgchain is affordable and mam_bgchain_ctmc
780 * only discovers it after the partition is fixed, so the default-method chooser
781 * needs it up front. The count follows the partition below: a pass carries the
782 * tagged closed chain as background class 0 and the demand-similar groups of the
783 * other closed chains as classes 1..G, each enumerating the compositions of its
784 * population over the stations its members visit. Merging two chains onto the
785 * UNION of their supports can raise the count as easily as lower it, so the
786 * passes are enumerated rather than bounded and the largest returned: that is
787 * the one mam_bgchain_ctmc would refuse. Returns 0 when bgchain does not apply
788 * to the model at all.
789 */
790template <class T>
792 const std::size_t M = L.nstations, C = L.nchains;
794
795 std::vector<std::size_t> closedChains;
796 for (std::size_t c = 0; c < C; ++c) {
797 bool open = false;
798 for (std::size_t k : L.inchain[c])
799 if (std::isinf(L.classes[k - 1].population)) open = true;
800 if (!open && dem.Nchain[c] > 0.0) closedChains.push_back(c);
801 }
802 const std::size_t R = closedChains.size();
803 if (R == 0) return 0.0;
804
805 std::vector<std::size_t> cst;
806 for (std::size_t i = 0; i < M; ++i) {
807 bool visited = false;
808 for (std::size_t c : closedChains)
809 if (num_traits<T>::to_double(dem.Vchain(i, c)) > 1e-14) visited = true;
810 if (visited) cst.push_back(i);
811 }
812 const std::size_t Mc = cst.size();
813 if (Mc == 0) return 0.0;
814
815 const std::size_t bgaggr_opt = (opt.bgaggr > 0) ? opt.bgaggr : 1;
816 const std::size_t naggr =
817 std::min(std::max<std::size_t>(bgaggr_opt, 1), std::max<std::size_t>(R - 1, 1));
818 const bool noAggr = (R == 1) || (naggr >= R - 1);
819 const std::size_t npass = noAggr ? 1 : R;
820
821 // nchoosek in floating point, so a chain far above any usable size still compares
822 auto binomial = [](double n, double k) {
823 if (k < 0.0 || k > n) return 0.0;
824 const double kk = std::min(k, n - k);
825 double acc = 1.0;
826 for (double i = 1.0; i <= kk; i += 1.0) acc = acc * (n - kk + i) / i;
827 return acc;
828 };
829
830 double worst = 0.0;
831 for (std::size_t pidx = 0; pidx < npass; ++pidx) {
832 std::vector<std::vector<std::size_t>> members;
833 if (noAggr) {
834 for (std::size_t c : closedChains) members.push_back({c});
835 } else {
836 std::vector<std::size_t> others;
837 for (std::size_t oi = 0; oi < R; ++oi)
838 if (oi != pidx) others.push_back(closedChains[oi]);
839 std::vector<std::vector<double>> D(Mc, std::vector<double>(others.size(), 0.0));
840 for (std::size_t ii = 0; ii < Mc; ++ii)
841 for (std::size_t oi = 0; oi < others.size(); ++oi)
842 D[ii][oi] = num_traits<T>::to_double(dem.Lchain(cst[ii], others[oi]));
843 const std::vector<std::size_t> grp = bgchain_detail::mam_bgchain_groups(D, naggr);
844 members.push_back({closedChains[pidx]});
845 for (std::size_t g = 0; g < naggr; ++g) {
846 std::vector<std::size_t> mem;
847 for (std::size_t oi = 0; oi < others.size(); ++oi)
848 if (grp[oi] == g) mem.push_back(others[oi]);
849 members.push_back(mem);
850 }
851 }
852
853 double n = 1.0;
854 for (const std::vector<std::size_t>& mem : members) {
855 double Nb = 0.0;
856 for (std::size_t o : mem) Nb += std::llround(dem.Nchain[o]);
857 std::size_t m = 0;
858 for (std::size_t ii = 0; ii < Mc; ++ii)
859 for (std::size_t o : mem)
860 if (num_traits<T>::to_double(dem.Vchain(cst[ii], o)) > 1e-14) {
861 ++m;
862 break;
863 }
864 if (m == 0) m = 1; // an empty class still needs one slot to be indexed by
865 n *= binomial(Nb + static_cast<double>(m) - 1.0, static_cast<double>(m) - 1.0);
866 if (!std::isfinite(n)) return std::numeric_limits<double>::infinity();
867 }
868 worst = std::max(worst, n);
869 }
870 return worst;
871}
872
873template <class T>
875 if constexpr (!num_traits<T>::has_transcendental) {
876 throw UnsupportedError(
877 "solver_mam_bgchain: the level-dependent QBD recursion inverts a matrix per level and "
878 "falls back to a pseudo-inverse when a level is singular, neither of which is exact "
879 "arithmetic; rerun with --arith double or --arith real");
880 } else {
882 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
883 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
884
885 // ---- class-level service times ---------------------------------------
886 Matrix<T> S(M, K, zero);
887 for (std::size_t i = 0; i < M; ++i)
888 for (std::size_t k = 0; k < K; ++k) {
889 const double r = num_traits<T>::to_double(L.rates(i, k));
890 if (std::isfinite(r) && r > 0.0) S(i, k) = T(one / L.rates(i, k));
891 }
892
894 const Matrix<T>& rtst = rtv.rtst;
895 const Matrix<T>& V = rtv.Vst;
897
898 std::vector<bool> isopenchain(C, false);
899 std::vector<std::size_t> openChains, closedChains;
900 for (std::size_t c = 0; c < C; ++c) {
901 bool open = false;
902 for (std::size_t k : L.inchain[c])
903 if (std::isinf(L.classes[k - 1].population)) open = true;
904 isopenchain[c] = open;
905 if (open) openChains.push_back(c);
906 else if (dem.Nchain[c] > 0.0) closedChains.push_back(c);
907 }
908 const std::size_t R = closedChains.size();
909 if (R == 0)
910 throw UnsupportedError(
911 "solver_mam_bgchain: the bgchain method requires at least one closed class: the "
912 "background chain IS the closed population vector, so a purely open model has nothing "
913 "to build it from. Use dec.source.");
914
915 // ---- the support of the background chain -----------------------------
916 std::vector<std::size_t> cst;
917 for (std::size_t i = 0; i < M; ++i) {
918 bool visited = false;
919 for (std::size_t c : closedChains)
920 if (num_traits<T>::to_double(dem.Vchain(i, c)) > 1e-14) visited = true;
921 if (visited) cst.push_back(i);
922 }
923 const std::size_t Mc = cst.size();
924 if (Mc == 0)
925 throw UnsupportedError("solver_mam_bgchain: the closed classes of this model visit no "
926 "station");
927
928 // ---- chain-level station routing, folding the class axis of rt --------
929 std::vector<Matrix<T>> Pchain(C, Matrix<T>(M, M, zero));
930 for (std::size_t c = 0; c < C; ++c) {
931 Matrix<T> P(M, M, zero);
932 for (std::size_t i = 0; i < M; ++i)
933 for (std::size_t k1 : L.inchain[c]) {
934 const T a = dem.alpha(i, k1 - 1);
935 if (!(num_traits<T>::to_double(a) > 0.0)) continue;
936 for (std::size_t j = 0; j < M; ++j) {
937 T acc = zero;
938 for (std::size_t k2 : L.inchain[c])
939 acc += rtst(i * K + (k1 - 1), j * K + (k2 - 1));
940 if (acc != zero) P(i, j) += T(a * acc);
941 }
942 }
943 Pchain[c] = P;
944 }
945
946 // ---- open arrival streams --------------------------------------------
947 std::vector<double> lambdaChain(C, 0.0);
948 std::map<std::size_t, Map<T>> chainArrival;
949 for (std::size_t c : openChains) {
950 const std::size_t isrc = L.classes[L.inchain[c][0] - 1].refstat; // 1-based
951 double lam = 0.0;
952 std::vector<Mmap<T>> parts;
953 for (std::size_t k : L.inchain[c]) {
954 const double rk = num_traits<T>::to_double(L.rates(isrc - 1, k - 1));
955 if (!std::isfinite(rk) || !(rk > 0.0)) continue;
956 lam += rk;
957 const Map<T> mk = lang::dist_to_map(L.service[isrc - 1][k - 1]);
958 Mmap<T> mm;
959 mm.D0 = mk.D0;
960 mm.D1 = mk.D1;
961 mm.Dc.assign(1, mk.D1);
962 parts.push_back(mm);
963 }
964 lambdaChain[c] = lam;
965 if (!parts.empty()) {
966 const Mmap<T> sup =
967 (parts.size() == 1) ? parts[0] : mmap_super_safe(parts, opt.space_max);
968 chainArrival[c] = Map<T>{sup.D0, sup.D1};
969 }
970 }
971
972 Matrix<T> lambdaOpen(M, K, zero);
973 std::vector<bool> isopenclass(K, false);
974 for (std::size_t c : openChains)
975 for (std::size_t k : L.inchain[c]) {
976 isopenclass[k - 1] = true;
977 for (std::size_t i = 0; i < M; ++i)
978 lambdaOpen(i, k - 1) = T(num_traits<T>::from_double(lambdaChain[c]) * V(i, k - 1));
979 }
980
981 // ---- fixed-point state -----------------------------------------------
983 sol.Q = Matrix<T>(M, K, zero);
984 sol.U = Matrix<T>(M, K, zero);
985 sol.R = Matrix<T>(M, K, zero);
986 sol.Tp = Matrix<T>(M, K, zero);
987 sol.C.assign(K, zero);
988 sol.X.assign(K, zero);
989
990 int Ntot = 0;
991 for (std::size_t c : closedChains) Ntot += static_cast<int>(std::llround(dem.Nchain[c]));
992
993 // cshare[i][e]: mean number of servers of station i that its e closed jobs
994 // hold once the open work has taken its share. Starts at min(e,c).
995 std::vector<std::vector<double>> cshare(M, std::vector<double>(Ntot + 1, 0.0));
996 std::vector<double> nsrvAll(M, 1.0);
997 std::vector<bool> isinfAll(M, false);
998 for (std::size_t i = 0; i < M; ++i) {
999 nsrvAll[i] = L.stations[i].nservers;
1000 isinfAll[i] = (L.stations[i].sched == SchedStrategy::INF);
1001 for (int e = 0; e <= Ntot; ++e)
1002 cshare[i][e] = std::min(static_cast<double>(e), nsrvAll[i]);
1003 }
1004 std::vector<double> Xclosed(C, 0.0);
1005 for (std::size_t c : closedChains) {
1006 double denom = 0.0;
1007 for (std::size_t i = 0; i < M; ++i) denom += num_traits<T>::to_double(dem.Lchain(i, c));
1008 if (denom > 0.0) Xclosed[c] = dem.Nchain[c] / denom;
1009 }
1010
1011 const std::size_t states_max = (opt.bgstates_max > 0) ? opt.bgstates_max : 20000;
1012 const std::size_t phases_max = (opt.qbdphases_max > 0) ? opt.qbdphases_max : 500;
1013
1014 // How many aggregate classes the background chain carries, and which chains
1015 // share each of them. opt.bgaggr is the number of AGGREGATE classes G: G = 1
1016 // is the classic tagged/aggregate pair, G >= R-1 aggregates nothing.
1017 const std::size_t bgaggr_opt = (opt.bgaggr > 0) ? opt.bgaggr : 1;
1018 const std::size_t naggr =
1019 std::min(std::max<std::size_t>(bgaggr_opt, 1), std::max<std::size_t>(R - 1, 1));
1020 // With nothing left to aggregate ONE background chain carries every closed
1021 // chain exactly, so the tagged loop would solve the same chain R times over.
1022 const bool noAggr = (R == 1) || (naggr >= R - 1);
1023
1024 // The grouping is a property of the demands, not of the iterate, so it is
1025 // fixed once here rather than recomputed inside the fixed point.
1026 std::vector<std::vector<std::size_t>> othersOf(R), grpOf(R);
1027 for (std::size_t ridx = 0; ridx < R; ++ridx) {
1028 for (std::size_t oi = 0; oi < R; ++oi)
1029 if (oi != ridx) othersOf[ridx].push_back(closedChains[oi]);
1030 if (!noAggr && !othersOf[ridx].empty()) {
1031 std::vector<std::vector<double>> D(Mc, std::vector<double>(othersOf[ridx].size(), 0.0));
1032 for (std::size_t ii = 0; ii < Mc; ++ii)
1033 for (std::size_t oi = 0; oi < othersOf[ridx].size(); ++oi)
1034 D[ii][oi] = num_traits<T>::to_double(dem.Lchain(cst[ii], othersOf[ridx][oi]));
1035 grpOf[ridx] = bgchain_detail::mam_bgchain_groups(D, naggr);
1036 }
1037 }
1038 const std::size_t npass = noAggr ? 1 : R;
1039
1040 Matrix<T> TNprev(M, K, num_traits<T>::from_double(1e300));
1041 int totiter = 0;
1042 const double relax = 0.5;
1043
1044 auto maxdiff = [&](const Matrix<T>& a, const Matrix<T>& b) {
1045 double m = 0.0;
1046 for (std::size_t i = 0; i < a.rows(); ++i)
1047 for (std::size_t j = 0; j < a.cols(); ++j)
1048 m = std::max(m, std::fabs(num_traits<T>::to_double(a(i, j)) -
1049 num_traits<T>::to_double(b(i, j))));
1050 return m;
1051 };
1052
1053 while (maxdiff(sol.Tp, TNprev) > opt.tol && totiter < opt.iter_max) {
1054 ++totiter;
1055 TNprev = sol.Tp;
1056
1057 std::vector<double> QopenAcc(M, 0.0);
1058 std::vector<std::vector<double>> cshareAcc(M, std::vector<double>(Ntot + 1, 0.0));
1059
1060 for (std::size_t pidx = 0; pidx < npass; ++pidx) {
1061 const std::size_t r = closedChains[pidx];
1062
1063 // Background classes: class 0 is the tagged chain, classes 1..G the
1064 // flow-equivalent aggregates of the demand-similar groups. With no
1065 // aggregation every closed chain is a class of its own, in chain order.
1066 std::vector<std::vector<std::size_t>> members;
1067 if (noAggr) {
1068 for (std::size_t c : closedChains) members.push_back({c});
1069 } else {
1070 members.push_back({r});
1071 for (std::size_t g = 0; g < naggr; ++g) {
1072 std::vector<std::size_t> mem;
1073 for (std::size_t oi = 0; oi < othersOf[pidx].size(); ++oi)
1074 if (grpOf[pidx][oi] == g) mem.push_back(othersOf[pidx][oi]);
1075 members.push_back(mem);
1076 }
1077 }
1078 const std::size_t B = members.size();
1079
1080 std::vector<int> Nb(B, 0);
1081 std::vector<std::vector<T>> STb(Mc, std::vector<T>(B, zero));
1082 std::vector<Matrix<T>> Pb;
1083 // A class can only hold jobs at the stations its members visit; see
1084 // mam_bgchain_ctmc on why the union makes the chain reducible.
1085 std::vector<std::vector<bool>> suppb(Mc, std::vector<bool>(B, false));
1086 for (std::size_t b = 0; b < B; ++b) {
1087 const std::vector<std::size_t>& mem = members[b];
1088 for (std::size_t o : mem) {
1089 Nb[b] += static_cast<int>(std::llround(dem.Nchain[o]));
1090 for (std::size_t ii = 0; ii < Mc; ++ii)
1091 if (num_traits<T>::to_double(dem.Vchain(cst[ii], o)) > 1e-14)
1092 suppb[ii][b] = true;
1093 }
1094 if (mem.size() == 1) {
1095 // a group of one is carried exactly: no mean to take
1096 Matrix<T> P0(Mc, Mc, zero);
1097 for (std::size_t ii = 0; ii < Mc; ++ii) {
1098 STb[ii][b] = dem.STchain(cst[ii], mem[0]);
1099 for (std::size_t jj = 0; jj < Mc; ++jj)
1100 P0(ii, jj) = Pchain[mem[0]](cst[ii], cst[jj]);
1101 }
1102 Pb.push_back(P0);
1103 } else if (mem.empty()) {
1104 Pb.push_back(Matrix<T>(Mc, Mc, zero));
1105 } else {
1106 std::vector<std::vector<double>> w(Mc, std::vector<double>(mem.size(), 0.0));
1107 for (std::size_t ii = 0; ii < Mc; ++ii) {
1108 double rowsum = 0.0;
1109 for (std::size_t oi = 0; oi < mem.size(); ++oi) {
1110 w[ii][oi] = Xclosed[mem[oi]] *
1111 num_traits<T>::to_double(dem.Vchain(cst[ii], mem[oi]));
1112 rowsum += w[ii][oi];
1113 }
1114 for (std::size_t oi = 0; oi < mem.size(); ++oi)
1115 w[ii][oi] = (rowsum > 0.0) ? w[ii][oi] / rowsum
1116 : 1.0 / static_cast<double>(mem.size());
1117 }
1118 Matrix<T> Pagg(Mc, Mc, zero);
1119 for (std::size_t ii = 0; ii < Mc; ++ii) {
1120 T st = zero;
1121 for (std::size_t oi = 0; oi < mem.size(); ++oi) {
1122 const T wv = num_traits<T>::from_double(w[ii][oi]);
1123 st += T(wv * dem.STchain(cst[ii], mem[oi]));
1124 for (std::size_t jj = 0; jj < Mc; ++jj)
1125 Pagg(ii, jj) += T(wv * Pchain[mem[oi]](cst[ii], cst[jj]));
1126 }
1127 STb[ii][b] = st;
1128 }
1129 Pb.push_back(Pagg);
1130 }
1131 }
1132 // Row-normalize, leaving an all-zero row as a self-loop so the chain
1133 // stays a proper Markov chain on its support.
1134 for (std::size_t b = 0; b < B; ++b) {
1135 for (std::size_t ii = 0; ii < Mc; ++ii) {
1136 T s = zero;
1137 for (std::size_t jj = 0; jj < Mc; ++jj) s += Pb[b](ii, jj);
1138 if (num_traits<T>::to_double(s) > 1e-14) {
1139 for (std::size_t jj = 0; jj < Mc; ++jj) Pb[b](ii, jj) /= s;
1140 } else {
1141 for (std::size_t jj = 0; jj < Mc; ++jj) Pb[b](ii, jj) = zero;
1142 Pb[b](ii, ii) = one;
1143 }
1144 }
1145 }
1146
1147 std::vector<bool> isinfC(Mc, false);
1148 std::vector<double> nsrvC(Mc, 1.0);
1149 std::vector<std::vector<double>> cshareC(Mc);
1150 for (std::size_t ii = 0; ii < Mc; ++ii) {
1151 isinfC[ii] = isinfAll[cst[ii]];
1152 nsrvC[ii] = nsrvAll[cst[ii]];
1153 cshareC[ii] = cshare[cst[ii]];
1154 }
1155
1156 const BgchainCtmc<T> bg =
1157 mam_bgchain_ctmc(Nb, STb, Pb, isinfC, nsrvC, cshareC, suppb, states_max);
1158
1159 // ---- closed-class metrics of every chain this pass carries EXACTLY:
1160 // the tagged one always, and every chain when nothing was aggregated.
1161 const std::size_t bmax = noAggr ? B : 1;
1162 for (std::size_t b = 0; b < bmax; ++b) {
1163 if (members[b].empty()) continue;
1164 const std::size_t rb = members[b][0];
1165 for (std::size_t k : L.inchain[rb])
1166 for (std::size_t i = 0; i < M; ++i) {
1167 sol.Q(i, k - 1) = zero;
1168 sol.U(i, k - 1) = zero;
1169 sol.R(i, k - 1) = zero;
1170 sol.Tp(i, k - 1) = zero;
1171 }
1172 for (std::size_t ii = 0; ii < Mc; ++ii) {
1173 const std::size_t i = cst[ii];
1174 for (std::size_t k : L.inchain[rb]) {
1175 const T a = dem.alpha(i, k - 1);
1176 if (!(num_traits<T>::to_double(a) > 0.0)) continue;
1177 // THROUGHPUT splits by VISIT share, OCCUPANCY by DEMAND
1178 // share. A chain queue divided by alpha alone gives every
1179 // class of a station the same response time, impossible
1180 // at a Delay where R must be the class service time; the
1181 // weight is alpha*ST/STchain, as sn_deaggregate_chain_
1182 // results applies it.
1183 const T stc = dem.STchain(i, rb);
1184 const T w = (num_traits<T>::to_double(stc) > 1e-14) ? T(a * S(i, k - 1) / stc) : a;
1185 const T q = T(bg.QLen[ii][b] * w);
1186 const T x = T(bg.Tput[ii][b] * a);
1187 sol.Q(i, k - 1) = q;
1188 sol.Tp(i, k - 1) = x;
1189 sol.U(i, k - 1) = isinfC[ii] ? q : T(bg.Ubusy[ii][b] * w);
1190 sol.R(i, k - 1) = (num_traits<T>::to_double(x) > 1e-14) ? T(q / x) : zero;
1191 }
1192 }
1193 const std::size_t iref = L.classes[L.inchain[rb][0] - 1].refstat; // 1-based
1194 T tputref = zero;
1195 for (std::size_t k : L.inchain[rb]) tputref += sol.Tp(iref - 1, k - 1);
1196 const double vref = num_traits<T>::to_double(dem.Vchain(iref - 1, rb));
1197 Xclosed[rb] = (vref > 1e-14) ? num_traits<T>::to_double(tputref) / vref
1198 : num_traits<T>::to_double(tputref);
1199 }
1200
1201 std::vector<double> Uclosed(M, 0.0);
1202 for (std::size_t ii = 0; ii < Mc; ++ii) {
1203 double u = 0.0;
1204 for (std::size_t b = 0; b < B; ++b) u += num_traits<T>::to_double(bg.Ubusy[ii][b]);
1205 Uclosed[cst[ii]] = u;
1206 }
1207
1208 // ---- one pass of the open side ---------------------------------
1209 for (std::size_t i = 0; i < M; ++i) {
1210 bool solved = false;
1211 const SchedStrategy sc = L.stations[i].sched;
1212 if (sc != SchedStrategy::EXT && sc != SchedStrategy::INF) {
1213 std::vector<std::size_t> kopen;
1214 for (std::size_t k = 0; k < K; ++k)
1215 if (num_traits<T>::to_double(lambdaOpen(i, k)) > 1e-14) kopen.push_back(k);
1216 if (!kopen.empty()) {
1217 // aggregate open arrival MAP at the station
1218 bool haveDa = false;
1219 Map<T> Da;
1220 for (std::size_t c : openChains) {
1221 if (!(lambdaChain[c] > 1e-14)) continue;
1222 const auto it = chainArrival.find(c);
1223 if (it == chainArrival.end()) continue;
1224 T vsum = zero;
1225 for (std::size_t k : L.inchain[c]) vsum += V(i, k - 1);
1226 const double rate_ic =
1227 lambdaChain[c] * num_traits<T>::to_double(vsum);
1228 if (!(rate_ic > 1e-14)) continue;
1229 const Map<T> scaled =
1230 map_scale(it->second, T(num_traits<T>::from_double(1.0 / rate_ic)));
1231 if (!haveDa) {
1232 Da = scaled;
1233 haveDa = true;
1234 } else {
1235 std::vector<Mmap<T>> two(2);
1236 two[0].D0 = Da.D0; two[0].D1 = Da.D1; two[0].Dc.assign(1, Da.D1);
1237 two[1].D0 = scaled.D0; two[1].D1 = scaled.D1;
1238 two[1].Dc.assign(1, scaled.D1);
1239 const Mmap<T> sup = mmap_super_safe(two, opt.space_max);
1240 Da = Map<T>{sup.D0, sup.D1};
1241 }
1242 }
1243 if (haveDa) {
1244 // arrival-weighted phase-type mixture of the open service laws
1245 T lamtot = zero, svcwork = zero;
1246 for (std::size_t k : kopen) {
1247 lamtot += lambdaOpen(i, k);
1248 svcwork += T(lambdaOpen(i, k) * S(i, k));
1249 }
1250 std::vector<std::vector<T>> pies;
1251 std::vector<Matrix<T>> subgens;
1252 std::size_t msTotal = 0;
1253 // PROCESSOR SHARING IS INSENSITIVE to the service law beyond its mean, so
1254 // carrying the phase-type representation at a PS station is not merely unnecessary,
1255 // it is WRONG. This QBD tracks ONE service phase for the whole station, which makes
1256 // the open queue length inherit the SCV-sensitivity of an M/PH/1 FCFS queue;
1257 // measured against SolverCTMC, a HyperExp of SCV 4 then read 21% high where the
1258 // exact answer is the exponential one to five digits. The exponential of the same
1259 // mean is exact here, and it shrinks the QBD's phase count as a side effect.
1260 const bool isPSstation = (sc == SchedStrategy::PS);
1261 for (std::size_t k : kopen) {
1262 Map<T> phk;
1263 if (isPSstation) {
1264 const T mu = T(one / S(i, k));
1265 phk.D0 = Matrix<T>(1, 1, T(-mu));
1266 phk.D1 = Matrix<T>(1, 1, mu);
1267 } else {
1268 phk = map_scale(lang::dist_to_map(L.service[i][k]), S(i, k));
1269 }
1270 std::vector<T> pik = map_pie(phk);
1271 const T w = T(lambdaOpen(i, k) / lamtot);
1272 for (T& v : pik) v = T(v * w);
1273 pies.push_back(pik);
1274 subgens.push_back(phk.D0);
1275 msTotal += phk.D0.rows();
1276 }
1277 std::vector<T> alphaS(msTotal, zero);
1278 Matrix<T> Tblk(msTotal, msTotal, zero);
1279 std::size_t off = 0;
1280 for (std::size_t idx = 0; idx < pies.size(); ++idx) {
1281 const std::size_t n = subgens[idx].rows();
1282 for (std::size_t a = 0; a < n; ++a) {
1283 alphaS[off + a] = pies[idx][a];
1284 for (std::size_t b = 0; b < n; ++b)
1285 Tblk(off + a, off + b) = subgens[idx](a, b);
1286 }
1287 off += n;
1288 }
1289
1290 Matrix<T> Aenv(1, 1, zero);
1291 std::vector<int> esup(1, 0);
1292 const auto pos = std::find(cst.begin(), cst.end(), i);
1293 if (pos != cst.end()) {
1295 bg, static_cast<std::size_t>(pos - cst.begin()));
1296 Aenv = env.A;
1297 esup = env.esup;
1298 }
1299
1300 const std::size_t nphases = Da.D0.rows() * esup.size() * msTotal;
1301 if (nphases > phases_max)
1302 throw UnsupportedError(
1303 "solver_mam_bgchain: the modulated QBD of station " +
1304 std::to_string(i + 1) + " needs " + std::to_string(nphases) +
1305 " phases (" + std::to_string(Da.D0.rows()) + " arrival x " +
1306 std::to_string(esup.size()) + " environment x " +
1307 std::to_string(msTotal) + " service), above the limit of " +
1308 std::to_string(phases_max) +
1309 ". The environment axis is the closed population held by the "
1310 "station, so it grows with the closed population. Raise "
1311 "options.config.qbdphases_max, or reduce the closed population "
1312 "or the order of the arrival and service processes.");
1313
1314 std::vector<double> gref(esup.size(), 0.0);
1315 for (std::size_t e = 0; e < esup.size(); ++e)
1316 gref[e] = cshare[i][std::min<std::size_t>(
1317 static_cast<std::size_t>(esup[e]),
1318 cshare[i].size() - 1)];
1319
1320 // truncation level: the explicit cutoff, else enough
1321 // levels for the geometric tail left by the closed
1322 // traffic to be negligible
1323 std::size_t Kmax;
1324 if (opt.cutoff > 0) {
1325 Kmax = std::max<std::size_t>(2, opt.cutoff);
1326 } else {
1327 const double free = std::max(1e-8, 1.0 - Uclosed[i]);
1328 const double lam = num_traits<T>::to_double(lamtot);
1329 const double Smix = num_traits<T>::to_double(svcwork) / lam;
1330 double rho = lam * Smix / (nsrvAll[i] * free);
1331 rho = std::min(std::max(rho, 1e-3), 1.0 - 1e-3);
1332 const long kv =
1333 static_cast<long>(std::ceil(std::log(1e-8) / std::log(rho)));
1334 Kmax = static_cast<std::size_t>(std::min<long>(
1335 std::max<long>(kv, 20), 200));
1336 }
1337
1338 const BgchainStation<T> st =
1339 mam_bgchain_station(Da.D0, Da.D1, alphaS, Tblk, Aenv, esup,
1340 nsrvAll[i], gref, Kmax);
1341 QopenAcc[i] += num_traits<T>::to_double(st.QLen);
1342 // The QBD only saw the environment states the chain
1343 // reaches; interpolate the rest so the next chain has
1344 // a share wherever it may go, clipped to what a closed
1345 // job can physically hold.
1346 for (int e = 0; e <= Ntot; ++e) {
1347 double g;
1348 const std::size_t n = st.esup.size();
1349 if (n == 1) {
1350 g = st.cshare[0];
1351 } else if (e <= st.esup[0]) {
1352 const double slope = (st.cshare[1] - st.cshare[0]) /
1353 (st.esup[1] - st.esup[0]);
1354 g = st.cshare[0] + slope * (e - st.esup[0]);
1355 } else if (e >= st.esup[n - 1]) {
1356 const double slope = (st.cshare[n - 1] - st.cshare[n - 2]) /
1357 (st.esup[n - 1] - st.esup[n - 2]);
1358 g = st.cshare[n - 1] + slope * (e - st.esup[n - 1]);
1359 } else {
1360 std::size_t lo = 0;
1361 while (lo + 1 < n && st.esup[lo + 1] < e) ++lo;
1362 const double w = static_cast<double>(e - st.esup[lo]) /
1363 static_cast<double>(st.esup[lo + 1] - st.esup[lo]);
1364 g = st.cshare[lo] + w * (st.cshare[lo + 1] - st.cshare[lo]);
1365 }
1366 cshareAcc[i][e] +=
1367 std::min(std::max(g, 0.0),
1368 std::min(static_cast<double>(e), nsrvAll[i]));
1369 }
1370 solved = true;
1371 }
1372 }
1373 }
1374 if (!solved) {
1375 // a station with no open queue keeps the share it had
1376 for (int e = 0; e <= Ntot; ++e) cshareAcc[i][e] += cshare[i][e];
1377 }
1378 }
1379 }
1380
1381 std::vector<double> Qopen(M, 0.0);
1382 for (std::size_t i = 0; i < M; ++i) {
1383 Qopen[i] = QopenAcc[i] / static_cast<double>(npass);
1384 for (int e = 0; e <= Ntot; ++e)
1385 cshare[i][e] = (1.0 - relax) * cshare[i][e] +
1386 relax * (cshareAcc[i][e] / static_cast<double>(npass));
1387 }
1388
1389 // ---- open-class metrics from the aggregate station results --------
1390 for (std::size_t i = 0; i < M; ++i) {
1391 std::vector<std::size_t> kopen;
1392 for (std::size_t k = 0; k < K; ++k)
1393 if (isopenclass[k] && num_traits<T>::to_double(lambdaOpen(i, k)) > 1e-14)
1394 kopen.push_back(k);
1395 const SchedStrategy sc = L.stations[i].sched;
1396 if (kopen.empty()) {
1397 for (std::size_t k = 0; k < K; ++k)
1398 if (isopenclass[k]) {
1399 sol.Tp(i, k) = lambdaOpen(i, k);
1400 sol.Q(i, k) = zero;
1401 sol.U(i, k) = zero;
1402 sol.R(i, k) = zero;
1403 }
1404 continue;
1405 }
1406 T lamtot = zero, work = zero;
1407 for (std::size_t k : kopen) {
1408 lamtot += lambdaOpen(i, k);
1409 work += T(lambdaOpen(i, k) * S(i, k));
1410 }
1411 const T Smix = T(work / lamtot);
1412 for (std::size_t k : kopen) {
1413 sol.Tp(i, k) = lambdaOpen(i, k);
1414 if (sc == SchedStrategy::EXT) {
1415 sol.Q(i, k) = zero;
1416 sol.U(i, k) = zero;
1417 sol.R(i, k) = zero;
1418 } else if (sc == SchedStrategy::INF) {
1419 sol.R(i, k) = S(i, k);
1420 sol.Q(i, k) = T(lambdaOpen(i, k) * S(i, k));
1421 sol.U(i, k) = sol.Q(i, k);
1422 } else {
1423 const T Rtot = T(num_traits<T>::from_double(Qopen[i]) / lamtot);
1424 T rk;
1425 if (sc == SchedStrategy::PS) {
1426 // processor sharing: residence scales with the demand
1427 rk = T(Rtot * S(i, k) / Smix);
1428 } else {
1429 // FCFS and its variants: the wait is class-blind, the
1430 // service time is not
1431 const T cand = T(Rtot - Smix + S(i, k));
1433 ? cand
1434 : S(i, k);
1435 }
1436 sol.R(i, k) = rk;
1437 sol.Q(i, k) = T(lambdaOpen(i, k) * rk);
1438 // Utilization Law: a c-server station holds TN*S/c
1439 sol.U(i, k) =
1440 T(lambdaOpen(i, k) * S(i, k) / num_traits<T>::from_double(nsrvAll[i]));
1441 }
1442 }
1443 }
1444 }
1445
1446 for (std::size_t c = 0; c < C; ++c)
1447 for (std::size_t k : L.inchain[c])
1448 sol.X[k - 1] = num_traits<T>::from_double(isopenchain[c] ? lambdaChain[c] : Xclosed[c]);
1449 for (std::size_t k = 0; k < K; ++k) {
1450 T acc = zero;
1451 for (std::size_t i = 0; i < M; ++i) acc += sol.R(i, k);
1452 sol.C[k] = acc;
1453 }
1454 sol.iter = totiter;
1455 return sol;
1456 }
1457}
1458
1459} // namespace mam
1460} // namespace line
1461
1462#endif // LINE_SOLVERS_MAM_SOLVER_MAM_BGCHAIN_H
std::size_t cols() const
Definition matrix.h:90
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< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< JobClass > classes
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
Steady-state distribution of a continuous-time Markov chain.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Level-dependent QBD processes with finitely many levels: the rate matrices R^(n) by the backward matr...
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
The option and result types SolverMAM shares with its analyzers.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
MAP constructors and structural transformations.
Dense matrix and non-owning view.
The MMAP assembly primitives solver_mam_basic.m builds its per-station arrival stream from: mmap_expo...
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
The option and result types every MVA analyzer shares.
SnRtStations< T > sn_rt_stations(const qn::NetworkStruct< T > &sn)
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
double bgchain_states(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_bgchain.m.
mva::MvaSolution< T > solver_mam_bgchain(const qn::NetworkStruct< T > &L, const MamOptions &opt)
BgchainStation< T > mam_bgchain_station(const Matrix< T > &Da0, const Matrix< T > &Da1, const std::vector< T > &alpha_s, const Matrix< T > &Tsvc, const Matrix< T > &Ain, const std::vector< int > &esup_in, double nservers, const std::vector< double > &gref_in, std::size_t Kmax)
Solve the open classes of one station as a modulated level-dependent QBD (mam_bgchain_station....
BgchainEnv< T > mam_bgchain_env(const BgchainCtmc< T > &bg, std::size_t i)
Lump the background chain onto the closed occupancy of station i (mam_bgchain_env....
Matrix< T > kron(const Matrix< T > &A, const Matrix< T > &B)
Kronecker product.
Definition mmap_lambda.h:57
BgchainCtmc< T > mam_bgchain_ctmc(const std::vector< int > &Nb, const std::vector< std::vector< T > > &STb, const std::vector< Matrix< T > > &Pb, const std::vector< bool > &isinf_i, const std::vector< double > &nsrv, const std::vector< std::vector< double > > &cshare, const std::vector< std::vector< bool > > &supp, std::size_t states_max)
Build and solve the background chain (mam_bgchain_ctmc.m).
LdqbdResult< T > ldqbd(const std::vector< Matrix< T > > &q0, const std::vector< Matrix< T > > &q1, const std::vector< Matrix< T > > &q2)
Solve a level-dependent QBD: rate matrices and stationary law (ldqbd.m).
Definition ldqbd.h:246
Map< T > map_scale(const Map< T > &in, const T &new_mean)
Rescale time so that the mean inter-arrival time becomes new_mean.
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
Mmap< T > mmap_super_safe(const std::vector< Mmap< T > > &in, std::size_t maxorder)
Order-bounded superposition of several MMAPs (mmap_super_safe.m).
Matrix< T > ctmc_makeinfgen(const Matrix< T > &Q)
Set the diagonal so that every row sums to zero (ctmc_makeinfgen).
Definition ctmc_solve.h:58
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
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::vector< T > > space_closed_single(std::size_t m, std::size_t n)
Port of State.spaceClosedSingle: the ways to place n jobs over m phases.
Definition state.h:437
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
A queueing network and its refreshed NetworkStruct.
Chain aggregation and de-aggregation.
Port of matlab/src/api/sn/sn_rt_stations.m.
Port of the MATLAB +State package: the encoding that turns a station's state row into marginal job co...
What sn_rt_stations returns: the complemented routing and the station visits.
Matrix< T > rtst
(nstations*nclasses) square, STATION-major rows
Matrix< T > Vst
(nstations x nclasses), cellsum(sn.visits) by station
The solved background modulating chain.
std::vector< std::vector< int > > totocc
totocc[s][i]: closed jobs of any background class held by station i.
std::vector< std::vector< std::vector< int > > > space
space[s][i][b]: class-b jobs held by station i in state s.
std::vector< std::vector< T > > QLen
std::vector< std::vector< T > > Ubusy
(Mc x B)
std::vector< std::vector< T > > Tput
The environment one station sees: the background chain lumped onto its occupancy.
std::vector< int > esup
ascending
What one modulated station QBD returns.
std::vector< double > cshare
E[min(e+k,c) e/(e+k) | e], indexed by esup.
R and the stationary distribution together (ldqbd.m).
Definition ldqbd.h:239
LdqbdPi< T > pi
Definition ldqbd.h:241
The options SolverMAM reads.
Definition mam_types.h:29
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
An MMAP: the underlying MAP plus the per-class arrival matrices.
Definition mmap_lambda.h:45
Matrix< T > D0
Definition mmap_lambda.h:46
Matrix< T > D1
Definition mmap_lambda.h:47
std::vector< Matrix< T > > Dc
per-class matrices, sum_c Dc = D1
Definition mmap_lambda.h:48
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
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
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
std::vector< T > C
Definition mva_types.h:98