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 * REFERENCE DEFECT, NOT REPRODUCED (found 2026-08-01, verified in MATLAB
33 * R2025a). `map_pntbisect.m` weights V(n,k) by w_n instead of w_k -- the
34 * Poisson weight of the ARRIVAL count rather than of the uniformization step
35 * count -- and never propagates V(0,k), leaving it zero for every k >= 1. Both
36 * are wrong and neither is visible on a Poisson process, because there K = 0
37 * collapses V(n,k) to delta(n,k) and the two weights coincide on the only
38 * surviving term. On an Erlang-2 MAP of mean 1 at t = 1.3 the reference
39 * returns, against the identities any counting law must satisfy:
40 *
41 * P_0(t) vs exp(D0 t) max |diff| = 1.93e-1
42 * sum_n P_n(t) vs exp((D0+D1) t) max |diff| = 4.97e-1
43 * sum_n n pie P_n(t) e vs lambda t 0 against 1.3
44 *
45 * The version here satisfies all three to round-off, and the tests assert them.
46 * `map_pntiter` and `map_pntquad` have NO CALLER in any codebase, so the defect
47 * is latent and changes no published result; fixing MATLAB, the JAR and Python
48 * is tracked separately.
49 *
50 * ARITHMETIC: transcendental, for the Poisson weights.
51 */
52
53#include <cmath>
54#include <cstddef>
55#include <vector>
56
58#include "line/util/ode.h"
59#include "line/num/number.h"
60#include "line/util/error.h"
61#include "line/util/matrix.h"
62
63namespace line {
64namespace mam {
65
66namespace detail {
67
68/** Poisson weight e^{-tau t} (tau t)^r / r!, formed in logs. */
69inline double pnt_weight(double tau, double t, std::size_t r) {
70 const double x = tau * t;
71 if (!(x > 0.0)) return r == 0 ? 1.0 : 0.0;
72 double logw = -x + static_cast<double>(r) * std::log(x);
73 for (std::size_t k = 2; k <= r; ++k) logw -= std::log(static_cast<double>(k));
74 return std::exp(logw);
75}
76
77/** Smallest N whose Poisson tail beyond it is below machine epsilon. */
78inline std::size_t pnt_truncation(double tau, double t) {
79 const std::size_t kMax = 2000;
80 double acc = 0.0;
81 for (std::size_t N = 1; N <= kMax; ++N) {
82 acc += pnt_weight(tau, t, N);
83 if (1.0 - acc < 2.3e-16) return N;
84 }
85 return kMax;
86}
87
88} // namespace detail
89
90/**
91 * P_0(t) .. P_na(t) by uniformization on one interval, without the squaring.
92 *
93 * @param m the MAP
94 * @param na highest arrival count to return
95 * @param t interval length
96 */
97template <class T>
98std::vector<Matrix<T>> map_pntbisect(const Map<T>& m, std::size_t na, const T& t) {
100 "map_pntbisect weights the terms by a Poisson law");
101 const std::size_t n = m.order();
102 if (n == 0 || m.D1.rows() != n) throw InputError("map_pntbisect: D0 and D1 disagree");
103 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
104
105 double tau = 0.0;
106 for (std::size_t i = 0; i < n; ++i)
107 tau = std::max(tau, -num_traits<T>::to_double(m.D0(i, i)));
108 const double tv = num_traits<T>::to_double(t);
109 std::vector<Matrix<T>> P(na + 1, Matrix<T>(n, n, zero));
110 if (!(tau > 0.0) || !(tv > 0.0)) { // no clock, or no time: nothing happens
111 for (std::size_t i = 0; i < n; ++i) P[0](i, i) = one;
112 return P;
113 }
114
115 const T taut = num_traits<T>::from_double(tau);
116 Matrix<T> K(n, n, zero), K1(n, n, zero);
117 for (std::size_t i = 0; i < n; ++i)
118 for (std::size_t j = 0; j < n; ++j) {
119 K(i, j) = T(m.D0(i, j) / taut + (i == j ? one : zero));
120 K1(i, j) = T(m.D1(i, j) / taut);
121 }
122
123 const std::size_t N = detail::pnt_truncation(tau, tv);
124 // V[n] is the current step's V(n,k); the previous step's is kept in Vprev.
125 std::vector<Matrix<T>> V(na + 1, Matrix<T>(n, n, zero));
126 for (std::size_t i = 0; i < n; ++i) V[0](i, i) = one; // V(0,0) = I
127 for (std::size_t a = 0; a <= na; ++a) {
128 const T w = num_traits<T>::from_double(detail::pnt_weight(tau, tv, 0));
129 for (std::size_t i = 0; i < n; ++i)
130 for (std::size_t j = 0; j < n; ++j) P[a](i, j) = T(P[a](i, j) + w * V[a](i, j));
131 }
132 for (std::size_t k = 1; k <= N; ++k) {
133 const std::vector<Matrix<T>> Vprev = V;
134 for (std::size_t a = 0; a <= na; ++a) {
135 Matrix<T> next(n, n, zero);
136 for (std::size_t i = 0; i < n; ++i)
137 for (std::size_t j = 0; j < n; ++j) {
138 T acc = zero;
139 for (std::size_t q = 0; q < n; ++q) acc += Vprev[a](i, q) * K(q, j);
140 if (a > 0)
141 for (std::size_t q = 0; q < n; ++q) acc += Vprev[a - 1](i, q) * K1(q, j);
142 next(i, j) = acc;
143 }
144 V[a] = next;
145 }
146 const T w = num_traits<T>::from_double(detail::pnt_weight(tau, tv, k));
147 for (std::size_t a = 0; a <= na; ++a)
148 for (std::size_t i = 0; i < n; ++i)
149 for (std::size_t j = 0; j < n; ++j) P[a](i, j) = T(P[a](i, j) + w * V[a](i, j));
150 }
151 return P;
152}
153
154/**
155 * P_0(t) .. P_na(t), evaluated on a short interval and squared up.
156 *
157 * @param m the MAP
158 * @param na highest arrival count to return
159 * @param t interval length
160 * @param M number of squarings; negative selects the reference's default
161 * ceil(log2(100 t / mean)), and a value below zero after that means
162 * the direct evaluation
163 */
164template <class T>
165std::vector<Matrix<T>> map_pnt(const Map<T>& m, std::size_t na, const T& t, long M = -1) {
167 "map_pnt weights the terms by a Poisson law");
168 long steps = M;
169 if (steps < 0) {
170 const double mean = num_traits<T>::to_double(map_mean(m));
171 const double tv = num_traits<T>::to_double(t);
172 if (!(mean > 0.0) || !(tv > 0.0)) return map_pntbisect(m, na, t);
173 steps = static_cast<long>(std::ceil(std::log2(tv * 100.0 / mean)));
174 if (steps < 0) return map_pntbisect(m, na, t);
175 }
176
177 const T half = num_traits<T>::from_double(std::pow(2.0, static_cast<double>(steps)));
178 std::vector<Matrix<T>> P = map_pntbisect(m, na, T(t / half));
179 const std::size_t n = m.order();
180 const T zero = num_traits<T>::from_int(0);
181 for (long s = 0; s < steps; ++s) {
182 const std::vector<Matrix<T>> Pold = P;
183 for (std::size_t a = 0; a <= na; ++a) {
184 Matrix<T> acc(n, n, zero);
185 for (std::size_t j = 0; j <= a; ++j)
186 for (std::size_t i = 0; i < n; ++i)
187 for (std::size_t c = 0; c < n; ++c) {
188 T v = zero;
189 for (std::size_t q = 0; q < n; ++q)
190 v += Pold[j](i, q) * Pold[a - j](q, c);
191 acc(i, c) = T(acc(i, c) + v);
192 }
193 P[a] = acc;
194 }
195 }
196 return P;
197}
198
199/**
200 * The same counting probabilities by NUMERICAL INTEGRATION, `map_pntquad`.
201 *
202 * Port of matlab/lib/kpctoolbox/map/map_pntquad.m, which integrates the forward
203 * equations
204 * dP_0/dt = P_0 D0, dP_n/dt = P_n D0 + P_{n-1} D1
205 * from P_0(0) = I with ode45. The reference stacks all na+1 matrices into one
206 * state vector and integrates them together, which is what is done here with
207 * `ode_rosenbrock4`.
208 *
209 * IT IS A SECOND ROUTE TO THE SAME OBJECT, not a different quantity, and that
210 * is its value: `map_pnt` reaches P_n(t) by uniformization and this one by
211 * quadrature, so the two agreeing is evidence neither is wrong. The
212 * uniformization route is the cheaper one and is what callers should use; this
213 * exists because the reference exposes it and because it is the independent
214 * check the tests apply.
215 *
216 * @param m the MAP
217 * @param na highest arrival count to return
218 * @param t interval length
219 */
220template <class T>
221std::vector<Matrix<T>> map_pntquad(const Map<T>& m, std::size_t na, const T& t) {
223 "map_pntquad integrates the forward equations");
224 const std::size_t K = m.order();
225 if (K == 0 || m.D1.rows() != K) throw InputError("map_pntquad: D0 and D1 disagree");
226 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
227
228 const std::size_t blk = K * K, dim = (na + 1) * blk;
229 std::vector<T> y0(dim, zero);
230 for (std::size_t i = 0; i < K; ++i) y0[i * K + i] = one; // P_0(0) = I
231
232 auto rhs = [&](const T&, const std::vector<T>& y) {
233 std::vector<T> dy(dim, zero);
234 for (std::size_t n = 0; n <= na; ++n)
235 for (std::size_t i = 0; i < K; ++i)
236 for (std::size_t j = 0; j < K; ++j) {
237 T acc = zero;
238 for (std::size_t q = 0; q < K; ++q) acc += y[n * blk + i * K + q] * m.D0(q, j);
239 if (n > 0)
240 for (std::size_t q = 0; q < K; ++q)
241 acc += y[(n - 1) * blk + i * K + q] * m.D1(q, j);
242 dy[n * blk + i * K + j] = acc;
243 }
244 return dy;
245 };
246
247 const std::vector<T> yt = ode_rosenbrock4_endpoint(rhs, zero, t, y0);
248 std::vector<Matrix<T>> P(na + 1, Matrix<T>(K, K, zero));
249 for (std::size_t n = 0; n <= na; ++n)
250 for (std::size_t i = 0; i < K; ++i)
251 for (std::size_t j = 0; j < K; ++j) P[n](i, j) = yt[n * blk + i * K + j];
252 return P;
253}
254
255/** The reference's entry point: only the highest count is returned. */
256template <class T>
257Matrix<T> map_pntiter(const Map<T>& m, std::size_t na, const T& t, long M = -1) {
258 return map_pnt(m, na, t, M)[na];
259}
260
261} // namespace mam
262} // namespace line
263
264#endif // LINE_API_MAM_MAP_PNT_H
InputError(const std::string &what)
Definition error.h:39
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:98
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:165
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:221
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:257
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:496
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