LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ssa_getters.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_SSA_SOLVER_SSA_GETTERS_H
6#define LINE_SOLVERS_SSA_SOLVER_SSA_GETTERS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The SolverSSA queries that are not the average table: `getProb`,
12 * `getProbAggr`, `getProbSys`, `getProbSysAggr`, and the four samplers
13 * `sample`, `sampleAggr`, `sampleSys`, `sampleSysAggr`.
14 *
15 * THEY ALL READ ONE SAMPLE PATH, which is why they live together. The reference
16 * splits them across nine files in `@@SolverSSA/`, but every one of them runs
17 * the analyzer and then reduces the SAME trajectory: a probability is the
18 * fraction of simulated time spent in a state, and a sample is that trajectory
19 * printed. Running the engine once and reducing it several ways is therefore
20 * not an optimization -- it is what makes the four probabilities mutually
21 * consistent, which they would not be if each drew its own path.
22 *
23 * THE SERIAL ENGINE, NEVER THE NRM, and that is the reference's own choice:
24 * `@@SolverSSA/getProb.m` rewrites `default` and `nrm` to `serial` before it
25 * asks for the trajectory. The NRM does not simulate the state ENCODING at all
26 * -- it moves per-(node, class, phase) counts -- so it has no row to compare a
27 * requested state against and no per-event trace to print. A caller who asked
28 * for `nrm` and then for a probability gets the serial engine's answer, as they
29 * do in MATLAB.
30 *
31 * TIME-WEIGHTED, NOT EVENT-COUNTED. A probability here is the fraction of
32 * SIMULATED TIME the path spent in the state, not the fraction of firings that
33 * landed in it. The two differ by the holding-time distribution and only the
34 * first estimates the stationary law; the reference's `TSS(:,1) = [TSS(1,1);
35 * diff(TSS(:,1))]` is exactly that reweighting, and `SsaSerialRun::pi` has
36 * already applied it, so these functions sum `pi` and never counts.
37 *
38 * A STATE THAT WAS NEVER VISITED HAS PROBABILITY 0, and the reference warns
39 * before returning it. That zero is a measurement of a finite path and not a
40 * statement about the chain: a rare state simply did not occur in the sample.
41 * `SsaProbResult::seen` carries the distinction so a caller can tell "the path
42 * says this state is impossible" from "the path never got there".
43 */
44
45#include <cstddef>
46#include <string>
47#include <vector>
48
50#include "line/lang/qn/state.h"
54#include "line/util/error.h"
55#include "line/util/matrix.h"
56
57namespace line {
58namespace ssa {
59
60/** One probability query: the estimate, and whether the state occurred at all. */
62 double prob = 0.0;
63 bool seen = false;
64};
65
66/** The four probabilities `-a prob` reports, over one requested state. */
68 SsaProbResult sys, sys_aggr; ///< getProbSys, getProbSysAggr
69 std::vector<SsaProbResult> marg, aggr; ///< getProb, getProbAggr, per station
70 std::size_t samples = 0;
71 unsigned long seed = 0;
72 double simulated_time = 0.0;
73};
74
75/** One trajectory, in the shape the reference's `sampleSys` returns it. */
76template <class T>
78 std::vector<double> t; ///< the event times, increasing
79 std::vector<std::size_t> event; ///< which synchronization fired
80 Matrix<T> state; ///< per event: the state OCCUPIED until then
81 Matrix<T> aggr; ///< the same, as per-(stateful, class) counts
82 std::size_t samples = 0;
83 unsigned long seed = 0;
84};
85
86namespace getters_detail {
87
88/** The per-class counts of one node's local row, `State.toMarginal`'s `nir`. */
89template <class T>
90std::vector<double> node_counts(const qn::NetworkStruct<T>& sn, std::size_t ind,
91 const std::vector<T>& row) {
92 const std::pair<T, std::vector<T>> m = qn::to_marginal_aggr(sn, ind, row);
93 std::vector<double> out(m.second.size(), 0.0);
94 for (std::size_t k = 0; k < m.second.size(); ++k)
95 out[k] = num_traits<T>::to_double(m.second[k]);
96 return out;
97}
98
99/** Exact equality of two encoded rows, at the tolerance a count deserves. */
100template <class T>
101bool same_row(const std::vector<T>& a, const std::vector<T>& b) {
102 if (a.size() != b.size()) return false;
103 for (std::size_t j = 0; j < a.size(); ++j)
104 if (std::fabs(num_traits<T>::to_double(a[j]) - num_traits<T>::to_double(b[j])) > 1e-9)
105 return false;
106 return true;
107}
108
109inline bool same_counts(const std::vector<double>& a, const std::vector<double>& b) {
110 if (a.size() != b.size()) return false;
111 for (std::size_t j = 0; j < a.size(); ++j)
112 if (std::fabs(a[j] - b[j]) > 1e-9) return false;
113 return true;
114}
115
116/**
117 * The node's name for a message, or its index when there is no such node.
118 *
119 * A query naming a node past the end is the case that made this necessary:
120 * reading `sn.nodes[ind - 1].name` to SAY SO is itself the out-of-range access,
121 * so the refusal aborted instead of being thrown.
122 */
123template <class T>
124std::string node_label(const qn::NetworkStruct<T>& sn, std::size_t ind) {
125 if (ind == 0 || ind > sn.nodes.size()) return "#" + std::to_string(ind);
126 return sn.nodes[ind - 1].name;
127}
128
129} // namespace getters_detail
130
131/**
132 * `getProb(node, state)`: the time fraction the path spent with node `ind` in
133 * `row`.
134 *
135 * The row is compared at the ENCODING width the path lives at, which is why the
136 * caller is expected to pass a row taken from the same widening the engine
137 * applies (`serial_detail::wide_init_state`, or a row of `run.space`). The
138 * reference left-pads a short row with zeros; a row that is short here would
139 * compare unequal to every state, so it is refused by name instead of silently
140 * matching nothing.
141 */
142template <class T>
144 const std::vector<T>& row) {
145 const std::size_t isf = sn.stateful_index(ind);
146 if (isf == 0)
147 throw InputError("ssa_prob: node '" + getters_detail::node_label(sn, ind) +
148 "' is stateless, so it has no state to report a probability of");
149 SsaProbResult out;
150 for (std::size_t s = 0; s < r.space.size(); ++s) {
151 if (!getters_detail::same_row(r.space[s].local[isf - 1], row)) continue;
152 out.prob += r.pi[s];
153 out.seen = true;
154 }
155 return out;
156}
157
158/** `getProbAggr(node, n)`: the same, over the per-class counts alone. */
159template <class T>
161 std::size_t ind, const std::vector<double>& counts) {
162 const std::size_t isf = sn.stateful_index(ind);
163 if (isf == 0)
164 throw InputError("ssa_prob_aggr: node '" + getters_detail::node_label(sn, ind) +
165 "' is stateless, so it holds no jobs to count");
166 SsaProbResult out;
167 for (std::size_t s = 0; s < r.space.size(); ++s) {
168 if (!getters_detail::same_counts(
169 getters_detail::node_counts(sn, ind, r.space[s].local[isf - 1]), counts))
170 continue;
171 out.prob += r.pi[s];
172 out.seen = true;
173 }
174 return out;
175}
176
177/** `getProbSys()`: the joint state of every stateful node at once. */
178template <class T>
180 const qn::NetState<T>& st) {
181 SsaProbResult out;
182 for (std::size_t s = 0; s < r.space.size(); ++s) {
183 bool all = true;
184 for (std::size_t f = 0; f < sn.stateful_nodes.size() && all; ++f)
185 if (!getters_detail::same_row(r.space[s].local[f], st.local[f])) all = false;
186 if (!all) continue;
187 out.prob += r.pi[s];
188 out.seen = true;
189 }
190 return out;
191}
192
193/** `getProbSysAggr()`: the joint per-class counts of every stateful node. */
194template <class T>
196 const qn::NetState<T>& st) {
197 const std::size_t NF = sn.stateful_nodes.size();
198 std::vector<std::vector<double>> want(NF);
199 for (std::size_t f = 0; f < NF; ++f)
200 want[f] = getters_detail::node_counts(sn, sn.stateful_nodes[f], st.local[f]);
201 SsaProbResult out;
202 for (std::size_t s = 0; s < r.space.size(); ++s) {
203 bool all = true;
204 for (std::size_t f = 0; f < NF && all; ++f)
205 if (!getters_detail::same_counts(
206 getters_detail::node_counts(sn, sn.stateful_nodes[f], r.space[s].local[f]),
207 want[f]))
208 all = false;
209 if (!all) continue;
210 out.prob += r.pi[s];
211 out.seen = true;
212 }
213 return out;
214}
215
216/**
217 * `sampleSys` and `sampleSysAggr`: the trajectory itself.
218 *
219 * THE STATE ON ROW i IS THE ONE OCCUPIED UNTIL `t[i]`, not the one entered at
220 * it, which is the convention `run.tran_state` already records and the only one
221 * under which the time weighting of `pi` and the rows printed here agree. The
222 * event on the same row is the synchronization that ENDED that sojourn.
223 */
224template <class T>
226 const std::size_t NF = sn.stateful_nodes.size();
228 out.t = r.tran_time;
229 out.event = r.tran_sync;
230 out.samples = r.samples;
231 out.seed = r.seed;
232
233 std::size_t width = 0;
234 for (std::size_t f = 0; f < NF; ++f)
235 width += r.space.empty() ? 0 : r.space[0].local[f].size();
236 const std::size_t n = r.tran_state.size();
237 out.state = Matrix<T>(n, width, num_traits<T>::from_int(0));
238 out.aggr = Matrix<T>(n, r.ssq.cols(), num_traits<T>::from_int(0));
239 for (std::size_t i = 0; i < n; ++i) {
240 const std::size_t s = r.tran_state[i];
241 std::size_t col = 0;
242 for (std::size_t f = 0; f < NF; ++f)
243 for (std::size_t j = 0; j < r.space[s].local[f].size(); ++j)
244 out.state(i, col++) = r.space[s].local[f][j];
245 for (std::size_t c = 0; c < r.ssq.cols(); ++c) out.aggr(i, c) = r.ssq(s, c);
246 }
247 return out;
248}
249
250/**
251 * `sample(node)` and `sampleAggr(node)`: the same trajectory, one node's block.
252 *
253 * The aggregate here is the node's own per-class counts and NOT a column slice
254 * of `ssq`: `ssq` is indexed by STATION and a Cache, a Join or a Transition is
255 * stateful without being one, so slicing would silently address the wrong node
256 * on every model that has one.
257 */
258template <class T>
260 std::size_t ind) {
261 const std::size_t isf = sn.stateful_index(ind);
262 if (isf == 0)
263 throw InputError("ssa_sample_node: node '" + getters_detail::node_label(sn, ind) +
264 "' is stateless, so no trajectory passes through its state");
266 out.t = r.tran_time;
267 out.event = r.tran_sync;
268 out.samples = r.samples;
269 out.seed = r.seed;
270 const std::size_t n = r.tran_state.size();
271 const std::size_t width = r.space.empty() ? 0 : r.space[0].local[isf - 1].size();
272 out.state = Matrix<T>(n, width, num_traits<T>::from_int(0));
273 out.aggr = Matrix<T>(n, sn.nclasses, num_traits<T>::from_int(0));
274 for (std::size_t i = 0; i < n; ++i) {
275 const std::size_t s = r.tran_state[i];
276 for (std::size_t j = 0; j < width; ++j) out.state(i, j) = r.space[s].local[isf - 1][j];
277 const std::vector<double> c = getters_detail::node_counts(sn, ind, r.space[s].local[isf - 1]);
278 for (std::size_t k = 0; k < c.size() && k < sn.nclasses; ++k)
279 out.aggr(i, k) = num_traits<T>::from_double(c[k]);
280 }
281 return out;
282}
283
284/**
285 * The whole `-a prob` report over the model's DEFAULT INITIAL STATE, which is
286 * the state SolverCTMC's own `-a prob` reports on.
287 *
288 * SAME STATE, SAME QUESTION, DIFFERENT ESTIMATOR: the CTMC answer is the
289 * stationary law of the chain and this one is a time average of a finite path,
290 * so the pair is a direct measurement of the simulation error on a model small
291 * enough for both. That is what makes the report worth having at the same
292 * state rather than at a state of the simulator's choosing.
293 */
294template <class T>
297 if (!sim.fjclassmap.empty())
298 throw UnsupportedError(
299 "SolverSSA: a probability query on a fork-join model would be asked in the CALLER's "
300 "class space and answered in the tag-augmented one, whose sibling classes the caller "
301 "never declared and cannot name a state in; SolverCTMC reports the same query on the "
302 "augmented chain, so use it, or ask for -a avg here");
303
305 copt.cutoff = opt.cutoff;
306 copt.state_max = opt.state_max;
307 const qn::NetState<T> init =
308 serial_detail::wide_init_state(sn, ctmc::analyzer_detail::resolve_cutoff(sn, copt));
309
310 SsaProbReport out;
311 out.samples = sim.run.samples;
312 out.seed = sim.run.seed;
313 out.simulated_time = sim.run.simulated_time;
314 out.sys = ssa_prob_sys(sn, sim.run, init);
315 out.sys_aggr = ssa_prob_sys_aggr(sn, sim.run, init);
316 for (std::size_t i = 0; i < sn.nstations; ++i) {
317 const std::size_t ind = sn.node_of_station(i + 1);
318 const std::size_t isf = sn.stateful_index(ind);
319 if (isf == 0) {
320 out.marg.push_back(SsaProbResult());
321 out.aggr.push_back(SsaProbResult());
322 continue;
323 }
324 out.marg.push_back(ssa_prob(sn, sim.run, ind, init.local[isf - 1]));
325 out.aggr.push_back(ssa_prob_aggr(
326 sn, sim.run, ind, getters_detail::node_counts(sn, ind, init.local[isf - 1])));
327 }
328 return out;
329}
330
331/**
332 * `getCdfRespT`: refused, and the refusal is the ANSWER rather than a gap.
333 *
334 * `@@SolverSSA/getCdfRespT.m` raises the same error in MATLAB, for the reason it
335 * states: SSA samples state trajectories, not per-job sojourn times, so there is
336 * nothing to build an empirical CDF from. The inherited implementation would
337 * fabricate an exponential law with the right mean, which carries no information
338 * about the tail and would be indistinguishable, to the caller, from a measured
339 * distribution. Reproducing THAT would be the defect.
340 */
341inline void ssa_cdf_respt_refuse() {
342 throw UnsupportedError(
343 "SolverSSA does not record per-job response times, so it cannot return an empirical "
344 "response time CDF; the inherited exponential fit would carry no information about the "
345 "tail while looking measured. Use SolverJMT for a measured CDF, SolverFluid or SolverMAM "
346 "for an analytical one");
347}
348
349} // namespace ssa
350} // namespace line
351
352#endif // LINE_SOLVERS_SSA_SOLVER_SSA_GETTERS_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
std::pair< T, std::vector< T > > to_marginal_aggr(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &state_i)
Port of State.toMarginalAggr: the job counts of one node's state row, without the per-phase detail to...
SsaSerialSolution< T > solver_ssa_serial_analyzer(const qn::NetworkStruct< T > &sn, const SsaSerialOptions &opt)
Port of solver_ssa_analyzer_serial.m plus the fork-join wrapper @@SolverSSA/runAnalyzer....
SsaProbResult ssa_prob_aggr(const qn::NetworkStruct< T > &sn, const SsaSerialRun< T > &r, std::size_t ind, const std::vector< double > &counts)
getProbAggr(node, n): the same, over the per-class counts alone.
SsaProbReport solver_ssa_prob(const qn::NetworkStruct< T > &sn, const SsaSerialOptions &opt)
The whole -a prob report over the model's DEFAULT INITIAL STATE, which is the state SolverCTMC's own ...
SsaSamplePath< T > ssa_sample_node(const qn::NetworkStruct< T > &sn, const SsaSerialRun< T > &r, std::size_t ind)
sample(node) and sampleAggr(node): the same trajectory, one node's block.
SsaSamplePath< T > ssa_sample_sys(const qn::NetworkStruct< T > &sn, const SsaSerialRun< T > &r)
sampleSys and sampleSysAggr: the trajectory itself.
SsaProbResult ssa_prob_sys(const qn::NetworkStruct< T > &sn, const SsaSerialRun< T > &r, const qn::NetState< T > &st)
getProbSys(): the joint state of every stateful node at once.
SsaProbResult ssa_prob_sys_aggr(const qn::NetworkStruct< T > &sn, const SsaSerialRun< T > &r, const qn::NetState< T > &st)
getProbSysAggr(): the joint per-class counts of every stateful node.
void ssa_cdf_respt_refuse()
getCdfRespT: refused, and the refusal is the ANSWER rather than a gap.
SsaProbResult ssa_prob(const qn::NetworkStruct< T > &sn, const SsaSerialRun< T > &r, std::size_t ind, const std::vector< T > &row)
getProb(node, state): the time fraction the path spent with node ind in row.
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...
SolverSSA, the serial method: a port of solver_ssa_reachability.m, of the run loop of solver_ssa....
Port of the MATLAB +State package: the encoding that turns a station's state row into marginal job co...
Port of the event half of MATLAB's +State package: the successor states an event produces at one node...
The SolverCTMC knobs this port honours.
std::size_t state_max
refuse a space larger than this
double cutoff
< 0 = not given
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
The four probabilities -a prob reports, over one requested state.
SsaProbResult sys_aggr
getProbSys, getProbSysAggr
std::vector< SsaProbResult > aggr
getProb, getProbAggr, per station
std::vector< SsaProbResult > marg
One probability query: the estimate, and whether the state occurred at all.
One trajectory, in the shape the reference's sampleSys returns it.
std::vector< double > t
the event times, increasing
Matrix< T > aggr
the same, as per-(stateful, class) counts
std::vector< std::size_t > event
which synchronization fired
Matrix< T > state
per event: the state OCCUPIED until then
The serial engine's knobs: SsaOptions plus the three the serial path reads and the NRM has no use for...
One sample path, in the shape solver_ssa.m returns it.
std::vector< qn::NetState< T > > space
The DISTINCT states visited, in first-visit order (the reference's u).
std::vector< double > tran_time
tranSysState{1}: the cumulative time at each firing.
std::vector< std::size_t > tran_state
The row of space the path OCCUPIED over [t-dt, t], one per firing.
Matrix< T > ssq
SSq: the per-(station, class) job counts of each distinct state.
unsigned long seed
the stream this path came from
std::size_t samples
firings actually performed
std::vector< double > pi
pi: the fraction of simulated time spent in each of them.
std::vector< std::size_t > tran_sync
tranSync: which synchronization fired, sync.size() + g for a global one.
The serial analyzer's return: the metric table, the path, and the stream.