LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
map_sample.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_SAMPLE_H
6#define LINE_API_MAM_MAP_SAMPLE_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Sample the inter-arrival times of a MAP, a RAP or a matrix exponential.
12 *
13 * Templated port of matlab/lib/kpctoolbox/map/map_sample.m, rap_sample.m and
14 * me_sample.m, together with m3a's randp.m.
15 *
16 * A MAP is simulated on its own state space: from the current phase the process
17 * takes hidden transitions (D0's off-diagonal) until one of the arrival
18 * transitions (D1) fires, and the inter-arrival time is the sum of the
19 * exponential holding times spent on the way. The reference draws the whole path
20 * and then adds the holding times; this port adds them as it goes, which is the
21 * same sum with no path buffer and no growing `visits` matrix.
22 *
23 * A RAP IS NOT SIMULATED THAT WAY. Its D0 has negative off-diagonals, so there
24 * is no embedded jump chain to walk: the "phase" is a signed vector, not a
25 * state. The reference samples it by INVERSE TRANSFORM on the conditional
26 * distribution, advancing the row vector
27 * a <- a exp(D0 x) D1 / (a exp(D0 x) D1 e)
28 * after each arrival at x. That is what `rap_sample` does here, and it is why it
29 * costs a matrix exponential per sample where `map_sample` costs a handful of
30 * deviates. An ME is a RENEWAL process, so it does not pay that price: it lives
31 * in me_sample.h, which tabulates its CDF once.
32 *
33 * RANDOMNESS. The generator is `line::pfqn::McRng` passed by reference and
34 * advanced by the call, the convention every Monte Carlo entry point in this
35 * tree uses. The stream is NOT comparable with MATLAB's -- different generator,
36 * different mapping from bits to deviates -- so the oracle for these functions
37 * is distributional, never path-for-path.
38 *
39 * ARITHMETIC: transcendental. Exponential deviates and, for the RAP and ME
40 * paths, a matrix exponential.
41 */
42
43#include <cmath>
44#include <cstddef>
45#include <vector>
46
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/matrix.h"
52
53namespace line {
54namespace mam {
55
56/**
57 * Draw an index from a discrete law, m3a's `randp`.
58 *
59 * The weights need not be normalized; they must be non-negative and not all
60 * zero, which the reference reports rather than assumes.
61 */
62template <class T>
63std::size_t randp(const std::vector<T>& P, pfqn::McRng& rng) {
64 const T zero = num_traits<T>::from_int(0);
65 T total = zero;
66 for (std::size_t i = 0; i < P.size(); ++i) {
67 if (P[i] < zero) throw InputError("randp: all probabilities should be 0 or larger");
68 total += P[i];
69 }
70 if (P.empty() || !(total > zero)) throw InputError("randp: all zero probabilities");
71 const double u = pfqn::mc_uniform01(rng);
72 T acc = zero;
73 for (std::size_t i = 0; i < P.size(); ++i) {
74 acc += P[i];
75 if (num_traits<T>::to_double(acc / total) >= u) return i;
76 }
77 return P.size() - 1;
78}
79
80/** The state a sampled inter-arrival began and ended in. */
82 std::vector<std::size_t> first; ///< phase at the start of each interval, 0-based
83 std::vector<std::size_t> last; ///< phase entered on each arrival, 0-based
84};
85
86/**
87 * @brief Sample the inter-arrival times of a MAP, a RAP or a matrix
88 * exponential.
89 *
90 * @param m the MAP
91 * @param n number of inter-arrival times to draw
92 * @param rng generator, advanced by the call
93 * @param pie0 initial phase law; empty selects map_pie, the reference's
94 * interval-stationary initialization
95 * @param trace optional per-sample start and end phases
96 */
97template <class T>
98std::vector<T> map_sample(const Map<T>& m, std::size_t n, pfqn::McRng& rng,
99 const std::vector<T>& pie0 = std::vector<T>(),
100 SampleTrace* trace = 0) {
102 "map_sample draws exponential deviates");
103 const std::size_t K = m.order();
104 if (K == 0 || m.D1.rows() != K) throw InputError("map_sample: D0 and D1 disagree");
105 const T zero = num_traits<T>::from_int(0);
106
107 std::vector<T> out;
108 out.reserve(n);
109 if (trace != 0) {
110 trace->first.assign(n, 0);
111 trace->last.assign(n, 0);
112 }
113
114 // The exponential case has no phase to walk.
115 if (K == 1) {
116 const T mean = map_mean(m);
117 for (std::size_t i = 0; i < n; ++i)
118 out.push_back(T(mean * num_traits<T>::from_double(-std::log(pfqn::mc_uniform01(rng)))));
119 return out;
120 }
121
122 const std::vector<T> start = pie0.empty() ? map_pie(m) : pie0;
123 if (start.size() != K) throw InputError("map_sample: the initial law has the wrong length");
124 std::size_t cur = randp(start, rng);
125
126 // Row i of the jump law over the 2K destinations: hidden moves first, then
127 // the arrival moves, each divided by the total rate out of i.
128 std::vector<std::vector<T>> jump(K, std::vector<T>(2 * K, zero));
129 std::vector<T> hold(K, zero);
130 for (std::size_t i = 0; i < K; ++i) {
131 const T rate = -m.D0(i, i);
132 if (!(num_traits<T>::to_double(rate) > 0.0))
133 throw InputError("map_sample: a phase has no exit rate");
134 hold[i] = T(num_traits<T>::from_int(1) / rate);
135 for (std::size_t j = 0; j < K; ++j) {
136 if (i != j) jump[i][j] = T(m.D0(i, j) / rate);
137 jump[i][K + j] = T(m.D1(i, j) / rate);
138 }
139 }
140
141 for (std::size_t s = 0; s < n; ++s) {
142 if (trace != 0) trace->first[s] = cur;
143 T acc = zero;
144 for (;;) {
145 // One exponential holding time in the current phase.
146 acc += T(hold[cur] * num_traits<T>::from_double(-std::log(pfqn::mc_uniform01(rng))));
147 const std::size_t d = randp(jump[cur], rng);
148 if (d >= K) { // an arrival: the interval ends here
149 cur = d - K;
150 break;
151 }
152 cur = d;
153 }
154 if (trace != 0) trace->last[s] = cur;
155 out.push_back(acc);
156 }
157 return out;
158}
159
160namespace sampledetail {
161
162/** exp(A t) by scaling and squaring around a truncated Taylor series. */
163template <class T>
164Matrix<T> expm(const Matrix<T>& A, const T& t) {
165 const std::size_t n = A.rows();
166 double nrm = 0.0;
167 for (std::size_t i = 0; i < n; ++i) {
168 double r = 0.0;
169 for (std::size_t j = 0; j < n; ++j)
170 r += std::fabs(num_traits<T>::to_double(A(i, j)) * num_traits<T>::to_double(t));
171 nrm = std::max(nrm, r);
172 }
173 int s = 0;
174 while (nrm > 0.5) {
175 nrm /= 2.0;
176 ++s;
177 }
178 const T h = T(t / num_traits<T>::from_double(std::pow(2.0, s)));
179 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
180 Matrix<T> R(n, n, zero), term(n, n, zero);
181 for (std::size_t i = 0; i < n; ++i) {
182 R(i, i) = one;
183 term(i, i) = one;
184 }
185 for (int k = 1; k <= 40; ++k) {
186 Matrix<T> nx(n, n, zero);
187 for (std::size_t i = 0; i < n; ++i)
188 for (std::size_t j = 0; j < n; ++j) {
189 T v = zero;
190 for (std::size_t q = 0; q < n; ++q) v += term(i, q) * A(q, j) * h;
191 nx(i, j) = T(v / num_traits<T>::from_int(k));
192 }
193 term = nx;
194 for (std::size_t i = 0; i < n; ++i)
195 for (std::size_t j = 0; j < n; ++j) R(i, j) += term(i, j);
196 }
197 for (int k = 0; k < s; ++k) {
198 Matrix<T> sq(n, n, zero);
199 for (std::size_t i = 0; i < n; ++i)
200 for (std::size_t j = 0; j < n; ++j) {
201 T v = zero;
202 for (std::size_t q = 0; q < n; ++q) v += R(i, q) * R(q, j);
203 sq(i, j) = v;
204 }
205 R = sq;
206 }
207 return R;
208}
209
210/** P(X > x) = a exp(D0 x) e, the conditional survival from entry law a. */
211template <class T>
212T survival(const Matrix<T>& D0, const std::vector<T>& a, const T& x) {
213 // QUALIFIED deliberately. Unqualified, ordinary lookup finds this
214 // namespace's `expm` while ADL on `line::Matrix<T>` also drags in
215 // `line::expm` from util/expm.h, and the call is AMBIGUOUS in any
216 // translation unit that includes both headers. Nothing did until the
217 // native LDES engine, which is why it compiled for so long.
218 const Matrix<T> E = sampledetail::expm(D0, x);
219 T s = num_traits<T>::from_int(0);
220 for (std::size_t i = 0; i < a.size(); ++i)
221 for (std::size_t j = 0; j < a.size(); ++j) s += a[i] * E(i, j);
222 return s;
223}
224
225} // namespace sampledetail
226
227/**
228 * Sample a RAP or a matrix exponential by inverse transform.
229 *
230 * There is no embedded jump chain to walk -- D0 may carry negative
231 * off-diagonals -- so each sample is the root of a exp(D0 x) e = u, found by
232 * bracketing and bisection, after which the entry law is advanced to
233 * a exp(D0 x) D1, renormalized.
234 *
235 * @param m the RAP or ME as a (D0, D1) pair
236 * @param n number of inter-arrival times to draw
237 * @param rng generator, advanced by the call
238 * @param a0 initial entry law; empty selects map_pie
239 * @param a_out when non-null, receives the entry law AFTER the last sample, so
240 * a caller drawing one variate at a time can chain the calls and
241 * keep the process correlated. A RAP's state is this real-valued
242 * vector and not a discrete phase, so it cannot be recovered from
243 * the sampled times the way a MAP's can from `SampleTrace`:
244 * without it, repeated n=1 calls silently restart the process
245 * from its stationary entry law every time and deliver a RENEWAL
246 * stream with the right marginal and no autocorrelation.
247 */
248template <class T>
249std::vector<T> rap_sample(const Map<T>& m, std::size_t n, pfqn::McRng& rng,
250 const std::vector<T>& a0 = std::vector<T>(),
251 std::vector<T>* a_out = 0) {
253 "rap_sample inverts a matrix-exponential survival function");
254 const std::size_t K = m.order();
255 if (K == 0 || m.D1.rows() != K) throw InputError("rap_sample: D0 and D1 disagree");
256 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
257
258 std::vector<T> a = a0.empty() ? map_pie(m) : a0;
259 if (a.size() != K) throw InputError("rap_sample: the initial law has the wrong length");
260 const double mean = num_traits<T>::to_double(map_mean(m));
261 if (!(mean > 0.0)) throw InputError("rap_sample: the process has no positive mean");
262
263 std::vector<T> out;
264 out.reserve(n);
265 for (std::size_t s = 0; s < n; ++s) {
266 const double u = pfqn::mc_uniform01(rng);
267 // Bracket the root of survival(x) = u, then bisect.
268 double lo = 0.0, hi = mean;
269 for (int k = 0; k < 200; ++k) {
271 sampledetail::survival(m.D0, a, num_traits<T>::from_double(hi))) <= u)
272 break;
273 lo = hi;
274 hi *= 2.0;
275 }
276 for (int k = 0; k < 200; ++k) {
277 const double mid = 0.5 * (lo + hi);
278 const double sv = num_traits<T>::to_double(
279 sampledetail::survival(m.D0, a, num_traits<T>::from_double(mid)));
280 if (sv > u)
281 lo = mid;
282 else
283 hi = mid;
284 if (hi - lo < 1e-14 * (1.0 + hi)) break;
285 }
286 const T x = num_traits<T>::from_double(0.5 * (lo + hi));
287 out.push_back(x);
288
289 // Advance the entry law: a <- a exp(D0 x) D1, renormalized.
290 const Matrix<T> E = sampledetail::expm(m.D0, x);
291 std::vector<T> b(K, zero), c(K, zero);
292 for (std::size_t j = 0; j < K; ++j)
293 for (std::size_t i = 0; i < K; ++i) b[j] += a[i] * E(i, j);
294 T tot = zero;
295 for (std::size_t j = 0; j < K; ++j) {
296 for (std::size_t i = 0; i < K; ++i) c[j] += b[i] * m.D1(i, j);
297 tot += c[j];
298 }
299 if (!(num_traits<T>::to_double(tot) > 0.0))
300 throw NumericError("rap_sample: the entry law lost all its mass");
301 for (std::size_t j = 0; j < K; ++j) a[j] = T(c[j] / tot);
302 }
303 (void)one;
304 if (a_out != 0) *a_out = a;
305 return out;
306}
307
308} // namespace mam
309} // namespace line
310
311#endif // LINE_API_MAM_MAP_SAMPLE_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
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::size_t randp(const std::vector< T > &P, pfqn::McRng &rng)
Draw an index from a discrete law, m3a's randp.
Definition map_sample.h:63
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
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
std::vector< T > rap_sample(const Map< T > &m, std::size_t n, pfqn::McRng &rng, const std::vector< T > &a0=std::vector< T >(), std::vector< T > *a_out=0)
Sample a RAP or a matrix exponential by inverse transform.
Definition map_sample.h:249
std::vector< T > map_sample(const Map< T > &m, std::size_t n, pfqn::McRng &rng, const std::vector< T > &pie0=std::vector< T >(), SampleTrace *trace=0)
Sample the inter-arrival times of a MAP, a RAP or a matrix exponential.
Definition map_sample.h:98
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
double mc_uniform01(McRng &g)
Uniform deviate on [0,1) with 53 significant bits, as a double.
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
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
The state a sampled inter-arrival began and ended in.
Definition map_sample.h:81
std::vector< std::size_t > first
phase at the start of each interval, 0-based
Definition map_sample.h:82
std::vector< std::size_t > last
phase entered on each arrival, 0-based
Definition map_sample.h:83