LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
markov_chain.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_LANG_PROCESSES_MARKOV_CHAIN_H
6#define LINE_LANG_PROCESSES_MARKOV_CHAIN_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * The MarkovProcess / MarkovChain object surface.
12 *
13 * Port of `matlab/src/lang/processes/MarkovProcess.m` (a CTMC, carrying a
14 * generator) and `MarkovChain.m` (a DTMC, carrying a transition matrix). Both
15 * are thin objects over `api/mc`: the algorithms already lived there before
16 * this header did, and what was missing was the surface that names them and
17 * fixes which one each method calls. That distinction is the whole content of
18 * the two classes and it is not cosmetic -- `solve` and `transient` each pick a
19 * DIFFERENT primitive for the two chain kinds, and `toDTMC` and `toEmbedded`
20 * both turn a CTMC into a DTMC while disagreeing about what the result means.
21 *
22 * ONE MODEL TYPE FOR BOTH KINDS. `MarkovChainModel<T>::discrete` says which
23 * class the object would be in MATLAB. The reference dispatches on the class,
24 * so every function here dispatches on that flag, and the constructors'
25 * normalization (`ctmc_makeinfgen` / `dtmc_makestochastic`) is applied by the
26 * factories exactly as the two constructors apply it. This type was previously
27 * declared inside `solvers/ctmc/solver_ctmc_chain.h`, which aliases it now.
28 *
29 * WHAT IS DELIBERATELY ABSENT:
30 * - `plot` and `plot3`. They call graphViz4Matlab and MATLAB's `digraph`, a
31 * rendering layer this tree has no counterpart for. A caller wanting the
32 * graph has `mat` and `state_space` and can emit whatever format it wants.
33 * - `getGenerator` / `getTransMat` / `setStateSpace`, which are field access:
34 * `mat` and `state_space` are public members.
35 * - `isfinite`. It is a flag the reference stores and never reads.
36 *
37 * TWO PLACES WHERE THE REFERENCE IS NOT REPRODUCIBLE, and how this port stands:
38 * - `toMarkovChain` with no argument picks `q = max|Q| + rand`, so the SAME
39 * chain uniformizes to a different P on every call. Any `q > max|Q|` is a
40 * valid uniformization rate and all of them carry the same stationary law,
41 * but the returned matrix is not the same object, so a golden recorded
42 * against it would be noise. The default here is deterministic,
43 * `q = max|Q| * (1 + 1/16)`, and the rate is an explicit parameter for a
44 * caller that wants the reference's own draw.
45 * - `MarkovChain.sample` draws its own uniform initial law before simulating.
46 * Here the initial law is a parameter, empty meaning that same random draw,
47 * so the caller can make the path reproducible without a fork of the
48 * primitive.
49 */
50
51#include <algorithm>
52#include <cmath>
53#include <cstddef>
54#include <map>
55#include <random>
56#include <string>
57#include <vector>
58
80#include "line/num/number.h"
81#include "line/util/error.h"
82#include "line/util/lu.h"
83#include "line/util/matrix.h"
84
85namespace line {
86namespace lang {
87namespace processes {
88
89/** A user-supplied chain: a MarkovProcess when `discrete` is false, else a MarkovChain. */
90template <class T>
92 Matrix<T> mat; ///< generator Q (CTMC) or transition matrix P (DTMC)
93 bool discrete = false; ///< true for a MarkovChain
94 Matrix<T> state_space; ///< optional; empty means the chain carries none
95
97
98 /** Mirrors the constructors: a generator is closed, a transition matrix normalized. */
100 const Matrix<T>& space = Matrix<T>()) {
102 m.mat = mc::ctmc_makeinfgen(infgen);
103 m.discrete = false;
104 m.state_space = space;
105 return m;
106 }
107
108 static MarkovChainModel<T> chain(const Matrix<T>& transmat,
109 const Matrix<T>& space = Matrix<T>()) {
111 m.mat = mc::dtmc_makestochastic(transmat);
112 m.discrete = true;
113 m.state_space = space;
114 return m;
115 }
116
117 std::size_t order() const { return mat.rows(); }
118};
119
120namespace detail {
121
122/** Every entry point below reads a square matrix; say so once, by name. */
123template <class T>
124void require_square(const MarkovChainModel<T>& m, const char* who) {
125 if (m.mat.rows() == 0 || m.mat.cols() != m.mat.rows())
126 throw InputError(std::string(who) + ": the chain matrix is empty or not square");
127}
128
129template <class T>
130void require_kind(const MarkovChainModel<T>& m, bool discrete, const char* who) {
131 if (m.discrete != discrete)
132 throw InputError(std::string(who) + ": this is a method of " +
133 (discrete ? "MarkovChain (a DTMC); the object is a MarkovProcess"
134 : "MarkovProcess (a CTMC); the object is a MarkovChain"));
135}
136
137/** The uniform law over n states, the default every `pi0` argument falls back to. */
138template <class T>
139std::vector<T> uniform_law(std::size_t n) {
140 return std::vector<T>(n, num_traits<T>::from_int(1) / num_traits<T>::from_int(
141 static_cast<int>(n)));
142}
143
144template <class T>
145std::vector<T> law_or_uniform(const std::vector<T>& pi0, std::size_t n, const char* who) {
146 if (pi0.empty()) return uniform_law<T>(n);
147 if (pi0.size() != n)
148 throw InputError(std::string(who) + ": the initial distribution has the wrong length");
149 return pi0;
150}
151
152} // namespace detail
153
154// ---------------------------------------------------------------------------
155// Conversions between the two chain kinds
156// ---------------------------------------------------------------------------
157
158/**
159 * `MarkovChain.toMarkovProcess` / `toCTMC`: read the DTMC as a CTMC with unit
160 * exit rates, Q = P - I. The stationary law is preserved, since P and P - I
161 * have the same left null structure up to the shift.
162 */
163template <class T>
165 detail::require_square(m, "to_markov_process");
166 detail::require_kind(m, true, "to_markov_process");
167 Matrix<T> Q = m.mat;
168 const std::size_t n = Q.rows();
169 for (std::size_t i = 0; i < n; ++i) Q(i, i) -= num_traits<T>::from_int(1);
171 return out;
172}
173
174/** The uniformization rate this port uses when the caller names none. */
175template <class T>
177 const std::size_t n = Q.rows();
178 T qmax = num_traits<T>::from_int(0);
179 for (std::size_t i = 0; i < n; ++i)
180 for (std::size_t j = 0; j < n; ++j) {
181 const T a = num_abs(Q(i, j));
182 if (a > qmax) qmax = a;
183 }
184 // Strictly above max|Q|, as the reference's `+rand` guarantees, but fixed.
185 return T(qmax * num_traits<T>::from_double(17.0 / 16.0));
186}
187
188/**
189 * `MarkovProcess.toMarkovChain` / `toDTMC`: the UNIFORMIZED chain, P = Q/q + I.
190 *
191 * This is the conversion that PRESERVES the stationary distribution, and it is
192 * the one to reach for when the question is about long-run behaviour. Contrast
193 * `to_embedded`, which does not.
194 */
195template <class T>
197 detail::require_square(m, "to_markov_chain");
198 detail::require_kind(m, false, "to_markov_chain");
199 if (!(q > num_traits<T>::from_int(0)))
200 throw InputError("to_markov_chain: the uniformization rate must be positive");
201 const std::size_t n = m.mat.rows();
202 Matrix<T> P = m.mat;
203 for (std::size_t i = 0; i < n; ++i)
204 for (std::size_t j = 0; j < n; ++j) P(i, j) = T(P(i, j) / q);
205 for (std::size_t i = 0; i < n; ++i) P(i, i) += num_traits<T>::from_int(1);
207}
208
209template <class T>
211 detail::require_square(m, "to_markov_chain");
212 detail::require_kind(m, false, "to_markov_chain");
214}
215
216/** `toDTMC`, the backwards-compatible alias of `toMarkovChain`. */
217template <class T>
221
222template <class T>
224 return to_markov_chain(m, q);
225}
226
227/**
228 * `MarkovProcess.toEmbedded`: the JUMP CHAIN, the DTMC of the states visited at
229 * transition epochs.
230 *
231 * IT DOES NOT PRESERVE THE STATIONARY LAW, and that is the point of having it
232 * separate from `to_markov_chain`. Dividing each off-diagonal row by the exit
233 * rate throws away how long the chain lingers, so a state with a fast exit rate
234 * is visited as often as a slow one and weighs the same here while weighing far
235 * less in the CTMC. An absorbing state (exit rate zero) has no next jump, and
236 * the reference makes it absorbing in the jump chain too rather than leaving an
237 * all-zero row that `dtmc_makestochastic` would have to invent a law for.
238 */
239template <class T>
241 detail::require_square(m, "to_embedded");
242 detail::require_kind(m, false, "to_embedded");
243 const std::size_t n = m.mat.rows();
244 const T zero = num_traits<T>::from_int(0);
245 Matrix<T> P = m.mat;
246 for (std::size_t i = 0; i < n; ++i) {
247 const T exit_rate = T(zero - m.mat(i, i));
248 P(i, i) = zero;
249 if (exit_rate > zero) {
250 for (std::size_t j = 0; j < n; ++j) P(i, j) = T(P(i, j) / exit_rate);
251 } else {
252 P(i, i) = num_traits<T>::from_int(1);
253 }
254 }
256}
257
258/** `toTimeReversed` for either kind: the chain run backwards in time. */
259template <class T>
261 detail::require_square(m, "to_time_reversed");
262 if (m.discrete)
265}
266
267// ---------------------------------------------------------------------------
268// Stationary analysis
269// ---------------------------------------------------------------------------
270
271/**
272 * `MarkovProcess.solve` / `MarkovChain.solve`.
273 *
274 * THE REDUCIBLE SOLVER IS THE NUMERIC PATH IN BOTH CLASSES, not a fallback:
275 * the reference reserves the plain `ctmc_solve` / `dtmc_solve` for a SYMBOLIC
276 * matrix, where the reducible variant's component decomposition has nothing to
277 * decide. This port has no symbolic element type, so the reducible one is what
278 * every call takes. `solver_ctmc_chain` in `solvers/ctmc` deliberately does the
279 * other thing -- primary first, reducible on a failed validity test -- because
280 * it ports `solver_ctmc_chain.m`, not the class method, and those two disagree.
281 */
282template <class T>
283std::vector<T> chain_solve(const MarkovChainModel<T>& m) {
284 detail::require_square(m, "chain_solve");
285 if (m.discrete) return mc::dtmc_solve_reducible(m.mat).pi;
286 return mc::ctmc_solve_reducible(m.mat).pi;
287}
288
289/**
290 * `MarkovProcess.solveRelative`: the equilibrium vector normalized so that
291 * `refstate` carries one, which exists even where the normalizing constant does
292 * not. `refstate` is 0-based here and 1-based in the reference.
293 */
294template <class T>
295std::vector<T> solve_relative(const MarkovChainModel<T>& m, std::size_t refstate = 0) {
296 detail::require_square(m, "solve_relative");
297 detail::require_kind(m, false, "solve_relative");
298 return mc::ctmc_relsolve(m.mat, refstate);
299}
300
301/** What `getProbState` returns: the probability and the two determinants behind it. */
302template <class T>
304 T pi_i; ///< probability of the state
305 T num; ///< determinant of the numerator matrix
306 T den; ///< determinant of the denominator matrix
307};
308
309/**
310 * `MarkovProcess.getProbState`: the probability of ONE state by Cramer's rule.
311 *
312 * Column 0 of the generator is replaced by ones, which imposes the
313 * normalization in place of the column the balance equations make redundant;
314 * the numerator matrix additionally zeroes row `i` and puts a one back in its
315 * first entry. The quotient of the two determinants is the probability.
316 *
317 * WHY A DETERMINANT AND NOT A SOLVE. This exists so that the probability of one
318 * state can be written as a RATIO OF POLYNOMIALS in the generator's entries,
319 * which is what makes it useful symbolically in the reference. Numerically a
320 * full solve is cheaper and better conditioned, and `chain_solve` is that; this
321 * one is kept faithful because a caller reaching for it wants `num` and `den`
322 * separately.
323 */
324template <class T>
326 detail::require_square(m, "get_prob_state");
327 detail::require_kind(m, false, "get_prob_state");
328 const std::size_t n = m.mat.rows();
329 if (i >= n) throw InputError("get_prob_state: state index is out of range");
330 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
331
332 Matrix<T> Q = m.mat;
333 for (std::size_t r = 0; r < n; ++r) Q(r, 0) = one;
334 Matrix<T> Qi = Q;
335 for (std::size_t j = 0; j < n; ++j) Qi(i, j) = zero;
336 Qi(i, 0) = one;
337
339 out.num = lu_det(Qi);
340 out.den = lu_det(Q);
341 if (out.den == zero)
342 throw NumericError(
343 "get_prob_state: the normalized generator is singular, so Cramer's rule has no "
344 "quotient; the chain is reducible -- use chain_solve, which decomposes it");
345 out.pi_i = T(out.num / out.den);
346 return out;
347}
348
349/** Row index of `state` in the chain's state space, or `n` when it carries none. */
350template <class T>
351std::size_t match_state(const MarkovChainModel<T>& m, const std::vector<T>& state) {
352 const std::size_t rows = m.state_space.rows(), cols = m.state_space.cols();
353 if (rows == 0)
354 throw InputError("match_state: the chain carries no state space to match against");
355 if (state.size() != cols)
356 throw InputError("match_state: the state has the wrong number of columns");
357 for (std::size_t r = 0; r < rows; ++r) {
358 bool hit = true;
359 for (std::size_t c = 0; c < cols && hit; ++c) hit = (m.state_space(r, c) == state[c]);
360 if (hit) return r;
361 }
362 return rows;
363}
364
365/** `getProbState` addressed by the state itself rather than by its index. */
366template <class T>
367ProbStateResult<T> get_prob_state(const MarkovChainModel<T>& m, const std::vector<T>& state) {
368 const std::size_t i = match_state(m, state);
369 if (i >= m.state_space.rows())
370 throw InputError("get_prob_state: the state is not in the chain's state space");
371 return get_prob_state(m, i);
372}
373
374/** `isFeasible`: a valid generator, or a stochastic transition matrix. */
375template <class T>
377 detail::require_square(m, "is_feasible");
378 if (m.discrete) return mc::dtmc_isfeasible(m.mat) > 0;
379 return mc::ctmc_isfeasible(m.mat);
380}
381
382// ---------------------------------------------------------------------------
383// Transient analysis
384// ---------------------------------------------------------------------------
385
386/** What the CTMC `transient` returns: the law at t and the truncation it used. */
387template <class T>
389 std::vector<T> pi; ///< distribution at time t
390 std::size_t kmax; ///< Poisson terms used (the right truncation point for 'foxglynn')
391};
392
393/**
394 * `MarkovProcess.transient`: the law at ONE time t, by uniformization.
395 *
396 * `method` is `"unif"` (Jensen, the default) or `"foxglynn"`, whose weights are
397 * built by the Fox-Glynn recursion instead of by evaluating Poisson terms, so
398 * it survives a `q t` large enough to underflow them. Note that `t` is a TIME
399 * here; the DTMC counterpart takes a step count, which is why the two are
400 * separate functions rather than one dispatching on `discrete`.
401 */
402template <class T>
403TransientAtResult<T> chain_transient_at(const MarkovChainModel<T>& m, const std::vector<T>& pi0in,
404 const T& t, const std::string& method = "unif") {
405 detail::require_square(m, "chain_transient_at");
406 detail::require_kind(m, false, "chain_transient_at");
407 const std::size_t n = m.mat.rows();
408 const std::vector<T> pi0 = detail::law_or_uniform(pi0in, n, "chain_transient_at");
409
411 if (method == "foxglynn") {
412 const mc::FoxGlynnResult<T> r = mc::ctmc_foxglynn(pi0, m.mat, t);
413 out.pi = r.pi;
414 out.kmax = r.right < 0 ? 0 : static_cast<std::size_t>(r.right);
415 } else if (method == "unif" || method.empty()) {
417 out.pi = r.pi;
418 out.kmax = r.kmax;
419 } else {
420 throw InputError("chain_transient_at: unknown transient method '" + method +
421 "'; the reference offers 'unif' and 'foxglynn'");
422 }
423 return out;
424}
425
426/**
427 * `MarkovChain.transient`: the law at every step 0..steps, one row per step.
428 *
429 * The DTMC's clock is the step count, so unlike the CTMC method this returns
430 * the whole trajectory and takes no time argument.
431 */
432template <class T>
433Matrix<T> chain_transient_steps(const MarkovChainModel<T>& m, const std::vector<T>& pi0in,
434 std::size_t steps = 1) {
435 detail::require_square(m, "chain_transient_steps");
436 detail::require_kind(m, true, "chain_transient_steps");
437 const std::size_t n = m.mat.rows();
438 const std::vector<T> pi0 = detail::law_or_uniform(pi0in, n, "chain_transient_steps");
439 return mc::dtmc_transient(m.mat, pi0, steps);
440}
441
442/**
443 * `MarkovChain.transientUnif`: the DTMC read as the randomized image of a CTMC,
444 * so `t` is CONTINUOUS here where `chain_transient_steps` counts steps. The two
445 * answer different questions about the same matrix and the reference keeps both.
446 */
447template <class T>
449 const std::vector<T>& pi0in, const T& t) {
450 detail::require_square(m, "chain_transient_unif");
451 detail::require_kind(m, true, "chain_transient_unif");
452 const std::size_t n = m.mat.rows();
453 const std::vector<T> pi0 = detail::law_or_uniform(pi0in, n, "chain_transient_unif");
456 out.pi = r.pi;
457 out.kmax = r.kmax;
458 return out;
459}
460
461/** What `timeAverage` returns. */
462template <class T>
464 std::vector<T> pi_time_avg; ///< time-averaged law over [0, t]
465 std::vector<T> pi_exit; ///< law at t
466 std::size_t kmax;
467};
468
469/** `MarkovProcess.timeAverage`: the law averaged over [0,t], and its endpoint. */
470template <class T>
471TimeAverageOut<T> time_average(const MarkovChainModel<T>& m, const std::vector<T>& pi0in,
472 const T& t) {
473 detail::require_square(m, "time_average");
474 detail::require_kind(m, false, "time_average");
475 const std::size_t n = m.mat.rows();
476 const std::vector<T> pi0 = detail::law_or_uniform(pi0in, n, "time_average");
479 out.pi_time_avg = r.piTimeAvg;
480 out.pi_exit = r.piExit;
481 out.kmax = r.kmax;
482 return out;
483}
484
485/**
486 * `MarkovProcess.sens`: the derivative of the stationary law with respect to a
487 * scalar parameter, given the derivative `dQ` of the generator. The reference
488 * feeds it its own `solve()`, so this one does too.
489 */
490template <class T>
491std::vector<T> chain_sens(const MarkovChainModel<T>& m, const Matrix<T>& dQ) {
492 detail::require_square(m, "chain_sens");
493 detail::require_kind(m, false, "chain_sens");
494 return mc::ctmc_sens(m.mat, dQ, chain_solve(m));
495}
496
497// ---------------------------------------------------------------------------
498// Aggregation-disaggregation
499// ---------------------------------------------------------------------------
500
501/** What `aggregate` returns: the approximate law and the two NCD indices. */
502template <class T>
504 std::vector<T> p; ///< approximate stationary vector, ORIGINAL state ordering
505 T eps; ///< nearly-complete-decomposability index of the partition
506 T epsMAX; ///< the largest index for which the approximation is meant to hold
507};
508
509/**
510 * `MarkovProcess.aggregate`: aggregation-disaggregation over a macrostate
511 * partition.
512 *
513 * `method` is `"courtois"` (the default; `param` is the randomization rate q),
514 * `"kms"` or `"takahashi"` (`param` is the sweep count, default 10), or
515 * `"multi"`, which needs the SECOND-LEVEL partition and therefore does not go
516 * through `param` at all -- it is refused here without `MSS`, as the reference
517 * refuses it.
518 *
519 * READ `eps` BEFORE THE ANSWER. These are approximations whose error is
520 * governed by how nearly decomposable the partition is; `eps > epsMAX` means
521 * the partition does not justify the method, and the vector is returned anyway
522 * because the reference returns it. It is a diagnostic, not a gate.
523 *
524 * `MS` is 0-based here and 1-based in the reference.
525 */
526template <class T>
528 const std::vector<std::vector<std::size_t>>& MS,
529 const std::string& method = "courtois",
530 const T* param = nullptr) {
531 detail::require_square(m, "aggregate");
532 detail::require_kind(m, false, "aggregate");
533 if (MS.empty()) throw InputError("aggregate: the macrostate partition is empty");
534
536 if (method == "courtois" || method.empty()) {
537 const mc::CourtoisResult<T> r =
538 param ? mc::ctmc_courtois(m.mat, MS, *param) : mc::ctmc_courtois(m.mat, MS);
539 out.p = r.p;
540 out.eps = r.eps;
541 out.epsMAX = r.epsMAX;
542 } else if (method == "kms") {
543 const std::size_t steps =
544 param ? static_cast<std::size_t>(num_traits<T>::to_double(*param)) : 10;
545 const mc::KmsResult<T> r = mc::ctmc_kms(m.mat, MS, steps);
546 out.p = r.p;
547 out.eps = r.eps;
548 out.epsMAX = r.epsMAX;
549 } else if (method == "takahashi") {
550 const std::size_t steps =
551 param ? static_cast<std::size_t>(num_traits<T>::to_double(*param)) : 10;
552 const mc::TakahashiResult<T> r = mc::ctmc_takahashi(m.mat, MS, steps);
553 out.p = r.p;
554 out.eps = r.eps;
555 out.epsMAX = r.epsMAX;
556 } else if (method == "multi") {
557 throw InputError(
558 "aggregate: the 'multi' method requires the second-level partition MSS; call the "
559 "aggregate_multi overload, which takes it");
560 } else {
561 throw InputError("aggregate: unknown aggregation method '" + method + "'");
562 }
563 return out;
564}
565
566/**
567 * The `"multi"` arm of `aggregate`, separated because its parameter is a
568 * PARTITION OF THE PARTITION and not a scalar. `MSS` partitions the macrostate
569 * indices 0..|MS|-1.
570 */
571template <class T>
573 const std::vector<std::vector<std::size_t>>& MS,
574 const std::vector<std::vector<std::size_t>>& MSS) {
575 detail::require_square(m, "aggregate_multi");
576 detail::require_kind(m, false, "aggregate_multi");
577 if (MS.empty() || MSS.empty())
578 throw InputError("aggregate_multi: both partitions must be non-empty");
579 const mc::MultiResult<T> r = mc::ctmc_multi(m.mat, MS, MSS);
581 out.p = r.p;
582 out.eps = r.eps;
583 out.epsMAX = r.epsMAX;
584 return out;
585}
586
587// ---------------------------------------------------------------------------
588// Stochastic complementation
589// ---------------------------------------------------------------------------
590
591/** What `stochCompFull` returns for a CTMC; a DTMC fills the same blocks from P. */
592template <class T>
594 Matrix<T> S; ///< the complement on the selected states
595 Matrix<T> A11; ///< the four blocks of the partitioned matrix
599 Matrix<T> T12; ///< the return-path term, so that S = A11 + T12
600};
601
602/**
603 * `stochComp` / `stochCompFull` for either kind.
604 *
605 * `I` is 0-based here and 1-based in the reference. An empty `I` takes the
606 * reference's own default, the first half of the state space.
607 *
608 * THE DTMC ARM FILLS ONLY `S`. `dtmc_stochcomp` returns the complement alone,
609 * as `MarkovChain.stochCompFull` reports blocks its own primitive does not
610 * separate; the four blocks are left empty rather than reconstructed here,
611 * which would be a different function under the same name.
612 */
613template <class T>
615 const std::vector<std::size_t>& I = std::vector<std::size_t>()) {
616 detail::require_square(m, "stoch_comp_full");
617 StochCompOut<T> out;
618 if (m.discrete) {
619 std::vector<std::size_t> keep = I;
620 if (keep.empty()) {
621 const std::size_t half = m.mat.rows() / 2;
622 if (half == 0)
623 throw InputError(
624 "stoch_comp_full: the default partition takes the first half of the state "
625 "space, which is empty on a chain of order one; pass I explicitly");
626 for (std::size_t i = 0; i < half; ++i) keep.push_back(i);
627 }
628 out.S = mc::dtmc_stochcomp(m.mat, keep);
629 return out;
630 }
631 const mc::StochCompResult<T> r =
632 I.empty() ? mc::ctmc_stochcomp(m.mat) : mc::ctmc_stochcomp(m.mat, I);
633 out.S = r.S;
634 out.A11 = r.Q11;
635 out.A12 = r.Q12;
636 out.A21 = r.Q21;
637 out.A22 = r.Q22;
638 out.T12 = r.T12;
639 return out;
640}
641
642/** `stochComp`: the complement alone. */
643template <class T>
645 const std::vector<std::size_t>& I = std::vector<std::size_t>()) {
646 return stoch_comp_full(m, I).S;
647}
648
649// ---------------------------------------------------------------------------
650// Hitting times and sampling
651// ---------------------------------------------------------------------------
652
653/**
654 * `hittingTime`: the mean time (CTMC) or step count (DTMC) to reach any state
655 * in `target`, zero on the target set itself and infinite from a state that
656 * cannot reach it. `target` is 0-based here and 1-based in the reference.
657 */
658template <class T>
659std::vector<T> hitting_time(const MarkovChainModel<T>& m,
660 const std::vector<std::size_t>& target) {
661 detail::require_square(m, "hitting_time");
662 if (target.empty()) throw InputError("hitting_time: the target set is empty");
663 if (m.discrete) return mc::dtmc_hitting_time(m.mat, target);
664 return mc::ctmc_hitting_time(m.mat, target);
665}
666
667/** A sampled path: the states visited, with their holding times for a CTMC. */
668template <class T>
669struct ChainPath {
670 std::vector<std::size_t> states; ///< 0-based state index at each step
671 std::vector<T> sojourn; ///< holding times; empty for a DTMC, which has none
672};
673
674/**
675 * `sample`: simulate `n` steps.
676 *
677 * The reference draws the initial state from a uniform law it randomizes itself
678 * (`MarkovChain.sample`) or from the primitive's default (`MarkovProcess`).
679 * Here `pi0` is explicit and empty reproduces that default, so a caller can
680 * make the path reproducible by naming the law and seeding `gen`.
681 */
682template <class T, class Gen>
683ChainPath<T> chain_sample(const MarkovChainModel<T>& m, const std::vector<T>& pi0in,
684 std::size_t n, Gen& gen) {
685 detail::require_square(m, "chain_sample");
686 ChainPath<T> out;
687 const std::size_t order = m.mat.rows();
688 if (m.discrete) {
689 std::vector<T> pi0 = pi0in;
690 if (pi0.empty()) {
691 std::uniform_real_distribution<double> unif(0.0, 1.0);
692 pi0.resize(order);
693 double total = 0.0;
694 for (std::size_t i = 0; i < order; ++i) {
695 const double u = unif(gen);
696 pi0[i] = num_traits<T>::from_double(u);
697 total += u;
698 }
699 for (std::size_t i = 0; i < order; ++i)
700 pi0[i] = T(pi0[i] / num_traits<T>::from_double(total));
701 } else if (pi0.size() != order) {
702 throw InputError("chain_sample: the initial distribution has the wrong length");
703 }
704 out.states = mc::dtmc_simulate(m.mat, pi0, n, gen);
705 return out;
706 }
707 if (!pi0in.empty() && pi0in.size() != order)
708 throw InputError("chain_sample: the initial distribution has the wrong length");
709 pfqn::McRng rng(gen());
710 const mc::CtmcPath<T> p = mc::ctmc_simulate(m.mat, pi0in, n, rng);
711 out.states = p.states;
712 out.sojourn = p.sojourn;
713 return out;
714}
715
716// ---------------------------------------------------------------------------
717// Construction
718// ---------------------------------------------------------------------------
719
720/** `MarkovProcess.rand`: a random generator of the given order. */
721template <class T, class Gen>
722MarkovChainModel<T> rand_process(std::size_t n, Gen& gen) {
723 std::uniform_real_distribution<double> unif(0.0, 1.0);
724 auto draw = [&]() { return unif(gen); };
726}
727
728/** `MarkovChain.rand`: a random transition matrix of the given order. */
729template <class T, class Gen>
730MarkovChainModel<T> rand_chain(std::size_t n, Gen& gen) {
731 std::uniform_real_distribution<double> unif(0.0, 1.0);
732 auto draw = [&]() { return unif(gen); };
734}
735
736/**
737 * `MarkovChain.fromSampleSysAggr`: estimate a DTMC from an observed trajectory.
738 *
739 * `sample_state` holds one row per observation, the columns being the aggregate
740 * state; the reference joins the per-node trajectories COLUMN-WISE first, which
741 * is time alignment, so a caller must pass the joined matrix. Distinct rows
742 * become the state space in first-appearance order, transition counts between
743 * consecutive observations become the matrix, and `dtmc_makestochastic`
744 * normalizes it.
745 *
746 * FIRST-APPEARANCE ORDER, NOT SORTED ORDER, is a deliberate departure: MATLAB's
747 * `unique(...,'rows')` sorts, and reproducing a lexicographic sort over rows of
748 * an arbitrary element type would be a second, unstated, definition of order.
749 * The estimated chain is the same up to the permutation, and `state_space`
750 * carries the labelling, so a caller reading the two together is unaffected.
751 * A caller comparing raw matrix entries against MATLAB is, and should permute.
752 */
753template <class T>
755 const std::size_t obs = sample_state.rows(), cols = sample_state.cols();
756 if (obs < 2)
757 throw InputError(
758 "from_sample_sys_aggr: at least two observations are needed, since the estimate is "
759 "built from the transitions between consecutive ones");
760
761 std::vector<std::size_t> hash(obs);
762 std::vector<std::vector<T>> space;
763 for (std::size_t r = 0; r < obs; ++r) {
764 std::vector<T> row(cols);
765 for (std::size_t c = 0; c < cols; ++c) row[c] = sample_state(r, c);
766 std::size_t found = space.size();
767 for (std::size_t s = 0; s < space.size(); ++s) {
768 bool hit = true;
769 for (std::size_t c = 0; c < cols && hit; ++c) hit = (space[s][c] == row[c]);
770 if (hit) {
771 found = s;
772 break;
773 }
774 }
775 if (found == space.size()) space.push_back(row);
776 hash[r] = found;
777 }
778
779 const std::size_t n = space.size();
780 Matrix<T> counts(n, n, num_traits<T>::from_int(0));
781 for (std::size_t r = 1; r < obs; ++r)
782 counts(hash[r - 1], hash[r]) += num_traits<T>::from_int(1);
783
784 Matrix<T> ss(n, cols);
785 for (std::size_t s = 0; s < n; ++s)
786 for (std::size_t c = 0; c < cols; ++c) ss(s, c) = space[s][c];
787 return MarkovChainModel<T>::chain(counts, ss);
788}
789
790} // namespace processes
791} // namespace lang
792} // namespace line
793
794#endif // LINE_LANG_PROCESSES_MARKOV_CHAIN_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
Courtois decomposition of a nearly completely decomposable (NCD) CTMC.
Transient distribution of a CTMC by uniformization with Fox-Glynn Poisson weights.
Feasibility predicates for generators and stochastic matrices.
Koury-McAllister-Stewart aggregation-disaggregation for a nearly completely decomposable CTMC.
Two-level multigrid aggregation-disaggregation for a nearly completely decomposable CTMC.
First passage times into a target STATE SET, for Markov and semi-Markov chains.
Random infinitesimal generator of a CTMC.
Equilibrium distribution relative to a reference state.
Sensitivity of the steady-state distribution of a CTMC to a scalar parameter.
Sample path of a continuous-time Markov chain given its generator.
Steady-state distribution of a continuous-time Markov chain.
Limiting distribution of a CTMC whose generator may be reducible.
Takahashi's aggregation-disaggregation for a nearly completely decomposable CTMC.
Time-reversed generator and transition matrix.
Transient distribution of a CTMC by uniformization (Jensen's method), and the time-averaged distribut...
Normalize a non-negative matrix into a stochastic transition matrix.
Random DTMC kernels, trajectory simulation and the weak-component split.
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
Limiting distribution of a discrete-time Markov chain whose transition matrix may be reducible.
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
Discrete-time transient distributions, hitting times and uniformization.
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
MarkovChainModel< T > to_markov_chain(const MarkovChainModel< T > &m, const T &q)
MarkovProcess.toMarkovChain / toDTMC: the UNIFORMIZED chain, P = Q/q + I.
std::vector< T > hitting_time(const MarkovChainModel< T > &m, const std::vector< std::size_t > &target)
hittingTime: the mean time (CTMC) or step count (DTMC) to reach any state in target,...
TransientAtResult< T > chain_transient_unif(const MarkovChainModel< T > &m, const std::vector< T > &pi0in, const T &t)
MarkovChain.transientUnif: the DTMC read as the randomized image of a CTMC, so t is CONTINUOUS here w...
MarkovChainModel< T > rand_chain(std::size_t n, Gen &gen)
MarkovChain.rand: a random transition matrix of the given order.
MarkovChainModel< T > to_markov_process(const MarkovChainModel< T > &m)
MarkovChain.toMarkovProcess / toCTMC: read the DTMC as a CTMC with unit exit rates,...
std::vector< T > solve_relative(const MarkovChainModel< T > &m, std::size_t refstate=0)
MarkovProcess.solveRelative: the equilibrium vector normalized so that refstate carries one,...
MarkovChainModel< T > to_embedded(const MarkovChainModel< T > &m)
MarkovProcess.toEmbedded: the JUMP CHAIN, the DTMC of the states visited at transition epochs.
MarkovChainModel< T > to_dtmc(const MarkovChainModel< T > &m)
toDTMC, the backwards-compatible alias of toMarkovChain.
Matrix< T > stoch_comp(const MarkovChainModel< T > &m, const std::vector< std::size_t > &I=std::vector< std::size_t >())
stochComp: the complement alone.
std::vector< T > chain_solve(const MarkovChainModel< T > &m)
MarkovProcess.solve / MarkovChain.solve.
AggregateResult< T > aggregate_multi(const MarkovChainModel< T > &m, const std::vector< std::vector< std::size_t > > &MS, const std::vector< std::vector< std::size_t > > &MSS)
The "multi" arm of aggregate, separated because its parameter is a PARTITION OF THE PARTITION and not...
Matrix< T > chain_transient_steps(const MarkovChainModel< T > &m, const std::vector< T > &pi0in, std::size_t steps=1)
MarkovChain.transient: the law at every step 0..steps, one row per step.
T default_uniformization_rate(const Matrix< T > &Q)
The uniformization rate this port uses when the caller names none.
ChainPath< T > chain_sample(const MarkovChainModel< T > &m, const std::vector< T > &pi0in, std::size_t n, Gen &gen)
sample: simulate n steps.
TransientAtResult< T > chain_transient_at(const MarkovChainModel< T > &m, const std::vector< T > &pi0in, const T &t, const std::string &method="unif")
MarkovProcess.transient: the law at ONE time t, by uniformization.
TimeAverageOut< T > time_average(const MarkovChainModel< T > &m, const std::vector< T > &pi0in, const T &t)
MarkovProcess.timeAverage: the law averaged over [0,t], and its endpoint.
ProbStateResult< T > get_prob_state(const MarkovChainModel< T > &m, std::size_t i)
MarkovProcess.getProbState: the probability of ONE state by Cramer's rule.
StochCompOut< T > stoch_comp_full(const MarkovChainModel< T > &m, const std::vector< std::size_t > &I=std::vector< std::size_t >())
stochComp / stochCompFull for either kind.
AggregateResult< T > aggregate(const MarkovChainModel< T > &m, const std::vector< std::vector< std::size_t > > &MS, const std::string &method="courtois", const T *param=nullptr)
MarkovProcess.aggregate: aggregation-disaggregation over a macrostate partition.
MarkovChainModel< T > rand_process(std::size_t n, Gen &gen)
MarkovProcess.rand: a random generator of the given order.
std::size_t match_state(const MarkovChainModel< T > &m, const std::vector< T > &state)
Row index of state in the chain's state space, or n when it carries none.
MarkovChainModel< T > to_time_reversed(const MarkovChainModel< T > &m)
toTimeReversed for either kind: the chain run backwards in time.
std::vector< T > chain_sens(const MarkovChainModel< T > &m, const Matrix< T > &dQ)
MarkovProcess.sens: the derivative of the stationary law with respect to a scalar parameter,...
MarkovChainModel< T > from_sample_sys_aggr(const Matrix< T > &sample_state)
MarkovChain.fromSampleSysAggr: estimate a DTMC from an observed trajectory.
bool is_feasible(const MarkovChainModel< T > &m)
isFeasible: a valid generator, or a stochastic transition matrix.
ReducibleResult< T > dtmc_solve_reducible(const Matrix< T > &P, const std::vector< T > &pin, double zeroColTol=1e-12)
Limiting distribution of a discrete-time Markov chain whose transition matrix may be reducible.
Matrix< T > dtmc_makestochastic(const Matrix< T > &Pin)
Normalize a non-negative matrix into a stochastic transition matrix.
Matrix< T > ctmc_makeinfgen(const Matrix< T > &Q)
Set the diagonal so that every row sums to zero (ctmc_makeinfgen).
Definition ctmc_solve.h:58
KmsResult< T > ctmc_kms(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, std::size_t numSteps)
Koury-McAllister-Stewart aggregation-disaggregation for a nearly completely decomposable CTMC.
Definition ctmc_kms.h:100
Matrix< T > dtmc_stochcomp(const Matrix< T > &P, const std::vector< std::size_t > &keep)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
StochCompResult< T > ctmc_stochcomp(const Matrix< T > &Q, const std::vector< std::size_t > &I)
Definition dtmc_solve.h:150
UniformizationResult< T > ctmc_uniformization(const std::vector< T > &pi0, const Matrix< T > &Q, const T &t, double tol=1e-12, long maxiter=-1)
Transient distribution of a CTMC by uniformization (Jensen's method), and the time-averaged distribut...
Matrix< T > dtmc_timereverse(const Matrix< T > &P)
Transition matrix of the time-reversed DTMC.
TakahashiResult< T > ctmc_takahashi(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, std::size_t numSteps, double massTol=1e-14)
Takahashi's aggregation-disaggregation for a nearly completely decomposable CTMC.
Matrix< T > ctmc_rand(std::size_t n, Gen &gen)
Random infinitesimal generator of a CTMC.
Definition ctmc_rand.h:71
ReducibleResult< T > ctmc_solve_reducible(const Matrix< T > &Q, const std::vector< T > &pi0, double zeroColTol=1e-12)
Limiting distribution of a CTMC whose generator may be reducible.
std::vector< T > dtmc_hitting_time(const Matrix< T > &P, const std::vector< std::size_t > &target)
Expected number of steps to reach the target set, zero on the target set itself.
MultiResult< T > ctmc_multi(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, const std::vector< std::vector< std::size_t > > &MSS, const T &q)
Two-level multigrid aggregation-disaggregation for a nearly completely decomposable CTMC.
Definition ctmc_multi.h:61
TimeAverageResult< T > ctmc_timeaverage(const std::vector< T > &pi0, const Matrix< T > &Q, const T &t, double tol=1e-12, long maxiter=-1)
Time-averaged distribution (1/t) int_0^t pi(u) du, plus pi(t) itself.
int dtmc_isfeasible(const Matrix< T > &P)
Largest precision level 1..15 at which P is stochastic, or 0 when none holds.
UniformizationResult< T > dtmc_uniformization(const std::vector< T > &pi0, const Matrix< T > &P, const T &t, double tol=1e-12, long maxiter=-1)
Transient law of a DTMC through the uniformized generator of P.
bool ctmc_isfeasible(const Matrix< T > &Q, const T &tol)
True when Q is square, has nonnegative off-diagonals, nonpositive diagonal and zero row sums.
Matrix< T > dtmc_rand(std::size_t n, Gen &gen)
Random stochastic matrix, the uniformization of a random generator.
Definition dtmc_rand.h:45
Matrix< T > ctmc_timereverse(const Matrix< T > &Q)
Generator of the time-reversed CTMC.
std::vector< T > ctmc_relsolve(const Matrix< T > &Qin, std::size_t refstate)
Stationary measure scaled so that entry refstate equals one.
CtmcPath< T > ctmc_simulate(const Matrix< T > &Q, const std::vector< T > &pi0, std::size_t n, pfqn::McRng &rng)
Simulate n steps of the CTMC with generator Q.
Matrix< T > dtmc_transient(const Matrix< T > &P, const std::vector< T > &pi0, std::size_t steps)
Trajectory of the law over steps transitions, row k holding pi0 P^k.
FoxGlynnResult< T > ctmc_foxglynn(const std::vector< T > &pi0, const Matrix< T > &Q, const T &t, double tol=1e-12, long maxiter=-1)
Transient distribution of a CTMC by uniformization with Fox-Glynn Poisson weights.
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.
CourtoisResult< T > ctmc_courtois(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, const T &q)
Courtois decomposition of a nearly completely decomposable (NCD) CTMC.
std::vector< T > ctmc_sens(const Matrix< T > &Q, const Matrix< T > &dQ, const std::vector< T > &pi)
Sensitivity of the steady-state distribution of a CTMC to a scalar parameter.
Definition ctmc_sens.h:52
std::vector< std::size_t > dtmc_simulate(const Matrix< T > &P, const std::vector< T > &pi0, std::size_t n, Gen &gen)
Sample path of a DTMC, n states starting from pi0.
Definition dtmc_rand.h:56
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
T num_abs(const T &v)
Definition number.h:172
T lu_det(const Matrix< T > &A)
Determinant of a square matrix, by the same partial-pivoting elimination.
Definition lu.h:122
Number-type abstraction for the templated API port.
What aggregate returns: the approximate law and the two NCD indices.
T eps
nearly-complete-decomposability index of the partition
T epsMAX
the largest index for which the approximation is meant to hold
std::vector< T > p
approximate stationary vector, ORIGINAL state ordering
A sampled path: the states visited, with their holding times for a CTMC.
std::vector< T > sojourn
holding times; empty for a DTMC, which has none
std::vector< std::size_t > states
0-based state index at each step
A user-supplied chain: a MarkovProcess when discrete is false, else a MarkovChain.
Matrix< T > mat
generator Q (CTMC) or transition matrix P (DTMC)
Matrix< T > state_space
optional; empty means the chain carries none
bool discrete
true for a MarkovChain
static MarkovChainModel< T > chain(const Matrix< T > &transmat, const Matrix< T > &space=Matrix< T >())
static MarkovChainModel< T > process(const Matrix< T > &infgen, const Matrix< T > &space=Matrix< T >())
Mirrors the constructors: a generator is closed, a transition matrix normalized.
What getProbState returns: the probability and the two determinants behind it.
T num
determinant of the numerator matrix
T den
determinant of the denominator matrix
What stochCompFull returns for a CTMC; a DTMC fills the same blocks from P.
Matrix< T > A11
the four blocks of the partitioned matrix
Matrix< T > S
the complement on the selected states
Matrix< T > T12
the return-path term, so that S = A11 + T12
std::vector< T > pi_exit
law at t
std::vector< T > pi_time_avg
time-averaged law over [0, t]
What the CTMC transient returns: the law at t and the truncation it used.
std::vector< T > pi
distribution at time t
std::size_t kmax
Poisson terms used (the right truncation point for 'foxglynn').
T eps
NCD index: largest ROW sum of B, ||B||_inf (MATLAB and the JAR).
std::vector< T > p
approximate stationary vector, ORIGINAL state ordering
T epsMAX
(1 - max subdominant block eigenvalue modulus) / 2
One simulated sample path: the state visited at each step and its holding time.
std::vector< std::size_t > states
0-based state index at each step
std::vector< T > sojourn
holding time spent in that state
long right
right truncation point
std::vector< T > pi
distribution at time t
T epsMAX
maximum admissible NCD index
Definition ctmc_kms.h:63
T eps
NCD index, as ctmc_courtois defines it.
Definition ctmc_kms.h:62
std::vector< T > p
estimate after numSteps sweeps, ORIGINAL ordering
Definition ctmc_kms.h:58
std::vector< T > p
approximate stationary vector, ORIGINAL ordering
Definition ctmc_multi.h:44
T eps
NCD index of the fine level.
Definition ctmc_multi.h:47
T epsMAX
maximum admissible NCD index of the fine level
Definition ctmc_multi.h:48
Matrix< T > S
stochastic complement on the selected states
Definition dtmc_solve.h:137
Matrix< T > Q11
the four blocks, as MATLAB returns them
Definition dtmc_solve.h:138
Matrix< T > T12
Q12 (-Q22)^-1 Q21, the correction term.
Definition dtmc_solve.h:142
T eps
NCD index, as ctmc_courtois defines it.
T epsMAX
maximum admissible NCD index
std::vector< T > p
estimate after numSteps sweeps
std::vector< T > piTimeAvg
time-averaged distribution over [0, t]
std::vector< T > piExit
distribution at time t
std::size_t kmax
number of Poisson terms actually used
std::vector< T > pi
distribution at time t