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. The mlmc method uses the dedicated finite-horizon
29 * solver_ssa_get_tran_reward getter; the metric analyzer refuses them.
30 *
31 * WITHIN `nrm` THERE ARE TWO ENGINES, and both are reached from here. The
32 * reference picks between them on `options.config.state_space_gen`
33 * (`solver_ssa_analyzer_nrm.m` lines 52-68): `none` and `default` take the
34 * plain engine of `solver_ssa_nrm.m`, which integrates the metrics along the
35 * sample path, and any other value takes the tabulating engine of
36 * `solver_ssa_nrm_space.m`, which records the distinct states visited and forms
37 * the means as `pi * A`. `SsaOptions::state_space_gen` is that switch, and the
38 * eligibility gate below runs before it, exactly as the reference's does: it is
39 * the NRM's gate, not one engine's. The space engine then applies its OWN
40 * further refusals (an open model, a phase-type service, a space above the cap),
41 * each by name, in `NrmSpaceEngine::check`.
42 *
43 * WHY THE GATE IS SO LONG, AND WHAT IT IS FOR. It is `isNrmEligible`
44 * (`solver_ssa_analyzer.m` lines 365-386) and it answers ONE question: can the
45 * NRM run this model? Under `method='nrm'` a no is a refusal BY NAME, because a
46 * caller who spelled out the estimator asked for that estimator; under
47 * `default` and `parallel` the same no is a fallback to the serial engine, as
48 * the reference's is. The two share a body (`raise`), so they cannot drift.
49 *
50 * WHAT THE GATE MEANS SINCE THE FALLBACK WAS RESTORED. It is the NRM's reach and
51 * NOT SolverSSA's: everything it rejects, the serial engine runs (a Fork, a
52 * BLOCKING region rule, global dependence), so a rejection under `default`
53 * costs the caller the NRM's SPEED and not the answer. Under `method='nrm'` it
54 * is still a refusal by name, because an estimator that was asked for by name
55 * must be the one that runs. What the NRM lacks falls in two groups:
56 *
57 * NOT REPRESENTABLE the struct has no field at all, so a model using the
58 * feature cannot be built (SelfLoopingClass --
59 * `JobClassType` is OPEN or CLOSED only). RROBIN, WRROBIN,
60 * JSQ and SQ are NOT here: the NRM resolves all four at
61 * firing time (`resolve_state_dependent_dest`).
62 * REFUSED IN EVERY CODEBASE'S NRM Fork/Join, the blocking region rules
63 * BAS / BBS / RSRD, and global (Whittle) dependence; the
64 * reference's `isNrmEligible` excludes the same set.
65 * Reneging, balking and retrial orbits are refused by
66 * `ssa_check_impatience`, and G-network signals
67 * (`issignal` and friends) downstream, by the engines that
68 * would have to simulate them.
69 *
70 * The sub-engines that have LEFT the refusal list, all in `solver_ssa_nrm.h`
71 * unless named: the *PRIO shares on 2026-08-15; the stochastic Petri net path
72 * on 2026-09-13 (`solver_ssa_nrm_spn.h`, which still refuses the four firing
73 * laws `spn_nrm_supported` names); and on 2026-09-24 class- and joint-dependent
74 * scaling (`cdfac`), finite capacity regions under DROP and WAITQ
75 * (`fcr_outcome` / `fcr_release_cascade`), PAS / OI (`pas_rate` /
76 * `pas_depart`), POLLING (`PollCtrl` and one switchover reaction per station)
77 * and the Cache (`cache_fire`, drawing each READ from `after_event_cache`). The
78 * tabulating engine of `solver_ssa_nrm_space.h` does not carry those five and
79 * refuses them itself, by name.
80 *
81 * Nothing falls through silently: a model with a construct the reaction table
82 * does not model would run to completion and return numbers that are simply
83 * not the model's, which is why the gate stays even where the serial engine can
84 * answer.
85 */
86
87#include <algorithm>
88#include <cmath>
89#include <string>
90#include <vector>
91
102#include "line/util/error.h"
105
106namespace line {
107namespace ssa {
108
109namespace detail {
110
111/**
112 * The scheduling whitelist of `solver_ssa_analyzer_nrm.m` lines 15-26,
113 * restricted to what this port's rate laws cover.
114 *
115 * The three priority-weighted shares PSPRIO / DPSPRIO / GPSPRIO joined the
116 * whitelist on 2026-08-15, and PAS / OI and POLLING on 2026-09-24, when their
117 * sub-engines (the pass-and-swap list and the polling controller) were ported.
118 *
119 * `raise` is what makes this both the REFUSAL and the ELIGIBILITY test, as the
120 * reference's single `isNrmEligible` is: `raise=true` names the offending
121 * station and throws, `raise=false` answers false. One body, so the message a
122 * caller of `method='nrm'` reads and the predicate `default` and `parallel`
123 * branch on can never disagree.
124 */
125template <class T>
126bool ssa_check_scheduling(const qn::NetworkStruct<T>& sn, bool raise = true) {
128 for (std::size_t i = 0; i < sn.nstations; ++i) {
129 const SchedStrategy s = sn.stations[i].sched;
130 switch (s) {
147 // Pass-and-swap and order-independent: served since 2026-09-24 through
148 // the ordered-list sub-engine (`pas_rate` / `pas_depart`).
151 // Polling: served since 2026-09-24 through the controller sub-engine
152 // (`PollCtrl`, one switchover reaction per polling station).
154 continue;
155 default:
156 if (!raise) return false;
157 throw UnsupportedError(ErrorKind{"SchedStrategy"}, "SolverSSA(method='nrm'): the scheduling policy '" +
158 std::string(lang::sched_to_text(s)) + "' at station '" +
159 sn.stations[i].name +
160 "' is not supported by the NRM in any codebase");
161 }
162 }
163 return true;
164}
165
166/**
167 * Node kinds the NRM reaction grid cannot express in this port.
168 *
169 * A Place and a Transition are NOT among them since 2026-09-13:
170 * `solver_ssa_nrm_spn.h` ports the reference's SPN builder and run loop, so a
171 * net carrying them is routed to that engine instead of refused. What it can
172 * and cannot read is `spn_nrm_supported`, asked separately below, because the
173 * reference asks it separately too -- its four conditions are about the FIRING
174 * LAWS of the net, not about the presence of a node kind.
175 */
176template <class T>
177bool ssa_check_nodes(const qn::NetworkStruct<T>& sn, bool raise = true) {
178 for (const qn::NodeDef& nd : sn.nodes) {
179 switch (nd.nodetype) {
180 // Cache: served since 2026-09-24 through the cache-read reaction
181 // (`NrmEngine::cache_fire`).
182 case qn::NodeType::Fork:
183 case qn::NodeType::Join:
184 if (!raise) return false;
185 throw UnsupportedError(ErrorKind{"Fork"}, "SolverSSA(method='nrm'): node '" + nd.name +
186 "' makes this a fork-join model, which the NRM does not "
187 "handle in any codebase (isNrmEligible excludes it); the "
188 "reference falls back to the serial engine, whose own "
189 "fork handler (sn.fjsync / State.afterFJEvent) is not "
190 "ported either");
191 default:
192 break;
193 }
194 }
195 return true;
196}
197
198/**
199 * Routing strategies this port resolves, plus what the model layer itself
200 * cannot represent.
201 *
202 * RROBIN, WRROBIN, JSQ and SQ are resolved AT FIRING TIME by the NRM engine
203 * (`build_state_dependent_dest` / `resolve_state_dependent_dest`) from the
204 * declared out-arcs and the live population, which is what the reference does
205 * and what makes a round-robin dispatcher a dispatcher rather than a coin.
206 * `refresh_routing` still expands them into a probability split, because a
207 * matrix solver has nothing else to read; the engine ignores that split for
208 * these nodes and walks the arcs itself.
209 *
210 * SDR is the one that stays refused: its routing is a function the struct does
211 * not carry, so there is nothing here to evaluate.
212 */
213template <class T>
214bool ssa_check_routing(const qn::NetworkStruct<T>& sn, bool raise = true) {
215 for (const qn::NodeDef& nd : sn.nodes)
216 for (std::size_t r = 0; r < nd.routing.size(); ++r) {
217 const qn::RoutingStrategy rs = nd.routing[r];
218 if (rs == qn::RoutingStrategy::PROB || rs == qn::RoutingStrategy::RAND ||
219 rs == qn::RoutingStrategy::DISABLED || rs == qn::RoutingStrategy::RROBIN ||
220 rs == qn::RoutingStrategy::WRROBIN || rs == qn::RoutingStrategy::JSQ ||
221 rs == qn::RoutingStrategy::SQ)
222 continue;
223 if (!raise) return false;
224 throw UnsupportedError(ErrorKind{"RoutingStrategy"},
225 "SolverSSA(method='nrm'): node '" + nd.name + "' routes class '" +
226 sn.classes[r].name + "' by '" + std::string(lang::routing_to_text(rs)) +
227 "'. Its destination is a function the C++ NetworkStruct does not carry, so "
228 "there is nothing to resolve at firing time");
229 }
230 return true;
231}
232
233/**
234 * `phaseNrmOK` (solver_ssa_analyzer.m lines 407-...): where a non-exponential
235 * service process may sit.
236 *
237 * The INF / PS family expands exactly, because every job present is in service
238 * and the class share splits across the phases in the ratio kir/nir. The
239 * non-preemptive buffered family expands through the auxiliary in-service
240 * multiset. EXT is deliberately excluded and the reason is worth repeating: a
241 * phase-type ARRIVAL process is not a service law, and expanding it fires one
242 * arrival per PHASE instead of one per RENEWAL, so an Erlang-2 source doubles
243 * lambda. LCFSPR is excluded because preempt-resume would have to remember the
244 * preempted job's phase.
245 */
246template <class T>
247bool ssa_check_phases(const qn::NetworkStruct<T>& sn, bool raise = true) {
249 for (std::size_t i = 0; i < sn.nstations; ++i) {
250 const SchedStrategy s = sn.stations[i].sched;
251 const bool exact = s == SchedStrategy::INF || s == SchedStrategy::PS ||
257 // A MAP/MMPP2 service at FCFS chains its draws through a memory phase (and, with c > 1,
258 // the landing phase of each draw, `ctmc_mapc`), which the in-service multiset has no
259 // column for: the NRM would restart every draw from the entry law.
260 if (s == SchedStrategy::FCFS)
261 for (std::size_t r = 0; r < sn.nclasses; ++r) {
262 if (sn.disabled[i][r]) continue;
263 const lang::ProcessType pt = sn.service[i][r].type;
264 if (pt != lang::ProcessType::MAP && pt != lang::ProcessType::MMPP2) continue;
265 if (!raise) return false;
266 throw UnsupportedError(ErrorKind{"MAP"},
267 "SolverSSA(method='nrm'): class '" + sn.classes[r].name + "' has MAP service "
268 "at FCFS station '" + sn.stations[i].name + "', whose draws are chained "
269 "through a memory phase the NRM does not carry; ask for 'default', which "
270 "falls back to the serial engine");
271 }
272 if (exact) continue;
273 for (std::size_t r = 0; r < sn.nclasses; ++r) {
274 if (sn.disabled[i][r]) continue;
275 const lang::ProcessType pt = sn.service[i][r].type;
278 continue;
279 if (!raise) return false;
280 throw UnsupportedError(ErrorKind{"NonExponential"},
281 "SolverSSA(method='nrm'): class '" + sn.classes[r].name + "' has non-exponential "
282 "service at station '" + sn.stations[i].name + "', whose '" +
283 std::string(lang::sched_to_text(s)) +
284 "' discipline the NRM phase expansion does not cover; ask for 'default', which "
285 "falls back to the serial engine as the reference does");
286 }
287 }
288 return true;
289}
290
291/**
292 * Global (Whittle) dependence, refused because the NRM does not SCALE by it.
293 *
294 * Class- and joint-dependent scaling are served since 2026-09-24: `cdfac` in
295 * `solver_ssa_nrm.h` evaluates eta_i(n) .* beta_{i,r}(n) on the station's
296 * per-class population at firing time, as the reference does, and utilization
297 * is normalized by the declared peaks. The reference's gate (`ssa_nrm_guards.gd`)
298 * excludes only the global handle, and so does this one.
299 */
300template <class T>
301bool ssa_check_cdscaling(const qn::NetworkStruct<T>& sn, bool raise = true) {
302 // The NRM's propensities receive the per-station population slice, not the
303 // whole population matrix a global handle reads.
304 if (sn.gdscaling) {
305 if (!raise) return false;
306 throw UnsupportedError(ErrorKind{"GlobalDependence"},
307 "SolverSSA(method='nrm'): the model declares a global dependence "
308 "(setGlobalDependence). The NRM builds its propensities from the per-station "
309 "population slice and never sees the whole population matrix the handle reads, so "
310 "the sample path would run at the unscaled rates; ask for method='serial', which "
311 "applies it");
312 }
313 return true;
314}
315
316/**
317 * Finite capacity regions: served since 2026-09-24, under DROP and WAITQ.
318 *
319 * `solver_ssa_nrm.h` ports `fcrPrecompute` / `fcrRefusingRegion` /
320 * `fcrReleaseCascade`: an admission gate on every arrival into a region and a
321 * head-of-line release cascade from a per-region FIFO after every firing. What
322 * stays refused is a BLOCKING region rule (BAS, BBS, RSRD), which no codebase's
323 * NRM carries.
324 */
325template <class T>
326bool ssa_check_regions(const qn::NetworkStruct<T>& sn, bool raise = true) {
327 for (const typename qn::NetworkStruct<T>::Region& rg : sn.regions)
328 for (std::size_t r = 0; r < rg.rule.size(); ++r) {
329 const qn::DropStrategy d = rg.rule[r];
330 if (d != qn::DropStrategy::BAS && d != qn::DropStrategy::BBS &&
331 d != qn::DropStrategy::RSRD)
332 continue;
333 if (!raise) return false;
334 throw UnsupportedError(ErrorKind{"Region"},
335 "SolverSSA(method='nrm'): finite capacity region '" + rg.name +
336 "' declares a blocking rule for class '" + sn.classes[r].name +
337 "'. The NRM region gate implements DROP and WAITQ only, as the reference's does");
338 }
339 return true;
340}
341
342/**
343 * Synchronous calls (REPLY signals), `ssa_nrm_guards.reply`: refused by the NRM in every codebase.
344 *
345 * A calling job leaves its caller and keeps the server there until the reply returns. The
346 * serial engine carries that hold in the reply block of the local row (`after_event`), while
347 * the NRM's reactions see per-class counts only and have no held-server counter, so the caller
348 * would free its server at once. `default` and `parallel` therefore take the serial engine,
349 * and an explicit `nrm` falls back to it with a warning, as the reference does.
350 */
351template <class T>
352bool ssa_check_reply(const qn::NetworkStruct<T>& sn, bool raise = true) {
353 for (std::size_t ind = 0; ind < sn.replyblock.size(); ++ind)
354 for (std::size_t k = 0; k < sn.replyblock[ind].size(); ++k) {
355 if (!sn.replyblock[ind][k]) continue;
356 if (!raise) return false;
357 throw UnsupportedError(ErrorKind{"SyncCall"},
358 "SolverSSA(method='nrm'): class '" + sn.classes[k].name + "' makes a synchronous "
359 "call (REPLY signal) from node '" + sn.nodes[ind].name + "'. The NRM has no "
360 "held-server counter for a pending reply; use method='serial'");
361 }
362 return true;
363}
364
365/**
366 * Reneging, balking and retrial orbits: refused by the NRM, whose reactions are per-class
367 * service completions and arrivals with no abandonment, balking or retry channel, so the
368 * model would be answered without them. The serial engine fires all three through
369 * `after_event`, which is where `default` and `parallel` send such a model.
370 */
371template <class T>
372bool ssa_check_impatience(const qn::NetworkStruct<T>& sn, bool raise = true) {
373 // A law the serial engine refuses too is reported as such, not as a pointer to 'serial'.
374 const std::string both = qn::state_impatience_unsupported(sn, "SolverSSA");
375 if (!both.empty()) {
376 if (!raise) return false;
377 throw UnsupportedError(ErrorKind{"Impatience"}, both);
378 }
379 for (std::size_t i = 0; i < sn.stations.size(); ++i) {
380 const qn::Station<T>& st = sn.stations[i];
381 bool any = qn::station_has_reneging(sn, i + 1);
382 for (std::size_t r = 0; r < st.balking.size(); ++r)
383 any = any || st.balking[r].strategy != lang::BalkingStrategy::NONE;
384 const auto rit = sn.retrialparam.find(i + 1);
385 if (rit != sn.retrialparam.end())
386 for (std::size_t r = 0; r < rit->second.retrial_proc.size(); ++r)
387 any = any || !rit->second.retrial_proc[r].disabled;
388 if (!any) continue;
389 if (!raise) return false;
390 throw UnsupportedError(ErrorKind{"Impatience"},
391 "SolverSSA(method='nrm'): station '" + st.name + "' declares reneging, balking or a "
392 "retrial orbit. The NRM has no reaction for any of them; use method='serial'");
393 }
394 return true;
395}
396
397/**
398 * `isNrmEligible` (`solver_ssa_analyzer.m` lines 365-386): can the NRM run this
399 * model at all?
400 *
401 * It is the SAME tests the refusal above takes, asked with `raise=false`,
402 * so the predicate cannot drift from the message. It is deliberately the C++
403 * NRM's reach and not the reference's: this port's NRM covers less, and a
404 * `default` that consulted MATLAB's wider predicate would send a model to a
405 * refusal the serial engine can actually answer.
406 */
407template <class T>
408bool ssa_nrm_eligible(const qn::NetworkStruct<T>& sn) {
409 // Immediate feedback keeps the fed-back job on the server it just used, which
410 // the serial engine expresses through `after_event`'s `no_promote` arc. The
411 // NRM has no such arc: its reactions see per-class counts, not which job holds
412 // the server, so a self-loop there is a class switch with re-queueing. An
413 // explicit `method='nrm'` still RUNS the model with that approximation,
414 // warning in as many words (`ssa_nrm_supports`, the GATE, deliberately does
415 // not test this); this only keeps `default` and `parallel` from PREFERRING an
416 // approximation over the exact serial engine.
417 if (sn.has_immediate_feedback()) return false;
418 return ssa_check_nodes(sn, false) && ssa_check_scheduling(sn, false) &&
419 ssa_check_routing(sn, false) && ssa_check_phases(sn, false) &&
420 ssa_check_cdscaling(sn, false) && ssa_check_regions(sn, false) &&
421 ssa_check_reply(sn, false) && ssa_check_impatience(sn, false) &&
422 spn_nrm_supported(sn, false);
423}
424
425/**
426 * The same tests as a SENTENCE, so a report can say why `nrm` is withheld.
427 *
428 * `ssa_nrm_eligible` above answers the dispatch's question (may I PREFER the
429 * NRM?) and nothing answered the gate's (may I OFFER it?), so `ssa.nrm` was
430 * reported runnable on every model and an explicit request then raised. The
431 * catch is the ADAPTER between those two callers and not error suppression: the
432 * checks already carry the exact wording a user should see, and re-deriving it
433 * here is how the message and the predicate drift apart.
434 */
435template <class T>
436std::string ssa_nrm_supports(const qn::NetworkStruct<T>& sn) {
437 try {
438 ssa_check_nodes(sn, true);
439 ssa_check_scheduling(sn, true);
440 ssa_check_routing(sn, true);
441 ssa_check_phases(sn, true);
442 ssa_check_cdscaling(sn, true);
443 ssa_check_regions(sn, true);
444 ssa_check_reply(sn, true);
445 ssa_check_impatience(sn, true);
446 spn_nrm_supported(sn, true);
447 } catch (const UnsupportedError& e) {
448 return e.what();
449 }
450 return "";
451}
452
453/**
454 * The tabulating engine's knobs from the caller's.
455 *
456 * `state_max` keeps its own default because `SsaOptions` carries no cap: a
457 * caller who wants a different one calls `solver_ssa_nrm_space_analyzer`
458 * directly, which is also where the tabulated path and the propensity table
459 * survive rather than being reduced to the metrics.
460 */
461inline SsaNrmSpaceOptions ssa_space_options(const SsaOptions& o) {
462 SsaNrmSpaceOptions s;
463 static_cast<SsaOptions&>(s) = o;
464 return s;
465}
466
467/**
468 * The serial and replicated engines' knobs from the caller's.
469 *
470 * `state_max`, `nreplicas` and `eventcache` keep their
471 * own defaults for the same reason `state_max` does above: `SsaOptions` has no
472 * field for them, and `nreplicas = 8` is `SolverOptions('SSA')`'s own default
473 * rather than a number chosen here. A caller who wants a different R calls
474 * `solver_ssa_parallel` directly, which is also where the R per-replica tables
475 * and their standard errors survive rather than being reduced to the mean.
476 */
477inline SsaSerialOptions ssa_serial_options(const SsaOptions& o) {
478 SsaSerialOptions s;
479 static_cast<SsaOptions&>(s) = o;
480 return s;
481}
482
483inline SsaParallelOptions ssa_parallel_options(const SsaOptions& o) {
484 SsaParallelOptions p;
485 static_cast<SsaOptions&>(p) = o;
486 return p;
487}
488
489/**
490 * One serial run, keeping the cache write-back the metric table does not carry.
491 *
492 * Written once because `solver_ssa` reaches the serial engine from two arms
493 * (the `default` fallback and the named method), and a caller that got its
494 * cache shares from one arm and not the other would report the offered split
495 * for the same model under a different spelling of the same request.
496 */
497template <class T>
498SsaSolution ssa_serial_avg(const qn::NetworkStruct<T>& sn, const SsaOptions& opt,
499 std::vector<SsaCacheRatio>* cache) {
500 SsaSerialSolution<T> s = solver_ssa_serial_analyzer(sn, ssa_serial_options(opt));
501 if (cache) *cache = s.cache;
502 return s.avg;
503}
504
505/**
506 * `@@SolverSSA/runAnalyzer.m:227`: a Place counts TOKENS, an engine counts
507 * FIRING EVENTS, and the two are the same number only where every arc has
508 * multiplicity one and no immediate transition moves the tokens.
509 *
510 * WHAT THE ENGINES HAND OVER. Every SPN engine accumulates a Place's
511 * throughput as the summed propensity of the TIMED modes consuming from it, so
512 * an arc of weight 2 counts one departure where two tokens left, and an
513 * IMMEDIATE consumer counts none at all -- a firing that takes zero time is not
514 * a timed event and no engine, here or in the reference, ever sees it as one.
515 * A Place drained only by an immediate transition therefore arrives here with
516 * TN = 0, which then makes RespT 0 and leaves the neighbouring Place's ArvR 0
517 * once the caller derives it from this table.
518 *
519 * `sn_pn_avg_rates` is the reference's repair and recovers the missing rates
520 * from the flow-balance equations, which is why it runs at the SOLVER level and
521 * on the ORIGINAL struct: both engines fold a fork-join tagging back before
522 * returning, so the table is already in the caller's own coordinates here, as
523 * it is after `solver_tr_fjtag_analyzer`'s lift in the reference. A model with
524 * no Place, or one whose firing rates cannot be recovered, is returned
525 * untouched.
526 *
527 * AN IS LEFT TO THE CALLER, exactly as `[TN,~,RN] = sn_pn_avg_rates(...)` leaves
528 * it in the reference: `SsaSolution` carries no arrival-rate table, and every
529 * consumer derives one with `sn_get_arvr_from_tput` from the TN repaired here.
530 */
531template <class T>
532SsaSolution ssa_pn_token_rates(const qn::NetworkStruct<T>& sn, const SsaSolution& in) {
533 bool has_place = false;
534 for (std::size_t a = 0; a < sn.nodes.size() && !has_place; ++a)
535 if (sn.nodes[a].nodetype == qn::NodeType::Place) has_place = true;
536 if (!has_place || in.TN.rows() == 0) return in;
537
538 const std::size_t M = in.TN.rows(), K = in.TN.cols();
539 Matrix<T> QN(M, K), TN(M, K), RN(M, K);
540 for (std::size_t i = 0; i < M; ++i)
541 for (std::size_t k = 0; k < K; ++k) {
542 QN(i, k) = num_traits<T>::from_double(in.QN(i, k));
543 TN(i, k) = num_traits<T>::from_double(in.TN(i, k));
544 RN(i, k) = num_traits<T>::from_double(in.RN(i, k));
545 }
546 const api::SnPnAvgRates<T> pn = api::sn_pn_avg_rates(sn, QN, TN, Matrix<T>(), RN);
547
548 SsaSolution out = in;
549 for (std::size_t i = 0; i < M; ++i)
550 for (std::size_t k = 0; k < K; ++k) {
551 out.TN(i, k) = num_traits<T>::to_double(pn.TN(i, k));
552 out.RN(i, k) = num_traits<T>::to_double(pn.RN(i, k));
553 }
554 return out;
555}
556
557} // namespace detail
558
559/**
560 * Port of `SolverSSA.listValidMethods`.
561 *
562 * Six names for three engines, because the reference spells the same engine
563 * more than one way: 'ssa' and 'serial' are the serial trajectory, 'para' and
564 * 'parallel' the replicated one, 'nrm' the next-reaction method, and 'default'
565 * is the ladder `solver_ssa` walks -- the NRM when it can run the model and the
566 * serial engine when it cannot. The reward method mlmc is
567 * dispatched by solver_ssa_get_tran_reward; solver_ssa below refuses them.
568 */
569inline std::vector<std::string> list_valid_methods() {
570 // the names and their order are this solver's row of the method registry (doc/line-methods.json)
571 return ::line::method_registry::methods("SSA");
572}
573
574/**
575 * `solver_ssa_analyzer_nrm.m`: run the NRM and return the metric table.
576 *
577 * The reference's post-processing (`QN(isnan(QN)) = 0` and the rest) is inside
578 * the engine already: it never produces a NaN, because every division is
579 * guarded at the point it is taken.
580 */
581template <class T>
583 std::vector<SsaCacheRatio>* cache = nullptr) {
584 // `if constexpr`, not a run-time test: the engine reaches `dist_to_map`,
585 // whose APH fit static_asserts on transcendental arithmetic, so a Rational
586 // instantiation would fail to COMPILE rather than refuse. The gate has to
587 // keep the body from being instantiated at all.
588 if constexpr (!std::is_same<T, double>::value) {
589 (void)sn;
590 (void)opt;
591 (void)cache;
592 throw UnsupportedError(ErrorKind{"Arithmetic"},
593 "solver_ssa_nrm: an SSA sample path is generated from exponential clocks, which are "
594 "logarithms of uniform draws; there is no exact value to compute and a wider float "
595 "carries no information the Monte Carlo error does not swamp. Rerun with --arith "
596 "double");
597 } else {
598 detail::ssa_check_nodes(sn);
599 detail::ssa_check_scheduling(sn);
600 detail::ssa_check_routing(sn);
601 detail::ssa_check_phases(sn);
602 detail::ssa_check_cdscaling(sn);
603 detail::ssa_check_regions(sn);
604 detail::ssa_check_reply(sn);
605 detail::ssa_check_impatience(sn);
606 detail::spn_nrm_supported(sn);
607 // `solver_ssa_nrm.m:174`: a net carrying a Transition is a STOCHASTIC
608 // PETRI NET and goes to the SPN builder and run loop, which reads a
609 // marking of tokens rather than a grid of jobs in service. The branch is
610 // taken before the space/plain switch below because the tabulating
611 // engine tabulates the queueing state vector, which an SPN does not
612 // have; the reference branches at the same point and for the same
613 // reason.
614 for (const qn::NodeDef& nd : sn.nodes)
615 if (nd.nodetype == qn::NodeType::Transition) {
617 return spn.run();
618 }
619 // The reference's engine switch, taken AFTER the gate above because the
620 // gate is the NRM's and not one engine's.
621 if (opt.state_space_gen != "none" && opt.state_space_gen != "default")
622 return solver_ssa_nrm_space_analyzer(sn, detail::ssa_space_options(opt)).avg;
623 NrmEngine<T> eng(sn, opt);
624 SsaSolution out = eng.run();
625 if (cache) *cache = eng.cache();
626 return out;
627 }
628}
629
630/**
631 * `solver_ssa_analyzer.m`: choose the method.
632 *
633 * The ladder is the reference's, in its order:
634 *
635 * `default` the NRM when it is eligible, ELSE the serial engine, and only a
636 * model neither engine runs is refused -- by the NRM's message,
637 * which names the missing sub-engine.
638 * `nrm` the NRM alone, reached by naming the estimator: a model it
639 * cannot run is refused rather than silently answered by the
640 * other engine, because a caller who spelled out an estimator
641 * asked for that estimator's variance as well as its mean.
642 * `ssa` the reference's alias for `serial` (line 128), not for the NRM.
643 * `serial` one run of the event-driven engine.
644 * `para` the NRM when eligible (the reference prefers one fast run over
645 * `parallel` R replicated ones, lines 143-157), else the replica mean of
646 * `nreplicas` independent serial runs.
647 *
648 * `default`'s FALLBACK IS THE REFERENCE'S (lines 66-78) and was restored on
649 * 2026-07-31, when the serial engine gained fork-join and the finite capacity
650 * regions. It had been held back while the serial engine covered less than the
651 * NRM gate rejects, on the ground that a second failure further downstream is
652 * less informative than the NRM's own message. That ground is gone for every
653 * construct the serial engine now runs, and where it still holds -- a model
654 * NEITHER engine covers -- the NRM's message is what the caller reads, because
655 * `serial_can_run` is asked BEFORE the fallback is taken rather than after it
656 * has failed.
657 *
658 * `cache`, when given, receives the serial engine's cache write-back -- the
659 * realized hit and miss shares of every Cache node, which are a SOLVER RESULT
660 * and the only thing that tells a node table apart from the 1/2-1/2 `link()`
661 * offers. It is an out-parameter rather than a field of `SsaSolution` because
662 * that struct is the metric table the three engines share, and only two of them
663 * have a cache to report, the serial engine and (since 2026-09-24) the NRM; the
664 * parallel engine averages replicas that carry none.
665 */
666template <class T>
668 std::vector<SsaCacheRatio>* cache) {
669 // Resolve the warmup discard once, so every engine below (NRM, its SPN and
670 // tabulating variants, serial, replicated) reads the same fraction, as
671 // `solver_ssa_analyzer.m` does. `SsaOptions` carries no timespan: every run
672 // reaching here is a steady-state one.
673 SsaOptions opt = opt_in;
674 opt.warmupfrac = ssa_resolve_warmupfrac(sn, opt_in.warmupfrac);
675 // A FLUID PETRI NET HAS ONE ENGINE, the NRM Petri runner (`ssa_fluid_refusal`), so it is sent there under
676 // every method that can reach it and refused by name under the rest, before any arm below can pick the
677 // serial engine.
678 if (sn.has_fluid_places()) {
679 const api::SnFluidVerdict fv = detail::ssa_fluid_refusal(sn, opt.method);
680 if (!fv.ok) throw UnsupportedError(ErrorKind{"Structure"}, fv.reason);
681 opt.method = "nrm";
683 }
684 // A fed-back job keeps its server, which the reference encodes in
685 // `State.afterEvent`'s immfeed self-loop, and only the SERIAL engine has that
686 // arc -- here as in the reference. `solver_ssa_analyzer_nrm.m:29` warns in as
687 // many words that the NRM "does not model immediate feedback (immfeed);
688 // self-loops are treated as class-switching with re-queueing" and then runs,
689 // so the name stays offerable and only the approximation is announced.
690 // `ssa_nrm_eligible` excludes immediate feedback, so `default` and `parallel`
691 // reach the exact serial engine and only an EXPLICIT `nrm` warns.
692 if (sn.has_immediate_feedback()) {
693 const std::string& mm = opt.method;
694 const bool runs_nrm =
695 mm == "nrm" || ((mm == "default" || mm == "para" || mm == "pana" || mm == "parallel") &&
696 detail::ssa_nrm_eligible(sn));
697 if (runs_nrm)
698 std::cerr << "[LINE] Warning: SolverSSA(method=nrm) does not model immediate feedback "
699 "(immfeed); self-loops are treated as class-switching with re-queueing. "
700 "Use method='serial' for immediate feedback."
701 << std::endl;
702 }
703 const std::string& m = opt.method;
704 if (m == "default") {
705 // The reference's ladder: the NRM when it can run the model, the serial
706 // engine when it cannot, and the NRM's own refusal -- which names the
707 // sub-engine that is missing -- when neither can. Asking
708 // `serial_can_run` first is what keeps that message: falling through to
709 // the serial engine and letting IT fail would report whichever guard it
710 // hit, about a model the caller never asked it to run.
711 if (detail::ssa_nrm_eligible(sn)) return solver_ssa_nrm_analyzer(sn, opt, cache);
712 if (serial_detail::serial_can_run(sn))
713 return detail::ssa_serial_avg(sn, opt, cache);
714 return solver_ssa_nrm_analyzer(sn, opt, cache); // raises, by name
715 }
716 if (m == "nrm") {
717 // `solver_ssa_analyzer.m`'s explicit 'nrm' arm: a synchronous call falls back to the
718 // serial engine with a warning rather than refusing, in every codebase.
719 if (!detail::ssa_check_reply(sn, false)) {
720 std::cerr << "[LINE] Warning: SolverSSA: NRM does not support synchronous calls "
721 "(REPLY signals); falling back to the serial method."
722 << std::endl;
723 return detail::ssa_serial_avg(sn, opt, cache);
724 }
726 }
727 if (m == "ssa" || m == "serial") return detail::ssa_serial_avg(sn, opt, cache);
728 if (m == "para" || m == "pana" || m == "parallel") {
729 // The reference's own preference, `solver_ssa_analyzer.m` lines 143-157:
730 // an NRM-eligible model runs once on the NRM rather than R times here.
731 if (detail::ssa_nrm_eligible(sn)) return solver_ssa_nrm_analyzer(sn, opt, cache);
732 return solver_ssa_parallel_analyzer(sn, detail::ssa_parallel_options(opt)).avg;
733 }
734 throw UnsupportedError(ErrorKind{"Method"}, "SolverSSA: '" + m +
735 "' is not a valid method; the reference offers 'default', 'ssa', "
736 "'serial', 'para', 'parallel' and 'nrm', all of which this port "
737 "implements");
738}
739
740/**
741 * `@@SolverSSA/runAnalyzer` itself: the engine the method selects, then the
742 * result assembly the reference does before it hands the table to a caller.
743 *
744 * Only one step of that assembly belongs here rather than to the caller. A
745 * Place is measured in TOKENS and every engine counts FIRING EVENTS, so
746 * `ssa_pn_token_rates` rewrites the Place rows of TN and RN exactly where
747 * `runAnalyzer.m:227` does; the arrival rates and residence times each consumer
748 * still derives for itself, as before. A model with no Place is untouched, so
749 * this costs an ordinary queueing network one scan of the node list.
750 */
751template <class T>
753 const std::vector<SsaCacheRatio>& cache);
754template <class T>
755SsaSolution ssa_imm_tput_balance(const qn::NetworkStruct<T>& sn, const SsaSolution& in);
756
757template <class T>
759 std::vector<SsaCacheRatio>* cache = nullptr) {
760 if(opt.method=="mlmc")
761 throw UnsupportedError(ErrorKind{"Analysis"}, "SSA MLMC estimates finite-horizon rewards; use solver_ssa_get_tran_reward");
762 // The Immediate-station balance needs the measured cache split even when the caller does not ask for it.
763 std::vector<SsaCacheRatio> local;
764 std::vector<SsaCacheRatio>* cr = cache ? cache : &local;
765 const SsaSolution raw = solver_ssa_engine(sn, opt, cr);
766 // solver_ssa_analyzer.m / solver_ssa_analyzer_nrm.m apply ssa_imm_tput_balance before runAnalyzer's token rates.
767 return detail::ssa_pn_token_rates(sn, ssa_imm_tput_balance(sn_with_ssa_cache_split<T>(sn, *cr), raw));
768}
769
770/**
771 * `CacheMetrics` from the serial engine's cache write-back.
772 *
773 * The shares are matched to their Cache node BY INDEX, not by position: the
774 * base struct walks every Cache the model declares while the engine reports
775 * only the nodes it simulated, so pairing the two off in order would write one
776 * cache's measurement onto another on any model holding more than one.
777 *
778 * `residt` becomes `latency` unchanged, NaN included. The reference warns that
779 * retrieval latency is not implemented and reports NaN in every codebase, so
780 * carrying the NaN IS parity; dropping the field would report "not computed"
781 * about a quantity the engine did state.
782 */
783template <class T>
785 const std::vector<SsaCacheRatio>& cache) {
787 sn, std::vector<T>(), std::vector<T>(), std::vector<T>(), std::vector<T>(), Matrix<T>(),
788 Matrix<T>(), std::vector<T>());
789 for (std::size_t c = 0; c < out.caches.size(); ++c) {
790 for (std::size_t j = 0; j < cache.size(); ++j) {
791 if (cache[j].node != out.caches[c].node) continue;
793 m.hitprob.clear();
794 m.missprob.clear();
795 m.delayedprob.clear();
796 m.latency.clear();
797 for (std::size_t r = 0; r < cache[j].hitprob.size(); ++r)
798 m.hitprob.push_back(num_traits<T>::from_double(cache[j].hitprob[r]));
799 for (std::size_t r = 0; r < cache[j].missprob.size(); ++r)
800 m.missprob.push_back(num_traits<T>::from_double(cache[j].missprob[r]));
801 for (std::size_t r = 0; r < cache[j].delayedprob.size(); ++r)
802 m.delayedprob.push_back(num_traits<T>::from_double(cache[j].delayedprob[r]));
803 for (std::size_t r = 0; r < cache[j].residt.size(); ++r)
804 m.latency.push_back(num_traits<T>::from_double(cache[j].residt[r]));
805 break;
806 }
807 }
808 return out;
809}
810
811/**
812 * The struct with the cache split the SIMULATION MEASURED, visits rebuilt.
813 *
814 * A cache splits the read stream into a hit stream and a miss stream, and that
815 * split IS routing: the visit ratios of everything downstream depend on it. The
816 * base struct carries only what `link()` offered, an even share over the hit and
817 * miss classes, because the split is a RESULT and cannot be known before the
818 * solve. Anything derived from visits after the solve therefore has to be taken
819 * on the rewritten struct, not on the base one -- `sn_get_residt_from_respt`
820 * reported ResidT = RespT/2 for both classes of tut06_cache_lru_zipf (0.1 and
821 * 0.5 against the reference's 0.16475 and 0.17625), which is the even split
822 * showing through, not a residence time.
823 *
824 * The rewrite is the one `da_cacheqn` performs between passes, with the measured
825 * shares in place of the analytical ones: the cache row of `rtnodes` is cleared
826 * and the hit and miss mass sent to every connected successor, after which
827 * `da_recompute_visits_from_rtnodes` rebuilds `visits` and `nodevisits`. A share
828 * the engine left undefined (NaN, a class that does not read this cache) leaves
829 * that row alone rather than zeroing a routing the model does have.
830 */
831template <class T>
833 const std::vector<SsaCacheRatio>& cache) {
834 if (cache.empty()) return base;
836 const std::size_t I = sn.nodes.size(), K = sn.nclasses;
837 if (I == 0 || K == 0 || sn.rtnodes.rows() < I * K) return base;
838 const T zero = num_traits<T>::from_int(0);
839 // The connectivity is read BEFORE any row is rewritten, as in `da_cacheqn`:
840 // a cache's own read self-switch is not a downstream node.
841 std::vector<std::vector<bool> > conn(I, std::vector<bool>(I, false));
842 for (std::size_t a = 0; a < I; ++a)
843 for (std::size_t b = 0; b < I; ++b) {
844 if (a == b) continue;
845 for (std::size_t r = 0; r < K && !conn[a][b]; ++r)
846 for (std::size_t s = 0; s < K && !conn[a][b]; ++s)
847 if (sn.rtnodes(a * K + r, b * K + s) > zero) conn[a][b] = true;
848 }
849 bool rewrote = false;
850 for (std::size_t j = 0; j < cache.size(); ++j) {
851 const std::size_t ind = cache[j].node; // 1-based node index
852 if (ind == 0 || ind > I) continue;
853 typename std::map<std::size_t, qn::CacheParam<T> >::const_iterator ci =
854 sn.nodeparam.find(ind);
855 if (ci == sn.nodeparam.end()) continue;
856 const std::size_t nd = ind - 1;
857 for (std::size_t r = 0; r < K; ++r) {
858 if (ci->second.hitclass.size() <= r || ci->second.missclass.size() <= r) continue;
859 const std::size_t hc = ci->second.hitclass[r], mc = ci->second.missclass[r];
860 if (hc == 0 || mc == 0 || hc > K || mc > K) continue;
861 if (cache[j].hitprob.size() <= r || cache[j].missprob.size() <= r) continue;
862 const double hp = cache[j].hitprob[r], mp = cache[j].missprob[r];
863 if (!(hp == hp) || !(mp == mp)) continue; // NaN: the engine measured none
864 for (std::size_t col = 0; col < I * K; ++col) sn.rtnodes(nd * K + r, col) = zero;
865 for (std::size_t jnd = 0; jnd < I; ++jnd) {
866 if (!conn[nd][jnd]) continue;
867 sn.rtnodes(nd * K + r, jnd * K + (hc - 1)) = num_traits<T>::from_double(hp);
868 sn.rtnodes(nd * K + r, jnd * K + (mc - 1)) = num_traits<T>::from_double(mp);
869 }
870 rewrote = true;
871 }
872 }
873 if (!rewrote) return base;
874 sn.da_recompute_visits_from_rtnodes();
875 return sn;
876}
877
878/**
879 * Port of `ssa_imm_tput_balance.m`: the throughput of every station-class pair
880 * whose service is Immediate, taken from flow balance rather than from the
881 * sample path.
882 *
883 * Such a station holds a job for zero time, so its departure rate IS its
884 * arrival rate in every sample path. Every engine here estimates TN as the time
885 * integral of the departure rate, which at an Immediate station integrates the
886 * encoding rate `GlobalConstants::Immediate` over ~1e-8 sojourns: each visit
887 * contributes rate*dt ~ Exp(1) instead of one departure, a unit-variance number
888 * uncorrelated with the timed flows that fix the rate. On tut06_cache_lru_zipf
889 * that left Client Tput 2.71334 against the ArvR 2.86586 its feeders imply.
890 *
891 * TN there is replaced by the arrival rate of `sn_get_arvr_from_tput`, the same
892 * flow balance that reports the ArvR column, taken on the struct carrying the
893 * MEASURED cache split (`sn_with_ssa_cache_split`), and swept until a chain of
894 * Immediate stations settles (at most nstations sweeps, to FineTol). Sources
895 * are excluded, timed stations are untouched, and XN = TN(refstat),
896 * RN = QN/TN at the replaced pairs and CN = N/XN follow. Skipped under JSQ/SQ
897 * routing, whose split is state dependent and absent from `rt`.
898 */
899template <class T>
901 const std::size_t M = sn.nstations, R = sn.nclasses;
902 if (in.TN.rows() != M || in.TN.cols() != R) return in;
903 std::vector<std::vector<bool> > imm(M, std::vector<bool>(R, false));
904 bool any_imm = false;
905 for (std::size_t ist = 0; ist < M; ++ist) {
906 if (sn.stations[ist].nodetype == qn::NodeType::Source) continue;
907 for (std::size_t r = 0; r < R; ++r)
908 if (sn.service[ist][r].type == lang::ProcessType::IMMEDIATE) imm[ist][r] = any_imm = true;
909 }
910 if (!any_imm) return in;
911 for (const qn::NodeDef& nd : sn.nodes)
912 for (std::size_t r = 0; r < nd.routing.size(); ++r)
913 if (nd.routing[r] == qn::RoutingStrategy::JSQ || nd.routing[r] == qn::RoutingStrategy::SQ) return in;
914
915 SsaSolution out = in;
916 Matrix<T> TN(M, R);
917 for (std::size_t i = 0; i < M; ++i)
918 for (std::size_t r = 0; r < R; ++r) TN(i, r) = num_traits<T>::from_double(in.TN(i, r));
919 for (std::size_t sweep = 0; sweep < M; ++sweep) {
920 const Matrix<T> AN = mva::sn_get_arvr_from_tput(sn, TN, TN);
921 if (AN.rows() != M || AN.cols() != R) break;
922 double gap = 0.0;
923 for (std::size_t i = 0; i < M; ++i)
924 for (std::size_t r = 0; r < R; ++r)
925 if (imm[i][r])
926 gap = std::max(gap, std::fabs(num_traits<T>::to_double(AN(i, r)) -
927 num_traits<T>::to_double(TN(i, r))));
928 if (gap <= lang::GlobalConstants::FineTol) break;
929 for (std::size_t i = 0; i < M; ++i)
930 for (std::size_t r = 0; r < R; ++r)
931 if (imm[i][r]) TN(i, r) = AN(i, r);
932 }
933 for (std::size_t i = 0; i < M; ++i)
934 for (std::size_t r = 0; r < R; ++r) out.TN(i, r) = num_traits<T>::to_double(TN(i, r));
935 for (std::size_t r = 0; r < R; ++r) {
936 const std::size_t rs = sn.classes[r].refstat;
937 if (rs > 0 && rs <= M && r < out.XN.size()) out.XN[r] = out.TN(rs - 1, r);
938 for (std::size_t i = 0; i < M; ++i)
939 if (imm[i][r]) out.RN(i, r) = out.TN(i, r) > 0.0 ? out.QN(i, r) / out.TN(i, r) : 0.0;
940 if (r < out.XN.size() && r < out.CN.size() && out.XN[r] > 0.0)
941 out.CN[r] = sn.classes[r].population / out.XN[r];
942 }
943 return out;
944}
945
946} // namespace ssa
947} // namespace line
948
949#endif // LINE_SOLVERS_SSA_SSA_DISPATCH_H
What a solver observed about the Cache nodes of a model.
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
The NRM engine: the reaction network built from sn, and the sample path.
const std::vector< SsaCacheRatio > & cache() const
The cache write-back of the last run(), one entry per Cache node.
SsaSolution run()
Run opt.samples firings and return the time-averaged metrics.
The SPN Next-Reaction-Method engine, solver_ssa_nrm_spn of the reference.
The exception types the port throws.
The method names of each solver, in listing order.
SnPnAvgRates< T > sn_pn_avg_rates(const qn::NetworkStruct< T > &sn, const Matrix< T > &QN, const Matrix< T > &TN, const Matrix< T > &AN, const Matrix< T > &RN)
Port of sn_pn_avg_rates: rewrite the place rows of TN, AN and RN so that they agree with the recovere...
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:431
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:235
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:409
bool in(const std::string &s, std::initializer_list< const char * > set)
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
Port of sn_get_arvr_from_tput: the arrival rate each station sees, from the throughputs and the class...
std::string state_impatience_unsupported(const NetworkStruct< T > &sn, const std::string &solver)
Port of solver_ctmc_state_supports.m: the impatience laws the state machinery cannot serve,...
bool station_has_reneging(const NetworkStruct< T > &sn, std::size_t ist)
True when station ist declares exponential reneging for some class.
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, std::vector< SsaCacheRatio > *cache=nullptr)
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_engine(const qn::NetworkStruct< T > &sn, const SsaOptions &opt_in, std::vector< SsaCacheRatio > *cache)
solver_ssa_analyzer.m: choose the method.
SsaSolution solver_ssa(const qn::NetworkStruct< T > &sn, const SsaOptions &opt, std::vector< SsaCacheRatio > *cache=nullptr)
SsaSolution ssa_imm_tput_balance(const qn::NetworkStruct< T > &sn, const SsaSolution &in)
Port of ssa_imm_tput_balance.m: the throughput of every station-class pair whose service is Immediate...
qn::NetworkStruct< T > sn_with_ssa_cache_split(const qn::NetworkStruct< T > &base, const std::vector< SsaCacheRatio > &cache)
@@SolverSSA/runAnalyzer itself: the engine the method selects, then the result assembly the reference...
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.
double ssa_resolve_warmupfrac(const Struct &sn, double warmupfrac, bool steady=true)
ssa_warmupfrac.m: the effective warmup (initial-transient) discard fraction.
Definition ssa_types.h:132
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Ports of matlab/src/api/sn/sn_pn_firing_rates.m and sn_pn_avg_rates.m.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
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 on a STOCHASTIC PETRI NET: the port of solver_ssa_nrm_spn, the sub-engine solver_ssa_nrm....
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.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The verdict of sn_fluid_refusal: ok when no rule is broken, else the reason.
static constexpr double FineTol
Definition lang_types.h:765
A node of the network.
std::vector< RoutingStrategy > routing
sn.routing, per class.
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 latency per read request (hits wait 0), EMPTY = not computed
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:71
double warmupfrac
options.config.warmupfrac: the leading fraction of the path discarded before the means are taken.
Definition ssa_types.h:96
What the analyzer returns, in the same shape as the MVA and fluid results.
Definition ssa_types.h:147
std::vector< double > XN
Definition ssa_types.h:149
std::vector< double > CN
Definition ssa_types.h:149
Matrix< double > RN
Definition ssa_types.h:148
Matrix< double > TN
Definition ssa_types.h:148
Matrix< double > QN
Definition ssa_types.h:148