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