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 * PRIORITIES: distinct class priorities under HOL reach BUTools'
35 * `MMAPPH1NPPR`, and under FCFSPRPRIO `MMAPPH1PRPR` (`mmapph1prio.h`). Neither
36 * exports a PH form, so the law is TABULATED as the reference does: one solve
37 * for two sojourn moments sizes a shared grid (max over classes of mean +
38 * 10 sigma), a second evaluates the CDF on it, with F(0) = 0 prepended since
39 * the Erlangization cannot be evaluated at t = 0.
40 */
41
42#include <algorithm>
43#include <cmath>
44#include <cstddef>
45#include <string>
46#include <vector>
47
58#include "line/util/error.h"
59#include "line/util/matrix.h"
60
61namespace line {
62namespace mam {
63
64/** One class's response-time CDF, the reference's `RD{station, class} = [F, X]`. */
65template <class T>
66struct RespTCdf {
67 std::vector<T> F; ///< CDF values
68 std::vector<T> X; ///< the points they are evaluated at
69};
70
71/**
72 * Port of `solver_mam_passage_time.m`.
73 *
74 * @return one entry per class, in class order; the Source contributes none
75 * (the reference leaves `RD{idx_arv,k}` empty)
76 */
77template <class T>
78std::vector<RespTCdf<T>> solver_mam_passage_time(const qn::NetworkStruct<T>& L,
79 const MamOptions& opt) {
80 if constexpr (!num_traits<T>::has_transcendental) {
81 throw UnsupportedError(
82 "solver_mam_passage_time: the sojourn-time law comes from the age process, whose "
83 "first-return matrix is a tolerance-terminated Riccati doubling; rerun with "
84 "--arith double or --arith real");
85 } else {
88 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
89 const std::size_t M = L.nstations, K = L.nclasses;
90
91 bool allopen = true;
92 for (const qn::JobClass& c : L.classes)
93 if (!std::isinf(c.population)) allopen = false;
94 if (M != 2 || !allopen)
95 throw UnsupportedError(
96 "solver_mam_passage_time: the MAM response-time distribution covers a single open "
97 "queue only (exactly two stations, a Source and one queue, every class open); this "
98 "model has " + std::to_string(M) +
99 " stations. The reference warns and returns no result at all for it");
100
101 std::size_t src = 0, q = 0;
102 for (std::size_t i = 1; i <= M; ++i) {
103 if (L.stations[i - 1].sched == SchedStrategy::EXT) src = i;
104 else q = i;
105 }
106 if (src == 0 || q == 0)
107 throw UnsupportedError(
108 "solver_mam_passage_time: the model must have a Source and one queueing station");
109
110 const SchedStrategy qs = L.stations[q - 1].sched;
111 if (!(qs == SchedStrategy::FCFS || qs == SchedStrategy::HOL ||
112 qs == SchedStrategy::FCFSPRPRIO || qs == SchedStrategy::PS))
113 throw UnsupportedError(std::string("solver_mam_passage_time: the ") +
115 " discipline is not covered; the reference supports FCFS, HOL, "
116 "FCFSPRPRIO and PS only");
117 // Priorities select the law only under a priority DISCIPLINE -- a plain
118 // FCFS or PS queue serves in arrival or processor order whatever the prio
119 // column says, exactly as the other three codebases gate it
120 bool prio_distinct = false;
121 if (qs == SchedStrategy::HOL || qs == SchedStrategy::FCFSPRPRIO)
122 for (std::size_t r = 1; r < K; ++r)
123 if (L.classes[r].prio != L.classes[0].prio) prio_distinct = true;
124
125 const std::size_t npts =
126 opt.num_cdf_pts > 0 ? opt.num_cdf_pts : static_cast<std::size_t>(100);
127
128 // The arrival MMAP: each class's source process marked as its own class.
129 Mmap<T> A;
130 {
131 std::vector<Mmap<T>> parts;
132 for (std::size_t r = 0; r < K; ++r) {
133 Mmap<T> m;
134 const Map<T> s = lang::dist_to_map(L.service[src - 1][r]);
135 m.D0 = s.D0;
136 m.D1 = s.D1;
137 m.Dc.assign(1, s.D1);
138 parts.push_back(m);
139 }
140 A = parts[0];
141 for (std::size_t p = 1; p < parts.size(); ++p)
142 A = mmap_super(A, parts[p]);
143 }
144
145 std::vector<RespTCdf<T>> out(K);
146
147 if (qs == SchedStrategy::PS) {
148 // MAP/M/1-PS: the reference requires exponential service and, with
149 // several classes, one shared rate.
150 std::vector<T> mu(K, zero);
151 for (std::size_t r = 0; r < K; ++r) {
152 const Map<T> sv = lang::dist_to_map(L.service[q - 1][r]);
153 if (sv.D0.rows() != 1)
154 throw UnsupportedError(
155 "solver_mam_passage_time: a PS queue requires exponential (order-1) service "
156 "times; the MAP/M/1-PS sojourn law has no phase-type generalization here");
157 mu[r] = T(-sv.D0(0, 0));
158 }
159 for (std::size_t r = 1; r < K; ++r)
160 if (std::fabs(num_traits<T>::to_double(mu[r]) - num_traits<T>::to_double(mu[0])) >
162 throw UnsupportedError(
163 "solver_mam_passage_time: multi-class PS requires identical service rates");
164
165 Matrix<T> Dagg(A.order(), A.order(), zero);
166 for (std::size_t c = 0; c < A.classes(); ++c)
167 for (std::size_t i = 0; i < Dagg.rows(); ++i)
168 for (std::size_t j = 0; j < Dagg.cols(); ++j) Dagg(i, j) += A.Dc[c](i, j);
169 const T lambda = map_lambda(Map<T>{A.D0, Dagg});
170 const T rho = T(lambda / mu[0]);
171 if (!(num_traits<T>::to_double(rho) < 1.0))
172 throw NumericError(
173 "solver_mam_passage_time: the PS queue is unstable, so it has no sojourn-time "
174 "distribution");
175 const T mean = T(one / T(mu[0] * T(one - rho)));
176 const T xmax = T(mean * num_traits<T>::from_int(10));
177 std::vector<T> x(npts, zero);
178 for (std::size_t i = 0; i < npts; ++i)
179 x[i] = (npts == 1) ? xmax
181 static_cast<double>(i) /
182 static_cast<double>(npts - 1)));
183 const MapM1psResult<T> res = map_m1ps_cdfrespt(A.D0, Dagg, mu[0], x);
184 for (std::size_t r = 0; r < K; ++r) {
185 out[r].X = x;
186 out[r].F.resize(npts);
187 for (std::size_t i = 0; i < npts; ++i) out[r].F[i] = T(one - res.w_bar[i]);
188 }
189 return out;
190 }
191
192 // FCFS / HOL: the sojourn law is phase-type, straight out of the age process.
193 std::vector<PhService<T>> svc(K);
194 for (std::size_t r = 0; r < K; ++r) {
195 const double ns = L.stations[q - 1].nservers;
196 const Map<T> raw = lang::dist_to_map(L.service[q - 1][r]);
197 const T target = std::isfinite(ns)
199 : map_mean(raw);
200 const Map<T> sc = map_scale(raw, target);
201 svc[r].sigma = map_pie(sc);
202 svc[r].S = sc.D0;
203 }
204
205 if (prio_distinct) {
206 // BUTools orders the marks LOWEST priority first and LINE's lower prio
207 // is the higher priority, so the classes go in descending prio; output
208 // j belongs to class iK[j].
209 std::vector<std::size_t> iK(K);
210 for (std::size_t r = 0; r < K; ++r) iK[r] = r;
211 std::stable_sort(iK.begin(), iK.end(), [&L](std::size_t a, std::size_t b) {
212 return L.classes[a].prio > L.classes[b].prio;
213 });
214 for (std::size_t j = 1; j < K; ++j)
215 if (L.classes[iK[j]].prio == L.classes[iK[j - 1]].prio)
216 throw UnsupportedError(
217 "solver_mam_passage_time: SolverMAM requires either identical priorities or "
218 "all distinct priorities");
219 Mmap<T> pa;
220 pa.D0 = A.D0;
221 pa.D1 = A.D1;
222 std::vector<PhService<T>> sl;
223 for (std::size_t j = 0; j < K; ++j) {
224 pa.Dc.push_back(A.Dc[iK[j]]);
225 sl.push_back(svc[iK[j]]);
226 }
227 const bool pr = (qs == SchedStrategy::FCFSPRPRIO);
228 const std::vector<std::vector<T>> moms =
229 pr ? mmapph1prpr_stmoms(pa, sl, 2) : mmapph1nppr_stmoms(pa, sl, 2);
230 double xmax = 0.0;
231 for (std::size_t j = 0; j < K; ++j) {
232 const double m1 = num_traits<T>::to_double(moms[j][0]);
233 const double m2 = num_traits<T>::to_double(moms[j][1]);
234 xmax = std::max(xmax, m1 + 10.0 * std::sqrt(std::max(m2 - m1 * m1, 0.0)));
235 }
236 std::vector<T> x(npts, zero);
237 for (std::size_t i = 0; i < npts; ++i)
238 x[i] = (npts == 1) ? num_traits<T>::from_double(xmax)
239 : num_traits<T>::from_double(xmax * static_cast<double>(i) /
240 static_cast<double>(npts - 1));
241 if (npts < 2)
242 throw InputError("solver_mam_passage_time: the priority CDF grid needs at least two "
243 "points, since F(0) = 0 is prepended rather than evaluated");
244 const std::vector<T> pts(x.begin() + 1, x.end());
245 const std::vector<std::vector<T>> dist =
246 pr ? mmapph1prpr_stdistr(pa, sl, pts) : mmapph1nppr_stdistr(pa, sl, pts);
247 for (std::size_t j = 0; j < K; ++j) {
248 RespTCdf<T>& o = out[iK[j]];
249 o.X = x;
250 o.F.assign(1, zero);
251 o.F.insert(o.F.end(), dist[j].begin(), dist[j].end());
252 }
253 return out;
254 }
255
256 const std::vector<StDistrPh<T>> ph = mmapph1fcfs_stdistr_ph(A, svc);
257
258 for (std::size_t r = 0; r < K; ++r) {
259 // Read the PH pair back as the MAP {A, (-A e) alpha} the moment and CDF
260 // routines take.
261 const std::size_t n = ph[r].A.rows();
262 Matrix<T> D1(n, n, zero);
263 for (std::size_t i = 0; i < n; ++i) {
264 T s = zero;
265 for (std::size_t j = 0; j < n; ++j) s += ph[r].A(i, j);
266 for (std::size_t j = 0; j < n; ++j) D1(i, j) = T(-s * ph[r].alpha[j]);
267 }
268 const Map<T> RDph{ph[r].A, D1};
269 const T mean = map_mean(RDph);
270 const T sigma = num_traits<T>::from_double(
271 std::sqrt(num_traits<T>::to_double(map_var(RDph))));
272 // Widen until the tail beyond the grid is negligible, as the reference does.
273 int nsig = 5;
274 for (;;) {
275 const T probe = T(mean + num_traits<T>::from_int(nsig) * sigma);
276 const std::vector<T> c = map_cdf(RDph, std::vector<T>{probe});
277 if (num_traits<T>::to_double(c[0]) >= 1.0 - GlobalConstants::FineTol) break;
278 if (++nsig > 1000)
279 throw NumericError(
280 "solver_mam_passage_time: the sojourn-time CDF did not reach one within 1000 "
281 "standard deviations");
282 }
283 const T xmax = T(mean + num_traits<T>::from_int(nsig) * sigma);
284 std::vector<T> x(npts, zero);
285 for (std::size_t i = 0; i < npts; ++i)
286 x[i] = (npts == 1) ? xmax
288 static_cast<double>(i) /
289 static_cast<double>(npts - 1)));
290 out[r].X = x;
291 out[r].F = map_cdf(RDph, x);
292 }
293 return out;
294 } // if constexpr has_transcendental
295}
296
297/**
298 * Port of the CDF path of `@@SolverMAM/getPerctRespT.m`: linear interpolation of
299 * the response-time CDF at the requested percentile levels.
300 *
301 * Duplicate CDF values are collapsed keeping the LAST, as MATLAB's
302 * `unique(probs,'last')` does, so a flat tail interpolates from the largest
303 * time carrying that probability rather than the smallest.
304 *
305 * The FJ_codes branch of the reference is not reachable here: it reads
306 * percentiles stored by `solver_mam_fj`, which is not ported and whose models
307 * the dispatch refuses.
308 */
309template <class T>
310std::vector<T> mam_percentiles_from_cdf(const RespTCdf<T>& cdf, const std::vector<double>& pcts) {
311 const T zero = num_traits<T>::from_int(0);
312 if (cdf.F.empty()) throw InputError("mam_percentiles_from_cdf: the CDF is empty");
313 std::vector<double> p, t;
314 for (std::size_t i = 0; i < cdf.F.size(); ++i) {
315 const double fi = num_traits<T>::to_double(cdf.F[i]);
316 if (!p.empty() && std::fabs(fi - p.back()) < 1e-15) {
317 t.back() = num_traits<T>::to_double(cdf.X[i]); // keep the LAST
318 continue;
319 }
320 p.push_back(fi);
321 t.push_back(num_traits<T>::to_double(cdf.X[i]));
322 }
323 std::vector<T> out(pcts.size(), zero);
324 for (std::size_t k = 0; k < pcts.size(); ++k) {
325 // Percentiles above 1 are read as percentages, as the reference does.
326 const double want = pcts[k] > 1.0 ? pcts[k] / 100.0 : pcts[k];
327 if (p.size() == 1) {
328 out[k] = num_traits<T>::from_double(t[0]);
329 continue;
330 }
331 std::size_t lo = 0;
332 while (lo + 2 < p.size() && p[lo + 1] < want) ++lo;
333 const double denom = p[lo + 1] - p[lo];
334 const double frac = (std::fabs(denom) < 1e-300) ? 0.0 : (want - p[lo]) / denom;
335 out[k] = num_traits<T>::from_double(t[lo] + frac * (t[lo + 1] - t[lo]));
336 }
337 return out;
338}
339
340} // namespace mam
341} // namespace line
342
343#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...
The MMAP[K]/PH[K]/1 priority queue, preemptive resume (mmapph1prpr_*) and non-preemptive (mmapph1nppr...
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< std::vector< T > > mmapph1prpr_stdistr(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, const std::vector< T > &points, const PrioQueueOptions &opt=PrioQueueOptions())
Per-class sojourn-time CDF at the (strictly positive) points, preemptive resume priority.
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
std::vector< std::vector< T > > mmapph1nppr_stdistr(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, const std::vector< T > &points, const PrioQueueOptions &opt=PrioQueueOptions())
Per-class sojourn-time CDF at the (strictly positive) points, non-preemptive priority.
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< std::vector< T > > mmapph1prpr_stmoms(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, std::size_t n, const PrioQueueOptions &opt=PrioQueueOptions())
Per-class sojourn-time moments 1..n, preemptive resume priority.
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
std::vector< std::vector< T > > mmapph1nppr_stmoms(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, std::size_t n, const PrioQueueOptions &opt=PrioQueueOptions())
Per-class sojourn-time moments 1..n, non-preemptive priority.
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
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:759
static constexpr double FineTol
Definition lang_types.h:760
The options SolverMAM reads.
Definition mam_types.h:30
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