LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_sample.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_SOLVERS_CTMC_SOLVER_CTMC_SAMPLE_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_SAMPLE_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of the `@@SolverCTMC` sampling surface: `sample`, `sampleAggr`,
12 * `sampleSys`, `sampleSysAggr`.
13 *
14 * WHAT IS BEING SAMPLED, and why it is not simulation in the SSA sense: the
15 * chain has already been BUILT and solved, so a sample path here is a walk on a
16 * generator that is exactly right, not a Monte Carlo estimate of one. The
17 * randomness is in the path, not in the model, and a longer run buys a longer
18 * trace rather than a more accurate answer.
19 *
20 * The reference builds the walk by treating the whole generator as a MARKED
21 * Markovian arrival process -- D1 = sum_a filt[a], D0 = Q - D1, one mark per
22 * synchronization -- and calling `mmap_sample`. The mark is what makes the
23 * trace usable: it says WHICH event fired, which the state sequence alone does
24 * not determine, because two synchronizations can carry the chain between the
25 * same pair of states. This port samples that MMAP directly rather than
26 * routing through a general MMAP sampler, which is the same walk with the
27 * per-event decomposition already in hand.
28 */
29
30#include <cmath>
31#include <cstddef>
32#include <string>
33#include <vector>
34
39#include "line/util/error.h"
40#include "line/util/matrix.h"
41
42namespace line {
43namespace ctmc {
44
45/** One sampled trajectory of the chain. */
46template <class T>
48 std::vector<T> t; ///< time at which each state was ENTERED
49 std::vector<std::size_t> state; ///< 0-based index into `chain.space`
50 std::vector<std::size_t> event; ///< synchronization that fired to LEAVE it
52};
53
54/**
55 * Port of `@@SolverCTMC/sampleSys`: a marked walk on the whole network state.
56 *
57 * Requires the event filtration, so `opt.keep_filtration` is forced on -- the
58 * mark cannot be recovered from Q, whose entries have already summed every
59 * synchronization's contribution.
60 *
61 * @param nevents number of transitions to draw
62 * @param seed the stream; two runs are the same trace only if this matches
63 * @param sn the refreshed network struct
64 * @param opt_in CTMC options (state-space cutoff, tolerances, method)
65 */
66template <class T>
68 std::size_t nevents, unsigned long seed = 23000) {
70 "solver_ctmc_sample_sys draws exponential holding times as -mean*log(u), which "
71 "is transcendental; use --arith double or real");
72 assert_phase_type_states(sn, "sampleSys");
73
74 CtmcOptions opt = opt_in;
75 opt.keep_filtration = true;
78 const std::size_t n = out.chain.chain.space.size();
79 const std::size_t A = out.chain.chain.filt.size();
80
81 // The walk starts at the model's default initial state, as the reference
82 // does with `pi0(matchrow(stateSpace,s0)) = 1`. A missing initial state is
83 // an error here rather than a fallback: a trace has to start somewhere
84 // specific for its event sequence to mean anything.
85 std::size_t cur = analyzer_detail::init_state_index(sn, out.chain.chain.space);
86 if (cur == static_cast<std::size_t>(-1))
87 throw InputError(
88 "solver_ctmc_sample_sys: the initial state is not contained in the state space, so "
89 "there is no state to start the trace from");
90
91 pfqn::McRng rng(seed);
92 T now = num_traits<T>::from_int(0);
93 for (std::size_t step = 0; step < nevents; ++step) {
94 out.t.push_back(now);
95 out.state.push_back(cur);
96
97 // The total exit rate is minus the diagonal, which is what makes the
98 // holding time exponential with that mean.
99 const double exit = -num_traits<T>::to_double(out.chain.chain.Q(cur, cur));
100 if (!(exit > 0)) {
101 // An absorbing state: the chain stays there forever, so the trace
102 // ends rather than being padded with fictitious transitions.
103 out.event.push_back(static_cast<std::size_t>(-1));
104 break;
105 }
106 now = T(now + num_traits<T>::from_double(
108
109 // Draw the (event, destination) pair proportionally to the rate each
110 // synchronization contributes -- the MMAP mark and the jump together,
111 // because they are not independent.
112 const double u = num_traits<T>::to_double(pfqn::mc_uniform<T>(rng)) * exit;
113 double acc = 0;
114 std::size_t pick_a = static_cast<std::size_t>(-1), pick_s = cur;
115 for (std::size_t a = 0; a < A && pick_a == static_cast<std::size_t>(-1); ++a)
116 for (std::size_t j = 0; j < n; ++j) {
117 if (j == cur) continue; // a self-loop leaves the chain where it is
118 const double w = num_traits<T>::to_double(out.chain.chain.filt[a](cur, j));
119 if (w <= 0) continue;
120 acc += w;
121 if (acc >= u) {
122 pick_a = a;
123 pick_s = j;
124 break;
125 }
126 }
127 // Rounding can leave `u` just past the accumulated total; fall back to
128 // the last positive entry rather than stalling the walk.
129 if (pick_a == static_cast<std::size_t>(-1))
130 for (std::size_t a = 0; a < A; ++a)
131 for (std::size_t j = 0; j < n; ++j)
132 if (j != cur && num_traits<T>::to_double(out.chain.chain.filt[a](cur, j)) > 0) {
133 pick_a = a;
134 pick_s = j;
135 }
136 out.event.push_back(pick_a);
137 cur = pick_s;
138 }
139 return out;
140}
141
142/**
143 * Port of `@@SolverCTMC/sampleSysAggr`: the same walk, reported as per-(station,
144 * class) job counts rather than as detailed states.
145 *
146 * @return row `i` is the aggregate state at `path.t[i]`, in `(ist-1)*K + k`
147 * column order
148 */
149template <class T>
151 const CtmcSamplePath<T>& path) {
152 const Matrix<T> A = ctmc_state_space_aggr(sn, path.chain.chain.space);
153 Matrix<T> out(path.state.size(), A.cols(), num_traits<T>::from_int(0));
154 for (std::size_t i = 0; i < path.state.size(); ++i)
155 for (std::size_t c = 0; c < A.cols(); ++c) out(i, c) = A(path.state[i], c);
156 return out;
157}
158
159/**
160 * Port of `@@SolverCTMC/sample`: the walk restricted to ONE stateful node's
161 * local block.
162 *
163 * @param ind 1-based node index
164 * @param sn the refreshed network struct
165 * @param path sample path to label
166 * @return row `i` is that node's local state at `path.t[i]`
167 */
168template <class T>
170 std::size_t ind) {
171 const std::size_t isf = sn.stateful_index(ind);
172 if (isf == 0) throw InputError("solver_ctmc_sample: node " + std::to_string(ind) +
173 " is not stateful, so it has no state to sample");
174 const std::size_t w = path.chain.chain.space[0].local[isf - 1].size();
175 Matrix<T> out(path.state.size(), w, num_traits<T>::from_int(0));
176 for (std::size_t i = 0; i < path.state.size(); ++i)
177 for (std::size_t c = 0; c < w; ++c)
178 out(i, c) = path.chain.chain.space[path.state[i]].local[isf - 1][c];
179 return out;
180}
181
182/** Port of `@@SolverCTMC/sampleAggr`: one node's per-class counts over time. */
183template <class T>
185 std::size_t ind) {
186 const std::size_t isf = sn.stateful_index(ind);
187 if (isf == 0) throw InputError("solver_ctmc_sample_aggr: node " + std::to_string(ind) +
188 " is not stateful, so it has no state to sample");
189 const std::size_t K = sn.nclasses;
190 Matrix<T> out(path.state.size(), K, num_traits<T>::from_int(0));
191 for (std::size_t i = 0; i < path.state.size(); ++i) {
192 const std::vector<T> m = prob_detail::marginal_of(
193 sn, ind, path.chain.chain.space[path.state[i]].local[isf - 1]);
194 for (std::size_t k = 0; k < K && k < m.size(); ++k) out(i, k) = m[k];
195 }
196 return out;
197}
198
199} // namespace ctmc
200} // namespace line
201
202#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_SAMPLE_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
void assert_phase_type_states(const NetworkStruct< T > &sn, const std::string &what)
Port of @@SolverCTMC/assertPhaseTypeStates: refuse a query whose answer would be a per-state probabil...
Matrix< T > solver_ctmc_sample(const NetworkStruct< T > &sn, const CtmcSamplePath< T > &path, std::size_t ind)
Port of @@SolverCTMC/sample: the walk restricted to ONE stateful node's local block.
Matrix< T > solver_ctmc_sample_aggr(const NetworkStruct< T > &sn, const CtmcSamplePath< T > &path, std::size_t ind)
Port of @@SolverCTMC/sampleAggr: one node's per-class counts over time.
CtmcSamplePath< T > solver_ctmc_sample_sys(const NetworkStruct< T > &sn, const CtmcOptions &opt_in, std::size_t nevents, unsigned long seed=23000)
Port of @@SolverCTMC/sampleSys: a marked walk on the whole network state.
CtmcSolution< T > solver_ctmc_analyzer(const NetworkStruct< T > &sn_in, const CtmcOptions &opt)
Port of solver_ctmc_analyzer.m plus the fork-join wrapper of @@SolverCTMC/runAnalyzer....
Matrix< T > ctmc_state_space_aggr(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space)
Port of StateSpaceAggr: the per-(station, class) job counts of every state, as an (nstates x nstation...
Matrix< T > solver_ctmc_sample_sys_aggr(const NetworkStruct< T > &sn, const CtmcSamplePath< T > &path)
Port of @@SolverCTMC/sampleSysAggr: the same walk, reported as per-(station, class) job counts rather...
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
T mc_uniform(McRng &g)
The same deviate materialized in the working arithmetic.
A queueing network and its refreshed NetworkStruct.
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
The SolverCTMC probability family: solver_ctmc_joint, _jointaggr, _marg, _margaggr,...
The SolverCTMC knobs this port honours.
One sampled trajectory of the chain.
std::vector< std::size_t > event
synchronization that fired to LEAVE it
std::vector< std::size_t > state
0-based index into chain.space
std::vector< T > t
time at which each state was ENTERED
Everything one CTMC solve produces.