LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ssa_serial.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_SERIAL_H
6#define LINE_SOLVERS_SSA_SOLVER_SSA_SERIAL_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverSSA, the `serial` method: a port of `solver_ssa_reachability.m`, of the
12 * run loop of `solver_ssa.m`, and of `solver_ssa_analyzer_serial.m`.
13 *
14 * WHAT THE METHOD IS, AND HOW IT DIFFERS FROM THE NRM. The NRM rewrites the
15 * network as a reaction grid over (node, class, phase) counts and never touches
16 * the state ENCODING. The serial engine simulates the network in its own
17 * encoding instead: at every step it applies the SAME event handlers the CTMC
18 * generator applies (`after_event`, `after_global_event`) to the current network
19 * state, collects every enabled synchronization with its rate, and takes one
20 * Gillespie direct-method step. That is why it reaches models the NRM refuses --
21 * anything the encoding can express, the handlers can move -- and why it is
22 * slower: the enabled set is rebuilt from scratch at every firing, as the
23 * reference's own inlined `solver_ssa_findenabled` does.
24 *
25 * TWO ENGINES, TWO STREAMS. A seed-fixed result from the serial engine cannot be
26 * reproduced by the NRM and vice versa: they consume different draws in a
27 * different order. Neither can be reproduced by MATLAB, the JAR or native
28 * Python, for the reason `ssa_types.h` states at length. A cross-codebase check
29 * against this engine is STATISTICAL. A simulated number without its sample
30 * count and its seed is not a measurement, which is why `SsaSerialSolution`
31 * carries both and why the tests compare against the exact CTMC only inside a
32 * stated Monte Carlo band.
33 *
34 * THE JUMP CHAIN IS DRAWN IN ONE STEP, NOT TWO. MATLAB calls `State.afterEvent`
35 * with `isSimulation = true`, which SAMPLES one successor row and returns its
36 * probability; the direct method then picks among the sampled rows. The C++
37 * `after_event` is the enumeration-mode handler and returns EVERY successor with
38 * its probability, so this port flattens the (synchronization, active row,
39 * passive row) triples into one weighted list and draws from it once. The two
40 * are the same jump chain -- P(triple) = rate * p_active * p_route * p_passive,
41 * normalized -- reached with a different number of uniforms, which is exactly
42 * the stream difference above and not a modelling difference.
43 *
44 * ZERO-RATE ROWS ARE DROPPED, NOT FLOORED. The reference rewrites a zero or NaN
45 * rate to 1e-38 "so that it is never selected", which leaves it in the enabled
46 * list and in the arrival/departure rate statistics. Dropping it is the same
47 * sample path to within 1e-38 and keeps `depRates` exactly the rate the CTMC
48 * generator would accumulate for the same state, which is what makes the
49 * throughput comparable between the two solvers.
50 *
51 * THE STATE ROW DOES NOT GROW HERE, SO IT STARTS WIDE. MATLAB's simulation-mode
52 * handlers widen a buffer when a job arrives and no slot is free, and
53 * `solver_ssa_reachability` left-pads the stored spaces to match. The C++
54 * handlers derive the buffer width from the row they are handed, so a path
55 * seeded at the natural width of the EMPTY marginal would silently saturate at
56 * one waiting job per station. The initial state is therefore padded to the
57 * WIDEST row each stateful node's local encoding admits (`serial_detail::
58 * max_row_width`), which is the width `space_generator` gives every state of
59 * that node, and the path then lives inside the enumerated encoding for free.
60 *
61 * DOUBLE ONLY, for the reason `solver_ssa_nrm.h` gives: the sample path is
62 * generated from exponential clocks drawn as `-log(u)`, there is no exact value
63 * to compute, and the answer's error is the Monte Carlo error rather than the
64 * rounding. A non-`double` backend is refused BY NAME.
65 */
66
67#include <algorithm>
68#include <cmath>
69#include <cstddef>
70#include <limits>
71#include <map>
72#include <string>
73#include <type_traits>
74#include <vector>
75
77#include "line/lang/qn/fj_tag.h"
80#include "line/lang/qn/state.h"
87#include "line/util/error.h"
88#include "line/util/matrix.h"
89
90namespace line {
91namespace ssa {
92
93/**
94 * The serial engine's knobs: `SsaOptions` plus the three the serial path reads
95 * and the NRM has no use for.
96 *
97 * `cutoff` bounds an open class exactly as SolverCTMC's does, and for the same
98 * reason: it fixes how wide a station's buffer encoding is, so it decides where
99 * the truncation sits. A refused arrival at the truncation boundary is a LOSS
100 * (`arrival_is_lost` on an open class), which is what the CTMC truncation does
101 * too, so the two solvers truncate the same model the same way.
102 */
104 double cutoff = -1.0; ///< < 0 = the reference's automatic value
105 std::size_t state_max = 3000000; ///< refuse a reachable space larger than this
106};
107
108/**
109 * Port of `solver_ssa_reachability.m`'s return: `[SSq, SSh, sn.space]`.
110 *
111 * `node_space[i]` is the reference's `space{i}`, the distinct local rows node
112 * `i` was seen in; `hash` is `SSh`, one 1-BASED index into `node_space[i]` per
113 * stateful node per state; `ssq` is `SSq`, the same states with their local rows
114 * concatenated. The three are redundant by construction and the reference
115 * returns all three because its callers index states by node (`SSh`) and read
116 * them flat (`SSq`).
117 *
118 * THE ORDER IS THE WALK'S, NOT THE REFERENCE'S. The reference pushes and pops a
119 * stack of its own; this reuses `reachable_space_generator`, whose stack order
120 * differs. The SET is the same and nothing downstream indexes it positionally
121 * across codebases, so the difference is not observable in a metric.
122 */
123template <class T>
125 std::vector<qn::NetState<T>> space; ///< the reachable states
126 std::vector<std::vector<std::vector<T>>> node_space; ///< `sn.space`, per stateful node
127 std::vector<std::vector<std::size_t>> hash; ///< `SSh`, 1-based per node
128 Matrix<T> ssq; ///< `SSq`, states x concatenated width
129};
130
131/** One sample path, in the shape `solver_ssa.m` returns it. */
132template <class T>
134 /** The DISTINCT states visited, in first-visit order (the reference's `u`). */
135 std::vector<qn::NetState<T>> space;
136 /**
137 * The region token FIFOs of each of those states, the reference's `fcrBuf`.
138 *
139 * Empty (one empty vector per region, or no vectors at all) on every model
140 * without a WAITQ region. A parked job is in NO station, so it appears in no
141 * queue length and is visible only here -- the JMT report convention, which
142 * `ctmc_waitq_parked` states for the exact solver and
143 * `SsaSerialSolution::parked` for this one.
144 */
145 std::vector<std::vector<std::vector<std::size_t>>> buf;
146 /** `pi`: the fraction of simulated time spent in each of them. */
147 std::vector<double> pi;
148 /** `SSq`: the per-(station, class) job counts of each distinct state. */
150 /**
151 * `arvRates` / `depRates`, indexed [distinct state][stateful-1][class-1].
152 *
153 * They are a deterministic function of the state, so one sample per state is
154 * the exact value and not an estimate -- which is what lets the analyzer
155 * multiply them by `pi` and get a throughput rather than a sample mean. The
156 * reference says as much where it keeps `arvRatesSamples(ui(s),...)`.
157 */
158 std::vector<std::vector<std::vector<double>>> arv_rates, dep_rates;
159 /**
160 * The DERIVED rates per state, laid out like `arv_rates`: how fast the
161 * transitions enabled in that state START a class-r service at a stateful
162 * node, and how fast they PUSH a class-r job in service back into the
163 * buffer there. Annotations on the arcs the engine already walks, so no
164 * rate, probability or state depends on them.
165 */
166 std::vector<std::vector<std::vector<double>>> start_rates, preempt_rates;
167 /**
168 * The rate of the cache MERGE transitions, i.e. of the delayed hits, in the
169 * same [state][stateful-1][class-1] shape and by the same argument.
170 *
171 * A delayed hit is invisible in `dep_rates`: the merged request is released
172 * later, in the HIT class, and is indistinguishable there from a true hit.
173 * The merge itself is the only cache transition that EMPTIES the node -- it
174 * decrements the read class and adds nothing -- which is what identifies it.
175 */
176 std::vector<std::vector<std::vector<double>>> dly_rates;
177 /** `tranSysState{1}`: the cumulative time at each firing. */
178 std::vector<double> tran_time;
179 /** `tranSync`: which synchronization fired, `sync.size() + g` for a global one. */
180 std::vector<std::size_t> tran_sync;
181 /**
182 * The row of `space` the path OCCUPIED over `[t-dt, t]`, one per firing.
183 *
184 * The trace and the distinct-state table are two views of the same path and
185 * the samplers need both: `sampleSys` prints the state at each event and
186 * `getProb` sums the time spent in one state, so keeping only `pi` would
187 * lose the order and keeping only the rows would lose the aggregation. It
188 * indexes the state BEFORE the firing, exactly as `pi` weights it.
189 */
190 std::vector<std::size_t> tran_state;
191 double simulated_time = 0.0;
192 std::size_t samples = 0; ///< firings actually performed
193 std::size_t warmup = 0; ///< leading firings excluded from `pi`
194 unsigned long seed = 0; ///< the stream this path came from
195};
196
197/** What the cache write-back of `solver_ssa_analyzer_serial.m` produces. */
199 std::size_t node = 0; ///< 1-based Cache node index
200 std::vector<double> hitprob, missprob; ///< per class, NaN where undefined
201 /**
202 * The delayed-hit share, EMPTY off a retrieval system.
203 *
204 * `hitprob` carries the TRUE hits alone once this is filled: the three shares
205 * partition every read, hit + delayed + miss = 1, which is the convention NC
206 * and LDES report and what makes `ArvR = arvr*(missprob + delayedprob)`.
207 */
208 std::vector<double> delayedprob;
209 /**
210 * `actualresidt`: NaN, and NOT a port gap. The reference warns
211 * "Retrieval-system expected latency is not currently implemented; reporting
212 * NaN" and reports NaN in every codebase, so reproducing the NaN IS parity.
213 */
214 std::vector<double> residt;
215};
216
217/** The serial analyzer's return: the metric table, the path, and the stream. */
218template <class T>
220 SsaSolution avg; ///< QN, UN, RN, TN, XN, CN; `method` = "serial"
222 unsigned long seed = 0; ///< carried beside the numbers, never implied
223 std::vector<SsaCacheRatio> cache;
224 /**
225 * `fjclassmap` of the tag augmentation, empty on a model with no Fork.
226 *
227 * The PATH inside `run` is the augmented one -- its classes are the sibling
228 * classes `fj_tag` invented -- while `avg` has been folded back onto the
229 * classes the caller declared. The map is what relates the two, and it is
230 * returned rather than discarded for the reason SolverCTMC returns it: a
231 * caller reading the trajectory needs to know which class a sibling came
232 * from.
233 */
234 std::vector<std::size_t> fjclassmap;
235 /**
236 * Mean number of jobs parked in a region FIFO, per class of the struct that
237 * ran. Zero everywhere without a WAITQ region.
238 *
239 * IT IS IN NO QLen, so a population check on a closed model has to add it
240 * back by hand. That is the JMT convention `ctmc_waitq_parked` reports for
241 * the exact solver and not an omission here.
242 */
243 std::vector<double> parked;
244};
245
246namespace serial_detail {
247
248using lang::NodeType;
250
251/**
252 * `solver_ssa.m`'s own guards, plus what this port cannot represent.
253 *
254 * The reference's remaining guards -- non-exponential reneging patience,
255 * non-QUEUE_LENGTH balking, heterogeneous servers (`nodeparam.nservertypes`) --
256 * have NO field in the C++ `NetworkStruct`, so a model declaring one cannot be
257 * built and the guard would be dead code. Reneging is the sharpest case: it
258 * enters the chain only through `refresh_sync`'s `impatience_classes` argument,
259 * which no analyzer has a source for, so no RENEGE synchronization exists here
260 * at all. The same holds for the class-switch mask violation the reference
261 * raises inside its scan: `sn.csmask` is not carried, and the state-dependent
262 * routing that can violate it is refused when the struct is built.
263 */
264template <class T>
265bool serial_check(const qn::NetworkStruct<T>& sn, bool raise = true, bool skip_fork = false) {
266 // A WAITQ region parks refused jobs in a FIFO the engine carries beside the
267 // network state, and the combinations that FIFO has no meaning against are
268 // the CTMC's own: one list, so the two solvers refuse the same models with
269 // the same sentence and neither can silently accept what the other rejects.
270 if (!sn.regions.empty()) {
271 // The rule decides which machinery runs, so the gate is the matching
272 // one: `ctmc_check_waitq_support` for the token FIFO, and the DROP-only
273 // checker otherwise, which is what refuses BAS/BBS/RSRD by name.
274 try {
277 else
279 } catch (const UnsupportedError&) {
280 if (!raise) return false;
281 throw;
282 }
283 }
284 // A Fork fires through `fjsync`, which only the TAG-AUGMENTED struct carries
285 // (`fj_tag`). A raw fork-join struct reaching the engine would find no
286 // firing at all: the fork would never emit, the branches would stay empty
287 // and the path would report a network that transparently swallows every
288 // forked task. The analyzer augments before it runs, so this refusal is
289 // reachable only by a caller who drove the engine directly.
290 if (!skip_fork && sn.has_fork() && !sn.isfjaugmented) {
291 if (!raise) return false;
292 throw UnsupportedError(
293 "SolverSSA(method='serial'): the model contains a Fork node and has not been "
294 "tag-augmented. A fork fires through `sn.fjsync` / `State.afterFJEvent`, which only "
295 "`fj_tag` builds; call `solver_ssa_serial_analyzer`, which augments and folds the "
296 "sibling classes back, rather than the engine directly");
297 }
298 for (std::size_t i = 0; i < sn.nstations; ++i) {
299 const typename std::map<std::size_t, qn::RetrialParam<T>>::const_iterator rit =
300 sn.retrialparam.find(i + 1);
301 if (rit == sn.retrialparam.end()) continue;
302 bool any = false;
303 std::size_t served = 0;
304 for (std::size_t r = 0; r < rit->second.retrial_proc.size(); ++r) {
305 if (rit->second.retrial_proc[r].disabled) continue;
306 any = true;
307 if (rit->second.retrial_proc[r].type != lang::ProcessType::EXP) {
308 if (!raise) return false;
309 throw UnsupportedError(
310 "SolverSSA(method='serial'): station '" + sn.stations[i].name +
311 "' retries with non-exponential patience. SOLVER_SSA supports only "
312 "exponential (memoryless) retrial delay in every codebase, because the orbit "
313 "carries no remaining-delay phase");
314 }
315 if (r < rit->second.max_attempts.size() && rit->second.max_attempts[r] > 0) {
316 if (!raise) return false;
317 throw UnsupportedError(
318 "SolverSSA(method='serial'): station '" + sn.stations[i].name +
319 "' declares a finite retrial max-attempts count. SOLVER_SSA supports only "
320 "unlimited retrials in every codebase, because the attempt counter is not "
321 "part of the state");
322 }
323 }
324 if (!any) continue;
325 for (std::size_t r = 0; r < sn.nclasses; ++r)
326 if (!sn.disabled[i][r] && sn.service[i][r].D0.rows() > 0) ++served;
327 if (served > 1) {
328 if (!raise) return false;
329 throw UnsupportedError(
330 "SolverSSA(method='serial'): station '" + sn.stations[i].name +
331 "' is a multi-class retrial station. SOLVER_SSA supports retrial only for "
332 "single-class stations in every codebase");
333 }
334 }
335 return true;
336}
337
338/**
339 * Can `solver_ssa_serial_analyzer` run this model at all?
340 *
341 * The dispatcher's fallback test, and it is the SAME body as the refusal above
342 * so the two cannot drift. The fork guard is skipped because the analyzer
343 * augments before the engine sees the struct, so a raw fork-join model IS one
344 * the serial path runs -- answering otherwise would send it back to the NRM,
345 * which excludes fork-join in every codebase.
346 */
347template <class T>
348bool serial_can_run(const qn::NetworkStruct<T>& sn) {
349 return serial_check(sn, false, true);
350}
351
352/**
353 * The widest local row stateful node `ind` admits, which is the width
354 * `space_generator` gives EVERY state of that node.
355 *
356 * WHY IT IS COMPUTED RATHER THAN ENUMERATED. Taking the width from
357 * `space_generator` would mean building the whole cartesian product across
358 * nodes, which is the cost simulation exists to avoid. The width of one node's
359 * row is a property of that node alone: for an ordered (class-tag) buffer it
360 * grows with the TOTAL jobs held and not with how they split across classes, and
361 * for every other encoding it is constant. So one call to `from_marginal_node`
362 * at a maximal admissible marginal settles it, and the split is chosen to fill
363 * the classes in order precisely because a lopsided multiset has the fewest
364 * permutations for `from_marginal` to enumerate.
365 *
366 * The descent to a smaller total is not defensive padding: `from_marginal_node`
367 * returns NO rows for a marginal the station cannot hold, and the joint bound
368 * (`cap` against the sum of the per-class bounds) can be met by a marginal that
369 * some other constraint inside the handler still rejects.
370 */
371template <class T>
372std::size_t max_row_width(const qn::NetworkStruct<T>& sn, std::size_t ind,
373 const std::vector<std::size_t>& cutoff) {
374 const std::size_t R = sn.nclasses;
375 const std::size_t ist = sn.nodes[ind - 1].station;
376 std::vector<std::size_t> ph(R, 1);
377 if (ist != 0)
378 for (std::size_t r = 0; r < R; ++r) ph[r] = sn.phasessz_of(ist, r + 1);
379
380 // A stateful non-station (a Cache, a Join, a Transition) holds a per-class
381 // count and its local variables, both of fixed width, so the empty marginal
382 // already gives the final width.
383 if (ist == 0 || sn.stations[ist - 1].nodetype == NodeType::Source) {
384 std::vector<T> row;
385 if (!qn::from_marginal_node_first(sn, ind, std::vector<std::size_t>(R, 0), ph, row))
386 throw UnsupportedError("SolverSSA(method='serial'): node '" + sn.nodes[ind - 1].name +
387 "' admits no state at all, so no sample path can start");
388 return row.size();
389 }
390
391 // The per-class bound: the class population when closed, the cutoff when
392 // open, never above the station's own per-class capacity.
393 std::vector<std::size_t> bound(R, 0);
394 for (std::size_t r = 0; r < R; ++r) {
395 const double nj = sn.njobs()[r];
396 double b = std::isfinite(nj) ? nj : static_cast<double>(cutoff[r]);
397 const double cc = sn.classcap[ist - 1][r];
398 if (cc < b) b = cc;
399 if (!(b > 0)) b = 0;
400 bound[r] = static_cast<std::size_t>(b);
401 }
402 double tcapd = sn.cap[ist - 1];
403 std::size_t total = 0;
404 for (std::size_t r = 0; r < R; ++r) total += bound[r];
405 if (std::isfinite(tcapd) && tcapd < static_cast<double>(total))
406 total = static_cast<std::size_t>(tcapd);
407
408 // A PAS / OI station is the one encoding `from_marginal_node` cannot size:
409 // its row is the ORDERED JOB LIST, one position per job, and the function
410 // builds the ordinary [buffer | server] split instead -- which for a
411 // single-server station returns a ONE-COLUMN row however many jobs the
412 // marginal holds. A path seeded at that width would hold one job and block
413 // every further arrival, so the width is taken from the job bound directly.
414 if (sn.stations[ist - 1].sched == SchedStrategy::PAS ||
415 sn.stations[ist - 1].sched == SchedStrategy::OI)
416 return total + sn.nvars_of(ind);
417
418 for (std::size_t t = total + 1; t-- > 0;) {
419 std::vector<std::size_t> n(R, 0);
420 std::size_t left = t;
421 for (std::size_t r = 0; r < R && left > 0; ++r) {
422 n[r] = std::min(left, bound[r]);
423 left -= n[r];
424 }
425 if (left > 0) continue; // this total does not fit the per-class bounds
426 const std::vector<std::vector<T>> rows = qn::from_marginal_node(sn, ind, n, ph);
427 if (!rows.empty()) return rows[0].size();
428 }
429 throw UnsupportedError("SolverSSA(method='serial'): station '" + sn.stations[ist - 1].name +
430 "' admits no state at all, so no sample path can start");
431}
432
433/**
434 * The initial network state, padded to the encoding width of every node.
435 *
436 * The marginal is `Network.initDefault`'s -- every closed class's jobs at its
437 * reference station -- taken from the CTMC analyzer so the two solvers start the
438 * same model in the same state. The LEFT pad is not a convention chosen here: it
439 * is what `space_generator` does to the narrow rows, and every slicer in
440 * `state_events.h` measures its blocks from the RIGHT-hand end, so a right pad
441 * would shift the server block and silently decode the wrong queue.
442 */
443template <class T>
444qn::NetState<T> wide_init_state(const qn::NetworkStruct<T>& sn,
445 const std::vector<std::size_t>& cutoff) {
446 qn::NetState<T> init;
447 if (!ctmc::analyzer_detail::default_init_state(sn, init))
448 throw UnsupportedError(
449 "SolverSSA(method='serial'): the model's initial state admits no state; check the "
450 "class populations against their reference stations");
451 for (std::size_t f = 0; f < sn.stateful_nodes.size(); ++f) {
452 const std::size_t w = max_row_width(sn, sn.stateful_nodes[f], cutoff);
453 if (init.local[f].size() > w) continue; // already at or beyond the encoding width
454 init.local[f].insert(init.local[f].begin(), w - init.local[f].size(),
455 num_traits<T>::from_int(0));
456 }
457 return init;
458}
459
460} // namespace serial_detail
461
462/**
463 * Port of `solver_ssa_reachability.m`: the states the DYNAMICS can occupy,
464 * decomposed per stateful node.
465 *
466 * The walk itself is `reachable_space_generator`, which applies the same
467 * handlers to the same synchronization list and additionally walks `gsync`. The
468 * reference's reachability walks only `sync`, so an SPN's firing states reach it
469 * through `solver_ssa`'s own global scan instead of through this function; here
470 * they are in the space from the start, which is strictly more of the reachable
471 * set and never less.
472 *
473 * WHAT THIS ADDS OVER THE WALK is the decomposition the reference returns and
474 * the CTMC does not need: the per-node list of distinct local rows, and the
475 * per-state index into it. That is the reference's `space{i}` / `SSh` pair, and
476 * it exists so a caller can address a state by node without carrying the rows.
477 */
478template <class T>
481 serial_detail::serial_check(sn);
483 copt.cutoff = opt.cutoff;
484 copt.state_max = opt.state_max;
485 const std::vector<std::size_t> cutoff = ctmc::analyzer_detail::resolve_cutoff(sn, copt);
486 const std::vector<qn::Sync<T>> sync = qn::refresh_sync(sn);
487 const std::vector<qn::GlobalSync<T>> gsync = qn::refresh_global_sync(sn);
488 const qn::NetState<T> init = serial_detail::wide_init_state(sn, cutoff);
489
491 // A WAITQ region's states are AUGMENTED with the token FIFO, which a
492 // `NetState` cannot hold, so this decomposition has no row to report them
493 // in and refuses rather than returning the region-free walk under the
494 // region's name. The metrics do not need it: the engine carries the FIFO
495 // itself and `SsaSerialRun::buf` reports it.
497 throw UnsupportedError(
498 "solver_ssa_reachability: the model declares a WAITQ finite capacity region, whose "
499 "states carry a per-region token FIFO outside every node; this decomposition is "
500 "per-node and cannot represent it. Use `solver_ssa_serial_analyzer`, whose run carries "
501 "the FIFO, or `solver_ctmc_waitq` for the exact augmented space");
502 // The cutoff resolved above bounds the walk, as it bounds SolverCTMC's:
503 // without it an open model's Source produces forever and the walk runs to
504 // `state_max` rather than answering.
505 out.space = ctmc::reachable_space_generator(sn, init, sync, gsync, opt.state_max,
506 std::vector<qn::FjSync<T>>(), cutoff);
507 // A DROP region censors the space exactly as it censors SolverCTMC's: the
508 // forbidden states are never occupied, so leaving them in would report a
509 // reachable set the dynamics cannot reach.
511
512 const std::size_t NF = sn.stateful_nodes.size();
513 out.node_space.assign(NF, std::vector<std::vector<T>>());
514 out.hash.assign(out.space.size(), std::vector<std::size_t>(NF, 0));
515 // One map per node, keyed on the row itself: the reference's `matchrow`,
516 // which is a linear scan and turns the decomposition quadratic on a space
517 // large enough to be worth walking.
518 std::vector<std::map<std::vector<double>, std::size_t>> seen(NF);
519 for (std::size_t s = 0; s < out.space.size(); ++s)
520 for (std::size_t f = 0; f < NF; ++f) {
521 std::vector<double> key(out.space[s].local[f].size(), 0.0);
522 for (std::size_t j = 0; j < key.size(); ++j)
523 key[j] = num_traits<T>::to_double(out.space[s].local[f][j]);
524 const typename std::map<std::vector<double>, std::size_t>::const_iterator it =
525 seen[f].find(key);
526 if (it != seen[f].end()) {
527 out.hash[s][f] = it->second;
528 } else {
529 out.node_space[f].push_back(out.space[s].local[f]);
530 seen[f][key] = out.node_space[f].size();
531 out.hash[s][f] = out.node_space[f].size();
532 }
533 }
534
535 std::size_t width = 0;
536 for (std::size_t f = 0; f < NF; ++f)
537 width += out.space.empty() ? 0 : out.space[0].local[f].size();
538 out.ssq = Matrix<T>(out.space.size(), width, num_traits<T>::from_int(0));
539 for (std::size_t s = 0; s < out.space.size(); ++s) {
540 std::size_t col = 0;
541 for (std::size_t f = 0; f < NF; ++f)
542 for (std::size_t j = 0; j < out.space[s].local[f].size(); ++j)
543 out.ssq(s, col++) = out.space[s].local[f][j];
544 }
545 return out;
546}
547
548/**
549 * The serial engine: the sample path of `solver_ssa.m`'s main loop.
550 *
551 * It is a class for the reason `NrmEngine` is: the loop threads the current
552 * state, the visited-state table, the two rate tables and the trace through
553 * every step, and the reference threads the same through one long function.
554 */
555template <class T>
557public:
558 /**
559 * `fjsync` is the fork firing list of the TAG-AUGMENTED struct, empty for a
560 * model with no Fork. It is passed in rather than derived because the
561 * augmentation produces the struct and the firing list together and `sn`
562 * must be the augmented one: deriving it here would leave the two able to
563 * disagree about which classes the branches carry.
564 */
566 const std::vector<qn::FjSync<T>>& fjsync = std::vector<qn::FjSync<T>>())
567 : sn_(sn), opt_(opt), rng_(opt.seed), fjsync_(fjsync) {
568 serial_detail::serial_check(sn);
570 copt.cutoff = opt.cutoff;
571 copt.state_max = opt.state_max;
572 const std::vector<std::size_t> cutoff = ctmc::analyzer_detail::resolve_cutoff(sn, copt);
573 sync_ = qn::refresh_sync(sn);
574 gsync_ = qn::refresh_global_sync(sn);
575 sdr_ = sn.has_sdr_routing();
576 // WHICH REGION MACHINERY, decided exactly as SolverCTMC decides it: a
577 // model whose every region class applies DROP is CENSORED, because a
578 // refused job is destroyed and the chain simply never occupies the
579 // forbidden states; one WAITQ class anywhere puts the whole model on the
580 // token-FIFO relation, which handles its DROP classes inline. Choosing
581 // differently from the CTMC here would make the simulator and the exact
582 // solver answer different models under one model file.
584 if (!sn.regions.empty()) {
585 if (waitq_) {
586 caps_ = ctmc::waitq_detail::extract_caps(sn);
587 ctmc::waitq_detail::resolve_lmax(sn, cutoff, caps_);
588 } // the DROP-only rule set is checked by `serial_check` above
589 }
590 init_.net = serial_detail::wide_init_state(sn, cutoff);
591 init_.buf.assign(caps_.size(), std::vector<std::size_t>());
592 if (!sn.regions.empty() && !region_admissible(init_.net))
593 throw InputError(
594 "SolverSSA(method='serial'): the model's initial state violates a finite capacity "
595 "region; the region cannot hold the model's initial population, so no sample path "
596 "can start");
597 }
598
599 /** Run `opt.samples` firings and return the path with its statistics. */
601
602 /** The synchronization list the trace's `tran_sync` indexes. */
603 const std::vector<qn::Sync<T>>& sync() const { return sync_; }
604 /** The state the path starts from, at full encoding width. */
605 const qn::NetState<T>& init_state() const { return init_.net; }
606
607private:
608 /** One enabled transition: where it goes and what it contributes. */
609 struct Move {
610 std::size_t sync = 0; ///< index into `sync_`, or `sync_.size() + g`
611 double weight = 0.0; ///< rate * p_active * p_route * p_passive
613 };
614
615 const qn::NetworkStruct<T>& sn_;
616 SsaSerialOptions opt_;
617 SsaRng rng_;
618 std::vector<qn::Sync<T>> sync_;
619 std::vector<qn::GlobalSync<T>> gsync_;
620 std::vector<qn::FjSync<T>> fjsync_;
621 std::vector<ctmc::waitq_detail::RegionCaps<T>> caps_;
622 bool waitq_ = false;
623 /** The routing must be re-evaluated at every state visited (Krzesinski SDR). */
624 bool sdr_ = false;
626
627 /** `ctmc_region_admissible` on one network state, the DROP censoring test. */
628 bool region_admissible(const qn::NetState<T>& st) const {
629 if (sn_.regions.empty()) return true;
630 std::vector<qn::NetState<T>> one(1, st);
631 const Matrix<T> A = ctmc::ctmc_state_space_aggr(sn_, one);
632 std::vector<T> nir(A.cols());
633 for (std::size_t c = 0; c < A.cols(); ++c) nir[c] = A(0, c);
634 return ctmc::ctmc_region_admissible(sn_, nir);
635 }
636
637 /**
638 * The reference's inlined `solver_ssa_findenabled`: every synchronization
639 * that can fire in `st`, with the arrival and departure rates it carries.
640 */
641 void enabled(const ctmc::WaitqState<T>& st, std::vector<Move>& moves,
642 std::vector<std::vector<double>>& arv, std::vector<std::vector<double>>& dep,
643 std::vector<std::vector<double>>& dly,
644 std::vector<std::vector<double>>& start,
645 std::vector<std::vector<double>>& preempt) const;
646
647 /**
648 * `phi(n)` on the CURRENT sample-path state, as an (nstations*nclasses)
649 * row-major vector of scalings. The twin of `ctmc_gd_factor`'s single row.
650 */
651 std::vector<T> gd_factor_now(const qn::NetState<T>& ns) const {
652 const std::size_t M = sn_.stations.size(), K = sn_.nclasses;
653 const T zero = num_traits<T>::from_int(0);
654 std::vector<T> npop(M * K, zero);
655 for (std::size_t ist = 1; ist <= M; ++ist) {
656 const std::size_t isf = sn_.stateful_of_station(ist);
657 if (isf == 0) continue;
658 if (sn_.stations[ist - 1].nodetype == lang::NodeType::Source) continue;
659 const std::size_t ind = sn_.node_of_station(ist);
660 std::vector<std::size_t> ph(K, 1), shift(K, 0);
661 std::size_t w = 0;
662 for (std::size_t k = 0; k < K; ++k) {
663 ph[k] = sn_.phasessz_of(ist, k + 1);
664 shift[k] = w;
665 w += ph[k];
666 }
667 const qn::Marginal<T> m =
668 qn::to_marginal(sn_, ist, ns.local[isf - 1], ph, shift, sn_.nvars_of(ind));
669 for (std::size_t k = 0; k < K; ++k) npop[(ist - 1) * K + k] = m.nir[k];
670 }
671 const std::vector<T> v = sn_.gdscaling(npop);
672 std::vector<T> out(M * K, num_traits<T>::from_int(1));
673 for (std::size_t i = 0; i < M; ++i)
674 for (std::size_t r = 0; r < K; ++r) {
675 const T f = v.size() == 1 ? v[0] : (v.size() == M ? v[i] : v[i * K + r]);
676 if (!(num_traits<T>::to_double(f) >= 0))
677 throw InputError(
678 "the global dependence handle returned a non-finite or negative scaling");
679 out[i * K + r] = f;
680 }
681 return out;
682 }
683};
684
685template <class T>
686void SsaSerialEngine<T>::enabled(const ctmc::WaitqState<T>& st, std::vector<Move>& moves,
687 std::vector<std::vector<double>>& arv,
688 std::vector<std::vector<double>>& dep,
689 std::vector<std::vector<double>>& dly,
690 std::vector<std::vector<double>>& start,
691 std::vector<std::vector<double>>& preempt) const {
692 const std::size_t local = sn_.nodes.size() + 1; // the dummy passive node
693 const std::size_t R = sn_.nclasses;
694 moves.clear();
695 for (std::size_t f = 0; f < arv.size(); ++f)
696 for (std::size_t r = 0; r < R; ++r) {
697 arv[f][r] = 0.0;
698 dep[f][r] = 0.0;
699 dly[f][r] = 0.0;
700 start[f][r] = 0.0;
701 preempt[f][r] = 0.0;
702 }
703
704 // A WAITQ REGION REPLACES THE WHOLE ENUMERATION rather than filtering it.
705 // The token FIFO is state the network encoding cannot hold, a refused job
706 // parks instead of being lost, and every firing runs a release cascade to a
707 // fixed point, so there is no per-move filter that turns the ordinary
708 // relation into this one. `waitq_successors` IS that relation, shared with
709 // the CTMC generator. Its own support gate has already refused an SPN and a
710 // fork-join model beside a WAITQ region, so no global or fork firing can
711 // reach here on this branch.
712 if (waitq_) {
713 std::vector<ctmc::waitq_detail::Successor<T>> succ;
714 ctmc::waitq_detail::waitq_successors(sn_, sync_, caps_, st, succ);
715 for (std::size_t i = 0; i < succ.size(); ++i) {
716 const ctmc::waitq_detail::Successor<T>& su = succ[i];
717 const double w = num_traits<T>::to_double(su.w);
718 if (!(w > 0)) continue;
719 Move m;
720 m.sync = su.sync;
721 m.weight = w;
722 m.next = su.next;
723 moves.push_back(m);
724 if (su.dep_isf != 0) dep[su.dep_isf - 1][su.dep_cls - 1] += w;
725 for (std::size_t q = 0; q < su.arv.size(); ++q)
726 arv[su.arv[q].first - 1][su.arv[q].second - 1] += w;
727 }
728 return;
729 }
730
731 const qn::NetState<T>& base = st.net;
732 // Global (Whittle) rate scaling declared through `set_global_dependence`. It
733 // reads the FULL population matrix, so it is a CONSTANT within one state and
734 // factors out of the per-transition rates, exactly as in `solver_ctmc`. The
735 // CTMC tabulates it once per state of the enumerated space; a simulator has
736 // one state at a time, so the table collapses to this single row.
737 std::vector<T> gd_now;
738 const bool has_gd = static_cast<bool>(sn_.gdscaling);
739 if (has_gd) gd_now = gd_factor_now(base);
740 // The state-dependent routing table, for the same reason and at the same
741 // scope: one evaluation of eq. (10) per state, not one per synchronization.
742 // The CTMC tabulates it over the enumerated space; a sample path holds one
743 // state at a time, so the table collapses to this single matrix.
744 Matrix<T> rt_now;
745 if (sdr_) rt_now = qn::rt_state(sn_, base.local);
746 for (std::size_t a = 0; a < sync_.size(); ++a) {
747 const qn::Sync<T>& sy = sync_[a];
748 const std::size_t isf_a = sn_.stateful_index(sy.active.node);
749 if (isf_a == 0) continue; // a stateless node schedules nothing
750 const std::size_t isf_p =
751 sy.passive.node == local ? 0 : sn_.stateful_index(sy.passive.node);
752 if (sy.passive.node != local && isf_p == 0) continue;
753
754 const qn::EventOutcome<T> oa = qn::after_event(sn_, sy.active.node, base.local[isf_a - 1],
755 sy.active.event, sy.active.cls);
756 // PHASE is scaled too, or phase-type service would advance unscaled
757 const bool gd_here = has_gd && sn_.nodes[sy.active.node - 1].station != 0 &&
758 (sy.active.event == lang::EventType::DEP ||
759 sy.active.event == lang::EventType::PHASE);
760 const double gd_f =
761 gd_here ? num_traits<T>::to_double(
762 gd_now[(sn_.nodes[sy.active.node - 1].station - 1) * R +
763 (sy.active.cls - 1)])
764 : 1.0;
765 // A cache READ whose successor holds one job FEWER is the delayed-hit
766 // merge: the request joined an in-flight fetch and is held in block B, so
767 // it leaves no departure to count and is released later in the hit class.
768 // Every other cache READ keeps the server block flat.
769 const bool cache_read = sy.active.event == lang::EventType::READ &&
770 sn_.nodes[sy.active.node - 1].nodetype == lang::NodeType::Cache;
771 double srv_pre = 0.0;
772 if (cache_read)
773 for (std::size_t r = 0; r < R && r < base.local[isf_a - 1].size(); ++r)
774 srv_pre += num_traits<T>::to_double(base.local[isf_a - 1][r]);
775
776 double fired = 0.0; // what this synchronization contributes from here
777 for (std::size_t ia = 0; ia < oa.space.size(); ++ia) {
778 const double rate = num_traits<T>::to_double(oa.rate[ia]) * gd_f;
779 const double pa = num_traits<T>::to_double(oa.prob[ia]);
780 if (!(rate > 0) || !(pa > 0)) continue;
781 bool merged = false;
782 if (cache_read) {
783 double srv_post = 0.0;
784 for (std::size_t r = 0; r < R && r < oa.space[ia].size(); ++r)
785 srv_post += num_traits<T>::to_double(oa.space[ia][r]);
786 merged = srv_post - srv_pre == -1.0;
787 }
788
789 if (sy.passive.node == local) {
790 Move m;
791 m.sync = a;
792 m.weight = rate * pa;
793 m.next = st;
794 m.next.net.local[isf_a - 1] = oa.space[ia];
795 // A DROP region CENSORS the chain: a transition into a state the
796 // region forbids is not taken at all, which is what deleting the
797 // state from the CTMC's space and re-closing its rows amounts
798 // to. The move is dropped whole, so it contributes neither a
799 // departure nor an arrival, exactly as the deleted column does.
800 if (!region_admissible(m.next.net)) continue;
801 fired += m.weight;
802 if (merged) dly[isf_a - 1][sy.active.cls - 1] += m.weight;
803 // The START/PREEMPT tags of this arc, weighted like the rate it
804 // carries: they annotate the transition itself. Written for
805 // EVERY action, not only for departures -- a retrial or a
806 // polling switchover starts service without being a DEP.
807 ssa_detail::add_tag_rates(start, preempt, sn_, sy.active.node, oa, ia, m.weight);
808 moves.push_back(m);
809 continue;
810 }
811 // A self-loop synchronization reads the passive node's state AFTER
812 // the active half has been applied, since they are the same node.
813 const std::vector<T>& src =
814 sy.passive.node == sy.active.node ? oa.space[ia] : base.local[isf_p - 1];
815 const qn::EventOutcome<T> op = qn::after_event(sn_, sy.passive.node, src,
816 sy.passive.event, sy.passive.cls);
817 // NO ROWS is a BLOCK, not a loss: the destination has no room and
818 // cannot take the job, so the upstream departure is disabled and the
819 // synchronization simply does not appear in the enabled list. This
820 // is the reference's `prob_sync_p = 0`.
821 // The routing probability, read at the state the job LEAVES from,
822 // which is what `sub_sdr` reads.
823 double proute =
824 sy.passive.statedep
825 ? num_traits<T>::to_double(rt_now(sy.passive.rt_row, sy.passive.rt_col))
826 : num_traits<T>::to_double(sy.passive.prob);
827 // ROUND-ROBIN reads the pointer the ACTIVE node carries once its own
828 // departure has advanced it, exactly as the CTMC generator does; the
829 // uniform expansion in `sy.passive.prob` would make the dispatcher a
830 // coin. The pointer lives in the state, so the serial engine needs
831 // no cursor of its own -- unlike the NRM, which walks the arcs.
832 if (sy.active.event == lang::EventType::DEP &&
833 sn_.rr_var_slot(sy.active.node, sy.active.cls) != 0) {
834 const std::size_t w = sn_.nvars_of(sy.active.node);
835 const std::vector<T>& arow = oa.space[ia];
836 std::size_t dest = 0;
837 if (arow.size() >= w) {
838 const std::vector<T> var(arow.end() - w, arow.end());
839 dest = sn_.rr_dest(sy.active.node, sy.active.cls, var);
840 }
841 proute = (dest == sy.passive.node && sy.passive.cls == sy.active.cls) ? 1.0 : 0.0;
842 }
843 for (std::size_t ip = 0; ip < op.space.size(); ++ip) {
844 const double pp = num_traits<T>::to_double(op.prob[ip]);
845 if (!(pp > 0)) continue;
846 Move m;
847 m.sync = a;
848 m.weight = rate * pa * proute * pp;
849 if (!(m.weight > 0)) continue;
850 m.next = st;
851 m.next.net.local[isf_a - 1] = oa.space[ia];
852 m.next.net.local[isf_p - 1] = op.space[ip];
853 if (!region_admissible(m.next.net)) continue;
854 fired += m.weight;
855 if (merged) dly[isf_a - 1][sy.active.cls - 1] += m.weight;
856 // both halves are tagged: the arrival half is where most
857 // service starts happen
858 ssa_detail::add_tag_rates(start, preempt, sn_, sy.active.node, oa, ia, m.weight);
859 ssa_detail::add_tag_rates(start, preempt, sn_, sy.passive.node, op, ip, m.weight);
860 moves.push_back(m);
861 }
862 }
863 // A DEP synchronization is one job LEAVING the active node and ENTERING
864 // the passive one, so the same rate is a departure there and an arrival
865 // here. The passive half of a LOCAL action is the dummy node, which is
866 // nobody's arrival.
867 if (sy.active.event == lang::EventType::DEP && fired > 0) {
868 dep[isf_a - 1][sy.active.cls - 1] += fired;
869 if (isf_p != 0) arv[isf_p - 1][sy.passive.cls - 1] += fired;
870 }
871 }
872
873 for (std::size_t g = 0; g < gsync_.size(); ++g) {
874 const qn::GlobalOutcome<T> go = qn::after_global_event(sn_, base, gsync_[g]);
875 for (std::size_t io = 0; io < go.space.size(); ++io) {
876 const double w = num_traits<T>::to_double(go.rate[io]) *
877 num_traits<T>::to_double(go.prob[io]);
878 if (!(w > 0)) continue;
879 Move m;
880 m.sync = sync_.size() + g;
881 m.weight = w;
882 m.next = st;
883 m.next.net = go.space[io];
884 if (!region_admissible(m.next.net)) continue;
885 moves.push_back(m);
886 // A FIRE consumes from its PRE places and produces to its POST
887 // places, which is a departure and an arrival respectively; an
888 // ENABLE only reads the markings and moves nothing.
889 if (gsync_[g].active.event != lang::EventType::FIRE) continue;
890 for (std::size_t j = 0; j < gsync_[g].passive.size(); ++j) {
891 const qn::ModeEvent<T>& pev = gsync_[g].passive[j];
892 const std::size_t pisf = sn_.stateful_index(pev.node);
893 if (pisf == 0 || pev.cls == 0 || pev.cls > R) continue;
894 if (pev.event == lang::EventType::PRE) dep[pisf - 1][pev.cls - 1] += w;
895 else if (pev.event == lang::EventType::POST) arv[pisf - 1][pev.cls - 1] += w;
896 }
897 }
898 }
899
900 // FORK FIRINGS, atomic across the fork and every branch head, so like an SPN
901 // firing they take the whole network state and cannot be decomposed into
902 // sync halves. `refresh_sync` emits no DEP for a Fork, which is why nothing
903 // above has already counted them.
904 //
905 // ONE DEPARTURE, B ARRIVALS. The parent leaves the fork in its own class and
906 // one sibling enters each branch head in the tag's auxiliary class, so the
907 // rate statistics are accumulated by hand exactly as `solver_ctmc` does:
908 // an ordinary synchronization has no way to express a one-to-many emission.
909 for (std::size_t k = 0; k < fjsync_.size(); ++k) {
910 const qn::FjSync<T>& e = fjsync_[k];
911 const std::size_t isf_f = sn_.stateful_index(e.fork);
912 if (isf_f == 0) continue;
913 const qn::GlobalOutcome<T> fo = qn::after_fj_event(sn_, e, base);
914 double fired = 0.0;
915 for (std::size_t io = 0; io < fo.space.size(); ++io) {
916 const double w = num_traits<T>::to_double(fo.rate[io]) *
917 num_traits<T>::to_double(fo.prob[io]);
918 if (!(w > 0)) continue;
919 Move m;
920 m.sync = sync_.size() + gsync_.size() + k;
921 m.weight = w;
922 m.next = st;
923 m.next.net = fo.space[io];
924 if (!region_admissible(m.next.net)) continue;
925 fired += w;
926 moves.push_back(m);
927 }
928 if (!(fired > 0)) continue;
929 dep[isf_f - 1][e.cls - 1] += fired;
930 for (std::size_t b = 0; b < e.branchheads.size(); ++b) {
931 const std::size_t isf_b = sn_.stateful_index(e.branchheads[b]);
932 if (isf_b != 0) arv[isf_b - 1][e.auxclasses[b] - 1] += fired;
933 }
934 }
935}
936
937template <class T>
939 // The exponential holding time is drawn as -log(u)/lambda, so the backend
940 // must have a logarithm at all. The analyzer gates on `double` before it
941 // instantiates this, so a caller reaching the assert is one that reached
942 // past the gate and would otherwise get a compile error deep inside the
943 // uniform draw instead of a sentence naming the reason.
945 "solver_ssa_serial: an SSA sample path is generated from exponential clocks "
946 "drawn as -log(u)/rate, which needs transcendental arithmetic");
947
948 const std::size_t NF = sn_.stateful_nodes.size();
949 const std::size_t R = sn_.nclasses;
950 SsaSerialRun<T> out;
951 out.seed = opt_.seed;
952 out.warmup = static_cast<std::size_t>(
953 std::floor(std::max(0.0, std::min(0.99, opt_.warmupfrac)) *
954 static_cast<double>(opt_.samples)));
955
956 ctmc::WaitqState<T> cur = init_;
957 std::vector<Move> moves;
958 std::vector<std::vector<double>> arv(NF, std::vector<double>(R, 0.0));
959 std::vector<std::vector<double>> dep(NF, std::vector<double>(R, 0.0));
960 std::vector<std::vector<double>> dly(NF, std::vector<double>(R, 0.0));
961 // Derived START/PREEMPT rates, sampled exactly like the three above: the
962 // rate at which the transitions enabled in the current state start a
963 // class-r service, or push a class-r job in service back into the buffer.
964 std::vector<std::vector<double>> start(NF, std::vector<double>(R, 0.0));
965 std::vector<std::vector<double>> preempt(NF, std::vector<double>(R, 0.0));
966 std::vector<double> weights;
967 std::map<std::vector<double>, std::size_t> index; // state key -> row of `space`
968
969 double cur_time = 0.0;
970 out.tran_time.reserve(opt_.samples);
971 out.tran_sync.reserve(opt_.samples);
972 for (std::size_t n = 0; n < opt_.samples; ++n) {
973 enabled(cur, moves, arv, dep, dly, start, preempt);
974 if (moves.empty())
975 throw NumericError(
976 "solver_ssa_serial: the sample path entered a deadlock before collecting all "
977 "samples, no synchronization is enabled");
978
979 weights.resize(moves.size());
980 double tot = 0.0;
981 for (std::size_t i = 0; i < moves.size(); ++i) {
982 weights[i] = moves[i].weight;
983 tot += weights[i];
984 }
985 const std::size_t sel = rng_.draw(weights);
986 // The transition is drawn BEFORE the holding time, as the reference
987 // draws them: the two are independent, so the order changes only which
988 // stream this engine is, and being a nameable stream is the point.
989 const double dt = -std::log(rng_.uniform()) / tot;
990
991 // The state is recorded with the time spent IN it, so the pair belongs
992 // to the state before the firing, not after.
993 //
994 // THE KEY IS THE AUGMENTED ONE and the stored row is the network half.
995 // Two states that agree on every node but differ in a region FIFO are
996 // DIFFERENT states -- their enabled sets differ -- so merging them would
997 // put two rate rows on one entry and report whichever was seen first.
998 // `space` therefore may hold the same network row twice, once per FIFO
999 // content, which every consumer here handles: each row carries its own
1000 // `pi` and the metrics are sums over rows, never lookups by row.
1001 const std::vector<double> key = ctmc::waitq_detail::waitq_key(cur);
1002 std::size_t si;
1003 const typename std::map<std::vector<double>, std::size_t>::const_iterator it =
1004 index.find(key);
1005 if (it != index.end()) {
1006 si = it->second;
1007 } else {
1008 si = out.space.size();
1009 index[key] = si;
1010 out.space.push_back(cur.net);
1011 out.buf.push_back(cur.buf);
1012 out.pi.push_back(0.0);
1013 out.arv_rates.push_back(arv);
1014 out.dep_rates.push_back(dep);
1015 out.dly_rates.push_back(dly);
1016 out.start_rates.push_back(start);
1017 out.preempt_rates.push_back(preempt);
1018 }
1019 // The warmup discard drops the transient from the TIME AVERAGE only: the
1020 // states themselves stay in the table, so a state visited only during
1021 // the transient keeps its (exact) rate row and contributes zero weight.
1022 if (n >= out.warmup) {
1023 out.pi[si] += dt;
1024 out.simulated_time += dt;
1025 }
1026 cur_time += dt;
1027 out.tran_time.push_back(cur_time);
1028 out.tran_sync.push_back(moves[sel].sync);
1029 out.tran_state.push_back(si);
1030
1031 cur = moves[sel].next;
1032 out.samples = n + 1;
1033 }
1034
1035 double tot_pi = 0.0;
1036 for (std::size_t s = 0; s < out.pi.size(); ++s) tot_pi += out.pi[s];
1037 if (tot_pi > 0)
1038 for (std::size_t s = 0; s < out.pi.size(); ++s) out.pi[s] /= tot_pi;
1039 out.ssq = ctmc::ctmc_state_space_aggr(sn_, out.space);
1040 return out;
1041}
1042
1043namespace serial_detail {
1044
1045/** `map_mean(PH{ist}{k})`, or a negative sentinel when the pair has no process. */
1046template <class T>
1047double service_mean(const qn::NetworkStruct<T>& sn, std::size_t ist, std::size_t k) {
1048 const lang::Distrib<T>& d = sn.service[ist - 1][k - 1];
1049 if (d.disabled || d.D0.rows() == 0) return -1.0;
1050 mam::Map<T> m;
1051 m.D0 = d.D0;
1052 m.D1 = d.D1;
1053 try {
1055 } catch (const Error&) {
1056 return -1.0; // a zero-rate process has no mean; the reference skips it too
1057 }
1058}
1059
1060} // namespace serial_detail
1061
1062/**
1063 * Port of `solver_ssa_analyzer_serial.m`: run the serial engine and reduce its
1064 * path to the metric table.
1065 *
1066 * THE UTILIZATION ESTIMATOR IS THE REFERENCE'S, discipline by discipline. An
1067 * INF station is utilized by every job it holds; a PS-family station takes the
1068 * ARRIVAL rate over rate*servers, because the offered load is what a processor
1069 * sharing server carries; every other discipline takes the arrival rate times
1070 * the mean service time over the servers. A class that can be DROPPED -- an open
1071 * class at a station with a finite capacity -- is measured on the CARRIED rate
1072 * instead, because the offered rate counts arrivals that never entered service.
1073 *
1074 * ONE DIVERGENCE FROM THE REFERENCE, stated rather than hidden:
1075 *
1076 * THE CACHE LOOP IS INDEXED BY NODE, NOT BY STATEFUL INDEX. The reference
1077 * writes `sn.nodetype(isf) == NodeType.Cache` with `isf` running over the
1078 * STATEFUL nodes, so on any model whose stateful indices differ from its node
1079 * indices -- one with a Source, a Router or a ClassSwitch, which is most of
1080 * them -- it tests the type of the wrong node. Reproducing that would report
1081 * hit ratios for a node that is not the cache.
1082 *
1083 * A PAS STATION takes the reference's `otherwise` branch, T*E[S]/c, and NOT the
1084 * in-service occupancy `solver_ctmc_avg_from_pi` computes for the same station.
1085 * The two disagree because a pass-and-swap job does not engage a single server;
1086 * the reference serial analyzer is what is ported here, and the disagreement is
1087 * named so it is not mistaken for a defect in either.
1088 */
1089template <class T>
1091 const SsaSerialOptions& opt,
1092 const std::vector<qn::FjSync<T>>& fjsync) {
1093 // `if constexpr`, not a run-time test: the engine reaches `map_mean` and the
1094 // logarithm of a uniform, so a Rational instantiation would fail to COMPILE
1095 // rather than refuse. The gate has to keep the body from being instantiated.
1096 if constexpr (!std::is_same<T, double>::value) {
1097 (void)sn;
1098 (void)opt;
1099 throw UnsupportedError(
1100 "solver_ssa_serial: an SSA sample path is generated from exponential clocks, which "
1101 "are logarithms of uniform draws; there is no exact value to compute and a wider "
1102 "float carries no information the Monte Carlo error does not swamp. Rerun with "
1103 "--arith double");
1104 } else {
1105 using lang::SchedStrategy;
1106 const std::size_t M = sn.nstations, K = sn.nclasses;
1108 out.seed = opt.seed;
1109
1110 SsaSerialEngine<T> eng(sn, opt, fjsync);
1111 out.run = eng.run();
1112 const SsaSerialRun<T>& r = out.run;
1113
1114 // The parked population, per class: a token carries the class its job
1115 // will enter in, so the mean is the time average of the FIFO contents.
1116 out.parked.assign(K, 0.0);
1117 for (std::size_t s = 0; s < r.buf.size() && s < r.pi.size(); ++s)
1118 for (std::size_t f = 0; f < r.buf[s].size(); ++f)
1119 for (std::size_t j = 0; j < r.buf[s][f].size(); ++j) {
1120 const std::size_t cls = (r.buf[s][f][j] - 1) % K + 1;
1121 out.parked[cls - 1] += r.pi[s];
1122 }
1123
1124 SsaSolution& a = out.avg;
1125 a.method = "serial";
1126 a.samples = r.samples;
1128 a.QN = Matrix<double>(M, K, 0.0);
1129 a.UN = Matrix<double>(M, K, 0.0);
1130 a.RN = Matrix<double>(M, K, 0.0);
1131 a.TN = Matrix<double>(M, K, 0.0);
1132 a.XN.assign(K, 0.0);
1133 a.CN.assign(K, 0.0);
1134 a.StartN = Matrix<double>(M, K, 0.0);
1135 a.PreemptN = Matrix<double>(M, K, 0.0);
1136
1137 // System throughput is the DEPARTURE rate at each class's reference
1138 // station, which is what makes X a per-class quantity rather than a sum.
1139 for (std::size_t k = 1; k <= K; ++k) {
1140 const std::size_t refsf = sn.stateful_of_station(sn.classes[k - 1].refstat);
1141 if (refsf == 0) continue;
1142 for (std::size_t s = 0; s < r.space.size(); ++s)
1143 a.XN[k - 1] += r.pi[s] * r.dep_rates[s][refsf - 1][k - 1];
1144 }
1145
1146 // The reference's `isempty(sn.lldscaling) && isempty(sn.cdscaling)` test
1147 // is over the WHOLE matrix, so one load-dependent station puts every
1148 // station on the scaling branch. That is kept: at a station with no
1149 // scaling the branch degenerates to T*E[S]/c, which differs from the
1150 // unscaled branch only in using the carried rather than the offered
1151 // rate, and reproducing the reference's table means reproducing that.
1152 bool scaled = static_cast<bool>(sn.gdscaling);
1153 for (std::size_t i = 0; i < M; ++i)
1154 if (!sn.stations[i].lldscaling.empty() || sn.stations[i].cdscaling) scaled = true;
1155
1156 for (std::size_t ist = 1; ist <= M; ++ist) {
1157 const std::size_t isf = sn.stateful_of_station(ist);
1158 if (isf == 0) continue;
1159 const SchedStrategy sched = sn.stations[ist - 1].sched;
1160 const double S = sn.stations[ist - 1].nservers;
1161 for (std::size_t k = 1; k <= K; ++k) {
1162 for (std::size_t s = 0; s < r.space.size(); ++s) {
1163 a.TN(ist - 1, k - 1) += r.pi[s] * r.dep_rates[s][isf - 1][k - 1];
1164 a.QN(ist - 1, k - 1) +=
1165 r.pi[s] * num_traits<T>::to_double(r.ssq(s, (ist - 1) * K + k - 1));
1166 // same time average as TN, over the derived tag rates
1167 if (s < r.start_rates.size()) {
1168 a.StartN(ist - 1, k - 1) += r.pi[s] * r.start_rates[s][isf - 1][k - 1];
1169 a.PreemptN(ist - 1, k - 1) += r.pi[s] * r.preempt_rates[s][isf - 1][k - 1];
1170 }
1171 }
1172 }
1173
1174 const bool is_ps = sched == SchedStrategy::PS || sched == SchedStrategy::DPS ||
1175 sched == SchedStrategy::GPS || sched == SchedStrategy::LPS;
1176 // A SOURCE holds no jobs, so QLen, Util and hence RespT are zero
1177 // there BY DEFINITION and only its throughput is a quantity -- the
1178 // rule `solver_ssa_nrm.h` states at length and `solver_ctmc_avg_from_pi`
1179 // applies by the same `continue`. The reference serial analyzer has
1180 // no such branch and lets a Source fall into `otherwise`, where it
1181 // divides the arrival rate by a server count `solver_ssa.m` has
1182 // meanwhile overwritten with the station's capacity; that product is
1183 // not a utilization of anything.
1184 if (sn.stations[ist - 1].nodetype == lang::NodeType::Source ||
1185 sched == SchedStrategy::EXT)
1186 continue;
1187 if (sched == SchedStrategy::INF) {
1188 for (std::size_t k = 1; k <= K; ++k) a.UN(ist - 1, k - 1) = a.QN(ist - 1, k - 1);
1189 continue;
1190 }
1191 if (scaled) {
1192 // The EFFECTIVE server count a load-dependent station can
1193 // deliver: `max(c, max_n lld(n))`. A class-dependent station
1194 // normalizes by its DECLARED peak instead, which is the only
1195 // thing the utilization can be a fraction of.
1196 double ceff = S;
1197 const std::vector<T>& lld = sn.stations[ist - 1].lldscaling;
1198 for (std::size_t j = 0; j < lld.size(); ++j)
1199 ceff = std::max(ceff, num_traits<T>::to_double(lld[j]));
1200 const bool is_cd = static_cast<bool>(sn.stations[ist - 1].cdscaling);
1201 const bool is_jd = static_cast<bool>(sn.stations[ist - 1].jdscaling);
1202 // A global (Whittle) dependence rescales the service rate the
1203 // same way, so the peak IT declares normalizes Util too.
1204 const bool is_gd = static_cast<bool>(sn.gdscaling);
1205 std::vector<T> gdpk;
1206 if (is_gd)
1207 gdpk.assign(sn.gdscalingpeak.begin() + (ist - 1) * K,
1208 sn.gdscalingpeak.begin() + ist * K);
1209 for (std::size_t k = 1; k <= K; ++k) {
1210 const double mean = serial_detail::service_mean(sn, ist, k);
1211 if (mean < 0) continue;
1212 // The divisor is the PRODUCT of the declared peaks when either
1213 // dependence is present, and the effective server count only
1214 // otherwise: a station carrying both scales its rate by both,
1215 // so normalizing by one of them alone leaves the other's factor
1216 // in the reported utilization.
1217 double cdiv = ceff;
1218 if (is_cd || is_jd || is_gd) {
1219 cdiv = 1.0;
1220 const std::vector<T>* pks[3] = {&sn.stations[ist - 1].cdscalingpeak,
1221 &sn.stations[ist - 1].jdscalingpeak,
1222 &gdpk};
1223 const char* names[3] = {"setClassDependence", "setJointDependence",
1224 "setGlobalDependence"};
1225 const bool on[3] = {is_cd, is_jd, is_gd};
1226 for (std::size_t h = 0; h < 3; ++h) {
1227 if (!on[h]) continue;
1228 const std::vector<T>& pk = *pks[h];
1229 if (pk.size() < k || !(num_traits<T>::to_double(pk[k - 1]) > 0))
1230 throw InputError(
1231 "SolverSSA(method='serial'): station '" +
1232 sn.stations[ist - 1].name +
1233 "' declares a dependent scaling with no declared peak rate. "
1234 "Utilization there is T*E[S]/peak, so pass the peak to " +
1235 names[h]);
1236 cdiv *= num_traits<T>::to_double(pk[k - 1]);
1237 }
1238 }
1239 a.UN(ist - 1, k - 1) = cdiv > 0 ? a.TN(ist - 1, k - 1) * mean / cdiv : 0.0;
1240 }
1241 continue;
1242 }
1243
1244 // A class whose jobs can be lost here is measured on the carried
1245 // rate; everything else on the offered rate, which is exact in
1246 // steady state and is what the reference reports.
1247 for (std::size_t k = 1; k <= K; ++k) {
1248 const double mean = serial_detail::service_mean(sn, ist, k);
1249 if (mean < 0) continue;
1250 const bool can_drop = !std::isfinite(sn.njobs()[k - 1]) &&
1251 (std::isfinite(sn.cap[ist - 1]) ||
1252 std::isfinite(sn.classcap[ist - 1][k - 1]));
1253 double arv = 0.0;
1254 if (!can_drop)
1255 for (std::size_t s = 0; s < r.space.size(); ++s)
1256 arv += r.pi[s] * r.arv_rates[s][isf - 1][k - 1];
1257 if (is_ps) {
1258 const double mu = num_traits<T>::to_double(sn.rates(ist - 1, k - 1));
1259 if (!(mu > 0)) continue;
1260 a.UN(ist - 1, k - 1) =
1261 (can_drop ? a.TN(ist - 1, k - 1) / mu : arv / mu) / S;
1262 } else {
1263 a.UN(ist - 1, k - 1) =
1264 (can_drop ? a.TN(ist - 1, k - 1) * mean : arv * mean) / S;
1265 }
1266 }
1267 }
1268
1269 // Little's law per station, then the per-class system response time.
1270 for (std::size_t k = 1; k <= K; ++k) {
1271 for (std::size_t ist = 1; ist <= M; ++ist)
1272 a.RN(ist - 1, k - 1) = a.TN(ist - 1, k - 1) > 0
1273 ? a.QN(ist - 1, k - 1) / a.TN(ist - 1, k - 1)
1274 : 0.0;
1275 // The reference's `CN(k) = NK(k)/XN(k)` with NK the class population:
1276 // infinite for an open class, which its NaN sweep does NOT clear and
1277 // which the NRM engine reports identically.
1278 if (a.XN[k - 1] > 0) a.CN[k - 1] = sn.classes[k - 1].population / a.XN[k - 1];
1279 }
1280
1281 // The cache write-back: every read leaves as exactly one of hit or miss,
1282 // so the two departure streams divide the read rate between them and
1283 // their ratio is the realized hit probability. The reference stores it
1284 // into `sn.nodeparam{ind}.actualhitprob`; the struct is const here, so it
1285 // is returned beside the table instead.
1286 const double nan = std::numeric_limits<double>::quiet_NaN();
1287 for (typename std::map<std::size_t, qn::CacheParam<T>>::const_iterator ci =
1288 sn.nodeparam.begin();
1289 ci != sn.nodeparam.end(); ++ci) {
1290 const std::size_t ind = ci->first;
1291 if (ind == 0 || ind > sn.nodes.size()) continue;
1292 if (sn.nodes[ind - 1].nodetype != lang::NodeType::Cache) continue;
1293 const std::size_t isf = sn.stateful_index(ind);
1294 if (isf == 0) continue;
1295 SsaCacheRatio cr;
1296 cr.node = ind;
1297 cr.hitprob.assign(K, nan);
1298 cr.missprob.assign(K, nan);
1299 cr.residt.assign(K, nan);
1300 std::vector<double> dly(K, 0.0);
1301 bool any_delayed = false;
1302 for (std::size_t k = 1; k <= K; ++k) {
1303 if (ci->second.hitclass.size() < k || ci->second.missclass.size() < k) continue;
1304 const std::size_t h = ci->second.hitclass[k - 1];
1305 const std::size_t mi = ci->second.missclass[k - 1];
1306 if (h == 0 || mi == 0 || h > K || mi > K) continue;
1307 double th = 0.0, tm = 0.0, td = 0.0;
1308 for (std::size_t s = 0; s < r.space.size(); ++s) {
1309 th += r.pi[s] * r.dep_rates[s][isf - 1][h - 1];
1310 tm += r.pi[s] * r.dep_rates[s][isf - 1][mi - 1];
1311 if (s < r.dly_rates.size()) td += r.pi[s] * r.dly_rates[s][isf - 1][k - 1];
1312 }
1313 if (th + tm > 0) {
1314 // `th` already carries the released delayed hits, so the
1315 // delayed share is CARVED OUT of it rather than added as a
1316 // fourth share.
1317 cr.hitprob[k - 1] = std::max(th - td, 0.0) / (th + tm);
1318 cr.missprob[k - 1] = tm / (th + tm);
1319 dly[k - 1] = td / (th + tm);
1320 if (td > 0) any_delayed = true;
1321 }
1322 }
1323 if (any_delayed) cr.delayedprob = dly;
1324 out.cache.push_back(cr);
1325 }
1326 return out;
1327 }
1328}
1329
1330
1331/**
1332 * Port of `solver_ssa_analyzer_serial.m` plus the fork-join wrapper
1333 * `@@SolverSSA/runAnalyzer.m` puts in front of it.
1334 *
1335 * A FORK-JOIN MODEL IS SIMULATED ON THE TAG-AUGMENTED COPY, exactly as
1336 * SolverCTMC solves it there: the fork emits one sibling per branch in a class
1337 * of its own, the tag is what lets the Join recognize which siblings belong to
1338 * the same parent, and `fj_tag` is the only thing that builds the `fjsync`
1339 * firing list the engine fires. The sample path in the returned run is
1340 * therefore indexed by the AUGMENTED classes; only the metric table is folded
1341 * back, which is why `fjclassmap` travels with it.
1342 */
1343template <class T>
1345 const SsaSerialOptions& opt) {
1346 // The augmentation is skipped entirely on a non-`double` backend so the
1347 // refusal a caller reads is the arithmetic one, from inside the engine,
1348 // rather than a fork-join message about a model whose real problem is that
1349 // an exponential clock has no exact value.
1350 if constexpr (!std::is_same<T, double>::value) {
1351 return solver_ssa_serial_on_struct(sn, opt, std::vector<qn::FjSync<T>>());
1352 } else {
1353 if (!tr::has_fork_join(sn))
1354 return solver_ssa_serial_on_struct(sn, opt, std::vector<qn::FjSync<T>>());
1355 const qn::FjTagged<T> fjt = qn::fj_tag(sn);
1357 tr::fj_foldback(sn, out.avg, fjt.fjclassmap, fjt.korig);
1358 out.fjclassmap = fjt.fjclassmap;
1359 out.parked.resize(fjt.korig);
1360 return out;
1361 }
1362}
1363
1364/**
1365 * The `serial` entry of `solver_ssa_analyzer.m`.
1366 *
1367 * The reference reaches it from `default` (when the NRM eligibility gate fails),
1368 * from `ssa`, from `serial` and from `para`/`parallel` without the Parallel
1369 * Computing Toolbox. `para`/`parallel` is NOT that: it replicates the SAME
1370 * engine across workers and averages, so answering it with one replica would
1371 * report a number at a different variance from the one asked for, and it refuses
1372 * by name here.
1373 */
1374template <class T>
1376 const SsaSerialOptions& opt) {
1377 const std::string& m = opt.method;
1378 if (m == "default" || m == "ssa" || m == "serial") return solver_ssa_serial_analyzer(sn, opt);
1379 if (m == "para" || m == "parallel")
1380 throw UnsupportedError(
1381 "SolverSSA: the '" + m +
1382 "' method runs the serial engine on several workers and averages the replicas "
1383 "(solver_ssa_analyzer_parallel.m). The engine is ported; the replication is not, and "
1384 "one replica has a different variance from the average of many. Use 'serial'");
1385 throw UnsupportedError("SolverSSA(serial): '" + m +
1386 "' is not a method this entry accepts; it implements 'serial' and the "
1387 "'default' and 'ssa' aliases that reach it");
1388}
1389
1390} // namespace ssa
1391} // namespace line
1392
1393#endif // LINE_SOLVERS_SSA_SOLVER_SSA_SERIAL_H
Base error for the multiprecision C++ port.
Definition error.h:31
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
NumericError(const std::string &what)
Definition error.h:45
Requested feature or arithmetic mode is not ported yet.
Definition error.h:49
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t stateful_index(std::size_t ind) const
1-based stateful index of node ind, 0 when the node is not stateful.
std::size_t stateful_of_station(std::size_t st) const
GdScaling< T > gdscaling
sn.gdscaling: the network-level globally state-dependent (Whittle) rate scaling phi(n).
std::size_t rr_var_slot(std::size_t ind, std::size_t r) const
1-BASED index of the pointer of (ind, r) INSIDE the node's local-variable block, or 0 when that pair ...
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::size_t node_of_station(std::size_t st) const
1-based node index of a station, and the reverse; 0 when absent.
std::vector< NodeDef > nodes
every node, in creation order
std::size_t phasessz_of(std::size_t ist, std::size_t r) const
sn.phasessz(i,r) = max(sn.phases(i,r),1): THE WIDTH of class r's phase block in a state row,...
std::vector< Region > regions
The serial engine: the sample path of solver_ssa.m's main loop.
const std::vector< qn::Sync< T > > & sync() const
The synchronization list the trace's tran_sync indexes.
SsaSerialEngine(const qn::NetworkStruct< T > &sn, const SsaSerialOptions &opt, const std::vector< qn::FjSync< T > > &fjsync=std::vector< qn::FjSync< T > >())
fjsync is the fork firing list of the TAG-AUGMENTED struct, empty for a model with no Fork.
const qn::NetState< T > & init_state() const
The state the path starts from, at full encoding width.
SsaSerialRun< T > run()
Run opt.samples firings and return the path with its statistics.
The exception types the port throws.
Port of matlab/src/api/fj/sn_fj_validate.m and matlab/src/io/@@ModelAdapter/fjtag....
Fork-join TAG AUGMENTATION: the fold-back half of the transform/lift pair that CTMC and SSA share.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
bool ctmc_region_admissible(const NetworkStruct< T > &sn, const std::vector< T > &nir)
True when nir – the per-(station, class) counts of one state, in (ist-1)*K + k order – satisfies ever...
std::vector< NetState< T > > ctmc_filter_regions(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space)
The states of space a DROP region admits, in their original order.
void ctmc_check_region_rules(const NetworkStruct< T > &sn)
Refuse the region rules this port does not implement.
Matrix< T > ctmc_state_space_aggr(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space)
Port of StateSpaceAggr: the per-(station, class) job counts of every state, as an (nstates x nstation...
bool ctmc_has_waitq_region(const NetworkStruct< T > &sn)
True when the model declares a region that applies anything other than DROP.
std::vector< NetState< T > > reachable_space_generator(const NetworkStruct< T > &sn, const NetState< T > &init, const std::vector< Sync< T > > &sync, const std::vector< qn::GlobalSync< T > > &gsync=std::vector< qn::GlobalSync< T > >(), std::size_t maxst=3000000, const std::vector< qn::FjSync< T > > &fjsync=std::vector< qn::FjSync< T > >(), const std::vector< std::size_t > &cutoff=std::vector< std::size_t >(), const std::vector< std::vector< std::size_t > > &cutoff_mat=std::vector< std::vector< std::size_t > >())
Port of State.reachableSpaceGenerator: the states reachable from init.
void ctmc_check_waitq_support(const NetworkStruct< T > &sn)
The combinations the reference gates, plus the two this port cannot represent.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
@ PHASE
service advances a phase WITHOUT departing
Definition lang_types.h:116
@ READ
a cache item is read
Definition lang_types.h:117
@ 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
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
Marginal< T > to_marginal(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< T > &state_i, const std::vector< std::size_t > &phasesz, const std::vector< std::size_t > &phaseshift, std::size_t nvar=0)
Port of State.toMarginal for a STATION, one state row at a time.
Definition state.h:130
std::vector< std::vector< T > > from_marginal_node(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases)
Port of State.fromMarginal at its OWN signature: the reference indexes by NODE, not by station,...
Definition state.h:1473
bool from_marginal_node_first(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases, std::vector< T > &out)
The FIRST row from_marginal_node emits, BUILT rather than enumerated.
Definition state.h:2005
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.
std::vector< Sync< T > > refresh_sync(const NetworkStruct< T > &sn, const std::vector< std::vector< bool > > &impatience_classes=std::vector< std::vector< bool > >(), const std::vector< std::size_t > &breakdown_nodes=std::vector< std::size_t >())
Port of MNetwork.refreshSync: the synchronization list.
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< GlobalSync< T > > refresh_global_sync(const NetworkStruct< T > &sn)
Port of MNetwork.refreshGlobalSync: the ENABLE and FIRE synchronizations.
FjTagged< T > fj_tag(const NetworkStruct< T > &sn)
Port of ModelAdapter.fjtag.
Definition fj_tag.h:302
GlobalOutcome< T > after_fj_event(const NetworkStruct< T > &sn, const FjSync< T > &e, const NetState< T > &gl)
Port of State.afterFJEvent: fire ONE entry of the fork firing list.
Matrix< T > rt_state(const NetworkStruct< T > &sn, const std::vector< std::vector< T > > &local)
Port of sn.rtfun: the routing over the stateful nodes AT ONE STATE.
Definition state.h:375
SsaSerialSolution< T > solver_ssa_serial_analyzer(const qn::NetworkStruct< T > &sn, const SsaSerialOptions &opt)
Port of solver_ssa_analyzer_serial.m plus the fork-join wrapper @@SolverSSA/runAnalyzer....
SsaSerialSolution< T > solver_ssa_serial(const qn::NetworkStruct< T > &sn, const SsaSerialOptions &opt)
The serial entry of solver_ssa_analyzer.m.
SsaReachability< T > solver_ssa_reachability(const qn::NetworkStruct< T > &sn, const SsaSerialOptions &opt=SsaSerialOptions())
Port of solver_ssa_reachability.m: the states the DYNAMICS can occupy, decomposed per stateful node.
SsaSerialSolution< T > solver_ssa_serial_on_struct(const qn::NetworkStruct< T > &sn, const SsaSerialOptions &opt, const std::vector< qn::FjSync< T > > &fjsync)
Port of solver_ssa_analyzer_serial.m: run the serial engine and reduce its path to the metric table.
void fj_foldback(const qn::NetworkStruct< T > &sn, Avg &a, const std::vector< std::size_t > &fjclassmap, std::size_t korig)
Reduce the augmented metrics onto the original classes.
bool has_fork_join(const qn::NetworkStruct< T > &sn)
Whether the model needs the tag augmentation at all.
A queueing network and its refreshed NetworkStruct.
Port of solver_ctmc.m: the infinitesimal generator of a queueing network, assembled from the enumerat...
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
Finite Capacity Regions in SolverCTMC: the DROP rule, as a filter on the enumerated state space,...
Port of solver_ctmc_fcr_waitq.m: the reachability-built generator of a model whose finite capacity re...
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...
The SolverCTMC knobs this port honours.
std::size_t state_max
refuse a space larger than this
double cutoff
< 0 = not given
One augmented state: the network state, plus the token FIFO of every region.
std::vector< std::vector< std::size_t > > buf
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
One fork firing synchronization: sn.fjsync{k}.
The augmented struct and everything needed to read its results back.
Definition fj_tag.h:71
std::vector< FjSync< T > > fjsync
Definition fj_tag.h:76
NetworkStruct< T > V
Definition fj_tag.h:72
std::size_t korig
Definition fj_tag.h:78
std::vector< std::size_t > fjclassmap
fjclassmap[a-1] is the ORIGINAL class of auxiliary class a, 0 for originals.
Definition fj_tag.h:74
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2140
What the cache write-back of solver_ssa_analyzer_serial.m produces.
std::size_t node
1-based Cache node index
std::vector< double > delayedprob
The delayed-hit share, EMPTY off a retrieval system.
std::vector< double > missprob
per class, NaN where undefined
std::vector< double > hitprob
std::vector< double > residt
actualresidt: NaN, and NOT a port gap.
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:69
Port of solver_ssa_reachability.m's return: [SSq, SSh, sn.space].
std::vector< std::vector< std::vector< T > > > node_space
sn.space, per stateful node
Matrix< T > ssq
SSq, states x concatenated width
std::vector< qn::NetState< T > > space
the reachable states
std::vector< std::vector< std::size_t > > hash
SSh, 1-based per node
The serial engine's knobs: SsaOptions plus the three the serial path reads and the NRM has no use for...
std::size_t state_max
refuse a reachable space larger than this
double cutoff
< 0 = the reference's automatic value
One sample path, in the shape solver_ssa.m returns it.
std::vector< qn::NetState< T > > space
The DISTINCT states visited, in first-visit order (the reference's u).
std::vector< double > tran_time
tranSysState{1}: the cumulative time at each firing.
std::vector< std::size_t > tran_state
The row of space the path OCCUPIED over [t-dt, t], one per firing.
Matrix< T > ssq
SSq: the per-(station, class) job counts of each distinct state.
std::vector< std::vector< std::vector< double > > > start_rates
The DERIVED rates per state, laid out like arv_rates: how fast the transitions enabled in that state ...
std::size_t warmup
leading firings excluded from pi
unsigned long seed
the stream this path came from
std::vector< std::vector< std::vector< double > > > arv_rates
arvRates / depRates, indexed [distinct state][stateful-1][class-1].
std::vector< std::vector< std::vector< std::size_t > > > buf
The region token FIFOs of each of those states, the reference's fcrBuf.
std::size_t samples
firings actually performed
std::vector< std::vector< std::vector< double > > > dly_rates
The rate of the cache MERGE transitions, i.e.
std::vector< std::vector< std::vector< double > > > dep_rates
std::vector< std::vector< std::vector< double > > > preempt_rates
std::vector< double > pi
pi: the fraction of simulated time spent in each of them.
std::vector< std::size_t > tran_sync
tranSync: which synchronization fired, sync.size() + g for a global one.
The serial analyzer's return: the metric table, the path, and the stream.
std::vector< SsaCacheRatio > cache
std::vector< double > parked
Mean number of jobs parked in a region FIFO, per class of the struct that ran.
SsaSolution avg
QN, UN, RN, TN, XN, CN; method = "serial".
unsigned long seed
carried beside the numbers, never implied
std::vector< std::size_t > fjclassmap
fjclassmap of the tag augmentation, empty on a model with no Fork.
What the analyzer returns, in the same shape as the MVA and fluid results.
Definition ssa_types.h:101
std::vector< double > XN
Definition ssa_types.h:103
std::vector< double > CN
Definition ssa_types.h:103
Matrix< double > UN
Definition ssa_types.h:102
Matrix< double > RN
Definition ssa_types.h:102
Matrix< double > StartN
The DERIVED rates, (nstations x nclasses): how often per unit time a class-r service STARTS at statio...
Definition ssa_types.h:111
double simulated_time
Simulated time the metrics are averaged over; the reference's totalTime.
Definition ssa_types.h:115
Matrix< double > TN
Definition ssa_types.h:102
std::size_t samples
Reaction firings actually performed.
Definition ssa_types.h:117
Matrix< double > QN
Definition ssa_types.h:102
std::string method
The concrete algorithm, as the reference's method.
Definition ssa_types.h:113
Matrix< double > PreemptN
Definition ssa_types.h:111