LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_transient_qbd.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_TRANSIENT_QBD_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_TRANSIENT_QBD_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Transient analysis of a single-class open queue by the Laplace-domain
12 * transient QBD plus numerical inverse Laplace, the port of
13 * `matlab/src/solvers/MAM/solver_mam_transient_qbd.m`.
14 *
15 * Covers MAP/MAP/1 (infinite buffer) and MAP/MAP/1/N (finite buffer): arrival
16 * and service are both read as (D0,D1) MAPs, so M/M/1, M/PH/1 and correlated
17 * arrival or service all go through the same construction. That is exactly the
18 * set the expm / libQBD fast path in `solver_mam_ldqbd_transient` cannot
19 * represent, which is why `mam_transient_qbd_applicable` routes here.
20 *
21 * The level-to-level transform V(s,0,m) comes from `mam_transient2_open`
22 * (infinite) or `mam_transient2` (finite) and is inverted per metric with the
23 * CME-based numerical inverse Laplace transform.
24 *
25 * WHY THE INFINITE BRANCH IS EXACT RATHER THAN TRUNCATED. Above the boundary
26 * the chain is homogeneous, so V(s,0,m) = V(s,0,1) R^(m-1) for m >= 1 and the
27 * level sums close in closed form:
28 *
29 * E[N](s) = pi0 V(s,0,1) (I-R)^-2 e
30 * Tput(s) = pi0 V(s,0,1) (I-R)^-1 wDep
31 * P0(s) = pi0 V(s,0,0) e
32 *
33 * so no level truncation enters the infinite-buffer answer at all.
34 *
35 * ARITHMETIC. Double only. The transform is evaluated at complex quadrature
36 * nodes and the inversion weights are transcendental, so an exact
37 * instantiation refuses by name.
38 */
39
40#include <algorithm>
41#include <cmath>
42#include <complex>
43#include <cstddef>
44#include <vector>
45
54#include "line/util/error.h"
55#include "line/util/linalg.h"
56#include "line/util/matrix.h"
57
58namespace line {
59namespace mam {
60
61namespace transient_qbd_detail {
62
63/** Real matrix to complex, entrywise. */
64template <class T>
65inline CMat to_complex(const Matrix<T>& A) {
66 CMat C(A.rows(), A.cols());
67 for (std::size_t i = 0; i < A.rows(); ++i)
68 for (std::size_t j = 0; j < A.cols(); ++j)
69 C(i, j) = Complex(num_traits<T>::to_double(A(i, j)), 0.0);
70 return C;
71}
72
73/** Kronecker product of complex matrices. */
74inline CMat ckron(const CMat& A, const CMat& B) {
75 CMat C(A.rows() * B.rows(), A.cols() * B.cols(), Complex(0.0, 0.0));
76 for (std::size_t i = 0; i < A.rows(); ++i)
77 for (std::size_t j = 0; j < A.cols(); ++j) {
78 if (A(i, j) == Complex(0.0, 0.0)) continue;
79 for (std::size_t k = 0; k < B.rows(); ++k)
80 for (std::size_t l = 0; l < B.cols(); ++l)
81 C(i * B.rows() + k, j * B.cols() + l) = A(i, j) * B(k, l);
82 }
83 return C;
84}
85
86/** Kronecker sum, MATLAB's krons. */
87inline CMat ckrons(const CMat& A, const CMat& B) {
88 const CMat left = ckron(A, eye<Complex>(B.rows()));
89 const CMat right = ckron(eye<Complex>(A.rows()), B);
90 return transient_detail::madd(left, right);
91}
92
93/** pi0 V w, the scalar the inversion actually needs. */
94inline Complex quad(const std::vector<Complex>& pi0, const CMat& V, const std::vector<Complex>& w) {
95 if (pi0.size() != V.rows() || V.cols() != w.size())
96 throw InputError("solver_mam_transient_qbd: quadratic form dimension mismatch");
97 Complex acc(0.0, 0.0);
98 for (std::size_t i = 0; i < V.rows(); ++i) {
99 if (pi0[i] == Complex(0.0, 0.0)) continue;
100 Complex row(0.0, 0.0);
101 for (std::size_t j = 0; j < V.cols(); ++j) row += V(i, j) * w[j];
102 acc += pi0[i] * row;
103 }
104 return acc;
105}
106
107} // namespace transient_qbd_detail
108
109/**
110 * Port of `solver_mam_transient_qbd.m`.
111 *
112 * @param opt `timespan` bounds the horizon; `iter_max` sets the inversion
113 * budget exactly as the reference does
114 * @param L the refreshed struct
115 */
116template <class T>
118 if constexpr (!num_traits<T>::has_transcendental) {
119 throw UnsupportedError(
120 "solver_mam_transient_qbd: the level-to-level transform is evaluated at complex "
121 "Laplace nodes and inverted with transcendental CME weights; rerun with --arith "
122 "double or --arith real");
123 } else {
124 using namespace transient_qbd_detail;
126 const std::size_t M = L.nstations, K = L.nclasses;
127 if (K != 1)
128 throw UnsupportedError(
129 "solver_mam_transient_qbd: the transient QBD method requires a single-class model");
130 if (!std::isinf(L.classes[0].population))
131 throw UnsupportedError(
132 "solver_mam_transient_qbd: the transient QBD method requires an open model");
133
134 std::size_t src = 0, q = 0, nsrc = 0, nq = 0;
135 for (std::size_t i = 1; i <= M; ++i) {
136 if (L.stations[i - 1].sched == SchedStrategy::EXT) { src = i; ++nsrc; }
137 else if (L.stations[i - 1].sched == SchedStrategy::FCFS) { q = i; ++nq; }
138 }
139 if (nsrc != 1 || nq != 1)
140 throw UnsupportedError(
141 "solver_mam_transient_qbd: the transient QBD method requires exactly one Source "
142 "and one FCFS Queue");
143 if (L.stations[q - 1].nservers != 1.0)
144 throw UnsupportedError(
145 "solver_mam_transient_qbd: the Laplace transient QBD supports single-server "
146 "queues only");
147
148 // ---- arrival and service MAPs, uniform (D0,D1) ----------------------
149 const Map<T> arr = lang::dist_to_map(L.service[src - 1][0]);
150 const Map<T> svc = lang::dist_to_map(L.service[q - 1][0]);
151 const CMat Da0 = to_complex(arr.D0), Da1 = to_complex(arr.D1);
152 const CMat Ds0 = to_complex(svc.D0), Ds1 = to_complex(svc.D1);
153 const std::size_t na = Da0.rows(), ns = Ds0.rows();
154 const CMat Ina = eye<Complex>(na), Ins = eye<Complex>(ns);
155
156 // Repeating-level blocks (levels >= 1), the qbd_rg convention.
157 const CMat Lrep = ckrons(Da0, Ds0);
158 const CMat Frep = ckron(Da1, Ins);
159 const CMat Brep = ckron(Ina, Ds1);
160 // Level 0: the service phase is frozen, only the arrival evolves.
161 const CMat Lv0 = ckron(Da0, Ins);
162 const CMat F0 = ckron(Da1, Ins);
163 const CMat B0 = ckron(Ina, Ds1);
164
165 // Empty system, both phase processes stationary.
166 const std::vector<T> piArr = map_prob(arr);
167 const std::vector<T> piSvc = map_prob(svc);
168 std::vector<Complex> pi0(na * ns);
169 for (std::size_t i = 0; i < na; ++i)
170 for (std::size_t j = 0; j < ns; ++j)
171 pi0[i * ns + j] = Complex(num_traits<T>::to_double(piArr[i]) *
172 num_traits<T>::to_double(piSvc[j]),
173 0.0);
174
175 // Service-completion rate over the (arrival, service) phase pairs.
176 std::vector<Complex> wDep(na * ns, Complex(0.0, 0.0));
177 for (std::size_t i = 0; i < na; ++i)
178 for (std::size_t j = 0; j < ns; ++j) {
179 Complex s(0.0, 0.0);
180 for (std::size_t l = 0; l < ns; ++l) s += Ds1(j, l);
181 wDep[i * ns + j] = s;
182 }
183 const std::vector<Complex> wOne(na * ns, Complex(1.0, 0.0));
184
185 const double bufCap = L.cap[q - 1];
186 const bool isFinite = std::isfinite(bufCap);
187
188 // ---- time grid and inversion budget ---------------------------------
189 const double T_start = opt.timespan_start;
190 const double T_end = opt.timespan_end;
191 if (!(T_end > T_start) || !std::isfinite(T_end))
192 throw InputError(
193 "solver_mam_transient_qbd: the timespan must be a finite interval with a positive "
194 "duration");
195 const double dur = T_end - T_start;
196 const std::size_t nTimePoints = static_cast<std::size_t>(std::min(
197 101.0, std::max(11.0, static_cast<double>(std::llround(dur * 10.0)))));
198 std::vector<double> times(nTimePoints);
199 for (std::size_t i = 0; i < nTimePoints; ++i)
200 times[i] = nTimePoints == 1 ? T_start
201 : T_start + dur * static_cast<double>(i) /
202 static_cast<double>(nTimePoints - 1);
203 // The inversion is singular at t = 0 (s = beta/t -> Inf). The system
204 // starts empty there, so E[N] = U = Tput = 0 exactly; invert only on
205 // strictly positive times and write the initial condition directly.
206 std::vector<double> tpos;
207 std::vector<std::size_t> posIdx;
208 for (std::size_t i = 0; i < times.size(); ++i)
209 if (times[i] > 0.0) { tpos.push_back(times[i]); posIdx.push_back(i); }
210
211 std::size_t maxFnEvals = 100;
212 if (opt.iter_max > 1)
213 maxFnEvals = static_cast<std::size_t>(
214 std::min(1000L, std::max(11L, static_cast<long>(std::llround(
215 static_cast<double>(opt.iter_max))))));
216
217 std::vector<double> EN(times.size(), 0.0), DEP(times.size(), 0.0), Uval(times.size(), 0.0);
218
219 if (isFinite) {
220 // Closed piecewise QBD on T = [0, Ncap], one regime.
221 const long Ncap = static_cast<long>(std::llround(bufCap));
222 if (Ncap < 1)
223 throw InputError(
224 "solver_mam_transient_qbd: a finite buffer must hold at least one job");
225 // Arrivals are lost at a full buffer, so the top level keeps the
226 // arrival phase moving without a level change.
227 const CMat LvTop = transient_detail::madd(
228 ckron(transient_detail::madd(Da0, Da1), Ins), ckron(Ina, Ds0));
229 const TransientQbd qbd = make_transient_qbd({Brep}, {Lrep}, {Frep}, {Lv0, LvTop},
230 {0, Ncap});
231
232 const std::vector<double> en = matlab_ilt(
233 [&](const Complex& s) {
234 Complex acc(0.0, 0.0);
235 for (long m = 1; m <= Ncap; ++m)
236 acc += static_cast<double>(m) *
237 quad(pi0, mam_transient2(qbd, 0, m, s), wOne);
238 return acc;
239 },
240 tpos, maxFnEvals);
241 const std::vector<double> p0 = matlab_ilt(
242 [&](const Complex& s) { return quad(pi0, mam_transient2(qbd, 0, 0, s), wOne); },
243 tpos, maxFnEvals);
244 const std::vector<double> dep = matlab_ilt(
245 [&](const Complex& s) {
246 Complex acc(0.0, 0.0);
247 for (long m = 1; m <= Ncap; ++m)
248 acc += quad(pi0, mam_transient2(qbd, 0, m, s), wDep);
249 return acc;
250 },
251 tpos, maxFnEvals);
252 for (std::size_t i = 0; i < posIdx.size(); ++i) {
253 EN[posIdx[i]] = en[i];
254 DEP[posIdx[i]] = dep[i];
255 Uval[posIdx[i]] = 1.0 - p0[i];
256 }
257 } else {
258 // Open piecewise QBD on T = [0, 1]: regime 1 is the boundary level,
259 // regime 2 repeats. L{1} is never referenced from level 0, which is
260 // why the reference leaves it empty.
261 const TransientQbd qbd = make_transient_qbd({B0, Brep}, {CMat(), Lrep}, {F0, Frep},
262 {Lv0, Lrep}, {0, 1});
263 const std::size_t nrep = Lrep.rows();
264
265 // (I-R)^-1 and (I-R)^-2 close the geometric level sums exactly.
266 const auto geom = [&](const Complex& s, unsigned power,
267 const std::vector<Complex>& w) {
268 const CMat Lk = transient_detail::msub(Lrep, transient_detail::sI(s, nrep));
269 const CQbdFundMat gr = qbd_fundmat_laplace(Brep, Lk, Frep);
270 const CMat ImR = transient_detail::msub(eye<Complex>(nrep), gr.R);
271 CMat rhs(nrep, 1);
272 for (std::size_t i = 0; i < nrep; ++i) rhs(i, 0) = w[i];
273 for (unsigned p = 0; p < power; ++p) rhs = transient_detail::mldivide(ImR, rhs);
274 const CMat V1 = mam_transient2_open(qbd, 0, 1, s);
275 std::vector<Complex> col(nrep);
276 for (std::size_t i = 0; i < nrep; ++i) col[i] = rhs(i, 0);
277 return quad(pi0, V1, col);
278 };
279
280 const std::vector<double> en =
281 matlab_ilt([&](const Complex& s) { return geom(s, 2, wOne); }, tpos, maxFnEvals);
282 const std::vector<double> p0 = matlab_ilt(
283 [&](const Complex& s) { return quad(pi0, mam_transient2_open(qbd, 0, 0, s), wOne); },
284 tpos, maxFnEvals);
285 const std::vector<double> dep =
286 matlab_ilt([&](const Complex& s) { return geom(s, 1, wDep); }, tpos, maxFnEvals);
287 for (std::size_t i = 0; i < posIdx.size(); ++i) {
288 EN[posIdx[i]] = en[i];
289 DEP[posIdx[i]] = dep[i];
290 Uval[posIdx[i]] = 1.0 - p0[i];
291 }
292 }
293
294 TranResult<T> out;
295 out.Qt.assign(M, std::vector<TranCurve<T>>(K));
296 out.Ut.assign(M, std::vector<TranCurve<T>>(K));
297 out.Tt.assign(M, std::vector<TranCurve<T>>(K));
298 std::vector<T> qv(times.size()), uv(times.size()), tv(times.size());
299 for (std::size_t i = 0; i < times.size(); ++i) {
300 qv[i] = num_traits<T>::from_double(EN[i]);
301 uv[i] = num_traits<T>::from_double(Uval[i]);
302 tv[i] = num_traits<T>::from_double(DEP[i]);
303 }
304 out.Qt[q - 1][0].values = qv;
305 out.Qt[q - 1][0].times = times;
306 out.Ut[q - 1][0].values = uv;
307 out.Ut[q - 1][0].times = times;
308 out.Tt[q - 1][0].values = tv;
309 out.Tt[q - 1][0].times = times;
310 return out;
311 }
312}
313
314} // namespace mam
315} // namespace line
316
317#endif // LINE_SOLVERS_MAM_SOLVER_MAM_TRANSIENT_QBD_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
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< 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
std::complex<double> as a number type for the generic linear algebra.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Laplace-domain transient level-to-level transform V(s,n,m) of a piecewise level-dependent QBD,...
The option and result types SolverMAM shares with its analyzers.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Numerical inverse Laplace transform in the Abate-Whitt framework, the port of matlab/lib/thirdparty/i...
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
CMat mam_transient2_open(const TransientQbd &q, long n, long m, const Complex &s)
V(s,n,m) for an OPEN piecewise QBD, the port of mam_transient2_open.m.
CMat mam_transient2(const TransientQbd &q, long n, long m, const Complex &s)
V(s,n,m) for a FINITE piecewise QBD, the port of mam_transient2.m.
TranResult< T > solver_mam_transient_qbd(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_transient_qbd.m.
std::vector< T > map_prob(const Map< T > &m)
Stationary distribution of the phase process, pi (D0 + D1) = 0.
Definition map_moment.h:73
Matrix< Complex > CMat
CQbdFundMat qbd_fundmat_laplace(const CMat &B, const CMat &L, const CMat &F, double precision=1e-14, unsigned maxNumIt=50)
Port of qbd_fundmat.m at complex argument: cyclic reduction (Bini-Meini logarithmic reduction) on the...
TransientQbd make_transient_qbd(const std::vector< CMat > &B, const std::vector< CMat > &L, const std::vector< CMat > &F, const std::vector< CMat > &Lv, const std::vector< long > &T)
Build the 1-based padded form from plain 0-based vectors.
std::vector< double > matlab_ilt(const std::function< std::complex< double >(const std::complex< double > &)> &fun, const std::vector< double > &times, std::size_t maxFnEvals, IltMethod method=IltMethod::Cme)
Invert a Laplace transform at the requested time points.
Definition matlab_ilt.h:64
std::complex< double > Complex
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
A queueing network and its refreshed NetworkStruct.
Port of solver_mam_ldqbd_transient.m: transient queue length, utilization and throughput of a single-...
G and R of a QBD whose local block is complex.
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
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
The piecewise QBD blocks, 1-based exactly as the reference's cell arrays.