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 <cstddef>
27#include <cstdint>
28#include <limits>
29#include <random>
30#include <string>
31#include <vector>
32
33#include "line/util/matrix.h"
34
35namespace line {
36namespace ssa {
37
38/** Helpers shared by the SSA engines that do not belong to a single one. */
39namespace ssa_detail {
40
41/**
42 * Add the START/PREEMPT counts of one successor row to the per-state rate
43 * accumulators, weighted by the rate of the arc that carries them. A node that
44 * is not a station contributes nothing: only a station has a server to seize.
45 */
46template <class T, class Outcome>
47inline void add_tag_rates(std::vector<std::vector<double>>& start,
48 std::vector<std::vector<double>>& preempt,
49 const T& sn, std::size_t node, const Outcome& oc, std::size_t row,
50 double w) {
51 if (!(w != 0) || node == 0 || node > sn.nodes.size()) return;
52 const std::size_t isf = sn.stateful_index(node);
53 if (isf == 0 || isf > start.size()) return;
54 if (row < oc.start.size())
55 for (std::size_t j = 0; j < oc.start[row].size(); ++j) {
56 const std::size_t cls = oc.start[row][j];
57 if (cls >= 1 && cls <= start[isf - 1].size()) start[isf - 1][cls - 1] += w;
58 }
59 if (row < oc.preempt.size())
60 for (std::size_t j = 0; j < oc.preempt[row].size(); ++j) {
61 const std::size_t cls = oc.preempt[row][j];
62 if (cls >= 1 && cls <= preempt[isf - 1].size()) preempt[isf - 1][cls - 1] += w;
63 }
64}
65
66} // namespace ssa_detail
67
68/** Controls, defaulting to `SolverOptions('SSA')` in the reference. */
69struct SsaOptions {
70 /** `default` and `nrm` both select the Next Reaction Method here. */
71 std::string method = "default";
72 /** Reaction firings to simulate; `options.samples` in the reference. */
73 std::size_t samples = 10000;
74 /** `options.seed`; LINE's own default is 23000. */
75 unsigned long seed = 23000;
76 bool verbose = false;
77 /**
78 * `options.config.warmupfrac`: the leading fraction of the path discarded
79 * before the means are taken.
80 *
81 * DECLARED HERE, NOT ON `SsaSerialOptions`, although only the serial and
82 * replicated engines read one: `ssa_serial_options` builds the engine's
83 * knobs by copying the BASE slice of the caller's, so a field on the
84 * derived struct is unreachable from a caller and kept its default however
85 * the request was spelled -- which is what made `--warmupfrac` a knob the
86 * C++ CLI could not offer at all.
87 */
88 double warmupfrac = 0.0;
89 /**
90 * `options.config.state_space_gen` of `solver_ssa_analyzer_nrm.m`: which of
91 * the two NRM engines runs. `none` and `default` take the plain engine,
92 * which integrates the metrics along the path; anything else takes the
93 * tabulating one, which records the states it visits and forms the means as
94 * `pi * A` over them. The two answer the same question by different routes,
95 * which is what makes them testable against each other.
96 */
97 std::string state_space_gen = "default";
98};
99
100/** What the analyzer returns, in the same shape as the MVA and fluid results. */
103 std::vector<double> CN, XN;
104 /**
105 * The DERIVED rates, (nstations x nclasses): how often per unit time a
106 * class-r service STARTS at station i, and how often a class-r job in
107 * service is PUSHED BACK into the buffer there. At a lossless station with
108 * no in-service abandonment StartN == TN + PreemptN, up to simulation
109 * error; SolverCTMC reports the exact value of the same quantity.
110 */
112 /** The concrete algorithm, as the reference's `method`. */
113 std::string method = "nrm";
114 /** Simulated time the metrics are averaged over; the reference's `totalTime`. */
115 double simulated_time = 0.0;
116 /** Reaction firings actually performed. */
117 std::size_t samples = 0;
118};
119
120/** What the cache write-back of `solver_ssa_analyzer_serial.m` produces (and the NRM's). */
122 std::size_t node = 0; ///< 1-based Cache node index
123 std::vector<double> hitprob, missprob; ///< per class, NaN where undefined
124 /**
125 * The delayed-hit share, EMPTY off a retrieval system.
126 *
127 * `hitprob` carries the TRUE hits alone once this is filled: the three shares
128 * partition every read, hit + delayed + miss = 1, which is the convention NC
129 * and LDES report and what makes `ArvR = arvr*(missprob + delayedprob)`.
130 */
131 std::vector<double> delayedprob;
132 /**
133 * `actualresidt`: NaN, and NOT a port gap. The reference warns
134 * "Retrieval-system expected latency is not currently implemented; reporting
135 * NaN" and reports NaN in every codebase, so reproducing the NaN IS parity.
136 */
137 std::vector<double> residt;
138};
139
140/**
141 * The uniform source, MATLAB's `rand`.
142 *
143 * A 53-bit uniform assembled from two MT19937 words, the construction numpy
144 * and the JAR's RandomManager both use, shifted by half an ulp so the value is
145 * strictly inside (0,1). The shift is not cosmetic: `-log(u)` is the
146 * exponential clock of every reaction and an exact zero would make it
147 * infinite, which the run loop reads as a deadlock.
148 */
149class SsaRng {
150public:
151 explicit SsaRng(unsigned long seed) : g_(static_cast<std::uint_fast32_t>(seed)) {}
152
153 double uniform() {
154 const std::uint64_t a = g_() >> 5, b = g_() >> 6;
155 return (static_cast<double>(a) * 67108864.0 + static_cast<double>(b) + 0.5) /
156 9007199254740992.0;
157 }
158
159 /** Uniform index in [0, n), the reference's `1 + floor(rand*n)`. */
160 std::size_t index(std::size_t n) {
161 if (n == 0) return 0;
162 const std::size_t k = static_cast<std::size_t>(uniform() * static_cast<double>(n));
163 return k < n ? k : n - 1;
164 }
165
166 /**
167 * Index drawn from the unnormalized nonnegative weights `p`, the
168 * reference's `drawFromDist`: an all-zero weight vector yields index 0.
169 */
170 std::size_t draw(const std::vector<double>& p) {
171 double tot = 0.0;
172 for (double x : p) tot += x;
173 if (!(tot > 0.0)) return 0;
174 const double u = uniform() * tot;
175 double acc = 0.0;
176 for (std::size_t i = 0; i < p.size(); ++i) {
177 acc += p[i];
178 if (acc > u) return i;
179 }
180 return p.size() - 1;
181 }
182
183private:
184 std::mt19937 g_;
185};
186
187} // namespace ssa
188} // namespace line
189
190#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:170
std::size_t index(std::size_t n)
Uniform index in [0, n), the reference's 1 + floor(rand*n).
Definition ssa_types.h:160
SsaRng(unsigned long seed)
Definition ssa_types.h:151
Dense matrix and non-owning view.
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:121
std::size_t node
1-based Cache node index
Definition ssa_types.h:122
std::vector< double > delayedprob
The delayed-hit share, EMPTY off a retrieval system.
Definition ssa_types.h:131
std::vector< double > missprob
per class, NaN where undefined
Definition ssa_types.h:123
std::vector< double > hitprob
Definition ssa_types.h:123
std::vector< double > residt
actualresidt: NaN, and NOT a port gap.
Definition ssa_types.h:137
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:69
std::size_t samples
Reaction firings to simulate; options.samples in the reference.
Definition ssa_types.h:73
double warmupfrac
options.config.warmupfrac: the leading fraction of the path discarded before the means are taken.
Definition ssa_types.h:88
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:97
std::string method
default and nrm both select the Next Reaction Method here.
Definition ssa_types.h:71
unsigned long seed
options.seed; LINE's own default is 23000.
Definition ssa_types.h:75
What the analyzer returns, in the same shape as the MVA and fluid results.
Definition ssa_types.h:101
std::vector< double > XN
Definition ssa_types.h:103
std::vector< double > CN
Definition ssa_types.h:103
Matrix< double > UN
Definition ssa_types.h:102
Matrix< double > RN
Definition ssa_types.h:102
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:111
double simulated_time
Simulated time the metrics are averaged over; the reference's totalTime.
Definition ssa_types.h:115
Matrix< double > TN
Definition ssa_types.h:102
std::size_t samples
Reaction firings actually performed.
Definition ssa_types.h:117
Matrix< double > QN
Definition ssa_types.h:102
std::string method
The concrete algorithm, as the reference's method.
Definition ssa_types.h:113
Matrix< double > PreemptN
Definition ssa_types.h:111