LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_runner.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_RUNNER_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_RUNNER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The SolverMAM class surface: `@@SolverMAM/runAnalyzer.m` and the gates around
12 * it.
13 *
14 * Everything that happens BEFORE and AFTER one inner solve lives here: the
15 * method whitelist (`SolverMAM.listValidMethods`), the per-method structural
16 * gates (`SolverMAM.supportsModelMethod`), the process gate, and the metric
17 * filter. `runAnalyzer`'s Finite Capacity Region rejection needs no counterpart:
18 * the C++ `NetworkStruct` has no region field, so such a model cannot be built.
19 *
20 * THE METRIC FILTER IS SHARED WITH SolverMVA AND SolverNC VERBATIM
21 * (`filter_metric`, `sn_get_residt_from_respt`, `sn_get_arvr_from_tput` in
22 * `solver_mva_runner.h`), because `@@NetworkSolver/getAvg` is
23 * solver-independent: it is literally the same code path for all three in the
24 * reference.
25 *
26 * NON-MARKOVIAN SERVICE IS CONVERTED, NOT REFUSED (since 2026-08-01).
27 * `solver_mam_analyzer.m:40` runs `sn_nonmarkov_toph` on its own copy of the
28 * struct, and so does `solver_mam_solve` here: a Gamma, Weibull, Lognormal,
29 * Pareto or Uniform is replaced by the two-moment concentrated-ME fit, or by the
30 * 20-phase Bernstein density fit under `phfit_cme = false`, and a Det by a
31 * 20-phase Erlang when `preserve_det` is off. The conversion runs AFTER the
32 * slotted test, which would otherwise see a continuous fit where a Geometric was
33 * declared. What survives the conversion and still reaches `check_processes` is
34 * NHPP / MAPt / PHt, whose content is a schedule that no homogeneous surrogate
35 * can carry, and those are refused by name.
36 *
37 * TWO GATES REMAIN STRICTER HERE THAN IN THE REFERENCE, and both are the honest
38 * outcome rather than a limitation:
39 *
40 * - A CLASS-SWITCHING CHAIN AT AN FCFS STATION. The reference's own chain-1
41 * mark collapse makes the marking handed to the queue solver disagree with
42 * the class count, and MATLAB then fails on a dimension mismatch. The
43 * analyzer refuses by name with both counts; see solver_mam_basic.h.
44 * - LOAD DEPENDENCE UNDER 'default' OUTSIDE THE LD-QBD SHAPE. MATLAB has no
45 * counterpart because it has no LoadDependence declaration for MAM at all
46 * (mam_feature_set's declaration is a C++-only widening, solver_feature_sets.h).
47 * `check_model_method` refuses a load-dependent model here when its shape is
48 * not the one `solver_mam_ldqbd` requires, because the featset gate cannot
49 * see topology and 'default' would otherwise fall through to
50 * `solver_mam_basic`, which never reads `st.lldscaling`.
51 */
52
53#include <algorithm>
54#include <cmath>
55#include <string>
56#include <vector>
57
70#include "line/util/error.h"
72
73namespace line {
74namespace mam {
75
76using lang::GlobalConstants;
77
78/**
79 * Port of `SolverMAM.listValidMethods`.
80 *
81 * The reference's list verbatim, in its order (the comment there records that
82 * test files index into it, so nothing may be inserted in the middle). A listed
83 * name that has no ported analyzer passes this gate and is then refused BY NAME
84 * by the dispatch, which is the honest outcome and different from silently
85 * solving a different model.
86 *
87 * `exact` at the end is the RCAT ALIAS -- `mam_dispatch.h` groups it with
88 * `inap`/`inaprc`/`inapinf`, exactly as `solver_mam_analyzer.m` does -- and
89 * NOT the retired autocat of `solver_mam_autocat.h`, whose own refusal message
90 * predates the reference re-advertising the name. `retrial` names the
91 * BMAP/PH/N/N analyzer that `default` also resolves to on a retrial topology.
92 */
93inline std::vector<std::string> list_valid_methods() {
94 // the names and their order are this solver's row of the method registry (doc/line-methods.json)
95 return ::line::method_registry::methods("MAM");
96}
97
98/**
99 * True for the four RCAT names that moved to SolverAG, with the redirect message.
100 *
101 * It lives here, and is called from `check_method` BEFORE the unlisted-method
102 * throw, because that ordering is the whole point: the names are no longer in
103 * `list_valid_methods`, so a caller carrying an old `options.method` would
104 * otherwise be told only that the method is "unsupported by this solver" and
105 * left to guess. `check_model_method` calls the same helper so the two paths
106 * cannot drift into two different messages.
107 */
108inline bool rcat_moved_to_ag(const std::string& method, std::string& why) {
109 if (method != "inap" && method != "inaprc" && method != "inapinf"
110 && method != "exact") {
111 return false;
112 }
113 why = "SolverMAM: the '" + method + "' method moved to SolverAG: RCAT decomposes the "
114 "model into cooperating agents rather than decomposing traffic, and no MAM "
115 "algorithm shares its machinery. Solve it with -s ag";
116 return true;
117}
118
119/** An unlisted method is refused; one that MOVED is redirected by name. */
120inline void check_method(const std::string& method) {
121 const std::vector<std::string> valid = list_valid_methods();
122 if (std::find(valid.begin(), valid.end(), method) != valid.end()) return;
123 std::string why;
124 if (rcat_moved_to_ag(method, why)) throw UnsupportedError(ErrorKind{"Method"}, why);
125 throw UnsupportedError(ErrorKind{"Method"}, "SolverMAM: the '" + method + "' method is unsupported by this solver");
126}
127
128namespace runner_detail {
129
130/**
131 * The distributions `getFeatureSet` lists, plus the Markovian ones.
132 *
133 * Runs AFTER `sn_nonmarkov_toph`, so the families that have a Markovian
134 * surrogate are already gone; what can still arrive is a schedule family, whose
135 * whole content is the time dependence a single generator cannot carry.
136 *
137 * `is_discrete` is set when sn_is_discrete_time claimed the model: the lattice
138 * laws are then solved on their own time scale by solver_mam_dt and must not be
139 * refused for lacking a continuous phase-type fit, which is the only reason the
140 * refusal below exists.
141 */
142template <class T>
143void check_processes(const qn::NetworkStruct<T>& L, bool is_discrete) {
144 using lang::ProcessType;
145 if (is_discrete) return;
146 for (std::size_t i = 0; i < L.nstations; ++i)
147 for (std::size_t r = 0; r < L.nclasses; ++r) {
148 if (L.disabled[i][r]) continue;
149 const ProcessType p = L.service[i][r].type;
150 if (basic_detail::is_markovian_type(p) || p == ProcessType::DET) continue;
151 throw UnsupportedError(ErrorKind{"Distribution"},
152 std::string("SolverMAM: station '") + L.stations[i].name + "' class '" +
153 L.classes[r].name + "' uses a " + lang::process_to_text(p) +
154 " service process, which sn_nonmarkov_toph has no Markovian surrogate for: its "
155 "content is a piecewise-constant SCHEDULE, and collapsing it to one homogeneous "
156 "generator would answer for a stationary model the caller did not describe. Solve "
157 "it with SolverFLD or SolverLDES, which integrate the schedule");
158 }
159}
160
161/**
162 * True when any station declares a non-trivial load-dependent scaling, the
163 * same test `used_lang_features` (feature_set.h) applies to derive
164 * Feature::LoadDependence.
165 */
166template <class T>
167bool has_load_dependence(const qn::NetworkStruct<T>& L) {
168 for (const qn::Station<T>& st : L.stations)
169 if (!st.lldscaling.empty()) return true;
170 return false;
171}
172
173/** True when the model declares at least one G-network signal class. */
174template <class T>
175bool has_signal_class(const qn::NetworkStruct<T>& L) {
176 for (std::size_t r = 0; r < L.issignal.size(); ++r)
177 if (L.issignal[r]) return true;
178 return false;
179}
180
181/**
182 * `mam_ldqbd_applicable.m`: why the 'ldqbd' method cannot answer this model,
183 * or "" when it can. THE RULE IS A SHAPE: one class, and either one Source plus
184 * one FCFS Queue fed by a Poisson stream (open) or one Delay plus one FCFS Queue
185 * with a positive population (closed). Checking only the class count let the
186 * AUTO report offer mam.ldqbd on a closed FCFS tandem (BUG-105).
187 */
188template <class T>
189std::string mam_ldqbd_refusal(const qn::NetworkStruct<T>& L) {
191 if (L.nclasses != 1) return "SolverMAM: the ldqbd method requires a single-class model";
192 std::size_t nDelay = 0, nQueue = 0, nSource = 0, src = 0;
193 for (std::size_t i = 0; i < L.nstations; ++i) {
194 const SchedStrategy sc = L.stations[i].sched;
195 if (sc == SchedStrategy::INF) ++nDelay;
196 else if (sc == SchedStrategy::FCFS) ++nQueue;
197 else if (sc == SchedStrategy::EXT) {
198 if (nSource == 0) src = i + 1;
199 ++nSource;
200 }
201 }
202 const double pop = L.classes[0].population;
203 if (!std::isfinite(pop)) {
204 if (nSource != 1 || nQueue != 1 || L.nstations != 2)
205 return "SolverMAM: open LDQBD method requires exactly one Source and one Queue station";
206 if (L.procid(src, 1) != lang::ProcessType::EXP || L.phases_of(src, 1) > 1)
207 return "SolverMAM: open LDQBD method currently supports Poisson (exponential) arrivals "
208 "only; the Source uses a MAP/MMPP process";
209 return std::string();
210 }
211 if (!(pop > 0))
212 return "SolverMAM: closed LDQBD method requires a positive population";
213 if (nDelay != 1 || nQueue != 1 || L.nstations != 2)
214 return "SolverMAM: closed LDQBD method requires exactly one Delay and one Queue station";
215 return std::string();
216}
217
218/** `SolverMAM.supportsModelMethod`, the per-method structural gates. */
219template <class T>
220void check_model_method(const qn::NetworkStruct<T>& L, const std::string& method,
221 bool transient = false, std::size_t sync_phases = 0) {
222 using lang::ProcessType;
223 // The RCAT methods moved to SolverAG. `check_method` normally reports this
224 // first; the same helper is used here so a direct caller of
225 // check_model_method (the twin of SolverMAM.supportsModelMethod, which the
226 // reference also answers false for) gets the identical message.
227 {
228 std::string why;
229 if (rcat_moved_to_ag(method, why)) throw UnsupportedError(ErrorKind{"Method"}, why);
230 }
231 // G-network signals belong to the RCAT analyzer alone: no MAM algorithm
232 // reads issignal, so every one of them would solve the model with the
233 // signals turned into ordinary customers and report that as the answer.
234 if (has_signal_class(L)) {
235 throw UnsupportedError(
236 "SolverMAM: the " + method + " method does not support G-network signals: no MAM "
237 "algorithm reads issignal, so this method would solve the model with every signal "
238 "turned into an ordinary customer. Use SolverAG (-s ag), whose RCAT methods are "
239 "the only ones that model signals");
240 }
241 if (method == "mna") {
242 bool open = false, closed = false;
243 for (const qn::JobClass& c : L.classes) {
244 if (std::isinf(c.population)) open = true;
245 else closed = true;
246 }
247 if (open && closed)
248 throw UnsupportedError(ErrorKind{"MixedNetwork"},
249 "SolverMAM: the mna method does not support mixed open/closed models");
250 // mam_feature_set declares RoutingStrategy_RROBIN for 'mna' because
251 // solver_mna_open resolves the deterministic split through
252 // npfqn_traffic_split_rr. solver_mna_closed has no counterpart -- the
253 // reference corrects the open traffic equations only -- so a closed
254 // round-robin model would be solved as if the dispatcher were random.
255 if (!open) {
256 for (const qn::NodeDef& nd : L.nodes)
257 for (qn::RoutingStrategy rs : nd.routing)
258 if (rs == qn::RoutingStrategy::RROBIN)
259 throw UnsupportedError(ErrorKind{"RoutingStrategy_RROBIN"},
260 "SolverMAM: the mna method supports round-robin routing in open "
261 "models only");
262 }
263 } else if (method == "default") {
264 // mam_feature_set declares LoadDependence for 'default' because the
265 // single-class closed Delay+Queue shape routes here to solver_mam_ldqbd
266 // (mam_dispatch.h branch 2e), which reads st.lldscaling. The featset
267 // cannot see topology, so a load-dependent model outside that shape --
268 // or one carrying a setup/delay-off pair, which steals the same shape
269 // ahead of ldqbd -- must be refused here rather than falling through
270 // to solver_mam_basic, which never reads the field and would solve
271 // every level at the nominal rate.
272 if (has_load_dependence(L) &&
273 !(L.setupparam.empty() && dispatch_detail::is_closed_delay_queue(L)))
274 throw UnsupportedError(ErrorKind{"LoadDependence"},
275 "SolverMAM: this model uses load-dependent service rates outside the "
276 "single-class closed Delay+Queue shape that solver_mam_analyzer.m routes to the "
277 "level-dependent QBD (solver_mam_ldqbd); the 'default' method would otherwise "
278 "fall through to solver_mam_basic's dec.source decomposition, which does not "
279 "read the load-dependent scaling and would solve every level at the nominal "
280 "rate. Call method 'ldqbd' directly, which refuses by name if the shape still "
281 "does not fit, or restructure the model to the required shape");
282 // mam_feature_set declares Reneging for 'default' because the MAP/M/s+G
283 // shape routes to solver_mam_reneging (mam_dispatch.h branch 2d); any
284 // other reneging model would fall through to dec.source, which reads no
285 // patience. Same predicate as the dispatch.
286 const std::string renWhy = mam_reneging_default_refusal(L);
287 if (!renWhy.empty()) throw UnsupportedError(ErrorKind{"Reneging"}, renWhy);
288 } else if (method == "ldqbd") {
289 // As in SolverMAM.m, a transient run the transient QBD can take is exempt:
290 // that engine is not bound to the steady-state LD-QBD shape.
291 if (!(transient && mam_transient_qbd_applicable(L))) {
292 const std::string ldWhy = mam_ldqbd_refusal(L);
293 if (!ldWhy.empty()) throw UnsupportedError(ErrorKind{"LoadDependence"}, ldWhy);
294 }
295 } else if (method == "retrial") {
296 // A "MUST BE PRESENT" RULE, which is why it cannot live in a feature
297 // set: a FeatureSet says "I accept this construct", so it can refuse a
298 // model for HAVING something and never for LACKING it.
299 // solver_mam_retrial needs the BMAP/PH/N/N bufferless retrial topology
300 // of Dudin et al. to analyze, and a model without one is not a smaller
301 // retrial model, it is a different one. mam_retrial_refusal is the same
302 // predicate mam_dispatch's 'retrial' arm and auto_family_refusal ask,
303 // and it reports WHICH requirement the model missed.
304 const std::string retrialWhy = mam_retrial_refusal(L);
305 if (!retrialWhy.empty()) throw UnsupportedError(ErrorKind{"Retrial"}, retrialWhy);
306 } else if (method == "sync.faster" || method == "sync.slower") {
307 // One M/G/c/K station of a deterministic or phase-type law: the
308 // predicate the dispatch arm and solver_mam_sync also ask.
309 const std::string syncWhy = mam_sync_refusal(L, sync_phases);
310 if (!syncWhy.empty())
311 throw UnsupportedError(ErrorKind{"Structure"}, "SolverMAM: " + syncWhy);
312 } else if (method == "bgchain") {
313 // The closed classes ARE the background chain, so a purely open model has
314 // nothing to build it from. A purely CLOSED one is accepted: it is the
315 // degenerate case where the chain answers alone, with no open work to
316 // modulate it.
317 bool hasClosed = false;
318 for (std::size_t k = 0; k < L.nclasses; ++k) {
319 if (!std::isinf(L.classes[k].population)) hasClosed = true;
320 }
321 if (!hasClosed)
322 throw UnsupportedError(ErrorKind{"OpenClass"},
323 "SolverMAM: the bgchain method requires at least one closed class: the background "
324 "chain IS the closed population vector, which a purely open model does not have. "
325 "Use the dec.source method");
326 // Priority disciplines need the per-class QBD of MMAPPH1PRPR, which has
327 // no counterpart in the modulated level-dependent QBD this method builds.
328 bool prioSched = false;
329 for (std::size_t i = 0; i < L.nstations; ++i) {
330 const lang::SchedStrategy sc = L.stations[i].sched;
332 prioSched = true;
333 }
334 if (prioSched && L.has_distinct_priorities())
335 throw UnsupportedError(ErrorKind{"Priority"},
336 "SolverMAM: the bgchain method does not support class priorities: it aggregates "
337 "the open classes into one phase-type mixture per station, which cannot express a "
338 "priority order. Use the dec.source method");
339 if (L.has_fork())
340 throw UnsupportedError(ErrorKind{"Fork"},
341 "SolverMAM: the bgchain method does not support fork-join: the background chain "
342 "conserves the closed population per station, which a fork violates. Use the "
343 "dec.source method");
344 }
345}
346
347} // namespace runner_detail
348
349/**
350 * `check_model_method` asked WITHOUT raising: the same verdict as a sentence.
351 *
352 * `autosolver::auto_family_refusal` needs a reason rather than an exception --
353 * the report answers yes or no per (family, method) pair and prints why -- and
354 * asking the gate itself is what keeps the offered pairs and the runnable ones
355 * the same set. Duplicating the rules there is how a method comes to be offered
356 * on a model its own runner refuses, which is exactly what happened to
357 * 'dec.mmap' and 'retrial'.
358 */
359template <class T>
360std::string mam_model_method_refusal(const qn::NetworkStruct<T>& L, const std::string& method) {
361 try {
362 runner_detail::check_model_method(L, method);
363 } catch (const std::exception& e) {
364 return std::string(e.what());
365 }
366 return std::string();
367}
368
369/**
370 * The gates and the dispatch of `@@SolverMAM/runAnalyzer.m`, without the metric
371 * filter.
372 */
373template <class T>
375 check_method(opt.method);
376 runner_detail::check_model_method(L, opt.method, false, opt.sync_phases);
377 // Whether the slotted path will claim the model, which decides both the
378 // process gate below and branch -1 of the dispatch. Asking here keeps the
379 // two answers from diverging.
380 bool is_discrete = false;
381 if constexpr (num_traits<T>::has_transcendental) {
383 dtopt.timescale = opt.timescale;
384 dtopt.slotlength = opt.slotlength;
385 double slot = 1.0;
387 is_discrete = api::sn_is_discrete_time(L, dtopt, &slot, &dtinfo);
388 }
389
390 // `solver_mam_analyzer.m:40` runs sn_nonmarkov_toph on its own copy, AFTER
391 // the slotted test (which would otherwise see a continuous CME where a
392 // Geometric was declared) and with preserveDet on for every method but
393 // 'bgchain', so the exact MAP/D/c branch still sees its Det while bgchain,
394 // which has no deterministic-service branch, gets the Det fitted to PH.
395 // That resolution lives in MamOptions::preserve_det_resolved. The struct is
396 // copied only when something in it would actually be replaced.
397 // The RCAT methods build a CTMC per component out of (D0,D1), so a preserved
398 // Det would reach them with no matrix at all and be read back as its mean
399 // rate, and a concentrated matrix exponential is not a generator at all.
400 // They need a genuine phase-type, as SSA, Fluid and JMT do.
401 const bool preserve_det = opt.preserve_det_resolved();
402 qn::NetworkStruct<T> converted;
403 const qn::NetworkStruct<T>* Lp = &L;
404 if constexpr (num_traits<T>::has_transcendental) {
405 if (!is_discrete && api::nonmkv_enabled(opt.nonmkv) && api::sn_has_nonmarkov(L, preserve_det)) {
406 converted = L;
408 no.order = opt.nonmkv_order;
409 no.phfit = opt.phfit_cme ? api::PhFit::Cme : api::PhFit::Ph;
410 no.preserve_det = preserve_det;
411 api::sn_nonmarkov_toph(converted, no);
412 Lp = &converted;
413 }
414 }
415 const qn::NetworkStruct<T>& Lc = *Lp;
416
417 runner_detail::check_processes(Lc, is_discrete);
418 // runAnalyzerChecks' universal feature gate, AFTER the three structural
419 // checks so their specific messages (map_bernstein, single-class ldqbd,
420 // single-server inap, and the load-dependence shape check just above)
421 // still win. The declared set carries LoadDependence over MATLAB's for
422 // 'default' and 'ldqbd' only, without which every ldqbd model would
423 // refuse here; every other method leaves it undeclared, so a
424 // load-dependent model under 'dec.source', 'dec.poisson' or 'mna' is
425 // refused cleanly by the gate instead of silently solved at nominal rates.
426 qn::feature_gate("SolverMAM", qn::mam_feature_set(opt.method), Lc);
427 return mam_dispatch(Lc, opt);
428}
429
430/**
431 * Port of `@@SolverMAM/runAnalyzer.m` for the `lang='matlab'` path: solve, then
432 * apply the metric filter `@@NetworkSolver/getAvg` puts between the analyzer and
433 * the caller.
434 */
435template <class T>
437 const std::string origmethod = opt.method;
438 const MamSolution<T> d = solver_mam_solve(L, opt);
439 const mva::MvaSolution<T>& s = d.sol;
440
441 const std::size_t M = L.nstations, K = L.nclasses;
442 std::vector<std::vector<bool>> mask(M, std::vector<bool>(K, false));
443 for (std::size_t i = 0; i < M; ++i)
444 for (std::size_t k = 0; k < K; ++k)
445 mask[i][k] = num_traits<T>::to_double(s.R(i, k)) < 10.0 * GlobalConstants::FineTol;
446 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
447 for (std::size_t i = 0; i < M; ++i)
448 if (L.stations[i].nodetype == qn::NodeType::Source)
449 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
450
452 out.QN = mva::filter_metric(L, s.Q, mva::MetricKind::QLen, &mask);
453 out.UN = mva::filter_metric(L, s.U, mva::MetricKind::Util, &mask);
454 out.RN = mva::filter_metric(L, s.R, mva::MetricKind::RespT, nullptr);
455 out.TN = mva::filter_metric(L, s.Tp, mva::MetricKind::Tput, nullptr);
457 mva::MetricKind::ResidT, nullptr);
459 &srcmask);
460 // The saturated open-station rule of getAvg.m, applied AFTER out.WN so the
461 // residence times keep their pre-saturation value. Without it SolverMAM
462 // reported the `Uden = min(1-FineTol, sum(UN))` clamp's own artefact --
463 // `QLen 9.99e7, RespT 5.33e8` -- where MATLAB reports Inf. The clamp is in
464 // the reference too; what was missing is this layer above it.
465 if (mva::cap_unstable_open_stations(L, out.QN, out.UN, out.RN, out.TN))
466 out.warning = out.warning.empty()
468 : out.warning + " " + mva::unstable_open_warning();
469 out.CN = s.C;
470 out.XN = s.X;
471 out.method = origmethod;
472 // `runAnalyzer` reports 'default/<algorithm>' when the caller asked for the
473 // default, so the algorithm that produced the numbers is never lost.
474 out.actualmethod = (origmethod == "default" && !d.actualmethod.empty())
475 ? "default/" + d.actualmethod
476 : d.actualmethod;
477 out.iter = s.iter;
478 return out;
479}
480
481// ---------------------------------------------------------------------------
482// The rest of the @@SolverMAM class surface.
483//
484// Each of these mirrors one accessor and does its own gating; they take the
485// converged `AvgResult` where the reference calls `self.getAvg()` first, so a
486// caller cannot reach them on an unsolved model by accident.
487// ---------------------------------------------------------------------------
488
489/** `@@SolverMAM/getProb.m`: the joint (level, phase) table at a node. */
490template <class T>
492 std::size_t node, const mva::AvgResult<T>& avg) {
493 return solver_mam_getprob(L, opt, node, avg);
494}
495
496/** `@@SolverMAM/getProbMarg.m`: P(n jobs of one class) at a station. */
497template <class T>
499 std::size_t ist, std::size_t jobclass,
500 const mva::AvgResult<T>& avg) {
501 return solver_mam_getprobmarg(L, opt, ist, jobclass, avg);
502}
503
504/** `@@SolverMAM/getMAMResult.m`: the M/G/1-type internals of a single queue. */
505template <class T>
509
510/**
511 * `@@SolverMAM/getCdfRespT.m`: the response-time CDF per class.
512 *
513 * `getSjrnT` and `sjrnT` are aliases of this in the reference and are not given
514 * separate entry points here; there is nothing for them to do that this does
515 * not already do, and a second name for one function is a maintenance cost
516 * rather than a feature.
517 */
518template <class T>
519std::vector<RespTCdf<T>> solver_mam_get_cdf_respt(const qn::NetworkStruct<T>& L,
520 const MamOptions& opt) {
521 check_method(opt.method);
522 runner_detail::check_model_method(L, opt.method);
523 // The same non-Markovian conversion the steady-state entry runs. NOT
524 // exempted for a slotted model: solver_mam_passage_time reads the CONTINUOUS
525 // phase-type fit, so a lattice law has to be refused here even though the
526 // steady-state entry solves it on its own time scale.
527 const bool preserve_det = opt.preserve_det_resolved();
528 qn::NetworkStruct<T> converted;
529 const qn::NetworkStruct<T>* Lp = &L;
530 if constexpr (num_traits<T>::has_transcendental) {
531 if (api::nonmkv_enabled(opt.nonmkv) && api::sn_has_nonmarkov(L, preserve_det)) {
532 converted = L;
534 no.order = opt.nonmkv_order;
535 no.phfit = opt.phfit_cme ? api::PhFit::Cme : api::PhFit::Ph;
536 no.preserve_det = preserve_det;
537 api::sn_nonmarkov_toph(converted, no);
538 Lp = &converted;
539 }
540 }
541 runner_detail::check_processes(*Lp, false);
542 return solver_mam_passage_time(*Lp, opt);
543}
544
545/** `@@SolverMAM/getSjrnT.m` and `sjrnT.m`, both aliases of getCdfRespT. */
546template <class T>
547std::vector<RespTCdf<T>> solver_mam_get_sjrn_t(const qn::NetworkStruct<T>& L,
548 const MamOptions& opt) {
549 return solver_mam_get_cdf_respt(L, opt);
550}
551
552/**
553 * Whether `getPerctRespT` reads the FJ_codes table rather than inverting a CDF.
554 *
555 * The reference decides by whether the last solve left a `result.Percentile`
556 * behind, which happens exactly when the analyzer took branch 2a. That is a
557 * state test on a stateless interface here, so the condition is recomputed from
558 * the model. Exposed rather than inlined because the CLI needs the SAME answer:
559 * a fork-join model has no response-time CDF at all, so a caller that asked for
560 * one must be told which of the two it is getting.
561 *
562 * Compiled out below double: `mam_fj_is_homogeneous` fits a phase-type per
563 * branch and is not instantiable in an exact field.
564 */
565template <class T>
567 if constexpr (!num_traits<T>::has_transcendental) {
568 (void)L;
569 (void)opt;
570 return false;
571 } else {
572 // Branch 2a is reachable from 'default' and 'dec.source' only; any other
573 // method names a different analyzer, which stores no percentile table.
574 if (opt.method != "default" && opt.method != "dec.source") return false;
575 if (L.fj.empty()) return false;
576 return mam_fj_is_homogeneous(L).ok;
577 }
578}
579
580/**
581 * `@@SolverMAM/getPerctRespT.m`: response-time percentiles per class.
582 *
583 * TWO PATHS, split by `mam_has_fj_percentiles`: a homogeneous fork-join model
584 * reads the table FJ_codes stored, and everything else inverts the
585 * response-time CDF. Any other fork-join model is refused by name.
586 */
587template <class T>
588std::vector<std::vector<T>> solver_mam_get_perct_respt(const qn::NetworkStruct<T>& L,
589 const MamOptions& opt,
590 const std::vector<double>& percentiles) {
592 return solver_mam_fj_percentiles(L, opt, percentiles);
593 // A fork-join off the homogeneous FJ_codes topology was solved by dec.source.mmap, which yields means only, and
594 // the passage-time CDF has no fork-join arm: name the reason, as getPerctRespT.m does, instead of inverting it.
595 if (L.has_fork()) {
596 std::string why = "the model is solved in an exact field, where the FJ_codes phase-type fit is unavailable.";
597 if constexpr (num_traits<T>::has_transcendental) {
598 if (opt.method != "default" && opt.method != "dec.source")
599 why = "method '" + opt.method + "' stores no fork-join percentile table.";
600 else
601 why = mam_fj_is_homogeneous(L).why;
602 }
603 throw UnsupportedError(ErrorKind{"Fork"},
604 "SolverMAM computes fork-join response-time percentiles only on the Source->Fork->K identical "
605 "queues->Join->Sink topology (Qiu-Perez-Harrison FJ_codes): " + why +
606 " Use fj::fj_tail_forktail for the topology-driven approximation.");
607 }
608 const std::vector<RespTCdf<T>> rd = solver_mam_get_cdf_respt(L, opt);
609 std::vector<std::vector<T>> out;
610 out.reserve(rd.size());
611 for (const RespTCdf<T>& c : rd) out.push_back(mam_percentiles_from_cdf(c, percentiles));
612 return out;
613}
614
615/**
616 * Port of `@@SolverMAM/getTranAvg.m`: transient queue length, utilization and
617 * throughput.
618 *
619 * The reference forces `options.method = 'ldqbd'` and `options.lang = 'matlab'`
620 * before delegating, so the transient path is NOT selected by the caller's
621 * method; it is fixed. Reproduced: this ignores `opt.method` entirely.
622 *
623 * Which engine runs is decided by `mam_transient_qbd_applicable`: a correlated
624 * or non-Poisson arrival, or a non-renewal service, goes to the Laplace-domain
625 * transient QBD, and everything else to the QBD fast path.
626 */
627template <class T>
629 MamOptions opt = opt_in;
630 opt.method = "ldqbd";
631 runner_detail::check_model_method(L, opt.method, true);
632 // As in the passage-time entry: the transient engines are continuous-time,
633 // so a lattice law is refused rather than read through its continuous fit.
634 runner_detail::check_processes(L, false);
635 // The transient entry never reaches solver_mam_solve, so it needs its own
636 // gate, and that gate is NOT the steady-state ldqbd envelope even though the
637 // method name says ldqbd. `ldqbd` withdraws FiniteCapacity because the
638 // steady-state LD-QBD solves the buffer away, but BOTH transient engines
639 // honour it: solver_mam_transient_qbd builds the level-dependent generator up
640 // to the cap, and solver_mam_ldqbd_transient integrates that same generator.
641 // The M/M/1/5 case in test_mam.cpp checks the exact stationary law the
642 // construction never uses, so it is a genuine oracle for the limit. Gating
643 // this entry on the steady-state set refused a model these engines solve.
644 qn::FeatureSet tranSet = qn::mam_feature_set(opt.method);
645 tranSet.set(qn::Feature::FiniteCapacity);
646 qn::feature_gate("SolverMAM", tranSet, L);
649}
650
651} // namespace mam
652} // namespace line
653
654#endif // LINE_SOLVERS_MAM_SOLVER_MAM_RUNNER_H
UnsupportedError(const std::string &what)
Definition error.h:119
A subset of the registry: MATLAB's SolverFeatureSet, whose list is a flag per field.
FeatureSet & set(Feature f)
A network plus its refreshed NetworkStruct.
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< std::vector< bool > > disabled
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::vector< std::pair< std::size_t, std::size_t > > fj
fj(f,j): the Join node j that closes the Fork node f, 1-based.
The exception types the port throws.
Port of solver_mam_analyzer.m: one inner solve, choosing the analyzer that fits the model and the req...
The option and result types SolverMAM shares with its analyzers.
The method names of each solver, in listing order.
bool sn_has_nonmarkov(const qn::NetworkStruct< T > &sn, bool preserve_det=false)
Whether any law in the struct would be replaced, so a caller can skip copying the struct when there i...
bool nonmkv_enabled(const std::string &method)
Resolve options.config.nonmkv to NonmarkovOptions::enabled.
@ Ph
Bernstein density fit: a genuine phase-type, shape-carrying.
@ Cme
concentrated ME plus exponential tail: two moments, exactly
void sn_nonmarkov_toph(qn::NetworkStruct< T > &sn, const NonmarkovOptions &opts=NonmarkovOptions())
Replace every non-Markovian service and firing law by a Markovian surrogate.
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.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
std::vector< RespTCdf< T > > solver_mam_get_sjrn_t(const qn::NetworkStruct< T > &L, const MamOptions &opt)
@@SolverMAM/getSjrnT.m and sjrnT.m, both aliases of getCdfRespT.
qsys::BmapM1Result< T > solver_mam_getmamresult(const qn::NetworkStruct< T > &L)
Port of @@SolverMAM/getMAMResult.m: the matrix-analytic internals of a single-queue model,...
bool rcat_moved_to_ag(const std::string &method, std::string &why)
True for the four RCAT names that moved to SolverAG, with the redirect message.
void check_method(const std::string &method)
An unlisted method is refused; one that MOVED is redirected by name.
MamSolution< T > mam_dispatch(const qn::NetworkStruct< T > &L, const MamOptions &opt_in)
The ladder.
std::vector< T > mam_percentiles_from_cdf(const RespTCdf< T > &cdf, const std::vector< double > &pcts)
Port of the CDF path of @@SolverMAM/getPerctRespT.m: linear interpolation of the response-time CDF at...
TranResult< T > solver_mam_get_tran_avg(const qn::NetworkStruct< T > &L, const MamOptions &opt_in)
Port of @@SolverMAM/getTranAvg.m: transient queue length, utilization and throughput.
std::vector< std::string > list_valid_methods()
Port of SolverMAM.listValidMethods.
TranResult< T > solver_mam_transient_qbd(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_transient_qbd.m.
MamSolution< T > solver_mam_solve(const qn::NetworkStruct< T > &L, const MamOptions &opt)
The gates and the dispatch of @@SolverMAM/runAnalyzer.m, without the metric filter.
std::vector< RespTCdf< T > > solver_mam_get_cdf_respt(const qn::NetworkStruct< T > &L, const MamOptions &opt)
@@SolverMAM/getCdfRespT.m: the response-time CDF per class.
std::vector< RespTCdf< T > > solver_mam_passage_time(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_passage_time.m.
mva::AvgResult< T > solver_mam_run_analyzer(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of @@SolverMAM/runAnalyzer.m for the lang='matlab' path: solve, then apply the metric filter @@N...
ProbTable< T > solver_mam_get_prob(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::size_t node, const mva::AvgResult< T > &avg)
@@SolverMAM/getProb.m: the joint (level, phase) table at a node.
bool mam_transient_qbd_applicable(const qn::NetworkStruct< T > &L)
Port of mam_transient_qbd_applicable.m: true when the Laplace-domain transient QBD should run instead...
MamFjInfo mam_fj_is_homogeneous(const qn::NetworkStruct< T > &L)
Port of fj_is_homogeneous.m.
std::vector< std::vector< T > > solver_mam_get_perct_respt(const qn::NetworkStruct< T > &L, const MamOptions &opt, const std::vector< double > &percentiles)
@@SolverMAM/getPerctRespT.m: response-time percentiles per class.
bool mam_has_fj_percentiles(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Whether getPerctRespT reads the FJ_codes table rather than inverting a CDF.
std::vector< T > solver_mam_get_prob_marg(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::size_t ist, std::size_t jobclass, const mva::AvgResult< T > &avg)
@@SolverMAM/getProbMarg.m: P(n jobs of one class) at a station.
TranResult< T > solver_mam_ldqbd_transient(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_ldqbd_transient.m.
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,...
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...
ProbTable< T > solver_mam_getprob(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::size_t node, const mva::AvgResult< T > &avg)
Port of @@SolverMAM/getProb.m.
std::string mam_model_method_refusal(const qn::NetworkStruct< T > &L, const std::string &method)
check_model_method asked WITHOUT raising: the same verdict as a sentence.
std::vector< std::vector< T > > solver_mam_fj_percentiles(const qn::NetworkStruct< T > &L, const MamOptions &opt, const std::vector< double > &percentiles)
@@SolverMAM/getPerctRespT.m's fork-join path: the percentiles solver_mam_fj.m stores in percResults....
std::string mam_retrial_refusal(const qn::NetworkStruct< T > &L)
The 'retrial' method's applicability as one sentence; empty when applicable.
std::vector< T > solver_mam_getprobmarg(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::size_t ist, std::size_t jobclass, const mva::AvgResult< T > &avg)
Port of @@SolverMAM/getProbMarg.m: P(n jobs of class jobclass) at station ist, for n = 0....
qsys::BmapM1Result< T > solver_mam_get_mam_result(const qn::NetworkStruct< T > &L)
@@SolverMAM/getMAMResult.m: the M/G/1-type internals of a single queue.
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
Matrix< T > filter_metric(const qn::NetworkStruct< T > &L, const Matrix< T > &metric, MetricKind kind, const std::vector< std::vector< bool > > *zero_mask)
Port of filterMetric: what @@NetworkSolver/getAvg does between the analyzer and the caller.
bool cap_unstable_open_stations(const qn::NetworkStruct< T > &L, Matrix< T > &QN, Matrix< T > &UN, Matrix< T > &RN, const Matrix< T > &TN)
The saturated open-station rule of @@NetworkSolver/getAvg.m:221-308.
const char * unstable_open_warning()
The warning getAvg.m raises when cap_unstable_open_stations() fires.
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
Port of sn_get_arvr_from_tput: the arrival rate each station sees, from the throughputs and the class...
FeatureSet mam_feature_set(const std::string &method)
SolverMAM.getFeatureSet, the union of its four setTrue calls: 55 MATLAB names, WIDENED for 'default'/...
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
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.
Replace every non-Markovian service and firing law by a Markovian surrogate.
The DECLARED side of the gate: one feature set per solver.
Port of solver_mam_fj.m, the fork-join route of SolverMAM.
Port of solver_mam_ldqbd_transient.m: transient queue length, utilization and throughput of a single-...
Port of solver_mam_passage_time.m: the response-time (sojourn-time) distribution of a single open que...
Port of @@SolverMAM/getProb.m and @@SolverMAM/getProbMarg.m: the joint (level, phase) and marginal qu...
Transient analysis of a single-class open queue by the Laplace-domain transient QBD plus numerical in...
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
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.
options.config.nonmkv and friends.
std::size_t order
nonmkvorder, the phase budget
PhFit phfit
which surrogate family
bool preserve_det
Leave Det alone for the exact MAP/D/c branch, options.config.preserveDet.
static constexpr double FineTol
Definition lang_types.h:765
std::string why
the reference's errorMsg, empty when ok
The options SolverMAM reads.
Definition mam_types.h:30
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
The joint (level, phase) table getProb returns: rows levels, cols phases.
One class's response-time CDF, the reference's RD{station, class} = [F, X].
What getTranAvg returns: queue length, utilization and throughput curves.
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
std::string warning
The reference's own warning text, verbatim, empty when it did not warn.
Matrix< T > UN
utilization
Matrix< T > WN
residence time, per job
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
Matrix< T > AN
arrival rate
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:97
std::vector< T > X
Definition mva_types.h:99
std::vector< T > C
Definition mva_types.h:99
Everything qsys_bmapm1 returns, mirroring the MATLAB result struct.
Definition qsys_bmapm1.h:62