LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mna.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_MNA_H
6#define LINE_SOLVERS_MAM_SOLVER_MNA_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mna_open.m` and `solver_mna_closed.m`, the two analyzers
12 * behind SolverMAM's `mna` method.
13 *
14 * THE METHOD. MNA is QNA's flow decomposition with QNA's isolated-station
15 * solution REPLACED, at every FCFS station, by a matrix-analytic one. Each
16 * sweep superposes the per-class flows into a station as a rate a1(i,r) and a
17 * squared coefficient of variation a2(i,r), solves the station in isolation,
18 * and splits the departure stream along the outgoing arcs with the exact
19 * Bernoulli-thinning rule f2 = 1 + p (d2 - 1). What makes it MAM rather than
20 * QNA is the last step: the converged (a1, a2) pair is turned back into a
21 * phase-type arrival process -- one APH per class fitted to those two moments,
22 * marked and superposed into an MMAP -- and the station's queue length comes
23 * from MMAP[K]/PH[K]/1 FCFS rather than from a Whitt waiting-time formula. The
24 * flow SCVs d2 that drive the NEXT sweep still come from QNA's departure
25 * formula, so the fixed point is QNA's and only the reported queue lengths are
26 * matrix-analytic.
27 *
28 * THE TWO ANALYZERS DIFFER IN WHAT DRIVES THE OUTER LOOP, not in the sweep.
29 * The open one has its arrival rates fixed by the sources and runs a single
30 * flow fixed point. The closed one has no source: it wraps the same flow fixed
31 * point in an OUTER BISECTION on the per-class throughput, bracketed below by 0
32 * and above by the slowest service rate over the finite-server stations, and
33 * driven against the population target N. That is why the closed analyzer
34 * evaluates the matrix-analytic station solve once per outer step, and why it
35 * asks for the queue-length DISTRIBUTION (truncated at the class population)
36 * where the open one asks only for the mean.
37 *
38 * WHAT THE REFERENCE GETS WRONG, REPRODUCED RATHER THAN CORRECTED. Three
39 * things, each of which changes reported numbers:
40 *
41 * - A PS STATION IN THE OPEN ANALYZER REPORTS NOTHING. `solver_mna_open.m`'s
42 * PS branch assigns to `TN`, `UN`, `QN`, `RN`, which are fresh undefined
43 * variables in that function -- the metrics it returns are `T`, `U`, `Q`,
44 * `R`. So a PS station keeps the zeros it was initialised with, and its
45 * departure SCV d2 is never set either. The closed analyzer's PS branch
46 * writes the right names and does work. Reproduced: silently redirecting
47 * the writes would report queue lengths MATLAB does not report.
48 * - THE CLOSED ANALYZER READS ONE CLASS'S DISTRIBUTION FOR EVERY CLASS.
49 * `[pdistr] = MMAPPH1FCFS(..., 'ncDistr', maxLevel)` captures only the FIRST
50 * output, i.e. class 1's marginal, and the per-class truncation loop then
51 * truncates THAT at each class's own population. The reference labels the
52 * block "rough approximation" itself.
53 * - THROUGHPUT IS NEVER REPORTED. Both analyzers initialise `X = zeros(1,K)`
54 * and never assign it, so `getAvg`'s per-class throughput column is zero
55 * however the model is solved. Station throughputs in `T` are correct; it is
56 * only the class-level `X` that is dead.
57 *
58 * ONE REFERENCE BRANCH IS REFUSED RATHER THAN REPRODUCED. `config.dep_scv =
59 * 'etaqa'` is dead in MATLAB -- `qbd_depproc_jointmom` raises on every input it
60 * can be handed, so the try/catch around it always takes the QNA fallback --
61 * while the ported `qbd_depproc_jointmom` works. Answering with the working one
62 * would report a departure SCV the reference never produces, so the option is
63 * refused by name instead; see `solver_mna_open`.
64 *
65 * REFERENCE INDEXING, CHECKED RATHER THAN ASSUMED, exactly as `solver_qna.h`
66 * does for the same reason: both files index the STATEFUL-indexed `sn.rt` with
67 * station indices, and the closed one additionally indexes the CLASS-indexed
68 * `sn.njobs` and the length-C `lambda` with the same running index. Both are
69 * silently correct only on the model shapes the method is advertised for, and
70 * are refused by name otherwise.
71 *
72 * SELF-LOOPING CLASSES. `sn.isslc` guards three blocks in the closed analyzer.
73 * The C++ `JobClassType` is OPEN or CLOSED only, so no model this port can
74 * build enters them and they are not transcribed.
75 *
76 * Arithmetic: DOUBLE-ONLY, gated the way `solver_mam_basic.h` gates it. The
77 * flow fixed point stops on a tolerance, the APH fit takes a ceiling of a real
78 * reciprocal, and MMAP[K]/PH[K]/1 runs the ADDA doubling iteration.
79 */
80
81#include <algorithm>
82#include <cmath>
83#include <limits>
84#include <string>
85#include <utility>
86#include <vector>
87
88#include "line/api/da/da_fpi.h"
101#include "line/solvers/mam/solver_mam_bmap.h" // mam_detect_mmck, shared with solver_mam_basic
102#include "line/util/error.h"
103#include "line/util/matrix.h"
104
105namespace line {
106namespace mam {
107
108/**
109 * The `options.config` fields the two MNA analyzers read.
110 *
111 * Kept out of `MamOptions` because neither field is a SolverMAM option: both
112 * are written by the analyzer itself onto a LOCAL copy of `options.config`, so
113 * a caller has no way to reach them through the solver's option surface.
114 */
115struct MnaConfig {
116 /**
117 * `config.dep_scv`, read only by the OPEN analyzer. 'qna' takes Whitt's
118 * departure-SCV formula. 'etaqa' is refused by name; see the branch.
119 */
120 std::string dep_scv = "qna";
121};
122
123namespace mna_detail {
124
128
129// aph_from_2moments and aph_fit_mean_scv now live in
130// api/mam/aph_fit_moments.h: the closed setup/delay-off branch of
131// solver_mam_basic needs the same APH.fitMeanAndSCV, and that header
132// already includes this one, so keeping them here would have been a
133// cycle. Re-exported so every existing caller is unchanged.
136
137// `mam_detect_mmck` used to be transcribed a second time here, under the name
138// `detect_mmck` and a 0-based bool/out-param signature, with a comment saying
139// "the two must stay in step". Collapsed onto the single definition in
140// solver_mam_bmap.h: a duplicate whose own comment admits it must be kept in
141// step is the defect class that already cost this tree once, in the two live
142// ports of solver_fluid_initsol.m. The surviving form is the 1-based
143// struct-returning one because it is the one carrying a station-index range
144// check; the bodies were otherwise character-for-character the same.
145
146/** The gates both analyzers share, named after the indexing they protect. */
147template <class T>
148void check_gates(const qn::NetworkStruct<T>& L, const std::string& who) {
149 // The Fork test comes first because a Fork-Join model also fails the
150 // stateful-count test, and the reference's own refusal is the Fork one.
151 if (L.has_fork())
152 throw UnsupportedError(who +
153 ": Fork nodes are not supported yet by the QNA-family solvers, as "
154 "the reference's line_error states");
155 if (L.nof_stateful() != L.nstations)
156 throw UnsupportedError(
157 who + ": the reference indexes the stateful-indexed sn.rt with station indices, which "
158 "is only correct when every stateful node is a station; this model has " +
159 std::to_string(L.nof_stateful()) + " stateful nodes and " +
160 std::to_string(L.nstations) + " stations");
161}
162
163/** `sn.pie` and `sn.proc{ist}{k}{1}` at every station the analyzers solve. */
164template <class T>
165std::vector<std::vector<PhService<T>>> service_laws(const qn::NetworkStruct<T>& L) {
166 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
167 const std::size_t M = L.nstations, K = L.nclasses;
168 std::vector<std::vector<PhService<T>>> svc(M, std::vector<PhService<T>>(K));
169 for (std::size_t i = 0; i < M; ++i) {
170 const SchedStrategy sc = L.stations[i].sched;
171 if (!(sc == SchedStrategy::FCFS || sc == SchedStrategy::INF || sc == SchedStrategy::PS))
172 continue;
173 for (std::size_t r = 0; r < K; ++r) {
174 if (!L.has_service_law(i, r) || !(L.rates(i, r) > zero)) {
175 // The reference's `any(isnan(D0))` guard: a class this station
176 // never serves gets an Immediate service rather than a NaN pair.
177 // has_service_law also covers the Join, whose rates are Inf but
178 // whose process is the NaN Coxian this guard exists for.
179 const T imm = num_traits<T>::from_double(GlobalConstants::Immediate);
180 svc[i][r].sigma.assign(1, one);
181 svc[i][r].S = Matrix<T>(1, 1, T(-imm));
182 continue;
183 }
184 const Map<T> ph = lang::dist_to_map(L.service[i][r]);
185 svc[i][r].sigma = map_pie(ph);
186 svc[i][r].S = ph.D0;
187 }
188 }
189 return svc;
190}
191
192// `station_visits` now lives in basic_detail (solver_mam_basic.h), for the same
193// reason `mam_detect_mmck` does: solver_mam_basic and solver_mam_basic_mmap need
194// the identical cellsum(sn.visits), and a third body to keep in step is the
195// defect class this file already collapsed once. Re-exported so every caller
196// below is unchanged.
197using basic_detail::station_visits;
198
199/**
200 * f2 on every arc that does not end at a Source, the shared initial state.
201 *
202 * `kRR` is the round-robin split degree of each station-class
203 * (`npfqn_traffic_split_rr`); it is all ones in the closed analyzer, which the
204 * reference does not correct, and the entry then reduces to the plain 1 the
205 * Bernoulli-thinning rule gives at d2 = 1.
206 */
207template <class T>
208Matrix<T> init_flow_scv(const qn::NetworkStruct<T>& L, const std::vector<bool>& is_source,
209 const Matrix<T>& kRR) {
210 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
211 const std::size_t M = L.nstations, K = L.nclasses;
212 Matrix<T> f2(M * K, M * K, zero);
213 for (std::size_t i = 0; i < M; ++i)
214 for (std::size_t j = 0; j < M; ++j) {
215 if (is_source[j]) continue;
216 for (std::size_t r = 0; r < K; ++r)
217 for (std::size_t s = 0; s < K; ++s) {
218 const T p = L.rt(i * K + r, j * K + s);
219 if (p > zero) f2(i * K + r, j * K + s) = T(one + p * T(one - kRR(i, r)));
220 }
221 }
222 return f2;
223}
224
225/**
226 * The superposition step, identical in both analyzers: a1 accumulates the
227 * routed throughput and a2 the rate-weighted mixture of the incoming flow SCVs.
228 */
229template <class T>
230void superpose(const qn::NetworkStruct<T>& L, const Matrix<T>& Tp, const Matrix<T>& f2,
231 Matrix<T>& a1, Matrix<T>& a2) {
232 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
233 const std::size_t M = L.nstations, K = L.nclasses;
234 for (std::size_t i = 0; i < M; ++i) {
235 T lambda_i = zero;
236 for (std::size_t k = 0; k < K; ++k) lambda_i += Tp(i, k);
237 for (std::size_t r = 0; r < K; ++r) {
238 a1(i, r) = zero;
239 a2(i, r) = zero;
240 }
241 for (std::size_t j = 0; j < M; ++j)
242 for (std::size_t r = 0; r < K; ++r)
243 for (std::size_t s = 0; s < K; ++s) {
244 const T p = L.rt(j * K + s, i * K + r);
245 if (!(p > zero)) continue;
246 a1(i, r) = T(a1(i, r) + Tp(j, s) * p);
247 // A station with no throughput divides by zero in MATLAB and
248 // carries the resulting NaN into a2; the guard keeps a2 at
249 // zero there, which is the value every downstream branch
250 // reads once the NaN sweep at the end has run.
251 if (lambda_i > zero)
252 a2(i, r) = T(a2(i, r) + T(one / lambda_i) * f2(j * K + s, i * K + r) *
253 Tp(j, s) * p);
254 }
255 }
256}
257
258/**
259 * The splitting step, identical in both analyzers.
260 *
261 * A flow carrying a fraction p of a stream dispatched one-in-k is the k-fold
262 * convolution thinned at q = k p, hence C^2 = 1 + p (d2 - k); k = 1 is the
263 * Bernoulli-thinned renewal stream and the only case the closed analyzer sees.
264 */
265template <class T>
266void split(const qn::NetworkStruct<T>& L, const std::vector<bool>& is_source,
267 const std::vector<T>& d2, const Matrix<T>& kRR, Matrix<T>& f2) {
268 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
269 const std::size_t M = L.nstations, K = L.nclasses;
270 for (std::size_t i = 0; i < M; ++i)
271 for (std::size_t j = 0; j < M; ++j) {
272 if (is_source[j]) continue;
273 for (std::size_t r = 0; r < K; ++r)
274 for (std::size_t s = 0; s < K; ++s) {
275 const T p = L.rt(i * K + r, j * K + s);
276 if (p > zero) f2(i * K + r, j * K + s) = T(one + p * T(d2[i] - kRR(i, r)));
277 }
278 }
279}
280
281/** Whitt's departure-SCV formula, the `dep_scv = 'qna'` branch of both files. */
282template <class T>
283T qna_departure_scv(const qn::NetworkStruct<T>& L, std::size_t i0, const Matrix<T>& a1,
284 const Matrix<T>& a2, const Matrix<T>& scv, const T& lambda_ist, const T& rho,
285 const T& mi) {
286 using std::sqrt;
287 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
288 const T mubar = (rho > zero) ? T(lambda_ist / rho) : zero;
289 T c2 = T(-one);
290 for (std::size_t r = 0; r < L.nclasses; ++r) {
291 if (L.disabled[i0][r] || !(L.rates(i0, r) > zero) || !(lambda_ist > zero)) continue;
292 const T q = T(mubar / mi / L.rates(i0, r));
293 c2 += T(T(a1(i0, r) / lambda_ist) * q * q * T(scv(i0, r) + one));
294 }
295 T a2sum = zero;
296 for (std::size_t r = 0; r < L.nclasses; ++r) a2sum += a2(i0, r);
297 return T(one + T(rho * rho * T(c2 - one) / sqrt(mi)) + T(T(one - rho * rho) * T(a2sum - one)));
298}
299
300/**
301 * The station's aggregate utilization, as both files compute it: a1 over
302 * FineTol + rates, summed over classes and divided by the server count.
303 *
304 * The FineTol in the denominator is the reference's guard against a zero rate,
305 * and it is why a station whose classes are all disabled reports rho = 0 rather
306 * than a division by zero.
307 */
308template <class T>
309T station_rho(const qn::NetworkStruct<T>& L, std::size_t i0, const Matrix<T>& a1, const T& mi) {
310 const T zero = num_traits<T>::from_int(0);
311 const T ftol = num_traits<T>::from_double(GlobalConstants::FineTol);
312 T rho = zero;
313 for (std::size_t r = 0; r < L.nclasses; ++r) {
314 if (L.disabled[i0][r]) continue; // MATLAB's NaN rate, dropped by isnan
315 rho += T(a1(i0, r) / T(ftol + L.rates(i0, r)));
316 }
317 return T(rho / mi);
318}
319
320/**
321 * The arrival MMAP a station is solved against: one APH per class fitted to
322 * (1/a1, a2), marked as its own class and superposed.
323 *
324 * A class with no inflow contributes the reference's `map_exponential(Inf)`, a
325 * zero-rate order-1 stream. It is carried as a ONE-CLASS MMAP so that the
326 * superposition still delivers K marks -- the queue solver downstream is asked
327 * for K per-class answers, and a mark-free component would silently shorten
328 * that list. `solver_mam_basic.h` reads the same idiom the same way.
329 *
330 * @param bounded true for the closed analyzer, which superposes through
331 * mmap_super_safe under an order budget; the open one calls the
332 * unbounded mmap_super, its own space_max being dead code
333 * @param a1 per-station per-class mean interarrival times; row i0 is read
334 * @param a2 per-station per-class interarrival SCVs; row i0 is read
335 * @param i0 row index of the station whose arrivals are being built
336 * @param K number of classes, and of marks in the returned MMAP
337 * @param space_max order budget of the bounded superposition
338 */
339template <class T>
340Mmap<T> arrival_mmap(const Matrix<T>& a1, const Matrix<T>& a2, std::size_t i0, std::size_t K,
341 bool bounded, std::size_t space_max) {
342 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
343 Mmap<T> node;
344 for (std::size_t k = 0; k < K; ++k) {
345 Mmap<T> cur;
346 if (a1(i0, k) == zero) {
347 cur.D0 = Matrix<T>(1, 1, zero);
348 cur.D1 = Matrix<T>(1, 1, zero);
349 cur.Dc.assign(1, Matrix<T>(1, 1, zero));
350 } else {
351 const Map<T> ph = aph_fit_mean_scv(T(one / a1(i0, k)), a2(i0, k));
352 cur.D0 = ph.D0;
353 cur.D1 = ph.D1;
354 cur.Dc.assign(1, ph.D1);
355 }
356 if (k == 0) node = cur;
357 else if (bounded) node = mmap_super_safe(std::vector<Mmap<T>>{node, cur}, space_max);
358 else node = mmap_super(node, cur);
359 }
360 return node;
361}
362
363// `zero_nans` moved to basic_detail beside `station_visits`, for the same
364// reason: solver_mam_basic_mmap ends with the identical sweep.
365using basic_detail::zero_nans;
366
367} // namespace mna_detail
368
369/**
370 * Port of `solver_mna_open.m`.
371 *
372 * @param L the refreshed struct; open chains only
373 * @param opt SolverMAM's options; `tol` drives the saturation test and doubles
374 * as the fixed point's `iter_tol`, which SolverOptions('MAM') leaves
375 * at the same 1e-4
376 * @param cfg the `options.config` fields the analyzer reads
377 */
378template <class T>
380 const MnaConfig& cfg = MnaConfig()) {
381 if constexpr (!num_traits<T>::has_transcendental) {
382 (void)L;
383 (void)opt;
384 (void)cfg;
385 throw UnsupportedError(
386 "solver_mna_open: the flow fixed point stops on a tolerance, the APH arrival fit takes "
387 "a ceiling of a real reciprocal, and the MMAP[K]/PH[K]/1 station solve runs the ADDA "
388 "doubling iteration; rerun this model with --arith double or --arith real");
389 } else {
390 using namespace mna_detail;
391 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
392 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
393 const T tol = num_traits<T>::from_double(opt.tol);
394
395 check_gates(L, "solver_mna_open");
396 for (const qn::JobClass& c : L.classes)
397 if (std::isfinite(c.population))
398 throw UnsupportedError(
399 "solver_mna_open: MNA's open analyzer takes its arrival rates from the class "
400 "sources; a closed chain has none and belongs to solver_mna_closed");
401 if (cfg.dep_scv == "etaqa")
402 throw UnsupportedError(
403 "solver_mna_open: config.dep_scv = 'etaqa' reads the departure SCV off "
404 "qbd_depproc_jointmom, and the MATLAB routine of that name raises a dimension error "
405 "on EVERY input this branch can hand it -- it slices the level-0 vector as pi(1,:) "
406 "from a QBD_pi that returns one long row, so v0 is numLevels times too long "
407 "(measured: arrival/service orders 1/1, 1/2 and 2/2 all throw). The reference's "
408 "try/catch therefore always falls back to the QNA formula, while the ported "
409 "qbd_depproc_jointmom takes the correct slice and SUCCEEDS; running it here would "
410 "report a departure SCV the reference never produces. Use the default 'qna'");
411 if (cfg.dep_scv != "qna")
412 throw UnsupportedError("solver_mna_open: unknown config.dep_scv '" + cfg.dep_scv +
413 "'; the reference offers 'qna' and 'etaqa'");
414
415 Matrix<T> S(M, K, zero), scv(M, K, zero);
416 for (std::size_t i = 0; i < M; ++i)
417 for (std::size_t r = 0; r < K; ++r) {
418 if (!L.disabled[i][r] && L.rates(i, r) > zero) S(i, r) = T(one / L.rates(i, r));
419 const double v = num_traits<T>::to_double(L.scv(i, r));
420 scv(i, r) = std::isnan(v) ? zero : L.scv(i, r);
421 }
422 const Matrix<T> V = station_visits(L);
423 const std::vector<std::vector<PhService<T>>> svc = service_laws(L);
424
425 std::vector<bool> is_source(M, false);
426 for (std::size_t i = 0; i < M; ++i)
427 is_source[i] = (L.stations[i].nodetype == qn::NodeType::Source);
428 // deterministic (round-robin) split degrees, 1 where the split is Markovian
430 Matrix<T> f2 = init_flow_scv(L, is_source, kRR);
431
432 Matrix<T> Q(M, K, zero), U(M, K, zero), R(M, K, zero), Tp(M, K, zero);
433 Matrix<T> a1(M, K, zero), a2(M, K, zero);
434 std::vector<T> d2(M, zero), lambda(C, zero);
435
436 // ---- the source streams ----------------------------------------------
437 std::vector<T> d2c(C, zero);
438 std::size_t sourceIdx = 0;
439 for (std::size_t c = 0; c < C; ++c) {
440 const std::size_t ref = L.classes[L.inchain[c][0] - 1].refstat;
441 sourceIdx = ref;
442 std::vector<T> lam_in, scv_in;
443 for (std::size_t k : L.inchain[c]) {
444 lam_in.push_back(L.disabled[ref - 1][k - 1] ? zero : L.rates(ref - 1, k - 1));
445 scv_in.push_back(scv(ref - 1, k - 1));
446 }
447 for (const T& x : lam_in)
448 if (std::isfinite(num_traits<T>::to_double(x))) lambda[c] += x;
449 d2c[c] = da::da_traffic_superpos(lam_in, scv_in);
450 for (std::size_t a = 0; a < L.inchain[c].size(); ++a)
451 Tp(ref - 1, L.inchain[c][a] - 1) = lam_in[a];
452 }
453 // `d2(sourceIdx) = d2c(sourceIdx,:)*lambda'/sum(lambda)` runs ONCE, after
454 // the chain loop, so it seeds only the LAST chain's reference station. The
455 // row index into the 1 x C vector d2c is that same station, so MATLAB
456 // itself only evaluates this when the station is the first one.
457 if (sourceIdx != 1)
458 throw UnsupportedError(
459 "solver_mna_open: the reference seeds the source departure SCV with "
460 "d2c(sourceIdx,:), indexing the 1 x nchains vector d2c by the reference STATION " +
461 std::to_string(sourceIdx) +
462 "; MATLAB evaluates that only when the reference station is station 1");
463 {
464 T num = zero, den = zero;
465 for (std::size_t c = 0; c < C; ++c) {
466 num += T(d2c[c] * lambda[c]);
467 den += lambda[c];
468 }
469 if (den > zero) d2[sourceIdx - 1] = T(num / den);
470 }
471
472 // ---- the flow fixed point --------------------------------------------
473 auto sweep = [&](const std::vector<T>&,
474 std::size_t itnum) -> std::pair<std::vector<T>, std::vector<T>> {
475 std::vector<T> xref(2 * M * K, zero);
476 for (std::size_t i = 0; i < M; ++i)
477 for (std::size_t k = 0; k < K; ++k) {
478 xref[i * K + k] = a1(i, k);
479 xref[M * K + i * K + k] = a2(i, k);
480 }
481
482 if (itnum == 1)
483 for (std::size_t c = 0; c < C; ++c)
484 for (std::size_t m = 0; m < M; ++m)
485 for (std::size_t k : L.inchain[c]) Tp(m, k - 1) = T(V(m, k - 1) * lambda[c]);
486
487 superpose(L, Tp, f2, a1, a2);
488
489 // The reference walks the NODES and maps each to its station; no branch
490 // reads another station's iterate, so station order is equivalent.
491 for (std::size_t i = 0; i < M; ++i) {
492 const SchedStrategy sched = L.stations[i].sched;
493 const T mi = num_traits<T>::from_double(L.stations[i].nservers);
494 if (sched == SchedStrategy::INF) {
495 // MATLAB writes d2(ist,s) = a2(ist,s) across classes but every
496 // downstream read is the scalar d2(ist), i.e. column 1.
497 d2[i] = a2(i, 0);
498 for (std::size_t c = 0; c < C; ++c)
499 for (std::size_t k : L.inchain[c]) {
500 const std::size_t r = k - 1;
501 Tp(i, r) = a1(i, r);
502 U(i, r) = T(S(i, r) * Tp(i, r));
503 Q(i, r) = T(Tp(i, r) * S(i, r) * V(i, r));
504 R(i, r) = (Tp(i, r) > zero) ? T(Q(i, r) / Tp(i, r)) : zero;
505 }
506 } else if (sched == SchedStrategy::PS) {
507 // Deliberately empty: see the file header. The reference's PS
508 // branch writes TN/UN/QN/RN, which are not the metrics it
509 // returns, so this station contributes nothing and leaves d2 at
510 // whatever the previous sweep left there.
511 } else if (sched == SchedStrategy::FCFS) {
512 T lambda_ist = zero;
513 for (std::size_t r = 0; r < K; ++r) lambda_ist += a1(i, r);
514 const T rho = station_rho(L, i, a1, mi);
515 if (rho < T(one - tol)) {
516 d2[i] = qna_departure_scv(L, i, a1, a2, scv, lambda_ist, rho, mi);
517 } else {
518 for (std::size_t r = 0; r < K; ++r)
519 Q(i, r) = num_traits<T>::from_double(L.classes[r].population);
520 d2[i] = one;
521 }
522 for (std::size_t r = 0; r < K; ++r) {
523 Tp(i, r) = a1(i, r);
524 U(i, r) = T(Tp(i, r) * S(i, r) / mi);
525 }
526 } else if (sched != SchedStrategy::EXT) {
527 throw UnsupportedError(
528 std::string("solver_mna_open: no isolated-station solution for ") +
529 lang::sched_to_text(sched) + " scheduling at station '" +
530 L.stations[i].name + "'");
531 }
532 }
533
534 split(L, is_source, d2, kRR, f2);
535
536 std::vector<T> xnew(2 * M * K, zero);
537 for (std::size_t i = 0; i < M; ++i)
538 for (std::size_t k = 0; k < K; ++k) {
539 xnew[i * K + k] = a1(i, k);
540 xnew[M * K + i * K + k] = a2(i, k);
541 }
542 return std::make_pair(xnew, xref);
543 };
544
546 fo.iter_max = static_cast<std::size_t>(opt.iter_max) + 1; // the legacy loop ran one extra
547 fo.iter_tol = opt.tol;
548 fo.nanstop = true;
549 const da::FpiResult<T> fr = da::da_fpi<T>(sweep, std::vector<T>(2 * M * K, zero), fo);
550
551 // ---- the matrix-analytic pass over the FCFS stations -------------------
552 for (std::size_t i = 0; i < M; ++i) {
553 if (L.stations[i].sched != SchedStrategy::FCFS) continue;
554 const T mi = num_traits<T>::from_double(L.stations[i].nservers);
555 const T rho = station_rho(L, i, a1, mi);
556 if (!(rho < T(one - tol))) {
557 for (std::size_t r = 0; r < K; ++r) {
558 Q(i, r) = num_traits<T>::from_double(L.classes[r].population);
559 R(i, r) = (Tp(i, r) > zero) ? T(Q(i, r) / Tp(i, r)) : zero;
560 }
561 continue;
562 }
563 const Mmap<T> arv = arrival_mmap(a1, a2, i, K, false, opt.space_max);
564 std::vector<PhService<T>> sl;
565 for (std::size_t r = 0; r < K; ++r) sl.push_back(svc[i][r]);
566
567 if (std::isfinite(L.cap[i])) {
568 const std::size_t capK = static_cast<std::size_t>(std::llround(L.cap[i]));
569 T meanQ = zero, lossProb = zero;
570 const MmckDetection<T> det = mam_detect_mmck(L, i + 1, arv); // 1-based station index
571 if (det.isMmck) {
572 T lamTot = zero;
573 for (std::size_t r = 0; r < K; ++r)
574 if (!L.disabled[i][r]) lamTot += a1(i, r);
576 lamTot, det.muRate, static_cast<unsigned>(std::llround(L.stations[i].nservers)),
577 static_cast<unsigned>(capK));
578 meanQ = ex.meanQueueLength;
579 lossProb = ex.lossProbability;
580 } else {
581 const basic_detail::TruncRenorm<T> tr =
582 basic_detail::truncate_renorm(arv, sl, capK);
583 meanQ = tr.meanQ;
584 lossProb = tr.lossProb;
585 }
586 // Under FCFS the wait in queue is common to every class, so the
587 // aggregate mean queue length yields one Wq and R_k = Wq + S_k.
588 std::vector<T> eff(K, zero);
589 T sumT = zero;
590 for (std::size_t r = 0; r < K; ++r) {
591 const T inflow = L.disabled[i][r] ? zero : a1(i, r);
592 eff[r] = T(inflow * T(one - lossProb));
593 sumT += eff[r];
594 }
595 T Wq = zero;
596 if (sumT > zero) {
597 T sw = zero;
598 for (std::size_t r = 0; r < K; ++r)
599 if (!L.disabled[i][r]) sw += T(eff[r] * S(i, r));
600 const T w = T(T(meanQ / sumT) - T(sw / sumT));
601 Wq = (w > zero) ? w : zero;
602 }
603 for (std::size_t r = 0; r < K; ++r) {
604 Tp(i, r) = eff[r];
605 U(i, r) = T(Tp(i, r) * S(i, r) / mi);
606 if (Tp(i, r) > zero) {
607 R(i, r) = T(Wq + S(i, r));
608 Q(i, r) = T(Tp(i, r) * R(i, r));
609 } else {
610 R(i, r) = zero;
611 Q(i, r) = zero;
612 }
613 }
614 } else {
615 const std::vector<T> m = mmapph1fcfs_ncmean(arv, sl);
616 for (std::size_t r = 0; r < K; ++r) {
617 Q(i, r) = m[arv.classes() == 1 ? 0 : r];
618 R(i, r) = (Tp(i, r) > zero) ? T(Q(i, r) / Tp(i, r)) : zero;
619 }
620 }
621 }
622
624 out.Q = Q;
625 out.U = U;
626 out.R = R;
627 out.Tp = Tp;
628 out.C.assign(K, zero);
629 // X is left at zero: the reference never assigns it. See the file header.
630 out.X.assign(K, zero);
631 for (std::size_t k = 0; k < K; ++k)
632 for (std::size_t i = 0; i < M; ++i) out.C[k] += R(i, k);
633 for (std::size_t i = 0; i < M; ++i)
634 for (std::size_t k = 0; k < K; ++k)
635 if (out.Q(i, k) < zero) out.Q(i, k) = T(-out.Q(i, k));
636 zero_nans(out.Q);
637 zero_nans(out.U);
638 zero_nans(out.R);
639 for (std::size_t k = 0; k < K; ++k)
640 if (std::isnan(num_traits<T>::to_double(out.C[k]))) out.C[k] = zero;
641 out.method = "mna";
642 out.iter = static_cast<int>(fr.iterations);
643 out.lG = 0.0;
644 return out;
645 } // if constexpr has_transcendental
646}
647
648/**
649 * Port of `solver_mna_closed.m`.
650 *
651 * @param L the refreshed struct; closed chains only
652 * @param opt SolverMAM's options; `tol` drives the saturation test and doubles
653 * as both fixed points' `iter_tol`
654 */
655template <class T>
657 if constexpr (!num_traits<T>::has_transcendental) {
658 (void)L;
659 (void)opt;
660 throw UnsupportedError(
661 "solver_mna_closed: the throughput bisection and the inner flow fixed point stop on a "
662 "tolerance, the APH arrival fit takes a ceiling of a real reciprocal, and the "
663 "MMAP[K]/PH[K]/1 station solve runs the ADDA doubling iteration; rerun this model "
664 "with --arith double or --arith real");
665 } else {
666 using namespace mna_detail;
667 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
668 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
669 const T tol = num_traits<T>::from_double(opt.tol);
670 // The reference's local `config.space_max = 16`, the order budget the
671 // per-station arrival superposition is held under.
672 const std::size_t space_max = 16;
673
674 check_gates(L, "solver_mna_closed");
675 if (C != K)
676 throw UnsupportedError(
677 "solver_mna_closed: the reference drives its bisection over classes but stores the "
678 "throughput in the chain-indexed lambda, and renormalizes chain c's queue lengths "
679 "with the class-indexed sn.njobs(c); both are only correct when each chain holds "
680 "exactly one class, and this model has " +
681 std::to_string(C) + " chains over " + std::to_string(K) + " classes");
682 std::vector<double> Npop(K, 0.0);
683 for (std::size_t r = 0; r < K; ++r) {
684 if (!std::isfinite(L.classes[r].population))
685 throw UnsupportedError(
686 "solver_mna_closed: MNA's closed analyzer brackets each class's throughput by its "
687 "population; an open class has none and belongs to solver_mna_open");
688 Npop[r] = L.classes[r].population;
689 }
690
691 Matrix<T> S(M, K, zero), scv(M, K, zero);
692 for (std::size_t i = 0; i < M; ++i)
693 for (std::size_t r = 0; r < K; ++r) {
694 if (!L.disabled[i][r] && L.rates(i, r) > zero) S(i, r) = T(one / L.rates(i, r));
695 const double v = num_traits<T>::to_double(L.scv(i, r));
696 scv(i, r) = std::isnan(v) ? zero : L.scv(i, r);
697 }
698 const Matrix<T> V = station_visits(L);
699 const std::vector<std::vector<PhService<T>>> svc = service_laws(L);
700 std::vector<bool> is_source(M, false);
701 for (std::size_t i = 0; i < M; ++i)
702 is_source[i] = (L.stations[i].nodetype == qn::NodeType::Source);
703 // solver_mna_closed.m applies no round-robin correction: the deterministic
704 // split is carried by the open traffic equations only, and a closed model
705 // that dispatches round-robin is refused before it reaches here
706 // (check_model_method). The all-ones degree keeps the shared helpers on the
707 // Markovian branch, which is what the reference computes.
708 const Matrix<T> kRR_one(M, K, one);
709
710 // ---- the bisection bracket -------------------------------------------
711 // The upper bound is the slowest service the class can meet at a station
712 // that can queue: at that rate the class saturates whatever the routing.
713 std::vector<T> lambda_lb(K, zero), lambda_ub(K, zero), lambda(K, zero);
714 for (std::size_t r = 0; r < K; ++r) {
715 bool any = false;
716 double best = 0.0;
717 for (std::size_t i = 0; i < M; ++i) {
718 if (!std::isfinite(L.stations[i].nservers)) continue;
719 if (L.disabled[i][r]) continue; // MATLAB's NaN rate, dropped by min
720 const double v = num_traits<T>::to_double(L.rates(i, r));
721 if (!any || v < best) {
722 best = v;
723 any = true;
724 }
725 }
726 if (!any)
727 throw UnsupportedError(
728 "solver_mna_closed: class '" + L.classes[r].name +
729 "' is served at no finite-server station, so the reference's throughput upper "
730 "bound min(sn.rates(sn.nservers<Inf,k)) is empty and the assignment fails");
731 lambda_ub[r] = num_traits<T>::from_double(best);
732 }
733
734 Matrix<T> Q(M, K, zero), U(M, K, zero), R(M, K, zero), Tp(M, K, zero);
735 Matrix<T> a1(M, K, zero), a2(M, K, zero), f2(M * K, M * K, zero);
736 std::vector<T> d2(M, zero);
737 std::vector<T> QN(K, zero);
738 std::vector<T> QNc(K, zero);
739 for (std::size_t r = 0; r < K; ++r) QNc[r] = num_traits<T>::from_double(Npop[r]);
740
741 // The maximum queue length any single class can reach, and hence the level
742 // the queue-length distribution is evaluated up to.
743 std::size_t maxLevel = 1;
744 for (std::size_t r = 0; r < K; ++r) maxLevel += static_cast<std::size_t>(std::llround(Npop[r]));
745
746 // ---- the inner flow fixed point --------------------------------------
747 auto flow_sweep = [&](const std::vector<T>&,
748 std::size_t itnum) -> std::pair<std::vector<T>, std::vector<T>> {
749 std::vector<T> xref(2 * M * K, zero);
750 for (std::size_t i = 0; i < M; ++i)
751 for (std::size_t k = 0; k < K; ++k) {
752 xref[i * K + k] = a1(i, k);
753 xref[M * K + i * K + k] = a2(i, k);
754 }
755
756 // THE RENORMALIZATION IS NaN ON THE FIRST SWEEP, and stays NaN at every
757 // FCFS station for the whole inner loop: Q starts at zero, so this is
758 // N * 0 / 0. Nothing downstream depends on it -- the outer sweep
759 // overwrites every FCFS row and the INF/PS branches overwrite their own
760 // -- but the arithmetic is reproduced rather than guarded, because a
761 // guard would feed the FCFS branch a zero it never sees in MATLAB.
762 for (std::size_t c = 0; c < C; ++c) {
763 T colsum = zero;
764 for (std::size_t i = 0; i < M; ++i) colsum += Q(i, c);
765 for (std::size_t i = 0; i < M; ++i)
766 Q(i, c) = num_traits<T>::from_double(Npop[c] * num_traits<T>::to_double(Q(i, c)) /
768 }
769
770 if (itnum == 1)
771 for (std::size_t c = 0; c < C; ++c)
772 for (std::size_t m = 0; m < M; ++m)
773 for (std::size_t k : L.inchain[c]) Tp(m, k - 1) = T(V(m, k - 1) * lambda[c]);
774
775 superpose(L, Tp, f2, a1, a2);
776
777 for (std::size_t i = 0; i < M; ++i) {
778 if (L.stations[i].nodetype == qn::NodeType::Join) continue; // no-op in the reference
779 const SchedStrategy sched = L.stations[i].sched;
780 const T mi = num_traits<T>::from_double(L.stations[i].nservers);
781 if (sched == SchedStrategy::INF) {
782 d2[i] = a2(i, 0);
783 for (std::size_t c = 0; c < C; ++c)
784 for (std::size_t k : L.inchain[c]) {
785 const std::size_t r = k - 1;
786 Tp(i, r) = a1(i, r);
787 U(i, r) = T(S(i, r) * Tp(i, r));
788 Q(i, r) = T(Tp(i, r) * S(i, r) * V(i, r));
789 R(i, r) = (Tp(i, r) > zero) ? T(Q(i, r) / Tp(i, r)) : zero;
790 }
791 } else if (sched == SchedStrategy::PS) {
792 using std::pow;
793 for (std::size_t c = 0; c < C; ++c) {
794 double Nc = 0.0;
795 for (std::size_t k : L.inchain[c]) Nc += Npop[k - 1];
796 for (std::size_t k : L.inchain[c]) {
797 const std::size_t r = k - 1;
798 Tp(i, r) = T(lambda[c] * V(i, r));
799 U(i, r) = T(S(i, r) * Tp(i, r));
800 }
801 T usum = zero;
802 for (std::size_t r = 0; r < K; ++r) usum += U(i, r);
803 const T ftol = num_traits<T>::from_double(GlobalConstants::FineTol);
804 const T uden = (usum < T(one - ftol)) ? usum : T(one - ftol);
805 for (std::size_t k : L.inchain[c]) {
806 const std::size_t r = k - 1;
807 // The finite-population geometric bound: the U^(N+1)
808 // term is what keeps a closed class's queue length from
809 // running past its own population.
810 const T tail = pow(U(i, r), num_traits<T>::from_double(Nc + 1.0));
811 Q(i, r) = T(T(U(i, r) - tail) / T(one - uden));
812 R(i, r) = (Tp(i, r) > zero) ? T(Q(i, r) / Tp(i, r)) : zero;
813 }
814 }
815 } else if (sched == SchedStrategy::FCFS) {
816 T lambda_ist = zero;
817 for (std::size_t r = 0; r < K; ++r) lambda_ist += a1(i, r);
818 const T rho = station_rho(L, i, a1, mi);
819 if (rho < T(one - tol)) {
820 d2[i] = qna_departure_scv(L, i, a1, a2, scv, lambda_ist, rho, mi);
821 } else {
822 for (std::size_t r = 0; r < K; ++r) Q(i, r) = num_traits<T>::from_double(Npop[r]);
823 d2[i] = one;
824 }
825 for (std::size_t r = 0; r < K; ++r) {
826 Tp(i, r) = a1(i, r);
827 U(i, r) = T(Tp(i, r) * S(i, r) / mi);
828 R(i, r) = (Tp(i, r) > zero) ? T(Q(i, r) / Tp(i, r)) : zero;
829 }
830 } else if (sched != SchedStrategy::EXT) {
831 throw UnsupportedError(
832 std::string("solver_mna_closed: no isolated-station solution for ") +
833 lang::sched_to_text(sched) + " scheduling at station '" +
834 L.stations[i].name + "'");
835 }
836 }
837
838 split(L, is_source, d2, kRR_one, f2);
839
840 std::vector<T> xnew(2 * M * K, zero);
841 for (std::size_t i = 0; i < M; ++i)
842 for (std::size_t k = 0; k < K; ++k) {
843 xnew[i * K + k] = a1(i, k);
844 xnew[M * K + i * K + k] = a2(i, k);
845 }
846 return std::make_pair(xnew, xref);
847 };
848
849 // ---- the outer bisection on the per-class throughput -------------------
850 auto outer_sweep = [&](const std::vector<T>&,
851 std::size_t itout) -> std::pair<std::vector<T>, std::vector<T>> {
852 if (itout != 1) {
853 // Too few jobs in the network means the rate was too low, so the
854 // current iterate becomes the new lower bracket, and conversely.
855 for (std::size_t r = 0; r < K; ++r) {
856 if (QN[r] < QNc[r]) lambda_lb[r] = lambda[r];
857 else lambda_ub[r] = lambda[r];
858 lambda[r] = T(T(lambda_ub[r] + lambda_lb[r]) / num_traits<T>::from_int(2));
859 }
860 } else {
861 lambda = lambda_ub;
862 }
863
864 Q = Matrix<T>(M, K, zero);
865 U = Matrix<T>(M, K, zero);
866 R = Matrix<T>(M, K, zero);
867 Tp = Matrix<T>(M, K, zero);
868 a1 = Matrix<T>(M, K, zero);
869 a2 = Matrix<T>(M, K, zero);
870 d2.assign(M, zero);
871 f2 = init_flow_scv(L, is_source, kRR_one);
872
874 io.iter_max = static_cast<std::size_t>(opt.iter_max) + 1;
875 io.iter_tol = opt.tol;
876 io.nanstop = true;
877 da::da_fpi<T>(flow_sweep, std::vector<T>(2 * M * K, zero), io);
878
879 for (std::size_t i = 0; i < M; ++i) {
880 if (L.stations[i].sched != SchedStrategy::FCFS) continue;
881 const T mi = num_traits<T>::from_double(L.stations[i].nservers);
882 const T rho = station_rho(L, i, a1, mi);
883 if (!(rho < T(one - tol))) {
884 for (std::size_t r = 0; r < K; ++r) Q(i, r) = num_traits<T>::from_double(Npop[r]);
885 } else {
886 const Mmap<T> arv = arrival_mmap(a1, a2, i, K, true, space_max);
887 Map<T> probe;
888 probe.D0 = arv.D0;
889 probe.D1 = arv.Dc[0];
890 if (num_traits<T>::to_double(map_lambda(probe)) < GlobalConstants::FineTol) {
891 for (std::size_t r = 0; r < K; ++r)
892 Q(i, r) = (L.rates(i, 0) > zero)
893 ? T(num_traits<T>::from_double(GlobalConstants::FineTol) /
894 L.rates(i, 0))
895 : zero;
896 } else {
897 std::vector<PhService<T>> sl;
898 for (std::size_t r = 0; r < K; ++r) sl.push_back(svc[i][r]);
899 const std::vector<std::vector<T>> pd = mmapph1fcfs_ncdistr(arv, sl, maxLevel);
900 // CLASS 1's marginal is the only one read: the reference
901 // captures a single output from an analyzer that returns
902 // one distribution per class, and truncates that same
903 // vector at each class's own population.
904 const std::vector<T>& pdistr = pd[0];
905 T head = zero;
906 for (std::size_t n = 0; n + 1 < maxLevel; ++n) head += pdistr[n];
907 for (std::size_t r = 0; r < K; ++r) {
908 const std::size_t Nk = static_cast<std::size_t>(std::llround(Npop[r]));
909 std::vector<T> p(Nk + 1, zero);
910 for (std::size_t n = 0; n <= Nk; ++n) p[n] = num_abs(pdistr[n]);
911 p[Nk] = num_abs(T(one - head));
912 T mass = zero;
913 for (std::size_t n = 0; n <= Nk; ++n) mass += p[n];
914 T m = zero;
915 if (mass > zero)
916 for (std::size_t n = 0; n <= Nk; ++n)
917 m += num_traits<T>::from_int((int)n) * T(p[n] / mass);
918 if (m < zero) m = zero;
919 if (num_traits<T>::to_double(m) > static_cast<double>(Nk))
920 m = num_traits<T>::from_int((int)Nk);
921 Q(i, r) = m;
922 }
923 }
924 }
925 for (std::size_t r = 0; r < K; ++r)
926 R(i, r) = (Tp(i, r) > zero) ? T(Q(i, r) / Tp(i, r)) : zero;
927 }
928
929 for (std::size_t r = 0; r < K; ++r) {
930 QN[r] = zero;
931 for (std::size_t i = 0; i < M; ++i) QN[r] += Q(i, r);
932 }
933 return std::make_pair(QN, QNc);
934 };
935
937 fo.iter_max = static_cast<std::size_t>(opt.iter_max);
938 fo.iter_tol = opt.tol;
939 fo.nanstop = true;
940 const da::FpiResult<T> fr = da::da_fpi<T>(outer_sweep, std::vector<T>(K, zero), fo);
941
942 // ---- the terminal renormalization ------------------------------------
943 for (std::size_t c = 0; c < C; ++c) {
944 T colsum = zero;
945 for (std::size_t i = 0; i < M; ++i) colsum += Q(i, c);
946 for (std::size_t i = 0; i < M; ++i)
947 Q(i, c) = num_traits<T>::from_double(Npop[c] * num_traits<T>::to_double(Q(i, c)) /
949 }
950 // An infinite server's utilization IS its queue length.
951 for (std::size_t i = 0; i < M; ++i)
952 if (L.stations[i].sched == SchedStrategy::INF)
953 for (std::size_t r = 0; r < K; ++r) U(i, r) = Q(i, r);
954
956 out.Q = Q;
957 out.U = U;
958 out.R = R;
959 out.Tp = Tp;
960 out.C.assign(K, zero);
961 // X is left at zero: the reference never assigns it. See the file header.
962 out.X.assign(K, zero);
963 for (std::size_t k = 0; k < K; ++k)
964 for (std::size_t i = 0; i < M; ++i) out.C[k] += R(i, k);
965 for (std::size_t i = 0; i < M; ++i)
966 for (std::size_t k = 0; k < K; ++k)
967 if (out.Q(i, k) < zero) out.Q(i, k) = T(-out.Q(i, k));
968 zero_nans(out.Q);
969 zero_nans(out.U);
970 zero_nans(out.R);
971 for (std::size_t k = 0; k < K; ++k)
972 if (std::isnan(num_traits<T>::to_double(out.C[k]))) out.C[k] = zero;
973 out.method = "mna";
974 out.iter = static_cast<int>(fr.iterations);
975 out.lG = 0.0;
976 return out;
977 } // if constexpr has_transcendental
978}
979
980} // namespace mam
981} // namespace line
982
983#endif // LINE_SOLVERS_MAM_SOLVER_MNA_H
Acyclic phase-type fitters from the first two moments.
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t nof_stateful() const
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
bool has_service_law(std::size_t i, std::size_t r) const
Does (station i, class r) have a service law an analyzer may convert?
std::vector< std::vector< bool > > disabled
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
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
Damped fixed-point iteration, the shared driver of the decomposition algorithms.
Superposition of independent renewal flows (Whitt's QNA stationary-interval method).
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.
The MMAP assembly primitives solver_mam_basic.m builds its per-station arrival stream from: mmap_expo...
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
The MMAP[K]/PH[K]/1 FCFS queue: per-class mean number in system and per-class queue-length distributi...
T da_traffic_superpos(const std::vector< T > &lambda, const std::vector< T > &a2)
Superposition of independent renewal flows (Whitt's QNA stationary-interval method).
FpiResult< T > da_fpi(const std::function< std::pair< std::vector< T >, std::vector< T > >(const std::vector< T > &, std::size_t)> &iterfun, const std::vector< T > &x0, const FpiOptions &options=FpiOptions())
Damped fixed-point iteration, the shared driver of the decomposition algorithms.
Definition da_fpi.h:92
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
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
std::vector< T > mmapph1fcfs_ncmean(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc)
Per-class mean number of customers in the system, BUTools' 'ncMoms', 1.
Map< T > aph_fit_mean_scv(const T &mean, const T &scv)
Port of APH.fitMeanAndSCV, the entry point the analyzers fit arrivals with.
MmckDetection< T > mam_detect_mmck(const qn::NetworkStruct< T > &L, std::size_t ist, const Mmap< T > &arv)
Port of mam_detect_mmck.m: is the exact M/M/c/K closed form legitimate at this station?
Map< T > aph_from_2moments(const T &e1, const T &e2)
Port of BUTools' APHFrom2Moments.
std::vector< std::vector< T > > mmapph1fcfs_ncdistr(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, std::size_t levels)
Per-class queue-length distribution, BUTools' 'ncDistr', n: P(N_k = 0..n-1).
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
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
Mmap< T > mmap_super(const Mmap< T > &a, const Mmap< T > &b)
Superposition of two MMAPs: the phase process is the product chain, and the class list of the result ...
Definition mmap_lambda.h:88
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
Mmap< T > mmap_super_safe(const std::vector< Mmap< T > > &in, std::size_t maxorder)
Order-bounded superposition of several MMAPs (mmap_super_safe.m).
Matrix< T > npfqn_traffic_split_rr(const qn::NetworkStruct< T > &sn)
Port of npfqn_traffic_split_rr.m.
MmckResult< T > qsys_mmck(const T &lambda, const T &mu, unsigned c, unsigned K)
Exact analysis of the M/M/c/K queue (truncated Erlang form).
Definition qsys_mmck.h:63
T num_abs(const T &v)
Definition number.h:172
A queueing network and its refreshed NetworkStruct.
Deterministic (round-robin) split degrees of every station-class departure stream.
Exact analysis of the M/M/c/K queue (truncated Erlang form).
Port of solver_mam_basic.m, the dec.source analyzer and the default algorithm of SolverMAM.
The batch-arrival and batch-service queues of the MAM solver, and the two finite-capacity helpers sol...
Options mirroring the fields MATLAB reads off the options struct.
Definition da_fpi.h:50
std::size_t iter_max
Definition da_fpi.h:51
bool nanstop
stop when the increment norm is not finite
Definition da_fpi.h:55
std::size_t iterations
Definition da_fpi.h:78
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:667
The options SolverMAM reads.
Definition mam_types.h:29
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
An MMAP: the underlying MAP plus the per-class arrival matrices.
Definition mmap_lambda.h:45
std::size_t classes() const
Definition mmap_lambda.h:51
Matrix< T > D0
Definition mmap_lambda.h:46
std::vector< Matrix< T > > Dc
per-class matrices, sum_c Dc = D1
Definition mmap_lambda.h:48
What mam_detect_mmck returns; muRate is meaningful only when isMmck.
The options.config fields the two MNA analyzers read.
Definition solver_mna.h:115
std::string dep_scv
config.dep_scv, read only by the OPEN analyzer.
Definition solver_mna.h:120
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
double lG
log of the normalizing constant, the reference's lG.
Definition mva_types.h:118
std::vector< T > C
Definition mva_types.h:98
One job class of the network.
double population
infinite for an open class
T meanQueueLength
L, mean number in system.
Definition qsys_mmck.h:44