LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_ldqbd_transient.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_LDQBD_TRANSIENT_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_LDQBD_TRANSIENT_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_ldqbd_transient.m`: transient queue length, utilization
12 * and throughput of a single-class OPEN queue, and the fast path behind
13 * `getTranAvg`.
14 *
15 * TWO ENGINES, chosen by the buffer, and the split is forced rather than
16 * stylistic:
17 * - FINITE capacity: the generator is a finite matrix, so the law is
18 * pi(t) = pi(0) exp(Qt), stepped on a uniform grid by ONE matrix exponential
19 * `expm(Q dt)` reused at every point. That is the reference's construction
20 * and it is exact up to `expm`.
21 * - INFINITE capacity: there is no finite generator to exponentiate, so the
22 * reference calls libQBD's adaptive Taylor series, which grows the
23 * represented level depth as mass reaches it (`api/mam/libqbd_taylor.h`).
24 * The reference grid is libQBD's own, one point per 1/|min diagonal|.
25 *
26 * WHAT THE PORT DOES NOT DO. The reference's time grid in the finite branch is
27 * `min(101, max(11, round(10 T)))` points, which is reproduced exactly, because
28 * a transient result read off a different grid cannot be compared point for
29 * point with the reference at all.
30 *
31 * PH SERVICE IS SINGLE-SERVER ONLY, in both branches, and the reference says so
32 * (`Transient QBD with PH service supports single-server only`): the level
33 * phase would have to carry the multiset of in-service phases. Refused by name.
34 */
35
36#include <algorithm>
37#include <cmath>
38#include <cstddef>
39#include <string>
40#include <vector>
41
47#include "line/util/error.h"
48#include "line/util/expm.h"
49#include "line/util/linalg.h"
50#include "line/util/matrix.h"
51
52namespace line {
53namespace mam {
54
55/** One station-class transient curve, the reference's `[metric, time]` pair. */
56template <class T>
57struct TranCurve {
58 std::vector<T> values;
59 std::vector<double> times;
60};
61
62/** What `getTranAvg` returns: queue length, utilization and throughput curves. */
63template <class T>
64struct TranResult {
65 /** Indexed [station][class]; only the queue station is populated. */
66 std::vector<std::vector<TranCurve<T>>> Qt, Ut, Tt;
67};
68
69/**
70 * Port of `mam_transient_qbd_applicable.m`: true when the Laplace-domain
71 * transient QBD should run instead of this fast path.
72 *
73 * The Laplace solver is for single-server open queues whose ARRIVAL is
74 * non-Poisson or whose SERVICE is a correlated (non-renewal) MAP -- exactly
75 * what the level structure here cannot represent. Poisson arrival with PH or
76 * exponential service, and M/M/c, stay on the fast path.
77 */
78template <class T>
81 if (L.nclasses != 1) return false;
82 if (!std::isinf(L.classes[0].population)) return false;
83 std::size_t src = 0, q = 0, nsrc = 0, nq = 0;
84 for (std::size_t i = 1; i <= L.nstations; ++i) {
85 if (L.stations[i - 1].sched == SchedStrategy::EXT) { src = i; ++nsrc; }
86 else if (L.stations[i - 1].sched == SchedStrategy::FCFS) { q = i; ++nq; }
87 }
88 if (nsrc != 1 || nq != 1) return false;
89 if (L.stations[q - 1].nservers != 1.0) return false;
90 const Map<T> arv = lang::dist_to_map(L.service[src - 1][0]);
91 const Map<T> svc = lang::dist_to_map(L.service[q - 1][0]);
92 const bool arrivalIsPoisson = (arv.D0.rows() == 1);
93 const bool serviceIsRenewal = basic_detail::is_renewal_map(svc);
94 return !arrivalIsPoisson || !serviceIsRenewal;
95}
96
97/**
98 * Port of `solver_mam_ldqbd_transient.m`.
99 *
100 * @param opt `timespan` bounds the horizon; `tol` is the Taylor truncation
101 * target in the infinite branch
102 * @param L the refreshed struct
103 */
104template <class T>
106 if constexpr (!num_traits<T>::has_transcendental) {
107 throw UnsupportedError(
108 "solver_mam_ldqbd_transient: the transient law is a matrix exponential (finite "
109 "buffer) or a tolerance-truncated Taylor series with an incomplete-gamma error bound "
110 "(infinite buffer); rerun with --arith double or --arith real");
111 } else {
113 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
114 const std::size_t M = L.nstations, K = L.nclasses;
115 if (K != 1)
116 throw UnsupportedError(
117 "solver_mam_ldqbd_transient: the transient QBD method requires a single-class model");
118 if (!std::isinf(L.classes[0].population))
119 throw UnsupportedError(
120 "solver_mam_ldqbd_transient: the transient QBD method requires an open model");
121
122 std::size_t src = 0, q = 0, nsrc = 0, nq = 0;
123 for (std::size_t i = 1; i <= M; ++i) {
124 if (L.stations[i - 1].sched == SchedStrategy::EXT) { src = i; ++nsrc; }
125 else if (L.stations[i - 1].sched == SchedStrategy::FCFS) { q = i; ++nq; }
126 }
127 if (nsrc != 1 || nq != 1)
128 throw UnsupportedError(
129 "solver_mam_ldqbd_transient: the transient QBD method requires exactly one Source and "
130 "one FCFS Queue");
131
132 const T lambda = L.rates(src - 1, 0);
133 const Map<T> PHq = lang::dist_to_map(L.service[q - 1][0]);
134 const double nServers = L.stations[q - 1].nservers;
135 const double bufCap = L.cap[q - 1];
136 const std::size_t nPhases = PHq.D0.rows();
137 const bool isPH = nPhases > 1;
138 T mu = zero;
139 std::vector<T> alphaV;
140 Matrix<T> texit;
141 if (!isPH) {
142 mu = T(-PHq.D0(0, 0));
143 } else {
144 alphaV = map_pie(PHq);
145 texit = Matrix<T>(nPhases, 1, zero);
146 for (std::size_t i = 0; i < nPhases; ++i) {
147 T s = zero;
148 for (std::size_t j = 0; j < nPhases; ++j) s += PHq.D0(i, j);
149 texit(i, 0) = -s;
150 }
151 }
152 if (isPH && nServers > 1.0)
153 throw UnsupportedError(
154 "solver_mam_ldqbd_transient: transient QBD with PH service supports single-server "
155 "queues only; the level phase would have to carry the multiset of in-service phases");
156
157 const double T_start = opt.timespan_start;
158 const double T_end = opt.timespan_end;
159 if (!(T_end > T_start) || !std::isfinite(T_end))
160 throw InputError(
161 "solver_mam_ldqbd_transient: the timespan must be a finite interval with a positive "
162 "duration");
163 const double T_duration = T_end - T_start;
164 const unsigned c = static_cast<unsigned>(
165 std::isfinite(nServers) ? std::llround(nServers) : 1);
166
167 std::vector<double> times;
168 std::vector<T> qlen, util, tput;
169
170 if (std::isfinite(bufCap)) {
171 // ---- finite capacity: one expm, stepped ---------------------------
172 const std::size_t Cap = static_cast<std::size_t>(std::llround(bufCap));
173 std::size_t dim;
174 Matrix<T> Q;
175 if (!isPH) {
176 dim = Cap + 1;
177 Q = Matrix<T>(dim, dim, zero);
178 for (std::size_t n = 0; n <= Cap; ++n) {
179 const T dep = num_traits<T>::from_double(
180 std::min(static_cast<double>(n), static_cast<double>(c))) * mu;
181 const T arr = (n < Cap) ? lambda : zero;
182 if (n > 0) Q(n, n - 1) = dep;
183 if (n < Cap) Q(n, n + 1) = arr;
184 Q(n, n) = T(-(dep + arr));
185 }
186 } else {
187 dim = 1 + Cap * nPhases;
188 Q = Matrix<T>(dim, dim, zero);
189 Q(0, 0) = -lambda;
190 for (std::size_t j = 0; j < nPhases; ++j) Q(0, 1 + j) = T(lambda * alphaV[j]);
191 for (std::size_t n = 1; n <= Cap; ++n) {
192 const std::size_t r0 = 1 + (n - 1) * nPhases;
193 for (std::size_t i = 0; i < nPhases; ++i)
194 for (std::size_t j = 0; j < nPhases; ++j) {
195 Q(r0 + i, r0 + j) = PHq.D0(i, j);
196 if (i == j && n < Cap) Q(r0 + i, r0 + j) -= lambda;
197 }
198 if (n < Cap)
199 for (std::size_t i = 0; i < nPhases; ++i)
200 Q(r0 + i, r0 + nPhases + i) = lambda;
201 if (n == 1) {
202 for (std::size_t i = 0; i < nPhases; ++i) Q(r0 + i, 0) = texit(i, 0);
203 } else {
204 const std::size_t p0 = r0 - nPhases;
205 for (std::size_t i = 0; i < nPhases; ++i)
206 for (std::size_t j = 0; j < nPhases; ++j) Q(r0 + i, p0 + j) = PHq.D1(i, j);
207 }
208 }
209 }
210 const std::size_t nT = static_cast<std::size_t>(std::min<double>(
211 101.0, std::max<double>(11.0, std::round(T_duration * 10.0))));
212 const double dt = T_duration / static_cast<double>(nT - 1);
213 const Matrix<T> eQdt = expm(Q, num_traits<T>::from_double(dt));
214
215 std::vector<T> pi(dim, zero);
216 pi[0] = one;
217 for (std::size_t t = 0; t < nT; ++t) {
218 times.push_back(T_start + static_cast<double>(t) * dt);
219 T qv = zero, uv = zero, tv = zero;
220 for (std::size_t n = 0; n <= Cap; ++n) {
221 T pn = zero;
222 if (!isPH) {
223 pn = pi[n];
224 } else if (n == 0) {
225 pn = pi[0];
226 } else {
227 for (std::size_t i = 0; i < nPhases; ++i) pn += pi[1 + (n - 1) * nPhases + i];
228 }
229 qv += num_traits<T>::from_int(static_cast<int>(n)) * pn;
230 if (n >= 1) {
232 std::min(static_cast<double>(n), static_cast<double>(c)) /
233 static_cast<double>(c)) * pn;
234 if (!isPH) {
236 std::min(static_cast<double>(n), static_cast<double>(c))) * mu *
237 pn;
238 } else {
239 for (std::size_t i = 0; i < nPhases; ++i)
240 tv += pi[1 + (n - 1) * nPhases + i] * texit(i, 0);
241 }
242 }
243 }
244 qlen.push_back(qv);
245 util.push_back(uv);
246 tput.push_back(tv);
247 if (t + 1 < nT) pi = vecmul(pi, eQdt);
248 }
249 } else {
250 // ---- infinite capacity: libQBD's adaptive Taylor series -----------
251 LibQbdProcess<T> proc;
252 if (!isPH) {
253 proc.add_zero_level(Matrix<T>(1, 1, T(-lambda)), Matrix<T>(1, 1, lambda));
254 for (unsigned n = 1; n + 1 <= c; ++n) {
255 const T dep = num_traits<T>::from_int(static_cast<int>(n)) * mu;
256 proc.add_level(Matrix<T>(1, 1, dep), Matrix<T>(1, 1, T(-(lambda + dep))),
257 Matrix<T>(1, 1, lambda));
258 }
259 const T dep = num_traits<T>::from_int(static_cast<int>(c)) * mu;
260 proc.add_final_level(Matrix<T>(1, 1, dep), Matrix<T>(1, 1, T(-(lambda + dep))));
261 } else {
262 Matrix<T> up0(1, nPhases, zero);
263 for (std::size_t j = 0; j < nPhases; ++j) up0(0, j) = T(lambda * alphaV[j]);
264 proc.add_zero_level(Matrix<T>(1, 1, T(-lambda)), up0);
265 Matrix<T> A10(nPhases, nPhases, zero), A1p(nPhases, nPhases, zero);
266 for (std::size_t i = 0; i < nPhases; ++i) {
267 for (std::size_t j = 0; j < nPhases; ++j) A10(i, j) = PHq.D0(i, j);
268 A10(i, i) -= lambda;
269 A1p(i, i) = lambda;
270 }
271 proc.add_level(texit, A10, A1p);
272 proc.add_final_level(PHq.D1, A10);
273 }
274 std::vector<std::vector<T>> pi0(1, std::vector<T>(1, one));
275 const TaylorSeriesResult<T> ts =
276 taylor_series_adaptive(proc, pi0, opt.tol, T_duration);
277 for (std::size_t t = 0; t < ts.times.size(); ++t) {
278 times.push_back(ts.times[t] + T_start);
279 const std::vector<std::vector<T>>& d = ts.dists[t];
280 T qv = zero, uv = zero, tv = zero;
281 for (std::size_t n = 1; n < d.size(); ++n) {
282 T pn = zero;
283 for (const T& v : d[n]) pn += v;
284 qv += num_traits<T>::from_int(static_cast<int>(n)) * pn;
286 std::min(static_cast<double>(n), static_cast<double>(c)) /
287 static_cast<double>(c)) * pn;
288 if (!isPH) {
290 std::min(static_cast<double>(n), static_cast<double>(c))) * mu * pn;
291 } else {
292 for (std::size_t i = 0; i < d[n].size() && i < nPhases; ++i)
293 tv += d[n][i] * texit(i, 0);
294 }
295 }
296 qlen.push_back(qv);
297 util.push_back(uv);
298 tput.push_back(tv);
299 }
300 }
301
302 TranResult<T> out;
303 out.Qt.assign(M, std::vector<TranCurve<T>>(K));
304 out.Ut.assign(M, std::vector<TranCurve<T>>(K));
305 out.Tt.assign(M, std::vector<TranCurve<T>>(K));
306 out.Qt[q - 1][0] = TranCurve<T>{qlen, times};
307 out.Ut[q - 1][0] = TranCurve<T>{util, times};
308 out.Tt[q - 1][0] = TranCurve<T>{tput, times};
309 return out;
310 } // if constexpr has_transcendental
311}
312
313} // namespace mam
314} // namespace line
315
316#endif // LINE_SOLVERS_MAM_SOLVER_MAM_LDQBD_TRANSIENT_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A QBD's level blocks, as libQBD's QBD class holds them.
void add_zero_level(const Matrix< T > &Aplus)
Level zero from its upward block alone; the local block is the row-sum negative.
void add_final_level(const Matrix< T > &Aminus)
The repeating level: its up block is the previous one, reused for ever.
void add_level(const Matrix< T > &Aminus, const Matrix< T > &Aplus)
A level from its down and up blocks; the local block closes the rows.
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< double > cap
sn.cap and sn.classcap: the total and per-class buffers.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Transient distribution of a level-independent-in-the-tail QBD by an adaptive Taylor series (libQBD QB...
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
The option and result types SolverMAM shares with its analyzers.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
bool mam_transient_qbd_applicable(const qn::NetworkStruct< T > &L)
Port of mam_transient_qbd_applicable.m: true when the Laplace-domain transient QBD should run instead...
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
TranResult< T > solver_mam_ldqbd_transient(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_ldqbd_transient.m.
TaylorSeriesResult< T > taylor_series_adaptive(const LibQbdProcess< T > &proc, const std::vector< std::vector< T > > &pi0, double error, double max_time)
libQBD's TaylorSeriesAdaptive, restricted to the reference grid that solver_mam_ldqbd_transient reads...
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 > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
A queueing network and its refreshed NetworkStruct.
The options SolverMAM reads.
Definition mam_types.h:29
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
What the adaptive Taylor series returns: the reference grid and its laws.
std::vector< double > times
the reference points
std::vector< std::vector< std::vector< T > > > dists
per point, per level, per phase
One station-class transient curve, the reference's [metric, time] pair.
What getTranAvg returns: queue length, utilization and throughput curves.
std::vector< std::vector< TranCurve< T > > > Qt
Indexed [station][class]; only the queue station is populated.
std::vector< std::vector< TranCurve< T > > > Ut
std::vector< std::vector< TranCurve< T > > > Tt