LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_prob.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_PROB_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_PROB_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The SolverCTMC probability family: `solver_ctmc_joint`, `_jointaggr`,
12 * `_marg`, `_margaggr`, and the gate `@@SolverCTMC/assertPhaseTypeStates` puts
13 * in front of all four.
14 *
15 * THE DISTINCTION THE FOUR NAMES ENCODE, because it is easy to mix up:
16 *
17 * joint P(the whole network is in exactly this state), phases included
18 * jointaggr P(the whole network holds exactly these per-class counts),
19 * summed over every phase and buffer arrangement that realizes them
20 * marg per STATION, P(that station is in exactly this local state)
21 * margaggr per STATION, P(that station holds exactly these per-class counts)
22 *
23 * `joint` and `marg` are per-state answers and `jointaggr` and `margaggr` are
24 * aggregates of them, which is precisely why the ME gate below applies to all
25 * four and not only to the first two: an aggregate over PHASES is a probability
26 * under a matrix-exponential, but an aggregate over STATES sharing a marginal
27 * is not, because the sum still runs over signed terms.
28 */
29
30#include <cstddef>
31#include <string>
32#include <vector>
33
35#include "line/lang/qn/state.h"
37#include "line/util/error.h"
38#include "line/util/lu.h"
39#include "line/util/matrix.h"
40
41namespace line {
42namespace ctmc {
43
44/**
45 * Port of `@@SolverCTMC/assertPhaseTypeStates`: refuse a query whose answer
46 * would be a per-state probability under a matrix-exponential process.
47 *
48 * A matrix-exponential embeds in the generator with POSITIVE off-diagonal
49 * entries in D0 (equivalently a signed entry vector), so the stationary vector
50 * is a SIGNED measure: only its aggregates over each phase block are
51 * probabilities. Mean measures stay exact, being linear in that vector, but a
52 * per-state or transient answer is not a probability at all, and uniformization
53 * -- a Poisson mixture of powers of I + Q/lambda -- diverges on a signed
54 * generator. Such queries are refused rather than answered with a number that
55 * looks like a probability.
56 *
57 * THE TEST IS ON THE MATRICES, not on a flag. MATLAB carries `sn.isph`, which
58 * this NetworkStruct has no counterpart for; the property it records is exactly
59 * "D0 has no positive off-diagonal and D1 is non-negative", so that is what is
60 * checked here rather than a field being invented to hold the answer.
61 */
62template <class T>
63void assert_phase_type_states(const NetworkStruct<T>& sn, const std::string& what) {
64 // Same matrix test the avg reduction gates its clamp on, so one predicate
65 // decides both "refuse a per-state answer" and "keep the negative mass".
67 throw UnsupportedError(
68 what +
69 " is unavailable: the model has a matrix-exponential (ME) service or arrival "
70 "process, so the stationary vector of the generator is a signed measure and "
71 "per-state probabilities and uniformization-based transients do not exist. "
72 "Mean measures (getAvg, getAvgTable) remain exact");
73}
74
75namespace prob_detail {
76
77/** Left-pad a local row with zeros to the width the enumerated space uses. */
78template <class T>
79std::vector<T> align(const std::vector<T>& row, std::size_t width) {
80 if (row.size() >= width) return row;
81 std::vector<T> out(width - row.size(), num_traits<T>::from_int(0));
82 out.insert(out.end(), row.begin(), row.end());
83 return out;
84}
85
86/** The per-class marginal of one stateful node's local row, or empty. */
87template <class T>
88std::vector<T> marginal_of(const NetworkStruct<T>& sn, std::size_t ind,
89 const std::vector<T>& row) {
90 const std::size_t R = sn.nclasses;
91 const std::size_t ist = sn.nodes[ind - 1].station;
92 if (ist == 0) {
93 // A stateful non-station -- a Cache -- carries one count column per
94 // class ahead of its own local block, which is the marginal itself.
95 return std::vector<T>(row.begin(), row.begin() + std::min(R, row.size()));
96 }
97 std::vector<std::size_t> ph(R, 1), shift(R, 0);
98 std::size_t w = 0;
99 for (std::size_t r = 0; r < R; ++r) {
100 ph[r] = sn.phasessz_of(ist, r + 1);
101 shift[r] = w;
102 w += ph[r];
103 }
104 return qn::to_marginal(sn, ist, row, ph, shift, sn.nvars_of(ind)).nir;
105}
106
107} // namespace prob_detail
108
109/**
110 * Port of `solver_ctmc_joint`: P(the network is in exactly `state`).
111 *
112 * Returns zero when the state is not in the enumerated space, which is the
113 * honest answer for a state the encoding cannot represent or the dynamics
114 * cannot reach -- not an error, because `findrows` returning nothing is how the
115 * reference reports the same thing.
116 */
117template <class T>
119 const NetState<T>& state) {
120 assert_phase_type_states(sn, "getProbSys");
121 NetState<T> q = state;
122 for (std::size_t f = 0; f < q.local.size() && f < d.chain.space[0].local.size(); ++f)
123 q.local[f] = prob_detail::align(q.local[f], d.chain.space[0].local[f].size());
124 const std::vector<double> key = ctmc_detail::state_key(q);
125 for (std::size_t s = 0; s < d.chain.space.size(); ++s)
126 if (ctmc_detail::state_key(d.chain.space[s]) == key) return d.pi[s];
127 return num_traits<T>::from_int(0);
128}
129
130/**
131 * Port of `solver_ctmc_jointaggr`: P(the network holds exactly these per-class
132 * counts), summed over every phase and buffer arrangement that realizes them.
133 */
134template <class T>
136 const NetState<T>& state) {
137 assert_phase_type_states(sn, "getProbSysAggr");
138 const std::vector<std::size_t>& sfn = sn.stateful_nodes;
139 std::vector<std::vector<T>> want(sfn.size());
140 for (std::size_t f = 0; f < sfn.size(); ++f)
141 want[f] = prob_detail::marginal_of(sn, sfn[f], state.local[f]);
142
143 T acc = num_traits<T>::from_int(0);
144 for (std::size_t s = 0; s < d.chain.space.size(); ++s) {
145 bool same = true;
146 for (std::size_t f = 0; f < sfn.size() && same; ++f) {
147 const std::vector<T> got =
148 prob_detail::marginal_of(sn, sfn[f], d.chain.space[s].local[f]);
149 if (got.size() != want[f].size()) { same = false; break; }
150 for (std::size_t r = 0; r < got.size(); ++r)
151 if (num_traits<T>::to_double(got[r]) !=
152 num_traits<T>::to_double(want[f][r])) { same = false; break; }
153 }
154 if (same) acc += d.pi[s];
155 }
156 return acc;
157}
158
159/**
160 * Port of `solver_ctmc_marg`: per STATION, P(that station is in exactly its
161 * local slice of `state`), marginalized over every other node.
162 *
163 * @return one entry per station, indexed 0-based
164 */
165template <class T>
166std::vector<T> solver_ctmc_marg(const NetworkStruct<T>& sn, const CtmcSolution<T>& d,
167 const NetState<T>& state) {
168 assert_phase_type_states(sn, "getProb");
169 const T zero = num_traits<T>::from_int(0);
170 std::vector<T> out(sn.nstations, zero);
171 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
172 const std::size_t isf = sn.stateful_of_station(ist);
173 if (isf == 0) continue;
174 const std::vector<T> want =
175 prob_detail::align(state.local[isf - 1], d.chain.space[0].local[isf - 1].size());
176 for (std::size_t s = 0; s < d.chain.space.size(); ++s)
177 if (d.chain.space[s].local[isf - 1] == want) out[ist - 1] += d.pi[s];
178 }
179 return out;
180}
181
182/**
183 * Port of `solver_ctmc_margaggr`: per STATION, P(that station holds exactly
184 * these per-class counts).
185 *
186 * This is the aggregate `solver_ctmc_marg` is the refinement of: it sums the
187 * per-state probabilities of every local state sharing the marginal, so on a
188 * single-phase model with no buffer ordering the two coincide.
189 */
190template <class T>
192 const NetState<T>& state) {
193 assert_phase_type_states(sn, "getProbAggr");
194 const T zero = num_traits<T>::from_int(0);
195 std::vector<T> out(sn.nstations, zero);
196 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
197 const std::size_t isf = sn.stateful_of_station(ist);
198 const std::size_t ind = sn.node_of_station(ist);
199 if (isf == 0) continue;
200 const std::vector<T> want = prob_detail::marginal_of(sn, ind, state.local[isf - 1]);
201 for (std::size_t s = 0; s < d.chain.space.size(); ++s) {
202 const std::vector<T> got =
203 prob_detail::marginal_of(sn, ind, d.chain.space[s].local[isf - 1]);
204 bool same = got.size() == want.size();
205 for (std::size_t r = 0; r < got.size() && same; ++r)
207 same = false;
208 if (same) out[ist - 1] += d.pi[s];
209 }
210 }
211 return out;
212}
213
214/**
215 * Port of `solver_ctmc_ratecomplement`: the long-run rate of an action as seen
216 * from each TANGIBLE state, given the action's rate filter `D`.
217 *
218 * Vanishing states are removed from the generator by stochastic complementation,
219 * so an action that fires only in vanishing states -- a fork firing, a join
220 * departure, the firing of an immediate SPN mode -- would be lost if its rate
221 * were read off the tangible rows alone. The rate observed from tangible state
222 * s is the direct exit rate via the action plus the expected number of firings
223 * along the vanishing chain entered from s:
224 *
225 * r = D(nonimm,:)*1 + Q12*(-Q22)^-1*(D(imm,:)*1)
226 *
227 * NOT YET REACHED BY THIS PORT'S GENERATOR, which gives an immediate transition
228 * the reference's ~1e8 rate rather than eliminating it, so there are no
229 * vanishing states to complement out. It is ported at its own signature so the
230 * elimination can be added without re-deriving the correction.
231 *
232 * @param nonimm 0-based tangible row indices, in the order they appear in Q11
233 * @param imm 0-based vanishing row indices
234 * @param Q12 tangible-to-vanishing block
235 * @param Q22 vanishing-to-vanishing block
236 * @param D generator whose immediate states are being eliminated
237 */
238template <class T>
239std::vector<T> solver_ctmc_ratecomplement(const Matrix<T>& D,
240 const std::vector<std::size_t>& nonimm,
241 const std::vector<std::size_t>& imm,
242 const Matrix<T>& Q12, const Matrix<T>& Q22) {
243 const T zero = num_traits<T>::from_int(0);
244 std::vector<T> r(nonimm.size(), zero);
245 for (std::size_t a = 0; a < nonimm.size(); ++a)
246 for (std::size_t j = 0; j < D.cols(); ++j) r[a] += D(nonimm[a], j);
247 if (imm.empty()) return r;
248
249 std::vector<T> b(imm.size(), zero);
250 for (std::size_t a = 0; a < imm.size(); ++a)
251 for (std::size_t j = 0; j < D.cols(); ++j) b[a] += D(imm[a], j);
252 Matrix<T> negQ22(Q22.rows(), Q22.cols());
253 for (std::size_t a = 0; a < Q22.rows(); ++a)
254 for (std::size_t c = 0; c < Q22.cols(); ++c) negQ22(a, c) = T(-Q22(a, c));
255 const std::vector<T> x = solve(negQ22, b);
256 for (std::size_t a = 0; a < nonimm.size() && a < Q12.rows(); ++a)
257 for (std::size_t c = 0; c < Q12.cols() && c < x.size(); ++c) r[a] += T(Q12(a, c) * x[c]);
258 return r;
259}
260
261} // namespace ctmc
262} // namespace line
263
264#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_PROB_H
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
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...
T solver_ctmc_jointaggr(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const NetState< T > &state)
Port of solver_ctmc_jointaggr: P(the network holds exactly these per-class counts),...
std::vector< T > solver_ctmc_margaggr(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const NetState< T > &state)
Port of solver_ctmc_margaggr: per STATION, P(that station holds exactly these per-class counts).
std::vector< T > solver_ctmc_marg(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const NetState< T > &state)
Port of solver_ctmc_marg: per STATION, P(that station is in exactly its local slice of state),...
T solver_ctmc_joint(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const NetState< T > &state)
Port of solver_ctmc_joint: P(the network is in exactly state).
std::vector< T > solver_ctmc_ratecomplement(const Matrix< T > &D, const std::vector< std::size_t > &nonimm, const std::vector< std::size_t > &imm, const Matrix< T > &Q12, const Matrix< T > &Q22)
Port of solver_ctmc_ratecomplement: the long-run rate of an action as seen from each TANGIBLE state,...
bool ctmc_all_phasetype(const NetworkStruct< T > &sn)
MATLAB's all(sn.isph(:)), read off the matrices instead of off a flag.
Marginal< T > to_marginal(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< T > &state_i, const std::vector< std::size_t > &phasesz, const std::vector< std::size_t > &phaseshift, std::size_t nvar=0)
Port of State.toMarginal for a STATION, one state row at a time.
Definition state.h:130
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
A queueing network and its refreshed NetworkStruct.
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
Port of the MATLAB +State package: the encoding that turns a station's state row into marginal job co...
Everything one CTMC solve produces.
std::vector< T > pi
stationary distribution over chain.space
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2140
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2141