LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_prob.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_PROB_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_PROB_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `@@SolverMAM/getProb.m` and `@@SolverMAM/getProbMarg.m`: the joint
12 * (level, phase) and marginal queue-length distributions at the model's single
13 * queue, plus `@@SolverMAM/getMAMResult.m`, which exposes the matrix-analytic
14 * internals themselves.
15 *
16 * THE SINGLE-QUEUE RESTRICTION IS THE REFERENCE'S OWN, and it is stated there
17 * in prose: "the MAM solver uses QBD analysis, which is fundamentally a
18 * single-queue method". A model with two or more Queue nodes is refused by
19 * both, naming SolverCTMC and SolverSSA as the alternatives. The port carries
20 * the same gate and the same advice.
21 *
22 * WHAT `getProb` ACTUALLY RETURNS, and why the doc comment matters more than
23 * usual here. The reference computes the queue-length MARGINAL and multiplies
24 * it by an arrival-weighted average of the service-phase initial vectors:
25 *
26 * P(level, phase) ~= P(level) * avgPie(phase)
27 *
28 * That is a PRODUCT-FORM APPROXIMATION of the joint law, not the joint law of
29 * the QBD, and the reference says so at its line 155 ("For now, approximate by
30 * assuming phases are independent of level"). Reproduced exactly, including the
31 * fallback to a uniform phase distribution when the weighted average degenerates.
32 * The port does not silently improve it: a caller comparing against a CTMC
33 * joint distribution needs to know which object this is.
34 *
35 * THE CLOSED BRANCH is likewise an approximation the reference is explicit
36 * about: the arrival process is a single-phase MMAP built from the CONVERGED
37 * PER-CLASS THROUGHPUTS, i.e. a Poisson surrogate at the fixed point, and
38 * `getProbMarg` then reads class r's marginal off the aggregate distribution
39 * truncated at N(r). Both are reproduced; neither is repaired.
40 */
41
42#include <algorithm>
43#include <cmath>
44#include <cstddef>
45#include <string>
46#include <vector>
47
57#include "line/util/error.h"
58#include "line/util/matrix.h"
59
60namespace line {
61namespace mam {
62
63namespace prob_detail {
64
65/** The reference's queue-station count, and the gate both accessors share. */
66template <class T>
67void require_single_queue(const qn::NetworkStruct<T>& L, const char* fn) {
68 std::size_t nq = 0;
69 for (std::size_t i = 0; i < L.nstations; ++i)
70 if (L.stations[i].nodetype == qn::NodeType::Queue) ++nq;
71 if (nq > 1)
72 throw UnsupportedError(
73 std::string(fn) +
74 " is not supported for networks with multiple queues in SolverMAM. The MAM solver "
75 "uses QBD (quasi-birth-death) analysis, which is fundamentally a single-queue "
76 "method. Use SolverCTMC or SolverSSA for networks with multiple queues");
77 if (nq == 0)
78 throw UnsupportedError(std::string(fn) +
79 ": the model does not contain any queue stations");
80}
81
82/**
83 * The per-class service phase-type pairs at station `ist`, scaled by 1/(rate c)
84 * exactly as both accessors do.
85 */
86template <class T>
87std::vector<PhService<T>> station_services(const qn::NetworkStruct<T>& L, std::size_t ist) {
88 using lang::GlobalConstants;
89 const T one = num_traits<T>::from_int(1);
90 const std::size_t K = L.nclasses;
91 const double ns = L.stations[ist - 1].nservers;
92 std::vector<PhService<T>> svc(K);
93 for (std::size_t r = 0; r < K; ++r) {
94 if (L.disabled[ist - 1][r] || !(L.rates(ist - 1, r) > num_traits<T>::from_int(0))) {
95 // The reference's isnan(D0) guard: an unserved class becomes Immediate.
96 const T imm = num_traits<T>::from_double(GlobalConstants::Immediate);
97 svc[r].sigma.assign(1, one);
98 svc[r].S = Matrix<T>(1, 1, T(-imm));
99 continue;
100 }
101 const T target =
102 std::isfinite(ns)
103 ? T(T(one / L.rates(ist - 1, r)) / num_traits<T>::from_double(ns))
104 : T(one / L.rates(ist - 1, r));
105 const Map<T> sc = map_scale(lang::dist_to_map(L.service[ist - 1][r]), target);
106 svc[r].sigma = map_pie(sc);
107 svc[r].S = sc.D0;
108 }
109 return svc;
110}
111
112/** The aggregate queue-length marginal, normalized, as both accessors take it. */
113template <class T>
114std::vector<T> level_marginal(const qn::NetworkStruct<T>& L, const MamOptions& opt,
115 std::size_t ist, const mva::AvgResult<T>& avg,
116 std::vector<PhService<T>>& svc, std::vector<T>& classLambda,
117 bool& closed) {
118 using lang::GlobalConstants;
119 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
120 const std::size_t K = L.nclasses;
121 svc = station_services(L, ist);
122 classLambda.assign(K, zero);
123
124 closed = true;
125 for (const qn::JobClass& c : L.classes)
126 if (std::isinf(c.population)) closed = false;
127
128 Mmap<T> arr;
129 std::size_t maxLevel;
130 if (closed) {
131 // Poisson surrogate at the converged per-class throughputs.
132 std::size_t Ntot = 0;
133 for (const qn::JobClass& c : L.classes)
134 Ntot += static_cast<std::size_t>(std::llround(c.population));
135 maxLevel = Ntot + 1;
136 T tot = zero;
137 for (std::size_t r = 0; r < K; ++r) {
138 classLambda[r] = avg.TN(ist - 1, r);
139 tot += classLambda[r];
140 }
141 if (!(num_traits<T>::to_double(tot) >= GlobalConstants::FineTol))
142 return std::vector<T>(); // no traffic: the caller emits the point mass at 0
143 arr.D0 = Matrix<T>(1, 1, T(-tot));
144 arr.D1 = Matrix<T>(1, 1, tot);
145 for (std::size_t r = 0; r < K; ++r)
146 arr.Dc.push_back(Matrix<T>(1, 1, classLambda[r]));
147 } else {
148 const std::size_t src = L.sourceIdx;
149 if (src == 0) throw InputError("getProb: the open model has no Source");
150 maxLevel = opt.cutoff > 0 ? opt.cutoff : static_cast<std::size_t>(100);
151 std::vector<Map<T>> arrMaps(K);
152 T tot = zero;
153 for (std::size_t r = 0; r < K; ++r) {
154 if (L.disabled[src - 1][r]) {
155 arrMaps[r] = Map<T>{Matrix<T>(1, 1, zero), Matrix<T>(1, 1, zero)};
156 continue;
157 }
158 arrMaps[r] = lang::dist_to_map(L.service[src - 1][r]);
159 classLambda[r] = map_lambda(arrMaps[r]);
160 tot += classLambda[r];
161 }
162 if (!(num_traits<T>::to_double(tot) >= GlobalConstants::FineTol))
163 return std::vector<T>();
164 if (K == 1) {
165 arr.D0 = arrMaps[0].D0;
166 arr.D1 = arrMaps[0].D1;
167 arr.Dc.assign(1, arrMaps[0].D1);
168 } else {
169 // The reference superposes the per-class MAPs and then splits the
170 // aggregate D1 by arrival-rate share. NOTE: MATLAB writes
171 // `map_super({superMAP, arrMaps{k}})`, passing ONE cell argument to
172 // a two-argument function, so this branch RAISES there; see the
173 // divergence note in _kb. The port does what the code plainly
174 // intends, which is the two-argument superposition.
175 Map<T> super = arrMaps[0];
176 for (std::size_t r = 1; r < K; ++r) {
177 Map<T> m;
178 m.D0 = krons(super.D0, arrMaps[r].D0);
179 m.D1 = krons(super.D1, arrMaps[r].D1);
180 super = map_normalize(m);
181 }
182 arr.D0 = super.D0;
183 arr.D1 = super.D1;
184 for (std::size_t r = 0; r < K; ++r) {
185 Matrix<T> Dr = super.D1;
186 const T w = T(classLambda[r] / tot);
187 for (std::size_t i = 0; i < Dr.rows(); ++i)
188 for (std::size_t j = 0; j < Dr.cols(); ++j) Dr(i, j) *= w;
189 arr.Dc.push_back(Dr);
190 }
191 }
192 }
193
194 // Both accessors capture ONE output of MMAPPH1FCFS('ncDistr'), i.e. class
195 // 1's marginal, and renormalize it. Reproduced.
196 const std::vector<std::vector<T>> d = mmapph1fcfs_ncdistr(arr, svc, maxLevel);
197 std::vector<T> p(d[0].size(), zero);
198 T mass = zero;
199 for (std::size_t n = 0; n < d[0].size(); ++n) {
200 p[n] = num_abs(d[0][n]);
201 mass += p[n];
202 }
203 if (mass > zero)
204 for (T& v : p) v /= mass;
205 (void)one;
206 return p;
207}
208
209} // namespace prob_detail
210
211/** The joint (level, phase) table `getProb` returns: rows levels, cols phases. */
212template <class T>
213struct ProbTable {
215};
216
217/**
218 * Port of `@@SolverMAM/getProb.m`.
219 *
220 * @param node 1-based NODE index; must be a station
221 * @param avg the converged `getAvg` result, which the closed branch reads
222 * throughputs from
223 * @param L the refreshed struct
224 * @param opt SolverMAM's options
225 */
226template <class T>
228 std::size_t node, const mva::AvgResult<T>& avg) {
229 if constexpr (!num_traits<T>::has_transcendental) {
230 throw UnsupportedError(
231 "getProb: the queue-length distribution comes from the MMAP[K]/PH[K]/1 age process, "
232 "whose first-return matrix is a tolerance-terminated Riccati doubling; rerun with "
233 "--arith double or --arith real");
234 } else {
235 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
236 if (node == 0 || node > L.nof_nodes())
237 throw InputError("getProb: node number exceeds the number of nodes in the model");
238 const std::size_t ist = L.nodes[node - 1].station;
239 if (ist == 0) throw InputError("getProb: the specified node is not a station");
240 prob_detail::require_single_queue(L, "getProb");
241
242 const std::size_t K = L.nclasses;
243 std::vector<PhService<T>> svc;
244 std::vector<T> classLambda;
245 bool closed = false;
246 const std::vector<T> p =
247 prob_detail::level_marginal(L, opt, ist, avg, svc, classLambda, closed);
248
249 ProbTable<T> out;
250 if (p.empty()) {
251 // No traffic at this station: all the probability sits at (0, 1).
252 std::size_t levels = 1;
253 if (closed) {
254 std::size_t Ntot = 0;
255 for (const qn::JobClass& c : L.classes)
256 Ntot += static_cast<std::size_t>(std::llround(c.population));
257 levels = Ntot + 1;
258 } else {
259 levels = opt.cutoff > 0 ? opt.cutoff : static_cast<std::size_t>(100);
260 }
261 out.P = Matrix<T>(levels, 1, zero);
262 out.P(0, 0) = one;
263 return out;
264 }
265
266 std::size_t nPhases = 1;
267 for (std::size_t r = 0; r < K; ++r) nPhases = std::max(nPhases, svc[r].S.rows());
268
269 // The arrival-weighted average of the TIME-STATIONARY phase distributions,
270 // map_prob, not the initial vectors sigma. sigma is the embedded
271 // equilibrium at departure instants -- the phase a service STARTS in -- so
272 // for an Erlang-2 it is [1 0] and the joint gave P(phase 2) = 0 at every
273 // level for a server that spends half its busy time in phase 2. A PH law
274 // (sigma, S) is the MAP {S, (-S e) sigma}, so the stationary vector needs no
275 // extra state: PhService stays as MMAPPH1FCFS requires it.
276 T tot = zero;
277 for (std::size_t r = 0; r < K; ++r) tot += classLambda[r];
278 std::vector<T> avgPie(nPhases, zero);
279 if (tot > zero)
280 for (std::size_t r = 0; r < K; ++r) {
281 const std::size_t m = svc[r].S.rows();
282 Map<T> ph;
283 ph.D0 = svc[r].S;
284 ph.D1 = Matrix<T>(m, m, zero);
285 for (std::size_t i = 0; i < m; ++i) {
286 T exit = zero;
287 for (std::size_t j = 0; j < m; ++j) exit -= svc[r].S(i, j);
288 for (std::size_t j = 0; j < m && j < svc[r].sigma.size(); ++j)
289 ph.D1(i, j) = T(exit * svc[r].sigma[j]);
290 }
291 const std::vector<T> piq = map_prob(ph);
292 for (std::size_t i = 0; i < piq.size() && i < nPhases; ++i)
293 avgPie[i] += T(piq[i] * classLambda[r] / tot);
294 }
295 T s = zero;
296 for (const T& v : avgPie) s += v;
297 if (!(num_traits<T>::to_double(s) > 0.0) || !std::isfinite(num_traits<T>::to_double(s))) {
298 // The reference's fallback: a uniform phase distribution.
299 for (T& v : avgPie) v = T(one / num_traits<T>::from_int(static_cast<int>(nPhases)));
300 } else {
301 for (T& v : avgPie) v /= s;
302 }
303
304 out.P = Matrix<T>(p.size(), nPhases, zero);
305 for (std::size_t n = 0; n < p.size(); ++n)
306 for (std::size_t i = 0; i < nPhases; ++i) out.P(n, i) = T(p[n] * avgPie[i]);
307 return out;
308 } // if constexpr has_transcendental
309}
310
311/**
312 * Port of `@@SolverMAM/getProbMarg.m`: P(n jobs of class `jobclass`) at station
313 * `ist`, for n = 0..N(jobclass) closed, or over the whole cutoff range open.
314 */
315template <class T>
317 std::size_t ist, std::size_t jobclass,
318 const mva::AvgResult<T>& avg) {
319 if constexpr (!num_traits<T>::has_transcendental) {
320 throw UnsupportedError(
321 "getProbMarg: the queue-length distribution comes from the MMAP[K]/PH[K]/1 age "
322 "process, whose first-return matrix is a tolerance-terminated Riccati doubling; "
323 "rerun with --arith double or --arith real");
324 } else {
325 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
326 if (ist == 0 || ist > L.nstations)
327 throw InputError("getProbMarg: station number exceeds the number of stations");
328 if (jobclass == 0 || jobclass > L.nclasses)
329 throw InputError("getProbMarg: job class index exceeds the number of classes");
330 prob_detail::require_single_queue(L, "getProbMarg");
331
332 std::vector<PhService<T>> svc;
333 std::vector<T> classLambda;
334 bool closed = false;
335 const std::vector<T> p =
336 prob_detail::level_marginal(L, opt, ist, avg, svc, classLambda, closed);
337
338 if (p.empty()) {
339 const std::size_t levels =
340 closed ? static_cast<std::size_t>(std::llround(L.classes[jobclass - 1].population)) + 1
341 : static_cast<std::size_t>(100);
342 std::vector<T> out(levels, zero);
343 out[0] = one;
344 return out;
345 }
346 if (!closed) return p;
347
348 const double Nr = L.classes[jobclass - 1].population;
349 if (!(Nr > 0.0))
350 throw UnsupportedError(
351 "getProbMarg: class " + std::to_string(jobclass) +
352 " has zero population, so it has no marginal queue-length distribution");
353 const std::size_t Nk = static_cast<std::size_t>(std::llround(Nr));
354 std::vector<T> out(Nk + 1, zero);
355 T mass = zero;
356 for (std::size_t n = 0; n <= Nk && n < p.size(); ++n) {
357 out[n] = p[n];
358 mass += out[n];
359 }
360 if (mass > zero)
361 for (T& v : out) v /= mass;
362 return out;
363 } // if constexpr has_transcendental
364}
365
366/**
367 * Port of `@@SolverMAM/getMAMResult.m`: the matrix-analytic internals of a
368 * single-queue model, for a BMAP (or MAP) arrival stream into an exponential
369 * single server.
370 *
371 * The reference's other arm handles a retrial station through
372 * `qsys_bmapphnn_retrial`. That routine IS ported, but the C++ `NetworkStruct`
373 * has no retrial fields at all, so no model this port can build reaches it; the
374 * arm is recorded rather than written.
375 */
376template <class T>
378 if constexpr (!num_traits<T>::has_transcendental) {
379 throw UnsupportedError(
380 "getMAMResult: qsys_bmapm1 runs a functional iteration for G and an adaptive level "
381 "truncation, both tolerance-terminated; rerun with --arith double or --arith real");
382 } else {
383 std::size_t src = 0, q = 0;
384 for (std::size_t i = 1; i <= L.nstations; ++i) {
385 const qn::NodeType nt = L.nodes[L.node_of_station(i) - 1].nodetype;
386 if (nt == qn::NodeType::Source) src = i;
387 else if (nt == qn::NodeType::Queue) {
388 if (q != 0)
389 throw UnsupportedError(
390 "getMAMResult exposes the matrix-analytic internals of a single-queue model "
391 "only");
392 q = i;
393 }
394 }
395 if (src == 0 || q == 0)
396 throw UnsupportedError(
397 "getMAMResult requires an open model with one Source and one Queue");
398 if (L.nclasses > 1)
399 throw UnsupportedError(
400 "getMAMResult exposes the matrix-analytic internals of a single-class model only");
401 if (L.stations[q - 1].nservers != 1.0)
402 throw UnsupportedError("getMAMResult requires a single-server queue");
403
404 const Map<T> arv = lang::dist_to_map(L.service[src - 1][0]);
405 const Map<T> svc = lang::dist_to_map(L.service[q - 1][0]);
406 if (svc.D0.rows() != 1)
407 throw UnsupportedError(
408 "getMAMResult exposes the M/G/1-type internals for exponential service only; the "
409 "queue has a multi-phase service process");
410 const T mu = T(-svc.D0(0, 0));
411
412 // A MAP is the batch-size-one BMAP; a genuine BMAP would carry Dmark.
413 std::vector<Matrix<T>> D;
414 D.push_back(arv.D0);
415 const std::vector<Matrix<T>>& batches = L.service[src - 1][0].Dmark;
416 if (batches.empty()) {
417 D.push_back(arv.D1);
418 } else {
419 for (const Matrix<T>& Dk : batches) D.push_back(Dk);
420 }
421 return qsys::qsys_bmapm1(D, mu);
422 } // if constexpr has_transcendental
423}
424
425} // namespace mam
426} // namespace line
427
428#endif // LINE_SOLVERS_MAM_SOLVER_MAM_PROB_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t nof_nodes() const
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
std::size_t node_of_station(std::size_t st) const
1-based node index of a station, and the reverse; 0 when absent.
std::vector< NodeDef > nodes
every node, in creation order
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
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...
The MMAP[K]/PH[K]/1 FCFS queue: per-class mean number in system and per-class queue-length distributi...
mam::Map< T > dist_to_map(const Distrib< T > &d)
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
qsys::BmapM1Result< T > solver_mam_getmamresult(const qn::NetworkStruct< T > &L)
Port of @@SolverMAM/getMAMResult.m: the matrix-analytic internals of a single-queue model,...
Matrix< T > krons(const Matrix< T > &A, const Matrix< T > &B)
Kronecker sum, MATLAB's krons: kron(A, I_nb) + kron(I_na, B).
Definition mmap_lambda.h:71
std::vector< T > map_prob(const Map< T > &m)
Stationary distribution of the phase process, pi (D0 + D1) = 0.
Definition map_moment.h:73
std::vector< std::vector< T > > mmapph1fcfs_ncdistr(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, std::size_t levels)
Per-class queue-length distribution, BUTools' 'ncDistr', n: P(N_k = 0..n-1).
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
Map< T > map_normalize(const Map< T > &in)
Clamp negative off-diagonal entries of D0 and negative entries of D1 to zero, then rebuild the diagon...
ProbTable< T > solver_mam_getprob(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::size_t node, const mva::AvgResult< T > &avg)
Port of @@SolverMAM/getProb.m.
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
std::vector< T > solver_mam_getprobmarg(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::size_t ist, std::size_t jobclass, const mva::AvgResult< T > &avg)
Port of @@SolverMAM/getProbMarg.m: P(n jobs of class jobclass) at station ist, for n = 0....
BmapM1Result< T > qsys_bmapm1(const std::vector< Matrix< T > > &D, const T &mu, const T &qParam, std::size_t maxLevelParam, unsigned maxIter, const T &tol, double tailTol)
BMAP/M/1 by the matrix-analytic (M/G/1-type) method.
T num_abs(const T &v)
Definition number.h:172
A queueing network and its refreshed NetworkStruct.
BMAP/M/1 by the matrix-analytic (M/G/1-type) method.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:674
static constexpr double FineTol
Definition lang_types.h:668
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
The joint (level, phase) table getProb returns: rows levels, cols phases.
The metrics getAvg returns, after filtering.
One job class of the network.
double population
infinite for an open class
Everything qsys_bmapm1 returns, mirroring the MATLAB result struct.
Definition qsys_bmapm1.h:62