LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Toggle main menu visibility
Loading...
Searching...
No Matches
ctmc_simulate.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_SIMULATE_H
6
#define LINE_API_MC_CTMC_SIMULATE_H
7
8
/**
9
* @file
10
* @ingroup api_mc
11
* Sample path of a continuous-time Markov chain given its generator.
12
*
13
* Templated port of matlab/src/api/mc/ctmc_simulate.m. The chain is simulated
14
* by the standard jump-chain construction: from state i the holding time is
15
* exponential with mean -1/Q(i,i), and the next state is drawn from the
16
* embedded jump chain P(i,j) = Q(i,j) / sum_{k != i} Q(i,k).
17
*
18
* This is the one entry point in this batch that takes MATRICES and not an
19
* `sn`: its arguments are the generator, an initial distribution and a step
20
* count, so it is portable independently of the NetworkStruct layer.
21
*
22
* ARITHMETIC. Gated on num_traits<T>::has_transcendental. The holding times
23
* are exponential deviates, drawn by inverse transform as -mean * log(u), so
24
* there is no exact instantiation: the sample path is a realization, not a
25
* number that a rational field could represent.
26
*
27
* RANDOMNESS. The generator is `line::pfqn::McRng` (a std::mt19937_64) passed
28
* by reference and advanced by the call, the same convention every Monte Carlo
29
* entry point in this tree uses, so a caller controls reproducibility by
30
* controlling the generator state. The stream is NOT comparable with MATLAB's
31
* -- different generator, different mapping from bits to deviates -- so the
32
* oracle for this function is distributional, never path-for-path. Two
33
* generator steps are consumed per simulated step: one for the holding time
34
* and one for the jump.
35
*
36
* REFERENCE DEFECT: the initial state is not drawn from pi0.
37
*
38
* ctmc_simulate.m selects the starting state with
39
*
40
* [~, st] = min(abs(rand - cumsum(pi0)));
41
*
42
* which returns the state whose CUMULATIVE probability is nearest to the
43
* uniform deviate, ties going to the lowest index because min returns the
44
* first minimizer. That partitions [0,1] at the MIDPOINTS between consecutive
45
* DISTINCT cumulative values c_k = sum_{j<=k} p_j, instead of at the
46
* cumulative values themselves. Where the c_k are distinct this reduces to
47
*
48
* P(1) = p_1 + p_2/2, P(k) = (p_k + p_{k+1})/2, P(n) = p_n/2,
49
*
50
* i.e. the LAST state always receives about half its intended mass and the
51
* first receives an excess. Where two consecutive c_k coincide -- which is
52
* exactly what a zero-probability entry produces -- the first of them takes
53
* the whole window and the rest get nothing, so the naive midpoint reading
54
* above does NOT apply there. Measured in MATLAB over 400000 draws:
55
*
56
* pi0 = [0.5 0.5] -> [0.7498 0.2502] (want [0.5 0.5])
57
* pi0 = [1/3 1/3 1/3] -> [0.4997 0.3335 0.1668] (want [1/3 1/3 1/3])
58
* pi0 = [0.9 0 0.1] -> [0.9497 0.0000 0.0503] (want [0.9 0 0.1])
59
*
60
* the last of which is also reproducible through the entry point itself,
61
* ctmc_simulate(Q, [0.9;0;0.1], 1) over 20000 calls giving
62
* [0.9488 0.0000 0.0512]. The error washes out of a long ergodic run -- the
63
* time-average occupancy of a two-state chain still converges to the exact
64
* stationary law -- so it is invisible in steady-state use and corrupts
65
* exactly the transient and short-run use that passing pi0 is for.
66
*
67
* The port draws the initial state by correct inverse transform. Reproducing
68
* the defect was rejected: a sampler that does not sample from the
69
* distribution it is handed has no contract left to preserve, and unlike a
70
* closed-form value there is nothing downstream that could be calibrated
71
* against the wrong answer. `ctmc_simulate_reference_initial_law` below
72
* returns the law the reference actually realizes, so the discrepancy is
73
* available to a caller as a value rather than only as prose.
74
*
75
* A second, smaller divergence: an ABSORBING state (a row whose off-diagonal
76
* entries are all zero) gives MATLAB `F = 0/0 = NaN` on that row, after which
77
* `find(rand - NaN > 0)` is empty and the chain silently jumps to state 1,
78
* while the holding time is exprnd(Inf) = Inf. The port raises NumericError
79
* naming the state instead: there is no Inf in an exact field, and a silent
80
* jump to state 1 is not a property of the chain.
81
*/
82
83
#include <cmath>
84
#include <cstddef>
85
#include <vector>
86
87
#include "
line/api/pfqn/pfqn_mc_common.h
"
88
#include "
line/num/number.h
"
89
#include "
line/util/error.h
"
90
#include "
line/util/matrix.h
"
91
92
namespace
line
{
93
namespace
mc
{
94
95
/** One simulated sample path: the state visited at each step and its holding time. */
96
template
<
class
T>
97
struct
CtmcPath
{
98
std::vector<std::size_t>
states
;
///< 0-based state index at each step
99
std::vector<T>
sojourn
;
///< holding time spent in that state
100
};
101
102
/**
103
* The initial-state law that ctmc_simulate.m actually realizes for a given
104
* pi0, as opposed to pi0 itself. Provided so a caller can measure the
105
* reference defect documented above rather than take it on trust; it is not
106
* used by the simulation.
107
*/
108
template
<
class
T>
109
std::vector<T>
ctmc_simulate_reference_initial_law
(
const
std::vector<T>& pi0) {
110
const
std::size_t n = pi0.size();
111
if
(n == 0)
throw
InputError
(
"ctmc_simulate_reference_initial_law: empty distribution"
);
112
const
T zero =
num_traits<T>::from_int
(0);
113
const
T one =
num_traits<T>::from_int
(1);
114
const
T half =
num_traits<T>::from_rational
(1, 2);
115
116
T tot = zero;
117
for
(std::size_t i = 0; i < n; ++i) {
118
if
(pi0[i] < zero)
119
throw
InputError
(
"ctmc_simulate_reference_initial_law: negative entry"
);
120
tot += pi0[i];
121
}
122
if
(tot == zero)
throw
InputError
(
"ctmc_simulate_reference_initial_law: zero total mass"
);
123
124
std::vector<T> c(n);
125
{
126
T run = zero;
127
for
(std::size_t i = 0; i < n; ++i) {
128
run += pi0[i] / tot;
129
c[i] = run;
130
}
131
}
132
133
// tie-breaking rationale: see _kb/03-api-layer.md (cpp port notes: mc)
134
std::vector<std::size_t> rep;
135
for
(std::size_t k = 0; k < n; ++k)
136
if
(rep.empty() || c[k] != c[rep.back()]) rep.push_back(k);
137
138
std::vector<T> q(n, zero);
139
const
std::size_t M = rep.size();
140
for
(std::size_t m = 0; m < M; ++m) {
141
const
T lo = (m == 0) ? zero : T(half * (c[rep[m - 1]] + c[rep[m]]));
142
const
T hi = (m + 1 == M) ? one : T(half * (c[rep[m]] + c[rep[m + 1]]));
143
q[rep[m]] = hi - lo;
144
}
145
return
q;
146
}
147
148
/**
149
* Simulate n steps of the CTMC with generator Q.
150
*
151
* @param Q (m x m) generator, negative diagonal and zero row sums
152
* @param pi0 initial distribution; empty draws it uniformly at random and
153
* normalizes, as the reference does
154
* @param n number of steps
155
* @param rng generator, advanced by the call
156
*/
157
template
<
class
T>
158
CtmcPath<T>
ctmc_simulate
(
const
Matrix<T>
& Q,
const
std::vector<T>& pi0, std::size_t n,
159
pfqn::McRng
&
rng
) {
160
static_assert
(
num_traits<T>::has_transcendental
,
161
"ctmc_simulate requires transcendental arithmetic: the holding times are "
162
"exponential deviates drawn as -mean * log(u)"
);
163
const
std::size_t m = Q.
rows
();
164
if
(Q.
cols
() != m)
throw
InputError
(
"ctmc_simulate: the generator is not square"
);
165
if
(m == 0)
throw
InputError
(
"ctmc_simulate: empty generator"
);
166
const
T zero =
num_traits<T>::from_int
(0);
167
168
// Initial distribution: uniform random and normalized when not supplied,
169
// which is what `r = rand(length(Q),1); pi0 = r/sum(r)` does.
170
std::vector<T> p0;
171
if
(pi0.empty()) {
172
p0.resize(m);
173
T tot = zero;
174
for
(std::size_t i = 0; i < m; ++i) {
175
p0[i] =
pfqn::mc_uniform<T>
(
rng
);
176
tot += p0[i];
177
}
178
if
(tot == zero)
throw
NumericError
(
"ctmc_simulate: degenerate random initial distribution"
);
179
for
(std::size_t i = 0; i < m; ++i) p0[i] /= tot;
180
}
else
{
181
if
(pi0.size() != m)
182
throw
InputError
(
"ctmc_simulate: pi0 has the wrong length for the generator"
);
183
T tot = zero;
184
for
(std::size_t i = 0; i < m; ++i) {
185
if
(pi0[i] < zero)
throw
InputError
(
"ctmc_simulate: pi0 has a negative entry"
);
186
tot += pi0[i];
187
}
188
if
(tot == zero)
throw
InputError
(
"ctmc_simulate: pi0 has zero total mass"
);
189
p0 = pi0;
190
for
(std::size_t i = 0; i < m; ++i) p0[i] /= tot;
191
}
192
193
// Row-normalized cumulative jump probabilities over the off-diagonal.
194
Matrix<T>
F(m, m, zero);
195
for
(std::size_t i = 0; i < m; ++i) {
196
T run = zero;
197
for
(std::size_t j = 0; j < m; ++j) {
198
if
(j != i) {
199
if
(Q(i, j) < zero)
200
throw
InputError
(
"ctmc_simulate: negative off-diagonal rate in the generator"
);
201
run += Q(i, j);
202
}
203
F(i, j) = run;
204
}
205
if
(run == zero)
206
throw
NumericError
(
207
"ctmc_simulate: state "
+ std::to_string(i) +
208
" is absorbing (no outgoing rate), so the sample path cannot be continued; "
209
"the reference silently jumps to state 1 here with an infinite holding time"
);
210
for
(std::size_t j = 0; j < m; ++j) F(i, j) /= run;
211
}
212
213
// Correct inverse transform for the initial state; see the header note.
214
std::size_t st = m - 1;
215
{
216
const
T u =
pfqn::mc_uniform<T>
(
rng
);
217
T run = zero;
218
for
(std::size_t i = 0; i < m; ++i) {
219
run += p0[i];
220
if
(u < run) {
221
st = i;
222
break
;
223
}
224
}
225
}
226
227
CtmcPath<T>
path;
228
path.
states
.reserve(n);
229
path.
sojourn
.reserve(n);
230
using
std::log;
231
for
(std::size_t k = 0; k < n; ++k) {
232
path.
states
.push_back(st);
233
const
T rate = -Q(st, st);
234
if
(!(rate > zero))
235
throw
NumericError
(
"ctmc_simulate: state "
+ std::to_string(st) +
236
" has a non-negative diagonal, the holding time is undefined"
);
237
// Exponential of mean 1/rate by inverse transform. u is drawn on
238
// [0,1), so 1-u is in (0,1] and the logarithm is always finite.
239
const
T u =
pfqn::mc_uniform<T>
(
rng
);
240
path.
sojourn
.push_back(T(-log(T(
num_traits<T>::from_int
(1) - u)) / rate));
241
242
const
T v =
pfqn::mc_uniform<T>
(
rng
);
243
std::size_t nxt = m - 1;
244
for
(std::size_t j = 0; j < m; ++j) {
245
if
(v < F(st, j)) {
246
nxt = j;
247
break
;
248
}
249
}
250
st = nxt;
251
}
252
return
path;
253
}
254
255
}
// namespace mc
256
}
// namespace line
257
258
#endif
// LINE_API_MC_CTMC_SIMULATE_H
line::InputError::InputError
InputError(const std::string &what)
Definition
error.h:39
line::Matrix
Definition
matrix.h:56
line::Matrix::cols
std::size_t cols() const
Definition
matrix.h:90
line::Matrix::rows
std::size_t rows() const
Definition
matrix.h:89
line::NumericError::NumericError
NumericError(const std::string &what)
Definition
error.h:45
error.h
The exception types the port throws.
matrix.h
Dense matrix and non-owning view.
line::mc
Definition
ctmc_bicgstab.h:59
line::mc::ctmc_simulate_reference_initial_law
std::vector< T > ctmc_simulate_reference_initial_law(const std::vector< T > &pi0)
The initial-state law that ctmc_simulate.m actually realizes for a given pi0, as opposed to pi0 itsel...
Definition
ctmc_simulate.h:109
line::mc::ctmc_simulate
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.
Definition
ctmc_simulate.h:158
line::pfqn::McRng
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
Definition
pfqn_mc_common.h:62
line::pfqn::mc_uniform
T mc_uniform(McRng &g)
The same deviate materialized in the working arithmetic.
Definition
pfqn_mc_common.h:75
line::rng
Definition
rng_ssj.h:56
line
Definition
aoi_dist2ph.h:52
number.h
Number-type abstraction for the templated API port.
pfqn_mc_common.h
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
line::mc::CtmcPath
One simulated sample path: the state visited at each step and its holding time.
Definition
ctmc_simulate.h:97
line::mc::CtmcPath::states
std::vector< std::size_t > states
0-based state index at each step
Definition
ctmc_simulate.h:98
line::mc::CtmcPath::sojourn
std::vector< T > sojourn
holding time spent in that state
Definition
ctmc_simulate.h:99
line::num_traits
Definition
number.h:111
include
line
api
mc
ctmc_simulate.h
Generated by
1.18.0