LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_basic_mmap.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_MMAP_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_BASIC_MMAP_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_basic_mmap.m`, `solver_mam_basic_mmap_inner.m` and
12 * `solver_mam_basic_mmap_closed.m`: the MMAP fork-join decomposition, reached as
13 * the `dec.source.mmap` method and as branch 2b of the dispatch (every open
14 * fork-join model that is not in the homogeneous class `solver_mam_fj` serves).
15 *
16 * THE METHOD. It is a PARAMETRIC DECOMPOSITION, not a per-station isolation:
17 * unlike `dec.source`, which hands every station a rescaled copy of the chain's
18 * source process, this one carries a per-node DEPARTURE process table and
19 * recomputes the arrival stream at every node from the traffic equations each
20 * sweep (`solver_mam_traffic_mmap`, the fork-join aware traffic step). The
21 * departure process of an FCFS or PS station is the ETAQA truncation of its own
22 * QBD (`qbd_depproc_etaqa`, `qbd_depproc_etaqa_ps`), so the correlation a queue
23 * introduces travels downstream instead of being discarded. The fixed point is
24 * driven on the station queue lengths by `da_fpi` with a RELATIVE increment
25 * norm, and the reference starts testing convergence only from the third sweep
26 * (`config.da_miniter = 3`).
27 *
28 * WHAT MAKES IT THE FORK-JOIN ANALYZER. Two things, and neither is in
29 * `dec.source`:
30 * - the traffic step SYNCHRONIZES the flows arriving at a join along one sync
31 * group with `mmap_max` rather than superposing them, so the join's output
32 * process is the slowest branch's, blocking included;
33 * - the join's own metrics are derived AFTER the fixed point from the branch
34 * response times, as the expected maximum of independent exponentials with
35 * rates 1/R_b minus their mean. That difference is the synchronization delay,
36 * and `QN = (sum of branch throughputs) * delay` is Little's law on it.
37 *
38 * THE CLOSED WRAPPER has no source to fix the arrival rates, so it wraps the
39 * inner analyzer in a per-class BISECTION on a surrogate arrival rate, driven
40 * against the population, exactly as `solver_mna_closed` does. Three details of
41 * the reference are reproduced rather than tidied: the bracket's upper bound is
42 * the SLOWEST rate over the finite-server stations (the fallback to the
43 * infinite-server ones, and then to 1, is the reference's own); the loop breaks
44 * when every bracket has collapsed below the precision floor, undoing its own
45 * iteration count as it does so; and a diverged inner call is caught, treated as
46 * an overload, and the last successful metrics are restored if the FINAL trial
47 * is the one that diverged.
48 *
49 * SELF-LOOPING CLASSES. `sn.isslc` guards the surrogate-rate zeroing, the queue
50 * clamp and the final throughput pin in the reference's closed wrapper, and the
51 * PS denominator and the FCFS saturation test in the inner analyzer. The C++
52 * `JobClassType` is OPEN or CLOSED only, so no model this port can build enters
53 * them, and they are not transcribed -- the same decision `solver_mna.h` and
54 * `solver_mam_ag.h` record.
55 *
56 * THE REFERENCE'S OWN QUIRKS, REPRODUCED. `XN` is initialised to zeros and never
57 * assigned by either the inner analyzer or the closed wrapper, so the per-class
58 * throughput column is zero however the model is solved; the station
59 * throughputs in `TN` are the real ones. The inner analyzer's `try/catch` around
60 * the ETAQA departure process, and the closed wrapper's around the whole inner
61 * call, are the reference's control flow and not defensive additions: the first
62 * falls back to the scaled service process, the second to a bisection step
63 * downwards.
64 *
65 * ARITHMETIC. Double (or real) only, for the reasons `solver_mam_basic.h` lists:
66 * the fixed point stops on a tolerance, MMAP[K]/PH[K]/1 runs the ADDA doubling
67 * iteration, and both ETAQA departure processes static_assert on transcendental
68 * arithmetic.
69 */
70
71#include <algorithm>
72#include <cmath>
73#include <limits>
74#include <map>
75#include <string>
76#include <utility>
77#include <vector>
78
79#include "line/api/da/da_fpi.h"
92#include "line/solvers/mam/solver_mam_bmap.h" // mam_detect_mmck
94#include "line/util/error.h"
95#include "line/util/matrix.h"
96
97namespace line {
98namespace mam {
99
100/**
101 * The `options.config` fields the MMAP decomposition reads on top of
102 * `MamOptions`.
103 *
104 * Neither is a SolverMAM option: `etaqa_trunc` is defaulted by
105 * `solver_mam_analyzer.m` before the dispatch and `fj_sync_q_len` by
106 * `solver_mam_traffic_mmap.m` itself, so a caller cannot reach either through
107 * the solver's option surface. Kept out of `MamOptions` for that reason, as
108 * `MnaConfig` is.
109 */
111 /** `config.etaqa_trunc`, the ETAQA level truncation of the departure process. */
112 std::size_t etaqa_trunc = 8;
113 /** `config.fj_sync_q_len`, the synchronization queue at a join. */
114 std::size_t fj_sync_q_len = 2;
115};
116
117namespace basic_mmap_detail {
118
121
122/**
123 * The `PH`, `pie` and `D0` tables both MMAP analyzers build before the loop.
124 *
125 * `PH[i][r]` is the service process of class r at station i AFTER the
126 * reference's per-discipline rescaling (divided by the server count at FCFS,
127 * HOL and PS, left alone at INF and at a Source, whose entry is the ARRIVAL
128 * process the reference never overwrites), `svc[i][r]` is the same law as the
129 * (sigma, S) pair `MMAPPH1FCFS` consumes, and `known[i][r]` says whether the
130 * station serves the class at all.
131 *
132 * The NaN guard below is applied at EVERY station, where the reference applies
133 * it only inside its four rescaling branches. The difference is invisible: an
134 * entry it touches is one whose class the station never serves, whose
135 * throughput is therefore zero, so the reference's NaN and this port's mean of
136 * 1e8 both leave the product at zero -- the first through the terminal NaN
137 * sweep, the second directly.
138 *
139 * THE REFERENCE'S NaN GUARD IS INCONSISTENT, AND IS REPRODUCED AS WRITTEN. When
140 * `D0` comes back NaN -- the class is not served here -- it sets
141 * `D0 = -GlobalConstants.Immediate` (an IMMEDIATE service, rate 1e8) but
142 * `PH = map_exponential(GlobalConstants.Immediate)`, and `map_exponential` takes
143 * a MEAN, so that is a service of mean 1e8, the exact opposite. The two are read
144 * in different places -- `D0`/`pie` by the queue solver, `PH` only through
145 * `map_mean` in the utilization and surrogate-delay lines -- and every metric
146 * that reads `PH` multiplies it by a throughput that is identically zero on such
147 * a class, so the contradiction never reaches a reported number. Written the
148 * reference's way rather than tidied, because tidying it would change what
149 * MMAPPH1FCFS is handed.
150 */
151template <class T>
152struct MmapPhTable {
153 std::vector<std::vector<Map<T>>> PH;
154 std::vector<std::vector<PhService<T>>> svc;
155 std::vector<std::vector<bool>> known;
156};
157
158template <class T>
159MmapPhTable<T> mmap_ph_table(const qn::NetworkStruct<T>& L) {
160 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
161 const std::size_t M = L.nstations, K = L.nclasses;
162 const T imm = num_traits<T>::from_double(GlobalConstants::Immediate);
163 MmapPhTable<T> t;
164 t.PH.assign(M, std::vector<Map<T>>(K));
165 t.svc.assign(M, std::vector<PhService<T>>(K));
166 t.known.assign(M, std::vector<bool>(K, false));
167 for (std::size_t i = 0; i < M; ++i) {
168 const SchedStrategy sc = L.stations[i].sched;
169 // EVERY STATION IS FILLED, not only the ones with a branch. The
170 // reference's `PH = sn.proc` starts as the whole table and its station
171 // loop only OVERWRITES the entries it rescales, so a Source keeps its
172 // own ARRIVAL process there -- and both analyzers read it: `dec.mmap`'s
173 // trailing surrogate-delay block runs at every station, and the MMAP
174 // variant seeds DEP from PH at every station node.
175 // FCFS, HOL and PS are the disciplines the reference divides by the
176 // server count; INF is not (an infinite server has no queue to speed
177 // up), and neither is a Source.
178 const bool divide = (sc == SchedStrategy::FCFS || sc == SchedStrategy::HOL ||
179 sc == SchedStrategy::PS);
180 const double ns = L.stations[i].nservers;
181 for (std::size_t r = 0; r < K; ++r) {
182 if (!L.has_service_law(i, r) || !(L.rates(i, r) > zero)) {
183 t.PH[i][r] = map_exponential_mean(imm); // mean 1e8; see the struct note
184 t.svc[i][r].sigma.assign(1, one);
185 t.svc[i][r].S = Matrix<T>(1, 1, T(-imm));
186 continue;
187 }
188 Map<T> ph = lang::dist_to_map(L.service[i][r]);
189 if (divide && std::isfinite(ns) && ns > 0.0)
190 ph = map_scale(ph, T(map_mean(ph) / num_traits<T>::from_double(ns)));
191 t.PH[i][r] = ph;
192 t.svc[i][r].sigma = map_pie(ph);
193 t.svc[i][r].S = ph.D0;
194 t.known[i][r] = true;
195 }
196 }
197 return t;
198}
199
200/**
201 * The expected maximum of independent exponentials, by inclusion-exclusion.
202 *
203 * The reference writes it as
204 * sum_{p=0}^{n-1} (-1)^p sum(1 ./ sum(nchoosek(lambda, p+1), 2))
205 * i.e. the alternating sum over subsets of every size, which is the classical
206 * E[max] = sum_{S nonempty} (-1)^(|S|+1) / sum_{i in S} lambda_i. Enumerated
207 * here over bitmasks, which is the same set of subsets in the same signs; the
208 * cost is 2^n either way, and the reference's own nchoosek is what bounds n in
209 * practice. A join with more than 20 synchronized branches is refused by name
210 * rather than run into a 10^6-term sum.
211 */
212template <class T>
213T exp_max_mean(const std::vector<T>& rate) {
214 const std::size_t n = rate.size();
215 if (n > 20)
216 throw UnsupportedError(
217 "solver_mam_basic_mmap: the join synchronizes " + std::to_string(n) +
218 " branches, and the reference's expected-maximum formula is an alternating sum over "
219 "all 2^n subsets of them; that is not evaluable at this width");
220 const T zero = num_traits<T>::from_int(0);
221 T acc = zero;
222 for (std::size_t mask = 1; mask < (std::size_t(1) << n); ++mask) {
223 T s = zero;
224 std::size_t bits = 0;
225 for (std::size_t i = 0; i < n; ++i)
226 if (mask & (std::size_t(1) << i)) {
227 s += rate[i];
228 ++bits;
229 }
230 if (!(s > zero)) continue;
231 const T term = T(num_traits<T>::from_int(1) / s);
232 if (bits % 2 == 1) acc += term;
233 else acc -= term;
234 }
235 return acc;
236}
237
238/** `mmap_compress(ARV{ind}, config)` under the arithmetic gate the tree uses. */
239template <class T>
240Mmap<T> compress_arrival(const Mmap<T>& m) {
241 if constexpr (num_traits<T>::has_transcendental) {
243 } else {
244 (void)m;
245 throw UnsupportedError(
246 "solver_mam_basic_mmap: the arrival superposition passed config.space_max and must be "
247 "compressed, which fits an APH(2) and needs transcendental arithmetic");
248 }
249}
250
251} // namespace basic_mmap_detail
252
253/**
254 * Port of `solver_mam_basic_mmap_inner.m`.
255 *
256 * @param L the refreshed struct
257 * @param opt the SolverMAM options; `tol` is the fixed point's iter_tol
258 * @param cfg the two `options.config` fields the analyzer defaults itself
259 * @param lambda per-CLASS surrogate arrival rate, the reference's `lambda`
260 * @param totiter out: the sweeps the fixed point took
261 */
262template <class T>
264 const MamOptions& opt, const MmapDecConfig& cfg,
265 const std::vector<T>& lambda,
266 std::size_t* totiter) {
267 if constexpr (!num_traits<T>::has_transcendental) {
268 (void)L; (void)opt; (void)cfg; (void)lambda; (void)totiter;
269 throw UnsupportedError(
270 "solver_mam_basic_mmap_inner: the departure-process fixed point stops on a tolerance, "
271 "MMAP[K]/PH[K]/1 runs the ADDA doubling iteration and the ETAQA departure process "
272 "needs transcendental arithmetic; rerun with --arith double or --arith real");
273 } else {
274 using namespace basic_mmap_detail;
275 using basic_detail::station_visits;
276 using basic_detail::truncate_renorm;
277 using basic_detail::zero_nans;
278 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
279 const std::size_t I = L.nof_nodes(), M = L.nstations, K = L.nclasses;
280 const T ftol = num_traits<T>::from_double(GlobalConstants::FineTol);
281
282 if (lambda.size() != K)
283 throw InputError("solver_mam_basic_mmap_inner: lambda is not indexed over the classes");
284
285 const Matrix<T> V = station_visits(L);
286 Matrix<T> S(M, K, zero);
287 for (std::size_t i = 0; i < M; ++i)
288 for (std::size_t r = 0; r < K; ++r)
289 if (!L.disabled[i][r] && L.rates(i, r) > zero) S(i, r) = T(one / L.rates(i, r));
290
291 const MmapPhTable<T> ph = mmap_ph_table(L);
294 tcfg.fj_sync_q_len = cfg.fj_sync_q_len;
295
296 Matrix<T> QN(M, K, zero), UN(M, K, zero), RN(M, K, zero), TN(M, K, zero);
297 // The Source's throughput is its declared rate and is set once, before the
298 // loop, exactly as the reference's pre-loop station switch does.
299 for (std::size_t i = 0; i < M; ++i)
300 if (L.stations[i].sched == SchedStrategy::EXT)
301 for (std::size_t r = 0; r < K; ++r)
302 TN(i, r) = L.disabled[i][r] ? zero : L.rates(i, r);
303
304 DepTable<T> DEP(I, std::vector<Map<T>>(K));
305
306 auto sweep = [&](const std::vector<T>&,
307 std::size_t itnum) -> std::pair<std::vector<T>, std::vector<T>> {
308 // ---- the departure table, NODE-indexed --------------------------
309 if (itnum == 1) {
310 for (std::size_t ind = 0; ind < I; ++ind) {
311 const qn::NodeType ty = L.nodes[ind].nodetype;
312 const bool isfj = (ty == qn::NodeType::Fork || ty == qn::NodeType::Join);
313 if (L.nodes[ind].station != 0 && !isfj) {
314 const std::size_t ist = L.nodes[ind].station - 1;
315 for (std::size_t r = 0; r < K; ++r) {
316 if (V(ist, r) > zero && lambda[r] > zero)
317 DEP[ind][r] =
318 map_scale(ph.PH[ist][r], T(one / T(lambda[r] * V(ist, r))));
319 else
320 DEP[ind][r] = ph.PH[ist][r];
321 }
322 } else {
323 for (std::size_t r = 0; r < K; ++r)
324 DEP[ind][r] = map_exponential_mean(
325 lambda[r] > zero
326 ? T(one / lambda[r])
327 : T(one / num_traits<T>::from_double(GlobalConstants::Immediate)));
328 }
329 }
330 }
331
332 std::vector<Mmap<T>> ARV = solver_mam_traffic_mmap(L, DEP, tcfg, fj);
333
334 std::vector<T> xref(M * K, zero);
335 for (std::size_t i = 0; i < M; ++i)
336 for (std::size_t r = 0; r < K; ++r) xref[i * K + r] = QN(i, r);
337
338 // ---- one isolated-station solve per station ----------------------
339 for (std::size_t i = 0; i < M; ++i) {
340 const std::size_t ind = L.node_of_station(i + 1) - 1;
341 const qn::NodeType ty = L.nodes[ind].nodetype;
342 const SchedStrategy sc = L.stations[i].sched;
343 const double ns = L.stations[i].nservers;
344
345 if (ty == qn::NodeType::Join) {
346 // Zeroed here and rebuilt from the branch response times after
347 // the fixed point; the throughput is the surrogate rate.
348 for (std::size_t r = 0; r < K; ++r) {
349 TN(i, r) = lambda[r];
350 UN(i, r) = zero;
351 QN(i, r) = zero;
352 RN(i, r) = zero;
353 }
354 continue;
355 }
356 if (ty != qn::NodeType::Queue) {
357 if (sc == SchedStrategy::INF) {
358 if (ARV[ind].order() > 0) {
359 const std::vector<T> lam = mmap_lambda(ARV[ind]);
360 for (std::size_t r = 0; r < K; ++r) TN(i, r) = lam[r];
361 }
362 for (std::size_t r = 0; r < K; ++r)
363 if (TN(i, r) > zero) {
364 UN(i, r) = T(S(i, r) * TN(i, r));
365 QN(i, r) = T(TN(i, r) * S(i, r));
366 RN(i, r) = S(i, r);
367 }
368 }
369 // EXT: the throughput was pinned before the loop.
370 continue;
371 }
372 if (ARV[ind].order() == 0) continue;
373 if (ARV[ind].order() > tcfg.space_max) ARV[ind] = compress_arrival(ARV[ind]);
374
375 bool finiteCapUsed = false;
376 std::vector<PhService<T>> sl;
377 for (std::size_t r = 0; r < K; ++r) sl.push_back(ph.svc[i][r]);
378
379 if (sc == SchedStrategy::FCFS || sc == SchedStrategy::HOL) {
380 if (std::isfinite(L.stations[i].cap)) {
381 const std::size_t capK =
382 static_cast<std::size_t>(std::llround(L.stations[i].cap));
383 T meanQ = zero, lossProb = zero;
384 const MmckDetection<T> det = mam_detect_mmck(L, i + 1, ARV[ind]);
385 if (det.isMmck) {
386 const std::vector<T> lam = mmap_lambda(ARV[ind]);
387 T lamTot = zero;
388 for (const T& v : lam)
389 if (!std::isnan(num_traits<T>::to_double(v))) lamTot += v;
391 lamTot, det.muRate, static_cast<unsigned>(std::llround(ns)),
392 static_cast<unsigned>(capK));
393 meanQ = ex.meanQueueLength;
394 lossProb = ex.lossProbability;
395 } else {
396 const basic_detail::TruncRenorm<T> tr = truncate_renorm(ARV[ind], sl, capK);
397 meanQ = tr.meanQ;
398 lossProb = tr.lossProb;
399 }
400 const std::vector<T> lam = mmap_lambda(ARV[ind]);
401 std::vector<T> eff(K, zero), Sact(K, zero);
402 T sumTN = zero;
403 for (std::size_t r = 0; r < K; ++r) {
404 const T inflow =
405 std::isnan(num_traits<T>::to_double(lam[r])) ? zero : lam[r];
406 eff[r] = T(inflow * T(one - lossProb));
407 // The PH was divided by the server count, so the actual
408 // per-class service mean multiplies it back.
409 Sact[r] = T(map_mean(ph.PH[i][r]) * num_traits<T>::from_double(ns));
410 sumTN += eff[r];
411 }
412 T Wq = zero;
413 if (sumTN > zero) {
414 T sw = zero;
415 for (std::size_t r = 0; r < K; ++r) {
416 const T c = T(eff[r] * Sact[r]);
417 if (!std::isnan(num_traits<T>::to_double(c))) sw += c;
418 }
419 const T w = T(T(meanQ / sumTN) - T(sw / sumTN));
420 Wq = (w > zero) ? w : zero;
421 }
422 for (std::size_t r = 0; r < K; ++r) {
423 TN(i, r) = eff[r];
424 UN(i, r) = T(TN(i, r) * map_mean(ph.PH[i][r]));
425 if (TN(i, r) > zero) {
426 RN(i, r) = T(Wq + Sact[r]);
427 QN(i, r) = T(TN(i, r) * RN(i, r));
428 } else {
429 RN(i, r) = zero;
430 QN(i, r) = zero;
431 }
432 }
433 finiteCapUsed = true;
434 } else {
435 const std::vector<T> lam = mmap_lambda(ARV[ind]);
436 T rho = zero;
437 for (std::size_t r = 0; r < K; ++r) {
438 const T u = T(lam[r] * map_mean(ph.PH[i][r]));
439 if (!std::isnan(num_traits<T>::to_double(u))) rho += u;
440 }
441 if (rho < T(one - ftol)) {
442 // A correlated single-class service is answered by the
443 // exact MAP/MAP/1 QBD, which carries the service phase
444 // across departures; MMAPPH1FCFS would discard it.
445 const bool corr =
446 (K == 1) && (ns == 1.0) && ph.known[i][0] &&
447 std::fabs(num_traits<T>::to_double(
448 map_acf(ph.PH[i][0], std::vector<unsigned>{1})[0])) >
449 GlobalConstants::CoarseTol;
450 if (corr) {
451 Map<T> arv;
452 arv.D0 = ARV[ind].D0;
453 arv.D1 = ARV[ind].Dc[0];
454 QN(i, 0) = qbd_mapmap1(arv, ph.PH[i][0]).QN;
455 } else {
456 const std::vector<T> m = mmapph1fcfs_ncmean(ARV[ind], sl);
457 for (std::size_t r = 0; r < K; ++r)
458 QN(i, r) = m[ARV[ind].classes() == 1 ? 0 : r];
459 }
460 } else {
461 // Bounded rather than left to diverge: an overloaded
462 // station holds its class's own population, or 1/FineTol
463 // when the class is open.
464 for (std::size_t r = 0; r < K; ++r)
465 QN(i, r) = std::isfinite(L.classes[r].population)
466 ? num_traits<T>::from_double(L.classes[r].population)
467 : T(one / ftol);
468 }
469 for (std::size_t r = 0; r < K; ++r) TN(i, r) = lam[r];
470 }
471 } else if (sc == SchedStrategy::PS) {
472 const std::vector<T> lam = mmap_lambda(ARV[ind]);
473 for (std::size_t r = 0; r < K; ++r) {
474 TN(i, r) = lam[r];
475 // S, NOT the server-count-scaled PH mean: the reference
476 // writes 1./sn.rates here and overwrites UN with the PH mean
477 // in the surrogate-delay block below, so this value only ever
478 // reaches the sharing denominator.
479 UN(i, r) = T(TN(i, r) * S(i, r));
480 }
481 T usum = zero;
482 for (std::size_t r = 0; r < K; ++r) usum += UN(i, r);
483 const T uden = (usum < T(one - ftol)) ? usum : T(one - ftol);
484 for (std::size_t r = 0; r < K; ++r) QN(i, r) = T(UN(i, r) / T(one - uden));
485 }
486
487 if (!finiteCapUsed) {
488 for (std::size_t r = 0; r < K; ++r) {
489 UN(i, r) = T(TN(i, r) * map_mean(ph.PH[i][r]));
490 // The jobs at the surrogate delay server the c-fold service
491 // speedup removed.
492 if (std::isfinite(ns))
493 QN(i, r) = T(QN(i, r) + TN(i, r) *
494 T(map_mean(ph.PH[i][r]) *
496 num_traits<T>::from_double((ns - 1.0) / ns));
497 RN(i, r) = T(QN(i, r) / TN(i, r));
498 }
499 }
500 }
501
502 // ---- the departure processes for the next sweep -------------------
503 for (std::size_t i = 0; i < M; ++i) {
504 const std::size_t ind = L.node_of_station(i + 1) - 1;
505 const qn::NodeType ty = L.nodes[ind].nodetype;
506 const SchedStrategy sc = L.stations[i].sched;
507 if (ty == qn::NodeType::Join) {
508 for (std::size_t r = 0; r < K; ++r)
509 if (TN(i, r) > zero) DEP[ind][r] = map_exponential_mean(T(one / TN(i, r)));
510 continue;
511 }
512 if (ty != qn::NodeType::Queue || ARV[ind].order() == 0) continue;
513 const bool fcfs = (sc == SchedStrategy::FCFS || sc == SchedStrategy::HOL);
514 if (!fcfs && sc != SchedStrategy::PS) continue;
515
516 T rho = zero;
517 for (std::size_t r = 0; r < K; ++r) rho += UN(i, r);
518 for (std::size_t r = 0; r < K; ++r) {
519 const bool scalable = (V(i, r) > zero && lambda[r] > zero);
520 // The PS branch of the reference does nothing at all when the
521 // class does not flow through the station; the FCFS branch still
522 // recomputes the departure process and only skips the rescaling.
523 if (!fcfs && !scalable) continue;
524 const Mmap<T> A = mmap_hide_but(ARV[ind], r);
525 const Map<T>& Srv = ph.PH[i][r];
526 const std::size_t etaqa_sz =
527 (cfg.etaqa_trunc + 1) * A.order() * Srv.D0.rows();
528 Map<T> dep = Srv;
529 if (etaqa_sz <= tcfg.space_max && rho < T(one - ftol)) {
530 // The reference's own try/catch: an ETAQA truncation that
531 // fails to build falls back to the scaled service process.
532 try {
533 const Map<T> Am{A.D0, A.D1};
534 dep = map_normalize(fcfs ? qbd_depproc_etaqa(Am, Srv, cfg.etaqa_trunc)
535 : qbd_depproc_etaqa_ps(Am, Srv, cfg.etaqa_trunc));
536 } catch (const Error&) {
537 dep = Srv;
538 }
539 }
540 if (scalable) dep = map_scale(dep, T(one / T(lambda[r] * V(i, r))));
541 DEP[ind][r] = dep;
542 }
543 }
544
545 std::vector<T> xnew(M * K, zero);
546 for (std::size_t i = 0; i < M; ++i)
547 for (std::size_t r = 0; r < K; ++r) xnew[i * K + r] = QN(i, r);
548 return std::make_pair(xnew, xref);
549 };
550
552 fo.iter_max = static_cast<std::size_t>(opt.iter_max);
553 fo.iter_tol = opt.tol;
554 // `config.da_miniter = 3`: the legacy loop tested convergence only from the
555 // third sweep, and `config.da_norm` is the relative difference offset by
556 // FineTol so a zero reference entry does not divide by zero.
557 fo.miniter = 3;
558 fo.relative_norm = true;
559 fo.relative_eps = GlobalConstants::FineTol;
560 const da::FpiResult<T> fr = da::da_fpi<T>(sweep, std::vector<T>(M * K, zero), fo);
561 if (totiter) *totiter = fr.iterations;
562
563 // ---- the join, from the branch response times -------------------------
564 for (std::size_t j = 0; j < I; ++j) {
565 if (L.nodes[j].nodetype != qn::NodeType::Join) continue;
566 if (L.nodes[j].station == 0) continue;
567 const std::size_t jst = L.nodes[j].station - 1;
568 std::map<std::size_t, std::vector<std::size_t>> groups; // ordered, as unique() is
569 for (std::size_t b = 0; b < I; ++b) {
570 const std::size_t gid = fj.node_sync[j][b];
571 if (gid > 0) groups[gid].push_back(b);
572 }
573 for (std::size_t r = 0; r < K; ++r) {
574 if (!(TN(jst, r) > zero)) continue;
575 T syncDelay = zero, joinArrivalRate = zero;
576 for (const std::pair<const std::size_t, std::vector<std::size_t>>& g : groups) {
577 std::vector<T> branchRt, branchTput;
578 for (std::size_t b : g.second) {
579 if (L.nodes[b].station == 0) continue;
580 const std::size_t bst = L.nodes[b].station - 1;
581 if (!(RN(bst, r) > zero)) continue;
582 branchRt.push_back(RN(bst, r));
583 branchTput.push_back(TN(bst, r));
584 }
585 if (branchRt.size() < 2) continue;
586 std::vector<T> rate(branchRt.size(), zero);
587 T meanRt = zero;
588 for (std::size_t b = 0; b < branchRt.size(); ++b) {
589 rate[b] = T(one / branchRt[b]);
590 meanRt += branchRt[b];
591 }
592 meanRt /= num_traits<T>::from_int(static_cast<long>(branchRt.size()));
593 const T excess = T(exp_max_mean(rate) - meanRt);
594 if (excess > zero) syncDelay += excess;
595 for (const T& t : branchTput) joinArrivalRate += t;
596 }
597 RN(jst, r) = syncDelay;
598 QN(jst, r) = T(joinArrivalRate * syncDelay);
599 UN(jst, r) = zero;
600 }
601 }
602
604 out.Q = QN;
605 out.U = UN;
606 out.R = RN;
607 out.Tp = TN;
608 out.C.assign(K, zero);
609 for (std::size_t r = 0; r < K; ++r)
610 for (std::size_t i = 0; i < M; ++i) out.C[r] += RN(i, r);
611 // X is left at zero: the reference never assigns it. See the file header.
612 out.X.assign(K, zero);
613 zero_nans(out.Q);
614 zero_nans(out.U);
615 zero_nans(out.R);
616 zero_nans(out.Tp);
617 for (std::size_t r = 0; r < K; ++r)
618 if (std::isnan(num_traits<T>::to_double(out.C[r]))) out.C[r] = zero;
619 out.method = "dec.source.mmap";
620 out.iter = static_cast<int>(fr.iterations);
621 out.lG = 0.0;
622 return out;
623 } // if constexpr has_transcendental
624}
625
626/**
627 * Port of `solver_mam_basic_mmap_closed.m`: the per-class bisection on the
628 * surrogate arrival rate that makes the inner analyzer's queue lengths match the
629 * closed population.
630 */
631template <class T>
633 const MamOptions& opt, const MmapDecConfig& cfg,
634 std::size_t* totiter) {
635 if constexpr (!num_traits<T>::has_transcendental) {
636 (void)L; (void)opt; (void)cfg; (void)totiter;
637 throw UnsupportedError(
638 "solver_mam_basic_mmap_closed: the throughput bisection wraps the inner MMAP "
639 "decomposition, which needs transcendental arithmetic; rerun with --arith double or "
640 "--arith real");
641 } else {
642 using namespace basic_mmap_detail;
643 using basic_detail::zero_nans;
644 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
645 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
646
647 // REFERENCE INDEXING, CHECKED RATHER THAN ASSUMED, exactly as
648 // `solver_mna_closed` checks the same line: the terminal renormalization
649 // scales chain c's queue lengths to `sn.njobs(c)`, and `sn.njobs` is
650 // CLASS-indexed. That is only correct when each chain holds exactly one
651 // class; otherwise a chain would be scaled to another class's population,
652 // which is a wrong number with no symptom.
653 if (C != K)
654 throw UnsupportedError(
655 "solver_mam_basic_mmap_closed: the reference renormalizes chain c's queue lengths with "
656 "the class-indexed sn.njobs(c), which is only correct when each chain holds exactly "
657 "one class; this model has " +
658 std::to_string(C) + " chains over " + std::to_string(K) + " classes");
659
660 // ---- the bisection bracket -------------------------------------------
661 // The upper bound is the SLOWEST rate the class meets at a finite-server
662 // station; with none, the reference falls back to the FASTEST rate over the
663 // infinite-server ones, and to 1 when the class is served nowhere.
664 std::vector<T> lambda_lb(K, zero), lambda_ub(K, zero);
665 for (std::size_t r = 0; r < K; ++r) {
666 bool any = false;
667 double best = 0.0;
668 for (std::size_t i = 0; i < M; ++i) {
669 if (!std::isfinite(L.stations[i].nservers)) continue;
670 if (L.disabled[i][r] || !(L.rates(i, r) > zero)) continue;
671 const double v = num_traits<T>::to_double(L.rates(i, r));
672 if (!any || v < best) { best = v; any = true; }
673 }
674 if (!any) {
675 for (std::size_t i = 0; i < M; ++i) {
676 if (std::isfinite(L.stations[i].nservers)) continue;
677 if (L.disabled[i][r] || !(L.rates(i, r) > zero)) continue;
678 const double v = num_traits<T>::to_double(L.rates(i, r));
679 if (!any || v > best) { best = v; any = true; }
680 }
681 }
682 lambda_ub[r] = any ? num_traits<T>::from_double(best) : one;
683 }
684 std::vector<T> lambda = lambda_ub;
685
686 std::vector<T> QNc(K, zero);
687 for (std::size_t r = 0; r < K; ++r)
688 // An open class contributes 0: only the closed populations gate the loop.
689 QNc[r] = std::isfinite(L.classes[r].population)
690 ? num_traits<T>::from_double(L.classes[r].population)
691 : zero;
692 std::vector<T> QN_chain(K, zero);
693
694 MamOptions inner = opt;
695 inner.iter_max = std::max(20, (opt.iter_max + 9) / 10);
696 // The reference caps the MMAP phase truncation at 16 here: the inner call is
697 // made once per bisection step, and the compression above that width is
698 // O(dim^3) for no accuracy the bisection can use.
699 if (inner.space_max > 16) inner.space_max = 16;
700
701 mva::MvaSolution<T> sol, last;
702 bool have_good = false, algorithm_ok = false;
703 const double bisect_tol = std::max(opt.tol, 1e-3);
704 int it_out = 0;
705
706 for (;;) {
707 double gap = 0.0;
708 for (std::size_t r = 0; r < K; ++r)
709 gap = std::max(gap, std::fabs(num_traits<T>::to_double(T(QN_chain[r] - QNc[r]))));
710 if (!(gap > bisect_tol) || it_out >= opt.iter_max) break;
711 ++it_out;
712 if (it_out > 1) {
713 bool bracket_collapsed = true;
714 for (std::size_t r = 0; r < K; ++r) {
715 if (!std::isfinite(L.classes[r].population) || !(QNc[r] > zero)) continue;
716 if (QN_chain[r] < QNc[r]) lambda_lb[r] = lambda[r];
717 else lambda_ub[r] = lambda[r];
718 lambda[r] = T(T(lambda_lb[r] + lambda_ub[r]) / num_traits<T>::from_int(2));
719 // Bisection can still refine class r only while its bracket is
720 // wider than the precision floor below which lambda cannot move
721 // any reported metric.
722 const double width =
723 num_traits<T>::to_double(T(lambda_ub[r] - lambda_lb[r]));
724 if (width > GlobalConstants::FineTol *
725 std::max(1.0, std::fabs(num_traits<T>::to_double(lambda_ub[r]))))
726 bracket_collapsed = false;
727 }
728 if (bracket_collapsed) {
729 --it_out;
730 break;
731 }
732 }
733
734 try {
735 std::size_t inner_iter = 0;
736 sol = solver_mam_basic_mmap_inner(L, inner, cfg, lambda, &inner_iter);
737 algorithm_ok = true;
738 } catch (const Error&) {
739 // The reference's own catch: the inner algorithm diverged (typically
740 // MMAPPH1FCFS under saturation). Every chain is treated as
741 // overloaded, so the next bisection step drops lambda.
742 algorithm_ok = false;
743 }
744
745 if (algorithm_ok) {
746 for (std::size_t r = 0; r < K; ++r) {
747 QN_chain[r] = zero;
748 for (std::size_t i = 0; i < M; ++i) QN_chain[r] += sol.Q(i, r);
749 if (!std::isfinite(num_traits<T>::to_double(QN_chain[r])))
750 QN_chain[r] = T(one / num_traits<T>::from_double(GlobalConstants::FineTol));
751 }
752 last = sol;
753 have_good = true;
754 } else {
755 for (std::size_t r = 0; r < K; ++r)
756 QN_chain[r] = T(one / num_traits<T>::from_double(GlobalConstants::FineTol));
757 }
758 }
759
760 // If the LAST trial diverged, fall back to the most recent successful one.
761 if (!algorithm_ok && have_good) sol = last;
762 if (sol.Q.rows() != M) {
763 // The loop never ran a trial: the initial gap was already inside the
764 // tolerance, which for a model of zero population is the honest answer.
765 sol.Q = Matrix<T>(M, K, zero);
766 sol.U = Matrix<T>(M, K, zero);
767 sol.R = Matrix<T>(M, K, zero);
768 sol.Tp = Matrix<T>(M, K, zero);
769 sol.C.assign(K, zero);
770 sol.X.assign(K, zero);
771 }
772
773 // ---- population redistribution within each chain ----------------------
774 for (std::size_t c = 0; c < C; ++c) {
775 if (!std::isfinite(L.classes[c].population)) continue;
776 T sumQ = zero;
777 for (std::size_t k : L.inchain[c])
778 for (std::size_t i = 0; i < M; ++i) sumQ += sol.Q(i, k - 1);
779 if (!(sumQ > zero)) continue;
780 const T Nc = num_traits<T>::from_double(L.classes[c].population);
781 for (std::size_t k : L.inchain[c])
782 for (std::size_t i = 0; i < M; ++i) sol.Q(i, k - 1) = T(Nc * sol.Q(i, k - 1) / sumQ);
783 }
784
785 // An infinite server's utilization IS its queue length.
786 for (std::size_t i = 0; i < M; ++i)
787 if (L.stations[i].sched == SchedStrategy::INF)
788 for (std::size_t r = 0; r < K; ++r) sol.U(i, r) = sol.Q(i, r);
789
790 sol.C.assign(K, zero);
791 for (std::size_t r = 0; r < K; ++r)
792 for (std::size_t i = 0; i < M; ++i) sol.C[r] += sol.R(i, r);
793 zero_nans(sol.Q);
794 zero_nans(sol.U);
795 zero_nans(sol.R);
796 zero_nans(sol.Tp);
797 for (std::size_t r = 0; r < K; ++r)
798 if (std::isnan(num_traits<T>::to_double(sol.C[r]))) sol.C[r] = zero;
799 sol.iter = it_out;
800 if (totiter) *totiter = static_cast<std::size_t>(it_out);
801 return sol;
802 } // if constexpr has_transcendental
803}
804
805/**
806 * Port of `solver_mam_basic_mmap.m`, the top-level dispatcher of the MMAP
807 * fork-join decomposition: an open model goes straight to the inner algorithm
808 * with the arrival rates its sources declare, a closed one through the
809 * bisection wrapper.
810 */
811template <class T>
813 const T zero = num_traits<T>::from_int(0);
814 MmapDecConfig cfg;
815 if (!L.is_open_model()) {
816 std::size_t iter = 0;
817 return solver_mam_basic_mmap_closed(L, opt, cfg, &iter);
818 }
819 // The chain's arrival rate is the total rate its classes are released at by
820 // the reference station of its first class; every class of the chain carries
821 // that same total, as the reference assigns it.
822 std::vector<T> lambda(L.nclasses, zero);
823 for (std::size_t c = 0; c < L.nchains; ++c) {
824 if (L.inchain[c].empty()) continue;
825 const std::size_t rs = L.classes[L.inchain[c][0] - 1].refstat - 1;
826 T tot = zero;
827 for (std::size_t k : L.inchain[c]) {
828 if (L.disabled[rs][k - 1]) continue;
829 const double v = num_traits<T>::to_double(L.rates(rs, k - 1));
830 if (!std::isfinite(v)) continue;
831 tot += L.rates(rs, k - 1);
832 }
833 for (std::size_t k : L.inchain[c]) lambda[k - 1] = tot;
834 }
835 std::size_t iter = 0;
836 return solver_mam_basic_mmap_inner(L, opt, cfg, lambda, &iter);
837}
838
839} // namespace mam
840} // namespace line
841
842#endif // LINE_SOLVERS_MAM_SOLVER_MAM_BASIC_MMAP_H
Base error for the multiprecision C++ port.
Definition error.h:31
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
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.
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< 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::size_t node_of_station(std::size_t st) const
1-based node index of a station, and the reverse; 0 when absent.
std::vector< NodeDef > nodes
every node, in creation order
bool is_open_model() const
sn_is_open_model: EVERY class is open, which is not has_open_classes.
Damped fixed-point iteration, the shared driver of the decomposition algorithms.
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.
Compression of a marked MAP into a smaller representation, and the two M3A primitives it is built fro...
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...
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
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
FjSyncMap sn_build_fj_sync_map(const qn::NetworkStruct< T > &sn)
sn_build_fj_sync_map.
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
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.
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?
@ MixtureOrder1
'default', 'mixture', 'mixture.order1'
std::vector< std::vector< Map< T > > > DepTable
DEP{i,r}, the departure process of class r from i in (D0,D1) form.
mva::MvaSolution< T > solver_mam_basic_mmap_inner(const qn::NetworkStruct< T > &L, const MamOptions &opt, const MmapDecConfig &cfg, const std::vector< T > &lambda, std::size_t *totiter)
Port of solver_mam_basic_mmap_inner.m.
TrafficConfig traffic_config(const MamOptions &opt)
The traffic step's view of SolverOptions('MAM').
std::vector< T > mmap_lambda(const Mmap< T > &m)
Alias kept for parity with the MATLAB name.
Mmap< T > mmap_hide_but(const Mmap< T > &in, std::size_t keep)
mmap_hide(m, setdiff(1:K, keep)): keep ONE mark, hide every other.
mva::MvaSolution< T > solver_mam_basic_mmap(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_basic_mmap.m, the top-level dispatcher of the MMAP fork-join decomposition: an ope...
Map< T > qbd_depproc_etaqa(const Map< T > &arrival, const Map< T > &service, std::size_t n)
MAP descriptor of the departure process of a MAP/MAP/1-FCFS queue, ETAQA-truncated at level n (qbd_de...
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).
std::vector< Mmap< T > > solver_mam_traffic_mmap(const qn::NetworkStruct< T > &sn, const DepTable< T > &DEP, const TrafficConfig &config, const FjSyncMap &fjSyncMap)
Port of solver_mam_traffic_mmap.m, the fork-join aware traffic step.
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
Map< T > qbd_depproc_etaqa_ps(const Map< T > &arrival, const Map< T > &service, std::size_t n)
MAP descriptor of the departure process of a MAP/MAP/1-PS queue, ETAQA-truncated at level n (qbd_depp...
Mmap< T > mmap_compress(const Mmap< T > &in, MmapCompressMethod method)
Compress an MMAP (mmap_compress.m).
Map< T > map_exponential_mean(const T &mean)
Poisson process with the given mean inter-arrival time (map_exponential.m).
Map< T > map_scale(const Map< T > &in, const T &new_mean)
Rescale time so that the mean inter-arrival time becomes new_mean.
mva::MvaSolution< T > solver_mam_basic_mmap_closed(const qn::NetworkStruct< T > &L, const MamOptions &opt, const MmapDecConfig &cfg, std::size_t *totiter)
Port of solver_mam_basic_mmap_closed.m: the per-class bisection on the surrogate arrival rate that ma...
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
Map< T > map_normalize(const Map< T > &in)
Clamp negative off-diagonal entries of D0 and negative entries of D1 to zero, then rebuild the diagon...
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
A queueing network and its refreshed NetworkStruct.
Departure process of a MAP/MAP/1 queue: the ETAQA-truncated MAP descriptor under FCFS and under PS,...
The MAP/MAP/1 queue solved as a quasi-birth-death process.
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...
Port of solver_mam_traffic.m and solver_mam_traffic_mmap.m: the traffic step of the dec....
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
std::size_t miniter
iterations before the stopping test applies
Definition da_fpi.h:54
double relative_eps
Definition da_fpi.h:72
bool relative_norm
config.da_norm, the increment norm.
Definition da_fpi.h:71
std::size_t iterations
Definition da_fpi.h:78
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:667
sn_build_fj_sync_map: which incoming flows at a Join must be synchronized.
The options SolverMAM reads.
Definition mam_types.h:29
int iter_max
SolverOptions('MAM') lowers this from the global 1000 to 100.
Definition mam_types.h:33
std::size_t space_max
options.config.space_max: the order budget of the per-station arrival superposition,...
Definition mam_types.h:39
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
The options.config fields the MMAP decomposition reads on top of MamOptions.
std::size_t etaqa_trunc
config.etaqa_trunc, the ETAQA level truncation of the departure process.
std::size_t fj_sync_q_len
config.fj_sync_q_len, the synchronization queue at a join.
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::size_t order() const
Definition mmap_lambda.h:50
What mam_detect_mmck returns; muRate is meaningful only when isMmck.
The fields of options.config the traffic step reads.
std::size_t fj_sync_q_len
config.fj_sync_q_len, the join's synchronization queue; MATLAB's default.
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
T meanQueueLength
L, mean number in system.
Definition qsys_mmck.h:44