LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
dtime.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_API_MAM_DTIME_H
6#define LINE_API_MAM_DTIME_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Discrete-time (slotted) matrix-analytic primitives and queues.
12 *
13 * Port of matlab/src/api/mam/dph_from_dist.m, dph_to_dmap.m, dmap_to_dph.m,
14 * dmap_is_renewal.m, dmap_lambda.m, dmap_super.m, dmap_thin.m and
15 * mg1_dt_queue.m, plus the queue-length half of the Q-MAM discrete-time queues
16 * Q_DT_MAP_MAP_1.m and Q_DT_PH_PH_1.m.
17 *
18 * Everything measures time in SLOTS and follows the late arrival system with
19 * delayed access (LAS-DA): within a slot the service completion resolves first,
20 * arrivals are appended at the end of the slot and cannot enter service before
21 * the next one, and the level is read after both. The QBD blocks state it
22 * directly, A1 = kron(C1,D0) being the claim that a job arriving at the end of a
23 * slot is not served within it. The LDES slotted engine orders its intra-slot
24 * events the same way, so the two are directly comparable.
25 *
26 * A discrete phase-type law is (alpha, A) with P[X=k] = alpha A^(k-1) a,
27 * a = e - A e, k = 1,2,... A batch stream is a vector [A_0, A_1, ...] whose
28 * A_k carries the slots delivering k events; a plain D-MAP is the two-entry case.
29 *
30 * Two solver differences from the MATLAB twin, both deliberate and measured
31 * against it: the QBD fundamental matrix uses logarithmic reduction rather than
32 * SMCSolver's cyclic reduction (same minimal solution, both iterate to 1e-14),
33 * and the batch M/G/1-type chain is solved by level truncation rather than by
34 * MG1_CR, because C++ carries neither SMCSolver nor BuTools. The truncation
35 * level is chosen from the tail mass, so it is an accuracy knob, not a model
36 * change.
37 */
38
39#include <algorithm>
40#include <cmath>
41#include <cstddef>
42#include <vector>
43
44#include "line/api/mam/dmap.h"
47#include "line/num/number.h"
48#include "line/util/error.h"
49#include "line/util/linalg.h"
50#include "line/util/lu.h"
51#include "line/util/matrix.h"
52
53namespace line {
54namespace mam {
55
56/** A discrete phase-type law: initial row vector alpha and transient A. */
57template <class T>
58struct Dph {
59 std::vector<T> alpha;
61
62 std::size_t order() const { return A.rows(); }
63};
64
65/** A discrete batch arrival stream, entry k carrying the slots with k events. */
66template <class T>
67using DBatch = std::vector<Matrix<T>>;
68
69namespace detail {
70
71/** Kronecker product; C++ has no shared templated kron. */
72template <class T>
73Matrix<T> dt_kron(const Matrix<T>& A, const Matrix<T>& B) {
74 Matrix<T> C(A.rows() * B.rows(), A.cols() * B.cols(), num_traits<T>::from_int(0));
75 for (std::size_t i = 0; i < A.rows(); ++i)
76 for (std::size_t j = 0; j < A.cols(); ++j) {
77 const T& a = A(i, j);
78 for (std::size_t k = 0; k < B.rows(); ++k)
79 for (std::size_t l = 0; l < B.cols(); ++l)
80 C(i * B.rows() + k, j * B.cols() + l) = a * B(k, l);
81 }
82 return C;
83}
84
85template <class T>
86Matrix<T> dt_eye(std::size_t n) {
88 for (std::size_t i = 0; i < n; ++i) I(i, i) = num_traits<T>::from_int(1);
89 return I;
90}
91
92template <class T>
93Matrix<T> dt_add(const Matrix<T>& A, const Matrix<T>& B) {
94 Matrix<T> C = A;
95 for (std::size_t i = 0; i < A.rows(); ++i)
96 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = C(i, j) + B(i, j);
97 return C;
98}
99
100template <class T>
101Matrix<T> dt_sub(const Matrix<T>& A, const Matrix<T>& B) {
102 Matrix<T> C = A;
103 for (std::size_t i = 0; i < A.rows(); ++i)
104 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = C(i, j) - B(i, j);
105 return C;
106}
107
108template <class T>
109Matrix<T> dt_mul(const Matrix<T>& A, const Matrix<T>& B) {
111 for (std::size_t i = 0; i < A.rows(); ++i)
112 for (std::size_t k = 0; k < A.cols(); ++k) {
113 const T& a = A(i, k);
114 for (std::size_t j = 0; j < B.cols(); ++j) C(i, j) = C(i, j) + a * B(k, j);
115 }
116 return C;
117}
118
119/** Solves X * M = N for X, i.e. M^T X^T = N^T column by column. */
120template <class T>
121Matrix<T> dt_right_divide(const Matrix<T>& N, const Matrix<T>& M) {
122 std::size_t n = M.rows();
123 Matrix<T> Mt(n, n);
124 for (std::size_t i = 0; i < n; ++i)
125 for (std::size_t j = 0; j < n; ++j) Mt(i, j) = M(j, i);
126 Matrix<T> X(N.rows(), n);
127 for (std::size_t r = 0; r < N.rows(); ++r) {
128 std::vector<T> rhs(n);
129 for (std::size_t j = 0; j < n; ++j) rhs[j] = N(r, j);
130 std::vector<T> sol = solve(Mt, rhs);
131 for (std::size_t j = 0; j < n; ++j) X(r, j) = sol[j];
132 }
133 return X;
134}
135
136/** Left-multiplies the inverse: returns M^-1 * N. */
137template <class T>
138Matrix<T> dt_left_solve(const Matrix<T>& M, const Matrix<T>& N) {
139 std::size_t n = M.rows();
140 Matrix<T> X(n, N.cols());
141 for (std::size_t c = 0; c < N.cols(); ++c) {
142 std::vector<T> rhs(n);
143 for (std::size_t i = 0; i < n; ++i) rhs[i] = N(i, c);
144 std::vector<T> sol = solve(M, rhs);
145 for (std::size_t i = 0; i < n; ++i) X(i, c) = sol[i];
146 }
147 return X;
148}
149
150} // namespace detail
151
152/**
153 * Exact discrete phase-type representation of a lattice-valued law.
154 *
155 * Geometric, Det and DiscreteUniform are represented EXACTLY, not
156 * moment-matched: a fitted surrogate would leave the lattice the caller relies
157 * on, so any other family is an error here.
158 */
159template <class T>
160Dph<T> dph_from_dist(lang::ProcessType type, const T& mean_slots, const T& scv) {
161 const T one = num_traits<T>::from_int(1);
162 const T zero = num_traits<T>::from_int(0);
163 const double tol = 1e-8;
164
165 if (type == lang::ProcessType::GEOMETRIC) {
166 T p = one / mean_slots;
167 if (num_traits<T>::to_double(p) > 1 + tol || num_traits<T>::to_double(p) <= 0)
168 throw InputError("dph_from_dist: Geometric mean is outside the support {1,2,...}");
169 if (num_traits<T>::to_double(p) > 1) p = one;
170 Dph<T> d;
171 d.alpha.assign(1, one);
172 d.A = Matrix<T>(1, 1, one - p);
173 return d;
174 }
175
176 if (type == lang::ProcessType::DET) {
177 double m = num_traits<T>::to_double(mean_slots);
178 long k = static_cast<long>(m + 0.5);
179 if (std::abs(m - static_cast<double>(k)) > tol * std::max(1.0, m) || k < 1)
180 throw InputError("dph_from_dist: Det is not a positive integral number of slots");
181 std::size_t n = static_cast<std::size_t>(k);
182 Dph<T> d;
183 d.alpha.assign(n, zero);
184 d.alpha[0] = one;
185 d.A = Matrix<T>(n, n, zero);
186 for (std::size_t i = 0; i + 1 < n; ++i) d.A(i, i + 1) = one;
187 return d;
188 }
189
190 if (type == lang::ProcessType::DUNIFORM) {
191 double m = num_traits<T>::to_double(mean_slots);
192 double v = num_traits<T>::to_double(scv) * m * m;
193 double width = std::sqrt(std::max(0.0, 12 * v + 1)) - 1;
194 long lo = static_cast<long>(m - width / 2 + 0.5);
195 long hi = static_cast<long>(m + width / 2 + 0.5);
196 if (lo < 1 || hi < lo)
197 throw InputError("dph_from_dist: DiscreteUniform is outside the support {1,2,...}");
198 std::size_t n = static_cast<std::size_t>(hi);
199 Dph<T> d;
200 d.alpha.assign(n, zero);
201 d.alpha[0] = one;
202 d.A = Matrix<T>(n, n, zero);
203 for (long j = 1; j < hi; ++j) {
204 // hazard of absorbing at step j, zero below the lower bound
205 T h = j < lo ? zero : one / num_traits<T>::from_int(static_cast<int>(hi - j + 1));
206 d.A(static_cast<std::size_t>(j - 1), static_cast<std::size_t>(j)) = one - h;
207 }
208 return d;
209 }
210
211 throw InputError(
212 "dph_from_dist: the process type has no exact discrete phase-type representation. "
213 "The discrete-time path accepts Geometric, Det on the slot lattice, DiscreteUniform "
214 "and DMAP.");
215}
216
217/** Renewal D-MAP (A, a alpha) of a discrete phase-type law. */
218template <class T>
220 const T one = num_traits<T>::from_int(1);
221 std::size_t m = d.A.rows();
222 Dmap<T> out;
223 out.D0 = d.A;
224 out.D1 = Matrix<T>(m, m, num_traits<T>::from_int(0));
225 for (std::size_t i = 0; i < m; ++i) {
226 T rowsum = num_traits<T>::from_int(0);
227 for (std::size_t j = 0; j < m; ++j) rowsum = rowsum + d.A(i, j);
228 T a = one - rowsum;
229 for (std::size_t j = 0; j < m; ++j) out.D1(i, j) = a * d.alpha[j];
230 }
231 return out;
232}
233
234/** True when D1 has rank one, i.e. the process renews at every event. */
235template <class T>
236bool dmap_is_renewal(const Dmap<T>& d) {
237 std::size_t m = d.D0.rows();
238 if (m == 1) return true;
239 std::vector<T> row_mass(m, num_traits<T>::from_int(0));
240 std::size_t pivot = 0;
241 for (std::size_t i = 0; i < m; ++i) {
242 for (std::size_t j = 0; j < m; ++j) row_mass[i] = row_mass[i] + d.D1(i, j);
243 if (num_traits<T>::to_double(row_mass[i]) > num_traits<T>::to_double(row_mass[pivot]))
244 pivot = i;
245 }
246 if (num_traits<T>::to_double(row_mass[pivot]) <= 1e-14) return false;
247 for (std::size_t i = 0; i < m; ++i)
248 for (std::size_t j = 0; j < m; ++j) {
249 T expected = row_mass[i] * d.D1(pivot, j) / row_mass[pivot];
250 if (std::abs(num_traits<T>::to_double(d.D1(i, j) - expected)) > 1e-8) return false;
251 }
252 return true;
253}
254
255/** Discrete phase-type law underlying a renewal D-MAP. */
256template <class T>
258 if (!dmap_is_renewal(d))
259 throw InputError("dmap_to_dph: the D-MAP does not renew at events, so it has no DPH form");
260 std::size_t m = d.D0.rows();
261 std::vector<T> row_mass(m, num_traits<T>::from_int(0));
262 std::size_t pivot = 0;
263 for (std::size_t i = 0; i < m; ++i) {
264 for (std::size_t j = 0; j < m; ++j) row_mass[i] = row_mass[i] + d.D1(i, j);
265 if (num_traits<T>::to_double(row_mass[i]) > num_traits<T>::to_double(row_mass[pivot]))
266 pivot = i;
267 }
268 Dph<T> out;
269 out.A = d.D0;
270 out.alpha.resize(m);
271 for (std::size_t j = 0; j < m; ++j) out.alpha[j] = d.D1(pivot, j) / row_mass[pivot];
272 return out;
273}
274
275/**
276 * Mean number of EVENTS per slot, pi sum_k k A_k e. A slot carrying a batch of
277 * two counts twice, which is what Little's law consumes downstream.
278 */
279template <class T>
281 std::size_t m = A[0].rows();
283 for (std::size_t k = 0; k < A.size(); ++k) P = detail::dt_add(P, A[k]);
284 std::vector<T> pi = mc::dtmc_solve(P);
285 T lambda = num_traits<T>::from_int(0);
286 for (std::size_t k = 1; k < A.size(); ++k)
287 for (std::size_t i = 0; i < m; ++i)
288 for (std::size_t j = 0; j < m; ++j)
289 lambda = lambda + pi[i] * num_traits<T>::from_int(static_cast<int>(k)) * A[k](i, j);
290 return lambda;
291}
292
293/**
294 * Superposition, E_k = sum_{i+j=k} kron(A_i, B_j).
295 *
296 * NOT closed on D-MAPs: two slotted streams fire in the same slot with positive
297 * probability, so the merged stream carries batches. Folding E_2 into E_1 would
298 * conserve neither the arrival rate nor the slot in which the work appears, so
299 * the batch dimension is kept and the station is solved as an M/G/1-type chain
300 * instead of a QBD.
301 */
302template <class T>
304 std::size_t p = A.size() - 1, q = B.size() - 1;
305 DBatch<T> E;
306 E.reserve(p + q + 1);
307 for (std::size_t k = 0; k <= p + q; ++k) {
308 Matrix<T> Ek(A[0].rows() * B[0].rows(), A[0].cols() * B[0].cols(),
310 std::size_t lo = k > q ? k - q : 0;
311 for (std::size_t i = lo; i <= (k < p ? k : p); ++i)
312 Ek = detail::dt_add(Ek, detail::dt_kron(A[i], B[k - i]));
313 E.push_back(Ek);
314 }
315 return E;
316}
317
318/**
319 * Bernoulli thinning, B_k = sum_{n>=k} C(n,k) p^k (1-p)^(n-k) A_n. The phase
320 * process is untouched, so this is exact for PROB/RAND routing.
321 */
322template <class T>
323DBatch<T> dmap_thin(const DBatch<T>& A, const T& p) {
324 double pd = num_traits<T>::to_double(p);
325 if (pd < 0 || pd > 1) throw InputError("dmap_thin: the routing probability must lie in [0,1]");
326 const T one = num_traits<T>::from_int(1);
327 std::size_t n = A.size() - 1;
328 DBatch<T> B;
329 B.reserve(n + 1);
330 for (std::size_t k = 0; k <= n; ++k) {
331 Matrix<T> Bk(A[0].rows(), A[0].cols(), num_traits<T>::from_int(0));
332 for (std::size_t j = k; j <= n; ++j) {
334 for (std::size_t i = 0; i < k; ++i)
335 w = w * num_traits<T>::from_int(static_cast<int>(j - i)) /
336 num_traits<T>::from_int(static_cast<int>(i + 1));
337 for (std::size_t i = 0; i < k; ++i) w = w * p;
338 for (std::size_t i = 0; i < j - k; ++i) w = w * (one - p);
339 if (num_traits<T>::to_double(w) > 0)
340 for (std::size_t r = 0; r < Bk.rows(); ++r)
341 for (std::size_t c = 0; c < Bk.cols(); ++c)
342 Bk(r, c) = Bk(r, c) + w * A[j](r, c);
343 }
344 B.push_back(Bk);
345 }
346 // trailing zero batch levels carry no mass and only inflate the blocks
347 while (B.size() > 2) {
348 double worst = 0;
349 for (std::size_t r = 0; r < B.back().rows(); ++r)
350 for (std::size_t c = 0; c < B.back().cols(); ++c)
351 worst = std::max(worst, std::abs(num_traits<T>::to_double(B.back()(r, c))));
352 if (worst >= 1e-14) break;
353 B.pop_back();
354 }
355 return B;
356}
357
358namespace detail {
359
360/**
361 * Raw moments over {1,2,...} of a two-phase acyclic discrete phase-type law
362 * written as a mixture: with probability b the sum of two geometrics with
363 * success probabilities p1 and p2, otherwise the second geometric alone.
364 */
365inline void dph2_moments(double b, double p1, double p2, double* m1, double* m2, double* m3) {
366 const double A1 = 1 / p1, A2 = 1 / p2;
367 const double S1 = (2 - p1) / (p1 * p1), S2 = (2 - p2) / (p2 * p2);
368 const double C1 = (p1 * p1 - 6 * p1 + 6) / (p1 * p1 * p1);
369 const double C2 = (p2 * p2 - 6 * p2 + 6) / (p2 * p2 * p2);
370 *m1 = b * A1 + A2;
371 *m2 = b * (S1 + 2 * A1 * A2) + S2;
372 *m3 = b * (C1 + 3 * S1 * A2 + 3 * A1 * S2) + C2;
373}
374
375/**
376 * Fits the mixture above to three raw moments by a damped Newton iteration on
377 * (p1,p2), the mixing weight following from the first moment. Returns false
378 * when no feasible triple is reached from any start.
379 */
380inline bool dph2_from_3moments(double m1, double m2, double m3, double* b_out, double* p1_out,
381 double* p2_out) {
382 const double starts[4][2] = {{0.5, 0.5}, {0.9, 1 / std::max(1.0, m1)}, {0.2, 0.8}, {0.99, 0.3}};
383 for (int s = 0; s < 4; ++s) {
384 double p1 = starts[s][0], p2 = starts[s][1];
385 for (int it = 0; it < 200; ++it) {
386 const double b = (m1 - 1 / p2) * p1;
387 double f1, f2, f3;
388 dph2_moments(b, p1, p2, &f1, &f2, &f3);
389 const double r2 = f2 - m2, r3 = f3 - m3;
390 if (std::abs(r2) <= 1e-11 * std::max(1.0, std::abs(m2)) &&
391 std::abs(r3) <= 1e-11 * std::max(1.0, std::abs(m3))) {
392 if (b < -1e-9 || b > 1 + 1e-9 || p1 <= 0 || p1 > 1 || p2 <= 0 || p2 > 1) break;
393 *b_out = std::min(1.0, std::max(0.0, b));
394 *p1_out = p1;
395 *p2_out = p2;
396 return true;
397 }
398 const double h = 1e-7;
399 double a2, a3, c2, c3, d2, d3, dummy;
400 dph2_moments((m1 - 1 / p2) * (p1 + h), p1 + h, p2, &dummy, &a2, &a3);
401 dph2_moments((m1 - 1 / (p2 + h)) * p1, p1, p2 + h, &dummy, &c2, &c3);
402 const double j11 = (a2 - f2) / h, j12 = (c2 - f2) / h;
403 const double j21 = (a3 - f3) / h, j22 = (c3 - f3) / h;
404 const double det = j11 * j22 - j12 * j21;
405 if (std::abs(det) < 1e-18) break;
406 double dp1 = -(j22 * r2 - j12 * r3) / det;
407 double dp2 = -(-j21 * r2 + j11 * r3) / det;
408 // damping keeps the step inside (0,1], where the parameters live
409 double step = 1.0;
410 while (step > 1e-6 && (p1 + step * dp1 <= 1e-9 || p1 + step * dp1 > 1 ||
411 p2 + step * dp2 <= 1e-9 || p2 + step * dp2 > 1))
412 step /= 2;
413 if (step <= 1e-6) break;
414 p1 += step * dp1;
415 p2 += step * dp2;
416 }
417 }
418 return false;
419}
420
421/** BuTools FactorialMomsFromMoms: E[X(X-1)...(X-i+1)] from the raw moments. */
422inline std::vector<double> butools_factorial_moms(const std::vector<double>& m) {
423 const std::size_t n = m.size();
424 std::vector<double> fm(n, 0.0);
425 for (std::size_t i = 1; i <= n; ++i) {
426 // coefficients of prod_{j=0}^{i-1} (x - j), ascending in x
427 std::vector<double> c(1, 1.0);
428 for (std::size_t j = 0; j < i; ++j) {
429 std::vector<double> nc(c.size() + 1, 0.0);
430 for (std::size_t q = 0; q < c.size(); ++q) {
431 nc[q + 1] += c[q];
432 nc[q] -= static_cast<double>(j) * c[q];
433 }
434 c = nc;
435 }
436 double acc = 0.0;
437 for (std::size_t q = 1; q <= i; ++q) acc += c[q] * m[q - 1];
438 fm[i - 1] = acc;
439 }
440 return fm;
441}
442
443/** BuTools ReducedMomsFromMoms: m_i / i!. */
444inline std::vector<double> butools_reduced_moms(const std::vector<double>& m) {
445 std::vector<double> rm = m;
446 double f = 1.0;
447 for (std::size_t i = 0; i < m.size(); ++i) {
448 f /= static_cast<double>(i + 1);
449 rm[i] *= f;
450 }
451 return rm;
452}
453
454/**
455 * BuTools MEFromMoments (van de Liefvoort's "appie" construction) for a list of
456 * raw moments: the matrix-exponential pair (alpha, A) of order ceil(n/2).
457 * Throws NumericError where BuTools errors ("Insufficient matrix order!").
458 */
459inline void butools_me_from_moments(const std::vector<double>& moms, std::vector<double>& alpha_out,
460 std::vector<std::vector<double>>& A_out) {
461 using std::pow;
462 const std::vector<double> rmom = butools_reduced_moms(moms);
463 // ---- appie ----
464 std::size_t m = rmom.size();
465 std::vector<double> rm;
466 if (m % 2 == 0) {
467 rm.assign(rmom.begin(), rmom.begin() + static_cast<std::ptrdiff_t>(m - 1));
468 m = m / 2;
469 } else {
470 rm = rmom;
471 m = (m + 1) / 2;
472 }
473 rm.insert(rm.begin(), 1.0);
474 const std::size_t L = 2 * m;
475 std::vector<double> f(L, 0.0), y(L, 0.0);
476 f[0] = 1.0;
477 // dd: subdiagonal ones; dd*v shifts v down by one
478 auto shift = [L](const std::vector<double>& v) {
479 std::vector<double> o(L, 0.0);
480 for (std::size_t i = 1; i < L; ++i) o[i] = v[i - 1];
481 return o;
482 };
483 long n = 0;
484 std::size_t k = 0;
485 double q = 1.0;
486 std::vector<double> d(L + 1, 0.0), beta(L + 1, 0.0);
487 std::vector<std::vector<double>> alpha(L + 1, std::vector<double>(L + 1, 0.0));
488 for (std::size_t i = 0; i < L; ++i) {
489 double ro = 0.0;
490 for (std::size_t j = 0; j < L && j < rm.size(); ++j) ro += rm[j] * f[j];
491 ro *= q;
492 const long nold = n;
493 n = nold + 1;
494 const std::vector<double> yold = y;
495 if (n > 0 && ro != 0.0) {
496 if (k > 0) beta[k] = ro / pow(rm[1], d[k] + static_cast<double>(n) - 1.0);
497 k = k + 1;
498 d[k] = static_cast<double>(n);
499 n = -n;
500 q = q / ro;
501 y = shift(f);
502 } else if (n <= 0) {
503 const long j = nold + static_cast<long>(d[k]) + 1;
504 if (j >= 1 && static_cast<std::size_t>(j) <= L)
505 alpha[k][static_cast<std::size_t>(j)] = ro / pow(rm[1], static_cast<double>(j - 1));
506 }
507 const std::vector<double> sf = shift(f);
508 for (std::size_t j = 0; j < L; ++j) f[j] = sf[j] - ro * yold[j];
509 }
510 double dsum = 0.0;
511 for (std::size_t i = 1; i <= k; ++i) dsum += d[i];
512 if (static_cast<std::size_t>(dsum) != m || dsum != std::floor(dsum))
513 throw NumericError(ErrorKind{"Infeasible"}, "MEFromMoments: Insufficient matrix order!");
514 std::vector<std::vector<double>> K(m, std::vector<double>(m, 0.0));
515 K[0][0] = rm[1];
516 for (std::size_t i = 0; i + 1 < m; ++i) K[i][i + 1] = rm[1];
517 double ind = d[1];
518 for (std::size_t i = 2; i <= m; ++i) {
519 if (ind < static_cast<double>(m)) {
520 const double inc = d[i];
521 ind = ind + inc;
522 if (ind <= static_cast<double>(m)) {
523 const std::size_t row = static_cast<std::size_t>(ind) - 1;
524 const long col = static_cast<long>(ind - inc - d[i - 1] + 1.0) - 1;
525 if (col >= 0 && static_cast<std::size_t>(col) < m) K[row][static_cast<std::size_t>(col)] = beta[i - 1];
526 for (std::size_t j = 1; j <= static_cast<std::size_t>(inc); ++j) {
527 const long c2 = static_cast<long>(ind) - static_cast<long>(j); // ind-j+1, 0-based
528 if (c2 >= 0 && static_cast<std::size_t>(c2) < m)
529 K[row][static_cast<std::size_t>(c2)] = alpha[i][j];
530 }
531 }
532 }
533 }
534 // ---- alpha = e1 inv(T) U, A = inv(-inv(U) T K inv(T) U) ----
535 const std::size_t N = m;
536 Matrix<double> Tm(N, N, 0.0), U(N, N, 0.0), Km(N, N, 0.0);
537 for (std::size_t i = 0; i < N; ++i)
538 for (std::size_t j = 0; j < N; ++j) {
539 if (j <= i) Tm(i, j) = 1.0;
540 if (j >= i) U(i, j) = 1.0 / static_cast<double>(N - i);
541 Km(i, j) = K[i][j];
542 }
543 const Matrix<double> Ti = inverse(Tm), Ui = inverse(U);
544 const Matrix<double> TiU = matmul(Ti, U);
545 alpha_out.assign(N, 0.0);
546 for (std::size_t j = 0; j < N; ++j) alpha_out[j] = TiU(0, j);
547 Matrix<double> M = matmul(matmul(matmul(Ui, Tm), Km), TiU);
548 for (std::size_t i = 0; i < N; ++i)
549 for (std::size_t j = 0; j < N; ++j) M(i, j) = -M(i, j);
550 const Matrix<double> A = inverse(M);
551 A_out.assign(N, std::vector<double>(N, 0.0));
552 for (std::size_t i = 0; i < N; ++i)
553 for (std::size_t j = 0; j < N; ++j) A_out[i][j] = A(i, j);
554}
555
556/**
557 * BuTools DPH2From3Moments: MGFromMoments on the three raw moments, then
558 * CanonicalFromDPH2 (with its CheckMGRepresentation input test). Returns false
559 * wherever BuTools errors or the canonical form comes out complex, which is
560 * what the reference's try/catch turns into the Geometric fallback.
561 */
562inline bool dph2_from_3moments_butools(double m1, double m2, double m3, double alpha_out[2],
563 double A_out[2][2]) {
564 using std::sqrt;
565 // ---- MGFromMoments ----
566 std::vector<double> moms{m1, m2, m3};
567 std::vector<double> rfmoms = butools_reduced_moms(butools_factorial_moms(moms));
568 rfmoms.insert(rfmoms.begin(), 1.0);
569 std::vector<double> vlist(3, 0.0), tmp(4, 0.0);
570 double kf = 1.0;
571 tmp[0] = rfmoms[0];
572 for (std::size_t i = 1; i <= 3; ++i) {
573 tmp[i] = (i % 2 == 0 ? 1.0 : -1.0) * rfmoms[i];
574 kf *= static_cast<double>(i);
575 double acc = 0.0;
576 for (std::size_t j = 0; j <= i; ++j) acc += tmp[j];
577 vlist[i - 1] = kf * acc;
578 }
579 std::vector<double> beta;
580 std::vector<std::vector<double>> Cv;
581 try {
582 butools_me_from_moments(vlist, beta, Cv);
583 } catch (const Error&) {
584 return false;
585 }
586 if (beta.size() != 2) return false;
587 Matrix<double> C(2, 2, 0.0);
588 for (std::size_t i = 0; i < 2; ++i)
589 for (std::size_t j = 0; j < 2; ++j) C(i, j) = Cv[i][j];
590 Matrix<double> iC, B;
591 try {
592 iC = inverse(C);
593 Matrix<double> iCpI = iC;
594 iCpI(0, 0) += 1.0;
595 iCpI(1, 1) += 1.0;
596 B = matmul(iC, inverse(iCpI));
597 } catch (const Error&) {
598 return false;
599 }
600 for (std::size_t i = 0; i < 2; ++i) {
601 if (!std::isfinite(beta[i])) return false;
602 for (std::size_t j = 0; j < 2; ++j)
603 if (!std::isfinite(B(i, j))) return false;
604 }
605 // ---- CheckMGRepresentation (prec 1e-14) and CanonicalFromDPH2 ----
606 const double prec = 1e-14;
607 const double bsum = beta[0] + beta[1];
608 if (bsum < -prec || bsum > 1.0 + prec) return false;
609 const double tr = B(0, 0) + B(1, 1), det = B(0, 0) * B(1, 1) - B(0, 1) * B(1, 0);
610 const double disc = tr * tr / 4.0 - det;
611 if (disc < 0.0) return false; // complex eigenvalues: the largest is not real
612 double l1 = tr / 2.0 + sqrt(disc), l2 = tr / 2.0 - sqrt(disc);
613 // EigSort: descending |lambda|, ties by real part
614 if (std::abs(l1) < std::abs(l2) || (std::abs(l1) == std::abs(l2) && l1 < l2)) std::swap(l1, l2);
615 if (l1 > 1.0 + prec) return false;
616 const double p1 = beta[0] * (1.0 - B(0, 0) - B(0, 1)) + beta[1] * (1.0 - B(1, 0) - B(1, 1));
617 double b0, b1, B00, B01, B10, B11;
618 if (l1 > 0.0 && l2 > 0.0 && l1 != l2) {
619 const double d1 = (1.0 - l1) * (1.0 - p1 - l2) / (l1 - l2);
620 const double d2 = p1 - d1;
621 b0 = d1 * (l1 - l2) / ((1.0 - l1) * (1.0 - l2));
622 b1 = (d1 + d2) / (1.0 - l2);
623 B00 = l1; B01 = 1.0 - l1; B10 = 0.0; B11 = l2;
624 } else if (l1 > 0.0 && l1 == l2) {
625 const double d2 = p1;
626 const double d1 = (1.0 - l1) * (1.0 - d2 - l1) / l1;
627 b0 = d1 * l1 / ((1.0 - l1) * (1.0 - l1));
628 b1 = d2 / (1.0 - l1);
629 B00 = l1; B01 = 1.0 - l1; B10 = 0.0; B11 = l1;
630 } else if (l1 > 0.0) {
631 const double d1 = (1.0 - l1) * (1.0 - p1 - l2) / (l1 - l2);
632 const double d2 = p1 - d1;
633 b0 = (d1 * l1 + d2 * l2) / ((1.0 - l1) * (1.0 - l2));
634 b1 = (d1 + d2) * (1.0 - l1 - l2) / ((1.0 - l1) * (1.0 - l2));
635 B00 = l1 + l2; B01 = 1.0 - l1 - l2; B10 = l1 * l2 / (l1 + l2 - 1.0); B11 = 0.0;
636 } else {
637 return false; // the reference leaves beta, B undefined here, which errors
638 }
639 if (!std::isfinite(b0) || !std::isfinite(b1) || !std::isfinite(B00) || !std::isfinite(B01) ||
640 !std::isfinite(B10) || !std::isfinite(B11))
641 return false;
642 alpha_out[0] = b0;
643 alpha_out[1] = b1;
644 A_out[0][0] = B00; A_out[0][1] = B01; A_out[1][0] = B10; A_out[1][1] = B11;
645 return true;
646}
647
648} // namespace detail
649
650/**
651 * Reduces the order of a D-MAP by matching interevent moments.
652 *
653 * Leaves the process untouched while its order stays within `max_order`, and
654 * otherwise replaces it by the two-phase discrete phase-type law with the same
655 * first three interevent moments, read back as a renewal D-MAP. Correlation is
656 * NOT preserved, which is the reason the multi-station discrete-time path is an
657 * approximation. When the moment triple is outside the DPH(2) region the
658 * fallback keeps the exact mean with a Geometric, so the event rate of the
659 * decomposition is conserved in every branch.
660 *
661 * The two-phase fit is BuTools' DPH2From3Moments as the MATLAB twin calls it
662 * (MGFromMoments then CanonicalFromDPH2, transcribed in detail above), so the
663 * port takes the two-phase branch on exactly the triples the reference does;
664 * the Geometric fallback floor is GlobalConstants::Zero.
665 */
666template <class T>
667Dmap<T> dmap_compress(const Dmap<T>& d, std::size_t max_order) {
668 if (d.D0.rows() <= max_order) return d;
669
670 std::vector<T> moms = dmap_moment(d, std::vector<unsigned>{1u, 2u, 3u});
671 const double m1 = num_traits<T>::to_double(moms[0]);
672 const double m2 = num_traits<T>::to_double(moms[1]);
673 const double m3 = num_traits<T>::to_double(moms[2]);
674
675 double al[2] = {0.0, 0.0}, Am[2][2] = {{0.0, 0.0}, {0.0, 0.0}};
676 if (detail::dph2_from_3moments_butools(m1, m2, m3, al, Am)) {
677 Dph<T> fit;
678 fit.alpha.assign(2, num_traits<T>::from_int(0));
679 fit.alpha[0] = num_traits<T>::from_double(al[0]);
680 fit.alpha[1] = num_traits<T>::from_double(al[1]);
681 fit.A = Matrix<T>(2, 2, num_traits<T>::from_int(0));
682 for (std::size_t i = 0; i < 2; ++i)
683 for (std::size_t j = 0; j < 2; ++j) fit.A(i, j) = num_traits<T>::from_double(Am[i][j]);
684 try {
685 Dmap<T> cand = dph_to_dmap(fit);
686 if (dmap_isfeasible(cand)) return cand;
687 } catch (const Error&) {
688 // the reference's try/catch: fall through to the Geometric
689 }
690 }
691
692 // the moment triple is not DPH(2)-feasible: keep the rate, drop the shape
693 double p = 1 / m1;
694 p = std::min(1.0, std::max(lang::GlobalConstants::Zero, p));
695 Dph<T> geo;
696 geo.alpha.assign(1, num_traits<T>::from_int(1));
697 geo.A = Matrix<T>(1, 1, num_traits<T>::from_double(1 - p));
698 return dph_to_dmap(geo);
699}
700
701/**
702 * Order reduction of a BATCH stream.
703 *
704 * The process of NONEMPTY SLOTS is compressed as a plain D-MAP and the
705 * stationary batch-size distribution, conditional on the slot being nonempty,
706 * is reattached to it. Compressing the phase alone would keep the slot process
707 * and lose the batch sizes, which would silently rescale the event rate.
708 *
709 * MATLAB twin: dmap_compress_batch.m
710 */
711template <class T>
712DBatch<T> dmap_compress_batch(const DBatch<T>& A, std::size_t max_order) {
713 if (A[0].rows() <= max_order) return A;
714 const std::size_t nb = A.size() - 1, m = A[0].rows();
715
716 Matrix<T> Ptot = A[0];
717 for (std::size_t k = 1; k < A.size(); ++k) Ptot = detail::dt_add(Ptot, A[k]);
718 std::vector<T> pi_phase = mc::dtmc_solve(Ptot);
719
720 std::vector<T> qraw(nb, num_traits<T>::from_int(0));
721 T mass = num_traits<T>::from_int(0);
722 for (std::size_t k = 0; k < nb; ++k) {
723 for (std::size_t i = 0; i < m; ++i)
724 for (std::size_t j = 0; j < m; ++j) qraw[k] = qraw[k] + pi_phase[i] * A[k + 1](i, j);
725 mass = mass + qraw[k];
726 }
727 if (num_traits<T>::to_double(mass) <= 0)
728 throw InputError("dmap_compress_batch: the batch stream carries no events");
729
730 Dmap<T> marked;
731 marked.D0 = A[0];
732 marked.D1 = detail::dt_sub(Ptot, A[0]);
733 Dmap<T> markedC = dmap_compress(marked, max_order);
734
735 DBatch<T> out;
736 out.push_back(markedC.D0);
737 for (std::size_t k = 0; k < nb; ++k) {
738 Matrix<T> blk = markedC.D1;
739 const T w = qraw[k] / mass;
740 for (std::size_t i = 0; i < blk.rows(); ++i)
741 for (std::size_t j = 0; j < blk.cols(); ++j) blk(i, j) = blk(i, j) * w;
742 out.push_back(blk);
743 }
744 return out;
745}
746
747/**
748 * G matrix of a discrete-time QBD by logarithmic reduction (Latouche and
749 * Ramaswami). The blocks are stochastic, so no uniformization is needed; the
750 * MATLAB twin reaches the same minimal solution through SMCSolver's cyclic
751 * reduction.
752 */
753template <class T>
754Matrix<T> qbd_dt_g(const Matrix<T>& A0, const Matrix<T>& A1, const Matrix<T>& A2,
755 int max_iter = 200) {
756 std::size_t m = A1.rows();
757 Matrix<T> I = detail::dt_eye<T>(m);
758 Matrix<T> inv_local = detail::dt_left_solve(detail::dt_sub(I, A1), I);
759 Matrix<T> B0 = detail::dt_mul(inv_local, A0);
760 Matrix<T> B2 = detail::dt_mul(inv_local, A2);
761 Matrix<T> G = B0;
762 Matrix<T> PI = B2;
763
764 for (int it = 0; it < max_iter; ++it) {
765 Matrix<T> A1n = detail::dt_add(detail::dt_mul(B0, B2), detail::dt_mul(B2, B0));
766 Matrix<T> A0n = detail::dt_mul(B0, B0);
767 Matrix<T> A2n = detail::dt_mul(B2, B2);
768 Matrix<T> inv_n = detail::dt_left_solve(detail::dt_sub(I, A1n), I);
769 B0 = detail::dt_mul(inv_n, A0n);
770 B2 = detail::dt_mul(inv_n, A2n);
771 G = detail::dt_add(G, detail::dt_mul(PI, B0));
772 PI = detail::dt_mul(PI, B2);
773
774 double residual = 0;
775 for (std::size_t i = 0; i < m; ++i) {
776 T rowsum = num_traits<T>::from_int(0);
777 for (std::size_t j = 0; j < m; ++j) rowsum = rowsum + G(i, j);
778 residual = std::max(residual,
779 std::abs(1.0 - num_traits<T>::to_double(rowsum)));
780 }
781 if (residual < 1e-14) break;
782 }
783 return G;
784}
785
786/**
787 * G matrix of an M/G/1-type chain by functional iteration on
788 * G = sum_k A_k G^k, the blocks being stochastic.
789 *
790 * `A[0]` is the down-one block and `A[k]` the block raising the level by k-1.
791 * SMCSolver reaches the same minimal solution by cyclic reduction; the JAR port
792 * of that routine costs 19.8 s at order 17 where functional iteration costs
793 * 25 ms and agrees to 1.6e-15, which is why neither this port nor the JAR one
794 * takes it.
795 */
796template <class T>
797Matrix<T> mg1_dt_g(const std::vector<Matrix<T>>& A, int max_iter = 5000,
798 double tol = 1e-14) {
799 std::size_t m = A[0].rows();
800 Matrix<T> G = A[0];
801 for (int it = 0; it < max_iter; ++it) {
802 Matrix<T> Gnew = A[0];
803 Matrix<T> Gpow = G;
804 for (std::size_t k = 1; k < A.size(); ++k) {
805 Gnew = detail::dt_add(Gnew, detail::dt_mul(A[k], Gpow));
806 if (k + 1 < A.size()) Gpow = detail::dt_mul(Gpow, G);
807 }
808 double delta = 0;
809 for (std::size_t i = 0; i < m; ++i)
810 for (std::size_t j = 0; j < m; ++j)
811 delta = std::max(delta, std::abs(num_traits<T>::to_double(Gnew(i, j) - G(i, j))));
812 G = Gnew;
813 if (delta < tol) break;
814 }
815 return G;
816}
817
818/**
819 * Stationary vector of an M/G/1-type chain by the stable Ramaswami formula.
820 *
821 * `A` repeats from level one and `B` is the boundary row, both as block lists
822 * with `A[0]` the down-one block. The recursion is level-by-level, so it costs
823 * O(levels * m^3) and not O((levels*m)^3): a dense solve of the truncated chain
824 * is cubic in the WHOLE state space and stops being affordable at the second
825 * station of a decomposition, where the arrival process already carries the
826 * level space of the first.
827 *
828 * MATLAB twin: MG1_pi.m (SMCSolver, Van Houdt), default-boundary branch.
829 */
830template <class T>
831std::vector<T> mg1_dt_pi(const std::vector<Matrix<T>>& B, const std::vector<Matrix<T>>& A,
832 std::size_t max_num_comp = 1000) {
833 const T one = num_traits<T>::from_int(1);
834 const T zero = num_traits<T>::from_int(0);
835 std::size_t m = A[0].rows();
836 std::size_t dega = A.size() - 1;
837 std::size_t degb = B.size() - 1;
838 Matrix<T> I = detail::dt_eye<T>(m);
839 Matrix<T> G = mg1_dt_g(A);
840
841 // hatA_i = sum_{v>=i} A_v G^(v-i), while sumA accumulates the originals
842 std::vector<Matrix<T>> hatA = A;
843 Matrix<T> sumA = A[dega];
844 std::vector<T> beta(m, zero);
845 for (std::size_t i = 0; i < m; ++i)
846 for (std::size_t j = 0; j < m; ++j) beta[i] = beta[i] + sumA(i, j);
847 for (std::size_t i = dega; i-- > 1;) {
848 sumA = detail::dt_add(sumA, A[i]);
849 hatA[i] = detail::dt_add(A[i], detail::dt_mul(hatA[i + 1], G));
850 for (std::size_t r = 0; r < m; ++r)
851 for (std::size_t c = 0; c < m; ++c) beta[r] = beta[r] + sumA(r, c);
852 }
853 sumA = detail::dt_add(sumA, A[0]);
854
855 std::vector<T> theta = mc::dtmc_solve(sumA);
856 T drift = zero;
857 for (std::size_t i = 0; i < m; ++i) drift = drift + theta[i] * beta[i];
858 if (num_traits<T>::to_double(drift) >= 1)
859 throw InputError("mg1_dt_pi: the chain characterized by A is not positive recurrent");
860
861 Matrix<T> invBarA1 = detail::dt_left_solve(detail::dt_sub(I, hatA[1]), I);
862
863 // hatB_i = sum_{v>=i} B_v G^(v-i), sumBB0 = sum_{v>=1} B_v,
864 // Bbeta = sum_{v>=1} (v-1) B_v e
865 std::vector<Matrix<T>> hatB = B;
866 Matrix<T> sumBB0 = B[degb];
867 std::vector<T> Bbeta(m, zero);
868 for (std::size_t i = degb; i-- > 1;) {
869 for (std::size_t r = 0; r < m; ++r)
870 for (std::size_t c = 0; c < m; ++c) Bbeta[r] = Bbeta[r] + sumBB0(r, c);
871 sumBB0 = detail::dt_add(sumBB0, B[i]);
872 hatB[i] = detail::dt_add(B[i], detail::dt_mul(hatB[i + 1], G));
873 }
874
875 Matrix<T> Kmat = detail::dt_add(B[0], detail::dt_mul(hatB[1], G));
876 std::vector<T> kappa = mc::dtmc_solve(Kmat);
877 std::vector<T> g = mc::dtmc_solve(G);
878
879 // temp = rowsum(inv(I - sumA - (e - beta) g))
880 Matrix<T> W = detail::dt_sub(I, sumA);
881 for (std::size_t r = 0; r < m; ++r)
882 for (std::size_t c = 0; c < m; ++c) W(r, c) = W(r, c) - (one - beta[r]) * g[c];
883 Matrix<T> invW = detail::dt_left_solve(W, I);
884 std::vector<T> temp(m, zero);
885 for (std::size_t r = 0; r < m; ++r)
886 for (std::size_t c = 0; c < m; ++c) temp[r] = temp[r] + invW(r, c);
887
888 const T inv_slack = one / (one - drift);
889 std::vector<T> psi1(m, zero), psi2(m, one);
890 Matrix<T> M1 = detail::dt_sub(detail::dt_sub(I, A[0]), hatA[1]);
891 Matrix<T> M2 = detail::dt_sub(sumBB0, hatB[1]);
892 for (std::size_t r = 0; r < m; ++r) {
893 T acc1 = zero, acc2 = zero, a0row = zero;
894 for (std::size_t c = 0; c < m; ++c) {
895 acc1 = acc1 + M1(r, c) * temp[c];
896 acc2 = acc2 + M2(r, c) * temp[c];
897 a0row = a0row + A[0](r, c);
898 }
899 psi1[r] = acc1 + inv_slack * a0row;
900 psi2[r] = one + acc2 + inv_slack * Bbeta[r];
901 }
902 // tildekappa1 = psi2 + hatB_1 inv(I - hatA_1) psi1
903 std::vector<T> tmp(m, zero), tilde(m, zero);
904 for (std::size_t r = 0; r < m; ++r)
905 for (std::size_t c = 0; c < m; ++c) tmp[r] = tmp[r] + invBarA1(r, c) * psi1[c];
906 for (std::size_t r = 0; r < m; ++r) {
907 T acc = zero;
908 for (std::size_t c = 0; c < m; ++c) acc = acc + hatB[1](r, c) * tmp[c];
909 tilde[r] = psi2[r] + acc;
910 }
911 T denom = zero;
912 for (std::size_t r = 0; r < m; ++r) denom = denom + kappa[r] * tilde[r];
913
914 std::vector<std::vector<T>> pi;
915 std::vector<T> pi0(m, zero);
916 for (std::size_t r = 0; r < m; ++r) pi0[r] = kappa[r] / denom;
917 pi.push_back(pi0);
918
919 double sumpi = 0;
920 for (std::size_t r = 0; r < m; ++r) sumpi += num_traits<T>::to_double(pi0[r]);
921 std::size_t numit = 1;
922 while (sumpi < 1 - 1e-10 && numit < max_num_comp) {
923 std::vector<T> pin(m, zero);
924 if (numit <= degb)
925 for (std::size_t r = 0; r < m; ++r)
926 for (std::size_t c = 0; c < m; ++c)
927 pin[c] = pin[c] + pi0[r] * hatB[numit](r, c);
928 for (std::size_t j = 1; j <= std::min(numit - 1, dega - 1); ++j)
929 for (std::size_t r = 0; r < m; ++r)
930 for (std::size_t c = 0; c < m; ++c)
931 pin[c] = pin[c] + pi[numit - j][r] * hatA[j + 1](r, c);
932 std::vector<T> row(m, zero);
933 for (std::size_t r = 0; r < m; ++r)
934 for (std::size_t c = 0; c < m; ++c) row[c] = row[c] + pin[r] * invBarA1(r, c);
935 pi.push_back(row);
936 for (std::size_t r = 0; r < m; ++r) sumpi += num_traits<T>::to_double(row[r]);
937 ++numit;
938 }
939
940 std::vector<T> out;
941 out.reserve(pi.size() * m);
942 for (std::size_t i = 0; i < pi.size(); ++i)
943 for (std::size_t r = 0; r < m; ++r) out.push_back(pi[i][r]);
944 return out;
945}
946
947/**
948 * Queue length distribution of a discrete-time D-MAP/D-MAP/1/FCFS queue, the
949 * queue-length half of Q_DT_MAP_MAP_1.
950 *
951 * Entry i is Prob[i customers in system] under LAS-DA. The waiting and sojourn
952 * pmfs of the Q-MAM routine are deliberately not ported: no LINE caller consumes
953 * them, and the discrete-time solver path reads the queue length alone.
954 */
955template <class T>
956std::vector<T> q_dt_map_map_1(const Dmap<T>& arv, const Dmap<T>& svc,
957 std::size_t max_num_comp = 1000) {
958 const T one = num_traits<T>::from_int(1);
959 std::size_t ma = arv.D0.rows(), ms = svc.D0.rows(), mtot = ma * ms;
960
961 std::vector<T> pi_a = mc::dtmc_solve(detail::dt_add(arv.D0, arv.D1));
962 T avga = num_traits<T>::from_int(0);
963 for (std::size_t i = 0; i < ma; ++i)
964 for (std::size_t j = 0; j < ma; ++j) avga = avga + pi_a[i] * arv.D1(i, j);
965 std::vector<T> pi_s = mc::dtmc_solve(detail::dt_add(svc.D0, svc.D1));
966 T avgs = num_traits<T>::from_int(0);
967 for (std::size_t i = 0; i < ms; ++i)
968 for (std::size_t j = 0; j < ms; ++j) avgs = avgs + pi_s[i] * svc.D1(i, j);
969 if (num_traits<T>::to_double(avga / avgs) >= 1)
970 throw InputError("q_dt_map_map_1: the load of the system exceeds one");
971
972 Matrix<T> Ims = detail::dt_eye<T>(ms);
973 Matrix<T> Am1 = detail::dt_kron(arv.D0, svc.D1);
974 Matrix<T> A0 = detail::dt_add(detail::dt_kron(arv.D0, svc.D0),
975 detail::dt_kron(arv.D1, svc.D1));
976 Matrix<T> A1 = detail::dt_kron(arv.D1, svc.D0);
977 Matrix<T> B0 = detail::dt_kron(arv.D0, Ims);
978 Matrix<T> B1 = detail::dt_kron(arv.D1, Ims);
979
980 Matrix<T> G = qbd_dt_g(Am1, A0, A1);
981 // R = A1 (I - A0 - A1 G)^-1
982 Matrix<T> I = detail::dt_eye<T>(mtot);
983 Matrix<T> denom = detail::dt_sub(detail::dt_sub(I, A0), detail::dt_mul(A1, G));
984 Matrix<T> R = detail::dt_right_divide(A1, denom);
985
986 // General boundary [B1; A0 + R Am1]: the empty system has its own local
987 // block, so levels 0 and 1 are solved together (QBD_pi.m else-branch)
988 Matrix<T> lower = detail::dt_add(A0, detail::dt_mul(R, Am1));
989 Matrix<T> joint(2 * mtot, 2 * mtot, num_traits<T>::from_int(0));
990 for (std::size_t i = 0; i < mtot; ++i)
991 for (std::size_t j = 0; j < mtot; ++j) {
992 joint(i, j) = B0(i, j);
993 joint(mtot + i, j) = Am1(i, j);
994 joint(i, mtot + j) = B1(i, j);
995 joint(mtot + i, mtot + j) = lower(i, j);
996 }
997 std::vector<T> pi01 = mc::dtmc_solve(joint);
998
999 Matrix<T> temp = detail::dt_left_solve(detail::dt_sub(I, R), I);
1000 std::vector<T> pi0(pi01.begin(), pi01.begin() + mtot);
1001 std::vector<T> pi1(pi01.begin() + mtot, pi01.end());
1002 T norm = num_traits<T>::from_int(0);
1003 for (std::size_t i = 0; i < mtot; ++i) norm = norm + pi0[i];
1004 for (std::size_t i = 0; i < mtot; ++i)
1005 for (std::size_t j = 0; j < mtot; ++j) norm = norm + pi1[i] * temp(i, j);
1006 for (std::size_t i = 0; i < mtot; ++i) {
1007 pi0[i] = pi0[i] / norm;
1008 pi1[i] = pi1[i] / norm;
1009 }
1010
1011 std::vector<T> ql;
1012 T mass0 = num_traits<T>::from_int(0);
1013 for (std::size_t i = 0; i < mtot; ++i) mass0 = mass0 + pi0[i];
1014 ql.push_back(mass0);
1015 std::vector<T> cur = pi1;
1016 double acc = num_traits<T>::to_double(mass0);
1017 for (std::size_t it = 0; it < max_num_comp; ++it) {
1018 T mass = num_traits<T>::from_int(0);
1019 for (std::size_t i = 0; i < mtot; ++i) mass = mass + cur[i];
1020 ql.push_back(mass);
1021 acc += num_traits<T>::to_double(mass);
1022 if (acc > 1 - 1e-10) break;
1023 std::vector<T> nxt(mtot, num_traits<T>::from_int(0));
1024 for (std::size_t j = 0; j < mtot; ++j)
1025 for (std::size_t i = 0; i < mtot; ++i) nxt[j] = nxt[j] + cur[i] * R(i, j);
1026 cur = nxt;
1027 }
1028
1029 T total = num_traits<T>::from_int(0);
1030 for (std::size_t i = 0; i < ql.size(); ++i) total = total + ql[i];
1031 for (std::size_t i = 0; i < ql.size(); ++i) ql[i] = ql[i] / total;
1032 return ql;
1033}
1034
1035/** Queue length of a discrete-time DPH/DPH/1/FCFS queue, via the D-MAP route. */
1036template <class T>
1037std::vector<T> q_dt_ph_ph_1(const Dph<T>& arv, const Dph<T>& svc,
1038 std::size_t max_num_comp = 1000) {
1039 return q_dt_map_map_1(dph_to_dmap(arv), dph_to_dmap(svc), max_num_comp);
1040}
1041
1042/** Outcome of a slotted station solve. */
1043template <class T>
1048 std::vector<T> ql;
1050};
1051
1052/**
1053 * Discrete-time single-server queue with batch D-MAP arrivals, DBMAP/D-MAP/1.
1054 *
1055 * The chain is M/G/1-type because a slot may deliver a batch: with arrival
1056 * matrices A_k and service pair (S0,S1),
1057 * A^(-1) = kron(A_0,S1), A^(k) = kron(A_k,S0) + kron(A_{k+1},S1),
1058 * B^(k) = kron(A_k,I), the boundary row holding the empty system where no
1059 * service runs.
1060 *
1061 * Solved by the Ramaswami level recursion of `mg1_dt_pi`. The DEPARTURE process
1062 * is a different object: it is read off the chain truncated at the level where
1063 * the tail carries less than 1e-10, with arrivals that would cross the top held
1064 * there, which is a level cut and not a rate change.
1065 */
1066template <class T>
1068 std::size_t max_num_comp = 1000, bool want_departure = false) {
1069 const T one = num_traits<T>::from_int(1);
1070 std::size_t ms = svc.D0.rows(), ma = arv[0].rows(), K = arv.size() - 1;
1071 std::size_t m = ma * ms;
1072 Matrix<T> Ims = detail::dt_eye<T>(ms);
1073
1074 T lambda = dmap_lambda_batch(arv);
1075 DBatch<T> svc_batch;
1076 svc_batch.push_back(svc.D0);
1077 svc_batch.push_back(svc.D1);
1078 T mu = dmap_lambda_batch(svc_batch);
1079 if (num_traits<T>::to_double(lambda / mu) >= 1)
1080 throw InputError("mg1_dt_queue: the discrete-time load of the station is not below one");
1081
1082 // A^(-1) = kron(A_0,S1), A^(k) = kron(A_k,S0) + kron(A_{k+1},S1),
1083 // B^(k) = kron(A_k,I), the boundary row where no service runs
1084 std::vector<Matrix<T>> Ablocks, Bblocks;
1085 Ablocks.push_back(detail::dt_kron(arv[0], svc.D1));
1086 for (std::size_t k = 0; k <= K; ++k) {
1087 Matrix<T> blk = detail::dt_kron(arv[k], svc.D0);
1088 if (k + 1 <= K) blk = detail::dt_add(blk, detail::dt_kron(arv[k + 1], svc.D1));
1089 Ablocks.push_back(blk);
1090 }
1091 for (std::size_t k = 0; k <= K; ++k) Bblocks.push_back(detail::dt_kron(arv[k], Ims));
1092
1093 std::vector<T> pi = mg1_dt_pi(Bblocks, Ablocks, max_num_comp);
1094
1095 DtQueueResult<T> out;
1096 std::size_t nlev = pi.size() / m;
1097 out.ql.assign(nlev, num_traits<T>::from_int(0));
1098 for (std::size_t i = 0; i < nlev; ++i)
1099 for (std::size_t j = 0; j < m; ++j) out.ql[i] = out.ql[i] + pi[i * m + j];
1100 T total = num_traits<T>::from_int(0);
1101 for (std::size_t i = 0; i < nlev; ++i) total = total + out.ql[i];
1102 for (std::size_t i = 0; i < nlev; ++i) out.ql[i] = out.ql[i] / total;
1103
1104 out.QN = num_traits<T>::from_int(0);
1105 for (std::size_t i = 0; i < nlev; ++i)
1106 out.QN = out.QN + num_traits<T>::from_int(static_cast<int>(i)) * out.ql[i];
1107 out.UN = one - out.ql[0];
1108 out.TN = lambda;
1109
1110 if (want_departure) {
1111 double cum = 0;
1112 std::size_t L = nlev - 1;
1113 for (std::size_t i = 0; i < nlev; ++i) {
1114 cum += num_traits<T>::to_double(out.ql[i]);
1115 if (cum > 1 - 1e-10) {
1116 L = i;
1117 break;
1118 }
1119 }
1120 if (L < 1) L = 1;
1121 std::size_t nstates = (L + 1) * m;
1122 // departure process of the same truncated chain: D1 collects the
1123 // transitions carrying a completion, D0 the rest
1124 Matrix<T> D0(nstates, nstates, num_traits<T>::from_int(0));
1125 Matrix<T> D1(nstates, nstates, num_traits<T>::from_int(0));
1126 for (std::size_t i = 0; i <= L; ++i)
1127 for (std::size_t k = 0; k <= K; ++k) {
1128 if (i == 0) {
1129 std::size_t tgt = std::min(L, k);
1130 Matrix<T> blk = detail::dt_kron(arv[k], Ims);
1131 for (std::size_t r = 0; r < m; ++r)
1132 for (std::size_t c = 0; c < m; ++c)
1133 D0(i * m + r, tgt * m + c) = D0(i * m + r, tgt * m + c) + blk(r, c);
1134 } else {
1135 std::size_t tno = std::min(L, i + k);
1136 Matrix<T> no = detail::dt_kron(arv[k], svc.D0);
1137 for (std::size_t r = 0; r < m; ++r)
1138 for (std::size_t c = 0; c < m; ++c)
1139 D0(i * m + r, tno * m + c) = D0(i * m + r, tno * m + c) + no(r, c);
1140 std::size_t tdep = std::min(L, i - 1 + k);
1141 Matrix<T> dp = detail::dt_kron(arv[k], svc.D1);
1142 for (std::size_t r = 0; r < m; ++r)
1143 for (std::size_t c = 0; c < m; ++c)
1144 D1(i * m + r, tdep * m + c) = D1(i * m + r, tdep * m + c) + dp(r, c);
1145 }
1146 }
1147 out.dep.D0 = D0;
1148 out.dep.D1 = D1;
1149 }
1150 return out;
1151}
1152
1153} // namespace mam
1154} // namespace line
1155
1156#endif // LINE_API_MAM_DTIME_H
Base error for the multiprecision C++ port.
Definition error.h:73
InputError(const std::string &what)
Definition error.h:105
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:112
Discrete-time Markovian arrival processes (D-MAPs).
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
DBatch< T > dmap_compress_batch(const DBatch< T > &A, std::size_t max_order)
Order reduction of a BATCH stream.
Definition dtime.h:712
Matrix< T > mg1_dt_g(const std::vector< Matrix< T > > &A, int max_iter=5000, double tol=1e-14)
G matrix of an M/G/1-type chain by functional iteration on G = sum_k A_k G^k, the blocks being stocha...
Definition dtime.h:797
Dmap< T > dmap_compress(const Dmap< T > &d, std::size_t max_order)
Reduces the order of a D-MAP by matching interevent moments.
Definition dtime.h:667
bool dmap_isfeasible(const Dmap< T > &d)
True when D0 and D1 are nonnegative and D0 + D1 is stochastic.
Definition dmap.h:150
std::vector< T > q_dt_map_map_1(const Dmap< T > &arv, const Dmap< T > &svc, std::size_t max_num_comp=1000)
Queue length distribution of a discrete-time D-MAP/D-MAP/1/FCFS queue, the queue-length half of Q_DT_...
Definition dtime.h:956
std::vector< Matrix< T > > DBatch
A discrete batch arrival stream, entry k carrying the slots with k events.
Definition dtime.h:67
T dmap_lambda_batch(const DBatch< T > &A)
Mean number of EVENTS per slot, pi sum_k k A_k e.
Definition dtime.h:280
std::vector< T > q_dt_ph_ph_1(const Dph< T > &arv, const Dph< T > &svc, std::size_t max_num_comp=1000)
Queue length of a discrete-time DPH/DPH/1/FCFS queue, via the D-MAP route.
Definition dtime.h:1037
Matrix< T > qbd_dt_g(const Matrix< T > &A0, const Matrix< T > &A1, const Matrix< T > &A2, int max_iter=200)
G matrix of a discrete-time QBD by logarithmic reduction (Latouche and Ramaswami).
Definition dtime.h:754
DtQueueResult< T > mg1_dt_queue(const DBatch< T > &arv, const Dmap< T > &svc, std::size_t max_num_comp=1000, bool want_departure=false)
Discrete-time single-server queue with batch D-MAP arrivals, DBMAP/D-MAP/1.
Definition dtime.h:1067
Dph< T > dph_from_dist(lang::ProcessType type, const T &mean_slots, const T &scv)
Exact discrete phase-type representation of a lattice-valued law.
Definition dtime.h:160
Dmap< T > dph_to_dmap(const Dph< T > &d)
Renewal D-MAP (A, a alpha) of a discrete phase-type law.
Definition dtime.h:219
std::vector< T > dmap_moment(const Dmap< T > &d, const std::vector< unsigned > &orders)
Raw moments of the interarrival time in slots, for orders 1, 2 and 3 only.
Definition dmap.h:110
Dph< T > dmap_to_dph(const Dmap< T > &d)
Discrete phase-type law underlying a renewal D-MAP.
Definition dtime.h:257
DBatch< T > dmap_super(const DBatch< T > &A, const DBatch< T > &B)
Superposition, E_k = sum_{i+j=k} kron(A_i, B_j).
Definition dtime.h:303
bool dmap_is_renewal(const Dmap< T > &d)
True when D1 has rank one, i.e.
Definition dtime.h:236
std::vector< T > mg1_dt_pi(const std::vector< Matrix< T > > &B, const std::vector< Matrix< T > > &A, std::size_t max_num_comp=1000)
Stationary vector of an M/G/1-type chain by the stable Ramaswami formula.
Definition dtime.h:831
DBatch< T > dmap_thin(const DBatch< T > &A, const T &p)
Bernoulli thinning, B_k = sum_{n>=k} C(n,k) p^k (1-p)^(n-k) A_n.
Definition dtime.h:323
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
static constexpr double Zero
Definition lang_types.h:767
A discrete-time MAP: substochastic D0 (no arrival) and D1 (one arrival).
Definition dmap.h:48
Matrix< T > D0
Definition dmap.h:49
Matrix< T > D1
Definition dmap.h:50
A discrete phase-type law: initial row vector alpha and transient A.
Definition dtime.h:58
std::size_t order() const
Definition dtime.h:62
std::vector< T > alpha
Definition dtime.h:59
Matrix< T > A
Definition dtime.h:60
Outcome of a slotted station solve.
Definition dtime.h:1044
std::vector< T > ql
Definition dtime.h:1048