LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_basic.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_BASIC_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_BASIC_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_basic.m`, the `dec.source` analyzer and the default
12 * algorithm of SolverMAM.
13 *
14 * THE METHOD. Each queueing station is solved IN ISOLATION as a matrix-analytic
15 * queue, and the stations are coupled only through a fixed point on the
16 * per-chain throughput. The arrival stream a station sees is not derived from
17 * traffic equations: it is the chain's SOURCE process (or, for a closed chain,
18 * a Poisson surrogate at the current throughput iterate) rescaled to the visit
19 * rate of that station. That is what "dec.source" names, and it is why the
20 * method is cheap and why it is an approximation.
21 *
22 * THE FIXED POINT. `lambda(c)` is the chain arrival rate. Open chains have it
23 * fixed by their source. Closed chains start at the no-contention lower bound
24 * N/sum(D) and are then driven by an ITERATION-AVERAGED regula falsi towards
25 * QN = N; the averaging weight walks from the raw Newton step to a no-op as the
26 * iteration count approaches iter_max, which is what damps the oscillation a
27 * bare N/QN step shows on a saturated chain. A purely open or purely closed
28 * network backs off uniformly by 1/Umax when the busiest station saturates; a
29 * MIXED network instead backs off the CLOSED chains alone onto the capacity the
30 * open traffic leaves free, because scaling the open chains too would report a
31 * throughput below their own source rate.
32 *
33 * WHAT EACH STATION GETS. In branch order, as the reference writes them:
34 * INF delay: U = Q = T S, R = S
35 * PS U = T S / c and the M/M/1-PS-style Q = U/(1-Utot)
36 * FCFS / HOL / FCFSPRPRIO (all-distinct class priorities under HOL or
37 * FCFSPRPRIO take BUTools' MMAPPH1NPPR or MMAPPH1PRPR,
38 * api/mam/mmapph1prio.h; identical priorities are plain FCFS)
39 * open: PH/M/c (renewal arrival, exp service, exact), D/M/c, MAP/D/c,
40 * finite buffer (exact M/M/c/K, exact MMAP/G/1/K at one server,
41 * truncate-and-renormalize otherwise), RAP/RAP/1, MAP/MAP/1 for a
42 * correlated single-class service, else MMAP[K]/PH[K]/1 FCFS
43 * closed: the MMAP[K]/PH[K]/1 queue-length DISTRIBUTION truncated at the
44 * class population, which is what makes a closed chain's queue
45 * length respect its own population bound
46 *
47 * THE SURROGATE DELAY. A c-server station is solved as a single server of c
48 * times the speed and the missing T S (c-1)/c jobs are added back afterwards.
49 * The branches that are exact for c > 1 (PH/M/c, D/M/c, MAP/D/c, M/M/c/K) skip
50 * that correction, which is what `mapdcStations` records.
51 *
52 * THE SETUP / DELAY-OFF BRANCHES ARE WRITTEN, both of them, keyed on
53 * NetworkStruct::setupparam (which IS the reference's sn.hassetup). The open
54 * one collapses the station to one M/G/1-with-setup and splits the QBD's queue
55 * back by load share; the closed one is not a QBD at all but a cold-start race,
56 * charging the setup with the probability that the delay-off timer expired
57 * before the job's own think time did. SolverLN routes a setup-task layer
58 * here through `dec.poisson`.
59 *
60 * WHAT THIS PORT DOES NOT REACH, and why it is not a gap. The reference also
61 * carries a self-looping-class clamp and an ME/RAP branch.
62 * `lang/lang_types.h` has no SelfLoopingClass (JobClassType is OPEN or CLOSED)
63 * and no ME or RAP ProcessType, so no model the C++ layer can express reaches
64 * either. They are noted rather than written, because writing an unreachable
65 * branch is writing an untested one.
66 */
67
68#include <algorithm>
69#include <cmath>
70#include <limits>
71#include <iostream>
72#include <string>
73#include <vector>
74
101#include "line/util/error.h"
102#include "line/util/matrix.h"
103
104namespace line {
105namespace mam {
106
107using lang::GlobalConstants;
110
111namespace basic_detail {
112
113/**
114 * True for a process the MAM analyzer can read as a (D0,D1) pair as declared.
115 *
116 * ME AND RAP COUNT. The matrix-geometric algebra never needs D0 to be a
117 * generator -- R solves a quadratic in the blocks and the stationary vector
118 * follows from it -- so a rational arrival process is as solvable as a
119 * Markovian one, which is why `SolverMAM.getFeatureSet` lists both. They are
120 * also exactly what `sn_nonmarkov_toph`'s default two-moment fit produces, so
121 * excluding them would refuse the conversion the analyzer had just performed.
122 * The paths that DO need a generator -- the retrial solver -- test
123 * `is_markovian_map` on the pair itself and refuse there.
124 */
125inline bool is_markovian_type(ProcessType p) {
126 switch (p) {
127 case ProcessType::EXP:
128 case ProcessType::ERLANG:
129 case ProcessType::HYPEREXP:
130 case ProcessType::PH:
131 case ProcessType::APH:
132 case ProcessType::ME:
133 case ProcessType::RAP:
134 case ProcessType::MAP:
135 case ProcessType::MMAP:
136 case ProcessType::MPH:
137 case ProcessType::COXIAN:
138 case ProcessType::COX2:
139 case ProcessType::MMPP2:
140 case ProcessType::IMMEDIATE:
141 case ProcessType::DISABLED:
142 return true;
143 default:
144 return false;
145 }
146}
147
148/**
149 * True when station `i0` should be answered by MMAP[K]/G[K]/1.
150 *
151 * The generic mmapph1fcfs path reads the service law out of the PHASE-TYPE FIT:
152 * for a Uniform, Gamma, Pareto, Weibull, Lognormal or Det that fit matches the
153 * mean and, once the SCV exceeds one, nothing else. He (2001) needs only the
154 * TRANSFORM of the original law, which `lang::dist_lst` evaluates directly off
155 * `L.service`, so wherever a class declares one of those laws the exact analysis
156 * is available. A matrix-exponential service qualifies too, its transform being
157 * rational; a RAP does NOT, He's analysis assuming INDEPENDENT service times, so
158 * reading a correlated service through its marginal transform would discard
159 * exactly the autocorrelation the RAP was declared to carry. Mirrors MATLAB
160 * `mam_gk1_applicable`.
161 */
162template <class T>
163const lang::Distrib<T>& mam_declared_law(const lang::Distrib<T>& d) {
164 // sn_nonmarkov_toph has already replaced a Gamma or a Lognormal by its
165 // phase-type fit and retagged it PH/ME, keeping the original under
166 // `declared`. That original is the law whose transform this path reads.
167 return d.declared ? *d.declared : d;
168}
169
170template <class T>
171bool mam_gk1_applicable(const qn::NetworkStruct<T>& L, std::size_t i0, std::size_t K) {
172 for (std::size_t r = 0; r < K; ++r) {
173 const lang::ProcessType t = mam_declared_law(L.service[i0][r]).type;
178 return true;
179 }
180 }
181 return false;
182}
183
184/**
185 * Port of `mam_is_renewal_map`: D1 = (-D0 e) sigma to within tolerance, i.e.
186 * the phase after an arrival does not depend on the phase before it.
187 */
188template <class T>
189bool is_renewal_map(const Map<T>& m) {
190 const std::size_t n = m.D0.rows();
191 if (n == 1) return true;
192 double scale = 1.0;
193 for (std::size_t i = 0; i < n; ++i)
194 for (std::size_t j = 0; j < n; ++j)
195 scale = std::max(scale, std::fabs(num_traits<T>::to_double(m.D1(i, j))));
196 const double tol = 1e-9 * scale;
197 std::vector<double> exitrate(n, 0.0), sigma(n, 0.0);
198 double tot = 0.0;
199 for (std::size_t i = 0; i < n; ++i) {
200 for (std::size_t j = 0; j < n; ++j) exitrate[i] += num_traits<T>::to_double(m.D1(i, j));
201 tot += exitrate[i];
202 }
203 if (tot <= 0.0) return true;
204 // sigma from the first row with a positive exit rate.
205 std::size_t ref = n;
206 for (std::size_t i = 0; i < n; ++i)
207 if (exitrate[i] > tol) {
208 ref = i;
209 break;
210 }
211 if (ref == n) return true;
212 for (std::size_t j = 0; j < n; ++j)
213 sigma[j] = num_traits<T>::to_double(m.D1(ref, j)) / exitrate[ref];
214 for (std::size_t i = 0; i < n; ++i)
215 for (std::size_t j = 0; j < n; ++j)
216 if (std::fabs(num_traits<T>::to_double(m.D1(i, j)) - exitrate[i] * sigma[j]) > tol)
217 return false;
218 return true;
219}
220
221/**
222 * Port of `mam_corr_service_classes`: the classes whose MAP or MMPP2 service at
223 * station i0 (0-based) is NOT renewal, so successive service times are
224 * autocorrelated. MMAPPH1FCFS, MMAP[K]/G[K]/1 and the PH/c chain read only the
225 * marginal and would silently drop that. PHi is the (possibly 1/c-scaled) service
226 * MAP row; scaling does not change whether it is renewal.
227 */
228template <class T>
229std::vector<bool> corr_service_classes(const qn::NetworkStruct<T>& L, std::size_t i0,
230 const std::vector<Map<T>>& PHi,
231 const std::vector<bool>& known) {
232 const std::size_t K = L.nclasses;
233 std::vector<bool> corr(K, false);
234 for (std::size_t r = 0; r < K; ++r) {
235 const ProcessType pt = L.service[i0][r].type;
236 if (pt != ProcessType::MAP && pt != ProcessType::MMPP2) continue;
237 if (r >= PHi.size() || !known[r] || PHi[r].D0.rows() == 0) continue;
238 corr[r] = !is_renewal_map(PHi[r]);
239 }
240 return corr;
241}
242
243/** The class names flagged in `mask`, comma separated, for a warning. */
244template <class T>
245std::string class_names(const qn::NetworkStruct<T>& L, const std::vector<bool>& mask) {
246 std::string s;
247 for (std::size_t r = 0; r < mask.size(); ++r)
248 if (mask[r]) s += (s.empty() ? "" : ", ") + L.classes[r].name;
249 return s;
250}
251
252/**
253 * Port of `mam_warn_carried`: names what stays approximate at a station whose
254 * correlated MAP service is answered with its phase carried (qsys_mmapmap1).
255 * At c > 1 that runs on the single-fast-server surrogate; a class declaring a
256 * non-phase-type law next to a correlated one is read through its PH fit.
257 */
258template <class T>
259void warn_carried(const qn::NetworkStruct<T>& L, std::size_t i0, const std::vector<bool>& corr) {
260 const std::size_t K = L.nclasses;
261 const double ns = L.stations[i0].nservers;
262 const std::string name = L.stations[i0].name, corrNames = class_names(L, corr);
263 if (ns > 1.0 && K == 1)
264 std::cerr << "[LINE] Warning: Station " << name << " has a correlated MAP service (class "
265 << corrNames << ") at " << ns << " servers, and its exact MAP/MAP/c chain is "
266 << "too large. MAM carries the service phase on the single-fast-server "
267 << "surrogate, which is an approximation.\n";
268 else if (ns > 1.0)
269 std::cerr << "[LINE] Warning: Station " << name << " has a correlated MAP service (class "
270 << corrNames << ") at " << ns << " servers with " << K << " classes. No exact "
271 << "multiclass multiserver analysis exists, so MAM carries each class's service "
272 << "phase across its services on the single-fast-server surrogate, which is an "
273 << "approximation.\n";
274 std::vector<bool> fitted(K, false);
275 bool anyFitted = false;
276 for (std::size_t r = 0; r < K; ++r) {
277 const ProcessType pt = L.service[i0][r].type;
278 fitted[r] = !corr[r] &&
279 (pt == ProcessType::DET || pt == ProcessType::UNIFORM ||
280 pt == ProcessType::GAMMA || pt == ProcessType::PARETO ||
281 pt == ProcessType::WEIBULL || pt == ProcessType::LOGNORMAL ||
282 pt == ProcessType::ME);
283 anyFitted = anyFitted || fitted[r];
284 }
285 if (anyFitted)
286 std::cerr << "[LINE] Warning: Station " << name << " mixes a correlated MAP service (class "
287 << corrNames << ") with the non-phase-type service of class "
288 << class_names(L, fitted) << ", which is read through its phase-type fit: "
289 << "MMAP[K]/G[K]/1 admits only independent service times.\n";
290}
291
292/**
293 * Port of `mam_source_feeds_station`: station `jst` is a Source whose whole
294 * output IS the whole arrival stream of station `ist` (both 1-based).
295 *
296 * The exact GI/M/c closed forms (`qsys_phmc`, `qsys_dmc`) take an INTER-ARRIVAL
297 * law and read it out of some station's service slot. That substitution is only
298 * legitimate when the arrivals at `ist` are, sample path for sample path, the
299 * epochs that station generates: `jst` must be a Source (so its process is an
300 * inter-arrival law and not a service law), all of its flow must reach `ist`
301 * directly, and nothing else may reach `ist`. Anywhere else in a network the
302 * input of a station is a DEPARTURE process, which is not the upstream
303 * station's service law and which only the decomposition produces.
304 *
305 * Both tests are written over `sn_rt_stations` rather than over `sn.rt`,
306 * because `sn.rt` is indexed by stateful node and a Router, Cache or stateful
307 * class switch between the Source and `ist` would otherwise read as a foreign
308 * feeder.
309 */
310template <class T>
311bool mam_source_feeds_station(const qn::NetworkStruct<T>& L, std::size_t jst, std::size_t ist) {
312 const std::size_t M = L.nstations, K = L.nclasses;
313 if (jst < 1 || jst > M || ist < 1 || ist > M || jst == ist) return false;
314 if (L.nodes[L.node_of_station(jst) - 1].nodetype != qn::NodeType::Source) return false;
315
316 const Matrix<T> rtst = api::sn_rt_stations(L).rtst;
317 if (rtst.rows() < M * K) return false;
318 const double tol = GlobalConstants::FineTol;
319
320 // All of the source's flow reaches `ist` directly.
321 double emitted = 0.0;
322 for (std::size_t r = 0; r < K; ++r) {
323 const std::size_t row = (jst - 1) * K + r;
324 double into = 0.0, total = 0.0;
325 for (std::size_t c = 0; c < K; ++c)
326 into += num_traits<T>::to_double(rtst(row, (ist - 1) * K + c));
327 for (std::size_t c = 0; c < rtst.cols(); ++c)
328 total += num_traits<T>::to_double(rtst(row, c));
329 if (std::fabs(into - total) > tol) return false;
330 emitted += total;
331 }
332 if (emitted <= tol) return false;
333
334 // Nothing else reaches `ist`.
335 for (std::size_t kst = 1; kst <= M; ++kst) {
336 if (kst == jst) continue;
337 double into = 0.0;
338 for (std::size_t r = 0; r < K; ++r)
339 for (std::size_t c = 0; c < K; ++c)
340 into += num_traits<T>::to_double(rtst((kst - 1) * K + r, (ist - 1) * K + c));
341 if (into > tol) return false;
342 }
343 return true;
344}
345
346/** True when (D0,D1) is a genuine Markovian pair, as `is_markovian_map` tests. */
347template <class T>
348bool is_markovian_map(const Map<T>& m) {
349 const std::size_t n = m.D0.rows();
350 double scale = 1.0;
351 for (std::size_t i = 0; i < n; ++i)
352 for (std::size_t j = 0; j < n; ++j) {
353 scale = std::max(scale, std::fabs(num_traits<T>::to_double(m.D0(i, j))));
354 scale = std::max(scale, std::fabs(num_traits<T>::to_double(m.D1(i, j))));
355 }
356 const double tol = 1e-9 * scale;
357 for (std::size_t i = 0; i < n; ++i) {
358 double rowsum = 0.0;
359 for (std::size_t j = 0; j < n; ++j) {
360 const double a = num_traits<T>::to_double(m.D0(i, j));
361 const double b = num_traits<T>::to_double(m.D1(i, j));
362 if (i != j && a < -tol) return false;
363 if (b < -tol) return false;
364 rowsum += a + b;
365 }
366 if (std::fabs(rowsum) > tol) return false;
367 }
368 return true;
369}
370
371/** The PH mixture `mam_svc_mixture` builds: alpha = [w_k sigma_k], T = blkdiag(S_k). */
372template <class T>
373qsys::ServiceLaw<T> svc_mixture(const Mmap<T>& arv, const std::vector<PhService<T>>& svc) {
374 const T zero = num_traits<T>::from_int(0);
375 const std::size_t K = svc.size();
376 Matrix<T> Q = arv.D0;
377 for (std::size_t k = 0; k < arv.classes(); ++k)
378 for (std::size_t i = 0; i < Q.rows(); ++i)
379 for (std::size_t j = 0; j < Q.cols(); ++j) Q(i, j) += arv.Dc[k](i, j);
380 const std::vector<T> theta = mc::ctmc_solve(Q);
381 std::vector<T> lam(K, zero);
382 T sumL = zero;
383 for (std::size_t k = 0; k < K && k < arv.classes(); ++k) {
384 const std::vector<T> t = vecmul(theta, arv.Dc[k]);
385 for (const T& v : t) lam[k] += v;
386 sumL += lam[k];
387 }
388 std::vector<T> w(K, T(num_traits<T>::from_int(1) / num_traits<T>::from_int((int)K)));
389 if (sumL > zero)
390 for (std::size_t k = 0; k < K; ++k) w[k] = T(lam[k] / sumL);
391
392 std::size_t ntot = 0;
393 for (const PhService<T>& s : svc) ntot += s.S.rows();
394 std::vector<T> alpha(ntot, zero);
395 Matrix<T> Tm(ntot, ntot, zero);
396 std::size_t off = 0;
397 for (std::size_t k = 0; k < K; ++k) {
398 for (std::size_t i = 0; i < svc[k].S.rows(); ++i) {
399 alpha[off + i] = T(w[k] * svc[k].sigma[i]);
400 for (std::size_t j = 0; j < svc[k].S.cols(); ++j) Tm(off + i, off + j) = svc[k].S(i, j);
401 }
402 off += svc[k].S.rows();
403 }
404 return qsys::ServiceLaw<T>::phase_type(alpha, Tm);
405}
406
407/**
408 * `cellsum(sn.visits)`: V(i,k), the visits summed over the chains, at STATION
409 * level.
410 *
411 * `sn.visits` is stateful-indexed, so the station index has to be mapped
412 * through `stateful_of_station` before the sum -- the two agree only when every
413 * stateful node is a station, which is not the case in a fork-join model. Every
414 * MAM analyzer needs it, so it lives here rather than being written out again in
415 * each; `mna_detail` and `solver_mam_basic_mmap` both call this one.
416 */
417template <class T>
418Matrix<T> station_visits(const qn::NetworkStruct<T>& L) {
419 Matrix<T> V(L.nstations, L.nclasses, num_traits<T>::from_int(0));
420 for (std::size_t c = 0; c < L.nchains; ++c)
421 for (std::size_t i = 0; i < L.nstations; ++i) {
422 const std::size_t sf = L.stateful_of_station(i + 1) - 1;
423 for (std::size_t k = 0; k < L.nclasses; ++k) V(i, k) += L.visits[c](sf, k);
424 }
425 return V;
426}
427
428/** The reference's terminal `X(isnan(X))=0` sweep over the metric matrices. */
429template <class T>
430void zero_nans(Matrix<T>& A) {
431 for (std::size_t i = 0; i < A.rows(); ++i)
432 for (std::size_t j = 0; j < A.cols(); ++j)
433 if (std::isnan(num_traits<T>::to_double(A(i, j)))) A(i, j) = num_traits<T>::from_int(0);
434}
435
436/** Output of `mam_truncate_renorm`. */
437template <class T>
438struct TruncRenorm {
439 T meanQ;
440 T lossProb;
441 std::vector<T> p;
442};
443
444/**
445 * Port of `mam_truncate_renorm`: solve the infinite-buffer MMAP[K]/PH[K]/1
446 * FCFS queue, truncate the AGGREGATE marginal at the buffer capacity and
447 * renormalize.
448 *
449 * Multi-class input is first aggregated to a single-class MMAP/PH/1, because
450 * `ncDistr` returns the PER-CLASS marginal P(N_k = n) while the truncation
451 * needs the joint P(N_total = n).
452 */
453template <class T>
454TruncRenorm<T> truncate_renorm(const Mmap<T>& arv, const std::vector<PhService<T>>& svc,
455 std::size_t capK) {
456 const T zero = num_traits<T>::from_int(0);
457 Mmap<T> call = arv;
458 std::vector<PhService<T>> scall = svc;
459 if (svc.size() > 1) {
460 const qsys::ServiceLaw<T> mix = svc_mixture(arv, svc);
461 Matrix<T> Dsum(arv.order(), arv.order(), zero);
462 for (std::size_t k = 0; k < arv.classes(); ++k)
463 for (std::size_t i = 0; i < Dsum.rows(); ++i)
464 for (std::size_t j = 0; j < Dsum.cols(); ++j) Dsum(i, j) += arv.Dc[k](i, j);
465 call.D0 = arv.D0;
466 call.D1 = Dsum;
467 call.Dc.assign(1, Dsum);
468 scall.assign(1, PhService<T>{mix.ph_alpha, mix.ph_T});
469 }
470 const std::vector<std::vector<T>> d = mmapph1fcfs_ncdistr(call, scall, capK + 1);
471 TruncRenorm<T> out;
472 out.p.assign(capK + 1, zero);
473 T mass = zero;
474 for (std::size_t n = 0; n <= capK; ++n) {
475 out.p[n] = num_abs(d[0][n]);
476 mass += out.p[n];
477 }
478 if (!(mass > zero)) {
479 out.p.assign(capK + 1, zero);
480 out.p[0] = num_traits<T>::from_int(1);
481 } else {
482 for (std::size_t n = 0; n <= capK; ++n) out.p[n] /= mass;
483 }
484 T m = zero;
485 for (std::size_t n = 0; n <= capK; ++n) m += num_traits<T>::from_int((int)n) * out.p[n];
486 if (m < zero) m = zero;
487 if (num_traits<T>::to_double(m) > static_cast<double>(capK))
488 m = num_traits<T>::from_int((int)capK);
489 out.meanQ = m;
490 out.lossProb = out.p[capK];
491 return out;
492}
493
494/**
495 * The setup / delay-off pair of a station, or false when it has none.
496 *
497 * Presence in `setupparam` IS the reference's `sn.hassetup(ist)`, and the
498 * reference reads ONE pair per station (`{end}`, the last class that declares
499 * one) rather than a pair per class.
500 */
501template <class T>
502bool station_setup_pair(const qn::NetworkStruct<T>& L, std::size_t ist, lang::Distrib<T>& setup,
503 lang::Distrib<T>& delayoff) {
504 typename std::map<std::size_t, qn::SetupDelayOffParam<T>>::const_iterator it =
505 L.setupparam.find(ist);
506 if (it == L.setupparam.end()) return false;
507 return it->second.last(setup, delayoff);
508}
509
510/**
511 * The open-class setup/delay-off queue, solver_mam_basic.m:507-547.
512 *
513 * `mu_k` is the per-class service RATE the phase representation already carries
514 * scaled by S/nservers, so rho_k is per-class utilization. The aggregate rate
515 * is defined by Lambda / sum(rho_k), which makes the surrogate's utilization
516 * agree with the station's by construction; the QBD then answers one scalar
517 * mean queue length, split back by load share.
518 */
519template <class T>
520void mam_setup_qbd(const qn::NetworkStruct<T>& L, std::size_t ist,
521 const std::vector<T>& aggrLambda,
522 const std::vector<std::vector<PhService<T>>>& svc, const Matrix<T>& S,
523 const std::vector<std::vector<T>>& rates, std::size_t R, double ns,
524 const lang::Distrib<T>& setup, const lang::Distrib<T>& delayoff,
525 std::vector<T>& Qret) {
526 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
527 const std::size_t i0 = ist - 1, K = L.nclasses;
528 for (std::size_t r = 0; r < K; ++r) Qret[r] = zero;
529 if constexpr (!num_traits<T>::has_transcendental) {
530 throw UnsupportedError(ErrorKind{"Arithmetic"},
531 "solver_mam_basic: a setup/delay-off station is solved by a QBD whose G matrix needs "
532 "a logarithm; rerun with --arith double or real");
533 } else {
534 const Map<T> su = lang::dist_to_map(setup), doff = lang::dist_to_map(delayoff);
535 const T alpharate = map_lambda(su), alphascv = map_scv(su);
536 const T betarate = map_lambda(doff), betascv = map_scv(doff);
537
538 std::vector<T> rho(K, zero);
539 T rho_total = zero;
540 for (std::size_t r = 0; r < K; ++r) {
541 if (L.disabled[i0][r]) continue;
542 // mean of the already-scaled service phase: sigma (-S)^-1 e
543 const Matrix<T>& Sr = svc[i0][r].S;
544 if (Sr.rows() == 0) continue;
545 Matrix<T> negS = Sr;
546 for (std::size_t a = 0; a < negS.rows(); ++a)
547 for (std::size_t b = 0; b < negS.cols(); ++b) negS(a, b) = -negS(a, b);
548 const Matrix<T> negSinv = inverse(negS);
549 T mean = zero;
550 for (std::size_t a = 0; a < negSinv.rows() && a < svc[i0][r].sigma.size(); ++a)
551 for (std::size_t b = 0; b < negSinv.cols(); ++b)
552 mean += svc[i0][r].sigma[a] * negSinv(a, b);
553 if (!(mean > zero)) continue;
554 std::size_t c = L.nchains;
555 for (std::size_t cc = 0; cc < L.nchains; ++cc)
556 if (L.chains[cc][r]) c = cc;
557 if (c == L.nchains) continue;
558 // rho = lambda / mu, and mu is 1/mean of the already-scaled phase
559 rho[r] = T(rates[c][r] * mean);
560 rho_total += rho[r];
561 }
562 if (!(rho_total > zero)) return;
563
564 T lam_total = zero;
565 for (std::size_t r = 0; r < K && r < aggrLambda.size(); ++r) lam_total += aggrLambda[r];
566 if (R == 1 && !aggrLambda.empty()) lam_total = aggrLambda[0];
567 if (!(lam_total > zero)) return;
568 const T aggrRate = T(lam_total / rho_total);
569 const T qtot =
570 mam::qbd_setupdelayoff(lam_total, aggrRate, alpharate, alphascv, betarate, betascv);
571 for (std::size_t r = 0; r < K; ++r)
572 if (rho[r] > zero) Qret[r] = T(qtot * rho[r] / rho_total);
573 (void)S;
574 (void)ns;
575 (void)one;
576 }
577}
578
579/**
580 * The closed-class setup/delay-off queue, solver_mam_basic.m.
581 *
582 * THE CLOSED VACATION QUEUE, SOLVED. What stood here was the per-instance
583 * cold-start race `R = p_cold*E[setup] + S`: it raced the delay-off against the
584 * per-instance idle time and carried NO queueing term, so it described a
585 * serverless instance pool rather than a single-server vacation queue and
586 * reported the SAME response time across a tenfold change in the setup mean
587 * (BUG-78). `qbd_setupdelayoff_closed` solves the finite level-dependent chain
588 * the simulator walks, and the aggregate response it returns is split back over
589 * the classes by R_k = W + S_k -- the decomposition the finite-capacity branch
590 * already uses, and an identity when the chain holds one class.
591 */
592template <class T>
593void mam_setup_closed(const qn::NetworkStruct<T>& L, std::size_t ist,
594 const std::vector<std::vector<PhService<T>>>& svc, const Matrix<T>& S,
595 const std::vector<std::vector<T>>& rates, const std::vector<T>& ztchain,
596 const Matrix<T>& V, double ns, const lang::Distrib<T>& setup,
597 const lang::Distrib<T>& delayoff, const Matrix<T>& QNprev,
598 std::vector<T>& Qret) {
599 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
600 const std::size_t i0 = ist - 1, K = L.nclasses;
601 for (std::size_t r = 0; r < K; ++r) Qret[r] = zero;
602 if constexpr (!num_traits<T>::has_transcendental) {
603 throw UnsupportedError(ErrorKind{"Arithmetic"},
604 "solver_mam_basic: the closed setup/delay-off chain fits a phase-type setup and "
605 "delay-off, which needs a square root; rerun with --arith double or real");
606 } else {
607 const Map<T> su = lang::dist_to_map(setup), doff = lang::dist_to_map(delayoff);
608 const T alpharate = map_lambda(su), alphascv = map_scv(su);
609 const T betarate = map_lambda(doff), betascv = map_scv(doff);
610 const T ft = num_traits<T>::from_double(GlobalConstants::FineTol);
611
612 for (std::size_t c = 0; c < L.nchains; ++c) {
613 T Nc = zero;
614 bool finiteNc = true, anyActive = false;
615 for (std::size_t r = 0; r < K; ++r) {
616 if (!L.chains[c][r]) continue;
617 if (!std::isfinite(L.classes[r].population)) { finiteNc = false; break; }
618 Nc += num_traits<T>::from_double(L.classes[r].population);
619 if (rates[c][r] > zero && std::isfinite(num_traits<T>::to_double(S(i0, r))))
620 anyActive = true;
621 }
622 if (!finiteNc || !anyActive || !(Nc > zero)) continue;
623
624 T vtot = zero;
625 for (std::size_t j = 0; j < K; ++j)
626 if (L.chains[c][j]) vtot += V(i0, j);
627 const T ZT = T(ztchain[c] / (vtot > ft ? vtot : ft));
628
629 // THE COMPLEMENTARY DELAY, not the think demand alone.
630 // lambda(n) = (Nc-n)/Z is exact only when everything away from this
631 // station is a pure delay; with other queues in the network the think
632 // demand OVERSTATES the arrival rate and saturates the station. Z is
633 // the mean time a customer currently spends away,
634 // (Nc - QN_here)/lambda_here at this iterate, floored at ZT so it can
635 // never be shorter than the think time it contains. On a Delay+Queue
636 // the two coincide at convergence.
637 T lamHere = zero, qnHere = zero, tnS = zero, tnTot = zero, svcAny = zero;
638 std::size_t svcAnyCount = 0;
639 for (std::size_t r = 0; r < K; ++r) {
640 if (!L.chains[c][r]) continue;
641 T lamK = rates[c][r];
642 if (!std::isfinite(num_traits<T>::to_double(lamK))) lamK = zero;
643 T sk = S(i0, r);
644 if (!std::isfinite(num_traits<T>::to_double(sk))) sk = zero;
645 lamHere += lamK;
646 if (i0 < QNprev.rows() && r < QNprev.cols() &&
647 std::isfinite(num_traits<T>::to_double(QNprev(i0, r))))
648 qnHere += QNprev(i0, r);
649 tnTot += lamK;
650 tnS += T(lamK * sk);
651 if (sk > zero) { svcAny += sk; ++svcAnyCount; }
652 }
653 T Zc = ZT;
654 if (lamHere > ft && T(Nc - qnHere) > zero) {
655 const T zalt = T(T(Nc - qnHere) / lamHere);
656 Zc = zalt > ZT ? zalt : ZT;
657 }
658 // One server serves the whole chain, so the vacation cycle is a
659 // property of the STATION: the chain is solved on the aggregate.
660 T Sbar = tnTot > ft ? T(tnS / tnTot)
661 : (svcAnyCount > 0
662 ? T(svcAny / num_traits<T>::from_int(
663 static_cast<long>(svcAnyCount)))
664 : zero);
665 if (!(Sbar > ft)) continue;
666 const mam::SetupDelayoffClosed<T> cr = mam::qbd_setupdelayoff_closed(
667 Nc, Zc, T(one / Sbar), alpharate, alphascv, betarate, betascv);
668 if (!std::isfinite(num_traits<T>::to_double(cr.QN)) || !(cr.XN > zero)) continue;
669 T Wq = T(T(cr.QN / cr.XN) - Sbar);
670 if (!(Wq > zero)) Wq = zero;
671 for (std::size_t r = 0; r < K; ++r) {
672 if (!L.chains[c][r]) continue;
673 if (L.disabled[i0][r]) continue;
674 // S/c, NOT S: the caller adds the surrogate-delay jobs
675 // TN*S*(c-1)/c back, so a full S here counts the service term
676 // (2c-1)/c times. The two together make S. At c=1 the division
677 // is the identity. Wq still comes from a SINGLE-SERVER chain, so
678 // a closed multiserver setup station is approximated, not solved.
679 const T cserv = (std::isfinite(ns) && ns >= 1.0)
680 ? num_traits<T>::from_double(ns)
681 : one;
682 if (rates[c][r] > zero && std::isfinite(num_traits<T>::to_double(S(i0, r))))
683 Qret[r] = T(rates[c][r] * T(Wq + T(S(i0, r) / cserv)));
684 }
685 }
686 (void)svc;
687 }
688}
689
690/**
691 * One FCFS / HOL / FCFSPRPRIO station of the decomposition, the inner body of
692 * the reference's `case {SchedStrategy.FCFS, HOL, FCFSPRPRIO}` branch.
693 *
694 * The arrival stream reaching the station is assembled here, chain by chain, in
695 * the reference's order: mark the chain stream by the per-class visit shares,
696 * retarget the per-class mean inter-arrival times, then superpose across
697 * chains. Chain 1 is COLLAPSED to a single mark on the way in -- the reference
698 * writes `{aggr{1} aggr{2} aggr{2}}` after superposing with a zero-rate
699 * exponential -- so the number of marks reaching the queue solver is
700 * 1 + sum_{c>=2} |chain c|, not K. That equals K exactly when chain 1 holds one
701 * class; when it does not, MATLAB itself fails (the utilization line divides a
702 * 1 x R vector by a 1 x K one, and the queue solve requests K outputs from an
703 * R-class analyzer), so the mismatch is refused BY NAME here rather than
704 * answered with a silently different model.
705 */
706template <class T>
707void solve_fcfs_station(const qn::NetworkStruct<T>& L, const MamOptions& opt, std::size_t ist,
708 const std::vector<T>& lambda, const Matrix<T>& V, const Matrix<T>& S,
709 const std::vector<std::vector<bool>>& Sknown,
710 const std::vector<std::vector<Map<T>>>& PH,
711 const std::vector<std::vector<bool>>& PHset,
712 const std::vector<std::vector<PhService<T>>>& svc,
713 const std::vector<Mmap<T>>& chainSysArrivals,
714 const std::vector<T>& ztchain, Matrix<T>& QN, Matrix<T>& UN,
715 Matrix<T>& RN, Matrix<T>& TN, std::vector<bool>& exact_station,
716 std::vector<bool>& car_warned) {
717 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
718 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
719 const double ns = L.stations[ist - 1].nservers;
720 const std::size_t i0 = ist - 1;
721
722 // Distinct class priorities under HOL (FCFSPRPRIO) select BUTools' MMAPPH1NPPR
723 // (MMAPPH1PRPR), mmapph1prio.h; identical priorities fall through to FCFS.
724 const SchedStrategy sched_i = L.stations[i0].sched;
725 bool prio_distinct = false;
726 if (sched_i == SchedStrategy::HOL || sched_i == SchedStrategy::FCFSPRPRIO)
727 for (std::size_t r = 1; r < K; ++r)
728 if (L.classes[r].prio != L.classes[0].prio) prio_distinct = true;
729
730 // rates(ist,:) per chain: the visit rate of every class at this station.
731 std::vector<std::vector<T>> rates(C, std::vector<T>(K, zero));
732 for (std::size_t c = 0; c < C; ++c)
733 for (std::size_t r = 0; r < K; ++r) rates[c][r] = T(V(i0, r) * lambda[c]);
734
735 Mmap<T> aggr;
736 for (std::size_t c = 0; c < C; ++c) {
737 const std::vector<std::size_t>& ic = L.inchain[c];
738 T tot = zero;
739 for (std::size_t r : ic) tot += rates[c][r - 1];
740 Matrix<T> markProb(1, ic.size(), zero);
741 for (std::size_t j = 0; j < ic.size(); ++j)
742 markProb(0, j) = (tot > zero) ? T(rates[c][ic[j] - 1] / tot) : zero;
743 Mmap<T> cur = mmap_mark_probs(chainSysArrivals[c], markProb);
744 // Every arrival process the C++ lang layer can express is Markovian
745 // (there is no ME or RAP ProcessType), so mam_chain_arrival_is_markovian
746 // is unconditionally true and the normalization always applies.
747 cur = mmap_normalize(cur);
748 bool anypos = false;
749 for (std::size_t j = 0; j < ic.size(); ++j)
750 if (rates[c][ic[j] - 1] > zero) anypos = true;
751 if (anypos) {
752 std::vector<T> tgt(ic.size(), zero);
753 for (std::size_t j = 0; j < ic.size(); ++j)
754 tgt[j] = (rates[c][ic[j] - 1] > zero)
755 ? T(one / rates[c][ic[j] - 1])
756 : num_traits<T>::from_double(1.0 / GlobalConstants::Zero);
757 cur = mmap_scale_perclass(cur, tgt);
758 }
759 if (c == 0) {
760 // Chain 1 keeps its own per-class marks; lumping them into one gave
761 // the aggregate the wrong mark count. See _kb/06-solver-catalog.md.
762 aggr = cur;
763 } else {
764 aggr = mmap_super_safe(std::vector<Mmap<T>>{aggr, cur}, opt.space_max);
765 }
766 }
767
768 // The marks come out chain by chain and every reader below indexes them by
769 // CLASS, so permute them into class order.
770 {
771 std::vector<std::size_t> markorder;
772 for (std::size_t c = 0; c < C; ++c)
773 for (std::size_t r : L.inchain[c]) markorder.push_back(r);
774 if (markorder.size() == aggr.Dc.size() &&
775 !std::is_sorted(markorder.begin(), markorder.end())) {
776 std::vector<std::size_t> perm(markorder.size());
777 for (std::size_t j = 0; j < perm.size(); ++j) perm[j] = j;
778 std::stable_sort(perm.begin(), perm.end(),
779 [&markorder](std::size_t a, std::size_t b) {
780 return markorder[a] < markorder[b];
781 });
782 std::vector<Matrix<T>> reordered(perm.size());
783 for (std::size_t j = 0; j < perm.size(); ++j) reordered[j] = aggr.Dc[perm[j]];
784 aggr.Dc.swap(reordered);
785 }
786 }
787
788 const std::size_t R = aggr.classes();
789 lang::Distrib<T> setup_dist, delayoff_dist;
790 const bool has_setup = station_setup_pair(L, ist, setup_dist, delayoff_dist);
791
792 /**
793 * WHO ACTUALLY NEEDS ONE MARK PER CLASS, which is not everyone.
794 *
795 * The loop above superposes every chain, each contributing one mark per
796 * class, and permutes the result into class order, so R is K on any model
797 * whose chains partition the classes. It used to rewrite chain 1 as
798 * `{aggr{1} aggr{2} aggr{2}}` -- ONE aggregate mark, whatever that chain
799 * held -- which made R equal 1 + sum_{c>1}|inchain_c|, and the reference
800 * carried the same defect until 2026-08-16.
801 *
802 * The setup/delay-off branches do not read the marking at all: the closed
803 * one (solver_mam_basic.m:596-650) works from `rates`, `S` and the think
804 * demand, and the open one takes the SCALAR aggregate rate. That is the
805 * shape SolverLN sends here for a setup layer -- a single chain over the
806 * client, entry and call classes -- and MATLAB solves it, so refusing it
807 * was a WRONG REFUSAL and is what made lqn_setup unsolvable in this port.
808 *
809 * MMAPPH1FCFS and the finite-capacity analyzers do read it: each needs the
810 * r-th mark to be the arrival stream of the r-th service law. There the
811 * requirement stands, and it is enforced at those branches instead of here,
812 * so a shape one branch cannot use no longer refuses the ones that can.
813 */
814 const std::string mark_refusal =
815 std::string("solver_mam_basic: the assembled arrival stream at station '") +
816 L.stations[i0].name + "' carries " + std::to_string(R) +
817 " marked classes against the model's " + std::to_string(K) +
818 "; the reference collapses chain 1 to a single mark, so this analyzer has no arrival "
819 "stream to pair with each service law. A class-switching chain at an FCFS station is "
820 "refused rather than solved with a mismatched marking";
821
822 const std::vector<T> aggrLambda = mmap_lambda(aggr);
823 double aggrUtil = 0.0;
824 for (std::size_t r = 0; r < K; ++r) {
825 // A class this station never serves has a NaN rate in the reference and
826 // its term is dropped by 'omitnan'; the flag replaces the sentinel here.
827 if (L.disabled[i0][r]) continue;
828 const double lam = num_traits<T>::to_double(aggrLambda[R == 1 ? 0 : r]);
829 const double mu = num_traits<T>::to_double(L.rates(i0, r));
830 const double den = GlobalConstants::FineTol + mu * ns;
831 if (den > 0.0 && std::isfinite(den)) aggrUtil += lam / den;
832 }
833
834 std::vector<T> Qret(K, zero);
835 bool exact_here = false; // the reference's mapdcUsed
836 T exactRespT = zero;
837 bool finiteCapUsed = false;
838 T finiteCapMeanQ = zero, finiteCapLossProb = zero;
839 std::vector<T> finiteCapLossPerClass;
840
841 bool anyopen = false;
842 for (std::size_t r = 0; r < K; ++r)
843 if (std::isinf(L.classes[r].population)) anyopen = true;
844
845 if (prio_distinct && !car_warned[i0]) {
846 const std::vector<bool> corrP = corr_service_classes(L, i0, PH[i0], PHset[i0]);
847 if (std::find(corrP.begin(), corrP.end(), true) != corrP.end()) {
848 car_warned[i0] = true;
849 std::cerr << "[LINE] Warning: Station " << L.stations[i0].name
850 << " has a correlated MAP service (class " << class_names(L, corrP)
851 << ") under class priorities. The priority analysis reads it through its "
852 << "renewal marginal, which discards the autocorrelation; SolverCTMC "
853 << "carries it exactly.\n";
854 }
855 }
856
857 if (prio_distinct) {
858 // solver_mam_basic.m:359-380. BUTools orders the marks LOWEST priority
859 // first and LINE's lower classprio is the higher priority, so the
860 // classes go in descending classprio; output j belongs to class iK[j].
861 // Neither branch gates on utilization, as in the reference.
862 if (R != K) throw UnsupportedError(ErrorKind{"ClassSwitch"}, mark_refusal);
863 std::vector<std::size_t> iK(K);
864 for (std::size_t r = 0; r < K; ++r) iK[r] = r;
865 std::stable_sort(iK.begin(), iK.end(), [&L](std::size_t a, std::size_t b) {
866 return L.classes[a].prio > L.classes[b].prio;
867 });
868 for (std::size_t j = 1; j < K; ++j)
869 if (L.classes[iK[j]].prio == L.classes[iK[j - 1]].prio)
870 throw UnsupportedError(ErrorKind{"Priority"},
871 "solver_mam_basic: Solver MAM requires either identical priorities or all "
872 "distinct priorities");
873 Mmap<T> pa;
874 pa.D0 = aggr.D0;
875 pa.D1 = aggr.D1;
876 std::vector<PhService<T>> sl;
877 for (std::size_t j = 0; j < K; ++j) {
878 pa.Dc.push_back(aggr.Dc[iK[j]]);
879 sl.push_back(svc[i0][iK[j]]);
880 }
881 const std::vector<std::vector<T>> m = (sched_i == SchedStrategy::FCFSPRPRIO)
882 ? mmapph1prpr_ncmoms(pa, sl, 1)
883 : mmapph1nppr_ncmoms(pa, sl, 1);
884 for (std::size_t j = 0; j < K; ++j) Qret[iK[j]] = m[j][0];
885 } else if (!(aggrUtil < 1.0 - GlobalConstants::FineTol)) {
886 for (std::size_t r = 0; r < K; ++r)
887 Qret[r] = num_traits<T>::from_double(L.classes[r].population);
888 } else if (anyopen) {
889 bool isMapDc = (K == 1) && (L.service[i0][0].type == ProcessType::DET);
890 // D/M/c: the DET law must be the inter-arrival law of the Source feeding
891 // this queue. A DET station elsewhere carries a SERVICE law, and its
892 // departures are not deterministic at all.
893 bool isDMc = false;
894 std::size_t dmcSource = 0;
895 if (K == 1 && !isMapDc && L.service[i0][0].type == ProcessType::EXP) {
896 for (std::size_t j = 1; j <= M; ++j)
897 if (j != ist && L.service[j - 1][0].type == ProcessType::DET &&
898 mam_source_feeds_station(L, j, ist)) {
899 isDMc = true;
900 dmcSource = j;
901 break;
902 }
903 }
904 bool isPhM1 = false;
905 std::size_t phSource = 0;
906 if (K == 1 && !isMapDc && !isDMc && L.service[i0][0].type == ProcessType::EXP &&
907 std::isfinite(ns) && ns >= 1.0) {
908 for (std::size_t j = 1; j <= M; ++j) {
909 if (j == ist) continue;
910 const ProcessType sp = L.service[j - 1][0].type;
911 if (sp != ProcessType::EXP && sp != ProcessType::DET &&
912 sp != ProcessType::IMMEDIATE && sp != ProcessType::DISABLED) {
913 // qsys_phmc reads the arrival through its (pie, D0) marginal
914 // alone, so the gate is RENEWAL and not merely
915 // non-exponential: a correlated MAP would be answered as its
916 // renewal marginal.
917 if (!is_renewal_map(lang::dist_to_map(L.service[j - 1][0]))) continue;
918 // ... and the law must BE the arrival stream here, not some
919 // other station's service law.
920 if (!mam_source_feeds_station(L, j, ist)) continue;
921 isPhM1 = true;
922 phSource = j;
923 break;
924 } else if (sp == ProcessType::EXP && ns > 1.0 && M == 2 &&
925 L.nodes[L.node_of_station(j) - 1].nodetype == qn::NodeType::Source) {
926 // M/M/c: the single-fast-server surrogate is inexact for
927 // c > 1, so take the exact Erlang-C route instead.
928 isPhM1 = true;
929 phSource = j;
930 break;
931 }
932 }
933 }
934 // An infinite-buffer closed form would silently discard sn.cap and
935 // report a lossless queue, so a finite buffer takes precedence. The
936 // gate is a buffer that can BIND, not a finite sn.cap: refreshCapacity
937 // derives one from the chain population for every closed model.
938 const bool isFiniteCap = std::isfinite(sn::sn_get_buffer_size(L, i0 + 1));
939 if (isFiniteCap) {
940 isPhM1 = false;
941 isDMc = false;
942 }
943 // EACH CLOSED FORM IS A SURROGATE THAT MAY NOT APPLY, and the reference
944 // says so by WRAPPING EACH CALL IN `try ... catch <flag> = false`
945 // (solver_mam_basic.m:417-445): the gates above are structural (which
946 // laws are present), while the surrogate's own stability is only known
947 // once it is evaluated. The PH/M/c arm takes ANOTHER station's service
948 // law as the arrival process, so its rho is the ratio of two service
949 // rates and can exceed one on a perfectly stable network -- `oqn_basic`
950 // (Source Exp(0.1) -> Delay HyperExp -> Queue1 Exp(1)) is exactly that,
951 // and this port propagated `qsys_phmc: load rho must be strictly less
952 // than 1` out of SolverMAM instead of answering with the generic
953 // MMAPPH1FCFS path the reference falls back to. The catch is the GATE,
954 // not a mask: a refused surrogate leaves `exact_here` false and the
955 // model is still solved, by the analyzer the reference would use.
956 if (isPhM1) {
957 try {
958 const T muQ = T(one / S(i0, 0));
959 const Map<T> src = lang::dist_to_map(L.service[phSource - 1][0]);
960 const qsys::PhMcResult<T> r = qsys::qsys_phmc(
961 map_pie(src), src.D0, muQ, static_cast<unsigned>(std::llround(ns)));
962 Qret[0] = r.meanQueueLength;
963 exactRespT = r.meanSojournTime;
964 exact_here = true;
965 } catch (const std::exception&) {
966 isPhM1 = false;
967 }
968 }
969 if (!exact_here && isDMc) {
970 try {
971 const T muQ = T(one / S(i0, 0));
972 const qsys::DmcResult<T> r = qsys::qsys_dmc(
973 L.rates(dmcSource - 1, 0), muQ, static_cast<unsigned>(std::llround(ns)));
974 Qret[0] = r.meanQueueLength;
975 exactRespT = r.meanSojournTime;
976 exact_here = true;
977 } catch (const std::exception&) {
978 isDMc = false;
979 }
980 }
981 if (!exact_here && !isFiniteCap && isMapDc) {
982 try {
983 Map<T> arv;
984 arv.D0 = aggr.D0;
985 arv.D1 = aggr.D1;
986 const qsys::MapDcResult<T> r =
987 qsys::qsys_mapdc(arv, S(i0, 0), static_cast<unsigned>(std::llround(ns)));
988 Qret[0] = r.meanQueueLength;
989 exactRespT = r.meanSojournTime;
990 exact_here = true;
991 } catch (const std::exception&) {
992 isMapDc = false;
993 }
994 }
995 // MAP/M/c: the PH/M/c gate above refused this station because its
996 // aggregate arrival stream is NOT renewal. The single-fast-server
997 // surrogate the generic path would use ignores the arrival
998 // correlation; the level-dependent QBD is exact. c = 1 already goes to
999 // the exact MAP/MAP/1 path, so only c > 1 is claimed here.
1000 bool isMapMc = !exact_here && !isFiniteCap && (K == 1) && !isMapDc && !isDMc &&
1001 !isPhM1 && L.service[i0][0].type == ProcessType::EXP &&
1002 std::isfinite(ns) && ns > 1.0;
1003 if (isMapMc) {
1004 try {
1005 Map<T> arv;
1006 arv.D0 = aggr.D0;
1007 arv.D1 = aggr.D1;
1008 const T muQ = T(one / S(i0, 0));
1009 const qsys::MapMcResult<T> r =
1010 qsys::qsys_mapmc(arv, muQ, static_cast<unsigned>(std::llround(ns)));
1011 Qret[0] = r.meanQueueLength;
1012 exactRespT = r.meanSojournTime;
1013 exact_here = true;
1014 } catch (const std::exception&) {
1015 isMapMc = false;
1016 }
1017 }
1018 // Exact MAP/PH/c. The arms above cover c > 1 only for EXPONENTIAL
1019 // service; with a phase-type service law the generic path scales the
1020 // service by nservers and adds a surrogate delay, an approximation that
1021 // discards the service SHAPE. The service must be RENEWAL, since each
1022 // freed server restarts at alpha. DET goes to MAP/D/c above, and ME/RAP
1023 // have no phase-type configuration space at all.
1024 bool isMapPhc = false, isMapMapc = false;
1025 if (!exact_here && !isFiniteCap && K == 1 && std::isfinite(ns) && ns > 1.0) {
1026 const ProcessType st = L.service[i0][0].type;
1027 if (st != ProcessType::EXP && st != ProcessType::DET && st != ProcessType::ME &&
1028 st != ProcessType::RAP && st != ProcessType::IMMEDIATE &&
1029 st != ProcessType::DISABLED) {
1030 isMapPhc = is_renewal_map(lang::dist_to_map(L.service[i0][0]));
1031 // A CORRELATED service at c > 1: the multiset QBD over (current,
1032 // landing) pairs with the carried phase is exact, where the renewal
1033 // marginal would discard the autocorrelation.
1034 isMapMapc = !isMapPhc && Sknown[i0][0];
1035 }
1036 }
1037 if (isMapMapc) {
1038 try {
1039 Map<T> arv;
1040 arv.D0 = aggr.D0;
1041 arv.D1 = aggr.D1;
1042 const Map<T> svcTrue = map_scale(lang::dist_to_map(L.service[i0][0]), S(i0, 0));
1043 const qsys::MapPhcResult<T> r =
1044 qsys::qsys_mapmapc(arv, svcTrue, static_cast<unsigned>(std::llround(ns)),
1045 static_cast<std::size_t>(500), static_cast<std::size_t>(1));
1046 Qret[0] = r.meanQueueLength;
1047 exactRespT = r.meanSojournTime;
1048 exact_here = true;
1049 } catch (const std::exception&) {
1050 // Too many configurations for the level blocks: the station falls
1051 // to the carried-phase surrogate below, which says so by name.
1052 isMapMapc = false;
1053 }
1054 }
1055 if (isMapPhc) {
1056 try {
1057 Map<T> arv;
1058 arv.D0 = aggr.D0;
1059 arv.D1 = aggr.D1;
1060 const Map<T> svc0 = lang::dist_to_map(L.service[i0][0]);
1061 const qsys::MapPhcResult<T> r = qsys::qsys_mapphc(
1062 arv, map_pie(svc0), svc0.D0, static_cast<unsigned>(std::llround(ns)),
1063 static_cast<std::size_t>(500), static_cast<std::size_t>(1), std::vector<T>());
1064 Qret[0] = r.meanQueueLength;
1065 exactRespT = r.meanSojournTime;
1066 exact_here = true;
1067 } catch (const std::exception&) {
1068 isMapPhc = false;
1069 }
1070 }
1071 if (exact_here) {
1072 // one of the closed forms above answered the station
1073 } else if (isFiniteCap) {
1074 // The buffer analyzers split the loss by class, so each mark has to
1075 // be one class's stream; M/M/c/K is exempt, it uses the total rate.
1076 if (R != K && !(aggr.order() == 1)) throw UnsupportedError(ErrorKind{"ClassSwitch"}, mark_refusal);
1077 const std::size_t capK = static_cast<std::size_t>(std::llround(L.cap[i0]));
1078 // mam_detect_mmck: single-phase arrivals and one shared exponential
1079 // service rate across every active class.
1080 bool isMmck = (aggr.order() == 1);
1081 T muMmck = zero;
1082 if (isMmck) {
1083 bool any = false;
1084 double lo = 0.0, hi = 0.0;
1085 for (std::size_t r = 0; r < K; ++r) {
1086 if (L.disabled[i0][r]) continue;
1087 if (L.service[i0][r].type != ProcessType::EXP) {
1088 isMmck = false;
1089 break;
1090 }
1091 const double v = num_traits<T>::to_double(L.rates(i0, r));
1092 if (!(v > 0.0)) continue;
1093 if (!any) {
1094 lo = hi = v;
1095 any = true;
1096 muMmck = L.rates(i0, r);
1097 } else {
1098 lo = std::min(lo, v);
1099 hi = std::max(hi, v);
1100 }
1101 }
1102 if (!any) isMmck = false;
1103 if (isMmck && hi - lo > 1e-9 * std::max(1.0, hi)) isMmck = false;
1104 }
1105 if (isMmck) {
1106 T lamTot = zero;
1107 for (const T& v : aggrLambda) lamTot += v;
1108 const qsys::MmckResult<T> r =
1109 qsys::qsys_mmck(lamTot, muMmck, static_cast<unsigned>(std::llround(ns)),
1110 static_cast<unsigned>(capK));
1111 finiteCapMeanQ = r.meanQueueLength;
1112 finiteCapLossProb = r.lossProbability;
1113 } else if (ns == 1.0) {
1114 // Exact MMAP[K]/G/1/K: the embedded chain at departure epochs
1115 // resolves the buffer level jointly with the arrival phase, so
1116 // each class gets its own loss ratio.
1117 std::vector<PhService<T>> sl;
1118 for (std::size_t r = 0; r < K; ++r) sl.push_back(svc[i0][r]);
1119 const qsys::ServiceLaw<T> mix = svc_mixture(aggr, sl);
1120 const qsys::MmapG1kResult<T> r = qsys::qsys_mmapg1k(aggr.D0, aggr.Dc, mix, capK);
1121 finiteCapMeanQ = r.meanQueueLength;
1122 finiteCapLossProb = r.lossAggregate;
1123 finiteCapLossPerClass = r.lossRatio;
1124 } else {
1125 std::vector<PhService<T>> sl;
1126 for (std::size_t r = 0; r < K; ++r) sl.push_back(svc[i0][r]);
1127 const TruncRenorm<T> r = truncate_renorm(aggr, sl, capK);
1128 finiteCapMeanQ = r.meanQ;
1129 finiteCapLossProb = r.lossProb;
1130 }
1131 finiteCapUsed = true;
1132 exact_station[i0] = true;
1133 } else if (has_setup) {
1134 // OPEN SETUP / DELAY-OFF, solver_mam_basic.m:507-547. The station is
1135 // collapsed to ONE M/G/1 with setup: the aggregate arrival rate, and
1136 // an aggregate service rate chosen so the aggregate utilization is
1137 // exactly the sum of the per-class ones. The queue that QBD returns
1138 // is then split back over the classes by their share of the load.
1139 mam_setup_qbd(L, ist, aggrLambda, svc, S, rates, R, ns, setup_dist, delayoff_dist,
1140 Qret);
1141 } else {
1142 // MMAPPH1FCFS treats service as a renewal phase type, discarding
1143 // service autocorrelation. A single-class single-server station with
1144 // a genuinely correlated service takes the exact MAP/MAP/1 QBD,
1145 // which carries the service phase across departures.
1146 const bool corr =
1147 (K == 1) && (ns == 1.0) && PHset[i0][0] &&
1148 std::fabs(num_traits<T>::to_double(map_acf(PH[i0][0], std::vector<unsigned>{1})[0])) >
1150 // Classes whose MAP service is CORRELATED, which MMAPPH1FCFS and
1151 // MMAP[K]/G[K]/1 would read as i.i.d. and silently decorrelate.
1152 std::vector<bool> corrSvc(K, false);
1153 if (!corr) corrSvc = corr_service_classes(L, i0, PH[i0], PHset[i0]);
1154 const bool useCarried =
1155 std::find(corrSvc.begin(), corrSvc.end(), true) != corrSvc.end();
1156 if (useCarried && !car_warned[i0]) {
1157 car_warned[i0] = true;
1158 warn_carried(L, i0, corrSvc);
1159 }
1160 if (corr) {
1161 Map<T> arv;
1162 arv.D0 = aggr.D0;
1163 arv.D1 = aggr.Dc[0];
1164 const QbdMapMap1Result<T> r = qbd_mapmap1(arv, PH[i0][0]);
1165 Qret[0] = r.QN;
1166 } else if (useCarried) {
1167 // MMAP[K]/MAP[K]/1 with every correlated class's phase carried across
1168 // its own services: exact at one server; at c > 1 it runs on the
1169 // 1/c-scaled surrogate plus the surrogate delay, which warn_carried names.
1170 if (R != K) throw UnsupportedError(ErrorKind{"ClassSwitch"}, mark_refusal);
1171 std::vector<Map<T>> svcK(PH[i0].begin(), PH[i0].begin() + K);
1172 const qsys::MmapMap1Result<T> cr = qsys::qsys_mmapmap1(aggr, svcK, corrSvc);
1173 for (std::size_t r = 0; r < K; ++r) Qret[r] = cr.meanQueueLength[r];
1174 } else {
1175 // One arrival mark per service law, which is what the analyzer
1176 // pairs up; a collapsed marking has no such pairing.
1177 if (R != K) throw UnsupportedError(ErrorKind{"ClassSwitch"}, mark_refusal);
1178 // MMAP[K]/G[K]/1 whenever a class carries a service law that is
1179 // NOT phase type. mmapph1fcfs_ncmean below reads the PH FIT,
1180 // which matches the mean and, above SCV 1, nothing else; He's
1181 // transform analysis takes the ORIGINAL law, which L.service
1182 // still holds. The result is a /1, hence the single-server gate.
1183 bool gk_done = false;
1184 if (ns == 1.0 && mam_gk1_applicable(L, i0, K)) {
1185 try {
1186 std::vector<Matrix<T>> MM;
1187 MM.push_back(aggr.D0);
1188 Matrix<T> D1sum(aggr.D0.rows(), aggr.D0.cols(),
1189 num_traits<T>::from_int(0));
1190 for (std::size_t r = 0; r < K; ++r)
1191 for (std::size_t a = 0; a < D1sum.rows(); ++a)
1192 for (std::size_t b = 0; b < D1sum.cols(); ++b)
1193 D1sum(a, b) += aggr.Dc[r](a, b);
1194 MM.push_back(D1sum);
1195 for (std::size_t r = 0; r < K; ++r) MM.push_back(aggr.Dc[r]);
1196 std::vector<lang::Distrib<T>> laws;
1197 for (std::size_t r = 0; r < K; ++r)
1198 laws.push_back(mam_declared_law(L.service[i0][r]));
1199 const qsys::MmapGk1Result<T> gk = qsys::qsys_mmapgk1(
1200 MM, laws, std::vector<T>(), static_cast<std::size_t>(1), 1e-12,
1201 static_cast<std::size_t>(10000));
1202 for (std::size_t r = 0; r < K; ++r)
1203 Qret[r] = gk.lambdas[r] * gk.meanSojournTime[r];
1204 // NOT exact_here: that flag carries ONE scalar response
1205 // time for the whole station, which the single-class
1206 // arms above own. Here the answer is per class, and with
1207 // ns == 1 the generic tail computes RN = QN/TN and adds
1208 // no surrogate delay, which is exactly right.
1209 gk_done = true;
1210 } catch (const std::exception&) {
1211 gk_done = false;
1212 }
1213 }
1214 if (!gk_done) {
1215 std::vector<PhService<T>> sl;
1216 for (std::size_t r = 0; r < R; ++r) sl.push_back(svc[i0][r]);
1217 const std::vector<T> m = mmapph1fcfs_ncmean(aggr, sl);
1218 for (std::size_t r = 0; r < K; ++r) Qret[r] = m[r];
1219 }
1220 }
1221 }
1222 } else {
1223 // Every class closed. The queue-length DISTRIBUTION is truncated at each
1224 // class's own population, which is what stops the decomposition from
1225 // reporting more jobs than the chain owns.
1226 std::size_t maxLevel = 1;
1227 for (std::size_t r = 0; r < K; ++r)
1228 if (std::isfinite(L.classes[r].population))
1229 maxLevel += static_cast<std::size_t>(std::llround(L.classes[r].population));
1230 // "the station receives no arrivals" is a property of the AGGREGATE
1231 // stream. The reference passes {D0, Dc[0], ...} to map_lambda, which
1232 // reads only its second argument, so it tested CLASS 1 alone and a
1233 // station whose class 1 is disabled fell here however busy the rest was.
1234 const Map<T> probe = aggr.map();
1235 if (num_traits<T>::to_double(map_lambda(probe)) < GlobalConstants::FineTol) {
1236 for (std::size_t r = 0; r < K; ++r)
1237 Qret[r] = (L.rates(i0, 0) > zero)
1238 ? T(num_traits<T>::from_double(GlobalConstants::FineTol) /
1239 L.rates(i0, 0))
1240 : zero;
1241 } else if (has_setup) {
1242 // CLOSED SETUP / DELAY-OFF, solver_mam_basic.m:596-650. No QBD here:
1243 // a closed job returns after its own chain's think time, so what
1244 // matters is the race between that gap and the delay-off timer.
1245 // p_cold is the delay-off LST at 1/ZT, the probability the server
1246 // has already shut down when the job comes back, and the class holds
1247 // (expected setup paid + its own service) jobs by Little.
1248 mam_setup_closed(L, ist, svc, S, rates, ztchain, V, ns, setup_dist, delayoff_dist,
1249 QN, Qret);
1250 } else {
1251 // Same pairing requirement as the open branch above.
1252 if (R != K) throw UnsupportedError(ErrorKind{"ClassSwitch"}, mark_refusal);
1253 // The truncation needs each class's queue-length DISTRIBUTION, and the
1254 // carried-phase marks of one class have dependent counts, so only the mean
1255 // survives: a correlated service is read through its marginal, by name.
1256 const std::vector<bool> corrC = corr_service_classes(L, i0, PH[i0], PHset[i0]);
1257 if (std::find(corrC.begin(), corrC.end(), true) != corrC.end() && !car_warned[i0]) {
1258 car_warned[i0] = true;
1259 std::cerr << "[LINE] Warning: Station " << L.stations[i0].name
1260 << " has a correlated MAP service (class " << class_names(L, corrC)
1261 << ") in a closed chain. The closed decomposition reads it through "
1262 << "its renewal marginal, which discards the autocorrelation; method "
1263 << "'ldqbd' carries it exactly on a single-class Delay+Queue model, "
1264 << "and SolverCTMC on any model.\n";
1265 }
1266 std::vector<PhService<T>> sl;
1267 for (std::size_t r = 0; r < R; ++r) sl.push_back(svc[i0][r]);
1268 const std::vector<std::vector<T>> pd = mmapph1fcfs_ncdistr(aggr, sl, maxLevel);
1269 for (std::size_t r = 0; r < K; ++r) {
1270 const std::size_t Nk =
1271 static_cast<std::size_t>(std::llround(L.classes[r].population));
1272 std::vector<T> p(Nk + 1, zero);
1273 const std::vector<T>& src = pd[r];
1274 T acc = zero;
1275 for (std::size_t n = 0; n < Nk; ++n) {
1276 p[n] = num_abs(src[n]);
1277 acc += p[n];
1278 }
1279 // Truncating at N(k) folds the tail into that level, so the
1280 // complement must be taken over the TRUNCATED vector.
1281 p[Nk] = num_abs(T(one - acc));
1282 T mass = zero;
1283 for (std::size_t n = 0; n <= Nk; ++n) mass += p[n];
1284 T m = zero;
1285 if (mass > zero)
1286 for (std::size_t n = 0; n <= Nk; ++n)
1287 m += num_traits<T>::from_int((int)n) * T(p[n] / mass);
1288 if (m < zero) m = zero;
1289 if (num_traits<T>::to_double(m) > static_cast<double>(Nk))
1290 m = num_traits<T>::from_int((int)Nk);
1291 Qret[r] = m;
1292 }
1293 }
1294 }
1295
1296 // ---- write the station's metrics back --------------------------------
1297 if (finiteCapUsed) {
1298 // Under FCFS the wait in queue is common to every class, so per class
1299 // R_k = Wq + S_k with Wq recovered from the aggregate queue length.
1300 std::vector<T> inflow(K, zero), eff(K, zero);
1301 for (std::size_t r = 0; r < K; ++r) {
1302 std::size_t c = 0;
1303 for (std::size_t cc = 0; cc < C; ++cc)
1304 if (L.chains[cc][r]) {
1305 c = cc;
1306 break;
1307 }
1308 inflow[r] = rates[c][r];
1309 const T loss = finiteCapLossPerClass.empty() ? finiteCapLossProb
1310 : finiteCapLossPerClass[r];
1311 eff[r] = T(inflow[r] * T(one - loss));
1312 }
1313 T sumTN = zero;
1314 for (std::size_t r = 0; r < K; ++r) sumTN += eff[r];
1315 T Wq = zero;
1316 if (sumTN > zero) {
1317 T sw = zero;
1318 for (std::size_t r = 0; r < K; ++r)
1319 if (Sknown[i0][r]) sw += eff[r] * S(i0, r);
1320 const T w = T(T(finiteCapMeanQ / sumTN) - T(sw / sumTN));
1321 Wq = (w > zero) ? w : zero;
1322 }
1323 for (std::size_t r = 0; r < K; ++r) {
1324 TN(i0, r) = eff[r];
1325 UN(i0, r) = T(TN(i0, r) * S(i0, r) / num_traits<T>::from_double(ns));
1326 if (TN(i0, r) > zero) {
1327 RN(i0, r) = T(Wq + S(i0, r));
1328 QN(i0, r) = T(TN(i0, r) * RN(i0, r));
1329 } else {
1330 RN(i0, r) = zero;
1331 QN(i0, r) = zero;
1332 }
1333 }
1334 } else {
1335 for (std::size_t r = 0; r < K; ++r) {
1336 std::size_t c = 0;
1337 for (std::size_t cc = 0; cc < C; ++cc)
1338 if (L.chains[cc][r]) {
1339 c = cc;
1340 break;
1341 }
1342 TN(i0, r) = rates[c][r];
1343 UN(i0, r) = T(TN(i0, r) * S(i0, r) / num_traits<T>::from_double(ns));
1344 QN(i0, r) = Qret[r];
1345 if (exact_here) {
1346 RN(i0, r) = exactRespT;
1347 exact_station[i0] = true;
1348 } else {
1349 // Add back the jobs at the surrogate delay server the c-fold
1350 // service speedup removed.
1351 if (Sknown[i0][r] && std::isfinite(ns))
1352 QN(i0, r) = T(QN(i0, r) + TN(i0, r) * S(i0, r) *
1353 num_traits<T>::from_double((ns - 1.0) / ns));
1354 RN(i0, r) = (TN(i0, r) > zero) ? T(QN(i0, r) / TN(i0, r)) : zero;
1355 }
1356 }
1357 }
1358}
1359
1360} // namespace basic_detail
1361
1362/**
1363 * Port of `solver_mam_basic.m`.
1364 *
1365 * @param L the refreshed struct, with non-Markovian processes already gated
1366 * out by the runner
1367 * @param opt the MAM options; `space_max` is the arrival superposition budget
1368 */
1369template <class T>
1371 if constexpr (!num_traits<T>::has_transcendental) {
1372 throw UnsupportedError(ErrorKind{"Arithmetic"},
1373 "solver_mam_basic: the matrix-analytic station solves run tolerance-terminated "
1374 "iterations (the Riccati doubling behind MMAP[K]/PH[K]/1, the QBD cyclic reduction) "
1375 "and need transcendental arithmetic; rerun this model with --arith double or "
1376 "--arith real");
1377 } else {
1378 using namespace basic_detail;
1379 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1380 const std::size_t I = L.nof_nodes(), M = L.nstations, K = L.nclasses, C = L.nchains;
1381 const double tol = opt.tol;
1382
1383 // ---- service times, visits and chain demands -------------------------
1384 Matrix<T> S(M, K, zero);
1385 std::vector<std::vector<bool>> Sknown(M, std::vector<bool>(K, false));
1386 for (std::size_t i = 0; i < M; ++i)
1387 for (std::size_t r = 0; r < K; ++r)
1388 if (!L.disabled[i][r] && L.rates(i, r) > zero) {
1389 S(i, r) = T(one / L.rates(i, r));
1390 Sknown[i][r] = true;
1391 }
1392 const Matrix<T> V = station_visits(L);
1394 // Think demand of each chain, the sum of its demand at the INFINITE-server
1395 // stations. The closed setup branch divides it by the chain's visits to get
1396 // the gap between two visits of one job, which is what the cold-start race
1397 // is against (solver_mam_basic.m:630).
1398 std::vector<T> ztchain(C, zero);
1399 for (std::size_t c = 0; c < C; ++c)
1400 for (std::size_t i = 0; i < M; ++i)
1401 if (std::isinf(L.stations[i].nservers)) ztchain[c] += dem.Lchain(i, c);
1402
1403 Matrix<T> QN(M, K, zero), UN(M, K, zero), RN(M, K, zero), TN(M, K, zero);
1404 std::vector<T> CN(K, zero), XN(K, zero);
1405 std::vector<bool> exact_station(M, false); // the reference's mapdcStations
1406 std::vector<bool> car_warned(M, false); // one carried-phase warning per station
1407
1408 // ---- per-station service phase-type pairs ----------------------------
1409 // Det is left alone (preserveDet), so the exact MAP/D/c branch can claim it.
1410 std::vector<std::vector<Map<T>>> PH(M, std::vector<Map<T>>(K));
1411 std::vector<std::vector<bool>> PHset(M, std::vector<bool>(K, false));
1412 std::vector<std::vector<PhService<T>>> svc(M, std::vector<PhService<T>>(K));
1413 for (std::size_t i = 0; i < M; ++i) {
1414 const SchedStrategy sc = L.stations[i].sched;
1415 if (!(sc == SchedStrategy::FCFS || sc == SchedStrategy::HOL ||
1416 sc == SchedStrategy::FCFSPRPRIO || sc == SchedStrategy::PS))
1417 continue;
1418 for (std::size_t r = 0; r < K; ++r) {
1419 if (L.service[i][r].type == ProcessType::DET && opt.preserve_det_resolved()) continue;
1420 const double ns = L.stations[i].nservers;
1421 const T target = std::isfinite(ns) ? T(S(i, r) / num_traits<T>::from_double(ns))
1422 : S(i, r);
1423 if (L.disabled[i][r] || !Sknown[i][r] || !(target > zero)) {
1424 // The reference's `any(isnan(D0))` guard: a class this station
1425 // never serves gets an Immediate service rather than a NaN pair,
1426 // so it contributes nothing and cannot poison the phase-type
1427 // checks downstream.
1429 // MATLAB map_exponential takes a MEAN; the rate spelling gives mean 1e-8.
1430 PH[i][r] = map_exponential_mean(imm);
1431 PHset[i][r] = true;
1432 svc[i][r].sigma.assign(1, one);
1433 svc[i][r].S = Matrix<T>(1, 1, T(-imm));
1434 continue;
1435 }
1436 // ME and RAP are rescaled by the rate ALONE (solver_mam_basic.m:82-88):
1437 // map_scale finishes with map_normalize, whose clamp on the negative
1438 // entries is a repair for a MAP and destruction for a matrix
1439 // exponential, whose negative off-diagonals ARE the representation.
1440 const ProcessType pt = L.service[i][r].type;
1441 PH[i][r] = (pt == ProcessType::ME || pt == ProcessType::RAP)
1442 ? map_scale_rate(lang::dist_to_map(L.service[i][r]), target)
1443 : map_scale(lang::dist_to_map(L.service[i][r]), target);
1444 PHset[i][r] = true;
1445 svc[i][r].sigma = map_pie(PH[i][r]);
1446 svc[i][r].S = PH[i][r].D0;
1447 }
1448 }
1449
1450 // ---- chain classification and the open-chain arrival MMAPs -----------
1451 std::vector<bool> isopenchain(C, false), isclosedchain(C, false);
1452 std::vector<T> lambda(C, zero);
1453 std::vector<Mmap<T>> chainSysArrivals(C);
1454 for (std::size_t c = 0; c < C; ++c) {
1455 double tot = 0.0;
1456 bool inf = false;
1457 for (std::size_t r : L.inchain[c]) {
1458 const double n = L.classes[r - 1].population;
1459 if (std::isinf(n)) inf = true;
1460 else tot += n;
1461 }
1462 (void)tot;
1463 isopenchain[c] = inf;
1464 isclosedchain[c] = !inf;
1465 const std::size_t ist = L.classes[L.inchain[c][0] - 1].refstat;
1466 if (!inf) continue;
1467 // Open chain: the arrival stream is the superposition of the per-class
1468 // source processes, each marked as its own class.
1469 std::vector<Mmap<T>> parts;
1470 for (std::size_t r : L.inchain[c]) {
1471 Mmap<T> m;
1472 if (L.disabled[ist - 1][r - 1] || !(L.rates(ist - 1, r - 1) > zero)) {
1473 // No arrivals from this class: the reference substitutes
1474 // map_exponential(Inf), a zero-rate stream.
1475 m.D0 = Matrix<T>(1, 1, zero);
1476 m.D1 = Matrix<T>(1, 1, zero);
1477 m.Dc.assign(1, Matrix<T>(1, 1, zero));
1478 } else {
1479 const Map<T> src = lang::dist_to_map(L.service[ist - 1][r - 1]);
1480 m.D0 = src.D0;
1481 m.D1 = src.D1;
1482 m.Dc.assign(1, src.D1);
1483 lambda[c] += L.rates(ist - 1, r - 1);
1484 }
1485 parts.push_back(m);
1486 }
1487 chainSysArrivals[c] = parts[0];
1488 for (std::size_t p = 1; p < parts.size(); ++p)
1489 chainSysArrivals[c] = mmap_super_safe(
1490 std::vector<Mmap<T>>{chainSysArrivals[c], parts[p]}, opt.space_max);
1491 for (std::size_t r : L.inchain[c])
1492 TN(ist - 1, r - 1) = L.disabled[ist - 1][r - 1] ? zero : L.rates(ist - 1, r - 1);
1493 }
1494
1495 std::vector<bool> finite_srv(M, false);
1496 for (std::size_t i = 0; i < M; ++i) finite_srv[i] = std::isfinite(L.stations[i].nservers);
1497
1498 bool ismixed = false;
1499 {
1500 bool anyc = false, anyo = false;
1501 for (std::size_t c = 0; c < C; ++c) {
1502 anyc = anyc || isclosedchain[c];
1503 anyo = anyo || isopenchain[c];
1504 }
1505 ismixed = anyc && anyo;
1506 }
1507 const double Ulim = 1.0 - GlobalConstants::CoarseTol;
1508
1509 // Floor R at one full service time and restate Q = R*T; see _kb/06-solver-catalog.md (MAM closed-chain population)
1510 std::vector<std::size_t> all_classes(K);
1511 for (std::size_t r = 0; r < K; ++r) all_classes[r] = r + 1;
1512 auto resptime_floor = [&](const std::vector<std::size_t>& rlist) {
1513 for (std::size_t i = 0; i < M; ++i) {
1514 if (exact_station[i]) continue;
1515 for (std::size_t r : rlist) {
1516 const std::size_t k = r - 1;
1517 if (V(i, k) > zero) {
1518 if (!finite_srv[i]) {
1519 RN(i, k) = S(i, k);
1520 } else {
1521 const T byLittle = (TN(i, k) > zero) ? T(QN(i, k) / TN(i, k)) : zero;
1522 RN(i, k) = (S(i, k) > byLittle) ? S(i, k) : byLittle;
1523 }
1524 } else {
1525 RN(i, k) = zero;
1526 }
1527 QN(i, k) = T(RN(i, k) * TN(i, k));
1528 }
1529 }
1530 };
1531
1532 // ---- the throughput fixed point --------------------------------------
1533 Matrix<T> TN_1(M, K, zero);
1534 double delta = std::numeric_limits<double>::infinity();
1535 int it = 0;
1536 while (delta > tol && it <= opt.iter_max) {
1537 ++it;
1538 TN_1 = TN;
1539 double Umax = 0.0;
1540 for (std::size_t i = 0; i < M; ++i) {
1541 if (!finite_srv[i]) continue;
1542 double s = 0.0;
1543 for (std::size_t r = 0; r < K; ++r) s += num_traits<T>::to_double(UN(i, r));
1544 if (s > Umax) Umax = s;
1545 }
1546 if (ismixed || Umax < 1.0) {
1547 for (std::size_t c = 0; c < C; ++c) {
1548 if (!isclosedchain[c]) continue;
1549 double Nc = 0.0;
1550 for (std::size_t r : L.inchain[c]) Nc += L.classes[r - 1].population;
1551 double QNc = 0.0;
1552 for (std::size_t i = 0; i < M; ++i)
1553 for (std::size_t r : L.inchain[c]) QNc += num_traits<T>::to_double(QN(i, r - 1));
1554 QNc = std::max(tol, QNc);
1555 T Dsum = zero;
1556 for (std::size_t i = 0; i < M; ++i) Dsum += dem.Lchain(i, c);
1557 if (it == 1) {
1558 lambda[c] = (Dsum > zero) ? T(num_traits<T>::from_double(Nc) / Dsum) : zero;
1559 } else {
1560 // Iteration-averaged regula falsi: the raw N/QN Newton step
1561 // is blended with a no-op, the no-op's weight growing to one
1562 // at iter_max.
1563 const double w = static_cast<double>(it) / opt.iter_max;
1564 const T step = T(num_traits<T>::from_double(Nc / QNc) * lambda[c]);
1565 lambda[c] = T(lambda[c] * num_traits<T>::from_double(w) +
1566 step * num_traits<T>::from_double(1.0 - w));
1567 }
1568 }
1569 }
1570 if (ismixed) {
1571 double theta = 1.0;
1572 bool binding = false;
1573 for (std::size_t i = 0; i < M; ++i) {
1574 if (!finite_srv[i]) continue;
1575 double Uopen = 0.0, Uclosed = 0.0;
1576 for (std::size_t c = 0; c < C; ++c) {
1577 const double u =
1579 if (isclosedchain[c]) Uclosed += u;
1580 else Uopen += u;
1581 }
1582 if (Uclosed > tol) {
1583 binding = true;
1584 theta = std::min(theta, (Ulim - Uopen) / Uclosed);
1585 }
1586 }
1587 if (binding && theta < 1.0)
1588 for (std::size_t c = 0; c < C; ++c)
1589 if (isclosedchain[c])
1590 lambda[c] = T(lambda[c] * num_traits<T>::from_double(std::max(0.0, theta)));
1591 } else if (Umax >= 1.0) {
1592 for (std::size_t c = 0; c < C; ++c)
1593 lambda[c] = T(lambda[c] / num_traits<T>::from_double(Umax));
1594 }
1595
1596 for (std::size_t c = 0; c < C; ++c) {
1597 if (isclosedchain[c]) {
1598 // Poisson surrogate at the current throughput iterate. An open
1599 // chain keeps its source MMAP, whose rate is re-imposed per
1600 // station by the per-class scaling below.
1601 chainSysArrivals[c] =
1602 mmap_exponential_vec(std::vector<T>(L.inchain[c].size(), lambda[c]), 1);
1603 }
1604 for (std::size_t i = 0; i < M; ++i)
1605 for (std::size_t r : L.inchain[c]) TN(i, r - 1) = T(V(i, r - 1) * lambda[c]);
1606 }
1607
1608 for (std::size_t ind = 1; ind <= I; ++ind) {
1609 const qn::NodeDef& nd = L.nodes[ind - 1];
1610 if (nd.station == 0) continue;
1611 const std::size_t ist = nd.station;
1612 if (nd.nodetype == qn::NodeType::Join) {
1613 for (std::size_t c = 0; c < C; ++c)
1614 for (std::size_t r : L.inchain[c]) {
1615 std::size_t fanin = 0;
1616 if (L.rtnodes.rows() > 0)
1617 for (std::size_t row = 0; row < L.rtnodes.rows(); ++row)
1618 if (L.rtnodes(row, (ind - 1) * K + (r - 1)) != zero) ++fanin;
1619 if (fanin == 0) fanin = 1;
1620 TN(ist - 1, r - 1) =
1621 T(lambda[c] * V(ist - 1, r - 1) / num_traits<T>::from_int((int)fanin));
1622 UN(ist - 1, r - 1) = zero;
1623 QN(ist - 1, r - 1) = zero;
1624 RN(ist - 1, r - 1) = zero;
1625 }
1626 continue;
1627 }
1628 const SchedStrategy sched = L.stations[ist - 1].sched;
1629 const double ns = L.stations[ist - 1].nservers;
1630 if (sched == SchedStrategy::INF) {
1631 for (std::size_t c = 0; c < C; ++c)
1632 for (std::size_t r : L.inchain[c]) {
1633 const std::size_t k = r - 1;
1634 if (!(V(ist - 1, k) > zero)) {
1635 TN(ist - 1, k) = UN(ist - 1, k) = QN(ist - 1, k) = RN(ist - 1, k) = zero;
1636 continue;
1637 }
1638 TN(ist - 1, k) = T(lambda[c] * V(ist - 1, k));
1639 // An infinite server reports U = QLen = T S: dividing by
1640 // Inf would annihilate it.
1641 UN(ist - 1, k) = T(S(ist - 1, k) * TN(ist - 1, k));
1642 QN(ist - 1, k) = T(TN(ist - 1, k) * S(ist - 1, k));
1643 RN(ist - 1, k) = (TN(ist - 1, k) > zero)
1644 ? T(QN(ist - 1, k) / TN(ist - 1, k))
1645 : zero;
1646 }
1647 } else if (sched == SchedStrategy::PS) {
1648 for (std::size_t c = 0; c < C; ++c) {
1649 for (std::size_t r : L.inchain[c]) {
1650 const std::size_t k = r - 1;
1651 if (!(V(ist - 1, k) > zero)) {
1652 TN(ist - 1, k) = UN(ist - 1, k) = zero;
1653 continue;
1654 }
1655 TN(ist - 1, k) = T(lambda[c] * V(ist - 1, k));
1656 UN(ist - 1, k) = T(S(ist - 1, k) * TN(ist - 1, k) /
1658 }
1659 double Uden = 0.0;
1660 for (std::size_t k = 0; k < K; ++k) Uden += num_traits<T>::to_double(UN(ist - 1, k));
1661 Uden = std::min(1.0 - GlobalConstants::FineTol, Uden);
1662 for (std::size_t r : L.inchain[c]) {
1663 const std::size_t k = r - 1;
1664 if (!(V(ist - 1, k) > zero)) {
1665 QN(ist - 1, k) = RN(ist - 1, k) = zero;
1666 continue;
1667 }
1668 QN(ist - 1, k) =
1669 T(UN(ist - 1, k) / num_traits<T>::from_double(1.0 - Uden));
1670 RN(ist - 1, k) = (TN(ist - 1, k) > zero)
1671 ? T(QN(ist - 1, k) / TN(ist - 1, k))
1672 : zero;
1673 }
1674 }
1675 } else if (sched == SchedStrategy::FCFS || sched == SchedStrategy::HOL ||
1676 sched == SchedStrategy::FCFSPRPRIO) {
1677 solve_fcfs_station(L, opt, ist, lambda, V, S, Sknown, PH, PHset, svc,
1678 chainSysArrivals, ztchain, QN, UN, RN, TN, exact_station,
1679 car_warned);
1680 } else if (sched != SchedStrategy::EXT) {
1681 // The Source is skipped: it has no queue of its own, and the
1682 // dispatch overwrites its throughput with sn.rates afterwards.
1683 // The reference's switch has no default arm, so it skips every
1684 // other discipline SILENTLY; the featset gate in the runner
1685 // rejects them first, so reaching here is a defect and is named.
1686 throw UnsupportedError(ErrorKind{"SchedStrategy"},
1687 std::string("solver_mam_basic: station '") + L.stations[ist - 1].name +
1688 "' uses the " + lang::sched_to_text(sched) +
1689 " discipline, which the dec.source decomposition does not model (it solves "
1690 "INF, PS, FCFS and HOL stations only)");
1691 }
1692 }
1693
1694 // Calibrate the fixed point on the REPORTED QN; see _kb/06-solver-catalog.md (MAM closed-chain population)
1695 resptime_floor(all_classes);
1696
1697 delta = 0.0;
1698 for (std::size_t i = 0; i < M; ++i)
1699 for (std::size_t r = 0; r < K; ++r)
1700 delta = std::max(delta, std::fabs(num_traits<T>::to_double(TN(i, r)) -
1701 num_traits<T>::to_double(TN_1(i, r))));
1702 }
1703
1704 // ---- the two rescaling passes ----------------------------------------
1705 for (std::size_t i = 0; i < M; ++i)
1706 for (std::size_t r = 0; r < K; ++r) QN(i, r) = num_abs(QN(i, r));
1707 for (int pass = 0; pass < 2; ++pass) {
1708 for (std::size_t c = 0; c < C; ++c) {
1709 double Nc = 0.0;
1710 bool closed = true;
1711 for (std::size_t r : L.inchain[c]) {
1712 if (std::isinf(L.classes[r - 1].population)) closed = false;
1713 else Nc += L.classes[r - 1].population;
1714 }
1715 if (closed) {
1716 T QNc = zero;
1717 for (std::size_t i = 0; i < M; ++i)
1718 for (std::size_t r : L.inchain[c]) QNc += QN(i, r - 1);
1719 if (QNc > zero) {
1720 const T f = T(num_traits<T>::from_double(Nc) / QNc);
1721 for (std::size_t i = 0; i < M; ++i)
1722 for (std::size_t r : L.inchain[c]) QN(i, r - 1) *= f;
1723 }
1724 }
1725 resptime_floor(L.inchain[c]);
1726 if (closed && Nc == 0.0) {
1727 for (std::size_t i = 0; i < M; ++i)
1728 for (std::size_t r : L.inchain[c]) {
1729 QN(i, r - 1) = UN(i, r - 1) = RN(i, r - 1) = TN(i, r - 1) = zero;
1730 }
1731 for (std::size_t r : L.inchain[c]) CN[r - 1] = XN[r - 1] = zero;
1732 }
1733 }
1734 }
1735
1736 for (std::size_t r = 0; r < K; ++r) {
1737 T s = zero;
1738 for (std::size_t i = 0; i < M; ++i) s += RN(i, r);
1739 CN[r] = s;
1740 }
1741 for (std::size_t c = 0; c < C; ++c)
1742 for (std::size_t r : L.inchain[c]) XN[r - 1] = TN(L.classes[r - 1].refstat - 1, r - 1);
1743
1745 out.Q = QN;
1746 out.U = UN;
1747 out.R = RN;
1748 out.Tp = TN;
1749 out.C = CN;
1750 out.X = XN;
1751 out.method = opt.method;
1752 out.iter = it + 2;
1753 return out;
1754 } // if constexpr has_transcendental
1755}
1756
1757} // namespace mam
1758} // namespace line
1759
1760#endif // LINE_SOLVERS_MAM_SOLVER_MAM_BASIC_H
Acyclic phase-type fitters from the first two moments.
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
std::size_t nof_nodes() const
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
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
std::vector< NodeDef > nodes
every node, in creation order
Steady-state distribution of a continuous-time Markov chain.
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...
MAP constructors and structural transformations.
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...
The MMAP[K]/PH[K]/1 priority queue, preemptive resume (mmapph1prpr_*) and non-preemptive (mmapph1nppr...
SnRtStations< T > sn_rt_stations(const qn::NetworkStruct< T > &sn)
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:235
double tot(const std::vector< double > &pops)
std::string name(const Element *el, const std::string &key="name")
std::vector< T > map_acf(const Map< T > &m, const std::vector< unsigned > &lags)
Autocorrelation coefficients of the inter-arrival times at the given lags,.
Definition map_moment.h:168
Mmap< T > mmap_normalize(const Mmap< T > &in)
Clamp negative off-diagonal and per-class entries to zero and rebuild D1 and the diagonal of D0 from ...
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.
std::vector< T > mmap_lambda(const Mmap< T > &m)
Alias kept for parity with the MATLAB name.
Mmap< T > mmap_mark_probs(const Mmap< T > &in, const Matrix< T > &prob)
Re-mark an MMAP by a (K x R) probability matrix (mmap_mark.m): a type-k arrival is reported as class ...
Mmap< T > mmap_scale_perclass(const Mmap< T > &in, const std::vector< T > &M)
Retarget the per-class MEAN inter-arrival times (mmap_scale.m, vector form).
QbdMapMap1Result< T > qbd_mapmap1(const Map< T > &arrival, const Map< T > &service_in, const T &util, std::size_t max_levels)
MAP/MAP/1 queue (qbd_mapmap1.m).
T qbd_setupdelayoff(const T &lambda, const T &mu, const T &alpharate, const T &alphascv, const T &betarate, const T &betascv)
Mean queue length of the M/M/1 queue with setup delay and delay-off (qbd_setupdelayoff....
Mmap< T > mmap_exponential_vec(const std::vector< T > &lambda, std::size_t n=1)
Order-n MMAP with the given per-class arrival rates (mmap_exponential.m).
mva::MvaSolution< T > solver_mam_basic(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_basic.m.
std::vector< std::vector< T > > mmapph1prpr_ncmoms(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, std::size_t n, const PrioQueueOptions &opt=PrioQueueOptions())
Per-class moments 1..n of the number of jobs, MMAP[K]/PH[K]/1 preemptive resume priority.
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).
Map< T > map_exponential_mean(const T &mean)
Poisson process with the given mean inter-arrival time (map_exponential.m).
Map< T > map_scale_rate(const Map< T > &in, const T &new_mean)
Rescale to a target mean WITHOUT the feasibility repair, for a matrix exponential.
Map< T > map_scale(const Map< T > &in, const T &new_mean)
Rescale time so that the mean inter-arrival time becomes new_mean.
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
T map_scv(const Map< T > &m)
Squared coefficient of variation.
Definition map_moment.h:140
SetupDelayoffClosed< T > qbd_setupdelayoff_closed(const T &N, const T &Z, const T &mu, const T &alpharate, const T &alphascv, const T &betarate, const T &betascv)
Mean queue length and throughput of a FINITE-POPULATION queue with setup delay and delay-off,...
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
std::vector< std::vector< T > > mmapph1nppr_ncmoms(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, std::size_t n, const PrioQueueOptions &opt=PrioQueueOptions())
Per-class moments 1..n of the number of jobs, MMAP[K]/PH[K]/1 non-preemptive priority.
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).
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
MapMcResult< T > qsys_mapmc(const mam::Map< T > &arrival, const T &mu, unsigned c, std::size_t dist_size)
MAP/M/c by the matrix-geometric solution.
Definition qsys_mapmc.h:104
MapPhcResult< T > qsys_mapmapc(const mam::Map< T > &arrival, const mam::Map< T > &service, unsigned c, std::size_t dist_size=500, std::size_t num_w_moms=3)
MAP/MAP/c FCFS with the service phase carried in start order, exactly.
MmapGk1Result< T > qsys_mmapgk1(const std::vector< Matrix< T > > &MMAP, const std::vector< MmapGk1Service< T > > &svc, const std::vector< T > &w_points, std::size_t num_w_moms, double tol, std::size_t iter_max, std::size_t quad_nodes)
MMAP[K]/G[K]/1 FCFS, per type.
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
MapPhcResult< T > qsys_mapphc(const mam::Map< T > &arrival, const std::vector< T > &alpha, const Matrix< T > &S, unsigned c, std::size_t dist_size, std::size_t num_w_moms, const std::vector< T > &w_points)
MAP/PH/c FCFS, exactly.
MmapMap1Result< T > qsys_mmapmap1(const mam::Mmap< T > &arrival, const std::vector< mam::Map< T > > &svc, std::vector< bool > carry=std::vector< bool >())
MMAP[K]/MAP[K]/1 FCFS with per-class carried service phases, exactly.
MmapG1kResult< T > qsys_mmapg1k(const Matrix< T > &D0, const std::vector< Matrix< T > > &D1c, const ServiceLaw< T > &svc, std::size_t K, const T &tol, std::size_t nmaxCap)
MMAP[K]/G/1/K with tail drop.
DmcResult< T > qsys_dmc(const T &lambda, const T &mu, unsigned c, unsigned truncation, unsigned quadSteps)
D/M/c: deterministic interarrival times, exponential service.
Definition qsys_dmc.h:122
PhMcResult< T > qsys_phmc(const std::vector< T > &alpha, const Matrix< T > &Tm, const T &mu, unsigned c, unsigned maxIter, const T &tol)
Exact PH/M/c by Neuts' matrix-geometric method.
Definition qsys_phmc.h:97
MapDcResult< T > qsys_mapdc(const mam::Map< T > &arrival, const T &s, unsigned c, std::size_t dist_size, unsigned max_arrivals, std::size_t max_levels, const T &tol, std::size_t num_steps)
MAP/D/c by Crommelin's exact embedded lattice chain.
Definition qsys_mapdc.h:157
double sn_get_buffer_size(const qn::NetworkStruct< T > &sn, std::size_t ist)
Physical buffer size of a station, in jobs, the one in service included.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:184
std::vector< T > vecmul(const std::vector< T > &v, const Matrix< T > &A)
Row vector times matrix, v A.
Definition linalg.h:50
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
A queueing network and its refreshed NetworkStruct.
The MAP/MAP/1 queue solved as a quasi-birth-death process.
Mean queue length of an M/M/1 queue with a setup delay and a delayed-off period, solved as a QBD.
D/M/c: deterministic interarrival times, exponential service.
The MAP/D/c FCFS queue: c servers, deterministic service of length s, fed by a Markovian arrival proc...
The MAP/MAP/c FCFS queue whose service MAP is CARRIED in service-start order.
The MAP/M/c FCFS queue: c identical exponential servers of rate mu fed by a Markovian arrival process...
The MAP/PH/c FCFS queue, solved exactly.
Exact per-class throughput and loss ratio of an MMAP[K]/G/1/K tail-drop queue.
The MMAP[K]/G[K]/1 FCFS queue: K customer types with class-dependent GENERAL service,...
The MMAP[K]/MAP[K]/1 FCFS queue in which each class's service phase is CARRIED.
Exact analysis of the M/M/c/K queue (truncated Erlang form).
Exact PH/M/c by Neuts' matrix-geometric method.
Chain aggregation and de-aggregation.
Physical buffer size of a station, in jobs, the one in service included.
Port of matlab/src/api/sn/sn_rt_stations.m.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:771
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double CoarseTol
Definition lang_types.h:766
The options SolverMAM reads.
Definition mam_types.h:30
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
Matrix< T > D0
Definition mmap_lambda.h:46
Matrix< T > D1
Definition mmap_lambda.h:47
std::vector< Matrix< T > > Dc
per-class matrices, sum_c Dc = D1
Definition mmap_lambda.h:48
One class's phase-type service law, He's (sigma_k, S_k).
Definition mmapph1fcfs.h:71
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
Matrix< T > Lchain
(M x C) demand
Definition sn_chain.h:47
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
A node of the network.
static ServiceLaw phase_type(const std::vector< T > &alpha, const Matrix< T > &Tmat)
Phase type (alpha, Tmat).