LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
map_transform.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_TRANSFORM_H
6#define LINE_API_MAM_MAP_TRANSFORM_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * MAP constructors and structural transformations.
12 *
13 * Templated port of the kpctoolbox MAP algebra that the QBD solvers consume
14 * (matlab/lib/kpctoolbox/map/map_normalize.m, map_scale.m, map_erlang.m,
15 * map_exponential.m, map_hyperexp.m, map_sum.m, map_sumind.m, map_mixture.m,
16 * map_renewal.m, map_stochcomp.m, map2ph.m, map_skew.m, map_kurt.m,
17 * map_joint.m, map_isfeasible.m).
18 *
19 * Everything here except map_hyperexp and map_skew is a finite sequence of
20 * field operations on the entries of (D0, D1) -- block assembly, a Kronecker
21 * product, one linear solve -- so the exact instantiation carries the MAP
22 * identities (row sums of D0 + D1 vanish, the embedded chain is stochastic)
23 * with no residual at all. map_hyperexp needs a square root of the moment
24 * discriminant and map_skew a square root of the SCV, so both are gated on
25 * num_traits<T>::has_transcendental.
26 *
27 * Naming note: map_exponential in map_moment.h is rate-parameterized,
28 * map_exponential(lambda), whereas the MATLAB map_exponential(MEAN) is
29 * mean-parameterized. map_exponential_mean below is the MATLAB spelling; the
30 * two differ by the reciprocal and are otherwise identical.
31 */
32
33#include <cmath>
34#include <cstddef>
35#include <vector>
36
38#include "line/num/number.h"
39#include "line/util/error.h"
40#include "line/util/linalg.h"
41#include "line/util/lu.h"
42#include "line/util/matrix.h"
43
44namespace line {
45namespace mam {
46
47/**
48 * Clamp negative off-diagonal entries of D0 and negative entries of D1 to
49 * zero, then rebuild the diagonal of D0 so that every row of D0 + D1 sums to
50 * zero (map_normalize.m).
51 */
52template <class T>
54 const T zero = num_traits<T>::from_int(0);
55 Map<T> m = in;
56 const std::size_t n = m.order();
57 if (m.D1.rows() != n || m.D0.cols() != n || m.D1.cols() != n)
58 throw InputError("map_normalize: D0 and D1 must be square and of equal order");
59 for (std::size_t i = 0; i < n; ++i)
60 for (std::size_t j = 0; j < n; ++j) {
61 if (i != j && m.D0(i, j) < zero) m.D0(i, j) = zero;
62 if (m.D1(i, j) < zero) m.D1(i, j) = zero;
63 }
64 for (std::size_t i = 0; i < n; ++i) {
65 m.D0(i, i) = zero;
66 T s = zero;
67 for (std::size_t j = 0; j < n; ++j) s += m.D0(i, j) + m.D1(i, j);
68 m.D0(i, i) = -s;
69 }
70 return m;
71}
72
73/** Rescale time so that the mean inter-arrival time becomes new_mean. */
74template <class T>
75Map<T> map_scale(const Map<T>& in, const T& new_mean) {
76 if (new_mean == num_traits<T>::from_int(0)) throw InputError("map_scale: zero target mean");
77 const T ratio = map_mean(in) / new_mean;
78 Map<T> m = in;
79 for (std::size_t i = 0; i < m.order(); ++i)
80 for (std::size_t j = 0; j < m.order(); ++j) {
81 m.D0(i, j) *= ratio;
82 m.D1(i, j) *= ratio;
83 }
84 return map_normalize(m);
85}
86
87/**
88 * Rescale to a target mean WITHOUT the feasibility repair, for a matrix
89 * exponential.
90 *
91 * `map_scale` finishes with `map_normalize`, which zeroes every negative entry;
92 * that is a repair for a MAP whose blocks drifted, and it is DESTRUCTION for an
93 * ME or a RAP, whose negative off-diagonals are the representation. Scaling both
94 * blocks by one positive factor already preserves every normalized moment and
95 * the zero row sums of D0 + D1, so nothing needs repairing.
96 *
97 * The distinction is the reference's: `solver_mam_basic.m:82-88` branches on
98 * `procid == ME || procid == RAP` and rescales by the rate alone. Without the
99 * branch the clamp turns a two-moment CME fit into a different process
100 * altogether -- measured on M/Pareto/1 at rho 0.5, which reported the queue
101 * length of an infinite server (0.5) against the exact 0.95.
102 */
103template <class T>
104Map<T> map_scale_rate(const Map<T>& in, const T& new_mean) {
105 if (new_mean == num_traits<T>::from_int(0)) throw InputError("map_scale_rate: zero target mean");
106 const T ratio = map_mean(in) / new_mean;
107 Map<T> m = in;
108 for (std::size_t i = 0; i < m.order(); ++i)
109 for (std::size_t j = 0; j < m.order(); ++j) {
110 m.D0(i, j) *= ratio;
111 m.D1(i, j) *= ratio;
112 }
113 return m;
114}
115
116/** Poisson process with the given mean inter-arrival time (map_exponential.m). */
117template <class T>
119 if (mean == num_traits<T>::from_int(0)) throw InputError("map_exponential_mean: zero mean");
120 return map_exponential(T(num_traits<T>::from_int(1) / mean));
121}
122
123/** Erlang-k renewal MAP with the given mean (map_erlang.m). */
124template <class T>
125Map<T> map_erlang(const T& mean, unsigned k) {
126 if (k == 0) throw InputError("map_erlang: k must be positive");
127 if (mean == num_traits<T>::from_int(0)) throw InputError("map_erlang: zero mean");
128 const T mu = num_traits<T>::from_int(static_cast<long>(k)) / mean;
129 const T zero = num_traits<T>::from_int(0);
130 Map<T> m;
131 m.D0 = Matrix<T>(k, k, zero);
132 m.D1 = Matrix<T>(k, k, zero);
133 for (unsigned i = 0; i + 1 < k; ++i) m.D0(i, i + 1) = mu;
134 m.D1(k - 1, 0) = mu;
135 return map_normalize(m);
136}
137
138/**
139 * Two-phase hyperexponential renewal MAP matching a mean and an SCV >= 1,
140 * with branching probability p (map_hyperexp.m, default p = 0.99).
141 *
142 * Gated on transcendental arithmetic: the phase rates come from the root of a
143 * quadratic moment-matching condition and need a square root, which has no
144 * exact rational counterpart. MATLAB falls back to the second root and then to
145 * a smaller p when the first root is infeasible; the same two fallbacks are
146 * reproduced here, and an infeasible result throws rather than returning the
147 * empty MAP that MATLAB returns.
148 */
149template <class T>
150Map<T> map_hyperexp(const T& mean, const T& scv, const T& p_in) {
152 "map_hyperexp requires transcendental arithmetic");
153 using std::sqrt;
154 const T one = num_traits<T>::from_int(1);
155 const T two = num_traits<T>::from_int(2);
156 const T four = num_traits<T>::from_int(4);
157 T p = p_in;
158 for (unsigned attempt = 0; attempt < 8; ++attempt) {
159 const T E2 = T((one + scv) * mean * mean);
160 const T Delta = T(-four * p * mean * mean + four * p * p * mean * mean + two * E2 * p -
161 two * E2 * p * p);
162 if (Delta >= num_traits<T>::from_int(0)) {
163 const T sD = T(sqrt(Delta));
164 const T den = T(E2 * p - two * mean * mean);
165 if (den != num_traits<T>::from_int(0)) {
166 for (int root = 0; root < 2; ++root) {
167 const T mu2 = T((root == 0 ? T(-two * mean + two * p * mean + sD)
168 : T(-two * mean + two * p * mean - sD)) /
169 den);
170 const T dd = T(p - one + mean * mu2);
171 if (dd == num_traits<T>::from_int(0)) continue;
172 const T mu1 = T(mu2 * p / dd);
173 Map<T> m;
176 m.D0(0, 0) = -mu1;
177 m.D0(1, 1) = -mu2;
178 m.D1(0, 0) = mu1 * p;
179 m.D1(0, 1) = mu1 * (one - p);
180 m.D1(1, 0) = mu2 * p;
181 m.D1(1, 1) = mu2 * (one - p);
182 if (mu1 > num_traits<T>::from_int(0) && mu2 > num_traits<T>::from_int(0))
183 return m;
184 }
185 }
186 }
188 }
189 throw NumericError("map_hyperexp: no feasible two-phase fit for this (mean, scv)");
190}
191
192/** map_hyperexp with the MATLAB default branching probability p = 0.99. */
193template <class T>
194Map<T> map_hyperexp(const T& mean, const T& scv) {
195 return map_hyperexp(mean, scv, T(num_traits<T>::from_rational(99, 100)));
196}
197
198/**
199 * n-fold convolution of a MAP with itself: the inter-arrival time of the
200 * result is the sum of n consecutive inter-arrival times (map_sum.m).
201 */
202template <class T>
203Map<T> map_sum(const Map<T>& in, unsigned n) {
204 if (n == 0) throw InputError("map_sum: n must be positive");
205 const std::size_t order = in.order();
206 const std::size_t N = order * n;
207 const T zero = num_traits<T>::from_int(0);
208 Map<T> m;
209 m.D0 = Matrix<T>(N, N, zero);
210 m.D1 = Matrix<T>(N, N, zero);
211 std::size_t cur = 0;
212 for (unsigned i = 0; i < n; ++i) {
213 for (std::size_t a = 0; a < order; ++a)
214 for (std::size_t b = 0; b < order; ++b) m.D0(cur + a, cur + b) = in.D0(a, b);
215 if (i + 1 < n) {
216 for (std::size_t a = 0; a < order; ++a)
217 for (std::size_t b = 0; b < order; ++b)
218 m.D0(cur + a, cur + order + b) = in.D1(a, b);
219 } else {
220 for (std::size_t a = 0; a < order; ++a)
221 for (std::size_t b = 0; b < order; ++b) m.D1(cur + a, b) = in.D1(a, b);
222 }
223 cur += order;
224 }
225 return m;
226}
227
228/**
229 * Sum of independent, not necessarily identical MAPs: after each component
230 * completes, the next one restarts from its own stationary arrival phase
231 * distribution pie (map_sumind.m).
232 */
233template <class T>
234Map<T> map_sumind(const std::vector<Map<T>>& maps) {
235 if (maps.empty()) throw InputError("map_sumind: empty list");
236 const std::size_t n = maps.size();
237 std::vector<std::size_t> order(n), off(n + 1, 0);
238 for (std::size_t i = 0; i < n; ++i) {
239 order[i] = maps[i].order();
240 off[i + 1] = off[i] + order[i];
241 }
242 const std::size_t N = off[n];
243 const T zero = num_traits<T>::from_int(0);
244 Map<T> m;
245 m.D0 = Matrix<T>(N, N, zero);
246 m.D1 = Matrix<T>(N, N, zero);
247 for (std::size_t i = 0; i < n; ++i) {
248 for (std::size_t a = 0; a < order[i]; ++a)
249 for (std::size_t b = 0; b < order[i]; ++b)
250 m.D0(off[i] + a, off[i] + b) = maps[i].D0(a, b);
251 // D1 of component i, collapsed to a column, times the entry vector of
252 // the next component (or of the first one, closing the cycle).
253 const std::size_t nxt = (i + 1 < n) ? i + 1 : 0;
254 const std::vector<T> pie = map_pie(maps[nxt]);
255 std::vector<T> rowsum(order[i], zero);
256 for (std::size_t a = 0; a < order[i]; ++a)
257 for (std::size_t b = 0; b < order[i]; ++b) rowsum[a] += maps[i].D1(a, b);
258 for (std::size_t a = 0; a < order[i]; ++a)
259 for (std::size_t b = 0; b < order[nxt]; ++b) {
260 const T val = rowsum[a] * pie[b];
261 if (i + 1 < n)
262 m.D0(off[i] + a, off[nxt] + b) = val;
263 else
264 m.D1(off[i] + a, off[nxt] + b) = val;
265 }
266 }
267 return m;
268}
269
270/**
271 * Probabilistic mixture of MAPs with weights alpha: after an arrival from
272 * component i the process jumps to component j with probability alpha(j),
273 * entering at its stationary arrival phase (map_mixture.m).
274 */
275template <class T>
276Map<T> map_mixture(const std::vector<T>& alpha, const std::vector<Map<T>>& maps) {
277 if (maps.empty()) throw InputError("map_mixture: empty list");
278 if (alpha.size() != maps.size()) throw InputError("map_mixture: weight/list size mismatch");
279 const std::size_t n = maps.size();
280 std::vector<std::size_t> order(n), off(n + 1, 0);
281 for (std::size_t i = 0; i < n; ++i) {
282 order[i] = maps[i].order();
283 off[i + 1] = off[i] + order[i];
284 }
285 const std::size_t N = off[n];
286 const T zero = num_traits<T>::from_int(0);
287 Map<T> m;
288 m.D0 = Matrix<T>(N, N, zero);
289 m.D1 = Matrix<T>(N, N, zero);
290 std::vector<std::vector<T>> pies(n);
291 for (std::size_t j = 0; j < n; ++j) pies[j] = map_pie(maps[j]);
292 for (std::size_t i = 0; i < n; ++i) {
293 for (std::size_t a = 0; a < order[i]; ++a)
294 for (std::size_t b = 0; b < order[i]; ++b)
295 m.D0(off[i] + a, off[i] + b) = maps[i].D0(a, b);
296 std::vector<T> rowsum(order[i], zero);
297 for (std::size_t a = 0; a < order[i]; ++a)
298 for (std::size_t b = 0; b < order[i]; ++b) rowsum[a] += maps[i].D1(a, b);
299 for (std::size_t j = 0; j < n; ++j)
300 for (std::size_t a = 0; a < order[i]; ++a)
301 for (std::size_t b = 0; b < order[j]; ++b)
302 m.D1(off[i] + a, off[j] + b) = rowsum[a] * alpha[j] * pies[j][b];
303 }
304 return map_normalize(m);
305}
306
307/**
308 * Renewal process with the same inter-arrival distribution: D1 is replaced by
309 * (D1 e) pie, which destroys the correlation but preserves every marginal
310 * moment (map_renewal.m).
311 */
312template <class T>
314 const std::size_t n = in.order();
315 const T zero = num_traits<T>::from_int(0);
316 const std::vector<T> pie = map_pie(in);
317 Map<T> m;
318 m.D0 = in.D0;
319 m.D1 = Matrix<T>(n, n, zero);
320 for (std::size_t a = 0; a < n; ++a) {
321 T rs = zero;
322 for (std::size_t b = 0; b < n; ++b) rs += in.D1(a, b);
323 for (std::size_t b = 0; b < n; ++b) m.D1(a, b) = rs * pie[b];
324 }
325 return m;
326}
327
328/** A phase-type representation (alpha, T) of a MAP's inter-arrival time. */
329template <class T>
330struct PhType {
331 std::vector<T> alpha; ///< entry distribution, = pie
332 Matrix<T> subgen; ///< sub-generator, = D0
333};
334
335/** (alpha, T) of the inter-arrival distribution: alpha = pie, T = D0 (map2ph.m). */
336template <class T>
338 PhType<T> ph;
339 ph.alpha = map_pie(in);
340 ph.subgen = in.D0;
341 return ph;
342}
343
344/** MAP whose inter-arrival time is the PH (alpha, T): the renewal MAP with D1 = (-T e) alpha. */
345template <class T>
347 const std::size_t n = ph.subgen.rows();
348 if (ph.alpha.size() != n) throw InputError("ph2map: alpha and T have different orders");
349 const T zero = num_traits<T>::from_int(0);
350 Map<T> m;
351 m.D0 = ph.subgen;
352 m.D1 = Matrix<T>(n, n, zero);
353 for (std::size_t a = 0; a < n; ++a) {
354 T rs = zero;
355 for (std::size_t b = 0; b < n; ++b) rs += ph.subgen(a, b);
356 for (std::size_t b = 0; b < n; ++b) m.D1(a, b) = -rs * ph.alpha[b];
357 }
358 return m;
359}
360
361/**
362 * Stochastic complement of a MAP on the retained phases (map_stochcomp.m):
363 * the eliminated phases are censored out of the generator and of D1, giving a
364 * smaller MAP with the same behaviour observed on the retained phases.
365 */
366template <class T>
367Map<T> map_stochcomp(const Map<T>& in, const std::vector<std::size_t>& retain) {
368 const std::size_t n = in.order();
369 std::vector<bool> keep(n, false);
370 for (std::size_t r : retain) {
371 if (r >= n) throw InputError("map_stochcomp: retained index out of range");
372 keep[r] = true;
373 }
374 std::vector<std::size_t> elim;
375 for (std::size_t i = 0; i < n; ++i)
376 if (!keep[i]) elim.push_back(i);
377 const std::size_t nr = retain.size(), ne = elim.size();
378 if (nr == 0) throw InputError("map_stochcomp: no phase retained");
379 const Matrix<T> Q = map_infgen(in);
380 Map<T> out;
381 out.D0 = Matrix<T>(nr, nr);
382 out.D1 = Matrix<T>(nr, nr);
383 if (ne == 0) {
384 for (std::size_t a = 0; a < nr; ++a)
385 for (std::size_t b = 0; b < nr; ++b) {
386 out.D0(a, b) = in.D0(retain[a], retain[b]);
387 out.D1(a, b) = in.D1(retain[a], retain[b]);
388 }
389 return map_normalize(out);
390 }
391 Matrix<T> Q_EE(ne, ne), Q_RE(nr, ne), Q_ER(ne, nr), D1_ER(ne, nr);
392 for (std::size_t a = 0; a < ne; ++a)
393 for (std::size_t b = 0; b < ne; ++b) Q_EE(a, b) = Q(elim[a], elim[b]);
394 for (std::size_t a = 0; a < nr; ++a)
395 for (std::size_t b = 0; b < ne; ++b) Q_RE(a, b) = Q(retain[a], elim[b]);
396 for (std::size_t a = 0; a < ne; ++a)
397 for (std::size_t b = 0; b < nr; ++b) {
398 Q_ER(a, b) = Q(elim[a], retain[b]);
399 D1_ER(a, b) = in.D1(elim[a], retain[b]);
400 }
401 Matrix<T> negQEE(ne, ne);
402 for (std::size_t a = 0; a < ne; ++a)
403 for (std::size_t b = 0; b < ne; ++b) negQEE(a, b) = -Q_EE(a, b);
404 const Matrix<T> inv = inverse(negQEE);
405 const Matrix<T> corrQ = matmul(Q_RE, matmul(inv, Q_ER));
406 const Matrix<T> corrD1 = matmul(Q_RE, matmul(inv, D1_ER));
407 for (std::size_t a = 0; a < nr; ++a)
408 for (std::size_t b = 0; b < nr; ++b) {
409 const T Qnew = Q(retain[a], retain[b]) + corrQ(a, b);
410 out.D1(a, b) = in.D1(retain[a], retain[b]) + corrD1(a, b);
411 out.D0(a, b) = Qnew - out.D1(a, b);
412 }
413 return map_normalize(out);
414}
415
416/** Kurtosis of the inter-arrival time (map_kurt.m); rational in the entries. */
417template <class T>
418T map_kurt(const Map<T>& m) {
419 const T m1 = map_moment(m, 1);
420 const T m2 = map_moment(m, 2);
421 const T m3 = map_moment(m, 3);
422 const T m4 = map_moment(m, 4);
423 const T v = map_var(m);
424 const T num = T(m4 - num_traits<T>::from_int(4) * m3 * m1 +
425 num_traits<T>::from_int(6) * m2 * m1 * m1 -
426 num_traits<T>::from_int(3) * m1 * m1 * m1 * m1);
427 return num / (v * v);
428}
429
430/**
431 * Skewness of the inter-arrival time (map_skew.m). Gated on transcendental
432 * arithmetic: the denominator is (sqrt(SCV) * mean)^3, and the square root of
433 * a rational SCV is irrational in general.
434 */
435template <class T>
436T map_skew(const Map<T>& m) {
437 static_assert(num_traits<T>::has_transcendental, "map_skew requires transcendental arithmetic");
438 using std::sqrt;
439 const T m1 = map_moment(m, 1);
440 const T m2 = map_moment(m, 2);
441 const T m3 = map_moment(m, 3);
442 const T M3 = T(m3 - num_traits<T>::from_int(3) * m2 * m1 +
443 num_traits<T>::from_int(2) * m1 * m1 * m1);
444 const T s = T(sqrt(map_scv(m)));
445 const T den = T(s * m1);
446 return M3 / (den * den * den);
447}
448
449/**
450 * Joint moment of K consecutive inter-arrival times observed at the cumulative
451 * lags a, with orders i (map_joint.m). a and i have the same length K; a[0] is
452 * ignored as a base point exactly as MATLAB's cumsum makes it.
453 */
454template <class T>
455T map_joint(const Map<T>& m, const std::vector<unsigned>& a, const std::vector<unsigned>& i) {
456 if (a.size() != i.size() || a.empty()) throw InputError("map_joint: a and i size mismatch");
457 const std::size_t n = m.order();
458 const std::size_t K = a.size();
459 std::vector<unsigned> ca(K);
460 unsigned acc = 0;
461 for (std::size_t k = 0; k < K; ++k) {
462 acc += a[k];
463 ca[k] = acc;
464 }
465 Matrix<T> negD0 = m.D0;
466 for (std::size_t r = 0; r < n; ++r)
467 for (std::size_t c = 0; c < n; ++c) negD0(r, c) = -negD0(r, c);
468 const Matrix<T> invD0 = inverse(negD0);
469 const Matrix<T> P = matmul(invD0, m.D1);
470 Matrix<T> JM = eye<T>(n);
471 for (std::size_t k = 0; k + 1 < K; ++k) {
472 Matrix<T> blk = matpow(invD0, i[k]);
473 const T f = num_factorial<T>(i[k]);
474 for (std::size_t r = 0; r < n; ++r)
475 for (std::size_t c = 0; c < n; ++c) blk(r, c) *= f;
476 JM = matmul(JM, matmul(blk, matpow(P, ca[k + 1] - ca[k])));
477 }
478 Matrix<T> last = matpow(invD0, i[K - 1]);
479 const T fl = num_factorial<T>(i[K - 1]);
480 for (std::size_t r = 0; r < n; ++r)
481 for (std::size_t c = 0; c < n; ++c) last(r, c) *= fl;
482 JM = matmul(JM, last);
483 const std::vector<T> v = vecmul(map_pie(m), JM);
485 for (const T& x : v) s += x;
486 return s;
487}
488
489/**
490 * Structural feasibility of a MAP within a tolerance (map_isfeasible.m):
491 * off-diagonal D0 and all of D1 non-negative, diagonal of D0 non-positive,
492 * D0 + D1 a generator, and the embedded chain P = (-D0)^-1 D1 non-negative and
493 * stochastic. The eigenvalue-multiplicity screen of the MATLAB routine is not
494 * reproduced -- it needs a full eigendecomposition, which is not in this port;
495 * a MAP that passes here can in principle still have a defective generator.
496 */
497template <class T>
498bool map_isfeasible(const Map<T>& m, const T& tol) {
499 const std::size_t n = m.order();
500 const T zero = num_traits<T>::from_int(0);
501 const T one = num_traits<T>::from_int(1);
502 const T ntol = T(num_traits<T>::from_int(static_cast<long>(n)) * tol);
503 // The reference's first guard: a NaN or an infinity anywhere makes every
504 // test below meaningless, and a NaN comparison is false in BOTH directions,
505 // so without this an unusable pair reads as feasible.
506 for (std::size_t i = 0; i < n; ++i)
507 for (std::size_t j = 0; j < n; ++j) {
508 const double a = num_traits<T>::to_double(m.D0(i, j));
509 const double b = num_traits<T>::to_double(m.D1(i, j));
510 if (std::isnan(a) || std::isnan(b) || std::isinf(a) || std::isinf(b)) return false;
511 }
512 for (std::size_t i = 0; i < n; ++i)
513 for (std::size_t j = 0; j < n; ++j) {
514 if (i != j && m.D0(i, j) < -tol) return false;
515 if (i == j && m.D0(i, j) > tol) return false;
516 if (m.D1(i, j) < -tol) return false;
517 }
518 const Matrix<T> Q = map_infgen(m);
519 for (std::size_t i = 0; i < n; ++i) {
520 T rs = zero;
521 for (std::size_t j = 0; j < n; ++j) {
522 if (i != j && Q(i, j) < -tol) return false;
523 rs += Q(i, j);
524 }
525 if (num_abs(T(rs)) > ntol) return false;
526 }
527 Matrix<T> negD0 = m.D0;
528 for (std::size_t i = 0; i < n; ++i)
529 for (std::size_t j = 0; j < n; ++j) negD0(i, j) = -negD0(i, j);
530 Matrix<T> P;
531 try {
532 P = matmul(inverse(negD0), m.D1);
533 } catch (const NumericError&) {
534 return false; // -D0 singular: no embedded chain, hence infeasible
535 }
536 for (std::size_t i = 0; i < n; ++i) {
537 T rs = zero;
538 for (std::size_t j = 0; j < n; ++j) {
539 if (P(i, j) < -tol) return false;
540 rs += P(i, j);
541 }
542 if (num_abs(T(rs - one)) > ntol) return false;
543 }
544 return true;
545}
546
547/**
548 * The reference's `map_checkfeasible`, i.e. `map_isfeasible` at a given
549 * tolerance. It is a nested function of map_isfeasible.m and a top-level class
550 * in the JAR; both spellings name the same test.
551 */
552template <class T>
553bool map_checkfeasible(const Map<T>& m, const T& tol) {
554 return map_isfeasible(m, tol);
555}
556
557/**
558 * `map_isfeasible(MAP)` with no tolerance, which is NOT the zero-tolerance test.
559 *
560 * The reference scans k from 15 downwards, i.e. from the tightest tolerance
561 * 1e-15 to the loosest 1e-1, stops at the first k whose `map_checkfeasible`
562 * passes, and reports feasible iff that tightest passing tolerance is tighter
563 * than `map_feastol` (1e-8). A MAP whose row sums are exact only to rounding is
564 * therefore feasible to the reference and INFEASIBLE to a zero-tolerance test.
565 *
566 * This overload used to call the zero-tolerance form, which no assembled MAP
567 * can pass in floating point: `map_block` was sent to its fallback on moment
568 * sets MATLAB fits exactly, purely on the row sums' last bits.
569 */
570template <class T>
571bool map_isfeasible(const Map<T>& m) {
572 for (int k = 15; k >= 1; --k) {
573 const T tol = num_traits<T>::from_double(std::pow(10.0, -static_cast<double>(k)));
574 if (map_isfeasible(m, tol)) return k > map_feastol();
575 }
576 return false;
577}
578
579} // namespace mam
580} // namespace line
581
582#endif // LINE_API_MAM_MAP_TRANSFORM_H
InputError(const std::string &what)
Definition error.h:39
The algorithm cannot proceed on this instance (singular matrix, ...).
Definition error.h:43
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LU factorization with partial pivoting, templated on the number type.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
Matrix< T > map_infgen(const Map< T > &m)
Generator of the underlying phase process, D0 + D1.
Definition map_moment.h:62
T map_joint(const Map< T > &m, const std::vector< unsigned > &a, const std::vector< unsigned > &i)
Joint moment of K consecutive inter-arrival times observed at the cumulative lags a,...
Map< T > map_hyperexp(const T &mean, const T &scv, const T &p_in)
Two-phase hyperexponential renewal MAP matching a mean and an SCV >= 1, with branching probability p ...
T map_skew(const Map< T > &m)
Skewness of the inter-arrival time (map_skew.m).
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
int map_feastol()
Tolerance exponent shared by the KPC feasibility checks (map_feastol.m).
Definition map_moment.h:49
Map< T > map_renewal(const Map< T > &in)
Renewal process with the same inter-arrival distribution: D1 is replaced by (D1 e) pie,...
Map< T > map_exponential(const T &lambda)
Two-phase MAP constructor for a Poisson process of rate lambda.
Definition map_moment.h:213
Map< T > map_erlang(const T &mean, unsigned k)
Erlang-k renewal MAP with the given mean (map_erlang.m).
bool map_isfeasible(const Map< T > &m, const T &tol)
Structural feasibility of a MAP within a tolerance (map_isfeasible.m): off-diagonal D0 and all of D1 ...
Map< T > map_exponential_mean(const T &mean)
Poisson process with the given mean inter-arrival time (map_exponential.m).
Map< T > map_sumind(const std::vector< Map< T > > &maps)
Sum of independent, not necessarily identical MAPs: after each component completes,...
Map< T > map_mixture(const std::vector< T > &alpha, const std::vector< Map< T > > &maps)
Probabilistic mixture of MAPs with weights alpha: after an arrival from component i the process jumps...
Map< T > map_sum(const Map< T > &in, unsigned n)
n-fold convolution of a MAP with itself: the inter-arrival time of the result is the sum of n consecu...
Map< T > map_scale_rate(const Map< T > &in, const T &new_mean)
Rescale to a target mean WITHOUT the feasibility repair, for a matrix exponential.
Map< T > ph2map(const PhType< T > &ph)
MAP whose inter-arrival time is the PH (alpha, T): the renewal MAP with D1 = (-T e) alpha.
bool map_checkfeasible(const Map< T > &m, const T &tol)
The reference's map_checkfeasible, i.e.
Map< T > map_scale(const Map< T > &in, const T &new_mean)
Rescale time so that the mean inter-arrival time becomes new_mean.
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
Map< T > map_stochcomp(const Map< T > &in, const std::vector< std::size_t > &retain)
Stochastic complement of a MAP on the retained phases (map_stochcomp.m): the eliminated phases are ce...
T map_scv(const Map< T > &m)
Squared coefficient of variation.
Definition map_moment.h:140
Map< T > map_normalize(const Map< T > &in)
Clamp negative off-diagonal entries of D0 and negative entries of D1 to zero, then rebuild the diagon...
T map_moment(const Map< T > &m, unsigned k)
Raw moment of order k of the inter-arrival time: k!
Definition map_moment.h:118
T map_kurt(const Map< T > &m)
Kurtosis of the inter-arrival time (map_kurt.m); rational in the entries.
PhType< T > map2ph(const Map< T > &in)
(alpha, T) of the inter-arrival distribution: alpha = pie, T = D0 (map2ph.m).
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
T num_abs(const T &v)
Definition number.h:172
std::vector< T > vecmul(const std::vector< T > &v, const Matrix< T > &A)
Row vector times matrix, v A.
Definition linalg.h:50
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
Matrix< T > matpow(const Matrix< T > &A, unsigned k)
Integer matrix power, by repeated squaring.
Definition linalg.h:89
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
Number-type abstraction for the templated API port.
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
A phase-type representation (alpha, T) of a MAP's inter-arrival time.
std::vector< T > alpha
entry distribution, = pie
Matrix< T > subgen
sub-generator, = D0