LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_decmmap.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_DECMMAP_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_DECMMAP_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam.m`, the `dec.mmap` method: the per-class departure-process
12 * decomposition.
13 *
14 * IT IS THE MMAP DECOMPOSITION WITHOUT THE FORK-JOIN MACHINERY. The sweep is the
15 * same one `solver_mam_basic_mmap_inner` runs -- rebuild every arrival stream
16 * from the departure table through the traffic equations, solve each station in
17 * isolation, and replace each departure process by the ETAQA truncation of that
18 * station's own QBD -- with three differences, all of them the reference's:
19 * - the traffic step is `solver_mam_traffic`, the plain one, so DEP is
20 * STATION-indexed and a Fork or a Join in the model is refused by the
21 * traffic step itself rather than synchronized;
22 * - there is no Join post-processing, because there is no Join;
23 * - the increment norm is `max(|xn-xr|./xr)`, WITHOUT the FineTol offset the
24 * fork-join variant adds, so the first sweep's norm is NaN by construction
25 * (Q starts at zero) and `config.da_miniter = 3` is what carries the loop
26 * past it.
27 *
28 * WHAT THE REFERENCE REFUSES, AND HOW THIS PORT REFUSES IT. Two model classes
29 * get NO ANSWER from `solver_mam.m`, and neither refusal is an error there:
30 * - a station whose discipline is not EXT, FCFS, HOL, FCFSPRPRIO or PS makes
31 * it return `[]` for all six metrics with `method = ''`, after a warning.
32 * Note that INF is NOT in that list: a model with a Delay is refused;
33 * - a model that is not purely open returns the ZERO matrices it initialised,
34 * again after a warning.
35 * Returning empty or all-zero metrics under a method name is exactly the
36 * silently-wrong answer this port refuses to produce, so both are thrown BY NAME
37 * here, with the reference's own reason in the message.
38 *
39 * THE TRAILING SURROGATE-DELAY BLOCK IS OUTSIDE THE NODE SWITCH in the
40 * reference, so it runs at EVERY station and not only at the queues -- including
41 * the Source, whose `PH` is its own ARRIVAL process (the EXT branch never
42 * assigns one) and whose utilization therefore comes out as lambda * (1/lambda)
43 * = 1. Reproduced as written: it is what the reference reports, the surrogate
44 * term is zero at the Source because it has one server, and `getAvg`'s metric
45 * filter is what decides whether a Source utilization is shown.
46 *
47 * ARITHMETIC. Double (or real) only, for the reasons `solver_mam_basic.h` lists.
48 */
49
50#include <algorithm>
51#include <cmath>
52#include <string>
53#include <utility>
54#include <vector>
55
56#include "line/api/da/da_fpi.h"
67#include "line/solvers/mam/solver_mam_bmap.h" // mam_detect_mmck
69#include "line/util/error.h"
70#include "line/util/matrix.h"
71
72namespace line {
73namespace mam {
74
75namespace decmmap_detail {
76
77using lang::GlobalConstants;
79
80/** `solver_mam.m`'s opening station loop: the disciplines it has a branch for. */
81template <class T>
82void check_disciplines(const qn::NetworkStruct<T>& L) {
83 for (std::size_t i = 0; i < L.nstations; ++i) {
84 const SchedStrategy sc = L.stations[i].sched;
85 if (sc == SchedStrategy::EXT || sc == SchedStrategy::FCFS || sc == SchedStrategy::HOL ||
87 continue;
88 throw UnsupportedError(
89 std::string("SolverMAM: the dec.mmap method has no branch for ") +
90 lang::sched_to_text(sc) + " scheduling at station '" + L.stations[i].name +
91 "'. solver_mam.m warns and returns EMPTY metrics with a cleared method name for such a "
92 "model, which is no answer at all; note that INF is not in its list either, so a model "
93 "carrying a Delay belongs to 'dec.source' or 'dec.source.mmap'");
94 }
95}
96
97} // namespace decmmap_detail
98
99/**
100 * Port of `solver_mam.m`.
101 *
102 * @param L the refreshed struct; open classes only
103 * @param opt the SolverMAM options; `tol` is the fixed point's iter_tol
104 */
105template <class T>
107 if constexpr (!num_traits<T>::has_transcendental) {
108 (void)L; (void)opt;
109 throw UnsupportedError(
110 "solver_mam_decmmap: the departure-process fixed point stops on a tolerance, "
111 "MMAP[K]/PH[K]/1 runs the ADDA doubling iteration and the ETAQA departure process "
112 "needs transcendental arithmetic; rerun with --arith double or --arith real");
113 } else {
114 using namespace decmmap_detail;
115 using namespace basic_mmap_detail;
116 using basic_detail::station_visits;
117 using basic_detail::truncate_renorm;
118 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
119 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
120 const T ftol = num_traits<T>::from_double(GlobalConstants::FineTol);
121 MmapDecConfig cfg;
122
123 check_disciplines(L);
124 if (!L.is_open_model())
125 throw UnsupportedError(
126 "SolverMAM: the dec.mmap method solves open models only. solver_mam.m warns and "
127 "returns the ZERO matrices it initialised for anything else, which reports an empty "
128 "network rather than refusing; use 'dec.source', 'dec.source.mmap' or 'mna'");
129
130 // The chain arrival rate, as solver_mam.m computes it: the total rate the
131 // chain's classes are released at by the reference station of its first
132 // class, carried by every class of the chain.
133 std::vector<T> lambda(K, zero);
134 for (std::size_t c = 0; c < C; ++c) {
135 if (L.inchain[c].empty()) continue;
136 const std::size_t rs = L.classes[L.inchain[c][0] - 1].refstat - 1;
137 T tot = zero;
138 for (std::size_t k : L.inchain[c]) {
139 if (L.disabled[rs][k - 1]) continue;
140 if (!std::isfinite(num_traits<T>::to_double(L.rates(rs, k - 1)))) continue;
141 tot += L.rates(rs, k - 1);
142 }
143 for (std::size_t k : L.inchain[c]) lambda[k - 1] = tot;
144 }
145
146 const Matrix<T> V = station_visits(L);
147 const MmapPhTable<T> ph = mmap_ph_table(L);
148 const TrafficConfig tcfg = traffic_config(opt);
149
150 Matrix<T> QN(M, K, zero), UN(M, K, zero), RN(M, K, zero), TN(M, K, zero);
151 for (std::size_t i = 0; i < M; ++i)
152 if (L.stations[i].sched == SchedStrategy::EXT)
153 for (std::size_t r = 0; r < K; ++r)
154 TN(i, r) = L.disabled[i][r] ? zero : L.rates(i, r);
155
156 // STATION-indexed here, node-indexed in the fork-join variant: the plain
157 // traffic step reads DEP[station][class].
158 DepTable<T> DEP(M, std::vector<Map<T>>(K));
159
160 auto sweep = [&](const std::vector<T>&,
161 std::size_t itnum) -> std::pair<std::vector<T>, std::vector<T>> {
162 if (itnum == 1)
163 for (std::size_t i = 0; i < M; ++i)
164 for (std::size_t r = 0; r < K; ++r)
165 DEP[i][r] = (lambda[r] > zero && V(i, r) > zero)
166 ? map_scale(ph.PH[i][r], T(one / T(lambda[r] * V(i, r))))
167 // The reference divides unconditionally; a
168 // zero denominator would scale to an
169 // infinite mean, which the traffic step
170 // reads as a silent stream. Left as the
171 // unscaled process instead, which is what
172 // that stream carries.
173 : ph.PH[i][r];
174
175 std::vector<Mmap<T>> ARV = solver_mam_traffic(L, DEP, tcfg);
176
177 std::vector<T> xref(M * K, zero);
178 for (std::size_t i = 0; i < M; ++i)
179 for (std::size_t r = 0; r < K; ++r) xref[i * K + r] = QN(i, r);
180
181 for (std::size_t i = 0; i < M; ++i) {
182 const std::size_t ind = L.node_of_station(i + 1) - 1;
183 const SchedStrategy sc = L.stations[i].sched;
184 const double ns = L.stations[i].nservers;
185 bool finiteCapUsed = false;
186
187 if (L.nodes[ind].nodetype == qn::NodeType::Queue && ARV[ind].order() > 0) {
188 if (ARV[ind].order() > tcfg.space_max) ARV[ind] = compress_arrival(ARV[ind]);
189 const std::vector<T> lam = mmap_lambda(ARV[ind]);
190 for (std::size_t r = 0; r < K; ++r) TN(i, r) = lam[r];
191
192 std::vector<PhService<T>> sl;
193 for (std::size_t r = 0; r < K; ++r) sl.push_back(ph.svc[i][r]);
194
195 if (sc == SchedStrategy::FCFS || sc == SchedStrategy::HOL ||
196 sc == SchedStrategy::FCFSPRPRIO) {
197 if (std::isfinite(L.stations[i].cap)) {
198 const std::size_t capK =
199 static_cast<std::size_t>(std::llround(L.stations[i].cap));
200 T meanQ = zero, lossProb = zero;
201 const MmckDetection<T> det = mam_detect_mmck(L, i + 1, ARV[ind]);
202 if (det.isMmck) {
203 T lamTot = zero;
204 for (std::size_t r = 0; r < K; ++r)
205 if (!std::isnan(num_traits<T>::to_double(TN(i, r))))
206 lamTot += TN(i, r);
208 lamTot, det.muRate, static_cast<unsigned>(std::llround(ns)),
209 static_cast<unsigned>(capK));
210 meanQ = ex.meanQueueLength;
211 lossProb = ex.lossProbability;
212 } else {
213 const basic_detail::TruncRenorm<T> tr =
214 truncate_renorm(ARV[ind], sl, capK);
215 meanQ = tr.meanQ;
216 lossProb = tr.lossProb;
217 }
218 std::vector<T> eff(K, zero), Sact(K, zero);
219 T sumTN = zero;
220 for (std::size_t r = 0; r < K; ++r) {
221 const T inflow =
222 std::isnan(num_traits<T>::to_double(TN(i, r))) ? zero : TN(i, r);
223 eff[r] = T(inflow * T(one - lossProb));
224 Sact[r] = T(map_mean(ph.PH[i][r]) * num_traits<T>::from_double(ns));
225 sumTN += eff[r];
226 }
227 T Wq = zero;
228 if (sumTN > zero) {
229 T sw = zero;
230 for (std::size_t r = 0; r < K; ++r) {
231 const T c = T(eff[r] * Sact[r]);
232 if (!std::isnan(num_traits<T>::to_double(c))) sw += c;
233 }
234 const T w = T(T(meanQ / sumTN) - T(sw / sumTN));
235 Wq = (w > zero) ? w : zero;
236 }
237 for (std::size_t r = 0; r < K; ++r) {
238 TN(i, r) = eff[r];
239 UN(i, r) = T(TN(i, r) * map_mean(ph.PH[i][r]));
240 if (TN(i, r) > zero) {
241 RN(i, r) = T(Wq + Sact[r]);
242 QN(i, r) = T(TN(i, r) * RN(i, r));
243 } else {
244 RN(i, r) = zero;
245 QN(i, r) = zero;
246 }
247 }
248 finiteCapUsed = true;
249 } else {
250 // No saturation test and no MAP/MAP/1 fast path here:
251 // solver_mam.m goes straight to MMAP[K]/PH[K]/1, and its
252 // fork-join sibling is the one that added both.
253 const std::vector<T> m = mmapph1fcfs_ncmean(ARV[ind], sl);
254 for (std::size_t r = 0; r < K; ++r)
255 QN(i, r) = m[ARV[ind].classes() == 1 ? 0 : r];
256 }
257 } else if (sc == SchedStrategy::PS) {
258 for (std::size_t r = 0; r < K; ++r)
259 UN(i, r) = T(TN(i, r) * map_mean(ph.PH[i][r]));
260 T usum = zero;
261 for (std::size_t r = 0; r < K; ++r) usum += UN(i, r);
262 const T uden = (usum < T(one - ftol)) ? usum : T(one - ftol);
263 for (std::size_t r = 0; r < K; ++r) QN(i, r) = T(UN(i, r) / T(one - uden));
264 }
265 }
266
267 // OUTSIDE the node switch in the reference; see the file header.
268 if (!finiteCapUsed) {
269 for (std::size_t r = 0; r < K; ++r) {
270 UN(i, r) = T(TN(i, r) * map_mean(ph.PH[i][r]));
271 if (std::isfinite(ns))
272 QN(i, r) = T(QN(i, r) + TN(i, r) *
273 T(map_mean(ph.PH[i][r]) *
275 num_traits<T>::from_double((ns - 1.0) / ns));
276 RN(i, r) = T(QN(i, r) / TN(i, r));
277 }
278 }
279 }
280
281 // ---- the departure processes for the next sweep -------------------
282 for (std::size_t i = 0; i < M; ++i) {
283 const std::size_t ind = L.node_of_station(i + 1) - 1;
284 if (L.nodes[ind].nodetype != qn::NodeType::Queue || ARV[ind].order() == 0) continue;
285 const SchedStrategy sc = L.stations[i].sched;
286 const bool fcfs = (sc == SchedStrategy::FCFS || sc == SchedStrategy::HOL ||
287 sc == SchedStrategy::FCFSPRPRIO);
288 if (!fcfs && sc != SchedStrategy::PS) continue;
289 T rho = zero;
290 for (std::size_t r = 0; r < K; ++r) rho += UN(i, r);
291 for (std::size_t r = 0; r < K; ++r) {
292 const Mmap<T> A = mmap_hide_but(ARV[ind], r);
293 const Map<T>& Srv = ph.PH[i][r];
294 const std::size_t etaqa_sz = (cfg.etaqa_trunc + 1) * A.order() * Srv.D0.rows();
295 Map<T> dep = Srv;
296 if (etaqa_sz <= tcfg.space_max && rho < T(one - ftol)) {
297 // The reference's own try/catch, as in the fork-join variant.
298 try {
299 const Map<T> Am{A.D0, A.D1};
300 dep = map_normalize(fcfs ? qbd_depproc_etaqa(Am, Srv, cfg.etaqa_trunc)
301 : qbd_depproc_etaqa_ps(Am, Srv, cfg.etaqa_trunc));
302 } catch (const Error&) {
303 dep = Srv;
304 }
305 }
306 // Unconditional in the reference, which divides by zero when the
307 // class does not flow here; left unscaled instead, as at the
308 // initialisation above.
309 if (lambda[r] > zero && V(i, r) > zero)
310 dep = map_scale(dep, T(one / T(lambda[r] * V(i, r))));
311 DEP[i][r] = dep;
312 }
313 }
314
315 std::vector<T> xnew(M * K, zero);
316 for (std::size_t i = 0; i < M; ++i)
317 for (std::size_t r = 0; r < K; ++r) xnew[i * K + r] = QN(i, r);
318 return std::make_pair(xnew, xref);
319 };
320
322 fo.iter_max = static_cast<std::size_t>(opt.iter_max);
323 fo.iter_tol = opt.tol;
324 fo.miniter = 3; // config.da_miniter
325 fo.relative_norm = true; // config.da_norm, WITHOUT the FineTol offset
326 const da::FpiResult<T> fr = da::da_fpi<T>(sweep, std::vector<T>(M * K, zero), fo);
327
329 out.Q = QN;
330 out.U = UN;
331 out.R = RN;
332 out.Tp = TN;
333 // CN and XN are the zeros solver_mam.m initialises and never assigns.
334 out.C.assign(K, zero);
335 out.X.assign(K, zero);
336 out.method = "dec.mmap";
337 out.iter = static_cast<int>(fr.iterations);
338 out.lG = 0.0;
339 return out;
340 } // if constexpr has_transcendental
341}
342
343} // namespace mam
344} // namespace line
345
346#endif // LINE_SOLVERS_MAM_SOLVER_MAM_DECMMAP_H
Base error for the multiprecision C++ port.
Definition error.h:31
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< std::vector< bool > > disabled
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
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
bool is_open_model() const
sn_is_open_model: EVERY class is open, which is not has_open_classes.
Damped fixed-point iteration, the shared driver of the decomposition algorithms.
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.
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
The MMAP[K]/PH[K]/1 FCFS queue: per-class mean number in system and per-class queue-length distributi...
FpiResult< T > da_fpi(const std::function< std::pair< std::vector< T >, std::vector< T > >(const std::vector< T > &, std::size_t)> &iterfun, const std::vector< T > &x0, const FpiOptions &options=FpiOptions())
Damped fixed-point iteration, the shared driver of the decomposition algorithms.
Definition da_fpi.h:92
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
std::vector< T > mmapph1fcfs_ncmean(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc)
Per-class mean number of customers in the system, BUTools' 'ncMoms', 1.
MmckDetection< T > mam_detect_mmck(const qn::NetworkStruct< T > &L, std::size_t ist, const Mmap< T > &arv)
Port of mam_detect_mmck.m: is the exact M/M/c/K closed form legitimate at this station?
std::vector< std::vector< Map< T > > > DepTable
DEP{i,r}, the departure process of class r from i in (D0,D1) form.
TrafficConfig traffic_config(const MamOptions &opt)
The traffic step's view of SolverOptions('MAM').
std::vector< T > mmap_lambda(const Mmap< T > &m)
Alias kept for parity with the MATLAB name.
Mmap< T > mmap_hide_but(const Mmap< T > &in, std::size_t keep)
mmap_hide(m, setdiff(1:K, keep)): keep ONE mark, hide every other.
Map< T > qbd_depproc_etaqa(const Map< T > &arrival, const Map< T > &service, std::size_t n)
MAP descriptor of the departure process of a MAP/MAP/1-FCFS queue, ETAQA-truncated at level n (qbd_de...
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
Map< T > qbd_depproc_etaqa_ps(const Map< T > &arrival, const Map< T > &service, std::size_t n)
MAP descriptor of the departure process of a MAP/MAP/1-PS queue, ETAQA-truncated at level n (qbd_depp...
Map< T > map_scale(const Map< T > &in, const T &new_mean)
Rescale time so that the mean inter-arrival time becomes new_mean.
mva::MvaSolution< T > solver_mam_decmmap(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam.m.
std::vector< Mmap< T > > solver_mam_traffic(const qn::NetworkStruct< T > &sn, const DepTable< T > &DEP, const TrafficConfig &config)
Port of solver_mam_traffic.m.
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...
MmckResult< T > qsys_mmck(const T &lambda, const T &mu, unsigned c, unsigned K)
Exact analysis of the M/M/c/K queue (truncated Erlang form).
Definition qsys_mmck.h:63
A queueing network and its refreshed NetworkStruct.
Departure process of a MAP/MAP/1 queue: the ETAQA-truncated MAP descriptor under FCFS and under PS,...
Exact analysis of the M/M/c/K queue (truncated Erlang form).
Port of solver_mam_basic.m, the dec.source analyzer and the default algorithm of SolverMAM.
Port of solver_mam_basic_mmap.m, solver_mam_basic_mmap_inner.m and solver_mam_basic_mmap_closed....
The batch-arrival and batch-service queues of the MAM solver, and the two finite-capacity helpers sol...
Port of solver_mam_traffic.m and solver_mam_traffic_mmap.m: the traffic step of the dec....
Options mirroring the fields MATLAB reads off the options struct.
Definition da_fpi.h:50
std::size_t iter_max
Definition da_fpi.h:51
std::size_t miniter
iterations before the stopping test applies
Definition da_fpi.h:54
bool relative_norm
config.da_norm, the increment norm.
Definition da_fpi.h:71
std::size_t iterations
Definition da_fpi.h:78
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 > D0
Definition map_moment.h:54
The options.config fields the MMAP decomposition reads on top of MamOptions.
std::size_t etaqa_trunc
config.etaqa_trunc, the ETAQA level truncation of the departure process.
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::size_t order() const
Definition mmap_lambda.h:50
What mam_detect_mmck returns; muRate is meaningful only when isMmck.
The fields of options.config the traffic step reads.
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
double lG
log of the normalizing constant, the reference's lG.
Definition mva_types.h:118
std::vector< T > C
Definition mva_types.h:98
T meanQueueLength
L, mean number in system.
Definition qsys_mmck.h:44