LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ssa_nrm_spn.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_SPN_H
6#define LINE_SOLVERS_SSA_SOLVER_SSA_NRM_SPN_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverSSA on a STOCHASTIC PETRI NET: the port of `solver_ssa_nrm_spn`, the
12 * sub-engine `solver_ssa_nrm.m:174` hands a net carrying Transition nodes to.
13 *
14 * WHY IT IS A SEPARATE ENGINE. The queueing reaction builder of
15 * `solver_ssa_nrm.h` reads a state vector of JOBS PER (node, class, phase) and
16 * a departure is the absorption of a service process. A Petri net has neither:
17 * a Place holds TOKENS, a Transition mode is a reaction whose stoichiometry is
18 * the arc incidence, and nothing in the net has a service phase. The reference
19 * therefore branches on `any(sn.nodetype == NodeType.Transition)` before it
20 * builds a single reaction, and so does this port.
21 *
22 * THE MAPPING IS EXACT, not an approximation. A Place holds a per-class token
23 * count (one slot of the state vector), and a timed Transition mode is a
24 * reaction: input (enabling) arcs consume, output (firing) arcs produce.
25 * Enabling is a propensity gate -- every input place at or above its arc
26 * weight, every inhibitor place strictly below its threshold -- and a
27 * single-server mode fires at its exponential rate while an infinite or
28 * k-server mode fires at that rate times its enabling degree. One firing
29 * applies the stoichiometry once, which is the atomic GSPN firing the exact
30 * CTMC, JMT and the standard GSPN tools all take.
31 *
32 * IMMEDIATE MODES ARE NOT REACTIONS. They fire in zero time, so they are
33 * resolved by VANISHING-MARKING ELIMINATION: after every timed firing (and once
34 * on the initial marking) every enabled immediate mode is fired -- highest
35 * firing priority first and, among equal priority, drawn in proportion to
36 * firing weight -- until the marking is tangible. The timed race therefore only
37 * ever samples from tangible markings and the immediate modes consume no
38 * simulated time.
39 *
40 * A SOURCE IS NOT A TRANSITION and needs a reaction of its own, or the Place it
41 * feeds stays empty and the net deadlocks on the first draw. Splitting a
42 * Poisson stream by independent routing probabilities yields independent
43 * Poisson streams, so the edge of probability p carries rate lambda*p exactly;
44 * the reaction has an EMPTY enabling set (a constant propensity) and deposits
45 * one token into the routed Place slot.
46 *
47 * WHAT IT REFUSES, and each refusal is the reference's: a non-exponential timed
48 * firing (representing an in-flight firing's phase would need per-mode phase
49 * state the reaction network does not carry), a non-exponential Source arrival
50 * into a Place, a Source that reaches no Place, an infinite initial marking, and
51 * a marking-dependent firing rate -- the last under EVERY SSA method, since no
52 * SSA engine applies the g(marking) multiplier and answering with the nominal
53 * rate would be silently wrong. `spn_nrm_supported` is the predicate; the
54 * `default` dispatch reads it to decide whether to prefer this engine, and the
55 * explicit `nrm` arm reads it to refuse by name.
56 *
57 * THE SLOT LAYOUT IS FLAT, one slot per (node, class), where the queueing
58 * engine's is per (node, class, PHASE). Nothing here has a phase: a Place holds
59 * tokens rather than jobs in service, and the reference's own SPN path only
60 * ever addresses `phOff(place, class) + 1`, the first phase slot of the pair.
61 * The two layouts therefore agree on every slot this engine touches.
62 *
63 * NOT A SAMPLE-PATH TWIN OF THE REFERENCE. The random stream is this port's
64 * MT19937 (`SsaRng`) and the draw order is the algorithm's, so a seeded run
65 * matches the reference STATISTICALLY and never bit for bit, exactly as the
66 * queueing NRM does.
67 *
68 * A FLUID NET (FSPN, `Place::set_fluid`) runs a PIECEWISE-DETERMINISTIC loop instead, the reference's
69 * `spnFluidRun`. Between two firings the marking is constant, so every fluid level moves LINEARLY at the slope
70 * its running flows give it: a flow of a timed mode counts once per running server, min(enabling degree,
71 * servers), and only while every guard of the mode holds. Three things end a segment and the first one wins:
72 * a FIRING (a Gibson-Bruck clock runs out; a mode whose rate depends on a level has a hazard that varies along
73 * the segment, so its clock is integrated by adaptive quadrature and the epoch is a root of the cumulative
74 * hazard), a GUARD crossing (nothing fires; the guards are re-read and the immediate modes the crossing enabled
75 * fire in zero time), or a BOUND hit at 0 or the capacity (nothing fires; the slope that would carry the level
76 * out is cut from then on). A firing applies the token stoichiometry and then the mode's IMPULSES, each clipped
77 * at the bounds. A guard is inclusive and read with a LOOKAHEAD at the threshold: it holds there only if the
78 * slope keeps it holding, and a guarded flow that pushes its own level back across its own guard (a sliding
79 * mode) is refused by name. A fluid place reports its time-average level as QN and everything that leaves it
80 * as TN. `ssa_fluid_refusal` is the gate: a fluid net has this one engine, so `serial` is refused.
81 */
82
83#include <algorithm>
84#include <cmath>
85#include <cstddef>
86#include <functional>
87#include <limits>
88#include <string>
89#include <vector>
90
94#include "line/util/error.h"
96#include "line/util/matrix.h"
97#include "line/util/rootfind.h"
98
99namespace line {
100namespace ssa {
101
102namespace detail {
103
104/**
105 * One reaction of the net: a timed Transition mode, an immediate one, or a
106 * Source arrival. The reference's `spnEmptyRx` record, field for field.
107 */
108struct SpnReaction {
109 std::size_t node = 0; ///< 1-based node the reaction belongs to
110 std::size_t mode = 0; ///< 1-based mode; 0 marks a Source arrival
111 /** Stoichiometry over the (node, class) slots: consume negative, produce positive. */
112 std::vector<double> S;
113 std::vector<std::size_t> en_slot; ///< input arcs: the slots read
114 std::vector<double> en_w; ///< and the tokens each one needs
115 std::vector<std::size_t> inh_slot; ///< inhibitor arcs: the slots read
116 std::vector<double> inh_thr; ///< and the count at which each blocks
117 double base_rate = 0.0; ///< exponential firing rate, sum(D1)
118 double nservers = 1.0; ///< concurrent firings the mode may run
119 double weight = 1.0; ///< immediate: weight among equal priorities
120 double prio = 1.0; ///< immediate: firing priority
121 std::vector<std::size_t> dep_slots; ///< slots this reaction DEPOSITS into
122 // FSPN arcs, each fluid place named by its fluid index q (0-based)
123 std::vector<std::size_t> flow_q; ///< flows: the level each one moves
124 std::vector<double> flow_rate; ///< and its signed rate per running server
125 std::vector<std::size_t> guard_q; ///< guards: the level each one reads
126 std::vector<int> guard_sgn; ///< +1 for '>=', -1 for '<='
127 std::vector<double> guard_thr; ///< and the threshold
128 std::vector<std::size_t> imp_q; ///< impulses: the level a firing moves
129 std::vector<double> imp_d; ///< and by how much
130 std::vector<std::size_t> fdep_q; ///< level-dependent rate: the level read
131 std::vector<std::function<double(double)>> fdep_fun; ///< and the multiplier it gives
132 /** Marking-dependent flow multiplier per flow (aligned with flow_q; empty: none), on the flat I*K marking. */
133 std::vector<std::function<double(const std::vector<double>&)>> flow_fun;
134 std::vector<std::vector<std::size_t>> flow_read; ///< and the 0-based token places each one reads
135 /** Marking-dependent firing multiplier g on the flat (node, class) marking of `gdep_read`, as `state_events.h`. */
136 std::function<double(const std::vector<double>&)> gdep;
137 std::vector<std::size_t> gdep_read; ///< 0-based input places g reads
138};
139
140/**
141 * Enabling degree: how many concurrent firings the marking supports, the
142 * minimum over the input arcs of floor(tokens / weight), zeroed by any active
143 * inhibitor. A mode with no input arc is single-degree -- which is what makes a
144 * Source arrival a constant-propensity reaction.
145 */
146inline double spn_en_degree(const std::vector<double>& n, const SpnReaction& rx) {
147 for (std::size_t i = 0; i < rx.inh_slot.size(); ++i)
148 if (n[rx.inh_slot[i]] >= rx.inh_thr[i]) return 0.0;
149 if (rx.en_slot.empty()) return 1.0;
150 double d = std::numeric_limits<double>::infinity();
151 for (std::size_t i = 0; i < rx.en_slot.size(); ++i)
152 d = std::min(d, std::floor(n[rx.en_slot[i]] / rx.en_w[i]));
153 return d;
154}
155
156/**
157 * Propensity of a timed mode: the exponential rate times the effective server
158 * count, min(enabling degree, mode servers). A single-server mode therefore
159 * fires at its rate whenever enabled and an infinite-server one at the rate
160 * scaled by the enabling degree.
161 */
162inline double spn_propensity(const std::vector<double>& n, const SpnReaction& rx) {
163 const double eff = std::min(spn_en_degree(n, rx), rx.nservers);
164 return eff <= 0.0 ? 0.0 : rx.base_rate * eff;
165}
166
167/**
168 * `ssa_firingdep_refusal.m`: the NRM cannot answer a net whose firing rates
169 * depend on the marking, and says so by name.
170 *
171 * The NRM builds one CONSTANT-propensity reaction per timed mode, so the
172 * g(marking) multiplier has no place in it and answering with the nominal rate
173 * would be a wrong number under the caller's own method name.
174 *
175 * THE SAME SCOPE AS THE REFERENCE'S. The refusal binds the NRM only, because
176 * the SERIAL engine reaches the multiplier for real -- the FIRE arm of
177 * `after_global_event` (`state_events.h`) scales the mode's firing rate by
178 * `firingdep[m](marking)` on the state it is leaving, which is exactly the
179 * semantics SolverCTMC gives it. The gate therefore reads it with `raise=false`,
180 * so `default` FALLS BACK to the serial engine, and with `raise=true` only when
181 * the caller named `nrm`. MATLAB, the JAR and Python refused it under every SSA
182 * method until 2026-10-18; they now share this rule.
183 *
184 * A FLUID NET IS EXEMPT. Its runner (`run_fluid`) recomputes every propensity at each segment, and the marking is
185 * constant within a segment, so it applies g(marking) exactly, on the input-place marking the serial engine reads.
186 */
187template <class T>
188bool ssa_check_firingdep(const qn::NetworkStruct<T>& sn, bool raise = true) {
189 if (sn.has_fluid_places()) return true;
190 for (typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
191 sn.transparam.begin();
192 it != sn.transparam.end(); ++it) {
193 const qn::TransitionParam<T>& tp = it->second;
194 for (std::size_t m = 0; m < tp.nmodes && m < tp.firingdep.size(); ++m) {
195 if (!tp.firingdep[m]) continue;
196 if (!raise) return false;
197 const std::size_t ind = it->first;
198 throw UnsupportedError(ErrorKind{"MarkingDependence"},
199 "SolverSSA: transition '" +
200 (ind >= 1 && ind <= sn.nodes.size() ? sn.nodes[ind - 1].name : std::string("?")) +
201 "' mode " + std::to_string(m + 1) +
202 " uses a marking-dependent firing rate (setFiringRateDependence), which the "
203 "NRM does not support: it builds one constant-propensity reaction per timed "
204 "mode and never evaluates the handle. Ask for method='serial', which applies "
205 "it, or use SolverCTMC or SolverLDES");
206 }
207 }
208 return true;
209}
210
211/**
212 * `ssa_nrm_guards.m`'s `g.spn`: the shapes the SPN reaction builder can read.
213 *
214 * FOUR CONDITIONS, and all four are the reference's. Each one the reference
215 * RAISES from inside the builder, and each is a condition the serial engine
216 * serves, so `default` falls back rather than refusing -- which is why this is
217 * asked with `raise = false` by the eligibility test and with `raise = true`
218 * only when the caller named `nrm` itself.
219 */
220template <class T>
221bool spn_nrm_supported(const qn::NetworkStruct<T>& sn, bool raise = true) {
222 const std::size_t I = sn.nodes.size();
223 const std::size_t K = sn.nclasses;
224 bool any_transition = false;
225 for (std::size_t i = 0; i < I; ++i)
226 if (sn.nodes[i].nodetype == qn::NodeType::Transition) any_transition = true;
227 if (!any_transition) return true;
228 if (!ssa_check_firingdep(sn, raise)) return false;
229
230 for (std::size_t i = 0; i < I; ++i) {
231 if (sn.nodes[i].nodetype == qn::NodeType::Transition) {
232 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
233 sn.transparam.find(i + 1);
234 if (it == sn.transparam.end()) continue;
235 const qn::TransitionParam<T>& tp = it->second;
236 for (std::size_t m = 0; m < tp.nmodes; ++m) {
237 if (m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE)
238 continue;
239 const bool one_phase = m < tp.firingphases.size() && tp.firingphases[m] == 1;
240 const bool has_proc = m < tp.firingproc.size() && tp.firingproc[m].D1.rows() > 0;
241 if (one_phase && has_proc) continue;
242 if (!raise) return false;
243 throw UnsupportedError(ErrorKind{"NonExponential"},
244 "SolverSSA(method='nrm'): transition '" + sn.nodes[i].name + "' mode " +
245 std::to_string(m + 1) +
246 " has a non-exponential timed firing. The reaction network carries one "
247 "constant-rate reaction per mode and no per-mode phase, so an in-flight "
248 "firing has nowhere to keep its phase; use method='serial'");
249 }
250 } else if (sn.nodes[i].nodetype == qn::NodeType::Source) {
251 const std::size_t ist = sn.nodes[i].station;
252 if (ist == 0) continue;
253 for (std::size_t r = 0; r < K; ++r) {
254 const double lambda = num_traits<T>::to_double(sn.rates(ist - 1, r));
255 if (std::isnan(lambda) || lambda <= 0.0) continue;
256 if (sn.service[ist - 1][r].type != lang::ProcessType::EXP) {
257 if (!raise) return false;
258 throw UnsupportedError(ErrorKind{"NonExponential"},
259 "SolverSSA(method='nrm'): source '" + sn.nodes[i].name + "' class '" +
260 sn.classes[r].name +
261 "' has a non-exponential arrival. An arrival into a Place is one "
262 "constant-propensity reaction here, which only a Poisson stream is; "
263 "use method='serial'");
264 }
265 bool feeds = false;
266 for (std::size_t j = 0; j < I && !feeds; ++j) {
267 if (sn.nodes[j].nodetype != qn::NodeType::Place) continue;
268 for (std::size_t s = 0; s < K && !feeds; ++s)
269 if (num_traits<T>::to_double(sn.rtnodes(i * K + r, j * K + s)) > 0.0)
270 feeds = true;
271 }
272 if (!feeds) {
273 if (!raise) return false;
274 throw UnsupportedError(ErrorKind{"Topology"},
275 "SolverSSA(method='nrm'): source '" + sn.nodes[i].name + "' class '" +
276 sn.classes[r].name +
277 "' routes to no Place. The SPN path deposits an arrival into a Place "
278 "slot and has nowhere else to put one; use method='serial'");
279 }
280 }
281 } else if (sn.nodes[i].nodetype == qn::NodeType::Place) {
282 const typename std::map<std::size_t, std::vector<T>>::const_iterator im =
283 sn.initmarking.find(i + 1);
284 if (im == sn.initmarking.end()) continue;
285 for (std::size_t r = 0; r < im->second.size(); ++r) {
286 if (std::isfinite(num_traits<T>::to_double(im->second[r]))) continue;
287 if (!raise) return false;
288 throw UnsupportedError(ErrorKind{"Place"}, "SolverSSA(method='nrm'): place '" + sn.nodes[i].name +
289 "' declares an infinite initial marking, which the "
290 "reaction network cannot count; use method='serial'");
291 }
292 }
293 }
294 return true;
295}
296
297/**
298 * `ssa_fluid_refusal.m`: whether SolverSSA can simulate this Fluid Stochastic Petri Net under `method`.
299 *
300 * A FLUID NET RUNS ON THE NRM PETRI RUNNER ONLY. Its levels move continuously between firings, so the
301 * simulator is a piecewise-deterministic Markov process whose levels, guard crossings and bound hits ride
302 * beside the NRM clocks as scheduled non-firing events. The serial engine has no level to move, so `serial`
303 * (and its alias `ssa`) is refused by name and `default`/`parallel` run the NRM. A net the reaction builder
304 * cannot read (a non-exponential firing) has no engine left and is refused with the builder's reason. A
305 * marking-dependent firing or flow rate is read by the runner, segment by segment. Always OK for a net with no fluid.
306 *
307 * @param sn the network structure
308 * @param method the SSA method, without an `ssa.` prefix
309 */
310template <class T>
311api::SnFluidVerdict ssa_fluid_refusal(const qn::NetworkStruct<T>& sn, const std::string& method) {
312 api::SnFluidVerdict v;
313 if (!sn.has_fluid_places()) return v;
314 v = api::sn_fluid_refusal(sn);
315 if (!v.ok) {
316 v.reason = "SolverSSA: " + v.reason;
317 return v;
318 }
319 if (method == "serial" || method == "ssa") {
320 v.ok = false;
321 v.reason = "SolverSSA: the model is a fluid stochastic Petri net (Place.setFluid), whose levels only the "
322 "NRM Petri runner moves. Use options.method='nrm' or 'default'.";
323 return v;
324 }
325 for (const qn::NodeDef& nd : sn.nodes)
326 if (nd.nodetype != qn::NodeType::Place && nd.nodetype != qn::NodeType::Transition &&
327 nd.nodetype != qn::NodeType::Source && nd.nodetype != qn::NodeType::Sink) {
328 v.ok = false;
329 v.reason = "SolverSSA: a fluid stochastic Petri net may hold only Place, Transition, Source and Sink "
330 "nodes; the NRM Petri runner has no hand-off between a level and a queueing station.";
331 return v;
332 }
333 if (!spn_nrm_supported(sn, false)) {
334 v.ok = false;
335 v.reason = "SolverSSA: the fluid net has a non-exponential firing or Source arrival, or an infinite "
336 "marking, which the NRM Petri runner (the only engine that moves fluid levels) has no form for.";
337 }
338 return v;
339}
340
341/**
342 * `spnGuardHolds`: every fluid guard of a mode, inclusive, with the lookahead of the slope `v` at the
343 * threshold itself, so a crossing switches the guard at the instant of the crossing.
344 */
345inline bool spn_fluid_guard_holds(const SpnReaction& rec, const std::vector<double>& f,
346 const std::vector<double>& v) {
347 for (std::size_t g = 0; g < rec.guard_q.size(); ++g) {
348 const std::size_t q = rec.guard_q[g];
349 const double sg = static_cast<double>(rec.guard_sgn[g]);
350 const double thr = rec.guard_thr[g];
351 const double tolf = 1e-12 * std::max(1.0, std::fabs(thr));
352 const double d = sg * (f[q] - thr);
353 if (d > tolf) continue;
354 if (d >= -tolf && sg * v[q] >= 0.0) continue;
355 return false;
356 }
357 return true;
358}
359
360/** `spnFluidDep`: the level-dependent multiplier of a mode at time `t` into the segment. */
361inline double spn_fluid_dep(const SpnReaction& rec, const std::vector<double>& f, const std::vector<double>& v,
362 double t) {
363 double h = 1.0;
364 for (std::size_t i = 0; i < rec.fdep_q.size(); ++i) {
365 const std::size_t q = rec.fdep_q[i];
366 h *= rec.fdep_fun[i](std::max(0.0, f[q] + v[q] * t));
367 }
368 return h;
369}
370
371/** `spnFluidVarying`: does the hazard of a mode change along the segment? */
372inline bool spn_fluid_varying(const SpnReaction& rec, const std::vector<double>& v) {
373 for (std::size_t i = 0; i < rec.fdep_q.size(); ++i)
374 if (v[rec.fdep_q[i]] != 0.0) return true;
375 return false;
376}
377
378/** One adaptive Simpson panel: recurse until the Richardson estimate meets `tol` or the depth runs out. */
379template <class F>
380double spn_simpson_panel(const F& h, double a, double b, double fa, double fm, double fb, double whole,
381 double tol, int depth) {
382 const double m = 0.5 * (a + b);
383 const double lm = 0.5 * (a + m), rm = 0.5 * (m + b);
384 const double flm = h(lm), frm = h(rm);
385 const double left = (m - a) / 6.0 * (fa + 4.0 * flm + fm);
386 const double right = (b - m) / 6.0 * (fm + 4.0 * frm + fb);
387 const double err = left + right - whole;
388 if (depth <= 0 || std::fabs(err) <= 15.0 * tol) return left + right + err / 15.0;
389 return spn_simpson_panel(h, a, m, fa, flm, fm, left, 0.5 * tol, depth - 1) +
390 spn_simpson_panel(h, m, b, fm, frm, fb, right, 0.5 * tol, depth - 1);
391}
392
393/**
394 * `spnFluidHazardIntegral`: the cumulative hazard of a mode over [0, dt] of the segment. Closed form when the
395 * hazard is constant; otherwise adaptive Simpson at the reference's `integral` tolerances (AbsTol 1e-12,
396 * RelTol 1e-10).
397 */
398inline double spn_fluid_hazard(const SpnReaction& rec, double a0, const std::vector<double>& f,
399 const std::vector<double>& v, double dt) {
400 if (dt <= 0.0) return 0.0;
401 if (!spn_fluid_varying(rec, v)) return a0 * spn_fluid_dep(rec, f, v, 0.0) * dt;
402 const auto h = [&](double t) { return spn_fluid_dep(rec, f, v, t); };
403 const double fa = h(0.0), fm = h(0.5 * dt), fb = h(dt);
404 const double whole = dt / 6.0 * (fa + 4.0 * fm + fb);
405 const double tol = std::max(1e-12, 1e-10 * std::fabs(whole));
406 return a0 * spn_simpson_panel(h, 0.0, dt, fa, fm, fb, whole, tol, 50);
407}
408
409/**
410 * `spnFluidFireTime`: when the cumulative hazard of a mode reaches `need`, or infinity if not before
411 * `horizon`. A constant hazard is closed form; a varying one is bracketed (doubling when the horizon is
412 * infinite, at most 200 times) and the epoch found by Brent's method, the reference's `fzero`.
413 */
414inline double spn_fluid_fire_time(const SpnReaction& rec, double a0, const std::vector<double>& f,
415 const std::vector<double>& v, double need, double horizon) {
416 const double inf = std::numeric_limits<double>::infinity();
417 if (!spn_fluid_varying(rec, v)) {
418 const double a = a0 * spn_fluid_dep(rec, f, v, 0.0);
419 return a <= 0.0 ? inf : need / a;
420 }
421 if (need <= 0.0) return 0.0;
422 double hi;
423 if (std::isfinite(horizon)) {
424 hi = horizon;
425 if (spn_fluid_hazard(rec, a0, f, v, hi) < need) return inf;
426 } else {
427 const double a = std::max(a0 * spn_fluid_dep(rec, f, v, 0.0), std::numeric_limits<double>::min());
428 hi = need / a;
429 int grow = 0;
430 while (spn_fluid_hazard(rec, a0, f, v, hi) < need) {
431 hi *= 2.0;
432 if (++grow > 200) return inf;
433 }
434 }
435 const auto g = [&](double t) { return spn_fluid_hazard(rec, a0, f, v, t) - need; };
436 return root_brent<double>(g, 0.0, hi, 1e-14 * std::max(1.0, hi)).root;
437}
438
439} // namespace detail
440
441/**
442 * The SPN Next-Reaction-Method engine, `solver_ssa_nrm_spn` of the reference.
443 *
444 * DOUBLE ONLY, for the reason `NrmEngine` is: the sample path is generated from
445 * exponential clocks, which are logarithms of uniform draws, so there is no
446 * exact value a wider arithmetic could carry. The analyzer refuses a non-double
447 * backend by name before this class is instantiated.
448 */
449template <class T>
451public:
453 : sn_(sn), opt_(opt), rng_(opt.seed) {
454 build();
455 }
456
457 /** Run `opt.samples` firings and return the time-averaged metrics. */
459
460 /** The reaction count, for the tests that assert the builder. */
461 std::size_t nreactions() const { return rx_.size(); }
462 /** The immediate-mode count, likewise. */
463 std::size_t nimmediate() const { return imm_.size(); }
464
465private:
466 using Rx = detail::SpnReaction;
467
468 const qn::NetworkStruct<T>& sn_;
469 SsaOptions opt_;
470 SsaRng rng_;
471
472 std::size_t I_ = 0, K_ = 0, M_ = 0, NS_ = 0;
473
474 std::vector<Rx> rx_; ///< the timed reactions, the ones that race
475 std::vector<Rx> imm_; ///< the immediate modes, resolved between races
476 /** Timed reactions consuming from each (0-based node, class): the Place throughput. */
477 std::vector<std::vector<std::vector<std::size_t>>> consumers_;
478 /** Arrival reactions injecting at each (0-based Source node, class). */
479 std::vector<std::vector<std::vector<std::size_t>>> producers_;
480
481 std::vector<double> nvec0_; ///< the initial marking, before the collapse
482 std::vector<double> pcap_slot_; ///< per-(place, class) capacity, infinite when none
483 /** Per-place total capacities: the bound and the slots it is taken over. */
484 std::vector<std::pair<double, std::vector<std::size_t>>> place_total_caps_;
485 bool has_caps_ = false;
486
487 // FSPN fluid places (`fl` of the reference), indexed by the fluid index q
488 std::vector<long> fq_; ///< per 0-based node: its fluid index, -1 when not fluid
489 std::vector<std::size_t> fl_node_; ///< 0-based node of each fluid place
490 std::vector<std::size_t> fl_ist_; ///< 0-based station of each fluid place
491 std::vector<double> fl_cap_; ///< level ceiling, infinite when unbounded
492 std::vector<double> fl_f0_; ///< level at time zero
493 /** Per timed reaction, the marking multiplier of each of its flows for the current segment (empty: all 1). */
494 std::vector<std::vector<double>> fm_;
495
496 /** The livelock guard of the vanishing-marking collapse, the reference's 1e5. */
497 static const std::size_t kMaxImmSteps = 100000;
498
499 std::size_t slot(std::size_t node0, std::size_t cls) const { return node0 * K_ + cls; }
500
501 void build();
502 Rx build_mode(std::size_t ind0, std::size_t m) const;
503 void apply_caps(std::vector<double>& n, const std::vector<std::size_t>& deposited) const;
504 void collapse(std::vector<double>& n);
505
506 // the piecewise-deterministic loop of a fluid net, `spnFluidRun` and its helpers
507 SsaSolution run_fluid();
508 std::size_t fluid_index(std::size_t node1) const;
509 void fluid_mark_mul(const std::vector<double>& n, std::vector<double>& gk);
510 void fluid_resolve(const std::vector<double>& n, const std::vector<double>& f, std::vector<double>& eff,
511 std::vector<double>& v, std::vector<double>& vin, std::vector<double>& vout) const;
512 void fluid_slope(const std::vector<double>& f, const std::vector<double>& eff, std::vector<double>& v,
513 std::vector<double>& vin, std::vector<double>& vout) const;
514 void fluid_next_det(const std::vector<double>& f, const std::vector<double>& v, double& tdet,
515 std::size_t& qdet, double& fdet) const;
516 void fluid_impulse(std::vector<double>& f, const Rx& rec, std::vector<double>& OF) const;
517 void collapse_fluid(std::vector<double>& n, std::vector<double>& f, const std::vector<double>& v,
518 std::vector<double>& OF);
519};
520
521/**
522 * The reaction list, the initial marking and the capacity tables.
523 *
524 * ONE PASS PER NODE KIND, in the reference's order: the Transition modes become
525 * reactions (timed) or immediate records, then the Source arrivals become
526 * constant-propensity reactions, then the Places contribute the marking and
527 * their capacities.
528 */
529template <class T>
530void NrmSpnEngine<T>::build() {
531 I_ = sn_.nodes.size();
532 K_ = sn_.nclasses;
533 M_ = sn_.nstations;
534 NS_ = I_ * K_;
535 consumers_.assign(I_, std::vector<std::vector<std::size_t>>(K_));
536 producers_.assign(I_, std::vector<std::vector<std::size_t>>(K_));
537
538 // Refused here as well as in the gate, as the reference raises it from
539 // inside its own builder too (`solver_ssa_nrm.m:3426`): that is the path a
540 // caller reaches with the checks disabled, and this engine must not run a
541 // net whose rates it would silently ignore however it was entered.
542 detail::ssa_check_firingdep(sn_, true);
543
544 // Fluid places (FSPN): one real level each, indexed q = fq_[node]. The structural rules are re-read here
545 // for the same reason as the firingdep refusal above: a builder entered with the gate skipped must not run.
546 fq_.assign(I_, -1);
547 if (sn_.has_fluid_places()) {
548 const api::SnFluidVerdict fv = api::sn_fluid_refusal(sn_);
549 if (!fv.ok) throw UnsupportedError(ErrorKind{"Structure"}, "solver_ssa_nrm_spn: " + fv.reason);
550 for (std::size_t ind = 0; ind < I_; ++ind) {
551 if (sn_.nodes[ind].nodetype != qn::NodeType::Place || !sn_.is_fluid_node(ind + 1)) continue;
552 const std::size_t ist = sn_.nodes[ind].station;
553 const qn::Station<T>& st = sn_.stations[ist - 1];
554 fq_[ind] = static_cast<long>(fl_node_.size());
555 fl_node_.push_back(ind);
556 fl_ist_.push_back(ist - 1);
557 fl_cap_.push_back(st.fluidcap);
558 fl_f0_.push_back(st.fluidinit);
559 }
560 }
561
562 for (std::size_t ind = 0; ind < I_; ++ind) {
563 if (sn_.nodes[ind].nodetype != qn::NodeType::Transition) continue;
564 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
565 sn_.transparam.find(ind + 1);
566 if (it == sn_.transparam.end()) continue;
567 const qn::TransitionParam<T>& tp = it->second;
568 for (std::size_t m = 0; m < tp.nmodes; ++m) {
569 const Rx rec = build_mode(ind, m);
570 if (m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE) {
571 imm_.push_back(rec);
572 continue;
573 }
574 rx_.push_back(rec);
575 const std::size_t ridx = rx_.size() - 1;
576 for (std::size_t a = 0; a < rec.en_slot.size(); ++a)
577 consumers_[rec.en_slot[a] / K_][rec.en_slot[a] % K_].push_back(ridx);
578 }
579 }
580
581 // Source arrivals. Thinning a Poisson stream by the independent routing
582 // probabilities leaves independent Poisson streams, so one reaction per
583 // routed (Source, class) -> (Place, class) edge carries rate lambda*p
584 // exactly. `producers_` is what makes the Source station report that rate as
585 // its throughput, which is the open class's reference-station throughput.
586 for (std::size_t ind = 0; ind < I_; ++ind) {
587 if (sn_.nodes[ind].nodetype != qn::NodeType::Source) continue;
588 const std::size_t ist = sn_.nodes[ind].station;
589 if (ist == 0) continue;
590 for (std::size_t r = 0; r < K_; ++r) {
591 const double lambda = num_traits<T>::to_double(sn_.rates(ist - 1, r));
592 if (std::isnan(lambda) || lambda <= 0.0) continue;
593 if (sn_.service[ist - 1][r].type != lang::ProcessType::EXP)
594 throw UnsupportedError(ErrorKind{"NonExponential"},
595 "solver_ssa_nrm_spn: source '" + sn_.nodes[ind].name + "' class '" +
596 sn_.classes[r].name +
597 "' has a non-exponential arrival, which the SPN reaction network cannot "
598 "express; use method='serial' or SolverJMT");
599 bool found_place = false;
600 for (std::size_t jnd = 0; jnd < I_; ++jnd) {
601 if (sn_.nodes[jnd].nodetype != qn::NodeType::Place) continue;
602 for (std::size_t s = 0; s < K_; ++s) {
603 const double p =
604 num_traits<T>::to_double(sn_.rtnodes(ind * K_ + r, jnd * K_ + s));
605 if (p <= 0.0) continue;
606 found_place = true;
607 Rx rec;
608 rec.node = ind + 1;
609 rec.mode = 0;
610 rec.S.assign(NS_, 0.0);
611 rec.S[slot(jnd, s)] += 1.0;
612 rec.base_rate = lambda * p;
613 rec.nservers = 1.0;
614 rec.dep_slots.push_back(slot(jnd, s));
615 rx_.push_back(rec);
616 producers_[ind][r].push_back(rx_.size() - 1);
617 }
618 }
619 if (!found_place)
620 throw UnsupportedError(ErrorKind{"Topology"}, "solver_ssa_nrm_spn: source '" + sn_.nodes[ind].name +
621 "' class '" + sn_.classes[r].name +
622 "' does not route to any Place; the SPN path needs a "
623 "Source->Place arc");
624 }
625 }
626
627 if (rx_.empty())
628 throw InputError(
629 "solver_ssa_nrm_spn: the stochastic Petri net has no timed reaction; there is "
630 "nothing to simulate");
631
632 // THE INITIAL MARKING, in the reference's two steps. MATLAB reads it off
633 // `sn.state`, which `initDefault` fills by putting each closed class's whole
634 // population at its reference station and which `Place.setState` then
635 // overrides. This port has no State package -- the same position
636 // `NrmEngine::build_initial_state` takes, and for the same reason -- so the
637 // two steps are taken directly: a closed class whose reference station IS a
638 // Place seeds that Place with its population, and a DECLARED `initmarking`
639 // then replaces that Place's whole row, because an explicit marking is a
640 // statement about every class of the place and not an addition to one.
641 //
642 // WITHOUT THE FIRST STEP a closed Petri net starts empty and deadlocks on the
643 // first draw, since `initmarking` is only written by an explicit
644 // `set_initial_marking` and a net that declares its tokens through a
645 // ClosedClass population carries none.
646 nvec0_.assign(NS_, 0.0);
647 for (std::size_t r = 0; r < K_; ++r) {
648 const double pop = sn_.classes[r].population;
649 if (!std::isfinite(pop) || pop <= 0.0) continue;
650 const std::size_t rs = sn_.classes[r].refstat;
651 if (rs < 1 || rs > M_) continue;
652 const std::size_t ind = sn_.station_to_node[rs - 1] - 1;
653 if (sn_.nodes[ind].nodetype != qn::NodeType::Place) continue;
654 nvec0_[slot(ind, r)] = pop;
655 }
656 for (std::size_t ind = 0; ind < I_; ++ind) {
657 if (sn_.nodes[ind].nodetype != qn::NodeType::Place) continue;
658 const typename std::map<std::size_t, std::vector<T>>::const_iterator im =
659 sn_.initmarking.find(ind + 1);
660 if (im == sn_.initmarking.end()) continue;
661 for (std::size_t r = 0; r < K_; ++r) {
662 const double v =
663 r < im->second.size() ? num_traits<T>::to_double(im->second[r]) : 0.0;
664 if (!std::isfinite(v))
665 throw UnsupportedError(ErrorKind{"Place"}, "solver_ssa_nrm_spn: place '" + sn_.nodes[ind].name +
666 "' declares an infinite initial marking, which the "
667 "reaction network cannot count");
668 nvec0_[slot(ind, r)] = v > 0.0 ? v : 0.0;
669 }
670 }
671
672 // FINITE-CAPACITY PLACES DROP. A Place with a finite per-class (`classcap`)
673 // or total (`cap`) capacity loses any arriving token that would exceed it,
674 // which is the loss semantics JMT and the exact CTMC both give it: an
675 // M/M/1/1 Place at rho = 0.5 holds a mean 1/3, not the unbounded 1. Without
676 // the clamp the deposit accumulates past the bound.
677 const double inf = std::numeric_limits<double>::infinity();
678 pcap_slot_.assign(NS_, inf);
679 for (std::size_t ind = 0; ind < I_; ++ind) {
680 if (sn_.nodes[ind].nodetype != qn::NodeType::Place) continue;
681 const std::size_t ist = sn_.nodes[ind].station;
682 if (ist == 0) continue;
683 std::vector<std::size_t> slots_here;
684 for (std::size_t r = 0; r < K_; ++r) {
685 slots_here.push_back(slot(ind, r));
686 if (ist - 1 < sn_.classcap.size() && r < sn_.classcap[ist - 1].size()) {
687 const double cc = sn_.classcap[ist - 1][r];
688 if (std::isfinite(cc)) pcap_slot_[slot(ind, r)] = cc;
689 }
690 }
691 if (ist - 1 < sn_.cap.size() && std::isfinite(sn_.cap[ist - 1]))
692 place_total_caps_.push_back(std::make_pair(sn_.cap[ist - 1], slots_here));
693 }
694 has_caps_ = !place_total_caps_.empty();
695 for (std::size_t j = 0; j < NS_ && !has_caps_; ++j)
696 if (std::isfinite(pcap_slot_[j])) has_caps_ = true;
697}
698
699/**
700 * `spnBuildMode`: the reaction record of transition `ind0` mode `m`.
701 *
702 * The enabling, firing and inhibiting matrices are (nnodes x nclasses), so a
703 * nonzero entry (p, c) is an arc on class c of place p and its slot is the
704 * (p, c) pair. An inhibiting entry is an arc only when it is FINITE: infinity
705 * is how "this place never blocks the mode" is written, here as in the
706 * reference.
707 */
708template <class T>
709typename NrmSpnEngine<T>::Rx NrmSpnEngine<T>::build_mode(std::size_t ind0, std::size_t m) const {
710 const qn::TransitionParam<T>& tp = sn_.transparam.at(ind0 + 1);
711 Rx rec;
712 rec.node = ind0 + 1;
713 rec.mode = m + 1;
714 rec.S.assign(NS_, 0.0);
715
716 if (m < tp.enabling.size()) {
717 const Matrix<T>& en = tp.enabling[m];
718 for (std::size_t p = 0; p < en.rows() && p < I_; ++p)
719 for (std::size_t c = 0; c < en.cols() && c < K_; ++c) {
720 const double w = num_traits<T>::to_double(en(p, c));
721 if (w == 0.0) continue;
722 rec.en_slot.push_back(slot(p, c));
723 rec.en_w.push_back(w);
724 rec.S[slot(p, c)] -= w;
725 }
726 }
727 if (m < tp.firing.size()) {
728 const Matrix<T>& fir = tp.firing[m];
729 for (std::size_t p = 0; p < fir.rows() && p < I_; ++p)
730 for (std::size_t c = 0; c < fir.cols() && c < K_; ++c) {
731 const double w = num_traits<T>::to_double(fir(p, c));
732 if (w == 0.0) continue;
733 rec.S[slot(p, c)] += w;
734 }
735 }
736 if (m < tp.inhibiting.size()) {
737 const Matrix<T>& inh = tp.inhibiting[m];
738 for (std::size_t p = 0; p < inh.rows() && p < I_; ++p)
739 for (std::size_t c = 0; c < inh.cols() && c < K_; ++c) {
740 const double thr = num_traits<T>::to_double(inh(p, c));
741 if (!std::isfinite(thr)) continue;
742 rec.inh_slot.push_back(slot(p, c));
743 rec.inh_thr.push_back(thr);
744 }
745 }
746 for (std::size_t j = 0; j < NS_; ++j)
747 if (rec.S[j] > 0.0) rec.dep_slots.push_back(j);
748
749 // The exponential firing rate is the single-phase completion rate sum(D1).
750 // A non-exponential firing is refused by `spn_nrm_supported` upstream and
751 // again here, so no route reaches the run loop with a rate it invented.
752 if (!(m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE)) {
753 const bool one_phase = m < tp.firingphases.size() && tp.firingphases[m] == 1;
754 if (!one_phase || m >= tp.firingproc.size() || tp.firingproc[m].D1.rows() == 0)
755 throw UnsupportedError(ErrorKind{"NonExponential"}, "solver_ssa_nrm_spn: transition '" + sn_.nodes[ind0].name +
756 "' mode " + std::to_string(m + 1) +
757 " has a non-exponential firing, which the SPN reaction "
758 "network cannot express; use method='serial'");
759 const Matrix<T>& d1 = tp.firingproc[m].D1;
760 double s = 0.0;
761 for (std::size_t a = 0; a < d1.rows(); ++a)
762 for (std::size_t b = 0; b < d1.cols(); ++b) s += num_traits<T>::to_double(d1(a, b));
763 rec.base_rate = s;
764 }
765 rec.nservers = m < tp.nmodeservers.size() ? tp.nmodeservers[m] : 1.0;
766 if (std::isinf(rec.nservers)) rec.nservers = lang::GlobalConstants::MaxInt;
767 rec.weight = m < tp.fireweight.size() ? num_traits<T>::to_double(tp.fireweight[m]) : 1.0;
768 rec.prio = m < tp.firingprio.size() ? tp.firingprio[m] : 1.0;
769 // Fluid arcs, with each place named by its fluid index q.
770 for (const auto& a : tp.fluid_rates(m)) {
771 rec.flow_q.push_back(fluid_index(a.node));
772 rec.flow_rate.push_back(a.value);
773 }
774 for (const auto& g : tp.fluid_guards(m)) {
775 rec.guard_q.push_back(fluid_index(g.node));
776 rec.guard_sgn.push_back(g.sgn);
777 rec.guard_thr.push_back(g.thr);
778 }
779 for (const auto& a : tp.fluid_impulses(m)) {
780 rec.imp_q.push_back(fluid_index(a.node));
781 rec.imp_d.push_back(a.value);
782 }
783 for (const auto& d : tp.fluid_deps(m)) {
784 rec.fdep_q.push_back(fluid_index(d.node));
785 rec.fdep_fun.push_back(d.fun);
786 }
787 // marking-dependent flow multipliers, aligned with flow_q
788 const std::vector<typename qn::TransitionParam<T>::FluidArc>& fr = tp.fluid_rates(m);
789 rec.flow_fun.assign(fr.size(), std::function<double(const std::vector<double>&)>());
790 rec.flow_read.assign(fr.size(), std::vector<std::size_t>());
791 for (const auto& fd : tp.fluid_flow_deps(m))
792 for (std::size_t t = 0; t < fr.size(); ++t) {
793 if (fr[t].node != fd.node) continue;
794 rec.flow_fun[t] = fd.fun;
795 for (std::size_t q : fd.read) rec.flow_read[t].push_back(q - 1);
796 break;
797 }
798 // the marking-dependent firing multiplier, read on the input places as the serial engine reads it
799 // (`after_global_event`'s ep); only a fluid net reaches here with one, `ssa_check_firingdep` refusing the rest
800 const bool timed = !(m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE);
801 if (timed && m < tp.firingdep.size() && tp.firingdep[m]) {
802 const std::function<T(const std::vector<T>&)> g = tp.firingdep[m];
803 rec.gdep = [g](const std::vector<double>& mk) -> double {
804 std::vector<T> mt(mk.size());
805 for (std::size_t i = 0; i < mk.size(); ++i) mt[i] = num_traits<T>::from_double(mk[i]);
806 return num_traits<T>::to_double(g(mt));
807 };
808 if (m < tp.enabling.size()) {
809 const Matrix<T>& en = tp.enabling[m];
810 for (std::size_t p = 0; p < en.rows() && p < I_; ++p)
811 for (std::size_t c = 0; c < en.cols(); ++c)
812 if (num_traits<T>::to_double(en(p, c)) > 0.0) {
813 rec.gdep_read.push_back(p);
814 break;
815 }
816 }
817 }
818 return rec;
819}
820
821/**
822 * `spnFluidMarkMul`: the marking-dependent multipliers of marking `n`, `gk[k]` the firing multiplier g of timed
823 * reaction k and `fm_[k]` the multiplier of each of its flows (empty when none carries one). Each function reads
824 * the marking restricted to its declared token places, as the flat (node, class) marking, whose slot layout is
825 * the engine's own and the node-major one of `state_events.h`.
826 */
827template <class T>
828void NrmSpnEngine<T>::fluid_mark_mul(const std::vector<double>& n, std::vector<double>& gk) {
829 const std::size_t nrx = rx_.size();
830 gk.assign(nrx, 1.0);
831 fm_.assign(nrx, std::vector<double>());
832 for (std::size_t k = 0; k < nrx; ++k) {
833 const Rx& rec = rx_[k];
834 if (rec.gdep) {
835 std::vector<double> mk(NS_, 0.0);
836 for (std::size_t p : rec.gdep_read)
837 for (std::size_t c = 0; c < K_; ++c) mk[slot(p, c)] = n[slot(p, c)];
838 gk[k] = rec.gdep(mk);
839 }
840 for (std::size_t t = 0; t < rec.flow_fun.size(); ++t) {
841 if (!rec.flow_fun[t]) continue;
842 if (fm_[k].empty()) fm_[k].assign(rec.flow_q.size(), 1.0);
843 std::vector<double> mk(NS_, 0.0);
844 for (std::size_t p : rec.flow_read[t])
845 for (std::size_t c = 0; c < K_; ++c) mk[slot(p, c)] = n[slot(p, c)];
846 fm_[k][t] = rec.flow_fun[t](mk);
847 }
848 }
849}
850
851/** The fluid index of 1-based node `node1`; `sn_fluid_refusal` has already refused a fluid arc on a token place. */
852template <class T>
853std::size_t NrmSpnEngine<T>::fluid_index(std::size_t node1) const {
854 if (node1 == 0 || node1 > I_ || fq_[node1 - 1] < 0)
855 throw UnsupportedError(ErrorKind{"Structure"},
856 "solver_ssa_nrm_spn: a fluid arc names node " + std::to_string(node1) + ", which is not a fluid place");
857 return static_cast<std::size_t>(fq_[node1 - 1]);
858}
859
860/**
861 * `applyPlaceCaps`: drop the tokens a firing pushed above a Place's bound.
862 *
863 * Only the just-deposited slots can overflow, so the clamp is local to them --
864 * which also keeps it from disturbing a marking that was already over a bound
865 * when the engine started.
866 */
867template <class T>
868void NrmSpnEngine<T>::apply_caps(std::vector<double>& n,
869 const std::vector<std::size_t>& deposited) const {
870 for (std::size_t a = 0; a < deposited.size(); ++a) {
871 const std::size_t j = deposited[a];
872 if (n[j] > pcap_slot_[j]) n[j] = pcap_slot_[j];
873 }
874 for (std::size_t p = 0; p < place_total_caps_.size(); ++p) {
875 const double tcap = place_total_caps_[p].first;
876 const std::vector<std::size_t>& slots = place_total_caps_[p].second;
877 double total = 0.0;
878 for (std::size_t a = 0; a < slots.size(); ++a) total += n[slots[a]];
879 double excess = total - tcap;
880 for (std::size_t a = 0; a < deposited.size() && excess > 0.0; ++a) {
881 const std::size_t j = deposited[a];
882 if (std::find(slots.begin(), slots.end(), j) == slots.end()) continue;
883 if (n[j] <= 0.0) continue;
884 const double d = std::min(excess, n[j]);
885 n[j] -= d;
886 excess -= d;
887 }
888 }
889}
890
891/**
892 * `spnCollapse`: vanishing-marking elimination.
893 *
894 * Fire enabled immediate modes until the marking is tangible, highest firing
895 * priority first and ties drawn in proportion to firing weight. These firings
896 * take zero time and advance no clock, so the timed race only ever resumes from
897 * a tangible marking.
898 */
899template <class T>
900void NrmSpnEngine<T>::collapse(std::vector<double>& n) {
901 if (imm_.empty()) return;
902 std::size_t steps = 0;
903 while (true) {
904 std::vector<std::size_t> enabled;
905 for (std::size_t m = 0; m < imm_.size(); ++m)
906 if (detail::spn_en_degree(n, imm_[m]) >= 1.0) enabled.push_back(m);
907 if (enabled.empty()) return;
908 double top_prio = -std::numeric_limits<double>::infinity();
909 for (std::size_t i = 0; i < enabled.size(); ++i)
910 top_prio = std::max(top_prio, imm_[enabled[i]].prio);
911 std::vector<std::size_t> top;
912 std::vector<double> w;
913 for (std::size_t i = 0; i < enabled.size(); ++i)
914 if (imm_[enabled[i]].prio == top_prio) {
915 top.push_back(enabled[i]);
916 w.push_back(imm_[enabled[i]].weight);
917 }
918 const std::size_t pick = top.size() == 1 ? top[0] : top[rng_.draw(w)];
919 for (std::size_t j = 0; j < NS_; ++j) n[j] += imm_[pick].S[j];
920 if (++steps > kMaxImmSteps)
921 throw NumericError(ErrorKind{"NonConvergence"},
922 "solver_ssa_nrm_spn: immediate-transition livelock -- the vanishing-marking "
923 "collapse did not reach a tangible marking");
924 }
925}
926
927/**
928 * The Next-Reaction-Method run loop over the tangible markings.
929 *
930 * EVERY PROPENSITY IS REFRESHED after a firing rather than a dependency subset:
931 * a firing plus its immediate cascade can change any place, and the reaction
932 * count of a Petri net is small, so refreshing all of them removes any
933 * dependency-graph blind spot at no cost worth measuring.
934 *
935 * A PLACE IS AN INF STATION, so its utilization IS its mean token count -- the
936 * SPN convention the CTMC analyzer also reports -- and its throughput is the
937 * summed firing rate of the modes consuming from it, counted once per firing.
938 */
939template <class T>
941 // A fluid net runs the piecewise-deterministic loop instead (`spnFluidRun`).
942 if (!fl_node_.empty()) return run_fluid();
943 const std::size_t nrx = rx_.size();
944 SsaSolution out;
945 out.QN = Matrix<double>(M_, K_, 0.0);
946 out.UN = Matrix<double>(M_, K_, 0.0);
947 out.RN = Matrix<double>(M_, K_, 0.0);
948 out.TN = Matrix<double>(M_, K_, 0.0);
949 out.CN.assign(K_, 0.0);
950 out.XN.assign(K_, 0.0);
951 out.StartN = Matrix<double>(M_, K_, 0.0);
952 out.PreemptN = Matrix<double>(M_, K_, 0.0);
953 out.method = "nrm";
954
955 std::vector<double> nvec = nvec0_;
956 collapse(nvec);
957 if (has_caps_) {
958 std::vector<std::size_t> all(NS_);
959 for (std::size_t j = 0; j < NS_; ++j) all[j] = j;
960 apply_caps(nvec, all);
961 }
962
963 std::vector<double> Ak(nrx, 0.0), Pk(nrx, 0.0), Tk(nrx, 0.0), tau(nrx, 0.0);
964 const double inf = std::numeric_limits<double>::infinity();
965 for (std::size_t k = 0; k < nrx; ++k) {
966 Ak[k] = detail::spn_propensity(nvec, rx_[k]);
967 Pk[k] = -std::log(rng_.uniform());
968 tau[k] = Ak[k] > 0.0 ? (Pk[k] - Tk[k]) / Ak[k] : inf;
969 }
970
971 double total_time = 0.0;
972 std::size_t n = 0;
973 line::util::LineConsole::loop("drawing the sample path: %zu samples requested",
974 static_cast<std::size_t>(opt_.samples));
975 const std::size_t console_every = std::max<std::size_t>(1, opt_.samples / 20);
976 // Warmup discard, as in the queueing-network NRM loop (`ssa_resolve_warmupfrac`).
977 const std::size_t n_warm = ssa_warmup_firings(opt_.warmupfrac, opt_.samples);
978 for (; n < opt_.samples; ++n) {
979 if (n == n_warm && n_warm > 0) {
980 out.QN = Matrix<double>(M_, K_, 0.0);
981 out.UN = Matrix<double>(M_, K_, 0.0);
982 out.TN = Matrix<double>(M_, K_, 0.0);
983 total_time = 0.0;
984 }
985 if ((n + 1) % console_every == 0)
987 static_cast<long>((n + 1) / console_every),
988 "simulated %zu of %zu samples (%.0f%%), simulated time %.4g", n + 1,
989 static_cast<std::size_t>(opt_.samples),
990 100.0 * static_cast<double>(n + 1) / static_cast<double>(opt_.samples),
991 total_time);
992 std::size_t kfire = 0;
993 double dt = inf;
994 for (std::size_t k = 0; k < nrx; ++k)
995 if (tau[k] < dt) {
996 dt = tau[k];
997 kfire = k;
998 }
999 if (std::isinf(dt))
1000 throw NumericError(ErrorKind{"Deadlock"},
1001 "solver_ssa_nrm_spn: deadlock -- no transition is enabled, so the sample path "
1002 "cannot advance");
1003 total_time += dt;
1004
1005 for (std::size_t ist = 0; ist < M_; ++ist) {
1006 const std::size_t ind = sn_.station_to_node[ist] - 1;
1007 for (std::size_t c = 0; c < K_; ++c) {
1008 const double tokens = nvec[slot(ind, c)];
1009 out.QN(ist, c) += tokens * dt;
1010 out.UN(ist, c) += tokens * dt;
1011 double depr = 0.0;
1012 const std::vector<std::size_t>& cons = consumers_[ind][c];
1013 for (std::size_t a = 0; a < cons.size(); ++a) depr += Ak[cons[a]];
1014 // A Source has no consuming transition: its throughput is the
1015 // aggregate arrival rate it injects, which is what makes the
1016 // reference station report the open class's arrival rate.
1017 const std::vector<std::size_t>& prod = producers_[ind][c];
1018 for (std::size_t a = 0; a < prod.size(); ++a) depr += Ak[prod[a]];
1019 out.TN(ist, c) += depr * dt;
1020 }
1021 }
1022
1023 // One atomic firing, then the immediate cascade the new marking enabled.
1024 // A capacity-bound Place loses the tokens pushed above its bound BEFORE
1025 // the cascade sees the marking.
1026 for (std::size_t j = 0; j < NS_; ++j) nvec[j] += rx_[kfire].S[j];
1027 if (has_caps_) apply_caps(nvec, rx_[kfire].dep_slots);
1028 collapse(nvec);
1029
1030 // Advance the Gibson and Bruck clocks with the PRE-firing propensities,
1031 // then refresh every propensity from the new marking.
1032 for (std::size_t k = 0; k < nrx; ++k) Tk[k] += Ak[k] * dt;
1033 for (std::size_t k = 0; k < nrx; ++k) Ak[k] = detail::spn_propensity(nvec, rx_[k]);
1034 Pk[kfire] -= std::log(rng_.uniform());
1035 for (std::size_t k = 0; k < nrx; ++k)
1036 tau[k] = Ak[k] > 0.0 ? (Pk[k] - Tk[k]) / Ak[k] : inf;
1037 }
1038
1039 if (total_time > 0.0)
1040 for (std::size_t ist = 0; ist < M_; ++ist)
1041 for (std::size_t c = 0; c < K_; ++c) {
1042 out.QN(ist, c) /= total_time;
1043 out.UN(ist, c) /= total_time;
1044 out.TN(ist, c) /= total_time;
1045 }
1046 for (std::size_t c = 0; c < K_; ++c) {
1047 out.XN[c] = out.TN(sn_.classes[c].refstat - 1, c);
1048 for (std::size_t ist = 0; ist < M_; ++ist)
1049 out.RN(ist, c) = out.TN(ist, c) > 0.0 ? out.QN(ist, c) / out.TN(ist, c) : 0.0;
1050 if (out.XN[c] > 0.0) out.CN[c] = sn_.classes[c].population / out.XN[c];
1051 }
1052 out.simulated_time = total_time;
1053 out.samples = n;
1054 return out;
1055}
1056
1057/**
1058 * `spnFluidRun`: the piecewise-deterministic loop of a fluid stochastic Petri net.
1059 *
1060 * Each pass resolves the running servers and level slopes, schedules the first guard or bound event, races
1061 * it against the firing epochs of the timed clocks and advances the levels linearly to whichever comes first.
1062 * Only a FIRING counts as a sample; a guard or bound event re-reads the guards and collapses the immediate
1063 * modes it enabled. The level reported is its time average, and the outflow (TN) is everything that leaves the
1064 * place: running outflows, cut at the empty level to the inflow, plus the part of each negative impulse that
1065 * was actually there to remove.
1066 */
1067template <class T>
1068SsaSolution NrmSpnEngine<T>::run_fluid() {
1069 const std::size_t nrx = rx_.size();
1070 const std::size_t nf = fl_node_.size();
1071 SsaSolution out;
1072 out.QN = Matrix<double>(M_, K_, 0.0);
1073 out.UN = Matrix<double>(M_, K_, 0.0);
1074 out.RN = Matrix<double>(M_, K_, 0.0);
1075 out.TN = Matrix<double>(M_, K_, 0.0);
1076 out.CN.assign(K_, 0.0);
1077 out.XN.assign(K_, 0.0);
1078 out.StartN = Matrix<double>(M_, K_, 0.0);
1079 out.PreemptN = Matrix<double>(M_, K_, 0.0);
1080 out.method = "nrm";
1081 std::vector<bool> fluid_st(M_, false);
1082 for (std::size_t q = 0; q < nf; ++q) fluid_st[fl_ist_[q]] = true;
1083
1084 std::vector<double> f = fl_f0_;
1085 std::vector<double> nvec = nvec0_;
1086 std::vector<double> QF(nf, 0.0), OF(nf, 0.0);
1087 {
1088 // the outflow of the initial collapse precedes the observation window and is discarded
1089 std::vector<double> of0(nf, 0.0);
1090 collapse_fluid(nvec, f, std::vector<double>(nf, 0.0), of0);
1091 }
1092 if (has_caps_) {
1093 std::vector<std::size_t> all(NS_);
1094 for (std::size_t j = 0; j < NS_; ++j) all[j] = j;
1095 apply_caps(nvec, all);
1096 }
1097 std::vector<double> Pk(nrx, 0.0), Tk(nrx, 0.0), a0(nrx, 0.0), Ik(nrx, 0.0), tau(nrx, 0.0);
1098 for (std::size_t k = 0; k < nrx; ++k) Pk[k] = -std::log(rng_.uniform());
1099
1100 const double inf = std::numeric_limits<double>::infinity();
1101 double total_time = 0.0;
1102 std::size_t n = 0, zero_run = 0, ndet = 0;
1103 const std::size_t n_warm = ssa_warmup_firings(opt_.warmupfrac, opt_.samples);
1104 bool warm_pending = n_warm > 0;
1105 std::vector<double> eff, v, vin, vout;
1106 std::vector<double> gk(nrx, 1.0);
1107 fm_.assign(nrx, std::vector<double>());
1108 bool has_mark_dep = false;
1109 for (std::size_t k = 0; k < nrx; ++k) {
1110 has_mark_dep = has_mark_dep || static_cast<bool>(rx_[k].gdep);
1111 for (const auto& ff : rx_[k].flow_fun) has_mark_dep = has_mark_dep || static_cast<bool>(ff);
1112 }
1113 line::util::LineConsole::loop("drawing the sample path: %zu samples requested",
1114 static_cast<std::size_t>(opt_.samples));
1115 const std::size_t console_every = std::max<std::size_t>(1, opt_.samples / 20);
1116 while (n < opt_.samples) {
1117 if (warm_pending && n == n_warm) {
1118 out.QN = Matrix<double>(M_, K_, 0.0);
1119 out.UN = Matrix<double>(M_, K_, 0.0);
1120 out.TN = Matrix<double>(M_, K_, 0.0);
1121 std::fill(QF.begin(), QF.end(), 0.0);
1122 std::fill(OF.begin(), OF.end(), 0.0);
1123 total_time = 0.0;
1124 warm_pending = false;
1125 }
1126 // the marking multipliers hold for the whole segment: only a firing or an immediate collapse changes the
1127 // marking, and either ends the segment
1128 if (has_mark_dep) fluid_mark_mul(nvec, gk);
1129 fluid_resolve(nvec, f, eff, v, vin, vout);
1130 for (std::size_t k = 0; k < nrx; ++k) a0[k] = rx_[k].base_rate * eff[k] * gk[k];
1131
1132 // the next scheduled non-firing event
1133 double tdet = inf, fdet = 0.0;
1134 std::size_t qdet = 0;
1135 fluid_next_det(f, v, tdet, qdet, fdet);
1136
1137 // the next firing, from each clock's remaining unit-rate time
1138 double dt_r = inf;
1139 std::size_t kfire = 0;
1140 for (std::size_t k = 0; k < nrx; ++k) {
1141 tau[k] = a0[k] > 0.0 ? detail::spn_fluid_fire_time(rx_[k], a0[k], f, v, Pk[k] - Tk[k], tdet) : inf;
1142 if (tau[k] < dt_r) {
1143 dt_r = tau[k];
1144 kfire = k;
1145 }
1146 }
1147 const bool fire = dt_r <= tdet;
1148 const double dt = fire ? dt_r : tdet;
1149 if (std::isinf(dt))
1150 throw NumericError(ErrorKind{"Deadlock"},
1151 "solver_ssa_nrm_spn: deadlock -- no transition is enabled and no fluid level is moving, so the "
1152 "sample path cannot advance");
1153
1154 // tallies over the segment
1155 for (std::size_t k = 0; k < nrx; ++k)
1156 Ik[k] = a0[k] > 0.0 ? detail::spn_fluid_hazard(rx_[k], a0[k], f, v, dt) : 0.0;
1157 total_time += dt;
1158 for (std::size_t ist = 0; ist < M_; ++ist) {
1159 if (fluid_st[ist]) continue;
1160 const std::size_t ind = sn_.station_to_node[ist] - 1;
1161 for (std::size_t c = 0; c < K_; ++c) {
1162 const double tokens = nvec[slot(ind, c)];
1163 out.QN(ist, c) += tokens * dt;
1164 out.UN(ist, c) += tokens * dt;
1165 double fired = 0.0;
1166 for (std::size_t k : consumers_[ind][c]) fired += Ik[k];
1167 for (std::size_t k : producers_[ind][c]) fired += Ik[k];
1168 out.TN(ist, c) += fired;
1169 }
1170 }
1171 for (std::size_t q = 0; q < nf; ++q) {
1172 QF[q] += f[q] * dt + 0.5 * v[q] * dt * dt;
1173 // an empty place cut to its inflow passes on exactly what it receives
1174 if (v[q] == 0.0 && vin[q] < vout[q] && f[q] <= 0.0)
1175 OF[q] += vin[q] * dt;
1176 else
1177 OF[q] += vout[q] * dt;
1178 }
1179 for (std::size_t k = 0; k < nrx; ++k) Tk[k] += Ik[k];
1180 for (std::size_t q = 0; q < nf; ++q) f[q] = std::min(std::max(f[q] + v[q] * dt, 0.0), fl_cap_[q]);
1181
1182 if (!fire) {
1183 f[qdet] = fdet;
1184 ++ndet;
1185 if (dt <= 0.0) {
1186 if (++zero_run > 1000)
1187 throw NumericError(ErrorKind{"Deadlock"},
1188 "solver_ssa_nrm_spn: the fluid level of place " + sn_.nodes[fl_node_[qdet]].name +
1189 " keeps re-crossing a guard at the same instant: a guarded flow drives its own level back "
1190 "across its own guard (a sliding mode), which a sample path has no value for");
1191 } else {
1192 zero_run = 0;
1193 }
1194 collapse_fluid(nvec, f, v, OF);
1195 if (ndet > 1000 * std::max<std::size_t>(opt_.samples, 1))
1196 throw NumericError(ErrorKind{"Deadlock"},
1197 "solver_ssa_nrm_spn: more than 1000 guard or bound events per firing: the fluid levels "
1198 "chatter. Check the guards against the flows they gate");
1199 continue;
1200 }
1201 zero_run = 0;
1202
1203 // One atomic firing, its impulses, then the immediate cascade the new marking enabled.
1204 for (std::size_t j = 0; j < NS_; ++j) nvec[j] += rx_[kfire].S[j];
1205 if (has_caps_) apply_caps(nvec, rx_[kfire].dep_slots);
1206 fluid_impulse(f, rx_[kfire], OF);
1207 collapse_fluid(nvec, f, v, OF);
1208 Pk[kfire] -= std::log(rng_.uniform());
1209
1210 ++n;
1211 if (n % console_every == 0)
1213 static_cast<long>(n / console_every),
1214 "simulated %zu of %zu samples (%.0f%%), simulated time %.4g", n,
1215 static_cast<std::size_t>(opt_.samples),
1216 100.0 * static_cast<double>(n) / static_cast<double>(opt_.samples), total_time);
1217 }
1218
1219 if (total_time > 0.0) {
1220 for (std::size_t ist = 0; ist < M_; ++ist)
1221 for (std::size_t c = 0; c < K_; ++c) {
1222 out.QN(ist, c) /= total_time;
1223 out.UN(ist, c) /= total_time;
1224 out.TN(ist, c) /= total_time;
1225 }
1226 for (std::size_t q = 0; q < nf; ++q) {
1227 QF[q] /= total_time;
1228 OF[q] /= total_time;
1229 }
1230 }
1231 // A fluid place reports its level in the first class column: QN the mean level, UN the level over the
1232 // capacity (the level itself when unbounded), TN the outflow.
1233 for (std::size_t q = 0; q < nf; ++q) {
1234 const std::size_t ist = fl_ist_[q];
1235 for (std::size_t c = 0; c < K_; ++c) out.QN(ist, c) = out.UN(ist, c) = out.TN(ist, c) = 0.0;
1236 if (K_ == 0) continue;
1237 out.QN(ist, 0) = QF[q];
1238 out.UN(ist, 0) = std::isfinite(fl_cap_[q]) ? QF[q] / fl_cap_[q] : QF[q];
1239 out.TN(ist, 0) = OF[q];
1240 }
1241 for (std::size_t c = 0; c < K_; ++c) {
1242 out.XN[c] = out.TN(sn_.classes[c].refstat - 1, c);
1243 for (std::size_t ist = 0; ist < M_; ++ist)
1244 out.RN(ist, c) = out.TN(ist, c) > 0.0 ? out.QN(ist, c) / out.TN(ist, c) : 0.0;
1245 if (out.XN[c] > 0.0) out.CN[c] = sn_.classes[c].population / out.XN[c];
1246 }
1247 out.simulated_time = total_time;
1248 out.samples = n;
1249 return out;
1250}
1251
1252/**
1253 * `spnFluidResolve`: the running servers of every timed mode and the slope of every level, solved together.
1254 * A guard is read with the lookahead of the slope and the slope sums the flows of the modes whose guards hold;
1255 * starting from the inclusive reading the two are iterated to agreement, and a pair that never agrees within
1256 * 20 rounds is a sliding mode.
1257 */
1258template <class T>
1259void NrmSpnEngine<T>::fluid_resolve(const std::vector<double>& n, const std::vector<double>& f,
1260 std::vector<double>& eff, std::vector<double>& v, std::vector<double>& vin,
1261 std::vector<double>& vout) const {
1262 const std::size_t nrx = rx_.size();
1263 const std::vector<double> still(f.size(), 0.0);
1264 std::vector<double> deg(nrx, 0.0);
1265 std::vector<bool> gok(nrx, true), gnew(nrx, true);
1266 for (std::size_t k = 0; k < nrx; ++k) {
1267 deg[k] = std::min(detail::spn_en_degree(n, rx_[k]), rx_[k].nservers);
1268 gok[k] = detail::spn_fluid_guard_holds(rx_[k], f, still);
1269 }
1270 eff.assign(nrx, 0.0);
1271 for (int it = 0; it < 20; ++it) {
1272 for (std::size_t k = 0; k < nrx; ++k) eff[k] = gok[k] ? deg[k] : 0.0;
1273 fluid_slope(f, eff, v, vin, vout);
1274 for (std::size_t k = 0; k < nrx; ++k) gnew[k] = detail::spn_fluid_guard_holds(rx_[k], f, v);
1275 if (gnew == gok) return;
1276 gok = gnew;
1277 }
1278 throw NumericError(ErrorKind{"Deadlock"},
1279 "solver_ssa_nrm_spn: a guarded fluid flow switches its own guard on and off at the threshold (a sliding "
1280 "mode), which a sample path has no value for");
1281}
1282
1283/**
1284 * `spnFluidSlope`: running inflows minus running outflows, each scaled by its marking multiplier (`fm_`), cut to
1285 * zero where a bound stops the level.
1286 */
1287template <class T>
1288void NrmSpnEngine<T>::fluid_slope(const std::vector<double>& f, const std::vector<double>& eff,
1289 std::vector<double>& v, std::vector<double>& vin,
1290 std::vector<double>& vout) const {
1291 const std::size_t nf = fl_node_.size();
1292 vin.assign(nf, 0.0);
1293 vout.assign(nf, 0.0);
1294 for (std::size_t k = 0; k < rx_.size(); ++k) {
1295 if (eff[k] <= 0.0) continue;
1296 for (std::size_t t = 0; t < rx_[k].flow_q.size(); ++t) {
1297 const std::size_t q = rx_[k].flow_q[t];
1298 double rt = rx_[k].flow_rate[t] * eff[k];
1299 if (k < fm_.size() && !fm_[k].empty()) rt *= fm_[k][t];
1300 if (rt > 0.0)
1301 vin[q] += rt;
1302 else
1303 vout[q] -= rt;
1304 }
1305 }
1306 v.assign(nf, 0.0);
1307 for (std::size_t q = 0; q < nf; ++q) {
1308 v[q] = vin[q] - vout[q];
1309 if ((f[q] <= 0.0 && v[q] < 0.0) || (f[q] >= fl_cap_[q] && v[q] > 0.0)) v[q] = 0.0;
1310 }
1311}
1312
1313/** `spnFluidNextDet`: the first guard threshold or bound a moving level reaches, when, and at which level. */
1314template <class T>
1315void NrmSpnEngine<T>::fluid_next_det(const std::vector<double>& f, const std::vector<double>& v, double& tdet,
1316 std::size_t& qdet, double& fdet) const {
1317 tdet = std::numeric_limits<double>::infinity();
1318 qdet = 0;
1319 fdet = 0.0;
1320 for (std::size_t q = 0; q < fl_node_.size(); ++q) {
1321 if (v[q] > 0.0 && std::isfinite(fl_cap_[q])) {
1322 const double t = (fl_cap_[q] - f[q]) / v[q];
1323 if (t < tdet) {
1324 tdet = t;
1325 qdet = q;
1326 fdet = fl_cap_[q];
1327 }
1328 } else if (v[q] < 0.0) {
1329 const double t = f[q] / (-v[q]);
1330 if (t < tdet) {
1331 tdet = t;
1332 qdet = q;
1333 fdet = 0.0;
1334 }
1335 }
1336 }
1337 const std::vector<Rx>* sets[2] = {&rx_, &imm_};
1338 for (int s = 0; s < 2; ++s)
1339 for (const Rx& rec : *sets[s])
1340 for (std::size_t g = 0; g < rec.guard_q.size(); ++g) {
1341 const std::size_t q = rec.guard_q[g];
1342 const double thr = rec.guard_thr[g];
1343 if (v[q] == 0.0 || std::fabs(f[q] - thr) <= 1e-12 * std::max(1.0, std::fabs(thr))) continue;
1344 const double t = (thr - f[q]) / v[q];
1345 if (t > 0.0 && t < tdet) {
1346 tdet = t;
1347 qdet = q;
1348 fdet = thr;
1349 }
1350 }
1351 tdet = std::max(tdet, 0.0);
1352}
1353
1354/**
1355 * `spnFluidImpulse`: the impulses of a firing, each clipped at the bounds of its place; what an impulse would
1356 * carry past them is lost, and the part of a negative impulse that was there to remove is outflow.
1357 */
1358template <class T>
1359void NrmSpnEngine<T>::fluid_impulse(std::vector<double>& f, const Rx& rec, std::vector<double>& OF) const {
1360 for (std::size_t t = 0; t < rec.imp_q.size(); ++t) {
1361 const std::size_t q = rec.imp_q[t];
1362 const double fnew = std::min(std::max(f[q] + rec.imp_d[t], 0.0), fl_cap_[q]);
1363 if (fnew < f[q]) OF[q] += f[q] - fnew;
1364 f[q] = fnew;
1365 }
1366}
1367
1368/**
1369 * `spnCollapseFluid`: `collapse` with the guards and impulses of the immediate modes. A guard is read with the
1370 * lookahead of the slope `v` the level had coming in.
1371 */
1372template <class T>
1373void NrmSpnEngine<T>::collapse_fluid(std::vector<double>& n, std::vector<double>& f, const std::vector<double>& v,
1374 std::vector<double>& OF) {
1375 if (imm_.empty()) return;
1376 std::size_t steps = 0;
1377 while (true) {
1378 std::vector<std::size_t> enabled;
1379 for (std::size_t m = 0; m < imm_.size(); ++m)
1380 if (detail::spn_en_degree(n, imm_[m]) >= 1.0 && detail::spn_fluid_guard_holds(imm_[m], f, v))
1381 enabled.push_back(m);
1382 if (enabled.empty()) return;
1383 double top_prio = -std::numeric_limits<double>::infinity();
1384 for (std::size_t i = 0; i < enabled.size(); ++i) top_prio = std::max(top_prio, imm_[enabled[i]].prio);
1385 std::vector<std::size_t> top;
1386 std::vector<double> w;
1387 for (std::size_t i = 0; i < enabled.size(); ++i)
1388 if (imm_[enabled[i]].prio == top_prio) {
1389 top.push_back(enabled[i]);
1390 w.push_back(imm_[enabled[i]].weight);
1391 }
1392 const std::size_t pick = top.size() == 1 ? top[0] : top[rng_.draw(w)];
1393 for (std::size_t j = 0; j < NS_; ++j) n[j] += imm_[pick].S[j];
1394 fluid_impulse(f, imm_[pick], OF);
1395 if (++steps > kMaxImmSteps)
1396 throw NumericError(ErrorKind{"NonConvergence"},
1397 "solver_ssa_nrm_spn: immediate-transition livelock -- the vanishing-marking collapse did not reach "
1398 "a tangible marking");
1399 }
1400}
1401
1402} // namespace ssa
1403} // namespace line
1404
1405#endif // LINE_SOLVERS_SSA_SOLVER_SSA_NRM_SPN_H
InputError(const std::string &what)
Definition error.h:105
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
NrmSpnEngine(const qn::NetworkStruct< T > &sn, const SsaOptions &opt)
SsaSolution run()
Run opt.samples firings and return the time-averaged metrics.
std::size_t nimmediate() const
The immediate-mode count, likewise.
std::size_t nreactions() const
The reaction count, for the tests that assert the builder.
The uniform source, MATLAB's rand.
Definition ssa_types.h:195
static void loop(const char *fmt,...)
Announce an iteration loop and reset its reporting budget.
static void iter(long k, const char *fmt,...)
Report iteration k of the current loop.
The exception types the port throws.
Running progress log of a LINE solver run (the "solver console").
Dense matrix and non-owning view.
SnFluidVerdict sn_fluid_refusal(const qn::NetworkStruct< T > &sn)
Whether the net breaks a structural rule of an FSPN.
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:370
InputError err(const std::string &msg, int line=-1)
Definition srvn_reader.h:97
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
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
RootResult< T > root_brent(F f, const T &a0, const T &b0, const T &tol, unsigned maxiter=200)
Brent's method on a bracket with a sign change.
Definition rootfind.h:130
A queueing network and its refreshed NetworkStruct.
Deterministic scalar root finding.
Port of matlab/src/api/sn/sn_fluid_refusal.m.
Controls, results and the random source of SolverSSA.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static constexpr double MaxInt
Stand-in for an unbounded COUNT, MATLAB GlobalConstants.MaxInt.
Definition lang_types.h:776
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:71
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