LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ssa_types.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_SSA_TYPES_H
6#define LINE_SOLVERS_SSA_SSA_TYPES_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Controls, results and the random source of SolverSSA.
12 *
13 * WHICH RANDOM STREAM, AND WHAT THAT COSTS. An SSA answer is a function of the
14 * random stream, so a seed-fixed golden belongs to one implementation only.
15 * MATLAB draws from `rand` (its own Mersenne Twister wrapper); the JAR and
16 * native Python share an MT19937. This port uses MT19937 too, but it does NOT
17 * claim stream compatibility with any of them: the ORDER in which the sample
18 * path consumes draws is part of the algorithm, and no two of the four
19 * implementations consume in the same order (the JAR, for instance, spends a
20 * draw on a single-phase entry-phase pick where this port does not). So a
21 * cross-codebase check against this engine is STATISTICAL, never exact --
22 * which is what `_kb/14-cpp-multiprecision.md` records and what the tests
23 * assert.
24 */
25
26#include <algorithm>
27#include <cmath>
28#include <cstddef>
29#include <cstdint>
30#include <limits>
31#include <random>
32#include <string>
33#include <vector>
34
35#include "line/util/matrix.h"
36
37namespace line {
38namespace ssa {
39
40/** Helpers shared by the SSA engines that do not belong to a single one. */
41namespace ssa_detail {
42
43/**
44 * Add the START/PREEMPT counts of one successor row to the per-state rate
45 * accumulators, weighted by the rate of the arc that carries them. A node that
46 * is not a station contributes nothing: only a station has a server to seize.
47 */
48template <class T, class Outcome>
49inline void add_tag_rates(std::vector<std::vector<double>>& start,
50 std::vector<std::vector<double>>& preempt,
51 const T& sn, std::size_t node, const Outcome& oc, std::size_t row,
52 double w) {
53 if (!(w != 0) || node == 0 || node > sn.nodes.size()) return;
54 const std::size_t isf = sn.stateful_index(node);
55 if (isf == 0 || isf > start.size()) return;
56 if (row < oc.start.size())
57 for (std::size_t j = 0; j < oc.start[row].size(); ++j) {
58 const std::size_t cls = oc.start[row][j];
59 if (cls >= 1 && cls <= start[isf - 1].size()) start[isf - 1][cls - 1] += w;
60 }
61 if (row < oc.preempt.size())
62 for (std::size_t j = 0; j < oc.preempt[row].size(); ++j) {
63 const std::size_t cls = oc.preempt[row][j];
64 if (cls >= 1 && cls <= preempt[isf - 1].size()) preempt[isf - 1][cls - 1] += w;
65 }
66}
67
68} // namespace ssa_detail
69
70/** Controls, defaulting to `SolverOptions('SSA')` in the reference. */
71struct SsaOptions {
72 /** `default` and `nrm` both select the Next Reaction Method here. */
73 std::string method = "default";
74 /** Reaction firings to simulate; `options.samples` in the reference. */
75 std::size_t samples = 10000;
76 /** `options.seed`; LINE's own default is 23000. */
77 unsigned long seed = 23000;
78 bool verbose = false;
79 /**
80 * `options.config.warmupfrac`: the leading fraction of the path discarded
81 * before the means are taken.
82 *
83 * DECLARED HERE, NOT ON `SsaSerialOptions`, although only the serial and
84 * replicated engines read one: `ssa_serial_options` builds the engine's
85 * knobs by copying the BASE slice of the caller's, so a field on the
86 * derived struct is unreachable from a caller and kept its default however
87 * the request was spelled -- which is what made `--warmupfrac` a knob the
88 * C++ CLI could not offer at all.
89 *
90 * NEGATIVE = UNSET, the sentinel `SolverOptions::warmupfrac` and line-cli's
91 * `--warmupfrac` already use. `solver_ssa` resolves it once through
92 * `ssa_resolve_warmupfrac` (0.2 on a steady-state run of a model with an open
93 * class, else 0); an engine reached directly reads an unset value as 0, so a
94 * sample path keeps its initial transient.
95 */
96 double warmupfrac = -1.0;
97 /**
98 * `options.config.state_space_gen` of `solver_ssa_analyzer_nrm.m`: which of
99 * the two NRM engines runs. `none` and `default` take the plain engine,
100 * which integrates the metrics along the path; anything else takes the
101 * tabulating one, which records the states it visits and forms the means as
102 * `pi * A` over them. The two answer the same question by different routes,
103 * which is what makes them testable against each other.
104 */
105 std::string state_space_gen = "default";
106 /**
107 * `options.cutoff`: the bound on an open class's jobs at a station, which
108 * the serial and replicated engines resolve as SolverCTMC does (< 0 = the
109 * reference's automatic value). On the base for the reason `warmupfrac` is:
110 * on `SsaSerialOptions` no caller could set it, so an open model was always
111 * truncated at the default 10, which lost a retrial orbit's jobs at the
112 * boundary and read a throughput of 0.744 where the exact one is 0.8.
113 */
114 double cutoff = -1.0;
115};
116
117/**
118 * `ssa_warmupfrac.m`: the effective warmup (initial-transient) discard fraction.
119 *
120 * An explicit value (>= 0, 0 included) is honoured, clamped to [0, 0.99]. When it
121 * is unset (negative) the rule is the same in all four codebases: a STEADY-STATE
122 * run (`steady`, i.e. an infinite timespan end) of a model with at least one OPEN
123 * class drops the first 20% of its samples, the LDES default, and any other run
124 * drops none. An open model starts EMPTY, far from its stationary load, and on a
125 * heavily loaded queue that empty start biased QN low by several percent at the
126 * default sample budget; a closed model starts at a feasible state of its fixed
127 * population, whose transient is short against the run. A transient run and a
128 * sample path keep every sample, since the initial transient is what they ask for.
129 * `Struct` is any network struct with `has_open_classes()`.
130 */
131template <class Struct>
132inline double ssa_resolve_warmupfrac(const Struct& sn, double warmupfrac, bool steady = true) {
133 if (warmupfrac >= 0.0) return std::max(0.0, std::min(0.99, warmupfrac));
134 return (steady && sn.has_open_classes()) ? 0.2 : 0.0;
135}
136
137/**
138 * Firings whose tallies an engine discards: floor(warmupfrac * samples), with an
139 * unset (negative) fraction read as 0 by an engine reached without `solver_ssa`.
140 */
141inline std::size_t ssa_warmup_firings(double warmupfrac, std::size_t samples) {
142 const double wf = std::max(0.0, std::min(0.99, warmupfrac));
143 return static_cast<std::size_t>(std::floor(wf * static_cast<double>(samples)));
144}
145
146/** What the analyzer returns, in the same shape as the MVA and fluid results. */
149 std::vector<double> CN, XN;
150 /**
151 * The DERIVED rates, (nstations x nclasses): how often per unit time a
152 * class-r service STARTS at station i, and how often a class-r job in
153 * service is PUSHED BACK into the buffer there. At a lossless station with
154 * no in-service abandonment StartN == TN + PreemptN, up to simulation
155 * error; SolverCTMC reports the exact value of the same quantity.
156 */
158 /** The concrete algorithm, as the reference's `method`. */
159 std::string method = "nrm";
160 /** Simulated time the metrics are averaged over; the reference's `totalTime`. */
161 double simulated_time = 0.0;
162 /** Reaction firings actually performed. */
163 std::size_t samples = 0;
164};
165
166/** What the cache write-back of `solver_ssa_analyzer_serial.m` produces (and the NRM's). */
168 std::size_t node = 0; ///< 1-based Cache node index
169 std::vector<double> hitprob, missprob; ///< per class, NaN where undefined
170 /**
171 * The delayed-hit share, EMPTY off a retrieval system.
172 *
173 * `hitprob` carries the TRUE hits alone once this is filled: the three shares
174 * partition every read, hit + delayed + miss = 1, which is the convention NC
175 * and LDES report, so DelayedHitRate = arvr*delayedprob in the cache table.
176 */
177 std::vector<double> delayedprob;
178 /**
179 * `actualresidt`: NaN, and NOT a port gap. The reference warns
180 * "Retrieval-system expected latency is not currently implemented; reporting
181 * NaN" and reports NaN in every codebase, so reproducing the NaN IS parity.
182 */
183 std::vector<double> residt;
184};
185
186/**
187 * The uniform source, MATLAB's `rand`.
188 *
189 * A 53-bit uniform assembled from two MT19937 words, the construction numpy
190 * and the JAR's RandomManager both use, shifted by half an ulp so the value is
191 * strictly inside (0,1). The shift is not cosmetic: `-log(u)` is the
192 * exponential clock of every reaction and an exact zero would make it
193 * infinite, which the run loop reads as a deadlock.
194 */
195class SsaRng {
196public:
197 explicit SsaRng(unsigned long seed) : g_(static_cast<std::uint_fast32_t>(seed)) {}
198
199 double uniform() {
200 const std::uint64_t a = g_() >> 5, b = g_() >> 6;
201 return (static_cast<double>(a) * 67108864.0 + static_cast<double>(b) + 0.5) /
202 9007199254740992.0;
203 }
204
205 /** Uniform index in [0, n), the reference's `1 + floor(rand*n)`. */
206 std::size_t index(std::size_t n) {
207 if (n == 0) return 0;
208 const std::size_t k = static_cast<std::size_t>(uniform() * static_cast<double>(n));
209 return k < n ? k : n - 1;
210 }
211
212 /**
213 * Index drawn from the unnormalized nonnegative weights `p`, the
214 * reference's `drawFromDist`: an all-zero weight vector yields index 0.
215 */
216 std::size_t draw(const std::vector<double>& p) {
217 double tot = 0.0;
218 for (double x : p) tot += x;
219 if (!(tot > 0.0)) return 0;
220 const double u = uniform() * tot;
221 double acc = 0.0;
222 for (std::size_t i = 0; i < p.size(); ++i) {
223 acc += p[i];
224 if (acc > u) return i;
225 }
226 return p.size() - 1;
227 }
228
229private:
230 std::mt19937 g_;
231};
232
233} // namespace ssa
234} // namespace line
235
236#endif // LINE_SOLVERS_SSA_SSA_TYPES_H
std::size_t draw(const std::vector< double > &p)
Index drawn from the unnormalized nonnegative weights p, the reference's drawFromDist: an all-zero we...
Definition ssa_types.h:216
std::size_t index(std::size_t n)
Uniform index in [0, n), the reference's 1 + floor(rand*n).
Definition ssa_types.h:206
SsaRng(unsigned long seed)
Definition ssa_types.h:197
Dense matrix and non-owning view.
std::size_t ssa_warmup_firings(double warmupfrac, std::size_t samples)
Firings whose tallies an engine discards: floor(warmupfrac * samples), with an unset (negative) fract...
Definition ssa_types.h:141
double ssa_resolve_warmupfrac(const Struct &sn, double warmupfrac, bool steady=true)
ssa_warmupfrac.m: the effective warmup (initial-transient) discard fraction.
Definition ssa_types.h:132
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
What the cache write-back of solver_ssa_analyzer_serial.m produces (and the NRM's).
Definition ssa_types.h:167
std::size_t node
1-based Cache node index
Definition ssa_types.h:168
std::vector< double > delayedprob
The delayed-hit share, EMPTY off a retrieval system.
Definition ssa_types.h:177
std::vector< double > missprob
per class, NaN where undefined
Definition ssa_types.h:169
std::vector< double > hitprob
Definition ssa_types.h:169
std::vector< double > residt
actualresidt: NaN, and NOT a port gap.
Definition ssa_types.h:183
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:71
std::size_t samples
Reaction firings to simulate; options.samples in the reference.
Definition ssa_types.h:75
double warmupfrac
options.config.warmupfrac: the leading fraction of the path discarded before the means are taken.
Definition ssa_types.h:96
std::string state_space_gen
options.config.state_space_gen of solver_ssa_analyzer_nrm.m: which of the two NRM engines runs.
Definition ssa_types.h:105
std::string method
default and nrm both select the Next Reaction Method here.
Definition ssa_types.h:73
unsigned long seed
options.seed; LINE's own default is 23000.
Definition ssa_types.h:77
double cutoff
options.cutoff: the bound on an open class's jobs at a station, which the serial and replicated engin...
Definition ssa_types.h:114
What the analyzer returns, in the same shape as the MVA and fluid results.
Definition ssa_types.h:147
std::vector< double > XN
Definition ssa_types.h:149
std::vector< double > CN
Definition ssa_types.h:149
Matrix< double > UN
Definition ssa_types.h:148
Matrix< double > RN
Definition ssa_types.h:148
Matrix< double > StartN
The DERIVED rates, (nstations x nclasses): how often per unit time a class-r service STARTS at statio...
Definition ssa_types.h:157
double simulated_time
Simulated time the metrics are averaged over; the reference's totalTime.
Definition ssa_types.h:161
Matrix< double > TN
Definition ssa_types.h:148
std::size_t samples
Reaction firings actually performed.
Definition ssa_types.h:163
Matrix< double > QN
Definition ssa_types.h:148
std::string method
The concrete algorithm, as the reference's method.
Definition ssa_types.h:159
Matrix< double > PreemptN
Definition ssa_types.h:157