LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
me_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_ME_SAMPLE_H
6#define LINE_API_MAM_ME_SAMPLE_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Sample a matrix exponential by numerical inversion of its exact CDF.
12 *
13 * Templated port of matlab/lib/kpctoolbox/map/me_sample.m, cross-checked against
14 * jar/src/main/java/jline/api/mam/Me_sample.java.
15 *
16 * AN ME IS A RENEWAL PROCESS, which is what separates this from `rap_sample`.
17 * The inter-arrival times are i.i.d. draws from F(t) = 1 - pie exp(D0 t) e, so
18 * the entry law is the SAME at every sample and the inversion table built from
19 * it can be reused. `rap_sample` instead advances the entry law after each
20 * arrival, which is correct for a RAP and wrong for an ME: it delivers a
21 * correlated stream where the reference delivers an independent one.
22 *
23 * WHY A TABLE AND NOT A ROOT SOLVE. Routing ME through `rap_sample` costs a
24 * full matrix exponential per bisection step, some fifty of them per variate;
25 * on a 15-phase CME that is 140x the Java engine on the same model, enough for
26 * the LDES wrapper to hit its wall-clock budget and return an empty result. The
27 * reference tabulates the CDF once on a grid whose horizon is doubled until the
28 * survival is below TAILTOL, locates the variate by binary search and polishes
29 * it with Newton steps on the exact CDF and density, so the answer is not
30 * limited by the linear interpolation of the table.
31 *
32 * ONE UNIFORM PER VARIATE, as in `rap_sample`, so a caller switching between the
33 * two consumes the generator at the same rate.
34 *
35 * The grid itself is generated by a single expm(D0 h) and repeated
36 * vector-matrix products, and the Newton step propagates the row vector by a
37 * scaled Taylor series rather than re-exponentiating: both are O(K^2) where a
38 * matrix exponential is O(K^3).
39 *
40 * ARITHMETIC: transcendental.
41 */
42
43#include <cmath>
44#include <cstddef>
45#include <vector>
46
49#include "line/num/number.h"
50#include "line/util/eig.h"
51#include "line/util/error.h"
52#include "line/util/expm.h"
53#include "line/util/matrix.h"
54
55namespace line {
56namespace mam {
57
58namespace mesampledetail {
59
60/** Number of tabulated grid points, including t = 0 (me_sample.m GRIDPTS). */
61const std::size_t grid_points = 1000;
62/** Cap on horizon doublings (me_sample.m MAXDBL). */
63const int max_doublings = 40;
64/** Survival mass left beyond the horizon (me_sample.m TAILTOL). */
65const double tail_mass_tol = 1e-12;
66/** Newton refinements applied to each inverted quantile (me_sample.m NEWTON). */
67const int newton_steps = 3;
68/** Largest ||A||*d a single Taylor substep handles. */
69const double taylor_theta = 0.5;
70/** Substep cap before falling back to a matrix exponential. */
71const int taylor_max_steps = 64;
72/** Taylor terms per substep. */
73const int taylor_max_terms = 30;
74
75/** w <- w M, the row-vector product the grid and the Taylor walk both need. */
76template <class T>
77std::vector<T> vec_times_mat(const std::vector<T>& w, const Matrix<T>& M) {
78 const std::size_t n = w.size();
79 std::vector<T> out(n, num_traits<T>::from_int(0));
80 for (std::size_t j = 0; j < n; ++j)
81 for (std::size_t i = 0; i < n; ++i) out[j] += w[i] * M(i, j);
82 return out;
83}
84
85/**
86 * w exp(A d) by a scaled Taylor series on the VECTOR.
87 *
88 * Falls back to a matrix exponential when the substep count would exceed the
89 * cap, which is the only regime where the series is the more expensive of the
90 * two.
91 */
92template <class T>
93std::vector<T> expm_propagate(const std::vector<T>& w, const Matrix<T>& A, double norm_a, double d) {
94 const std::size_t n = w.size();
95 if (!(d > 0.0)) return w;
96 int steps = 1;
97 const double theta = norm_a * d;
98 if (theta > taylor_theta) {
99 steps = static_cast<int>(std::ceil(theta / taylor_theta));
100 if (steps > taylor_max_steps)
101 return vec_times_mat(w, expm(A, num_traits<T>::from_double(d)));
102 }
103 const double ds = d / steps;
104 std::vector<T> acc = w;
105 for (int s = 0; s < steps; ++s) {
106 std::vector<T> term = acc, next = acc;
107 for (int k = 1; k <= taylor_max_terms; ++k) {
108 term = vec_times_mat(term, A);
109 const T c = num_traits<T>::from_double(ds / k);
110 double max_term = 0.0, max_acc = 0.0;
111 for (std::size_t j = 0; j < n; ++j) {
112 term[j] = T(term[j] * c);
113 next[j] += term[j];
114 const double at = std::fabs(num_traits<T>::to_double(term[j]));
115 if (at > max_term) max_term = at;
116 const double an = std::fabs(num_traits<T>::to_double(next[j]));
117 if (an > max_acc) max_acc = an;
118 }
119 if (max_term <= 1e-18 * (max_acc > 1e-300 ? max_acc : 1e-300)) break;
120 }
121 acc = next;
122 }
123 return acc;
124}
125
126/**
127 * The dominant (least negative) eigenvalue of D0, which governs the tail decay.
128 *
129 * The spectrum is taken in double precision whatever T is: the extrapolation it
130 * feeds is a tail approximation, so its own last digits do not matter, and the
131 * fallback -1/mean is what the reference uses when the decomposition fails.
132 */
133template <class T>
134double dominant_rate(const Matrix<T>& A, double mean) {
135 double eta = -std::numeric_limits<double>::infinity();
136 const std::size_t n = A.rows();
137 Matrix<double> Ad(n, n);
138 for (std::size_t i = 0; i < n; ++i)
139 for (std::size_t j = 0; j < n; ++j) Ad(i, j) = num_traits<T>::to_double(A(i, j));
140 try {
141 const std::vector<std::complex<double> > ev = eig_values(Ad);
142 for (std::size_t i = 0; i < ev.size(); ++i)
143 if (ev[i].real() > eta) eta = ev[i].real();
144 } catch (const std::exception&) {
145 eta = -std::numeric_limits<double>::infinity();
146 }
147 if (!(eta < 0.0) || !std::isfinite(eta)) eta = (mean > 0.0) ? -1.0 / mean : -1.0;
148 return eta;
149}
150
151} // namespace mesampledetail
152
153/**
154 * Stateful ME sampler holding the inversion table.
155 *
156 * The table is what makes the LDES usage pattern -- one variate per service
157 * event, from a generator that lives as long as the station does -- cost a
158 * binary search rather than a matrix exponential.
159 */
160template <class T>
162public:
163 MeSampler() : n_(0), exponential_(false), exp_rate_(0.0), h_(0.0), t_end_(0.0), s_end_(0.0),
164 eta_(-1.0), norm_a_(0.0) {}
165
166 /** Build the table of `m`, whose D0 is the ME matrix and whose entry law is map_pie. */
167 explicit MeSampler(const Map<T>& m, const std::vector<T>& a0 = std::vector<T>()) {
169 "MeSampler inverts a matrix-exponential distribution function");
170 n_ = m.order();
171 if (n_ == 0) throw InputError("me_sample: the representation is empty");
172 A_ = m.D0;
173 std::vector<T> alpha = a0.empty() ? map_pie(m) : a0;
174 if (alpha.size() != n_) throw InputError("me_sample: the entry law has the wrong length");
175
176 row_sum_.assign(n_, num_traits<T>::from_int(0));
177 norm_a_ = 0.0;
178 for (std::size_t i = 0; i < n_; ++i) {
179 double abs_row = 0.0;
180 for (std::size_t j = 0; j < n_; ++j) {
181 row_sum_[i] += A_(i, j);
182 abs_row += std::fabs(num_traits<T>::to_double(A_(i, j)));
183 }
184 if (abs_row > norm_a_) norm_a_ = abs_row;
185 }
186
187 // A one-phase ME with alpha = 1 IS an exponential; inverting it in
188 // closed form skips a thousand-point table no caller would gain from.
189 exponential_ = (n_ == 1) && std::fabs(num_traits<T>::to_double(alpha[0]) - 1.0) < 1e-12 &&
190 num_traits<T>::to_double(A_(0, 0)) < 0.0;
191 exp_rate_ = exponential_ ? -num_traits<T>::to_double(A_(0, 0)) : 0.0;
192 if (exponential_) {
193 h_ = t_end_ = s_end_ = 0.0;
194 eta_ = -exp_rate_;
195 return;
196 }
197
198 const double mean = num_traits<T>::to_double(map_mean(m));
199 const double var = num_traits<T>::to_double(map_var(m));
200 const double sigma = (var > 0.0) ? std::sqrt(var) : 0.0;
201 double horizon = mean + 10.0 * sigma;
202 if (!(horizon > 0.0) || !std::isfinite(horizon)) horizon = 1.0;
203 for (int k = 0; k < mesampledetail::max_doublings; ++k) {
204 if (survival_by_expm(alpha, horizon) < mesampledetail::tail_mass_tol) break;
205 horizon *= 2.0;
206 }
207
208 const std::size_t g = mesampledetail::grid_points;
209 h_ = horizon / static_cast<double>(g - 1);
210 t_.resize(g);
211 F_.resize(g);
212 w_.resize(g);
213 // ONE matrix exponential for the whole grid: exp(D0 (k+1)h) applied to
214 // the entry law is exp(D0 kh) applied to it and then multiplied by
215 // exp(D0 h), so the table is a walk of row-vector products.
216 const Matrix<T> Eh = expm(A_, num_traits<T>::from_double(h_));
217 w_[0] = alpha;
218 t_[0] = 0.0;
219 F_[0] = clamp_unit(1.0 - sum_of(alpha));
220 for (std::size_t i = 1; i < g; ++i) {
221 w_[i] = mesampledetail::vec_times_mat(w_[i - 1], Eh);
222 t_[i] = static_cast<double>(i) * h_;
223 // Forced nondecreasing: roundoff breaks monotonicity in the tail,
224 // where consecutive values differ by less than eps.
225 double c = clamp_unit(1.0 - sum_of(w_[i]));
226 if (c < F_[i - 1]) c = F_[i - 1];
227 F_[i] = c;
228 }
229 t_end_ = t_[g - 1];
230 const double surv = 1.0 - F_[g - 1];
231 s_end_ = (surv > 0.0) ? surv : 0.0;
232 eta_ = mesampledetail::dominant_rate(A_, mean);
233 }
234
235 /** One variate, consuming exactly one uniform. */
236 double next(pfqn::McRng& rng) const {
237 const double u = pfqn::mc_uniform01(rng);
238 return quantile(u);
239 }
240
241 /** The inverse CDF at `u`, exposed so a caller can supply its own uniform. */
242 double quantile(double u) const {
243 if (exponential_) return -std::log(1.0 - u) / exp_rate_;
244 const std::size_t g = F_.size();
245 if (u <= F_[0]) return 0.0;
246 if (u >= F_[g - 1]) {
247 // Exponential tail S(x) ~ S(tEnd) exp(eta (x - tEnd)). Clamping to
248 // the grid endpoint instead truncates the tail and biases the mean.
249 const double tail = 1.0 - u;
250 if (s_end_ <= 0.0 || tail <= 0.0 || !(eta_ < 0.0)) return t_end_;
251 const double x = t_end_ + std::log(s_end_ / tail) / (-eta_);
252 return (x > t_end_) ? x : t_end_;
253 }
254 std::size_t lo = 0, hi = g - 1;
255 while (hi - lo > 1) {
256 const std::size_t mid = (lo + hi) / 2;
257 if (F_[mid] <= u)
258 lo = mid;
259 else
260 hi = mid;
261 }
262 const double den = F_[lo + 1] - F_[lo];
263 double x = (den > 0.0) ? t_[lo] + (u - F_[lo]) / den * h_ : t_[lo];
264 const double left = t_[lo], right = t_[lo] + h_;
265
266 for (int k = 0; k < mesampledetail::newton_steps; ++k) {
267 const std::vector<T> w =
268 mesampledetail::expm_propagate(w_[lo], A_, norm_a_, x - left);
269 const double surv = sum_of(w);
270 double f = 0.0;
271 for (std::size_t j = 0; j < n_; ++j)
272 f -= num_traits<T>::to_double(w[j]) * num_traits<T>::to_double(row_sum_[j]);
273 if (!(f > 0.0)) break;
274 const double err = (1.0 - surv) - u;
275 if (std::fabs(err) < 1e-14) break;
276 const double xn = x - err / f;
277 if (!(xn > left) || !(xn < right)) break;
278 const bool converged =
279 std::fabs(xn - x) <= 1e-15 * (std::fabs(x) > 1.0 ? std::fabs(x) : 1.0);
280 x = xn;
281 if (converged) break;
282 }
283 return x;
284 }
285
286private:
287 double survival_by_expm(const std::vector<T>& alpha, double t) const {
288 const std::vector<T> w =
289 mesampledetail::vec_times_mat(alpha, expm(A_, num_traits<T>::from_double(t)));
290 const double s = sum_of(w);
291 return (s > 0.0) ? s : 0.0;
292 }
293
294 static double sum_of(const std::vector<T>& v) {
295 double s = 0.0;
296 for (std::size_t i = 0; i < v.size(); ++i) s += num_traits<T>::to_double(v[i]);
297 return s;
298 }
299
300 static double clamp_unit(double x) { return x < 0.0 ? 0.0 : (x > 1.0 ? 1.0 : x); }
301
302 std::size_t n_;
303 Matrix<T> A_;
304 std::vector<T> row_sum_;
305 bool exponential_;
306 double exp_rate_;
307 std::vector<double> t_;
308 std::vector<double> F_;
309 std::vector<std::vector<T> > w_;
310 double h_;
311 double t_end_;
312 double s_end_;
313 double eta_;
314 double norm_a_;
315};
316
317/**
318 * `me_sample`: n INDEPENDENT variates of the matrix exponential (D0, D1).
319 *
320 * Passing a MAP or a RAP here samples its stationary marginal independently,
321 * which is a deliberate renewal approximation: the autocorrelation is dropped.
322 * Use `rap_sample` to retain it.
323 *
324 * @param m the process, read through its entry law map_pie and its D0
325 * @param n number of inter-arrival times to draw
326 * @param rng generator, advanced by one uniform per variate
327 * @param a0 entry law; empty selects map_pie
328 */
329template <class T>
330std::vector<T> me_sample(const Map<T>& m, std::size_t n, pfqn::McRng& rng,
331 const std::vector<T>& a0 = std::vector<T>()) {
332 const MeSampler<T> s(m, a0);
333 std::vector<T> out;
334 out.reserve(n);
335 for (std::size_t i = 0; i < n; ++i)
336 out.push_back(num_traits<T>::from_double(s.next(rng)));
337 return out;
338}
339
340} // namespace mam
341} // namespace line
342
343#endif // LINE_API_MAM_ME_SAMPLE_H
InputError(const std::string &what)
Definition error.h:39
Stateful ME sampler holding the inversion table.
Definition me_sample.h:161
MeSampler(const Map< T > &m, const std::vector< T > &a0=std::vector< T >())
Build the table of m, whose D0 is the ME matrix and whose entry law is map_pie.
Definition me_sample.h:167
double next(pfqn::McRng &rng) const
One variate, consuming exactly one uniform.
Definition me_sample.h:236
double quantile(double u) const
The inverse CDF at u, exposed so a caller can supply its own uniform.
Definition me_sample.h:242
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
T map_var(const Map< T > &m)
Variance of the inter-arrival time.
Definition map_moment.h:133
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 > me_sample(const Map< T > &m, std::size_t n, pfqn::McRng &rng, const std::vector< T > &a0=std::vector< T >())
me_sample: n INDEPENDENT variates of the matrix exponential (D0, D1).
Definition me_sample.h:330
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.
std::vector< std::complex< double > > eig_values(const Matrix< double > &A)
Eigenvalues of a general real square matrix, in LAPACK's order.
Definition eig.h:59
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 > D0
Definition map_moment.h:54
std::size_t order() const
Definition map_moment.h:57