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, std::size_t ist, 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(ErrorKind{"Topology"},
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(ErrorKind{"Topology"}, std::string(fn) +
79 ": the model does not contain any queue stations");
80 // The QBD describes the one queue of the model, so any other station (e.g. a Delay) is refused by name
81 if (L.stations[ist - 1].nodetype != qn::NodeType::Queue)
82 throw UnsupportedError(ErrorKind{"Topology"},
83 std::string(fn) + " in SolverMAM analyzes the queue station of the model; station " +
84 L.stations[ist - 1].name + " is not a queue. Use SolverCTMC or SolverSSA for its probabilities");
85}
86
87/**
88 * The per-class service phase-type pairs at station `ist`, scaled by 1/(rate c)
89 * exactly as both accessors do.
90 */
91template <class T>
92std::vector<PhService<T>> station_services(const qn::NetworkStruct<T>& L, std::size_t ist) {
93 using lang::GlobalConstants;
94 const T one = num_traits<T>::from_int(1);
95 const std::size_t K = L.nclasses;
96 const double ns = L.stations[ist - 1].nservers;
97 std::vector<PhService<T>> svc(K);
98 for (std::size_t r = 0; r < K; ++r) {
99 if (L.disabled[ist - 1][r] || !(L.rates(ist - 1, r) > num_traits<T>::from_int(0))) {
100 // The reference's isnan(D0) guard: an unserved class becomes Immediate.
101 const T imm = num_traits<T>::from_double(GlobalConstants::Immediate);
102 svc[r].sigma.assign(1, one);
103 svc[r].S = Matrix<T>(1, 1, T(-imm));
104 continue;
105 }
106 const T target =
107 std::isfinite(ns)
108 ? T(T(one / L.rates(ist - 1, r)) / num_traits<T>::from_double(ns))
109 : T(one / L.rates(ist - 1, r));
110 const Map<T> sc = map_scale(lang::dist_to_map(L.service[ist - 1][r]), target);
111 svc[r].sigma = map_pie(sc);
112 svc[r].S = sc.D0;
113 }
114 return svc;
115}
116
117/** The aggregate queue-length marginal, normalized, as both accessors take it. */
118template <class T>
119std::vector<T> level_marginal(const qn::NetworkStruct<T>& L, const MamOptions& opt,
120 std::size_t ist, const mva::AvgResult<T>& avg,
121 std::vector<PhService<T>>& svc, std::vector<T>& classLambda,
122 bool& closed) {
123 using lang::GlobalConstants;
124 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
125 const std::size_t K = L.nclasses;
126 svc = station_services(L, ist);
127 classLambda.assign(K, zero);
128
129 closed = true;
130 for (const qn::JobClass& c : L.classes)
131 if (std::isinf(c.population)) closed = false;
132
133 Mmap<T> arr;
134 std::size_t maxLevel;
135 if (closed) {
136 // Poisson surrogate at the converged per-class throughputs.
137 std::size_t Ntot = 0;
138 for (const qn::JobClass& c : L.classes)
139 Ntot += static_cast<std::size_t>(std::llround(c.population));
140 maxLevel = Ntot + 1;
141 T tot = zero;
142 for (std::size_t r = 0; r < K; ++r) {
143 classLambda[r] = avg.TN(ist - 1, r);
144 tot += classLambda[r];
145 }
146 if (!(num_traits<T>::to_double(tot) >= GlobalConstants::FineTol))
147 return std::vector<T>(); // no traffic: the caller emits the point mass at 0
148 arr.D0 = Matrix<T>(1, 1, T(-tot));
149 arr.D1 = Matrix<T>(1, 1, tot);
150 for (std::size_t r = 0; r < K; ++r)
151 arr.Dc.push_back(Matrix<T>(1, 1, classLambda[r]));
152 } else {
153 const std::size_t src = L.sourceIdx;
154 if (src == 0) throw InputError("getProb: the open model has no Source");
155 maxLevel = opt.cutoff > 0 ? opt.cutoff : static_cast<std::size_t>(100);
156 std::vector<Map<T>> arrMaps(K);
157 T tot = zero;
158 for (std::size_t r = 0; r < K; ++r) {
159 if (L.disabled[src - 1][r]) {
160 arrMaps[r] = Map<T>{Matrix<T>(1, 1, zero), Matrix<T>(1, 1, zero)};
161 continue;
162 }
163 arrMaps[r] = lang::dist_to_map(L.service[src - 1][r]);
164 classLambda[r] = map_lambda(arrMaps[r]);
165 tot += classLambda[r];
166 }
167 if (!(num_traits<T>::to_double(tot) >= GlobalConstants::FineTol))
168 return std::vector<T>();
169 if (K == 1) {
170 arr.D0 = arrMaps[0].D0;
171 arr.D1 = arrMaps[0].D1;
172 arr.Dc.assign(1, arrMaps[0].D1);
173 } else {
174 // The reference superposes the per-class MAPs and then splits the
175 // aggregate D1 by arrival-rate share. NOTE: MATLAB writes
176 // `map_super({superMAP, arrMaps{k}})`, passing ONE cell argument to
177 // a two-argument function, so this branch RAISES there; see the
178 // divergence note in _kb. The port does what the code plainly
179 // intends, which is the two-argument superposition.
180 Map<T> super = arrMaps[0];
181 for (std::size_t r = 1; r < K; ++r) {
182 Map<T> m;
183 m.D0 = krons(super.D0, arrMaps[r].D0);
184 m.D1 = krons(super.D1, arrMaps[r].D1);
185 super = map_normalize(m);
186 }
187 arr.D0 = super.D0;
188 arr.D1 = super.D1;
189 for (std::size_t r = 0; r < K; ++r) {
190 Matrix<T> Dr = super.D1;
191 const T w = T(classLambda[r] / tot);
192 for (std::size_t i = 0; i < Dr.rows(); ++i)
193 for (std::size_t j = 0; j < Dr.cols(); ++j) Dr(i, j) *= w;
194 arr.Dc.push_back(Dr);
195 }
196 }
197 }
198
199 // Both accessors capture ONE output of MMAPPH1FCFS('ncDistr'), i.e. class
200 // 1's marginal, and renormalize it. Reproduced.
201 const std::vector<std::vector<T>> d = mmapph1fcfs_ncdistr(arr, svc, maxLevel);
202 std::vector<T> p(d[0].size(), zero);
203 T mass = zero;
204 for (std::size_t n = 0; n < d[0].size(); ++n) {
205 p[n] = num_abs(d[0][n]);
206 mass += p[n];
207 }
208 if (mass > zero)
209 for (T& v : p) v /= mass;
210 (void)one;
211 return p;
212}
213
214} // namespace prob_detail
215
216/** The joint (level, phase) table `getProb` returns: rows levels, cols phases. */
217template <class T>
218struct ProbTable {
220};
221
222/**
223 * Port of `@@SolverMAM/getProb.m`.
224 *
225 * @param node 1-based NODE index; must be a station
226 * @param avg the converged `getAvg` result, which the closed branch reads
227 * throughputs from
228 * @param L the refreshed struct
229 * @param opt SolverMAM's options
230 */
231template <class T>
233 std::size_t node, const mva::AvgResult<T>& avg) {
234 if constexpr (!num_traits<T>::has_transcendental) {
235 throw UnsupportedError(ErrorKind{"Arithmetic"},
236 "getProb: the queue-length distribution comes from the MMAP[K]/PH[K]/1 age process, "
237 "whose first-return matrix is a tolerance-terminated Riccati doubling; rerun with "
238 "--arith double or --arith real");
239 } else {
240 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
241 if (node == 0 || node > L.nof_nodes())
242 throw InputError("getProb: node number exceeds the number of nodes in the model");
243 const std::size_t ist = L.nodes[node - 1].station;
244 if (ist == 0) throw InputError("getProb: the specified node is not a station");
245 prob_detail::require_single_queue(L, ist, "getProb");
246
247 const std::size_t K = L.nclasses;
248 std::vector<PhService<T>> svc;
249 std::vector<T> classLambda;
250 bool closed = false;
251 const std::vector<T> p =
252 prob_detail::level_marginal(L, opt, ist, avg, svc, classLambda, closed);
253
254 ProbTable<T> out;
255 if (p.empty()) {
256 // No traffic at this station: all the probability sits at (0, 1).
257 std::size_t levels = 1;
258 if (closed) {
259 std::size_t Ntot = 0;
260 for (const qn::JobClass& c : L.classes)
261 Ntot += static_cast<std::size_t>(std::llround(c.population));
262 levels = Ntot + 1;
263 } else {
264 levels = opt.cutoff > 0 ? opt.cutoff : static_cast<std::size_t>(100);
265 }
266 out.P = Matrix<T>(levels, 1, zero);
267 out.P(0, 0) = one;
268 return out;
269 }
270
271 std::size_t nPhases = 1;
272 for (std::size_t r = 0; r < K; ++r) nPhases = std::max(nPhases, svc[r].S.rows());
273
274 // The arrival-weighted average of the TIME-STATIONARY phase distributions,
275 // map_prob, not the initial vectors sigma. sigma is the embedded
276 // equilibrium at departure instants -- the phase a service STARTS in -- so
277 // for an Erlang-2 it is [1 0] and the joint gave P(phase 2) = 0 at every
278 // level for a server that spends half its busy time in phase 2. A PH law
279 // (sigma, S) is the MAP {S, (-S e) sigma}, so the stationary vector needs no
280 // extra state: PhService stays as MMAPPH1FCFS requires it.
281 T tot = zero;
282 for (std::size_t r = 0; r < K; ++r) tot += classLambda[r];
283 std::vector<T> avgPie(nPhases, zero);
284 if (tot > zero)
285 for (std::size_t r = 0; r < K; ++r) {
286 const std::size_t m = svc[r].S.rows();
287 Map<T> ph;
288 ph.D0 = svc[r].S;
289 ph.D1 = Matrix<T>(m, m, zero);
290 for (std::size_t i = 0; i < m; ++i) {
291 T exit = zero;
292 for (std::size_t j = 0; j < m; ++j) exit -= svc[r].S(i, j);
293 for (std::size_t j = 0; j < m && j < svc[r].sigma.size(); ++j)
294 ph.D1(i, j) = T(exit * svc[r].sigma[j]);
295 }
296 const std::vector<T> piq = map_prob(ph);
297 for (std::size_t i = 0; i < piq.size() && i < nPhases; ++i)
298 avgPie[i] += T(piq[i] * classLambda[r] / tot);
299 }
300 T s = zero;
301 for (const T& v : avgPie) s += v;
302 if (!(num_traits<T>::to_double(s) > 0.0) || !std::isfinite(num_traits<T>::to_double(s))) {
303 // The reference's fallback: a uniform phase distribution.
304 for (T& v : avgPie) v = T(one / num_traits<T>::from_int(static_cast<int>(nPhases)));
305 } else {
306 for (T& v : avgPie) v /= s;
307 }
308
309 out.P = Matrix<T>(p.size(), nPhases, zero);
310 for (std::size_t n = 0; n < p.size(); ++n)
311 for (std::size_t i = 0; i < nPhases; ++i) out.P(n, i) = T(p[n] * avgPie[i]);
312 return out;
313 } // if constexpr has_transcendental
314}
315
316/**
317 * Port of `@@SolverMAM/getProbMarg.m`: P(n jobs of class `jobclass`) at station
318 * `ist`, for n = 0..N(jobclass) closed, or over the whole cutoff range open.
319 */
320template <class T>
322 std::size_t ist, std::size_t jobclass,
323 const mva::AvgResult<T>& avg) {
324 if constexpr (!num_traits<T>::has_transcendental) {
325 throw UnsupportedError(ErrorKind{"Arithmetic"},
326 "getProbMarg: the queue-length distribution comes from the MMAP[K]/PH[K]/1 age "
327 "process, whose first-return matrix is a tolerance-terminated Riccati doubling; "
328 "rerun with --arith double or --arith real");
329 } else {
330 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
331 if (ist == 0 || ist > L.nstations)
332 throw InputError("getProbMarg: station number exceeds the number of stations");
333 if (jobclass == 0 || jobclass > L.nclasses)
334 throw InputError("getProbMarg: job class index exceeds the number of classes");
335 prob_detail::require_single_queue(L, ist, "getProbMarg");
336
337 std::vector<PhService<T>> svc;
338 std::vector<T> classLambda;
339 bool closed = false;
340 const std::vector<T> p =
341 prob_detail::level_marginal(L, opt, ist, avg, svc, classLambda, closed);
342
343 if (p.empty()) {
344 const std::size_t levels =
345 closed ? static_cast<std::size_t>(std::llround(L.classes[jobclass - 1].population)) + 1
346 : static_cast<std::size_t>(100);
347 std::vector<T> out(levels, zero);
348 out[0] = one;
349 return out;
350 }
351 if (!closed) return p;
352
353 const double Nr = L.classes[jobclass - 1].population;
354 if (!(Nr > 0.0))
355 throw UnsupportedError(ErrorKind{"Population"},
356 "getProbMarg: class " + std::to_string(jobclass) +
357 " has zero population, so it has no marginal queue-length distribution");
358 const std::size_t Nk = static_cast<std::size_t>(std::llround(Nr));
359 std::vector<T> out(Nk + 1, zero);
360 T mass = zero;
361 for (std::size_t n = 0; n <= Nk && n < p.size(); ++n) {
362 out[n] = p[n];
363 mass += out[n];
364 }
365 if (mass > zero)
366 for (T& v : out) v /= mass;
367 return out;
368 } // if constexpr has_transcendental
369}
370
371/**
372 * Port of `@@SolverMAM/getMAMResult.m`: the matrix-analytic internals of a
373 * single-queue model, for a BMAP (or MAP) arrival stream into an exponential
374 * single server.
375 *
376 * The reference's other arm handles a retrial station through
377 * `qsys_bmapphnn_retrial`. That routine IS ported, but the C++ `NetworkStruct`
378 * has no retrial fields at all, so no model this port can build reaches it; the
379 * arm is recorded rather than written.
380 */
381template <class T>
383 if constexpr (!num_traits<T>::has_transcendental) {
384 throw UnsupportedError(ErrorKind{"Arithmetic"},
385 "getMAMResult: qsys_bmapm1 runs a functional iteration for G and an adaptive level "
386 "truncation, both tolerance-terminated; rerun with --arith double or --arith real");
387 } else {
388 std::size_t src = 0, q = 0;
389 for (std::size_t i = 1; i <= L.nstations; ++i) {
390 const qn::NodeType nt = L.nodes[L.node_of_station(i) - 1].nodetype;
391 if (nt == qn::NodeType::Source) src = i;
392 else if (nt == qn::NodeType::Queue) {
393 if (q != 0)
394 throw UnsupportedError(ErrorKind{"Topology"},
395 "getMAMResult exposes the matrix-analytic internals of a single-queue model "
396 "only");
397 q = i;
398 }
399 }
400 if (src == 0 || q == 0)
401 throw UnsupportedError(ErrorKind{"ClosedClass"},
402 "getMAMResult requires an open model with one Source and one Queue");
403 if (L.nclasses > 1)
404 throw UnsupportedError(
405 "getMAMResult exposes the matrix-analytic internals of a single-class model only");
406 if (L.stations[q - 1].nservers != 1.0)
407 throw UnsupportedError(ErrorKind{"MultiServer"}, "getMAMResult requires a single-server queue");
408
409 const Map<T> arv = lang::dist_to_map(L.service[src - 1][0]);
410 const Map<T> svc = lang::dist_to_map(L.service[q - 1][0]);
411 if (svc.D0.rows() != 1)
412 throw UnsupportedError(ErrorKind{"NonExponential"},
413 "getMAMResult exposes the M/G/1-type internals for exponential service only; the "
414 "queue has a multi-phase service process");
415 const T mu = T(-svc.D0(0, 0));
416
417 // A MAP is the batch-size-one BMAP; a genuine BMAP would carry Dmark.
418 std::vector<Matrix<T>> D;
419 D.push_back(arv.D0);
420 const std::vector<Matrix<T>>& batches = L.service[src - 1][0].Dmark;
421 if (batches.empty()) {
422 D.push_back(arv.D1);
423 } else {
424 for (const Matrix<T>& Dk : batches) D.push_back(Dk);
425 }
426 return qsys::qsys_bmapm1(D, mu);
427 } // if constexpr has_transcendental
428}
429
430} // namespace mam
431} // namespace line
432
433#endif // LINE_SOLVERS_MAM_SOLVER_MAM_PROB_H
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
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 station st, 0 when the map has no entry.
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)
The (D0,D1) pair that reaches sn.proc.
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
double tot(const std::vector< double > &pops)
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.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:184
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.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:771
static constexpr double FineTol
Definition lang_types.h:765
The options SolverMAM reads.
Definition mam_types.h:30
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