LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_passage.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_MC_CTMC_PASSAGE_H
6#define LINE_API_MC_CTMC_PASSAGE_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * First passage times into a target STATE SET, for Markov and semi-Markov
12 * chains.
13 *
14 * Port of matlab/src/api/mc/ctmc_passage_*.m and smp_passage_*.m, which are the
15 * reference. Source: P. G. Harrison and W. J. Knottenbelt, "Passage Time
16 * Distributions in Large Markov Chains", 2002 -- Eqs. 1-3 for the Markov case,
17 * Eqs. 4-8 for the semi-Markov one.
18 *
19 * THE IDENTITY THE WHOLE FAMILY RESTS ON. The first passage time from an
20 * initial law pi0 into a target set B is PHASE-TYPE. With A the complement,
21 *
22 * S = Q(A,A) sub-generator: the passage has not completed
23 * s0 = -S*1 exit vector, equal to Q(A,B)*1
24 * alpha = pi0(A) UNNORMALIZED, see below
25 * atom = sum pi0(B)
26 *
27 * so L(s) = alpha (sI-S)^{-1} s0 + atom and F(t) = 1 - alpha exp(St) 1. The
28 * paper writes the same system as n scalar equations with L_i = 1 on B.
29 *
30 * ALPHA IS DELIBERATELY NOT NORMALIZED. Its mass is 1 - atom; the missing mass
31 * is the ATOM AT ZERO carried by initial states already inside the target. A
32 * caller that normalizes alpha and forgets the atom reports F(0) = 0 for a
33 * passage that has already completed with probability atom.
34 *
35 * THIS IS NOT THE SPLIT `solver_ctmc_cdf.h` USES. That one is by EVENT (the
36 * tagged job arriving at or departing from a station, through the filtration),
37 * this one is by STATE SET. The two are complementary and must not be merged.
38 *
39 * ARITHMETIC: needs `expm` and transcendentals, so exact arithmetic is refused
40 * by name rather than silently producing a wrong type.
41 */
42
43#include <algorithm>
44#include <cmath>
45#include <complex>
46#include <cstddef>
47#include <functional>
48#include <limits>
49#include <vector>
50
53#include "line/num/number.h"
54#include "line/util/error.h"
55#include "line/util/expm.h"
56#include "line/util/lu.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace mc {
61
62/** The phase-type form of a first passage time. */
63template <class T>
64struct PassagePh {
65 std::vector<T> alpha; ///< pi0 restricted to the non-target block, UNNORMALIZED
66 Matrix<T> S; ///< sub-generator Q(A,A)
67 std::vector<T> s0; ///< exit vector -S*1
68 std::vector<std::size_t> keep; ///< row of S -> state index of Q
69 T atom; ///< mass of pi0 already inside the target: F(0)
70};
71
72namespace passage_detail {
73
74inline std::vector<std::size_t> unique_target(const std::vector<std::size_t>& target,
75 std::size_t n, const char* fn) {
76 std::vector<std::size_t> t = target;
77 std::sort(t.begin(), t.end());
78 t.erase(std::unique(t.begin(), t.end()), t.end());
79 if (t.empty())
80 throw InputError(std::string(fn) +
81 ": the target state set is empty: a first passage time into no state "
82 "is undefined");
83 if (t.back() >= n)
84 throw InputError(std::string(fn) + ": a target state index is outside the state space");
85 return t;
86}
87
88/**
89 * Gaussian elimination with partial pivoting for a COMPLEX system.
90 *
91 * `line::solve` cannot serve here: its pivot search compares magnitudes with
92 * `operator>`, which std::complex does not have, so instantiating it on
93 * complex is a compile error rather than a silent wrong answer. The transform
94 * routes need complex coefficients, so they carry their own solve.
95 */
96inline std::vector<std::complex<double>> solve_cplx(Matrix<std::complex<double>> A,
97 std::vector<std::complex<double>> b,
98 const char* fn) {
99 using C = std::complex<double>;
100 const std::size_t n = A.rows();
101 if (A.cols() != n || b.size() != n)
102 throw InputError(std::string(fn) + ": complex system is not square");
103 for (std::size_t k = 0; k < n; ++k) {
104 std::size_t piv = k;
105 double best = std::abs(A(k, k));
106 for (std::size_t i = k + 1; i < n; ++i) {
107 const double m = std::abs(A(i, k));
108 if (m > best) {
109 best = m;
110 piv = i;
111 }
112 }
113 if (!(best > 0.0))
114 throw NumericError(std::string(fn) + ": singular transform matrix");
115 if (piv != k) {
116 for (std::size_t j = 0; j < n; ++j) std::swap(A(k, j), A(piv, j));
117 std::swap(b[k], b[piv]);
118 }
119 for (std::size_t i = k + 1; i < n; ++i) {
120 const C f = A(i, k) / A(k, k);
121 if (f == C(0.0, 0.0)) continue;
122 for (std::size_t j = k; j < n; ++j) A(i, j) -= f * A(k, j);
123 b[i] -= f * b[k];
124 }
125 }
126 std::vector<C> x(n, C(0.0, 0.0));
127 for (std::size_t i = n; i-- > 0;) {
128 C s = b[i];
129 for (std::size_t j = i + 1; j < n; ++j) s -= A(i, j) * x[j];
130 x[i] = s / A(i, i);
131 }
132 return x;
133}
134
135/** Backward reachability closure over the transition graph. */
136template <class T>
137std::vector<bool> reaches_target(const Matrix<T>& Q, const std::vector<std::size_t>& keep,
138 const std::vector<std::size_t>& target) {
139 const std::size_t n = Q.rows();
140 std::vector<bool> seen(n, false);
141 std::vector<std::size_t> frontier;
142 for (std::size_t k : target) {
143 seen[k] = true;
144 frontier.push_back(k);
145 }
146 while (!frontier.empty()) {
147 std::vector<std::size_t> next;
148 for (std::size_t j : frontier)
149 for (std::size_t i = 0; i < n; ++i)
150 if (i != j && !seen[i] && Q(i, j) != T(0)) {
151 seen[i] = true;
152 next.push_back(i);
153 }
154 frontier.swap(next);
155 }
156 std::vector<bool> out(keep.size(), false);
157 for (std::size_t a = 0; a < keep.size(); ++a) out[a] = seen[keep[a]];
158 return out;
159}
160
161} // namespace passage_detail
162
163/**
164 * Phase-type representation of the first passage time from `pi0` into `target`.
165 *
166 * @param pi0 empty selects the conditional stationary law on the complement
167 * @param target 0-based state indices (1-based in the MATLAB reference)
168 */
169template <class T>
170PassagePh<T> ctmc_passage_ph(const Matrix<T>& Q, const std::vector<T>& pi0,
171 const std::vector<std::size_t>& target) {
172 const std::size_t n = Q.rows();
173 if (Q.cols() != n) throw InputError("ctmc_passage_ph: the generator must be square");
174 const std::vector<std::size_t> tgt =
175 passage_detail::unique_target(target, n, "ctmc_passage_ph");
176
177 double scale = 1.0;
178 for (std::size_t i = 0; i < n; ++i)
179 for (std::size_t j = 0; j < n; ++j)
180 scale = std::max(scale, std::abs(num_traits<T>::to_double(Q(i, j))));
181 for (std::size_t i = 0; i < n; ++i) {
182 T rs = T(0);
183 for (std::size_t j = 0; j < n; ++j) rs += Q(i, j);
184 if (std::abs(num_traits<T>::to_double(rs)) > 1e-8 * scale)
185 throw InputError(
186 "ctmc_passage_ph: Q is not an infinitesimal generator: its rows do not sum to "
187 "zero. Pass it through ctmc_makeinfgen first");
188 }
189
190 std::vector<bool> is_target(n, false);
191 for (std::size_t k : tgt) is_target[k] = true;
192 PassagePh<T> out;
193 for (std::size_t i = 0; i < n; ++i)
194 if (!is_target[i]) out.keep.push_back(i);
195
196 const std::size_t nA = out.keep.size();
197 out.S = Matrix<T>(nA, nA);
198 for (std::size_t a = 0; a < nA; ++a)
199 for (std::size_t c = 0; c < nA; ++c) out.S(a, c) = Q(out.keep[a], out.keep[c]);
200 out.s0.assign(nA, T(0));
201 for (std::size_t a = 0; a < nA; ++a) {
202 T r = T(0);
203 for (std::size_t c = 0; c < nA; ++c) r += out.S(a, c);
204 out.s0[a] = -r;
205 }
206
207 out.alpha.assign(nA, T(0));
208 out.atom = T(0);
209 if (pi0.empty()) {
210 // An empty initial law selects the conditional stationary one on the
211 // complement of the target set, the contract of the MATLAB reference
212 // and of the python and JAR api twins.
213 const std::vector<T> p = ctmc_solve(Q);
214 T mass = T(0);
215 for (std::size_t a = 0; a < nA; ++a) mass += p[out.keep[a]];
216 if (!(num_traits<T>::to_double(mass) > 0.0))
217 throw InputError(
218 "ctmc_passage_ph: the stationary law puts no mass outside the target set, so "
219 "there is no passage to time");
220 for (std::size_t a = 0; a < nA; ++a) out.alpha[a] = p[out.keep[a]] / mass;
221 return out;
222 }
223 if (pi0.size() != n)
224 throw InputError(
225 "ctmc_passage_ph: pi0 must be a distribution over the state space, one entry per "
226 "state");
227 for (std::size_t a = 0; a < nA; ++a) out.alpha[a] = pi0[out.keep[a]];
228 for (std::size_t k : tgt) out.atom += pi0[k];
229 return out;
230}
231
232/**
233 * L(s) = alpha (sI-S)^{-1} s0 + atom at the (complex) points `s`. Eqs. 1-2: one
234 * linear system per value of s.
235 *
236 * ONE SOLVE PER s, NOT PER (s,t) PAIR. The saving over a dense matrix
237 * exponential is that the solves are sparse, so this route reaches chains a
238 * dense expm cannot hold. It is NOT a saving in the number of time points:
239 * every Abate-Whitt inverter places its nodes at s = beta/t, so a grid of T
240 * points costs T*|beta| solves.
241 */
242template <class T>
243std::vector<std::complex<double>> ctmc_passage_lst(const Matrix<T>& Q, const std::vector<T>& pi0,
244 const std::vector<std::size_t>& target,
245 const std::vector<std::complex<double>>& s) {
246 const PassagePh<T> ph = ctmc_passage_ph(Q, pi0, target);
247 const std::size_t nA = ph.S.rows();
248 using C = std::complex<double>;
249 std::vector<C> out(s.size(), C(0.0, 0.0));
250 for (std::size_t is = 0; is < s.size(); ++is) {
251 Matrix<C> A(nA, nA);
252 for (std::size_t i = 0; i < nA; ++i)
253 for (std::size_t j = 0; j < nA; ++j)
254 A(i, j) = (i == j ? s[is] : C(0.0, 0.0)) -
255 C(num_traits<T>::to_double(ph.S(i, j)), 0.0);
256 std::vector<C> b(nA);
257 for (std::size_t i = 0; i < nA; ++i) b[i] = C(num_traits<T>::to_double(ph.s0[i]), 0.0);
258 const std::vector<C> x = passage_detail::solve_cplx(A, b, "ctmc_passage_lst");
259 C acc(num_traits<T>::to_double(ph.atom), 0.0);
260 for (std::size_t i = 0; i < nA; ++i)
261 acc += C(num_traits<T>::to_double(ph.alpha[i]), 0.0) * x[i];
262 out[is] = acc;
263 }
264 return out;
265}
266
267/** Per-source and pi0-weighted passage moments. */
268template <class T>
270 Matrix<T> mall; ///< (nstates x nmax), zero on the target, inf where unreachable
271 std::vector<T> m; ///< the pi0-weighted moment vector
272};
273
274/**
275 * Moments of order 1..nmax of the first passage time into `target`.
276 *
277 * This is Eq. 3, -q_ii M_i(n) = sum_{k not in B} q_ik M_k(n) + n M_i(n-1),
278 * i.e. (-S) M(n) = n M(n-1) with M(0) = 1: nmax linear solves and no transform
279 * inversion at all. The equivalent closed form n! alpha (-S)^{-n} 1 is NOT how
280 * it is evaluated -- forming the inverse of the sub-generator destroys the
281 * sparsity the recursion preserves.
282 */
283template <class T>
284PassageMoments<T> ctmc_passage_moments(const Matrix<T>& Q, const std::vector<T>& pi0,
285 const std::vector<std::size_t>& target,
286 std::size_t nmax = 1) {
288 "ctmc_passage_moments marks unreachable states with an infinity and therefore "
289 "requires an arithmetic that has one");
290 if (nmax == 0) throw InputError("ctmc_passage_moments: nmax must be positive");
291 const PassagePh<T> ph = ctmc_passage_ph(Q, pi0, target);
292 const std::size_t n = Q.rows();
293 const std::vector<std::size_t> tgt =
294 passage_detail::unique_target(target, n, "ctmc_passage_moments");
295 const std::size_t nA = ph.keep.size();
296
298 out.mall = Matrix<T>(n, nmax);
299 out.m.assign(nmax, T(0));
300 if (nA == 0) return out;
301
302 // A state that cannot reach the target has an infinite passage time; the
303 // sub-generator is singular on that block, and a solve that ignored this
304 // would return a finite number instead of saying so.
305 const std::vector<bool> reach = passage_detail::reaches_target(Q, ph.keep, tgt);
306 bool all_reach = true;
307 for (bool r : reach) all_reach = all_reach && r;
308
309 Matrix<T> A(nA, nA);
310 for (std::size_t i = 0; i < nA; ++i)
311 for (std::size_t j = 0; j < nA; ++j) A(i, j) = -ph.S(i, j);
312
313 const T inf = num_traits<T>::from_double(std::numeric_limits<double>::infinity());
314 std::vector<T> x(nA, T(1));
315 for (std::size_t k = 1; k <= nmax; ++k) {
316 std::vector<T> rhs(nA);
317 for (std::size_t i = 0; i < nA; ++i) rhs[i] = num_traits<T>::from_double(double(k)) * x[i];
318 if (all_reach) {
319 x = solve(A, rhs);
320 } else {
321 // Restrict to the reachable block: the unreachable rows are exactly
322 // the singular ones, and they are reported as infinite rather than
323 // regularized away.
324 std::vector<std::size_t> idx;
325 for (std::size_t i = 0; i < nA; ++i)
326 if (reach[i]) idx.push_back(i);
327 Matrix<T> Ar(idx.size(), idx.size());
328 std::vector<T> br(idx.size());
329 for (std::size_t a = 0; a < idx.size(); ++a) {
330 for (std::size_t c = 0; c < idx.size(); ++c) Ar(a, c) = A(idx[a], idx[c]);
331 br[a] = rhs[idx[a]];
332 }
333 const std::vector<T> xr = solve(Ar, br);
334 x.assign(nA, inf);
335 for (std::size_t a = 0; a < idx.size(); ++a) x[idx[a]] = xr[a];
336 }
337 for (std::size_t i = 0; i < nA; ++i)
338 out.mall(ph.keep[i], k - 1) = reach[i] ? x[i] : inf;
339 if (!all_reach)
340 for (std::size_t i = 0; i < nA; ++i)
341 if (!reach[i]) x[i] = T(1); // keeps the recursion finite on the reachable block
342 }
343
344 bool unreachable_start = false;
345 for (std::size_t i = 0; i < nA; ++i)
346 if (!reach[i] && num_traits<T>::to_double(ph.alpha[i]) > 0.0) unreachable_start = true;
347 for (std::size_t k = 0; k < nmax; ++k) {
348 if (unreachable_start) {
349 out.m[k] = inf;
350 continue;
351 }
352 T acc = T(0);
353 for (std::size_t i = 0; i < nA; ++i)
354 if (reach[i]) acc += ph.alpha[i] * out.mall(ph.keep[i], k);
355 out.m[k] = acc;
356 }
357 return out;
358}
359
360/**
361 * Mean time to reach any state in `target` from each state of a CTMC.
362 *
363 * Continuous-time twin of `dtmc_hitting_time` and the first-moment special case
364 * of `ctmc_passage_moments`: (-S) h = 1 on the non-target block, where
365 * `dtmc_hitting_time` solves (I - P_NT) h = 1. Unreachable states give infinity.
366 */
367template <class T>
368std::vector<T> ctmc_hitting_time(const Matrix<T>& Q, const std::vector<std::size_t>& target) {
369 const std::size_t n = Q.rows();
370 // mall does not depend on the initial law, so a uniform one is passed
371 // rather than requiring the caller to invent one.
372 std::vector<T> pi0(n, num_traits<T>::from_double(1.0 / double(n)));
373 const PassageMoments<T> pm = ctmc_passage_moments(Q, pi0, target, 1);
374 std::vector<T> h(n);
375 for (std::size_t i = 0; i < n; ++i) h[i] = pm.mall(i, 0);
376 return h;
377}
378
379/** A passage-time law on a grid. */
380template <class T>
382 std::vector<double> t;
383 std::vector<double> F;
384 std::vector<double> f;
385 double atom = 0.0;
386};
387
388/**
389 * CDF and density of the first passage time on the grid `tset`:
390 * F(t) = 1 - alpha exp(St) 1 and f(t) = alpha exp(St) s0.
391 *
392 * `method` is "expm" (default) or "lt". The transform route exists for chains
393 * whose non-target block is too large for a dense exp(St), not because it needs
394 * fewer time points; on a small chain "expm" is both faster and more accurate,
395 * which is why it is the default.
396 */
397template <class T>
398PassageCurve<T> ctmc_passage_time(const Matrix<T>& Q, const std::vector<T>& pi0,
399 const std::vector<std::size_t>& target,
400 const std::vector<double>& tset,
401 const std::string& method = "expm",
402 const std::string& lti_method = "euler") {
404 "ctmc_passage_time takes a matrix exponential and therefore requires an "
405 "arithmetic with transcendental functions");
406 const PassagePh<T> ph = ctmc_passage_ph(Q, pi0, target);
407 const std::size_t nA = ph.S.rows();
408 PassageCurve<T> out;
409 out.t = tset;
410 out.F.assign(tset.size(), 0.0);
411 out.f.assign(tset.size(), 0.0);
413
414 if (method == "expm") {
415 bool uniform = tset.size() > 2;
416 const double dt = tset.size() > 1 ? tset[1] - tset[0] : 0.0;
417 if (uniform && !(dt > 0.0)) uniform = false;
418 for (std::size_t i = 1; uniform && i + 1 < tset.size(); ++i)
419 if (std::abs((tset[i + 1] - tset[i]) - dt) > 1e-12 * std::max(1.0, std::abs(dt)))
420 uniform = false;
421
422 std::vector<double> v(nA, 0.0);
423 Matrix<double> Sd(nA, nA);
424 for (std::size_t i = 0; i < nA; ++i)
425 for (std::size_t j = 0; j < nA; ++j) Sd(i, j) = num_traits<T>::to_double(ph.S(i, j));
426 std::vector<double> s0d(nA), ad(nA);
427 for (std::size_t i = 0; i < nA; ++i) {
428 s0d[i] = num_traits<T>::to_double(ph.s0[i]);
429 ad[i] = num_traits<T>::to_double(ph.alpha[i]);
430 }
431
432 auto step = [&](const Matrix<double>& E, std::vector<double>& w) {
433 std::vector<double> z(nA, 0.0);
434 for (std::size_t j = 0; j < nA; ++j) {
435 double acc = 0.0;
436 for (std::size_t i = 0; i < nA; ++i) acc += w[i] * E(i, j);
437 z[j] = acc;
438 }
439 w.swap(z);
440 };
441
442 if (uniform) {
443 // One exponential, then propagate: recomputing expm(S*t) at every
444 // grid point is the same answer at a cost linear in the grid.
445 Matrix<double> Sdt = Sd;
446 for (std::size_t i = 0; i < nA; ++i)
447 for (std::size_t j = 0; j < nA; ++j) Sdt(i, j) = Sd(i, j) * dt;
448 const Matrix<double> E = expm(Sdt);
449 Matrix<double> S0 = Sd;
450 for (std::size_t i = 0; i < nA; ++i)
451 for (std::size_t j = 0; j < nA; ++j) S0(i, j) = Sd(i, j) * tset[0];
452 const Matrix<double> E0 = expm(S0);
453 v.assign(nA, 0.0);
454 for (std::size_t j = 0; j < nA; ++j) {
455 double acc = 0.0;
456 for (std::size_t i = 0; i < nA; ++i) acc += ad[i] * E0(i, j);
457 v[j] = acc;
458 }
459 for (std::size_t i = 0; i < tset.size(); ++i) {
460 if (i > 0) step(E, v);
461 double sF = 0.0, sf = 0.0;
462 for (std::size_t j = 0; j < nA; ++j) {
463 sF += v[j];
464 sf += v[j] * s0d[j];
465 }
466 out.F[i] = 1.0 - sF;
467 out.f[i] = sf;
468 }
469 } else {
470 for (std::size_t i = 0; i < tset.size(); ++i) {
471 if (tset[i] < 0.0) continue;
472 Matrix<double> St = Sd;
473 for (std::size_t a = 0; a < nA; ++a)
474 for (std::size_t b = 0; b < nA; ++b) St(a, b) = Sd(a, b) * tset[i];
475 const Matrix<double> E = expm(St);
476 double sF = 0.0, sf = 0.0;
477 for (std::size_t j = 0; j < nA; ++j) {
478 double acc = 0.0;
479 for (std::size_t a = 0; a < nA; ++a) acc += ad[a] * E(a, j);
480 sF += acc;
481 sf += acc * s0d[j];
482 }
483 out.F[i] = 1.0 - sF;
484 out.f[i] = sf;
485 }
486 }
487 } else if (method == "lt") {
488 const double atom = out.atom;
489 const lti::LaplaceFn L = [&](std::complex<double> s) {
490 std::vector<std::complex<double>> sv(1, s);
491 return ctmc_passage_lst(Q, pi0, target, sv)[0];
492 };
493 const lti::LaplaceMethod lm = lti::laplace_method(lti_method);
494 out.F = lti::laplace_invert_cdf(L, tset, lm);
495 const lti::LaplaceFn Ld = [&](std::complex<double> s) { return L(s) - atom; };
496 out.f = lti::laplace_invert_pdf(Ld, tset, lm);
497 } else {
498 throw InputError("ctmc_passage_time: unknown method '" + method +
499 "', expected expm or lt");
500 }
501
502 for (std::size_t i = 0; i < out.F.size(); ++i) {
503 out.F[i] = std::min(1.0, std::max(0.0, out.F[i]));
504 out.f[i] = std::max(0.0, out.f[i]);
505 }
506 return out;
507}
508
509// ---------------------------------------------------------------------------
510// Semi-Markov chains (Sec. 3)
511// ---------------------------------------------------------------------------
512
513/**
514 * Moments of the semi-Markov first passage time from the per-state holding
515 * moments m_i(r), Eq. 7 with the u_i(r) recurrence of Eq. 8:
516 *
517 * u_i(r) = -sum_{j=1..r} C(r,j) m_i(j) u_i(r-j), u_i(0) = 1,
518 *
519 * which are the derivatives at the origin of 1/h*_i(s). Cheaper than Eq. 6
520 * because it needs no per-pair moments.
521 *
522 * @param hmom (nstates x nmax): hmom(i,r-1) is the r-th moment of the sojourn
523 * in state i
524 */
525template <class T>
527 const std::vector<T>& pi0,
528 const std::vector<std::size_t>& target,
529 std::size_t nmax = 1) {
530 const std::size_t n = P.rows();
531 if (P.cols() != n)
532 throw InputError("smp_passage_moments: the embedded transition matrix must be square");
533 if (nmax == 0) throw InputError("smp_passage_moments: nmax must be positive");
534 for (std::size_t i = 0; i < n; ++i) {
535 T rs = T(0);
536 for (std::size_t j = 0; j < n; ++j) rs += P(i, j);
537 if (std::abs(num_traits<T>::to_double(rs) - 1.0) > 1e-8)
538 throw InputError(
539 "smp_passage_moments: the embedded transition matrix rows must sum to one");
540 }
541 if (hmom.rows() != n)
542 throw InputError("smp_passage_moments: hmom must carry one row per state");
543 if (hmom.cols() < nmax)
544 throw InputError(
545 "smp_passage_moments: hmom must carry at least nmax holding-time moments per state");
546 const std::vector<std::size_t> tgt =
547 passage_detail::unique_target(target, n, "smp_passage_moments");
548
549 std::vector<bool> is_target(n, false);
550 for (std::size_t k : tgt) is_target[k] = true;
551 std::vector<std::size_t> A;
552 for (std::size_t i = 0; i < n; ++i)
553 if (!is_target[i]) A.push_back(i);
554 const std::size_t nA = A.size();
555
557 out.mall = Matrix<T>(n, nmax);
558 out.m.assign(nmax, T(0));
559 if (nA == 0) return out;
560
561 // Eq. 8, per state.
562 Matrix<T> u(nA, nmax);
563 for (std::size_t r = 1; r <= nmax; ++r) {
564 for (std::size_t a = 0; a < nA; ++a) {
565 T acc = T(0);
566 for (std::size_t j = 1; j <= r; ++j) {
567 const T base = (r - j == 0) ? T(1) : u(a, r - j - 1);
568 double c = 1.0;
569 for (std::size_t q = 0; q < j; ++q)
570 c = c * double(r - q) / double(q + 1);
571 acc += num_traits<T>::from_double(c) * hmom(A[a], j - 1) * base;
572 }
573 u(a, r - 1) = -acc;
574 }
575 }
576
577 Matrix<T> IPAA(nA, nA);
578 for (std::size_t a = 0; a < nA; ++a)
579 for (std::size_t c = 0; c < nA; ++c)
580 IPAA(a, c) = (a == c ? T(1) : T(0)) - P(A[a], A[c]);
581
582 Matrix<T> M(nA, nmax);
583 for (std::size_t q = 1; q <= nmax; ++q) {
584 std::vector<T> b(nA, T(0));
585 for (std::size_t r = 1; r <= q; ++r) {
586 double c = 1.0;
587 for (std::size_t k = 0; k < r; ++k) c = c * double(q - k) / double(k + 1);
588 for (std::size_t a = 0; a < nA; ++a) {
589 const T base = (r < q) ? M(a, q - r - 1) : T(1);
590 b[a] -= num_traits<T>::from_double(c) * u(a, r - 1) * base;
591 }
592 }
593 const std::vector<T> x = solve(IPAA, b);
594 for (std::size_t a = 0; a < nA; ++a) M(a, q - 1) = x[a];
595 }
596
597 for (std::size_t a = 0; a < nA; ++a)
598 for (std::size_t q = 0; q < nmax; ++q) out.mall(A[a], q) = M(a, q);
599 if (pi0.size() == n)
600 for (std::size_t q = 0; q < nmax; ++q) {
601 T acc = T(0);
602 for (std::size_t i = 0; i < n; ++i) acc += pi0[i] * out.mall(i, q);
603 out.m[q] = acc;
604 }
605 return out;
606}
607
608/**
609 * L(s) of the semi-Markov first passage time, Eqs. 4-5:
610 *
611 * L_i(s) = sum_{k not in B} r*_ik(s) L_k(s) + sum_{k in B} r*_ik(s),
612 *
613 * so (I - R*_AA(s)) L_A(s) = R*_AB(s) 1, one linear system per value of s.
614 *
615 * @param hlst per-state sojourn transforms h*_i(s), so r*_ik(s) = P(i,k) h*_i(s)
616 * and the complex numbers stay on the DIAGONAL of the system
617 */
618template <class T>
619std::vector<std::complex<double>> smp_passage_lst(
620 const Matrix<T>& P, const std::vector<std::function<std::complex<double>(std::complex<double>)>>& hlst,
621 const std::vector<T>& pi0, const std::vector<std::size_t>& target,
622 const std::vector<std::complex<double>>& s) {
623 const std::size_t n = P.rows();
624 const std::vector<std::size_t> tgt = passage_detail::unique_target(target, n, "smp_passage_lst");
625 if (hlst.size() != n)
626 throw InputError("smp_passage_lst: hlst must carry one transform per state");
627 std::vector<bool> is_target(n, false);
628 for (std::size_t k : tgt) is_target[k] = true;
629 std::vector<std::size_t> A;
630 for (std::size_t i = 0; i < n; ++i)
631 if (!is_target[i]) A.push_back(i);
632 const std::size_t nA = A.size();
633
634 using C = std::complex<double>;
635 double atom = 0.0;
636 if (pi0.size() == n)
637 for (std::size_t k : tgt) atom += num_traits<T>::to_double(pi0[k]);
638
639 std::vector<C> out(s.size(), C(0.0, 0.0));
640 for (std::size_t is = 0; is < s.size(); ++is) {
641 std::vector<C> h(nA);
642 for (std::size_t a = 0; a < nA; ++a) h[a] = hlst[A[a]](s[is]);
643 Matrix<C> M(nA, nA);
644 std::vector<C> b(nA, C(0.0, 0.0));
645 for (std::size_t a = 0; a < nA; ++a) {
646 for (std::size_t c = 0; c < nA; ++c)
647 M(a, c) = (a == c ? C(1.0, 0.0) : C(0.0, 0.0)) -
648 h[a] * C(num_traits<T>::to_double(P(A[a], A[c])), 0.0);
649 double pb = 0.0;
650 for (std::size_t k : tgt) pb += num_traits<T>::to_double(P(A[a], k));
651 b[a] = h[a] * C(pb, 0.0);
652 }
653 const std::vector<C> x = passage_detail::solve_cplx(M, b, "smp_passage_lst");
654 C acc(atom, 0.0);
655 for (std::size_t a = 0; a < nA; ++a)
656 acc += C(pi0.size() == n ? num_traits<T>::to_double(pi0[A[a]]) : 1.0 / double(n), 0.0) *
657 x[a];
658 out[is] = acc;
659 }
660 return out;
661}
662
663/**
664 * CDF and density of the semi-Markov first passage time, by inverting
665 * `smp_passage_lst` through api/lti.
666 *
667 * There is no matrix-exponential route here: a semi-Markov chain has no
668 * generator to exponentiate, which is exactly the case uniformization does not
669 * reach and the transform does.
670 *
671 * `lti_method` defaults to "euler" RATHER THAN "weeks". Semi-Markov passage
672 * densities are the case Sec. 4.2 singles out as slow-converging for a Laguerre
673 * series, and `laplace_weeks_scaling` then refuses by name rather than
674 * returning noise.
675 */
676template <class T>
678 const Matrix<T>& P, const std::vector<std::function<std::complex<double>(std::complex<double>)>>& hlst,
679 const std::vector<T>& pi0, const std::vector<std::size_t>& target,
680 const std::vector<double>& tset, const std::string& lti_method = "euler") {
681 const std::size_t n = P.rows();
682 const std::vector<std::size_t> tgt =
683 passage_detail::unique_target(target, n, "smp_passage_time");
684 double atom = 0.0;
685 if (pi0.size() == n)
686 for (std::size_t k : tgt) atom += num_traits<T>::to_double(pi0[k]);
687
688 const lti::LaplaceFn L = [&](std::complex<double> s) {
689 std::vector<std::complex<double>> sv(1, s);
690 return smp_passage_lst(P, hlst, pi0, target, sv)[0];
691 };
692 const lti::LaplaceMethod lm = lti::laplace_method(lti_method);
693 PassageCurve<T> out;
694 out.t = tset;
695 out.atom = atom;
696 out.F = lti::laplace_invert_cdf(L, tset, lm);
697 const lti::LaplaceFn Ld = [&](std::complex<double> s) { return L(s) - atom; };
698 out.f = lti::laplace_invert_pdf(Ld, tset, lm);
699 return out;
700}
701
702} // namespace mc
703} // namespace line
704
705#endif // LINE_API_MC_CTMC_PASSAGE_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
Steady-state distribution of a continuous-time Markov chain.
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Numerical inversion of a Laplace transform: Euler, Talbot, Gaver-Stehfest.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
std::vector< double > laplace_invert_cdf(const LaplaceFn &F, const std::vector< double > &t, LaplaceMethod method=LaplaceMethod::Euler, std::size_t n=0)
The DISTRIBUTION on a grid, from the transform of the DENSITY.
std::vector< double > laplace_invert_pdf(const LaplaceFn &F, const std::vector< double > &t, LaplaceMethod method=LaplaceMethod::Euler, std::size_t n=0)
The DENSITY on a grid: the inversion clamped at zero.
LaplaceMethod
The methods laplace_invert accepts.
LaplaceMethod laplace_method(const std::string &s)
Parse the reference's method names, including its two Gaver spellings.
std::function< Cplx(Cplx)> LaplaceFn
The transform, evaluated at complex argument.
std::vector< std::complex< double > > ctmc_passage_lst(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< std::complex< double > > &s)
L(s) = alpha (sI-S)^{-1} s0 + atom at the (complex) points s.
PassageMoments< T > ctmc_passage_moments(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, std::size_t nmax=1)
Moments of order 1..nmax of the first passage time into target.
std::vector< std::complex< double > > smp_passage_lst(const Matrix< T > &P, const std::vector< std::function< std::complex< double >(std::complex< double >)> > &hlst, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< std::complex< double > > &s)
L(s) of the semi-Markov first passage time, Eqs.
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
PassageMoments< T > smp_passage_moments(const Matrix< T > &P, const Matrix< T > &hmom, const std::vector< T > &pi0, const std::vector< std::size_t > &target, std::size_t nmax=1)
Moments of the semi-Markov first passage time from the per-state holding moments m_i(r),...
PassageCurve< T > ctmc_passage_time(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< double > &tset, const std::string &method="expm", const std::string &lti_method="euler")
CDF and density of the first passage time on the grid tset: F(t) = 1 - alpha exp(St) 1 and f(t) = alp...
PassageCurve< T > smp_passage_time(const Matrix< T > &P, const std::vector< std::function< std::complex< double >(std::complex< double >)> > &hlst, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< double > &tset, const std::string &lti_method="euler")
CDF and density of the semi-Markov first passage time, by inverting smp_passage_lst through api/lti.
PassagePh< T > ctmc_passage_ph(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target)
Phase-type representation of the first passage time from pi0 into target.
std::vector< T > ctmc_hitting_time(const Matrix< T > &Q, const std::vector< std::size_t > &target)
Mean time to reach any state in target from each state of a CTMC.
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.
A passage-time law on a grid.
std::vector< double > t
std::vector< double > F
std::vector< double > f
Per-source and pi0-weighted passage moments.
Matrix< T > mall
(nstates x nmax), zero on the target, inf where unreachable
std::vector< T > m
the pi0-weighted moment vector
The phase-type form of a first passage time.
std::vector< T > alpha
pi0 restricted to the non-target block, UNNORMALIZED
std::vector< std::size_t > keep
row of S -> state index of Q
T atom
mass of pi0 already inside the target: F(0)
std::vector< T > s0
exit vector -S*1
Matrix< T > S
sub-generator Q(A,A)