LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
map_pnt.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_MAP_PNT_H
6#define LINE_API_MAM_MAP_PNT_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Counting probabilities of a MAP: P_n(t), the matrix whose (i,j) entry is the
12 * probability of n arrivals in [0,t) ending in phase j, given phase i at 0.
13 *
14 * Port of matlab/lib/kpctoolbox/map/map_pntiter.m (and its map_pntbisect
15 * helper), mirrored by jline.api.mam.Map_pntiter and the native Python
16 * `map_pntiter`.
17 *
18 * The method is uniformization. With tau = max_i |D0(i,i)|, K = D0/tau + I and
19 * K1 = D1/tau, the number of uniformization steps in [0,t) is Poisson(tau t),
20 * and conditioning on it gives
21 *
22 * V(0,0) = I, V(0,k) = V(0,k-1) K,
23 * V(n,k) = V(n,k-1) K + V(n-1,k-1) K1,
24 * P_n(t) = sum_{k=0..N} w_k V(n,k), w_k = e^{-tau t} (tau t)^k / k!,
25 *
26 * truncated at the N for which the Poisson tail falls below machine epsilon.
27 * `map_pntiter` evaluates this at t/2^M and then squares M times through the
28 * discrete convolution P_n <- sum_{j=0..n} P_j P_{n-j}, which is exact because
29 * the counts over disjoint intervals add and the phase at the split is summed
30 * over by the matrix product.
31 *
32 * The Poisson mixing weight is that of the uniformization STEP count k, not of
33 * the arrival count n, and V(0,k) propagates for every k. A Poisson process
34 * cannot see either fault (there K = 0 collapses V(n,k) to delta(n,k)), so the
35 * tests check instead the identities any counting law must satisfy:
36 * P_0(t) = exp(D0 t), sum_n P_n(t) = exp((D0+D1) t), sum_n n pie P_n(t) e =
37 * lambda t.
38 */
39
40#include <cmath>
41#include <cstddef>
42#include <vector>
43
45#include "line/util/ode.h"
46#include "line/num/number.h"
47#include "line/util/error.h"
48#include "line/util/matrix.h"
49
50namespace line {
51namespace mam {
52
53namespace detail {
54
55/** Poisson weight e^{-tau t} (tau t)^r / r!, formed in logs. */
56inline double pnt_weight(double tau, double t, std::size_t r) {
57 const double x = tau * t;
58 if (!(x > 0.0)) return r == 0 ? 1.0 : 0.0;
59 double logw = -x + static_cast<double>(r) * std::log(x);
60 for (std::size_t k = 2; k <= r; ++k) logw -= std::log(static_cast<double>(k));
61 return std::exp(logw);
62}
63
64/** Smallest N whose Poisson tail beyond it is below machine epsilon. */
65inline std::size_t pnt_truncation(double tau, double t) {
66 const std::size_t kMax = 2000;
67 double acc = 0.0;
68 for (std::size_t N = 1; N <= kMax; ++N) {
69 acc += pnt_weight(tau, t, N);
70 if (1.0 - acc < 2.3e-16) return N;
71 }
72 return kMax;
73}
74
75} // namespace detail
76
77/**
78 * P_0(t) .. P_na(t) by uniformization on one interval, without the squaring.
79 *
80 * @param m the MAP
81 * @param na highest arrival count to return
82 * @param t interval length
83 */
84template <class T>
85std::vector<Matrix<T>> map_pntbisect(const Map<T>& m, std::size_t na, const T& t) {
87 "map_pntbisect weights the terms by a Poisson law");
88 const std::size_t n = m.order();
89 if (n == 0 || m.D1.rows() != n) throw InputError("map_pntbisect: D0 and D1 disagree");
90 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
91
92 double tau = 0.0;
93 for (std::size_t i = 0; i < n; ++i)
94 tau = std::max(tau, -num_traits<T>::to_double(m.D0(i, i)));
95 const double tv = num_traits<T>::to_double(t);
96 std::vector<Matrix<T>> P(na + 1, Matrix<T>(n, n, zero));
97 if (!(tau > 0.0) || !(tv > 0.0)) { // no clock, or no time: nothing happens
98 for (std::size_t i = 0; i < n; ++i) P[0](i, i) = one;
99 return P;
100 }
101
102 const T taut = num_traits<T>::from_double(tau);
103 Matrix<T> K(n, n, zero), K1(n, n, zero);
104 for (std::size_t i = 0; i < n; ++i)
105 for (std::size_t j = 0; j < n; ++j) {
106 K(i, j) = T(m.D0(i, j) / taut + (i == j ? one : zero));
107 K1(i, j) = T(m.D1(i, j) / taut);
108 }
109
110 const std::size_t N = detail::pnt_truncation(tau, tv);
111 // V[n] is the current step's V(n,k); the previous step's is kept in Vprev.
112 std::vector<Matrix<T>> V(na + 1, Matrix<T>(n, n, zero));
113 for (std::size_t i = 0; i < n; ++i) V[0](i, i) = one; // V(0,0) = I
114 for (std::size_t a = 0; a <= na; ++a) {
115 const T w = num_traits<T>::from_double(detail::pnt_weight(tau, tv, 0));
116 for (std::size_t i = 0; i < n; ++i)
117 for (std::size_t j = 0; j < n; ++j) P[a](i, j) = T(P[a](i, j) + w * V[a](i, j));
118 }
119 for (std::size_t k = 1; k <= N; ++k) {
120 const std::vector<Matrix<T>> Vprev = V;
121 for (std::size_t a = 0; a <= na; ++a) {
122 Matrix<T> next(n, n, zero);
123 for (std::size_t i = 0; i < n; ++i)
124 for (std::size_t j = 0; j < n; ++j) {
125 T acc = zero;
126 for (std::size_t q = 0; q < n; ++q) acc += Vprev[a](i, q) * K(q, j);
127 if (a > 0)
128 for (std::size_t q = 0; q < n; ++q) acc += Vprev[a - 1](i, q) * K1(q, j);
129 next(i, j) = acc;
130 }
131 V[a] = next;
132 }
133 const T w = num_traits<T>::from_double(detail::pnt_weight(tau, tv, k));
134 for (std::size_t a = 0; a <= na; ++a)
135 for (std::size_t i = 0; i < n; ++i)
136 for (std::size_t j = 0; j < n; ++j) P[a](i, j) = T(P[a](i, j) + w * V[a](i, j));
137 }
138 return P;
139}
140
141/**
142 * P_0(t) .. P_na(t), evaluated on a short interval and squared up.
143 *
144 * @param m the MAP
145 * @param na highest arrival count to return
146 * @param t interval length
147 * @param M number of squarings; negative selects the reference's default
148 * ceil(log2(100 t / mean)), and a value below zero after that means
149 * the direct evaluation
150 */
151template <class T>
152std::vector<Matrix<T>> map_pnt(const Map<T>& m, std::size_t na, const T& t, long M = -1) {
154 "map_pnt weights the terms by a Poisson law");
155 long steps = M;
156 if (steps < 0) {
157 const double mean = num_traits<T>::to_double(map_mean(m));
158 const double tv = num_traits<T>::to_double(t);
159 if (!(mean > 0.0) || !(tv > 0.0)) return map_pntbisect(m, na, t);
160 steps = static_cast<long>(std::ceil(std::log2(tv * 100.0 / mean)));
161 if (steps < 0) return map_pntbisect(m, na, t);
162 }
163
164 const T half = num_traits<T>::from_double(std::pow(2.0, static_cast<double>(steps)));
165 std::vector<Matrix<T>> P = map_pntbisect(m, na, T(t / half));
166 const std::size_t n = m.order();
167 const T zero = num_traits<T>::from_int(0);
168 for (long s = 0; s < steps; ++s) {
169 const std::vector<Matrix<T>> Pold = P;
170 for (std::size_t a = 0; a <= na; ++a) {
171 Matrix<T> acc(n, n, zero);
172 for (std::size_t j = 0; j <= a; ++j)
173 for (std::size_t i = 0; i < n; ++i)
174 for (std::size_t c = 0; c < n; ++c) {
175 T v = zero;
176 for (std::size_t q = 0; q < n; ++q)
177 v += Pold[j](i, q) * Pold[a - j](q, c);
178 acc(i, c) = T(acc(i, c) + v);
179 }
180 P[a] = acc;
181 }
182 }
183 return P;
184}
185
186/**
187 * The same counting probabilities by NUMERICAL INTEGRATION, `map_pntquad`.
188 *
189 * Port of matlab/lib/kpctoolbox/map/map_pntquad.m, which integrates the forward
190 * equations
191 * dP_0/dt = P_0 D0, dP_n/dt = P_n D0 + P_{n-1} D1
192 * from P_0(0) = I with ode45. The reference stacks all na+1 matrices into one
193 * state vector and integrates them together, which is what is done here with
194 * `ode_rosenbrock4`.
195 *
196 * IT IS A SECOND ROUTE TO THE SAME OBJECT, not a different quantity, and that
197 * is its value: `map_pnt` reaches P_n(t) by uniformization and this one by
198 * quadrature, so the two agreeing is evidence neither is wrong. The
199 * uniformization route is the cheaper one and is what callers should use; this
200 * exists because the reference exposes it and because it is the independent
201 * check the tests apply.
202 *
203 * @param m the MAP
204 * @param na highest arrival count to return
205 * @param t interval length
206 */
207template <class T>
208std::vector<Matrix<T>> map_pntquad(const Map<T>& m, std::size_t na, const T& t) {
210 "map_pntquad integrates the forward equations");
211 const std::size_t K = m.order();
212 if (K == 0 || m.D1.rows() != K) throw InputError("map_pntquad: D0 and D1 disagree");
213 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
214
215 const std::size_t blk = K * K, dim = (na + 1) * blk;
216 std::vector<T> y0(dim, zero);
217 for (std::size_t i = 0; i < K; ++i) y0[i * K + i] = one; // P_0(0) = I
218
219 auto rhs = [&](const T&, const std::vector<T>& y) {
220 std::vector<T> dy(dim, zero);
221 for (std::size_t n = 0; n <= na; ++n)
222 for (std::size_t i = 0; i < K; ++i)
223 for (std::size_t j = 0; j < K; ++j) {
224 T acc = zero;
225 for (std::size_t q = 0; q < K; ++q) acc += y[n * blk + i * K + q] * m.D0(q, j);
226 if (n > 0)
227 for (std::size_t q = 0; q < K; ++q)
228 acc += y[(n - 1) * blk + i * K + q] * m.D1(q, j);
229 dy[n * blk + i * K + j] = acc;
230 }
231 return dy;
232 };
233
234 const std::vector<T> yt = ode_rosenbrock4_endpoint(rhs, zero, t, y0);
235 std::vector<Matrix<T>> P(na + 1, Matrix<T>(K, K, zero));
236 for (std::size_t n = 0; n <= na; ++n)
237 for (std::size_t i = 0; i < K; ++i)
238 for (std::size_t j = 0; j < K; ++j) P[n](i, j) = yt[n * blk + i * K + j];
239 return P;
240}
241
242/** The reference's entry point: only the highest count is returned. */
243template <class T>
244Matrix<T> map_pntiter(const Map<T>& m, std::size_t na, const T& t, long M = -1) {
245 return map_pnt(m, na, t, M)[na];
246}
247
248} // namespace mam
249} // namespace line
250
251#endif // LINE_API_MAM_MAP_PNT_H
InputError(const std::string &what)
Definition error.h:105
The exception types the port throws.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
std::vector< Matrix< T > > map_pntbisect(const Map< T > &m, std::size_t na, const T &t)
P_0(t) .
Definition map_pnt.h:85
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
std::vector< Matrix< T > > map_pnt(const Map< T > &m, std::size_t na, const T &t, long M=-1)
P_0(t) .
Definition map_pnt.h:152
std::vector< Matrix< T > > map_pntquad(const Map< T > &m, std::size_t na, const T &t)
The same counting probabilities by NUMERICAL INTEGRATION, map_pntquad.
Definition map_pnt.h:208
Matrix< T > map_pntiter(const Map< T > &m, std::size_t na, const T &t, long M=-1)
The reference's entry point: only the highest count is returned.
Definition map_pnt.h:244
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
std::vector< T > ode_rosenbrock4_endpoint(const F &f, const T &t0, const T &t1, const std::vector< T > &y0)
Integrate with the default options and return only the state at t1.
Definition ode.h:495
Number-type abstraction for the templated API port.
Adaptive stiff ODE integrator: a four-stage Rosenbrock method of order four with an embedded order-th...
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
std::size_t order() const
Definition map_moment.h:57