LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_retrial.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_RETRIAL_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_RETRIAL_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_retrial.m`, the customer-impatience analyzer of SolverMAM.
12 *
13 * The reference file carries TWO analyzers behind one name, and tries them in
14 * this order:
15 *
16 * 1. RENEGING, a MAP/M/s+G queue whose waiting jobs abandon after a generally
17 * distributed patience (Gursoy, Mehr and Akar). Solved as a multi-regime
18 * Markov fluid queue; ported in solver_mam_reneging.h on mfq_multiregime.
19 * 2. RETRIAL, the BMAP/PH/N/N bufferless orbit queue with flexible retrials
20 * admission control (Dudin et al., Mathematics 2025, 13(9), 1434). Solved
21 * by `qsys_bmapphnn_retrial`, which IS ported (api/qsys).
22 *
23 * BOTH ROUTES ARE REACHABLE, IN THE REFERENCE'S ORDER. The reneging gate
24 * (`mam_reneging_detect`) reads `stations[i].impatience` and
25 * `stations[i].patience`, the C++ counterparts of `sn.impatienceClass` and
26 * `sn.patienceProc`, and is tried first; a queue with a waiting line whose jobs
27 * abandon is never handed to the orbit generator below.
28 *
29 * WHAT THE ORBIT IS. A retrial station has no waiting room: the buffer equals
30 * the server count. A job that finds every server busy joins an ORBIT and
31 * re-attempts at rate alpha per orbiting job; a completion does NOT promote
32 * from the orbit. That is why the queue length reported here is
33 * `L_orbit + N_server` and not the marginal of an ordered buffer.
34 *
35 * THE TRUNCATION IS THE ANSWER'S ACCURACY, AND IT IS ITERATED HERE. The orbit
36 * is unbounded, so `qsys_bmapphnn_retrial` truncates it; the reference does not
37 * accept the first truncation but doubles the level until the relative tail
38 * contribution `truncLevel * mass(top level) / L_orbit` falls under
39 * `TailTolerance`. The ported engine implements ONE solve at a given level and
40 * returns `truncLevel`, `topLevelMass` and `L_orbit`, which is exactly the
41 * triple that refinement test needs, so the loop lives here rather than being
42 * dropped. Without it a model is answered at the reference's FIXED default
43 * level, whose formula depends on neither alpha nor gamma -- the two parameters
44 * that actually govern the tail decay -- and the error is silent.
45 *
46 * WHERE THIS IS STRICTER THAN THE REFERENCE, and why each is the honest outcome:
47 *
48 * - A NON-CONVERGED TRUNCATION IS AN ERROR, NOT A WARNING. When doubling hits
49 * the dimension cap before reaching the tolerance, `solver_mam_retrial.m`
50 * emits `line_warning` and returns the underestimate. The C++ port has no
51 * warning channel, so the same situation would return a number with nothing
52 * attached to it; it refuses instead, and names `orbit_maxlevel` as the way
53 * to ask for that number deliberately.
54 * - A NON-PHASE-TYPE SERVICE IS REFUSED, NOT APPROXIMATED. The reference's
55 * `warnIfApproximated` lets a Det, Replayer, Uniform, Gamma, Pareto, Weibull
56 * or Lognormal service through under an Erlang fit of matching mean and SCV,
57 * and says so in a warning. Everything but Det is already gated out by
58 * `runner_detail::check_processes`; Det reaches here, and `dist_to_map` would
59 * silently give it a 20-phase Erlang -- which is not only a different model
60 * but blows the per-level block up by C(n + 19, 19) states per busy server.
61 * - A BOUNDED RETRY COUNT IS REFUSED. `RetrialParam::max_attempts` and
62 * `DropStrategy::RETRIAL_WITH_LIMIT` say a job gives up after k failed
63 * retries. The generator has no attempt counter in its state descriptor, so
64 * there is no level at which that job could be told apart from a persistent
65 * one.
66 *
67 * WHAT THE C++ MODEL LAYER CANNOT SAY, so what is pinned at its reference
68 * default rather than read: the orbit abandonment rate gamma (`sn.orbitImpatience`,
69 * default 0), the batch rejection probability p (`sn.batchRejectProb`, default
70 * 0), the finite orbit `sn.orbitMaxJobs`, the CONSTANT retrial policy
71 * (`sn.retrialPolicy`; the ported engine is LINEAR-only, which is the
72 * reference's own default), and the admission threshold R, which the reference
73 * lowers below N-1 only from a Finite Capacity Region and `NetworkStruct` has no
74 * region field. Each is stated here so that a later model-layer addition has a
75 * list of what to wire, and none of them is invented from a capacity heuristic.
76 *
77 * REFERENCE DEFECTS in solver_mam_retrial.m and qsys_bmapphnn_retrial.m:
78 *
79 * 1. `Tolerance` IS PARSED AND NEVER USED. `solver_mam_retrial.m` reads
80 * `options.tol`, defaults it to 1e-10 and forwards it as 'Tolerance';
81 * `qsys_bmapphnn_retrial.m` assigns it on line 83 and no later line reads
82 * it. The single dense solve behind the generator is direct, so there is no
83 * iteration for it to terminate. Not propagated: `MamOptions::tol` is
84 * likewise not forwarded here, and the comment records why rather than
85 * leaving a caller to wonder why lowering the tolerance changes nothing.
86 * 2. THE DEFAULT RETRIAL RATE IS A MAGIC 0.1. When no retrial process is
87 * attached, `alpha = 0.1` -- a rate with no relation to the model. It is
88 * reproduced, because a station carrying only a RETRIAL drop rule is a
89 * model the reference accepts and would otherwise be answered differently
90 * by the two codebases, but a caller reaching it has almost certainly
91 * forgotten `setRetrial`.
92 * 3. `extractPHParams` NORMALIZES A BAD beta INSTEAD OF REPORTING IT. When the
93 * entry vector recovered from D1 does not sum to one it is rescaled, and
94 * when it sums to zero it is replaced by the uniform vector. Both hide a
95 * malformed service representation. Not reproduced: `map_pie` is the
96 * codebase's own embedded entry vector and is exact for any Markovian pair,
97 * and a pair that is not Markovian is refused above by name.
98 *
99 * ARITHMETIC. The generator is a rational expression in its inputs and the
100 * stationary law is one linear solve, so nothing here needs a transcendental
101 * function and this analyzer carries no `has_transcendental` gate -- unlike
102 * `solver_mam_basic`, whose station solves run tolerance-terminated Riccati
103 * iterations. `qsys_bmapphnn_retrial` documents the same property, and the
104 * exact instantiation returns the stationary law of the truncated chain
105 * exactly.
106 */
107
108#include <algorithm>
109#include <cmath>
110#include <cstddef>
111#include <limits>
112#include <map>
113#include <string>
114#include <vector>
115
123#include "line/util/error.h"
124#include "line/util/matrix.h"
125
126namespace line {
127namespace mam {
128
129/**
130 * The knobs `solver_mam_retrial.m` reads from `options.config` and from model
131 * fields the C++ `NetworkStruct` does not carry.
132 *
133 * Kept as a separate argument rather than added to `MamOptions`, so that the
134 * options every MAM analyzer shares do not grow a field only one of them reads.
135 * The defaults are the reference's.
136 */
138 /** `options.config.orbit_maxlevel`; 0 asks for the adaptive refinement. */
139 std::size_t orbit_maxlevel = 0;
140 /** `options.config.orbit_tailtol`, the relative orbit-truncation target. */
141 double orbit_tailtol = 1e-6;
142 /** `'MaxDim'`: the largest generator dimension the refinement may reach. */
143 double max_dim = 2e5;
144 /** `'MaxBlockSize'`: the largest per-level block V*d that may be assembled. */
145 double max_block = 5e3;
146};
147
148/** What `qsys_is_retrial.m` returns: the station it found, or why it found none. */
150 bool ok = false;
151 std::size_t station = 0; ///< 1-based station index of the retrial queue
152 std::size_t source = 0; ///< 1-based station index of the Source
153 std::size_t cls = 1; ///< 1-based class index; the reference is single-class
154 int nservers = 0; ///< N
155 long R = 0; ///< admission threshold, N-1 unless an FCR lowers it
156 double alpha = 0.1; ///< `retInfo.alpha`: the reference's placeholder, overridden by the solver
157 double gamma = 0.0; ///< `retInfo.gamma`: orbit impatience default
158 double p = 0.0; ///< `retInfo.p`: batch rejection probability default
159 std::string why; ///< the reference's `errorMsg`, empty when ok
160};
161
162namespace retrial_detail {
163
164/** Does any class at this station declare a retrial process? */
165template <class T>
166bool has_retrial_proc(const qn::NetworkStruct<T>& L, std::size_t ist) {
167 const typename std::map<std::size_t, qn::RetrialParam<T> >::const_iterator it =
168 L.retrialparam.find(ist);
169 if (it == L.retrialparam.end()) return false;
170 for (std::size_t r = 0; r < it->second.retrial_proc.size(); ++r)
171 if (!it->second.retrial_proc[r].disabled) return true;
172 return false;
173}
174
175/**
176 * The reference's `assertMarkovian`, as a refusal naming the station and the
177 * class. A process that is not a (D0, D1) pair as declared has no place in a
178 * matrix-analytic generator, and the C++ layer would hand it over as an Erlang
179 * of matching moments instead of reporting it.
180 */
181template <class T>
182void assert_markovian(const qn::NetworkStruct<T>& L, std::size_t ist, std::size_t r,
183 const char* role) {
184 const lang::Distrib<T>& d = L.service[ist - 1][r - 1];
185 if (L.disabled[ist - 1][r - 1] || d.disabled)
186 throw UnsupportedError(ErrorKind{"Retrial"}, std::string("SolverMAM: the ") + role + " process of class '" +
187 L.classes[r - 1].name + "' at station '" + L.stations[ist - 1].name +
188 "' is disabled; the matrix-analytic retrial solver has no state for "
189 "a class that does not visit its own station");
190 if (!basic_detail::is_markovian_type(d.type))
191 throw UnsupportedError(ErrorKind{"Distribution"},
192 std::string("SolverMAM: the ") + role + " process of class '" + L.classes[r - 1].name +
193 "' at station '" + L.stations[ist - 1].name + "' is " + lang::process_to_text(d.type) +
194 ", which has no Markovian (D0,D1) representation. The reference replaces it by an "
195 "Erlang of matching mean and squared coefficient of variation and warns that the "
196 "answer is approximate; this port refuses instead, because the substitute is a "
197 "different model under the same method name. Use SolverCTMC, SolverSSA or SolverLDES");
198 const Map<T> m = lang::dist_to_map(d);
199 if (!basic_detail::is_markovian_map(m))
200 throw UnsupportedError(ErrorKind{"Model"}, std::string("SolverMAM: the ") + role + " process of class '" +
201 L.classes[r - 1].name + "' at station '" +
202 L.stations[ist - 1].name +
203 "' has a (D0,D1) pair that is not a Markovian generator (a negative "
204 "off-diagonal, a negative arrival entry, or rows that do not sum to "
205 "zero)");
206}
207
208/**
209 * The per-level block width V*d of the generator, where d counts the multisets
210 * of service phases over the busy servers.
211 *
212 * Computed BEFORE the first solve, because the engine assembles one dense
213 * (truncLevel + 1) * V*d square matrix and an oversized block has to be
214 * reported rather than attempted. In double throughout: the binomials overflow
215 * an integer long before they reach a block size anyone would wait for.
216 */
217inline double retrial_block_width(std::size_t V, std::size_t M, int N) {
218 double d = 0.0, term = 1.0; // term = C(n + M - 1, M - 1), starting at n = 0
219 for (int n = 0; n <= N; ++n) {
220 if (n > 0) term *= static_cast<double>(n + static_cast<int>(M) - 1) / static_cast<double>(n);
221 d += term;
222 if (d > 1e12) return 1e12; // already past any usable cap; stop growing
223 }
224 return static_cast<double>(V) * d;
225}
226
227/**
228 * `orbitTruncationError`: the relative contribution the truncated tail would
229 * add to the mean orbit length, which the mass sitting at the top level bounds.
230 */
231template <class T>
232double orbit_trunc_error(const qsys::BmapPhNnRetrialResult<T>& r) {
233 const double lorbit = num_traits<T>::to_double(r.L_orbit);
234 const double top = num_traits<T>::to_double(r.topLevelMass);
235 const double denom = lorbit > std::numeric_limits<double>::min()
236 ? lorbit
237 : std::numeric_limits<double>::min();
238 return static_cast<double>(r.truncLevel) * top / denom;
239}
240
241} // namespace retrial_detail
242
243/**
244 * Port of `qsys_is_retrial.m`, plus the reneging gate of `solver_mam_retrial.m`.
245 *
246 * Returns rather than throws, because `solver_mam_analyzer.m` uses it as the
247 * 2c/2d branch predicate and only errors once every branch has declined. The
248 * rejection reason travels in `why` so the dispatch can quote it.
249 */
250template <class T>
252 using lang::DropStrategy;
253 MamRetrialInfo info;
254
255 // A mixed model has a closed chain whose population the orbit generator has
256 // no level for, so the predicate must be "every class open", not "any".
257 if (!L.is_open_model()) {
258 info.why = "the BMAP/PH/N/N retrial solver requires an open queueing model";
259 return info;
260 }
261 if (L.nclasses != 1) {
262 info.why = "the BMAP/PH/N/N retrial solver supports a single class only";
263 return info;
264 }
265 info.cls = 1;
266
267 // A retrial station is one that DECLARES an orbit, or one that is
268 // bufferless: capacity equal to the server count leaves no waiting line, so
269 // a blocked job has nowhere to go but an orbit.
270 std::vector<std::size_t> bufferless;
271 for (std::size_t i = 1; i <= L.nstations; ++i) {
272 if (L.stations[i - 1].nodetype != qn::NodeType::Queue) continue;
273 const double cap = L.cap[i - 1];
274 const bool isbufferless = std::isfinite(cap) && cap == L.stations[i - 1].nservers;
275 if (retrial_detail::has_retrial_proc(L, i) || isbufferless) bufferless.push_back(i);
276 }
277 if (bufferless.empty()) {
278 info.why = "no retrial queue found (declare one with setRetrial, or give a station a "
279 "capacity equal to its server count)";
280 return info;
281 }
282
283 for (std::size_t i : bufferless) {
284 for (std::size_t r = 0; r < L.nclasses; ++r)
285 if (L.droprule[i - 1][r] == DropStrategy::RETRIAL ||
286 L.droprule[i - 1][r] == DropStrategy::RETRIAL_WITH_LIMIT) {
287 info.station = i;
288 break;
289 }
290 if (info.station != 0) break;
291 }
292 if (info.station == 0) {
293 info.why = "no RETRIAL drop strategy is configured on a bufferless queue";
294 return info;
295 }
296 // The BMAP/PH/N/N analysis of Dudin et al. takes an EXPONENTIAL retrial
297 // delay and orbit abandonment (one rate each) and an unlimited number of
298 // attempts; anything else is a different model, not an approximation of it.
299 {
300 typename std::map<std::size_t, qn::RetrialParam<T> >::const_iterator rp =
301 L.retrialparam.find(info.station);
302 const std::size_t c = info.cls - 1;
303 if (rp != L.retrialparam.end() && c < rp->second.retrial_proc.size() &&
304 !rp->second.retrial_proc[c].disabled) {
305 if (rp->second.retrial_proc[c].type != lang::ProcessType::EXP) {
306 info.station = 0;
307 info.why = "the BMAP/PH/N/N retrial solver requires an exponential retrial delay; "
308 "use SolverLDES for a non-exponential one";
309 return info;
310 }
311 if (c < rp->second.max_attempts.size() && rp->second.max_attempts[c] > 0) {
312 info.station = 0;
313 info.why = "the BMAP/PH/N/N retrial solver supports only unlimited retrial attempts; "
314 "use SolverLDES for a finite cap";
315 return info;
316 }
317 }
318 const auto& rst = L.stations[info.station - 1];
319 if (c < rst.orbit_impatience.size() && !rst.orbit_impatience[c].disabled &&
320 rst.orbit_impatience[c].type != lang::ProcessType::EXP) {
321 info.station = 0;
322 info.why = "the BMAP/PH/N/N retrial solver requires an exponential orbit impatience; "
323 "use SolverLDES for a non-exponential one";
324 return info;
325 }
326 }
327
328 for (std::size_t i = 1; i <= L.nstations; ++i)
329 if (L.stations[i - 1].nodetype == qn::NodeType::Source) {
330 info.source = i;
331 break;
332 }
333 if (info.source == 0) {
334 info.why = "no Source node found";
335 return info;
336 }
337
338 const double ns = L.stations[info.station - 1].nservers;
339 if (!std::isfinite(ns) || ns < 1.0) {
340 info.why = "the retrial station must have a finite, positive server count";
341 return info;
342 }
343 info.nservers = static_cast<int>(ns);
344
345 // Validate the arrival and service processes: the reference requires a
346 // representation {alpha, A} / {D0, D1} of length >= 2 at both ends, and a
347 // disabled process is stored as a 1 x 1 NaN, which fails that test.
348 if (L.disabled[info.source - 1][info.cls - 1] ||
349 L.service[info.source - 1][info.cls - 1].disabled) {
350 info.why = "Invalid arrival process at source.";
351 return info;
352 }
353 if (L.disabled[info.station - 1][info.cls - 1] ||
354 L.service[info.station - 1][info.cls - 1].disabled) {
355 info.why = "Invalid service process at queue.";
356 return info;
357 }
358
359 // Admission threshold: N-1 (no admission control) unless a Finite Capacity
360 // Region holding this station carries a global cap at or below N-1. The
361 // last column of the region's cap matrix is the station's global bound.
362 info.R = static_cast<long>(info.nservers) - 1;
363 for (std::size_t f = 0; f < L.regions.size(); ++f) {
364 const std::vector<std::vector<double> >& cap = L.regions[f].cap;
365 if (cap.size() < info.station) continue;
366 const std::vector<double>& row = cap[info.station - 1];
367 if (row.empty()) continue;
368 const double rfcr = row[row.size() - 1];
369 if (rfcr > 0.0) {
370 if (rfcr <= static_cast<double>(info.nservers) - 1.0)
371 info.R = static_cast<long>(rfcr);
372 break;
373 }
374 }
375 info.ok = true;
376 return info;
377}
378
379/**
380 * The 'retrial' method's applicability as one sentence; empty when applicable.
381 *
382 * A "MUST BE PRESENT" RULE, which is why it cannot live in a feature set: a
383 * FeatureSet says "I accept this construct", so it can refuse a model for HAVING
384 * something and never for LACKING it. This analyzer needs the BMAP/PH/N/N
385 * bufferless retrial topology to work on, and a model without one is not a
386 * smaller retrial model, it is a different one.
387 *
388 * ONE PREDICATE, THREE CALLERS: `mam_dispatch`'s 'retrial' arm raises it,
389 * `runner_detail::check_model_method` raises it ahead of the dispatch, and
390 * `autosolver::auto_family_refusal` returns it through
391 * `mam_model_method_refusal`, so the method the report offers and the method
392 * that runs are the same set.
393 */
394template <class T>
396 const MamRetrialInfo info = mam_retrial_detect(L);
397 if (info.ok) return std::string();
398 // mam_retrial_applicable.m: the MAP/M/s+G reneging shape makes the method
399 // applicable as well. It is a SHAPE, not a presence, so a patience on a
400 // two-queue model is still refused.
402 if (ren.ok) return std::string();
404 return "SolverMAM: the 'retrial' method needs a BMAP/PH/N/N bufferless retrial topology "
405 "or a MAP/M/s+G reneging shape (one Source, one FCFS Queue with exponential "
406 "service, one open class, a patience distribution); this model has neither. " +
407 ren.why + " Use method 'default'";
408 return "SolverMAM: the 'retrial' method needs a BMAP/PH/N/N bufferless retrial topology; "
409 "this model declares no orbit (" + info.why + "). Use method 'default'";
410}
411
412/**
413 * Port of `solver_mam_retrial.m`.
414 *
415 * @param L the refreshed struct, with non-Markovian processes already gated
416 * out by the runner
417 * @param opt the MAM options; only `mapmsg_quantization` (reneging route) is read,
418 * none reaches the orbit engine, see defect 1
419 * @param cfg the orbit truncation controls
420 */
421template <class T>
423 const MamRetrialConfig& cfg = MamRetrialConfig()) {
424 using lang::DropStrategy;
425 // see defect 1: the reference forwards options.tol to the orbit engine and nothing reads it
426 const T zero = num_traits<T>::from_int(0);
427
428 // Route 1, reneging, is tried first, as solver_mam_retrial.m does.
430 if (ren.ok) return solver_mam_reneging(L, opt, ren);
431
432 const MamRetrialInfo info = mam_retrial_detect(L);
433 if (!info.ok)
434 throw UnsupportedError(ErrorKind{"Reneging"},
435 "SolverMAM: no valid impatience configuration was detected (retrial or reneging): " +
436 (mam_declares_reneging(L) ? ren.why : info.why));
437
438 const std::size_t q = info.station, src = info.source, r = info.cls;
439
440 retrial_detail::assert_markovian(L, src, r, "arrival");
441 retrial_detail::assert_markovian(L, q, r, "service");
442
443 // A bounded retry count is a per-job attribute; the orbit level counts jobs
444 // and nothing else, so a job on its last attempt is indistinguishable there.
445 if (L.droprule[q - 1][r - 1] == DropStrategy::RETRIAL_WITH_LIMIT)
446 throw UnsupportedError(ErrorKind{"Retrial"},
447 "SolverMAM: station '" + L.stations[q - 1].name +
448 "' declares DropStrategy.RETRIAL_WITH_LIMIT. The BMAP/PH/N/N generator indexes its "
449 "levels by the orbit population alone and has no per-job attempt counter, so a job "
450 "about to give up cannot be told from a persistent one. Use SolverCTMC or SolverSSA");
451
452 // ---- the arrival BMAP ---------------------------------------------------
453 // A genuine BATCH arrival would give D = {D0, D1, ..., DK}; lang::Distrib
454 // holds a single (D0, D1) pair, so every model the C++ layer can build is
455 // the K = 1 case. The engine takes the general list, so nothing is lost
456 // here beyond the batch models the model layer cannot express.
457 const Map<T> arv = lang::dist_to_map(L.service[src - 1][r - 1]);
458 std::vector<Matrix<T> > D;
459 D.push_back(arv.D0);
460 D.push_back(arv.D1);
461
462 // ---- the PH service -----------------------------------------------------
463 const Map<T> svc = lang::dist_to_map(L.service[q - 1][r - 1]);
464 const Matrix<T> S = svc.D0;
465 const std::vector<T> beta = map_pie(svc);
466
467 // ---- the orbit parameters ----------------------------------------------
468 // The retrial process is an inter-retry TIME, and the generator carries a
469 // single scalar rate per orbiting job, so only a one-phase process can be
470 // read. The reference takes -D0(1,1) whatever the order is, which for a
471 // multi-phase process is the exit rate of its FIRST phase and not the mean
472 // retry rate of anything.
473 T alpha = num_traits<T>::from_double(0.1); // see defect 2
474 const typename std::map<std::size_t, qn::RetrialParam<T> >::const_iterator rit =
475 L.retrialparam.find(q);
476 if (rit != L.retrialparam.end()) {
477 const qn::RetrialParam<T>& rp = rit->second;
478 if (rp.max_attempts.size() >= r && rp.max_attempts[r - 1] > 0)
479 throw UnsupportedError(ErrorKind{"Retrial"},
480 "SolverMAM: station '" + L.stations[q - 1].name + "' bounds the retrial count at " +
481 std::to_string(rp.max_attempts[r - 1]) +
482 " attempts. The BMAP/PH/N/N generator has no per-job attempt counter in its state "
483 "descriptor. Use SolverCTMC or SolverSSA");
484 if (rp.retrial_proc.size() >= r && !rp.retrial_proc[r - 1].disabled) {
485 const Map<T> ret = lang::dist_to_map(rp.retrial_proc[r - 1]);
486 if (ret.order() != 1)
487 throw UnsupportedError(ErrorKind{"Retrial"},
488 "SolverMAM: the retrial process of class '" + L.classes[r - 1].name +
489 "' at station '" + L.stations[q - 1].name + "' has " +
490 std::to_string(ret.order()) +
491 " phases. The generator retries each orbiting job at a single scalar rate, so "
492 "only an exponential inter-retry time can be represented; the reference reads "
493 "-D0(1,1) regardless, which is the exit rate of the first phase and not the "
494 "retry rate of the process");
495 alpha = T(zero - ret.D0(0, 0));
496 } else if (rp.retrial_rate.size() >= r && rp.retrial_rate[r - 1] > zero) {
497 alpha = rp.retrial_rate[r - 1];
498 }
499 }
500 if (!(alpha > zero))
501 throw InputError("SolverMAM: the retrial rate at station '" + L.stations[q - 1].name +
502 "' must be positive; a zero rate means an orbiting job never retries and "
503 "the orbit is not positive recurrent at any load");
504
505 const T gamma = zero; // sn.orbitImpatience, absent from NetworkStruct
506 const T p = zero; // sn.batchRejectProb, absent from NetworkStruct
507
508 // ---- the truncation, and the refinement the engine leaves to its caller --
509 const double blockw =
510 retrial_detail::retrial_block_width(arv.order(), svc.order(), info.nservers);
511 if (blockw > cfg.max_block)
512 throw UnsupportedError(ErrorKind{"StateSpace"},
513 "SolverMAM: the per-level block of the retrial generator at station '" +
514 L.stations[q - 1].name + "' is " + std::to_string(static_cast<long long>(blockw)) +
515 " states wide, past the " + std::to_string(static_cast<long long>(cfg.max_block)) +
516 "-state cap: the service has " + std::to_string(svc.order()) +
517 " phases and the station " + std::to_string(info.nservers) +
518 " servers, and the busy servers' phase multisets grow as C(n + phases - 1, phases - 1). "
519 "Fit the service with fewer phases, reduce the server count, or raise "
520 "MamRetrialConfig::max_block if the memory cost is acceptable");
521
522 const std::vector<long> Rv(1, info.R);
524 qopt.maxLevel = cfg.orbit_maxlevel;
525 qopt.tailTolerance = cfg.orbit_tailtol;
526 qopt.maxDim = cfg.max_dim;
527 qopt.maxBlockSize = cfg.max_block;
529 qsys::qsys_bmapphnn_retrial(D, beta, S, info.nservers, alpha, gamma, p, Rv, qopt);
530
531 if (cfg.orbit_maxlevel == 0) {
532 // The reference doubles the level until the tail contribution is under
533 // TailTolerance. `blockw` is the per-level width, so (level + 1) * blockw
534 // is the dimension the next solve would assemble.
535 while (retrial_detail::orbit_trunc_error(res) > cfg.orbit_tailtol) {
536 const std::size_t next = 2 * res.truncLevel;
537 if (static_cast<double>(next + 1) * blockw > cfg.max_dim)
538 throw NumericError(ErrorKind{"NonConvergence"},
539 "SolverMAM: the orbit truncation of station '" + L.stations[q - 1].name +
540 "' did not reach the requested accuracy: the tail still contributes " +
541 std::to_string(retrial_detail::orbit_trunc_error(res)) + " of the mean orbit "
542 "length at level " + std::to_string(res.truncLevel) +
543 ", against a target of " + std::to_string(cfg.orbit_tailtol) +
544 ", and doubling would pass the dimension cap. The orbit measures would be "
545 "underestimated, which the reference reports as a warning and this port has no "
546 "channel for. Raise MamRetrialConfig::max_dim, or set "
547 "MamRetrialConfig::orbit_maxlevel to accept a fixed truncation");
548 qopt.maxLevel = next;
549 res = qsys::qsys_bmapphnn_retrial(D, beta, S, info.nservers, alpha, gamma, p, Rv, qopt);
550 }
551 }
552
553 // ---- the LINE metric tuple ---------------------------------------------
555 const std::size_t M = L.nstations, K = L.nclasses;
556 out.Q = Matrix<T>(M, K, zero);
557 out.U = Matrix<T>(M, K, zero);
558 out.R = Matrix<T>(M, K, zero);
559 out.Tp = Matrix<T>(M, K, zero);
560 out.C.assign(K, zero);
561 out.X.assign(K, zero);
562
563 // The orbit is part of the station: a job waiting to retry has not left it.
564 out.Q(q - 1, r - 1) = T(res.L_orbit + res.N_server);
565 out.U(q - 1, r - 1) = res.utilization;
566 out.Tp(q - 1, r - 1) = res.throughput;
567 if (res.throughput > zero)
568 out.R(q - 1, r - 1) = T(out.Q(q - 1, r - 1) / res.throughput);
569 // else the reference writes Inf, and the NaN sweep at the end of
570 // mam_dispatch zeroes every non-finite entry before any caller sees it; a
571 // zero written here is the same value by a route that also holds at
572 // T = Rational, which has no infinity to write.
573
574 out.X[r - 1] = res.throughput;
575 out.C[r - 1] = out.R(q - 1, r - 1);
576 // The reference's `totiter` for this analyzer is the truncation level that
577 // produced the answer, not an iteration count.
578 out.iter = static_cast<int>(res.truncLevel);
579 return out;
580}
581
582} // namespace mam
583} // namespace line
584
585#endif // LINE_SOLVERS_MAM_SOLVER_MAM_RETRIAL_H
InputError(const std::string &what)
Definition error.h:105
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
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::map< std::size_t, RetrialParam< T > > retrialparam
Retrial parameters, keyed by 1-based STATION index.
std::vector< double > cap
sn.cap and sn.classcap: the total and per-class buffers.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
bool is_open_model() const
sn_is_open_model: EVERY class is open, which is not has_open_classes.
std::vector< std::vector< DropStrategy > > droprule
std::vector< Region > regions
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
The option and result types SolverMAM shares with its analyzers.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:431
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
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...
MamRenegingInfo mam_reneging_detect(const qn::NetworkStruct< T > &L)
Port of mam_reneging_applicable.m: is this the MAP/M/s+G shape?
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
MamRetrialInfo mam_retrial_detect(const qn::NetworkStruct< T > &L)
Port of qsys_is_retrial.m, plus the reneging gate of solver_mam_retrial.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.
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.
BmapPhNnRetrialResult< T > qsys_bmapphnn_retrial(const std::vector< Matrix< T > > &D, const std::vector< T > &beta, const Matrix< T > &S, int N, const T &alpha, const T &gamma, const T &p, const std::vector< long > &R, const BmapPhNnRetrialOptions &opt)
The BMAP/PH/N/N bufferless retrial queue.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
The BMAP/PH/N/N bufferless retrial queue with flexible retrial admission control.
Port of solver_mam_basic.m, the dec.source analyzer and the default algorithm of SolverMAM.
The reneging route of solver_mam_retrial.m: the MAP/M/s+G queue whose waiting jobs abandon after a ge...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The options SolverMAM reads.
Definition mam_types.h:30
What mam_reneging_applicable.m returns: the station it found, or why it found none.
std::string why
the reference's errorMsg, empty when ok
The knobs solver_mam_retrial.m reads from options.config and from model fields the C++ NetworkStruct ...
double max_dim
'MaxDim': the largest generator dimension the refinement may reach.
double orbit_tailtol
options.config.orbit_tailtol, the relative orbit-truncation target.
std::size_t orbit_maxlevel
options.config.orbit_maxlevel; 0 asks for the adaptive refinement.
double max_block
'MaxBlockSize': the largest per-level block V*d that may be assembled.
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
std::size_t station
1-based station index of the retrial queue
double alpha
retInfo.alpha: the reference's placeholder, overridden by the solver
std::size_t cls
1-based class index; the reference is single-class
long R
admission threshold, N-1 unless an FCR lowers it
double p
retInfo.p: batch rejection probability default
double gamma
retInfo.gamma: orbit impatience default
std::size_t source
1-based station index of the Source
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D0
Definition map_moment.h:54
std::size_t order() const
Definition map_moment.h:57
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
The parameters of a retrial station: MATLAB sn.retrialProc and friends.
std::vector< int > max_attempts
0 = unbounded
std::vector< T > retrial_rate
mu_r, the per-class orbit retry rate
std::vector< lang::Distrib< T > > retrial_proc
retrial_proc[r] is the class-r retrial process; empty = not a retrial class.
Options of qsys_bmapphnn_retrial (the name-value pairs of the .m file).
std::size_t maxLevel
Fixed orbit truncation level ('MaxLevel'); 0 selects the reference's adaptive choice,...
double maxDim
'MaxDim': cap on the total generator dimension of the refinement.
double tailTolerance
'TailTolerance': relative orbit-truncation error target.
double maxBlockSize
'MaxBlockSize': cap on the per-level block size V*d; exceeding it is an error.
Result of qsys_bmapphnn_retrial.
T L_orbit
mean number of customers in orbit
std::size_t truncLevel
truncation level used
T N_server
mean number of busy servers