LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ssa_nrm_space.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_NRM_SPACE_H
6#define LINE_SOLVERS_SSA_SOLVER_SSA_NRM_SPACE_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverSSA, the EXPLICIT STATE SPACE variant of the Next Reaction Method: a
12 * port of `solver_ssa_nrm_space.m` and of the `else` branch of
13 * `solver_ssa_analyzer_nrm.m` that consumes it, together with
14 * `solver_ssa_findenabled.m`.
15 *
16 * HOW IT DIFFERS FROM THE PLAIN NRM. `solver_ssa_nrm.m` integrates the metrics
17 * along the path and never stores a state, so its cost is independent of how
18 * large the state space is. This variant instead TABULATES the path: it records
19 * every distinct state it visits, the time spent in each, and the whole
20 * propensity vector at each, and the analyzer then forms the means as `pi * A`
21 * rather than as a running time integral. That is the same answer by a different
22 * route -- which is exactly what makes the two testable against each other, and
23 * what the reference's `options.config.state_space_gen` switch selects between
24 * (`none` and `default` take the plain engine, anything else takes this one).
25 *
26 * WHAT THE TABLE BUYS. A propensity is a deterministic function of the state, so
27 * one observation of it IS its exact value and not a sample mean. The departure
28 * rates this variant reports are therefore exact per state, and all the Monte
29 * Carlo error left in the answer sits in `pi`. The plain engine cannot make that
30 * separation, because it never learns that two instants were the same state.
31 *
32 * WHAT IT COSTS, AND WHY THIS PORT REFUSES OPEN MODELS. The table has one row
33 * per distinct state, so the variant is only viable when the reachable space is
34 * small enough to enumerate. In the AGGREGATE reaction network an open model has
35 * no such space: `rtnodes` gives a Sink no outgoing routing, so jobs accumulate
36 * in its slot forever, and a Source's slot is a fictitious token that every
37 * arrival decrements and nothing replenishes. Every firing therefore reaches a
38 * state never seen before, the table grows one row per sample, and `pi` becomes
39 * a uniform law over the trace. The reference does not guard this; here an open
40 * model is REFUSED BY NAME, as is any closed model whose space exceeds the
41 * declared cap, because a truncated table would report a normalized `pi` over
42 * whichever states happened to fit.
43 *
44 * NO PHASE EXPANSION. The state is the (node, class) population vector, so a
45 * non-exponential service process is not represented: `solver_ssa_nrm_space.m`
46 * reads `sn.rates` alone and its propensity switch has five arms (EXT, INF, PS,
47 * FCFS/LCFS, and non-station). A phase-type process is refused by name rather
48 * than silently collapsed onto its mean rate, which would report the right
49 * throughput and the wrong queue length.
50 *
51 * ONE DELIBERATE DIVERGENCE, and it is a reference defect that MATLAB has
52 * already fixed on the other side. `solver_ssa_nrm_space.m` line 295 draws the
53 * destination with `1+find(rand>=cdfVec{kfire},1)`. `find(...,1)` returns the
54 * FIRST true index and the predicate holds on a prefix, so the expression
55 * collapses to 2 for every draw above `cdfVec(1)`: with three or more
56 * destinations the third onwards are never selected. `solver_ssa_nrm.m` line
57 * 1464 names this misuse in a comment and uses the inverse CDF instead, and
58 * `NrmEngine` is ported from the fixed form. This file uses the fixed form too,
59 * because reproducing the defect would silently misroute jobs and would make the
60 * two engines disagree on exactly the models they exist to cross-check.
61 *
62 * DOUBLE ONLY, for the reason `solver_ssa_nrm.h` gives: the clocks are `-log(u)`
63 * of a uniform, there is no exact value to compute, and the answer's error is
64 * the Monte Carlo error rather than the rounding.
65 *
66 * WHY `ssa_find_enabled` IS HERE. It is the standalone form of the scan the
67 * serial engine inlines (`solver_ssa.m` carries both and switches on
68 * `use_inline`), and it answers the same question this variant is built around:
69 * what can fire from a given state, tabulated rather than evaluated on the fly.
70 * It works at the STATE ENCODING level, not on the aggregate populations, so it
71 * does not interoperate with the engine below and is not used by it; the two are
72 * independent ports that share a file.
73 */
74
75#include <algorithm>
76#include <cmath>
77#include <cstddef>
78#include <limits>
79#include <map>
80#include <string>
81#include <type_traits>
82#include <vector>
83
86#include "line/lang/qn/state.h"
89#include "line/util/error.h"
90#include "line/util/matrix.h"
91
92namespace line {
93namespace ssa {
94
95// ---------------------------------------------------------------------------
96// solver_ssa_findenabled.m
97// ---------------------------------------------------------------------------
98
99/** One transition the enabled scan found: where it goes, and at what rate. */
100template <class T>
102 /** `enabled_sync`: index into `sync`, or `sync.size() + g` for a global one. */
103 std::size_t sync = 0;
104 /** `enabled_rates`: rate * p_active * p_route * p_passive. */
105 double rate = 0.0;
106 /** `enabled_next_states{act}`: the whole network state after it fires. */
108};
109
110/**
111 * Port of `solver_ssa_findenabled.m`: every synchronization that can fire in
112 * `state`, with the arrival and departure rates each carries.
113 *
114 * THE ENUMERATION IS WIDER THAN THE REFERENCE'S, and deliberately so. MATLAB
115 * calls `State.afterEvent` with `isSimulation = true`, which SAMPLES one
116 * successor row and returns the probability it was drawn with; the C++
117 * `after_event` is the enumeration-mode handler and returns every successor with
118 * its probability. So one reference entry becomes one entry per (active row,
119 * passive row) pair here, and the caller draws from the flattened list in one
120 * step instead of two. The induced jump chain is identical; the number of
121 * uniforms consumed is not, which is a statement about which random stream this
122 * is and not about which model it simulates.
123 *
124 * A ZERO RATE IS DROPPED rather than rewritten to 1e-38 "so that it is never
125 * selected". Keeping it leaves it in the arrival and departure statistics, where
126 * it is not a rounding difference but a rate the CTMC generator does not have,
127 * and these rates are what the analyzers integrate.
128 *
129 * `arv` and `dep` are indexed [stateful-1][class-1] and are OVERWRITTEN, not
130 * accumulated, so a caller may reuse one pair of buffers across states.
131 */
132template <class T>
133std::vector<EnabledEvent<T>> ssa_find_enabled(const qn::NetworkStruct<T>& sn,
134 const std::vector<qn::Sync<T>>& sync,
135 const std::vector<qn::GlobalSync<T>>& gsync,
136 const qn::NetState<T>& state,
137 std::vector<std::vector<double>>* arv = nullptr,
138 std::vector<std::vector<double>>* dep = nullptr) {
139 const std::size_t local = sn.nodes.size() + 1; // the dummy passive node
140 const std::size_t R = sn.nclasses;
141 const std::size_t NF = sn.stateful_nodes.size();
142 std::vector<EnabledEvent<T>> out;
143 if (arv) arv->assign(NF, std::vector<double>(R, 0.0));
144 if (dep) dep->assign(NF, std::vector<double>(R, 0.0));
145
146 for (std::size_t a = 0; a < sync.size(); ++a) {
147 const qn::Sync<T>& sy = sync[a];
148 const std::size_t isf_a = sn.stateful_index(sy.active.node);
149 if (isf_a == 0) continue; // a stateless node schedules nothing
150 const std::size_t isf_p =
151 sy.passive.node == local ? 0 : sn.stateful_index(sy.passive.node);
152 if (sy.passive.node != local && isf_p == 0) continue;
153
154 const qn::EventOutcome<T> oa = qn::after_event(sn, sy.active.node, state.local[isf_a - 1],
155 sy.active.event, sy.active.cls);
156 double fired = 0.0;
157 for (std::size_t ia = 0; ia < oa.space.size(); ++ia) {
158 const double rate = num_traits<T>::to_double(oa.rate[ia]);
159 const double pa = num_traits<T>::to_double(oa.prob[ia]);
160 if (!(rate > 0) || !(pa > 0)) continue;
161
162 if (sy.passive.node == local) {
164 e.sync = a;
165 e.rate = rate * pa;
166 e.next = state;
167 e.next.local[isf_a - 1] = oa.space[ia];
168 fired += e.rate;
169 out.push_back(e);
170 continue;
171 }
172 // A self-loop synchronization reads the passive node AFTER the
173 // active half has been applied, since they are the same node.
174 const std::vector<T>& src =
175 sy.passive.node == sy.active.node ? oa.space[ia] : state.local[isf_p - 1];
176 const qn::EventOutcome<T> op =
177 qn::after_event(sn, sy.passive.node, src, sy.passive.event, sy.passive.cls);
178 // NO ROWS is the reference's `prob_sync_p = 0`: the destination
179 // cannot take the job, so the upstream departure is disabled rather
180 // than fired into a state that does not exist.
181 for (std::size_t ip = 0; ip < op.space.size(); ++ip) {
182 const double pp = num_traits<T>::to_double(op.prob[ip]);
183 if (!(pp > 0)) continue;
185 e.sync = a;
186 e.rate = rate * pa * num_traits<T>::to_double(sy.passive.prob) * pp;
187 if (!(e.rate > 0)) continue;
188 e.next = state;
189 e.next.local[isf_a - 1] = oa.space[ia];
190 e.next.local[isf_p - 1] = op.space[ip];
191 fired += e.rate;
192 out.push_back(e);
193 }
194 }
195 // A DEP is one job leaving the active node and entering the passive one,
196 // so the same rate is a departure there and an arrival here.
197 if (sy.active.event == lang::EventType::DEP && fired > 0) {
198 if (dep) (*dep)[isf_a - 1][sy.active.cls - 1] += fired;
199 if (arv && isf_p != 0) (*arv)[isf_p - 1][sy.passive.cls - 1] += fired;
200 }
201 }
202
203 for (std::size_t g = 0; g < gsync.size(); ++g) {
204 const qn::GlobalOutcome<T> go = qn::after_global_event(sn, state, gsync[g]);
205 for (std::size_t io = 0; io < go.space.size(); ++io) {
206 const double w = num_traits<T>::to_double(go.rate[io]) *
208 if (!(w > 0)) continue;
210 e.sync = sync.size() + g;
211 e.rate = w;
212 e.next = go.space[io];
213 out.push_back(e);
214 if (gsync[g].active.event != lang::EventType::FIRE) continue;
215 for (std::size_t j = 0; j < gsync[g].passive.size(); ++j) {
216 const qn::ModeEvent<T>& pev = gsync[g].passive[j];
217 const std::size_t pisf = sn.stateful_index(pev.node);
218 if (pisf == 0 || pev.cls == 0 || pev.cls > R) continue;
219 if (pev.event == lang::EventType::PRE && dep)
220 (*dep)[pisf - 1][pev.cls - 1] += w;
221 else if (pev.event == lang::EventType::POST && arv)
222 (*arv)[pisf - 1][pev.cls - 1] += w;
223 }
224 }
225 }
226 return out;
227}
228
229// ---------------------------------------------------------------------------
230// solver_ssa_nrm_space.m
231// ---------------------------------------------------------------------------
232
233/**
234 * The knobs the space variant reads.
235 *
236 * `state_max` is the size of the table this variant is willing to build. It is
237 * not a truncation: the run refuses by name on reaching it, because a `pi`
238 * normalized over the states that happened to fit is a distribution over the
239 * wrong chain.
240 */
242 std::size_t state_max = 20000;
243};
244
245/**
246 * One state of the aggregate chain: the (node, class) populations, and the
247 * ordered buffer contents of every buffered node.
248 *
249 * THE BUFFERS ARE PART OF THE STATE, not bookkeeping beside it. Two FCFS states
250 * with the same populations but a different waiting order have different
251 * propensities, since the rate reads the jobs actually in service; the reference
252 * says as much where its hash function concatenates the buffer contents onto the
253 * population vector. The buffer is newest-first, so a departure takes the last
254 * entry under FCFS and the first under LCFS.
255 */
257 std::vector<double> n;
258 std::vector<std::vector<std::size_t>> buf;
259};
260
261/** What `solver_ssa_nrm_space.m` returns, plus what makes it a measurement. */
262template <class T>
264 /** `outspace`: the distinct states visited, in first-visit order. */
265 std::vector<NrmSpaceState> space;
266 /** `pi`: the fraction of simulated time spent in each of them. */
267 std::vector<double> pi;
268 /**
269 * `depRates`, (states x nnodes*nclasses): the departure rate of each (node,
270 * class) in each state, read off the cached propensity vector.
271 *
272 * EXACT PER STATE, not a sample mean: a propensity is a function of the
273 * state, so observing it once is knowing it. The reference stores the same
274 * vector in `reactCache` for the same reason.
275 */
277 /** `t`: the cumulative time at each firing. */
278 std::vector<double> tran_time;
279 /** `kfires`: which reaction fired. */
280 std::vector<std::size_t> tran_rx;
281 double simulated_time = 0.0;
282 std::size_t samples = 0;
283 unsigned long seed = 0;
284};
285
286/** The metric table, the table it came from, and the stream that produced it. */
287template <class T>
293
294namespace space_detail {
295
296using lang::NodeType;
298
299/** True for the two ordered-buffer disciplines this variant's rate law covers. */
300inline bool space_sched_buffered(SchedStrategy s) {
301 return s == SchedStrategy::FCFS || s == SchedStrategy::LCFS;
302}
303
304} // namespace space_detail
305
306/**
307 * The aggregate NRM engine of `solver_ssa_nrm_space.m`.
308 *
309 * It is a class for the reason `NrmEngine` is, and it is a SEPARATE class rather
310 * than a mode of that one because the two disagree about what a state is: this
311 * one has no phase dimension, five rate laws instead of thirteen, and a table
312 * keyed on (population, buffers) that the plain engine has no place to put. The
313 * pieces that look alike -- the stoichiometry from `rtnodes`, the dependency
314 * sets, the newest-first buffer -- are alike because both are ports of the same
315 * reference construction, and are written out here rather than shared because
316 * `NrmEngine` exposes none of them.
317 */
318template <class T>
320public:
322 : sn_(sn), opt_(opt), rng_(opt.seed) {
323 check();
324 build_layout();
325 build_reactions();
326 build_dependencies();
327 build_initial_state();
328 }
329
330 /** Run `opt.samples` firings and return the tabulated path. */
332
333 /**
334 * The reachable aggregate state space, closed forward from the initial state
335 * over the REACTIONS alone.
336 *
337 * This is the space the run indexes into, and it is derived from the
338 * reaction network rather than from the state handlers, so comparing it with
339 * `reachable_space_generator`'s answer compares two independent accounts of
340 * what the model can do.
341 */
342 std::vector<NrmSpaceState> enumerate_space() const;
343
344 std::size_t nreactions() const { return nrx_; }
345 std::size_t nslots() const { return NS_; }
346 const NrmSpaceState& initial() const { return init_; }
347 /** The whole propensity vector at a state, the reference's `reactCache` entry. */
348 std::vector<double> propensities(const NrmSpaceState& s) const {
349 std::vector<double> A(nrx_, 0.0);
350 for (std::size_t k = 0; k < nrx_; ++k) A[k] = propensity(k, s);
351 return A;
352 }
353
354private:
355 using SchedStrategy = lang::SchedStrategy;
356
357 const qn::NetworkStruct<T>& sn_;
359 SsaRng rng_;
360
361 std::size_t I_ = 0, K_ = 0, M_ = 0, NS_ = 0, nrx_ = 0;
362 std::vector<bool> is_station_;
363 std::vector<std::size_t> to_station_;
364 std::vector<SchedStrategy> sched_;
365 std::vector<double> mi_;
366 std::vector<std::vector<double>> rate_;
367
369 /** Per reaction: the slot it consumes, its destination slots and their CDF. */
370 std::vector<std::size_t> rx_node_, rx_class_, rx_from_, rx_det_dest_;
371 std::vector<std::vector<std::size_t>> rx_to_;
372 std::vector<std::vector<double>> rx_cdf_;
373 std::vector<std::size_t> rx_nnzp_;
374 std::vector<std::vector<std::size_t>> D_;
375
376 NrmSpaceState init_;
377
378 void check();
379 void build_layout();
380 void build_reactions();
381 void build_dependencies();
382 void build_initial_state();
383
384 double class_pop(const std::vector<double>& X, std::size_t ind, std::size_t r) const {
385 return X[ind * K_ + r];
386 }
387 double node_pop(const std::vector<double>& X, std::size_t ind) const {
388 double s = 0.0;
389 for (std::size_t r = 0; r < K_; ++r) s += X[ind * K_ + r];
390 return s;
391 }
392 double propensity(std::size_t j, const NrmSpaceState& s) const;
393 void update_buffers(std::size_t kfire, const std::vector<double>& n,
394 std::vector<std::vector<std::size_t>>& bufs, std::size_t dest_pos,
395 bool have_dest) const;
396
397 static constexpr std::size_t npos = static_cast<std::size_t>(-1);
398};
399
400/**
401 * What this variant cannot represent, refused before a single firing.
402 *
403 * The scheduling list is the propensity switch of `solver_ssa_nrm_space.m`, and
404 * it is much shorter than the plain NRM's: the reference's own analyzer stub
405 * narrows it further still, to INF/EXT/PS.
406 */
407template <class T>
408void NrmSpaceEngine<T>::check() {
409 for (std::size_t i = 0; i < sn_.nstations; ++i) {
410 const SchedStrategy s = sn_.stations[i].sched;
411 if (s == SchedStrategy::INF || s == SchedStrategy::EXT || s == SchedStrategy::PS ||
412 s == SchedStrategy::FCFS || s == SchedStrategy::LCFS)
413 continue;
414 throw UnsupportedError(
415 "solver_ssa_nrm_space: station '" + sn_.stations[i].name + "' uses '" +
416 std::string(lang::sched_to_text(s)) +
417 "' scheduling, which has no rate law in the explicit state-space variant. Its "
418 "propensity switch covers EXT, INF, PS, FCFS and LCFS only; use method='nrm', whose "
419 "engine carries the full set");
420 }
421 for (std::size_t i = 0; i < sn_.nstations; ++i)
422 for (std::size_t r = 0; r < sn_.nclasses; ++r) {
423 if (sn_.disabled[i][r]) continue;
424 const lang::ProcessType pt = sn_.service[i][r].type;
427 continue;
428 throw UnsupportedError(
429 "solver_ssa_nrm_space: class '" + sn_.classes[r].name +
430 "' has non-exponential service at station '" + sn_.stations[i].name +
431 "'. The explicit state-space variant carries no phase dimension -- its state is "
432 "the (node, class) population vector and it reads sn.rates alone -- so a "
433 "phase-type process cannot be represented; use method='nrm', which expands it");
434 }
435 for (std::size_t r = 0; r < sn_.nclasses; ++r)
436 if (!std::isfinite(sn_.classes[r].population))
437 throw UnsupportedError(
438 "solver_ssa_nrm_space: class '" + sn_.classes[r].name +
439 "' is open. In the aggregate reaction network a Sink has no outgoing routing and "
440 "a Source's slot is a fictitious token that every arrival consumes, so the "
441 "aggregate state space of an open model is unbounded: every firing would add a "
442 "row to the table and pi would become the uniform law over the trace. Use "
443 "method='nrm', which integrates the metrics and stores no states");
444 for (const qn::NodeDef& nd : sn_.nodes) {
445 switch (nd.nodetype) {
447 throw UnsupportedError(
448 "solver_ssa_nrm_space: node '" + nd.name +
449 "' is a Cache. The aggregate reaction network is built from sn.rtnodes and "
450 "sn.rates alone and carries no cache contents, so the hit/miss class switch "
451 "cannot be resolved at firing time");
454 throw UnsupportedError(
455 "solver_ssa_nrm_space: node '" + nd.name +
456 "' makes this model a stochastic Petri net. A firing is atomic across every "
457 "arc it touches and is not a (node, class) departure, which is the only "
458 "reaction shape this variant builds");
461 throw UnsupportedError(
462 "solver_ssa_nrm_space: node '" + nd.name +
463 "' makes this a fork-join model. A fork emits on several branches at once, "
464 "which no single-destination reaction can express, and the NRM does not "
465 "handle fork-join in any codebase");
466 default:
467 break;
468 }
469 }
470 // `sn.isslc`, the SELF-LOOPING CLASS whose reaction consumes a job and
471 // produces none, has no field in the C++ NetworkStruct, so the reference's
472 // `Srow(from) = -Inf` branch is unreachable here rather than unported.
473}
474
475template <class T>
476void NrmSpaceEngine<T>::build_layout() {
477 I_ = sn_.nof_nodes();
478 K_ = sn_.nclasses;
479 M_ = sn_.nstations;
480 NS_ = I_ * K_;
481
482 is_station_.assign(I_, false);
483 to_station_.assign(I_, npos);
484 for (std::size_t i = 0; i < I_; ++i)
485 if (sn_.nodes[i].station != 0) {
486 is_station_[i] = true;
487 to_station_[i] = sn_.nodes[i].station - 1;
488 }
489 sched_.assign(M_, SchedStrategy::FCFS);
490 for (std::size_t i = 0; i < M_; ++i) sched_[i] = sn_.stations[i].sched;
491
492 // A non-station is given the immediate rate and an unbounded server count,
493 // which is what makes a Router or a ClassSwitch a pass-through in the
494 // aggregate chain rather than a place a job can accumulate.
495 mi_.assign(I_, lang::GlobalConstants::MaxInt);
496 rate_.assign(I_, std::vector<double>(K_, 0.0));
497 for (std::size_t i = 0; i < I_; ++i) {
498 if (is_station_[i]) {
499 const std::size_t ist = to_station_[i];
500 mi_[i] = sn_.stations[ist].nservers;
501 if (!std::isfinite(mi_[i])) mi_[i] = lang::GlobalConstants::MaxInt;
502 for (std::size_t r = 0; r < K_; ++r)
503 if (!sn_.disabled[ist][r])
504 rate_[i][r] = num_traits<T>::to_double(sn_.rates(ist, r));
505 } else {
506 for (std::size_t r = 0; r < K_; ++r) rate_[i][r] = lang::GlobalConstants::Immediate;
507 }
508 }
509}
510
511/**
512 * The stoichiometry, one reaction per (node, class).
513 *
514 * There is no phase dimension, so a reaction IS a departure: it consumes one job
515 * from its own slot and deposits the routing probability into every slot
516 * `rtnodes` sends it to. The fractional entries are not a fluid relaxation --
517 * they are the weights the destination draw reads, and exactly one destination
518 * receives the whole job at firing time.
519 */
520template <class T>
521void NrmSpaceEngine<T>::build_reactions() {
522 nrx_ = I_ * K_;
523 rx_node_.assign(nrx_, 0);
524 rx_class_.assign(nrx_, 0);
525 rx_from_.assign(nrx_, 0);
526 rx_det_dest_.assign(nrx_, npos);
527 rx_to_.assign(nrx_, std::vector<std::size_t>());
528 rx_cdf_.assign(nrx_, std::vector<double>());
529 rx_nnzp_.assign(nrx_, 0);
530 S_ = Matrix<double>(NS_, nrx_, 0.0);
531
532 for (std::size_t ind = 0; ind < I_; ++ind)
533 for (std::size_t r = 0; r < K_; ++r) {
534 const std::size_t k = ind * K_ + r;
535 rx_node_[k] = ind;
536 rx_class_[k] = r;
537 rx_from_[k] = k;
538 S_(k, k) -= 1.0;
539 for (std::size_t jnd = 0; jnd < I_; ++jnd)
540 for (std::size_t s = 0; s < K_; ++s) {
541 const double p =
542 num_traits<T>::to_double(sn_.rtnodes(ind * K_ + r, jnd * K_ + s));
543 if (!(p > 0.0)) continue;
544 S_(jnd * K_ + s, k) += p;
545 }
546 }
547
548 // `P = S; P(P<0) = P(P<0)+1`: a class routing back into its own slot must
549 // compete with the other destinations on equal footing, which subtracting
550 // the consumed job first is what achieves.
551 for (std::size_t k = 0; k < nrx_; ++k) {
552 std::vector<double> Pcol(NS_, 0.0);
553 for (std::size_t i = 0; i < NS_; ++i) {
554 const double v = S_(i, k);
555 Pcol[i] = v < 0.0 ? v + 1.0 : v;
556 if (v > 0.0 && rx_det_dest_[k] == npos) rx_det_dest_[k] = i;
557 }
558 for (std::size_t i = 0; i < NS_; ++i)
559 if (Pcol[i] != 0.0) ++rx_nnzp_[k];
560 // A job that routes back into the slot it came from with probability one
561 // leaves an ALL-ZERO stoichiometry column, so `find(S(:,k) > 0)` finds
562 // nothing and the reference records no destination. At a buffered
563 // station that loses the job from the buffer while leaving the
564 // population alone, breaking numel(buf) == max(0, total - mi) and
565 // silencing the rate law. Naming the source as the destination is the
566 // same arithmetic on the population and the correct one on the buffer.
567 if (rx_nnzp_[k] == 1 && rx_det_dest_[k] == npos && Pcol[rx_from_[k]] != 0.0)
568 rx_det_dest_[k] = rx_from_[k];
569 if (rx_nnzp_[k] > 1) {
570 double acc = 0.0;
571 for (std::size_t i = 0; i < NS_; ++i)
572 if (Pcol[i] != 0.0) {
573 rx_to_[k].push_back(i);
574 acc += Pcol[i];
575 rx_cdf_[k].push_back(acc);
576 }
577 }
578 }
579}
580
581/**
582 * The dependency sets: which propensities a firing invalidates.
583 *
584 * Every rate law reads its node's WHOLE class-count vector, so a firing that
585 * touches any slot of a node invalidates every reaction consuming from that
586 * node, not merely the slot that moved.
587 */
588template <class T>
589void NrmSpaceEngine<T>::build_dependencies() {
590 D_.assign(nrx_, std::vector<std::size_t>());
591 for (std::size_t k = 0; k < nrx_; ++k) {
592 std::vector<bool> touched(I_, false);
593 for (std::size_t i = 0; i < NS_; ++i)
594 if (S_(i, k) != 0.0) touched[i / K_] = true;
595 for (std::size_t ind = 0; ind < I_; ++ind) {
596 if (!touched[ind]) continue;
597 for (std::size_t r = 0; r < K_; ++r) D_[k].push_back(ind * K_ + r);
598 }
599 std::sort(D_[k].begin(), D_[k].end());
600 D_[k].erase(std::unique(D_[k].begin(), D_[k].end()), D_[k].end());
601 }
602}
603
604/**
605 * The initial state.
606 *
607 * The reference reads `sn.state` through `State.toMarginalAggr`; this port has
608 * no State package on the aggregate side, so it uses the same rule
609 * `solver_ssa_nrm.h` does -- a closed class starts entirely at its reference
610 * station. The chain is ergodic, so the steady-state means do not depend on the
611 * choice.
612 */
613template <class T>
614void NrmSpaceEngine<T>::build_initial_state() {
615 init_.n.assign(NS_, 0.0);
616 init_.buf.assign(I_, std::vector<std::size_t>());
617 for (std::size_t r = 0; r < K_; ++r) {
618 const double pop = sn_.classes[r].population;
619 if (!(pop > 0.0)) continue;
620 const std::size_t rs = sn_.classes[r].refstat;
621 if (rs < 1 || rs > M_)
622 throw InputError("solver_ssa_nrm_space: class '" + sn_.classes[r].name +
623 "' has no reference station");
624 init_.n[(sn_.station_to_node[rs - 1] - 1) * K_ + r] = pop;
625 }
626 // The buffer holds exactly the waiting jobs, so its length is the excess
627 // over the server count; which classes wait is immaterial to the steady
628 // state, so they are taken in class order.
629 for (std::size_t ind = 0; ind < I_; ++ind) {
630 if (!is_station_[ind] || !space_detail::space_sched_buffered(sched_[to_station_[ind]]))
631 continue;
632 double waiting = std::max(0.0, node_pop(init_.n, ind) - mi_[ind]);
633 for (std::size_t r = 0; r < K_ && waiting > 0.0; ++r) {
634 const double take = std::min(waiting, init_.n[ind * K_ + r]);
635 for (std::size_t c = 0; c < static_cast<std::size_t>(take); ++c)
636 init_.buf[ind].push_back(r);
637 waiting -= take;
638 }
639 }
640}
641
642/** The five-armed propensity switch of `solver_ssa_nrm_space.m` lines 136-161. */
643template <class T>
644double NrmSpaceEngine<T>::propensity(std::size_t j, const NrmSpaceState& st) const {
645 const std::size_t ind = rx_node_[j], r = rx_class_[j];
646 const std::vector<double>& X = st.n;
647 if (!is_station_[ind]) {
648 // A pass-through node moves at most one job at a time at the immediate
649 // rate: the min is what keeps an empty node silent rather than firing at
650 // 1e8 into a slot that holds nothing.
651 return rate_[ind][r] * std::min(1.0, class_pop(X, ind, r));
652 }
653 const std::size_t ist = to_station_[ind];
654 const double eps = lang::GlobalConstants::Zero;
655 switch (sched_[ist]) {
656 case SchedStrategy::EXT:
657 return rate_[ind][r];
658 case SchedStrategy::INF:
659 return rate_[ind][r] * class_pop(X, ind, r);
660 case SchedStrategy::PS: {
661 if (K_ == 1) return rate_[ind][r] * std::min(mi_[ind], class_pop(X, ind, r));
662 const double tot = node_pop(X, ind);
663 return rate_[ind][r] * (class_pop(X, ind, r) / (eps + tot)) *
664 std::min(mi_[ind], eps + tot);
665 }
666 case SchedStrategy::FCFS:
667 case SchedStrategy::LCFS: {
668 // The jobs actually in service are the class population less the
669 // class-r jobs still waiting, which is why the buffer has to be part
670 // of the state and not merely of the bookkeeping.
671 double waiting = 0.0;
672 for (std::size_t c : st.buf[ind])
673 if (c == r) waiting += 1.0;
674 return rate_[ind][r] * std::max(0.0, class_pop(X, ind, r) - waiting);
675 }
676 default:
677 throw UnsupportedError(
678 "solver_ssa_nrm_space: the scheduling policy '" +
679 std::string(lang::sched_to_text(sched_[ist])) + "' at station '" +
680 sn_.stations[ist].name + "' has no rate law in the explicit state-space variant");
681 }
682}
683
684/**
685 * `updateBuffers`: the ordered buffers after reaction `kfire` has fired.
686 *
687 * A departure frees a server, so the discipline promotes a waiting job: FCFS
688 * takes the oldest, which is the LAST entry of a newest-first buffer, and LCFS
689 * the newest, which is the first. An arrival joins the buffer only when it finds
690 * every server busy, and the test is made on the POST-firing population, which
691 * already counts the arriving job.
692 */
693template <class T>
694void NrmSpaceEngine<T>::update_buffers(std::size_t kfire, const std::vector<double>& n,
695 std::vector<std::vector<std::size_t>>& bufs,
696 std::size_t dest_pos, bool have_dest) const {
697 const std::size_t ind = rx_node_[kfire];
698 if (is_station_[ind] && !bufs[ind].empty()) {
699 const SchedStrategy s = sched_[to_station_[ind]];
700 if (s == SchedStrategy::FCFS) bufs[ind].pop_back();
701 else if (s == SchedStrategy::LCFS) bufs[ind].erase(bufs[ind].begin());
702 }
703 if (!have_dest || dest_pos == npos) return;
704 const std::size_t jnd = dest_pos / K_, s = dest_pos % K_;
705 if (!is_station_[jnd] || !space_detail::space_sched_buffered(sched_[to_station_[jnd]])) return;
706 if (node_pop(n, jnd) > mi_[jnd]) bufs[jnd].insert(bufs[jnd].begin(), s);
707}
708
709namespace space_detail {
710
711/** The table key: the populations, then each node's buffer behind a separator. */
712inline std::vector<double> space_key(const NrmSpaceState& s) {
713 std::vector<double> key = s.n;
714 for (std::size_t i = 0; i < s.buf.size(); ++i) {
715 // The separator keeps two different splits of the same concatenation
716 // from colliding, which a plain flatten would allow.
717 key.push_back(-1.0);
718 for (std::size_t j = 0; j < s.buf[i].size(); ++j)
719 key.push_back(static_cast<double>(s.buf[i][j]) + 1.0);
720 }
721 return key;
722}
723
724} // namespace space_detail
725
726template <class T>
727std::vector<NrmSpaceState> NrmSpaceEngine<T>::enumerate_space() const {
728 std::vector<NrmSpaceState> out;
729 std::map<std::vector<double>, std::size_t> seen;
730 std::vector<std::size_t> stack;
731 seen[space_detail::space_key(init_)] = 0;
732 out.push_back(init_);
733 stack.push_back(0);
734
735 while (!stack.empty()) {
736 const std::size_t si = stack.back();
737 stack.pop_back();
738 const NrmSpaceState st = out[si]; // by value: `out` grows inside the loop
739 for (std::size_t k = 0; k < nrx_; ++k) {
740 if (!(propensity(k, st) > 0.0)) continue;
741 // Every destination the firing could draw is a distinct successor,
742 // which is what makes this the reachable set of the CHAIN rather
743 // than of one realization of it.
744 std::vector<std::size_t> dests;
745 if (rx_nnzp_[k] > 1) dests = rx_to_[k];
746 else if (rx_det_dest_[k] != npos) dests.push_back(rx_det_dest_[k]);
747 else dests.push_back(npos);
748 for (std::size_t d = 0; d < dests.size(); ++d) {
749 NrmSpaceState ns = st;
750 ns.n[rx_from_[k]] -= 1.0;
751 if (dests[d] != npos) ns.n[dests[d]] += 1.0;
752 update_buffers(k, ns.n, ns.buf, dests[d], dests[d] != npos);
753 const std::vector<double> key = space_detail::space_key(ns);
754 if (seen.find(key) != seen.end()) continue;
755 if (out.size() >= opt_.state_max)
756 throw UnsupportedError(
757 "solver_ssa_nrm_space: the reachable aggregate state space exceeds the "
758 "cap of " + std::to_string(opt_.state_max) +
759 " states. The explicit state-space variant tabulates one row per state, "
760 "so a larger space is refused rather than truncated: a pi normalized over "
761 "the states that happened to fit is the law of a different chain. Raise "
762 "state_max, or use method='nrm', which stores no states");
763 seen[key] = out.size();
764 out.push_back(ns);
765 stack.push_back(out.size() - 1);
766 }
767 }
768 }
769 return out;
770}
771
772template <class T>
774 // The clocks are -log(u), so the backend must have a logarithm at all. The
775 // analyzer gates on `double` before instantiating this; a caller reaching
776 // the assert got past that gate and deserves a sentence rather than a
777 // compile error inside the uniform draw.
779 "solver_ssa_nrm_space: the Next Reaction Method draws its clocks as -log(u), "
780 "which needs transcendental arithmetic");
781
783 out.seed = opt_.seed;
784 if (nrx_ == 0) return out;
785
786 NrmSpaceState cur = init_;
787 std::vector<double> Ak(nrx_, 0.0), Pk(nrx_, 0.0), Tk(nrx_, 0.0), tau(nrx_, 0.0);
788 for (std::size_t k = 0; k < nrx_; ++k) {
789 Ak[k] = propensity(k, cur);
790 Pk[k] = -std::log(rng_.uniform());
791 tau[k] = Ak[k] > 0.0 ? (Pk[k] - Tk[k]) / Ak[k] : std::numeric_limits<double>::infinity();
792 }
793
794 // The table. `pi` accumulates holding time and the propensity vector is
795 // stored the first time a state is seen, exactly as `reactCache` does.
796 std::map<std::vector<double>, std::size_t> index;
797 std::vector<std::vector<double>> cached;
798 double total_time = 0.0;
799
800 out.tran_time.reserve(opt_.samples);
801 out.tran_rx.reserve(opt_.samples);
802 for (std::size_t n = 0; n < opt_.samples; ++n) {
803 std::size_t kfire = 0;
804 double dt = std::numeric_limits<double>::infinity();
805 for (std::size_t k = 0; k < nrx_; ++k)
806 if (tau[k] < dt) {
807 dt = tau[k];
808 kfire = k;
809 }
810 if (std::isinf(dt))
811 throw NumericError(
812 "solver_ssa_nrm_space: deadlock -- every reaction has propensity zero, so the "
813 "sample path cannot advance");
814
815 // The state is recorded with the time spent IN it, so the pair belongs
816 // to the state before the firing.
817 const std::vector<double> key = space_detail::space_key(cur);
818 const std::map<std::vector<double>, std::size_t>::const_iterator it = index.find(key);
819 std::size_t si;
820 if (it != index.end()) {
821 si = it->second;
822 } else {
823 if (out.space.size() >= opt_.state_max)
824 throw UnsupportedError(
825 "solver_ssa_nrm_space: the path has visited more than the cap of " +
826 std::to_string(opt_.state_max) +
827 " distinct states. The explicit state-space variant tabulates one row per "
828 "state, so it is refused rather than truncated: a pi normalized over the "
829 "states that happened to fit is the law of a different chain. Raise "
830 "state_max, or use method='nrm', which stores no states");
831 si = out.space.size();
832 index[key] = si;
833 out.space.push_back(cur);
834 out.pi.push_back(0.0);
835 cached.push_back(Ak);
836 }
837 out.pi[si] += dt;
838 total_time += dt;
839 out.tran_time.push_back(total_time);
840 out.tran_rx.push_back(kfire);
841
842 // Apply the firing.
843 std::size_t dest = npos;
844 if (rx_nnzp_[kfire] > 1) {
845 // Inverse CDF: the smallest index whose cumulative weight exceeds
846 // the draw. See the divergence note at the top of this file.
847 const double u = rng_.uniform();
848 std::size_t sel = rx_cdf_[kfire].size() - 1;
849 for (std::size_t i = 0; i < rx_cdf_[kfire].size(); ++i)
850 if (rx_cdf_[kfire][i] > u) {
851 sel = i;
852 break;
853 }
854 dest = rx_to_[kfire][sel];
855 cur.n[rx_from_[kfire]] -= 1.0;
856 cur.n[dest] += 1.0;
857 } else {
858 for (std::size_t i = 0; i < NS_; ++i)
859 if (S_(i, kfire) != 0.0) cur.n[i] += S_(i, kfire);
860 dest = rx_det_dest_[kfire];
861 }
862 update_buffers(kfire, cur.n, cur.buf, dest, dest != npos);
863
864 for (std::size_t k = 0; k < nrx_; ++k) Tk[k] += Ak[k] * dt;
865 // D covers every node the stoichiometry touched, source and every
866 // candidate destination alike, and the arrival's buffer join is already
867 // applied when those are refreshed. What it does NOT cover is a
868 // PROMOTION: the departing job's replacement enters service without any
869 // slot moving, and at a self-routing reaction the column is all zeros
870 // and D is empty outright. A firing at a buffered node therefore forces
871 // a sweep, which is the same order as the clock scan above and so costs
872 // nothing asymptotically.
873 for (std::size_t k : D_[kfire]) Ak[k] = propensity(k, cur);
874 if (is_station_[rx_node_[kfire]] &&
875 space_detail::space_sched_buffered(sched_[to_station_[rx_node_[kfire]]]))
876 for (std::size_t k = 0; k < nrx_; ++k) Ak[k] = propensity(k, cur);
877
878 Pk[kfire] -= std::log(rng_.uniform());
879 for (std::size_t k = 0; k < nrx_; ++k)
880 tau[k] =
881 Ak[k] > 0.0 ? (Pk[k] - Tk[k]) / Ak[k] : std::numeric_limits<double>::infinity();
882 out.samples = n + 1;
883 }
884
885 double tot = 0.0;
886 for (std::size_t s = 0; s < out.pi.size(); ++s) tot += out.pi[s];
887 if (tot > 0)
888 for (std::size_t s = 0; s < out.pi.size(); ++s) out.pi[s] /= tot;
889 out.simulated_time = total_time;
890
891 // The departure rate of (node, class) in a state IS the propensity of its
892 // reaction there, which is why the cache is what the analyzer reads.
893 out.dep_rates = Matrix<double>(out.space.size(), NS_, 0.0);
894 for (std::size_t s = 0; s < out.space.size(); ++s)
895 for (std::size_t k = 0; k < nrx_; ++k)
896 out.dep_rates(s, rx_from_[k]) += cached[s][k];
897 return out;
898}
899
900/** `solver_ssa_nrm_space.m`: run the tabulating engine. */
901template <class T>
907
908/**
909 * Port of the `else` branch of `solver_ssa_analyzer_nrm.m`, the one
910 * `state_space_gen` selects: the means as `pi * A` over the tabulated states.
911 *
912 * THE STANDALONE `solver_ssa_nrm_space_analyzer.m` IS A STUB. Its whole body is
913 * the INF/EXT/PS scheduling gate and a debug line; it assigns none of its nine
914 * declared outputs, so calling it in MATLAB raises "Output argument not
915 * assigned". The working analyzer is the branch ported here, whose utilization
916 * switch admits FCFS and LCFS as well -- which is also the set the engine has
917 * rate laws for, so narrowing to the stub's three would refuse models this
918 * variant can simulate. The stub's narrower gate is therefore NOT reproduced,
919 * and the divergence is named rather than hidden.
920 *
921 * A SOURCE REPORTS ZERO QLen AND ZERO Util, not the negative number the
922 * reference's `UN(ist,:) = QN(ist,:)` produces for an EXT station: the Source
923 * slot is a fictitious token that arrivals consume, so its time average is
924 * 1 - E[jobs in system]. `solver_ssa_nrm.h` states the rule at length and this
925 * port applies it everywhere. It is moot for the models this variant accepts,
926 * since an open one is refused outright, and it is kept so the rule holds
927 * uniformly.
928 */
929template <class T>
931 const SsaNrmSpaceOptions& opt) {
932 // `if constexpr`, not a run-time test: the engine takes the logarithm of a
933 // uniform, so a Rational instantiation would fail to COMPILE rather than
934 // refuse. The gate has to keep the body from being instantiated at all.
935 if constexpr (!std::is_same<T, double>::value) {
936 (void)sn;
937 (void)opt;
938 throw UnsupportedError(
939 "solver_ssa_nrm_space: an SSA sample path is generated from exponential clocks, which "
940 "are logarithms of uniform draws; there is no exact value to compute and a wider "
941 "float carries no information the Monte Carlo error does not swamp. Rerun with "
942 "--arith double");
943 } else {
945 const std::size_t M = sn.nstations, K = sn.nclasses;
947 out.seed = opt.seed;
948
950 out.run = eng.run();
951 const SsaNrmSpaceRun<T>& r = out.run;
952
953 SsaSolution& a = out.avg;
954 a.method = "nrm.space";
955 a.samples = r.samples;
957 a.QN = Matrix<double>(M, K, 0.0);
958 a.UN = Matrix<double>(M, K, 0.0);
959 a.RN = Matrix<double>(M, K, 0.0);
960 a.TN = Matrix<double>(M, K, 0.0);
961 a.XN.assign(K, 0.0);
962 a.CN.assign(K, 0.0);
963
964 for (std::size_t k = 0; k < K; ++k) {
965 const std::size_t refnd = sn.station_to_node[sn.classes[k].refstat - 1];
966 for (std::size_t s = 0; s < r.space.size(); ++s)
967 a.XN[k] += r.pi[s] * r.dep_rates(s, (refnd - 1) * K + k);
968 }
969
970 for (std::size_t ist = 0; ist < M; ++ist) {
971 const std::size_t ind = sn.station_to_node[ist];
972 for (std::size_t k = 0; k < K; ++k)
973 for (std::size_t s = 0; s < r.space.size(); ++s) {
974 a.TN(ist, k) += r.pi[s] * r.dep_rates(s, (ind - 1) * K + k);
975 a.QN(ist, k) += r.pi[s] * r.space[s].n[(ind - 1) * K + k];
976 }
977
978 const SchedStrategy sched = sn.stations[ist].sched;
979 if (sched == SchedStrategy::EXT ||
980 sn.stations[ist].nodetype == lang::NodeType::Source) {
981 for (std::size_t k = 0; k < K; ++k) a.QN(ist, k) = 0.0;
982 continue;
983 }
984 if (sched == SchedStrategy::INF) {
985 for (std::size_t k = 0; k < K; ++k) a.UN(ist, k) = a.QN(ist, k);
986 continue;
987 }
988 // PS, FCFS and LCFS: the carried load T/(mu*c). A class-dependent
989 // station normalizes by its DECLARED peak instead, which is the only
990 // thing its utilization can be a fraction of.
991 const bool is_cd = static_cast<bool>(sn.stations[ist].cdscaling);
992 const bool is_jd = static_cast<bool>(sn.stations[ist].jdscaling);
993 for (std::size_t k = 0; k < K; ++k) {
994 if (sn.disabled[ist][k]) continue;
995 const double mu = num_traits<T>::to_double(sn.rates(ist, k));
996 if (!(mu > 0)) continue;
997 // The divisor is the PRODUCT of whichever declared peaks are
998 // present -- `solver_ssa_nrm.m:1784-1785` -- and the server count
999 // when neither is.
1000 double sdiv = sn.stations[ist].nservers;
1001 if (is_cd || is_jd) {
1002 sdiv = 1.0;
1003 const std::vector<T>* pks[2] = {&sn.stations[ist].cdscalingpeak,
1004 &sn.stations[ist].jdscalingpeak};
1005 const char* names[2] = {"setClassDependence", "setJointDependence"};
1006 const bool on[2] = {is_cd, is_jd};
1007 for (std::size_t h = 0; h < 2; ++h) {
1008 if (!on[h]) continue;
1009 const std::vector<T>& pk = *pks[h];
1010 if (pk.size() <= k || !(num_traits<T>::to_double(pk[k]) > 0))
1011 throw InputError(
1012 "solver_ssa_nrm_space: station '" + sn.stations[ist].name +
1013 "' declares a dependent scaling with no declared peak rate. "
1014 "Utilization there is T/mu/peak, so pass the peak to " + names[h]);
1015 sdiv *= num_traits<T>::to_double(pk[k]);
1016 }
1017 }
1018 a.UN(ist, k) = sdiv > 0 ? a.TN(ist, k) / mu / sdiv : 0.0;
1019 }
1020 }
1021
1022 for (std::size_t k = 0; k < K; ++k) {
1023 for (std::size_t ist = 0; ist < M; ++ist)
1024 a.RN(ist, k) = a.TN(ist, k) > 0 ? a.QN(ist, k) / a.TN(ist, k) : 0.0;
1025 if (a.XN[k] > 0) a.CN[k] = sn.classes[k].population / a.XN[k];
1026 }
1027 return out;
1028 }
1029}
1030
1031} // namespace ssa
1032} // namespace line
1033
1034#endif // LINE_SOLVERS_SSA_SOLVER_SSA_NRM_SPACE_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t nof_nodes() const
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< std::vector< bool > > disabled
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::size_t > station_to_node
(nstations) 1-based node index
The aggregate NRM engine of solver_ssa_nrm_space.m.
NrmSpaceEngine(const qn::NetworkStruct< T > &sn, const SsaNrmSpaceOptions &opt)
std::vector< double > propensities(const NrmSpaceState &s) const
The whole propensity vector at a state, the reference's reactCache entry.
SsaNrmSpaceRun< T > run()
Run opt.samples firings and return the tabulated path.
std::vector< NrmSpaceState > enumerate_space() const
The reachable aggregate state space, closed forward from the initial state over the REACTIONS alone.
const NrmSpaceState & initial() const
The uniform source, MATLAB's rand.
Definition ssa_types.h:129
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Dense matrix and non-owning view.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
@ POST
produce to a place or queue buffer
Definition lang_types.h:122
@ DEP
a job departs
Definition lang_types.h:115
@ FIRE
an SPN mode fires
Definition lang_types.h:120
@ PRE
consume from a place or queue buffer, no server effect
Definition lang_types.h:121
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
SchedStrategy
The three scheduling disciplines the AMVA and Schmidt recursions branch on.
GlobalOutcome< T > after_global_event(const NetworkStruct< T > &sn, const NetState< T > &glspace, const GlobalSync< T > &gl)
Port of State.afterGlobalEvent: an SPN mode ENABLEs or FIREs.
EventOutcome< T > after_event(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls, bool no_promote=false, const T &aux_rate=num_traits< T >::from_int(0))
Port of State.afterEvent: the successors of one event at one NODE.
std::vector< EnabledEvent< T > > ssa_find_enabled(const qn::NetworkStruct< T > &sn, const std::vector< qn::Sync< T > > &sync, const std::vector< qn::GlobalSync< T > > &gsync, const qn::NetState< T > &state, std::vector< std::vector< double > > *arv=nullptr, std::vector< std::vector< double > > *dep=nullptr)
Port of solver_ssa_findenabled.m: every synchronization that can fire in state, with the arrival and ...
SsaNrmSpaceSolution< T > solver_ssa_nrm_space_analyzer(const qn::NetworkStruct< T > &sn, const SsaNrmSpaceOptions &opt)
Port of the else branch of solver_ssa_analyzer_nrm.m, the one state_space_gen selects: the means as p...
SsaNrmSpaceRun< T > solver_ssa_nrm_space(const qn::NetworkStruct< T > &sn, const SsaNrmSpaceOptions &opt)
solver_ssa_nrm_space.m: run the tabulating engine.
A queueing network and its refreshed NetworkStruct.
Controls, results and the random source of SolverSSA.
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...
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:674
static constexpr double Zero
Definition lang_types.h:670
static constexpr double MaxInt
Stand-in for an unbounded COUNT, MATLAB GlobalConstants.MaxInt.
Definition lang_types.h:679
What one event produces at one node: the successor rows, their rates and their probabilities,...
std::vector< T > prob
per-row probability of the choice
std::vector< std::vector< T > > space
successor local state rows
std::vector< T > rate
per-row rate, -1 on a passive half
What one global event produces: a whole network state per outcome.
std::vector< T > rate
std::vector< NetState< T > > space
std::vector< T > prob
A GLOBAL synchronization: an SPN mode event and the place arcs it drives.
One half of a GLOBAL synchronization: a mode event at a node.
std::size_t node
1-based node index (a Transition, or a place)
std::size_t cls
1-based class the arc moves
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
One synchronization: an ACTIVE event and the PASSIVE event it drives.
SyncEvent< T > passive
SyncEvent< T > active
One transition the enabled scan found: where it goes, and at what rate.
qn::NetState< T > next
enabled_next_states{act}: the whole network state after it fires.
double rate
enabled_rates: rate * p_active * p_route * p_passive.
std::size_t sync
enabled_sync: index into sync, or sync.size() + g for a global one.
One state of the aggregate chain: the (node, class) populations, and the ordered buffer contents of e...
std::vector< std::vector< std::size_t > > buf
The knobs the space variant reads.
What solver_ssa_nrm_space.m returns, plus what makes it a measurement.
std::vector< double > tran_time
t: the cumulative time at each firing.
std::vector< double > pi
pi: the fraction of simulated time spent in each of them.
std::vector< std::size_t > tran_rx
kfires: which reaction fired.
Matrix< double > dep_rates
depRates, (states x nnodes*nclasses): the departure rate of each (node, class) in each state,...
std::vector< NrmSpaceState > space
outspace: the distinct states visited, in first-visit order.
The metric table, the table it came from, and the stream that produced it.
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:69
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
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