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