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 <string>
50#include <vector>
51
55#include "line/num/number.h"
56#include "line/util/error.h"
57#include "line/util/expm.h"
58#include "line/util/lu.h"
59#include "line/util/matrix.h"
60
61namespace line {
62namespace mc {
63
64/** The phase-type form of a first passage time. */
65template <class T>
66struct PassagePh {
67 std::vector<T> alpha; ///< pi0 restricted to the non-target block, UNNORMALIZED
68 Matrix<T> S; ///< sub-generator Q(A,A)
69 std::vector<T> s0; ///< exit vector -S*1
70 std::vector<std::size_t> keep; ///< row of S -> state index of Q
71 T atom; ///< mass of pi0 already inside the target: F(0)
72};
73
74/** Moments of a first passage time, `[Mall, m]` of the reference. */
75template <class T>
77 Matrix<T> mall; ///< (nstates x nmax), zero on the target, inf where unreachable
78 std::vector<T> m; ///< the pi0-weighted moment vector
79};
80
81namespace passage_detail {
82
83inline std::vector<std::size_t> unique_target(const std::vector<std::size_t>& target,
84 std::size_t n, const char* fn) {
85 std::vector<std::size_t> t = target;
86 std::sort(t.begin(), t.end());
87 t.erase(std::unique(t.begin(), t.end()), t.end());
88 if (t.empty())
89 throw InputError(std::string(fn) +
90 ": the target state set is empty: a first passage time into no state "
91 "is undefined");
92 if (t.back() >= n)
93 throw InputError(std::string(fn) + ": a target state index is outside the state space");
94 return t;
95}
96
97/**
98 * Gaussian elimination with partial pivoting for a COMPLEX system.
99 *
100 * `line::solve` cannot serve here: its pivot search compares magnitudes with
101 * `operator>`, which std::complex does not have, so instantiating it on
102 * complex is a compile error rather than a silent wrong answer. The transform
103 * routes need complex coefficients, so they carry their own solve.
104 */
105inline std::vector<std::complex<double>> solve_cplx(Matrix<std::complex<double>> A,
106 std::vector<std::complex<double>> b,
107 const char* fn) {
108 using C = std::complex<double>;
109 const std::size_t n = A.rows();
110 if (A.cols() != n || b.size() != n)
111 throw InputError(std::string(fn) + ": complex system is not square");
112 for (std::size_t k = 0; k < n; ++k) {
113 std::size_t piv = k;
114 double best = std::abs(A(k, k));
115 for (std::size_t i = k + 1; i < n; ++i) {
116 const double m = std::abs(A(i, k));
117 if (m > best) {
118 best = m;
119 piv = i;
120 }
121 }
122 if (!(best > 0.0))
123 throw NumericError(ErrorKind{"Singular"}, std::string(fn) + ": singular transform matrix");
124 if (piv != k) {
125 for (std::size_t j = 0; j < n; ++j) std::swap(A(k, j), A(piv, j));
126 std::swap(b[k], b[piv]);
127 }
128 for (std::size_t i = k + 1; i < n; ++i) {
129 const C f = A(i, k) / A(k, k);
130 if (f == C(0.0, 0.0)) continue;
131 for (std::size_t j = k; j < n; ++j) A(i, j) -= f * A(k, j);
132 b[i] -= f * b[k];
133 }
134 }
135 std::vector<C> x(n, C(0.0, 0.0));
136 for (std::size_t i = n; i-- > 0;) {
137 C s = b[i];
138 for (std::size_t j = i + 1; j < n; ++j) s -= A(i, j) * x[j];
139 x[i] = s / A(i, i);
140 }
141 return x;
142}
143
144/**
145 * The reference's `m = pi0 * Mall` of smp_passage_moments: empty when pi0 is
146 * not given, refused when it is not a law over the state space.
147 */
148template <class T>
149void smp_weighted_moments(PassageMoments<T>& out, const std::vector<T>& pi0, std::size_t n,
150 std::size_t nmax, const char* fn) {
151 out.m.clear();
152 if (pi0.empty()) return;
153 if (pi0.size() != n)
154 throw InputError(std::string(fn) +
155 ": PI0 must be a distribution over the state space, one entry per state");
156 out.m.assign(nmax, T(0));
157 for (std::size_t q = 0; q < nmax; ++q) {
158 T acc = T(0);
159 for (std::size_t i = 0; i < n; ++i) acc += pi0[i] * out.mall(i, q);
160 out.m[q] = acc;
161 }
162}
163
164/** Backward reachability closure over the transition graph. */
165template <class T>
166std::vector<bool> reaches_target(const Matrix<T>& Q, const std::vector<std::size_t>& keep,
167 const std::vector<std::size_t>& target) {
168 const std::size_t n = Q.rows();
169 std::vector<bool> seen(n, false);
170 std::vector<std::size_t> frontier;
171 for (std::size_t k : target) {
172 seen[k] = true;
173 frontier.push_back(k);
174 }
175 while (!frontier.empty()) {
176 std::vector<std::size_t> next;
177 for (std::size_t j : frontier)
178 for (std::size_t i = 0; i < n; ++i)
179 if (i != j && !seen[i] && Q(i, j) != T(0)) {
180 seen[i] = true;
181 next.push_back(i);
182 }
183 frontier.swap(next);
184 }
185 std::vector<bool> out(keep.size(), false);
186 for (std::size_t a = 0; a < keep.size(); ++a) out[a] = seen[keep[a]];
187 return out;
188}
189
190} // namespace passage_detail
191
192/**
193 * Phase-type representation of the first passage time from `pi0` into `target`.
194 *
195 * @param pi0 empty selects the conditional stationary law on the complement
196 * @param target 0-based state indices (1-based in the MATLAB reference)
197 */
198template <class T>
199PassagePh<T> ctmc_passage_ph(const Matrix<T>& Q, const std::vector<T>& pi0,
200 const std::vector<std::size_t>& target) {
201 const std::size_t n = Q.rows();
202 if (Q.cols() != n) throw InputError("ctmc_passage_ph: the generator must be square");
203 const std::vector<std::size_t> tgt =
204 passage_detail::unique_target(target, n, "ctmc_passage_ph");
205
206 double scale = 1.0;
207 for (std::size_t i = 0; i < n; ++i)
208 for (std::size_t j = 0; j < n; ++j)
209 scale = std::max(scale, std::abs(num_traits<T>::to_double(Q(i, j))));
210 for (std::size_t i = 0; i < n; ++i) {
211 T rs = T(0);
212 for (std::size_t j = 0; j < n; ++j) rs += Q(i, j);
213 if (std::abs(num_traits<T>::to_double(rs)) > 1e-8 * scale)
214 throw InputError(
215 "ctmc_passage_ph: Q is not an infinitesimal generator: its rows do not sum to "
216 "zero. Pass it through ctmc_makeinfgen first");
217 }
218
219 std::vector<bool> is_target(n, false);
220 for (std::size_t k : tgt) is_target[k] = true;
221 PassagePh<T> out;
222 for (std::size_t i = 0; i < n; ++i)
223 if (!is_target[i]) out.keep.push_back(i);
224
225 const std::size_t nA = out.keep.size();
226 out.S = Matrix<T>(nA, nA);
227 for (std::size_t a = 0; a < nA; ++a)
228 for (std::size_t c = 0; c < nA; ++c) out.S(a, c) = Q(out.keep[a], out.keep[c]);
229 out.s0.assign(nA, T(0));
230 for (std::size_t a = 0; a < nA; ++a) {
231 T r = T(0);
232 for (std::size_t c = 0; c < nA; ++c) r += out.S(a, c);
233 out.s0[a] = -r;
234 }
235
236 out.alpha.assign(nA, T(0));
237 out.atom = T(0);
238 if (pi0.empty()) {
239 // An empty initial law selects the conditional stationary one on the
240 // complement of the target set, the contract of the MATLAB reference
241 // and of the python and JAR api twins.
242 const std::vector<T> p = ctmc_solve(Q);
243 T mass = T(0);
244 for (std::size_t a = 0; a < nA; ++a) mass += p[out.keep[a]];
245 if (!(num_traits<T>::to_double(mass) > 0.0))
246 throw InputError(
247 "ctmc_passage_ph: the stationary law puts no mass outside the target set, so "
248 "there is no passage to time");
249 for (std::size_t a = 0; a < nA; ++a) out.alpha[a] = p[out.keep[a]] / mass;
250 return out;
251 }
252 if (pi0.size() != n)
253 throw InputError(
254 "ctmc_passage_ph: pi0 must be a distribution over the state space, one entry per "
255 "state");
256 for (std::size_t a = 0; a < nA; ++a) out.alpha[a] = pi0[out.keep[a]];
257 for (std::size_t k : tgt) out.atom += pi0[k];
258 return out;
259}
260
261/**
262 * L(s) = alpha (sI-S)^{-1} s0 + atom at the (complex) points `s`. Eqs. 1-2: one
263 * linear system per value of s.
264 *
265 * ONE SOLVE PER s, NOT PER (s,t) PAIR. The saving over a dense matrix
266 * exponential is that the solves are sparse, so this route reaches chains a
267 * dense expm cannot hold. It is NOT a saving in the number of time points:
268 * every Abate-Whitt inverter places its nodes at s = beta/t, so a grid of T
269 * points costs T*|beta| solves.
270 */
271template <class T>
272std::vector<std::complex<double>> ctmc_passage_lst(const Matrix<T>& Q, const std::vector<T>& pi0,
273 const std::vector<std::size_t>& target,
274 const std::vector<std::complex<double>>& s) {
275 const PassagePh<T> ph = ctmc_passage_ph(Q, pi0, target);
276 const std::size_t nA = ph.S.rows();
277 using C = std::complex<double>;
278 std::vector<C> out(s.size(), C(0.0, 0.0));
279 for (std::size_t is = 0; is < s.size(); ++is) {
280 Matrix<C> A(nA, nA);
281 for (std::size_t i = 0; i < nA; ++i)
282 for (std::size_t j = 0; j < nA; ++j)
283 A(i, j) = (i == j ? s[is] : C(0.0, 0.0)) -
284 C(num_traits<T>::to_double(ph.S(i, j)), 0.0);
285 std::vector<C> b(nA);
286 for (std::size_t i = 0; i < nA; ++i) b[i] = C(num_traits<T>::to_double(ph.s0[i]), 0.0);
287 const std::vector<C> x = passage_detail::solve_cplx(A, b, "ctmc_passage_lst");
288 C acc(num_traits<T>::to_double(ph.atom), 0.0);
289 for (std::size_t i = 0; i < nA; ++i)
290 acc += C(num_traits<T>::to_double(ph.alpha[i]), 0.0) * x[i];
291 out[is] = acc;
292 }
293 return out;
294}
295
296/**
297 * Moments of order 1..nmax of the first passage time into `target`.
298 *
299 * This is Eq. 3, -q_ii M_i(n) = sum_{k not in B} q_ik M_k(n) + n M_i(n-1),
300 * i.e. (-S) M(n) = n M(n-1) with M(0) = 1: nmax linear solves and no transform
301 * inversion at all. The equivalent closed form n! alpha (-S)^{-n} 1 is NOT how
302 * it is evaluated -- forming the inverse of the sub-generator destroys the
303 * sparsity the recursion preserves.
304 */
305template <class T>
306PassageMoments<T> ctmc_passage_moments(const Matrix<T>& Q, const std::vector<T>& pi0,
307 const std::vector<std::size_t>& target,
308 std::size_t nmax = 1) {
310 "ctmc_passage_moments marks unreachable states with an infinity and therefore "
311 "requires an arithmetic that has one");
312 if (nmax == 0) throw InputError("ctmc_passage_moments: nmax must be positive");
313 const PassagePh<T> ph = ctmc_passage_ph(Q, pi0, target);
314 const std::size_t n = Q.rows();
315 const std::vector<std::size_t> tgt =
316 passage_detail::unique_target(target, n, "ctmc_passage_moments");
317 const std::size_t nA = ph.keep.size();
318
320 out.mall = Matrix<T>(n, nmax);
321 out.m.assign(nmax, T(0));
322 if (nA == 0) return out;
323
324 // A state that cannot reach the target has an infinite passage time; the
325 // sub-generator is singular on that block, and a solve that ignored this
326 // would return a finite number instead of saying so.
327 const std::vector<bool> reach = passage_detail::reaches_target(Q, ph.keep, tgt);
328 bool all_reach = true;
329 for (bool r : reach) all_reach = all_reach && r;
330
331 Matrix<T> A(nA, nA);
332 for (std::size_t i = 0; i < nA; ++i)
333 for (std::size_t j = 0; j < nA; ++j) A(i, j) = -ph.S(i, j);
334
335 const T inf = num_traits<T>::from_double(std::numeric_limits<double>::infinity());
336 std::vector<T> x(nA, T(1));
337 for (std::size_t k = 1; k <= nmax; ++k) {
338 std::vector<T> rhs(nA);
339 for (std::size_t i = 0; i < nA; ++i) rhs[i] = num_traits<T>::from_double(double(k)) * x[i];
340 if (all_reach) {
341 x = solve(A, rhs);
342 } else {
343 // Restrict to the reachable block: the unreachable rows are exactly
344 // the singular ones, and they are reported as infinite rather than
345 // regularized away.
346 std::vector<std::size_t> idx;
347 for (std::size_t i = 0; i < nA; ++i)
348 if (reach[i]) idx.push_back(i);
349 Matrix<T> Ar(idx.size(), idx.size());
350 std::vector<T> br(idx.size());
351 for (std::size_t a = 0; a < idx.size(); ++a) {
352 for (std::size_t c = 0; c < idx.size(); ++c) Ar(a, c) = A(idx[a], idx[c]);
353 br[a] = rhs[idx[a]];
354 }
355 const std::vector<T> xr = solve(Ar, br);
356 x.assign(nA, inf);
357 for (std::size_t a = 0; a < idx.size(); ++a) x[idx[a]] = xr[a];
358 }
359 for (std::size_t i = 0; i < nA; ++i)
360 out.mall(ph.keep[i], k - 1) = reach[i] ? x[i] : inf;
361 if (!all_reach)
362 for (std::size_t i = 0; i < nA; ++i)
363 if (!reach[i]) x[i] = T(1); // keeps the recursion finite on the reachable block
364 }
365
366 bool unreachable_start = false;
367 for (std::size_t i = 0; i < nA; ++i)
368 if (!reach[i] && num_traits<T>::to_double(ph.alpha[i]) > 0.0) unreachable_start = true;
369 for (std::size_t k = 0; k < nmax; ++k) {
370 if (unreachable_start) {
371 out.m[k] = inf;
372 continue;
373 }
374 T acc = T(0);
375 for (std::size_t i = 0; i < nA; ++i)
376 if (reach[i]) acc += ph.alpha[i] * out.mall(ph.keep[i], k);
377 out.m[k] = acc;
378 }
379 return out;
380}
381
382/**
383 * Mean time to reach any state in `target` from each state of a CTMC.
384 *
385 * Continuous-time twin of `dtmc_hitting_time` and the first-moment special case
386 * of `ctmc_passage_moments`: (-S) h = 1 on the non-target block, where
387 * `dtmc_hitting_time` solves (I - P_NT) h = 1. Unreachable states give infinity.
388 */
389template <class T>
390std::vector<T> ctmc_hitting_time(const Matrix<T>& Q, const std::vector<std::size_t>& target) {
391 const std::size_t n = Q.rows();
392 // mall does not depend on the initial law, so a uniform one is passed
393 // rather than requiring the caller to invent one.
394 std::vector<T> pi0(n, num_traits<T>::from_double(1.0 / double(n)));
395 const PassageMoments<T> pm = ctmc_passage_moments(Q, pi0, target, 1);
396 std::vector<T> h(n);
397 for (std::size_t i = 0; i < n; ++i) h[i] = pm.mall(i, 0);
398 return h;
399}
400
401/** A passage-time law on a grid. */
402template <class T>
404 std::vector<double> t;
405 std::vector<double> F;
406 std::vector<double> f;
407 double atom = 0.0;
408};
409
410/**
411 * CDF and density of the first passage time on the grid `tset`:
412 * F(t) = 1 - alpha exp(St) 1 and f(t) = alpha exp(St) s0.
413 *
414 * `method` is "expm" (default) or "lt". The transform route exists for chains
415 * whose non-target block is too large for a dense exp(St), not because it needs
416 * fewer time points; on a small chain "expm" is both faster and more accurate,
417 * which is why it is the default.
418 */
419template <class T>
420PassageCurve<T> ctmc_passage_time(const Matrix<T>& Q, const std::vector<T>& pi0,
421 const std::vector<std::size_t>& target,
422 const std::vector<double>& tset,
423 const std::string& method = "expm",
424 const std::string& lti_method = "euler") {
426 "ctmc_passage_time takes a matrix exponential and therefore requires an "
427 "arithmetic with transcendental functions");
428 const PassagePh<T> ph = ctmc_passage_ph(Q, pi0, target);
429 const std::size_t nA = ph.S.rows();
430 PassageCurve<T> out;
431 out.t = tset;
432 out.F.assign(tset.size(), 0.0);
433 out.f.assign(tset.size(), 0.0);
435
436 if (method == "expm") {
437 bool uniform = tset.size() > 2;
438 const double dt = tset.size() > 1 ? tset[1] - tset[0] : 0.0;
439 if (uniform && !(dt > 0.0)) uniform = false;
440 for (std::size_t i = 1; uniform && i + 1 < tset.size(); ++i)
441 if (std::abs((tset[i + 1] - tset[i]) - dt) > 1e-12 * std::max(1.0, std::abs(dt)))
442 uniform = false;
443
444 std::vector<double> v(nA, 0.0);
445 Matrix<double> Sd(nA, nA);
446 for (std::size_t i = 0; i < nA; ++i)
447 for (std::size_t j = 0; j < nA; ++j) Sd(i, j) = num_traits<T>::to_double(ph.S(i, j));
448 std::vector<double> s0d(nA), ad(nA);
449 for (std::size_t i = 0; i < nA; ++i) {
450 s0d[i] = num_traits<T>::to_double(ph.s0[i]);
451 ad[i] = num_traits<T>::to_double(ph.alpha[i]);
452 }
453
454 auto step = [&](const Matrix<double>& E, std::vector<double>& w) {
455 std::vector<double> z(nA, 0.0);
456 for (std::size_t j = 0; j < nA; ++j) {
457 double acc = 0.0;
458 for (std::size_t i = 0; i < nA; ++i) acc += w[i] * E(i, j);
459 z[j] = acc;
460 }
461 w.swap(z);
462 };
463
464 if (uniform) {
465 // One exponential, then propagate: recomputing expm(S*t) at every
466 // grid point is the same answer at a cost linear in the grid.
467 Matrix<double> Sdt = Sd;
468 for (std::size_t i = 0; i < nA; ++i)
469 for (std::size_t j = 0; j < nA; ++j) Sdt(i, j) = Sd(i, j) * dt;
470 const Matrix<double> E = expm(Sdt);
471 Matrix<double> S0 = Sd;
472 for (std::size_t i = 0; i < nA; ++i)
473 for (std::size_t j = 0; j < nA; ++j) S0(i, j) = Sd(i, j) * tset[0];
474 const Matrix<double> E0 = expm(S0);
475 v.assign(nA, 0.0);
476 for (std::size_t j = 0; j < nA; ++j) {
477 double acc = 0.0;
478 for (std::size_t i = 0; i < nA; ++i) acc += ad[i] * E0(i, j);
479 v[j] = acc;
480 }
481 for (std::size_t i = 0; i < tset.size(); ++i) {
482 if (i > 0) step(E, v);
483 double sF = 0.0, sf = 0.0;
484 for (std::size_t j = 0; j < nA; ++j) {
485 sF += v[j];
486 sf += v[j] * s0d[j];
487 }
488 out.F[i] = 1.0 - sF;
489 out.f[i] = sf;
490 }
491 } else {
492 for (std::size_t i = 0; i < tset.size(); ++i) {
493 if (tset[i] < 0.0) continue;
494 Matrix<double> St = Sd;
495 for (std::size_t a = 0; a < nA; ++a)
496 for (std::size_t b = 0; b < nA; ++b) St(a, b) = Sd(a, b) * tset[i];
497 const Matrix<double> E = expm(St);
498 double sF = 0.0, sf = 0.0;
499 for (std::size_t j = 0; j < nA; ++j) {
500 double acc = 0.0;
501 for (std::size_t a = 0; a < nA; ++a) acc += ad[a] * E(a, j);
502 sF += acc;
503 sf += acc * s0d[j];
504 }
505 out.F[i] = 1.0 - sF;
506 out.f[i] = sf;
507 }
508 }
509 } else if (method == "lt") {
510 const double atom = out.atom;
511 const lti::LaplaceFn L = [&](std::complex<double> s) {
512 std::vector<std::complex<double>> sv(1, s);
513 return ctmc_passage_lst(Q, pi0, target, sv)[0];
514 };
515 const lti::LaplaceMethod lm = lti::laplace_method(lti_method);
516 out.F = lti::laplace_invert_cdf(L, tset, lm);
517 const lti::LaplaceFn Ld = [&](std::complex<double> s) { return L(s) - atom; };
518 out.f = lti::laplace_invert_pdf(Ld, tset, lm);
519 } else {
520 throw InputError("ctmc_passage_time: unknown method '" + method +
521 "', expected expm or lt");
522 }
523
524 for (std::size_t i = 0; i < out.F.size(); ++i) {
525 out.F[i] = std::min(1.0, std::max(0.0, out.F[i]));
526 out.f[i] = std::max(0.0, out.f[i]);
527 }
528 return out;
529}
530
531// ---------------------------------------------------------------------------
532// First passage under a taboo set
533// ---------------------------------------------------------------------------
534
535/**
536 * First passage into a target set B under a TABOO set C, `[F, f, out]` of
537 * `ctmc_passage_taboo.m`: the passage completes by entering B and is lost for
538 * good by entering C first.
539 *
540 * The chain is cut into four parts: B and C, made absorbing; N, the states that
541 * can reach neither, also absorbing since they can only hold the passage up
542 * forever; and the transient states K, on which S = Q(K,K) is nonsingular by
543 * construction. A state in both B and C counts as B.
544 */
545template <class T>
547 std::vector<double> t; ///< the grid the curve was asked on
548 std::vector<double> F; ///< DEFECTIVE CDF Pr{B entered by t, C not before}, clamped to [0, 1]
549 std::vector<double> f; ///< its density alpha exp(St) sB
550 T atom; ///< mass of pi0 already in B, i.e. F(0)
551 T reachprob; ///< Pr{B entered before C}, the limit of F
552 T tabooprob; ///< Pr{C entered before B}
553 std::vector<T> pB; ///< (nstates) reachprob per starting state, one on B
554 std::vector<T> pC; ///< (nstates) tabooprob per starting state, one on C
555 std::vector<T> alpha; ///< pi0 restricted to K, UNNORMALIZED
556 Matrix<T> S; ///< Q(K,K)
557 std::vector<T> sB; ///< Q(K,B)*1, the exit rates into the target
558 std::vector<T> sC; ///< Q(K,C)*1, the exit rates into the taboo set
559 std::vector<std::size_t> keep; ///< row of S -> state index of Q (the set K)
560 std::vector<std::size_t> target; ///< B, sorted and unique
561 std::vector<std::size_t> taboo; ///< C minus B, sorted and unique
562 std::string method;
563};
564
565/** Conditional moments under a taboo set, `[mall, m, pB]` of `ctmc_passage_taboo_moments.m`. */
566template <class T>
568 Matrix<T> mall; ///< (nstates x nmax): zero on B, NaN where B cannot be entered first
569 std::vector<T> m; ///< the pi0-weighted conditional moments, NaN if B is never entered first
570 std::vector<T> pB; ///< (nstates) probability of entering B before C
571};
572
573namespace passage_detail {
574
575/**
576 * States with a path into `from` that enters no `stop` state before its last
577 * step. A stop state is absorbing for the passage, so it is never a predecessor.
578 */
579template <class T>
580std::vector<bool> backward_closure(const Matrix<T>& Q, const std::vector<bool>& from,
581 const std::vector<bool>& stop) {
582 const std::size_t n = Q.rows();
583 std::vector<bool> seen = from;
584 std::vector<std::size_t> frontier;
585 for (std::size_t i = 0; i < n; ++i)
586 if (from[i]) frontier.push_back(i);
587 while (!frontier.empty()) {
588 std::vector<std::size_t> next;
589 for (std::size_t j : frontier)
590 for (std::size_t i = 0; i < n; ++i)
591 if (i != j && !seen[i] && !stop[i] && Q(i, j) != T(0)) {
592 seen[i] = true;
593 next.push_back(i);
594 }
595 frontier.swap(next);
596 }
597 return seen;
598}
599
600/** The partition, the sub-generator and the ultimate probabilities: everything but the curve. */
601template <class T>
602PassageTaboo<T> taboo_partition(const Matrix<T>& Q, const std::vector<T>& pi0,
603 const std::vector<std::size_t>& target,
604 const std::vector<std::size_t>& taboo, const char* fn) {
605 const std::size_t n = Q.rows();
606 if (Q.cols() != n) throw InputError(std::string(fn) + ": the generator must be square");
607 PassageTaboo<T> out;
608 out.target = unique_target(target, n, fn);
609 std::vector<std::size_t> tb = taboo;
610 std::sort(tb.begin(), tb.end());
611 tb.erase(std::unique(tb.begin(), tb.end()), tb.end());
612 if (!tb.empty() && tb.back() >= n)
613 throw InputError(std::string(fn) + ": a target or taboo state index is outside the state space");
614
615 double scale = 1.0;
616 for (std::size_t i = 0; i < n; ++i)
617 for (std::size_t j = 0; j < n; ++j)
618 scale = std::max(scale, std::abs(num_traits<T>::to_double(Q(i, j))));
619 for (std::size_t i = 0; i < n; ++i) {
620 T rs = T(0);
621 for (std::size_t j = 0; j < n; ++j) rs += Q(i, j);
622 if (std::abs(num_traits<T>::to_double(rs)) > 1e-8 * scale)
623 throw InputError(std::string(fn) +
624 ": Q is not an infinitesimal generator: its rows do not sum to zero. "
625 "Pass it through ctmc_makeinfgen first");
626 }
627
628 std::vector<bool> isB(n, false), isC(n, false);
629 for (std::size_t k : out.target) isB[k] = true;
630 for (std::size_t k : tb)
631 if (!isB[k]) {
632 isC[k] = true;
633 out.taboo.push_back(k);
634 }
635
636 std::vector<T> p0;
637 if (pi0.empty()) {
638 // The conditional stationary law on the complement of B, as in ctmc_passage_ph.
639 p0 = ctmc_solve(Q);
640 T mass = T(0);
641 for (std::size_t i = 0; i < n; ++i) {
642 if (isB[i]) p0[i] = T(0);
643 mass += p0[i];
644 }
645 if (!(num_traits<T>::to_double(mass) > 0.0))
646 throw InputError(std::string(fn) +
647 ": the stationary law puts no mass outside the target set, so there "
648 "is no passage to time");
649 for (std::size_t i = 0; i < n; ++i) p0[i] = T(p0[i] / mass);
650 } else {
651 if (pi0.size() != n)
652 throw InputError(std::string(fn) +
653 ": PI0 must be a distribution over the state space, one entry per "
654 "state");
655 p0 = pi0;
656 }
657
658 std::vector<bool> isBC(n, false);
659 for (std::size_t i = 0; i < n; ++i) isBC[i] = isB[i] || isC[i];
660 const std::vector<bool> toB = backward_closure(Q, isB, isBC);
661 const std::vector<bool> toBC = backward_closure(Q, isBC, isBC);
662
663 std::vector<std::size_t> absorb, Nset;
664 for (std::size_t i = 0; i < n; ++i)
665 if (isB[i]) absorb.push_back(i);
666 for (std::size_t i = 0; i < n; ++i)
667 if (isC[i]) absorb.push_back(i);
668 for (std::size_t i = 0; i < n; ++i) {
669 if (!toBC[i]) Nset.push_back(i);
670 else if (!isBC[i]) out.keep.push_back(i);
671 }
672 absorb.insert(absorb.end(), Nset.begin(), Nset.end());
673 const std::size_t nB = out.target.size(), nC = out.taboo.size(), nK = out.keep.size();
674
675 out.pB.assign(n, T(0));
676 out.pC.assign(n, T(0));
677 for (std::size_t i = 0; i < n; ++i) {
678 if (isB[i]) out.pB[i] = T(1);
679 if (isC[i]) out.pC[i] = T(1);
680 }
681 out.S = Matrix<T>(nK, nK);
682 if (nK > 0) {
683 // The ultimate probabilities are the exit-probability matrix of the
684 // stochastic complement onto B, C and N, with every non-K row zeroed so
685 // that the three sets are absorbing.
686 Matrix<T> Qabs = Q;
687 std::vector<bool> isK(n, false);
688 for (std::size_t k : out.keep) isK[k] = true;
689 for (std::size_t i = 0; i < n; ++i)
690 if (!isK[i])
691 for (std::size_t j = 0; j < n; ++j) Qabs(i, j) = T(0);
692 const StochCompResult<T> sc = ctmc_stochcomp(Qabs, absorb);
693 out.S = sc.Q22;
694 for (std::size_t a = 0; a < nK; ++a) {
695 T b = T(0), c = T(0);
696 for (std::size_t j = 0; j < nB; ++j) b += sc.X(a, j);
697 for (std::size_t j = 0; j < nC; ++j) c += sc.X(a, nB + j);
698 if (toB[out.keep[a]]) out.pB[out.keep[a]] = b;
699 out.pC[out.keep[a]] = c;
700 }
701 }
702
703 out.alpha.assign(nK, T(0));
704 out.sB.assign(nK, T(0));
705 out.sC.assign(nK, T(0));
706 for (std::size_t a = 0; a < nK; ++a) {
707 const std::size_t i = out.keep[a];
708 out.alpha[a] = p0[i];
709 for (std::size_t j : out.target) out.sB[a] += Q(i, j);
710 for (std::size_t j : out.taboo) out.sC[a] += Q(i, j);
711 }
712 out.atom = T(0);
713 for (std::size_t j : out.target) out.atom += p0[j];
714 out.reachprob = T(0);
715 out.tabooprob = T(0);
716 for (std::size_t i = 0; i < n; ++i) {
717 out.reachprob += T(p0[i] * out.pB[i]);
718 out.tabooprob += T(p0[i] * out.pC[i]);
719 }
720 return out;
721}
722
723} // namespace passage_detail
724
725/**
726 * First passage into `target` under the taboo set `taboo`, the reference's
727 * `ctmc_passage_taboo`. F is the DEFECTIVE distribution
728 *
729 * F(t) = Pr{ B is entered by time t, and C is not entered before B }
730 * = atom + alpha (-S)^{-1} (I - exp(St)) sB
731 *
732 * on the grid `tset`, and f its density alpha exp(St) sB; F tends to
733 * `reachprob` < 1 rather than to one. In temporal-logic terms this is the
734 * time-bounded until Pr{ not C U<=t B }.
735 *
736 * The ultimate probabilities come from the exit-probability matrix X of
737 * `ctmc_stochcomp`, so they are one linear solve and need no time grid. An empty
738 * `tset` computes only those; an infinite entry returns `reachprob` and a
739 * negative one zero. With an empty `taboo`, F equals the CDF of
740 * `ctmc_passage_time`.
741 *
742 * @param pi0 empty selects the conditional stationary law on the complement of B
743 * @param target 0-based state indices of B (1-based in the MATLAB reference)
744 * @param taboo 0-based state indices of C; a state also in B counts as B
745 * @param method "expm" (default) or "lt", as in `ctmc_passage_time`
746 *
747 * Reference: C. Baier, B. Haverkort, H. Hermanns, J.-P. Katoen, "Model-checking
748 * algorithms for continuous-time Markov chains", IEEE TSE 29(6), 2003.
749 */
750template <class T>
751PassageTaboo<T> ctmc_passage_taboo(const Matrix<T>& Q, const std::vector<T>& pi0,
752 const std::vector<std::size_t>& target,
753 const std::vector<std::size_t>& taboo,
754 const std::vector<double>& tset = std::vector<double>(),
755 const std::string& method = "expm",
756 const std::string& lti_method = "euler") {
758 "ctmc_passage_taboo takes a matrix exponential and therefore requires an "
759 "arithmetic with transcendental functions");
760 PassageTaboo<T> out =
761 passage_detail::taboo_partition(Q, pi0, target, taboo, "ctmc_passage_taboo");
762 out.method = method;
763 out.t = tset;
764 out.F.assign(tset.size(), 0.0);
765 out.f.assign(tset.size(), 0.0);
766 if (tset.empty()) return out;
767
768 const double inf = std::numeric_limits<double>::infinity();
769 const double atom = num_traits<T>::to_double(out.atom);
770 std::vector<std::size_t> pos; // finite, nonnegative entries
771 for (std::size_t i = 0; i < tset.size(); ++i) {
772 if (tset[i] == inf) out.F[i] = num_traits<T>::to_double(out.reachprob);
773 else if (std::isfinite(tset[i]) && tset[i] >= 0.0) pos.push_back(i);
774 }
775 std::vector<double> tf(pos.size());
776 for (std::size_t i = 0; i < pos.size(); ++i) tf[i] = tset[pos[i]];
777 std::vector<double> Ff(tf.size(), atom), ff(tf.size(), 0.0);
778
779 const std::size_t nK = out.keep.size();
780 if (nK > 0 && !tf.empty()) {
781 std::vector<double> ad(nK), sBd(nK);
782 for (std::size_t a = 0; a < nK; ++a) {
783 ad[a] = num_traits<T>::to_double(out.alpha[a]);
784 sBd[a] = num_traits<T>::to_double(out.sB[a]);
785 }
786 if (method == "expm") {
787 // One absorbing column for B appended to S: its mass at time t is the
788 // probability of having entered B by t without leaving K elsewhere first.
789 const std::size_t m = nK + 1;
790 Matrix<double> G(m, m, 0.0);
791 for (std::size_t a = 0; a < nK; ++a) {
792 for (std::size_t c = 0; c < nK; ++c) G(a, c) = num_traits<T>::to_double(out.S(a, c));
793 G(a, nK) = sBd[a];
794 }
795 auto scaled = [&](double s) {
796 Matrix<double> Gs(m, m);
797 for (std::size_t a = 0; a < m; ++a)
798 for (std::size_t c = 0; c < m; ++c) Gs(a, c) = G(a, c) * s;
799 return expm(Gs);
800 };
801 auto times = [&](const std::vector<double>& w, const Matrix<double>& E) {
802 std::vector<double> z(m, 0.0);
803 for (std::size_t c = 0; c < m; ++c) {
804 double acc = 0.0;
805 for (std::size_t a = 0; a < m; ++a) acc += w[a] * E(a, c);
806 z[c] = acc;
807 }
808 return z;
809 };
810 auto record = [&](std::size_t i, const std::vector<double>& v) {
811 Ff[i] = atom + v[nK];
812 double acc = 0.0;
813 for (std::size_t a = 0; a < nK; ++a) acc += v[a] * sBd[a];
814 ff[i] = acc;
815 };
816 std::vector<double> v0(m, 0.0);
817 for (std::size_t a = 0; a < nK; ++a) v0[a] = ad[a];
818
819 bool uniform = tf.size() > 2;
820 const double dt = tf.size() > 1 ? tf[1] - tf[0] : 0.0;
821 if (uniform && !(dt > 0.0)) uniform = false;
822 for (std::size_t i = 1; uniform && i + 1 < tf.size(); ++i)
823 if (!(std::abs((tf[i + 1] - tf[i]) - dt) < 1e-12 * std::max(1.0, std::abs(dt))))
824 uniform = false;
825 if (uniform) {
826 const Matrix<double> E = scaled(dt);
827 std::vector<double> v = times(v0, scaled(tf[0]));
828 for (std::size_t i = 0; i < tf.size(); ++i) {
829 if (i > 0) v = times(v, E);
830 record(i, v);
831 }
832 } else {
833 for (std::size_t i = 0; i < tf.size(); ++i) record(i, times(v0, scaled(tf[i])));
834 }
835 } else if (method == "lt") {
836 using C = std::complex<double>;
837 const Matrix<T>& S = out.S;
838 const lti::LaplaceFn L = [&](C s) {
839 Matrix<C> A(nK, nK);
840 for (std::size_t a = 0; a < nK; ++a)
841 for (std::size_t c = 0; c < nK; ++c)
842 A(a, c) = (a == c ? s : C(0.0, 0.0)) -
843 C(num_traits<T>::to_double(S(a, c)), 0.0);
844 std::vector<C> b(nK);
845 for (std::size_t a = 0; a < nK; ++a) b[a] = C(sBd[a], 0.0);
846 const std::vector<C> x = passage_detail::solve_cplx(A, b, "ctmc_passage_taboo");
847 C acc(atom, 0.0);
848 for (std::size_t a = 0; a < nK; ++a) acc += C(ad[a], 0.0) * x[a];
849 return acc;
850 };
851 const lti::LaplaceMethod lm = lti::laplace_method(lti_method);
852 Ff = lti::laplace_invert_cdf(L, tf, lm);
853 const lti::LaplaceFn Ld = [&](C s) { return L(s) - atom; };
854 ff = lti::laplace_invert_pdf(Ld, tf, lm);
855 } else {
856 throw InputError("ctmc_passage_taboo: unknown passage-time method '" + method +
857 "', expected expm or lt");
858 }
859 }
860 for (std::size_t i = 0; i < pos.size(); ++i) {
861 out.F[pos[i]] = Ff[i];
862 out.f[pos[i]] = ff[i];
863 }
864 for (std::size_t i = 0; i < out.F.size(); ++i) {
865 out.F[i] = std::min(1.0, std::max(0.0, out.F[i]));
866 out.f[i] = std::max(0.0, out.f[i]);
867 }
868 return out;
869}
870
871/**
872 * Moments of order 1..nmax of the first passage time into `target`, CONDITIONAL
873 * on entering it before `taboo`, the reference's `ctmc_passage_taboo_moments`:
874 *
875 * E[T^k | B before C] = k! alpha (-S)^{-(k+1)} sB / Pr{B before C}
876 *
877 * evaluated as (-S) y_k = k y_(k-1) from y_0 = pB, one solve per order on one
878 * factorization, with S, pB and the partition of `ctmc_passage_taboo`.
879 *
880 * `mall` is zero on B and NaN where B cannot be entered before C, since the
881 * conditional moment is then undefined; `m` is NaN when B is never entered
882 * first. With an empty `taboo` and B entered surely, the moments equal those of
883 * `ctmc_passage_moments`.
884 */
885template <class T>
887 const std::vector<std::size_t>& target,
888 const std::vector<std::size_t>& taboo,
889 std::size_t nmax = 1) {
891 "ctmc_passage_taboo_moments marks undefined conditional moments with a NaN "
892 "and therefore requires an arithmetic that has one");
893 if (nmax == 0) throw InputError("ctmc_passage_taboo_moments: nmax must be positive");
894 const PassageTaboo<T> pt =
895 passage_detail::taboo_partition(Q, pi0, target, taboo, "ctmc_passage_taboo_moments");
896 const std::size_t n = Q.rows(), nK = pt.keep.size();
897 const T nan = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
898
900 out.pB = pt.pB;
901 out.mall = Matrix<T>(n, nmax, nan);
902 for (std::size_t i : pt.target)
903 for (std::size_t k = 0; k < nmax; ++k) out.mall(i, k) = T(0);
904
905 Matrix<T> Y(nK, nmax, T(0));
906 if (nK > 0) {
907 Matrix<T> LU(nK, nK);
908 for (std::size_t a = 0; a < nK; ++a)
909 for (std::size_t c = 0; c < nK; ++c) LU(a, c) = T(-pt.S(a, c));
910 const std::vector<std::size_t> piv = lu_factor(LU);
911 std::vector<T> y(nK);
912 for (std::size_t a = 0; a < nK; ++a) y[a] = pt.pB[pt.keep[a]];
913 for (std::size_t k = 1; k <= nmax; ++k) {
914 const T kk = num_traits<T>::from_double(static_cast<double>(k));
915 for (std::size_t a = 0; a < nK; ++a) y[a] = T(kk * y[a]);
916 lu_solve(LU, piv, y);
917 for (std::size_t a = 0; a < nK; ++a) Y(a, k - 1) = y[a];
918 }
919 for (std::size_t a = 0; a < nK; ++a) {
920 const T p = pt.pB[pt.keep[a]];
921 if (!(num_traits<T>::to_double(p) > 0.0)) continue;
922 for (std::size_t k = 0; k < nmax; ++k) out.mall(pt.keep[a], k) = T(Y(a, k) / p);
923 }
924 }
925
926 T PB = pt.atom;
927 for (std::size_t a = 0; a < nK; ++a) PB += T(pt.alpha[a] * pt.pB[pt.keep[a]]);
928 out.m.assign(nmax, nan);
929 if (num_traits<T>::to_double(PB) > 0.0)
930 for (std::size_t k = 0; k < nmax; ++k) {
931 T acc = T(0);
932 for (std::size_t a = 0; a < nK; ++a) acc += T(pt.alpha[a] * Y(a, k));
933 out.m[k] = T(acc / PB);
934 }
935 return out;
936}
937
938// ---------------------------------------------------------------------------
939// Semi-Markov chains (Sec. 3)
940// ---------------------------------------------------------------------------
941
942/**
943 * Moments of the semi-Markov first passage time from the per-state holding
944 * moments m_i(r), Eq. 7 with the u_i(r) recurrence of Eq. 8:
945 *
946 * u_i(r) = -sum_{j=1..r} C(r,j) m_i(j) u_i(r-j), u_i(0) = 1,
947 *
948 * which are the derivatives at the origin of 1/h*_i(s). Cheaper than Eq. 6
949 * because it needs no per-pair moments.
950 *
951 * @param hmom (nstates x nmax): hmom(i,r-1) is the r-th moment of the sojourn
952 * in state i
953 */
954template <class T>
956 const std::vector<T>& pi0,
957 const std::vector<std::size_t>& target,
958 std::size_t nmax = 1) {
959 const std::size_t n = P.rows();
960 if (P.cols() != n)
961 throw InputError("smp_passage_moments: the embedded transition matrix must be square");
962 if (nmax == 0) throw InputError("smp_passage_moments: nmax must be positive");
963 for (std::size_t i = 0; i < n; ++i) {
964 T rs = T(0);
965 for (std::size_t j = 0; j < n; ++j) rs += P(i, j);
966 if (std::abs(num_traits<T>::to_double(rs) - 1.0) > 1e-8)
967 throw InputError(
968 "smp_passage_moments: the embedded transition matrix rows must sum to one");
969 }
970 if (hmom.rows() != n)
971 throw InputError("smp_passage_moments: hmom must carry one row per state");
972 if (hmom.cols() < nmax)
973 throw InputError(
974 "smp_passage_moments: hmom must carry at least nmax holding-time moments per state");
975 const std::vector<std::size_t> tgt =
976 passage_detail::unique_target(target, n, "smp_passage_moments");
977
978 std::vector<bool> is_target(n, false);
979 for (std::size_t k : tgt) is_target[k] = true;
980 std::vector<std::size_t> A;
981 for (std::size_t i = 0; i < n; ++i)
982 if (!is_target[i]) A.push_back(i);
983 const std::size_t nA = A.size();
984
986 out.mall = Matrix<T>(n, nmax);
987 out.m.clear(); // the reference's `m = []` when pi0 is not given
988 if (nA == 0) {
989 out.m.assign(nmax, T(0)); // the reference's `m = zeros(1,nmax)` here, pi0 or not
990 return out;
991 }
992
993 // Eq. 8, per state.
994 Matrix<T> u(nA, nmax);
995 for (std::size_t r = 1; r <= nmax; ++r) {
996 for (std::size_t a = 0; a < nA; ++a) {
997 T acc = T(0);
998 for (std::size_t j = 1; j <= r; ++j) {
999 const T base = (r - j == 0) ? T(1) : u(a, r - j - 1);
1000 double c = 1.0;
1001 for (std::size_t q = 0; q < j; ++q)
1002 c = c * double(r - q) / double(q + 1);
1003 acc += num_traits<T>::from_double(c) * hmom(A[a], j - 1) * base;
1004 }
1005 u(a, r - 1) = -acc;
1006 }
1007 }
1008
1009 Matrix<T> IPAA(nA, nA);
1010 for (std::size_t a = 0; a < nA; ++a)
1011 for (std::size_t c = 0; c < nA; ++c)
1012 IPAA(a, c) = (a == c ? T(1) : T(0)) - P(A[a], A[c]);
1013
1014 Matrix<T> M(nA, nmax);
1015 for (std::size_t q = 1; q <= nmax; ++q) {
1016 std::vector<T> b(nA, T(0));
1017 for (std::size_t r = 1; r <= q; ++r) {
1018 double c = 1.0;
1019 for (std::size_t k = 0; k < r; ++k) c = c * double(q - k) / double(k + 1);
1020 for (std::size_t a = 0; a < nA; ++a) {
1021 const T base = (r < q) ? M(a, q - r - 1) : T(1);
1022 b[a] -= num_traits<T>::from_double(c) * u(a, r - 1) * base;
1023 }
1024 }
1025 const std::vector<T> x = solve(IPAA, b);
1026 for (std::size_t a = 0; a < nA; ++a) M(a, q - 1) = x[a];
1027 }
1028
1029 for (std::size_t a = 0; a < nA; ++a)
1030 for (std::size_t q = 0; q < nmax; ++q) out.mall(A[a], q) = M(a, q);
1031 passage_detail::smp_weighted_moments(out, pi0, n, nmax, "smp_passage_moments");
1032 return out;
1033}
1034
1035/**
1036 * Moments of the semi-Markov first passage time from the PER-PAIR kernel
1037 * moments, Eq. 6: hmom[i][k] holds the moments of the sojourn in state i when
1038 * the next state is k (an empty entry is a zero vector), so the recursion runs
1039 * over the kernel r_ik(t) = P(i,k) h_ik(t) directly,
1040 *
1041 * M_i(q) = sum_{k not in B} sum_{r=1..q} C(q,r) m_ik(r) M_k(q-r)
1042 * + sum_{k in B} m_ik(q), M_k(0) = 1.
1043 *
1044 * This is the reference's `iscell(hmom)` branch; the (nstates x nmax) matrix
1045 * form above is its `else` branch.
1046 *
1047 * @param hmom (nstates x nstates) table of moment vectors; hmom[i][k][r-1] is
1048 * the r-th moment of the sojourn in i given the next state k, and
1049 * the entries are expected to already carry the P(i,k) weight, as in
1050 * the reference
1051 */
1052template <class T>
1054 const std::vector<std::vector<std::vector<T>>>& hmom,
1055 const std::vector<T>& pi0,
1056 const std::vector<std::size_t>& target,
1057 std::size_t nmax = 1) {
1058 const std::size_t n = P.rows();
1059 if (P.cols() != n)
1060 throw InputError("smp_passage_moments: the embedded transition matrix must be square");
1061 if (nmax == 0) throw InputError("smp_passage_moments: nmax must be positive");
1062 for (std::size_t i = 0; i < n; ++i) {
1063 T rs = T(0);
1064 for (std::size_t j = 0; j < n; ++j) rs += P(i, j);
1065 if (std::abs(num_traits<T>::to_double(rs) - 1.0) > 1e-8)
1066 throw InputError(
1067 "smp_passage_moments: the embedded transition matrix rows must sum to one");
1068 }
1069 if (hmom.size() != n)
1070 throw InputError(
1071 "smp_passage_moments: a cell HMOM must be (nstates x nstates), entry (i,k) holding "
1072 "the moments of the sojourn in i when the next state is k");
1073 for (std::size_t i = 0; i < n; ++i)
1074 if (hmom[i].size() != n)
1075 throw InputError(
1076 "smp_passage_moments: a cell HMOM must be (nstates x nstates), entry (i,k) "
1077 "holding the moments of the sojourn in i when the next state is k");
1078 const std::vector<std::size_t> tgt =
1079 passage_detail::unique_target(target, n, "smp_passage_moments");
1080
1081 std::vector<bool> is_target(n, false);
1082 for (std::size_t k : tgt) is_target[k] = true;
1083 std::vector<std::size_t> A;
1084 for (std::size_t i = 0; i < n; ++i)
1085 if (!is_target[i]) A.push_back(i);
1086 const std::size_t nA = A.size();
1087
1089 out.mall = Matrix<T>(n, nmax, T(0));
1090 out.m.clear();
1091 if (nA == 0) {
1092 out.m.assign(nmax, T(0)); // the reference's `m = zeros(1,nmax)` here, pi0 or not
1093 return out;
1094 }
1095
1096 // the reference's local_kernel_moments: an empty entry is zeros(1, nmax)
1097 const auto mik = [&](std::size_t i, std::size_t k, std::size_t r) {
1098 const std::vector<T>& v = hmom[i][k];
1099 return r - 1 < v.size() ? v[r - 1] : T(0);
1100 };
1101
1102 Matrix<T> IPAA(nA, nA);
1103 for (std::size_t a = 0; a < nA; ++a)
1104 for (std::size_t c = 0; c < nA; ++c)
1105 IPAA(a, c) = (a == c ? T(1) : T(0)) - P(A[a], A[c]);
1106
1107 Matrix<T> M(nA, nmax, T(0));
1108 for (std::size_t q = 1; q <= nmax; ++q) {
1109 std::vector<T> b(nA, T(0));
1110 for (std::size_t a = 0; a < nA; ++a) {
1111 const std::size_t i = A[a];
1112 T acc = T(0);
1113 for (std::size_t c = 0; c < nA; ++c) {
1114 const std::size_t k = A[c];
1115 for (std::size_t r = 1; r <= q; ++r) {
1116 double bin = 1.0;
1117 for (std::size_t t = 0; t < r; ++t) bin = bin * double(q - t) / double(t + 1);
1118 const T base = (r < q) ? M(c, q - r - 1) : T(1); // M_k(0) = 1
1119 acc += num_traits<T>::from_double(bin) * mik(i, k, r) * base;
1120 }
1121 }
1122 for (std::size_t k : tgt) acc += mik(i, k, q);
1123 b[a] = acc;
1124 }
1125 const std::vector<T> x = solve(IPAA, b);
1126 for (std::size_t a = 0; a < nA; ++a) M(a, q - 1) = x[a];
1127 }
1128
1129 for (std::size_t a = 0; a < nA; ++a)
1130 for (std::size_t q = 0; q < nmax; ++q) out.mall(A[a], q) = M(a, q);
1131 passage_detail::smp_weighted_moments(out, pi0, n, nmax, "smp_passage_moments");
1132 return out;
1133}
1134
1135/**
1136 * L(s) of the semi-Markov first passage time, Eqs. 4-5:
1137 *
1138 * L_i(s) = sum_{k not in B} r*_ik(s) L_k(s) + sum_{k in B} r*_ik(s),
1139 *
1140 * so (I - R*_AA(s)) L_A(s) = R*_AB(s) 1, one linear system per value of s.
1141 *
1142 * @param hlst per-state sojourn transforms h*_i(s), so r*_ik(s) = P(i,k) h*_i(s)
1143 * and the complex numbers stay on the DIAGONAL of the system
1144 */
1145template <class T>
1146std::vector<std::complex<double>> smp_passage_lst(
1147 const Matrix<T>& P, const std::vector<std::function<std::complex<double>(std::complex<double>)>>& hlst,
1148 const std::vector<T>& pi0, const std::vector<std::size_t>& target,
1149 const std::vector<std::complex<double>>& s) {
1150 const std::size_t n = P.rows();
1151 const std::vector<std::size_t> tgt = passage_detail::unique_target(target, n, "smp_passage_lst");
1152 if (hlst.size() != n)
1153 throw InputError("smp_passage_lst: hlst must carry one transform per state");
1154 std::vector<bool> is_target(n, false);
1155 for (std::size_t k : tgt) is_target[k] = true;
1156 std::vector<std::size_t> A;
1157 for (std::size_t i = 0; i < n; ++i)
1158 if (!is_target[i]) A.push_back(i);
1159 const std::size_t nA = A.size();
1160
1161 using C = std::complex<double>;
1162 double atom = 0.0;
1163 if (pi0.size() == n)
1164 for (std::size_t k : tgt) atom += num_traits<T>::to_double(pi0[k]);
1165
1166 std::vector<C> out(s.size(), C(0.0, 0.0));
1167 for (std::size_t is = 0; is < s.size(); ++is) {
1168 std::vector<C> h(nA);
1169 for (std::size_t a = 0; a < nA; ++a) h[a] = hlst[A[a]](s[is]);
1170 Matrix<C> M(nA, nA);
1171 std::vector<C> b(nA, C(0.0, 0.0));
1172 for (std::size_t a = 0; a < nA; ++a) {
1173 for (std::size_t c = 0; c < nA; ++c)
1174 M(a, c) = (a == c ? C(1.0, 0.0) : C(0.0, 0.0)) -
1175 h[a] * C(num_traits<T>::to_double(P(A[a], A[c])), 0.0);
1176 double pb = 0.0;
1177 for (std::size_t k : tgt) pb += num_traits<T>::to_double(P(A[a], k));
1178 b[a] = h[a] * C(pb, 0.0);
1179 }
1180 const std::vector<C> x = passage_detail::solve_cplx(M, b, "smp_passage_lst");
1181 C acc(atom, 0.0);
1182 for (std::size_t a = 0; a < nA; ++a)
1183 acc += C(pi0.size() == n ? num_traits<T>::to_double(pi0[A[a]]) : 1.0 / double(n), 0.0) *
1184 x[a];
1185 out[is] = acc;
1186 }
1187 return out;
1188}
1189
1190/**
1191 * L(s) of the semi-Markov first passage time from the (n x n) KERNEL of
1192 * transforms, the reference's non-`perState` branch of Eq. 5: hlst[i][k] is
1193 * r*_ik(s), the transform of the sojourn in i jointly with the next state being
1194 * k (so it already carries P(i,k)); an empty entry is zero. Then
1195 *
1196 * (I - R*_AA(s)) L_A(s) = R*_AB(s) 1,
1197 *
1198 * with R*(s) the matrix of those transforms, and P is read only for its size.
1199 *
1200 * @param hlst (nstates x nstates) table of transforms, empty where the kernel
1201 * has no mass
1202 */
1203template <class T>
1204std::vector<std::complex<double>> smp_passage_lst(
1205 const Matrix<T>& P,
1206 const std::vector<std::vector<std::function<std::complex<double>(std::complex<double>)>>>& hlst,
1207 const std::vector<T>& pi0, const std::vector<std::size_t>& target,
1208 const std::vector<std::complex<double>>& s) {
1209 const std::size_t n = P.rows();
1210 const std::vector<std::size_t> tgt = passage_detail::unique_target(target, n, "smp_passage_lst");
1211 if (hlst.size() != n)
1212 throw InputError("smp_passage_lst: a kernel hlst must be (nstates x nstates)");
1213 for (std::size_t i = 0; i < n; ++i)
1214 if (hlst[i].size() != n)
1215 throw InputError("smp_passage_lst: a kernel hlst must be (nstates x nstates)");
1216 if (!pi0.empty() && pi0.size() != n)
1217 throw InputError(
1218 "smp_passage_lst: PI0 must be a distribution over the state space, one entry per "
1219 "state");
1220 std::vector<bool> is_target(n, false);
1221 for (std::size_t k : tgt) is_target[k] = true;
1222 std::vector<std::size_t> A;
1223 for (std::size_t i = 0; i < n; ++i)
1224 if (!is_target[i]) A.push_back(i);
1225 const std::size_t nA = A.size();
1226
1227 using C = std::complex<double>;
1228 // the reference's `if isempty(pi0), pi0 = ones(1,n)/n`
1229 std::vector<double> p0(n, 1.0 / double(n));
1230 if (pi0.size() == n)
1231 for (std::size_t i = 0; i < n; ++i) p0[i] = num_traits<T>::to_double(pi0[i]);
1232 double atom = 0.0;
1233 for (std::size_t k : tgt) atom += p0[k];
1234
1235 std::vector<C> out(s.size(), C(0.0, 0.0));
1236 for (std::size_t is = 0; is < s.size(); ++is) {
1237 Matrix<C> M(nA, nA);
1238 std::vector<C> b(nA, C(0.0, 0.0));
1239 for (std::size_t a = 0; a < nA; ++a) {
1240 for (std::size_t c = 0; c < nA; ++c) {
1241 const C r = hlst[A[a]][A[c]] ? hlst[A[a]][A[c]](s[is]) : C(0.0, 0.0);
1242 M(a, c) = (a == c ? C(1.0, 0.0) : C(0.0, 0.0)) - r;
1243 }
1244 for (std::size_t k : tgt)
1245 if (hlst[A[a]][k]) b[a] += hlst[A[a]][k](s[is]);
1246 }
1247 const std::vector<C> x = passage_detail::solve_cplx(M, b, "smp_passage_lst");
1248 C acc(atom, 0.0);
1249 for (std::size_t a = 0; a < nA; ++a) acc += C(p0[A[a]], 0.0) * x[a];
1250 out[is] = acc;
1251 }
1252 return out;
1253}
1254
1255/**
1256 * CDF and density of the semi-Markov first passage time, by inverting
1257 * `smp_passage_lst` through api/lti.
1258 *
1259 * There is no matrix-exponential route here: a semi-Markov chain has no
1260 * generator to exponentiate, which is exactly the case uniformization does not
1261 * reach and the transform does.
1262 *
1263 * `lti_method` defaults to "euler" RATHER THAN "weeks". Semi-Markov passage
1264 * densities are the case Sec. 4.2 singles out as slow-converging for a Laguerre
1265 * series, and `laplace_weeks_scaling` then refuses by name rather than
1266 * returning noise.
1267 */
1268template <class T>
1270 const Matrix<T>& P, const std::vector<std::function<std::complex<double>(std::complex<double>)>>& hlst,
1271 const std::vector<T>& pi0, const std::vector<std::size_t>& target,
1272 const std::vector<double>& tset, const std::string& lti_method = "euler") {
1273 const std::size_t n = P.rows();
1274 const std::vector<std::size_t> tgt =
1275 passage_detail::unique_target(target, n, "smp_passage_time");
1276 double atom = 0.0;
1277 if (pi0.size() == n)
1278 for (std::size_t k : tgt) atom += num_traits<T>::to_double(pi0[k]);
1279
1280 const lti::LaplaceFn L = [&](std::complex<double> s) {
1281 std::vector<std::complex<double>> sv(1, s);
1282 return smp_passage_lst(P, hlst, pi0, target, sv)[0];
1283 };
1284 const lti::LaplaceMethod lm = lti::laplace_method(lti_method);
1285 PassageCurve<T> out;
1286 out.t = tset;
1287 out.atom = atom;
1288 out.F = lti::laplace_invert_cdf(L, tset, lm);
1289 const lti::LaplaceFn Ld = [&](std::complex<double> s) { return L(s) - atom; };
1290 out.f = lti::laplace_invert_pdf(Ld, tset, lm);
1291 return out;
1292}
1293
1294} // namespace mc
1295} // namespace line
1296
1297#endif // LINE_API_MC_CTMC_PASSAGE_H
InputError(const std::string &what)
Definition error.h:105
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:112
Steady-state distribution of a continuous-time Markov chain.
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
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
StochCompResult< T > ctmc_stochcomp(const Matrix< T > &Q, const std::vector< std::size_t > &I)
Definition dtmc_solve.h:158
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.
PassageTabooMoments< T > ctmc_passage_taboo_moments(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< std::size_t > &taboo, std::size_t nmax=1)
Moments of order 1..nmax of the first passage time into target, CONDITIONAL on entering it before tab...
PassageTaboo< T > ctmc_passage_taboo(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< std::size_t > &taboo, const std::vector< double > &tset=std::vector< double >(), const std::string &method="expm", const std::string &lti_method="euler")
First passage into target under the taboo set taboo, the reference's ctmc_passage_taboo.
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.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
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
Moments of a first passage time, [Mall, m] of the reference.
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)
Conditional moments under a taboo set, [mall, m, pB] of ctmc_passage_taboo_moments....
std::vector< T > m
the pi0-weighted conditional moments, NaN if B is never entered first
Matrix< T > mall
(nstates x nmax): zero on B, NaN where B cannot be entered first
std::vector< T > pB
(nstates) probability of entering B before C
First passage into a target set B under a TABOO set C, [F, f, out] of ctmc_passage_taboo....
std::vector< std::size_t > taboo
C minus B, sorted and unique.
std::vector< T > sC
Q(K,C)*1, the exit rates into the taboo set.
T tabooprob
Pr{C entered before B}.
std::vector< T > pB
(nstates) reachprob per starting state, one on B
std::vector< T > sB
Q(K,B)*1, the exit rates into the target.
std::vector< T > pC
(nstates) tabooprob per starting state, one on C
std::vector< std::size_t > target
B, sorted and unique.
std::vector< T > alpha
pi0 restricted to K, UNNORMALIZED
T atom
mass of pi0 already in B, i.e. F(0)
std::vector< double > F
DEFECTIVE CDF Pr{B entered by t, C not before}, clamped to [0, 1].
Matrix< T > S
Q(K,K).
std::vector< double > t
the grid the curve was asked on
T reachprob
Pr{B entered before C}, the limit of F.
std::vector< double > f
its density alpha exp(St) sB
std::vector< std::size_t > keep
row of S -> state index of Q (the set K)