LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_passage_time.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_PASSAGE_TIME_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_PASSAGE_TIME_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_passage_time.m`: the response-time (sojourn-time)
12 * distribution of a single open queue, which is what `getCdfRespT`,
13 * `getSjrnT` / `sjrnT` and the CDF path of `getPerctRespT` return.
14 *
15 * THE REGIME IS NARROW AND THE REFERENCE SAYS SO. The whole analyzer is inside
16 * `if M == 2 && all(isinf(N))`: exactly two stations, a Source and one queue,
17 * every class open. Anything else makes MATLAB warn and return with NO result
18 * at all -- an empty `RD` that then surfaces as a confusing failure further up.
19 * The port refuses by name instead, which is the same information delivered
20 * where it can be acted on.
21 *
22 * TWO ENGINES, chosen by the queue's discipline:
23 *
24 * - FCFS / HOL: the sojourn time is a phase-type law read straight out of the
25 * age process (`mmapph1fcfs_stdistr_ph`). The evaluation grid is the
26 * reference's: start at mean + 5 sigma and widen until the CDF is within
27 * FineTol of one, then lay down `num_cdf_pts` points from zero.
28 * - PS: the MAP/M/1-PS sojourn law of `map_m1ps_cdfrespt`, which returns the
29 * COMPLEMENTARY CDF, so the port takes 1 - W_bar as the reference does. The
30 * grid there is 10x the M/M/1-PS mean, and the reference requires
31 * exponential service (order-1 subgenerator) and, for several classes,
32 * identical service rates -- both refused by name here.
33 *
34 * WHAT IS NOT PORTED, and refuses: the priority branch. Distinct priorities
35 * under HOL reach BUTools' `MMAPPH1NPPR` (`MMAPPH1PRPR` when preemptive),
36 * whose sojourn law MATLAB, the JAR and python tabulate through 'stMoms' +
37 * 'stDistr' since 2026-08-27 (no vendored analyzer exports a PH form); neither
38 * analyzer is ported to C++, exactly as in `solver_mam_basic`, so this arm is
39 * the one member of the family still refusing it.
40 */
41
42#include <cmath>
43#include <cstddef>
44#include <string>
45#include <vector>
46
56#include "line/util/error.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace mam {
61
62/** One class's response-time CDF, the reference's `RD{station, class} = [F, X]`. */
63template <class T>
64struct RespTCdf {
65 std::vector<T> F; ///< CDF values
66 std::vector<T> X; ///< the points they are evaluated at
67};
68
69/**
70 * Port of `solver_mam_passage_time.m`.
71 *
72 * @return one entry per class, in class order; the Source contributes none
73 * (the reference leaves `RD{idx_arv,k}` empty)
74 */
75template <class T>
76std::vector<RespTCdf<T>> solver_mam_passage_time(const qn::NetworkStruct<T>& L,
77 const MamOptions& opt) {
78 if constexpr (!num_traits<T>::has_transcendental) {
79 throw UnsupportedError(
80 "solver_mam_passage_time: the sojourn-time law comes from the age process, whose "
81 "first-return matrix is a tolerance-terminated Riccati doubling; rerun with "
82 "--arith double or --arith real");
83 } else {
86 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
87 const std::size_t M = L.nstations, K = L.nclasses;
88
89 bool allopen = true;
90 for (const qn::JobClass& c : L.classes)
91 if (!std::isinf(c.population)) allopen = false;
92 if (M != 2 || !allopen)
93 throw UnsupportedError(
94 "solver_mam_passage_time: the MAM response-time distribution covers a single open "
95 "queue only (exactly two stations, a Source and one queue, every class open); this "
96 "model has " + std::to_string(M) +
97 " stations. The reference warns and returns no result at all for it");
98
99 std::size_t src = 0, q = 0;
100 for (std::size_t i = 1; i <= M; ++i) {
101 if (L.stations[i - 1].sched == SchedStrategy::EXT) src = i;
102 else q = i;
103 }
104 if (src == 0 || q == 0)
105 throw UnsupportedError(
106 "solver_mam_passage_time: the model must have a Source and one queueing station");
107
108 const SchedStrategy qs = L.stations[q - 1].sched;
109 if (!(qs == SchedStrategy::FCFS || qs == SchedStrategy::HOL || qs == SchedStrategy::PS))
110 throw UnsupportedError(std::string("solver_mam_passage_time: the ") +
112 " discipline is not covered; the reference supports FCFS, HOL and "
113 "PS only");
114 // Priorities select the law only under a priority DISCIPLINE (HOL here) --
115 // a plain FCFS or PS queue serves in arrival or processor order whatever
116 // the prio column says, exactly as the other three codebases gate it
117 if (qs == SchedStrategy::HOL) {
118 bool distinct = false;
119 for (std::size_t r = 1; r < K; ++r)
120 if (L.classes[r].prio != L.classes[0].prio) distinct = true;
121 if (distinct)
122 throw UnsupportedError(
123 "solver_mam_passage_time: non-identical class priorities under HOL route to "
124 "BUTools' MMAPPH1NPPR (MMAPPH1PRPR when preemptive), whose tabulated sojourn law "
125 "MATLAB, the JAR and python now serve; neither priority analyzer is ported to "
126 "C++, so this arm still refuses by name");
127 }
128
129 const std::size_t npts =
130 opt.num_cdf_pts > 0 ? opt.num_cdf_pts : static_cast<std::size_t>(100);
131
132 // The arrival MMAP: each class's source process marked as its own class.
133 Mmap<T> A;
134 {
135 std::vector<Mmap<T>> parts;
136 for (std::size_t r = 0; r < K; ++r) {
137 Mmap<T> m;
138 const Map<T> s = lang::dist_to_map(L.service[src - 1][r]);
139 m.D0 = s.D0;
140 m.D1 = s.D1;
141 m.Dc.assign(1, s.D1);
142 parts.push_back(m);
143 }
144 A = parts[0];
145 for (std::size_t p = 1; p < parts.size(); ++p)
146 A = mmap_super(A, parts[p]);
147 }
148
149 std::vector<RespTCdf<T>> out(K);
150
151 if (qs == SchedStrategy::PS) {
152 // MAP/M/1-PS: the reference requires exponential service and, with
153 // several classes, one shared rate.
154 std::vector<T> mu(K, zero);
155 for (std::size_t r = 0; r < K; ++r) {
156 const Map<T> sv = lang::dist_to_map(L.service[q - 1][r]);
157 if (sv.D0.rows() != 1)
158 throw UnsupportedError(
159 "solver_mam_passage_time: a PS queue requires exponential (order-1) service "
160 "times; the MAP/M/1-PS sojourn law has no phase-type generalization here");
161 mu[r] = T(-sv.D0(0, 0));
162 }
163 for (std::size_t r = 1; r < K; ++r)
164 if (std::fabs(num_traits<T>::to_double(mu[r]) - num_traits<T>::to_double(mu[0])) >
166 throw UnsupportedError(
167 "solver_mam_passage_time: multi-class PS requires identical service rates");
168
169 Matrix<T> Dagg(A.order(), A.order(), zero);
170 for (std::size_t c = 0; c < A.classes(); ++c)
171 for (std::size_t i = 0; i < Dagg.rows(); ++i)
172 for (std::size_t j = 0; j < Dagg.cols(); ++j) Dagg(i, j) += A.Dc[c](i, j);
173 const T lambda = map_lambda(Map<T>{A.D0, Dagg});
174 const T rho = T(lambda / mu[0]);
175 if (!(num_traits<T>::to_double(rho) < 1.0))
176 throw NumericError(
177 "solver_mam_passage_time: the PS queue is unstable, so it has no sojourn-time "
178 "distribution");
179 const T mean = T(one / T(mu[0] * T(one - rho)));
180 const T xmax = T(mean * num_traits<T>::from_int(10));
181 std::vector<T> x(npts, zero);
182 for (std::size_t i = 0; i < npts; ++i)
183 x[i] = (npts == 1) ? xmax
185 static_cast<double>(i) /
186 static_cast<double>(npts - 1)));
187 const MapM1psResult<T> res = map_m1ps_cdfrespt(A.D0, Dagg, mu[0], x);
188 for (std::size_t r = 0; r < K; ++r) {
189 out[r].X = x;
190 out[r].F.resize(npts);
191 for (std::size_t i = 0; i < npts; ++i) out[r].F[i] = T(one - res.w_bar[i]);
192 }
193 return out;
194 }
195
196 // FCFS / HOL: the sojourn law is phase-type, straight out of the age process.
197 std::vector<PhService<T>> svc(K);
198 for (std::size_t r = 0; r < K; ++r) {
199 const double ns = L.stations[q - 1].nservers;
200 const Map<T> raw = lang::dist_to_map(L.service[q - 1][r]);
201 const T target = std::isfinite(ns)
203 : map_mean(raw);
204 const Map<T> sc = map_scale(raw, target);
205 svc[r].sigma = map_pie(sc);
206 svc[r].S = sc.D0;
207 }
208 const std::vector<StDistrPh<T>> ph = mmapph1fcfs_stdistr_ph(A, svc);
209
210 for (std::size_t r = 0; r < K; ++r) {
211 // Read the PH pair back as the MAP {A, (-A e) alpha} the moment and CDF
212 // routines take.
213 const std::size_t n = ph[r].A.rows();
214 Matrix<T> D1(n, n, zero);
215 for (std::size_t i = 0; i < n; ++i) {
216 T s = zero;
217 for (std::size_t j = 0; j < n; ++j) s += ph[r].A(i, j);
218 for (std::size_t j = 0; j < n; ++j) D1(i, j) = T(-s * ph[r].alpha[j]);
219 }
220 const Map<T> RDph{ph[r].A, D1};
221 const T mean = map_mean(RDph);
222 const T sigma = num_traits<T>::from_double(
223 std::sqrt(num_traits<T>::to_double(map_var(RDph))));
224 // Widen until the tail beyond the grid is negligible, as the reference does.
225 int nsig = 5;
226 for (;;) {
227 const T probe = T(mean + num_traits<T>::from_int(nsig) * sigma);
228 const std::vector<T> c = map_cdf(RDph, std::vector<T>{probe});
229 if (num_traits<T>::to_double(c[0]) >= 1.0 - GlobalConstants::FineTol) break;
230 if (++nsig > 1000)
231 throw NumericError(
232 "solver_mam_passage_time: the sojourn-time CDF did not reach one within 1000 "
233 "standard deviations");
234 }
235 const T xmax = T(mean + num_traits<T>::from_int(nsig) * sigma);
236 std::vector<T> x(npts, zero);
237 for (std::size_t i = 0; i < npts; ++i)
238 x[i] = (npts == 1) ? xmax
240 static_cast<double>(i) /
241 static_cast<double>(npts - 1)));
242 out[r].X = x;
243 out[r].F = map_cdf(RDph, x);
244 }
245 return out;
246 } // if constexpr has_transcendental
247}
248
249/**
250 * Port of the CDF path of `@@SolverMAM/getPerctRespT.m`: linear interpolation of
251 * the response-time CDF at the requested percentile levels.
252 *
253 * Duplicate CDF values are collapsed keeping the LAST, as MATLAB's
254 * `unique(probs,'last')` does, so a flat tail interpolates from the largest
255 * time carrying that probability rather than the smallest.
256 *
257 * The FJ_codes branch of the reference is not reachable here: it reads
258 * percentiles stored by `solver_mam_fj`, which is not ported and whose models
259 * the dispatch refuses.
260 */
261template <class T>
262std::vector<T> mam_percentiles_from_cdf(const RespTCdf<T>& cdf, const std::vector<double>& pcts) {
263 const T zero = num_traits<T>::from_int(0);
264 if (cdf.F.empty()) throw InputError("mam_percentiles_from_cdf: the CDF is empty");
265 std::vector<double> p, t;
266 for (std::size_t i = 0; i < cdf.F.size(); ++i) {
267 const double fi = num_traits<T>::to_double(cdf.F[i]);
268 if (!p.empty() && std::fabs(fi - p.back()) < 1e-15) {
269 t.back() = num_traits<T>::to_double(cdf.X[i]); // keep the LAST
270 continue;
271 }
272 p.push_back(fi);
273 t.push_back(num_traits<T>::to_double(cdf.X[i]));
274 }
275 std::vector<T> out(pcts.size(), zero);
276 for (std::size_t k = 0; k < pcts.size(); ++k) {
277 // Percentiles above 1 are read as percentages, as the reference does.
278 const double want = pcts[k] > 1.0 ? pcts[k] / 100.0 : pcts[k];
279 if (p.size() == 1) {
280 out[k] = num_traits<T>::from_double(t[0]);
281 continue;
282 }
283 std::size_t lo = 0;
284 while (lo + 2 < p.size() && p[lo + 1] < want) ++lo;
285 const double denom = p[lo + 1] - p[lo];
286 const double frac = (std::fabs(denom) < 1e-300) ? 0.0 : (want - p[lo]) / denom;
287 out[k] = num_traits<T>::from_double(t[lo] + frac * (t[lo + 1] - t[lo]));
288 }
289 return out;
290}
291
292} // namespace mam
293} // namespace line
294
295#endif // LINE_SOLVERS_MAM_SOLVER_MAM_PASSAGE_TIME_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
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.
Cumulative distribution of the inter-arrival time of a MAP.
Sojourn time distribution in a MAP/M/1 processor-sharing queue.
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...
The MMAP[K]/PH[K]/1 FCFS queue: per-class mean number in system and per-class queue-length distributi...
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
std::vector< T > mam_percentiles_from_cdf(const RespTCdf< T > &cdf, const std::vector< double > &pcts)
Port of the CDF path of @@SolverMAM/getPerctRespT.m: linear interpolation of the response-time CDF at...
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
T map_var(const Map< T > &m)
Variance of the inter-arrival time.
Definition map_moment.h:133
std::vector< RespTCdf< T > > solver_mam_passage_time(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_passage_time.m.
std::vector< T > map_cdf(const Map< T > &m, const std::vector< T > &points)
Cumulative distribution of the inter-arrival time at the given points.
Definition map_cdf.h:63
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
std::vector< StDistrPh< T > > mmapph1fcfs_stdistr_ph(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, double precision=1e-14)
Per-class SOJOURN TIME as a continuous phase-type law, BUTools' 'stDistrPH'.
Mmap< T > mmap_super(const Mmap< T > &a, const Mmap< T > &b)
Superposition of two MMAPs: the phase process is the product chain, and the class list of the result ...
Definition mmap_lambda.h:88
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
MapM1psResult< T > map_m1ps_cdfrespt(const Matrix< T > &C, const Matrix< T > &D, const T &mu, const std::vector< T > &x, const T &epsilon, const T &epsilon_prime)
Complementary sojourn time distribution of a MAP/M/1-PS queue by the spectral-radius truncation (map_...
Definition map_m1ps.h:494
A queueing network and its refreshed NetworkStruct.
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:667
static constexpr double FineTol
Definition lang_types.h:668
The options SolverMAM reads.
Definition mam_types.h:29
What the two MAP/M/1-PS sojourn entry points return.
Definition map_m1ps.h:300
std::vector< T > w_bar
Pr[W > x] at each requested point.
Definition map_m1ps.h:301
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
An MMAP: the underlying MAP plus the per-class arrival matrices.
Definition mmap_lambda.h:45
std::size_t classes() const
Definition mmap_lambda.h:51
Matrix< T > D0
Definition mmap_lambda.h:46
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
std::size_t order() const
Definition mmap_lambda.h:50
One class's response-time CDF, the reference's RD{station, class} = [F, X].
std::vector< T > X
the points they are evaluated at
std::vector< T > F
CDF values.
One job class of the network.
double population
infinite for an open class