LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mam_dispatch.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_MAM_DISPATCH_H
6#define LINE_SOLVERS_MAM_MAM_DISPATCH_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_analyzer.m`: one inner solve, choosing the analyzer that
12 * fits the model and the requested method.
13 *
14 * THE ORDER IS THE CONTRACT, exactly as in `mva_dispatch.h`. The reference's
15 * sequence, top to bottom:
16 *
17 * -1 discrete-time (slotted) models -> solver_mam_dt <- ported
18 * 0 exact MAP/MAP/1 fast path, tried before any method dispatch <- ported
19 * 1 method 'dec.mmap' -> solver_mam <- ported
20 * 2 method 'default' / 'dec.source':
21 * 2a homogeneous Fork-Join -> solver_mam_fj ("qiu") <- ported
22 * 2b any other Fork-Join, open -> solver_mam_basic_mmap <- ported
23 * 2c BMAP/PH/N/N bufferless retrial -> solver_mam_retrial <- ported
24 * 2d reneging (MAP/M/s+G) -> solver_mam_reneging <- ported
25 * 2e single-class closed Delay+Queue -> solver_mam_ldqbd <- ported
26 * 2f otherwise -> solver_mam_basic <- ported
27 * 3 method 'dec.poisson' -> solver_mam_basic with space_max = 1 <- ported
28 * 4 method 'mna' -> solver_mna_open / solver_mna_closed <- ported
29 * 5 method 'ldqbd' -> solver_mam_ldqbd <- ported
30 * 5b method 'bgchain' -> solver_mam_bgchain <- ported
31 * 6 methods 'inap' / 'inaprc' / 'inapinf' -> moved to SolverAG (ag_dispatch.h)
32 * 6b method 'exact' -> removed from the reference (SolverMAM.m:39-40:
33 * "'exact' method removed - autocat moved to line-legacy.git"); not in
34 * list_valid_methods either, so unreachable through solver_mam_solve
35 * 6c methods 'sync.faster' / 'sync.slower' -> solver_mam_sync <- ported
36 * 7 method 'dec.source.mmap' -> solver_mam_basic_mmap <- ported
37 *
38 * 2a IS DOUBLE ONLY. The FJ_codes engine behind it needs an ordered real Schur
39 * factorization and two Bartels-Stewart Sylvester solves, i.e. LAPACK, so
40 * `solver_mam_fj` refuses at Rational and Real AFTER running the reference's two
41 * validation steps -- a model outside the homogeneous class is still named as
42 * such at every arithmetic.
43 *
44 * WHAT IS NOT PORTED IS REFUSED BY NAME and never allowed to fall through to
45 * `solver_mam_basic`. A fork-join model or a level-dependent closed model solved
46 * as an ordinary open decomposition returns numbers that are simply not the
47 * model's. The rule extends ONE step past the reference at 2b: a CLOSED
48 * fork-join model, which the reference itself lets fall through to
49 * `solver_mam_basic`, is refused here for exactly that reason; see the branch.
50 *
51 * AFTER the analyzer, the reference overwrites the throughput of every EXT
52 * (Source) station with `sn.rates`, and zeroes every NaN across its six metrics
53 * (the four matrices Q, U, R, Tp and the two vectors C, X). Both are reproduced
54 * in `finish_dispatch`, which every branch below step 0 returns through. Step 0
55 * bypasses it, as the reference's own early return does; see `finish_dispatch`.
56 * The C++ sweep tests `!isfinite` rather than NaN alone, so it also zeroes an
57 * infinity the reference would keep -- deliberate, because the `disabled` flags
58 * this port carries mean a non-finite entry here is a division artefact and
59 * never a modelled infinity.
60 */
61
62#include <limits>
63#include <cmath>
64#include <string>
65#include <vector>
66
82#include "line/util/error.h"
83
84namespace line {
85namespace mam {
86
87namespace dispatch_detail {
88
89/** Is there any Fork node? A Join alone cannot occur without one. */
90template <class T>
91bool has_fork(const qn::NetworkStruct<T>& L) {
92 return L.has_fork();
93}
94
95/**
96 * The reference's `sn_has_fork_join`, which is `any(sn.fj(:) > 0)` and NOT a
97 * scan for Fork nodes: it is the branch-2b gate, and a Fork whose Join the
98 * model never declared leaves `fj` empty and fails it.
99 */
100template <class T>
101bool has_fork_join(const qn::NetworkStruct<T>& L) {
102 return !L.fj.empty();
103}
104
105/**
106 * `sn_is_closed_model`, which is `all(isfinite(sn.njobs))`. NetworkStruct
107 * carries `is_open_model` but no counterpart, and the mna branch needs both:
108 * the reference's own else is the mixed-model refusal, so "not open" is not the
109 * same test as "closed". The empty-class guard mirrors `is_open_model`, where
110 * MATLAB's `all([])` would answer true for both.
111 */
112template <class T>
113bool is_closed_model(const qn::NetworkStruct<T>& L) {
114 for (const qn::JobClass& c : L.classes)
115 if (!std::isfinite(c.population)) return false;
116 return !L.classes.empty();
117}
118
119/** The reference's `isClosedDelayQueue`: one class, two stations, one INF one FCFS. */
120template <class T>
121bool is_closed_delay_queue(const qn::NetworkStruct<T>& L) {
122 if (L.nclasses != 1 || L.nstations != 2) return false;
123 if (!std::isfinite(L.classes[0].population)) return false;
124 std::size_t ndelay = 0, nqueue = 0;
125 for (const qn::Station<T>& st : L.stations) {
126 if (st.sched == lang::SchedStrategy::INF) ++ndelay;
127 else if (st.sched == lang::SchedStrategy::FCFS) ++nqueue;
128 }
129 return ndelay == 1 && nqueue == 1;
130}
131
132/**
133 * Whether a setup/delay-off station, if there is one, is inside the exact
134 * regime of `solver_mam_ldqbd`: one server, exponential service, no load
135 * dependence. True when the model declares no setup at all.
136 */
137template <class T>
138bool ldqbd_setup_ok(const qn::NetworkStruct<T>& L) {
139 if (L.setupparam.empty()) return true;
140 for (typename std::map<std::size_t, qn::SetupDelayOffParam<T>>::const_iterator it =
141 L.setupparam.begin();
142 it != L.setupparam.end(); ++it) {
143 const std::size_t ist = it->first;
144 if (ist == 0 || ist > L.stations.size()) return false;
145 lang::Distrib<T> su, doff;
146 if (!it->second.last(su, doff) || doff.disabled) continue; // not actually declared
147 const qn::Station<T>& st = L.stations[ist - 1];
148 if (st.nservers > 1) return false;
149 // NETWORK-WIDE, as the MATLAB reference's sn_has_load_dependence: the
150 // exact chain assumes a plain delay away from this station, so a
151 // load-dependent station ANYWHERE takes the model out of the regime, not
152 // only a load-dependent setup station. Reading st.lldscaling alone
153 // admitted a load-dependent Delay that MATLAB and the JAR refuse.
154 if (api::sn_has_load_dependence(L)) return false;
155 if (L.service.size() < ist || L.service[ist - 1].empty()) return false;
156 if (lang::dist_to_map(L.service[ist - 1][0]).D0.rows() != 1) return false;
157 }
158 return true;
159}
160
161/**
162 * The reference's `mnaApplies`: whether the closed MNA analyzer covers this
163 * model. The round-robin split is carried by the open traffic equations only, a
164 * self-looping class has no inter-station flow to decompose, and a station whose
165 * discipline the flow sweep does not update would keep a zero queue length. Any
166 * of those routes `default` to dec.source instead.
167 */
168template <class T>
169bool mna_applies(const qn::NetworkStruct<T>& L) {
170 // solver_mna_closed drives its bisection over CLASSES but stores the throughput in
171 // the CHAIN-indexed lambda, and renormalizes chain c's queue lengths with the
172 // class-indexed njobs(c). Both are only correct when each chain holds exactly one
173 // class, and the analyzer refuses otherwise by name -- so the DEFAULT has to stop
174 // here rather than propagate that refusal (cqn_twoclass_hyperl, 1 chain over 2
175 // classes, died at MAM under lang='cpp' while MATLAB answered dec.source).
176 if (L.nchains != L.nclasses) return false;
177
178 for (const qn::NodeDef& nd : L.nodes)
179 for (std::size_t r = 0; r < nd.routing.size(); ++r)
180 if (nd.routing[r] == lang::RoutingStrategy::RROBIN) return false;
181
182 for (const qn::Station<T>& st : L.stations) {
183 if (st.sched != lang::SchedStrategy::INF && st.sched != lang::SchedStrategy::PS &&
184 st.sched != lang::SchedStrategy::FCFS && st.sched != lang::SchedStrategy::EXT)
185 return false;
186 // The PS branch of solver_mna_closed forms U = S*T and the geometric bound
187 // from it WITHOUT dividing by the number of servers, so a multiserver PS
188 // station is misrepresented; dec.source is exact on the non-queueing regime
189 // (c >> N) that shape usually stands for.
190 if (st.sched == lang::SchedStrategy::PS && st.nservers > 1) return false;
191 // A multiclass FCFS station makes the flow sweep superpose one MMAP per class
192 // and then solve MMAPPH1FCFS at level sum(N)+1: measured against an exact CTMC
193 // that costs 12-61s where dec.source costs 0.03s and is not more accurate
194 // (mean relative error 0.11-0.38 against 0.04-0.19). The single-class case is
195 // both cheap and better, so keep only that one.
196 if (st.sched == lang::SchedStrategy::FCFS && L.nclasses > 1) return false;
197 }
198
199 const Matrix<T> V = basic_detail::station_visits(L);
200 for (std::size_t k = 0; k < L.nclasses; ++k) {
201 if (!std::isfinite(L.classes[k].population)) continue;
202 std::size_t seen = 0, at = 0;
203 for (std::size_t i = 0; i < L.nstations; ++i)
204 if (num_traits<T>::to_double(V(i, k)) > 1e-8) {
205 ++seen;
206 at = i;
207 }
208 if (seen == 1 && L.stations[at].sched != lang::SchedStrategy::INF &&
209 L.stations[at].sched != lang::SchedStrategy::EXT)
210 return false;
211 }
212 return true;
213}
214
215/**
216 * The reference's `mnaConserves`: whether the closed MNA outer bisection closed on
217 * N. `solver_mna_closed` rescales each chain onto its population as a last step, so
218 * a diverged bisection still returns queue lengths that sum to N and the failure is
219 * invisible in Q. R and Tp are NOT rescaled, so Little's law over the whole network,
220 * sum_i Tp(i,k)*R(i,k) = N_k, still reads the raw iterate: a converged run lands
221 * within 5e-4 of N and a diverged one is orders of magnitude out, or negative.
222 */
223template <class T, class Sol>
224bool mna_conserves(const qn::NetworkStruct<T>& L, const Sol& sol) {
225 for (std::size_t i = 0; i < L.nstations; ++i)
226 for (std::size_t k = 0; k < L.nclasses; ++k)
227 if (!std::isfinite(num_traits<T>::to_double(sol.Q(i, k))) ||
228 !std::isfinite(num_traits<T>::to_double(sol.R(i, k))) ||
229 !std::isfinite(num_traits<T>::to_double(sol.Tp(i, k))))
230 return false;
231
232 for (std::size_t k = 0; k < L.nclasses; ++k) {
233 const double nk = L.classes[k].population;
234 if (!std::isfinite(nk) || nk <= 0) continue;
235 double npred = 0;
236 for (std::size_t i = 0; i < L.nstations; ++i)
237 npred += num_traits<T>::to_double(sol.Tp(i, k)) * num_traits<T>::to_double(sol.R(i, k));
238 if (std::fabs(npred - nk) > 0.01 * nk) return false;
239 }
240 return true;
241}
242
243/**
244 * The reference's `bgchainApplies`: no class priorities, no fork-join, and at
245 * least one station visited by the closed classes, which is what the background
246 * chain is built over.
247 */
248template <class T>
249bool bgchain_applies(const qn::NetworkStruct<T>& L, const MamOptions& opt) {
250 bool prio_sched = false;
251 for (const qn::Station<T>& st : L.stations)
252 if (st.sched == lang::SchedStrategy::HOL || st.sched == lang::SchedStrategy::FCFSPRPRIO)
253 prio_sched = true;
254 if (prio_sched)
255 for (std::size_t k = 1; k < L.nclasses; ++k)
256 if (L.classes[k].prio != L.classes[0].prio) return false;
257
258 if (has_fork_join(L)) return false;
259
260 const mva::ChainDemands<T> dem = mva::sn_get_demands_chain(L);
261 bool visited = false;
262 for (std::size_t c = 0; c < L.nchains && !visited; ++c) {
263 if (!std::isfinite(num_traits<T>::to_double(dem.Nchain[c])) ||
264 num_traits<T>::to_double(dem.Nchain[c]) <= 0.0)
265 continue;
266 for (std::size_t i = 0; i < L.nstations; ++i)
267 if (num_traits<T>::to_double(dem.Vchain(i, c)) > 1e-14) {
268 visited = true;
269 break;
270 }
271 }
272 if (!visited) return false;
273
274 // The background chain enumerates the closed population vector over the
275 // stations the closed classes visit, so a large closed population makes
276 // mam_bgchain_ctmc refuse the model outright. That refusal is right when the
277 // user asked for bgchain by name and wrong as a default, which must land on
278 // a method that answers: size the chain first and leave those models to
279 // dec.source. The limit is the one mam_bgchain_ctmc enforces.
280 const double states_max = (opt.bgstates_max > 0) ? static_cast<double>(opt.bgstates_max) : 20000.0;
281 return bgchain_states(L, opt) <= states_max;
282}
283
284/**
285 * The reference's `bgchainClosedExact`: whether the background chain represents
286 * this closed model's service laws exactly.
287 *
288 * A station that is not PS or INF must satisfy BOTH conditions below, because the
289 * background chain makes two separate first-moment substitutions there.
290 *
291 * 1 mam_bgchain_ctmc builds its generator from the MEAN service time alone.
292 * That is exact at a PS or INF station, which is insensitive to the service
293 * law beyond its first moment, and exact under any discipline when the law
294 * IS exponential. Measured on a closed Delay+FCFS cycle with Erlang-3
295 * service, the mean-only chain reads 2.8% off SolverCTMC.
296 * 2 The capacity a station's closed jobs hold is split over the background
297 * classes in proportion to their COUNTS, which is service in random order.
298 * That is exact under PS, and exact under FCFS only when the classes are
299 * served at the SAME rate -- an FCFS station with class-dependent rates
300 * reads 25.2% off SolverCTMC on a two-chain closed cycle, against 3.5e-16
301 * when the two rates are made equal.
302 *
303 * solver_mna_closed carries the phase-type representation instead, so neither
304 * surrogate may be chosen as the DEFAULT. Asking for bgchain by name still gets
305 * it, with both approximations documented in solver_mam_bgchain.
306 */
307template <class T>
308bool bgchain_closed_exact(const qn::NetworkStruct<T>& L) {
309 for (std::size_t i = 0; i < L.nstations; ++i) {
310 const lang::SchedStrategy sc = L.stations[i].sched;
313 continue;
314 double rate_here = std::numeric_limits<double>::quiet_NaN();
315 for (std::size_t r = 0; r < L.nclasses; ++r) {
316 const double rate = num_traits<T>::to_double(L.rates(i, r));
317 if (!std::isfinite(rate) || rate <= 0.0) continue;
318 if (L.service[i][r].type != lang::ProcessType::EXP) return false;
319 if (std::isnan(rate_here)) {
320 rate_here = rate;
321 } else if (std::fabs(rate - rate_here) > 1e-3 * rate_here) {
322 return false;
323 }
324 }
325 }
326 return true;
327}
328
329/** Is any station a Cache, a Place or a Transition, i.e. outside the MAM envelope? */
330template <class T>
331void reject_unsupported_nodes(const qn::NetworkStruct<T>& L) {
332 for (const qn::NodeDef& nd : L.nodes) {
333 switch (nd.nodetype) {
334 case qn::NodeType::Cache:
335 throw UnsupportedError(ErrorKind{"Cache"},
336 "SolverMAM: Cache nodes are outside the MAM feature set; use SolverMVA, "
337 "SolverNC, SolverCTMC or SolverLDES");
338 case qn::NodeType::Place:
339 case qn::NodeType::Transition:
340 throw UnsupportedError(ErrorKind{"Place"},
341 "SolverMAM: stochastic Petri net models are outside the MAM feature set; "
342 "use SolverCTMC, SolverSSA or SolverLDES");
343 default:
344 break;
345 }
346 }
347 // A BMAP SERVICE is a BULK SERVER (one firing completes several jobs). No
348 // MAM analyzer reads the batch blocks: every route took the BMAP as the MAP
349 // of its firing epochs, one job per firing, and answered a saturated queue.
350 for (std::size_t i = 0; i < L.nstations; ++i) {
351 if (L.stations[i].nodetype == qn::NodeType::Source) continue;
352 for (std::size_t r = 0; r < L.nclasses; ++r)
353 if (!L.service[i][r].disabled &&
354 (L.service[i][r].type == lang::ProcessType::BMAP ||
355 L.service[i][r].type == lang::ProcessType::BMMAPT))
356 throw UnsupportedError(ErrorKind{"BMAP"},
357 "SolverMAM: station '" + L.stations[i].name + "' serves class '" +
358 L.classes[r].name + "' with a batch (BMAP) process, i.e. a bulk server "
359 "whose firing completes several jobs; MAM has no bulk-server analysis. "
360 "Use SolverLDES");
361 }
362}
363
364} // namespace dispatch_detail
365
366
367/**
368 * What `solver_mam_analyzer.m` does AFTER whichever analyzer ran: pin the
369 * throughput of every EXT (Source) station to its declared rate, and zero every
370 * non-finite entry.
371 *
372 * Factored out because THREE branches return early and must still take this
373 * tail: 2a (`qiu`), 2c (`retrial`) and the 2e `ldqbd` preference on `default`.
374 * The reference has no early return at any of them -- its `case` bodies fall
375 * through to the tail -- so routing them here is what keeps the C++ ladder
376 * equivalent rather than an optimization.
377 *
378 * THE ONE EARLY RETURN THAT DOES NOT COME HERE is the exact MAP/MAP/1 fast
379 * path, and it matches the reference: `solver_mam_analyzer.m` lines 10-17
380 * return before the tail as well. It is not an omission -- that analyzer sets
381 * the Source throughput itself (`solver_mam_mapmap1_exact.h`, `Tp(src,0) =
382 * lambda`), so there is nothing for the tail to pin.
383 */
384template <class T>
387 const T zero = num_traits<T>::from_int(0);
388 for (std::size_t i = 0; i < L.nstations; ++i)
389 if (L.stations[i].sched == SchedStrategy::EXT)
390 for (std::size_t r = 0; r < L.nclasses; ++r)
391 out.sol.Tp(i, r) = L.disabled[i][r] ? zero : L.rates(i, r);
392 // The reference's terminal NaN sweep. The C++ layer carries `disabled`
393 // flags rather than NaN sentinels, so nothing here should be non-finite;
394 // the sweep stays because a division by a zero iterate can still produce
395 // one at double, and reporting NaN would be worse than reporting zero.
396 Matrix<T>* mats[4] = {&out.sol.Q, &out.sol.U, &out.sol.R, &out.sol.Tp};
397 for (Matrix<T>* m : mats)
398 for (std::size_t i = 0; i < m->rows(); ++i)
399 for (std::size_t j = 0; j < m->cols(); ++j)
400 if (!std::isfinite(num_traits<T>::to_double((*m)(i, j)))) (*m)(i, j) = zero;
401 for (T& v : out.sol.C)
402 if (!std::isfinite(num_traits<T>::to_double(v))) v = zero;
403 for (T& v : out.sol.X)
404 if (!std::isfinite(num_traits<T>::to_double(v))) v = zero;
405 return out;
406}
407
408/** The ladder. */
409template <class T>
412 MamOptions opt = opt_in;
413 MamSolution<T> out;
414
415 dispatch_detail::reject_unsupported_nodes(L);
416
417 // -1. the discrete-time (slotted) path, ahead of EVERYTHING. It has to run
418 // before any phase-type conversion: by the time a law reaches `proc` a
419 // Geometric has already been fitted to a CONTINUOUS MAP and the lattice is
420 // no longer visible, so the test reads procid/rates/scv instead.
421 if constexpr (num_traits<T>::has_transcendental) {
423 dtopt.timescale = opt.timescale;
424 dtopt.slotlength = opt.slotlength;
425 double slot = 1.0;
427 if (api::sn_is_discrete_time(L, dtopt, &slot, &dtinfo)) {
428 out = solver_mam_dt(L, opt, slot);
429 out.sol.method = opt.method;
430 return out;
431 }
432 }
433
434 // 0. the exact MAP/MAP/1 fast path, ahead of any method dispatch. A station
435 // that powers down is NOT an M/M/1 whatever its shape looks like: the setup
436 // is extra work the QBD below does not carry, so it must not be taken here.
437 if (L.setupparam.empty()) {
439 if (ex.ok) {
440 out.sol = ex.sol;
441 out.sol.method = opt.method;
442 out.actualmethod = "exact.mapmap1";
443 return out;
444 }
445 }
446
447 const std::string& method = opt.method;
448
449 if (method == "dec.mmap") {
450 out.sol = solver_mam_decmmap(L, opt);
451 out.actualmethod = "dec.mmap";
452 return finish_dispatch(L, out);
453 }
454
455 if (method == "default" || method == "dec.source" || method == "dec.poisson") {
456 if (method == "dec.poisson") opt.space_max = 1;
457 if (method != "dec.poisson") {
458 if constexpr (!num_traits<T>::has_transcendental) {
459 if (dispatch_detail::has_fork(L) || dispatch_detail::has_fork_join(L))
460 throw UnsupportedError(ErrorKind{"Arithmetic"},
461 "SolverMAM: model '" + L.name +
462 "' forks, and every fork-join route here first classifies the branches by "
463 "fitting a phase-type to each, which is transcendental and has no exact "
464 "counterpart; use --arith double or real");
465 } else {
466 // 2a: `fj_is_homogeneous` -- one fork-join pair, K identical open
467 // FCFS/PS branches -- is the ONLY class FJ_codes is defined on, and
468 // most fork-join models fail it, in which case 2b takes them. See
469 // solver_mam_fj.h for why no other FJ approximation in this tree may
470 // be substituted under this method name.
471 // THE PREDICATE IS NOT ARITHMETIC-NEUTRAL. mam_fj_is_homogeneous
472 // compares branches by fitting a PH to each (dist_to_map ->
473 // aph_fit), which static_asserts on transcendental arithmetic, so
474 // calling it unconditionally made the WHOLE MAM dispatch
475 // uninstantiable at Rational -- a model with no Fork at all stopped
476 // compiling. It is therefore reached only from the exact-capable
477 // branch below, and exact arithmetic refuses a forking model BY
478 // NAME rather than skipping the gate and answering it as ordinary.
479 const MamFjInfo fjinfo = mam_fj_is_homogeneous(L);
480 if (fjinfo.ok) {
481 out.sol = solver_mam_fj(L, opt);
482 out.actualmethod = "qiu";
483 return finish_dispatch(L, out);
484 }
485 // 2b: every other OPEN fork-join model, which the reference sends to
486 // the MMAP decomposition.
487 if (dispatch_detail::has_fork_join(L) && L.is_open_model()) {
488 out.sol = solver_mam_basic_mmap(L, opt);
489 out.actualmethod = "dec.source.mmap";
490 return finish_dispatch(L, out);
491 }
492 // DELIBERATE DEVIATION, and the only one in this ladder. A CLOSED
493 // fork-join model passes both reference gates and falls through to
494 // solver_mam_basic, whose dec.source decomposition has no Join
495 // synchronization at all: it zeroes the Join's own metrics and
496 // charges no waiting for the slowest sibling branch, so the response
497 // times it reports are the branches' and not the model's, with
498 // nothing in the output to say so. Refused by name instead.
499 //
500 // SANCTIONED (2026-07-28), not provisional: the refusal is preferred
501 // over bit-parity with MATLAB because the alternative here is a
502 // SILENTLY WRONG answer, not a less accurate one. Do not "restore
503 // parity" by deleting this without reopening that decision.
504 if (dispatch_detail::has_fork(L))
505 throw UnsupportedError(ErrorKind{"Fork"},
506 "SolverMAM: model '" + L.name +
507 "' carries a Fork but is not an open fork-join network, and "
508 "solver_mam_analyzer.m has no branch for it: it falls through to "
509 "solver_mam_basic, whose dec.source decomposition never charges the "
510 "synchronization delay at the Join, so the response times would be those of "
511 "the branches alone. Call method 'dec.source.mmap', which routes the model to "
512 "the MMAP decomposition's closed wrapper and does charge the join, or use "
513 "SolverMVA, SolverNC, SolverJMT or SolverLDES");
514 }
515
516 // 2c: the BMAP/PH/N/N bufferless retrial queue of Dudin et al. A
517 // station is claimed only when it declares an orbit or is
518 // bufferless AND carries a RETRIAL drop rule, exactly as
519 // qsys_is_retrial.m requires; nothing is inferred from capacity
520 // alone, which would claim finite buffers that are not orbits.
521 // The DETECTION is arithmetic-neutral (it reads retrialparam and
522 // the drop rule), but the ANALYZER fits a phase-type to the arrival
523 // and service processes at solver_mam_retrial.h:360, so the orbit
524 // route is transcendental even though qsys_bmapphnn_retrial itself
525 // is exact. Refuse the orbit BY NAME under an exact type rather
526 // than fall through to 2e/2f, which would answer it as an ordinary
527 // waiting line -- the very substitution wiring this branch removed.
528 const MamRetrialInfo retinfo = mam_retrial_detect(L);
529 if (retinfo.ok) {
530 if constexpr (!num_traits<T>::has_transcendental) {
531 throw UnsupportedError(ErrorKind{"Arithmetic"},
532 "SolverMAM: model '" + L.name +
533 "' declares a retrial orbit, whose analyzer fits a phase-type to the "
534 "arrival and service processes and has no exact counterpart; use --arith "
535 "double or real");
536 } else {
537 out.sol = solver_mam_retrial(L, opt);
538 out.actualmethod = "retrial";
539 return finish_dispatch(L, out);
540 }
541 }
542 // An orbit OUTSIDE that shape (a non-exponential retrial delay or
543 // orbit abandonment, a finite attempt cap, several classes...) must
544 // not fall through to 2e/2f either: they read no retrial field and
545 // would answer it as an ordinary waiting line.
546 for (std::size_t i = 1; i <= L.nstations; ++i)
547 if (retrial_detail::has_retrial_proc(L, i))
548 throw UnsupportedError(ErrorKind{"Retrial"},
549 "SolverMAM: model '" + L.name + "' declares a retrial orbit outside the "
550 "BMAP/PH/N/N bufferless shape the retrial analyzer solves (" + retinfo.why +
551 "). Use SolverLDES for any retrial law, or SolverCTMC/SolverSSA for an "
552 "exponential retrial delay with unlimited attempts and no orbit abandonment.");
553 // 2d: reneging (`mam_has_reneging_patience`) reaches the same
554 // analyzer's OTHER route, the MAP/M/s+G fluid queue of
555 // solver_mam_reneging.h. A declared reneging OUTSIDE that shape (or a
556 // non-Markovian patience) must not fall through to 2e/2f, which read
557 // no patience and would answer it as an ordinary waiting line;
558 // check_model_method refuses it ahead of the dispatch with the same
559 // sentence, which is raised again here for a direct caller.
560 if (mam_declares_reneging(L)) {
561 const std::string renWhy = mam_reneging_default_refusal(L);
562 if (!renWhy.empty()) throw UnsupportedError(ErrorKind{"Reneging"}, renWhy);
564 out.actualmethod = "reneging";
565 return finish_dispatch(L, out);
566 }
567 //
568 // 2e: the level-dependent QBD is EXACT for this shape, and the
569 // reference prefers it over dec.source on 'default'. The OPEN
570 // Source+Queue regime of solver_mam_ldqbd is deliberately NOT
571 // claimed here: it truncates at options.cutoff, so it is not
572 // unconditionally better than dec.source and stays opt-in through
573 // method='ldqbd', exactly as the reference's isClosedDelayQueue
574 // comment states.
575 // A closed setup task IS a Delay+Queue tandem by shape, and ldqbd
576 // now models it exactly -- but only at a single server with
577 // exponential service and no load dependence, which is what
578 // qbd_setupdelayoff_closed covers. Outside that it refuses by name,
579 // so the shape test has to exclude those or the default would reach
580 // the refusal instead of falling through to the decomposition.
581 if (method == "default" && dispatch_detail::ldqbd_setup_ok(L) &&
582 dispatch_detail::is_closed_delay_queue(L)) {
583 out.sol = solver_mam_ldqbd(L, opt).sol;
584 out.actualmethod = "ldqbd";
585 return finish_dispatch(L, out);
586 }
587
588 // 2e-bis: a closed model is the degenerate case of the background chain.
589 // With no open work to take a share of the servers the chain is the EXACT
590 // closed CTMC at chain granularity, so it dominates the mna fixed point
591 // wherever its state space fits. bgchain_applies sizes that state space
592 // and bgchain_closed_exact checks the chain is built from a service law it
593 // represents exactly.
594 // bgchain DROPS setup/delay-off: it would answer with the
595 // always-warm chain and nothing would say so. Only solver_mam_basic
596 // and solver_mam_ldqbd read it, so a setup model must not reach here.
597 if (method == "default" && dispatch_detail::is_closed_model(L) &&
598 L.setupparam.empty() &&
599 dispatch_detail::bgchain_applies(L, opt) &&
600 dispatch_detail::bgchain_closed_exact(L)) {
601 out.sol = solver_mam_bgchain(L, opt);
602 out.actualmethod = "bgchain";
603 return finish_dispatch(L, out);
604 }
605
606 // 2f: a closed model has no arrival stream for dec.source to build its
607 // Poisson surrogate from -- it replaces each closed chain by a source at
608 // the current throughput iterate and never enforces the population, so
609 // the answer neither conserves N nor separates the classes. MNA closes
610 // the same traffic equations by bisecting the per-class throughput
611 // against N.
612 // mna DROPS setup/delay-off for the same reason as bgchain.
613 if (method == "default" && dispatch_detail::is_closed_model(L) &&
614 L.setupparam.empty() &&
615 dispatch_detail::mna_applies(L)) {
616 out.sol = solver_mna_closed(L, opt);
617 out.actualmethod = "mna";
618 if (!dispatch_detail::mna_conserves(L, out.sol)) {
619 // The outer bisection did not close on the population: the last
620 // step rescales each chain onto N regardless, so the failure is
621 // invisible in Q alone and only Little's law on the unrescaled R
622 // and Tp still shows it.
623 out.sol = solver_mam_basic(L, opt);
624 out.actualmethod = "dec.source";
625 }
626 return finish_dispatch(L, out);
627 }
628
629 // 2g: mixed. The closed classes are solved exactly as a background chain
630 // and the open ones as QBDs driven by it, which is 4-5 significant
631 // digits against CTMC where dec.source is 10-24% out.
632 // The mixed branch drops setup/delay-off exactly as the closed one
633 // does, so it takes the same guard.
634 if (method == "default" && !L.is_open_model() && L.setupparam.empty() &&
635 !dispatch_detail::is_closed_model(L) && dispatch_detail::bgchain_applies(L, opt)) {
636 out.sol = solver_mam_bgchain(L, opt);
637 out.actualmethod = "bgchain";
638 return finish_dispatch(L, out);
639 }
640 }
641 out.sol = solver_mam_basic(L, opt);
642 out.actualmethod = (method == "default") ? "dec.source" : method;
643 } else if (method == "mna") {
644 // The two analyzers are disjoint by population regime, so unlike the
645 // fork-join and retrial branches this pair carries no order hazard; the
646 // reference's open-then-closed sequence is kept anyway.
647 if (L.is_open_model()) {
648 out.sol = solver_mna_open(L, opt);
649 } else if (dispatch_detail::is_closed_model(L)) {
650 out.sol = solver_mna_closed(L, opt);
651 } else {
652 // The reference's own line_error. `SolverMAM.supportsModelMethod`
653 // already rejects a mixed model one level up, in
654 // runner_detail::check_model_method, so this is reachable only by
655 // calling mam_dispatch directly; it is transcribed rather than
656 // dropped because that caller exists.
657 throw UnsupportedError(ErrorKind{"MixedNetwork"},
658 "SolverMAM: the mna method in SolverMAM does not support mixed models");
659 }
660 out.actualmethod = "mna";
661 } else if (method == "bgchain") {
662 // Mixed networks: the closed classes are a background modulating chain,
663 // the open classes are QBDs driven by it. With several closed chains an
664 // outer iteration tags one chain at a time and aggregates the rest, so
665 // the background chain always carries two classes.
666 out.sol = solver_mam_bgchain(L, opt);
667 out.actualmethod = "bgchain";
668 } else if (method == "ldqbd") {
669 out.sol = solver_mam_ldqbd(L, opt).sol;
670 out.actualmethod = "ldqbd";
671 } else if (method == "inap" || method == "inaprc" || method == "inapinf"
672 || method == "exact") {
673 // The RCAT methods moved to SolverAG. Name them rather than reporting an
674 // unknown method, so a caller carrying an old options.method is told
675 // where they went.
676 throw UnsupportedError(ErrorKind{"Method"},
677 "SolverMAM: the '" + method + "' method moved to SolverAG: RCAT decomposes the "
678 "model into cooperating agents rather than decomposing traffic, and no MAM "
679 "algorithm shares its machinery. Solve it with -s ag, or call "
680 "line::ag::solver_ag directly");
681 } else if (method == "retrial") {
682 // The BMAP/PH/N/N retrial analyzer BY NAME, which branch 2c also
683 // resolves to from 'default' on a retrial topology. The reference
684 // advertises the name (SolverMAM.m) and dispatches it
685 // (solver_mam_analyzer.m 'case retrial'), so it must be reachable here
686 // and must refuse off that topology rather than answer a waiting line.
687 {
688 // The predicate `check_model_method` and `auto_family_refusal` ask,
689 // so the method the report offers and the method that runs are the
690 // same set, with the same sentence when they are not.
691 const std::string retrialWhy = mam_retrial_refusal(L);
692 if (!retrialWhy.empty()) throw UnsupportedError(ErrorKind{"Retrial"}, retrialWhy);
693 if constexpr (!num_traits<T>::has_transcendental) {
694 throw UnsupportedError(ErrorKind{"Arithmetic"},
695 "SolverMAM: the retrial analyzer fits a phase-type to the arrival and "
696 "service processes and has no exact counterpart; use --arith double or real");
697 } else {
698 out.sol = solver_mam_retrial(L, opt);
699 out.actualmethod = "retrial";
700 }
701 }
702 } else if (method == "sync.faster" || method == "sync.slower") {
703 // Synchronous approximation of an M/G/c/K station with dual bounds; the
704 // shape is mam_sync_refusal's, which the gate and solver_mam_sync ask.
705 const std::string syncWhy = mam_sync_refusal(L, opt.sync_phases);
706 if (!syncWhy.empty()) throw UnsupportedError(ErrorKind{"Structure"}, "SolverMAM: " + syncWhy);
707 if constexpr (!num_traits<T>::has_transcendental) {
708 throw UnsupportedError(ErrorKind{"Arithmetic"},
709 "SolverMAM: the sync methods uniformize the service law, whose coefficients need "
710 "exp; use --arith double or real");
711 } else {
712 out.sol = solver_mam_sync(L, opt);
713 out.actualmethod = method;
714 }
715 } else if (method == "dec.source.mmap") {
716 // solver_mam_basic_mmap.m: the inner MMAP decomposition for an open
717 // model, the throughput bisection around it for a closed one. Unlike
718 // branch 2b this is reachable BY NAME on a model with no fork at all,
719 // which is what the reference's own dispatcher allows.
720 out.sol = solver_mam_basic_mmap(L, opt);
721 out.actualmethod = "dec.source.mmap";
722 } else {
723 throw UnsupportedError(ErrorKind{"Method"}, "SolverMAM: unknown method '" + method + "'");
724 }
725
726 return finish_dispatch(L, out);
727}
728
729} // namespace mam
730} // namespace line
731
732#endif // LINE_SOLVERS_MAM_MAM_DISPATCH_H
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
std::map< std::size_t, SetupDelayOffParam< T > > setupparam
Setup / delay-off, keyed by 1-based STATION index.
std::vector< std::vector< bool > > disabled
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 ...
bool is_open_model() const
sn_is_open_model: EVERY class is open, which is not has_open_classes.
The exception types the port throws.
The option and result types SolverMAM shares with its analyzers.
bool sn_has_load_dependence(const qn::NetworkStruct< T > &sn)
size(sn.lldscaling,2)>0: some station carries a limited-load scaling row.
bool sn_is_discrete_time(const qn::NetworkStruct< T > &sn, const DiscreteTimeOptions &options, double *slot_length, DiscreteTimeInfo *info)
True when every law of sn is lattice-valued on slot_length.
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
@ BMMAPT
BMMAPT crosses the BATCH axis with the two above: a block is indexed by segment, mark and batch size,...
Definition lang_types.h:579
mva::MvaSolution< T > solver_mna_open(const qn::NetworkStruct< T > &L, const MamOptions &opt, const MnaConfig &cfg=MnaConfig())
Port of solver_mna_open.m.
Definition solver_mna.h:379
MamSolution< T > & finish_dispatch(const qn::NetworkStruct< T > &L, MamSolution< T > &out)
What solver_mam_analyzer.m does AFTER whichever analyzer ran: pin the throughput of every EXT (Source...
double bgchain_states(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Number of states of the background-chain CTMC solver_mam_bgchain would build on this model,...
mva::MvaSolution< T > solver_mam_bgchain(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_bgchain.m.
MamSolution< T > mam_dispatch(const qn::NetworkStruct< T > &L, const MamOptions &opt_in)
The ladder.
bool mam_declares_reneging(const qn::NetworkStruct< T > &L)
True when some station-class pair declares RENEGING, with or without a patience law this analyzer can...
mva::MvaSolution< T > solver_mam_basic_mmap(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_basic_mmap.m, the top-level dispatcher of the MMAP fork-join decomposition: an ope...
MamRenegingInfo mam_reneging_detect(const qn::NetworkStruct< T > &L)
Port of mam_reneging_applicable.m: is this the MAP/M/s+G shape?
mva::MvaSolution< T > solver_mam_fj(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::vector< std::vector< T > > *percentiles_out)
Port of solver_mam_fj.m.
mva::MvaSolution< T > solver_mam_sync(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_sync.m: fills Q, U, R, T, C, X from qsys_mgck_sync.
LdqbdSolution< T > solver_mam_ldqbd(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_ldqbd.m.
mva::MvaSolution< T > solver_mam_basic(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_basic.m.
MamFjInfo mam_fj_is_homogeneous(const qn::NetworkStruct< T > &L)
Port of fj_is_homogeneous.m.
MamSolution< T > solver_mam_dt(const qn::NetworkStruct< T > &sn, const MamOptions &opt, double slot_length)
Discrete-time analysis of sn, returning the same metric tuple as every other MAM analyzer.
mva::MvaSolution< T > solver_mna_closed(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mna_closed.m.
Definition solver_mna.h:656
MamRetrialInfo mam_retrial_detect(const qn::NetworkStruct< T > &L)
Port of qsys_is_retrial.m, plus the reneging gate of solver_mam_retrial.m.
MapMap1Exact< T > solver_mam_mapmap1_exact(const qn::NetworkStruct< T > &L)
std::string mam_sync_refusal(const qn::NetworkStruct< T > &L, std::size_t nph, MamSyncParams< T > *par=nullptr)
mam_sync_applicable.m: why the sync methods cannot answer this model, or "" when they can,...
mva::MvaSolution< T > solver_mam_decmmap(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam.m.
std::string mam_reneging_default_refusal(const qn::NetworkStruct< T > &L)
The sentence supportsModelMethod('default') refuses a declared reneging with when the model is not th...
mva::MvaSolution< T > solver_mam_retrial(const qn::NetworkStruct< T > &L, const MamOptions &opt, const MamRetrialConfig &cfg=MamRetrialConfig())
Port of solver_mam_retrial.m.
mva::MvaSolution< T > solver_mam_reneging(const qn::NetworkStruct< T > &L, const MamOptions &opt, const MamRenegingInfo &info)
The reneging route of solver_mam_retrial.m.
std::string mam_retrial_refusal(const qn::NetworkStruct< T > &L)
The 'retrial' method's applicability as one sentence; empty when applicable.
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Port of matlab/src/api/sn/sn_is_discrete_time.m.
Ports of the sn_has_* / sn_is_* predicate family of matlab/src/api/sn.
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....
Port of solver_mam_bgchain.m and its three helpers: the analyzer that treats the CLOSED classes as a ...
Port of solver_mam.m, the dec.mmap method: the per-class departure-process decomposition.
Port of solver_mam_dt.m: discrete-time (slotted) analysis of an open network whose interarrival and s...
Port of solver_mam_fj.m, the fork-join route of SolverMAM.
Port of solver_mam_ldqbd.m: the level-dependent QBD analyzer for a single-class network of one infini...
Port of solver_mam_mapmap1_exact.m: the exact fast path SolverMAM tries BEFORE anything else,...
Port of solver_mam_retrial.m, the customer-impatience analyzer of SolverMAM.
The 'sync.faster' and 'sync.slower' methods of SolverMAM: the synchronous approximation of an M/G/c/K...
Port of solver_mna_open.m and solver_mna_closed.m, the two analyzers behind SolverMAM's mna method.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Which laws the model carries, and why it was refused when it was.
How the caller may override the automatic decision.
std::string timescale
"auto", "discrete" or "continuous".
double slotlength
Slot length in model time units.
What fj_is_homogeneous.m returns: the fork-join pair, or why there is none.
The options SolverMAM reads.
Definition mam_types.h:30
What qsys_is_retrial.m returns: the station it found, or why it found none.
std::string why
the reference's errorMsg, empty when ok
What the MAM dispatch returns: the metrics plus the algorithm that ran.
Definition mam_types.h:168
std::string actualmethod
The concrete algorithm, as the reference's actualmethod.
Definition mam_types.h:171
mva::MvaSolution< T > sol
Definition mam_types.h:169
Result of the fast path; ok false means the model is not in its regime.