LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_testpf_kolmogorov.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_TESTPF_KOLMOGOROV_H
6#define LINE_API_MC_CTMC_TESTPF_KOLMOGOROV_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Kolmogorov reversibility criterion, used as a product-form test.
12 *
13 * Templated port of jar/src/main/java/jline/api/mc/Ctmc_testpf_kolmogorov.java,
14 * which has no MATLAB twin. Kolmogorov's criterion states that an irreducible
15 * chain is reversible iff for every cycle c0 -> c1 -> ... -> c0 the product of
16 * the rates around it equals the product around the reverse cycle,
17 * prod_i q(c_i, c_i+1) = prod_i q(c_i+1, c_i),
18 * both products taken on the SAME generator. A cycle whose reverse edges are
19 * not all present fails outright.
20 *
21 * REFERENCE DEFECT (fixed here and in the JAR, 2026-08-01). The reference took
22 * the reverse product on the time-reversed generator Qr instead of on Q. Since
23 * Qr(a,b) = Q(b,a) pi_b / pi_a, that product is
24 * prod_i Q(c_i, c_i+1) pi_c_i / pi_c_i+1,
25 * whose pi factors telescope to 1 around any cycle, so it equals the forward
26 * product identically and the test returned true for every chain, reversible or
27 * not. The two formulations agree exactly when the chain IS reversible, which is
28 * why the defect never showed up as a wrong "false".
29 *
30 * COST. Every simple cycle through every edge is enumerated, so the work is
31 * exponential in the number of states. That is the reference algorithm and is
32 * kept: the callers apply it to small chains only.
33 *
34 * ARITHMETIC: the tolerance 1e-6 on the relative gap is a double comparison, so
35 * the verdict is a floating-point one even at Rational.
36 */
37
38#include <cmath>
39#include <cstddef>
40#include <vector>
41
43#include "line/num/number.h"
44#include "line/util/error.h"
45#include "line/util/matrix.h"
46
47namespace line {
48namespace mc {
49
50namespace detail {
51
52/**
53 * Every simple path from `start` to `target` in the adjacency matrix `adj`,
54 * avoiding the states already marked used. A path is returned as the list of
55 * its states, `start` first and `target` last.
56 */
57template <class T>
58void kolmogorov_paths(const Matrix<T>& adj, std::vector<bool>& used, std::vector<std::size_t>& path,
59 std::size_t start, std::size_t target,
60 std::vector<std::vector<std::size_t>>& out) {
61 const T zero = num_traits<T>::from_int(0);
62 const bool wasUsed = used[start];
63 used[start] = true;
64 path.push_back(start);
65 if (start == target) {
66 out.push_back(path);
67 } else {
68 for (std::size_t j = 0; j < adj.cols(); ++j)
69 if (adj(start, j) > zero && !used[j]) kolmogorov_paths(adj, used, path, j, target, out);
70 }
71 path.pop_back();
72 used[start] = wasUsed;
73}
74
75} // namespace detail
76
77/**
78 * @brief Kolmogorov reversibility criterion, used as a product-form test.
79 *
80 * @param Qin generator of an irreducible CTMC
81 * @return true when Kolmogorov's criterion holds on every simple cycle
82 */
83template <class T>
85 const std::size_t n = Qin.rows();
86 if (Qin.cols() != n) throw InputError("ctmc_testpf_kolmogorov: generator is not square");
87 const T zero = num_traits<T>::from_int(0);
88
89 // Clip the negative off-diagonals, then reset the diagonal to close the rows.
90 Matrix<T> Q = Qin;
91 for (std::size_t i = 0; i < n; ++i)
92 for (std::size_t j = 0; j < n; ++j)
93 if (i != j && Q(i, j) < zero) Q(i, j) = zero;
94 Q = ctmc_makeinfgen(Q);
95
96 Matrix<T> A(n, n, zero);
97 const T one = num_traits<T>::from_int(1);
98 for (std::size_t i = 0; i < n; ++i)
99 for (std::size_t j = 0; j < n; ++j)
100 if (i != j && Q(i, j) > zero) A(i, j) = one;
101
102 for (std::size_t start = 0; start < n; ++start) {
103 for (std::size_t target = 0; target < n; ++target) {
104 if (target == start || !(A(target, start) > zero)) continue;
105 std::vector<bool> used(n, false);
106 std::vector<std::size_t> path;
107 std::vector<std::vector<std::size_t>> cycles;
108 detail::kolmogorov_paths(A, used, path, start, target, cycles);
109
110 for (std::size_t c = 0; c < cycles.size(); ++c) {
111 std::vector<std::size_t> cyc = cycles[c];
112 cyc.push_back(start); // close the cycle back onto its origin
113 T q = one, qr = one;
114 for (std::size_t i = 0; i + 1 < cyc.size(); ++i) {
115 q = T(q * Q(cyc[i], cyc[i + 1]));
116 qr = T(qr * Q(cyc[i + 1], cyc[i]));
117 }
118 const double qd = num_traits<T>::to_double(q);
119 const double qrd = num_traits<T>::to_double(qr);
120 if (qd == 0.0) continue;
121 if (std::fabs(qd - qrd) / std::fabs(qd) > 1e-6) return false;
122 }
123 }
124 }
125 return true;
126}
127
128} // namespace mc
129} // namespace line
130
131#endif // LINE_API_MC_CTMC_TESTPF_KOLMOGOROV_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
Steady-state distribution of a continuous-time Markov chain.
The exception types the port throws.
Dense matrix and non-owning view.
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
bool ctmc_testpf_kolmogorov(const Matrix< T > &Qin)
Kolmogorov reversibility criterion, used as a product-form test.
Number-type abstraction for the templated API port.