LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ssa_dispatch.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_SSA_DISPATCH_H
6#define LINE_SOLVERS_SSA_SSA_DISPATCH_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The SolverSSA entry surface: a port of `@@SolverSSA/runAnalyzer.m`'s method
12 * whitelist, of `solver_ssa_analyzer.m`'s eligibility gate (`isNrmEligible` and
13 * the per-feature `*NrmOK` predicates) and of `solver_ssa_analyzer_nrm.m`'s own
14 * scheduling validation.
15 *
16 * EVERY METHOD THE REFERENCE OFFERS IS REACHED FROM HERE. `SolverSSA.m` line 55
17 * lists `default`, `ssa`, `serial`, `para`/`parallel` and `nrm`; the JAR's
18 * `SolverSSA.listValidMethods` and native Python's list the same set. All five
19 * resolve to one of the three ported engines: the NRM of `solver_ssa_nrm.h`,
20 * the event-driven engine of `solver_ssa_serial.h`, and the replica mean of
21 * `solver_ssa_parallel.h`. `para`/`parallel` prefers the NRM whenever the model
22 * is eligible and replicates the serial engine otherwise, exactly as the
23 * reference does; `ssa` is the reference's alias for `serial`, NOT for the NRM;
24 * `default` is the reference's ladder -- the NRM when eligible, the serial
25 * engine otherwise -- as `solver_ssa` sets out below.
26 * `solver_ssa` returns the metric table alone, so a caller who needs the sample
27 * path, the per-replica tables or their standard errors calls the engine's own
28 * entry.
29 *
30 * WITHIN `nrm` THERE ARE TWO ENGINES, and both are reached from here. The
31 * reference picks between them on `options.config.state_space_gen`
32 * (`solver_ssa_analyzer_nrm.m` lines 52-68): `none` and `default` take the
33 * plain engine of `solver_ssa_nrm.m`, which integrates the metrics along the
34 * sample path, and any other value takes the tabulating engine of
35 * `solver_ssa_nrm_space.m`, which records the distinct states visited and forms
36 * the means as `pi * A`. `SsaOptions::state_space_gen` is that switch, and the
37 * eligibility gate below runs before it, exactly as the reference's does: it is
38 * the NRM's gate, not one engine's. The space engine then applies its OWN
39 * further refusals (an open model, a phase-type service, a space above the cap),
40 * each by name, in `NrmSpaceEngine::check`.
41 *
42 * WHY THE GATE IS SO LONG, AND WHAT IT IS FOR. It is `isNrmEligible`
43 * (`solver_ssa_analyzer.m` lines 365-386) and it answers ONE question: can the
44 * NRM run this model? Under `method='nrm'` a no is a refusal BY NAME, because a
45 * caller who spelled out the estimator asked for that estimator; under
46 * `default` and `parallel` the same no is a fallback to the serial engine, as
47 * the reference's is. The two share a body (`raise`), so they cannot drift.
48 *
49 * WHAT THE GATE MEANS SINCE THE FALLBACK WAS RESTORED. It is the NRM's reach and
50 * NOT SolverSSA's: almost everything it rejects, the serial engine now runs (a
51 * Cache access, an SPN firing, a Fork, a finite capacity region under either
52 * rule, PAS / OI, POLLING, the priority shares, class-dependent scaling), so a
53 * rejection under `default` costs the caller the NRM's SPEED and not the answer.
54 * Under `method='nrm'` it is still a refusal by name, because an estimator that
55 * was asked for by name must be the one that runs. The three-way split below
56 * therefore classifies what the NRM lacks, not what SolverSSA lacks:
57 *
58 * NOT REPRESENTABLE the struct has no field at all, so a model using the
59 * feature cannot be built (balking, reneging patience,
60 * SelfLoopingClass -- `JobClassType` is OPEN or CLOSED
61 * only). WRROBIN / JSQ / SQ / RL routing belongs here too
62 * by a different route: the enumerators exist, but
63 * `NetworkStruct::refresh_routing` refuses those
64 * state-dependent strategies when the struct is built, so
65 * the gate's own test is reached only by a caller who
66 * filled `routing` by hand. RROBIN is the exception: the
67 * refresh now EXPANDS it uniformly, for QNA/MNA, which
68 * recover the determinism from the split degree. An
69 * RROBIN model therefore reaches this gate for real, and
70 * the refusal below is what keeps the NRM from simulating
71 * it as random routing.
72 * REPRESENTABLE, NOT PORTED IN THE NRM the struct carries the parameters and
73 * the C++ NRM sub-engine is not written (Cache access with
74 * its replacement policies and retrieval system, PAS / OI
75 * pass-and-swap, POLLING with its switchover controller,
76 * the stochastic Petri net path, Fork/Join, finite
77 * capacity regions). The *PRIO scheduling family LEFT this
78 * list on 2026-08-15: `psprioshare`, `dpsprioshare` and
79 * `gpsprioshare` are ported, so the NRM runs those
80 * stations itself. EVERY ONE OF THESE
81 * IS ANSWERED BY THE SERIAL ENGINE, which is what makes
82 * the refusals below a routing decision rather than a
83 * capability boundary. Retrial orbits (`retrialparam`) and
84 * G-network signals (`issignal` and friends) are
85 * representable too; they are refused downstream, by the
86 * engines that would have to simulate them, not by this
87 * gate.
88 * REPRESENTABLE, NOT MEANINGFUL class-dependent scaling. The NRM builds its
89 * reaction rates without ever evaluating the handle, so the
90 * sample path would run at the UNSCALED rates while the
91 * utilization column, normalized by the declared peak
92 * (`sn.cdscalingpeak`), still looked consistent. The serial
93 * engine evaluates it and answers the model.
94 *
95 * Nothing falls through silently: a cache model simulated as an ordinary
96 * queueing network returns numbers, they are simply not the model's. That is
97 * why the gate stays even where the serial engine can answer -- the NRM's
98 * reaction table would run such a model to completion and report it.
99 */
100
101#include <string>
102#include <vector>
103
111#include "line/util/error.h"
112
113namespace line {
114namespace ssa {
115
116namespace detail {
117
118/**
119 * The scheduling whitelist of `solver_ssa_analyzer_nrm.m` lines 15-26,
120 * restricted to what this port's rate laws cover.
121 *
122 * The three priority-weighted shares PSPRIO / DPSPRIO / GPSPRIO joined the
123 * whitelist on 2026-08-15; PAS/OI and POLLING are still refused here by name
124 * because their rate laws are genuine sub-engines (the pass-and-swap
125 * enumeration over the ordered job list, and the polling controller with its
126 * switchover phase-type) that are not written.
127 *
128 * `raise` is what makes this both the REFUSAL and the ELIGIBILITY test, as the
129 * reference's single `isNrmEligible` is: `raise=true` names the offending
130 * station and throws, `raise=false` answers false. One body, so the message a
131 * caller of `method='nrm'` reads and the predicate `default` and `parallel`
132 * branch on can never disagree.
133 */
134template <class T>
135bool ssa_check_scheduling(const qn::NetworkStruct<T>& sn, bool raise = true) {
137 for (std::size_t i = 0; i < sn.nstations; ++i) {
138 const SchedStrategy s = sn.stations[i].sched;
139 switch (s) {
156 continue;
159 if (!raise) return false;
160 throw UnsupportedError(
161 "SolverSSA(method='nrm'): station '" + sn.stations[i].name +
162 "' uses pass-and-swap / order-independent scheduling. The reference NRM "
163 "supports it (solver_ssa_nrm.m, oirate/oiDepart/pasInSvc) by enumerating the "
164 "ordered job list against mu(c) and the swap graph; that sub-engine is not "
165 "ported to C++");
167 if (!raise) return false;
168 throw UnsupportedError(
169 "SolverSSA(method='nrm'): station '" + sn.stations[i].name +
170 "' uses POLLING scheduling. The reference NRM carries a polling controller "
171 "[mode, pos, swphase, ctr] with its own switchover reactions "
172 "(State.pollingInfo / State.pollingNext); that sub-engine is not ported to "
173 "C++");
174 default:
175 if (!raise) return false;
176 throw UnsupportedError("SolverSSA(method='nrm'): the scheduling policy '" +
177 std::string(lang::sched_to_text(s)) + "' at station '" +
178 sn.stations[i].name +
179 "' is not supported by the NRM in any codebase");
180 }
181 }
182 return true;
183}
184
185/** Node kinds the NRM reaction grid cannot express in this port. */
186template <class T>
187bool ssa_check_nodes(const qn::NetworkStruct<T>& sn, bool raise = true) {
188 for (const qn::NodeDef& nd : sn.nodes) {
189 switch (nd.nodetype) {
190 case qn::NodeType::Place:
191 case qn::NodeType::Transition:
192 if (!raise) return false;
193 throw UnsupportedError(
194 "SolverSSA(method='nrm'): node '" + nd.name +
195 "' makes this model a stochastic Petri net. The reference routes it to "
196 "solver_ssa_nrm_spn, a separate builder and run loop with immediate-mode "
197 "vanishing-marking collapse; that path is not ported to C++");
198 case qn::NodeType::Cache:
199 if (!raise) return false;
200 throw UnsupportedError(
201 "SolverSSA(method='nrm'): node '" + nd.name +
202 "' is a Cache. The reference NRM models a cache access as a state-dependent "
203 "class switch and applies State.afterEventCache's replacement policies (and "
204 "the delayed-hit retrieval system) to the cache contents; that sub-engine is "
205 "not ported to C++");
206 case qn::NodeType::Fork:
207 case qn::NodeType::Join:
208 if (!raise) return false;
209 throw UnsupportedError("SolverSSA(method='nrm'): node '" + nd.name +
210 "' makes this a fork-join model, which the NRM does not "
211 "handle in any codebase (isNrmEligible excludes it); the "
212 "reference falls back to the serial engine, whose own "
213 "fork handler (sn.fjsync / State.afterFJEvent) is not "
214 "ported either");
215 default:
216 break;
217 }
218 }
219 return true;
220}
221
222/**
223 * Routing strategies this port resolves, plus what the model layer itself
224 * cannot represent.
225 *
226 * RROBIN, WRROBIN, JSQ and SQ are resolved AT FIRING TIME by the NRM engine
227 * (`build_state_dependent_dest` / `resolve_state_dependent_dest`) from the
228 * declared out-arcs and the live population, which is what the reference does
229 * and what makes a round-robin dispatcher a dispatcher rather than a coin.
230 * `refresh_routing` still expands them into a probability split, because a
231 * matrix solver has nothing else to read; the engine ignores that split for
232 * these nodes and walks the arcs itself.
233 *
234 * SDR is the one that stays refused: its routing is a function the struct does
235 * not carry, so there is nothing here to evaluate.
236 */
237template <class T>
238bool ssa_check_routing(const qn::NetworkStruct<T>& sn, bool raise = true) {
239 for (const qn::NodeDef& nd : sn.nodes)
240 for (std::size_t r = 0; r < nd.routing.size(); ++r) {
241 const qn::RoutingStrategy rs = nd.routing[r];
242 if (rs == qn::RoutingStrategy::PROB || rs == qn::RoutingStrategy::RAND ||
243 rs == qn::RoutingStrategy::DISABLED || rs == qn::RoutingStrategy::RROBIN ||
244 rs == qn::RoutingStrategy::WRROBIN || rs == qn::RoutingStrategy::JSQ ||
245 rs == qn::RoutingStrategy::SQ)
246 continue;
247 if (!raise) return false;
248 throw UnsupportedError(
249 "SolverSSA(method='nrm'): node '" + nd.name + "' routes class '" +
250 sn.classes[r].name + "' by '" + std::string(lang::routing_to_text(rs)) +
251 "'. Its destination is a function the C++ NetworkStruct does not carry, so "
252 "there is nothing to resolve at firing time");
253 }
254 return true;
255}
256
257/**
258 * `phaseNrmOK` (solver_ssa_analyzer.m lines 407-...): where a non-exponential
259 * service process may sit.
260 *
261 * The INF / PS family expands exactly, because every job present is in service
262 * and the class share splits across the phases in the ratio kir/nir. The
263 * non-preemptive buffered family expands through the auxiliary in-service
264 * multiset. EXT is deliberately excluded and the reason is worth repeating: a
265 * phase-type ARRIVAL process is not a service law, and expanding it fires one
266 * arrival per PHASE instead of one per RENEWAL, so an Erlang-2 source doubles
267 * lambda. LCFSPR is excluded because preempt-resume would have to remember the
268 * preempted job's phase.
269 */
270template <class T>
271bool ssa_check_phases(const qn::NetworkStruct<T>& sn, bool raise = true) {
273 for (std::size_t i = 0; i < sn.nstations; ++i) {
274 const SchedStrategy s = sn.stations[i].sched;
275 const bool exact = s == SchedStrategy::INF || s == SchedStrategy::PS ||
281 if (exact) continue;
282 for (std::size_t r = 0; r < sn.nclasses; ++r) {
283 if (sn.disabled[i][r]) continue;
284 const lang::ProcessType pt = sn.service[i][r].type;
287 continue;
288 if (!raise) return false;
289 throw UnsupportedError(
290 "SolverSSA(method='nrm'): class '" + sn.classes[r].name + "' has non-exponential "
291 "service at station '" + sn.stations[i].name + "', whose '" +
292 std::string(lang::sched_to_text(s)) +
293 "' discipline the NRM phase expansion does not cover; ask for 'default', which "
294 "falls back to the serial engine as the reference does");
295 }
296 }
297 return true;
298}
299
300/**
301 * Class- or joint-dependent scaling, refused because the NRM does not SCALE by
302 * it.
303 *
304 * The NRM builds its own reaction rates from `sn.rates` and a load-dependent
305 * table (`solver_ssa_nrm.h:355-388`) rather than going through
306 * `state_events.h`, so it never evaluates beta_r(n) or eta_i(n): the sample path
307 * would run at the unscaled rates. Utilization WOULD be normalized correctly
308 * (the declared peaks are carried), which is exactly why this must be a refusal
309 * -- the answer would look internally consistent. The serial engine does scale,
310 * so the eligibility predicate sends such a model there.
311 */
312template <class T>
313bool ssa_check_cdscaling(const qn::NetworkStruct<T>& sn, bool raise = true) {
314 for (std::size_t i = 0; i < sn.nstations; ++i)
315 if (sn.stations[i].cdscaling || sn.stations[i].jdscaling) {
316 if (!raise) return false;
317 throw UnsupportedError(
318 "SolverSSA(method='nrm'): station '" + sn.stations[i].name +
319 "' declares a class- or joint-dependent scaling. The NRM builds its reaction "
320 "rates without evaluating the dependence handle, so the sample path would run at "
321 "the unscaled rates; ask for method='serial', which applies it");
322 }
323 // A GLOBAL (Whittle) dependence is refused for the same reason and one more:
324 // the NRM's propensity closures receive the per-station population slice, not
325 // the whole population matrix a global handle reads.
326 if (sn.gdscaling) {
327 if (!raise) return false;
328 throw UnsupportedError(
329 "SolverSSA(method='nrm'): the model declares a global dependence "
330 "(setGlobalDependence). The NRM builds its propensities from the per-station "
331 "population slice and never sees the whole population matrix the handle reads, so "
332 "the sample path would run at the unscaled rates; ask for method='serial', which "
333 "applies it");
334 }
335 return true;
336}
337
338/**
339 * Finite capacity regions, refused because the NRM builds no admission or
340 * release machinery for them at all.
341 *
342 * The reference carries a per-region FIFO (`fcrBuf`) with an admission gate on
343 * every arrival into the region and a release cascade after every firing
344 * (`fcr_release`), under both the WAITQ and the DROP rule. Nothing in
345 * `solver_ssa_nrm.h` builds that: the reaction table has no region term, so a
346 * region-bearing model would be simulated as if the region were absent and
347 * report a plausible but region-free answer. `solver_ssa_serial.h`'s own
348 * `serial_check` refuses the same field for the same reason; this mirrors it.
349 */
350template <class T>
351bool ssa_check_regions(const qn::NetworkStruct<T>& sn, bool raise = true) {
352 if (!sn.regions.empty()) {
353 if (!raise) return false;
354 throw UnsupportedError(
355 "SolverSSA(method='nrm'): the model declares a finite capacity region. The "
356 "reference carries a per-region FIFO (`fcrBuf`) with an admission gate on every "
357 "arrival into the region and a release cascade after every firing (`fcr_release`), "
358 "under both the WAITQ and the DROP rule; that sub-engine is not ported to C++");
359 }
360 return true;
361}
362
363/**
364 * `isNrmEligible` (`solver_ssa_analyzer.m` lines 365-386): can the NRM run this
365 * model at all?
366 *
367 * It is the SAME six tests the refusal above takes, asked with `raise=false`,
368 * so the predicate cannot drift from the message. It is deliberately the C++
369 * NRM's reach and not the reference's: this port's NRM covers less, and a
370 * `default` that consulted MATLAB's wider predicate would send a model to a
371 * refusal the serial engine can actually answer.
372 */
373template <class T>
374bool ssa_nrm_eligible(const qn::NetworkStruct<T>& sn) {
375 return ssa_check_nodes(sn, false) && ssa_check_scheduling(sn, false) &&
376 ssa_check_routing(sn, false) && ssa_check_phases(sn, false) &&
377 ssa_check_cdscaling(sn, false) && ssa_check_regions(sn, false);
378}
379
380/**
381 * The same six tests as a SENTENCE, so a report can say why `nrm` is withheld.
382 *
383 * `ssa_nrm_eligible` above answers the dispatch's question (may I PREFER the
384 * NRM?) and nothing answered the gate's (may I OFFER it?), so `ssa.nrm` was
385 * reported runnable on every model and an explicit request then raised. The
386 * catch is the ADAPTER between those two callers and not error suppression: the
387 * checks already carry the exact wording a user should see, and re-deriving it
388 * here is how the message and the predicate drift apart.
389 */
390template <class T>
391std::string ssa_nrm_supports(const qn::NetworkStruct<T>& sn) {
392 try {
393 ssa_check_nodes(sn, true);
394 ssa_check_scheduling(sn, true);
395 ssa_check_routing(sn, true);
396 ssa_check_phases(sn, true);
397 ssa_check_cdscaling(sn, true);
398 ssa_check_regions(sn, true);
399 } catch (const UnsupportedError& e) {
400 return e.what();
401 }
402 return "";
403}
404
405/**
406 * The tabulating engine's knobs from the caller's.
407 *
408 * `state_max` keeps its own default because `SsaOptions` carries no cap: a
409 * caller who wants a different one calls `solver_ssa_nrm_space_analyzer`
410 * directly, which is also where the tabulated path and the propensity table
411 * survive rather than being reduced to the metrics.
412 */
413inline SsaNrmSpaceOptions ssa_space_options(const SsaOptions& o) {
414 SsaNrmSpaceOptions s;
415 static_cast<SsaOptions&>(s) = o;
416 return s;
417}
418
419/**
420 * The serial and replicated engines' knobs from the caller's.
421 *
422 * `cutoff`, `state_max`, `nreplicas` and `eventcache` keep their
423 * own defaults for the same reason `state_max` does above: `SsaOptions` has no
424 * field for them, and `nreplicas = 8` is `SolverOptions('SSA')`'s own default
425 * rather than a number chosen here. A caller who wants a different R calls
426 * `solver_ssa_parallel` directly, which is also where the R per-replica tables
427 * and their standard errors survive rather than being reduced to the mean.
428 */
429inline SsaSerialOptions ssa_serial_options(const SsaOptions& o) {
430 SsaSerialOptions s;
431 static_cast<SsaOptions&>(s) = o;
432 return s;
433}
434
435inline SsaParallelOptions ssa_parallel_options(const SsaOptions& o) {
436 SsaParallelOptions p;
437 static_cast<SsaOptions&>(p) = o;
438 return p;
439}
440
441/**
442 * One serial run, keeping the cache write-back the metric table does not carry.
443 *
444 * Written once because `solver_ssa` reaches the serial engine from two arms
445 * (the `default` fallback and the named method), and a caller that got its
446 * cache shares from one arm and not the other would report the offered split
447 * for the same model under a different spelling of the same request.
448 */
449template <class T>
450SsaSolution ssa_serial_avg(const qn::NetworkStruct<T>& sn, const SsaOptions& opt,
451 std::vector<SsaCacheRatio>* cache) {
452 SsaSerialSolution<T> s = solver_ssa_serial_analyzer(sn, ssa_serial_options(opt));
453 if (cache) *cache = s.cache;
454 return s.avg;
455}
456
457} // namespace detail
458
459/**
460 * Port of `SolverSSA.listValidMethods`.
461 *
462 * Six names for three engines, because the reference spells the same engine
463 * more than one way: 'ssa' and 'serial' are the serial trajectory, 'para' and
464 * 'parallel' the replicated one, 'nrm' the next-reaction method, and 'default'
465 * is the ladder `solver_ssa` walks -- the NRM when it can run the model and the
466 * serial engine when it cannot. Every name here is dispatched by `solver_ssa`
467 * below, which refuses anything else by name.
468 */
469inline std::vector<std::string> list_valid_methods() {
470 return {"default", "ssa", "serial", "para", "parallel", "nrm"};
471}
472
473/**
474 * `solver_ssa_analyzer_nrm.m`: run the NRM and return the metric table.
475 *
476 * The reference's post-processing (`QN(isnan(QN)) = 0` and the rest) is inside
477 * the engine already: it never produces a NaN, because every division is
478 * guarded at the point it is taken.
479 */
480template <class T>
482 // `if constexpr`, not a run-time test: the engine reaches `dist_to_map`,
483 // whose APH fit static_asserts on transcendental arithmetic, so a Rational
484 // instantiation would fail to COMPILE rather than refuse. The gate has to
485 // keep the body from being instantiated at all.
486 if constexpr (!std::is_same<T, double>::value) {
487 (void)sn;
488 (void)opt;
489 throw UnsupportedError(
490 "solver_ssa_nrm: an SSA sample path is generated from exponential clocks, which are "
491 "logarithms of uniform draws; there is no exact value to compute and a wider float "
492 "carries no information the Monte Carlo error does not swamp. Rerun with --arith "
493 "double");
494 } else {
495 detail::ssa_check_nodes(sn);
496 detail::ssa_check_scheduling(sn);
497 detail::ssa_check_routing(sn);
498 detail::ssa_check_phases(sn);
499 detail::ssa_check_cdscaling(sn);
500 detail::ssa_check_regions(sn);
501 // The reference's engine switch, taken AFTER the gate above because the
502 // gate is the NRM's and not one engine's.
503 if (opt.state_space_gen != "none" && opt.state_space_gen != "default")
504 return solver_ssa_nrm_space_analyzer(sn, detail::ssa_space_options(opt)).avg;
505 NrmEngine<T> eng(sn, opt);
506 return eng.run();
507 }
508}
509
510/**
511 * `solver_ssa_analyzer.m`: choose the method.
512 *
513 * The ladder is the reference's, in its order:
514 *
515 * `default` the NRM when it is eligible, ELSE the serial engine, and only a
516 * model neither engine runs is refused -- by the NRM's message,
517 * which names the missing sub-engine.
518 * `nrm` the NRM alone, reached by naming the estimator: a model it
519 * cannot run is refused rather than silently answered by the
520 * other engine, because a caller who spelled out an estimator
521 * asked for that estimator's variance as well as its mean.
522 * `ssa` the reference's alias for `serial` (line 128), not for the NRM.
523 * `serial` one run of the event-driven engine.
524 * `para` the NRM when eligible (the reference prefers one fast run over
525 * `parallel` R replicated ones, lines 143-157), else the replica mean of
526 * `nreplicas` independent serial runs.
527 *
528 * `default`'s FALLBACK IS THE REFERENCE'S (lines 66-78) and was restored on
529 * 2026-07-31, when the serial engine gained fork-join and the finite capacity
530 * regions. It had been held back while the serial engine covered less than the
531 * NRM gate rejects, on the ground that a second failure further downstream is
532 * less informative than the NRM's own message. That ground is gone for every
533 * construct the serial engine now runs, and where it still holds -- a model
534 * NEITHER engine covers -- the NRM's message is what the caller reads, because
535 * `serial_can_run` is asked BEFORE the fallback is taken rather than after it
536 * has failed.
537 *
538 * `cache`, when given, receives the serial engine's cache write-back -- the
539 * realized hit and miss shares of every Cache node, which are a SOLVER RESULT
540 * and the only thing that tells a node table apart from the 1/2-1/2 `link()`
541 * offers. It is an out-parameter rather than a field of `SsaSolution` because
542 * that struct is the metric table the three engines share, and only one of them
543 * has a cache to report: the NRM refuses a Cache node by name and the parallel
544 * engine averages replicas that carry none.
545 */
546template <class T>
548 std::vector<SsaCacheRatio>* cache = nullptr) {
549 // THE REFERENCE DOES NOT REFUSE THIS ON THE NRM, and this used to. A
550 // fed-back job keeps its server, which the reference encodes in
551 // `State.afterEvent`'s immfeed self-loop, and only its SERIAL engine has
552 // that arc: `solver_ssa_analyzer_nrm.m:29` warns in as many words that the
553 // NRM "does not model immediate feedback (immfeed); self-loops are treated
554 // as class-switching with re-queueing" and then runs. Refusing both engines
555 // made a model the reference solves unsolvable here, which is a worse
556 // divergence than the approximation the reference chose -- and the warning
557 // is what keeps the approximation from being silent.
558 if (sn.has_immediate_feedback()) {
559 const std::string& mm = opt.method;
560 // `default` and `parallel` both fall through to the NRM when the model
561 // is NRM-eligible and to the serial engine when it is not, so the test
562 // has to be on the engine that will actually run and not on the name.
563 const bool runs_nrm =
564 mm == "nrm" || ((mm == "default" || mm == "para" || mm == "parallel") &&
565 detail::ssa_nrm_eligible(sn));
566 if (!runs_nrm)
567 throw UnsupportedError(
568 "SolverSSA(method='" + mm +
569 "'): immediate feedback (setImmediateFeedback) is not ported to this engine. The "
570 "reference holds the server across the fed-back service (solver_ssa.m's "
571 "immfeed_selfloop into State.afterEvent) and this port has no such arc, so the "
572 "trajectory would release and re-queue the job; use method='nrm' on an "
573 "NRM-eligible model, which approximates it as the reference's own NRM does, or "
574 "SolverMVA / SolverNC");
575 std::cerr << "[LINE] Warning: SolverSSA(method=nrm) does not model immediate feedback "
576 "(immfeed); self-loops are treated as class-switching with re-queueing. Use "
577 "method='serial' in the reference for immediate feedback."
578 << std::endl;
579 }
580 const std::string& m = opt.method;
581 if (m == "default") {
582 // The reference's ladder: the NRM when it can run the model, the serial
583 // engine when it cannot, and the NRM's own refusal -- which names the
584 // sub-engine that is missing -- when neither can. Asking
585 // `serial_can_run` first is what keeps that message: falling through to
586 // the serial engine and letting IT fail would report whichever guard it
587 // hit, about a model the caller never asked it to run.
588 if (detail::ssa_nrm_eligible(sn)) return solver_ssa_nrm_analyzer(sn, opt);
589 if (serial_detail::serial_can_run(sn))
590 return detail::ssa_serial_avg(sn, opt, cache);
591 return solver_ssa_nrm_analyzer(sn, opt); // raises, by name
592 }
593 if (m == "nrm") return solver_ssa_nrm_analyzer(sn, opt);
594 if (m == "ssa" || m == "serial") return detail::ssa_serial_avg(sn, opt, cache);
595 if (m == "para" || m == "parallel") {
596 // The reference's own preference, `solver_ssa_analyzer.m` lines 143-157:
597 // an NRM-eligible model runs once on the NRM rather than R times here.
598 if (detail::ssa_nrm_eligible(sn)) return solver_ssa_nrm_analyzer(sn, opt);
599 return solver_ssa_parallel_analyzer(sn, detail::ssa_parallel_options(opt)).avg;
600 }
601 throw UnsupportedError("SolverSSA: '" + m +
602 "' is not a valid method; the reference offers 'default', 'ssa', "
603 "'serial', 'para', 'parallel' and 'nrm', all of which this port "
604 "implements");
605}
606
607/**
608 * `CacheMetrics` from the serial engine's cache write-back.
609 *
610 * The shares are matched to their Cache node BY INDEX, not by position: the
611 * base struct walks every Cache the model declares while the engine reports
612 * only the nodes it simulated, so pairing the two off in order would write one
613 * cache's measurement onto another on any model holding more than one.
614 *
615 * `residt` becomes `latency` unchanged, NaN included. The reference warns that
616 * retrieval latency is not implemented and reports NaN in every codebase, so
617 * carrying the NaN IS parity; dropping the field would report "not computed"
618 * about a quantity the engine did state.
619 */
620template <class T>
622 const std::vector<SsaCacheRatio>& cache) {
624 sn, std::vector<T>(), std::vector<T>(), std::vector<T>(), std::vector<T>(), Matrix<T>(),
625 Matrix<T>(), std::vector<T>());
626 for (std::size_t c = 0; c < out.caches.size(); ++c) {
627 for (std::size_t j = 0; j < cache.size(); ++j) {
628 if (cache[j].node != out.caches[c].node) continue;
630 m.hitprob.clear();
631 m.missprob.clear();
632 m.delayedprob.clear();
633 m.latency.clear();
634 for (std::size_t r = 0; r < cache[j].hitprob.size(); ++r)
635 m.hitprob.push_back(num_traits<T>::from_double(cache[j].hitprob[r]));
636 for (std::size_t r = 0; r < cache[j].missprob.size(); ++r)
637 m.missprob.push_back(num_traits<T>::from_double(cache[j].missprob[r]));
638 for (std::size_t r = 0; r < cache[j].delayedprob.size(); ++r)
639 m.delayedprob.push_back(num_traits<T>::from_double(cache[j].delayedprob[r]));
640 for (std::size_t r = 0; r < cache[j].residt.size(); ++r)
641 m.latency.push_back(num_traits<T>::from_double(cache[j].residt[r]));
642 break;
643 }
644 }
645 return out;
646}
647
648/**
649 * The struct with the cache split the SIMULATION MEASURED, visits rebuilt.
650 *
651 * A cache splits the read stream into a hit stream and a miss stream, and that
652 * split IS routing: the visit ratios of everything downstream depend on it. The
653 * base struct carries only what `link()` offered, an even share over the hit and
654 * miss classes, because the split is a RESULT and cannot be known before the
655 * solve. Anything derived from visits after the solve therefore has to be taken
656 * on the rewritten struct, not on the base one -- `sn_get_residt_from_respt`
657 * reported ResidT = RespT/2 for both classes of tut06_cache_lru_zipf (0.1 and
658 * 0.5 against the reference's 0.16475 and 0.17625), which is the even split
659 * showing through, not a residence time.
660 *
661 * The rewrite is the one `da_cacheqn` performs between passes, with the measured
662 * shares in place of the analytical ones: the cache row of `rtnodes` is cleared
663 * and the hit and miss mass sent to every connected successor, after which
664 * `da_recompute_visits_from_rtnodes` rebuilds `visits` and `nodevisits`. A share
665 * the engine left undefined (NaN, a class that does not read this cache) leaves
666 * that row alone rather than zeroing a routing the model does have.
667 */
668template <class T>
670 const std::vector<SsaCacheRatio>& cache) {
671 if (cache.empty()) return base;
673 const std::size_t I = sn.nodes.size(), K = sn.nclasses;
674 if (I == 0 || K == 0 || sn.rtnodes.rows() < I * K) return base;
675 const T zero = num_traits<T>::from_int(0);
676 // The connectivity is read BEFORE any row is rewritten, as in `da_cacheqn`:
677 // a cache's own read self-switch is not a downstream node.
678 std::vector<std::vector<bool> > conn(I, std::vector<bool>(I, false));
679 for (std::size_t a = 0; a < I; ++a)
680 for (std::size_t b = 0; b < I; ++b) {
681 if (a == b) continue;
682 for (std::size_t r = 0; r < K && !conn[a][b]; ++r)
683 for (std::size_t s = 0; s < K && !conn[a][b]; ++s)
684 if (sn.rtnodes(a * K + r, b * K + s) > zero) conn[a][b] = true;
685 }
686 bool rewrote = false;
687 for (std::size_t j = 0; j < cache.size(); ++j) {
688 const std::size_t ind = cache[j].node; // 1-based node index
689 if (ind == 0 || ind > I) continue;
690 typename std::map<std::size_t, qn::CacheParam<T> >::const_iterator ci =
691 sn.nodeparam.find(ind);
692 if (ci == sn.nodeparam.end()) continue;
693 const std::size_t nd = ind - 1;
694 for (std::size_t r = 0; r < K; ++r) {
695 if (ci->second.hitclass.size() <= r || ci->second.missclass.size() <= r) continue;
696 const std::size_t hc = ci->second.hitclass[r], mc = ci->second.missclass[r];
697 if (hc == 0 || mc == 0 || hc > K || mc > K) continue;
698 if (cache[j].hitprob.size() <= r || cache[j].missprob.size() <= r) continue;
699 const double hp = cache[j].hitprob[r], mp = cache[j].missprob[r];
700 if (!(hp == hp) || !(mp == mp)) continue; // NaN: the engine measured none
701 for (std::size_t col = 0; col < I * K; ++col) sn.rtnodes(nd * K + r, col) = zero;
702 for (std::size_t jnd = 0; jnd < I; ++jnd) {
703 if (!conn[nd][jnd]) continue;
704 sn.rtnodes(nd * K + r, jnd * K + (hc - 1)) = num_traits<T>::from_double(hp);
705 sn.rtnodes(nd * K + r, jnd * K + (mc - 1)) = num_traits<T>::from_double(mp);
706 }
707 rewrote = true;
708 }
709 }
710 if (!rewrote) return base;
711 sn.da_recompute_visits_from_rtnodes();
712 return sn;
713}
714
715} // namespace ssa
716} // namespace line
717
718#endif // LINE_SOLVERS_SSA_SSA_DISPATCH_H
What a solver observed about the Cache nodes of a model.
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The NRM engine: the reaction network built from sn, and the sample path.
SsaSolution run()
Run opt.samples firings and return the time-averaged metrics.
The exception types the port throws.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:389
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:402
CacheMetrics< T > cache_metrics_of(const qn::NetworkStruct< T > &sn, const std::vector< T > &hitprob, const std::vector< T > &missprob, const std::vector< T > &delayedprob, const std::vector< T > &latency, const Matrix< T > &hitproblist, const Matrix< T > &itemprob, const std::vector< T > &listcost)
Assemble CacheMetrics from what a cache analyzer returned.
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....
SsaSolution solver_ssa_nrm_analyzer(const qn::NetworkStruct< T > &sn, const SsaOptions &opt)
solver_ssa_analyzer_nrm.m: run the NRM and return the metric table.
SsaParallelSolution< T > solver_ssa_parallel_analyzer(const qn::NetworkStruct< T > &sn, const SsaParallelOptions &opt)
solver_ssa_analyzer_parallel.m: run R replicas of the serial engine and combine their estimates.
SsaNrmSpaceSolution< T > solver_ssa_nrm_space_analyzer(const qn::NetworkStruct< T > &sn, const SsaNrmSpaceOptions &opt)
Port of the else branch of solver_ssa_analyzer_nrm.m, the one state_space_gen selects: the means as p...
std::vector< std::string > list_valid_methods()
Port of SolverSSA.listValidMethods.
SsaSolution solver_ssa(const qn::NetworkStruct< T > &sn, const SsaOptions &opt, std::vector< SsaCacheRatio > *cache=nullptr)
solver_ssa_analyzer.m: choose the method.
qn::NetworkStruct< T > sn_with_ssa_cache_split(const qn::NetworkStruct< T > &base, const std::vector< SsaCacheRatio > &cache)
The struct with the cache split the SIMULATION MEASURED, visits rebuilt.
solvers::CacheMetrics< T > cache_metrics_of_ssa(const qn::NetworkStruct< T > &sn, const std::vector< SsaCacheRatio > &cache)
CacheMetrics from the serial engine's cache write-back.
A queueing network and its refreshed NetworkStruct.
SolverSSA, the nrm method: a port of solver_ssa_nrm.m and its analyzer solver_ssa_analyzer_nrm....
SolverSSA, the EXPLICIT STATE SPACE variant of the Next Reaction Method: a port of solver_ssa_nrm_spa...
SolverSSA, the para / parallel method: a port of solver_ssa_analyzer_parallel.m.
SolverSSA, the serial method: a port of solver_ssa_reachability.m, of the run loop of solver_ssa....
Controls, results and the random source of SolverSSA.
Every Cache node of the model, in node order; empty on a model with none.
std::vector< CacheNodeMetrics< T > > caches
One Cache node's measured behaviour.
std::vector< T > hitprob
(K) TRUE hit fraction, EMPTY = not computed
std::vector< T > delayedprob
(K) delayed-hit fraction, EMPTY off a retrieval system
std::vector< T > latency
(K) expected retrieval latency, EMPTY = not computed
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:69
What the analyzer returns, in the same shape as the MVA and fluid results.
Definition ssa_types.h:101