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