LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
line_cli.cpp
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 *
5 * line-cli: multiprecision C++ front end, flag-compatible with
6 * jar/src/main/java/jline/cli/LineCLI.java.
7 *
8 * The flag surface is the Java one plus --arith and --list-api. Anything not
9 * yet ported is refused explicitly, naming what is missing; nothing is
10 * silently approximated or answered from a partial implementation.
11 *
12 * ONE BINARY FOR BOTH MODEL KINDS, as the Java reference is: a Network model
13 * (-i json) reaches solve_model_dispatch and a layered one (-i lqnx|xml)
14 * reaches solve_lqn_dispatch. They were two executables until the LQN path was
15 * folded in here; the split had no user-visible justification, since the Java
16 * CLI has always taken both from one entry point.
17 */
18#include <algorithm>
19#include <chrono>
20#include <cctype>
21#include <cmath>
22#include <cstdio>
23#include <cstdlib>
24#include <cstring>
25#include <fstream>
26#include <iostream>
27#include <iterator>
28#include <limits>
29#include <map>
30#include <sstream>
31#include <string>
32#include <vector>
33
42#include "line/io/jmt_writer.h"
43#include "line/io/jsim_reader.h"
45#include "line/io/pnml.h"
52#include "line/num/number.h"
54#include "line/reg/registry.h"
96#include "line/cli/cli_run.h"
97#include "line/io/marshal.h"
98#include "line/util/error.h"
99#include "line/util/websocket.h"
100
101namespace {
102
103// THE PRODUCT VERSION, and it must track the other six sites. `-V`, the help banner
104// and `--install` all print it, so a stale value here makes the tree report two
105// different versions depending on which entry point is asked. It sat at 0.1.0
106// while everything else said 3.0.8, because `release/post-release.sh` bumped a
107// list this file was not on; it is on that list now, and on its straggler scan.
108const char* kVersion = "3.0.9";
109
110/**
111 * Whether `-a avg` prints its table as JSON, i.e. `-o json`.
112 *
113 * A FILE-SCOPE FLAG, and the one place in this port that has one. The library
114 * under `include/line/` keeps no global mutable state, deliberately, so a
115 * pybind11 or MEX host can call it reentrantly; `line_cli.cpp` is a `main()`
116 * translation unit and the output format is a property of ONE process
117 * invocation, decided before any solve begins and never changed after. The
118 * alternative is threading a bool through `solve_model_{mva,nc,mam,ba}`,
119 * `solve_ctmc_avg`, `solve_fluid_avg` and `solve_ssa_avg`, three of which do
120 * not take the `Knobs` struct at all, to carry a value none of them varies.
121 */
122bool g_json_output = false;
123
124/**
125 * Where a JSON document goes when a host is collecting them, and nothing when
126 * no host is.
127 *
128 * IT HOLDS STRINGS AND NOT PARSED VALUES, which is the whole reason the sink
129 * sits at the emission sites rather than around them. The eleven sites use
130 * three different indents (`dump()`, `dump(1)`, `dump(2)`) and print their
131 * numbers at full precision precisely so a cross-codebase diff is not capped;
132 * parsing here and re-dumping in the host would silently reformat both, and the
133 * reformatting would stay invisible until someone diffed a golden. Storing the
134 * bytes the CLI printed is also what makes the byte-identity test possible: a
135 * host's document must appear VERBATIM in the binary's stdout.
136 */
137std::vector<std::string>* g_json_sink = nullptr;
138
139/** A host asked, through the C ABI, for the run to stop. */
140struct InterruptRequested : line::ExternalError {
141 explicit InterruptRequested(const std::string& what) : line::ExternalError(line::ErrorKind{"Interrupted"}, what) {}
142};
143
144int (*g_interrupt_cb)(void*) = nullptr;
145void* g_interrupt_user = nullptr;
146
147/**
148 * Poll the host's interrupt, if one was installed.
149 *
150 * Called where the CLI is between two units of work and holds nothing that a
151 * throw would leak: after a document is emitted, and between the analyses of a
152 * comma-separated `-a` list. It deliberately does NOT reach inside a solve.
153 */
154void poll_interrupt() {
155 if (g_interrupt_cb != nullptr && g_interrupt_cb(g_interrupt_user) != 0)
156 throw InterruptRequested("the run was interrupted by the host");
157}
158
159/**
160 * Print one JSON document, and record it when a host is collecting.
161 *
162 * The printf is unchanged and unconditional, so `line-cli` prints exactly what
163 * it printed before this sink existed. That is the acceptance test for this
164 * whole change: with `g_json_sink == nullptr` the binary must be byte-identical.
165 */
166void emit_document(const std::string& text) {
167 if (g_json_sink != nullptr) g_json_sink->push_back(text);
168 std::printf("%s\n", text.c_str());
169 poll_interrupt();
170}
171
172/**
173 * Replace every non-finite number in a document with the spelling this wire
174 * gives one.
175 *
176 * JSON HAS NO INFINITE LITERAL, and nlohmann serialises any non-finite double
177 * as `null`. That is correct for a NaN -- `null` is how this wire says "no
178 * value", and it is what the readable table's `nan` means -- and wrong for an
179 * infinity, which is an ANSWER rather than the absence of one. A queue at or
180 * past its stability boundary has an infinite mean queue length and response
181 * time; a host that reads that `null` as "not computed", or as zero, inverts
182 * the conclusion it draws from it.
183 *
184 * The spelling is the JAR's (`LineCLI.appendDoubleArray`): an infinity rides as
185 * the STRING "Infinity" / "-Infinity", which MATLAB `linemodel_load`, Python
186 * `linemodel_io` and the R `from_wire_num` all accept.
187 *
188 * It walks the document immediately before the dump rather than at each push
189 * site because there are dozens of the latter, spread across every analysis,
190 * and a rule applied at some of them is a rule that holds for some answers.
191 */
192void finitize_document(line::reg::Json& j) {
193 if (j.is_object() || j.is_array()) {
194 for (line::reg::Json& child : j) finitize_document(child);
195 return;
196 }
197 if (!j.is_number_float()) return;
198 const double v = j.get<double>();
199 if (std::isinf(v)) j = (v > 0.0 ? "Infinity" : "-Infinity");
200}
201
202/**
203 * `dump()` with the rule above applied. Takes its document BY VALUE: emission
204 * is not the end of a document's life here (the `-a` comma list emits one per
205 * analysis and several arms read their own back), so rewriting the caller's
206 * copy would change what it sees after printing.
207 */
208std::string dump_document(line::reg::Json j, int indent = -1) {
209 finitize_document(j);
210 return indent < 0 ? j.dump() : j.dump(indent);
211}
212
213/**
214 * The solver knobs the CLI exposes, gathered so the dispatcher can check them
215 * against the solver actually chosen.
216 *
217 * A sentinel means "not given" rather than a default, because the distinction
218 * is what lets an option be REFUSED for a solver that has no such setting
219 * instead of being accepted and dropped. That silent acceptance is the defect
220 * this struct exists to prevent: `--samples 1e6` was previously taken without
221 * complaint and the run still used 10000.
222 */
223struct Knobs {
224 std::string method;
225 double tol = -1.0; // < 0 = not given
226 double iter_tol = -1.0;
227 int iter_max = -1;
228 // --max-states, `options.config.maxStates`: the truncation level of an OPEN
229 // agent's queue-length dimension in SolverAG. < 0 = not given, so AgOptions
230 // keeps its own 100. It is a TRUNCATION and therefore part of the ANSWER,
231 // not a budget: a run truncated at 100 that the caller asked to truncate at
232 // 500 is a different number reported as theirs, which is why it travels
233 // rather than being dropped the way the execution backends are.
234 long long max_states = -1;
235 // --multiserver, the AMVA rule that decides WHICH algorithm serves a
236 // multiserver model. Empty = not given, so the solver keeps its default:
237 // a rule this CLI silently dropped made every delegated solve answer under
238 // 'default' while reporting the caller's choice.
239 std::string multiserver;
240 // --fork-join, `options.config.fork_join`: WHICH fork-join arm the shared
241 // fixed point takes on a model with a Fork. Empty = not given, so the solver
242 // keeps 'default' (the MMT transform); 'ht' is Heidelberger-Trivedi, which
243 // is a different answer to the same model rather than a faster one.
244 std::string fork_join;
245 // The solver console has NO knob of its own: it IS VerboseLevel::DEBUG,
246 // so `-v debug` is what asks for the running progress log.
247 std::size_t samples = 0; // 0 = not given
248 unsigned long seed = 0;
249 double cutoff = -1.0; // < 0 = not given
250 // --cutoff AS A MATRIX, `r1c1,r1c2;r2c1,r2c2`: the reference's
251 // `options.cutoff` is a (station x class) table wherever a model needs a
252 // different truncation per station, and reading only the scalar form left
253 // `atof` silently taking the first number, i.e. truncating every station at
254 // the first station's first class. Empty = not given.
255 std::vector<std::vector<std::size_t>> cutoff_mat;
256 /** True when `--cutoff` was given in EITHER spelling. */
257 bool has_cutoff() const { return cutoff >= 0.0 || !cutoff_mat.empty(); }
258 // --force, `options.force`: downgrade SolverCTMC's memory pre-gate from a
259 // refusal to a warning. The gate exists because the alternative to refusing
260 // is the OOM killer, so this is opt-in and never a default.
261 bool force = false;
262 // --fj-accuracy / --fj-tmode, `options.config.fj_accuracy` and
263 // `options.config.fj_tmode` of solver_mam_fj.m. The first is the FJ_codes
264 // truncation C of the queue-length difference between the two branches and
265 // is the accuracy knob of that approximation; the second picks the route to
266 // the T matrix. Kept apart from --tol/--iter_max for the same reason
267 // --mdd-tol is: C is a STATE-SPACE bound, not a convergence threshold.
268 int fj_accuracy = 0; // 0 = not given
269 std::string fj_tmode; // empty = not given
270 // --sync-phases, `options.config.sync_phases` of the MAM 'sync.faster' /
271 // 'sync.slower' methods: the synchronous phase expansion, 0 = none.
272 int sync_phases = -1; // -1 = not given
273 // --mapmsg-quantization, `options.config.mapmsg_quantization` of the MAM
274 // MAP/M/s+G reneging analyzer: the regime count of the patience CDF.
275 int mapmsg_quantization = -1; // -1 = not given
276 // --timescale, `options.config.timescale` of sn_is_discrete_time.m. "auto"
277 // lets the distributions decide whether the model is slotted; "discrete"
278 // and "continuous" force the reading, the first raising when the model
279 // mixes lattice and non-lattice laws rather than solving the wrong time
280 // scale. The slot itself comes from the SHARED `--slotlength` below, the
281 // same one SolverNC's discrete product form reads.
282 std::string timescale; // empty = not given
283 // --mdd-tol / --mdd-maxiter, the level iteration of `-s ctmc --method mdd`.
284 // They are DELIBERATELY not --tol / --iter_tol: that iteration is an INNER
285 // numerical solve whose fixed point is verified against the model's
286 // population invariant at 1e-6, so an AMVA-sized tolerance converges short
287 // of it and trips the guard. The reference keeps them apart for the same
288 // reason (options.config.mdd_tol, not options.iter_tol).
289 double mdd_tol = -1.0; // < 0 = not given
290 int mdd_maxiter = 0; // 0 = not given
291 // --level, the hierarchy level of the pbh/bjbh/cbh/sib families.
292 // 0 = not given, so BaOptions keeps its 2.
293 int level = 0;
294 // --busyperiod / --busyperiod-subnet, the orders and the subnetwork of
295 // `-a busyperiod`. The flag names are `ldes_cli`'s, so ONE spelling drives
296 // the transform (solver_nc_busyp) and the simulation. The subnetwork has no
297 // default -- a busy period is defined for a named set of stations and
298 // choosing one here would answer about a subnetwork the caller never
299 // named -- while the orders default to the ordinary busy period, 1.
300 std::vector<std::size_t> busy_orders;
301 std::vector<std::size_t> busy_subnet;
302 // --qrf-params / --qrf-alpha, the blocking parameterisation and the
303 // load-dependent scaling of the QRF arms of SolverBA. Both are JSON, given
304 // inline or as a path; empty = not given.
305 std::string qrf_params;
306 std::string qrf_alpha;
307 // --tspan, the horizon the transient CTMC analyses integrate over. There is
308 // no default: pi(t) on an unstated horizon is not a quantity, and picking
309 // one here would answer a question the caller did not ask.
310 double t0 = 0.0, t1 = -1.0; // t1 < 0 = not given
311 std::size_t node = 0; // --node, 1-based; 0 = not given
312 // --class and --marg-states, the second and third arguments of
313 // `@@SolverMVA/getProbMarg`. The class is 1-based and 0 = not given, i.e.
314 // every class; the state list is the reference's `state_m` and empty = not
315 // given, i.e. the default range each of the three laws picks for itself.
316 // They are NOT folded into --node: a marginal is indexed by a PAIR, and one
317 // flag carrying both would make "station 2" and "class 2" the same token.
318 std::size_t jobclass = 0; // --class, 1-based; 0 = not given
319 std::vector<long> marg_states; // --marg-states; empty = not given
320 // --warmupfrac, `options.config.warmupfrac`: the leading fraction of an SSA
321 // path discarded before the means are taken. The JAR CLI has carried it
322 // since the SSA branch existed and this port did not, so a delegated solve
323 // asking for a warmup discard silently kept the whole transient.
324 // < 0 = not given, left unset: `ssa_resolve_warmupfrac` then picks 0.2 for a
325 // model with an open class and 0 otherwise.
326 double warmupfrac = -1.0;
327 // --pstar, `options.config.pstar`: the exponent of the fluid p-norm
328 // smoothing (Ruuskanen et al., PEVA 151 (2021), eq. (26)). It selects the
329 // DRIFT the matrix method integrates, so a solve that never receives it
330 // returns the unsmoothed mean-field fixed point under the caller's choice.
331 // < 0 = not given.
332 double pstar = -1.0;
333 // --fluid-boundary, `options.config.fluid_boundary`: how the fluid Petri `dae` route reports a fluid place
334 // (FSPN) level held at a bound, 'reflect' (the bound) or 'diffusion' (a reflected Brownian motion's mean).
335 // Empty = not given.
336 std::string fluid_boundary;
337 // --nonmkv, `options.config.nonmkv`: 'bernstein' fits a phase-type surrogate to a non-Markovian
338 // law before -s ctmc, mam, fluid and ag solve, 'none' skips the fit. Empty = not given.
339 std::string nonmkv;
340 // --notation, which form of the exported ODE document is wanted; empty = not
341 // given, and only `-a odes` has one.
342 std::string notation;
343 // --cdf-algorithm, `options.config.algorithm` of `@@SolverNC/getCdfRespT`:
344 // 'exact' is the pfqn_stdf sojourn-time inversion, 'rd' the pfqn_stdf_heur
345 // reduction. Empty = not given, so the solver keeps the reference's 'exact'.
346 // It is NOT --method: the ladder that computes the constants and the
347 // algorithm that inverts the sojourn law are separate choices, and folding
348 // them would make one name silently select the other.
349 std::string cdf_algorithm;
350 // -s ctmc -a firstpasst: the two state sets of `getCdfFirstPassT(A, B)`.
351 // Each is either a 1-based index list into the state space ("3,5") or
352 // semicolon-separated state rows ("0,2;1,1"), resolved against the space
353 // the engine enumerates -- rows travel across the boundary because the two
354 // enumerations need not order (or even purge) states identically.
355 std::string passage_from;
356 std::string passage_into;
357 // "expm" (default) or "lt", `options.config.passage_method` of the reference
358 std::string passage_method;
359 // -s ctmc -a firstpasstmom: the highest moment order, `nmax` of
360 // `getFirstPassTMoments(A, B, nmax)`. 0 means the reference's default of 3.
361 std::size_t passage_orders = 0;
362 // --passage-taboo: the TABOO set C of `getCdfFirstPassT(A, B, C)` and
363 // `getFirstPassTMoments(A, B, nmax, C)`, in the --passage-into syntax. A
364 // passage entering C before B is lost. Empty = no taboo set.
365 std::string passage_taboo;
366 // --passage-tset: the time grid of `getCdfFirstPassT(A, B, C, tset)`, comma
367 // separated, 'inf' allowed. Empty = not given, the default horizon grid.
368 std::string passage_tset;
369 // --perm-engine, the permanent estimator of `@@SolverNC/getProbSysMarg.m`:
370 // 'exact' is Ryser's expansion with column multiplicities, and 'spm',
371 // 'bethe', 'heur', 'huberlaw', 'adapart' are the five approximations. It is
372 // NOT --method: the ladder that computes the normalizing constant and the
373 // estimator that evaluates the permanent are separate choices. The five
374 // approximations REFUSE a demand matrix with a structural zero rather than
375 // flooring it, since they need full support. 'spm' is the only one that does
376 // not expand the matrix to order sum(N), so it is the one whose cost does
377 // not grow with the population and whose error falls as it grows.
378 std::string method_perm = "exact";
379 // --symbolic, `options.config.symbolic` of `@@SolverFLD/getJacobian`: auto
380 // to search for a line-sage-rest backend, a URL, an image name, or none to
381 // stay with the locally differentiated Jacobian. Empty = not given, i.e.
382 // auto. --equilibria is the reference's fourth output, which is REQUESTED
383 // and not implied: solving f(x) = 0 needs the backend, so implying it would
384 // turn a Jacobian this port answers on its own into one that fails without
385 // a container.
386 std::string symbolic;
387 bool equilibria = false;
388 // ---- the layered path's own knobs, on the same not-given discipline ----
389 // They are refused on the Network path rather than dropped, exactly as the
390 // ones above are refused for a solver that has no such setting.
391 bool no_interlocking = false; // --no-interlocking was passed
392 // --interlock-method / --interlock-maxpaths / --interlock-refpath-scope, the
393 // config.interlock_* of SolverLN. Empty / negative = not given.
394 std::string interlock_method;
395 double interlock_maxpaths = -1.0;
396 std::string interlock_refpath_scope;
397 bool interlock_knobs_given() const {
398 return !interlock_method.empty() || interlock_maxpaths >= 0.0 ||
399 !interlock_refpath_scope.empty();
400 }
401 // --lld STATION=EXPR (repeatable), --nlp-alpha, --nlp-warm, --nlp-maxiter,
402 // --nlp-fork, --warmstart, --mol-iter-max, --mol-iter-tol,
403 // --mol-relax-factor: config.lld / nlp_* / warmstart / mol_* of SolverLN.
404 // Empty / negative = not given.
405 std::map<std::string, std::string> lld;
406 double nlp_alpha = -1.0;
407 long nlp_warm = -1;
408 long nlp_maxiter = -1;
409 std::string nlp_fork;
410 std::string warmstart;
411 long mol_iter_max = -1;
412 double mol_iter_tol = -1.0;
413 double mol_relax_factor = -1.0;
414 bool nlp_knobs_given() const {
415 return !lld.empty() || nlp_alpha >= 0.0 || nlp_warm >= 0 || nlp_maxiter >= 0 || !nlp_fork.empty() ||
416 !warmstart.empty() || mol_iter_max >= 0 || mol_iter_tol >= 0.0 || mol_relax_factor >= 0.0;
417 }
418 int repeat = 0; // --repeat; 0 = not given, i.e. one run
419 std::string layer_solver; // --layer-solver; empty = not given
420 /**
421 * `--stage-solver`, which solver runs each stage of an ENVIRONMENT.
422 *
423 * IT IS NOT `--layer-solver`, and folding the two would be wrong: a LAYER of
424 * an LQN is solved in STEADY STATE and a STAGE of a random environment is
425 * solved TRANSIENTLY, so their admissible solver sets are different sets for
426 * different reasons. Empty = not given, i.e. the coupling's own default
427 * (fluid for the mean-field one, ctmc for the state-vector one). `mva` and
428 * `nc` have no transient and serve the 'avg'/'dec' limits only.
429 */
430 std::string stage_solver;
431 /*
432 * --env-init: `options.config.env_init` of SolverENV method 'occupation',
433 * the initial probability of each stage as a comma-separated list. Empty =
434 * not given, i.e. the first stage.
435 */
436 std::string env_init;
437 /*
438 * --map-env / --map-env-method / --map-env-maxstages: the random-environment
439 * FALLBACK for a model whose only unsupported feature is a non-renewal
440 * process. Unlike `--stage-solver` these are not `-s env` knobs: they apply
441 * to whichever solver was asked for, because the fallback is decided above
442 * the runner, in the `getAvg` funnel that `run_avg_engine` plays here.
443 * Empty / 0 = not given.
444 */
445 std::string map_env;
446 std::string map_env_method;
447 std::size_t map_env_maxstages = 0;
448 // --ln-transient / --ln-transient-channels, the coupling of the layered
449 // transient and which inter-layer channels it injects. Empty = not given.
450 std::string ln_transient;
451 std::string ln_transient_channels;
452 // --sens-method / --sens-scheme / --sens-step, the name-value contract of
453 // getSensitivityTable. They are NOT --method: --method names the LN update
454 // (default / moment3 / mw.*), and the branch that differentiates it is a
455 // separate choice. Empty / <= 0 = not given.
456 std::string sens_method;
457 std::string sens_scheme;
458 double sens_step = -1.0;
459 // --uq-solver, the engine SolverUQ runs at each design point. Empty is not
460 // a default here but a missing argument: UQ computes nothing itself, so
461 // there is no engine to fall back to, and `-s uq` without it is refused.
462 std::string uq_solver;
463 // --tran-points, the resolution of the uniform transient grid the ENV
464 // mean-field coupling sums its exit metrics over. It is that coupling's
465 // accuracy knob, not a cosmetic one: the sum is a Riemann-Stieltjes
466 // quadrature against the holding-time CDF, so the answer moves with the
467 // grid. 0 = not given, i.e. EnvOptions' own default.
468 std::size_t tran_points = 0;
469 // ---- the LQNS wrapper's own knobs -------------------------------------
470 // `options.keep` and `options.verbose` of the reference wrapper, plus the
471 // two that pick a REMOTE lqns. They apply to `-s lqns` alone and are
472 // refused elsewhere: nothing else in this CLI runs a child process whose
473 // working directory a caller might want to inspect.
474 bool keep = false;
475 bool verbose = false;
476 bool remote = false;
477 std::string remote_url; // empty = not given, i.e. the wrapper's default
478 int timeout_seconds = 0; // 0 = not given, i.e. no deadline
479 // ---- the simulator's own knobs, all `--ldes-*` -------------------------
480 // They are PREFIXED rather than folded into the shared names because they
481 // are an external engine's settings and not this port's: --ldes-tranfilter
482 // is the warmup filter of a simulation run and has nothing to do with
483 // --tol, and an unprefixed --warmupfrac would read as a knob every solver
484 // has. Same not-given discipline as the rest -- empty, <= 0 or false means
485 // not given -- so the engine keeps its own default and the command line
486 // stays minimal, which is what an older AOT native binary can still parse.
487 std::string ldes_tranfilter; // mser5 | fixed | none
488 double ldes_warmupfrac = -1.0;
489 std::string ldes_cimethod; // obm | bm | spectral | none
490 bool ldes_cnvgon = false;
491 double ldes_cnvgtol = -1.0;
492 bool ldes_slotted = false;
493 double ldes_slotlength = -1.0;
494 /**
495 * `--slotted` / `--slotlength`: the discrete time scale for the
496 * ANALYTICAL solvers, distinct from the `--ldes-*` pair above, which
497 * configures the simulator. SolverNC reads it and routes to the
498 * discrete-time product form.
499 */
500 bool slotted = false;
501 double slotlength = -1.0;
502 int ldes_replications = 0;
503 int ldes_numthreads = 0;
504 double ldes_maxtime = -1.0;
505 std::vector<double> ldes_initsol; // station-major warm-start placement
506 std::string ldes_rest_url;
507 /**
508 * `--jmt-replications`, SolverJMT's `options.config.replications`: the JSIM runs the transient
509 * ensemble of -a tran / -a tranprob averages over. 0 keeps the engine default of 10.
510 */
511 int jmt_replications = 0;
512 // ---- the JAR CLI's own five, ported so `line-cli` answers every question
513 // `jline.cli.LineCLI` answers ------------------------------------------
514 /**
515 * `--state`, the state vector `-a prob` asks about.
516 *
517 * WITHOUT IT THE QUERY IS ABOUT THE MODEL'S DEFAULT INITIAL STATE, which is
518 * what this CLI reported before and remains the default. The JAR takes an
519 * explicit one because `getProb(node, state)` is a different question from
520 * `getProb(node)`: the second names a state the model already holds, the
521 * first names any state of the node's own space, and a caller sweeping a
522 * marginal law needs the first. Empty = not given.
523 */
524 std::vector<long> state;
525 /**
526 * `--events`, the length of a sampled trajectory, `options.samples` of
527 * `@@SolverSSA/sample`. It is NOT `--samples`: the JAR keeps them apart
528 * because `--samples` is a simulation run length or a Monte Carlo draw
529 * count and reaches a solver's options, while this is the number of EVENTS
530 * one `sample` call walks. Folding them would make `-s ssa -a avg --events`
531 * silently lengthen the run. 0 = not given, i.e. the reference's 1000.
532 */
533 std::size_t events = 0;
534 /**
535 * `--timestep`, the fixed output step of a transient analysis. Without it
536 * the grid is whatever the integrator chose, which is the reference's
537 * adaptive default; with it the trajectory is resampled onto a uniform
538 * lattice of that step, which is what a caller diffing two transients needs.
539 * <= 0 = not given.
540 */
541 double timestep = -1.0;
542 /**
543 * `--transient-method`, `options.config.transient_method` of
544 * `solver_ctmc_transient_analyzer.m`: "ode" integrates the forward
545 * equation, "fau" marches fast adaptive uniformization over the output
546 * grid. Empty = not given, so the solver keeps the reference's "ode".
547 *
548 * It is NOT `--method`: the state-space path and the way the forward
549 * equation is advanced on it are separate choices, and the reference keeps
550 * this one out of its valid-method list because it changes no stationary
551 * answer.
552 */
553 std::string transient_method;
554 /**
555 * `--fau-epsilon` and `--fau-delta`, the two tolerances of the "fau"
556 * transient: the total probability mass the grid may discard, and the
557 * occupancy below which a state is dropped from the support. <= 0 = not
558 * given, i.e. the reference's 1e-6 and 1e-12.
559 */
560 double fau_epsilon = -1.0;
561 double fau_delta = -1.0;
562 /**
563 * `--rate-sched`, `options.config.rate_sched` of
564 * `solver_ctmc_transient_analyzer.m`: a JSON array (inline, or the path of a
565 * file holding it) of {station, class, tgrid, rates[, nominal]} objects,
566 * with station and class given by name or 1-based index. Empty = not given.
567 */
568 std::string rate_sched;
569 /// `--ctmc-tv-ngrid`, `options.config.ctmc_tv_ngrid`; 0 = not given (100).
570 std::size_t ctmc_tv_ngrid = 0;
571 /**
572 * `--percentiles`, the levels `getPerctRespT` is read at. Empty = not
573 * given, i.e. the reference's `pers_stored` {0.50, 0.90, 0.95, 0.99}.
574 * Accepted as fractions (0.9) or as percents (90), told apart by magnitude:
575 * a level above 1 cannot be a probability.
576 */
577 std::vector<double> percentiles;
578 /**
579 * `-v/--verbosity`: silent | standard | debug.
580 *
581 * IT WAS ACCEPTED AND DISCARDED, which is the silent-acceptance defect this
582 * struct exists to prevent one layer up: a caller who asked for `silent`
583 * still received every warning the arms print on stderr. It gates them now,
584 * and nothing else -- the tables on stdout are the answer and are printed
585 * whatever the level, exactly as the JAR prints them.
586 */
587 std::string verbosity;
588 /**
589 * `--reward-name`, which declared reward `-a reward-value` returns the
590 * value function of. Empty = not given, and `-a reward-value` without it is
591 * refused rather than defaulted to the first reward: the value functions of
592 * two rewards are different objects, and picking one silently would label
593 * the wrong matrix with the caller's question.
594 */
595 std::string reward_name;
596};
597
598/**
599 * The three `--map-env*` knobs, as the gate wants them.
600 *
601 * One helper rather than three assignments per arm, for the reason the Knobs
602 * struct itself exists: the fallback is decided in every `-a avg` ladder and in
603 * `run_avg_engine`, and an arm that read a different subset of the three would
604 * accept a different model for the same command line.
605 */
606inline line::solvers::MapEnvConfig map_env_config(const Knobs& k) {
608 if (!k.map_env.empty()) c.mode = k.map_env;
609 if (!k.map_env_method.empty()) c.method = k.map_env_method;
610 if (k.map_env_maxstages) c.max_stages = k.map_env_maxstages;
611 return c;
612}
613
614/**
615 * `--cutoff` written as a per-(station,class) matrix, `';'`-separated rows of
616 * `','`-separated non-negative counts. Empty on anything that is not one.
617 *
618 * The spelling is the JAR CLI's, so one example pins one string for both
619 * engines. A zero entry is legal and means the station may not hold that class
620 * at all, which is how the reference bounds a queue in the classes it does not
621 * serve.
622 */
623std::vector<std::vector<std::size_t>> parse_cutoff_matrix(const std::string& s) {
624 std::vector<std::vector<std::size_t>> out;
625 std::string::size_type pos = 0;
626 while (pos <= s.size()) {
627 const std::string::size_type semi = s.find(';', pos);
628 const std::string row = s.substr(pos, semi == std::string::npos ? std::string::npos
629 : semi - pos);
630 std::vector<std::size_t> cells;
631 std::string::size_type cp = 0;
632 while (cp <= row.size()) {
633 const std::string::size_type comma = row.find(',', cp);
634 const std::string cell = row.substr(cp, comma == std::string::npos ? std::string::npos
635 : comma - cp);
636 if (cell.empty()) return std::vector<std::vector<std::size_t>>();
637 for (std::string::size_type i = 0; i < cell.size(); ++i)
638 if (!std::isdigit(static_cast<unsigned char>(cell[i])))
639 return std::vector<std::vector<std::size_t>>();
640 cells.push_back(static_cast<std::size_t>(std::atol(cell.c_str())));
641 if (comma == std::string::npos) break;
642 cp = comma + 1;
643 }
644 if (cells.empty()) return std::vector<std::vector<std::size_t>>();
645 if (!out.empty() && cells.size() != out[0].size())
646 return std::vector<std::vector<std::size_t>>();
647 out.push_back(cells);
648 if (semi == std::string::npos) break;
649 pos = semi + 1;
650 }
651 return out;
652}
653
654/**
655 * The model text piped on stdin, drained ONCE and kept.
656 *
657 * `-s auto` parses the model twice: once to choose an engine and once for the
658 * engine to solve. A stream can only be drained once, so without this buffer the
659 * second parse would see nothing and the chooser would be unusable on a piped
660 * model. It is deliberately not a function template -- a static local inside one
661 * would be per-instantiation, and the two parses need not share an arithmetic.
662 */
663std::string g_stdin_buf;
664bool g_stdin_loaded = false;
665
666/**
667 * The model text a host supplied INSTEAD of the process's standard input.
668 *
669 * A library has no standard input worth reading: R's is the user's terminal,
670 * and draining it would hang the session. When a host passes a document it is
671 * installed here, and `stdin_model_text` returns it without touching std::cin.
672 */
673void set_stdin_override(const std::string& text) {
674 g_stdin_buf = text;
675 g_stdin_loaded = true;
676}
677
678const std::string& stdin_model_text() {
679 if (!g_stdin_loaded) {
680 g_stdin_buf.assign(std::istreambuf_iterator<char>(std::cin),
681 std::istreambuf_iterator<char>());
682 g_stdin_loaded = true;
683 }
684 return g_stdin_buf;
685}
686
687/**
688 * Whether `-i` selected a JMT document rather than a model.json.
689 *
690 * A FILE-SCOPE FLAG AND NOT A PARAMETER, because `read_model` is called from
691 * every solver arm and threading the format through all of them would touch
692 * fifty signatures to carry one bit that main already knows. It is set once,
693 * before any arm runs, and never again.
694 */
695bool g_jsim_input = false;
696
697/**
698 * Whether `-i` selected a PNML place/transition net.
699 *
700 * A second flag rather than a format string for the same reason `g_jsim_input`
701 * is one: `read_model` is called from every solver arm, and the three input
702 * kinds it can be handed are mutually exclusive, so two booleans say what a
703 * threaded enum would and touch no signature. Set once in main.
704 */
705bool g_pnml_input = false;
706
707/**
708 * `-v/--verbosity`, hoisted to file scope for the same reason as `g_jsim_input`.
709 *
710 * The knobs carry it too, but the priority warning below is raised where the
711 * model is READ rather than inside a solver arm, and that function receives no
712 * knobs. Set once in main, before any arm runs.
713 */
714std::string g_verbosity = "standard";
715
716/**
717 * Put every piece of process state back where a fresh process would have it.
718 *
719 * THESE ARE DEFECTS AND NOT HYGIENE, and one of them is live in shipped code:
720 * `run_invocation_captured` serves one request per connection on the same
721 * process, so a request carrying `-i jsimg` used to set `g_jsim_input` and
722 * every later request on that server took the JSIM reader for a model.json.
723 * The header on `g_jsim_input` states the assumption exactly -- "It is set
724 * once, before any arm runs, and never again" -- which is true of a process
725 * that runs one invocation and exits, and false of every other caller.
726 *
727 * The stdin buffer is reset only when no host override is pending, because a
728 * host that just supplied a document means it for the call about to run.
729 */
730void reset_invocation_state(bool keep_stdin_override) {
731 g_json_output = false;
732 g_jsim_input = false;
733 g_pnml_input = false;
734 g_verbosity = "standard";
735 if (!keep_stdin_override) {
736 g_stdin_buf.clear();
737 g_stdin_loaded = false;
738 }
739 // The console keeps the level of the LAST run, so a `-v debug` call would
740 // narrate the one after it. LineConsole::reset() drops any open run; the
741 // level is set again from g_verbosity by every entry point below.
743}
744
745/**
746 * Say so when class priorities were declared and no station will read them.
747 *
748 * The twin of the block at the tail of MATLAB's `@MNetwork/refreshStruct.m` and
749 * of `Network.refreshStruct` in the JAR. It is a WARNING and not a refusal: a
750 * priority at a station whose discipline ignores one is a legitimate model --
751 * `prio_hol_open` is built on it -- and only becomes a defect when NO station
752 * reads it, at which point the metrics are not the priority ones the caller is
753 * about to read them as. Priority-awareness is a property of the declared
754 * policy and is never inferred from the data; see network_struct.h.
755 */
756template <class T>
757void warn_priorities_ignored(line::qn::Network<T>& net) {
758 if (g_verbosity == "silent") return;
759 // `raw_struct` and not `get_struct`: the priorities and the disciplines are
760 // written when the classes and stations are added, and nothing in the
761 // refresh chain touches either, so reading them here costs no refresh.
762 if (!net.raw_struct().priorities_ignored()) return;
763 std::fprintf(stderr,
764 "Warning: Priority classes are specified but no priority-aware scheduling "
765 "policy (PSPRIO, DPSPRIO, GPSPRIO, HOL, FCFSPRIO, FCFSPRPRIO, FCFSPIPRIO, "
766 "LCFSPRIO, LCFSPRPRIO, LCFSPIPRIO, SRPTPRIO) is used in the model. "
767 "Priorities will be ignored.\n");
768}
769
770/** Read the model named by `file`, or stdin when it is empty. */
771template <class T>
772line::qn::Network<T> read_model(const std::string& file) {
773 if (g_pnml_input) {
774 // The PNML reader walks a DOM and has no stdin form, so a piped document
775 // is staged to a temporary file, as the JSIM arm below does.
776 if (!file.empty()) {
778 warn_priorities_ignored(net);
779 return net;
780 }
781 const std::string tmp = line::io::jsim_stage_stdin(stdin_model_text());
783 std::remove(tmp.c_str());
784 warn_priorities_ignored(net);
785 return net;
786 }
787 if (g_jsim_input) {
788 // The JSIM reader walks a DOM and has no stdin form, so a piped JMT
789 // document is staged to a temporary file rather than refused: `cat
790 // model.jsimg | line-cli -i jsimg` is how the JAR CLI is used and the
791 // Docker image documents it.
792 if (!file.empty()) {
794 warn_priorities_ignored(net);
795 return net;
796 }
797 const std::string tmp = line::io::jsim_stage_stdin(stdin_model_text());
799 std::remove(tmp.c_str());
800 warn_priorities_ignored(net);
801 return net;
802 }
803 if (!file.empty()) {
805 warn_priorities_ignored(net);
806 return net;
807 }
808 std::istringstream in(stdin_model_text());
809 line::io::detail::json root;
810 in >> root;
812 warn_priorities_ignored(net);
813 return net;
814}
815
816/** Read the Environment model.json named by `file`, or stdin when it is empty. */
817template <class T>
818line::env::Environment<T> read_env_model(const std::string& file) {
819 if (!file.empty()) return line::io::read_environment_json<T>(file);
820 std::istringstream in(stdin_model_text());
821 line::io::detail::json root;
822 in >> root;
824}
825
826/** One row of an average table, already reduced to double. */
827struct AvgRow {
828 double q, u, r, w, a, t;
829};
830
831/**
832 * Render an average table, as text or as the JSON the hosts parse.
833 *
834 * THE ONE PLACE `-o json` IS HONOURED, reached by every solver arm through a
835 * per-(station,class) accessor. Do not give a solution type its own printer: a
836 * per-solver printer is how `-o json` came to be accepted and ignored.
837 *
838 * THE JSON FORM IS THE JAR's, key for key. `jline.cli.LineCLI -a avg -o json`
839 * emits {"avg": {"type": "AvgTable", "Station": [...], "JobClass": [...],
840 * "QLen": [...], ...}}, column-oriented, and the Python wrapper's
841 * `station_matrices_via_jar` parses exactly that shape. Emitting anything else
842 * would force a second parser into the wrapper for a table that is the same
843 * table, so the host can treat `lang='cpp'` and `lang='java'` as one transport
844 * with two binaries. The JAR's `data` key (the rendered text) is NOT reproduced:
845 * no caller reads it, and a second rendering of the same numbers is one more
846 * thing that can disagree with the first.
847 *
848 * The rows are also byte-identical across the solvers that share it, which
849 * matters more than it looks: `parity/compare_parity.py` diffs these tables
850 * column by column, and a solver whose printer drifted by a space would read as
851 * a parity failure in a harness that is supposed to be measuring the numbers.
852 */
853template <class Get>
854void emit_avg_table_named(const std::vector<std::string>& stations,
855 const std::vector<std::string>& classes, const char* arith,
856 const std::string& method, Get get, const line::reg::Json& extra,
857 const line::reg::Json& envelope) {
858 line::reg::Json rows = line::reg::Json::object();
859 for (const char* key : {"Station", "JobClass", "QLen", "Util", "RespT", "ResidT", "ArvR",
860 "Tput"})
861 rows[key] = line::reg::Json::array();
862 rows["type"] = "AvgTable";
863
864 if (!g_json_output)
865 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s\n", "Station", "JobClass", "QLen",
866 "Util", "RespT", "ResidT", "ArvR", "Tput");
867 for (std::size_t i = 0; i < stations.size(); ++i) {
868 for (std::size_t c = 0; c < classes.size(); ++c) {
869 const AvgRow v = get(i, c);
870 if (v.q == 0.0 && v.u == 0.0 && v.r == 0.0 && v.w == 0.0 && v.a == 0.0 && v.t == 0.0)
871 continue;
872 if (!g_json_output) {
873 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
874 stations[i].c_str(), classes[c].c_str(), v.q, v.u, v.r, v.w, v.a, v.t);
875 continue;
876 }
877 // FULL PRECISION on the JSON path, against the readable path's
878 // %12.6g. The table is for a human to read; the JSON is for a host
879 // to compare against another codebase's answer, and six digits
880 // would cap any parity check at six digits.
881 rows["Station"].push_back(stations[i]);
882 rows["JobClass"].push_back(classes[c]);
883 rows["QLen"].push_back(v.q);
884 rows["Util"].push_back(v.u);
885 rows["RespT"].push_back(v.r);
886 rows["ResidT"].push_back(v.w);
887 rows["ArvR"].push_back(v.a);
888 rows["Tput"].push_back(v.t);
889 }
890 }
891 if (g_json_output) {
892 // `extra` NESTS INSIDE the "avg" payload, for emit_analysis's reason: a
893 // solver-specific key at envelope level can collide with an analysis's
894 // own name, and a per-list cost qualifies THIS answer.
895 for (line::reg::Json::const_iterator it = extra.begin(); it != extra.end(); ++it)
896 rows[it.key()] = it.value();
897 line::reg::Json out = line::reg::Json::object();
898 out["avg"] = rows;
899 out["arith"] = arith;
900 out["method"] = method;
901 // `envelope`, unlike `extra`, sits BESIDE "avg" rather than inside it.
902 // Provenance of the solve as a whole belongs there: the iteration count
903 // and the convergence flag qualify the answer, not any one row, and a
904 // host reads them where the JAR CLI puts them.
905 for (line::reg::Json::const_iterator it = envelope.begin(); it != envelope.end(); ++it)
906 out[it.key()] = it.value();
907 emit_document(dump_document(out));
908 }
909}
910
911/**
912 * The same table, labelled from a struct.
913 *
914 * THE NAMES, NOT THE STRUCT, are what the printer needs, and one solver has no
915 * struct to give it: `-s ldes` forwards the model document to an external engine
916 * and is labelled from the names that engine reports, precisely so a model this
917 * port cannot itself parse still prints the same table. Everything else calls
918 * this overload and nothing about those rows changes.
919 */
920template <class T, class Get>
921void emit_avg_table(const line::qn::NetworkStruct<T>& sn, const std::string& method, Get get,
922 const line::reg::Json& extra = line::reg::Json::object(),
923 const line::reg::Json& envelope = line::reg::Json::object()) {
924 std::vector<std::string> stations, classes;
925 stations.reserve(sn.nstations);
926 classes.reserve(sn.nclasses);
927 for (std::size_t i = 0; i < sn.nstations; ++i) stations.push_back(sn.stations[i].name);
928 for (std::size_t c = 0; c < sn.nclasses; ++c) classes.push_back(sn.classes[c].name);
929 emit_avg_table_named(stations, classes, line::num_traits<T>::name(), method, get, extra,
930 envelope);
931}
932
933/** NaN as this arithmetic spells it, the reference's "not computed" marker. */
934template <class T>
935T cache_nan() {
936 return line::num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
937}
938
939/** `v[r]` when the vector reaches r, NaN otherwise -- the reference's nanGetAt. */
940template <class T>
941double cache_at(const std::vector<T>& v, std::size_t r) {
942 if (r >= v.size()) return std::numeric_limits<double>::quiet_NaN();
944}
945
946/**
947 * The readable cache rows of a solve that reports beside the AvgTable.
948 *
949 * A Cache answers in (node, read class) and in (node, read class, list), which
950 * the AvgTable cannot carry, so a caller that only got the AvgTable was told
951 * nothing about the cache it asked about. `-a cache` has its own richer table
952 * built from the model's Source rates; this one reports the surface alone and
953 * is what the `-s env` arm uses, where there is no single model whose Source
954 * rate describes the blend.
955 *
956 * ABSENT PRINTS NaN, NEVER 0: an empty field is a coupling that did not measure
957 * the quantity, and a zero would read as a cache that never hits.
958 */
959template <class T>
960void print_cache_rows(const line::qn::NetworkStruct<T>& sn,
961 const line::solvers::CacheMetrics<T>& cache) {
962 if (cache.empty()) return;
963 const double dnan = std::numeric_limits<double>::quiet_NaN();
964 std::printf("\n%-12s %-12s %5s %12s %12s %12s\n", "Cache", "JobClass", "List", "HitProb",
965 "DelayedHitP", "MissProb");
966 for (std::size_t c = 0; c < cache.caches.size(); ++c) {
967 const line::solvers::CacheNodeMetrics<T>& m = cache.caches[c];
968 const typename std::map<std::size_t, line::qn::CacheParam<T> >::const_iterator it =
969 sn.nodeparam.find(m.node);
970 if (it == sn.nodeparam.end()) continue;
971 const std::vector<std::size_t>& hitclass = it->second.hitclass;
972 for (std::size_t cl = 0; cl < sn.nclasses; ++cl) {
973 // A READ CLASS IS ONE WITH A HIT CLASS DEFINED, the same rule the
974 // `-a cache` table applies; every other class has no ratio of its
975 // own and gets no row rather than a row of zeros.
976 if (cl >= hitclass.size() || hitclass[cl] == 0) continue;
977 const double ph = cache_at(m.hitprob, cl), pm = cache_at(m.missprob, cl),
978 pd = cache_at(m.delayedprob, cl);
979 std::printf("%-12s %-12s %5d %12.6g %12.6g %12.6g\n", m.name.c_str(),
980 sn.classes[cl].name.c_str(), 0, ph, pd, pm);
981 for (std::size_t l = 0; l < m.hitproblist.cols() && cl < m.hitproblist.rows(); ++l)
982 std::printf("%-12s %-12s %5d %12.6g %12.6g %12.6g\n", m.name.c_str(),
983 sn.classes[cl].name.c_str(), static_cast<int>(l + 1),
984 line::num_traits<T>::to_double(m.hitproblist(cl, l)), dnan, dnan);
985 }
986 }
987}
988
989/**
990 * Print one analysis that is NOT the average table, as the JSON a host parses.
991 *
992 * ONE ENVELOPE FOR EVERY ANALYSIS: the payload sits under a key named after the
993 * `-a` it answers, and the arithmetic and the resolved method sit beside it,
994 * exactly as `emit_avg_table` places them beside "avg". A host therefore reads
995 * the provenance the same way whatever it asked for, and a caller that asked for
996 * `-a cdf` and received a "states" key knows the answer is not its own instead of
997 * misreading the numbers as its own.
998 *
999 * `method` MAY BE EMPTY, in which case the key is omitted rather than filled
1000 * with the requested name: `-a reward` returns rewards and no solved chain, so
1001 * there is no resolved method to report, and echoing back "default" would claim
1002 * a resolution that never happened.
1003 *
1004 * EVERY INDEX IN A PAYLOAD IS 0-BASED, against the readable tables' 1-based
1005 * columns, and each payload carries `indexBase` so a host cannot get it wrong
1006 * silently. The two conventions are deliberate: the table is read by a human
1007 * diffing it against MATLAB, whose indices start at 1, while the JSON is
1008 * consumed by code that will index a numpy array or a std::vector with it.
1009 */
1010template <class T>
1011void emit_analysis(const char* key, const line::reg::Json& payload, const std::string& method,
1012 const line::reg::Json& extra = line::reg::Json::object()) {
1013 line::reg::Json body = payload;
1014 // `extra` GOES INSIDE THE PAYLOAD, not beside it. At envelope level a
1015 // solver-specific key can collide with the analysis's own name -- the CTMC
1016 // state count is "states" and so is the `-a states` payload, and the merge
1017 // silently replaced the whole answer with the integer 3. Nesting it makes
1018 // that class of collision unrepresentable, and it is where the value belongs
1019 // anyway: a cutoff qualifies THIS answer.
1020 for (line::reg::Json::const_iterator it = extra.begin(); it != extra.end(); ++it)
1021 body[it.key()] = it.value();
1022 line::reg::Json out = line::reg::Json::object();
1023 out[key] = body;
1024 out["arith"] = line::num_traits<T>::name();
1025 if (!method.empty()) out["method"] = method;
1026 emit_document(dump_document(out));
1027}
1028
1029/** A Matrix as a row-major array of arrays, each entry reduced to double. */
1030template <class T>
1031line::reg::Json matrix_json(const line::Matrix<T>& M) {
1032 line::reg::Json rows = line::reg::Json::array();
1033 for (std::size_t i = 0; i < M.rows(); ++i) {
1034 line::reg::Json row = line::reg::Json::array();
1035 for (std::size_t j = 0; j < M.cols(); ++j)
1036 row.push_back(line::num_traits<T>::to_double(M(i, j)));
1037 rows.push_back(row);
1038 }
1039 return rows;
1040}
1041
1042/** A vector of field elements as a JSON array of doubles. */
1043template <class T>
1044line::reg::Json vector_json(const std::vector<T>& v) {
1045 line::reg::Json a = line::reg::Json::array();
1046 for (std::size_t i = 0; i < v.size(); ++i) a.push_back(line::num_traits<T>::to_double(v[i]));
1047 return a;
1048}
1049
1050/** A vector of sizes as a JSON array, unchanged: a width is not a measurement. */
1051line::reg::Json index_json(const std::vector<std::size_t>& v) {
1052 line::reg::Json a = line::reg::Json::array();
1053 for (std::size_t i = 0; i < v.size(); ++i) a.push_back(v[i]);
1054 return a;
1055}
1056
1057/**
1058 * The per-Cache result block, as `-a avg` carries it inside the "avg" payload.
1059 *
1060 * PER-CACHE RESULTS RIDE WITH THE AVG TABLE, because a cache's hit, miss and
1061 * delayed-hit fractions are a SOLVER RESULT and a host that solves through this
1062 * CLI has no other way to get them back onto its own Cache node. Emitted per
1063 * node, and each vector is OMITTED when the solver computed none: absent must
1064 * CLEAR the host's copy, and a zero-filled vector would instead assert that
1065 * nothing hits.
1066 *
1067 * FACTORED OUT of `print_avg_table` because the SSA and Fluid arms build the
1068 * table themselves rather than through it, and so carried no block at all:
1069 * `CPPLINE.restoreCacheResults` then cleared MATLAB's Cache node, refreshed the
1070 * visits, and reported link()'s offered 1/2-1/2 for a split both engines had
1071 * measured. On cache_replc_rr that is hit 1.0 against 1.1246 (fluid) and
1072 * 1.1460 (ssa). A second copy of this serialization here would be free to drop
1073 * a field again, so there is one.
1074 */
1075template <class T>
1076line::reg::Json cache_extra_json(const line::solvers::CacheMetrics<T>& cache) {
1077 line::reg::Json caches = line::reg::Json::array();
1078 for (std::size_t c = 0; c < cache.caches.size(); ++c) {
1079 const line::solvers::CacheNodeMetrics<T>& m = cache.caches[c];
1080 line::reg::Json e = line::reg::Json::object();
1081 // NAME FIRST, because the index is not portable. `node` is an index
1082 // into THIS process's node order, which is not the model.json
1083 // declaration order: on retrieval_simple the JSON declares Source,
1084 // Cache, Queue, Sink and this struct holds Source, Queue, Sink,
1085 // Cache, so the Cache is 2 to the host and 4 here. A host matching
1086 // on the index wrote onto its Sink, found no Cache and silently kept
1087 // the PREVIOUS solver's numbers. `node` stays for provenance.
1088 e["name"] = m.name;
1089 e["node"] = m.node;
1090 if (!m.hitprob.empty()) e["HitProb"] = vector_json(m.hitprob);
1091 if (!m.missprob.empty()) e["MissProb"] = vector_json(m.missprob);
1092 if (!m.delayedprob.empty()) e["DelayedHitProb"] = vector_json(m.delayedprob);
1093 if (!m.latency.empty()) e["ResidT"] = vector_json(m.latency);
1094 if (!m.listcost.empty()) e["ListCost"] = vector_json(m.listcost);
1095 // The per-list breakdown, (K x h). Carried because it is an ANSWER a
1096 // host cannot rebuild from HitProb: only the row SUM is HitProb, and
1097 // which list served the hit is what a replacement policy is judged on.
1098 if (m.hitproblist.rows() > 0) e["HitProbList"] = matrix_json(m.hitproblist);
1099 if (!m.delayedhitqlen.empty()) {
1100 e["DelayedHitQLen"] = vector_json(m.delayedhitqlen);
1101 e["DelayedHitQLenFull"] = vector_json(m.delayedhitqlenfull);
1102 }
1103 caches.push_back(e);
1104 }
1105 return caches;
1106}
1107
1108/**
1109 * The fork-join section block, `getAvgFJTable` keyed as the JAR CLI keys `-a fj` (Fork, Join, JobClass,
1110 * FJQLen, FJRespT, FJTput), one entry per (pair, class) with a value > 0. JSON only: the readable avg
1111 * table is what the parity harness diffs, so it is left as it was. Null when there is no row.
1112 */
1113inline line::reg::Json fj_extra_json(const std::vector<std::string>& forks, const std::vector<std::string>& joins,
1114 const std::vector<std::string>& classes, const line::Matrix<double>& Q,
1115 const line::Matrix<double>& R, const line::Matrix<double>& X) {
1116 line::reg::Json t = line::reg::Json::object();
1117 for (const char* key : {"Fork", "Join", "JobClass", "FJQLen", "FJRespT", "FJTput"})
1118 t[key] = line::reg::Json::array();
1119 t["type"] = "AvgFJTable";
1120 bool any = false;
1121 for (std::size_t p = 0; p < forks.size() && p < joins.size(); ++p)
1122 for (std::size_t c = 0; c < classes.size(); ++c) {
1123 const double q = (p < Q.rows() && c < Q.cols()) ? Q(p, c) : 0.0;
1124 const double r = (p < R.rows() && c < R.cols()) ? R(p, c) : 0.0;
1125 const double x = (p < X.rows() && c < X.cols()) ? X(p, c) : 0.0;
1126 if (!(q > 0.0 || r > 0.0 || x > 0.0)) continue;
1127 t["Fork"].push_back(forks[p]);
1128 t["Join"].push_back(joins[p]);
1129 t["JobClass"].push_back(classes[c]);
1130 t["FJQLen"].push_back(q);
1131 t["FJRespT"].push_back(r);
1132 t["FJTput"].push_back(x);
1133 any = true;
1134 }
1135 return any ? t : line::reg::Json();
1136}
1137
1138/** The same block from a struct-indexed result (MVA, NC, Fluid, JMT). */
1139template <class T, class U>
1141 if (fj.empty()) return line::reg::Json();
1143 std::vector<std::string> forks, joins, classes;
1144 for (std::size_t p = 0; p < d.npairs(); ++p) {
1145 forks.push_back(sn.nodes[d.forks[p] - 1].name);
1146 joins.push_back(sn.nodes[d.joins[p] - 1].name);
1147 }
1148 for (std::size_t c = 0; c < sn.nclasses; ++c) classes.push_back(sn.classes[c].name);
1149 return fj_extra_json(forks, joins, classes, d.QLen, d.RespT, d.Tput);
1150}
1151
1152/**
1153 * Print an AvgResult as the parity table.
1154 *
1155 * Shared by every solver that returns `mva::AvgResult` -- MVA, NC, MAM and BA
1156 * -- so the rows stay byte-identical across them.
1157 */
1158template <class T>
1159void print_avg_table(const line::qn::NetworkStruct<T>& sn, const line::mva::AvgResult<T>& r,
1160 const line::reg::Json& extra_in = line::reg::Json::object()) {
1161 // THE JSON FORM IS THE JAR's, key for key. `jline.cli.LineCLI -a avg -o json`
1162 // emits {"avg": {"type": "AvgTable", "Station": [...], "JobClass": [...],
1163 // "QLen": [...], ...}}, column-oriented, and the Python wrapper's
1164 // `station_matrices_via_jar` parses exactly that shape. Emitting anything
1165 // else here would force a second parser into the wrapper for a table that is
1166 // the same table, so the host can treat `lang='cpp'` and `lang='java'` as one
1167 // transport with two binaries. The JAR's `data` key (the rendered text) is
1168 // NOT reproduced: no caller reads it, and a second rendering of the same
1169 // numbers is one more thing that can disagree with the first.
1170 // A CARRIED WARNING IS ONLY A WARNING IF SOMEONE SEES IT. `AvgResult`
1171 // carries the reference's text verbatim for the cases where the answer is
1172 // usable but not one the reference stands behind (the SJN starvation cap,
1173 // immediate feedback approximated as re-queueing), and until now no CLI
1174 // path printed it -- so the table looked authoritative exactly where the
1175 // reference declines. On STDERR, not stdout: stdout is the table a parity
1176 // harness diffs and the JSON a wrapper parses, and a warning line in either
1177 // would be read as data. Python's `lang='cpp'` bridge re-raises anything on
1178 // stderr as a Python warning, so it reaches that caller too.
1179 if (!r.warning.empty())
1180 std::fprintf(stderr, "Warning: %s\n", r.warning.c_str());
1181
1182 // Mean per-list cache storage cost, the ListCost column of getAvgCacheTable.
1183 // Present only on a cache model carrying item sizes, and omitted entirely
1184 // otherwise rather than emitted empty, so a host can test for the key.
1185 line::reg::Json extra = line::reg::Json::object();
1186 if (!r.listcost.empty()) {
1187 line::reg::Json lc = line::reg::Json::array();
1188 for (std::size_t j = 0; j < r.listcost.size(); ++j)
1189 lc.push_back(line::num_traits<T>::to_double(r.listcost[j]));
1190 extra["ListCost"] = lc;
1191 }
1192
1193 // WHAT THE CALLER ADDS, merged rather than replaced. The JMT arm carries the
1194 // finite-capacity-region rows this way: they are metric rows past the last
1195 // station, and the station table drops them, so without a channel of their
1196 // own a host reading `-a avg` cannot see a region at all (MATLAB's
1197 // `getAvgNodeTable` then filtered the FCR node out for having no numbers).
1198 if (extra_in.is_object())
1199 for (line::reg::Json::const_iterator it = extra_in.begin(); it != extra_in.end(); ++it)
1200 extra[it.key()] = it.value();
1201
1202 // See `cache_extra_json`: the split is a solver result and the host has no
1203 // other channel back to its own Cache node.
1204 if (!r.cache.empty()) extra["Cache"] = cache_extra_json<T>(r.cache);
1205 // getAvgFJTable's rows, for the reason the Cache block rides here: no other channel back to the host.
1206 {
1207 const line::reg::Json fjb = fj_extra_json(sn, r.fj);
1208 if (!fjb.is_null()) extra["FJ"] = fjb;
1209 }
1210
1211 // Provenance of the solve, beside "avg" and keyed as the JAR CLI keys it, so
1212 // one host-side reduction reads both backends. `converged` is OMITTED when
1213 // the handler reports none: absent is not false, and there the count is the
1214 // signal a caller may fall back on.
1215 line::reg::Json envelope = line::reg::Json::object();
1216 envelope["iter"] = r.iter;
1217 if (r.converged.has_value()) envelope["converged"] = r.converged.value();
1218 // `@SolverNC/getProbNormConstAggr`, keyed as the reference names the field it
1219 // stores it in. OMITTED for every solver that computes no constant: log G = 0
1220 // is the constant of an empty network, so a zero here would be a claim.
1221 if (r.lognormconst.has_value()) envelope["logNormConstAggr"] = r.lognormconst.value();
1222
1223 emit_avg_table<T>(sn, r.actualmethod, [&](std::size_t i, std::size_t c) {
1224 AvgRow row;
1225 row.q = line::num_traits<T>::to_double(r.QN(i, c));
1226 row.u = line::num_traits<T>::to_double(r.UN(i, c));
1227 row.r = line::num_traits<T>::to_double(r.RN(i, c));
1228 row.w = line::num_traits<T>::to_double(r.WN(i, c));
1229 row.a = line::num_traits<T>::to_double(r.AN(i, c));
1230 row.t = line::num_traits<T>::to_double(r.TN(i, c));
1231 return row;
1232 }, extra, envelope);
1233}
1234
1235/**
1236 * Solve a Network model.json with SolverMVA and print its AvgTable.
1237 *
1238 * The columns and their order -- Station, JobClass, then QLen, Util, RespT,
1239 * ResidT, ArvR, Tput -- are the ones `parity/compare_parity.py` parses, so the
1240 * printed table is directly diffable against the MATLAB and Python rows. A
1241 * (station, class) pair with no presence at all (every metric zero) is dropped,
1242 * as `getAvgTable` drops its unvisited rows.
1243 */
1244template <class T>
1245int solve_model_mva(const std::string& file, const Knobs& k) {
1246 line::qn::Network<T> net = read_model<T>(file);
1248 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1249 if (k.tol >= 0.0) opt.tol = k.tol;
1250 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
1251 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1252 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
1253 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
1254 line::Matrix<T> init;
1255 // The map_env funnel, the same one `run_avg_engine` applies to the node,
1256 // sys, chain and nodechain views: `-a avg` and `-a node` must accept the
1257 // same models for the same command line. Compile-time because the driver is
1258 // double-only -- see the note on the nc arm of `run_avg_engine`.
1260 if constexpr (std::is_same_v<T, double>) {
1262 net.get_struct(), "SolverMVA",
1264 net.get_struct()),
1265 map_env_config(k),
1266 [&opt, &init](const line::qn::NetworkStruct<double>& m) {
1267 return line::mva::solver_mva_run_analyzer(m, opt, init);
1268 },
1270 } else {
1271 r = line::mva::solver_mva_run_analyzer(net.get_struct(), opt, init);
1272 }
1273 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1274
1275 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1276 r.actualmethod.c_str(),
1277 line::util::method_type("MVA", r.actualmethod).c_str());
1278 print_avg_table<T>(sn, r);
1279 return 0;
1280}
1281
1282
1283/**
1284 * `-s jmt -a tran`: `getTranAvg`, the transient ensemble of `default` over a finite timespan.
1285 *
1286 * A SINGLE SAMPLE PATH IS NOT E[N](t). There is no time ergodicity at a fixed
1287 * t, so the mean is estimated over independent replications -- `--jmt-replications`
1288 * of them, which is what `options.config.replications` names in the reference --
1289 * replication k seeded `seed + k - 1` and resampled onto the union of their event
1290 * grids. An explicit `--method jsim` is ONE run. `jmt_transient_replications`
1291 * states the horizon and method rules itself, so they are not restated here.
1292 *
1293 * The payload is the SAME `TranAvgTable` the CTMC, fluid, MAM and LDES
1294 * transients emit, key for key, so one host reader serves every solver that
1295 * answers `-a tran`.
1296 */
1297int solve_model_jmt_tran(const line::qn::NetworkStruct<double>& sn,
1298 const line::jmt::JmtOptions& o, const Knobs& k) {
1300 const std::size_t reps = line::jmt::jmt_transient_replication_count(o, "getTranAvg");
1301 const std::size_t M = sn.nstations, K = sn.nclasses, nt = r.t.size();
1302
1303 if (g_json_output) {
1304 line::reg::Json p = line::reg::Json::object();
1305 p["type"] = "TranAvgTable";
1306 p["indexBase"] = 0;
1307 p["t0"] = k.t0 >= 0.0 ? k.t0 : 0.0;
1308 p["t1"] = o.max_simulated_time;
1309 // HOW MANY PATHS THE MEAN IS OVER is part of the answer on a simulated
1310 // transient: a replication that produced no log contributes nothing, so
1311 // the count asked for and the count averaged are not the same number.
1312 p["replications"] = r.valid;
1313 p["replicationsRequested"] = reps;
1314 line::reg::Json curves = line::reg::Json::array();
1315 for (std::size_t i = 0; i < M && i < r.QNt.size(); ++i)
1316 for (std::size_t c = 0; c < K && c < r.QNt[i].size(); ++c) {
1317 // A disabled pair is OMITTED, not sent as zeros, as on the CTMC
1318 // arm: the host turns an absent curve into the disabled
1319 // handle's NaN, where zeros would read as a genuinely idle
1320 // station.
1321 if (sn.disabled[i][c]) continue;
1322 line::reg::Json e = line::reg::Json::object();
1323 e["Station"] = sn.stations[i].name;
1324 e["JobClass"] = sn.classes[c].name;
1325 e["station"] = i;
1326 e["jobclass"] = c;
1327 line::reg::Json tt = line::reg::Json::array(), q = line::reg::Json::array(),
1328 u = line::reg::Json::array(), x = line::reg::Json::array();
1329 for (std::size_t j = 0; j < nt; ++j) {
1330 tt.push_back(r.t[j]);
1331 q.push_back(r.QNt[i][c][j]);
1332 u.push_back(r.UNt[i][c][j]);
1333 x.push_back(r.TNt[i][c][j]);
1334 }
1335 e["t"] = tt;
1336 e["QLen"] = q;
1337 e["Util"] = u;
1338 e["Tput"] = x;
1339 curves.push_back(e);
1340 }
1341 p["curves"] = curves;
1342 // ALWAYS jsim: every replication is a simulated path, and JMVA computes
1343 // means from a product form and logs no trajectory at all, so naming the
1344 // requested method would name an engine that never ran.
1345 emit_analysis<double>("tran", p, std::string("jsim"));
1346 return 0;
1347 }
1348 std::printf("SolverJMT arith=double method=jsim\n");
1349 std::printf("TranAvg times=%zu replications=%zu/%zu\n", nt, r.valid, reps);
1350 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
1351 "Tput");
1352 for (std::size_t i = 0; i < M && i < r.QNt.size(); ++i)
1353 for (std::size_t c = 0; c < K && c < r.QNt[i].size(); ++c) {
1354 if (sn.disabled[i][c]) continue;
1355 for (std::size_t j = 0; j < nt; ++j)
1356 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
1357 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), r.t[j],
1358 r.QNt[i][c][j], r.UNt[i][c][j], r.TNt[i][c][j]);
1359 }
1360 return 0;
1361}
1362
1363/**
1364 * `-s jmt -a tranprob`: `getTranProbAggr`, pi(t) over ONE station's aggregate
1365 * state, estimated over independent replications.
1366 *
1367 * THE AGGREGATE LAW ONLY, and refused rather than aliased for the detailed one:
1368 * a JMT log records per-class job counts at a node and nothing about the buffer
1369 * order or the service phase, so the encoding `getTranProb` is a law over is
1370 * not observed at all -- the same rule the `-a prob` arm states.
1371 *
1372 * `--node` names the station, defaulting to the first one: the estimate is
1373 * over one station's own state, not over the network's, so there is no system
1374 * view to fall back to here.
1375 */
1376int solve_model_jmt_tranprob(const line::qn::NetworkStruct<double>& sn,
1377 const line::jmt::JmtOptions& o, const Knobs& k) {
1378 std::size_t station = 1;
1379 if (k.node) {
1380 // OUT OF RANGE IS REFUSED, not silently folded onto station 1: falling
1381 // back to the default would answer a question about a different node
1382 // than the one named, which is the silent-wrong-answer failure the
1383 // whole Knobs struct exists to prevent.
1384 if (k.node > sn.nodes.size() || sn.nodes[k.node - 1].station == 0)
1385 throw line::InputError(
1386 "--node " + std::to_string(k.node) +
1387 " is not a station, so it holds no per-class job count to take a law over");
1388 station = sn.nodes[k.node - 1].station;
1389 }
1390 const std::size_t reps = line::jmt::jmt_transient_replication_count(o, "getTranProbAggr");
1391 std::vector<std::vector<double> > states;
1392 const std::pair<std::vector<double>, std::vector<std::vector<double> > > r =
1393 line::jmt::jmt_get_tran_prob_aggr(sn, station, o, states);
1394 const std::vector<double>& t = r.first;
1395 const std::vector<std::vector<double> >& pit = r.second;
1396
1397 if (g_json_output) {
1398 line::reg::Json p = line::reg::Json::object();
1399 p["type"] = "TranProb";
1400 p["indexBase"] = 0;
1401 p["scope"] = sn.stations[station - 1].name;
1402 p["node"] = sn.station_to_node[station - 1] - 1;
1403 p["station"] = station - 1;
1404 p["replications"] = reps;
1405 line::reg::Json span = line::reg::Json::array();
1406 // THE HORIZON IS PART OF THE ANSWER, as on the CTMC arm: pi(t) over an
1407 // unstated span is not a quantity, and here it is also what makes the
1408 // replications' grids commensurable in the first place.
1409 span.push_back(k.t0 >= 0.0 ? k.t0 : 0.0);
1410 span.push_back(o.max_simulated_time);
1411 p["tspan"] = span;
1412 p["t"] = vector_json(t);
1413 // ONLY THE AGGREGATE LABELS, and the key says so: a host reading
1414 // `labels` here would be reading the detailed law of another solver.
1415 line::reg::Json labels = line::reg::Json::array();
1416 for (std::size_t s = 0; s < states.size(); ++s) labels.push_back(vector_json(states[s]));
1417 p["labelsAggr"] = labels;
1418 line::reg::Json rows = line::reg::Json::array();
1419 for (std::size_t g = 0; g < pit.size(); ++g) rows.push_back(vector_json(pit[g]));
1420 p["pitAggr"] = rows;
1421 emit_analysis<double>("tranprob", p, std::string("jsim"));
1422 return 0;
1423 }
1424 std::printf("SolverJMT arith=double method=jsim\n");
1425 std::printf("TranProb times=%zu tspan=%g:%g scope=%s replications=%zu\n", t.size(),
1426 k.t0 >= 0.0 ? k.t0 : 0.0, o.max_simulated_time,
1427 sn.stations[station - 1].name.c_str(), reps);
1428 // The labels come FIRST and the occupancy after, as on the CTMC arm: pi(t)
1429 // is a row per time over columns that mean nothing until the state they
1430 // index is named.
1431 std::printf("%8s %s\n", "State", "Aggregate");
1432 for (std::size_t s = 0; s < states.size(); ++s) {
1433 std::printf("%8zu ", s + 1);
1434 for (std::size_t c = 0; c < states[s].size(); ++c) std::printf(" %g", states[s][c]);
1435 std::printf("\n");
1436 }
1437 std::printf("%14s", "Time");
1438 for (std::size_t s = 0; s < states.size(); ++s) std::printf(" %12zu", s + 1);
1439 std::printf("\n");
1440 for (std::size_t g = 0; g < t.size(); ++g) {
1441 std::printf("%14.8g", t[g]);
1442 for (std::size_t s = 0; s < states.size(); ++s)
1443 std::printf(" %12.6g", s < pit[g].size() ? pit[g][s] : 0.0);
1444 std::printf("\n");
1445 }
1446 return 0;
1447}
1448
1449/**
1450 * `-s jmt -a sample`: `sampleSysAggr`, one simulated trajectory; with `--node`,
1451 * that node's own block (`sampleAggr`) instead.
1452 *
1453 * `--events` NAMES THE TRUNCATION, and it is a truncation rather than a
1454 * stopping rule: JMT's `maxEvents` is global and cannot be asked for a count at
1455 * one node, so what comes back is a prefix of the run the engine produced --
1456 * exactly the caveat `jmt_sample_aggr` carries. `--samples` stands in when
1457 * `--events` is absent, as on the CTMC arm, and neither bounds the engine.
1458 */
1459int solve_model_jmt_sample(const line::qn::NetworkStruct<double>& sn,
1460 const line::jmt::JmtOptions& o, const Knobs& k) {
1461 const std::size_t nevents = k.events ? k.events : 0;
1462 const std::size_t K = sn.nclasses;
1463
1464 if (k.node) {
1465 if (k.node > sn.nodes.size() || sn.nodes[k.node - 1].station == 0)
1466 throw line::InputError(
1467 "--node " + std::to_string(k.node) +
1468 " is not a station, so no per-class job count is logged for it");
1470 line::jmt::jmt_sample_aggr(sn, k.node, nevents, o);
1471 if (g_json_output) {
1472 line::reg::Json p = line::reg::Json::object();
1473 p["type"] = "SamplePath";
1474 p["indexBase"] = 0;
1475 p["scope"] = sn.nodes[k.node - 1].name;
1476 p["node"] = k.node - 1;
1477 p["events"] = nevents;
1478 p["seed"] = o.seed;
1479 p["drawn"] = tr.t.size();
1480 p["t"] = vector_json(tr.t);
1481 line::reg::Json st = line::reg::Json::array();
1482 for (std::size_t j = 0; j < tr.qlen.size(); ++j) st.push_back(vector_json(tr.qlen[j]));
1483 p["state"] = st;
1484 line::reg::Json cols = line::reg::Json::array();
1485 for (std::size_t c = 0; c < K; ++c) cols.push_back(sn.classes[c].name);
1486 p["columns"] = cols;
1487 emit_analysis<double>("sample", p, std::string("jsim"));
1488 return 0;
1489 }
1490 std::printf("SolverJMT arith=double method=jsim\n");
1491 std::printf("%14s %s\n", "Time", ("NodeState (" + sn.nodes[k.node - 1].name +
1492 ", per class)").c_str());
1493 for (std::size_t j = 0; j < tr.t.size(); ++j) {
1494 std::printf("%14.8g ", tr.t[j]);
1495 for (std::size_t c = 0; c < K && c < tr.qlen[j].size(); ++c)
1496 std::printf(" %g", tr.qlen[j][c]);
1497 std::printf("\n");
1498 }
1499 return 0;
1500 }
1501
1503 const std::size_t M = sn.nstations, n = tr.t.size();
1504 if (g_json_output) {
1505 line::reg::Json p = line::reg::Json::object();
1506 p["type"] = "SamplePath";
1507 p["indexBase"] = 0;
1508 p["scope"] = "(system)";
1509 p["events"] = nevents;
1510 p["seed"] = o.seed;
1511 p["drawn"] = n;
1512 p["t"] = vector_json(tr.t);
1513 line::reg::Json st = line::reg::Json::array();
1514 for (std::size_t j = 0; j < n; ++j) {
1515 line::reg::Json row = line::reg::Json::array();
1516 for (std::size_t i = 0; i < M; ++i)
1517 for (std::size_t c = 0; c < K; ++c)
1518 row.push_back(i < tr.state.size() && j < tr.state[i].size() &&
1519 c < tr.state[i][j].size()
1520 ? tr.state[i][j][c]
1521 : 0.0);
1522 st.push_back(row);
1523 }
1524 p["state"] = st;
1525 line::reg::Json cols = line::reg::Json::array();
1526 for (std::size_t i = 0; i < M; ++i)
1527 for (std::size_t c = 0; c < K; ++c)
1528 cols.push_back(sn.stations[i].name + "," + sn.classes[c].name);
1529 p["columns"] = cols;
1530 emit_analysis<double>("sample", p, std::string("jsim"));
1531 return 0;
1532 }
1533 std::printf("SolverJMT arith=double method=jsim\n");
1534 std::printf("%14s %s\n", "Time", "SysState (station-major, per class)");
1535 for (std::size_t j = 0; j < n; ++j) {
1536 std::printf("%14.8g ", tr.t[j]);
1537 for (std::size_t i = 0; i < M; ++i)
1538 for (std::size_t c = 0; c < K; ++c)
1539 std::printf(" %g", i < tr.state.size() && j < tr.state[i].size() &&
1540 c < tr.state[i][j].size()
1541 ? tr.state[i][j][c]
1542 : 0.0);
1543 std::printf("\n");
1544 }
1545 return 0;
1546}
1547
1548/**
1549 * Solve a Network model.json with SolverJMT: write the JMT document, run the
1550 * external engine, print the same table every other solver prints.
1551 *
1552 * DOUBLE ONLY, and refused rather than relabelled under another arithmetic:
1553 * JMT simulates in double and reports in double, so an answer tagged
1554 * `real:128` would name a precision that never touched the computation. The
1555 * same rule the LDES arm applies, for the same reason.
1556 */
1557int solve_model_jmt(const std::string& file, const Knobs& k, const std::string& analysis) {
1558 line::qn::Network<double> net = read_model<double>(file);
1560
1562 if (!k.method.empty() && k.method != "default") o.method = k.method;
1563 if (k.samples > 0) o.samples = static_cast<double>(k.samples);
1564 if (k.seed != 0) o.seed = static_cast<long>(k.seed);
1565 o.keep = k.keep;
1566 if (k.t1 >= 0.0) o.max_simulated_time = k.t1;
1567 // `--jmt-replications` IS THE ENSEMBLE SIZE of the two arms that average over independent
1568 // runs, `options.config.replications`; the gate above accepts it for `-a tran` and `-a tranprob`.
1569 if (k.jmt_replications > 0) o.replications = k.jmt_replications;
1570 // AN EXPLICIT `jsim` IS ONE RUN, not the ensemble, so a count given with it would be dropped.
1571 if (k.jmt_replications > 0 && o.method == "jsim")
1573 "--jmt-replications sizes the transient ensemble of --method default; --method jsim "
1574 "is a single JSIM run, so the count would be ignored rather than honoured");
1575 o.verbose = k.verbose;
1576
1577 if (analysis == "tran") return solve_model_jmt_tran(sn, o, k);
1578 if (analysis == "tranprob") return solve_model_jmt_tranprob(sn, o, k);
1579 if (analysis == "sample") return solve_model_jmt_sample(sn, o, k);
1580
1581 // `-a prob`: `getProbAggr` per station and `getProbSysAggr`, weighed off ONE
1582 // instrumented run. The DETAILED pair -- `getProb` and `getProbSys` -- has
1583 // no counterpart here and is OMITTED rather than aliased to the aggregate:
1584 // a JMT log records per-class job counts at a node and nothing about the
1585 // buffer order or the service phase, so the encoding those two are
1586 // probabilities of is not observed at all.
1587 if (analysis == "prob") {
1588 std::size_t target = 0;
1589 if (k.node) {
1590 if (k.node <= sn.nodes.size()) target = sn.nodes[k.node - 1].station;
1591 if (target == 0)
1592 throw line::InputError(
1593 "--node " + std::to_string(k.node) +
1594 " is not a station, so it holds no per-class job count to ask about");
1595 }
1597 sn, o, target, std::vector<double>(k.state.begin(), k.state.end()));
1598 if (g_json_output) {
1599 line::reg::Json p = line::reg::Json::object();
1600 p["type"] = "ProbAggr";
1601 p["indexBase"] = 0;
1602 p["ProbSysAggr"] = r.sys;
1603 // WHETHER THE STATE OCCURRED AT ALL, beside the number. On an exact
1604 // solver a zero probability is a property of the model; on a
1605 // simulation it is far more often a property of the run length, and
1606 // a caller cannot tell the two apart from the zero alone.
1607 p["SysStateSeen"] = r.sys_seen;
1608 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
1609 sv = line::reg::Json::array();
1610 for (std::size_t i = 0; i < sn.nstations; ++i) {
1611 st.push_back(sn.stations[i].name);
1612 pa.push_back(r.station[i]);
1613 sv.push_back(static_cast<bool>(r.station_seen[i]));
1614 }
1615 p["Station"] = st;
1616 p["ProbAggr"] = pa;
1617 p["StateSeen"] = sv;
1618 // ALWAYS jsim, whatever `--method` asked for: this answer is read
1619 // off a simulated trajectory, and JMVA computes means from a
1620 // product form and logs nothing, so labelling it with the
1621 // requested method would name an engine that never ran.
1622 emit_analysis<double>("prob", p, std::string("jsim"));
1623 return 0;
1624 }
1625 std::printf("SolverJMT arith=double method=jsim\n");
1626 std::printf("ProbSysAggr %.10g%s\n", r.sys, r.sys_seen ? "" : " (state never observed)");
1627 std::printf("%-16s %14s\n", "Station", "ProbAggr");
1628 for (std::size_t i = 0; i < sn.nstations; ++i)
1629 std::printf("%-16s %14.10g%s\n", sn.stations[i].name.c_str(), r.station[i],
1630 r.station_seen[i] ? "" : " (state never observed)");
1631 return 0;
1632 }
1633
1634 if (analysis == "cdf" || analysis == "trancdf" || analysis == "trancdfpasst") {
1635 // -a cdf preloads the rounded steady-state queue lengths, the seeded
1636 // getCdfRespT pipeline; the transient names start from the default
1637 // initial state, getTranCdfRespT's contract
1638 const std::map<std::pair<std::size_t, std::size_t>,
1639 std::vector<std::pair<double, double> > >
1640 rd = line::jmt::jmt_get_cdf_resp_t(sn, o, analysis == "cdf");
1641 line::reg::Json cdf = line::reg::Json::object();
1642 for (std::map<std::pair<std::size_t, std::size_t>,
1643 std::vector<std::pair<double, double> > >::const_iterator it = rd.begin();
1644 it != rd.end(); ++it) {
1645 line::reg::Json rows = line::reg::Json::array();
1646 for (std::size_t i = 0; i < it->second.size(); ++i) {
1647 line::reg::Json row = line::reg::Json::array();
1648 row.push_back(it->second[i].first);
1649 row.push_back(it->second[i].second);
1650 rows.push_back(row);
1651 }
1652 const std::string key =
1653 sn.nodes[sn.station_to_node[it->first.first - 1] - 1].name + "/" +
1654 sn.classes[it->first.second - 1].name;
1655 cdf[key] = rows;
1656 if (!g_json_output)
1657 std::printf("%-24s %8zu points respT(max)=%.6g\n", key.c_str(),
1658 it->second.size(), it->second.back().second);
1659 }
1660 if (g_json_output) {
1661 line::reg::Json out = line::reg::Json::object();
1662 out["cdf"] = cdf;
1663 emit_document(dump_document(out, 2));
1664 }
1665 return 0;
1666 }
1667
1669 std::printf("SolverJMT arith=double method=%s\n", r.avg.actualmethod.c_str());
1670
1671 // The metric matrices carry `nregions` EXTRA rows past the stations; the
1672 // station table takes the first `nstations` of them and the regions are
1673 // reported separately, as the LDES arm reports its own FCR block.
1675 if (table.QN.rows() > sn.nstations) {
1676 const std::size_t M = sn.nstations, K = sn.nclasses;
1677 line::Matrix<double>* dst[6] = {&table.QN, &table.UN, &table.RN,
1678 &table.TN, &table.AN, &table.WN};
1679 const line::Matrix<double>* src[6] = {&r.avg.QN, &r.avg.UN, &r.avg.RN,
1680 &r.avg.TN, &r.avg.AN, &r.avg.WN};
1681 for (int m = 0; m < 6; ++m) {
1682 line::Matrix<double> t(M, K, 0.0);
1683 for (std::size_t i = 0; i < M; ++i)
1684 for (std::size_t c = 0; c < K; ++c) t(i, c) = (*src[m])(i, c);
1685 *dst[m] = t;
1686 }
1687 }
1688 // THE REGION ROWS TRAVEL BESIDE THE STATION TABLE, in the same shape the
1689 // node view prints them: a host solving through `-a avg` has no other way to
1690 // reach them, and MATLAB's `getAvgNodeTable` needs `result.Avg` to carry
1691 // `nstations + nregions` rows or it filters the FCR node out for having no
1692 // numbers at all (fcr_mm1waitq[M2C], 'row FCR1 missing').
1693 line::reg::Json fcr_extra = line::reg::Json::object();
1694 if (r.avg.QN.rows() > sn.nstations && sn.regions.size() > 0) {
1695 const std::size_t M = sn.nstations, K = sn.nclasses;
1696 line::reg::Json names = line::reg::Json::array();
1697 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array();
1698 line::reg::Json rt = line::reg::Json::array(), w = line::reg::Json::array();
1699 line::reg::Json a = line::reg::Json::array(), t = line::reg::Json::array();
1700 for (std::size_t f = 0; f < sn.regions.size() && M + f < r.avg.QN.rows(); ++f) {
1701 names.push_back(f < sn.regions.size() && !sn.regions[f].name.empty()
1702 ? sn.regions[f].name
1703 : "FCR" + std::to_string(f + 1));
1704 for (std::size_t c = 0; c < K; ++c) {
1705 q.push_back(r.avg.QN(M + f, c));
1706 u.push_back(r.avg.UN(M + f, c));
1707 rt.push_back(r.avg.RN(M + f, c));
1708 w.push_back(r.avg.WN(M + f, c));
1709 a.push_back(r.avg.AN(M + f, c));
1710 t.push_back(r.avg.TN(M + f, c));
1711 }
1712 }
1713 fcr_extra["Region"] = names;
1714 fcr_extra["QLen"] = q;
1715 fcr_extra["Util"] = u;
1716 fcr_extra["RespT"] = rt;
1717 fcr_extra["ResidT"] = w;
1718 fcr_extra["ArvR"] = a;
1719 fcr_extra["Tput"] = t;
1720 }
1721 line::reg::Json avg_extra = line::reg::Json::object();
1722 if (!fcr_extra.empty()) avg_extra["FCR"] = fcr_extra;
1723 // THE CONFIDENCE INTERVALS ARE PART OF A SIMULATED MEAN. JMT reports a
1724 // half-width per measure at `--confint`, `JmtResult` carries them, and both
1725 // consumers used to take `r.avg` and drop them -- so the one solver here
1726 // whose answer is an estimate was the one quoting a point estimate as if it
1727 // were exact. Same block, same keys and same reason as
1728 // `solve_model_ldes_avg`'s, so one host reader serves both simulators.
1729 line::reg::Json ci = line::reg::Json::object();
1730 if (!r.QCI.empty()) ci["QNCI"] = matrix_json<double>(r.QCI);
1731 if (!r.UCI.empty()) ci["UNCI"] = matrix_json<double>(r.UCI);
1732 if (!r.RCI.empty()) ci["RNCI"] = matrix_json<double>(r.RCI);
1733 if (!r.TCI.empty()) ci["TNCI"] = matrix_json<double>(r.TCI);
1734 if (!r.ACI.empty()) ci["ANCI"] = matrix_json<double>(r.ACI);
1735 if (!ci.empty()) avg_extra["CI"] = ci;
1736 print_avg_table<double>(sn, table, avg_extra);
1737
1738 // The readable rendering prints the half-widths beside the table rather
1739 // than inside it: the columns are the same six every solver prints, and a
1740 // JMVA run (which computes no interval) would leave them blank.
1741 if (!g_json_output && !r.QCI.empty()) {
1742 std::printf("%-16s %-14s %12s %12s %12s %12s %12s\n", "Station", "JobClass", "QLenCI",
1743 "UtilCI", "RespTCI", "TputCI", "ArvRCI");
1744 for (std::size_t i = 0; i < sn.nstations && i < r.QCI.rows(); ++i)
1745 for (std::size_t c = 0; c < sn.nclasses; ++c)
1746 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g\n",
1747 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), r.QCI(i, c),
1748 r.UCI.empty() ? 0.0 : r.UCI(i, c), r.RCI.empty() ? 0.0 : r.RCI(i, c),
1749 r.TCI.empty() ? 0.0 : r.TCI(i, c),
1750 r.ACI.empty() ? 0.0 : r.ACI(i, c));
1751 }
1752
1753 if (!g_json_output && r.TNfcr.rows() > 0) {
1754 std::printf("%-16s %-14s %12s %12s\n", "Region", "JobClass", "Tput", "DropRate");
1755 for (std::size_t f = 0; f < r.TNfcr.rows(); ++f)
1756 for (std::size_t c = 0; c < sn.nclasses; ++c)
1757 // THE REGION'S DECLARED NAME, not the JSIM document's internal
1758 // `FCRegion<n>` label: that spelling is what the writer puts in
1759 // the exported model, and reporting it back renamed the user's
1760 // own region (`FCR1` in every other codebase's node table).
1761 std::printf("%-16s %-14s %12.6g %12.6g\n",
1762 (f < sn.regions.size() && !sn.regions[f].name.empty()
1763 ? sn.regions[f].name
1764 : "FCR" + std::to_string(f + 1))
1765 .c_str(),
1766 sn.classes[c].name.c_str(), r.TNfcr(f, c), r.DropRateNfcr(f, c));
1767 }
1768 if (!g_json_output && !r.cache_hit_prob.empty())
1769 for (std::map<std::size_t, std::vector<double> >::const_iterator it =
1770 r.cache_hit_prob.begin();
1771 it != r.cache_hit_prob.end(); ++it)
1772 for (std::size_t c = 0; c < it->second.size(); ++c)
1773 std::printf("%-16s %-14s hitProb=%12.6g\n", sn.nodes[it->first - 1].name.c_str(),
1774 sn.classes[c].name.c_str(), it->second[c]);
1775 return 0;
1776}
1777
1778/**
1779 * `-s nc -a avg` under `--method cftp` / `cftp.approx`: the AvgTable from
1780 * perfect sampling.
1781 *
1782 * THE BANNER NAMES THE RUN LENGTH because these numbers carry Monte Carlo error
1783 * and are not comparable to an exact solver's at solver tolerance. The mean
1784 * coalescence horizon travels with it: it is the cost the exact sampler paid,
1785 * has no a-priori bound, and is the one number that says whether the draw was
1786 * cheap or the model is nearly saturated. No `lognormconst=`: the sampler
1787 * yields no normalizing constant.
1788 */
1789template <class T>
1790int solve_nc_cftp_avg(const line::qn::NetworkStruct<T>& sn, const line::nc::NcSolverOptions& opt,
1791 const line::nc::NcCftpOptions& cftpopt) {
1793 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
1794 "the cftp methods draw random states and form the station balance functions in the "
1795 "log domain, neither of which exists in exact rational arithmetic; rerun with --arith "
1796 "double or --arith real");
1797 } else {
1798 // The run's own gates, in the run's order: the structural predicate
1799 // first, then the declared feature set.
1800 const std::string why = line::nc::solver_nc_cftp_supports(sn);
1801 if (!why.empty()) throw line::UnsupportedError(line::ErrorKind{"Model"}, why);
1803 const line::nc::NcCftpSolution<T> s = line::nc::solver_nc_cftp(sn, opt, cftpopt);
1804 const line::mva::AvgResult<T> r =
1806 double horizon = 0.0;
1807 for (std::size_t i = 0; i < s.horizon.size(); ++i)
1808 horizon += static_cast<double>(s.horizon[i]);
1809 if (!s.horizon.empty()) horizon /= static_cast<double>(s.horizon.size());
1810 std::printf(
1811 "SolverNC arith=%s method=%s type=%s samples=%zu seed=%lu distinct=%zu "
1812 "meanhorizon=%.6g\n",
1814 line::util::method_type("NC", s.actualmethod).c_str(), cftpopt.samples,
1815 cftpopt.seed, s.distinct_states.size(), horizon);
1816 print_avg_table<T>(sn, r);
1817 return 0;
1818 }
1819}
1820
1821/**
1822 * Solve a Network model.json with SolverNC and print the same table.
1823 *
1824 * `--samples` and `--seed` are wired because NC has stochastic methods that read
1825 * them ('mci', 'imci', 'ls', 'is', 'sampling' and 'mcmc'), and a run length nobody
1826 * can set is a method nobody can drive. `highvar` still has no flag and keeps its
1827 * SolverOptions('NC') default rather than being invented here.
1828 */
1829template <class T>
1830int solve_model_nc(const std::string& file, const Knobs& k) {
1831 line::qn::Network<T> net = read_model<T>(file);
1833 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1834 if (k.tol >= 0.0) opt.tol = k.tol;
1835 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
1836 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1837 if (k.samples) opt.samples = k.samples;
1838 if (k.seed) opt.seed = k.seed;
1839 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
1840 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
1841 if (k.slotted) opt.slotted = true;
1842 if (k.slotlength > 0.0) {
1843 opt.slotted = true;
1844 opt.slotlength = k.slotlength;
1845 }
1846 // The perfect sampler reports its run length beside the table, so it is
1847 // served by its own arm. The dispatcher has already refused every analysis
1848 // but `avg` for it.
1849 if (opt.method == "cftp" || opt.method == "cftp.approx") {
1851 cftpopt.samples = opt.samples;
1852 cftpopt.seed = opt.seed;
1853 return solve_nc_cftp_avg<T>(net.get_struct(), opt, cftpopt);
1854 }
1855 // The map_env funnel, as on the mva arm above and for the same reason.
1857 if constexpr (std::is_same_v<T, double>) {
1859 net.get_struct(), "SolverNC", line::qn::nc_feature_set(opt.method), map_env_config(k),
1860 [&opt](const line::qn::NetworkStruct<double>& m) {
1861 return line::nc::solver_nc_run_analyzer(m, opt);
1862 },
1864 } else {
1866 }
1867 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1868
1869 // `lognormconst=` rides ON THE BANNER LINE rather than on one of its own,
1870 // as the fluid arm's `iters=` does: the banner is provenance and a new line
1871 // between it and the table is one more thing a table parser has to skip.
1872 // It is `getProbNormConstAggr`, which no other `-a` reports.
1873 std::printf("SolverNC arith=%s method=%s type=%s lognormconst=%.10g\n",
1875 line::util::method_type("NC", r.actualmethod).c_str(),
1876 r.lognormconst.has_value() ? r.lognormconst.value() : 0.0);
1877 print_avg_table<T>(sn, r);
1878 return 0;
1879}
1880
1881/**
1882 * Solve a Network model.json with SolverMAM and print the same table.
1883 *
1884 * DOUBLE ONLY, and refused by name otherwise in the dispatcher: the analyzer
1885 * fits phase-type representations (aph_fit), which static_asserts on
1886 * transcendental arithmetic, so an exact instantiation would fail to COMPILE
1887 * rather than refuse at run time. `MamOptions`' tol, iter_max, space_max and
1888 * preserveDet keep their SolverOptions('MAM') defaults.
1889 */
1890line::mam::MamOptions mam_options(const Knobs& k);
1891
1892template <class T>
1893int solve_model_mam(const std::string& file, const Knobs& k) {
1894 line::qn::Network<T> net = read_model<T>(file);
1895 // the shared builder, so --timescale/--slotlength and the fork-join knobs reach -a avg
1896 const line::mam::MamOptions opt = mam_options(k);
1898 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1899
1900 std::printf("SolverMAM arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1901 r.actualmethod.c_str(),
1902 line::util::method_type("MAM", r.actualmethod).c_str());
1903 print_avg_table<T>(sn, r);
1904 return 0;
1905}
1906
1907/**
1908 * The AgOptions a command line asks for.
1909 *
1910 * ONE PLACE, because there are three call sites (-a avg, -a cdf and the
1911 * run_avg_engine views) and a knob added to only some of them is a knob that
1912 * works or not depending on which view was asked for.
1913 */
1914void apply_ag_knobs(const Knobs& k, line::ag::AgOptions& opt) {
1915 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1916 if (k.tol >= 0.0) opt.tol = k.tol;
1917 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1918 if (k.max_states > 0) opt.max_states = static_cast<std::size_t>(k.max_states);
1919 if (!k.nonmkv.empty()) opt.nonmkv = k.nonmkv;
1920}
1921
1922/**
1923 * Solve a Network model.json with SolverAG and print the same table.
1924 *
1925 * The RCAT/INAP arm, reachable from the CLI rather than API-only. It was the
1926 * one engine `run_avg_engine` already served for `-a node` and its three
1927 * sibling views while `-a avg` -- the view every parity row and every wrapper
1928 * asks for first -- refused the token outright, so `-s ag` reported an argument
1929 * error where the solver was present and working.
1930 *
1931 * `--method` picks between inap, inaprc, inapinf and exact; `default`
1932 * resolves to inap inside the analyzer, as it does in every codebase. `--tol`
1933 * and `--iter_max` are the fixed point's, matching -s mam.
1934 */
1935template <class T>
1936int solve_model_ag(const std::string& file, const Knobs& k) {
1937 line::qn::Network<T> net = read_model<T>(file);
1939 apply_ag_knobs(k, opt);
1941 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1942
1943 std::printf("SolverAG arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1944 r.actualmethod.c_str(),
1945 line::util::method_type("AG", r.actualmethod).c_str());
1946 print_avg_table<T>(sn, r);
1947 return 0;
1948}
1949
1950/**
1951 * The station a per-node MAM query is about: `--node` when given, and otherwise
1952 * the model's only Queue.
1953 *
1954 * Defaulting is legitimate here and nowhere else: `getProb`, `getProbMarg` and
1955 * `getMAMResult` all run `require_single_queue`, so a model that reaches them
1956 * has exactly ONE queue and there is nothing to choose. `--node` is still
1957 * accepted, because naming the node one means is clearer than relying on that.
1958 */
1959template <class T>
1960std::size_t mam_query_node(const line::qn::NetworkStruct<T>& sn, const Knobs& k) {
1961 if (k.node) return k.node;
1962 for (std::size_t a = 0; a < sn.nof_nodes(); ++a)
1963 if (sn.nodes[a].nodetype == line::qn::NodeType::Queue) return a + 1;
1964 throw line::InputError(
1965 "the MAM per-node analyses report a queue's internals and this model has no Queue node");
1966}
1967
1968/** The MamOptions every `-s mam` entry point builds from the CLI knobs. */
1969line::mam::MamOptions mam_options(const Knobs& k) {
1971 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1972 if (k.tol >= 0.0) opt.tol = k.tol;
1973 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1974 if (k.cutoff >= 0.0) opt.cutoff = static_cast<std::size_t>(k.cutoff);
1975 if (!k.nonmkv.empty()) opt.nonmkv = k.nonmkv;
1976 if (k.fj_accuracy > 0) opt.fj_accuracy = static_cast<std::size_t>(k.fj_accuracy);
1977 if (!k.fj_tmode.empty()) opt.fj_tmode = k.fj_tmode;
1978 if (k.sync_phases >= 0) opt.sync_phases = static_cast<std::size_t>(k.sync_phases);
1979 if (k.mapmsg_quantization >= 0)
1980 opt.mapmsg_quantization = static_cast<std::size_t>(k.mapmsg_quantization);
1981 if (!k.timescale.empty()) opt.timescale = k.timescale;
1982 if (k.slotlength > 0.0) opt.slotlength = k.slotlength;
1983 if (k.t1 >= 0.0) {
1984 opt.timespan_start = k.t0;
1985 opt.timespan_end = k.t1;
1986 }
1987 return opt;
1988}
1989
1990/**
1991 * `-s mam -a prob`: `@@SolverMAM/getProb` and `@@SolverMAM/getProbMarg`.
1992 *
1993 * BOTH, in one answer, because they are two views of the same queue-length law:
1994 * the joint (level, phase) table the first returns, and the per-class marginal
1995 * P(n jobs of class r) the second does. Reporting only one would leave the other
1996 * unreachable again, which is the state this wiring closes.
1997 *
1998 * `--cutoff` is the level truncation an OPEN model needs -- its queue length is
1999 * unbounded, so the table has to stop somewhere -- and is passed straight
2000 * through as `options.cutoff`; a closed model bounds itself by its population
2001 * and ignores it.
2002 */
2003template <class T>
2004int solve_model_mam_prob(const std::string& file, const Knobs& k) {
2005 line::qn::Network<T> net = read_model<T>(file);
2006 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2007 const line::mam::MamOptions opt = mam_options(k);
2008 // The reference's getters read `self.getAvg()` first, so the solve comes
2009 // before the query and a caller cannot reach them on an unsolved model.
2011 const std::size_t node = mam_query_node<T>(sn, k);
2012 const std::size_t ist = sn.nodes[node - 1].station;
2013 const line::mam::ProbTable<T> P = line::mam::solver_mam_get_prob(sn, opt, node, avg);
2014 std::vector<std::vector<T> > marg(sn.nclasses);
2015 for (std::size_t r = 0; r < sn.nclasses; ++r)
2016 marg[r] = line::mam::solver_mam_get_prob_marg(sn, opt, ist, r + 1, avg);
2017
2018 if (g_json_output) {
2019 line::reg::Json p = line::reg::Json::object();
2020 p["type"] = "ProbTable";
2021 p["indexBase"] = 0;
2022 p["node"] = node - 1;
2023 p["Node"] = sn.nodes[node - 1].name;
2024 p["levels"] = P.P.rows();
2025 p["phases"] = P.P.cols();
2026 line::reg::Json joint = line::reg::Json::array();
2027 for (std::size_t n = 0; n < P.P.rows(); ++n) {
2028 line::reg::Json row = line::reg::Json::array();
2029 for (std::size_t j = 0; j < P.P.cols(); ++j)
2030 row.push_back(line::num_traits<T>::to_double(P.P(n, j)));
2031 joint.push_back(row);
2032 }
2033 p["joint"] = joint;
2034 line::reg::Json mj = line::reg::Json::array();
2035 for (std::size_t r = 0; r < sn.nclasses; ++r) {
2036 line::reg::Json e = line::reg::Json::object();
2037 e["JobClass"] = sn.classes[r].name;
2038 e["jobclass"] = r;
2039 e["P"] = vector_json(marg[r]);
2040 mj.push_back(e);
2041 }
2042 p["marginal"] = mj;
2043 emit_analysis<T>("prob", p, avg.actualmethod);
2044 return 0;
2045 }
2046 std::printf("SolverMAM arith=%s method=%s node=%s levels=%zu phases=%zu\n",
2048 sn.nodes[node - 1].name.c_str(), P.P.rows(), P.P.cols());
2049 std::printf("%-8s %-8s %16s\n", "Level", "Phase", "Prob");
2050 for (std::size_t n = 0; n < P.P.rows(); ++n)
2051 for (std::size_t j = 0; j < P.P.cols(); ++j)
2052 std::printf("%-8zu %-8zu %16.10g\n", n, j + 1,
2054 std::printf("%-14s %-8s %16s\n", "JobClass", "Jobs", "Prob");
2055 for (std::size_t r = 0; r < sn.nclasses; ++r)
2056 for (std::size_t n = 0; n < marg[r].size(); ++n)
2057 std::printf("%-14s %-8zu %16.10g\n", sn.classes[r].name.c_str(), n,
2058 line::num_traits<T>::to_double(marg[r][n]));
2059 return 0;
2060}
2061
2062/**
2063 * The levels `getPerctRespT` is read at: `--percentiles`, or the reference's
2064 * `pers_stored` when the caller named none.
2065 */
2066std::vector<double> percentile_levels(const Knobs& k) {
2067 if (!k.percentiles.empty()) return k.percentiles;
2068 std::vector<double> pcts;
2069 pcts.push_back(0.50);
2070 pcts.push_back(0.90);
2071 pcts.push_back(0.95);
2072 pcts.push_back(0.99);
2073 return pcts;
2074}
2075
2076/**
2077 * `-s mam -a cdf`: `@@SolverMAM/getCdfRespT` (and its aliases getSjrnT / sjrnT),
2078 * with `@@SolverMAM/getPerctRespT` beside it.
2079 *
2080 * THE PERCENTILE LEVELS COME FROM `--percentiles`, defaulting to the
2081 * {0.50, 0.90, 0.95, 0.99} that `solver_mam_fj.m` stores as `pers_stored`: the
2082 * reference takes them as an argument to `getPerctRespT`, and the flag is that
2083 * argument. On the CDF path they are a READING of the same curve --
2084 * `mam_percentiles_from_cdf` inverts the CDF that is printed above them -- so a
2085 * level the caller names costs nothing beyond the inversion.
2086 *
2087 * A FORK-JOIN MODEL HAS NO CDF HERE, and that is the reference's structure
2088 * rather than a gap in this port: `getPerctRespT.m` reads the table
2089 * `solver_mam_fj.m` stored, and its own fallback comment records that
2090 * `getCdfRespT` is "not available for this model". The two are separate methods
2091 * in MATLAB and only this CLI bundles them, so the fork-join case prints the
2092 * percentiles alone and says why. The condition is tested explicitly, not
2093 * discovered by catching the CDF's refusal: a caught exception cannot tell "no
2094 * fork-join route exists" from "the passage-time engine failed on this model".
2095 */
2096template <class T>
2097int solve_model_mam_cdf(const std::string& file, const Knobs& k, const char* key,
2098 const char* type) {
2099 line::qn::Network<T> net = read_model<T>(file);
2100 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2101 const line::mam::MamOptions opt = mam_options(k);
2102 const std::vector<double> pcts = percentile_levels(k);
2103
2104 if (line::mam::mam_has_fj_percentiles(sn, opt)) {
2105 const std::vector<std::vector<T> > perc =
2107 if (g_json_output) {
2108 line::reg::Json p = line::reg::Json::object();
2109 p["type"] = "PerctRespT";
2110 p["indexBase"] = 0;
2111 p["source"] = "fjcodes";
2112 line::reg::Json arr = line::reg::Json::array();
2113 for (std::size_t r = 0; r < perc.size(); ++r) {
2114 line::reg::Json e = line::reg::Json::object();
2115 e["JobClass"] = sn.classes[r].name;
2116 e["jobclass"] = r;
2117 e["percentileLevels"] = pcts;
2118 e["percentiles"] = vector_json(perc[r]);
2119 arr.push_back(e);
2120 }
2121 p["respt"] = arr;
2122 emit_analysis<T>(key, p, opt.method);
2123 return 0;
2124 }
2125 std::printf("SolverMAM arith=%s method=%s classes=%zu\n", line::num_traits<T>::name(),
2126 opt.method.c_str(), sn.nclasses);
2127 std::printf("# the response-time CDF has no fork-join route in the reference; these are "
2128 "the FJ_codes percentiles getPerctRespT reads\n");
2129 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
2130 for (std::size_t r = 0; r < perc.size(); ++r)
2131 for (std::size_t j = 0; j < perc[r].size(); ++j)
2132 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
2133 line::num_traits<T>::to_double(perc[r][j]));
2134 return 0;
2135 }
2136
2137 const std::vector<line::mam::RespTCdf<T> > rd = line::mam::solver_mam_get_cdf_respt(sn, opt);
2138 std::vector<std::vector<T> > perc;
2139 for (std::size_t r = 0; r < rd.size(); ++r)
2140 perc.push_back(line::mam::mam_percentiles_from_cdf(rd[r], pcts));
2141
2142 if (g_json_output) {
2143 line::reg::Json p = line::reg::Json::object();
2144 p["type"] = type;
2145 p["indexBase"] = 0;
2146 line::reg::Json arr = line::reg::Json::array();
2147 for (std::size_t r = 0; r < rd.size(); ++r) {
2148 // An empty curve is not a degenerate one: the class has no passage
2149 // through a queue here, and it is OMITTED rather than sent as a flat
2150 // zero law.
2151 if (rd[r].X.empty()) continue;
2152 line::reg::Json e = line::reg::Json::object();
2153 e["JobClass"] = sn.classes[r].name;
2154 e["jobclass"] = r;
2155 e["t"] = vector_json(rd[r].X);
2156 e["F"] = vector_json(rd[r].F);
2157 e["percentileLevels"] = pcts;
2158 e["percentiles"] = vector_json(perc[r]);
2159 arr.push_back(e);
2160 }
2161 p["respt"] = arr;
2162 emit_analysis<T>(key, p, opt.method);
2163 return 0;
2164 }
2165 std::printf("SolverMAM arith=%s method=%s classes=%zu\n", line::num_traits<T>::name(),
2166 opt.method.c_str(), sn.nclasses);
2167 std::printf("%-14s %14s %14s\n", "JobClass", "Time", "F(t)");
2168 for (std::size_t r = 0; r < rd.size(); ++r)
2169 for (std::size_t j = 0; j < rd[r].X.size(); ++j)
2170 std::printf("%-14s %14.8g %14.10g\n", sn.classes[r].name.c_str(),
2172 line::num_traits<T>::to_double(rd[r].F[j]));
2173 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
2174 for (std::size_t r = 0; r < perc.size(); ++r)
2175 for (std::size_t j = 0; j < perc[r].size(); ++j)
2176 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
2177 line::num_traits<T>::to_double(perc[r][j]));
2178 return 0;
2179}
2180
2181/**
2182 * `-s mam -a perct-respt`: `@@SolverMAM/getPerctRespT` ALONE.
2183 *
2184 * The percentiles ride beside the curve under `-a cdf` as well, and this arm
2185 * exists because they are a separate METHOD in the reference and a separate
2186 * answer to a caller: a service-level question ("what is the 99th percentile")
2187 * wants four numbers, not the whole law printed above them. On a fork-join
2188 * model it is the only route -- `getCdfRespT` has none there -- and on every
2189 * other model the levels are inverted from the CDF the other arm prints, so
2190 * the two never disagree.
2191 */
2192template <class T>
2193int solve_model_mam_perct(const std::string& file, const Knobs& k) {
2194 line::qn::Network<T> net = read_model<T>(file);
2195 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2196 const line::mam::MamOptions opt = mam_options(k);
2197 const std::vector<double> pcts = percentile_levels(k);
2198
2199 std::vector<std::vector<T> > perc;
2200 const bool fj = line::mam::mam_has_fj_percentiles(sn, opt);
2201 if (fj) {
2202 perc = line::mam::solver_mam_get_perct_respt(sn, opt, pcts);
2203 } else {
2204 const std::vector<line::mam::RespTCdf<T> > rd =
2206 for (std::size_t r = 0; r < rd.size(); ++r)
2207 perc.push_back(line::mam::mam_percentiles_from_cdf(rd[r], pcts));
2208 }
2209
2210 if (g_json_output) {
2211 line::reg::Json p = line::reg::Json::object();
2212 p["type"] = "PerctRespT";
2213 p["indexBase"] = 0;
2214 p["source"] = fj ? "fjcodes" : "cdf";
2215 line::reg::Json arr = line::reg::Json::array();
2216 for (std::size_t r = 0; r < perc.size(); ++r) {
2217 if (perc[r].empty()) continue;
2218 line::reg::Json e = line::reg::Json::object();
2219 e["JobClass"] = sn.classes[r].name;
2220 e["jobclass"] = r;
2221 e["percentileLevels"] = pcts;
2222 e["percentiles"] = vector_json(perc[r]);
2223 arr.push_back(e);
2224 }
2225 p["respt"] = arr;
2226 emit_analysis<T>("perct", p, opt.method);
2227 return 0;
2228 }
2229 std::printf("SolverMAM arith=%s method=%s classes=%zu source=%s\n",
2230 line::num_traits<T>::name(), opt.method.c_str(), sn.nclasses,
2231 fj ? "fjcodes" : "cdf");
2232 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
2233 for (std::size_t r = 0; r < perc.size(); ++r)
2234 for (std::size_t j = 0; j < perc[r].size(); ++j)
2235 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
2236 line::num_traits<T>::to_double(perc[r][j]));
2237 return 0;
2238}
2239
2240/**
2241 * `-s mam -a tran`: `@@SolverMAM/getTranAvg`.
2242 *
2243 * The reference FORCES `options.method = 'ldqbd'` before delegating, so the
2244 * transient path is not the caller's method and `--method` does not select it;
2245 * which engine runs -- the Laplace-domain transient QBD or the QBD fast path --
2246 * is decided from the model by `mam_transient_qbd_applicable`. `--tspan` is
2247 * required: a transient curve on an unstated horizon is not a quantity.
2248 */
2249template <class T>
2250int solve_model_mam_tran(const std::string& file, const Knobs& k) {
2251 line::qn::Network<T> net = read_model<T>(file);
2252 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2253 const line::mam::MamOptions opt = mam_options(k);
2255
2256 if (g_json_output) {
2257 line::reg::Json p = line::reg::Json::object();
2258 p["type"] = "TranAvgTable";
2259 p["indexBase"] = 0;
2260 p["t0"] = opt.timespan_start;
2261 p["t1"] = opt.timespan_end;
2262 line::reg::Json arr = line::reg::Json::array();
2263 for (std::size_t i = 0; i < tr.Qt.size(); ++i)
2264 for (std::size_t r = 0; r < tr.Qt[i].size(); ++r) {
2265 if (tr.Qt[i][r].times.empty()) continue;
2266 line::reg::Json e = line::reg::Json::object();
2267 e["Station"] = sn.stations[i].name;
2268 e["JobClass"] = sn.classes[r].name;
2269 e["station"] = i;
2270 e["jobclass"] = r;
2271 e["t"] = tr.Qt[i][r].times;
2272 e["QLen"] = vector_json(tr.Qt[i][r].values);
2273 e["Util"] = vector_json(tr.Ut[i][r].values);
2274 e["Tput"] = vector_json(tr.Tt[i][r].values);
2275 arr.push_back(e);
2276 }
2277 p["curves"] = arr;
2278 emit_analysis<T>("tran", p, "ldqbd");
2279 return 0;
2280 }
2281 std::printf("SolverMAM arith=%s method=ldqbd tspan=[%g,%g]\n", line::num_traits<T>::name(),
2282 opt.timespan_start, opt.timespan_end);
2283 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
2284 "Tput");
2285 for (std::size_t i = 0; i < tr.Qt.size(); ++i)
2286 for (std::size_t r = 0; r < tr.Qt[i].size(); ++r)
2287 for (std::size_t j = 0; j < tr.Qt[i][r].times.size(); ++j)
2288 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
2289 sn.stations[i].name.c_str(), sn.classes[r].name.c_str(),
2290 tr.Qt[i][r].times[j],
2291 line::num_traits<T>::to_double(tr.Qt[i][r].values[j]),
2292 line::num_traits<T>::to_double(tr.Ut[i][r].values[j]),
2293 line::num_traits<T>::to_double(tr.Tt[i][r].values[j]));
2294 return 0;
2295}
2296
2297/**
2298 * `-s mam -a internals`: `@@SolverMAM/getMAMResult`, the M/G/1-type internals of
2299 * a single queue.
2300 *
2301 * It is NOT a metric table and is deliberately not folded into `-a avg`: the
2302 * answer is the matrix-analytic machinery itself -- the randomized blocks, the
2303 * G matrix, the drift, the decay rate, the level probabilities -- which is what
2304 * a caller checking a queue's stability or tail decay asks for, and what a
2305 * cross-codebase comparison of the QBD assembly needs to see.
2306 */
2307template <class T>
2308int solve_model_mam_internals(const std::string& file, const Knobs& k) {
2309 line::qn::Network<T> net = read_model<T>(file);
2310 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2311 (void)k;
2313 const double q = line::num_traits<T>::to_double(r.q);
2314
2315 if (g_json_output) {
2316 line::reg::Json p = line::reg::Json::object();
2317 p["type"] = "MAMResult";
2318 p["indexBase"] = 0;
2319 p["lambda"] = line::num_traits<T>::to_double(r.lambda);
2320 p["rho"] = line::num_traits<T>::to_double(r.rho);
2321 p["uniformization"] = q;
2322 p["drift"] = line::num_traits<T>::to_double(r.drift);
2323 p["decayRate"] = r.decayRate;
2324 p["pi0"] = line::num_traits<T>::to_double(r.pi0);
2328 p["truncLevel"] = r.truncLevel;
2329 p["truncError"] = r.truncError;
2330 p["gConverged"] = r.gConverged;
2331 p["theta"] = vector_json(r.theta);
2332 p["alpha"] = vector_json(r.alpha);
2333 line::reg::Json lv = line::reg::Json::array();
2334 for (std::size_t n = 0; n < r.levelProb.rows(); ++n) {
2335 line::reg::Json row = line::reg::Json::array();
2336 for (std::size_t j = 0; j < r.levelProb.cols(); ++j)
2337 row.push_back(line::num_traits<T>::to_double(r.levelProb(n, j)));
2338 lv.push_back(row);
2339 }
2340 p["levelProb"] = lv;
2341 emit_analysis<T>("internals", p, std::string());
2342 return 0;
2343 }
2344 std::printf("SolverMAM arith=%s stations=%zu\n", line::num_traits<T>::name(), sn.nstations);
2345 std::printf("lambda=%.10g rho=%.10g q=%.10g drift=%.10g decayRate=%.10g\n",
2348 std::printf("pi0=%.10g QLen=%.10g Util=%.10g Tput=%.10g truncLevel=%zu truncError=%.3g "
2349 "gConverged=%s\n",
2354 r.gConverged ? "yes" : "no");
2355 std::printf("%-8s %16s\n", "Level", "Prob");
2356 for (std::size_t n = 0; n < r.levelProb.rows(); ++n) {
2357 double s = 0.0;
2358 for (std::size_t j = 0; j < r.levelProb.cols(); ++j)
2360 std::printf("%-8zu %16.10g\n", n, s);
2361 }
2362 return 0;
2363}
2364
2365/** A JSON flag value, given inline or as the path of a file holding it. */
2366line::reg::Json read_json_arg(const std::string& spec, const char* flag) {
2367 std::string text = spec;
2368 const std::size_t at = spec.find_first_not_of(" \t\r\n");
2369 if (at == std::string::npos || (spec[at] != '{' && spec[at] != '[')) {
2370 std::ifstream in(spec.c_str());
2371 if (!in)
2372 throw line::InputError(std::string(flag) + " is neither inline JSON nor a readable "
2373 "file (got '" + spec + "')");
2374 text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
2375 }
2376 try {
2377 return line::reg::Json::parse(text);
2378 } catch (const line::reg::Json::parse_error& e) {
2379 throw line::InputError(std::string("malformed ") + flag + " JSON: " + e.what());
2380 }
2381}
2382
2383/** A JSON array of arrays as an integer table. */
2384std::vector<std::vector<int> > json_int_table(const line::reg::Json& j, const char* name) {
2385 if (!j.is_array())
2386 throw line::InputError(std::string("--qrf-params ") + name + " must be an array of rows");
2387 std::vector<std::vector<int> > out;
2388 for (std::size_t m = 0; m < j.size(); ++m) {
2389 if (!j[m].is_array())
2390 throw line::InputError(std::string("--qrf-params ") + name + " must be an array of "
2391 "rows");
2392 std::vector<int> row;
2393 for (std::size_t c = 0; c < j[m].size(); ++c) row.push_back(j[m][c].get<int>());
2394 out.push_back(row);
2395 }
2396 return out;
2397}
2398
2399/**
2400 * Decode `--qrf-params` into `BaOptions::qrf_params`.
2401 *
2402 * Required: f, MR, BB, MM, MM1, ZZ, exactly what `sn_to_qrf_params` demands.
2403 * F is optional and falls back to sn.cap. ZM is DERIVED from ZZ and a supplied
2404 * one is ignored: it is max(ZZ) by definition, and a larger one empties the
2405 * polytope through THM3I instead of failing cleanly.
2406 */
2407void decode_qrf_params(const std::string& spec, line::ba::BaOptions& opt) {
2408 const line::reg::Json j = read_json_arg(spec, "--qrf-params");
2409 if (!j.is_object()) throw line::InputError("--qrf-params must be a JSON object");
2410 const char* required[] = {"f", "MR", "BB", "MM", "MM1", "ZZ"};
2411 std::string missing;
2412 for (std::size_t i = 0; i < 6; ++i)
2413 if (!j.contains(required[i]))
2414 missing += (missing.empty() ? "" : ", ") + std::string(required[i]);
2415 if (!missing.empty())
2416 throw line::InputError("--qrf-params is missing the field(s) " + missing +
2417 "; required are f, MR, BB, MM, MM1, ZZ (F is optional, ZM is "
2418 "derived from ZZ)");
2420 p.supplied = true;
2421 p.f = j["f"].get<int>();
2422 p.MR = j["MR"].get<int>();
2423 p.BB = json_int_table(j["BB"], "BB");
2424 p.MM = json_int_table(j["MM"], "MM");
2425 p.MM1 = json_int_table(j["MM1"], "MM1");
2426 if (!j["ZZ"].is_array()) throw line::InputError("--qrf-params ZZ must be an array");
2427 for (std::size_t i = 0; i < j["ZZ"].size(); ++i) p.ZZ.push_back(j["ZZ"][i].get<int>());
2428 if (j.contains("F"))
2429 for (std::size_t i = 0; i < j["F"].size(); ++i) p.F.push_back(j["F"][i].get<int>());
2430 opt.qrf_params = p;
2431}
2432
2433/** Decode `--qrf-alpha`, the (nstations x N) load-dependent scaling. */
2434void decode_qrf_alpha(const std::string& spec, line::ba::BaOptions& opt) {
2435 const line::reg::Json j = read_json_arg(spec, "--qrf-alpha");
2436 if (!j.is_array() || j.empty() || !j[0].is_array())
2437 throw line::InputError("--qrf-alpha must be a JSON array of rows, one per station");
2438 line::Matrix<double> a(j.size(), j[0].size(), 0.0);
2439 for (std::size_t i = 0; i < j.size(); ++i) {
2440 if (!j[i].is_array() || j[i].size() != j[0].size())
2441 throw line::InputError("--qrf-alpha rows must all have the same length");
2442 for (std::size_t n = 0; n < j[i].size(); ++n) a(i, n) = j[i][n].get<double>();
2443 }
2444 opt.qrf_alpha = a;
2445}
2446
2447/** The knobs the two SolverBA arms share. */
2448void apply_ba_knobs(const Knobs& k, line::ba::BaOptions& opt) {
2449 if (!k.method.empty() && k.method != "default") opt.method = k.method;
2450 if (k.level > 0) opt.level = k.level;
2451 if (!k.qrf_params.empty()) decode_qrf_params(k.qrf_params, opt);
2452 if (!k.qrf_alpha.empty()) decode_qrf_alpha(k.qrf_alpha, opt);
2453}
2454
2455/**
2456 * Solve a Network model.json with SolverBA and print the same table.
2457 *
2458 * A BOUND, not an estimate: `--method` names which side of which hierarchy is
2459 * wanted (`aba.upper`, `gb.lower`, ...) and `default` resolves to `gb.upper`,
2460 * as in the reference. `options.level` keeps its default of 2. The table is
2461 * therefore not comparable with an exact solver's row except as a bracket,
2462 * which is why the parity row is separate.
2463 */
2464template <class T>
2465int solve_model_ba(const std::string& file, const Knobs& k) {
2466 line::qn::Network<T> net = read_model<T>(file);
2468 apply_ba_knobs(k, opt);
2470 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2471
2472 std::printf("SolverBA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
2473 r.actualmethod.c_str(),
2474 line::util::method_type("BA", r.actualmethod).c_str());
2475 print_avg_table<T>(sn, r);
2476 return 0;
2477}
2478
2479/**
2480 * `-s ba -a bounds`: `SolverBA.getBounds` and its `getBoundsTable` row filter.
2481 *
2482 * NOT THE SAME ANSWER AS `-a avg`, which is why it is its own analysis. `-a avg`
2483 * reports ONE side of ONE family -- whichever `--method` named -- so a caller who
2484 * wants the bracket has to run the solver twice and know which two method names
2485 * pair up. `getBounds` takes the family (the method's prefix before the first
2486 * dot) and re-runs both sides under the caller's FULL option set, so a
2487 * hierarchical family tightens with `--level` as it should.
2488 *
2489 * A ONE-SIDED FAMILY REPORTS NaN ON THE SIDE IT LACKS, never zero: `cub` is
2490 * upper-only and `mbjb`/`ldac` are lower-only, and a zero there would read as
2491 * a lower bound of zero rather than as the absence of one.
2492 */
2493template <class T>
2494int solve_model_ba_bounds(const std::string& file, const Knobs& k) {
2495 line::qn::Network<T> net = read_model<T>(file);
2497 apply_ba_knobs(k, opt);
2498 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2499 const line::ba::BaBounds<T> b = line::ba::ba_bounds(sn, opt);
2500 const std::string am = line::ba::resolve_method(opt.method);
2501
2502 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
2503 if (g_json_output) {
2504 line::reg::Json p = line::reg::Json::object();
2505 p["type"] = "BoundsTable";
2506 p["indexBase"] = 0;
2507 p["family"] = am.substr(0, am.find('.'));
2508 p["hasLower"] = b.has_lower;
2509 p["hasUpper"] = b.has_upper;
2510 for (const char* key : {"Station", "JobClass", "QLower", "QUpper", "TLower", "TUpper"})
2511 p[key] = line::reg::Json::array();
2512 for (std::size_t i = 0; i < sn.nstations; ++i)
2513 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2514 if (!b.keep[i][c]) continue;
2515 p["Station"].push_back(sn.stations[i].name);
2516 p["JobClass"].push_back(sn.classes[c].name);
2517 p["QLower"].push_back(d(b.Qlower(i, c)));
2518 p["QUpper"].push_back(d(b.Qupper(i, c)));
2519 p["TLower"].push_back(d(b.Tlower(i, c)));
2520 p["TUpper"].push_back(d(b.Tupper(i, c)));
2521 }
2522 emit_analysis<T>("bounds", p, am);
2523 return 0;
2524 }
2525 std::printf("SolverBA arith=%s method=%s type=%s family=%s sides=%s\n",
2526 line::num_traits<T>::name(), am.c_str(),
2527 line::util::method_type("BA", am).c_str(), am.substr(0, am.find('.')).c_str(),
2528 b.has_lower ? (b.has_upper ? "lower,upper" : "lower") : "upper");
2529 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "QLower", "QUpper",
2530 "TLower", "TUpper");
2531 for (std::size_t i = 0; i < sn.nstations; ++i)
2532 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2533 if (!b.keep[i][c]) continue;
2534 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", sn.stations[i].name.c_str(),
2535 sn.classes[c].name.c_str(), d(b.Qlower(i, c)), d(b.Qupper(i, c)),
2536 d(b.Tlower(i, c)), d(b.Tupper(i, c)));
2537 }
2538 return 0;
2539}
2540
2541/**
2542 * `-s lqns` on a flat Network: its qns methods, the model handed to the external
2543 * `qnsolver` binary (or, closed and not product-form, through QN2LQN to lqns).
2544 *
2545 * The only wrapper on the model-solving path. The banner names it separately
2546 * from the table so a reader can tell an independent tool's numbers from the
2547 * port's own -- which is the whole reason the wrapper exists.
2548 */
2549template <class T>
2550int solve_model_lqns_network(const std::string& file, const Knobs& k) {
2551 line::qn::Network<T> net = read_model<T>(file);
2553 if (!k.method.empty()) opt.method = k.method;
2554 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
2555 // --samples is deliberately NOT forwarded: `options.samples` reaches the
2556 // JMVA document as `maxSamples`, which is JMT's Monte Carlo cap and which
2557 // qnsolver reads past. The shared knob ladder below refuses it for every
2558 // solver that draws nothing, and this path is one of them.
2559 opt.timeout = k.timeout_seconds;
2560 opt.keep = k.keep;
2562 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2563
2564 if (!g_json_output)
2565 std::printf("SolverLQNS arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
2566 r.actualmethod.c_str(),
2567 line::util::method_type("LQNS", r.actualmethod).c_str());
2568 print_avg_table<T>(sn, r);
2569 return 0;
2570}
2571
2572/**
2573 * Emit the base-class exponential response-time CDF fallback, the same
2574 * `CdfRespT` document every real distributional arm emits.
2575 *
2576 * This is `@@NetworkSolver/getCdfRespT.m`: the solvers without a
2577 * distributional result of their own (MVA, LQNS, BA, AG) inherit an exponential
2578 * law with the right mean in the reference, and refusing `-a cdf` for them
2579 * diverged from it. The curve says nothing about the tail; the banner names
2580 * the solver so the caller knows which mean it wraps.
2581 */
2582template <class T>
2583int emit_default_cdf(const char* solver_name, const line::qn::NetworkStruct<T>& sn,
2584 const line::mva::AvgResult<T>& r) {
2585 const std::vector<std::vector<line::solvers::DefaultCdfCurve>> RD =
2587 if (g_json_output) {
2588 line::reg::Json p = line::reg::Json::object();
2589 p["type"] = "CdfRespT";
2590 p["indexBase"] = 0;
2591 line::reg::Json arr = line::reg::Json::array();
2592 for (std::size_t i = 0; i < RD.size(); ++i)
2593 for (std::size_t c = 0; c < RD[i].size(); ++c) {
2594 if (RD[i][c].t.empty()) continue;
2595 line::reg::Json e = line::reg::Json::object();
2596 e["Station"] = sn.stations[i].name;
2597 e["JobClass"] = sn.classes[c].name;
2598 e["station"] = i;
2599 e["jobclass"] = c;
2600 e["t"] = line::reg::Json(RD[i][c].t);
2601 e["F"] = line::reg::Json(RD[i][c].F);
2602 arr.push_back(e);
2603 }
2604 p["respt"] = arr;
2605 emit_analysis<T>("cdf", p, std::string());
2606 return 0;
2607 }
2608 std::printf("%s arith=%s method=%s (exponential fallback with the solver's mean)\n",
2609 solver_name, line::num_traits<T>::name(), r.actualmethod.c_str());
2610 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
2611 for (std::size_t i = 0; i < RD.size(); ++i)
2612 for (std::size_t c = 0; c < RD[i].size(); ++c)
2613 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
2614 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
2615 sn.classes[c].name.c_str(), RD[i][c].t[j], RD[i][c].F[j]);
2616 return 0;
2617}
2618
2619/** `-s mva -a cdf`: the inherited exponential fallback over the MVA means. */
2620template <class T>
2621int solve_model_mva_cdf(const std::string& file, const Knobs& k) {
2622 line::qn::Network<T> net = read_model<T>(file);
2624 if (!k.method.empty() && k.method != "default") opt.method = k.method;
2625 if (k.tol >= 0.0) opt.tol = k.tol;
2626 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
2627 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
2628 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
2629 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
2630 line::Matrix<T> init;
2632 return emit_default_cdf<T>("SolverMVA", net.get_struct(), r);
2633}
2634
2635/** `-s ag -a cdf`: the inherited exponential fallback over the RCAT means. */
2636template <class T>
2637int solve_model_ag_cdf(const std::string& file, const Knobs& k) {
2638 line::qn::Network<T> net = read_model<T>(file);
2640 apply_ag_knobs(k, opt);
2642 return emit_default_cdf<T>("SolverAG", net.get_struct(), r);
2643}
2644
2645/** `-s ba -a cdf`: the inherited exponential fallback over the bound means. */
2646template <class T>
2647int solve_model_ba_cdf(const std::string& file, const Knobs& k) {
2648 line::qn::Network<T> net = read_model<T>(file);
2650 apply_ba_knobs(k, opt);
2652 return emit_default_cdf<T>("SolverBA", net.get_struct(), r);
2653}
2654
2655/** `-s lqns -a cdf` on a flat Network: the inherited exponential fallback over qnsolver's means. */
2656int solve_model_lqns_network_cdf(const std::string& file, const Knobs& k) {
2657 line::qn::Network<double> net = read_model<double>(file);
2659 if (!k.method.empty()) opt.method = k.method;
2660 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
2661 opt.timeout = k.timeout_seconds;
2662 opt.keep = k.keep;
2665 return emit_default_cdf<double>("SolverLQNS", net.get_struct(), r);
2666}
2667
2668
2669/**
2670 * `-a interval`: the support-only range, the reference's `getIntervalTable`.
2671 *
2672 * A SEPARATE FUNCTION because it is printed before the ensemble exists: on the
2673 * exact path there is no design to solve, so the interval arrives without a
2674 * `UqSolution` beside it.
2675 */
2676template <class T>
2677int print_uq_interval(const line::qn::NetworkStruct<T>& sn, const line::uq::UqInterval<T>& iv,
2678 const std::string& stage, std::size_t npriors) {
2679 auto v = [](const line::Matrix<T>& M, std::size_t i, std::size_t c) {
2680 return M.empty() ? 0.0 : line::num_traits<T>::to_double(M(i, c));
2681 };
2682 line::reg::Json p = line::reg::Json::object();
2683 p["type"] = "IntervalTable";
2684 p["indexBase"] = 0;
2685 p["exact"] = iv.exact;
2686 p["intervalMethod"] = iv.method;
2687 if (!iv.why.empty()) p["why"] = iv.why;
2688 if (iv.has_totals) {
2689 p["X"] = {line::num_traits<T>::to_double(iv.Xlo),
2691 p["Rtot"] = {line::num_traits<T>::to_double(iv.Rtot_lo),
2693 }
2694 for (const char* key : {"Station", "JobClass", "QLen_lo", "QLen_up", "Util_lo", "Util_up",
2695 "RespT_lo", "RespT_up", "Tput_lo", "Tput_up"})
2696 p[key] = line::reg::Json::array();
2697 if (!g_json_output) {
2698 std::printf("SolverUQ arith=%s interval=%s exact=%s priors=%zu stage=%s\n",
2699 line::num_traits<T>::name(), iv.method.c_str(), iv.exact ? "yes" : "no",
2700 npriors, stage.c_str());
2701 // A RANGE THAT IS NOT AN ENCLOSURE MUST SAY SO. The sampled path
2702 // spans the design points only, so on a continuous Prior it lies
2703 // strictly inside the true range; printing it beside an exact hull
2704 // without the reason would make the two indistinguishable.
2705 if (!iv.exact)
2706 std::fprintf(stderr,
2707 "Warning: exact interval MVA does not apply (%s); the range below is "
2708 "over the solved design points and is not an enclosure.\n",
2709 iv.why.c_str());
2710 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s %12s %12s\n", "Station",
2711 "JobClass", "QLen_lo", "QLen_up", "Util_lo", "Util_up", "RespT_lo",
2712 "RespT_up", "Tput_lo", "Tput_up");
2713 }
2714 for (std::size_t i = 0; i < sn.nstations; ++i)
2715 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2716 // The reference's row filter: an upper endpoint of zero on every
2717 // presence metric means the class never visits the station.
2718 if (v(iv.Qup, i, c) <= 0.0 && v(iv.Uup, i, c) <= 0.0 && v(iv.Tup, i, c) <= 0.0)
2719 continue;
2720 if (!g_json_output) {
2721 std::printf(
2722 "%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
2723 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), v(iv.Qlo, i, c),
2724 v(iv.Qup, i, c), v(iv.Ulo, i, c), v(iv.Uup, i, c), v(iv.Rlo, i, c),
2725 v(iv.Rup, i, c), v(iv.Tlo, i, c), v(iv.Tup, i, c));
2726 continue;
2727 }
2728 p["Station"].push_back(sn.stations[i].name);
2729 p["JobClass"].push_back(sn.classes[c].name);
2730 p["QLen_lo"].push_back(v(iv.Qlo, i, c));
2731 p["QLen_up"].push_back(v(iv.Qup, i, c));
2732 p["Util_lo"].push_back(v(iv.Ulo, i, c));
2733 p["Util_up"].push_back(v(iv.Uup, i, c));
2734 p["RespT_lo"].push_back(v(iv.Rlo, i, c));
2735 p["RespT_up"].push_back(v(iv.Rup, i, c));
2736 p["Tput_lo"].push_back(v(iv.Tlo, i, c));
2737 p["Tput_up"].push_back(v(iv.Tup, i, c));
2738 }
2739 if (g_json_output) emit_analysis<T>("interval", p, iv.method);
2740 return 0;
2741}
2742
2743/**
2744 * Solve a Network model.json carrying a Prior with SolverUQ.
2745 *
2746 * TWO FLAGS MEAN SOMETHING ELSE HERE, and both are UQ's own rather than the
2747 * stage solver's: `--method` names the DESIGN (quad or mc, the
2748 * reference's `options.method`), and `--samples` the number of nodes per
2749 * continuous Prior (`options.samples`, 11 by default and not the simulation
2750 * default, since each node is a full solver run). The engine that runs at each
2751 * point is `--uq-solver`, and it keeps its own defaults for everything except
2752 * the convergence knobs, which UQ does not have and therefore passes through.
2753 * A caller who wants an SSA run length AND a UQ design cannot state both, so
2754 * the SSA stage keeps its default sample count; that is stated in --help rather
2755 * than resolved by giving one flag two meanings.
2756 *
2757 * `-a posterior` prints the design itself -- every point, its weight and its
2758 * metrics -- because the expectation alone hides whether it averaged two nearby
2759 * models or two wildly different ones, and that spread IS the answer to an
2760 * uncertainty question.
2761 */
2762template <class T>
2763int solve_model_uq(const std::string& file, const Knobs& k, const std::string& analysis) {
2764 line::qn::Network<T> net = read_model<T>(file);
2766 if (!k.method.empty()) opt.method = k.method;
2767 if (k.samples) opt.samples = k.samples;
2768 if (k.seed) opt.seed = k.seed;
2770 so.solver = k.uq_solver;
2771 so.tol = k.tol;
2772 so.iter_tol = k.iter_tol;
2773 so.iter_max = k.iter_max;
2774 so.cutoff = k.cutoff;
2775
2776 if (analysis == "interval") {
2777 // BEFORE THE ENSEMBLE, because the exact path does not need one: it is
2778 // 2*(m+2) MVA calls over the demand box, and the reference's
2779 // `getInterval` likewise reaches `intervalByMVA` without touching
2780 // `self.results`. Only the sampling fallback solves the design.
2781 const line::uq::UqInterval<T> iv =
2783 const line::qn::NetworkStruct<T>& isn = net.get_struct();
2784 return print_uq_interval<T>(isn, iv, so.solver, line::uq::uq_detect_priors(isn).size());
2785 }
2786
2787 const line::uq::UqSolution<T> r =
2789 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2790
2791 if (analysis == "avg") {
2792 if (!g_json_output)
2793 std::printf("SolverUQ arith=%s design=%s points=%zu priors=%zu stage=%s method=%s\n",
2794 line::num_traits<T>::name(), r.method.c_str(), r.points.size(),
2795 r.sites.size(), so.solver.c_str(), r.avg.actualmethod.c_str());
2796 print_avg_table<T>(sn, r.avg);
2797 return 0;
2798 }
2799
2800 // -a posterior: the per-design-point table, the reference's getPosteriorTable.
2801 line::reg::Json p = line::reg::Json::object();
2802 p["type"] = "PosteriorTable";
2803 p["indexBase"] = 0;
2804 p["design"] = r.method;
2805 p["stage"] = so.solver;
2806 line::reg::Json sites = line::reg::Json::array();
2807 for (std::size_t l = 0; l < r.sites.size(); ++l) {
2808 line::reg::Json s = line::reg::Json::object();
2809 s["node"] = sn.nodes[r.sites[l].node - 1].name;
2810 s["class"] = sn.classes[r.sites[l].cls - 1].name;
2811 s["kind"] = r.sites[l].arrival ? "arrival" : "service";
2812 sites.push_back(s);
2813 }
2814 p["priors"] = sites;
2815 for (const char* key : {"Point", "Weight", "Station", "JobClass", "QLen", "Util", "RespT",
2816 "Tput"})
2817 p[key] = line::reg::Json::array();
2818 line::reg::Json means = line::reg::Json::array();
2819
2820 if (!g_json_output) {
2821 std::printf("SolverUQ arith=%s design=%s points=%zu priors=%zu stage=%s\n",
2822 line::num_traits<T>::name(), r.method.c_str(), r.points.size(), r.sites.size(),
2823 so.solver.c_str());
2824 // THE SUBSTITUTED MEANS ARE PROVENANCE, not decoration: a design point
2825 // is a model, and this line is what says which one.
2826 std::printf("%-6s %12s substituted means\n", "Point", "Weight");
2827 for (std::size_t e = 0; e < r.points.size(); ++e) {
2828 std::printf("%-6zu %12.6g ", e + 1, line::num_traits<T>::to_double(r.weights[e]));
2829 for (std::size_t l = 0; l < r.sites.size(); ++l)
2830 std::printf(" %s@%s=%.6g", sn.nodes[r.sites[l].node - 1].name.c_str(),
2831 sn.classes[r.sites[l].cls - 1].name.c_str(),
2832 line::num_traits<T>::to_double(r.design[e].dists[l].mean));
2833 std::printf("\n");
2834 }
2835 std::printf("%-6s %12s %-16s %-14s %12s %12s %12s %12s\n", "Point", "Weight", "Station",
2836 "JobClass", "QLen", "Util", "RespT", "Tput");
2837 }
2838 for (std::size_t e = 0; e < r.points.size(); ++e) {
2839 line::reg::Json row = line::reg::Json::array();
2840 for (std::size_t l = 0; l < r.sites.size(); ++l)
2841 row.push_back(line::num_traits<T>::to_double(r.design[e].dists[l].mean));
2842 means.push_back(row);
2843 const line::mva::AvgResult<T>& a = r.points[e];
2844 for (std::size_t i = 0; i < sn.nstations; ++i)
2845 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2846 const double q = line::num_traits<T>::to_double(a.QN(i, c));
2847 const double u = line::num_traits<T>::to_double(a.UN(i, c));
2848 const double rr = line::num_traits<T>::to_double(a.RN(i, c));
2849 const double t = line::num_traits<T>::to_double(a.TN(i, c));
2850 // getPosteriorTable's row filter, the PRESENCE metrics only: a
2851 // response time alone does not put a class at a station, and
2852 // reading it as presence kept rows the reference drops.
2853 if (q <= 0.0 && u <= 0.0 && t <= 0.0) continue;
2854 if (!g_json_output) {
2855 std::printf("%-6zu %12.6g %-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", e + 1,
2857 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), q, u, rr,
2858 t);
2859 continue;
2860 }
2861 p["Point"].push_back(e);
2862 p["Weight"].push_back(line::num_traits<T>::to_double(r.weights[e]));
2863 p["Station"].push_back(sn.stations[i].name);
2864 p["JobClass"].push_back(sn.classes[c].name);
2865 p["QLen"].push_back(q);
2866 p["Util"].push_back(u);
2867 p["RespT"].push_back(rr);
2868 p["Tput"].push_back(t);
2869 }
2870 }
2871 if (g_json_output) {
2872 p["substitutedMean"] = means;
2873 emit_analysis<T>("posterior", p, r.avg.actualmethod);
2874 }
2875 return 0;
2876}
2877
2878/**
2879 * The banner every `-s ctmc` analysis opens with.
2880 *
2881 * It carries the state count and the cutoff because a CTMC number is not the
2882 * model's number without them: the space is what was enumerated, and on an open
2883 * model the cutoff is what truncated it.
2884 */
2885template <class T>
2886void print_ctmc_banner(const line::ctmc::CtmcSolution<T>& d) {
2887 std::size_t cut = 0;
2888 for (std::size_t i = 0; i < d.cutoff.size(); ++i) cut = std::max(cut, d.cutoff[i]);
2889 if (cut)
2890 std::printf("SolverCTMC arith=%s method=%s type=%s states=%zu cutoff=%zu\n",
2892 line::util::method_type("CTMC", d.actualmethod).c_str(), d.chain.space.size(),
2893 cut);
2894 else
2895 std::printf("SolverCTMC arith=%s method=%s type=%s states=%zu\n",
2897 line::util::method_type("CTMC", d.actualmethod).c_str(), d.chain.space.size());
2898 // The library never writes to stderr, so an unseeded reducible mixture is
2899 // reported here or not at all -- and it is the one case where the printed
2900 // distribution is not the model's.
2901 if (!d.warning.empty()) std::fprintf(stderr, "warning: %s\n", d.warning.c_str());
2902}
2903
2904/**
2905 * The banner's own content as JSON, added to every `-s ctmc` payload.
2906 *
2907 * A CTMC number is not the model's number without them, which is why the
2908 * readable banner carries them: the space is what was actually enumerated, and
2909 * on an open model the cutoff is what truncated it. A host that reported the
2910 * occupancy of a truncated chain as the model's would be reporting a different
2911 * model's answer, so the two travel with the payload rather than only above it.
2912 */
2913template <class T>
2914line::reg::Json ctmc_meta(const line::ctmc::CtmcSolution<T>& d) {
2915 line::reg::Json m = line::reg::Json::object();
2916 m["states"] = d.chain.space.size();
2917 std::size_t cut = 0;
2918 for (std::size_t i = 0; i < d.cutoff.size(); ++i) cut = std::max(cut, d.cutoff[i]);
2919 if (cut) m["cutoff"] = cut;
2920 return m;
2921}
2922
2923/**
2924 * `-a avg`: the AvgTable, on whichever path the model's regions require.
2925 *
2926 * `solver_ctmc_analyzer_any` and not `solver_ctmc_analyzer`, so a WAITQ region
2927 * reaches the augmented walk that carries its token FIFO instead of the
2928 * lattice analyzer, which refuses it. Every other region rule keeps refusing.
2929 */
2930template <class T>
2931int solve_ctmc_avg(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
2934 print_ctmc_banner<T>(a.sol);
2935 print_avg_table<T>(sn, r);
2936 // A PARKED JOB IS IN NO QLen COLUMN: it left its station and sits in the
2937 // region's FIFO, so the model's population balances only once this is read
2938 // alongside the table rather than instead of it.
2939 if (a.waitq) {
2940 std::printf("%-16s %-14s %12s\n", "Region", "JobClass", "Parked");
2941 for (std::size_t c = 0; c < sn.nclasses && c < a.parked.size(); ++c)
2942 std::printf("%-16s %-14s %12.6g\n", "(all)", sn.classes[c].name.c_str(),
2944 }
2945 return 0;
2946}
2947
2948/**
2949 * `-a avg` under `--method mdd`: the AvgTable from the level aggregation.
2950 *
2951 * A SEPARATE ARM, and not a branch inside `solve_ctmc_avg`, because the method
2952 * never forms the |S|-state generator: there is no state space to print a state
2953 * count from and no cutoff to report, so the banner is the diagram's own -- the
2954 * cardinality it counts symbolically, what the levels actually hold, and whether
2955 * the answer is certified exact. Reporting the generator banner here would name
2956 * a chain that was never built.
2957 */
2958template <class T>
2959int solve_ctmc_mdd_avg(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
2960 const line::mdd::MddMcdOptions& mcdopt) {
2961 // EVERY BACKEND, `exact` included: the level solve picks Householder QR in
2962 // floating point and `line::lstsq` under exact arithmetic, so nothing here
2963 // takes a square root that a rational field has no answer for.
2966 d.avg = s.avg;
2969 std::size_t held = 0;
2970 for (std::size_t k = 0; k < s.level_sizes.size(); ++k) held += s.level_sizes[k];
2971 std::printf(
2972 "SolverCTMC arith=%s method=%s type=%s states=%lld held=%zu levels=%zu iters=%d "
2973 "encoding=%s exact=%s\n",
2975 line::util::method_type("CTMC", s.actualmethod).c_str(), s.num_states, held,
2976 s.level_sizes.size(), s.iters, s.encoding.c_str(),
2977 // "certified" is not "exact": a product-form model is exact however
2978 // much its diagram shares, so the false case says only that the
2979 // STRUCTURAL test did not fire.
2980 s.no_aggregation ? "certified" : "product-form-only");
2981 print_avg_table<T>(sn, r);
2982 return 0;
2983}
2984
2985/**
2986 * `-a prob`: the four SolverCTMC probability queries over the model's default
2987 * initial state -- `getProbSys`, `getProbSysAggr`, and `getProb`/`getProbAggr`
2988 * per station.
2989 *
2990 * ALL FOUR AND NOT ONE, because the joint and the aggregate answer different
2991 * questions and the pair is what makes either readable: `getProbSys` is the
2992 * probability of exactly that state, phases and buffer arrangement included,
2993 * while `getProbSysAggr` sums over every arrangement realizing the same
2994 * per-class counts. On a single-phase model with no buffer ordering the two
2995 * coincide, and where they do not the ratio is what the encoding added.
2996 *
2997 * NOT THE SAME NUMBERS AS `-s mva -a prob` OR `-s nc -a prob`, and that is the
2998 * point of having all three: MVA fits a binomial to its own means and NC takes a
2999 * ratio of normalizing constants under the product form, whereas these are the
3000 * stationary law of the chain itself and are exact for any model the chain
3001 * represents, product-form or not.
3002 */
3003template <class T>
3004int solve_ctmc_prob(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
3005 const Knobs& k) {
3008 if (!line::ctmc::analyzer_detail::default_init_state(sn, init))
3009 throw line::InputError(line::ErrorKind{"Model"},
3010 "-a prob reports the probability of the model's DEFAULT INITIAL STATE, and this "
3011 "model's initial marking admits no state; check the class populations against their "
3012 "reference stations");
3013 // `--state` is `getProb(node, state)`'s second argument: the ENCODED ROW of
3014 // one stateful node's own state space, not a per-class job count. The
3015 // reference substitutes it into `sn.state{node}` and leaves every other
3016 // node at its default, which is what happens here -- so the station
3017 // marginals below are the requested state's, and the two system
3018 // probabilities are that state's joined with the rest of the default
3019 // marking, exactly as `setState` followed by `getProbSys` reports it.
3020 if (!k.state.empty()) {
3021 if (!k.node)
3022 throw line::InputError(
3023 "--state is the state of ONE node and needs --node to say which; a bare state "
3024 "vector cannot be matched against a network whose nodes have different widths");
3025 const std::size_t isf = sn.stateful_index(k.node);
3026 if (isf == 0)
3027 throw line::InputError("--node " + std::to_string(k.node) +
3028 " is not a stateful node, so it holds no state to ask about");
3029 // THE WIDTH MUST MATCH EXACTLY, and a short vector is refused rather
3030 // than padded. Every row of a node's block is stored at the node's
3031 // widest encoding, so a padded row IS a state -- just not the one the
3032 // caller named: on an FCFS queue holding two jobs the encoding is
3033 // (buffer, in-service phase count) = (1,1), and padding `--state 2` to
3034 // (0,2) names a state the chain never visits, which would answer 0
3035 // where the caller expected the marginal. The width is reported so the
3036 // next attempt can be right.
3037 const std::size_t w = d.chain.space.empty() ? k.state.size()
3038 : d.chain.space[0].local[isf - 1].size();
3039 if (k.state.size() != w)
3040 throw line::InputError(
3041 "--state has " + std::to_string(k.state.size()) + " entries but node " +
3042 std::to_string(k.node) + " encodes its state in " + std::to_string(w) +
3043 "; getProb(node, state) takes the node's whole encoded row, and a shorter one "
3044 "padded with zeros is a different state rather than a partial one (use -a marg "
3045 "for a per-class job-count marginal)");
3046 std::vector<T> row(w, line::num_traits<T>::from_int(0));
3047 for (std::size_t i = 0; i < k.state.size(); ++i)
3048 row[i] = line::num_traits<T>::from_int(k.state[i]);
3049 init.local[isf - 1] = row;
3050 }
3051 const T psys = line::ctmc::solver_ctmc_joint(sn, d, init);
3052 const T psysaggr = line::ctmc::solver_ctmc_jointaggr(sn, d, init);
3053 const std::vector<T> pmarg = line::ctmc::solver_ctmc_marg(sn, d, init);
3054 const std::vector<T> pmargaggr = line::ctmc::solver_ctmc_margaggr(sn, d, init);
3055
3056 if (g_json_output) {
3057 line::reg::Json p = line::reg::Json::object();
3058 p["type"] = "ProbAggr";
3059 p["indexBase"] = 0;
3060 p["ProbSys"] = line::num_traits<T>::to_double(psys);
3061 p["ProbSysAggr"] = line::num_traits<T>::to_double(psysaggr);
3062 line::reg::Json st = line::reg::Json::array(), pm = line::reg::Json::array(),
3063 pa = line::reg::Json::array();
3064 for (std::size_t i = 0; i < sn.nstations; ++i) {
3065 st.push_back(sn.stations[i].name);
3066 pm.push_back(line::num_traits<T>::to_double(pmarg[i]));
3067 pa.push_back(line::num_traits<T>::to_double(pmargaggr[i]));
3068 }
3069 p["Station"] = st;
3070 p["Prob"] = pm;
3071 p["ProbAggr"] = pa;
3072 emit_analysis<T>("prob", p, d.actualmethod, ctmc_meta<T>(d));
3073 return 0;
3074 }
3075 print_ctmc_banner<T>(d);
3076 std::printf("ProbSys %.10g\n", line::num_traits<T>::to_double(psys));
3077 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(psysaggr));
3078 std::printf("%-16s %14s %14s\n", "Station", "Prob", "ProbAggr");
3079 for (std::size_t i = 0; i < sn.nstations; ++i)
3080 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(),
3082 line::num_traits<T>::to_double(pmargaggr[i]));
3083 return 0;
3084}
3085
3086/**
3087 * The derived START/PREEMPT filtration as one JSON block per (station, class),
3088 * each carrying its own 0-based Station and Class beside the usual From/To/Rate
3089 * triplets. An all-zero block is omitted: on a model with no preemption that is
3090 * every block of the PREEMPT filtration.
3091 */
3092template <class T>
3093line::reg::Json aux_filt_json(const std::vector<std::vector<line::Matrix<T> > >& filt,
3094 std::size_t n) {
3095 line::reg::Json blocks = line::reg::Json::array();
3096 for (std::size_t i = 0; i < filt.size(); ++i)
3097 for (std::size_t r = 0; r < filt[i].size(); ++r) {
3098 line::reg::Json bfrom = line::reg::Json::array(), bto = line::reg::Json::array(),
3099 brate = line::reg::Json::array();
3100 for (std::size_t a = 0; a < n; ++a)
3101 for (std::size_t b = 0; b < n; ++b) {
3102 const double q = line::num_traits<T>::to_double(filt[i][r](a, b));
3103 if (q == 0.0) continue;
3104 bfrom.push_back(a);
3105 bto.push_back(b);
3106 brate.push_back(q);
3107 }
3108 if (bfrom.empty()) continue;
3109 line::reg::Json e = line::reg::Json::object();
3110 e["Station"] = i;
3111 e["Class"] = r;
3112 e["From"] = bfrom;
3113 e["To"] = bto;
3114 e["Rate"] = brate;
3115 blocks.push_back(e);
3116 }
3117 return blocks;
3118}
3119
3120/**
3121 * `-a gen`: `getInfGen`, as Q in sparse triplets plus the synchronization list
3122 * its event filtration is indexed by.
3123 *
3124 * Q IS PRINTED SPARSELY. A generator's rows hold one entry per enabled
3125 * transition and the space can run to thousands of states, so the dense form
3126 * would be quadratic in a quantity that is linear in the model.
3127 */
3128template <class T>
3129int solve_ctmc_gen(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3131 o.keep_filtration = true; // the filtration is half of what getInfGen returns
3134 const std::size_t n = g.Q.rows();
3135 std::size_t nnz = 0;
3136 for (std::size_t i = 0; i < n; ++i)
3137 for (std::size_t j = 0; j < n; ++j)
3138 if (line::num_traits<T>::to_double(g.Q(i, j)) != 0.0) ++nnz;
3139 if (g_json_output) {
3140 line::reg::Json p = line::reg::Json::object();
3141 p["type"] = "InfGen";
3142 p["indexBase"] = 0;
3143 p["size"] = n;
3144 p["nnz"] = nnz;
3145 // SPARSE HERE TOO, for the reason the table is: a generator has one entry
3146 // per enabled transition, so the dense form is quadratic in a quantity
3147 // that is linear in the model. A host rebuilds Q with one scatter.
3148 line::reg::Json from = line::reg::Json::array(), to = line::reg::Json::array(),
3149 rate = line::reg::Json::array();
3150 for (std::size_t i = 0; i < n; ++i)
3151 for (std::size_t j = 0; j < n; ++j) {
3152 const double q = line::num_traits<T>::to_double(g.Q(i, j));
3153 if (q == 0.0) continue;
3154 from.push_back(i);
3155 to.push_back(j);
3156 rate.push_back(q);
3157 }
3158 p["From"] = from;
3159 p["To"] = to;
3160 p["Rate"] = rate;
3161 // The synchronization list is HALF of what getInfGen returns: without it
3162 // the filtration's index has no meaning, so it travels with Q. Its node
3163 // and class are the port's own 1-BASED indices, with 0 the LOCAL dummy
3164 // -- `indexBase` above governs the state indices, which is where a host
3165 // would otherwise be indexing a chain by an off-by-one.
3166 line::reg::Json sync = line::reg::Json::array();
3167 for (std::size_t a = 0; a < g.sync.size() && a < g.filt.size(); ++a) {
3168 // THE FILTER ITSELF, not only its nnz as the readable table reports:
3169 // `eventFilt` is half of what getInfGen returns, and a host handed
3170 // only Q cannot recover which synchronization contributed a rate --
3171 // Q's entries have already summed every one of them.
3172 std::size_t fnz = 0;
3173 line::reg::Json ffrom = line::reg::Json::array(), fto = line::reg::Json::array(),
3174 frate = line::reg::Json::array();
3175 for (std::size_t i = 0; i < n; ++i)
3176 for (std::size_t j = 0; j < n; ++j) {
3177 const double q = line::num_traits<T>::to_double(g.filt[a](i, j));
3178 if (q == 0.0) continue;
3179 ++fnz;
3180 ffrom.push_back(i);
3181 fto.push_back(j);
3182 frate.push_back(q);
3183 }
3184 line::reg::Json e = line::reg::Json::object();
3185 e["From"] = ffrom;
3186 e["To"] = fto;
3187 e["Rate"] = frate;
3188 e["activeEvent"] = line::lang::event_to_text(g.sync[a].active.event);
3189 e["activeNode"] = g.sync[a].active.node;
3190 e["activeClass"] = g.sync[a].active.cls;
3191 e["passiveEvent"] = line::lang::event_to_text(g.sync[a].passive.event);
3192 e["passiveNode"] = g.sync[a].passive.node;
3193 e["passiveClass"] = g.sync[a].passive.cls;
3194 e["nnz"] = fnz;
3195 sync.push_back(e);
3196 }
3197 p["sync"] = sync;
3198 // The DERIVED filtrations travel under their own keys, one block per
3199 // (station, class), because they are NOT synchronizations: a START
3200 // rides on an arc `sync` already carries, so folding them in would make
3201 // a host summing the filtration double-count the generator.
3202 p["startFilt"] = aux_filt_json<T>(g.start_filt, n);
3203 p["preemptFilt"] = aux_filt_json<T>(g.preempt_filt, n);
3204 emit_analysis<T>("gen", p, d.actualmethod, ctmc_meta<T>(d));
3205 return 0;
3206 }
3207 print_ctmc_banner<T>(d);
3208 std::printf("InfGen events=%zu nnz=%zu\n", g.sync.size(), nnz);
3209 std::printf("%8s %8s %16s\n", "From", "To", "Rate");
3210 for (std::size_t i = 0; i < n; ++i)
3211 for (std::size_t j = 0; j < n; ++j) {
3212 const double q = line::num_traits<T>::to_double(g.Q(i, j));
3213 if (q != 0.0) std::printf("%8zu %8zu %16.10g\n", i + 1, j + 1, q);
3214 }
3215 std::printf("%6s %-10s %6s %6s %-10s %6s %6s %8s\n", "Event", "ActEvent", "ActNode", "ActCls",
3216 "PasEvent", "PasNode", "PasCls", "Nnz");
3217 for (std::size_t a = 0; a < g.sync.size() && a < g.filt.size(); ++a) {
3218 std::size_t fnz = 0;
3219 for (std::size_t i = 0; i < n; ++i)
3220 for (std::size_t j = 0; j < n; ++j)
3221 if (line::num_traits<T>::to_double(g.filt[a](i, j)) != 0.0) ++fnz;
3222 std::printf("%6zu %-10s %6zu %6zu %-10s %6zu %6zu %8zu\n", a + 1,
3223 line::lang::event_to_text(g.sync[a].active.event), g.sync[a].active.node,
3224 g.sync[a].active.cls, line::lang::event_to_text(g.sync[a].passive.event),
3225 g.sync[a].passive.node, g.sync[a].passive.cls, fnz);
3226 }
3227 return 0;
3228}
3229
3230/** `-a states`: `getStateSpace` and `getStateSpaceAggr`, side by side. */
3231template <class T>
3232int solve_ctmc_states(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3236 if (g_json_output) {
3237 line::reg::Json p = line::reg::Json::object();
3238 p["type"] = "StateSpace";
3239 p["indexBase"] = 0;
3240 // The per-node widths are the ONLY thing that makes a flat row decodable:
3241 // they are where one stateful node's block ends and the next begins.
3242 p["NodeWidths"] = index_json(s.node_width);
3243 p["space"] = matrix_json(s.flat);
3244 p["spaceAggr"] = matrix_json(A);
3245 // `localStateSpace` of `[stateSpace, localStateSpace] = getStateSpace`.
3246 // One entry per stateful node, in stateful-node order, holding that
3247 // node's distinct local rows. A host with only the flat space can
3248 // return the second output as a SINGLE cell holding the whole space,
3249 // which is what `getStateSpace.m` does under `lang='cpp'`, and a caller
3250 // indexing it per node then reads the global space for every node.
3251 line::reg::Json loc = line::reg::Json::array();
3252 for (std::size_t f = 0; f < s.local.size(); ++f) loc.push_back(matrix_json(s.local[f]));
3253 p["localSpace"] = loc;
3254 // pi travels with the space because a row of the space is not an answer:
3255 // the pair (state, probability) is, and reading them from two invocations
3256 // would risk pairing one solve's states with another solve's law.
3257 p["pi"] = vector_json(d.pi);
3258 emit_analysis<T>("states", p, d.actualmethod, ctmc_meta<T>(d));
3259 return 0;
3260 }
3261 print_ctmc_banner<T>(d);
3262 // The per-node widths are printed because the flat row is only decodable
3263 // with them: they are where one node's block ends and the next begins.
3264 std::printf("NodeWidths");
3265 for (std::size_t f = 0; f < s.node_width.size(); ++f) std::printf(" %zu", s.node_width[f]);
3266 std::printf("\n");
3267 std::printf("%8s %12s %s\n", "State", "Prob", "Detailed | Aggregate");
3268 for (std::size_t i = 0; i < s.flat.rows(); ++i) {
3269 std::printf("%8zu %12.6g ", i + 1, line::num_traits<T>::to_double(d.pi[i]));
3270 for (std::size_t c = 0; c < s.flat.cols(); ++c)
3271 std::printf(" %g", line::num_traits<T>::to_double(s.flat(i, c)));
3272 std::printf(" |");
3273 for (std::size_t c = 0; c < A.cols(); ++c)
3274 std::printf(" %g", line::num_traits<T>::to_double(A(i, c)));
3275 std::printf("\n");
3276 }
3277 return 0;
3278}
3279
3280/**
3281 * `-a sens`: `getSensitivityRanking` over the exponential service rates.
3282 *
3283 * THE REWARD IS STATED, NOT INFERRED. The reference makes the caller supply one,
3284 * and a model.json carries no reward function, so the CLI has to name the reward
3285 * it ranks against or the numbers mean nothing: it is the mean number of jobs at
3286 * the QUEUEING stations, which excludes the Source (whose column is the infinite
3287 * reservoir) and the Delay stations (whose population is think time, not work).
3288 *
3289 * Only an exponential service pair becomes a parameter. Perturbing any other
3290 * distribution would mean replacing it with an exponential of the new rate,
3291 * which changes the model's shape rather than one of its parameters; those pairs
3292 * are listed as skipped rather than silently differenced.
3293 */
3294template <class T>
3295int solve_ctmc_sens(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3296 const std::size_t M = sn.nstations, K = sn.nclasses;
3299
3300 std::vector<bool> queueing(M, false);
3301 for (std::size_t i = 0; i < M; ++i)
3302 queueing[i] = sn.stations[i].nodetype != line::lang::NodeType::Source &&
3305
3306 std::vector<T> reward(d.chain.space.size(), line::num_traits<T>::from_int(0));
3307 for (std::size_t s = 0; s < reward.size(); ++s)
3308 for (std::size_t i = 0; i < M; ++i)
3309 if (queueing[i])
3310 for (std::size_t c = 0; c < K; ++c) reward[s] += A(s, i * K + c);
3311
3312 std::vector<line::ctmc::CtmcSensParam<T> > params;
3313 std::vector<std::string> skipped;
3314 for (std::size_t i = 0; i < M; ++i) {
3315 if (sn.stations[i].nodetype == line::lang::NodeType::Source) continue;
3316 for (std::size_t c = 0; c < K; ++c) {
3317 if (sn.disabled[i][c]) continue;
3318 const double mu = line::num_traits<T>::to_double(sn.rates(i, c));
3319 if (!(mu > 0.0)) continue;
3320 const std::string nm =
3321 "mu(" + sn.stations[i].name + "," + sn.classes[c].name + ")";
3322 if (sn.service[i][c].type != line::lang::ProcessType::EXP) {
3323 skipped.push_back(nm);
3324 continue;
3325 }
3327 p.name = nm;
3328 p.value = mu;
3329 p.set = [i, c](line::qn::NetworkStruct<T>& s, double v) {
3331 s.refresh_rates();
3332 };
3333 params.push_back(p);
3334 }
3335 }
3336 if (params.empty())
3337 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
3338 "-a sens found no exponential service rate to differentiate: every enabled "
3339 "(station, class) pair carries a non-exponential distribution, and replacing one with "
3340 "an exponential of the perturbed rate would change the model rather than a parameter "
3341 "of it");
3342
3343 const std::vector<line::ctmc::CtmcSensRank<T> > rank =
3344 line::ctmc::solver_ctmc_sensitivity_ranking(sn, opt, params, reward);
3345 if (g_json_output) {
3346 line::reg::Json p = line::reg::Json::object();
3347 p["type"] = "SensRanking";
3348 // THE REWARD IS STATED, NOT INFERRED, on this path too: a ranking is a
3349 // ranking against something, and a host that read these numbers without
3350 // knowing what was differentiated would be reading a sensitivity of an
3351 // unnamed functional.
3352 p["reward"] = "mean number of jobs at the queueing stations";
3353 line::reg::Json par = line::reg::Json::array(), val = line::reg::Json::array(),
3354 S = line::reg::Json::array(), SS = line::reg::Json::array();
3355 for (std::size_t l = 0; l < rank.size(); ++l) {
3356 par.push_back(rank[l].parameter);
3357 val.push_back(rank[l].value);
3358 S.push_back(line::num_traits<T>::to_double(rank[l].S));
3359 // null, not NaN and not 0: a zero mean reward makes the scaled form
3360 // undefined, which is a property of the model, and JSON has no NaN.
3361 if (rank[l].scaled_valid)
3362 SS.push_back(line::num_traits<T>::to_double(rank[l].SS));
3363 else
3364 SS.push_back(line::reg::Json());
3365 }
3366 p["Parameter"] = par;
3367 p["Value"] = val;
3368 p["Sens"] = S;
3369 p["ScaledSens"] = SS;
3370 line::reg::Json sk = line::reg::Json::array();
3371 for (std::size_t l = 0; l < skipped.size(); ++l) sk.push_back(skipped[l]);
3372 p["Skipped"] = sk;
3373 emit_analysis<T>("sens", p, d.actualmethod, ctmc_meta<T>(d));
3374 return 0;
3375 }
3376 print_ctmc_banner<T>(d);
3377 std::printf("Reward mean number of jobs at the queueing stations\n");
3378 std::printf("%-28s %14s %16s %16s\n", "Parameter", "Value", "Sens", "ScaledSens");
3379 for (std::size_t l = 0; l < rank.size(); ++l) {
3380 if (rank[l].scaled_valid)
3381 std::printf("%-28s %14.6g %16.8g %16.8g\n", rank[l].parameter.c_str(), rank[l].value,
3383 line::num_traits<T>::to_double(rank[l].SS));
3384 else
3385 // MATLAB reports NaN here; the reason is printed instead, since a
3386 // zero mean reward is a property of the model and not a failure.
3387 std::printf("%-28s %14.6g %16.8g %16s\n", rank[l].parameter.c_str(), rank[l].value,
3388 line::num_traits<T>::to_double(rank[l].S), "undefined(E[r]=0)");
3389 }
3390 for (std::size_t l = 0; l < skipped.size(); ++l)
3391 std::printf("Skipped %s: service is not exponential\n", skipped[l].c_str());
3392 return 0;
3393}
3394
3395/** `-a reward`: `getAvgReward`, the steady-state expectation of each reward. */
3396template <class T>
3397int solve_ctmc_reward(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3398 std::vector<std::string> names;
3399 const std::vector<T> r = line::ctmc::solver_ctmc_avg_reward(sn, opt, &names);
3400 if (g_json_output) {
3401 line::reg::Json p = line::reg::Json::object();
3402 p["type"] = "AvgReward";
3403 line::reg::Json nm = line::reg::Json::array();
3404 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
3405 p["Reward"] = nm;
3406 p["E"] = vector_json(r);
3407 // NO "method": this arm returns the expectations and no solved chain, so
3408 // there is no resolved method to report and none is invented.
3409 emit_analysis<T>("reward", p, std::string());
3410 return 0;
3411 }
3412 std::printf("SolverCTMC arith=%s rewards=%zu\n", line::num_traits<T>::name(), r.size());
3413 std::printf("%-28s %16s\n", "Reward", "E[r]");
3414 for (std::size_t l = 0; l < r.size(); ++l)
3415 std::printf("%-28s %16.10g\n", names[l].c_str(), line::num_traits<T>::to_double(r[l]));
3416 return 0;
3417}
3418
3419/**
3420 * `-a reward-value`: `@@SolverCTMC/getRewardValueFunction`, V^k(s).
3421 *
3422 * A DIFFERENT OBJECT FROM BOTH `-a reward` AND `-a tranreward`. `-a reward`
3423 * returns one number per reward, the steady-state E[r]; `-a tranreward` the
3424 * expected rate along a horizon; this returns the VALUE FUNCTION of ONE named
3425 * reward -- the reward accumulated over k uniformized steps, from every state
3426 * of the chain, as a (Tmax+1 x nstates) matrix. It is the object a policy
3427 * evaluation reads, and it is indexed by state, not by station.
3428 *
3429 * `--reward-name` IS REQUIRED and not defaulted to the first declared reward:
3430 * the value functions of two rewards are different matrices, and labelling one
3431 * with the caller's question would be a wrong answer rather than a missing one.
3432 */
3433template <class T>
3434int solve_ctmc_reward_value(const line::qn::NetworkStruct<T>& sn,
3435 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3436 if (k.reward_name.empty())
3437 throw line::InputError(
3438 "-a reward-value returns the value function of ONE reward and needs --reward-name to "
3439 "say which; -a reward returns the steady-state expectation of every declared reward");
3441 std::size_t which = rr.names.size();
3442 for (std::size_t l = 0; l < rr.names.size(); ++l)
3443 if (rr.names[l] == k.reward_name) which = l;
3444 if (which == rr.names.size()) {
3445 std::string avail;
3446 for (std::size_t l = 0; l < rr.names.size(); ++l)
3447 avail += (l ? ", " : "") + rr.names[l];
3448 throw line::InputError("--reward-name '" + k.reward_name +
3449 "' is not declared by this model; it declares: " +
3450 (avail.empty() ? std::string("(none)") : avail));
3451 }
3452 const line::Matrix<T>& V = rr.V[which];
3453
3454 if (g_json_output) {
3455 line::reg::Json p = line::reg::Json::object();
3456 p["type"] = "RewardValueFunction";
3457 p["indexBase"] = 0;
3458 p["Reward"] = rr.names[which];
3459 p["steps"] = V.rows();
3460 p["states"] = V.cols();
3461 p["t"] = vector_json(rr.t);
3462 p["V"] = matrix_json(V);
3463 p["stateSpaceAggr"] = matrix_json(rr.state_space_aggr);
3464 emit_analysis<T>("rewardvalue", p, std::string());
3465 return 0;
3466 }
3467 std::printf("SolverCTMC arith=%s reward=%s steps=%zu states=%zu\n",
3468 line::num_traits<T>::name(), rr.names[which].c_str(), V.rows(), V.cols());
3469 std::printf("%-10s %-10s %20s\n", "Step", "State", "V");
3470 for (std::size_t i = 0; i < V.rows(); ++i)
3471 for (std::size_t j = 0; j < V.cols(); ++j)
3472 std::printf("%-10zu %-10zu %20.10g\n", i, j, line::num_traits<T>::to_double(V(i, j)));
3473 return 0;
3474}
3475
3476/**
3477 * `-a tranreward`: `getTranReward`, E[r(X(t))] over the --tspan horizon.
3478 *
3479 * A DIFFERENT QUANTITY FROM `-a reward`, not a formatting of it. `-a reward`
3480 * returns the steady-state expectation, one number per reward; this returns the
3481 * expected reward RATE along the trajectory, which converges to that number but
3482 * is not it at any finite t. It is also not the accumulated reward `V`, which
3483 * the same header computes and which diverges -- the header calls that the
3484 * easiest mistake to make here, so the two are kept on separate flags.
3485 *
3486 * THE HORIZON IS REQUIRED, as it is for `-a tranprob`: E[r(X(t))] on an
3487 * unstated horizon is not a quantity, and the reference refuses an infinite one
3488 * rather than picking a bound.
3489 */
3490template <class T>
3491int solve_ctmc_tran_reward(const line::qn::NetworkStruct<T>& sn,
3492 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3494 (void)sn; (void)opt; (void)k;
3495 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
3496 "-a tranreward integrates the forward equation, which needs transcendental "
3497 "arithmetic; rerun with --arith double or --arith real");
3498 } else {
3499 if (k.t1 < 0.0)
3500 throw line::InputError(
3501 "-a tranreward integrates E[r(X(t))] and needs a horizon: pass --tspan <t0>:<t1>");
3502 std::vector<std::string> names;
3503 std::vector<T> t;
3504 const std::vector<std::vector<T> > r = line::ctmc::solver_ctmc_tran_reward(
3505 sn, opt, line::num_traits<T>::from_double(k.t0),
3506 line::num_traits<T>::from_double(k.t1), &t, &names);
3507 if (g_json_output) {
3508 line::reg::Json p = line::reg::Json::object();
3509 p["type"] = "TranReward";
3510 p["indexBase"] = 0;
3511 line::reg::Json nm = line::reg::Json::array();
3512 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
3513 p["Reward"] = nm;
3514 p["t"] = vector_json<T>(t);
3515 line::reg::Json e = line::reg::Json::array();
3516 for (std::size_t l = 0; l < r.size(); ++l) e.push_back(vector_json<T>(r[l]));
3517 p["E"] = e;
3518 emit_analysis<T>("tranreward", p, std::string());
3519 return 0;
3520 }
3521 std::printf("SolverCTMC arith=%s rewards=%zu points=%zu tspan=[%g,%g]\n",
3522 line::num_traits<T>::name(), r.size(), t.size(), k.t0, k.t1);
3523 std::printf("%-16s", "t");
3524 for (std::size_t l = 0; l < names.size(); ++l) std::printf(" %16s", names[l].c_str());
3525 std::printf("\n");
3526 for (std::size_t i = 0; i < t.size(); ++i) {
3527 std::printf("%-16.10g", line::num_traits<T>::to_double(t[i]));
3528 for (std::size_t l = 0; l < r.size(); ++l)
3529 std::printf(" %16.10g", line::num_traits<T>::to_double(r[l][i]));
3530 std::printf("\n");
3531 }
3532 return 0;
3533 }
3534}
3535
3536/**
3537 * `-a tranprob`: pi(t) over the --tspan horizon, labelled by the whole network
3538 * (`getTranProbSys` / `getTranProbSysAggr`) or by one node when `--node` names
3539 * it (`getTranProb` / `getTranProbAggr`).
3540 *
3541 * The forward equation is integrated ONCE and both label sets are taken off the
3542 * same `CtmcTransient`. Calling the (sn, opt, ...) overloads twice would solve
3543 * and integrate the chain twice for two views of one answer.
3544 */
3545template <class T>
3546int solve_ctmc_tranprob(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
3547 const Knobs& k) {
3548 if (k.t1 < 0.0)
3549 throw line::InputError(
3550 "-a tranprob integrates pi(t) and needs a horizon: pass --tspan <t0>:<t1>");
3553 const line::ctmc::CtmcTranProb<T> det =
3554 k.node ? line::ctmc::ctmc_get_tran_prob(sn, tr, k.node)
3555 : line::ctmc::ctmc_get_tran_prob_sys(sn, tr);
3556 const line::ctmc::CtmcTranProb<T> agg =
3557 k.node ? line::ctmc::ctmc_get_tran_prob_aggr(sn, tr, k.node)
3558 : line::ctmc::ctmc_get_tran_prob_sys_aggr(sn, tr);
3559 if (g_json_output) {
3560 line::reg::Json p = line::reg::Json::object();
3561 p["type"] = "TranProb";
3562 p["indexBase"] = 0;
3563 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
3564 if (k.node) p["node"] = k.node - 1;
3565 line::reg::Json span = line::reg::Json::array();
3566 span.push_back(k.t0);
3567 span.push_back(k.t1);
3568 // THE HORIZON IS PART OF THE ANSWER: pi(t) on an unstated span is not a
3569 // quantity, and a host that took the last row for "the" occupancy without
3570 // it would be quoting a time it does not know.
3571 p["tspan"] = span;
3572 p["t"] = vector_json(det.t);
3573 // BOTH VIEWS COME OFF ONE INTEGRATION, as on the readable path: the
3574 // detailed labels answer getTranProb(Sys) and the aggregate ones
3575 // getTranProb(Sys)Aggr, and a host asking for both would otherwise
3576 // integrate the same forward equation twice for one answer.
3577 p["labels"] = matrix_json(det.labels);
3578 p["labelsAggr"] = matrix_json(agg.labels);
3579 p["pit"] = matrix_json(det.pit);
3580 p["pitAggr"] = matrix_json(agg.pit);
3581 emit_analysis<T>("tranprob", p, tr.chain.actualmethod, ctmc_meta<T>(tr.chain));
3582 return 0;
3583 }
3584 print_ctmc_banner<T>(tr.chain);
3585 std::printf("TranProb times=%zu tspan=%g:%g scope=%s\n", det.t.size(), k.t0, k.t1,
3586 k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
3587 // The labels come FIRST and the occupancy after, because pi(t) is a row per
3588 // time over columns that mean nothing until the state they index is named.
3589 std::printf("%8s %s\n", "State", "Detailed | Aggregate");
3590 for (std::size_t s = 0; s < det.labels.rows(); ++s) {
3591 std::printf("%8zu ", s + 1);
3592 for (std::size_t c = 0; c < det.labels.cols(); ++c)
3593 std::printf(" %g", line::num_traits<T>::to_double(det.labels(s, c)));
3594 std::printf(" |");
3595 for (std::size_t c = 0; c < agg.labels.cols(); ++c)
3596 std::printf(" %g", line::num_traits<T>::to_double(agg.labels(s, c)));
3597 std::printf("\n");
3598 }
3599 std::printf("%14s", "Time");
3600 for (std::size_t s = 0; s < det.pit.cols(); ++s) std::printf(" %12zu", s + 1);
3601 std::printf("\n");
3602 for (std::size_t i = 0; i < det.t.size(); ++i) {
3603 std::printf("%14.8g", line::num_traits<T>::to_double(det.t[i]));
3604 for (std::size_t s = 0; s < det.pit.cols(); ++s)
3605 std::printf(" %12.6g", line::num_traits<T>::to_double(det.pit(i, s)));
3606 std::printf("\n");
3607 }
3608 return 0;
3609}
3610
3611/**
3612 * `-s ctmc -a tran`: `getTranAvg`, the transient MEANS Q(t), U(t) and X(t) over
3613 * the --tspan horizon.
3614 *
3615 * NOT `-a tranprob`, AND THE TWO ARE NOT REDUCIBLE TO ONE ANOTHER FOR A CALLER.
3616 * `tranprob` sends pi(t) with the labels that index it, from which a host COULD
3617 * form these means -- and that is exactly the computation that must not happen
3618 * in a host: the utilization is not a linear functional of the labels (the PS
3619 * and DPS shares divide by the state's own total, and every other discipline
3620 * takes min(n_k, c)/c with the reference's warning attached), so a host folding
3621 * pi(t) itself would be reimplementing `solver_ctmc_transient_analyzer`'s
3622 * discipline switch and would diverge from it silently. The analyzer already
3623 * computes all three trajectories on the way to pi(t); this arm reports them.
3624 *
3625 * The payload is the SAME `TranAvgTable` the fluid and MAM transients emit, key
3626 * for key, so one host reader serves every solver that answers `-a tran`.
3627 */
3628template <class T>
3629int solve_ctmc_tran(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
3630 const Knobs& k) {
3631 if (k.t1 < 0.0)
3632 throw line::InputError(
3633 "-a tran integrates the forward equation and needs a horizon: pass --tspan <t0>:<t1>");
3636 const std::size_t M = sn.nstations, K = sn.nclasses, nt = tr.t.size();
3637
3638 if (g_json_output) {
3639 line::reg::Json p = line::reg::Json::object();
3640 p["type"] = "TranAvgTable";
3641 p["indexBase"] = 0;
3642 p["t0"] = k.t0;
3643 p["t1"] = k.t1;
3644 line::reg::Json ts = line::reg::Json::array();
3645 for (std::size_t j = 0; j < nt; ++j) ts.push_back(line::num_traits<T>::to_double(tr.t[j]));
3646 line::reg::Json arr = line::reg::Json::array();
3647 for (std::size_t i = 0; i < M; ++i)
3648 for (std::size_t c = 0; c < K; ++c) {
3649 // A DISABLED PAIR IS OMITTED, not sent as zeros: the reference
3650 // leaves its cell empty and the host turns an absent curve into
3651 // the disabled handle's NaN. Zeros would read as a station that
3652 // is genuinely idle for that class.
3653 if (sn.disabled[i][c]) continue;
3654 line::reg::Json e = line::reg::Json::object();
3655 e["Station"] = sn.stations[i].name;
3656 e["JobClass"] = sn.classes[c].name;
3657 e["station"] = i;
3658 e["jobclass"] = c;
3659 e["t"] = ts;
3660 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array(),
3661 x = line::reg::Json::array();
3662 for (std::size_t j = 0; j < nt; ++j) {
3663 q.push_back(line::num_traits<T>::to_double(tr.QNt[i][c][j]));
3664 u.push_back(line::num_traits<T>::to_double(tr.UNt[i][c][j]));
3665 x.push_back(line::num_traits<T>::to_double(tr.TNt[i][c][j]));
3666 }
3667 e["QLen"] = q;
3668 e["Util"] = u;
3669 e["Tput"] = x;
3670 arr.push_back(e);
3671 }
3672 p["curves"] = arr;
3673 emit_analysis<T>("tran", p, tr.chain.actualmethod, ctmc_meta<T>(tr.chain));
3674 return 0;
3675 }
3676 print_ctmc_banner<T>(tr.chain);
3677 std::printf("TranAvg times=%zu tspan=%g:%g\n", nt, k.t0, k.t1);
3678 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
3679 "Tput");
3680 for (std::size_t i = 0; i < M; ++i)
3681 for (std::size_t c = 0; c < K; ++c) {
3682 if (sn.disabled[i][c]) continue;
3683 for (std::size_t j = 0; j < nt; ++j)
3684 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
3685 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(),
3687 line::num_traits<T>::to_double(tr.QNt[i][c][j]),
3688 line::num_traits<T>::to_double(tr.UNt[i][c][j]),
3689 line::num_traits<T>::to_double(tr.TNt[i][c][j]));
3690 }
3691 return 0;
3692}
3693
3694/**
3695 * `-a sample`: `sampleSys` and `sampleSysAggr`, one marked trajectory; with
3696 * `--node`, also that node's own block (`sample`) and per-class counts
3697 * (`sampleAggr`), which is the view MATLAB's per-node sampler returns.
3698 */
3699template <class T>
3700int solve_ctmc_sample(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
3701 const Knobs& k) {
3702 // `--events` NAMES THIS NUMBER, `--samples` only stands in for it. The
3703 // JAR keeps the two apart because a sampled trajectory is walked for a
3704 // number of EVENTS while `--samples` is a solver-wide run length, so a
3705 // caller who set the run length and then asked for a trajectory would
3706 // silently get one of that length. Both are honoured, --events first,
3707 // and the reference default of 1000 stands when neither is given.
3708 const std::size_t nevents = k.events ? k.events : (k.samples ? k.samples : 1000);
3709 const unsigned long seed = k.seed ? k.seed : 23000;
3711 line::ctmc::solver_ctmc_sample_sys<T>(sn, opt, nevents, seed);
3713 line::Matrix<T> L, LA;
3714 if (k.node) {
3715 L = line::ctmc::solver_ctmc_sample(sn, path, k.node);
3716 LA = line::ctmc::solver_ctmc_sample_aggr(sn, path, k.node);
3717 }
3718 if (g_json_output) {
3719 line::reg::Json p = line::reg::Json::object();
3720 p["type"] = "SamplePath";
3721 p["indexBase"] = 0;
3722 // The seed and the requested length are part of the ANSWER, as on the SSA
3723 // path: two runs are the same trace only if both are stated.
3724 p["events"] = nevents;
3725 p["seed"] = seed;
3726 p["drawn"] = path.state.size();
3727 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
3728 if (k.node) p["node"] = k.node - 1;
3729 p["t"] = vector_json(path.t);
3730 p["state"] = index_json(path.state);
3731 line::reg::Json ev = line::reg::Json::array();
3732 for (std::size_t i = 0; i < path.state.size(); ++i) {
3733 // null where the readable table prints "absorb": the walk left no
3734 // state, so there is no synchronization index, and any integer here
3735 // would name an event that did not fire.
3736 if (i < path.event.size() && path.event[i] != static_cast<std::size_t>(-1))
3737 ev.push_back(path.event[i]);
3738 else
3739 ev.push_back(line::reg::Json());
3740 }
3741 p["event"] = ev;
3742 p["sysAggr"] = matrix_json(A);
3743 // THE STATE SPACE TRAVELS WITH THE TRAJECTORY, so a host can turn the
3744 // visited indices into the states themselves without enumerating the
3745 // chain a second time in another process -- which would also be a second
3746 // chance for the two enumerations to disagree while looking paired.
3747 {
3750 p["space"] = matrix_json(s.flat);
3751 p["NodeWidths"] = index_json(s.node_width);
3752 }
3753 if (k.node) {
3754 p["nodeState"] = matrix_json(L);
3755 p["nodeAggr"] = matrix_json(LA);
3756 }
3757 emit_analysis<T>("sample", p, path.chain.actualmethod, ctmc_meta<T>(path.chain));
3758 return 0;
3759 }
3760 // The seed and the requested length are part of the ANSWER, as on the SSA
3761 // path: two runs are the same trace only if both are stated.
3762 std::printf("SolverCTMC arith=%s states=%zu events=%zu seed=%lu drawn=%zu scope=%s\n",
3763 line::num_traits<T>::name(), path.chain.chain.space.size(), nevents, seed,
3764 path.state.size(), k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
3765 std::printf("%14s %8s %8s %s\n", "Time", "State", "Event",
3766 k.node ? "SysAggregate | NodeState | NodeAggregate" : "SysAggregate");
3767 for (std::size_t i = 0; i < path.state.size(); ++i) {
3768 const std::size_t ev = i < path.event.size() ? path.event[i] : static_cast<std::size_t>(-1);
3769 std::printf("%14.8g %8zu ", line::num_traits<T>::to_double(path.t[i]), path.state[i] + 1);
3770 if (ev == static_cast<std::size_t>(-1))
3771 std::printf("%8s ", "absorb");
3772 else
3773 std::printf("%8zu ", ev + 1);
3774 for (std::size_t c = 0; c < A.cols(); ++c)
3775 std::printf(" %g", line::num_traits<T>::to_double(A(i, c)));
3776 if (k.node) {
3777 std::printf(" |");
3778 for (std::size_t c = 0; c < L.cols(); ++c)
3779 std::printf(" %g", line::num_traits<T>::to_double(L(i, c)));
3780 std::printf(" |");
3781 for (std::size_t c = 0; c < LA.cols(); ++c)
3782 std::printf(" %g", line::num_traits<T>::to_double(LA(i, c)));
3783 }
3784 std::printf("\n");
3785 }
3786 return 0;
3787}
3788
3789/** `-a cdf`: `getCdfRespT` per (station, class), and `getCdfSysRespT` per chain. */
3790template <class T>
3791int solve_ctmc_cdf(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3792 const std::vector<std::vector<line::ctmc::CdfCurve<T> > > RD =
3794 const std::vector<line::ctmc::CdfCurve<T> > RS = line::ctmc::solver_ctmc_cdf_sys_respt(sn, opt);
3795 if (g_json_output) {
3796 line::reg::Json p = line::reg::Json::object();
3797 p["type"] = "CdfRespT";
3798 p["chains"] = sn.nchains;
3799 // ONE OBJECT PER CURVE rather than four parallel columns: the grids are
3800 // per-pair and of different lengths, so a column-oriented form would have
3801 // to repeat the station and the class on every sample and leave the host
3802 // to re-group them.
3803 line::reg::Json rd = line::reg::Json::array();
3804 for (std::size_t i = 0; i < RD.size(); ++i)
3805 for (std::size_t c = 0; c < RD[i].size(); ++c) {
3806 // An empty curve is not a degenerate one: the chain never visits
3807 // that pair, so there is no arrival event to condition on, and it
3808 // is OMITTED rather than sent as a flat zero law.
3809 if (RD[i][c].empty()) continue;
3810 line::reg::Json e = line::reg::Json::object();
3811 e["Station"] = sn.stations[i].name;
3812 e["JobClass"] = sn.classes[c].name;
3813 e["station"] = i;
3814 e["jobclass"] = c;
3815 e["t"] = vector_json(RD[i][c].t);
3816 e["F"] = vector_json(RD[i][c].F);
3817 rd.push_back(e);
3818 }
3819 p["respt"] = rd;
3820 line::reg::Json rs = line::reg::Json::array();
3821 for (std::size_t c = 0; c < RS.size(); ++c) {
3822 if (RS[c].empty()) continue;
3823 line::reg::Json e = line::reg::Json::object();
3824 e["chain"] = c;
3825 e["t"] = vector_json(RS[c].t);
3826 e["F"] = vector_json(RS[c].F);
3827 rs.push_back(e);
3828 }
3829 p["sysrespt"] = rs;
3830 p["indexBase"] = 0;
3831 // NO "method", as on the reward arm: the response-time laws are computed
3832 // from tagged chains this function does not return, so there is no
3833 // resolved method to report and the requested one is not it.
3834 emit_analysis<T>("cdf", p, std::string());
3835 return 0;
3836 }
3837 std::printf("SolverCTMC arith=%s chains=%zu\n", line::num_traits<T>::name(), sn.nchains);
3838 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
3839 for (std::size_t i = 0; i < RD.size(); ++i)
3840 for (std::size_t c = 0; c < RD[i].size(); ++c) {
3841 // An empty curve is not a degenerate one: the chain never visits
3842 // that pair, so there is no arrival event to condition on.
3843 if (RD[i][c].empty()) continue;
3844 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
3845 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
3846 sn.classes[c].name.c_str(),
3847 line::num_traits<T>::to_double(RD[i][c].t[j]),
3848 line::num_traits<T>::to_double(RD[i][c].F[j]));
3849 }
3850 std::printf("%-16s %-14s %14s %14s\n", "System", "Chain", "Time", "F(t)");
3851 for (std::size_t c = 0; c < RS.size(); ++c) {
3852 if (RS[c].empty()) continue;
3853 for (std::size_t j = 0; j < RS[c].t.size(); ++j)
3854 std::printf("%-16s %-14zu %14.8g %14.10g\n", "(system)", c + 1,
3856 line::num_traits<T>::to_double(RS[c].F[j]));
3857 }
3858 return 0;
3859}
3860
3861/**
3862 * A `--passage-from`/`--passage-into` spec as a Matrix<double>: either a flat
3863 * comma list ("3,5", one row the resolver reads as 1-based indices when every
3864 * entry is one) or semicolon-separated state rows ("0,2;1,1"), which resolve
3865 * against the enumerated space by content.
3866 */
3867inline line::Matrix<double> parse_passage_set(const std::string& spec, const char* flag) {
3868 if (spec.empty()) return line::Matrix<double>(0, 0);
3869 std::vector<std::vector<double>> rows;
3870 std::size_t pos = 0;
3871 while (pos <= spec.size()) {
3872 std::size_t semi = spec.find(';', pos);
3873 if (semi == std::string::npos) semi = spec.size();
3874 std::string rowtxt = spec.substr(pos, semi - pos);
3875 std::vector<double> row;
3876 std::size_t p2 = 0;
3877 while (p2 <= rowtxt.size()) {
3878 std::size_t comma = rowtxt.find(',', p2);
3879 if (comma == std::string::npos) comma = rowtxt.size();
3880 std::string cell = rowtxt.substr(p2, comma - p2);
3881 if (!cell.empty()) {
3882 char* endp = 0;
3883 const double v = std::strtod(cell.c_str(), &endp);
3884 if (endp == cell.c_str() || *endp != '\0')
3885 throw line::InputError(std::string(flag) + ": '" + cell +
3886 "' is not a number");
3887 row.push_back(v);
3888 }
3889 p2 = comma + 1;
3890 }
3891 if (!row.empty()) rows.push_back(row);
3892 pos = semi + 1;
3893 }
3894 if (rows.empty()) return line::Matrix<double>(0, 0);
3895 for (std::size_t i = 1; i < rows.size(); ++i)
3896 if (rows[i].size() != rows[0].size())
3897 throw line::InputError(std::string(flag) +
3898 ": every state row must have the same width");
3899 line::Matrix<double> out(rows.size(), rows[0].size());
3900 for (std::size_t i = 0; i < rows.size(); ++i)
3901 for (std::size_t j = 0; j < rows[i].size(); ++j) out(i, j) = rows[i][j];
3902 return out;
3903}
3904
3905/**
3906 * A `--passage-tset` list as a grid: comma-separated numbers, each as strtod
3907 * reads it, so 'inf' names the ultimate probability. A NaN or an empty list is
3908 * refused: neither is a time.
3909 */
3910std::vector<double> parse_passage_tset(const std::string& spec) {
3911 std::vector<double> t;
3912 std::size_t pos = 0;
3913 while (pos <= spec.size()) {
3914 std::size_t comma = spec.find(',', pos);
3915 if (comma == std::string::npos) comma = spec.size();
3916 std::string cell = spec.substr(pos, comma - pos);
3917 const std::size_t b = cell.find_first_not_of(" \t");
3918 const std::size_t e = cell.find_last_not_of(" \t");
3919 cell = (b == std::string::npos) ? std::string() : cell.substr(b, e - b + 1);
3920 if (cell.empty())
3921 throw line::InputError("--passage-tset: an empty entry in '" + spec +
3922 "'; give the times as 't1,t2,...', 'inf' allowed");
3923 char* endp = nullptr;
3924 const double v = std::strtod(cell.c_str(), &endp);
3925 if (endp == cell.c_str() || *endp != '\0' || std::isnan(v))
3926 throw line::InputError("--passage-tset: '" + cell + "' is not a time");
3927 t.push_back(v);
3928 pos = comma + 1;
3929 }
3930 return t;
3931}
3932
3933/**
3934 * `-s ctmc -a firstpasst`: `@@SolverCTMC/getCdfFirstPassT(A, B, C, tset)`, the
3935 * first passage time between two state sets the caller names, optionally under
3936 * a taboo set C (`--passage-taboo`) and on a grid of its own (`--passage-tset`). `--passage-into` is
3937 * required; an empty `--passage-from` starts from the conditional stationary
3938 * law on the complement of the target, as the reference does.
3939 */
3940template <class T>
3941int solve_ctmc_firstpasst(const line::qn::NetworkStruct<T>& sn,
3942 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3943 if (k.passage_into.empty())
3944 throw line::InputError(
3945 "-a firstpasst times the passage INTO a state set and needs --passage-into; name it "
3946 "as 1-based rows of the state space ('3,5') or as state rows ('0,2;1,1')");
3947 const line::Matrix<double> A = parse_passage_set(k.passage_from, "--passage-from");
3948 const line::Matrix<double> B = parse_passage_set(k.passage_into, "--passage-into");
3949 const line::Matrix<double> C = parse_passage_set(k.passage_taboo, "--passage-taboo");
3950 const std::vector<double> tset =
3951 k.passage_tset.empty() ? std::vector<double>() : parse_passage_tset(k.passage_tset);
3952 const std::string method = k.passage_method.empty() ? "expm" : k.passage_method;
3953
3955 line::ctmc::ctmc_cdf_firstpasst<T>(sn, opt, A, B, C, tset, method);
3956
3957 if (g_json_output) {
3958 line::reg::Json p = line::reg::Json::object();
3959 p["type"] = "CdfFirstPassT";
3960 p["indexBase"] = 0;
3961 p["t"] = line::reg::Json(fp.t);
3962 p["F"] = line::reg::Json(fp.F);
3963 p["f"] = line::reg::Json(fp.f);
3964 line::reg::Json src = line::reg::Json::array(), tgt = line::reg::Json::array();
3965 for (std::size_t i : fp.source) src.push_back(static_cast<double>(i));
3966 for (std::size_t i : fp.target) tgt.push_back(static_cast<double>(i));
3967 p["source"] = src;
3968 p["target"] = tgt;
3969 line::reg::Json tab = line::reg::Json::array();
3970 for (std::size_t i : fp.taboo) tab.push_back(static_cast<double>(i));
3971 p["taboo"] = tab;
3972 p["reachprob"] = fp.reachprob;
3973 p["tabooprob"] = fp.tabooprob;
3974 emit_analysis<T>("firstpasst", p, method);
3975 return 0;
3976 }
3977 std::printf("SolverCTMC arith=%s method=%s getCdfFirstPassT\n",
3978 line::num_traits<T>::name(), method.c_str());
3979 std::printf("source states: %zu%s, target states: %zu, taboo states: %zu\n",
3980 fp.source.size(), fp.source.empty() ? " (conditional stationary law)" : "",
3981 fp.target.size(), fp.taboo.size());
3982 std::printf("P(target first) = %.10g, P(taboo first) = %.10g\n", fp.reachprob,
3983 fp.tabooprob);
3984 std::printf("%14s %14s %14s\n", "Time", "F(t)", "f(t)");
3985 for (std::size_t j = 0; j < fp.t.size(); j += 111)
3986 std::printf("%14.8g %14.10g %14.10g\n", fp.t[j], fp.F[j], fp.f[j]);
3987 std::printf("%14.8g %14.10g %14.10g\n", fp.t.back(), fp.F.back(), fp.f.back());
3988 return 0;
3989}
3990
3991/**
3992 * `-s ctmc -a firstpasstmom`: `@@SolverCTMC/getFirstPassTMoments(A, B, nmax)`,
3993 * the moments of the same passage `firstpasst` gives the curve of.
3994 *
3995 * These are EXACT and cost one linear solve per order, so a caller who wants a
3996 * variance or a skewness should ask for them here rather than integrate the
3997 * truncated curve the other arm returns. `--passage-orders` is the nmax; it
3998 * defaults to the reference's 3.
3999 */
4000template <class T>
4001int solve_ctmc_firstpasst_moments(const line::qn::NetworkStruct<T>& sn,
4002 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
4003 if (k.passage_into.empty())
4004 throw line::InputError(
4005 "-a firstpasstmom times the passage INTO a state set and needs --passage-into; name "
4006 "it as 1-based rows of the state space ('3,5') or as state rows ('0,2;1,1')");
4007 const line::Matrix<double> A = parse_passage_set(k.passage_from, "--passage-from");
4008 const line::Matrix<double> B = parse_passage_set(k.passage_into, "--passage-into");
4009 const line::Matrix<double> C = parse_passage_set(k.passage_taboo, "--passage-taboo");
4010 const std::size_t nmax = (k.passage_orders > 0) ? k.passage_orders : 3;
4011
4013 line::ctmc::ctmc_firstpasst_moments<T>(sn, opt, A, B, nmax, C);
4014
4015 if (g_json_output) {
4016 line::reg::Json p = line::reg::Json::object();
4017 p["type"] = "FirstPassTMoments";
4018 p["indexBase"] = 0;
4019 line::reg::Json m = line::reg::Json::array();
4020 for (std::size_t i = 0; i < fm.m.size(); ++i)
4021 m.push_back(line::num_traits<T>::to_double(fm.m[i]));
4022 p["m"] = m;
4023 line::reg::Json mall = line::reg::Json::array();
4024 for (std::size_t i = 0; i < fm.mall.rows(); ++i) {
4025 line::reg::Json row = line::reg::Json::array();
4026 for (std::size_t j = 0; j < fm.mall.cols(); ++j)
4027 row.push_back(line::num_traits<T>::to_double(fm.mall(i, j)));
4028 mall.push_back(row);
4029 }
4030 p["mall"] = mall;
4031 line::reg::Json src = line::reg::Json::array(), tgt = line::reg::Json::array();
4032 for (std::size_t i : fm.source) src.push_back(static_cast<double>(i));
4033 for (std::size_t i : fm.target) tgt.push_back(static_cast<double>(i));
4034 p["source"] = src;
4035 p["target"] = tgt;
4036 line::reg::Json tab = line::reg::Json::array(), pr = line::reg::Json::array();
4037 for (std::size_t i : fm.taboo) tab.push_back(static_cast<double>(i));
4038 for (std::size_t i = 0; i < fm.p.size(); ++i)
4039 pr.push_back(line::num_traits<T>::to_double(fm.p[i]));
4040 p["taboo"] = tab;
4041 p["p"] = pr;
4042 emit_analysis<T>("firstpasstmom", p, "moments");
4043 return 0;
4044 }
4045 std::printf("SolverCTMC arith=%s getFirstPassTMoments\n", line::num_traits<T>::name());
4046 std::printf("source states: %zu%s, target states: %zu, taboo states: %zu\n",
4047 fm.source.size(), fm.source.empty() ? " (conditional stationary law)" : "",
4048 fm.target.size(), fm.taboo.size());
4049 if (!fm.taboo.empty())
4050 std::printf("moments are CONDITIONAL on entering the target before the taboo set\n");
4051 std::printf("%8s %20s\n", "Order", "Moment");
4052 for (std::size_t i = 0; i < fm.m.size(); ++i)
4053 std::printf("%8zu %20.10g\n", i + 1, line::num_traits<T>::to_double(fm.m[i]));
4054 return 0;
4055}
4056
4057/**
4058 * `--rate-sched`: the JSON schedule, inline or from a file, resolved against the
4059 * model so that a station or class may be named rather than numbered.
4060 */
4061template <class T>
4062std::vector<line::ctmc::CtmcRateSched> parse_rate_sched(const std::string& arg,
4063 const line::qn::NetworkStruct<T>& sn) {
4064 std::string text = arg;
4065 const std::size_t first = arg.find_first_not_of(" \t\r\n");
4066 if (first == std::string::npos || (arg[first] != '[' && arg[first] != '{')) {
4067 std::ifstream in(arg.c_str());
4068 if (!in)
4069 throw line::InputError("--rate-sched: '" + arg +
4070 "' is neither inline JSON nor a readable file");
4071 std::ostringstream ss;
4072 ss << in.rdbuf();
4073 text = ss.str();
4074 }
4075 line::io::detail::json root;
4076 try {
4077 root = line::io::detail::json::parse(text);
4078 } catch (const std::exception& e) {
4079 throw line::InputError(std::string("--rate-sched: not valid JSON: ") + e.what());
4080 }
4081 if (root.is_object()) root = line::io::detail::json::array({root});
4082 if (!root.is_array() || root.empty())
4083 throw line::InputError("--rate-sched: expected a non-empty array of schedule objects");
4084
4085 const auto resolve = [](const line::io::detail::json& v, const std::vector<std::string>& names,
4086 const char* what) -> std::size_t {
4087 if (v.is_number_integer()) {
4088 const long long i = v.get<long long>();
4089 if (i < 1 || static_cast<std::size_t>(i) > names.size())
4090 throw line::InputError(std::string("--rate-sched: ") + what + " index " +
4091 std::to_string(i) + " is out of range (1-based)");
4092 return static_cast<std::size_t>(i);
4093 }
4094 if (v.is_string()) {
4095 const std::string nm = v.get<std::string>();
4096 for (std::size_t i = 0; i < names.size(); ++i)
4097 if (names[i] == nm) return i + 1;
4098 throw line::InputError(std::string("--rate-sched: no ") + what + " named '" + nm + "'");
4099 }
4100 throw line::InputError(std::string("--rate-sched: ") + what +
4101 " must be a name or a 1-based index");
4102 };
4103 std::vector<std::string> stations, classes;
4104 for (std::size_t i = 0; i < sn.stations.size(); ++i)
4105 stations.push_back(sn.nodes[sn.station_to_node[i] - 1].name);
4106 for (const auto& c : sn.classes) classes.push_back(c.name);
4107
4108 std::vector<line::ctmc::CtmcRateSched> out;
4109 for (const auto& e : root) {
4110 if (!e.is_object() || !e.contains("station") || !e.contains("class") ||
4111 !e.contains("tgrid") || !e.contains("rates"))
4112 throw line::InputError(
4113 "--rate-sched: every entry needs station, class, tgrid and rates");
4115 rs.station = resolve(e.at("station"), stations, "station");
4116 rs.cls = resolve(e.at("class"), classes, "class");
4117 rs.tgrid = e.at("tgrid").get<std::vector<double>>();
4118 rs.rates = e.at("rates").get<std::vector<double>>();
4119 if (e.contains("nominal") && !e.at("nominal").is_null())
4120 rs.nominal = e.at("nominal").get<double>();
4121 out.push_back(rs);
4122 }
4123 return out;
4124}
4125
4126/**
4127 * Solve a Network model.json with SolverCTMC and print what `-a` asked for.
4128 *
4129 * EXACT, and the only ported solver that is exact on a non-product-form model:
4130 * it enumerates the state space and solves pi Q = 0, so the numbers are the
4131 * chain's own and not an approximation of them. The price is the state space,
4132 * which is why an OPEN model needs `--cutoff`: without a bound on the open
4133 * population the chain is infinite. The banner reports the cutoff that was used,
4134 * because a truncated chain's answer is not the model's answer without it.
4135 *
4136 * Every arithmetic backend runs the stationary analyses: the generator assembly
4137 * and the stationary solve are field operations throughout, so `--arith exact`
4138 * returns the exact rational stationary law of a chain with rational rates. The
4139 * transient ones refuse by name under exact, since a forward integration, an
4140 * exponential clock and a matrix exponential are all transcendental.
4141 */
4142template <class T>
4143int solve_model_ctmc(const std::string& file, const Knobs& k, const std::string& analysis) {
4144 line::qn::Network<T> net = read_model<T>(file);
4146 if (!k.method.empty()) opt.method = k.method;
4147 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
4148 opt.cutoff_mat = k.cutoff_mat;
4149 opt.force = k.force;
4150 if (!k.nonmkv.empty()) opt.nonmkv = k.nonmkv;
4151 if (k.timestep > 0.0) opt.timestep = k.timestep; // `--timestep`, the fixed output grid
4152 // `--transient-method` and its two tolerances, `options.config` of the
4153 // reference's transient analyzer. The analyzer validates the name.
4154 if (!k.transient_method.empty()) opt.transient_method = k.transient_method;
4155 if (k.fau_epsilon > 0.0) opt.fau_epsilon = k.fau_epsilon;
4156 if (k.fau_delta >= 0.0) opt.fau_delta = k.fau_delta;
4157 if (k.ctmc_tv_ngrid > 0) opt.ctmc_tv_ngrid = k.ctmc_tv_ngrid;
4158 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4159 if (!k.rate_sched.empty()) opt.rate_sched = parse_rate_sched(k.rate_sched, sn);
4160
4161 // The method that never builds the generator is served by its own analyzer,
4162 // ahead of every state-space path below. The dispatcher has already refused
4163 // every analysis but `avg` for it, since it produces no state space, no
4164 // filtration and no trajectory to answer one from.
4165 if (opt.method == "mdd") {
4167 if (k.mdd_tol > 0.0) mcdopt.tol = k.mdd_tol;
4168 if (k.mdd_maxiter > 0) mcdopt.maxiter = k.mdd_maxiter;
4169 return solve_ctmc_mdd_avg<T>(sn, opt, mcdopt);
4170 }
4171
4172 // The field-arithmetic analyses: every step from the generator to the answer
4173 // is a ring operation, so exact returns the exact rational quantity.
4174 if (analysis == "avg") return solve_ctmc_avg<T>(sn, opt);
4175 if (analysis == "prob") return solve_ctmc_prob<T>(sn, opt, k);
4176 if (analysis == "gen") return solve_ctmc_gen<T>(sn, opt);
4177 if (analysis == "states") return solve_ctmc_states<T>(sn, opt);
4178 if (analysis == "sens") return solve_ctmc_sens<T>(sn, opt);
4179 if (analysis == "reward") return solve_ctmc_reward<T>(sn, opt);
4180 if (analysis == "rewardvalue") return solve_ctmc_reward_value<T>(sn, opt, k);
4181
4182 // The rest integrate a forward equation, draw an exponential clock or take a
4183 // matrix exponential, none of which exists in a rational field. Their
4184 // static_asserts are behind if-constexpr so the refusal is a message rather
4185 // than a compile error in the exact instantiation.
4187 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
4188 "the -a " + analysis +
4189 " analysis integrates the forward equation, draws exponential clocks or takes a matrix "
4190 "exponential, none of which exists in exact rational arithmetic; rerun with --arith "
4191 "double or --arith real");
4192 } else {
4193 if (analysis == "tran") return solve_ctmc_tran<T>(sn, opt, k);
4194 if (analysis == "tranprob") return solve_ctmc_tranprob<T>(sn, opt, k);
4195 if (analysis == "tranreward") return solve_ctmc_tran_reward<T>(sn, opt, k);
4196 if (analysis == "sample") return solve_ctmc_sample<T>(sn, opt, k);
4197 if (analysis == "firstpasst") return solve_ctmc_firstpasst<T>(sn, opt, k);
4198 if (analysis == "firstpasstmom") return solve_ctmc_firstpasst_moments<T>(sn, opt, k);
4199 return solve_ctmc_cdf<T>(sn, opt); // the dispatcher admitted no other name
4200 }
4201}
4202
4203/**
4204 * `-s ssa -a prob`: the four SolverSSA probability queries over the model's
4205 * DEFAULT INITIAL STATE, the same state `-s ctmc -a prob` reports on.
4206 *
4207 * THE PAIR IS THE POINT. The CTMC answer is the stationary law of the chain and
4208 * this one is a time average of a finite sample path, so running both on a model
4209 * small enough for the chain measures the simulation error directly instead of
4210 * inferring it. The banner therefore carries the run length and the seed, as
4211 * every simulated number on this CLI does.
4212 *
4213 * `seen` TRAVELS WITH EACH PROBABILITY because a zero here has two meanings: the
4214 * path visited the state and left immediately, or it never got there at all. The
4215 * reference warns on the second; a machine-readable answer has to carry the
4216 * distinction rather than print it.
4217 */
4218template <class T>
4219int solve_model_ssa_prob(const std::string& file, const Knobs& k) {
4220 line::qn::Network<T> net = read_model<T>(file);
4221 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4223 // `serial`, whatever `-m` said, and for the reference's own reason: the NRM
4224 // simulates per-(node, class, phase) counts rather than the state ENCODING,
4225 // so it has no row to compare a requested state against.
4226 // `@@SolverSSA/getProb.m` rewrites the method the same way.
4227 opt.method = "serial";
4228 if (k.samples) opt.samples = k.samples;
4229 if (k.seed) opt.seed = k.seed;
4230 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
4231 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
4233
4234 if (g_json_output) {
4235 line::reg::Json p = line::reg::Json::object();
4236 p["type"] = "ProbAggr";
4237 p["indexBase"] = 0;
4238 p["samples"] = r.samples;
4239 p["seed"] = r.seed;
4240 p["ProbSys"] = r.sys.prob;
4241 p["ProbSysAggr"] = r.sys_aggr.prob;
4242 p["ProbSysSeen"] = r.sys.seen;
4243 p["ProbSysAggrSeen"] = r.sys_aggr.seen;
4244 line::reg::Json st = line::reg::Json::array(), pm = line::reg::Json::array(),
4245 pa = line::reg::Json::array(), sm = line::reg::Json::array();
4246 for (std::size_t i = 0; i < sn.nstations; ++i) {
4247 st.push_back(sn.stations[i].name);
4248 pm.push_back(r.marg[i].prob);
4249 pa.push_back(r.aggr[i].prob);
4250 sm.push_back(r.marg[i].seen);
4251 }
4252 p["Station"] = st;
4253 p["Prob"] = pm;
4254 p["ProbAggr"] = pa;
4255 p["Seen"] = sm;
4256 emit_analysis<T>("prob", p, "serial");
4257 return 0;
4258 }
4259 std::printf("SolverSSA arith=%s method=serial samples=%zu seed=%lu time=%.6g\n",
4261 std::printf("ProbSys = %.8g%s\n", r.sys.prob, r.sys.seen ? "" : " (state never visited)");
4262 std::printf("ProbSysAggr = %.8g%s\n", r.sys_aggr.prob,
4263 r.sys_aggr.seen ? "" : " (state never visited)");
4264 std::printf("%-20s %14s %14s\n", "Station", "Prob", "ProbAggr");
4265 for (std::size_t i = 0; i < sn.nstations; ++i)
4266 std::printf("%-20s %14.8g %14.8g\n", sn.stations[i].name.c_str(), r.marg[i].prob,
4267 r.aggr[i].prob);
4268 return 0;
4269}
4270
4271/**
4272 * `-s ssa -a sample`: `sampleSys` and `sampleSysAggr`, one simulated trajectory;
4273 * with `--node`, also that node's own block (`sample`) and per-class counts
4274 * (`sampleAggr`).
4275 *
4276 * The SAME shape `-s ctmc -a sample` emits, deliberately: the CTMC sampler walks
4277 * the jump chain of an enumerated generator and this one walks the network's own
4278 * encoding, and a host that can read one trajectory should be able to read the
4279 * other. The event column indexes the synchronization list, so the two are
4280 * comparable only within a solver -- which is why it is printed and not
4281 * interpreted here.
4282 */
4283template <class T>
4284int solve_model_ssa_sample(const std::string& file, const Knobs& k) {
4285 line::qn::Network<T> net = read_model<T>(file);
4286 const line::qn::NetworkStruct<T>& sn_model = net.get_struct();
4288 opt.method = "serial";
4289 opt.samples = k.events ? k.events : (k.samples ? k.samples : 1000); // see -a sample above
4290 if (k.seed) opt.seed = k.seed;
4291 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
4292 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
4294 // the path's rows are in the layout of the struct it ran on (pair form, `ctmc_mapc`)
4295 const line::qn::NetworkStruct<T>& sn = sim.solved_sn ? *sim.solved_sn : sn_model;
4298 if (k.node) nodep = line::ssa::ssa_sample_node(sn, sim.run, k.node);
4299
4300 if (g_json_output) {
4301 line::reg::Json p = line::reg::Json::object();
4302 p["type"] = "SamplePath";
4303 p["indexBase"] = 0;
4304 p["events"] = opt.samples;
4305 p["seed"] = sys.seed;
4306 p["drawn"] = sys.t.size();
4307 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
4308 if (k.node) p["node"] = k.node - 1;
4309 p["t"] = vector_json(sys.t);
4310 p["event"] = index_json(sys.event);
4311 p["state"] = matrix_json(sys.state);
4312 // THE BLOCK BOUNDARIES TRAVEL, as under `-s ctmc -a sample`. A row of
4313 // `state` is the stateful nodes' local encodings laid end to end, and
4314 // `sampleSys` reports them one node at a time; a host that had to guess
4315 // the widths would cut the row at the wrong columns on any model with a
4316 // phase-type service, and read a phase index as a job count.
4317 {
4318 line::reg::Json w = line::reg::Json::array();
4319 for (std::size_t f = 0; f < sn.stateful_nodes.size(); ++f)
4320 w.push_back(sim.run.space.empty() ? 0 : sim.run.space[0].local[f].size());
4321 p["NodeWidths"] = w;
4322 }
4323 p["sysAggr"] = matrix_json(sys.aggr);
4324 if (k.node) {
4325 p["nodeState"] = matrix_json(nodep.state);
4326 p["nodeAggr"] = matrix_json(nodep.aggr);
4327 }
4328 emit_analysis<T>("sample", p, "serial");
4329 return 0;
4330 }
4331 std::printf("SolverSSA arith=%s method=serial events=%zu seed=%lu drawn=%zu scope=%s\n",
4332 line::num_traits<T>::name(), opt.samples, sys.seed, sys.t.size(),
4333 k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
4334 std::printf("%14s %8s %s\n", "Time", "Event",
4335 k.node ? "SysAggregate | NodeState | NodeAggregate" : "SysAggregate");
4336 for (std::size_t i = 0; i < sys.t.size(); ++i) {
4337 std::printf("%14.8g %8zu ", sys.t[i], sys.event[i]);
4338 for (std::size_t c = 0; c < sys.aggr.cols(); ++c)
4339 std::printf(" %g", line::num_traits<T>::to_double(sys.aggr(i, c)));
4340 if (k.node) {
4341 std::printf(" |");
4342 for (std::size_t c = 0; c < nodep.state.cols(); ++c)
4343 std::printf(" %g", line::num_traits<T>::to_double(nodep.state(i, c)));
4344 std::printf(" |");
4345 for (std::size_t c = 0; c < nodep.aggr.cols(); ++c)
4346 std::printf(" %g", line::num_traits<T>::to_double(nodep.aggr(i, c)));
4347 }
4348 std::printf("\n");
4349 }
4350 return 0;
4351}
4352
4353/**
4354 * A `Matrix<double>` read as this arithmetic's matrix.
4355 *
4356 * SSA and Fluid return plain-double solutions whatever `T` the CLI was asked
4357 * for, so every shared routine that takes a `Matrix<T>` -- the residence-time
4358 * conversion, the chain aggregation -- needs this one lift. Both are refused
4359 * outside `--arith double` anyway, so it is a type bridge and not a precision
4360 * claim.
4361 */
4362template <class T>
4363line::Matrix<T> to_matrix(const line::Matrix<double>& m) {
4365 for (std::size_t i = 0; i < m.rows(); ++i)
4366 for (std::size_t j = 0; j < m.cols(); ++j)
4367 out(i, j) = line::num_traits<T>::from_double(m(i, j));
4368 return out;
4369}
4370
4371/**
4372 * Solve a Network model.json with SolverSSA and print the same table.
4373 *
4374 * A SIMULATION: its numbers carry Monte Carlo error, so the row is compared
4375 * against the other codebases' SSA rows and not against an exact solver's.
4376 *
4377 * `--samples` and `--seed` set the run length and the stream; without them the
4378 * defaults are 10000 firings and seed 23000, which on a two-station model is
4379 * roughly 3300 job cycles and lands a few percent from the analytical answer.
4380 * Diffing THAT against an exact solver reads as a defect and is not one: a
4381 * measured -3.12% at 1e4 on an M/M/1 falls to +0.06% at 2.56e6. The banner
4382 * therefore carries both numbers, so a row quoting an SSA figure carries the
4383 * conditions that produced it. Double only, refused by name in the dispatcher.
4384 *
4385 * `-m` REACHES THE SOLVER'S OWN DISPATCHER, `ssa::solver_ssa`, and not one
4386 * engine's entry. Calling `solver_ssa_nrm_analyzer` here would run the NRM
4387 * whatever `-m` said, so `-m serial` would silently answer with a different
4388 * estimator than the one asked for -- and the three names the NRM entry cannot
4389 * serve (`serial`, `para`, `parallel`) are all methods the library honours.
4390 */
4391template <class T>
4392int solve_model_ssa(const std::string& file, const Knobs& k) {
4393 line::qn::Network<T> net = read_model<T>(file);
4395 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4396 if (k.samples) opt.samples = k.samples;
4397 if (k.seed) opt.seed = k.seed;
4398 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
4399 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
4400 // The cache write-back is COLLECTED HERE, not only on the `-a node` path:
4401 // the realized hit and miss shares are what the sample path measured, and
4402 // `-a avg` is the arm MATLAB's lang='cpp' bridge calls. Without it
4403 // `CPPLINE.restoreCacheResults` found no block, cleared the Cache node and
4404 // refreshed the visits back to link()'s offered 1/2-1/2.
4405 std::vector<line::ssa::SsaCacheRatio> cacheratio;
4406 const line::ssa::SsaSolution r = line::ssa::solver_ssa(net.get_struct(), opt, &cacheratio);
4407 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4408
4409 // The seed and the sample count are part of the ANSWER, not of the
4410 // invocation: two runs of a simulation are the same measurement only if
4411 // both are stated, so the banner carries them and a parity row that quotes
4412 // an SSA number carries them with it.
4413 //
4414 // `r.method` is the engine that ACTUALLY ran, never `k.method`: `-m
4415 // parallel` on an NRM-eligible model reports `nrm`, because that is what
4416 // produced the numbers below and the banner may not claim otherwise.
4417 std::printf("SolverSSA arith=%s method=%s type=%s samples=%zu seed=%lu time=%.6g\n",
4418 line::num_traits<T>::name(), r.method.c_str(),
4419 line::util::method_type("SSA", r.method).c_str(), r.samples, opt.seed,
4420 r.simulated_time);
4421 // ResidT IS NOT RespT UNLESS EVERY STATION IS VISITED ONCE PER CYCLE.
4422 // `sn_get_residt_from_respt` is the reference's own per-visit -> per-job
4423 // conversion and a pure function of `sn` and RN, so a solver that reports no
4424 // residence time of its own still owes the caller this one: reporting RespT
4425 // in its place was a factor of 3 out on sdroute_closed and 17 on Queue1 of
4426 // init_state_ps, both multi-visit closed models.
4427 //
4428 // TAKEN ON THE MEASURED CACHE SPLIT, not on the offered one: the visits this
4429 // conversion divides by are a function of the routing, and a cache's routing
4430 // is a RESULT. On tut06_cache_lru_zipf the base struct still carried
4431 // `link()`'s even hit/miss share, so both classes came back at exactly half
4432 // their response time (0.1 and 0.5 against 0.16475 and 0.17625) -- a number
4433 // that is not the residence time of any model.
4435 const line::Matrix<T> WN = line::mva::sn_get_residt_from_respt<T>(snw, to_matrix<T>(r.RN));
4436 line::reg::Json extra = line::reg::Json::object();
4438 if (!cache.empty()) extra["Cache"] = cache_extra_json<T>(cache);
4439 // ArvR IS NOT Tput, and reading it off the throughput column was wrong
4440 // wherever the two differ -- most visibly at a JOIN, which takes in one
4441 // sibling per branch and fires once per parent, so its arrival rate is the
4442 // fork degree times its throughput. `ssa_fj_foldback` already divides the
4443 // Join's QLen by the DERIVED rate to get its response time, so reporting
4444 // Tput in the ArvR column left the printed row self-inconsistent
4445 // (0.62372/1.02229 is 0.610, not the 0.3038 beside it). Taken on the
4446 // measured-cache-split struct for the same reason ResidT is.
4447 const line::Matrix<T> AN = line::mva::sn_get_arvr_from_tput<T>(snw, to_matrix<T>(r.TN));
4448 emit_avg_table<T>(sn, r.method, [&](std::size_t i, std::size_t c) {
4449 AvgRow row;
4450 row.q = r.QN(i, c);
4451 row.u = r.UN(i, c);
4452 row.r = r.RN(i, c);
4453 row.w = line::num_traits<T>::to_double(WN(i, c));
4454 row.t = r.TN(i, c);
4455 // A Source has no arrivals TO ITSELF, so its ArvR is 0 while its Tput is
4456 // the arrival rate.
4457 row.a = sn.stations[i].sched == line::lang::SchedStrategy::EXT
4458 ? 0.0
4459 : line::num_traits<T>::to_double(AN(i, c));
4460 return row;
4461 }, extra);
4462 return 0;
4463}
4464
4465/** The knobs the fluid solver reads, in one place so every fluid arm reads the
4466 * same set: an arm that quietly dropped one would answer a different model. */
4467inline line::fluid::FluidOptions fluid_options(const Knobs& k) {
4469 if (!k.method.empty()) opt.method = k.method;
4470 if (k.tol >= 0.0) opt.tol = k.tol;
4471 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4472 if (k.iter_max >= 0) opt.iter_max = static_cast<std::size_t>(k.iter_max);
4473 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
4474 if (!k.nonmkv.empty()) opt.nonmkv = k.nonmkv;
4475 // `pstar_set` is what makes the smoothing active under a method that did
4476 // not ask for it by name, exactly as `options.config.pstar` does in the
4477 // reference: setting the exponent alone would leave `-a avg` integrating
4478 // the hard-min drift while reporting the caller's choice.
4479 if (k.pstar > 0.0) {
4480 opt.pstar = k.pstar;
4481 opt.pstar_set = true;
4482 }
4483 if (!k.fluid_boundary.empty()) opt.fluid_boundary = k.fluid_boundary;
4484 return opt;
4485}
4486
4487/**
4488 * Solve a Network model.json with the fluid solver and print the same table as
4489 * the MVA path, so the parity harness can diff the two rows unchanged.
4490 *
4491 * ResidT and ArvR are reported as the per-visit response time and the
4492 * throughput: the fluid analyzer works at station level and, unlike the MVA
4493 * runner, has no chain-visit conversion behind it. That is what MATLAB's
4494 * fluid `getAvgTable` shows for these columns on a single-visit model.
4495 *
4496 * Only `double` reaches here -- the drift is integrated by LSODA -- so the
4497 * other backends are refused by name in `solve_model_dispatch` rather than
4498 * being narrowed silently.
4499 */
4500template <class T>
4501int solve_model_fluid(const std::string& file, const Knobs& k) {
4502 line::qn::Network<T> net = read_model<T>(file);
4503 const line::fluid::FluidOptions opt = fluid_options(k);
4504 // THE map_env FUNNEL, asked directly rather than through `run_avg`: this
4505 // runner returns a `FluidSolution` and not an `AvgResult`, so the arm cannot
4506 // pass itself as the `Run` callable. The decision and the config are the
4507 // ones `run_avg` would have used, so `-a avg` and the four
4508 // `run_avg_engine` views accept the same models for the same command line.
4509 // No arithmetic guard is needed here: this arm is instantiated at `double`
4510 // alone, the dispatcher having refused the other backends by name.
4511 {
4512 const line::solvers::MapEnvConfig mecfg = map_env_config(k);
4514 net.get_struct(), mecfg)
4515 .needed) {
4517 net.get_struct(), "SolverFluid", mecfg, line::solvers::fluid_stage_fn(opt),
4518 opt.method);
4519 std::printf("SolverFluid arith=%s method=%s type=%s iters=%d\n",
4521 line::util::method_type("FLD", rme.actualmethod).c_str(), rme.iter);
4522 print_avg_table<T>(net.get_struct(), rme);
4523 return 0;
4524 }
4525 }
4526 // `solver_fluid_run_analyzer` is runAnalyzer's resolution over the analyzer, so a
4527 // Cache model reaches the rmf branch, a DPS model the closing drift, and
4528 // anything the moment closure accepts reaches `minnormal`.
4529 // The converged hit/miss split is COLLECTED, for the reason the SSA arm
4530 // above collects its own: `-a avg` is what MATLAB's lang='cpp' bridge calls,
4531 // and a missing block there CLEARS the host's Cache node.
4533 // THE REFRESHED STRUCT IS TAKEN, not dropped: on a cache model the routing
4534 // the analyzer converged to carries the ACTUAL hit/miss split, where
4535 // `net.get_struct()` still carries link()'s offered one. The arrival rates
4536 // below are read off that routing, so the offered split reported 0.5/0.5
4537 // where the model converged to 0.4/0.6 (cache_replc_routing).
4540 net.get_struct(), opt, static_cast<line::qn::NetworkStruct<T>*>(nullptr),
4541 &refreshed, &cache);
4542 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4543 const line::qn::NetworkStruct<T>& snflow =
4544 refreshed.nstations == sn.nstations && refreshed.nclasses == sn.nclasses ? refreshed : sn;
4545
4546 std::printf("SolverFluid arith=%s method=%s type=%s iters=%zu\n", line::num_traits<T>::name(),
4547 r.method.c_str(),
4548 line::util::method_type("FLD", r.method).c_str(), r.iters);
4549 // On stderr, for the reason the AvgResult printer gives (print_avg_table).
4550 if (!r.warning.empty()) std::fprintf(stderr, "Warning: %s\n", r.warning.c_str());
4551 // The residence-time conversion the SSA arm above applies, for the same
4552 // reason: neither analyzer produces a per-job residence time, and
4553 // `sn_get_residt_from_respt` derives one from RN and the visit ratios.
4554 const line::Matrix<T> WN = line::mva::sn_get_residt_from_respt<T>(sn, to_matrix<T>(r.RN));
4555 // THE ARRIVAL RATE IS A FLOW, NOT A COPY OF THE THROUGHPUT. `runAnalyzer`
4556 // takes it from `sn_get_arvr_from_tput`, i.e. from the class-expanded
4557 // routing, and the two agree only where every job a station serves it also
4558 // completes. They part on a station a job LEAVES by another route: on
4559 // cache_replc_routing the fluid solution puts zero throughput on the two
4560 // Delay stations while 0.4 and 0.6 arrive at them, so copying the
4561 // throughput made both rows all-zero and the table dropped them.
4562 const line::Matrix<T> AN = line::mva::sn_get_arvr_from_tput<T>(snflow, to_matrix<T>(r.TN));
4563 line::reg::Json extra = line::reg::Json::object();
4564 if (!cache.empty()) extra["Cache"] = cache_extra_json<T>(cache);
4565 if (!r.fj.empty()) {
4566 const line::reg::Json fjb = fj_extra_json(sn, r.fj);
4567 if (!fjb.is_null()) extra["FJ"] = fjb;
4568 }
4569 emit_avg_table<T>(sn, r.method, [&](std::size_t i, std::size_t c) {
4570 AvgRow row;
4571 row.q = r.QN(i, c);
4572 row.u = r.UN(i, c);
4573 row.r = r.RN(i, c);
4574 row.w = line::num_traits<T>::to_double(WN(i, c));
4575 row.t = r.TN(i, c);
4576 // A Source has no arrivals TO ITSELF, so its ArvR is 0 while its Tput is
4577 // the arrival rate -- the same rule the SSA arm above applies. Without it
4578 // the two CLI paths disagreed on one column of the same model:
4579 // gallery_mm1 reported Source ArvR 0 under -s mva and 1 under -s fluid.
4580 row.a = sn.stations[i].sched == line::lang::SchedStrategy::EXT
4581 ? 0.0
4582 : line::num_traits<T>::to_double(AN(i, c));
4583 return row;
4584 }, extra);
4585 return 0;
4586}
4587
4588/**
4589 * `-s fluid -a statevec`: the converged FLUID STATE VECTOR, `result.odeStateVec`.
4590 *
4591 * NOT A METRIC AND NOT INDEXED LIKE ONE. The ODE state carries one coordinate
4592 * per (station, class, PHASE), so a two-phase Erlang service contributes two
4593 * entries where the AvgTable contributes one number, and the sum over a
4594 * station's phases is its mean queue length. It is what a caller needs to
4595 * restart an integration, to seed another solver, or to read the phase
4596 * occupancy the means average away -- which is why the JAR exposes it as its
4597 * own `-a statevec` rather than as a column.
4598 *
4599 * The index layout is `fluid_state_layout`'s and is emitted BESIDE the vector,
4600 * because a bare list of numbers cannot be related back to a station without it.
4601 */
4602template <class T>
4603int solve_model_fluid_statevec(const std::string& file, const Knobs& k) {
4604 line::qn::Network<T> net = read_model<T>(file);
4605 const line::fluid::FluidOptions opt = fluid_options(k);
4607 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4608 if (r.xvec.empty())
4609 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
4610 "-a statevec reports the converged ODE state and this solve produced none; the rmf "
4611 "and closing branches integrate a drift and fill it, so a branch that returns means "
4612 "directly has no state vector to report");
4613
4614 // The (station, class, phase) each coordinate belongs to, in the order the
4615 // ODE state is laid out: station-major, then class, then phase.
4616 std::vector<std::size_t> ist, cls, phs;
4617 for (std::size_t i = 0; i < sn.nstations; ++i)
4618 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4619 const std::size_t np = sn.phases_of(i + 1, c + 1);
4620 for (std::size_t j = 0; j < np; ++j) {
4621 ist.push_back(i);
4622 cls.push_back(c);
4623 phs.push_back(j);
4624 }
4625 }
4626 // The layout is a CLAIM about the solver's state ordering, so it is checked
4627 // rather than asserted in a comment: a mismatch means the labels below would
4628 // name the wrong station, which is worse than no labels at all.
4629 const bool labelled = ist.size() == r.xvec.size();
4630
4631 if (g_json_output) {
4632 line::reg::Json p = line::reg::Json::object();
4633 p["type"] = "FluidStateVec";
4634 p["indexBase"] = 0;
4635 p["xvec"] = vector_json(r.xvec);
4636 p["labelled"] = labelled;
4637 if (labelled) {
4638 line::reg::Json st = line::reg::Json::array(), cl = line::reg::Json::array(),
4639 ph = line::reg::Json::array();
4640 for (std::size_t j = 0; j < ist.size(); ++j) {
4641 st.push_back(sn.stations[ist[j]].name);
4642 cl.push_back(sn.classes[cls[j]].name);
4643 ph.push_back(phs[j]);
4644 }
4645 p["Station"] = st;
4646 p["JobClass"] = cl;
4647 p["Phase"] = ph;
4648 }
4649 emit_analysis<T>("statevec", p, r.method);
4650 return 0;
4651 }
4652 std::printf("SolverFluid arith=%s method=%s coords=%zu\n", line::num_traits<T>::name(),
4653 r.method.c_str(), r.xvec.size());
4654 if (!labelled) {
4655 std::printf("# the ODE state is %zu wide and the (station, class, phase) layout accounts "
4656 "for %zu; the coordinates are printed unlabelled\n",
4657 r.xvec.size(), ist.size());
4658 std::printf("%-10s %20s\n", "Index", "x");
4659 for (std::size_t j = 0; j < r.xvec.size(); ++j)
4660 std::printf("%-10zu %20.10g\n", j, r.xvec[j]);
4661 return 0;
4662 }
4663 std::printf("%-16s %-14s %-8s %20s\n", "Station", "JobClass", "Phase", "x");
4664 for (std::size_t j = 0; j < r.xvec.size(); ++j)
4665 std::printf("%-16s %-14s %-8zu %20.10g\n", sn.stations[ist[j]].name.c_str(),
4666 sn.classes[cls[j]].name.c_str(), phs[j], r.xvec[j]);
4667 return 0;
4668}
4669
4670/** `--env-init`: a comma-separated probability per stage, checked by the coupling. */
4671inline std::vector<double> parse_env_init(const std::string& spec) {
4672 std::vector<double> p;
4673 std::size_t pos = 0;
4674 while (pos <= spec.size()) {
4675 std::size_t comma = spec.find(',', pos);
4676 if (comma == std::string::npos) comma = spec.size();
4677 const std::string cell = spec.substr(pos, comma - pos);
4678 char* endp = nullptr;
4679 const double v = std::strtod(cell.c_str(), &endp);
4680 if (cell.empty() || endp == cell.c_str() || *endp != '\0' || !std::isfinite(v))
4681 throw line::InputError("--env-init: '" + cell + "' in '" + spec +
4682 "' is not a probability; give one per stage, as 'p1,p2,...'");
4683 p.push_back(v);
4684 pos = comma + 1;
4685 }
4686 return p;
4687}
4688
4689/** A per-stage vector on the ENV banner, as `[a b c]` with %g cells. */
4690inline std::string env_vector_text(const std::vector<double>& v) {
4691 std::string s = "[";
4692 char buf[64];
4693 for (std::size_t i = 0; i < v.size(); ++i) {
4694 std::snprintf(buf, sizeof buf, "%s%.6g", i ? " " : "", v[i]);
4695 s += buf;
4696 }
4697 return s + "]";
4698}
4699
4700/**
4701 * Run the coupling `o.method` names, at the arithmetic the caller asked for.
4702 *
4703 * WHY THIS IS NOT JUST `env::solver_env`. That entry instantiates BOTH
4704 * couplings, and the mean-field one solves each stage with the fluid transient
4705 * -- LSODA, hence double. Calling it at `Rational` does not merely give a worse
4706 * answer, it does not compile (`sqrt` on a rational), so the template below
4707 * carries only the state-vector coupling and the double overload beside it
4708 * carries the full dispatch. The dispatcher has already refused `-s env
4709 * --arith exact` without `--method statevec`, so a non-double run reaching here HAS
4710 * asked for the state-vector coupling and gets it, banner included.
4711 */
4712template <class T>
4714 const line::env::EnvOptions& o) {
4716 out.statevec =
4717 line::env::solver_env_statevec(e, line::env::dispatch_detail::env_statevec_options<T>(o));
4718 line::env::dispatch_detail::env_take_statevec(out);
4719 return out;
4720}
4721
4722/** The double case, where both couplings are available. */
4724 const line::env::EnvOptions& o) {
4725 return line::env::solver_env(e, o);
4726}
4727
4728/**
4729 * Solve an ENVIRONMENT model.json with SolverENV and print the same average
4730 * table every other arm prints.
4731 *
4732 * THE COLUMNS ARE THE REFERENCE'S, INCLUDING THE TWO IT LEAVES EMPTY.
4733 * `@@SolverENV/getEnsembleAvg` returns Q, U and T from the coupling, sets
4734 * `WNclass = QNclass ./ TNclass` and returns `RNclass` and `ANclass` as NaN --
4735 * ENV blends per-stage metrics over the environment process and computes no
4736 * response time or arrival rate at all. Printing Q/T under RespT here would
4737 * invent a number the reference declines to give, so RespT and ArvR are NaN and
4738 * ResidT carries the Little's-law ratio, exactly as MATLAB's table does.
4739 *
4740 * The station and class names come from stage 1. The mean-field coupling
4741 * already refuses an environment whose stages disagree on the station or class
4742 * count, so any stage names the same rows.
4743 */
4744template <class T>
4745int solve_model_env(const std::string& file, const Knobs& k) {
4746 line::env::Environment<T> e = read_env_model<T>(file);
4748 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4749 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4750 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
4751 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
4752 if (k.tran_points) opt.tran_points = k.tran_points;
4753 if (k.tol >= 0.0) opt.stage.tol = k.tol;
4754 // THE STAGE SOLVER FOLLOWS THE COUPLING, because each coupling has exactly
4755 // one. `EnvOptions::stage_solver` defaults to `fluid`, which is what the
4756 // mean-field coupling needs (a transient mean) and what the state-vector one
4757 // refuses (it needs an enumerated generator and state space, which only
4758 // SolverCTMC exposes). Leaving the default in place made every
4759 // `--method statevec` run die on "stage solver 'fluid' is not available",
4760 // with no flag to fix it. This is not a silent fallback: there is one
4761 // admissible stage solver per coupling, and picking the other would be the
4762 // error.
4763 if (opt.method == "statevec") opt.stage_solver = "ctmc";
4764 // `--stage-solver` OVERRIDES that default, and only the mean-field coupling
4765 // has a choice to make: it needs a transient mean, which both the fluid
4766 // analyzer and the enumerated CTMC produce, and the two are different
4767 // models rather than two routes to one answer (a chain holds whole jobs).
4768 // An ensemble built on SolverCTMC stages therefore has to say so, or the
4769 // engine answers the fluid ensemble under its name -- which is what the
4770 // hosts refused lang='cpp' for.
4771 if (!k.stage_solver.empty()) opt.stage_solver = k.stage_solver;
4772 if (k.cutoff >= 0.0) opt.stage_cutoff = k.cutoff;
4773 // Method 'occupation' reads SolverENV's OWN timespan, unbounded unless
4774 // --tspan names a horizon, and its initial stage law.
4775 if (k.t1 >= 0.0) {
4776 opt.env_timespan_start = k.t0;
4777 opt.env_timespan_end = k.t1;
4778 }
4779 if (!k.env_init.empty()) opt.env_init = parse_env_init(k.env_init);
4780
4781 // UNQUALIFIED, so the double overload above wins for T = double: naming the
4782 // template explicitly would send every arithmetic to the state-vector
4783 // coupling and quietly ignore `--method meanfield`.
4784 const line::env::EnvAnalyzerSolution<T> r = env_run(e, opt);
4785 const line::qn::NetworkStruct<T>& sn = e.stage(0).model;
4786
4787 // The horizon and the grid are part of the ANSWER on this path, for the
4788 // same reason the seed and the sample count are on the SSA one: the
4789 // mean-field exit metrics are a quadrature, and two runs are the same
4790 // measurement only if both knobs are stated.
4791 // `points` is the mean-field quadrature's and is printed only there: the
4792 // state-vector coupling carries the whole joint law across a switch and
4793 // never sums over that grid, so reporting a grid it did not use would
4794 // describe a computation that did not happen.
4795 // A closed-form limit reads neither knob: it solves each stage, or one
4796 // rate-averaged model, in STEADY STATE, so a horizon and an iteration count
4797 // would describe a transient and a fixed point that never ran.
4798 if (r.method == "avg" || r.method == "dec")
4799 std::printf("SolverENV arith=%s method=%s stages=%zu (closed-form limit)\n",
4800 line::num_traits<T>::name(), r.method.c_str(), e.nstages());
4801 else if (r.method == "occupation")
4802 // `getStageOccupation` rides on the banner, one value per stage, as the
4803 // occupation weights are what the table below is blended with.
4804 std::printf("SolverENV arith=%s method=%s stages=%zu occupation=%s weight=%s\n",
4805 line::num_traits<T>::name(), r.method.c_str(), e.nstages(),
4806 env_vector_text(r.limit.occupation).c_str(),
4807 env_vector_text(r.limit.occupation_weight).c_str());
4808 else if (r.method == "statevec")
4809 std::printf("SolverENV arith=%s method=%s stages=%zu horizon=%.6g iters=%d%s\n",
4811 r.iterations, r.converged ? "" : " (NOT CONVERGED)");
4812 else
4813 // Every remaining method -- meanfield, and the smp and statedep runs of
4814 // the same analyzer -- sums the same quadrature, so all of them report
4815 // the grid it was summed over.
4816 std::printf("SolverENV arith=%s method=%s stages=%zu horizon=%.6g points=%zu iters=%d%s\n",
4818 opt.tran_points, r.iterations, r.converged ? "" : " (NOT CONVERGED)");
4819 // A Cache answers on the NODE and in three index spaces, none of them
4820 // (station, class), so the AvgTable cannot carry it and an environment
4821 // solve that computed one used to drop it here. It rides in the JSON
4822 // envelope for a host and prints as its own rows for a reader.
4823 line::reg::Json extra = line::reg::Json::object();
4824 if (!r.cache.empty()) extra["Cache"] = cache_extra_json<T>(r.cache);
4825 if (r.method == "occupation") {
4826 // `getStageOccupation`, keyed as the reference stores it in `result`.
4827 extra["Occupation"] = vector_json<double>(r.limit.occupation);
4828 extra["OccupationWeight"] = vector_json<double>(r.limit.occupation_weight);
4829 }
4830 emit_avg_table<T>(
4831 sn, r.method,
4832 [&](std::size_t i, std::size_t c) {
4833 AvgRow row;
4834 row.q = line::num_traits<T>::to_double(r.QN(i, c));
4835 row.u = line::num_traits<T>::to_double(r.UN(i, c));
4836 row.t = line::num_traits<T>::to_double(r.TN(i, c));
4837 row.r = std::numeric_limits<double>::quiet_NaN();
4838 row.a = std::numeric_limits<double>::quiet_NaN();
4839 row.w = row.q / row.t;
4840 return row;
4841 },
4842 extra);
4843 if (!g_json_output) print_cache_rows<T>(sn, r.cache);
4844 return 0;
4845}
4846
4847/**
4848 * `-a var`: the SECOND moment of the queue length, which only the fluid solver
4849 * has and only through two of its methods.
4850 *
4851 * `minnormal` and `refined` report the STATIONARY covariance of the linear noise
4852 * approximation (`@@SolverFLD/getMoments`), `kp` the covariance integrated along
4853 * the trajectory (`@@SolverFLD/getTranAvgVar`); this prints the per-station,
4854 * per-class variance and its standard deviation, plus, on the JSON path, the full
4855 * state covariance so that cross-station terms survive rather than only the
4856 * per-block totals. Every other method carries a first moment only and is refused
4857 * by name -- a variance of zero would be a claim, not an absence.
4858 */
4859template <class T>
4860int solve_model_fluid_var(const std::string& file, const Knobs& k) {
4861 line::qn::Network<T> net = read_model<T>(file);
4862 const line::fluid::FluidOptions opt = fluid_options(k);
4863 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4865 if (!r.has_moments)
4866 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
4867 "-a var needs a fluid method that computes a second moment: 'minnormal', 'refined' or "
4868 "'dae' for the stationary covariance, 'kp' for the covariance along the trajectory. "
4869 "The '" +
4870 r.method + "' method integrates the mean only");
4871
4872 if (g_json_output) {
4873 line::reg::Json p = line::reg::Json::object();
4874 p["type"] = "QueueLengthVariance";
4875 p["indexBase"] = 0;
4876 p["Station"] = line::reg::Json::array();
4877 p["JobClass"] = line::reg::Json::array();
4878 p["QVar"] = line::reg::Json::array();
4879 p["QStd"] = line::reg::Json::array();
4880 for (std::size_t i = 0; i < sn.nstations; ++i)
4881 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4882 if (r.moments.QVar(i, c) == 0.0) continue;
4883 p["Station"].push_back(sn.stations[i].name);
4884 p["JobClass"].push_back(sn.classes[c].name);
4885 p["QVar"].push_back(r.moments.QVar(i, c));
4886 p["QStd"].push_back(r.moments.QStd(i, c));
4887 }
4888 p["Sigma"] = matrix_json<double>(r.moments.Sigma);
4889 emit_analysis<T>("var", p, r.method);
4890 return 0;
4891 }
4892 std::printf("SolverFluid arith=%s method=%s second moment\n", line::num_traits<T>::name(),
4893 r.method.c_str());
4894 std::printf("%-16s %-14s %12s %12s\n", "Station", "JobClass", "QVar", "QStd");
4895 for (std::size_t i = 0; i < sn.nstations; ++i)
4896 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4897 if (r.moments.QVar(i, c) == 0.0) continue;
4898 std::printf("%-16s %-14s %12.6g %12.6g\n", sn.stations[i].name.c_str(),
4899 sn.classes[c].name.c_str(), r.moments.QVar(i, c), r.moments.QStd(i, c));
4900 }
4901 return 0;
4902}
4903
4904/**
4905 * `-a odes`: `@@SolverFLD/exportODEs`, the drift itself rather than its fixed
4906 * point.
4907 *
4908 * The output is the LaTeX document the reference writes, printed to stdout so
4909 * that it can be redirected. It carries a machine-readable comment header
4910 * naming every state variable and every event, which is what makes the document
4911 * diffable against MATLAB's rather than only readable.
4912 */
4913template <class T>
4914int solve_model_fluid_odes(const std::string& file, const Knobs& k) {
4915 line::qn::Network<T> net = read_model<T>(file);
4917 if (!k.method.empty()) opt.method = k.method;
4918 if (k.tol >= 0.0) opt.tol = k.tol;
4919 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4920 // `--notation` REACHES THE EXPORTER, and is not fixed at "scalar" here: the
4921 // matrix form is a different document of the same drift, and hardcoding one
4922 // while accepting a flag naming the other is the silent-acceptance defect
4923 // this CLI refuses everywhere else. An unrecognised name is refused by
4924 // `export_odes_latex` itself rather than defaulted.
4925 const std::string notation = k.notation.empty() ? "scalar" : k.notation;
4926 const std::string tex = line::fluid::solver_fluid_export_odes(sn, opt, notation, sn.name);
4927 if (g_json_output) {
4928 line::reg::Json p = line::reg::Json::object();
4929 p["type"] = "ODEs";
4930 p["notation"] = notation;
4931 // The document goes in a STRING VALUE, escaped by the JSON writer: it is
4932 // LaTeX, so it carries backslashes and newlines that a host reading raw
4933 // stdout would have to re-parse out of the surrounding table.
4934 p["latex"] = tex;
4935 // the exporter reads the method UNRESOLVED (default exports matrix), so report that, not the runner's pick
4936 emit_analysis<T>("odes", p, (opt.method.empty() || opt.method == "default") ? "matrix" : opt.method);
4937 return 0;
4938 }
4939 std::printf("%s\n", tex.c_str());
4940 return 0;
4941}
4942
4943/**
4944 * `-a jacobian`: `@@SolverFLD/getJacobian`, d f_i / d x_j of the mean-field
4945 * drift, with the equilibria beside it when they are asked for.
4946 *
4947 * This is the fixed point's LOCAL BEHAVIOUR, which no integration reports: the
4948 * eigenvalues of J tell a stable fixed point from a limit cycle and give the
4949 * rate at which the fluid approximation converges to it.
4950 *
4951 * The method must be a smooth one. `fluid_symbolic_drift` refuses the min-scaled
4952 * drifts by the factor that carries the kink, before any backend is contacted.
4953 */
4954template <class T>
4955int solve_model_fluid_jacobian(const std::string& file, const Knobs& k) {
4956 line::qn::Network<T> net = read_model<T>(file);
4958 if (!k.method.empty()) opt.method = k.method;
4959 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4960 // pstar REACHES THE SYSTEM ONLY UNDER `pnorm`, which is the rule the
4961 // integrator and the exporter both follow: `matrix` and `default` leave it
4962 // at zero, selecting the hard min. That is why they have no Jacobian here
4963 // and `pnorm` does, and it is the reference's rule too -- MATLAB reads
4964 // options.config.pstar, which is unset unless asked for.
4965 std::string m = opt.method;
4966 if (m.compare(0, 4, "fld.") == 0) m = m.substr(4);
4968 sn, opt.method, (m == "pnorm" || opt.pstar_set) ? opt.pstar : 0.0, std::vector<double>());
4970 if (!k.symbolic.empty()) symopt.backend = k.symbolic;
4971 symopt.equilibria = k.equilibria;
4973
4974 if (g_json_output) {
4975 line::reg::Json p = line::reg::Json::object();
4976 p["type"] = "Jacobian";
4977 p["engine"] = jac.engine;
4978 p["vars"] = line::reg::Json(jac.vars);
4979 p["rhs"] = line::reg::Json(jac.rhs);
4980 line::reg::Json rows = line::reg::Json::array();
4981 for (std::size_t i = 0; i < jac.J.size(); ++i) rows.push_back(line::reg::Json(jac.J[i]));
4982 p["jacobian"] = rows;
4983 // `hasEquilibria` separates "asked and answered with none" from "never
4984 // asked"; an empty list alone would read as "this system has none".
4985 p["hasEquilibria"] = jac.has_equilibria;
4986 line::reg::Json eqs = line::reg::Json::array();
4987 for (std::size_t e = 0; e < jac.equilibria.size(); ++e) {
4988 line::reg::Json one = line::reg::Json::object();
4989 for (std::map<std::string, std::string>::const_iterator it = jac.equilibria[e].begin();
4990 it != jac.equilibria[e].end(); ++it)
4991 one[it->first] = it->second;
4992 eqs.push_back(one);
4993 }
4994 p["equilibria"] = eqs;
4995 emit_analysis<T>("jacobian", p,
4996 line::fluid::detail::fluid_resolve_method(sn, opt.method, opt));
4997 return 0;
4998 }
4999
5000 std::printf("engine=%s states=%zu\n", jac.engine.c_str(), jac.vars.size());
5001 for (std::size_t i = 0; i < jac.rhs.size(); ++i)
5002 std::printf("d%s/dt = %s\n", jac.vars[i].c_str(), jac.rhs[i].c_str());
5003 for (std::size_t i = 0; i < jac.J.size(); ++i)
5004 for (std::size_t j = 0; j < jac.J[i].size(); ++j) {
5005 // A structurally zero entry is printed, not skipped: a reader must be
5006 // able to tell a zero derivative from a row this port never emitted.
5007 std::printf("J[%s,%s] = %s\n", jac.vars[i].c_str(), jac.vars[j].c_str(),
5008 jac.J[i][j].c_str());
5009 }
5010 if (jac.has_equilibria) {
5011 if (jac.equilibria.empty())
5012 std::printf("equilibria: none in closed form (the solve found none, which is not a "
5013 "proof that none exist)\n");
5014 for (std::size_t e = 0; e < jac.equilibria.size(); ++e)
5015 for (std::map<std::string, std::string>::const_iterator it = jac.equilibria[e].begin();
5016 it != jac.equilibria[e].end(); ++it)
5017 std::printf("equilibrium %zu: %s = %s\n", e + 1, it->first.c_str(),
5018 it->second.c_str());
5019 }
5020 return 0;
5021}
5022
5023/**
5024 * `-s fluid -a tranvar`: `@@SolverFLD/getTranAvgVar`, the queue-length VARIANCE
5025 * along the trajectory, plus the full state covariance at each time point.
5026 *
5027 * NOT `-a var`, which reports the STATIONARY covariance of `minnormal` /
5028 * `refined` -- one number per (station, class) at the fixed point. This is the
5029 * diffusion limit of Ko and Pender integrated alongside the fluid limit, so it
5030 * has a value at every t, and only `--method kp` produces it. Asking any other
5031 * method for it is an error rather than a misleading zero, which is the header's
5032 * own rule and is left to the header to enforce so the two flags cannot drift.
5033 */
5034template <class T>
5035int solve_model_fluid_tranvar(const std::string& file, const Knobs& k) {
5036 line::qn::Network<T> net = read_model<T>(file);
5037 const line::fluid::FluidOptions opt = fluid_options(k);
5038 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5040 if (tr.t.empty())
5041 throw line::UnsupportedError(line::ErrorKind{"Analysis"}, "-a tranvar produced no trajectory points");
5042
5043 if (g_json_output) {
5044 line::reg::Json p = line::reg::Json::object();
5045 p["type"] = "TranAvgVarTable";
5046 p["indexBase"] = 0;
5047 p["t0"] = tr.t.front();
5048 p["t1"] = tr.t.back();
5049 line::reg::Json ts = line::reg::Json::array();
5050 for (std::size_t j = 0; j < tr.t.size(); ++j) ts.push_back(tr.t[j]);
5051 p["t"] = ts;
5052 line::reg::Json arr = line::reg::Json::array();
5053 for (std::size_t i = 0; i < sn.nstations; ++i)
5054 for (std::size_t c = 0; c < sn.nclasses; ++c) {
5055 if (sn.disabled[i][c]) continue;
5056 line::reg::Json e = line::reg::Json::object();
5057 e["Station"] = sn.stations[i].name;
5058 e["JobClass"] = sn.classes[c].name;
5059 e["station"] = i;
5060 e["jobclass"] = c;
5061 line::reg::Json v = line::reg::Json::array();
5062 for (std::size_t j = 0; j < tr.QVar.size(); ++j) v.push_back(tr.QVar[j](i, c));
5063 e["QVar"] = v;
5064 arr.push_back(e);
5065 }
5066 p["curves"] = arr;
5067 // The full covariance is the answer's other half: the per-pair variances
5068 // are its diagonal, and a caller asking for the diffusion limit wants the
5069 // off-diagonal correlations the limit is about.
5070 line::reg::Json sig = line::reg::Json::array();
5071 for (std::size_t j = 0; j < tr.Sigma.size(); ++j)
5072 sig.push_back(matrix_json<double>(tr.Sigma[j]));
5073 p["Sigma"] = sig;
5074 emit_analysis<T>("tranvar", p, "kp");
5075 return 0;
5076 }
5077 std::printf("SolverFluid arith=%s method=kp tspan=[%g,%g] points=%zu dim=%zu\n",
5078 line::num_traits<T>::name(), tr.t.front(), tr.t.back(), tr.t.size(),
5079 tr.Sigma.empty() ? std::size_t(0) : tr.Sigma.front().rows());
5080 std::printf("%-16s %-14s %12s %12s %12s\n", "Station", "JobClass", "Time", "QVar", "QStd");
5081 for (std::size_t i = 0; i < sn.nstations; ++i)
5082 for (std::size_t c = 0; c < sn.nclasses; ++c) {
5083 if (sn.disabled[i][c]) continue;
5084 for (std::size_t j = 0; j < tr.QVar.size(); ++j) {
5085 const double var = tr.QVar[j](i, c);
5086 std::printf("%-16s %-14s %12.6g %12.6g %12.6g\n", sn.stations[i].name.c_str(),
5087 sn.classes[c].name.c_str(), tr.t[j], var,
5088 var >= 0.0 ? std::sqrt(var) : std::numeric_limits<double>::quiet_NaN());
5089 }
5090 }
5091 return 0;
5092}
5093
5094/**
5095 * `-s fluid -a tran`: `@@SolverFLD/getTranAvg`, the metrics ALONG the
5096 * trajectory rather than at its fixed point.
5097 *
5098 * `--tspan` is optional here and required on the MAM arm, and the difference is
5099 * the reference's: `options.timespan` defaults to [0, Inf] for the fluid solver,
5100 * which does not mean "integrate forever" but "integrate until the state stops
5101 * moving" -- `solver_fluid_tran_avg` reproduces the horizon that adaptive loop
5102 * converges at. A caller that names a horizon gets exactly that one.
5103 *
5104 * The reference forces `closing` for a transient (the matrix and smoothed
5105 * variants are steady-state devices) and warns when it does; the port forces it
5106 * too, and the banner names the method that actually integrated. `dae` is the
5107 * one exception the reference itself makes -- it has a trajectory of its own,
5108 * with conservation carried as an algebraic equation -- so
5109 * `solver_fluid_run_transient` routes it rather than substituting the
5110 * first-order drift under its name.
5111 */
5112template <class T>
5113int solve_model_fluid_tran(const std::string& file, const Knobs& k) {
5114 line::qn::Network<T> net = read_model<T>(file);
5115 const line::fluid::FluidOptions opt = fluid_options(k);
5116 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5117 const std::vector<line::fluid::FluidTranPoint> tr =
5119 if (tr.empty()) throw line::UnsupportedError(line::ErrorKind{"Analysis"}, "-a tran produced no trajectory points");
5120 // the banner names the method that integrated: 'dae' keeps its own trajectory, every other one is 'closing'
5121 const std::string tran_method =
5122 line::fluid::detail::fluid_unqualify(opt.method) == "dae" ? std::string("dae") : std::string("closing");
5123
5124 if (g_json_output) {
5125 line::reg::Json p = line::reg::Json::object();
5126 p["type"] = "TranAvgTable";
5127 p["indexBase"] = 0;
5128 p["t0"] = 0.0;
5129 p["t1"] = tr.back().t;
5130 line::reg::Json ts = line::reg::Json::array();
5131 for (std::size_t j = 0; j < tr.size(); ++j) ts.push_back(tr[j].t);
5132 line::reg::Json arr = line::reg::Json::array();
5133 for (std::size_t i = 0; i < sn.nstations; ++i)
5134 for (std::size_t c = 0; c < sn.nclasses; ++c) {
5135 if (sn.disabled[i][c]) continue;
5136 line::reg::Json e = line::reg::Json::object();
5137 e["Station"] = sn.stations[i].name;
5138 e["JobClass"] = sn.classes[c].name;
5139 e["station"] = i;
5140 e["jobclass"] = c;
5141 e["t"] = ts;
5142 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array(),
5143 x = line::reg::Json::array();
5144 for (std::size_t j = 0; j < tr.size(); ++j) {
5145 q.push_back(tr[j].QN(i, c));
5146 u.push_back(tr[j].UN(i, c));
5147 x.push_back(tr[j].TN(i, c));
5148 }
5149 e["QLen"] = q;
5150 e["Util"] = u;
5151 e["Tput"] = x;
5152 arr.push_back(e);
5153 }
5154 p["curves"] = arr;
5155 emit_analysis<T>("tran", p, tran_method);
5156 return 0;
5157 }
5158 std::printf("SolverFluid arith=%s method=%s tspan=[0,%g] points=%zu\n",
5159 line::num_traits<T>::name(), tran_method.c_str(), tr.back().t, tr.size());
5160 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
5161 "Tput");
5162 for (std::size_t i = 0; i < sn.nstations; ++i)
5163 for (std::size_t c = 0; c < sn.nclasses; ++c) {
5164 if (sn.disabled[i][c]) continue;
5165 for (std::size_t j = 0; j < tr.size(); ++j)
5166 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
5167 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), tr[j].t,
5168 tr[j].QN(i, c), tr[j].UN(i, c), tr[j].TN(i, c));
5169 }
5170 return 0;
5171}
5172
5173/**
5174 * `-s fluid -a prob`: `@@SolverFLD/getProbAggr`, the probability that a station
5175 * holds the marginal population of the model's default state.
5176 *
5177 * IT IS NOT THE MVA ARM'S ANSWER AND IS NOT MEANT TO BE. The fluid solver has
5178 * no state space, so the law is fitted to the means it does produce -- a
5179 * binomial per closed class, the BCMP marginal per open one -- and under a
5180 * moment closure it is instead the multivariate normal the closure supplies,
5181 * correlation between the classes included. Two solvers disagreeing here is the
5182 * approximation showing, not a defect.
5183 *
5184 * The log-probability is reported beside it because the fitted law underflows
5185 * on a large population, where the linear value is 0 and the log one is not.
5186 */
5187template <class T>
5188int solve_model_fluid_prob(const std::string& file, const Knobs& k) {
5189 line::qn::Network<T> net = read_model<T>(file);
5190 const line::fluid::FluidOptions opt = fluid_options(k);
5191 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5193
5194 std::vector<double> pr(sn.nstations, 0.0), lg(sn.nstations, 0.0);
5195 for (std::size_t i = 0; i < sn.nstations; ++i)
5196 pr[i] = line::fluid::fluid_prob_aggr(sn, r, i + 1, &lg[i]);
5197
5198 if (g_json_output) {
5199 line::reg::Json p = line::reg::Json::object();
5200 p["type"] = "ProbAggr";
5201 p["indexBase"] = 0;
5202 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
5203 lp = line::reg::Json::array();
5204 for (std::size_t i = 0; i < sn.nstations; ++i) {
5205 st.push_back(sn.stations[i].name);
5206 pa.push_back(pr[i]);
5207 lp.push_back(lg[i]);
5208 }
5209 p["Station"] = st;
5210 p["ProbAggr"] = pa;
5211 p["logProbAggr"] = lp;
5212 emit_analysis<T>("prob", p, r.method);
5213 return 0;
5214 }
5215 std::printf("SolverFluid arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5216 r.method.c_str(), line::util::method_type("FLD", r.method).c_str());
5217 std::printf("%-16s %14s %14s\n", "Station", "ProbAggr", "logProbAggr");
5218 for (std::size_t i = 0; i < sn.nstations; ++i)
5219 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(), pr[i], lg[i]);
5220 return 0;
5221}
5222
5223/**
5224 * `-s fluid -a cdf`: `@@SolverFLD/getCdfRespT`, the WHOLE response-time law per
5225 * (station, class) and not only its mean.
5226 *
5227 * The law is read off a second integration in which the jobs present at the
5228 * steady state are MARKED and followed to their departure, so it is the
5229 * stationary response-time distribution of the fluid model. The solve that
5230 * produces the state to mark in is run here, as the reference runs it: its
5231 * `getCdfRespT` clears the cached result and re-runs `getAvg` first.
5232 */
5233template <class T>
5234int solve_model_fluid_cdf(const std::string& file, const Knobs& k) {
5235 line::qn::Network<T> net = read_model<T>(file);
5236 const line::fluid::FluidOptions opt = fluid_options(k);
5237 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5238 const std::vector<std::vector<line::fluid::FluidPassage> > RD =
5240
5241 if (g_json_output) {
5242 line::reg::Json p = line::reg::Json::object();
5243 p["type"] = "CdfRespT";
5244 p["indexBase"] = 0;
5245 line::reg::Json arr = line::reg::Json::array();
5246 for (std::size_t i = 0; i < RD.size(); ++i)
5247 for (std::size_t c = 0; c < RD[i].size(); ++c) {
5248 // An empty curve is an ABSENT law -- a Source, or a class the
5249 // station does not serve -- and is omitted rather than sent as a
5250 // degenerate one.
5251 if (RD[i][c].t.empty()) continue;
5252 line::reg::Json e = line::reg::Json::object();
5253 e["Station"] = sn.stations[i].name;
5254 e["JobClass"] = sn.classes[c].name;
5255 e["station"] = i;
5256 e["jobclass"] = c;
5257 e["t"] = line::reg::Json(RD[i][c].t);
5258 e["F"] = line::reg::Json(RD[i][c].cdf);
5259 arr.push_back(e);
5260 }
5261 p["respt"] = arr;
5262 emit_analysis<T>("cdf", p, std::string());
5263 return 0;
5264 }
5265 std::printf("SolverFluid arith=%s\n", line::num_traits<T>::name());
5266 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
5267 for (std::size_t i = 0; i < RD.size(); ++i)
5268 for (std::size_t c = 0; c < RD[i].size(); ++c)
5269 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
5270 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
5271 sn.classes[c].name.c_str(), RD[i][c].t[j], RD[i][c].cdf[j]);
5272 return 0;
5273}
5274
5275/**
5276 * `-s fluid -a aoi`: `@@SolverFLD/getAvgAoI` and `getCdfAoI` in one answer, the
5277 * Age of Information and Peak AoI laws of a status-update system.
5278 *
5279 * ONLY THE `mfq` METHOD HAS THEM, and only on the topology the age laws are
5280 * defined for: one open class through Source -> Queue -> Sink, a single server,
5281 * capacity 1 (bufferless) or 2 (single buffer), FCFS/LCFS/LCFSPR. The topology
5282 * is tested first so a model that is not one is told WHICH condition it fails
5283 * rather than being handed a number computed for a different system.
5284 *
5285 * `--method` may only say `mfq` here: silently overriding a caller who asked for
5286 * another method would report the age laws under a method that does not produce
5287 * them.
5288 */
5289template <class T>
5290int solve_model_fluid_aoi(const std::string& file, const Knobs& k) {
5291 line::qn::Network<T> net = read_model<T>(file);
5292 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5294 if (!top.ok)
5295 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
5296 "-a aoi reports the age of a status-update system and needs the topology the age laws "
5297 "are defined for: " +
5298 (top.error.empty() ? std::string("this model is not one") : top.error));
5299 line::fluid::FluidOptions opt = fluid_options(k);
5300 const std::string requested = line::fluid::detail::fluid_unqualify(opt.method);
5301 if (requested != "default" && requested != "mfq")
5302 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
5303 "-a aoi is the AoI branch of the 'mfq' method; '" + requested +
5304 "' integrates the mean-field drift and carries no age process");
5305 opt.method = "mfq";
5307 if (!r.has_aoi)
5308 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
5309 "-a aoi: the 'mfq' method did not take its AoI branch on this model");
5310
5311 // The grid `getCdfAoI` builds when the caller names no time points: five
5312 // mean ages, which covers the bulk of both laws.
5313 const double base = (std::isfinite(r.aoi.aoi.mean) && r.aoi.aoi.mean > 0.0) ? r.aoi.aoi.mean : 1.0;
5314 const std::size_t np = 200;
5315 std::vector<double> tv(np), fa(np), fp(np);
5316 for (std::size_t j = 0; j < np; ++j) {
5317 tv[j] = 5.0 * base * static_cast<double>(j) / static_cast<double>(np - 1);
5318 fa[j] = line::fluid::aoi_cdf(r.aoi.aoi, tv[j]);
5319 fp[j] = line::fluid::aoi_cdf(r.aoi.paoi, tv[j]);
5320 }
5321 const double asd = std::sqrt(std::max(0.0, r.aoi.aoi.var));
5322 const double psd = std::sqrt(std::max(0.0, r.aoi.paoi.var));
5323
5324 if (g_json_output) {
5325 line::reg::Json p = line::reg::Json::object();
5326 p["type"] = "AoI";
5327 p["systemType"] = r.aoi.system_type;
5328 p["preemption"] = r.aoi.preemption;
5329 p["AoIMean"] = r.aoi.aoi.mean;
5330 p["AoIVar"] = r.aoi.aoi.var;
5331 p["AoIStd"] = asd;
5332 p["PAoIMean"] = r.aoi.paoi.mean;
5333 p["PAoIVar"] = r.aoi.paoi.var;
5334 p["PAoIStd"] = psd;
5335 p["t"] = line::reg::Json(tv);
5336 p["AoICdf"] = line::reg::Json(fa);
5337 p["PAoICdf"] = line::reg::Json(fp);
5338 // The (g, A, h) DENSITY triples, not only the curve evaluated above.
5339 // `getCdfAoI` takes an optional t_values, and a caller who names their
5340 // own grid cannot be served from a fixed 200-point one; with the triple
5341 // they evaluate the same law at their own abscissae. `solve_mfq_aoi`
5342 // normalizes g so that g*expm(A t)*h is the density, so the survival
5343 // function carries an extra inv(A) -- the MATLAB getter's own note.
5344 p["AoI_g"] = line::reg::Json(r.aoi.aoi.g);
5345 p["AoI_A"] = matrix_json(r.aoi.aoi.A);
5346 p["AoI_h"] = line::reg::Json(r.aoi.aoi.h);
5347 p["PAoI_g"] = line::reg::Json(r.aoi.paoi.g);
5348 p["PAoI_A"] = matrix_json(r.aoi.paoi.A);
5349 p["PAoI_h"] = line::reg::Json(r.aoi.paoi.h);
5350 emit_analysis<T>("aoi", p, r.method);
5351 return 0;
5352 }
5353 std::printf("SolverFluid arith=%s method=mfq system=%s preemption=%.6g\n",
5355 std::printf("%-8s %14s %14s %14s\n", "Metric", "Mean", "Var", "Std");
5356 std::printf("%-8s %14.10g %14.10g %14.10g\n", "AoI", r.aoi.aoi.mean, r.aoi.aoi.var, asd);
5357 std::printf("%-8s %14.10g %14.10g %14.10g\n", "PAoI", r.aoi.paoi.mean, r.aoi.paoi.var, psd);
5358 std::printf("%14s %14s %14s\n", "Time", "F_AoI(t)", "F_PAoI(t)");
5359 for (std::size_t j = 0; j < np; ++j)
5360 std::printf("%14.8g %14.10g %14.10g\n", tv[j], fa[j], fp[j]);
5361 return 0;
5362}
5363
5364/**
5365 * Solve a Network model.json and print its aggregate state probabilities:
5366 * getProbSysAggr (the whole-system joint) and getProbAggr per station, over the
5367 * model's default initial state. These fit the MVA means and so need logarithms;
5368 * under exact/Rational they refuse by name.
5369 */
5370template <class T>
5371int solve_model_prob(const std::string& file, const Knobs& k) {
5373 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
5374 "the -a prob analysis fits a binomial/product-form law and needs transcendental "
5375 "arithmetic; rerun with --arith double or --arith real");
5376 } else {
5377 line::qn::Network<T> net = read_model<T>(file);
5379 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5380 if (k.tol >= 0.0) opt.tol = k.tol;
5381 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5382 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5383 line::Matrix<T> init;
5385 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5386 const line::mva::AggrResult<T> ps =
5388 if (g_json_output) {
5389 line::reg::Json p = line::reg::Json::object();
5390 p["type"] = "ProbAggr";
5391 p["indexBase"] = 0;
5392 p["ProbSysAggr"] = line::num_traits<T>::to_double(ps.P);
5393 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array();
5394 for (std::size_t i = 0; i < sn.nstations; ++i) {
5395 st.push_back(sn.stations[i].name);
5396 pa.push_back(line::num_traits<T>::to_double(
5397 line::mva::solver_mva_get_prob_aggr(sn, r, i + 1, opt.method).P));
5398 }
5399 p["Station"] = st;
5400 p["ProbAggr"] = pa;
5401 emit_analysis<T>("prob", p, r.actualmethod);
5402 return 0;
5403 }
5404 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5405 r.actualmethod.c_str(),
5406 line::util::method_type("MVA", r.actualmethod).c_str());
5407 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(ps.P));
5408 std::printf("%-16s %14s\n", "Station", "ProbAggr");
5409 for (std::size_t i = 0; i < sn.nstations; ++i) {
5410 const line::mva::AggrResult<T> pa =
5412 std::printf("%-16s %14.10g\n", sn.stations[i].name.c_str(),
5414 }
5415 return 0;
5416 }
5417}
5418
5419/**
5420 * `-s mva -a marg`: `@@SolverMVA/getProbMarg`, P(n jobs of class r at station i).
5421 *
5422 * THE WHOLE GRID BY DEFAULT, one curve per (station, class): the reference takes
5423 * the station and the class as arguments and this CLI has no notion of a
5424 * "current" pair, so reporting every pair is the only reading that answers the
5425 * method rather than a choice this file would be making on the caller's behalf.
5426 * `--node` and `--class` narrow it to one node's station and one class, and
5427 * `--marg-states` is the reference's third argument `state_m`: the n values to
5428 * report, in place of the default range each case picks for itself (0..N_r for a
5429 * closed class, mean + 5 sigma for a Poisson, the 1e-10 tail for a geometric).
5430 *
5431 * A NODE THAT IS NOT A STATION IS AN ERROR, not an empty answer: a queue-length
5432 * law at a ClassSwitch is not a quantity, and defaulting to the whole network
5433 * after the caller narrowed it would report more than was asked for.
5434 */
5435template <class T>
5436int solve_model_marg(const std::string& file, const Knobs& k) {
5438 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
5439 "the -a marg analysis fits a binomial / Poisson / geometric law and needs "
5440 "transcendental arithmetic; rerun with --arith double or --arith real");
5441 } else {
5442 line::qn::Network<T> net = read_model<T>(file);
5444 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5445 if (k.tol >= 0.0) opt.tol = k.tol;
5446 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5447 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5448 line::Matrix<T> init;
5450 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5451
5452 std::vector<std::size_t> ists; // 1-based station indices to report
5453 if (k.node) {
5454 if (k.node > sn.nof_nodes())
5455 throw line::InputError("--node " + std::to_string(k.node) +
5456 " exceeds the number of nodes in the model (" +
5457 std::to_string(sn.nof_nodes()) + ")");
5458 const std::size_t ist = sn.nodes[k.node - 1].station;
5459 if (!ist)
5460 throw line::InputError("--node " + std::to_string(k.node) + " ('" +
5461 sn.nodes[k.node - 1].name +
5462 "') is not a station, and a queue-length distribution is "
5463 "reported per station");
5464 ists.push_back(ist);
5465 } else {
5466 for (std::size_t i = 0; i < sn.nstations; ++i) ists.push_back(i + 1);
5467 }
5468 std::vector<std::size_t> rs; // 1-based class indices to report
5469 if (k.jobclass) {
5470 if (k.jobclass > sn.nclasses)
5471 throw line::InputError("--class " + std::to_string(k.jobclass) +
5472 " exceeds the number of classes in the model");
5473 rs.push_back(k.jobclass);
5474 } else {
5475 for (std::size_t c = 0; c < sn.nclasses; ++c) rs.push_back(c + 1);
5476 }
5477
5478 // Every curve first: a pair the reference refuses must not leave a
5479 // banner and a column header standing above an answer that never came.
5480 std::vector<line::mva::MargResult<T> > curves;
5481 for (std::size_t a = 0; a < ists.size(); ++a)
5482 for (std::size_t b = 0; b < rs.size(); ++b)
5483 curves.push_back(line::mva::solver_mva_get_prob_marg(sn, r, ists[a], rs[b],
5484 k.marg_states, opt.method));
5485
5486 if (g_json_output) {
5487 line::reg::Json p = line::reg::Json::object();
5488 p["type"] = "ProbMarg";
5489 p["indexBase"] = 0;
5490 line::reg::Json arr = line::reg::Json::array();
5491 for (std::size_t a = 0, q = 0; a < ists.size(); ++a)
5492 for (std::size_t b = 0; b < rs.size(); ++b, ++q) {
5493 const line::mva::MargResult<T>& m = curves[q];
5494 line::reg::Json e = line::reg::Json::object();
5495 e["station"] = ists[a] - 1;
5496 e["Station"] = sn.stations[ists[a] - 1].name;
5497 e["jobclass"] = rs[b] - 1;
5498 e["JobClass"] = sn.classes[rs[b] - 1].name;
5499 line::reg::Json jobs = line::reg::Json::array();
5500 for (std::size_t n = 0; n < m.P.size(); ++n)
5501 jobs.push_back(k.marg_states.empty() ? static_cast<long>(n)
5502 : k.marg_states[n]);
5503 e["Jobs"] = jobs;
5504 e["P"] = vector_json(m.P);
5505 e["logP"] = vector_json(m.logP);
5506 arr.push_back(e);
5507 }
5508 p["marginal"] = arr;
5509 emit_analysis<T>("marg", p, r.actualmethod);
5510 return 0;
5511 }
5512 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5513 r.actualmethod.c_str(),
5514 line::util::method_type("MVA", r.actualmethod).c_str());
5515 std::printf("%-16s %-14s %-8s %16s\n", "Station", "JobClass", "Jobs", "ProbMarg");
5516 for (std::size_t a = 0, q = 0; a < ists.size(); ++a)
5517 for (std::size_t b = 0; b < rs.size(); ++b, ++q) {
5518 const line::mva::MargResult<T>& m = curves[q];
5519 for (std::size_t n = 0; n < m.P.size(); ++n)
5520 std::printf("%-16s %-14s %-8ld %16.10g\n",
5521 sn.stations[ists[a] - 1].name.c_str(),
5522 sn.classes[rs[b] - 1].name.c_str(),
5523 k.marg_states.empty() ? static_cast<long>(n) : k.marg_states[n],
5525 }
5526 return 0;
5527 }
5528}
5529
5530/**
5531 * Mean busy period of a named subnetwork, Daduna (J. ACM 35(3), 1988).
5532 *
5533 * The transform `solver_nc_busyp` has been in the port since it landed, and
5534 * `ldes_cli` has answered `--busyperiod` all along, so the ONLY thing between
5535 * a caller and the analytical form was a `-a` token: asking this CLI for a busy
5536 * period meant simulating a quantity there is a closed form for.
5537 *
5538 * `--busyperiod-subnet` is 1-BASED, as every station index this CLI takes is,
5539 * and is required: a busy period is defined for a NAMED set of stations and
5540 * defaulting it would answer about a subnetwork the caller never chose. The
5541 * orders default to 1, the ordinary busy period.
5542 */
5543template <class T>
5544int solve_model_nc_busyp(const std::string& file, const Knobs& k) {
5545 if (k.busy_subnet.empty())
5546 throw line::InputError(
5547 "-a busyperiod needs --busyperiod-subnet: the busy period is defined for a named "
5548 "subnetwork of stations, and no default can choose one");
5549 line::qn::Network<T> net = read_model<T>(file);
5550 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5551 std::vector<std::size_t> subnet;
5552 for (std::size_t t = 0; t < k.busy_subnet.size(); ++t) {
5553 if (k.busy_subnet[t] > sn.nstations)
5554 throw line::InputError("--busyperiod-subnet names station " +
5555 std::to_string(k.busy_subnet[t]) + ", beyond the model's " +
5556 std::to_string(sn.nstations));
5557 subnet.push_back(k.busy_subnet[t] - 1);
5558 }
5559 std::vector<std::size_t> orders = k.busy_orders;
5560 if (orders.empty()) orders.push_back(1);
5561 const std::vector<double> b = line::nc::solver_nc_busyp(sn, subnet, orders);
5562
5563 if (g_json_output) {
5564 line::reg::Json p = line::reg::Json::object();
5565 p["type"] = "BusyPeriod";
5566 p["indexBase"] = 0;
5567 line::reg::Json sj = line::reg::Json::array();
5568 for (std::size_t t = 0; t < subnet.size(); ++t) sj.push_back(subnet[t]);
5569 line::reg::Json oj = line::reg::Json::array();
5570 for (std::size_t t = 0; t < orders.size(); ++t) oj.push_back(orders[t]);
5571 line::reg::Json bj = line::reg::Json::array();
5572 for (std::size_t t = 0; t < b.size(); ++t) bj.push_back(b[t]);
5573 p["subnet"] = sj;
5574 p["orders"] = oj;
5575 p["b"] = bj;
5576 emit_analysis<T>("busyperiod", p, "daduna");
5577 return 0;
5578 }
5579 std::printf("SolverNC arith=%s busy period, subnetwork {", line::num_traits<T>::name());
5580 for (std::size_t t = 0; t < k.busy_subnet.size(); ++t)
5581 std::printf("%s%zu", t ? "," : "", k.busy_subnet[t]);
5582 std::printf("}\n");
5583 for (std::size_t t = 0; t < orders.size(); ++t)
5584 std::printf(" order %zu %.10g\n", orders[t], b[t]);
5585 return 0;
5586}
5587
5588/**
5589 * `-a normconst`: `@@SolverMVA/getProbNormConstAggr` and `@@SolverNC`'s.
5590 *
5591 * ONE ANALYSIS, TWO SOLVERS, AND THEY DO NOT COMPUTE IT THE SAME WAY. The NC
5592 * arm reads the constant its own solve already formed. The MVA arm RE-ENTERS the
5593 * analyzer at method='exact', as the reference does, because only the exact MVA
5594 * recursion carries a G: an AMVA solve has none, and reporting the requested
5595 * method's number would attribute the constant to an algorithm that never
5596 * produced one. That re-entry is why this is a separate `-a` and not a field on
5597 * the `-s mva` banner, where it would charge every average solve for a second
5598 * exact one.
5599 *
5600 * WHAT A MODEL WITH NO CONSTANT REPORTS IS THE ANALYZER'S OWN ANSWER, not a
5601 * substitution made here, and the two cases differ: the branches that form no G
5602 * at all -- MVAC, the LCFS chain -- set lG to NaN at the source and print nan,
5603 * while the open-queue closed forms report lG = 0 exactly as
5604 * solver_mva_qsys_analyzer.m:54,96,235 does. Neither is edited on the way out.
5605 */
5606template <class T>
5607int solve_model_normconst(const std::string& file, const Knobs& k, const std::string& solver) {
5608 line::qn::Network<T> net = read_model<T>(file);
5609 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5610 double lG = 0.0;
5611 std::string method;
5612 if (solver == "nc") {
5614 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5615 if (k.tol >= 0.0) opt.tol = k.tol;
5616 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5617 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5619 lG = r.lognormconst.has_value() ? r.lognormconst.value()
5620 : std::numeric_limits<double>::quiet_NaN();
5621 method = r.actualmethod;
5622 } else {
5624 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5625 if (k.tol >= 0.0) opt.tol = k.tol;
5626 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5627 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5629 method = "exact";
5630 }
5631 if (g_json_output) {
5632 line::reg::Json p = line::reg::Json::object();
5633 p["type"] = "NormConst";
5634 p["indexBase"] = 0;
5635 // A NaN rides as JSON null, which is the table's nan on the wire.
5636 p["logNormConstAggr"] = lG;
5637 emit_analysis<T>("normconst", p, method);
5638 return 0;
5639 }
5640 std::printf("Solver%s arith=%s method=%s lognormconst=%.10g\n",
5641 solver == "nc" ? "NC" : "MVA", line::num_traits<T>::name(), method.c_str(), lG);
5642 return 0;
5643}
5644
5645/** The model's declared per-class placement, in `solver_nc_*`'s own container. */
5646template <class T>
5647line::nc::MarginalState nc_declared_marginal(const line::qn::NetworkStruct<T>& sn) {
5649 line::nc::MarginalState out(sn.nstations, std::vector<int>(sn.nclasses, 0));
5650 for (std::size_t i = 0; i < sn.nstations; ++i)
5651 for (std::size_t r = 0; r < sn.nclasses; ++r)
5652 out[i][r] = static_cast<int>(std::llround(line::num_traits<T>::to_double(nir(i, r))));
5653 return out;
5654}
5655
5656/**
5657 * The same two probabilities under SolverNC: `@@SolverNC/getProbSysAggr.m` and
5658 * `@@SolverNC/getProbAggr.m`, over the model's declared state.
5659 *
5660 * NOT THE SAME NUMBERS AS `-s mva -a prob`, and that is the point of having
5661 * both. SolverMVA fits a binomial to its own means (Schmidt 1997); these are a
5662 * ratio of normalizing constants and are the product-form model's own
5663 * probabilities exactly. A closed model therefore reports different figures
5664 * under the two solvers, and the NC ones are the reference.
5665 */
5666template <class T>
5667int solve_model_nc_prob(const std::string& file, const Knobs& k) {
5669 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
5670 "the -s nc -a prob analysis exponentiates a difference of log normalizing constants "
5671 "and needs transcendental arithmetic; rerun with --arith double or --arith real");
5672 } else {
5673 line::qn::Network<T> net = read_model<T>(file);
5675 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5676 if (k.tol >= 0.0) opt.tol = k.tol;
5677 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5678 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5679 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5680
5681 // The state is the MODEL'S OWN, which `model.json` now carries: a
5682 // stateful node's declared row is decoded to the per-class counts the
5683 // reference reads with `State.toMarginal(sn, ist, state{isf})`. Where a
5684 // station declares none, the default marking is rebuilt for it -- every
5685 // closed class at its reference station -- which is what `initDefault`
5686 // would have put there.
5687 line::nc::MarginalState nir = nc_declared_marginal<T>(sn);
5688 // `--state` is `getProb(node, state)`'s second argument, decoded the way
5689 // the reference decodes it: it substitutes the row into `sn.state{isf}`
5690 // and takes `State.toMarginal` of the result, so what reaches the
5691 // probability is that node's PER-CLASS COUNTS and every other node's
5692 // declared ones. Passing the row through untouched would treat an
5693 // encoding as a job vector, and on a station with phase-type service the
5694 // two differ in both width and meaning.
5695 if (!k.state.empty()) {
5696 if (!k.node)
5697 throw line::InputError(
5698 "--state is the state of ONE node and needs --node to say which");
5699 const std::size_t ist =
5700 k.node <= sn.nodes.size() ? sn.nodes[k.node - 1].station : 0;
5701 if (ist == 0)
5702 throw line::InputError("--node " + std::to_string(k.node) +
5703 " is not a station, so it has no queue-length state");
5704 std::vector<std::size_t> ph(sn.nclasses, 1), shift(sn.nclasses, 0);
5705 std::size_t w = 0;
5706 for (std::size_t c = 0; c < sn.nclasses; ++c) {
5707 ph[c] = sn.phases_of(ist, c + 1);
5708 shift[c] = w;
5709 w += ph[c];
5710 }
5711 std::vector<T> row(k.state.size());
5712 for (std::size_t i = 0; i < k.state.size(); ++i)
5713 row[i] = line::num_traits<T>::from_int(k.state[i]);
5714 const line::qn::Marginal<T> m =
5715 line::qn::to_marginal(sn, ist, row, ph, shift, sn.nvars_of(k.node));
5716 for (std::size_t c = 0; c < sn.nclasses; ++c)
5717 nir[ist - 1][c] =
5718 static_cast<int>(std::llround(line::num_traits<T>::to_double(m.nir[c])));
5719 }
5720
5721 // THE SOLVE'S lG IS NOT THIS lG. `solver_nc_solve` normalizes the
5722 // SEIDMANN-REDUCED model -- a multiserver station enters as demand/c
5723 // with the residual folded into the delay -- while the probability
5724 // identity F_i G_{-i} / G needs the constant of the load-dependent
5725 // lattice mu(n) = min(n, c) that F_i and G_{-i} are computed on. The two
5726 // coincide only without multiservers. `solver_nc_margaggr` computes its
5727 // own, once, for every station -- which is the reference's
5728 // `logNormConstAggr` caching, not a per-station resolve.
5729 // THREE OF THE FOUR VIEWS NEED A LOAD-DEPENDENT NORMALIZING CONSTANT
5730 // over the WHOLE network, and `pfqn_ncld` computes it with the caller's
5731 // own method (`solver_nc_margaggr.m:53`, `solver_nc_marg.m:35`,
5732 // `solver_nc_joint.m:32`). A method name with no load-dependent
5733 // algorithm behind it -- the stochastic estimators `ls`, `mci`, `imci`,
5734 // `mcmc`, and the load-INDEPENDENT exact ones such as `comom` -- has
5735 // nothing to run there, and MATLAB refuses `getProb`, `getProbSys` and
5736 // `getProbAggr` under such a name with `pfqn_ncld`'s own message
5737 // (`pfqn_ncld.m:210`). `solver_nc_jointaggr` is the one that escapes:
5738 // its lG comes from `solver_nc` and its per-station factors are
5739 // SINGLE-STATION, which `pfqn_ncld` answers from a degenerate return
5740 // before it ever looks at the name. So the three are OMITTED rather
5741 // than faked or allowed to fail the whole analysis, exactly as
5742 // `-s mva -a prob` omits the detailed pair it has no counterpart for;
5743 // `ProbSysAggr` is still reported, which is what the caller asked for.
5745 const bool has_ld = line::pfqn::ncld_method_try(opt.method, ldm);
5746
5748 T pjoint = line::num_traits<T>::from_int(0);
5749 if (has_ld) {
5750 mr = line::nc::solver_nc_margaggr(sn, opt, nir,
5751 std::numeric_limits<double>::quiet_NaN());
5752 // THE DETAILED PAIR TOO, because `getProb` and `getProbSys` are not
5753 // the aggregate ones with rounding: `solver_nc_marg` and
5754 // `solver_nc_joint` carry the class-within-chain split
5755 // `lg0_i - lG0_i` that the aggregate pair sums out, so on a
5756 // multichain model they are different numbers rather than the same
5757 // one to more places.
5758 mdet = line::nc::solver_nc_marg(sn, opt, nir,
5759 std::numeric_limits<double>::quiet_NaN());
5760 pjoint = line::nc::solver_nc_joint<T>(sn, opt, nir, nullptr);
5761 }
5762 const T ps = line::nc::solver_nc_getprob_sys_aggr(sn, opt, nir);
5763
5765 const std::string am = (opt.method == "default" && !d.actualmethod.empty() &&
5766 d.actualmethod != "default")
5767 ? "default/" + d.actualmethod
5768 : d.actualmethod;
5769 if (g_json_output) {
5770 line::reg::Json p = line::reg::Json::object();
5771 p["type"] = "ProbAggr";
5772 p["indexBase"] = 0;
5773 p["ProbSysAggr"] = line::num_traits<T>::to_double(ps);
5774 line::reg::Json st = line::reg::Json::array();
5775 for (std::size_t i = 0; i < sn.nstations; ++i) st.push_back(sn.stations[i].name);
5776 p["Station"] = st;
5777 if (has_ld) {
5778 p["ProbSys"] = line::num_traits<T>::to_double(pjoint);
5779 line::reg::Json pa = line::reg::Json::array(), pm = line::reg::Json::array();
5780 for (std::size_t i = 0; i < sn.nstations; ++i) {
5781 pa.push_back(line::num_traits<T>::to_double(mr.P[i]));
5782 pm.push_back(line::num_traits<T>::to_double(mdet.P[i]));
5783 }
5784 p["ProbAggr"] = pa;
5785 p["Prob"] = pm;
5786 }
5787 emit_analysis<T>("prob", p, am);
5788 return 0;
5789 }
5790 std::printf("SolverNC arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5791 am.c_str(), line::util::method_type("NC", am).c_str());
5792 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(ps));
5793 if (!has_ld) {
5794 std::printf("ProbSys, Prob and ProbAggr need a load-dependent normalizing constant, "
5795 "which method '%s' does not compute\n",
5796 opt.method.c_str());
5797 return 0;
5798 }
5799 std::printf("ProbSys %.10g\n", line::num_traits<T>::to_double(pjoint));
5800 std::printf("%-16s %14s %14s\n", "Station", "Prob", "ProbAggr");
5801 for (std::size_t i = 0; i < sn.nstations; ++i)
5802 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(),
5805 return 0;
5806 }
5807}
5808
5809/**
5810 * `-s nc -a sysmarg`: `@@SolverNC/getProbSysMarg.m`, the JOINT law of the
5811 * per-station total queue lengths.
5812 *
5813 * NEITHER `-a prob` NOR `-a marg`, and the three are worth telling apart.
5814 * `-a prob` fixes the PER-CLASS population of every station and is a product
5815 * form; `-a marg` is this law marginalized down to ONE station; this arm is the
5816 * joint over all of them, with the classes summed out. Each value is the sum of
5817 * `-a prob` over the whole fibre of per-class tables with these row sums, and
5818 * that fibre grows combinatorially, so it is evaluated as a permanent of the
5819 * demand matrix replicated once per job (Ryser 1963) rather than enumerated.
5820 *
5821 * The whole lattice of total states is swept, so the printed column sums to one
5822 * and the sweep pays for the normalizing constant once.
5823 */
5824template <class T>
5825int solve_model_nc_sysmarg(const std::string& file, const Knobs& k) {
5826 line::qn::Network<T> net = read_model<T>(file);
5828 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5829 if (k.tol >= 0.0) opt.tol = k.tol;
5830 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5831
5832 double Ntot = 0.0;
5833 for (std::size_t r = 0; r < sn.nclasses; ++r) {
5834 const double pop = sn.classes[r].population;
5835 if (!std::isfinite(pop))
5836 throw line::UnsupportedError(line::ErrorKind{"OpenClass"},
5837 "getProbSysMarg requires a closed model: the joint law of the total queue lengths "
5838 "is not defined when a class has an infinite population");
5839 Ntot += pop;
5840 }
5841 const std::vector<std::vector<int> > states = line::pfqn::multichoose_rows(
5842 static_cast<int>(sn.nstations), static_cast<int>(std::llround(Ntot)));
5843
5844 std::vector<double> P(states.size(), 0.0);
5845 for (std::size_t j = 0; j < states.size(); ++j)
5847 line::nc::solver_nc_getprob_sys_marg(sn, opt, states[j], k.method_perm));
5848
5849 if (g_json_output) {
5850 line::reg::Json p = line::reg::Json::object();
5851 p["type"] = "ProbSysMarg";
5852 p["indexBase"] = 0;
5853 p["engine"] = k.method_perm;
5854 line::reg::Json st = line::reg::Json::array(), arr = line::reg::Json::array();
5855 for (std::size_t i = 0; i < sn.nstations; ++i) st.push_back(sn.stations[i].name);
5856 for (std::size_t j = 0; j < states.size(); ++j) {
5857 line::reg::Json e = line::reg::Json::object();
5858 line::reg::Json n = line::reg::Json::array();
5859 for (std::size_t i = 0; i < sn.nstations; ++i) n.push_back(states[j][i]);
5860 e["n"] = n;
5861 e["P"] = P[j];
5862 arr.push_back(e);
5863 }
5864 p["Station"] = st;
5865 p["states"] = arr;
5866 emit_analysis<T>("sysmarg", p, opt.method);
5867 return 0;
5868 }
5869 std::printf("SolverNC arith=%s method=%s engine=%s\n", line::num_traits<T>::name(),
5870 opt.method.c_str(), k.method_perm.c_str());
5871 for (std::size_t i = 0; i < sn.nstations; ++i)
5872 std::printf("%10s", sn.stations[i].name.c_str());
5873 std::printf(" %14s\n", "ProbSysMarg");
5874 double total = 0.0;
5875 for (std::size_t j = 0; j < states.size(); ++j) {
5876 for (std::size_t i = 0; i < sn.nstations; ++i) std::printf("%10d", states[j][i]);
5877 std::printf(" %14.10g\n", P[j]);
5878 total += P[j];
5879 }
5880 std::printf("%*s %14.10g\n", static_cast<int>(10 * sn.nstations), "sum", total);
5881 return 0;
5882}
5883
5884/**
5885 * `-s nc -a marg`: `@@SolverNC/getProbMarg.m`, the TOTAL queue-length law.
5886 *
5887 * NOT THE SAME QUANTITY AS `-s mva -a marg`, although the reference gives both
5888 * methods the same name. SolverMVA's getProbMarg is per (station, CLASS) and is
5889 * a binomial / Poisson / geometric fitted to the solver's own means; SolverNC's
5890 * is the TOTAL number of jobs at a station, summed over classes, and is exact --
5891 * a ratio of normalizing constants, obtained either from one `pfqn_procomom`
5892 * solve (`--method comom`) or by summing the aggregate marginal over the
5893 * per-class partitions of n. `--class` and `--marg-states` are therefore refused
5894 * for it rather than ignored: this law has no class argument and its support is
5895 * 0..sum(N), which the model fixes.
5896 *
5897 * Both P and log P are reported. The log is not a formatting of the other: the
5898 * enumeration forms it first and a probability that underflows to 0 in double
5899 * still has a finite log, so dropping it would lose the only number left.
5900 */
5901template <class T>
5902int solve_model_nc_marg(const std::string& file, const Knobs& k) {
5903 line::qn::Network<T> net = read_model<T>(file);
5905 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5906 if (k.tol >= 0.0) opt.tol = k.tol;
5907 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5908 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5909 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5910
5911 std::vector<std::size_t> ists; // 1-based station indices to report
5912 if (k.node) {
5913 if (k.node > sn.nof_nodes())
5914 throw line::InputError("--node " + std::to_string(k.node) +
5915 " exceeds the number of nodes in the model (" +
5916 std::to_string(sn.nof_nodes()) + ")");
5917 const std::size_t ist = sn.nodes[k.node - 1].station;
5918 if (!ist)
5919 throw line::InputError("--node " + std::to_string(k.node) + " ('" +
5920 sn.nodes[k.node - 1].name +
5921 "') is not a station, and a queue-length distribution is "
5922 "reported per station");
5923 ists.push_back(ist);
5924 } else {
5925 for (std::size_t i = 0; i < sn.nstations; ++i) ists.push_back(i + 1);
5926 }
5927
5928 // Every curve first, for solve_model_marg's reason: a station the reference
5929 // refuses must not leave a header standing above an answer that never came.
5930 std::vector<line::nc::NcQueueLengthDist<T> > curves;
5931 for (std::size_t a = 0; a < ists.size(); ++a)
5932 curves.push_back(line::nc::solver_nc_getprob_marg(sn, opt, ists[a]));
5933
5934 if (g_json_output) {
5935 line::reg::Json p = line::reg::Json::object();
5936 p["type"] = "ProbMargAggr";
5937 p["indexBase"] = 0;
5938 line::reg::Json arr = line::reg::Json::array();
5939 for (std::size_t a = 0; a < ists.size(); ++a) {
5940 line::reg::Json e = line::reg::Json::object();
5941 e["station"] = ists[a] - 1;
5942 e["Station"] = sn.stations[ists[a] - 1].name;
5943 e["P"] = vector_json<T>(curves[a].P);
5944 e["logP"] = vector_json<T>(curves[a].logP);
5945 arr.push_back(e);
5946 }
5947 p["curves"] = arr;
5948 emit_analysis<T>("marg", p, opt.method);
5949 return 0;
5950 }
5951 std::printf("SolverNC arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5952 opt.method.c_str(), line::util::method_type("NC", opt.method).c_str());
5953 for (std::size_t a = 0; a < ists.size(); ++a) {
5954 std::printf("%-16s %-8s %14s %14s\n", "Station", "n", "P", "logP");
5955 for (std::size_t n = 0; n < curves[a].P.size(); ++n)
5956 std::printf("%-16s %-8zu %14.10g %14.10g\n", sn.stations[ists[a] - 1].name.c_str(), n,
5957 line::num_traits<T>::to_double(curves[a].P[n]),
5958 line::num_traits<T>::to_double(curves[a].logP[n]));
5959 }
5960 return 0;
5961}
5962
5963/**
5964 * `-s nc -a cdf`: `@@SolverNC/getCdfRespT.m` and its aliases `getSjrnT`/`sjrnT`.
5965 *
5966 * THE WHOLE LAW, NOT ITS MEAN. `-a avg` reports E[R]; this reports F(t) per
5967 * (station, class) on one shared logarithmic grid, so a percentile or a tail
5968 * probability can be read off it. The algorithm is `pfqn_stdf` (`--method-cdf
5969 * exact`, the default) or the `pfqn_stdf_heur` reduction (`rd`), selected
5970 * through `options.config.algorithm` exactly as in the reference.
5971 *
5972 * FCFS ONLY, and the reference says so by WARNING and returning an empty
5973 * result rather than raising: the sojourn law of a processor-sharing or
5974 * infinite-server station is not the one this inversion computes. That warning
5975 * is carried through to stderr here and the analysis reports no curve, which is
5976 * distinguishable from a curve that is flat.
5977 */
5978template <class T>
5979int solve_model_nc_cdf(const std::string& file, const Knobs& k) {
5981 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
5982 "the -s nc -a cdf analysis evaluates the sojourn law on a logarithmic time grid and "
5983 "inverts a generating function; it needs transcendental arithmetic, so rerun with "
5984 "--arith double or --arith real");
5985 } else {
5986 line::qn::Network<T> net = read_model<T>(file);
5988 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5989 if (k.tol >= 0.0) opt.tol = k.tol;
5990 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5991 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5992 if (!k.cdf_algorithm.empty()) opt.cdf_algorithm = k.cdf_algorithm;
5993 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5995 if (!r.warning.empty()) std::fprintf(stderr, "warning: %s\n", r.warning.c_str());
5996
5997 if (g_json_output) {
5998 line::reg::Json p = line::reg::Json::object();
5999 p["type"] = "CdfRespT";
6000 p["indexBase"] = 0;
6001 p["algorithm"] = opt.cdf_algorithm;
6002 // ONE OBJECT PER CURVE, as on the CTMC arm: the pairs that carry a
6003 // law are a subset of the grid, so a column form would leave the
6004 // host to re-group them.
6005 line::reg::Json rd = line::reg::Json::array();
6006 for (std::size_t i = 0; i < r.RD.size(); ++i)
6007 for (std::size_t c = 0; c < r.RD[i].size(); ++c) {
6008 // An empty entry is an ABSENT law -- the station is not FCFS
6009 // or does not serve the class -- and is omitted rather than
6010 // sent as a degenerate one.
6011 if (r.RD[i][c].empty()) continue;
6012 line::reg::Json e = line::reg::Json::object();
6013 e["Station"] = sn.stations[i].name;
6014 e["JobClass"] = sn.classes[c].name;
6015 e["station"] = i;
6016 e["jobclass"] = c;
6017 line::reg::Json tt = line::reg::Json::array(), ff = line::reg::Json::array();
6018 for (std::size_t j = 0; j < r.RD[i][c].rows(); ++j) {
6019 ff.push_back(line::num_traits<T>::to_double(r.RD[i][c](j, 0)));
6020 tt.push_back(line::num_traits<T>::to_double(r.RD[i][c](j, 1)));
6021 }
6022 e["t"] = tt;
6023 e["F"] = ff;
6024 rd.push_back(e);
6025 }
6026 p["respt"] = rd;
6027 p["tset"] = vector_json(r.tset);
6028 if (!r.warning.empty()) p["warning"] = r.warning;
6029 // NO "method": the law comes from the sojourn-time inversion and not
6030 // from the normalizing-constant ladder, so the requested method name
6031 // would not be the algorithm that produced these numbers.
6032 emit_analysis<T>("cdf", p, std::string());
6033 return 0;
6034 }
6035 std::printf("SolverNC arith=%s algorithm=%s grid=%zu\n", line::num_traits<T>::name(),
6036 opt.cdf_algorithm.c_str(), r.tset.size());
6037 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
6038 for (std::size_t i = 0; i < r.RD.size(); ++i)
6039 for (std::size_t c = 0; c < r.RD[i].size(); ++c) {
6040 if (r.RD[i][c].empty()) continue;
6041 for (std::size_t j = 0; j < r.RD[i][c].rows(); ++j)
6042 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
6043 sn.classes[c].name.c_str(),
6044 line::num_traits<T>::to_double(r.RD[i][c](j, 1)),
6045 line::num_traits<T>::to_double(r.RD[i][c](j, 0)));
6046 }
6047 return 0;
6048 }
6049}
6050
6051/**
6052 * The clean-up a sensitivity table applies before printing, for a quantity that
6053 * may legitimately be NEGATIVE.
6054 *
6055 * `ln_sanitize` below tests `x <= FineTol`, which is right for a queue length or
6056 * a utilization -- every metric it was written for is nonnegative, so that test
6057 * reads as "negligible". A DERIVATIVE is not: raising a service rate lowers the
6058 * response time, the queue length and the utilization, so the whole sensitivity
6059 * table is negative by construction and the unsigned test would print an exact
6060 * zero for every one of those columns. Only the MAGNITUDE decides negligibility
6061 * here. The NC and the layered tables share it, which is why it sits above both.
6062 */
6063double sens_sanitize_signed(double x) {
6064 if (std::fabs(x) <= line::lang::GlobalConstants::FineTol) return 0.0;
6065 return x;
6066}
6067
6068/**
6069 * `-s nc -a sens`: `@@NetworkSolver/getSensitivityTable.m` under SolverNC.
6070 *
6071 * ONE ROW PER (station, class) carrying dTput/dRate, dRespT/dRate, dQLen/dRate
6072 * and dUtil/dRate, i.e. the derivative of that row's means with respect to that
6073 * row's service RATE. Two branches produce them and the banner names the one
6074 * that ran: `exact` differentiates the product-form recursion analytically
6075 * (`pfqn_sens` at chain level for a closed model, the closed-form BCMP
6076 * derivatives for an open one), `fd` re-solves rate-perturbed copies of the
6077 * model with THIS solver and forms the quotient.
6078 *
6079 * NC IS ONE OF THE TWO ENGINES THAT CAN TAKE THE EXACT BRANCH, which is what
6080 * `@@SolverNC/supportsExactSensitivity.m` returns true for, so `auto` resolves
6081 * to `exact` whenever the model is in its scope (single-server queues plus
6082 * delays, not mixed) and only falls back to differences outside it. Asking for
6083 * `--sens-method exact` outside that scope is refused by name rather than
6084 * silently downgraded: the two branches answer to different precision.
6085 *
6086 * The struct is COPIED rather than referenced because the fd branch writes a
6087 * scaled service process into it between solves; `net.get_struct()` hands out a
6088 * const reference to the model's own, which must not move under the caller.
6089 */
6090template <class T>
6091int solve_model_nc_sens(const std::string& file, const Knobs& k) {
6092 line::qn::Network<T> net = read_model<T>(file);
6094 if (!k.method.empty() && k.method != "default") opt.method = k.method;
6095 if (k.tol >= 0.0) opt.tol = k.tol;
6096 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
6097 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
6099
6101 if (!k.sens_method.empty()) so.method = k.sens_method;
6102 if (!k.sens_scheme.empty()) so.scheme = k.sens_scheme;
6103 if (k.sens_step > 0.0) so.step = k.sens_step;
6104 so.simulation = false; // the normalizing-constant path is deterministic
6105
6106 // `getAvg`, not the raw analyzer: the reference's difference quotient is
6107 // taken on the metrics the solver reports, which are the filtered ones.
6109 sn, so, /*exact_available=*/true, [&sn, &opt]() {
6112 s.Q = a.QN;
6113 s.U = a.UN;
6114 s.R = a.RN;
6115 s.Tp = a.TN;
6116 s.C = a.CN;
6117 s.X = a.XN;
6118 s.method = a.actualmethod;
6119 s.iter = a.iter;
6120 return s;
6121 });
6122
6123 if (g_json_output) {
6124 line::reg::Json p = line::reg::Json::object();
6125 p["type"] = "SensitivityTable";
6126 p["indexBase"] = 0;
6127 p["branch"] = tbl.method;
6128 line::reg::Json rows = line::reg::Json::array();
6129 for (const line::sens::SensRow<T>& r : tbl.rows) {
6130 line::reg::Json o = line::reg::Json::object();
6131 o["Station"] = r.station;
6132 o["JobClass"] = r.jobclass;
6133 o["dTput_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dTput));
6134 o["dRespT_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dRespT));
6135 o["dQLen_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dQLen));
6136 o["dUtil_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dUtil));
6137 rows.push_back(o);
6138 }
6139 p["rows"] = rows;
6140 // The branch rides in "branch" because it is not a normalizing-constant
6141 // method name. The method slot is EMPTY on the exact branch, which
6142 // differentiates the recursion in closed form and never runs a solve:
6143 // naming the NC method there would attribute the numbers to an
6144 // algorithm that did not produce them.
6145 emit_analysis<T>("sens", p, tbl.method == "fd" ? opt.method : std::string());
6146 return 0;
6147 }
6148
6149 std::printf("SolverNC arith=%s branch=%s method=%s rows=%zu\n", line::num_traits<T>::name(),
6150 tbl.method.c_str(), tbl.method == "fd" ? opt.method.c_str() : "-",
6151 tbl.rows.size());
6152 std::printf("%-16s %-14s %14s %14s %14s %14s\n", "Station", "JobClass", "dTput_dRate",
6153 "dRespT_dRate", "dQLen_dRate", "dUtil_dRate");
6154 for (const line::sens::SensRow<T>& r : tbl.rows)
6155 std::printf("%-16s %-14s %14.6g %14.6g %14.6g %14.6g\n", r.station.c_str(),
6156 r.jobclass.c_str(),
6157 sens_sanitize_signed(line::num_traits<T>::to_double(r.dTput)),
6158 sens_sanitize_signed(line::num_traits<T>::to_double(r.dRespT)),
6159 sens_sanitize_signed(line::num_traits<T>::to_double(r.dQLen)),
6160 sens_sanitize_signed(line::num_traits<T>::to_double(r.dUtil)));
6161 return 0;
6162}
6163
6164/** The @@Solver method each `-a` stands for, which is what the chooser keys on. */
6165// ===================== SolverLDES, the simulator ==========================
6166
6167/**
6168 * The knobs one `-s ldes` invocation resolves to.
6169 *
6170 * `--samples` is the SERVICE-COMPLETION budget the engine stops at, `--seed` the
6171 * stream, and both are part of the answer rather than of the invocation, which is
6172 * why the banner carries them. Everything else is `--ldes-*`.
6173 *
6174 * A `--ldes-initsol` placement is NOT accompanied by a forced `fixed` warmup
6175 * filter here, deliberately: `initFromSolver` sets `tranfilter='fixed'` with
6176 * `warmupfrac=0` because the placement it computes IS a steady state, while a
6177 * placement handed in on the command line may equally be the start of a
6178 * transient. Pass `--ldes-tranfilter fixed --ldes-warmupfrac 0` alongside it to
6179 * reproduce `initFromSolver` exactly.
6180 */
6181inline line::ldes::LdesOptions ldes_options(const Knobs& k) {
6183 if (k.samples) o.samples = k.samples;
6184 if (k.seed) o.seed = static_cast<long>(k.seed);
6185 if (!k.method.empty()) o.method = k.method;
6186 if (!k.ldes_tranfilter.empty()) o.tranfilter = k.ldes_tranfilter;
6187 if (k.ldes_warmupfrac >= 0.0) o.warmupfrac = k.ldes_warmupfrac;
6188 if (!k.ldes_cimethod.empty()) o.cimethod = k.ldes_cimethod;
6189 if (k.ldes_cnvgon) o.cnvgon = true;
6190 if (k.ldes_cnvgtol > 0.0) o.cnvgtol = k.ldes_cnvgtol;
6191 if (k.ldes_slotted) o.slotted = true;
6192 if (k.ldes_slotlength > 0.0) {
6193 o.slotted = true;
6194 o.slot_length = k.ldes_slotlength;
6195 }
6196 if (k.ldes_replications > 0) o.replications = k.ldes_replications;
6197 if (k.ldes_numthreads > 0) o.numthreads = k.ldes_numthreads;
6198 if (k.ldes_maxtime > 0.0) o.timeout = k.ldes_maxtime;
6199 if (!k.ldes_initsol.empty()) o.init_sol = k.ldes_initsol;
6200 if (!k.ldes_rest_url.empty()) o.rest_url = k.ldes_rest_url;
6201 o.verbose = k.verbose;
6202 return o;
6203}
6204
6205/**
6206 * The model.json text the engine is handed.
6207 *
6208 * FORWARDED BYTE FOR BYTE, and never through `read_model`: the reader is scoped
6209 * to the subset the analytical solvers need, and a round trip through it would
6210 * degrade exactly the models LDES exists for. `-a reward` is the one arm that
6211 * also parses the document, because a reward DECLARATION is what it needs.
6212 */
6213inline std::string ldes_document(const std::string& file) {
6214 return file.empty() ? stdin_model_text() : line::ldes::detail::read_file(file);
6215}
6216
6217/** A reported entry, or 0 where the engine reported no such row. */
6218inline double ldes_at(const line::Matrix<double>& M, std::size_t i, std::size_t j) {
6219 return i < M.rows() && j < M.cols() ? M(i, j) : 0.0;
6220}
6221
6222/**
6223 * One LDES run.
6224 *
6225 * A HARD TIMEOUT IS A REFUSAL HERE, not an empty result. The two other clients
6226 * return an empty result flagged `timedOut` and warn, which suits a caller that
6227 * can inspect the flag; a CLI's caller reads a table, and a table of zeros that
6228 * means "the run was killed" is the silent-wrong-number outcome this CLI refuses
6229 * everywhere else.
6230 */
6231inline line::ldes::LdesResult ldes_run(const std::string& file,
6232 const line::ldes::LdesOptions& o,
6233 const std::vector<std::string>& extra) {
6234 const line::ldes::LdesResult r = line::ldes::solver_ldes_text(ldes_document(file), o, extra);
6235 if (r.timed_out)
6237 "SolverLDES exceeded its wall-clock budget (--ldes-maxtime) and was terminated before "
6238 "it wrote a result; raise the budget or lower --samples");
6239 if (r.station_names.empty())
6241 "SolverLDES: the engine reported no station names, so its metrics cannot be labelled; "
6242 "the run produced no result document");
6243 return r;
6244}
6245
6246/**
6247 * The provenance line every LDES arm prints first.
6248 *
6249 * `engine=` is not decoration: the AOT native image and the jar are two builds of
6250 * one engine and the first can lag the sources, so a number quoted from an LDES
6251 * run has to say which produced it. `stopping=` is the reason the run ended --
6252 * a `max_events` stop at a low `--samples` is a wide confidence interval and a
6253 * `max_time` one is a truncated run, and neither is visible in the means.
6254 */
6255inline void ldes_banner(const line::ldes::LdesResult& r, const line::ldes::LdesOptions& o) {
6256 std::printf("SolverLDES arith=double method=%s type=%s engine=%s samples=%zu seed=%ld "
6257 "time=%.6g events=%lld stopping=%s\n",
6258 r.method.c_str(), line::util::method_type("LDES", r.method).c_str(),
6259 r.engine.c_str(), o.events ? o.events : o.samples, o.seed, r.runtime,
6261}
6262
6263/** The envelope keys that qualify an LDES solve as a whole. */
6264inline line::reg::Json ldes_envelope(const line::ldes::LdesResult& r,
6265 const line::ldes::LdesOptions& o) {
6266 line::reg::Json e = line::reg::Json::object();
6267 e["engine"] = r.engine;
6268 e["samples"] = o.events ? o.events : o.samples;
6269 e["seed"] = o.seed;
6270 e["converged"] = r.converged;
6271 e["stoppingReason"] = r.stopping_reason;
6272 e["totalSimulatedEvents"] = r.total_simulated_events;
6273 e["runtime"] = r.runtime;
6274 return e;
6275}
6276
6277/**
6278 * `-s ldes -a avg`: the steady-state table, the engine's `getAvg`.
6279 *
6280 * THE FINITE-CAPACITY-REGION ROWS DO NOT JOIN THE STATION TABLE, unlike MATLAB's
6281 * `getAvgTable`, which appends them after the stations. A region is not a station
6282 * and the "Station" column of this CLI's table is read by a parity harness that
6283 * pairs rows with another codebase's stations; a region row there would pair with
6284 * nothing. They are printed as their own table and carried under `avg.fcr`, which
6285 * is the same information without the collision.
6286 */
6287int solve_model_ldes_avg(const std::string& file, const Knobs& k) {
6288 const line::ldes::LdesOptions o = ldes_options(k);
6289 const line::ldes::LdesResult r = ldes_run(file, o, std::vector<std::string>());
6290 if (!g_json_output) ldes_banner(r, o);
6291
6292 line::reg::Json extra = line::reg::Json::object();
6293 // The confidence intervals are the half-widths the engine reports, one per
6294 // metric; a simulation that quoted a mean without them would be quoting a
6295 // point estimate as if it were exact.
6296 line::reg::Json ci = line::reg::Json::object();
6297 if (!r.QNCI.empty()) ci["QNCI"] = matrix_json<double>(r.QNCI);
6298 if (!r.UNCI.empty()) ci["UNCI"] = matrix_json<double>(r.UNCI);
6299 if (!r.RNCI.empty()) ci["RNCI"] = matrix_json<double>(r.RNCI);
6300 if (!r.TNCI.empty()) ci["TNCI"] = matrix_json<double>(r.TNCI);
6301 if (!r.ANCI.empty()) ci["ANCI"] = matrix_json<double>(r.ANCI);
6302 if (!r.WNCI.empty()) ci["WNCI"] = matrix_json<double>(r.WNCI);
6303 if (!ci.empty()) extra["CI"] = ci;
6304 if (!r.QNfcr.empty()) {
6305 line::reg::Json f = line::reg::Json::object();
6306 f["nregions"] = r.nregions;
6307 f["QNfcr"] = matrix_json<double>(r.QNfcr);
6308 f["RNfcr"] = matrix_json<double>(r.RNfcr);
6309 f["TNfcr"] = matrix_json<double>(r.TNfcr);
6310 f["WNfcr"] = matrix_json<double>(r.WNfcr);
6311 if (!r.WeightNfcr.empty()) f["WeightNfcr"] = matrix_json<double>(r.WeightNfcr);
6312 if (!r.MemOccNfcr.empty()) f["MemOccNfcr"] = matrix_json<double>(r.MemOccNfcr);
6313 if (!r.DropRateNfcr.empty()) f["DropRateNfcr"] = matrix_json<double>(r.DropRateNfcr);
6314 extra["fcr"] = f;
6315 }
6316 if (!r.DropRateJoin.empty()) extra["DropRateJoin"] = matrix_json<double>(r.DropRateJoin);
6317 if (!r.fj_forks.empty()) {
6318 const line::reg::Json fjb =
6319 fj_extra_json(r.fj_forks, r.fj_joins, r.class_names, r.FJQLen, r.FJRespT, r.FJTput);
6320 if (!fjb.is_null()) extra["FJ"] = fjb;
6321 }
6322 if (!r.cache_metrics.empty()) {
6323 line::reg::Json cm = line::reg::Json::object();
6324 for (std::map<std::string, line::ldes::LdesCacheMetrics>::const_iterator it =
6325 r.cache_metrics.begin();
6326 it != r.cache_metrics.end(); ++it) {
6327 line::reg::Json c = line::reg::Json::object();
6328 if (!it->second.hit.empty()) c["hit"] = matrix_json<double>(it->second.hit);
6329 if (!it->second.delayed.empty()) c["delayed"] = matrix_json<double>(it->second.delayed);
6330 if (!it->second.miss.empty()) c["miss"] = matrix_json<double>(it->second.miss);
6331 if (!it->second.latency.empty()) c["latency"] = matrix_json<double>(it->second.latency);
6332 if (!it->second.hitList.empty()) c["hitList"] = matrix_json<double>(it->second.hitList);
6333 if (!it->second.itemProb.empty())
6334 c["itemProb"] = matrix_json<double>(it->second.itemProb);
6335 if (!it->second.listCost.empty())
6336 c["listCost"] = matrix_json<double>(it->second.listCost);
6337 cm[it->first] = c;
6338 }
6339 extra["cacheMetrics"] = cm;
6340 }
6341
6342 // THE RESIDENCE TIME IS DERIVED HERE, not taken from the engine. The engine
6343 // reports WN = RN because it counts one visit per station, which is only
6344 // true when every visit ratio is 1; `getAvg.m:204` therefore discards the
6345 // WN a solver returned and recomputes `sn_get_residt_from_respt(sn, RN)`,
6346 // and every other C++ solver already routes through the same helper. On
6347 // cqn_repairmen, whose Queue1 is visited 0.3 times per cycle, the engine's
6348 // WN came out 11.8136 against the reference's 3.5205 -- the response time
6349 // reported as if the station were visited once.
6350 //
6351 // MATCHED BY NAME. The engine's station order is its own; a Cache or a
6352 // Source can sit at a different index in the struct, and pairing the two
6353 // off positionally would scale one station's time by another's visits.
6354 line::Matrix<double> WNd(r.station_names.size(), r.class_names.size(), 0.0);
6355 {
6356 line::qn::Network<double> net = read_model<double>(file);
6358 std::vector<std::size_t> st_of(r.station_names.size(), 0); // 1-based, 0 = unmatched
6359 for (std::size_t i = 0; i < r.station_names.size(); ++i)
6360 for (std::size_t j = 0; j < sn.nstations; ++j)
6361 if (sn.stations[j].name == r.station_names[i]) { st_of[i] = j + 1; break; }
6362 std::vector<std::size_t> cl_of(r.class_names.size(), 0);
6363 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6364 for (std::size_t k = 0; k < sn.nclasses; ++k)
6365 if (sn.classes[k].name == r.class_names[c]) { cl_of[c] = k + 1; break; }
6366 line::Matrix<double> RNs(sn.nstations, sn.nclasses, 0.0);
6367 for (std::size_t i = 0; i < r.station_names.size(); ++i)
6368 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6369 if (st_of[i] && cl_of[c]) RNs(st_of[i] - 1, cl_of[c] - 1) = ldes_at(r.RN, i, c);
6371 for (std::size_t i = 0; i < r.station_names.size(); ++i)
6372 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6373 WNd(i, c) = (st_of[i] && cl_of[c]) ? WNs(st_of[i] - 1, cl_of[c] - 1)
6374 : ldes_at(r.WN, i, c);
6375 }
6376
6377 emit_avg_table_named(r.station_names, r.class_names, "double", r.method,
6378 [&](std::size_t i, std::size_t c) {
6379 AvgRow v;
6380 v.q = ldes_at(r.QN, i, c);
6381 v.u = ldes_at(r.UN, i, c);
6382 v.r = ldes_at(r.RN, i, c);
6383 v.w = WNd(i, c);
6384 v.a = ldes_at(r.AN, i, c);
6385 v.t = ldes_at(r.TN, i, c);
6386 return v;
6387 },
6388 extra, ldes_envelope(r, o));
6389
6390 if (!g_json_output && !r.QNfcr.empty()) {
6391 std::printf("%-16s %-14s %12s %12s %12s %12s %12s\n", "Region", "JobClass", "QLen", "RespT",
6392 "Tput", "Weight", "MemOcc");
6393 for (std::size_t i = 0; i < r.QNfcr.rows(); ++i)
6394 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6395 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g\n",
6396 ("Region" + std::to_string(i + 1)).c_str(), r.class_names[c].c_str(),
6397 ldes_at(r.QNfcr, i, c), ldes_at(r.RNfcr, i, c),
6398 ldes_at(r.TNfcr, i, c), ldes_at(r.WeightNfcr, i, c),
6399 ldes_at(r.MemOccNfcr, i, c));
6400 }
6401 if (!g_json_output && !r.cache_metrics.empty()) {
6402 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Cache", "JobClass", "Hit", "Delayed",
6403 "Miss", "Latency");
6404 for (std::map<std::string, line::ldes::LdesCacheMetrics>::const_iterator it =
6405 r.cache_metrics.begin();
6406 it != r.cache_metrics.end(); ++it)
6407 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6408 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", it->first.c_str(),
6409 r.class_names[c].c_str(), ldes_at(it->second.hit, 0, c),
6410 ldes_at(it->second.delayed, 0, c), ldes_at(it->second.miss, 0, c),
6411 ldes_at(it->second.latency, 0, c));
6412 }
6413 return 0;
6414}
6415
6416/**
6417 * `-s ldes -a tran`: `getTranAvg`, the per-bucket QNt / UNt / TNt series over
6418 * `--tspan`.
6419 *
6420 * THE HORIZON IS REQUIRED. `options.timespan` is what turns the engine's run into
6421 * a transient one, and there is no default: a trajectory over an unstated horizon
6422 * is not a quantity. A SINGLE PATH IS NOT E[N](t) either -- there is no time
6423 * ergodicity at fixed t -- so `--ldes-replications` is how an ensemble mean is
6424 * asked for, exactly as `runAnalyzer.m` passes `--replications` for the same
6425 * reason.
6426 *
6427 * The series are indexed by STATION, as `LDESResultIO` writes them
6428 * (`result.QNt = new Matrix[numStations][numClasses]`).
6429 */
6430int solve_model_ldes_tran(const std::string& file, const Knobs& k) {
6431 line::ldes::LdesOptions o = ldes_options(k);
6432 o.has_timespan = true;
6433 o.t0 = k.t0;
6434 o.t1 = k.t1;
6435 std::vector<std::string> extra;
6436 extra.push_back("--trajectory");
6437 const line::ldes::LdesResult r = ldes_run(file, o, extra);
6438 if (r.t.empty() || r.QNt.empty())
6440 "SolverLDES -a tran produced no trajectory: the engine ran but recorded no bucket over "
6441 "[" + line::ldes::detail::shortest(k.t0) + "," +
6442 line::ldes::detail::shortest(k.t1) + "]");
6443
6444 if (g_json_output) {
6445 line::reg::Json p = line::reg::Json::object();
6446 p["type"] = "TranAvgTable";
6447 p["indexBase"] = 0;
6448 p["t0"] = k.t0;
6449 p["t1"] = k.t1;
6450 line::reg::Json curves = line::reg::Json::array();
6451 for (std::size_t i = 0; i < r.QNt.size(); ++i)
6452 for (std::size_t c = 0; c < r.QNt[i].size(); ++c) {
6453 // An empty series is an ABSENT one -- the class does not visit
6454 // the station -- and is omitted rather than sent as a flat zero.
6455 if (r.QNt[i][c].empty()) continue;
6456 line::reg::Json e = line::reg::Json::object();
6457 e["Station"] = i < r.station_names.size() ? r.station_names[i]
6458 : "Station" + std::to_string(i);
6459 e["JobClass"] =
6460 c < r.class_names.size() ? r.class_names[c] : "Class" + std::to_string(c);
6461 e["station"] = i;
6462 e["jobclass"] = c;
6463 line::reg::Json tt = line::reg::Json::array(), q = line::reg::Json::array(),
6464 u = line::reg::Json::array(), x = line::reg::Json::array();
6465 for (std::size_t j = 0; j < r.QNt[i][c].rows(); ++j) {
6466 tt.push_back(r.QNt[i][c](j, 1));
6467 q.push_back(r.QNt[i][c](j, 0));
6468 }
6469 if (i < r.UNt.size() && c < r.UNt[i].size())
6470 for (std::size_t j = 0; j < r.UNt[i][c].rows(); ++j)
6471 u.push_back(r.UNt[i][c](j, 0));
6472 if (i < r.TNt.size() && c < r.TNt[i].size())
6473 for (std::size_t j = 0; j < r.TNt[i][c].rows(); ++j)
6474 x.push_back(r.TNt[i][c](j, 0));
6475 e["t"] = tt;
6476 e["QLen"] = q;
6477 e["Util"] = u;
6478 e["Tput"] = x;
6479 curves.push_back(e);
6480 }
6481 p["curves"] = curves;
6482 p["tset"] = vector_json(r.t);
6483 // The envelope is built ONCE into a local: `begin()` and `end()` taken
6484 // from two different temporaries are iterators into two different
6485 // objects, which is undefined behaviour and not a style point.
6486 const line::reg::Json env = ldes_envelope(r, o);
6487 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
6488 p[it.key()] = it.value();
6489 emit_analysis<double>("tran", p, r.method);
6490 return 0;
6491 }
6492 ldes_banner(r, o);
6493 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
6494 "Tput");
6495 for (std::size_t i = 0; i < r.QNt.size(); ++i)
6496 for (std::size_t c = 0; c < r.QNt[i].size(); ++c) {
6497 if (r.QNt[i][c].empty()) continue;
6498 for (std::size_t j = 0; j < r.QNt[i][c].rows(); ++j) {
6499 const bool hu = i < r.UNt.size() && c < r.UNt[i].size() &&
6500 j < r.UNt[i][c].rows();
6501 const bool hx = i < r.TNt.size() && c < r.TNt[i].size() &&
6502 j < r.TNt[i][c].rows();
6503 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
6504 (i < r.station_names.size() ? r.station_names[i].c_str() : "?"),
6505 (c < r.class_names.size() ? r.class_names[c].c_str() : "?"),
6506 r.QNt[i][c](j, 1), r.QNt[i][c](j, 0),
6507 hu ? r.UNt[i][c](j, 0) : 0.0, hx ? r.TNt[i][c](j, 0) : 0.0);
6508 }
6509 }
6510 return 0;
6511}
6512
6513/**
6514 * `-s ldes -a cdf`: `getCdfRespT`, the EMPIRICAL response-time law.
6515 *
6516 * A SIMULATOR MUST REPORT WHAT IT OBSERVED. The base solver's fallback fabricates
6517 * an exponential law with the right mean, which says nothing about the tail; the
6518 * engine records every per-job response time under `--respt-samples`, and the
6519 * curve here is the ecdf of those samples with repeated observations collapsed to
6520 * their largest F, exactly as `@@SolverLDES/getCdfRespT.m` builds it.
6521 *
6522 * `--respt-samples` POSTDATES the prebuilt AOT image, which is why the runner
6523 * order flips for it (see `ldes_runners`): on that image the flag is accepted and
6524 * ignored, and the arm would refuse for want of samples that were never asked for.
6525 */
6526int solve_model_ldes_cdf(const std::string& file, const Knobs& k, const char* key,
6527 const char* type) {
6528 const line::ldes::LdesOptions o = ldes_options(k);
6529 std::vector<std::string> extra;
6530 extra.push_back("--respt-samples");
6531 const line::ldes::LdesResult r = ldes_run(file, o, extra);
6532 if (r.respTimeSamples.empty())
6534 "SolverLDES -a cdf needs the per-job response times the engine records under "
6535 "--respt-samples and the run returned none; raise --samples so completions are "
6536 "observed at all");
6537
6538 // The ecdf of each (station, class): sorted observations, F = i/n, and one
6539 // pair per DISTINCT value carrying the largest F at it.
6540 std::vector<std::vector<std::vector<double>>> tt(r.respTimeSamples.size()), ff(
6541 r.respTimeSamples.size());
6542 for (std::size_t i = 0; i < r.respTimeSamples.size(); ++i) {
6543 tt[i].resize(r.respTimeSamples[i].size());
6544 ff[i].resize(r.respTimeSamples[i].size());
6545 for (std::size_t c = 0; c < r.respTimeSamples[i].size(); ++c) {
6546 std::vector<double> x = r.respTimeSamples[i][c];
6547 if (x.empty()) continue;
6548 std::sort(x.begin(), x.end());
6549 const double n = static_cast<double>(x.size());
6550 for (std::size_t j = 0; j < x.size(); ++j) {
6551 if (j + 1 < x.size() && x[j + 1] == x[j]) continue;
6552 tt[i][c].push_back(x[j]);
6553 ff[i][c].push_back(static_cast<double>(j + 1) / n);
6554 }
6555 }
6556 }
6557
6558 if (g_json_output) {
6559 line::reg::Json p = line::reg::Json::object();
6560 p["type"] = type;
6561 p["indexBase"] = 0;
6562 p["algorithm"] = "empirical";
6563 line::reg::Json rd = line::reg::Json::array();
6564 for (std::size_t i = 0; i < tt.size(); ++i)
6565 for (std::size_t c = 0; c < tt[i].size(); ++c) {
6566 if (tt[i][c].empty()) continue;
6567 line::reg::Json e = line::reg::Json::object();
6568 e["Station"] = i < r.station_names.size() ? r.station_names[i]
6569 : "Station" + std::to_string(i);
6570 e["JobClass"] =
6571 c < r.class_names.size() ? r.class_names[c] : "Class" + std::to_string(c);
6572 e["station"] = i;
6573 e["jobclass"] = c;
6574 e["t"] = vector_json(tt[i][c]);
6575 e["F"] = vector_json(ff[i][c]);
6576 e["samples"] = r.respTimeSamples[i][c].size();
6577 rd.push_back(e);
6578 }
6579 p["respt"] = rd;
6580 emit_analysis<double>(key, p, std::string());
6581 return 0;
6582 }
6583 ldes_banner(r, o);
6584 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
6585 for (std::size_t i = 0; i < tt.size(); ++i)
6586 for (std::size_t c = 0; c < tt[i].size(); ++c)
6587 for (std::size_t j = 0; j < tt[i][c].size(); ++j)
6588 std::printf("%-16s %-14s %14.8g %14.10g\n",
6589 (i < r.station_names.size() ? r.station_names[i].c_str() : "?"),
6590 (c < r.class_names.size() ? r.class_names[c].c_str() : "?"),
6591 tt[i][c][j], ff[i][c][j]);
6592 return 0;
6593}
6594
6595/**
6596 * `-s ldes -a sample`: `sampleSys` / `sampleSysAggr`, one simulated trajectory.
6597 *
6598 * The horizon is `[0, --samples]`, which is `runTransientJson`'s: the event budget
6599 * doubles as the transient horizon there because the engine ignores the budget in
6600 * transient mode, so the number the caller gave has to name the horizon or name
6601 * nothing. `--tspan` names a horizon in its own right and belongs to `-a tran`.
6602 *
6603 * The state is the per-class queue length at each station, which is why there is
6604 * no separate `sampleSysAggr` column: an LDES trajectory is ALREADY per class, so
6605 * the aggregate view is the row sum and the reference's two getters return the
6606 * same data with one flag flipped.
6607 */
6608int solve_model_ldes_sample(const std::string& file, const Knobs& k) {
6609 line::ldes::LdesOptions o = ldes_options(k);
6610 o.has_timespan = true;
6611 o.t0 = 0.0;
6612 o.t1 = static_cast<double>(o.events ? o.events : o.samples);
6613 std::vector<std::string> extra;
6614 extra.push_back("--trajectory");
6615 const line::ldes::LdesResult r = ldes_run(file, o, extra);
6616 if (r.t.empty() || r.QNt.empty())
6618 "SolverLDES -a sample produced no trajectory over [0," +
6619 line::ldes::detail::shortest(o.t1) + "]");
6620
6621 const std::size_t M = r.QNt.size(), K = r.class_names.size(), n = r.t.size();
6622 if (g_json_output) {
6623 line::reg::Json p = line::reg::Json::object();
6624 p["type"] = "SamplePath";
6625 p["indexBase"] = 0;
6626 p["scope"] = "(system)";
6627 p["drawn"] = n;
6628 p["t"] = vector_json(r.t);
6629 line::reg::Json st = line::reg::Json::array();
6630 for (std::size_t j = 0; j < n; ++j) {
6631 line::reg::Json row = line::reg::Json::array();
6632 for (std::size_t i = 0; i < M; ++i)
6633 for (std::size_t c = 0; c < K; ++c)
6634 row.push_back(c < r.QNt[i].size() && j < r.QNt[i][c].rows()
6635 ? r.QNt[i][c](j, 0)
6636 : 0.0);
6637 st.push_back(row);
6638 }
6639 p["state"] = st;
6640 line::reg::Json cols = line::reg::Json::array();
6641 for (std::size_t i = 0; i < M; ++i)
6642 for (std::size_t c = 0; c < K; ++c)
6643 cols.push_back((i < r.station_names.size() ? r.station_names[i] : "?") + "," +
6644 (c < K ? r.class_names[c] : "?"));
6645 p["columns"] = cols;
6646 // The envelope is built ONCE into a local: `begin()` and `end()` taken
6647 // from two different temporaries are iterators into two different
6648 // objects, which is undefined behaviour and not a style point.
6649 const line::reg::Json env = ldes_envelope(r, o);
6650 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
6651 p[it.key()] = it.value();
6652 emit_analysis<double>("sample", p, r.method);
6653 return 0;
6654 }
6655 ldes_banner(r, o);
6656 std::printf("%14s %s\n", "Time", "SysState (station-major, per class)");
6657 for (std::size_t j = 0; j < n; ++j) {
6658 std::printf("%14.8g ", r.t[j]);
6659 for (std::size_t i = 0; i < M; ++i)
6660 for (std::size_t c = 0; c < K; ++c)
6661 std::printf(" %g", c < r.QNt[i].size() && j < r.QNt[i][c].rows()
6662 ? r.QNt[i][c](j, 0)
6663 : 0.0);
6664 std::printf("\n");
6665 }
6666 return 0;
6667}
6668
6669/**
6670 * `-s ldes -a reward`: `getAvgReward`, E[r] on the EXACT joint-state histogram.
6671 *
6672 * The engine exports, under `--export-histogram`, the residence time of every
6673 * joint state it visited, in the aggregate layout `ctmc_state_space_aggr` builds.
6674 * The rewards are then evaluated here, state by state, so E[r] = sum_s (t_s /
6675 * sum t) r(state_s) is correct for a NONLINEAR reward too -- which is the whole
6676 * point of the histogram over the means: E[n^2] cannot be recovered from E[n].
6677 *
6678 * THIS IS THE ONE ARM THAT ALSO PARSES THE DOCUMENT, because a reward is a
6679 * DECLARATION and the declarations live in the model, not in the result. A model
6680 * outside this port's reader therefore reaches every other LDES arm and not this
6681 * one, and says so by the reader's own refusal.
6682 */
6683int solve_model_ldes_reward(const std::string& file, const Knobs& k) {
6684 line::qn::Network<double> net = read_model<double>(file);
6686 if (sn.reward.empty())
6687 throw line::InputError(
6688 "-s ldes -a reward needs a reward declared on the model (set_reward(name, fn), the "
6689 "`rewards` block of model.json); there is nothing to average");
6690
6691 const line::ldes::LdesOptions o = ldes_options(k);
6692 std::vector<std::string> extra;
6693 extra.push_back("--export-histogram");
6694 const line::ldes::LdesResult r = ldes_run(file, o, extra);
6697 "SolverLDES -a reward needs the joint-state residence-time histogram the engine "
6698 "exports under --export-histogram and the run returned none");
6699
6700 double total = 0.0;
6701 for (std::size_t s = 0; s < r.histogram_time.rows(); ++s)
6702 for (std::size_t c = 0; c < r.histogram_time.cols(); ++c) total += r.histogram_time(s, c);
6703 if (!(total > 0.0))
6704 throw line::NumericError(line::ErrorKind{"Degenerate"},
6705 "SolverLDES -a reward: the state histogram carries no residence time, so no state "
6706 "distribution can be formed from it");
6707
6708 const std::size_t ns = r.histogram_space.rows(), w = r.histogram_space.cols();
6709 std::vector<double> E(sn.reward.size(), 0.0);
6710 std::vector<std::string> names(sn.reward.size());
6711 for (std::size_t l = 0; l < sn.reward.size(); ++l) {
6712 names[l] = sn.reward[l].name;
6713 for (std::size_t s = 0; s < ns; ++s) {
6714 std::vector<double> row(w);
6715 for (std::size_t c = 0; c < w; ++c) row[c] = r.histogram_space(s, c);
6716 const double t = s < r.histogram_time.rows() && r.histogram_time.cols() > 0
6717 ? r.histogram_time(s, 0)
6718 : 0.0;
6719 E[l] += (t / total) * sn.reward[l].fn(row);
6720 }
6721 }
6722
6723 if (g_json_output) {
6724 line::reg::Json p = line::reg::Json::object();
6725 p["type"] = "AvgReward";
6726 line::reg::Json nm = line::reg::Json::array();
6727 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
6728 p["Reward"] = nm;
6729 p["E"] = vector_json(E);
6730 p["states"] = ns;
6731 // The envelope is built ONCE into a local: `begin()` and `end()` taken
6732 // from two different temporaries are iterators into two different
6733 // objects, which is undefined behaviour and not a style point.
6734 const line::reg::Json env = ldes_envelope(r, o);
6735 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
6736 p[it.key()] = it.value();
6737 // NO "method": these are expectations over an empirical distribution and
6738 // no chain was solved, so there is no resolved method to report.
6739 emit_analysis<double>("reward", p, std::string());
6740 return 0;
6741 }
6742 ldes_banner(r, o);
6743 std::printf("%-28s %16s\n", "Reward", "E[r]");
6744 for (std::size_t l = 0; l < E.size(); ++l)
6745 std::printf("%-28s %16.10g\n", names[l].c_str(), E[l]);
6746 return 0;
6747}
6748
6749/**
6750 * `-s ldes -a prob`: `getProb`, `getProbAggr`, `getProbSys` and `getProbSysAggr`,
6751 * all four read off the joint-state histogram of ONE run, as
6752 * `@@SolverLDES/getProb*.m` does through `ldesHistProb`.
6753 *
6754 * THE STATE IS THE MODEL'S DECLARED ONE (`api::sn_declared_marginal`, the same
6755 * per-class counts `-s jmt -a prob` weighs against), and `--node` with `--state`
6756 * overrides one station's counts, which is `getProbAggr(node, state)`'s second
6757 * argument. THE DETAILED AND THE AGGREGATE QUERIES COINCIDE HERE, and that is the
6758 * reference's definition rather than a shortcut: the histogram records per-class
6759 * counts per station and nothing about buffer order or phase, so MATLAB's
6760 * `getProb` marginalizes its state with `State.toMarginal` before matching, and
6761 * `getProbSysAggr` is literally `getProbSys`. Each probability is the residence
6762 * time of the matching histogram rows over the total, and `seen` says whether any
6763 * row matched at all, since on a simulation a zero is usually the run length.
6764 *
6765 * A Source holds no jobs, so it takes no part in the system match (its histogram
6766 * columns are zero, as the reference's own match would find them).
6767 */
6768int solve_model_ldes_prob(const std::string& file, const Knobs& k) {
6769 line::qn::Network<double> net = read_model<double>(file);
6771 const std::size_t M = sn.nstations, R = sn.nclasses;
6772
6774 if (!k.state.empty()) {
6775 std::size_t ist = 0;
6776 if (k.node && k.node <= sn.nodes.size()) ist = sn.nodes[k.node - 1].station;
6777 if (ist == 0)
6778 throw line::InputError("--node " + std::to_string(k.node) +
6779 " is not a station; the LDES state histogram records station "
6780 "queue lengths only");
6781 if (k.state.size() != R)
6782 throw line::InputError("-s ldes -a prob takes --state as one job count per class (" +
6783 std::to_string(R) + "), got " + std::to_string(k.state.size()));
6784 for (std::size_t r = 0; r < R; ++r) nir(ist - 1, r) = static_cast<double>(k.state[r]);
6785 }
6786
6787 const line::ldes::LdesOptions o = ldes_options(k);
6788 std::vector<std::string> extra;
6789 extra.push_back("--export-histogram");
6790 const line::ldes::LdesResult res = ldes_run(file, o, extra);
6791 const line::Matrix<double>& space = res.histogram_space;
6792 const line::Matrix<double>& tm = res.histogram_time;
6793 if (space.empty() || tm.empty())
6795 "SolverLDES -a prob needs the joint-state residence-time histogram the engine exports "
6796 "under --export-histogram and the run returned none");
6797 if (space.cols() < M * R)
6798 throw line::ExternalError(line::ErrorKind{"LDES"}, "SolverLDES: the state histogram holds " +
6799 std::to_string(space.cols()) + " columns, fewer than the " +
6800 std::to_string(M * R) + " of " + std::to_string(M) +
6801 " stations by " + std::to_string(R) + " classes");
6802
6803 std::vector<double> dwell(space.rows(), 0.0);
6804 double total = 0.0;
6805 for (std::size_t s = 0; s < space.rows() && s < tm.rows(); ++s)
6806 for (std::size_t c = 0; c < tm.cols(); ++c) dwell[s] += tm(s, c);
6807 for (double d : dwell) total += d;
6808 if (!(total > 0.0))
6809 throw line::NumericError(line::ErrorKind{"Degenerate"},
6810 "SolverLDES -a prob: the state histogram carries no residence time, so no state "
6811 "probability can be formed from it");
6812
6813 // ldesHistProb's match: |space - target| < 1e-9 on the station's R columns.
6814 auto matches = [&](std::size_t s, std::size_t i) {
6815 for (std::size_t r = 0; r < R; ++r)
6816 if (!(std::abs(space(s, i * R + r) - nir(i, r)) < 1e-9)) return false;
6817 return true;
6818 };
6819 std::vector<double> prob(M, 0.0);
6820 std::vector<bool> seen(M, false);
6821 double psys = 0.0;
6822 bool sys_seen = false;
6823 for (std::size_t s = 0; s < space.rows(); ++s) {
6824 bool all = true;
6825 for (std::size_t i = 0; i < M; ++i) {
6826 const bool src = sn.nodes[sn.station_to_node[i] - 1].nodetype == line::lang::NodeType::Source;
6827 if (matches(s, i)) {
6828 prob[i] += dwell[s];
6829 seen[i] = true;
6830 } else if (!src) {
6831 all = false;
6832 }
6833 }
6834 if (all) {
6835 psys += dwell[s];
6836 sys_seen = true;
6837 }
6838 }
6839 for (double& p : prob) p /= total;
6840 psys /= total;
6841
6842 if (g_json_output) {
6843 line::reg::Json p = line::reg::Json::object();
6844 p["type"] = "ProbAggr";
6845 p["indexBase"] = 0;
6846 p["ProbSys"] = psys;
6847 p["ProbSysAggr"] = psys;
6848 p["SysStateSeen"] = sys_seen;
6849 p["states"] = space.rows();
6850 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
6851 sv = line::reg::Json::array();
6852 for (std::size_t i = 0; i < M; ++i) {
6853 st.push_back(sn.stations[i].name);
6854 pa.push_back(prob[i]);
6855 sv.push_back(static_cast<bool>(seen[i]));
6856 }
6857 p["Station"] = st;
6858 p["Prob"] = pa;
6859 p["ProbAggr"] = pa;
6860 p["StateSeen"] = sv;
6861 emit_analysis<double>("prob", p, res.method);
6862 return 0;
6863 }
6864 ldes_banner(res, o);
6865 std::printf("ProbSys = ProbSysAggr %.10g%s\n", psys, sys_seen ? "" : " (state never observed)");
6866 std::printf("%-16s %14s\n", "Station", "Prob=ProbAggr");
6867 for (std::size_t i = 0; i < M; ++i)
6868 std::printf("%-16s %14.10g%s\n", sn.stations[i].name.c_str(), prob[i],
6869 seen[i] ? "" : " (state never observed)");
6870 return 0;
6871}
6872
6873/**
6874 * The `-s ldes` entry: one analysis per getter of the reference's surface.
6875 *
6876 * `-a prob` is `solve_model_ldes_prob`: the four getProb queries read off the
6877 * engine's joint-state histogram at the model's DECLARED state.
6878 */
6879int solve_model_ldes(const std::string& file, const Knobs& k, const std::string& analysis) {
6880 if (analysis == "avg") return solve_model_ldes_avg(file, k);
6881 if (analysis == "tran") return solve_model_ldes_tran(file, k);
6882 // THE SAME ECDF UNDER FOUR NAMES, which is the reference's own structure and
6883 // not a shortcut here: `@@SolverLDES/getCdfRespT`, `getTranCdfRespT` and
6884 // `getTranCdfPassT` all read `respTimeSamples`, and the JAR's
6885 // getTranCdfPassT is literally `return getTranCdfRespT(R)`. A simulator
6886 // observes one per-job passage and there is no second measurement to make;
6887 // emitting the curve under the key the caller asked for is what tells them
6888 // apart, and the payload's `type` says which question it answers.
6889 if (analysis == "cdf") return solve_model_ldes_cdf(file, k, "cdf", "CdfRespT");
6890 if (analysis == "cdfpasst") return solve_model_ldes_cdf(file, k, "cdfpasst", "CdfPassT");
6891 if (analysis == "trancdf") return solve_model_ldes_cdf(file, k, "trancdf", "TranCdfRespT");
6892 if (analysis == "trancdfpasst")
6893 return solve_model_ldes_cdf(file, k, "trancdfpasst", "TranCdfPassT");
6894 if (analysis == "sample") return solve_model_ldes_sample(file, k);
6895 if (analysis == "reward") return solve_model_ldes_reward(file, k);
6896 if (analysis == "prob") return solve_model_ldes_prob(file, k);
6897 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
6898 "SolverLDES ports -a avg (getAvg) and its four views -a node, -a sys, -a chain and "
6899 "-a nodechain, -a tran (getTranAvg), -a cdf / cdf-passt / "
6900 "tran-cdf-respt / tran-cdf-passt (the empirical passage law, one measurement under the "
6901 "four names the reference gives it), -a sample (sampleSys), -a reward "
6902 "(getAvgReward) and -a prob (getProb, getProbAggr, getProbSys, getProbSysAggr); got '" +
6903 analysis + "'");
6904}
6905
6906std::string auto_getter_of_analysis(const std::string& analysis) {
6907 if (analysis == "prob") return "getProbSysAggr";
6908 if (analysis == "marg") return "getProbMarg";
6909 if (analysis == "sysmarg") return "getProbSysMarg";
6910 if (analysis == "normconst") return "getProbNormConstAggr";
6911 if (analysis == "tranprob") return "getTranProbSysAggr";
6912 if (analysis == "sample") return "sampleSys";
6913 if (analysis == "cdf") return "getCdfRespT";
6914 if (analysis == "gen") return "getInfGen";
6915 if (analysis == "states") return "getStateSpace";
6916 if (analysis == "reward") return "getAvgReward";
6917 if (analysis == "sens") return "getSensitivityTable";
6918 if (analysis == "tran") return "getTranAvg";
6919 if (analysis == "internals") return "getMAMResult";
6920 // `-a bounds` and `-a tranreward` deliberately keep the AvgTable getter:
6921 // `getBoundsTable` and `getTranReward` are absent from chooseSolverHeur's
6922 // own method lists, so naming them here would ask the chooser about a getter
6923 // it does not rank. Each is served by exactly one engine anyway (BA and
6924 // CTMC), which refuses by name when `-s auto` sends the run elsewhere.
6925 if (analysis == "node") return "getAvgNodeTable";
6926 return "getAvgTable";
6927}
6928
6929/**
6930 * What `-s auto` resolved to: the engines to try, in order, and the method the
6931 * first of them runs.
6932 */
6933struct AutoPlan {
6934 std::vector<std::string> order; ///< CLI solver method names, chosen first
6935 std::string method; ///< the method the chosen engine runs, "" for its default
6936 std::string note; ///< what the ranking preferred and this port cannot build
6937 /** Per entry of `order`, the `--stage-solver` of an Environment candidate; "" elsewhere. */
6938 std::vector<std::string> stage;
6939};
6940
6941/** The CLI method name of a method family, or a refusal naming what the family needs. */
6942std::string auto_cli_token_of_family(const std::string& fam) {
6943 if (fam == "mva" || fam == "nc" || fam == "ctmc" || fam == "mam" || fam == "ag" ||
6944 fam == "ssa" || fam == "ba" || fam == "uq" || fam == "env")
6945 return fam;
6946 if (fam == "fld") return "fluid";
6947 if (fam == "ldes") {
6948 // The engine is not built here, it is RUN here, so the family resolves
6949 // whenever the machine has one and refuses -- naming what is missing --
6950 // when it does not, rather than diverting to a different simulator.
6953 "--method ldes names the discrete-event engine, and no engine was found beside "
6954 "this binary (common/ldes or common/ldes.jar, or $LINE_LDES_DIR); -s ssa is the "
6955 "simulator this port builds in process");
6956 return "ldes";
6957 }
6958 if (fam == "jmt") {
6959 // IT IS WRAPPED NOW. This refused the token outright until the JMT
6960 // client landed, and stayed behind: `-s jmt` drives jsim and jmva
6961 // through `solver_jmt_run_analyzer`, so refusing `--method jmt` denied
6962 // under `-s auto` what the very same binary answers under `-s jmt`.
6963 // Availability is left to the wrapper, which names what is missing (a
6964 // JVM, common/JMT.jar or a REST endpoint) rather than guessing here.
6965 return "jmt";
6966 }
6967 // On a flat Network the lqns family's methods are the qns ones, served by
6968 // `qnsolver` through `solve_model_lqns_network`.
6969 if (fam == "lqns") return "lqns";
6970 if (fam == "ln")
6972 "--method ln names the layered solver, which takes a LayeredNetwork: pass the model "
6973 "as -i lqnx -s ln rather than as a Network");
6974 throw line::InputError("SolverAUTO: no engine stands behind method family '" + fam + "'");
6975}
6976
6977/**
6978 * Is this model.json an Environment envelope rather than a Network?
6979 *
6980 * `-s auto` has to know before it parses: the two readers take different
6981 * documents, and the reference's chooser has an Environment arm that is
6982 * unreachable if every auto run is assumed to hold a Network. A malformed
6983 * document answers false, so the real reader reports the parse error.
6984 */
6985bool model_is_environment(const std::string& file) {
6986 try {
6987 line::io::detail::json root;
6988 if (file.empty()) {
6989 std::istringstream in(stdin_model_text());
6990 in >> root;
6991 } else {
6992 std::ifstream in(file.c_str());
6993 if (!in) return false;
6994 in >> root;
6995 }
6996 // The envelope may wrap the model, exactly as build_environment_from_json
6997 // unwraps it; `type` is what that reader keys on, so this reads the same
6998 // field rather than a second convention of its own.
6999 if (!root.is_object()) return false;
7000 const line::io::detail::json& model = root.contains("model") ? root.at("model") : root;
7001 return model.is_object() && model.value("type", std::string()) == "Environment";
7002 } catch (...) {
7003 return false;
7004 }
7005}
7006
7007/**
7008 * `-s auto`: which engine answers, by `chooseSolver.m` through solver_auto.h.
7009 *
7010 * THREE THINGS DECIDE, in the reference's own order. `--method` is resolved
7011 * first, because a method FAMILY ('nc', 'nc.comom') names the engine outright
7012 * and bypasses every ranking, while a selection INTENT ('exact', 'sim', 'fast',
7013 * 'accurate') picks which ranking runs. Then the model class: an Environment
7014 * envelope takes the Environment arm, a Network the Network one. Then the
7015 * GETTER the caller asked for, since the reference keys the ranking on the
7016 * metric family and not on the model alone.
7017 *
7018 * THE ORDER IS A RETRY LIST, not a single name. `delegate.m` tries the chosen
7019 * solver and then every feasible candidate, so a refusal moves to the next
7020 * engine instead of ending the run; the caller sees which one answered.
7021 *
7022 * The choice reads structure only -- traits, feature sets, product form,
7023 * populations -- so it is made in the arithmetic the run will use and costs one
7024 * extra parse of the model, nothing more.
7025 */
7026template <class T>
7027AutoPlan choose_auto_plan(const std::string& file, const std::string& analysis,
7028 const std::string& method_token) {
7030 try {
7031 tok = line::autosolver::auto_resolve_token(method_token);
7032 } catch (const line::InputError&) {
7033 // A bare algorithm name ('comom') resolves to the family declaring it on THIS model
7034 // (familyDeclaringMethod), which needs a Network; any other model class keeps the refusal.
7035 if (model_is_environment(file)) throw;
7036 tok = line::autosolver::auto_resolve_token(method_token, read_model<T>(file).get_struct());
7037 }
7038 AutoPlan plan;
7039 if (!tok.is_intent) {
7040 plan.order.push_back(auto_cli_token_of_family(tok.family));
7041 if (tok.submethod != "default") plan.method = tok.submethod;
7042 return plan;
7043 }
7044
7045 const std::string getter = auto_getter_of_analysis(analysis);
7046 if (model_is_environment(file)) {
7049 // One `-s env` attempt per candidate ensemble, each naming its stage
7050 // solver, as `delegate.m` walks SolverENV(model, @SolverX) in turn.
7051 for (std::size_t i = 0; i < ec.retry.size(); ++i) {
7052 plan.order.push_back("env");
7053 plan.stage.push_back(line::autosolver::auto_env_stage_solver(ec.retry[i]));
7054 }
7055 for (std::size_t i = 0; i < ec.skipped.size(); ++i)
7056 plan.note += std::string(i ? ", " : " (the ranking preferred ") +
7058 if (!ec.skipped.empty()) plan.note += ", which this port does not build)";
7059 return plan;
7060 }
7061
7062 line::qn::Network<T> net = read_model<T>(file);
7065 plan.method = c.method;
7066 const std::vector<line::autosolver::AutoSolver> proposed =
7068 for (std::size_t i = 0; i < proposed.size(); ++i)
7069 plan.order.push_back(line::autosolver::auto_solver_name(proposed[i]));
7070 for (std::size_t i = 0; i < c.skipped.size(); ++i)
7071 plan.note += std::string(i ? ", " : " (the ranking preferred ") +
7073 if (!c.skipped.empty()) plan.note += ", which this port does not build)";
7074 return plan;
7075}
7076
7077AutoPlan choose_auto_plan_dispatch(const std::string& arith, const std::string& file,
7078 const std::string& analysis, const std::string& method_token) {
7079 if (arith == "exact") return choose_auto_plan<line::Rational>(file, analysis, method_token);
7080 if (arith == "real:16") return choose_auto_plan<line::Real<16> >(file, analysis, method_token);
7081 if (arith == "real" || arith == "real:32")
7082 return choose_auto_plan<line::Real<32> >(file, analysis, method_token);
7083 if (arith == "real:64") return choose_auto_plan<line::Real<64> >(file, analysis, method_token);
7084 if (arith == "real:128")
7085 return choose_auto_plan<line::Real<128> >(file, analysis, method_token);
7086 if (arith == "real:256")
7087 return choose_auto_plan<line::Real<256> >(file, analysis, method_token);
7088 return choose_auto_plan<double>(file, analysis, method_token);
7089}
7090
7091/**
7092 * The numeric clean-up SolverLN.getAvgTable applies before printing.
7093 *
7094 * It is reproduced here because the reference's reported table IS the
7095 * sanitized one, so a row-by-row comparison against it has to compare like
7096 * with like. Two rules: snap a value to one decimal place when it is already
7097 * within CoarseTol of it relatively, and snap anything at or below FineTol to
7098 * zero. The second rule is what turns the residual queue length of a chain of
7099 * Immediate classes -- of order 1e-8 by construction, since Immediate has rate
7100 * 1e8 -- into the exact zero the reference prints.
7101 */
7102double ln_sanitize(double x) {
7103 const double r = std::round(x * 10.0);
7104 if (std::fabs(x * 10.0 - r) < line::lang::GlobalConstants::CoarseTol * x * 10.0) x = r / 10.0;
7105 if (x <= line::lang::GlobalConstants::FineTol) x = 0.0;
7106 return x;
7107}
7108
7109const char* ln_element_kind(const line::lqn::LqnStruct<double>& l, std::size_t i) {
7110 switch (l.type[i]) {
7111 case line::lang::LqnElement::HOST: return "Processor";
7112 case line::lang::LqnElement::TASK: return l.isref[i] ? "RefTask" : "Task";
7113 case line::lang::LqnElement::ENTRY: return "Entry";
7114 default: return "Activity";
7115 }
7116}
7117
7118/**
7119 * Print every layer's stations, classes and routing, in a form a MATLAB dump of
7120 * `solver.ensemble{k}` can be diffed against line by line.
7121 *
7122 * A layered result that is close but not equal across codebases is almost never
7123 * a difference in the MVA call; it is a layer that was BUILT differently -- a
7124 * class that is present in one and not the other, a population, a routing
7125 * probability. Comparing the final AvgTable cannot tell those apart, so the
7126 * structure has to be observable directly.
7127 */
7128template <class T>
7129void ln_dump_layers(const line::ln::SolverLN<T>& solver) {
7130 using namespace line;
7131 const std::vector<qn::Layer<T> >& ens = solver.layers();
7132 for (std::size_t k = 0; k < ens.size(); ++k) {
7133 const qn::Layer<T>& L = ens[k];
7134 std::printf("LAYER %zu %s nstations=%zu nclasses=%zu nchains=%zu\n", k + 1, L.name.c_str(),
7135 L.stations.size(), L.classes.size(), L.nchains);
7136 for (std::size_t i = 0; i < L.stations.size(); ++i)
7137 std::printf(" STATION %zu %s sched=%s nservers=%g\n", i + 1, L.stations[i].name.c_str(),
7138 lang::sched_to_text(L.stations[i].sched), L.stations[i].nservers);
7139 for (std::size_t r = 0; r < L.classes.size(); ++r)
7140 std::printf(" CLASS %zu %s pop=%.17g refstat=%zu completes=%d\n", r + 1,
7141 L.classes[r].name.c_str(), L.classes[r].population, L.classes[r].refstat,
7142 int(L.classes[r].completes));
7143 for (std::size_t i = 0; i < L.stations.size(); ++i)
7144 for (std::size_t r = 0; r < L.classes.size(); ++r) {
7145 if (L.disabled.empty() || L.disabled[i][r]) continue;
7146 std::printf(" RATE %s %s %.17g scv=%.17g\n", L.stations[i].name.c_str(),
7147 L.classes[r].name.c_str(), num_traits<T>::to_double(L.rates(i, r)),
7148 num_traits<T>::to_double(L.scv(i, r)));
7149 }
7150 for (const auto& kv : L.P) {
7151 const Matrix<T>& B = kv.second;
7152 for (std::size_t i = 0; i < B.rows(); ++i)
7153 for (std::size_t j = 0; j < B.cols(); ++j) {
7154 const double p = num_traits<T>::to_double(B(i, j));
7155 if (p == 0.0) continue;
7156 std::printf(" ROUTE %s->%s %s->%s %.17g\n",
7157 L.classes[kv.first.first - 1].name.c_str(),
7158 L.classes[kv.first.second - 1].name.c_str(),
7159 L.nodes[i].name.c_str(), L.nodes[j].name.c_str(), p);
7160 }
7161 }
7162 }
7163}
7164
7165/** The LnOptions a set of CLI knobs describes; arithmetic-independent. */
7166inline line::ln::LnOptions ln_options_from(const Knobs& k) {
7167 using namespace line;
7168 // The reference's own LnOptions defaults stand unless the caller overrode
7169 // them; the CLI does not restate them, so an untouched knob keeps whatever
7170 // SolverLN itself considers default.
7172 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
7173 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
7174 if (k.no_interlocking) opt.interlocking = false;
7175 if (!k.interlock_method.empty()) opt.interlock_method = k.interlock_method;
7176 if (k.interlock_maxpaths >= 0.0) opt.interlock_maxpaths = k.interlock_maxpaths;
7177 if (!k.interlock_refpath_scope.empty()) opt.interlock_refpath_scope = k.interlock_refpath_scope;
7178 if (!k.layer_solver.empty()) opt.layer_solver = k.layer_solver;
7179 if (!k.method.empty()) opt.method = k.method;
7180 // --samples and --seed reach whichever simulator runs the layers; for `ldes`
7181 // and `jmt` the seed is the BASE of SolverLN.pre's per-layer schedule.
7182 if (k.samples) {
7183 opt.layer_ssa.samples = k.samples;
7184 opt.layer_ldes.samples = static_cast<std::size_t>(k.samples);
7185 opt.layer_jmt.samples = static_cast<double>(k.samples);
7186 }
7187 if (k.seed) {
7188 opt.layer_ssa.seed = k.seed;
7189 opt.layer_ldes.seed = static_cast<long>(k.seed);
7190 opt.layer_jmt.seed = static_cast<long>(k.seed);
7191 }
7192 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
7193 if (k.tran_points) opt.tran_points = k.tran_points;
7194 if (!k.ln_transient.empty()) opt.ln_transient = k.ln_transient;
7195 if (!k.ln_transient_channels.empty()) opt.ln_transient_channels = k.ln_transient_channels;
7196 for (const auto& kv : k.lld) opt.lld[kv.first] = kv.second;
7197 if (k.nlp_alpha >= 0.0) opt.nlp_alpha = k.nlp_alpha;
7198 if (k.nlp_warm >= 0) opt.nlp_warm = int(k.nlp_warm);
7199 if (k.nlp_maxiter >= 0) opt.nlp_maxiter = int(k.nlp_maxiter);
7200 if (!k.nlp_fork.empty()) opt.nlp_fork = k.nlp_fork;
7201 if (!k.warmstart.empty()) opt.warmstart = k.warmstart;
7202 if (k.mol_iter_max >= 0) opt.mol_iter_max = std::size_t(k.mol_iter_max);
7203 if (k.mol_iter_tol >= 0.0) opt.mol_iter_tol = k.mol_iter_tol;
7204 if (k.mol_relax_factor >= 0.0) opt.mol_relax_factor = k.mol_relax_factor;
7205 return opt;
7206}
7207
7208/** The engine name the banner reports, which is never a hardcoded MVA. */
7209inline const char* ln_layer_engine_name(const std::string& layer_solver) {
7210 if (layer_solver == "fluid") return "Fluid";
7211 if (layer_solver == "nc") return "NC";
7212 if (layer_solver == "ssa") return "SSA";
7213 if (layer_solver == "mam") return "MAM";
7214 if (layer_solver == "ctmc") return "CTMC";
7215 if (layer_solver == "ldes") return "LDES";
7216 if (layer_solver == "jmt") return "JMT";
7217 return "MVA";
7218}
7219
7220/**
7221 * `-a tran`: the layered transient, one block of time series per layer.
7222 *
7223 * The blocks are printed rather than the block-diagonal cell array the
7224 * reference assembles, because the off-diagonal blocks of that array are empty
7225 * by construction and each layer keeps its own grid.
7226 */
7227template <class T>
7228int run_ln_tran(const std::string& file, const std::string& output, const Knobs& k) {
7229 using namespace line;
7230 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
7231 ln::LnOptions opt = ln_options_from(k);
7232 ln::SolverLN<T> solver(model, opt);
7233 const ln::LnTranSolution tr = solver.get_tran_avg();
7234 const std::vector<qn::Layer<T>>& layers = solver.layers();
7235
7236 if (output == "json") {
7237 reg::Json j = reg::Json::object();
7238 j["model"] = file;
7239 j["arith"] = num_traits<T>::name();
7240 j["mode"] = tr.mode;
7241 j["iterations"] = tr.iterations;
7242 j["gap"] = ln_sanitize(tr.gap);
7243 reg::Json ls = reg::Json::array();
7244 for (std::size_t e = 0; e < tr.layers.size(); ++e) {
7245 reg::Json le = reg::Json::object();
7246 le["layer"] = layers[e].name;
7247 reg::Json tt = reg::Json::array();
7248 for (double t : tr.layers[e].t) tt.push_back(ln_sanitize(t));
7249 le["t"] = tt;
7250 reg::Json series = reg::Json::array();
7251 for (std::size_t i = 0; i < tr.layers[e].QN.size(); ++i)
7252 for (std::size_t r = 0; r < tr.layers[e].QN[i].size(); ++r) {
7253 reg::Json s = reg::Json::object();
7254 s["station"] = layers[e].stations[i].name;
7255 s["class"] = layers[e].classes[r].name;
7256 auto arr = [&](const std::vector<double>& v) {
7257 reg::Json a = reg::Json::array();
7258 for (double x : v) a.push_back(ln_sanitize(x));
7259 return a;
7260 };
7261 s["QLen"] = arr(tr.layers[e].QN[i][r]);
7262 s["Util"] = arr(tr.layers[e].UN[i][r]);
7263 s["Tput"] = arr(tr.layers[e].TN[i][r]);
7264 series.push_back(s);
7265 }
7266 le["series"] = series;
7267 ls.push_back(le);
7268 }
7269 j["layers"] = ls;
7270 emit_document(dump_document(j, 1));
7271 return 0;
7272 }
7273
7274 std::printf("SolverLN(Solver%s) getTranAvg arith=%s mode=%s layers=%zu iterations=%ld gap=%.3e\n",
7275 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), tr.mode.c_str(),
7276 tr.layers.size(), tr.iterations, tr.gap);
7277 for (std::size_t e = 0; e < tr.layers.size(); ++e) {
7278 const std::vector<double>& t = tr.layers[e].t;
7279 if (t.empty()) continue;
7280 std::printf("\nLayer %s (%zu points on [%.6g, %.6g])\n", layers[e].name.c_str(), t.size(),
7281 t.front(), t.back());
7282 std::printf("%-30s %-24s %12s %12s %12s %12s\n", "Station", "JobClass", "QLen(0)",
7283 "QLen(end)", "Util(end)", "Tput(end)");
7284 for (std::size_t i = 0; i < tr.layers[e].QN.size(); ++i)
7285 for (std::size_t r = 0; r < tr.layers[e].QN[i].size(); ++r) {
7286 const std::vector<double>& q = tr.layers[e].QN[i][r];
7287 if (q.empty()) continue;
7288 std::printf("%-30s %-24s %12.6g %12.6g %12.6g %12.6g\n",
7289 layers[e].stations[i].name.c_str(), layers[e].classes[r].name.c_str(),
7290 ln_sanitize(q.front()), ln_sanitize(q.back()),
7291 ln_sanitize(tr.layers[e].UN[i][r].back()),
7292 ln_sanitize(tr.layers[e].TN[i][r].back()));
7293 }
7294 }
7295 return 0;
7296}
7297
7298/** `-a sens`: the layer sensitivity tables under a leading Layer column. */
7299template <class T>
7300int run_ln_sens(const std::string& file, const std::string& output, const Knobs& k) {
7301 using namespace line;
7302 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
7303 ln::LnOptions opt = ln_options_from(k);
7304 ln::SolverLN<T> solver(model, opt);
7306 if (!k.sens_method.empty()) so.method = k.sens_method;
7307 if (!k.sens_scheme.empty()) so.scheme = k.sens_scheme;
7308 if (k.sens_step > 0.0) so.step = k.sens_step;
7309 const ln::LnSensTable<T> tbl = solver.get_sensitivity_table(so);
7310
7311 if (output == "json") {
7312 reg::Json j = reg::Json::object();
7313 j["model"] = file;
7314 j["arith"] = num_traits<T>::name();
7315 j["method"] = tbl.method;
7316 reg::Json rows = reg::Json::array();
7317 for (const auto& r : tbl.rows) {
7318 reg::Json o = reg::Json::object();
7319 o["Layer"] = r.layer;
7320 o["Station"] = r.station;
7321 o["JobClass"] = r.jobclass;
7322 o["dTput_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dTput));
7323 o["dRespT_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dRespT));
7324 o["dQLen_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dQLen));
7325 o["dUtil_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dUtil));
7326 rows.push_back(o);
7327 }
7328 j["rows"] = rows;
7329 emit_document(dump_document(j, 1));
7330 return 0;
7331 }
7332
7333 std::printf("SolverLN(Solver%s) getSensitivityTable arith=%s method=%s rows=%zu\n",
7334 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), tbl.method.c_str(),
7335 tbl.rows.size());
7336 std::printf("%-28s %-28s %-20s %14s %14s %14s %14s\n", "Layer", "Station", "JobClass",
7337 "dTput_dRate", "dRespT_dRate", "dQLen_dRate", "dUtil_dRate");
7338 for (const auto& r : tbl.rows)
7339 std::printf("%-28s %-28s %-20s %14.6g %14.6g %14.6g %14.6g\n", r.layer.c_str(),
7340 r.station.c_str(), r.jobclass.c_str(),
7341 sens_sanitize_signed(num_traits<T>::to_double(r.dTput)),
7342 sens_sanitize_signed(num_traits<T>::to_double(r.dRespT)),
7343 sens_sanitize_signed(num_traits<T>::to_double(r.dQLen)),
7344 sens_sanitize_signed(num_traits<T>::to_double(r.dUtil)));
7345 return 0;
7346}
7347
7348/** `-a cdf`: the per-entry response-time distribution of the moment3 method. */
7349template <class T>
7350int run_ln_cdf(const std::string& file, const std::string& output, const Knobs& k) {
7351 using namespace line;
7352 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
7353 ln::LnOptions opt = ln_options_from(k);
7354 // getCdfRespT.m runs the ensemble under moment3 whatever the caller asked
7355 // for, and restores the method afterwards: the mean-based update forms no
7356 // distribution at all, so there is nothing else to report.
7357 opt.method = "moment3";
7358 ln::SolverLN<T> solver(model, opt);
7359 const std::vector<ln::LnCdf> cdf = solver.get_cdf_respt();
7361
7362 if (output == "json") {
7363 reg::Json j = reg::Json::object();
7364 j["model"] = file;
7365 j["arith"] = num_traits<T>::name();
7366 reg::Json rows = reg::Json::array();
7367 for (std::size_t e = 1; e <= model.nentries && e < cdf.size(); ++e) {
7368 reg::Json o = reg::Json::object();
7369 o["entry"] = names.names[model.eshift + e];
7370 reg::Json tt = reg::Json::array(), ff = reg::Json::array();
7371 for (std::size_t p = 0; p < cdf[e].t.size(); ++p) {
7372 tt.push_back(ln_sanitize(cdf[e].t[p]));
7373 ff.push_back(ln_sanitize(cdf[e].cdf[p]));
7374 }
7375 o["t"] = tt;
7376 o["F"] = ff;
7377 rows.push_back(o);
7378 }
7379 j["entries"] = rows;
7380 emit_document(dump_document(j, 1));
7381 return 0;
7382 }
7383
7384 std::printf("SolverLN(Solver%s) getCdfRespT arith=%s method=moment3 entries=%zu\n",
7385 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), model.nentries);
7386 for (std::size_t e = 1; e <= model.nentries && e < cdf.size(); ++e) {
7387 if (cdf[e].t.empty()) {
7388 std::printf("%-40s (no distribution: the entry has no fitted term)\n",
7389 names.names[model.eshift + e].c_str());
7390 continue;
7391 }
7392 // Quartiles, which is what a CDF is read for; the full grid goes to JSON.
7393 auto quantile = [&](double p) {
7394 for (std::size_t i = 0; i < cdf[e].cdf.size(); ++i)
7395 if (cdf[e].cdf[i] >= p) return cdf[e].t[i];
7396 return cdf[e].t.back();
7397 };
7398 std::printf("%-40s p25=%12.6g p50=%12.6g p75=%12.6g p95=%12.6g points=%zu\n",
7399 names.names[model.eshift + e].c_str(), ln_sanitize(quantile(0.25)),
7400 ln_sanitize(quantile(0.50)), ln_sanitize(quantile(0.75)),
7401 ln_sanitize(quantile(0.95)), cdf[e].t.size());
7402 }
7403 return 0;
7404}
7405
7406/**
7407 * Solve a .lqnx layered queueing network with SolverLN and print its AvgTable.
7408 *
7409 * The output is one row per LQN element, with its queue length, utilization,
7410 * response time, residence time and throughput, in the same element order as
7411 * MATLAB's getAvgTable, so a row-by-row numeric comparison against the
7412 * reference is a plain diff.
7413 *
7414 * `--repeat` re-runs the whole solve K times and reports the best wall-clock
7415 * time, which is what the arithmetic-backend benchmark reads.
7416 */
7417template <class T>
7418int run_ln(const std::string& file, const std::string& output, const Knobs& k) {
7419 using namespace line;
7420 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
7421
7422 ln::LnOptions opt = ln_options_from(k);
7423 const int repeat = k.repeat > 0 ? k.repeat : 1;
7424
7425 double best = 1e300;
7427 std::size_t nlayers = 0;
7428 for (int rep = 0; rep < repeat; ++rep) {
7429 const auto t0 = std::chrono::steady_clock::now();
7430 ln::SolverLN<T> solver(model, opt);
7431 sol = solver.get_ensemble_avg();
7432 nlayers = solver.nlayers();
7433 if (output == "layers") {
7434 ln_dump_layers(solver);
7435 return 0;
7436 }
7437 const auto t1 = std::chrono::steady_clock::now();
7438 best = std::min(best, std::chrono::duration<double>(t1 - t0).count());
7439 }
7440 if (!sol.warning.empty()) std::fprintf(stderr, "Warning: %s\n", sol.warning.c_str());
7441
7442 // element names and kinds are arithmetic-independent, so read them once
7444
7445 if (output == "json") {
7446 reg::Json j = reg::Json::object();
7447 j["model"] = file;
7448 j["arith"] = num_traits<T>::name();
7449 j["layers"] = nlayers;
7450 j["iterations"] = sol.iterations;
7451 j["converged"] = sol.converged;
7452 j["seconds"] = best;
7453 reg::Json rows = reg::Json::array();
7454 for (std::size_t i = 1; i <= model.nidx; ++i) {
7455 reg::Json r = reg::Json::object();
7456 r["node"] = names.names[i];
7457 r["type"] = ln_element_kind(names, i);
7458 auto put = [&](const char* key, const std::vector<T>& v, const std::vector<bool>& d) {
7459 if (d[i]) r[key] = ln_sanitize(num_traits<T>::to_double(v[i]));
7460 else if (sol.is_bound) r[key] = 0.0; // see the note in the table below
7461 else r[key] = nullptr;
7462 };
7463 put("QLen", sol.QN, sol.defined_Q);
7464 put("Util", sol.UN, sol.defined_U);
7465 put("RespT", sol.RN, sol.defined_R);
7466 put("ResidT", sol.WN, sol.defined_W);
7467 put("Tput", sol.TN, sol.defined_T);
7468 rows.push_back(r);
7469 }
7470 j["rows"] = rows;
7471 emit_document(dump_document(j, 1));
7472 return 0;
7473 }
7474
7475 // The banner names the LAYER solver actually used, not a hardcoded MVA: the
7476 // two converge to different fixed points, so a reader (or a parity row) that
7477 // cannot tell them apart is reading numbers it cannot attribute.
7478 std::printf(
7479 "SolverLN(Solver%s) arith=%s type=%s layers=%zu iterations=%d converged=%d time=%.4fs\n",
7480 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(),
7481 line::util::method_type("LN", opt.method).c_str(), nlayers, sol.iterations,
7482 int(sol.converged), best);
7483 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
7484 "RespT", "ResidT", "Tput");
7485 for (std::size_t i = 1; i <= model.nidx; ++i) {
7486 // NaN IS THE UNDEFINED MARKER EVERYWHERE EXCEPT UNDER A BOUND. MATLAB's
7487 // getAvgTable prints NaN for a measure the element does not have (a
7488 // processor has no queue length), and this reproduces that. A BOUND is
7489 // the one case where the reference prints 0 instead: `mw.*` defines
7490 // throughput and processor utilization only, and both MATLAB and the JAR
7491 // report the rest as zero rather than as absent (the JAR maps the NaN
7492 // explicitly, SolverLN.java:3294-3299). The `defined_*` flags still say
7493 // undefined to any caller of the API; only the printed table follows the
7494 // reference, so that a numeric parity row compares like with like.
7495 //
7496 // This table is for a human; a cross-codebase comparison must read the
7497 // `-o json` above, which carries the raw double, and quantize it itself.
7498 auto fmt = [&](const std::vector<T>& v, const std::vector<bool>& d, char* buf) {
7499 if (!d[i] && sol.is_bound) std::snprintf(buf, 24, "%12.6g", 0.0);
7500 else if (!d[i]) std::snprintf(buf, 24, "%12s", "NaN");
7501 else std::snprintf(buf, 24, "%12.6g", ln_sanitize(num_traits<T>::to_double(v[i])));
7502 };
7503 char q[24], u[24], rr[24], w[24], t[24];
7504 fmt(sol.QN, sol.defined_Q, q);
7505 fmt(sol.UN, sol.defined_U, u);
7506 fmt(sol.RN, sol.defined_R, rr);
7507 fmt(sol.WN, sol.defined_W, w);
7508 fmt(sol.TN, sol.defined_T, t);
7509 std::printf("%-62s %-10s %s %s %s %s %s\n", names.names[i].c_str(),
7510 ln_element_kind(names, i), q, u, rr, w, t);
7511 }
7512 return 0;
7513}
7514
7515/**
7516 * `-i lqnx -s ldes -a cdf`: the SIMULATED response time distribution per entry.
7517 *
7518 * The measured counterpart of `run_ln_cdf`, which fits an APH to three moments of
7519 * a fluid passage time and convolves. Here the engine timed every invocation from
7520 * the instant the request reached the entry to its reply -- the interval `RLN`
7521 * averages -- so this law's mean reproduces that row and its tail is observed
7522 * rather than extrapolated.
7523 */
7524int run_ln_ldes_cdf(const std::string& file, const std::string& output, const Knobs& k) {
7525 using namespace line;
7528 if (k.samples) o.samples = k.samples;
7529 if (k.seed) o.seed = static_cast<long>(k.seed);
7530
7532
7533 // The per-entry ecdf, through the same API function the other codebases
7534 // call getCdfRespTLN.
7535 const std::vector<ldes::engine::LnEntryCdf> cdf =
7537 std::vector<std::vector<double> > tt(model.nentries), ff(model.nentries);
7538 for (std::size_t e = 0; e < model.nentries; ++e) {
7539 tt[e] = cdf[e].t;
7540 ff[e] = cdf[e].F;
7541 }
7542
7543 if (output == "json") {
7544 reg::Json j = reg::Json::object();
7545 j["model"] = file;
7546 j["engine"] = "native-ln";
7547 reg::Json rows = reg::Json::array();
7548 for (std::size_t e = 0; e < model.nentries; ++e) {
7549 reg::Json ob = reg::Json::object();
7550 ob["entry"] = model.names[model.eshift + e + 1];
7551 reg::Json at = reg::Json::array(), af = reg::Json::array();
7552 for (std::size_t i = 0; i < tt[e].size(); ++i) {
7553 at.push_back(tt[e][i]);
7554 af.push_back(ff[e][i]);
7555 }
7556 ob["t"] = at;
7557 ob["F"] = af;
7558 ob["observations"] = static_cast<double>(
7559 e < r.entry_resp_samples.size() ? r.entry_resp_samples[e].size() : 0);
7560 rows.push_back(ob);
7561 }
7562 j["entries"] = rows;
7563 emit_document(dump_document(j, 1));
7564 return 0;
7565 }
7566
7567 std::printf("SolverLDES(native LN engine) getCdfRespT entries=%zu\n", model.nentries);
7568 for (std::size_t e = 0; e < model.nentries; ++e) {
7569 if (tt[e].empty()) {
7570 std::printf("%-40s (no observation)\n", model.names[model.eshift + e + 1].c_str());
7571 continue;
7572 }
7573 // Quartiles plus p95, which is what a measured law is read for.
7574 struct Q {
7575 const std::vector<double>& t;
7576 const std::vector<double>& f;
7577 double operator()(double p) const {
7578 for (std::size_t i = 0; i < f.size(); ++i)
7579 if (f[i] >= p) return t[i];
7580 return t.back();
7581 }
7582 } q = {tt[e], ff[e]};
7583 std::printf("%-40s p25=%12.6g p50=%12.6g p75=%12.6g p95=%12.6g n=%zu\n",
7584 model.names[model.eshift + e + 1].c_str(), q(0.25), q(0.50), q(0.75),
7585 q(0.95), r.entry_resp_samples[e].size());
7586 }
7587 return 0;
7588}
7589
7590/**
7591 * `-s ldes`: the layered model simulated directly, by the IN-PROCESS engine.
7592 *
7593 * IT IS NOT THE SUBPROCESS `-s ldes` OF THE FLAT PATH. That arm hands a
7594 * `model.json` to `common/ldes`, and the engine behind that wire refuses a
7595 * layered document outright ("LDES currently supports Network models only"), so
7596 * there is nothing to forward. This arm calls `ldes_ln_engine_solve` in process
7597 * -- the C++ twin of `Solver_ssj_ln.java` -- which simulates entries,
7598 * activities, task threads and synchronous calls directly instead of
7599 * decomposing the model into layers. So it is not a noisier route to `-s ln`:
7600 * SolverLN's decomposition is an APPROXIMATION and this is a sample path of the
7601 * model itself, which is what makes it the reference the layered solvers are
7602 * checked against.
7603 *
7604 * THE NaN MASK OF THE LAYERED TABLE IS PART OF THE ANSWER, and it belongs to
7605 * the table rather than to whichever solver filled it: a processor has no queue
7606 * length, a task no response time, an entry no residence, and `-s ln` and
7607 * `-s lqns` both print NaN there. This engine MEASURES more than that -- a
7608 * processor's completion rate is sitting in `LnResult::TLN` -- and printing it
7609 * under a column the other two arms leave empty would make one column mean
7610 * different things depending on who filled it; the JAR's `getLNAvgTable` masks
7611 * the same way. Nothing is discarded: the
7612 * unmasked measurements stay on `LnResult` for a programmatic caller, and only
7613 * the shared table is masked, so that it can be diffed row by row.
7614 *
7615 * THE NUMBERS ARE NOT `ln_sanitize`d, for the reason `run_lqns` states below:
7616 * that helper snaps to a tenth and floors at FineTol, which is right for a
7617 * fixed point this port computed and wrong for a measurement it made. A
7618 * simulated utilization of 1e-9 is a rare event that was observed, not a
7619 * negligible residue of an iteration, and the JAR's own layered LDES table
7620 * reports the raw estimate too -- so snapping here would put the two out of
7621 * step on exactly the models a parity row is read on.
7622 */
7623int run_ln_ldes(const std::string& file, const std::string& output, const Knobs& k) {
7624 using namespace line;
7626
7627 // The engine reads THREE settings and no more (`o.samples`, `o.events`,
7628 // `o.seed`); the dispatcher refuses the rest of the --ldes-* family rather
7629 // than letting this function drop them silently.
7631 if (k.samples) o.samples = k.samples;
7632 if (k.seed) o.seed = static_cast<long>(k.seed);
7633
7634 // --repeat is a TIMING loop and stays honest here only because the stream is
7635 // seeded: every replication of one command walks the same sample path, so
7636 // the table is that run's table and `time=` is the only thing that varies.
7637 // This is why -s lqns refuses the flag and this arm does not -- lqsim seeds
7638 // itself, so re-running it would report different numbers under one banner.
7639 const int repeat = k.repeat > 0 ? k.repeat : 1;
7640 double best = 1e300;
7642 for (int rep = 0; rep < repeat; ++rep) {
7643 const auto t0 = std::chrono::steady_clock::now();
7644 r = ldes::ldes_ln_engine_solve(model, o);
7645 const auto t1 = std::chrono::steady_clock::now();
7646 best = std::min(best, std::chrono::duration<double>(t1 - t0).count());
7647 }
7648
7649 // The mask lives beside the data it masks (`ldes::engine::ln_defined`), so
7650 // that the doctest suite can pin it against `LnSolution::defined_*` without
7651 // reaching into this file. Spelling it here instead would put the rule that
7652 // decides what the table MEANS inside a printer.
7653 typedef ldes::engine::LnColumn Col;
7654 const auto def = [&](std::size_t i, Col c) {
7655 return ldes::engine::ln_defined(model, r, i, c);
7656 };
7657
7658 if (output == "json") {
7659 reg::Json j = reg::Json::object();
7660 j["model"] = file;
7661 j["arith"] = "double";
7662 j["solver"] = "ldes";
7663 // WHICH ENGINE ANSWERED, on the same grounds the flat arm's `engine=` is
7664 // not decoration: `-s ldes` names two different simulators depending on
7665 // whether the model is layered, and a number quoted from one must not be
7666 // read as the other's.
7667 j["engine"] = "native-ln";
7668 j["samples"] = o.samples;
7669 j["seed"] = o.seed;
7670 j["simulatedTime"] = r.simulated_time;
7671 j["completions"] = r.completions;
7672 j["seconds"] = best;
7673 reg::Json rows = reg::Json::array();
7674 for (std::size_t i = 1; i <= model.nidx; ++i) {
7675 reg::Json row = reg::Json::object();
7676 row["node"] = model.names[i];
7677 row["type"] = ln_element_kind(model, i);
7678 auto put = [&](const char* key, double v, bool defined) {
7679 if (defined) row[key] = v;
7680 else row[key] = nullptr;
7681 };
7682 put("QLen", r.QLN(i, 0), def(i, Col::QLen));
7683 put("Util", r.ULN(i, 0), def(i, Col::Util));
7684 put("RespT", r.RLN(i, 0), def(i, Col::RespT));
7685 put("ResidT", r.WLN(i, 0), def(i, Col::ResidT));
7686 put("Tput", r.TLN(i, 0), def(i, Col::Tput));
7687 rows.push_back(row);
7688 }
7689 j["rows"] = rows;
7690 emit_document(dump_document(j, 1));
7691 return 0;
7692 }
7693
7694 std::printf("SolverLDES(native LN engine) arith=double type=%s samples=%zu seed=%ld "
7695 "simtime=%.6g completions=%lld time=%.4fs\n",
7696 line::util::method_type("LDES", o.method).c_str(), o.samples, o.seed,
7697 r.simulated_time, r.completions, best);
7698 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
7699 "RespT", "ResidT", "Tput");
7700 for (std::size_t i = 1; i <= model.nidx; ++i) {
7701 // Six digits, as on the two tables around it, so the three arms diff.
7702 auto fmt = [&](double v, bool defined, char* buf) {
7703 if (!defined) std::snprintf(buf, 24, "%12s", "NaN");
7704 else std::snprintf(buf, 24, "%12.6g", v);
7705 };
7706 char q[24], u[24], rr[24], w[24], t[24];
7707 fmt(r.QLN(i, 0), def(i, Col::QLen), q);
7708 fmt(r.ULN(i, 0), def(i, Col::Util), u);
7709 fmt(r.RLN(i, 0), def(i, Col::RespT), rr);
7710 fmt(r.WLN(i, 0), def(i, Col::ResidT), w);
7711 fmt(r.TLN(i, 0), def(i, Col::Tput), t);
7712 std::printf("%-62s %-10s %s %s %s %s %s\n", model.names[i].c_str(),
7713 ln_element_kind(model, i), q, u, rr, w, t);
7714 }
7715 return 0;
7716}
7717
7718/**
7719 * `-s lqns`: the same layered model, solved by the external binary.
7720 *
7721 * The table has the SAME columns as run_ln's so that the two can be diffed row
7722 * by row, but the numbers are not sanitized the same way: `ln_sanitize` also
7723 * snaps anything at or below FineTol to zero, which is right for a fixed point
7724 * this port computed and wrong for a measurement it did not -- lqns reports no
7725 * residence time and no arrival rate at all, and a zero there would read as a
7726 * computed zero. Those two columns print NaN, and only the snap-to-tenth of the
7727 * reference's getAvgTable is applied.
7728 */
7729template <class T>
7730int run_lqns(const std::string& file, const std::string& output, const Knobs& k) {
7731 using namespace line;
7732 const lqn::LqnModel<T> model =
7734
7736 if (!k.method.empty()) opt.method = k.method;
7737 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
7738 if (k.samples) opt.samples = static_cast<double>(k.samples);
7739 opt.verbose = k.verbose;
7740 opt.keep = k.keep;
7741 opt.remote = k.remote;
7742 if (!k.remote_url.empty()) opt.remote_url = k.remote_url;
7743 opt.timeout_seconds = k.timeout_seconds;
7744
7745 lqns::SolverLQNS<T> solver(model, opt);
7746 const lqns::LqnsSolution<T> sol = solver.get_ensemble_avg();
7747 const lqn::LqnStruct<T>& sn = solver.get_struct();
7749
7750 if (output == "json") {
7751 reg::Json j = reg::Json::object();
7752 j["model"] = file;
7753 j["arith"] = num_traits<T>::name();
7754 j["solver"] = lqns::SolverLQNS<T>::is_stochastic_method(opt.method) ? "lqsim" : "lqns";
7755 j["method"] = opt.method;
7756 j["iterations"] = sol.iterations;
7757 j["seconds"] = solver.runtime();
7758 reg::Json rows = reg::Json::array();
7759 for (std::size_t i = 1; i <= sn.nidx; ++i) {
7760 reg::Json r = reg::Json::object();
7761 r["node"] = names.names[i];
7762 r["type"] = ln_element_kind(names, i);
7763 auto put = [&](const char* key, const std::vector<T>& v, const std::vector<bool>& d) {
7764 if (d[i]) r[key] = lqns::detail::snap_to_tenth(num_traits<T>::to_double(v[i]));
7765 else r[key] = nullptr;
7766 };
7767 put("QLen", sol.QN, sol.defined_Q);
7768 put("Util", sol.UN, sol.defined_U);
7769 put("RespT", sol.RN, sol.defined_R);
7770 put("ResidT", sol.WN, sol.defined_W);
7771 put("Tput", sol.TN, sol.defined_T);
7772 rows.push_back(r);
7773 }
7774 j["rows"] = rows;
7775 emit_document(dump_document(j, 1));
7776 return 0;
7777 }
7778
7779 std::printf("SolverLQNS(%s) arith=%s type=%s iterations=%d time=%.4fs\n",
7781 line::util::method_type("LQNS", opt.method).c_str(), sol.iterations,
7782 solver.runtime());
7783 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
7784 "RespT", "ResidT", "Tput");
7785 for (std::size_t i = 1; i <= sn.nidx; ++i) {
7786 // Six digits, as on the SolverLN table above and for the same reason.
7787 auto fmt = [&](const std::vector<T>& v, const std::vector<bool>& d, char* buf) {
7788 if (!d[i]) std::snprintf(buf, 24, "%12s", "NaN");
7789 else
7790 std::snprintf(buf, 24, "%12.6g",
7791 lqns::detail::snap_to_tenth(num_traits<T>::to_double(v[i])));
7792 };
7793 char q[24], u[24], rr[24], w[24], t[24];
7794 fmt(sol.QN, sol.defined_Q, q);
7795 fmt(sol.UN, sol.defined_U, u);
7796 fmt(sol.RN, sol.defined_R, rr);
7797 fmt(sol.WN, sol.defined_W, w);
7798 fmt(sol.TN, sol.defined_T, t);
7799 std::printf("%-62s %-10s %s %s %s %s %s\n", names.names[i].c_str(),
7800 ln_element_kind(names, i), q, u, rr, w, t);
7801 }
7802 return 0;
7803}
7804
7805/** The solver method name as the console names it, e.g. "mva" -> "MVA". */
7806std::string upper_tag(const std::string& s) {
7807 std::string out = s;
7808 for (std::size_t i = 0; i < out.size(); ++i)
7809 out[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(out[i])));
7810 return out;
7811}
7812
7813/**
7814 * `-i lqnx|xml|srvn`: the layered path, sibling to solve_model_dispatch.
7815 *
7816 * `-s auto` CONSULTS THE LAYERED ARM of the chooser, which is keyed on the
7817 * metric and on one trait of the model: a cache task promotes the NC layer
7818 * engine, because the cache layer is where NC beats MVA. LQNS leads two of
7819 * those rankings and IS wrapped, so `-s auto` selects it wherever the binary is
7820 * installed -- which makes the choice machine-dependent, exactly as
7821 * `chooseAvgSolverHeur.m` makes it, since LINE ships no LQNS binary. The banner
7822 * always names what answered.
7823 *
7824 * THE METHOD NAMES NAME THE LAYER ENGINE, as the Java CLI's do: `ln.mva` runs
7825 * the layers under SolverMVA and `ln.comom` under SolverNC. The bare `ln` keeps
7826 * MVA layers although the Java CLI reads it as NC, since changing it would
7827 * re-baseline every existing layered result -- so `ln.comom` is the way to ask
7828 * for NC layers. `lqns` is not one of them: it does not solve LAYERS at all, it
7829 * hands the whole model to another program, so it takes the wrapper's own knobs
7830 * and none of SolverLN's.
7831 */
7832int solve_lqn_dispatch(const std::string& arith, const std::string& solver,
7833 const std::string& analysis, const std::string& output,
7834 const std::string& file, const Knobs& k) {
7835 if (file.empty())
7836 throw line::InputError(
7837 "a layered model is read from a file: pass -f <model.lqnx> or -f <model.json> (neither "
7838 "layered reader has a stdin form)");
7839 if (analysis != "avg" && analysis != "tran" && analysis != "sens" && analysis != "cdf")
7840 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
7841 "the layered path ports -a avg (getAvgTable), -a tran (getTranAvg), -a sens "
7842 "(getSensitivityTable) and -a cdf (getCdfRespT); got '" + analysis + "'");
7843 const std::string s = solver.empty() ? "auto" : solver;
7844 if (s != "auto" && s != "ln" && s != "ln.mva" && s != "ln.comom" && s != "lqns" &&
7845 s != "ldes")
7847 "the layered path takes -s ln, ln.mva, ln.comom, ldes, lqns or auto (got '" + s +
7848 "'); a Network solver cannot be applied to a LayeredNetwork directly");
7849
7850 // Solver console: the layered path has its own dispatcher and never
7851 // reaches solve_model_dispatch, so it opens the narrated run here. The
7852 // guard's destructor closes it, on an exception too.
7853 line::util::LineConsole::Run consoleRun(upper_tag(s), "", true);
7854
7855 Knobs kk = k;
7856 // What will actually answer: the token, or what `-s auto` resolves to.
7857 std::string engine = s;
7858 // `-s auto` with no explicit layer engine consults chooseLayeredSolver. An
7859 // explicit --layer-solver is a choice the caller already made, so the
7860 // chooser does not overrule it.
7861 if (s == "auto" && kk.layer_solver.empty()) {
7862 std::string getter = "getAvgTable";
7863 if (analysis == "tran") getter = "getTranAvg";
7864 else if (analysis == "cdf") getter = "getCdfRespT";
7865 else if (analysis == "sens") getter = "getSensitivityTable";
7866 // `iscache` is the one trait the ranking reads, and it costs one parse
7867 // of the .lqnx: the same document the solve parses again below.
7869 bool has_cache_task = false;
7870 for (std::size_t i = 0; i < probe.iscache.size(); ++i)
7871 if (probe.iscache[i]) has_cache_task = true;
7873 line::autosolver::auto_choose_layered_solver(getter, has_cache_task);
7874 const std::string token = line::autosolver::auto_layered_name(lc.solver);
7875 if (token == "lqns") engine = "lqns";
7876 else if (token == "ln.comom") kk.layer_solver = "nc";
7877 else if (token == "ln.fld") kk.layer_solver = "fluid";
7878 else if (token == "ln.mam") kk.layer_solver = "mam";
7879 else kk.layer_solver = "mva";
7880 std::string note;
7881 for (std::size_t i = 0; i < lc.skipped.size(); ++i)
7882 note += std::string(i ? ", " : " (the ranking preferred ") +
7884 if (!lc.skipped.empty()) note += ", which is not available here)";
7885 std::printf("SolverAUTO selected %s%s\n", token.c_str(), note.c_str());
7886 }
7887
7888 // ---- the wrapper, BEFORE the SolverLN knob ladder ---------------------
7889 // It is a wrapper, not a layer engine: --samples is the lqsim run length
7890 // rather than a simulated LAYER's, and --keep, --remote and --timeout
7891 // describe a child process no SolverLN run has. Asking the ladder below
7892 // about them would answer for the wrong solver.
7893 if (engine == "lqns") {
7894 if (analysis != "avg")
7895 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
7896 "SolverLQNS reports the mean table its binary computes; it has no transient, no "
7897 "sensitivity and no response-time distribution here, so it takes -a avg (got '" +
7898 analysis + "')");
7899 if (!kk.layer_solver.empty())
7900 throw line::InputError(
7901 "--layer-solver names the engine SolverLN runs on each layer; -s lqns solves no "
7902 "layers, it hands the whole model to the lqns binary");
7903 if (output == "layers")
7905 "-o layers dumps the stations and routing SolverLN BUILT from the model; lqns "
7906 "builds its own submodels inside another process and this port never sees them");
7907 if (kk.iter_tol >= 0.0 || kk.iter_max > 0)
7909 "--iter_tol and --iter_max are SolverLN's layer-iteration knobs; lqns runs its own "
7910 "iteration and takes neither (its --iteration-limit is unreliable as of 6.2.27, "
7911 "which is why the reference stopped passing it)");
7912 if (kk.seed)
7914 "--seed sets the stream of a simulator this port drives; lqsim seeds itself and "
7915 "the wrapper passes no seed, exactly as the reference does not");
7916 if (kk.repeat > 1)
7918 "--repeat times a solve by re-running it; re-running lqsim would report a "
7919 "different answer under the same banner");
7920 if (kk.no_interlocking || kk.interlock_knobs_given() || !kk.ln_transient.empty() || !kk.ln_transient_channels.empty() ||
7921 !kk.sens_method.empty() || !kk.sens_scheme.empty() || kk.sens_step > 0.0 || kk.nlp_knobs_given())
7923 "--no-interlocking, --interlock-*, --ln-transient*, --sens-*, --lld, --nlp-*, --warmstart and "
7924 "--mol-* are SolverLN options; -s lqns has none of them");
7925 if (arith != "double")
7926 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
7927 "SolverLQNS reads a result file another program wrote in decimal double "
7928 "precision; there is no higher precision to carry, so rerun with --arith double "
7929 "(got '" + arith + "')");
7930 // The Network-only knobs the SolverLN ladder below refuses are refused
7931 // HERE TOO. This branch returns before that ladder runs, so a knob left
7932 // out of it is silently DROPPED rather than refused -- and which branch
7933 // a bare `.lqnx` path takes depends on whether an lqns binary is
7934 // installed, so the same command line would be refused on one machine
7935 // and quietly ignored on another.
7936 if (kk.has_cutoff())
7938 "--cutoff bounds the open population of a CTMC state space and applies to -s "
7939 "ctmc; the layered path enumerates no states");
7940 if (kk.node)
7942 "--node selects the stateful node a CTMC query is labelled by; the layered path "
7943 "reports every LQN element");
7944 return run_lqns<double>(file, output, kk);
7945 }
7946 if (k.keep || k.verbose || k.remote || !k.remote_url.empty() || k.timeout_seconds)
7948 "--keep, --verbose, --remote, --remote-url and --timeout describe the child process "
7949 "of an external solver and apply to -s lqns only");
7950 // ---- the native LN simulator, BEFORE the SolverLN knob ladder ----------
7951 // It solves no LAYERS, so the ladder below asks its questions of a
7952 // decomposition this arm never builds: --iter_tol and --iter_max bound a
7953 // fixed point it does not iterate, --layer-solver names an engine it does
7954 // not run, and --samples and --seed -- which the ladder refuses outright
7955 // unless a layer is simulated -- are precisely this arm's two settings.
7956 if (engine == "ldes") {
7957 if (analysis != "avg" && analysis != "cdf")
7958 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
7959 "the native LN engine measures a sample path: it takes -a avg for the mean table "
7960 "and -a cdf for the per-entry response time distribution, and has no transient "
7961 "and no sensitivity here (got '" + analysis + "')");
7962 if (arith != "double")
7963 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
7964 "the native LN engine accumulates its estimators in double, so there is no higher "
7965 "precision to carry; rerun with --arith double (got '" + arith + "')");
7966 if (output == "layers")
7968 "-o layers dumps the stations and routing SolverLN BUILT from the model; -s ldes "
7969 "simulates the layered semantics directly and builds no submodels");
7970 if (!kk.layer_solver.empty())
7971 throw line::InputError(
7972 "--layer-solver names the engine SolverLN runs on each layer; -s ldes solves no "
7973 "layers, it simulates entries, activities and calls directly");
7974 if (kk.iter_tol >= 0.0 || kk.iter_max > 0 || kk.no_interlocking || kk.interlock_knobs_given())
7976 "--iter_tol, --iter_max, --no-interlocking and --interlock-* are SolverLN's layer-iteration "
7977 "knobs; a simulated sample path converges by run length, which is --samples");
7978 if (!kk.ln_transient.empty() || !kk.ln_transient_channels.empty() ||
7979 !kk.sens_method.empty() || !kk.sens_scheme.empty() || kk.sens_step > 0.0 || kk.nlp_knobs_given())
7981 "--ln-transient*, --sens-*, --lld, --nlp-*, --warmstart and --mol-* are SolverLN options; "
7982 "-s ldes has none of them");
7983 if (!kk.method.empty() && kk.method != "default")
7985 "--method on the layered path names the LN UPDATE (default, moment3, mw.*); "
7986 "-s ldes performs no update, it simulates the model (got '" + kk.method + "')");
7987 // The --ldes-* family is the SUBPROCESS engine's settings. The native LN
7988 // engine reads samples, events and seed and nothing else, so a warmup
7989 // filter or a CI estimator passed here would be DROPPED rather than
7990 // honoured -- and a dropped `--ldes-tranfilter none` reads as a run with
7991 // no warmup removal that in fact removed one.
7992 if (!kk.ldes_tranfilter.empty() || kk.ldes_warmupfrac >= 0.0 ||
7993 !kk.ldes_cimethod.empty() || kk.ldes_cnvgon || kk.ldes_cnvgtol > 0.0 ||
7994 kk.ldes_slotted || kk.ldes_slotlength > 0.0 || kk.ldes_replications > 0 ||
7995 kk.ldes_numthreads > 0 || kk.ldes_maxtime > 0.0 || !kk.ldes_initsol.empty() ||
7996 !kk.ldes_rest_url.empty())
7998 "the --ldes-* flags configure the SUBPROCESS engine that answers -s ldes on a "
7999 "Network (warmup filter, CI estimator, slot lattice, replications, warm-start "
8000 "placement); the native LN engine behind -i lqnx -s ldes reads --samples and "
8001 "--seed only");
8002 // The Network-only knobs the SolverLN ladder refuses below are refused
8003 // HERE TOO: this branch returns before that ladder runs, so a knob left
8004 // out of it would be silently dropped rather than refused.
8005 if (kk.has_cutoff())
8007 "--cutoff bounds the open population of a CTMC state space and applies to -s "
8008 "ctmc; the layered path enumerates no states");
8009 if (kk.node)
8011 "--node selects the stateful node a CTMC query is labelled by; the layered path "
8012 "reports every LQN element");
8013 if (kk.t1 >= 0.0)
8015 "--tspan sets the horizon of a transient analysis; the native LN engine runs to "
8016 "a completion budget, which is --samples");
8017 if (kk.tol >= 0.0)
8019 "--tol is not an LDES option; the run length is set with --samples");
8020 if (analysis == "cdf") return run_ln_ldes_cdf(file, output, kk);
8021 return run_ln_ldes(file, output, kk);
8022 }
8023 // The solver method name and --layer-solver name the same choice, so they may not
8024 // disagree: silently letting one win would report the other in the banner.
8025 if (s == "ln.comom") {
8026 if (!kk.layer_solver.empty() && kk.layer_solver != "nc")
8027 throw line::InputError("-s ln.comom already selects NC layers, but --layer-solver says '" +
8028 kk.layer_solver + "'");
8029 kk.layer_solver = "nc";
8030 } else if (s == "ln.mva") {
8031 if (!kk.layer_solver.empty() && kk.layer_solver != "mva")
8032 throw line::InputError("-s ln.mva already selects MVA layers, but --layer-solver says '" +
8033 kk.layer_solver + "'");
8034 kk.layer_solver = "mva";
8035 }
8036 // --layer-solver is this port's own flag, the C++ spelling of the
8037 // reference's solver FACTORY: `LN(model, @(m) MVA(m))` against
8038 // `LN(model, @(m) Fluid(m))`. They converge to DIFFERENT fixed points.
8039 if (!kk.layer_solver.empty() && kk.layer_solver != "mva" && kk.layer_solver != "nc" &&
8040 kk.layer_solver != "fluid" && kk.layer_solver != "ssa" && kk.layer_solver != "mam" &&
8041 kk.layer_solver != "ctmc" && kk.layer_solver != "ldes" && kk.layer_solver != "jmt")
8042 throw line::InputError("--layer-solver takes mva, nc, fluid, ssa, mam, ctmc, ldes or jmt (got '" +
8043 kk.layer_solver + "')");
8044 // ---- knobs the layered path does not have are refused, not dropped ----
8045 if ((k.samples || k.seed) && kk.layer_solver != "ssa" && kk.layer_solver != "ldes" &&
8046 kk.layer_solver != "jmt")
8048 "--samples and --seed set the run length and the stream of a SIMULATED layer; the "
8049 "layered path draws no random numbers unless --layer-solver ssa, ldes or jmt is in force");
8050 if (k.has_cutoff())
8052 "--cutoff bounds the open population of a CTMC state space and applies to -s ctmc; the "
8053 "layered path enumerates no states");
8054 if (k.t1 >= 0.0 && analysis != "tran")
8056 "--tspan sets the horizon of a transient analysis and applies to the layered path "
8057 "only with -a tran");
8058 if (analysis == "tran" && !(k.t1 >= 0.0))
8059 throw line::InputError(
8060 "-a tran integrates each layer's drift and needs a horizon: pass --tspan <t0>:<t1>");
8061 if (k.node)
8063 "--node selects the stateful node a CTMC query is labelled by; the layered path "
8064 "reports every LQN element");
8065 if (k.tol >= 0.0)
8067 "--tol is not a SolverLN option (LnOptions carries iter_tol and iter_max); "
8068 "use --iter_tol");
8069 // --method names the LN METHOD, which is a different question from the layer
8070 // engine: the srvn* / flat* names choose the LAYERING and the ENCODING
8071 // (SolverLN.listValidMethods, with the aliases ln_requested_method folds),
8072 // `moment3` reports a distribution the default never forms, the two bound
8073 // requests report a bound instead of a fixed point, and `nlp` builds no
8074 // layers at all, rooting the model as one program.
8075 {
8076 static const char* const ln_methods[] = {
8077 "default", "moment3", "mw.upper", "mw.lower", "srvn", "srvn.ph", "srvn.cs",
8078 "flat", "flat.cs", "flat.ph", "srvncs", "ph", "cs", "flatcs", "flatph",
8079 "squashed", "squashed.ph", "nlp", "qdamva", "qd-amva"};
8080 bool known = k.method.empty();
8081 for (const char* m : ln_methods)
8082 if (k.method == m) known = true;
8083 if (!known)
8085 "--method on the layered path takes default, srvn, srvn.ph, srvn.cs, flat, "
8086 "flat.cs, flat.ph, moment3, mw.upper, mw.lower or nlp (alias qdamva, qd-amva) (got '" +
8087 k.method + "'); the per-layer engine is chosen with --layer-solver");
8088 }
8089 if ((k.method == "nlp" || k.method == "qdamva" || k.method == "qd-amva") &&
8090 (analysis != "avg" || output == "layers"))
8092 "--method nlp states the model as one program and builds no layers, so it has no transient, "
8093 "no per-layer sensitivity, no response-time law and no layer to dump; use -a avg");
8094 if ((k.method == "mw.upper" || k.method == "mw.lower") && analysis != "avg")
8096 "--method mw.* reports a throughput and utilization BOUND and solves no layer, so "
8097 "it has no transient, no sensitivity and no response-time law; use -a avg");
8098 if (!k.sens_method.empty() && analysis != "sens")
8099 throw line::UnsupportedError(line::ErrorKind{"Option"}, "--sens-method applies to -a sens");
8100 if (!k.ln_transient.empty() && analysis != "tran")
8101 throw line::UnsupportedError(line::ErrorKind{"Option"}, "--ln-transient applies to -a tran");
8102
8103 if (analysis == "tran") {
8104 if (arith != "double")
8105 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
8106 "the layered transient integrates each layer's drift with LSODA, which is double "
8107 "precision by construction; rerun with --arith double (got '" + arith + "')");
8108 return run_ln_tran<double>(file, output, kk);
8109 }
8110 if (analysis == "cdf") {
8111 if (arith != "double")
8112 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
8113 "-a cdf fits an APH to a fluid passage time, integrated by LSODA in double "
8114 "precision; rerun with --arith double (got '" + arith + "')");
8115 return run_ln_cdf<double>(file, output, kk);
8116 }
8117 if (analysis == "sens") {
8118 if (arith == "double") return run_ln_sens<double>(file, output, kk);
8119 if (arith == "exact") return run_ln_sens<line::Rational>(file, output, kk);
8120 if (arith == "real:16") return run_ln_sens<line::Real<16> >(file, output, kk);
8121 if (arith == "real" || arith == "real:32")
8122 return run_ln_sens<line::Real<32> >(file, output, kk);
8123 if (arith == "real:64") return run_ln_sens<line::Real<64> >(file, output, kk);
8124 if (arith == "real:128") return run_ln_sens<line::Real<128> >(file, output, kk);
8125 if (arith == "real:256") return run_ln_sens<line::Real<256> >(file, output, kk);
8126 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8127 }
8128
8129 if (arith == "double") return run_ln<double>(file, output, kk);
8130 if (arith == "exact") return run_ln<line::Rational>(file, output, kk);
8131 // precision-ladder rationale (real:16 rung): see _kb/14-cpp-multiprecision.md
8132 if (arith == "real:16") return run_ln<line::Real<16> >(file, output, kk);
8133 if (arith == "real" || arith == "real:32") return run_ln<line::Real<32> >(file, output, kk);
8134 if (arith == "real:64") return run_ln<line::Real<64> >(file, output, kk);
8135 if (arith == "real:128") return run_ln<line::Real<128> >(file, output, kk);
8136 if (arith == "real:256") return run_ln<line::Real<256> >(file, output, kk);
8137 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8138}
8139
8140// Shared with the example corpus, which prints the same node tables.
8142
8143/**
8144 * The LDES result document mapped onto the station AvgResult, BY NAME.
8145 *
8146 * The engine reports its own station and class order, and pairing the two off
8147 * positionally would put one station's numbers on another's row; `-a avg`
8148 * already matches by name for exactly that reason and this is the same match.
8149 * `WN` is recomputed rather than taken, for the reason the `-a avg` arm states:
8150 * the engine counts one visit per station, so its residence time is the response
8151 * time whenever a visit ratio is not 1.
8152 *
8153 * REGION ROWS RIDE PAST THE STATIONS, in the (M+F) layout `jmt_map_measures`
8154 * already uses, so a caller reads both engines' finite-capacity rows the same
8155 * way.
8156 */
8157inline line::mva::AvgResult<double> avg_result_from_ldes(
8159 const std::size_t M = sn.nstations, K = sn.nclasses;
8160 const std::size_t F = a.QNfcr.empty() ? 0 : a.QNfcr.rows();
8162 line::Matrix<double>* dst[6] = {&r.QN, &r.UN, &r.RN, &r.TN, &r.AN, &r.WN};
8163 const line::Matrix<double>* src[6] = {&a.QN, &a.UN, &a.RN, &a.TN, &a.AN, &a.WN};
8164 const line::Matrix<double>* fcr[6] = {&a.QNfcr, &a.UNfcr, &a.RNfcr,
8165 &a.TNfcr, &a.ANfcr, &a.WNfcr};
8166 std::vector<std::size_t> st_of(a.station_names.size(), 0); // 1-based, 0 = unmatched
8167 for (std::size_t i = 0; i < a.station_names.size(); ++i)
8168 for (std::size_t j = 0; j < M; ++j)
8169 if (sn.stations[j].name == a.station_names[i]) { st_of[i] = j + 1; break; }
8170 std::vector<std::size_t> cl_of(a.class_names.size(), 0);
8171 for (std::size_t c = 0; c < a.class_names.size(); ++c)
8172 for (std::size_t j = 0; j < K; ++j)
8173 if (sn.classes[j].name == a.class_names[c]) { cl_of[c] = j + 1; break; }
8174 for (int m = 0; m < 6; ++m) {
8175 *dst[m] = line::Matrix<double>(M + F, K, 0.0);
8176 for (std::size_t i = 0; i < a.station_names.size(); ++i)
8177 for (std::size_t c = 0; c < a.class_names.size(); ++c)
8178 if (st_of[i] && cl_of[c])
8179 (*dst[m])(st_of[i] - 1, cl_of[c] - 1) = ldes_at(*src[m], i, c);
8180 for (std::size_t f = 0; f < F; ++f)
8181 for (std::size_t c = 0; c < a.class_names.size(); ++c)
8182 if (cl_of[c]) (*dst[m])(M + f, cl_of[c] - 1) = ldes_at(*fcr[m], f, c);
8183 }
8184 line::Matrix<double> RNs(M, K, 0.0);
8185 for (std::size_t i = 0; i < M; ++i)
8186 for (std::size_t c = 0; c < K; ++c) RNs(i, c) = r.RN(i, c);
8188 for (std::size_t i = 0; i < M; ++i)
8189 for (std::size_t c = 0; c < K; ++c) r.WN(i, c) = WNs(i, c);
8190 for (std::size_t c = 0; c < K; ++c) {
8191 r.CN.push_back(ldes_at(a.CN, 0, c));
8192 r.XN.push_back(ldes_at(a.XN, 0, c));
8193 }
8194 r.method = a.method;
8195 r.actualmethod = a.method;
8196 return r;
8197}
8198
8199/**
8200 * Solve for the station AvgResult with the named engine.
8201 *
8202 * SHARED BY EVERY @@NetworkSolver TABLE THAT IS NOT THE AvgTable -- `-a node`,
8203 * `-a sys`, `-a chain`, `-a nodechain` -- because each of them is a VIEW of the
8204 * same solved result and differs only in how it is indexed and aggregated.
8205 * Giving each arm its own engine ladder would let the five drift apart in which
8206 * knobs they honour, which is exactly the silent divergence the Knobs struct
8207 * exists to prevent one layer up.
8208 *
8209 * SSA and Fluid reach it through `avg_result_from_sim`, and only under
8210 * `--arith double`: both integrate transcendental quantities (exponential
8211 * clocks, an LSODA drift), so the dispatcher refuses the other backends by name
8212 * before the ladder is entered, exactly as their own `-a avg` arms do.
8213 *
8214 * THE TWO WRAPPERS ARE HERE FOR THE SAME REASON THE TWO SIMULATORS ARE.
8215 * `getAvgNodeTable` is @@NetworkSolver's and is a VIEW of whatever AvgResult a
8216 * solver returned; JMT and LDES both return one (`JmtResult::avg`, the LDES
8217 * result document), so refusing them the four views said "no C++ simulation
8218 * engine" about engines this port drives. `file` is what LDES needs and the
8219 * others ignore: it hands the model DOCUMENT to an external process rather
8220 * than reading the struct.
8221 */
8222template <class T>
8223line::mva::AvgResult<T> run_avg_engine(const line::qn::NetworkStruct<T>& sn, const Knobs& k,
8224 const std::string& s, std::string& banner,
8225 std::string* suffix = nullptr,
8226 const std::string* file = nullptr) {
8228 if (s == "jmt" || s == "ldes") {
8229 // COMPILE-TIME, as for the two simulators: both wrappers report in
8230 // double and the dispatcher has already refused every other arithmetic
8231 // by name, so the discarded branch is unreachable rather than narrowed.
8232 if constexpr (std::is_same_v<T, double>) {
8233 if (s == "jmt") {
8235 if (!k.method.empty() && k.method != "default") o.method = k.method;
8236 if (k.samples > 0) o.samples = static_cast<double>(k.samples);
8237 if (k.seed != 0) o.seed = static_cast<long>(k.seed);
8238 o.keep = k.keep;
8239 if (k.t1 >= 0.0) o.max_simulated_time = k.t1;
8240 o.verbose = k.verbose;
8242 r = a.avg;
8243 banner = "SolverJMT";
8244 // THE SEED IS PART OF THE ANSWER, as it is for SSA: two runs of
8245 // a simulation are the same measurement only if both state it.
8246 if (suffix) {
8247 char buf[128];
8248 std::snprintf(buf, sizeof(buf), " samples=%g seed=%ld", o.samples, o.seed);
8249 *suffix = buf;
8250 }
8251 } else {
8252 const line::ldes::LdesOptions o = ldes_options(k);
8253 const line::ldes::LdesResult a =
8254 ldes_run(file ? *file : std::string(), o, std::vector<std::string>());
8255 r = avg_result_from_ldes(sn, a);
8256 r.fj = line::ldes::ldes_fj_metrics(sn, a); // the engine's "forkJoin" block, for -a fj
8257 banner = "SolverLDES";
8258 if (suffix) {
8259 char buf[160];
8260 std::snprintf(buf, sizeof(buf), " engine=%s samples=%zu seed=%ld",
8261 a.engine.empty() ? "?" : a.engine.c_str(), o.samples, o.seed);
8262 *suffix = buf;
8263 }
8264 }
8265 } else {
8266 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"}, "-s " + s + " runs under --arith double only");
8267 }
8268 } else if (s == "ssa" || s == "fluid") {
8269 // COMPILE-TIME, not just run-time: both runners static_assert on
8270 // transcendental arithmetic inside (an exponential clock, a square root
8271 // in the Cox refit), so instantiating them at Rational is a hard error
8272 // and not a refusal. The dispatcher has already rejected every arith
8273 // but double by name, so the discarded branch is unreachable rather
8274 // than silently narrowed.
8275 if constexpr (std::is_same_v<T, double>) {
8276 if (s == "ssa") {
8278 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8279 if (k.samples) opt.samples = k.samples;
8280 if (k.seed) opt.seed = k.seed;
8281 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
8282 // The cache write-back rides beside the metric table, for the
8283 // reason `node_metrics` states: the realized hit and miss shares
8284 // are what the simulation MEASURED, and without them the node
8285 // table falls back to the split `link()` offered.
8286 std::vector<line::ssa::SsaCacheRatio> cache;
8288 // ResidT and ArvR are derived from the VISITS, and a cache's
8289 // split is routing, so both are taken on the struct carrying the
8290 // measured hit/miss shares rather than on `link()`'s even offer.
8291 r = avg_result_from_sim<T>(line::ssa::sn_with_ssa_cache_split<T>(sn, cache),
8292 a.QN, a.UN, a.RN, a.TN, a.CN, a.XN, a.method);
8294 // THE SEED AND THE SAMPLE COUNT ARE PART OF THE ANSWER, not of
8295 // the invocation, so they ride in the banner here as they do in
8296 // `-a avg`: two runs of a simulation are the same measurement
8297 // only if both are stated, and a parity row that quotes one of
8298 // these tables has to carry them with it.
8299 banner = "SolverSSA";
8300 // APPENDED, NOT PREFIXED. Every consumer recognises a banner by
8301 // `Solver<name> arith=`, so a fact wedged between the two makes
8302 // the table belong to no solver at all -- which is how the
8303 // parity harness lost the whole SSA section.
8304 if (suffix) {
8305 char buf[128];
8306 std::snprintf(buf, sizeof(buf), " samples=%zu seed=%lu time=%.6g", a.samples,
8307 static_cast<unsigned long>(opt.seed), a.simulated_time);
8308 *suffix = buf;
8309 }
8310 } else {
8311 const line::fluid::FluidOptions opt = fluid_options(k);
8312 // THE map_env FALLBACK IS ASKED FOR HERE RATHER THAN THROUGH
8313 // `run_avg`: the fluid runner returns a `FluidSolution` and not
8314 // an `AvgResult`, so this arm cannot pass itself as the `Run`
8315 // callable. It asks `needs_map_env` the same question with the
8316 // same config instead of carrying a second copy of the decision.
8317 const line::solvers::MapEnvConfig mecfg = map_env_config(k);
8319 mecfg)
8320 .needed) {
8321 r = line::solvers::map_env_approx<T>(sn, "SolverFluid", mecfg,
8323 opt.method);
8324 banner = "SolverFluid";
8325 if (suffix) *suffix = " map_env=" + r.actualmethod;
8326 return r;
8327 }
8328 // The cache decomposition renormalizes the self-switch at the
8329 // converged split; `-a node` needs that struct or it reports the
8330 // 1/2-1/2 `link()` left behind (see `node_metrics`).
8332 // The null must be typed: a bare `nullptr` is `std::nullptr_t`
8333 // and blocks deduction of T from the fourth argument.
8334 // THE SPLIT ITSELF IS TAKEN TOO, not only the struct it
8335 // renormalized: `node_metrics` prefers the stated split over the
8336 // visit ratios, and `-a cache` is built from nothing else.
8339 sn, opt, static_cast<line::qn::NetworkStruct<T>*>(nullptr), &refreshed, &cache);
8340 // Non-empty only where the cache branch ran, which is the same
8341 // test `node_metrics` makes on the pointer. IT IS ALSO THE
8342 // STRUCT THE ARRIVAL RATE IS READ FROM: that column is derived
8343 // from the class-expanded routing, and the base struct still
8344 // holds the 1/2-1/2 self-switch `link()` offered, so deriving it
8345 // there reports 0.5/0.5 where cache_replc_routing's Delay1 sees
8346 // 0.4/0.6. Every other column is indexed by station and is the
8347 // same in both structs.
8348 const bool has_ref = !refreshed.nodes.empty();
8349 r = avg_result_from_sim<T>(has_ref ? refreshed : sn, a.QN, a.UN, a.RN, a.TN, a.CN,
8350 a.XN, a.method);
8351 if (has_ref) r.refreshed_struct.reset(new line::qn::NetworkStruct<T>(refreshed));
8352 r.cache = cache;
8353 r.fj = a.fj; // the fork-join fixed point's section metrics, for -a fj
8354 r.iter = static_cast<int>(a.iters);
8355 banner = "SolverFluid";
8356 if (suffix) {
8357 char buf[64];
8358 std::snprintf(buf, sizeof(buf), " iters=%zu", a.iters);
8359 *suffix = buf;
8360 }
8361 }
8362 } else {
8363 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"}, "-s " + s + " runs under --arith double only");
8364 }
8365 } else if (s == "nc") {
8367 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8368 if (k.tol >= 0.0) opt.tol = k.tol;
8369 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
8370 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
8371 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
8372 // The map_env fallback, wired here for the reason this engine exists at
8373 // all: these views ARE the `getAvg` funnel of this CLI, so a view that
8374 // refused a MAP model the `-a avg` arm accepts would answer a different
8375 // model for the same command line.
8376 // COMPILE-TIME because the driver is double-only: `map_env_approx`
8377 // refuses `T != double` at run time, but its body still has to compile,
8378 // and an `EnvStageAvgFn<double>` does not convert to an
8379 // `EnvStageAvgFn<Rational>`.
8380 if constexpr (std::is_same_v<T, double>) {
8382 sn, "SolverNC", line::qn::nc_feature_set(opt.method), map_env_config(k),
8385 },
8387 } else {
8389 }
8390 banner = "SolverNC";
8391 } else if (s == "mam") {
8392 // The same builder as every other `-s mam` entry point, so --timescale,
8393 // --slotlength and the fork-join knobs reach -a avg too.
8394 const line::mam::MamOptions opt = mam_options(k);
8396 banner = "SolverMAM";
8397 } else if (s == "ag") {
8399 apply_ag_knobs(k, opt);
8401 banner = "SolverAG";
8402 } else if (s == "ba") {
8404 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8406 banner = "SolverBA";
8407 } else if (s == "ctmc") {
8409 if (!k.method.empty()) opt.method = k.method;
8410 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
8411 opt.cutoff_mat = k.cutoff_mat;
8412 opt.force = k.force;
8413 if (!k.nonmkv.empty()) opt.nonmkv = k.nonmkv;
8416 banner = "SolverCTMC";
8417 } else {
8419 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8420 if (k.tol >= 0.0) opt.tol = k.tol;
8421 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
8422 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
8423 // `-a avg` has honoured --multiserver since the flag existed; these
8424 // views are the SAME solve indexed differently, so ignoring it here made
8425 // `-a chain` answer a different model than `-a avg` for the same command
8426 // line. Measured on cqn_repairmen_multi, where softmin and the default
8427 // rule differ by 39% on the Delay queue length.
8428 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
8429 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
8430 line::Matrix<T> init;
8431 // Same funnel, same compile-time guard as the nc arm above.
8432 if constexpr (std::is_same_v<T, double>) {
8434 sn, "SolverMVA",
8436 map_env_config(k),
8437 [&opt, &init](const line::qn::NetworkStruct<double>& m) {
8438 return line::mva::solver_mva_run_analyzer(m, opt, init);
8439 },
8441 } else {
8443 }
8444 banner = "SolverMVA";
8445 }
8446 return r;
8447}
8448
8449/* `NodeMetrics` / `node_metrics` MOVED to line/solvers/solver_node_tables.h;
8450 * see the note above `run_avg_engine`. Used unchanged by both arms below. */
8453
8454/**
8455 * `-a node`: `@@NetworkSolver/getAvgNodeTable`, the means per NODE.
8456 *
8457 * A DIFFERENT INDEX SPACE FROM `-a avg`, not a relabelling of it. The AvgTable
8458 * is indexed by STATION, so a ClassSwitch, a Router, a Fork, a Join and a Sink
8459 * are absent from it entirely -- they hold no jobs, so they have no row -- yet
8460 * jobs flow through them and the flow is what a caller sizing a link or an
8461 * interconnect needs. This table has one row per node and reports the arrival
8462 * rate and the throughput at every one of them, which is the only place those
8463 * two numbers exist for a non-station node.
8464 *
8465 * QLen, Util, RespT and ResidT ARE the station numbers, scattered to the node
8466 * indices and left at zero elsewhere; that is the reference's own construction
8467 * and not a gap, because a node that is not a station holds no jobs and serves
8468 * nothing. ArvR and Tput are the two the reference recomputes, through
8469 * `sn_get_node_arvr_from_tput` and `sn_get_node_tput_from_tput`.
8470 *
8471 * THE F REGION PSEUDO-NODE ROWS OF THE REFERENCE ARE NOT EMITTED. MATLAB
8472 * appends one row per finite-capacity region, filled from `result.Avg` rows
8473 * M+1..M+F; the C++ `AvgResult` carries no per-region queue length or
8474 * utilization, so those rows have no data source here and are omitted rather
8475 * than fabricated as zeros, which would read as an empty region.
8476 */
8477template <class T>
8478int solve_model_node(const std::string& file, const Knobs& k, const std::string& s) {
8479 line::qn::Network<T> net = read_model<T>(file);
8481 std::string banner, suffix;
8482 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8483
8484 const std::size_t I = sn.nodes.size(), R = sn.nclasses;
8485 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
8486 const line::Matrix<T>&QNn = nm.QN, &UNn = nm.UN, &RNn = nm.RN, &WNn = nm.WN, &ANn = nm.AN,
8487 &TNn = nm.TN;
8488
8489 const std::vector<bool> hidden = line::solvers::node_table_hidden_classes<T>(sn);
8490 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
8491 // A FINITE CAPACITY REGION IS NOT A NODE, and the reference still prints it
8492 // in this table: `getAvgNodeTable` appends one row per region past the
8493 // nodes, because a WAITQ region holds jobs that are in no station's QLen and
8494 // the model's population only balances once they are read. The rows ride
8495 // past the stations in the returned AvgResult -- the (M+F) layout both
8496 // wrappers report -- and no analytical solver fills them, so this block is
8497 // empty for every engine that does not measure a region.
8498 //
8499 // Util AND ArvR ARE NaN, NOT ZERO. A region has no server to be busy and no
8500 // arrival process of its own; the reference reports both as missing, and a
8501 // zero there would be a number the run never measured.
8502 const std::size_t F =
8503 r.QN.rows() > sn.nstations ? r.QN.rows() - sn.nstations : static_cast<std::size_t>(0);
8504 const double region_nan = std::numeric_limits<double>::quiet_NaN();
8505 auto region_name = [&](std::size_t f) {
8506 return (f < sn.regions.size() && !sn.regions[f].name.empty())
8507 ? sn.regions[f].name
8508 : "FCR" + std::to_string(f + 1);
8509 };
8510 // The reference's own filter, the region twin of the all-zero row test: a
8511 // region no job ever entered is absent rather than a row of zeros.
8512 auto region_empty = [&](std::size_t f, std::size_t c) {
8513 return !(d(r.QN(sn.nstations + f, c)) > 0.0 || d(r.RN(sn.nstations + f, c)) > 0.0 ||
8514 d(r.TN(sn.nstations + f, c)) > 0.0);
8515 };
8516 if (g_json_output) {
8517 // The banner under `-o json` too, for print_chain_table's reason: the
8518 // envelope names the arithmetic and the method but never the SOLVER.
8519 std::printf("%s arith=%s method=%s nodes=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
8520 r.actualmethod.c_str(), I, suffix.c_str());
8521 line::reg::Json p = line::reg::Json::object();
8522 p["type"] = "AvgNodeTable";
8523 p["indexBase"] = 0;
8524 for (const char* key : {"Node", "JobClass", "QLen", "Util", "RespT", "ResidT", "ArvR",
8525 "Tput"})
8526 p[key] = line::reg::Json::array();
8527 for (std::size_t i = 0; i < I; ++i)
8528 for (std::size_t c = 0; c < R; ++c) {
8529 if (hidden[c]) continue; // auxiliary retrieval class, see node_table_hidden_classes
8530 // The reference's own row filter: a node a class never reaches
8531 // is absent, not a row of zeros, exactly as in the AvgTable.
8532 if (d(QNn(i, c)) == 0.0 && d(UNn(i, c)) == 0.0 && d(RNn(i, c)) == 0.0 &&
8533 d(WNn(i, c)) == 0.0 && d(ANn(i, c)) == 0.0 && d(TNn(i, c)) == 0.0)
8534 continue;
8535 p["Node"].push_back(sn.nodes[i].name);
8536 p["JobClass"].push_back(sn.classes[c].name);
8537 p["QLen"].push_back(d(QNn(i, c)));
8538 p["Util"].push_back(d(UNn(i, c)));
8539 p["RespT"].push_back(d(RNn(i, c)));
8540 p["ResidT"].push_back(d(WNn(i, c)));
8541 p["ArvR"].push_back(d(ANn(i, c)));
8542 p["Tput"].push_back(d(TNn(i, c)));
8543 }
8544 for (std::size_t f = 0; f < F; ++f)
8545 for (std::size_t c = 0; c < R; ++c) {
8546 if (region_empty(f, c)) continue;
8547 p["Node"].push_back(region_name(f));
8548 p["JobClass"].push_back(sn.classes[c].name);
8549 p["QLen"].push_back(d(r.QN(sn.nstations + f, c)));
8550 p["Util"].push_back(region_nan);
8551 p["RespT"].push_back(d(r.RN(sn.nstations + f, c)));
8552 p["ResidT"].push_back(d(r.WN(sn.nstations + f, c)));
8553 p["ArvR"].push_back(region_nan);
8554 p["Tput"].push_back(d(r.TN(sn.nstations + f, c)));
8555 }
8556 emit_analysis<T>("node", p, r.actualmethod);
8557 return 0;
8558 }
8559 std::printf("%s arith=%s method=%s nodes=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
8560 r.actualmethod.c_str(), I, suffix.c_str());
8561 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s\n", "Node", "JobClass", "QLen", "Util",
8562 "RespT", "ResidT", "ArvR", "Tput");
8563 for (std::size_t i = 0; i < I; ++i)
8564 for (std::size_t c = 0; c < R; ++c) {
8565 if (hidden[c]) continue; // auxiliary retrieval class, see node_table_hidden_classes
8566 if (d(QNn(i, c)) == 0.0 && d(UNn(i, c)) == 0.0 && d(RNn(i, c)) == 0.0 &&
8567 d(WNn(i, c)) == 0.0 && d(ANn(i, c)) == 0.0 && d(TNn(i, c)) == 0.0)
8568 continue;
8569 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
8570 sn.nodes[i].name.c_str(), sn.classes[c].name.c_str(), d(QNn(i, c)),
8571 d(UNn(i, c)), d(RNn(i, c)), d(WNn(i, c)), d(ANn(i, c)), d(TNn(i, c)));
8572 }
8573 for (std::size_t f = 0; f < F; ++f)
8574 for (std::size_t c = 0; c < R; ++c) {
8575 if (region_empty(f, c)) continue;
8576 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
8577 region_name(f).c_str(), sn.classes[c].name.c_str(),
8578 d(r.QN(sn.nstations + f, c)), region_nan, d(r.RN(sn.nstations + f, c)),
8579 d(r.WN(sn.nstations + f, c)), region_nan, d(r.TN(sn.nstations + f, c)));
8580 }
8581 return 0;
8582}
8583
8584/**
8585 * `-a cache`: `@@NetworkSolver/getAvgCacheTable`, per Cache node and READ class.
8586 *
8587 * ONE TOTAL ROW PER (node, read class), plus one row per cache list where the
8588 * solver reported a per-list breakdown and the cache has more than one list.
8589 * The `List` column tells them apart: 0 is the total over every list, l is
8590 * list l. Only the total row carries the delayed-hit and miss columns, because
8591 * a miss is a property of the cache and not of any one list.
8592 *
8593 * THE ArvR COLUMN IS THE READ-CLASS ARRIVAL RATE `arvr` (= HitRate +
8594 * DHitRate + MissRate) on the total row as on a list row. That is the
8595 * reference's choice and it is Little-consistent with ResidT: the residence
8596 * time reported beside it is the expected latency averaged over ALL read
8597 * requests (paper eq. latency tot, a hit waiting zero), so ArvR * ResidT is the
8598 * mean number of requests in the retrieval system, sum_i (phi_i + d_i).
8599 *
8600 * A READ CLASS IS ONE WITH A HIT CLASS DEFINED. A class that never reads the
8601 * cache has no row at all rather than a row of zeros, which is the same rule
8602 * the AvgTable applies to a class that never visits a station.
8603 */
8604template <class T>
8605int solve_model_cache(const std::string& file, const Knobs& k, const std::string& s) {
8606 line::qn::Network<T> net = read_model<T>(file);
8608 std::string banner, suffix;
8609 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8610 if (r.cache.empty())
8611 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
8612 "-a cache reports the per-Cache hit and miss table and this model has no Cache node, "
8613 "or the solver that ran analyzes none; SolverNC's cache branches are what fill it");
8614
8615 // The read-class arrival rate of an OPEN class is its SOURCE throughput: every read request enters the cache, so
8616 // this holds across solvers, including the simulators where a delayed hit is not folded into the hit throughput.
8617 // A CLOSED read class has no source row: its requests are the cache's departures in the hit and miss classes.
8618 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
8619 std::size_t srcnode = 0;
8620 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
8621 if (sn.nodes[i].nodetype == line::lang::NodeType::Source) { srcnode = i + 1; break; }
8622
8623 struct Row {
8624 std::string node, cls;
8625 double item, list, listcap, items, hitp, dhitp, missp, hitr, dhitr, missr, arvr, residt, cost;
8626 };
8627 std::vector<Row> rows;
8628 const double dnan = std::numeric_limits<double>::quiet_NaN();
8629
8630 for (std::size_t c = 0; c < r.cache.caches.size(); ++c) {
8631 const line::solvers::CacheNodeMetrics<T>& m = r.cache.caches[c];
8632 const typename std::map<std::size_t, line::qn::CacheParam<T> >::const_iterator it =
8633 sn.nodeparam.find(m.node);
8634 if (it == sn.nodeparam.end()) continue;
8635 const std::vector<std::size_t>& hitclass = it->second.hitclass;
8636 const std::size_t h = m.itemcap.size();
8637 double totcap = 0.0;
8638 for (std::size_t l = 0; l < h; ++l) totcap += m.itemcap[l];
8639 double totcost = dnan;
8640 if (!m.listcost.empty()) {
8641 totcost = 0.0;
8642 for (std::size_t l = 0; l < m.listcost.size(); ++l)
8644 }
8645
8646 for (std::size_t cl = 0; cl < sn.nclasses; ++cl) {
8647 if (cl >= hitclass.size() || hitclass[cl] == 0) continue; // not a read class
8648 double ph = cache_at(m.hitprob, cl), pm = cache_at(m.missprob, cl),
8649 pd = cache_at(m.delayedprob, cl);
8650 if (std::isnan(ph) && std::isnan(pm) && std::isnan(pd)) continue;
8651 if (std::isnan(ph)) ph = 0.0;
8652 if (std::isnan(pm)) pm = 0.0;
8653 if (std::isnan(pd)) pd = 0.0;
8654 double arvr = 0.0;
8655 if (!std::isfinite(sn.classes[cl].population)) {
8656 if (srcnode) arvr = line::num_traits<T>::to_double(nm.TN(srcnode - 1, cl));
8657 } else {
8658 arvr = line::num_traits<T>::to_double(nm.TN(m.node - 1, hitclass[cl] - 1));
8659 if (cl < it->second.missclass.size() && it->second.missclass[cl] > 0)
8660 arvr += line::num_traits<T>::to_double(nm.TN(m.node - 1, it->second.missclass[cl] - 1));
8661 }
8662 const double lat = cache_at(m.latency, cl);
8663
8664 Row t;
8665 t.node = sn.nodes[m.node - 1].name;
8666 t.cls = sn.classes[cl].name;
8667 // item a per-item read class requests (setItemReadClasses), 0 for an ordinary read class
8668 t.item = cl < it->second.classitem.size() ? static_cast<double>(it->second.classitem[cl]) : 0.0;
8669 t.list = 0;
8670 t.listcap = totcap;
8671 t.items = static_cast<double>(m.nitems);
8672 t.hitp = ph;
8673 t.dhitp = pd;
8674 t.missp = pm;
8675 t.hitr = arvr * ph;
8676 t.dhitr = arvr * pd;
8677 t.missr = arvr * pm;
8678 t.arvr = arvr;
8679 t.residt = lat;
8680 t.cost = totcost;
8681 rows.push_back(t);
8682
8683 // Per-list rows, only where a genuine multi-list breakdown exists.
8684 bool any = false;
8685 if (h > 1 && cl < m.hitproblist.rows())
8686 for (std::size_t l = 0; l < m.hitproblist.cols(); ++l)
8687 if (!std::isnan(line::num_traits<T>::to_double(m.hitproblist(cl, l))))
8688 any = true;
8689 if (!any) continue;
8690 for (std::size_t l = 0; l < h; ++l) {
8691 double phl = l < m.hitproblist.cols()
8693 : dnan;
8694 if (std::isnan(phl)) phl = 0.0;
8695 Row u;
8696 u.node = t.node;
8697 u.cls = t.cls;
8698 u.item = t.item;
8699 u.list = static_cast<double>(l + 1);
8700 u.listcap = m.itemcap[l];
8701 u.items = t.items;
8702 u.hitp = phl;
8703 u.dhitp = dnan;
8704 u.missp = dnan;
8705 u.hitr = arvr * phl;
8706 u.dhitr = dnan;
8707 u.missr = dnan;
8708 u.arvr = arvr;
8709 u.residt = dnan;
8710 u.cost = l < m.listcost.size()
8712 : dnan;
8713 rows.push_back(u);
8714 }
8715 }
8716 }
8717
8718 if (g_json_output) {
8719 line::reg::Json p = line::reg::Json::object();
8720 p["type"] = "AvgCacheTable";
8721 p["indexBase"] = 0;
8722 for (const char* key : {"Node", "JobClass", "Item", "List", "ListCap", "Items", "HitProb",
8723 "DelayedHitProb", "MissProb", "HitRate", "DelayedHitRate",
8724 "MissRate", "ArvR", "ResidT", "ListCost"})
8725 p[key] = line::reg::Json::array();
8726 for (std::size_t i = 0; i < rows.size(); ++i) {
8727 p["Node"].push_back(rows[i].node);
8728 p["JobClass"].push_back(rows[i].cls);
8729 p["Item"].push_back(rows[i].item);
8730 p["List"].push_back(rows[i].list);
8731 p["ListCap"].push_back(rows[i].listcap);
8732 p["Items"].push_back(rows[i].items);
8733 p["HitProb"].push_back(rows[i].hitp);
8734 p["DelayedHitProb"].push_back(rows[i].dhitp);
8735 p["MissProb"].push_back(rows[i].missp);
8736 p["HitRate"].push_back(rows[i].hitr);
8737 p["DelayedHitRate"].push_back(rows[i].dhitr);
8738 p["MissRate"].push_back(rows[i].missr);
8739 p["ArvR"].push_back(rows[i].arvr);
8740 p["ResidT"].push_back(rows[i].residt);
8741 p["ListCost"].push_back(rows[i].cost);
8742 }
8743 emit_analysis<T>("cache", p, r.actualmethod);
8744 return 0;
8745 }
8746 std::printf("%s arith=%s method=%s caches=%zu\n", banner.c_str(), line::num_traits<T>::name(),
8747 r.actualmethod.c_str(), r.cache.caches.size());
8748 std::printf("%-14s %-12s %5s %5s %8s %6s %10s %10s %10s %10s %10s %10s %10s %10s %10s\n", "Node",
8749 "JobClass", "Item", "List", "ListCap", "Items", "HitProb", "DHitProb", "MissProb",
8750 "HitRate", "DHitRate", "MissRate", "ArvR", "ResidT", "ListCost");
8751 for (std::size_t i = 0; i < rows.size(); ++i)
8752 std::printf(
8753 "%-14s %-12s %5g %5g %8g %6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g "
8754 "%10.6g\n",
8755 rows[i].node.c_str(), rows[i].cls.c_str(), rows[i].item, rows[i].list, rows[i].listcap,
8756 rows[i].items, rows[i].hitp, rows[i].dhitp, rows[i].missp, rows[i].hitr,
8757 rows[i].dhitr, rows[i].missr, rows[i].arvr, rows[i].residt, rows[i].cost);
8758 return 0;
8759}
8760
8761/**
8762 * `-a item`: `@@NetworkSolver/getAvgItemTable`, one row per (Cache, item, list).
8763 *
8764 * THE PER-ITEM OCCUPANCY, which only a solver that computes a genuine per-item
8765 * distribution has: the exact NC cache recursions and the delayed-hit retrieval
8766 * algorithms. Every other branch measures the aggregate hit probability and
8767 * never forms the item law, and the arm refuses rather than filling the column
8768 * with the uniform guess that would reproduce the same aggregate.
8769 *
8770 * `Cost` is `Size * Prob`, so summing it over the items of a list reproduces
8771 * that list's ListCost in the cache table -- which is what makes the two tables
8772 * checkable against each other.
8773 */
8774template <class T>
8775int solve_model_item(const std::string& file, const Knobs& k, const std::string& s) {
8776 line::qn::Network<T> net = read_model<T>(file);
8778 std::string banner, suffix;
8779 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8780 if (r.cache.empty())
8781 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
8782 "-a item reports the per-item cache occupancy and this model has no Cache node, or "
8783 "the solver that ran analyzes none");
8784
8785 const double dnan = std::numeric_limits<double>::quiet_NaN();
8786 struct Row {
8787 std::string node;
8788 double item, list, listcap, size, prob, cost, dhq, dhqf;
8789 };
8790 std::vector<Row> rows;
8791 for (std::size_t c = 0; c < r.cache.caches.size(); ++c) {
8792 const line::solvers::CacheNodeMetrics<T>& m = r.cache.caches[c];
8793 const std::size_t h = m.itemcap.size();
8794 // EITHER measurement earns the item its rows. A solver may form the
8795 // per-item occupancy (the NC and MVA cache recursions) or the per-item
8796 // delayed-hit queue length (the exact chain) and not the other, and
8797 // requiring both would drop the CTMC's whole table.
8798 if (h == 0 || (m.itemprob.rows() == 0 && m.delayedhitqlen.empty())) continue;
8799 const std::size_t nit =
8800 m.itemprob.rows() > 0 ? m.itemprob.rows() : m.delayedhitqlen.size();
8801 for (std::size_t i = 0; i < nit; ++i)
8802 for (std::size_t l = 0; l < h; ++l) {
8803 Row t;
8804 t.node = sn.nodes[m.node - 1].name;
8805 t.item = static_cast<double>(i + 1);
8806 t.list = static_cast<double>(l + 1);
8807 t.listcap = m.itemcap[l];
8808 t.size = i < m.itemsize.size() ? m.itemsize[i] : dnan;
8809 // Column 0 of `itemprob` is the MISS column, so list l is column
8810 // l+1; reading it as l would report every item one list too low.
8811 t.prob = (l + 1) < m.itemprob.cols()
8813 : dnan;
8814 t.cost = t.size * t.prob;
8815 t.dhq = i < m.delayedhitqlen.size()
8817 : dnan;
8818 t.dhqf = i < m.delayedhitqlenfull.size()
8820 : dnan;
8821 rows.push_back(t);
8822 }
8823 }
8824 if (rows.empty())
8825 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
8826 "-a item needs a per-item occupancy law and this solve produced none; the NC/MVA "
8827 "cache recursions (isolated and integrated alike) and the delayed-hit retrieval "
8828 "algorithms compute the embedded one, SolverCTMC the time-weighted one, and the "
8829 "simulators none");
8830
8831 if (g_json_output) {
8832 line::reg::Json p = line::reg::Json::object();
8833 p["type"] = "AvgItemTable";
8834 p["indexBase"] = 0;
8835 for (const char* key : {"Node", "Item", "List", "ListCap", "Size", "Prob", "Cost",
8836 "DelayedHitQLen", "DelayedHitQLenFull"})
8837 p[key] = line::reg::Json::array();
8838 for (std::size_t i = 0; i < rows.size(); ++i) {
8839 p["Node"].push_back(rows[i].node);
8840 p["Item"].push_back(rows[i].item);
8841 p["List"].push_back(rows[i].list);
8842 p["ListCap"].push_back(rows[i].listcap);
8843 p["Size"].push_back(rows[i].size);
8844 p["Prob"].push_back(rows[i].prob);
8845 p["Cost"].push_back(rows[i].cost);
8846 p["DelayedHitQLen"].push_back(rows[i].dhq);
8847 p["DelayedHitQLenFull"].push_back(rows[i].dhqf);
8848 }
8849 emit_analysis<T>("item", p, r.actualmethod);
8850 return 0;
8851 }
8852 std::printf("%s arith=%s method=%s rows=%zu\n", banner.c_str(), line::num_traits<T>::name(),
8853 r.actualmethod.c_str(), rows.size());
8854 std::printf("%-14s %6s %6s %8s %10s %12s %12s %14s %18s\n", "Node", "Item", "List", "ListCap",
8855 "Size", "Prob", "Cost", "DelayedHitQLen", "DelayedHitQLenFull");
8856 for (std::size_t i = 0; i < rows.size(); ++i)
8857 std::printf("%-14s %6g %6g %8g %10g %12.8g %12.8g %14.8g %18.8g\n", rows[i].node.c_str(),
8858 rows[i].item, rows[i].list, rows[i].listcap, rows[i].size, rows[i].prob,
8859 rows[i].cost, rows[i].dhq, rows[i].dhqf);
8860 return 0;
8861}
8862
8863/**
8864 * `-a sys`: `@@NetworkSolver/getAvgSysTable`, one row per CHAIN.
8865 *
8866 * SysRespT is the chain's CYCLE TIME and SysTput the flow that completes it,
8867 * both measured at the chain's reference station -- not a column of the
8868 * AvgTable summed up. On a closed chain the two are tied by Little's law and
8869 * the table is the standard capacity-planning view: N = X * R.
8870 */
8871template <class T>
8872int solve_model_sys(const std::string& file, const Knobs& k, const std::string& s) {
8873 line::qn::Network<T> net = read_model<T>(file);
8875 std::string banner, suffix;
8876 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8878 const std::vector<std::string> cn = line::solvers::chain_names(sn.nchains);
8879 const std::vector<std::string> cc = line::solvers::chain_class_labels<T>(sn);
8880 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
8881
8882 if (g_json_output) {
8883 // The banner under `-o json` too, for print_chain_table's reason: the
8884 // envelope names the arithmetic and the method but never the SOLVER.
8885 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
8886 r.actualmethod.c_str(), cn.size(), suffix.c_str());
8887 line::reg::Json p = line::reg::Json::object();
8888 p["type"] = "AvgSysTable";
8889 p["indexBase"] = 0;
8890 for (const char* key : {"Chain", "JobClasses", "SysRespT", "SysTput"})
8891 p[key] = line::reg::Json::array();
8892 for (std::size_t c = 0; c < sn.nchains; ++c) {
8893 p["Chain"].push_back(cn[c]);
8894 p["JobClasses"].push_back(cc[c]);
8895 p["SysRespT"].push_back(d(sys.CN[c]));
8896 p["SysTput"].push_back(d(sys.XN[c]));
8897 }
8898 emit_analysis<T>("sys", p, r.actualmethod);
8899 return 0;
8900 }
8901 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner.c_str(),
8902 line::num_traits<T>::name(), r.actualmethod.c_str(), sn.nchains, suffix.c_str());
8903 std::printf("%-10s %-24s %14s %14s\n", "Chain", "JobClasses", "SysRespT", "SysTput");
8904 for (std::size_t c = 0; c < sn.nchains; ++c)
8905 std::printf("%-10s %-24s %14.6g %14.6g\n", cn[c].c_str(), cc[c].c_str(), d(sys.CN[c]),
8906 d(sys.XN[c]));
8907 return 0;
8908}
8909
8910/** Render a station- or node-level chain table, as text or as the host's JSON. */
8911template <class T>
8912void print_chain_table(const char* key, const char* type, const char* rowlabel,
8913 const std::vector<std::string>& rows,
8914 const std::vector<std::string>& chains,
8915 const std::vector<std::string>& classes,
8916 const line::solvers::ChainResult<T>& t, const std::string& method,
8917 const char* banner, const char* arith, const char* suffix = "") {
8918 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
8919 if (g_json_output) {
8920 // THE BANNER IS PRINTED UNDER `-o json` TOO, as the AvgTable path does:
8921 // the envelope names the arithmetic and the method but never the
8922 // SOLVER, so a host that pairs a table with another codebase's by the
8923 // solver in its banner cannot attribute a bannerless one at all. Its
8924 // absence here made `-o json` unusable for these four analyses.
8925 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner, arith, method.c_str(),
8926 chains.size(), suffix);
8927 line::reg::Json p = line::reg::Json::object();
8928 p["type"] = type;
8929 p["indexBase"] = 0;
8930 for (const char* c : {rowlabel, "Chain", "JobClasses", "QLen", "Util", "RespT", "ResidT",
8931 "ArvR", "Tput"})
8932 p[c] = line::reg::Json::array();
8933 // ROW-MAJOR OVER (row, chain), the reference's `(ist-1)*C+c` ordering,
8934 // so a host reading the two codebases' tables side by side indexes them
8935 // the same way.
8936 for (std::size_t i = 0; i < rows.size(); ++i)
8937 for (std::size_t c = 0; c < chains.size(); ++c) {
8938 p[rowlabel].push_back(rows[i]);
8939 p["Chain"].push_back(chains[c]);
8940 p["JobClasses"].push_back(classes[c]);
8941 p["QLen"].push_back(d(t.QN(i, c)));
8942 p["Util"].push_back(d(t.UN(i, c)));
8943 p["RespT"].push_back(d(t.RN(i, c)));
8944 p["ResidT"].push_back(d(t.WN(i, c)));
8945 p["ArvR"].push_back(d(t.AN(i, c)));
8946 p["Tput"].push_back(d(t.TN(i, c)));
8947 }
8948 emit_analysis<T>(key, p, method);
8949 return;
8950 }
8951 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner, arith, method.c_str(),
8952 chains.size(), suffix);
8953 std::printf("%-16s %-10s %-20s %12s %12s %12s %12s %12s %12s\n", rowlabel, "Chain",
8954 "JobClasses", "QLen", "Util", "RespT", "ResidT", "ArvR", "Tput");
8955 for (std::size_t i = 0; i < rows.size(); ++i)
8956 for (std::size_t c = 0; c < chains.size(); ++c)
8957 std::printf("%-16s %-10s %-20s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
8958 rows[i].c_str(), chains[c].c_str(), classes[c].c_str(), d(t.QN(i, c)),
8959 d(t.UN(i, c)), d(t.RN(i, c)), d(t.WN(i, c)), d(t.AN(i, c)),
8960 d(t.TN(i, c)));
8961}
8962
8963/**
8964 * `-a chain`: `@@NetworkSolver/getAvgChainTable`, the station table by CHAIN.
8965 *
8966 * EVERY ROW IS EMITTED, including the all-zero ones, unlike the AvgTable and the
8967 * AvgNodeTable. The reference builds this table with a full (M x C) grid and no
8968 * row filter, and a chain that is absent from a station is information -- it is
8969 * the shape of the routing -- where a class absent from a station in the
8970 * AvgTable is only the class's own scope.
8971 */
8972template <class T>
8973int solve_model_chain(const std::string& file, const Knobs& k, const std::string& s) {
8974 line::qn::Network<T> net = read_model<T>(file);
8976 std::string banner, suffix;
8977 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8979 std::vector<std::string> rows;
8980 for (std::size_t i = 0; i < sn.nstations; ++i) rows.push_back(sn.stations[i].name);
8981 print_chain_table<T>("chain", "AvgChainTable", "Station", rows,
8984 banner.c_str(), line::num_traits<T>::name(), suffix.c_str());
8985 return 0;
8986}
8987
8988/** `-a nodechain`: `@@NetworkSolver/getAvgNodeChainTable`, the node table by CHAIN. */
8989template <class T>
8990int solve_model_nodechain(const std::string& file, const Knobs& k, const std::string& s) {
8991 line::qn::Network<T> net = read_model<T>(file);
8993 std::string banner, suffix;
8994 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8995 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
8997 line::solvers::solver_get_avg_node_chain<T>(sn, nm.QN, nm.UN, nm.RN, nm.WN, nm.AN, nm.TN);
8998 std::vector<std::string> rows;
8999 for (std::size_t i = 0; i < sn.nodes.size(); ++i) rows.push_back(sn.nodes[i].name);
9000 print_chain_table<T>("nodechain", "AvgNodeChainTable", "Node", rows,
9003 banner.c_str(), line::num_traits<T>::name(), suffix.c_str());
9004 return 0;
9005}
9006
9007/**
9008 * `-a fj`: `getAvgFJTable`, the fork-join section metrics of the same solve, one row per (Fork/Join
9009 * pair, class) with a value > 0, columns as the JAR CLI's `-a fj`. Empty for a solver that computes none.
9010 */
9011template <class T>
9012int solve_model_fj(const std::string& file, const Knobs& k, const std::string& s) {
9013 line::qn::Network<T> net = read_model<T>(file);
9015 std::string banner, suffix;
9016 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
9017 std::printf("%s arith=%s method=%s%s\n", banner.c_str(), line::num_traits<T>::name(),
9018 r.actualmethod.c_str(), suffix.c_str());
9019 line::reg::Json t = fj_extra_json(sn, r.fj);
9020 if (t.is_null()) {
9021 t = line::reg::Json::object();
9022 for (const char* key : {"Fork", "Join", "JobClass", "FJQLen", "FJRespT", "FJTput"})
9023 t[key] = line::reg::Json::array();
9024 t["type"] = "AvgFJTable";
9025 }
9026 if (g_json_output) {
9027 emit_analysis<T>("fj", t, r.actualmethod);
9028 return 0;
9029 }
9030 std::printf("%-16s %-16s %-14s %14s %14s %14s\n", "Fork", "Join", "JobClass", "FJQLen", "FJRespT",
9031 "FJTput");
9032 for (std::size_t i = 0; i < t["Fork"].size(); ++i)
9033 std::printf("%-16s %-16s %-14s %14.6g %14.6g %14.6g\n", t["Fork"][i].get<std::string>().c_str(),
9034 t["Join"][i].get<std::string>().c_str(), t["JobClass"][i].get<std::string>().c_str(),
9035 t["FJQLen"][i].get<double>(), t["FJRespT"][i].get<double>(), t["FJTput"][i].get<double>());
9036 return 0;
9037}
9038
9039/** The @@NetworkSolver tables that are VIEWS of one solved AvgResult (`fj` reads its fork-join block). */
9040inline bool is_avg_view(const std::string& analysis) {
9041 return analysis == "node" || analysis == "sys" || analysis == "chain" ||
9042 analysis == "nodechain" || analysis == "fj";
9043}
9044
9045/** The solvers whose analyzer runs `sn_nonmarkov_toph`, so `--nonmkv` reaches them. */
9046inline bool nonmkv_reader(const std::string& s) {
9047 return s == "ctmc" || s == "mam" || s == "fluid" || s == "fld" || s == "ag";
9048}
9049
9050/**
9051 * Dispatch one of those four views at a fixed arithmetic.
9052 *
9053 * The wrapper arms reach the views through this rather than through the shared
9054 * ladder further down: they return before it, having validated their own knobs
9055 * against an engine the ladder knows nothing about. The arithmetic is fixed
9056 * because both wrappers report in double and have already refused every other
9057 * backend by name.
9058 */
9059template <class T>
9060int solve_avg_view(const std::string& file, const Knobs& k, const std::string& s,
9061 const std::string& analysis) {
9062 if (analysis == "node") return solve_model_node<T>(file, k, s);
9063 if (analysis == "sys") return solve_model_sys<T>(file, k, s);
9064 if (analysis == "chain") return solve_model_chain<T>(file, k, s);
9065 if (analysis == "fj") return solve_model_fj<T>(file, k, s);
9066 return solve_model_nodechain<T>(file, k, s);
9067}
9068
9069int solve_model_dispatch(const std::string& arith, const std::string& solver,
9070 const std::string& analysis, const std::string& file, const Knobs& k) {
9071 std::string s = solver.empty() ? "auto" : solver;
9072 if (s != "mva" && s != "auto" && s != "fluid" && s != "fld" && s != "nc" && s != "mam" &&
9073 s != "ag" && s != "ba" && s != "ssa" && s != "ctmc" && s != "uq" && s != "env" &&
9074 s != "lqns" && s != "ldes" && s != "jmt")
9076 "the model-solving path ports -s mva, nc, ctmc, mam, ag, ba, ssa, fluid, ldes, jmt, "
9077 "uq, env and lqns (got '" + s + "'); other solvers remain API-only");
9078 // Every solver on this path but JMT and LQNS runs in-process, so the flags
9079 // that describe an external solver's child process have nothing to act on.
9080 // Those two do run one, and both write a scratch directory --keep names:
9081 // `solve_model_jmt` forwards it to JmtOptions.keep, which is what leaves
9082 // model.jsim behind, and refusing it here made the one document a parity
9083 // difference has to be read from unobtainable.
9084 if ((k.verbose && s != "ldes") || k.remote || !k.remote_url.empty() ||
9085 (k.keep && s != "lqns" && s != "jmt"))
9087 "--keep, --verbose, --remote and --remote-url describe the child process of an "
9088 "external solver; on this path -s jmt and -s lqns run one and take --keep, -s ldes "
9089 "runs one and takes --verbose (which echoes the resolved engine command line), and "
9090 "no path takes --remote or --remote-url");
9091 if (k.timeout_seconds && s != "lqns")
9093 "--timeout is the deadline of an external solver's child process; on this path only "
9094 "-s lqns runs one");
9095 // --fork-join names an arm of the fork-join FIXED POINT, which only the
9096 // mean-value arms drive: a simulator walks the fork on its sample path and
9097 // a CTMC enumerates it, so neither has a transform to choose. Refused by
9098 // name rather than ignored, which would report the default arm's numbers
9099 // under the caller's choice.
9100 if (!k.fork_join.empty() && s != "mva" && s != "nc")
9102 "--fork-join selects the fork-join transform of the shared mean-value fixed point "
9103 "and is read by -s mva and -s nc; -s " + s +
9104 " either simulates or enumerates the fork and applies no transform");
9105 // REFUSED BY NAME RATHER THAN IGNORED, on the same grounds as --fork-join
9106 // above: a knob silently dropped reports the DEFAULT arm's numbers under
9107 // the caller's choice, which is the one outcome stating the flag exists to
9108 // rule out.
9109 if (k.warmupfrac >= 0.0 && s != "ssa")
9111 "--warmupfrac discards a leading fraction of a SIMULATED path before the means are "
9112 "taken and is read by -s ssa; -s " + s +
9113 " has no path to discard (the LDES engine takes --ldes-warmupfrac)");
9114 if (k.pstar > 0.0 && s != "fluid" && s != "fld")
9116 "--pstar is the exponent of the fluid p-norm smoothing of the drift and is read by "
9117 "-s fluid; -s " + s + " integrates no drift");
9118 if (!k.fluid_boundary.empty() && s != "fluid" && s != "fld")
9120 "--fluid-boundary picks how the fluid Petri 'dae' route reports a fluid place level held at a "
9121 "bound and is read by -s fluid; -s " + s + " has no such report");
9122 // auto passes: its branch below keeps --nonmkv only for a candidate that reads it.
9123 if (!k.nonmkv.empty() && s != "auto" && !nonmkv_reader(s))
9125 "--nonmkv selects the phase-type fit of a non-Markovian law and is read by -s ctmc, "
9126 "mam, fluid and ag; -s " + s + " runs no such fit");
9127 if ((!k.busy_orders.empty() || !k.busy_subnet.empty()) && analysis != "busyperiod")
9129 "--busyperiod and --busyperiod-subnet name the orders and the subnetwork of "
9130 "-a busyperiod; got -a " + analysis);
9131 // Solver console: this dispatcher is the single point every model-solving
9132 // arm passes through, so the narrated run is opened here and closed by the
9133 // guard's destructor -- on an exception too, so a failed analysis still
9134 // reports what it had reached. The model name is not known before the file
9135 // is read, so the header names the file's model once the struct compiles.
9136 line::util::LineConsole::Run consoleRun(upper_tag(s), "", true);
9137
9138 // ---- SolverUQ, BEFORE the per-solver knob ladder ----------------------
9139 // It is a wrapper, not an engine: `--method`, `--samples` and `--seed`
9140 // describe its DESIGN and the convergence knobs belong to whatever
9141 // `--uq-solver` names, so the ladder below -- which asks "does THIS solver
9142 // have a sample count" -- answers about the wrong solver here.
9143 if (s == "uq") {
9144 if (analysis != "avg" && analysis != "posterior" && analysis != "interval")
9145 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
9146 "SolverUQ ports -a avg (the prior-weighted expectation), -a posterior (the "
9147 "per-design-point table) and -a interval (the support-only range); got '" +
9148 analysis + "'");
9149 if (k.uq_solver.empty())
9150 throw line::InputError(
9151 "-s uq needs --uq-solver: UQ computes nothing itself, it expands the Prior and "
9152 "runs another solver at each design point (the C++ spelling of UQ(model, "
9153 "@SolverMVA)). Naming one here by default would attribute the numbers to an "
9154 "engine the caller never chose");
9155 if (k.t1 >= 0.0 || k.node || !k.notation.empty())
9157 "--tspan, --node and --notation name a transient horizon, a stateful node and an "
9158 "ODE document; SolverUQ reports steady-state means over a design of models and "
9159 "has none of the three");
9160 if (k.no_interlocking || k.interlock_knobs_given() || k.repeat > 0 || !k.layer_solver.empty() ||
9161 k.nlp_knobs_given())
9163 "--no-interlocking, --interlock-*, --repeat, --layer-solver, --lld, --nlp-*, --warmstart and "
9164 "--mol-* are options of the layered solver "
9165 "and apply to -i lqnx; a Network model has no layers to interlock");
9166 if (arith == "double") return solve_model_uq<double>(file, k, analysis);
9167 if (arith == "exact") return solve_model_uq<line::Rational>(file, k, analysis);
9168 if (arith == "real:16") return solve_model_uq<line::Real<16> >(file, k, analysis);
9169 if (arith == "real" || arith == "real:32")
9170 return solve_model_uq<line::Real<32> >(file, k, analysis);
9171 if (arith == "real:64") return solve_model_uq<line::Real<64> >(file, k, analysis);
9172 if (arith == "real:128") return solve_model_uq<line::Real<128> >(file, k, analysis);
9173 if (arith == "real:256") return solve_model_uq<line::Real<256> >(file, k, analysis);
9174 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9175 }
9176 // ---- SolverENV, also BEFORE the per-solver knob ladder ----------------
9177 // It reads a DIFFERENT MODEL TYPE: an Environment envelope, whose stages
9178 // each hold a Network. The ladder below asks its questions of one Network
9179 // model -- `--cutoff` per open class, `--node` on a stateful node -- and
9180 // the model-level knobs it would validate belong to the STAGE solver here,
9181 // so ENV states its own refusals and routes around it.
9182 if (s == "env") {
9183 if (analysis != "avg")
9184 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
9185 "SolverENV reports -a avg, the environment-blended means; getEnsembleAvg is its "
9186 "only metric entry in the reference too (got '" + analysis + "')");
9187 if (k.samples || k.seed)
9189 "--samples and --seed describe a simulation; SolverENV iterates a fixed point over "
9190 "transient stage solves and draws nothing");
9191 // `--cutoff` IS ADMITTED WITH `--stage-solver ctmc`, and only then: every
9192 // stage is then enumerated, and an open stage's chain has to be
9193 // truncated somewhere. It stays refused for a fluid ensemble, which
9194 // enumerates nothing.
9195 if (k.has_cutoff() && k.stage_solver != "ctmc")
9197 "--cutoff bounds the open population of an enumerated state space and applies to "
9198 "-s env only beside --stage-solver ctmc; the " +
9199 (k.stage_solver.empty() ? std::string("fluid") : k.stage_solver) +
9200 " stages of this ensemble enumerate no states");
9201 if (k.node || !k.notation.empty())
9203 "--node and --notation name a stateful node and an ODE document of ONE network; "
9204 "an Environment holds a network per stage and "
9205 "SolverENV reports the blend over them");
9206 if (k.no_interlocking || k.interlock_knobs_given() || k.repeat > 0 || !k.layer_solver.empty() ||
9207 k.nlp_knobs_given())
9209 "--no-interlocking, --interlock-*, --repeat, --layer-solver, --lld, --nlp-*, --warmstart and "
9210 "--mol-* are options of the layered solver "
9211 "and apply to -i lqnx; an Environment has stages, not layers");
9212 if (k.t0 != 0.0)
9214 "--tspan on the ENV path states the transient HORIZON each stage solve integrates "
9215 "to, and every stage starts from its entry state at 0; a nonzero t0 would name a "
9216 "start the coupling has no state for");
9217 // The mean-field coupling integrates the stage drift with LSODA, which
9218 // is double; the state-vector one uniformizes a CTMC and carries the
9219 // whole ladder. Narrowing silently would report an `exact` banner over
9220 // a double solve, so the refusal names the coupling that decided it.
9221 // `blend` is a spelling of `default` since 2026-09-13 (it named the
9222 // state-vector coupling before that), so it normalizes to `meanfield`
9223 // here and every gate below reads it as such without repeating it.
9224 const std::string coupling =
9225 (k.method.empty() || k.method == "default" || k.method == "mean"
9226 || k.method == "blend" || k.method == "blending")
9227 ? "meanfield"
9228 : k.method;
9229 if (k.tran_points && coupling == "statevec")
9231 "--tran-points is the mean-field coupling's quadrature grid; the state-vector "
9232 "coupling carries the whole joint law across a switch and sums over no such grid, "
9233 "so the value would be accepted and never used");
9234 // `statedep` is a C++-API method and not a file one: it needs a rate
9235 // function PER ARC (`Environment::set_env_rate_reset`), which is a
9236 // function of the stage exit metrics and has no representation in
9237 // model.json -- the reference cannot serialize `resetEnvRatesFun`
9238 // either. Reaching it from a file would find no hook and refuse deeper
9239 // in, with a message about an environment the caller never wrote.
9240 if (coupling == "statedep")
9242 "--method statedep makes each environment transition depend on the state its "
9243 "stage is left in, through a rate function per arc that no model.json can carry "
9244 "(the reference cannot serialize resetEnvRatesFun either); it is reachable from "
9245 "the C++ API, through Environment::set_env_rate_reset");
9246 if (k.tran_points && coupling == "occupation")
9248 "--tran-points is the mean-field coupling's quadrature grid; --method occupation "
9249 "solves each stage in steady state and integrates the stage law in closed form, "
9250 "so the value would be accepted and never used");
9251 if (!k.env_init.empty() && coupling != "occupation")
9253 "--env-init is options.config.env_init, the initial stage law of --method "
9254 "occupation; the '" + coupling + "' coupling reads the stationary stage law and "
9255 "would accept the value and never use it");
9256 if ((k.tran_points || k.t1 >= 0.0) && (coupling == "avg" || coupling == "dec"))
9258 "--tran-points and --tspan state the grid and the horizon of a TRANSIENT stage "
9259 "solve; the closed-form limits --method avg and --method dec solve in steady "
9260 "state and carry nothing across a switch, so both would be accepted and never "
9261 "used");
9262 if (arith != "double" && coupling != "statevec")
9263 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
9264 "SolverENV solves a stage with the fluid analyzer on every method but the "
9265 "state-vector one -- the mean-field coupling transiently, the avg and dec limits "
9266 "in steady state -- and that analyzer is LSODA's and therefore double; --arith " +
9267 arith +
9268 " reaches ENV only through the state-vector coupling (--method statevec)");
9269 if (arith == "double") return solve_model_env<double>(file, k);
9270 if (arith == "exact") return solve_model_env<line::Rational>(file, k);
9271 if (arith == "real:16") return solve_model_env<line::Real<16> >(file, k);
9272 if (arith == "real" || arith == "real:32")
9273 return solve_model_env<line::Real<32> >(file, k);
9274 if (arith == "real:64") return solve_model_env<line::Real<64> >(file, k);
9275 if (arith == "real:128") return solve_model_env<line::Real<128> >(file, k);
9276 if (arith == "real:256") return solve_model_env<line::Real<256> >(file, k);
9277 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9278 }
9279 if (!k.uq_solver.empty())
9281 "--uq-solver names the engine SolverUQ runs at each design point and applies to -s uq; "
9282 "'" + s + "' solves one model, not a design of them");
9283 if (k.tran_points)
9285 "--tran-points is the resolution of the transient grid SolverENV sums its stage exit "
9286 "metrics over and applies to -s env; '" + s + "' has no such quadrature");
9287 // ---- SolverLDES, BEFORE the per-solver knob ladder --------------------
9288 // It hands the model DOCUMENT to an external engine instead of reading it,
9289 // so the ladder below -- which validates knobs against a parsed struct --
9290 // asks its questions of a model this path never parses. LDES states its own
9291 // refusals here for the same reason UQ and ENV state theirs.
9292 const bool ldes_knob = !k.ldes_tranfilter.empty() || k.ldes_warmupfrac >= 0.0 ||
9293 !k.ldes_cimethod.empty() || k.ldes_cnvgon || k.ldes_slotted ||
9294 k.ldes_replications > 0 || k.ldes_numthreads > 0 ||
9295 k.ldes_maxtime > 0.0 || !k.ldes_initsol.empty() ||
9296 !k.ldes_rest_url.empty();
9297 if (ldes_knob && s != "ldes" && s != "auto")
9299 "the --ldes-* flags are the discrete-event engine's own settings (warmup filter, "
9300 "confidence-interval estimator, slot lattice, replications, warm-start placement) and "
9301 "apply to -s ldes; '" + s + "' has none of them");
9302 if (k.jmt_replications > 0 && s != "jmt")
9304 "--jmt-replications is SolverJMT's transient ensemble size (-s jmt -a tran / -a "
9305 "tranprob); '" + s + "' has none (-s ldes takes --ldes-replications)");
9306 if (s == "jmt") {
9307 if (arith != "double")
9308 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
9309 "SolverJMT is a client of the Java Modelling Tools engine, which simulates in "
9310 "double and reports in double; --arith " + arith +
9311 " would label a double answer with an arithmetic that never touched it");
9312 // THE LIST AND `auto_family_metrics("jmt")` ARE ONE ANSWER, and they
9313 // had drifted: the family table advertised tran, tranprob and sample
9314 // while this gate refused all three, so a caller following
9315 // `--find-solver` was refused by name. `jmt_logs.h` implements every
9316 // one of them (`jmt_transient_replications`, `jmt_get_tran_prob_aggr`,
9317 // `jmt_sample_aggr`, `jmt_sample_sys_aggr`); what was missing was the
9318 // way in.
9319 if (analysis != "avg" && analysis != "cdf" && analysis != "trancdf" &&
9320 analysis != "trancdfpasst" && analysis != "prob" && analysis != "tran" &&
9321 analysis != "tranprob" && analysis != "sample" && !is_avg_view(analysis))
9322 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
9323 "-s jmt reports -a avg (the JSIM or JMVA mean table), its four views -a node, "
9324 "-a sys, -a chain and -a nodechain, -a cdf (the empirical response-time law read "
9325 "back from the JMT logs, preloaded at the rounded steady-state queue lengths), "
9326 "-a tran-cdf-respt and -a tran-cdf-passt (the same logged run from the default "
9327 "initial state, so the samples cover the transient), -a prob (the time each "
9328 "declared state is held for along the logged trajectory), -a tran (the transient "
9329 "means over --jmt-replications independent replications), -a tranprob (the same "
9330 "replications' aggregate state law at one station) and -a sample (one logged "
9331 "trajectory); the DETAILED laws -a states and getTranProb have no counterpart, "
9332 "since a JMT log records per-class job counts and nothing about the buffer order "
9333 "or the service phase");
9334 {
9335 const std::string removed = line::jmt::jmt_removed_method_refusal(k.method);
9336 if (!removed.empty()) throw line::UnsupportedError(line::ErrorKind{"Method"}, removed);
9337 }
9338 const std::vector<std::string> valid = line::jmt::jmt_list_valid_methods();
9339 if (!k.method.empty() &&
9340 std::find(valid.begin(), valid.end(), k.method) == valid.end())
9342 "SolverJMT methods are default, jsim and the jmva family (jmva, jmva.amva, "
9343 "jmva.mva, jmva.recal, jmva.comom, jmva.chow, jmva.bs, jmva.aql, jmva.lin, "
9344 "jmva.dmlin); got '" + k.method + "'");
9345 // `--iter_max` MEANS NOTHING TO JMT. It used to be the replication count of `-a tran` and
9346 // `-a tranprob`; that count is now `options.config.replications`, spelt --jmt-replications.
9347 const bool jmt_replicated = analysis == "tran" || analysis == "tranprob";
9348 if (k.iter_max >= 0)
9350 "--iter_max is not a SolverJMT option: the number of independent replications -a "
9351 "tran and -a tranprob average over is --jmt-replications (options.config."
9352 "replications, default 10)");
9353 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || !k.multiserver.empty())
9355 "--tol, --iter_tol and --multiserver configure a fixed-point iteration; JSIM "
9356 "simulates a sample path and JMVA takes its tolerance from the exported document. "
9357 "The simulation's stopping rule is --samples");
9358 if (k.jmt_replications > 0 && !jmt_replicated)
9360 "--jmt-replications sizes the transient ensemble of -a tran and -a tranprob; "
9361 "steady-state JSIM (-a " + analysis + ") is one run and reads no replication count");
9362 if (k.has_cutoff())
9364 "--cutoff truncates an enumerated state space; JMT enumerates none");
9365 // `--node` NAMES ONE STATION, on the three arms that answer about one.
9366 // On `-a prob` it says which station `--state` overrides, the second
9367 // argument of `getProbAggr(node, state_a)`; on `-a tranprob` it is the
9368 // station whose aggregate law is estimated; on `-a sample` it is the
9369 // node whose own trajectory is returned instead of the system's. Every
9370 // other JMT arm reports a table over all stations and classes.
9371 const bool jmt_node_arm =
9372 analysis == "prob" || analysis == "tranprob" || analysis == "sample";
9373 if ((k.node && !jmt_node_arm) || k.jobclass || !k.marg_states.empty())
9375 "--node, --class and --marg-states select the marginal law of one (node, class); "
9376 "the JMT arms report tables over every station and class, and --node applies to "
9377 "-a prob (which station --state overrides), -a tranprob (whose state law) and "
9378 "-a sample (whose trajectory)");
9379 if (!k.state.empty() && analysis != "prob")
9381 "--state names the state a probability is asked about and applies to -a prob");
9382 if (!k.state.empty() && !k.node)
9384 "--state is the per-class job count of ONE station and needs --node to say "
9385 "which; a bare count vector cannot be matched against a whole network");
9386 if (!k.notation.empty() || !k.symbolic.empty() || k.equilibria)
9388 "--notation, --symbolic and --equilibria describe an exported ODE document; JMT "
9389 "integrates no ODE");
9390 if (!k.cdf_algorithm.empty())
9392 "--cdf-algorithm selects between the two sojourn-time INVERSIONS of -s nc; the "
9393 "JMT response-time law is the ecdf of the passages its loggers recorded and is "
9394 "not computed from a transform");
9395 if (is_avg_view(analysis)) return solve_avg_view<double>(file, k, "jmt", analysis);
9396 return solve_model_jmt(file, k, analysis);
9397 }
9398 if (s == "ldes") {
9399 if (arith != "double")
9400 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
9401 "SolverLDES is a client of the SSJ engine, which simulates in double and reports "
9402 "in double; --arith " + arith +
9403 " would label a double answer with an arithmetic that never touched it");
9404 // 'parallel' asks the engine for INDEPENDENT REPLICATIONS and the mean
9405 // over them, which is what its parallel analyzer is; it is not a second
9406 // engine. It resolves to a replication count here, taking --ldes-
9407 // replications when given and 8 otherwise -- the same default the SSA
9408 // parallel analyzer uses. Mirrors SolverLDES.listValidMethods in every
9409 // codebase, which advertises exactly {default, para, parallel}, 'para'
9410 // being the short spelling of 'parallel' as in SolverSSA.
9411 if (!k.method.empty() && k.method != "default" && k.method != "para" && k.method != "parallel")
9413 "SolverLDES has the methods 'default' and 'parallel' (alias 'para'; listValidMethods returns "
9414 "exactly those in every codebase); got '" + k.method + "'");
9415
9416 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0 || !k.multiserver.empty())
9418 "--tol, --iter_tol, --iter_max and --multiserver are the knobs of a fixed-point "
9419 "iteration; LDES simulates a sample path and iterates nothing. Its stopping rule "
9420 "is --samples, or --ldes-cnvgon with --ldes-cnvgtol");
9421 if (k.has_cutoff())
9423 "--cutoff truncates an enumerated state space; a simulator visits the states the "
9424 "sample path reaches and enumerates none");
9425 if (k.jobclass || !k.marg_states.empty() || (k.node && analysis != "prob"))
9427 "--class and --marg-states select a marginal law of one (node, class), and --node "
9428 "applies to -a prob beside --state; the other LDES arms report tables over every "
9429 "station and class");
9430 if (!k.state.empty() && (analysis != "prob" || !k.node))
9432 "--state is getProbAggr(node, state)'s per-class job count of ONE station: it "
9433 "applies to -s ldes -a prob and needs --node to say which");
9434 if (!k.notation.empty() || !k.symbolic.empty() || k.equilibria)
9436 "--notation, --symbolic and --equilibria describe an exported ODE document; LDES "
9437 "integrates no ODE");
9438 if (!k.cdf_algorithm.empty())
9440 "--cdf-algorithm selects between the two sojourn-time INVERSIONS of -s nc; the "
9441 "LDES response-time law is the ecdf of the samples the engine recorded and is not "
9442 "computed from a transform");
9443 if (k.no_interlocking || k.interlock_knobs_given() || k.repeat > 0 || !k.layer_solver.empty() ||
9444 k.nlp_knobs_given())
9446 "--no-interlocking, --interlock-*, --repeat, --layer-solver, --lld, --nlp-*, --warmstart and "
9447 "--mol-* are options of the layered solver "
9448 "and apply to -i lqnx; the LDES layered path runs in the JAR's own ensemble "
9449 "backend and has no JSON interface to reach from here");
9450 if (!k.sens_method.empty() || !k.sens_scheme.empty() || k.sens_step >= 0.0)
9452 "--sens-method, --sens-scheme and --sens-step configure the layered sensitivity "
9453 "table; LDES reports no sensitivity");
9454 if (analysis == "tran" && !(k.t1 >= 0.0))
9455 throw line::InputError(
9456 "-s ldes -a tran needs --tspan <t1> or --tspan <t0>:<t1>: a trajectory over an unstated "
9457 "horizon is not a quantity, and the engine only records buckets once a timespan "
9458 "makes the run transient");
9459 if (k.t1 >= 0.0 && analysis != "tran")
9461 "--tspan names the horizon of -a tran; -a sample runs over [0, --samples], which "
9462 "is the horizon runTransientJson uses, and the other arms are steady state");
9463 // `-a tran-cdf-*` NAMES THE TRANSIENT LAW AND IS THE STEADY-STATE ONE
9464 // HERE, because a simulator has only the samples it observed: the
9465 // reference's `getTranCdfRespT` reads the same `respTimeSamples` its
9466 // `getCdfRespT` reads, and its `getTranCdfPassT` is a one-line delegation
9467 // to `getTranCdfRespT`. Warned rather than refused, so a script written
9468 // against the JAR runs and its author is told what the curve is.
9469 if ((analysis == "trancdf" || analysis == "trancdfpasst") &&
9470 k.verbosity != "silent")
9471 std::fprintf(stderr,
9472 "Warning: -a %s is the ecdf of the per-job response times the run "
9473 "observed, the same curve -a cdf reports; the reference's LDES "
9474 "getTranCdfRespT reads the same samples\n",
9475 analysis.c_str());
9476 // method='parallel' resolved to its replication count, after every
9477 // other knob has been validated against the caller's own Knobs.
9478 Knobs kldes = k;
9479 if ((kldes.method == "parallel" || kldes.method == "para") && kldes.ldes_replications <= 1)
9480 kldes.ldes_replications = 8;
9481 if (is_avg_view(analysis)) return solve_avg_view<double>(file, kldes, "ldes", analysis);
9482 return solve_model_ldes(file, kldes, analysis);
9483 }
9484 // SolverAUTO resolves to a real engine BEFORE the knob checks below, so a
9485 // model the chooser sends to SSA accepts --samples and one it sends to CTMC
9486 // accepts --cutoff: the checks must see the solver that will actually run.
9487 if (s == "auto") {
9488 // `chooseSolverHeur` picks an engine from a GETTER, and the age laws are
9489 // not one of its getters: a model whose table it would send to MVA does
9490 // not thereby have an AoI answer. Refusing by name here beats letting
9491 // the chosen engine refuse an analysis it was never asked about.
9492 if (analysis == "aoi")
9493 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
9494 "-a aoi is the AoI branch of the fluid 'mfq' method and no other engine reports "
9495 "it, so SolverAUTO does not choose for it: ask for it by name with -s fluid");
9496 const AutoPlan plan = choose_auto_plan_dispatch(arith, file, analysis, k.method);
9497 // `delegate.m` runs the chosen solver and, when it fails, every other
9498 // feasible candidate in slot order. Reproduced here by re-entering this
9499 // dispatch with a CONCRETE method name, so each attempt is validated against
9500 // the knobs of the solver that will actually run it. Only the first
9501 // attempt carries the method the ranking gated on ('exact'): a method
9502 // name is a solver's own vocabulary and does not travel to the next.
9503 if (plan.order.size() == 1) {
9504 // A forced method name (a method family, or the Environment envelope)
9505 // leaves ONE solver, and its own diagnostic is then the whole
9506 // story: the reference rethrows it rather than wrapping it.
9507 Knobs kk = k;
9508 kk.method = plan.method;
9509 if (!nonmkv_reader(plan.order[0])) kk.nonmkv.clear(); // as the reference's chosen solver ignores it
9510 if (!plan.stage.empty() && !plan.stage[0].empty() && kk.stage_solver.empty())
9511 kk.stage_solver = plan.stage[0];
9512 std::printf("SolverAUTO selected %s%s\n", plan.order[0].c_str(), plan.note.c_str());
9513 return solve_model_dispatch(arith, plan.order[0], analysis, file, kk);
9514 }
9515 std::string first_error, first_category, first_kind;
9516 for (std::size_t i = 0; i < plan.order.size(); ++i) {
9517 Knobs kk = k;
9518 kk.method = (i == 0) ? plan.method : std::string();
9519 if (!nonmkv_reader(plan.order[i])) kk.nonmkv.clear();
9520 // An Environment candidate names its ensemble, unless the caller already did.
9521 if (i < plan.stage.size() && !plan.stage[i].empty() && kk.stage_solver.empty())
9522 kk.stage_solver = plan.stage[i];
9523 if (i == 0)
9524 std::printf("SolverAUTO selected %s%s\n", plan.order[i].c_str(),
9525 plan.note.c_str());
9526 else
9527 std::printf("SolverAUTO retrying with %s\n", plan.order[i].c_str());
9528 try {
9529 return solve_model_dispatch(arith, plan.order[i], analysis, file, kk);
9530 } catch (const InterruptRequested&) {
9531 throw; // the host asked the run to stop, so no candidate may be tried after it
9532 } catch (const line::Error& e) {
9533 // The category decides, not the message (_kb/17-error-vocabulary.md, "SolverAUTO"):
9534 // input and internal failures propagate, since no other candidate can mend a
9535 // malformed model or a bug; unsupported, numerical and external ones move on. An
9536 // unclassified LINE:Error moves on too, but loudly: it may be a crash, not a refusal.
9537 const std::string cat = e.category();
9538 if (cat == "input" || cat == "internal") throw;
9539 if (cat.empty())
9540 std::fprintf(stderr,
9541 "line-cli: warning: SolverAUTO candidate %s failed with an "
9542 "unclassified error [%s] %s; trying the next candidate\n",
9543 plan.order[i].c_str(), e.id().c_str(), e.what());
9544 if (first_error.empty()) {
9545 first_error = plan.order[i] + ": " + e.what();
9546 first_category = cat;
9547 first_kind = e.kind();
9548 }
9549 std::printf("SolverAUTO: %s cannot serve this run ([%s] %s)\n",
9550 plan.order[i].c_str(), e.id().c_str(), e.what());
9551 } catch (const std::exception& e) {
9552 // A foreign exception is unclassified by definition: move on, with the warning.
9553 std::fprintf(stderr,
9554 "line-cli: warning: SolverAUTO candidate %s failed with a non-LINE "
9555 "exception (%s); trying the next candidate\n",
9556 plan.order[i].c_str(), e.what());
9557 if (first_error.empty()) first_error = plan.order[i] + ": " + e.what();
9558 std::printf("SolverAUTO: %s cannot serve this run (%s)\n", plan.order[i].c_str(),
9559 e.what());
9560 }
9561 }
9562 // Every candidate failed: report under the FIRST failure's id, the chosen engine's.
9563 const std::string all_failed =
9564 "SolverAUTO: every candidate refused this run. The chosen engine reported -- " +
9565 first_error;
9566 if (first_category == "numerical")
9567 throw line::NumericError(line::ErrorKind{first_kind}, all_failed);
9568 if (first_category == "external")
9569 throw line::ExternalError(line::ErrorKind{first_kind}, all_failed);
9570 if (first_category == "unsupported")
9571 throw line::UnsupportedError(line::ErrorKind{first_kind}, all_failed);
9572 throw line::Error(all_failed);
9573 }
9574 // ---- knobs the CHOSEN solver does not have are refused, not dropped ----
9575 // Accepting an option and discarding it is the one place the port would
9576 // answer a question it was not asked: the caller believes a value the
9577 // solver never saw. Every refusal below names the option and the solver.
9578 const bool is_sim = (s == "ssa");
9579 // `-s ctmc -a sample` walks the chain with an exponential clock, so it has a
9580 // run length and a stream in the same sense a simulation does; every other
9581 // CTMC analysis is a solve and still refuses both.
9582 // The NC cftp methods draw iid stationary states, so they too have a run
9583 // length and a stream, the NC `options.samples` / `options.seed`. They are
9584 // named here rather than through is_stochastic_method, which classifies
9585 // them as SolverCTMC did and is deliberately unchanged by their move.
9586 const bool is_cftp =
9587 (s == "nc" && (k.method == "cftp" || k.method == "cftp.approx"));
9588 // NC IS NOT ONE SOLVER HERE: its Monte Carlo integrators, logistic sampler,
9589 // importance-sampling estimators and Chen-O'Cinneide MCMC all read a run
9590 // length and a stream, which is why solve_model_nc wires k.samples and
9591 // k.seed into NcSolverOptions and says so in its own doc comment. This
9592 // guard contradicted that file for every NC method, so a caller naming one
9593 // of those methods was told 'nc' has no sample count -- the refusal that
9594 // made the statepr_sys_aggr_large [M2C] parity row skip, its MATLAB source
9595 // being NC(model,'seed',23000,'method','ls','samples',10000).
9596 // is_stochastic_method is the port of SolverNC.isStochasticMethod and
9597 // tokenizes on '.' and '/', so 'nc.ls' and 'default/imci' classify alike.
9598 // A method nobody named stays refused: 'default' RESOLVES to a stochastic
9599 // method only at runtime, and accepting a knob on the strength of what a
9600 // name might become is the same discard this block exists to prevent.
9601 const bool nc_draws = (s == "nc" && line::nc::is_stochastic_method(k.method));
9602 const bool draws_samples =
9603 is_sim || (s == "ctmc" && analysis == "sample") || is_cftp || nc_draws;
9604 if (k.samples && !draws_samples)
9605 throw line::UnsupportedError(line::ErrorKind{"Option"}, "--samples applies to the simulation solver (-s ssa), to "
9606 "-s ctmc -a sample and to the stochastic -s nc methods "
9607 "(mci, imci, ls, is, sampling, mcmc, cftp, cftp.approx); '" +
9608 s + "' has no sample count");
9609 if (k.seed && !draws_samples)
9610 throw line::UnsupportedError(line::ErrorKind{"Option"}, "--seed applies to the simulation solver (-s ssa), to "
9611 "-s ctmc -a sample and to the stochastic -s nc methods "
9612 "(mci, imci, ls, is, sampling, mcmc, cftp, cftp.approx); '" +
9613 s + "' draws no random numbers");
9614 // --mdd-tol / --mdd-maxiter drive the level iteration, which only the mdd
9615 // method runs. Accepting them elsewhere would let a caller believe a
9616 // tolerance was applied to a solve that has no iteration in it.
9617 if ((k.mdd_tol > 0.0 || k.mdd_maxiter > 0) && !(s == "ctmc" && k.method == "mdd"))
9619 "--mdd-tol and --mdd-maxiter set the coupled level iteration of -s ctmc --method mdd; "
9620 "'" + s + " / " + (k.method.empty() ? std::string("default") : k.method) +
9621 "' iterates no levels");
9622 // --tspan names a transient horizon, which only the CTMC transient analyses
9623 // have; accepting it elsewhere would let a caller believe a horizon was used.
9624 // The fluid solver integrates a forward equation too, and its horizon is what
9625 // `kp` reports its covariance AT, so --tspan reaches it as well; every other
9626 // fluid method restarts from its own end state until the moved mass stops
9627 // changing, and a horizon there caps that iteration rather than naming a time.
9628 if (k.t1 >= 0.0 &&
9629 !(s == "ctmc" &&
9630 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")) &&
9631 !(s == "fluid" || s == "fld") && !(s == "mam" && analysis == "tran"))
9633 "--tspan sets the horizon of a transient analysis and applies to -s ctmc -a tran, "
9634 "-a tranprob and -a tranreward, to -s mam -a tran and to -s fluid; '" + s + " / " +
9635 analysis + "' integrates no forward equation");
9636 // --node narrows an answer to one node's block of the state, which only the
9637 // per-node CTMC queries and the per-node MAM queue-length law have; every
9638 // other analysis reports the whole network.
9639 // `-s ctmc -a prob` takes it TOGETHER WITH --state and only then: the arm
9640 // reports every station's marginal, so a bare --node would narrow nothing,
9641 // while --state names a row of ONE node's own space and needs --node to say
9642 // whose.
9643 if (k.node && !(s == "ctmc" && (analysis == "tranprob" || analysis == "sample")) &&
9644 !(s == "ctmc" && analysis == "prob" && !k.state.empty()) &&
9645 !(s == "nc" && analysis == "prob" && !k.state.empty()) &&
9646 !(s == "ssa" && analysis == "sample") && !(s == "mam" && analysis == "prob") &&
9647 !((s == "mva" || s == "nc") && analysis == "marg"))
9649 "--node selects the stateful node a state query is labelled by and applies to -s ctmc "
9650 "-a tranprob and -a sample, to -s ctmc|nc -a prob beside --state, to -s ssa -a sample, "
9651 "to -s mam -a prob and to -s mva|nc -a marg; '" + s + " / " + analysis +
9652 "' reports the whole network");
9653 // --class and --marg-states are the remaining two arguments of getProbMarg,
9654 // and nothing else in the surface takes either: every other analysis reports
9655 // all classes, and no other law is evaluated at a caller-chosen job count.
9656 if ((k.jobclass || !k.marg_states.empty()) && !(s == "mva" && analysis == "marg"))
9658 "--class and --marg-states are the job class and the state list of getProbMarg and "
9659 "apply to -s mva -a marg; '" + s + " / " + analysis +
9660 "' reports every class over its own range");
9661 // --notation selects which document the ODE export writes, and only the
9662 // export writes one; every other analysis reports numbers, which have no
9663 // notation to choose.
9664 if (!k.notation.empty() && !((s == "fluid" || s == "fld") && analysis == "odes"))
9666 "--notation selects the form of the exported ODE document and applies to -s fluid -a "
9667 "odes; '" + s + " / " + analysis + "' exports no equations");
9668 // --symbolic and --equilibria select the computer-algebra backend and ask it
9669 // to solve f(x) = 0; only the Jacobian consults one.
9670 if ((!k.symbolic.empty() || k.equilibria) &&
9671 !((s == "fluid" || s == "fld") && analysis == "jacobian"))
9673 "--symbolic selects the computer-algebra backend and --equilibria asks it for the "
9674 "solutions of f(x) = 0; both apply to -s fluid -a jacobian, and '" + s + " / " +
9675 analysis + "' consults no backend");
9676 // ---- the five JAR knobs, each refused where it would be accepted and never
9677 // read. Same discipline as every knob above: a caller who passed one to an
9678 // arm that does not consult it would believe a setting had been applied.
9679 // `-s jmt -a prob` reads it too, and reads it DIFFERENTLY: an exact chain is
9680 // indexed by the ENCODED row of a node's own state space, while a JMT log
9681 // records per-class job counts and nothing else, so there the vector is
9682 // `getProbAggr(node, state_a)`'s per-class count. Both are "the state this
9683 // probability is about"; which encoding it is in follows the solver.
9684 if (!k.state.empty() &&
9685 !(analysis == "prob" && (s == "ctmc" || s == "auto" || s == "jmt" || s == "nc")))
9687 "--state names the state `getProb(node, state)` asks about and applies to -a prob "
9688 "under -s ctmc, -s nc and -s jmt, the arms whose answer is indexed by a state; '" + s +
9689 " / " + analysis + "' reports a mean or a law over all of them");
9690 if (k.events && analysis != "sample")
9692 "--events is the length of ONE sampled trajectory and applies to -a sample; use "
9693 "--samples for a solver's run length ('" + s + " / " + analysis + "')");
9694 if (!k.percentiles.empty() && !(s == "mam" && (analysis == "cdf" || analysis == "cdfpasst" ||
9695 analysis == "perct")))
9697 "--percentiles names the levels getPerctRespT is read at and applies to -s mam -a "
9698 "perct-respt (and to the percentiles printed beside -a cdf); '" + s + " / " +
9699 analysis + "' inverts no response-time law");
9700 if (!k.reward_name.empty() && analysis != "rewardvalue")
9702 "--reward-name selects which declared reward -a reward-value returns the value "
9703 "function of; -a reward returns every reward's steady-state expectation and needs no "
9704 "name ('" + s + " / " + analysis + "')");
9705 if (k.timestep > 0.0 &&
9706 !(s == "ctmc" &&
9707 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")))
9709 "--timestep is the fixed output grid of a transient CTMC solve, `options.timestep` of "
9710 "ctmc_transient.m, and applies to -s ctmc -a tran, -a tranprob and -a tranreward; the "
9711 "fluid "
9712 "and simulated transients report the points their own integrator or engine produced "
9713 "('" + s + " / " + analysis + "')");
9714 if ((!k.rate_sched.empty() || k.ctmc_tv_ngrid > 0) && !(s == "ctmc" && analysis == "tran"))
9716 "--rate-sched (and --ctmc-tv-ngrid) makes the CTMC generator time-inhomogeneous, "
9717 "`options.config.rate_sched` of solver_ctmc_transient_analyzer.m, and applies to "
9718 "-s ctmc -a tran only ('" + s + " / " + analysis + "')");
9719 if ((!k.transient_method.empty() || k.fau_epsilon > 0.0 || k.fau_delta >= 0.0) &&
9720 !(s == "ctmc" &&
9721 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")))
9723 "--transient-method (and --fau-epsilon / --fau-delta) selects how the CTMC forward "
9724 "equation is advanced, `options.config.transient_method` of "
9725 "solver_ctmc_transient_analyzer.m, and applies to -s ctmc -a tran, -a tranprob and "
9726 "-a tranreward; every other analysis solves no forward equation ('" +
9727 s + " / " + analysis + "')");
9728 // --cdf-algorithm selects how the sojourn law is inverted, which only the NC
9729 // response-time distribution does; the CTMC one is read off tagged chains
9730 // and has no such choice.
9731 if (!k.cdf_algorithm.empty() && !(s == "nc" && analysis == "cdf"))
9733 "--cdf-algorithm selects the sojourn-time inversion of the NC response-time "
9734 "distribution and applies to -s nc -a cdf; '" + s + " / " + analysis +
9735 "' inverts no generating function");
9736 // The passage flags name the two state sets of getCdfFirstPassT and of
9737 // getFirstPassTMoments, which are the only two arms that time a state-set
9738 // passage. --passage-method selects the inversion and so belongs to the
9739 // curve alone; the moments involve no inversion at all.
9740 const bool passage_arm =
9741 (s == "ctmc" && (analysis == "firstpasst" || analysis == "firstpasstmom"));
9742 if ((!k.passage_from.empty() || !k.passage_into.empty() || k.passage_orders > 0 ||
9743 !k.passage_taboo.empty()) &&
9744 !passage_arm)
9746 "--passage-from, --passage-into, --passage-taboo and --passage-orders name the state "
9747 "sets and the moment order of -s ctmc -a firstpasst / firstpasstmom; '" + s + " / " +
9748 analysis + "' times no state-set passage");
9749 // --passage-tset is the grid of the CURVE; the moments arm has no time axis.
9750 if (!k.passage_tset.empty() && !(s == "ctmc" && analysis == "firstpasst"))
9752 "--passage-tset is the time grid of -s ctmc -a firstpasst; '" + s + " / " +
9753 analysis + "' evaluates no passage-time curve");
9754 if (!k.passage_method.empty() && !(s == "ctmc" && analysis == "firstpasst"))
9756 "--passage-method selects the transform inversion of -s ctmc -a firstpasst; '" + s +
9757 " / " + analysis + "' inverts none (the moments arm solves for them directly)");
9758 // --perm-engine selects the permanent estimator, which only the NC joint law
9759 // of the per-station totals uses; nothing else in the tree evaluates one.
9760 if (k.method_perm != "exact" && !(s == "nc" && analysis == "sysmarg"))
9762 "--perm-engine selects the permanent estimator of the NC joint total-queue-length "
9763 "law and applies to -s nc -a sysmarg; '" + s + " / " + analysis +
9764 "' evaluates no permanent");
9765 // The layered path's own knobs, refused here for the same reason every other
9766 // knob above is: a caller who passed one to a Network solve would believe a
9767 // setting was applied that no Network solver has. --sens-* is the exception:
9768 // getSensitivityTable is a @@NetworkSolver method, so it selects the branch
9769 // of `-s nc -a sens` as much as of the layered one.
9770 const bool nc_sens = s == "nc" && analysis == "sens";
9771 if (!nc_sens && (!k.sens_method.empty() || !k.sens_scheme.empty() || k.sens_step > 0.0))
9773 "--sens-method, --sens-scheme and --sens-step select the branch of a sensitivity "
9774 "table and apply to -i lqnx -a sens or to -s nc -a sens; '" + s + " / " + analysis +
9775 "' differentiates nothing");
9776 if (k.no_interlocking || k.interlock_knobs_given() || k.repeat > 0 || !k.layer_solver.empty() ||
9777 !k.ln_transient.empty() || !k.ln_transient_channels.empty() || k.nlp_knobs_given())
9779 "--no-interlocking, --interlock-*, --repeat, --layer-solver, --ln-transient*, --lld, --nlp-*, "
9780 "--warmstart and --mol-* are options of the layered solver and apply to -i lqnx; a Network "
9781 "model has no layers to interlock");
9782 if (s == "ba" && (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0))
9784 "--tol, --iter_tol and --iter_max do not apply to -s ba: a bound is a closed form, "
9785 "with nothing to converge");
9786 // Silent acceptance is the defect these guard against: a caller who passed
9787 // a QRF table to another solver would believe a parameterisation was
9788 // applied that nothing read.
9789 if (s != "ba" && (!k.qrf_params.empty() || !k.qrf_alpha.empty()))
9791 "--qrf-params and --qrf-alpha parameterise the QRF reduction bounds and apply to "
9792 "-s ba; '" + s + "' solves no reduction program");
9793 if (s != "ba" && k.level > 0)
9795 "--level is the hierarchy level of the SolverBA bound families and applies to -s ba; "
9796 "'" + s + "' has no bound hierarchy");
9797 if (is_sim && (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0))
9799 "--tol, --iter_tol and --iter_max do not apply to -s ssa: a sample path is not an "
9800 "iteration; use --samples to set its length");
9801 if (s == "mam" && k.iter_tol >= 0.0)
9803 "--iter_tol is not a SolverMAM option (MamOptions carries tol and iter_max); "
9804 "use --tol");
9805 if (s == "ag" && k.iter_tol >= 0.0)
9807 "--iter_tol is not a SolverAG option (AgOptions carries tol and iter_max, the "
9808 "tolerance and the sweep budget of the reversed-rate fixed point); use --tol");
9809 // --max-states BOUNDS AN OPEN AGENT'S QUEUE-LENGTH DIMENSION, and only the
9810 // RCAT agents have one: a closed class is bounded by its own population
9811 // instead, and 'inapinf' ignores the level entirely and solves the open
9812 // agents on the infinite state space. Accepting it elsewhere would be the
9813 // silent-acceptance defect these guards exist for -- a caller who passed it
9814 // to -s ctmc would believe a truncation applied that nothing truncated.
9815 if (k.max_states >= 0 && s != "ag")
9817 "--max-states truncates the queue-length dimension of a SolverAG agent and applies "
9818 "to -s ag; '" + s + "' truncates no agent (use --cutoff for a CTMC state space)");
9819 // The cutoff BOUNDS A STATE SPACE, and only SolverCTMC and the MAM
9820 // queue-length law have one -- the latter because an OPEN queue's level
9821 // process is unbounded and `getProb` has to stop somewhere. Accepting it
9822 // elsewhere would be the silent-acceptance defect: a caller who passed it to
9823 // -s mva would believe the answer was truncated when nothing truncated it.
9824 if (k.has_cutoff() && s != "ctmc" && !(s == "mam" && analysis == "prob") && s != "env")
9826 "--cutoff bounds the open population of a CTMC state space, and the level truncation "
9827 "of -s mam -a prob; '" + s + " / " + analysis + "' enumerates no states");
9828 // THE MATRIX SPELLING IS NARROWER THAN THE SCALAR ONE. Only the CTMC state
9829 // space is enumerated per station, so only it can honour a per-station
9830 // bound; the MAM level truncation and an environment's stage cutoff are one
9831 // number each. Refused rather than reduced to a maximum, because that
9832 // silently answers a LARGER chain than the caller asked for.
9833 if (!k.cutoff_mat.empty() && s != "ctmc")
9835 "--cutoff as a per-(station,class) matrix bounds an enumerated state space per "
9836 "station and applies to -s ctmc; '" + s + "' takes one number");
9837 // `--stage-solver` names the solver each STAGE of an environment is run
9838 // with, and nothing else has stages: a layer of an LQN takes
9839 // `--layer-solver`, which is a different set for a different reason (a
9840 // layer is solved in steady state, a stage transiently).
9841 if (!k.env_init.empty() && s != "env")
9843 "--env-init is the initial stage law of a random environment (SolverENV method "
9844 "'occupation') and applies to -s env; '" + s + "' has no stages");
9845 if (!k.stage_solver.empty() && s != "env")
9847 "--stage-solver names the solver each stage of a random environment is run with and "
9848 "applies to -s env; '" + s + "' has no stages");
9849 // `mva` and `nc` are admitted here and refused by the COUPLING when it needs
9850 // a transient they lack (SolverENV.methodRefusal): they serve 'avg'/'dec'.
9851 if (!k.stage_solver.empty() && k.stage_solver != "fluid" && k.stage_solver != "ctmc" &&
9852 k.stage_solver != "mam" && k.stage_solver != "mva" && k.stage_solver != "nc")
9854 "--stage-solver '" + k.stage_solver +
9855 "' is not available: the stage solvers of this port are fluid, ctmc, mam (statevec "
9856 "only), mva and nc (the 'avg'/'dec' limits only)");
9857 // The three `--map-env` knobs. Refused beside `-s ba`, whose bounds the
9858 // environment image does not bracket, and beside `-s env`, which IS the
9859 // environment solver and has a model with stages already.
9860 if (!k.map_env.empty() && k.map_env != "auto" && k.map_env != "default" && k.map_env != "off")
9861 throw line::InputError(line::ErrorKind{"Option"}, "--map-env '" + k.map_env +
9862 "' is not a value: use 'auto' (or 'default') or 'off'");
9863 if (!k.map_env_method.empty() && k.map_env_method != "auto" && k.map_env_method != "dec" &&
9864 k.map_env_method != "avg" && k.map_env_method != "meanfield")
9865 throw line::InputError(line::ErrorKind{"Option"},
9866 "--map-env-method '" + k.map_env_method +
9867 "' is not an environment recombination: use 'auto', 'meanfield', 'dec' or 'avg'");
9868 if ((!k.map_env.empty() || !k.map_env_method.empty() || k.map_env_maxstages) &&
9869 (s == "ba" || s == "env"))
9871 "--map-env* asks a solver to fall back on a random-environment image of a non-renewal "
9872 "process; '-s " + s +
9873 "' takes no such fallback (a bound must not be computed on an approximation, and the "
9874 "environment solver already takes a model with stages)");
9875 // `mam` IS THE STATE-VECTOR COUPLING'S BACKEND ONLY. The mean-field one
9876 // carries marginal means and reads them off a transient mean the LD-QBD
9877 // reduction does not produce; accepting it there would run the CTMC
9878 // ensemble under the MAM name.
9879 if (k.stage_solver == "mam" && k.method != "statevec")
9881 "--stage-solver mam applies to -s env --method statevec: the LD-QBD backend flattens "
9882 "its blocks into a generator the state-vector coupling propagates a distribution "
9883 "across, and the mean-field coupling carries marginal MEANS instead");
9884 // The two FJ_codes knobs configure ONE analyzer, solver_mam_fj, which the
9885 // MAM dispatch reaches on a homogeneous fork-join model. Accepting them
9886 // anywhere else would let a caller believe an accuracy setting had been
9887 // honoured by a solver that never read it.
9888 if ((k.fj_accuracy > 0 || !k.fj_tmode.empty()) && s != "mam")
9890 "--fj-accuracy and --fj-tmode configure the FJ_codes fork-join approximation of "
9891 "solver_mam_fj.m and apply to -s mam; '" + s + "' does not run it");
9892 // --sync-phases configures the synchronous approximation alone, i.e. -s mam
9893 // with method sync.faster or sync.slower.
9894 if (k.sync_phases >= 0 && (s != "mam" || (k.method != "sync.faster" && k.method != "sync.slower")))
9896 "--sync-phases sets the synchronous phase expansion of -s mam --method sync.faster or "
9897 "sync.slower; it is not read by '" + s + (k.method.empty() ? "" : "." + k.method) + "'");
9898 // --mapmsg-quantization configures the MAP/M/s+G reneging analyzer of -s mam
9899 // alone ('default' or 'retrial' reach it on a reneging queue).
9900 if (k.mapmsg_quantization >= 0 && s != "mam")
9902 "--mapmsg-quantization sets the patience regime count of the -s mam reneging "
9903 "analyzer; '" + s + "' does not run it");
9904 // --timescale gates the slotted branch of the MAM dispatch alone. SolverNC
9905 // has its own discrete product form and takes --slotted for it, so a
9906 // caller that names a time scale for any other solver is told rather than
9907 // silently answered on the continuous one.
9908 if (!k.timescale.empty() && s != "mam")
9910 "--timescale selects the time scale of the MAM discrete-time path and applies to "
9911 "-s mam; '" + s + "' does not read it (SolverNC takes --slotted)");
9912 // ---- `-a node`, ahead of the per-solver whitelists ---------------------
9913 // getAvgNodeTable is @@NetworkSolver's, not any one solver's: it is the
9914 // station table scattered to the node index space plus the two flow columns
9915 // recomputed from it, so every solver that produces an AvgResult can answer
9916 // it and none of them needs its own arm.
9917 // `-a node`, `-a sys`, `-a chain` and `-a nodechain` are the four
9918 // @@NetworkSolver tables that are VIEWS of one solved AvgResult -- scattered
9919 // to nodes, aggregated to chains, or reduced to the reference station -- so
9920 // they share the engine whitelist and the arithmetic ladder. Adding a
9921 // per-arm copy of either would let the four drift on which solvers and
9922 // which arithmetics they accept, for tables built from the same numbers.
9923 // `-s ssa` and `-s fluid` return their own solution types rather than an
9924 // AvgResult; `run_avg_engine` bridges them (`avg_result_from_sim`), so the
9925 // reference's rule holds here too -- a solver that reports an AvgTable
9926 // reports its four views. The two external wrappers obey the same rule and
9927 // are dispatched in their own arms above, which run before this one: they
9928 // validate knobs the ladder here knows nothing about, and LDES is handed the
9929 // model DOCUMENT rather than a parsed struct.
9930 if (analysis == "node" || analysis == "sys" || analysis == "chain" ||
9931 analysis == "nodechain" || analysis == "cache" || analysis == "item" || analysis == "fj") {
9932 const bool is_sim_engine = (s == "ssa" || s == "fluid");
9933 // The two cache tables are read off the SAME solved result, so they
9934 // belong to the same group; SolverNC is the only engine here whose
9935 // branches fill `AvgResult::cache`, and the arms say so when it is
9936 // empty rather than being whitelisted to nc alone -- `-s auto` on a
9937 // cache model resolves to nc, and refusing the token would refuse the
9938 // model.
9939 //
9940 // BOTH SIMULATORS ARE ADMITTED TO `-a cache`, and only there.
9941 // `run_avg_engine` fills `r.cache` for `-s ssa` from
9942 // `cache_metrics_of_ssa` -- the realized hit, delayed-hit and miss
9943 // SHARES the sample path measured -- and for `-s fluid` from the
9944 // cacheqn decomposition's converged split, which is the same quantity
9945 // its refreshed struct is renormalized at. Both are how
9946 // `SSA(model).getAvgCacheTable()` and `Fluid(model).getAvgCacheTable()`
9947 // answer in the reference, which reads them off the node the analyzer
9948 // wrote. `-a item` stays refused for both: the per-item occupancy is a
9949 // recursion of the
9950 // NC/MVA cache branches and no simulator forms it, so admitting it
9951 // would report an empty table for a quantity that was never measured.
9952 const bool cache_table = (analysis == "cache" || analysis == "item");
9953 const bool sim_cache_ok = (analysis == "cache");
9954 // `-a cache` and `-a item` are the CACHE tables, which RCAT does not
9955 // form -- so `ag` joins the AvgResult views and not those two.
9956 const bool ag_view = (s == "ag" && !cache_table);
9957 if ((s != "mva" && s != "auto" && s != "nc" && s != "mam" && s != "ba" && s != "ctmc" &&
9958 !ag_view && !is_sim_engine) ||
9959 (cache_table && is_sim_engine && !sim_cache_ok))
9960 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
9961 // `-a node`'s own wording is kept verbatim: it is the message a
9962 // caller has been reading since the arm existed, and the group
9963 // it now shares does not change what it says.
9964 (analysis == "node"
9965 ? std::string("-a node reports the per-node table of -s mva, nc, mam, ag, ba, "
9966 "ctmc, ssa, fluid, jmt, ldes and auto; '")
9967 : "-a " + analysis +
9968 " is a view of the station AvgResult and is reported by -s mva, nc, "
9969 "mam, " + (analysis == "item" ? "" : "ag, ") + "ba, ctmc" +
9970 (analysis == "item" ? "" : ", ssa, fluid, jmt, ldes") + " and auto; '") +
9971 s + "' does not return the station AvgResult it is built from");
9972 // The same refusal their own `-a avg` arms raise, for the same reason:
9973 // an SSA sample path is generated from exponential clocks and a fluid
9974 // trajectory is integrated by LSODA, so neither is carried by an exact
9975 // or an extended-precision backend. Refused BY NAME here rather than
9976 // narrowed silently in the ladder below.
9977 if (is_sim_engine && arith != "double")
9978 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
9979 "-a " + analysis + " under -s " + s +
9980 " is read off a " + (s == "ssa" ? "sample path" : "fluid trajectory") +
9981 ", which is transcendental; rerun with --arith double (got '" + arith + "')");
9982 const std::string eng = (s == "auto") ? std::string("mva") : s;
9983#define LINE_CLI_TABLE_LADDER(FN) \
9984 do { \
9985 if (arith == "double") return FN<double>(file, k, eng); \
9986 if (arith == "exact") return FN<line::Rational>(file, k, eng); \
9987 if (arith == "real:16") return FN<line::Real<16> >(file, k, eng); \
9988 if (arith == "real" || arith == "real:32") return FN<line::Real<32> >(file, k, eng); \
9989 if (arith == "real:64") return FN<line::Real<64> >(file, k, eng); \
9990 if (arith == "real:128") return FN<line::Real<128> >(file, k, eng); \
9991 if (arith == "real:256") return FN<line::Real<256> >(file, k, eng); \
9992 } while (0)
9993 if (analysis == "node") LINE_CLI_TABLE_LADDER(solve_model_node);
9994 if (analysis == "sys") LINE_CLI_TABLE_LADDER(solve_model_sys);
9995 if (analysis == "chain") LINE_CLI_TABLE_LADDER(solve_model_chain);
9996 if (analysis == "nodechain") LINE_CLI_TABLE_LADDER(solve_model_nodechain);
9997 if (analysis == "fj") LINE_CLI_TABLE_LADDER(solve_model_fj);
9998 if (analysis == "cache") LINE_CLI_TABLE_LADDER(solve_model_cache);
9999 if (analysis == "item") LINE_CLI_TABLE_LADDER(solve_model_item);
10000#undef LINE_CLI_TABLE_LADDER
10001 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10002 }
10003 if (s == "ctmc") {
10004 // The CTMC surface the port reaches: the AvgTable, plus the @@SolverCTMC
10005 // methods that are not means -- the generator and the state space
10006 // (getInfGen, getStateSpace), the transient occupancy (getTranProbSysAggr),
10007 // a marked trajectory (sampleSys), the declared rewards (getAvgReward),
10008 // the response-time laws (getCdfRespT, getCdfSysRespT) and the parametric
10009 // sensitivity (getSensitivityRanking).
10010 if (analysis != "avg" && analysis != "prob" && analysis != "gen" && analysis != "states" &&
10011 analysis != "tran" && analysis != "tranprob" && analysis != "tranreward" &&
10012 analysis != "sample" && analysis != "reward" && analysis != "rewardvalue" &&
10013 analysis != "cdf" && analysis != "sens" && analysis != "firstpasst" &&
10014 analysis != "firstpasstmom")
10015 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10016 "the CTMC solver ports -a avg, node, sys, chain, nodechain, prob, gen, states, "
10017 "tran, tranprob, tranreward, sample, reward, reward-value, cdf, first-passt, "
10018 "first-passt-moments and sens (got '" + analysis + "')");
10019 // The generator-free method answers MEANS and nothing else: mdd holds
10020 // the reachable set in a diagram, so there is no state space to list, no
10021 // filtration to split and no trajectory to walk. Refused by name rather
10022 // than served from the enumerated chain, which would report an answer
10023 // under a method that did not produce it.
10024 if (k.method == "mdd" && analysis != "avg")
10025 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10026 "the '" + k.method +
10027 "' method never builds the explicit generator, so it serves -a avg only (got '" +
10028 analysis + "'); use --method default for the state-space analyses");
10029 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0)
10031 "--tol, --iter_tol and --iter_max do not apply to -s ctmc: the stationary vector "
10032 "is obtained by a direct solve of pi Q = 0, with nothing to converge. The mdd "
10033 "method's level iteration has --mdd-tol and --mdd-maxiter of its own");
10034 // Every step from the generator to the means is a field operation, so
10035 // there is no arithmetic to refuse: exact returns the exact rational
10036 // stationary law. The transient analyses refuse inside, by name.
10037 if (arith == "double") return solve_model_ctmc<double>(file, k, analysis);
10038 if (arith == "exact") return solve_model_ctmc<line::Rational>(file, k, analysis);
10039 if (arith == "real:16") return solve_model_ctmc<line::Real<16> >(file, k, analysis);
10040 if (arith == "real" || arith == "real:32")
10041 return solve_model_ctmc<line::Real<32> >(file, k, analysis);
10042 if (arith == "real:64") return solve_model_ctmc<line::Real<64> >(file, k, analysis);
10043 if (arith == "real:128") return solve_model_ctmc<line::Real<128> >(file, k, analysis);
10044 if (arith == "real:256") return solve_model_ctmc<line::Real<256> >(file, k, analysis);
10045 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10046 }
10047
10048 // ---- the solvers that carry their own arithmetic restriction -----------
10049 // Each refuses BY NAME rather than being narrowed silently, and the reason
10050 // is the solver's own, not a limitation of the CLI.
10051 if (s == "mam") {
10052 // The @@SolverMAM surface the port reaches: the AvgTable, the
10053 // queue-length law (getProb / getProbMarg), the response-time law
10054 // (getCdfRespT and its getSjrnT / sjrnT aliases, plus getPerctRespT),
10055 // the transient means (getTranAvg) and the M/G/1-type internals of the
10056 // queue (getMAMResult).
10057 if (analysis != "avg" && analysis != "prob" && analysis != "cdf" &&
10058 analysis != "cdfpasst" && analysis != "perct" && analysis != "tran" &&
10059 analysis != "internals")
10060 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10061 "the MAM solver ports -a avg, node, prob, cdf, cdf-passt, perct-respt, tran and "
10062 "internals (got '" + analysis + "')");
10063 if (arith != "double")
10064 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
10065 "the MAM solver fits phase-type representations, whose fitter requires "
10066 "transcendental arithmetic; rerun with --arith double (got '" + arith + "')");
10067 if (analysis == "tran" && k.t1 < 0.0)
10069 "-s mam -a tran integrates the transient queue length over a horizon and there is "
10070 "no default for it; pass --tspan t0 t1");
10071 if (analysis == "prob") return solve_model_mam_prob<double>(file, k);
10072 if (analysis == "cdf") return solve_model_mam_cdf<double>(file, k, "cdf", "CdfRespT");
10073 // `getCdfPassT` IS `getCdfRespT` here, and that is the reference's
10074 // construction rather than an alias invented in the CLI: SolverMAM.java
10075 // computes both from the SAME `solver_mam_passage_time(sn, sn.proc,
10076 // options)` call. It is emitted under its own key so a caller that
10077 // asked the passage-time question is answered it, and the payload's
10078 // `type` records which of the two names produced the curve.
10079 if (analysis == "cdfpasst")
10080 return solve_model_mam_cdf<double>(file, k, "cdfpasst", "CdfPassT");
10081 if (analysis == "perct") return solve_model_mam_perct<double>(file, k);
10082 if (analysis == "tran") return solve_model_mam_tran<double>(file, k);
10083 if (analysis == "internals") return solve_model_mam_internals<double>(file, k);
10084 return solve_model_mam<double>(file, k);
10085 }
10086 if (s == "ssa") {
10087 // `-a cdf` is refused BY NAME rather than falling into the generic
10088 // message, because the refusal is the reference's own answer and not a
10089 // port gap: `@@SolverSSA/getCdfRespT.m` raises the same error, since SSA
10090 // samples state trajectories and not per-job sojourn times.
10091 if (analysis == "cdf") line::ssa::ssa_cdf_respt_refuse();
10092 if (analysis != "avg" && analysis != "prob" && analysis != "sample")
10093 throw line::UnsupportedError(line::ErrorKind{"Analysis"}, "the SSA solver ports -a avg, -a prob and -a sample (got '" +
10094 analysis + "')");
10095 if (arith != "double")
10096 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
10097 "an SSA sample path is generated from exponential clocks, which are "
10098 "transcendental; rerun with --arith double (got '" + arith + "')");
10099 if (analysis == "prob") return solve_model_ssa_prob<double>(file, k);
10100 if (analysis == "sample") return solve_model_ssa_sample<double>(file, k);
10101 return solve_model_ssa<double>(file, k);
10102 }
10103 if (s == "nc") {
10104 if (analysis != "avg" && analysis != "prob" && analysis != "marg" &&
10105 analysis != "sysmarg" && analysis != "cdf" && analysis != "sens" &&
10106 analysis != "normconst" && analysis != "busyperiod")
10107 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10108 "the NC solver ports -a avg, -a node, -a prob, -a marg, -a sysmarg, -a cdf, "
10109 "-a sens, -a normconst and -a busyperiod (got '" + analysis + "')");
10110 // The perfect sampler answers MEANS from iid stationary draws and yields
10111 // no normalizing constant, so none of the other NC analyses has anything
10112 // to be computed from. Refused by name rather than served by a different
10113 // NC route under the caller's method.
10114 if ((k.method == "cftp" || k.method == "cftp.approx") && analysis != "avg")
10115 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10116 "the '" + k.method +
10117 "' method draws stationary states and yields no normalizing constant, so it "
10118 "serves -a avg only (got '" + analysis + "'); use another --method for the "
10119 "other NC analyses");
10120 if (analysis == "busyperiod") {
10121 if (arith == "double") return solve_model_nc_busyp<double>(file, k);
10122 if (arith == "exact") return solve_model_nc_busyp<line::Rational>(file, k);
10123 if (arith == "real:16") return solve_model_nc_busyp<line::Real<16> >(file, k);
10124 if (arith == "real" || arith == "real:32")
10125 return solve_model_nc_busyp<line::Real<32> >(file, k);
10126 if (arith == "real:64") return solve_model_nc_busyp<line::Real<64> >(file, k);
10127 if (arith == "real:128") return solve_model_nc_busyp<line::Real<128> >(file, k);
10128 if (arith == "real:256") return solve_model_nc_busyp<line::Real<256> >(file, k);
10129 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10130 }
10131 if (analysis == "sysmarg") {
10132 if (arith == "double") return solve_model_nc_sysmarg<double>(file, k);
10133 if (arith == "exact") return solve_model_nc_sysmarg<line::Rational>(file, k);
10134 if (arith == "real:16") return solve_model_nc_sysmarg<line::Real<16> >(file, k);
10135 if (arith == "real" || arith == "real:32")
10136 return solve_model_nc_sysmarg<line::Real<32> >(file, k);
10137 if (arith == "real:64") return solve_model_nc_sysmarg<line::Real<64> >(file, k);
10138 if (arith == "real:128") return solve_model_nc_sysmarg<line::Real<128> >(file, k);
10139 if (arith == "real:256") return solve_model_nc_sysmarg<line::Real<256> >(file, k);
10140 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10141 }
10142 if (analysis == "marg") {
10143 if (arith == "double") return solve_model_nc_marg<double>(file, k);
10144 if (arith == "exact") return solve_model_nc_marg<line::Rational>(file, k);
10145 if (arith == "real:16") return solve_model_nc_marg<line::Real<16> >(file, k);
10146 if (arith == "real" || arith == "real:32")
10147 return solve_model_nc_marg<line::Real<32> >(file, k);
10148 if (arith == "real:64") return solve_model_nc_marg<line::Real<64> >(file, k);
10149 if (arith == "real:128") return solve_model_nc_marg<line::Real<128> >(file, k);
10150 if (arith == "real:256") return solve_model_nc_marg<line::Real<256> >(file, k);
10151 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10152 }
10153 // Served under NC too so `-s auto -a normconst`, which chooseSolverHeur
10154 // sends to NC on a product-form model, lands on an arm that answers.
10155 if (analysis == "normconst") {
10156 if (arith == "double") return solve_model_normconst<double>(file, k, s);
10157 if (arith == "exact") return solve_model_normconst<line::Rational>(file, k, s);
10158 if (arith == "real:16") return solve_model_normconst<line::Real<16> >(file, k, s);
10159 if (arith == "real" || arith == "real:32")
10160 return solve_model_normconst<line::Real<32> >(file, k, s);
10161 if (arith == "real:64") return solve_model_normconst<line::Real<64> >(file, k, s);
10162 if (arith == "real:128") return solve_model_normconst<line::Real<128> >(file, k, s);
10163 if (arith == "real:256") return solve_model_normconst<line::Real<256> >(file, k, s);
10164 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10165 }
10166 if (analysis == "sens") {
10167 if (arith == "double") return solve_model_nc_sens<double>(file, k);
10168 if (arith == "exact") return solve_model_nc_sens<line::Rational>(file, k);
10169 if (arith == "real:16") return solve_model_nc_sens<line::Real<16> >(file, k);
10170 if (arith == "real" || arith == "real:32")
10171 return solve_model_nc_sens<line::Real<32> >(file, k);
10172 if (arith == "real:64") return solve_model_nc_sens<line::Real<64> >(file, k);
10173 if (arith == "real:128") return solve_model_nc_sens<line::Real<128> >(file, k);
10174 if (arith == "real:256") return solve_model_nc_sens<line::Real<256> >(file, k);
10175 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10176 }
10177 if (analysis == "cdf") {
10178 if (arith == "double") return solve_model_nc_cdf<double>(file, k);
10179 if (arith == "exact") return solve_model_nc_cdf<line::Rational>(file, k);
10180 if (arith == "real:16") return solve_model_nc_cdf<line::Real<16> >(file, k);
10181 if (arith == "real" || arith == "real:32")
10182 return solve_model_nc_cdf<line::Real<32> >(file, k);
10183 if (arith == "real:64") return solve_model_nc_cdf<line::Real<64> >(file, k);
10184 if (arith == "real:128") return solve_model_nc_cdf<line::Real<128> >(file, k);
10185 if (arith == "real:256") return solve_model_nc_cdf<line::Real<256> >(file, k);
10186 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10187 }
10188 if (analysis == "prob") {
10189 if (arith == "double") return solve_model_nc_prob<double>(file, k);
10190 if (arith == "exact") return solve_model_nc_prob<line::Rational>(file, k);
10191 if (arith == "real:16") return solve_model_nc_prob<line::Real<16> >(file, k);
10192 if (arith == "real" || arith == "real:32")
10193 return solve_model_nc_prob<line::Real<32> >(file, k);
10194 if (arith == "real:64") return solve_model_nc_prob<line::Real<64> >(file, k);
10195 if (arith == "real:128") return solve_model_nc_prob<line::Real<128> >(file, k);
10196 if (arith == "real:256") return solve_model_nc_prob<line::Real<256> >(file, k);
10197 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10198 }
10199 if (arith == "double") return solve_model_nc<double>(file, k);
10200 if (arith == "exact") return solve_model_nc<line::Rational>(file, k);
10201 if (arith == "real:16") return solve_model_nc<line::Real<16> >(file, k);
10202 if (arith == "real" || arith == "real:32") return solve_model_nc<line::Real<32> >(file, k);
10203 if (arith == "real:64") return solve_model_nc<line::Real<64> >(file, k);
10204 if (arith == "real:128") return solve_model_nc<line::Real<128> >(file, k);
10205 if (arith == "real:256") return solve_model_nc<line::Real<256> >(file, k);
10206 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10207 }
10208 if (s == "ag") {
10209 // The @@SolverAG surface the port reaches: the AvgTable and its four
10210 // views, plus -a cdf as the inherited base-class exponential fallback.
10211 // RCAT converges a fixed point over the synchronization rates and
10212 // reports mean measures; it forms no state probability and no
10213 // transient, so there is nothing else to expose.
10214 if (analysis != "avg" && analysis != "cdf")
10215 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10216 "the AG solver ports -a avg (with its views -a node, -a sys, -a chain and "
10217 "-a nodechain) and -a cdf, the inherited exponential fallback: RCAT converges a "
10218 "fixed point over the synchronization rates and reports mean measures, forming no "
10219 "state probability or transient (got '" + analysis + "')");
10220 if (analysis == "cdf") {
10221 if (arith == "double") return solve_model_ag_cdf<double>(file, k);
10222 if (arith == "exact") return solve_model_ag_cdf<line::Rational>(file, k);
10223 if (arith == "real:16") return solve_model_ag_cdf<line::Real<16> >(file, k);
10224 if (arith == "real" || arith == "real:32")
10225 return solve_model_ag_cdf<line::Real<32> >(file, k);
10226 if (arith == "real:64") return solve_model_ag_cdf<line::Real<64> >(file, k);
10227 if (arith == "real:128") return solve_model_ag_cdf<line::Real<128> >(file, k);
10228 if (arith == "real:256") return solve_model_ag_cdf<line::Real<256> >(file, k);
10229 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10230 }
10231 if (arith == "double") return solve_model_ag<double>(file, k);
10232 if (arith == "exact") return solve_model_ag<line::Rational>(file, k);
10233 if (arith == "real:16") return solve_model_ag<line::Real<16> >(file, k);
10234 if (arith == "real" || arith == "real:32") return solve_model_ag<line::Real<32> >(file, k);
10235 if (arith == "real:64") return solve_model_ag<line::Real<64> >(file, k);
10236 if (arith == "real:128") return solve_model_ag<line::Real<128> >(file, k);
10237 if (arith == "real:256") return solve_model_ag<line::Real<256> >(file, k);
10238 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10239 }
10240 if (s == "ba") {
10241 if (analysis != "avg" && analysis != "bounds" && analysis != "cdf")
10242 throw line::UnsupportedError(line::ErrorKind{"Analysis"}, "the BA solver ports -a avg, -a node, -a bounds and "
10243 "-a cdf, the inherited exponential fallback (got '" +
10244 analysis + "')");
10245 if (analysis == "cdf") {
10246 if (arith == "double") return solve_model_ba_cdf<double>(file, k);
10247 if (arith == "exact") return solve_model_ba_cdf<line::Rational>(file, k);
10248 if (arith == "real:16") return solve_model_ba_cdf<line::Real<16> >(file, k);
10249 if (arith == "real" || arith == "real:32")
10250 return solve_model_ba_cdf<line::Real<32> >(file, k);
10251 if (arith == "real:64") return solve_model_ba_cdf<line::Real<64> >(file, k);
10252 if (arith == "real:128") return solve_model_ba_cdf<line::Real<128> >(file, k);
10253 if (arith == "real:256") return solve_model_ba_cdf<line::Real<256> >(file, k);
10254 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10255 }
10256 if (analysis == "bounds") {
10257 if (arith == "double") return solve_model_ba_bounds<double>(file, k);
10258 if (arith == "exact") return solve_model_ba_bounds<line::Rational>(file, k);
10259 if (arith == "real:16") return solve_model_ba_bounds<line::Real<16> >(file, k);
10260 if (arith == "real" || arith == "real:32")
10261 return solve_model_ba_bounds<line::Real<32> >(file, k);
10262 if (arith == "real:64") return solve_model_ba_bounds<line::Real<64> >(file, k);
10263 if (arith == "real:128") return solve_model_ba_bounds<line::Real<128> >(file, k);
10264 if (arith == "real:256") return solve_model_ba_bounds<line::Real<256> >(file, k);
10265 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10266 }
10267 if (arith == "double") return solve_model_ba<double>(file, k);
10268 if (arith == "exact") return solve_model_ba<line::Rational>(file, k);
10269 if (arith == "real:16") return solve_model_ba<line::Real<16> >(file, k);
10270 if (arith == "real" || arith == "real:32") return solve_model_ba<line::Real<32> >(file, k);
10271 if (arith == "real:64") return solve_model_ba<line::Real<64> >(file, k);
10272 if (arith == "real:128") return solve_model_ba<line::Real<128> >(file, k);
10273 if (arith == "real:256") return solve_model_ba<line::Real<256> >(file, k);
10274 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10275 }
10276 if (s == "lqns") {
10277 if (analysis != "avg" && analysis != "cdf")
10278 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10279 "SolverLQNS on a Network reports -a avg and -a cdf, the inherited exponential "
10280 "fallback: "
10281 "qnsolver returns one chain-level table of means and computes no state "
10282 "probability and no transient (got '" + analysis + "')");
10283 // The numbers arrive as the decimal text qnsolver printed, so every
10284 // digit past double is one this port invented; the ladder is refused
10285 // rather than run at a width the answer does not have.
10286 if (arith != "double")
10287 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
10288 "SolverLQNS reads its results back as the decimal text an external binary printed, "
10289 "which is double at best; --arith " + arith +
10290 " would report a precision the tool never produced");
10291 if (analysis == "cdf") return solve_model_lqns_network_cdf(file, k);
10292 return solve_model_lqns_network<double>(file, k);
10293 }
10294 if (s == "fluid" || s == "fld") {
10295 if (analysis != "avg" && analysis != "odes" && analysis != "var" &&
10296 analysis != "tranvar" && analysis != "jacobian" && analysis != "tran" &&
10297 analysis != "prob" && analysis != "cdf" && analysis != "aoi" &&
10298 analysis != "statevec")
10299 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10300 "the fluid solver ports -a avg, -a tran, -a tranvar, -a prob, -a cdf, -a aoi, "
10301 "-a odes, -a statevec, -a var and -a jacobian (got '" + analysis + "')");
10302 // The drift is integrated by LSODA, whose coefficients assume double;
10303 // a higher-precision request is refused rather than quietly narrowed.
10304 if (arith != "double")
10305 throw line::UnsupportedError(line::ErrorKind{"Arithmetic"},
10306 "the fluid solver integrates its drift with LSODA, which is double precision by "
10307 "construction; rerun with --arith double (got '" + arith + "')");
10308 if (analysis == "odes") return solve_model_fluid_odes<double>(file, k);
10309 if (analysis == "statevec") return solve_model_fluid_statevec<double>(file, k);
10310 if (analysis == "jacobian") return solve_model_fluid_jacobian<double>(file, k);
10311 if (analysis == "var") return solve_model_fluid_var<double>(file, k);
10312 if (analysis == "tranvar") return solve_model_fluid_tranvar<double>(file, k);
10313 if (analysis == "tran") return solve_model_fluid_tran<double>(file, k);
10314 if (analysis == "prob") return solve_model_fluid_prob<double>(file, k);
10315 if (analysis == "cdf") return solve_model_fluid_cdf<double>(file, k);
10316 if (analysis == "aoi") return solve_model_fluid_aoi<double>(file, k);
10317 return solve_model_fluid<double>(file, k);
10318 }
10319 if (analysis == "prob") {
10320 if (arith == "double") return solve_model_prob<double>(file, k);
10321 if (arith == "exact") return solve_model_prob<line::Rational>(file, k);
10322 if (arith == "real:16") return solve_model_prob<line::Real<16> >(file, k);
10323 if (arith == "real" || arith == "real:32") return solve_model_prob<line::Real<32> >(file, k);
10324 if (arith == "real:64") return solve_model_prob<line::Real<64> >(file, k);
10325 if (arith == "real:128") return solve_model_prob<line::Real<128> >(file, k);
10326 if (arith == "real:256") return solve_model_prob<line::Real<256> >(file, k);
10327 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10328 }
10329 if (analysis == "marg") {
10330 if (arith == "double") return solve_model_marg<double>(file, k);
10331 if (arith == "exact") return solve_model_marg<line::Rational>(file, k);
10332 if (arith == "real:16") return solve_model_marg<line::Real<16> >(file, k);
10333 if (arith == "real" || arith == "real:32") return solve_model_marg<line::Real<32> >(file, k);
10334 if (arith == "real:64") return solve_model_marg<line::Real<64> >(file, k);
10335 if (arith == "real:128") return solve_model_marg<line::Real<128> >(file, k);
10336 if (arith == "real:256") return solve_model_marg<line::Real<256> >(file, k);
10337 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10338 }
10339 if (analysis == "normconst") {
10340 if (arith == "double") return solve_model_normconst<double>(file, k, s);
10341 if (arith == "exact") return solve_model_normconst<line::Rational>(file, k, s);
10342 if (arith == "real:16") return solve_model_normconst<line::Real<16> >(file, k, s);
10343 if (arith == "real" || arith == "real:32")
10344 return solve_model_normconst<line::Real<32> >(file, k, s);
10345 if (arith == "real:64") return solve_model_normconst<line::Real<64> >(file, k, s);
10346 if (arith == "real:128") return solve_model_normconst<line::Real<128> >(file, k, s);
10347 if (arith == "real:256") return solve_model_normconst<line::Real<256> >(file, k, s);
10348 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10349 }
10350 if (analysis == "cdf") {
10351 // The inherited base-class exponential fallback over the MVA means,
10352 // as @@NetworkSolver/getCdfRespT.m serves it for SolverMVA
10353 if (arith == "double") return solve_model_mva_cdf<double>(file, k);
10354 if (arith == "exact") return solve_model_mva_cdf<line::Rational>(file, k);
10355 if (arith == "real:16") return solve_model_mva_cdf<line::Real<16> >(file, k);
10356 if (arith == "real" || arith == "real:32")
10357 return solve_model_mva_cdf<line::Real<32> >(file, k);
10358 if (arith == "real:64") return solve_model_mva_cdf<line::Real<64> >(file, k);
10359 if (arith == "real:128") return solve_model_mva_cdf<line::Real<128> >(file, k);
10360 if (arith == "real:256") return solve_model_mva_cdf<line::Real<256> >(file, k);
10361 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
10362 }
10363 if (analysis != "avg")
10364 throw line::UnsupportedError(line::ErrorKind{"Analysis"},
10365 "the model-solving path ports -a avg, -a node, -a prob, -a marg, -a normconst and "
10366 "-a cdf (got '" +
10367 analysis + "')");
10368 // The mvaDispatch ladder's transcendental-only analyzers (open-queue closed
10369 // forms, DPS-exact, Marie, size-based) refuse by name under exact/real via
10370 // their if-constexpr guards, so the field-arithmetic branches (product-form
10371 // MVA, LD scaling) stay exact while a transcendental model is refused rather
10372 // than silently degraded.
10373 if (arith == "double") return solve_model_mva<double>(file, k);
10374 if (arith == "exact") return solve_model_mva<line::Rational>(file, k);
10375 if (arith == "real:16") return solve_model_mva<line::Real<16> >(file, k);
10376 if (arith == "real" || arith == "real:32") return solve_model_mva<line::Real<32> >(file, k);
10377 if (arith == "real:64") return solve_model_mva<line::Real<64> >(file, k);
10378 if (arith == "real:128") return solve_model_mva<line::Real<128> >(file, k);
10379 if (arith == "real:256") return solve_model_mva<line::Real<256> >(file, k);
10380 throw line::InputError(
10381 "--arith '" + arith +
10382 "' is not a model-solve backend; use double, exact or real:<16|32|64|128|256>");
10383}
10384
10385/**
10386 * The no-argument and `-h` message: the flags a first run actually needs.
10387 *
10388 * The full reference below is ~460 lines, which is not a thing a human reads at
10389 * a prompt; it stays one flag away under `--help-all` rather than being the
10390 * first thing the binary says.
10391 */
10392void print_brief_help() {
10393 std::printf(
10394 "LINE solver (C++), version %s\n"
10395 "\n"
10396 "Usage: line-cli -f <model> [-s <solver>] [-a <analysis>] [-o <format>]\n"
10397 " cat model.json | line-cli\n"
10398 "\n"
10399 "Common options:\n"
10400 " -f, --file <path> model file: .json (network), .lqnx or .in (layered),\n"
10401 " .jsimg (JMT), .pnml (Petri net); stdin if omitted\n"
10402 " -s, --solver <name> auto (default), mva, nc, ctmc, mam, fluid, ssa,\n"
10403 " ldes, jmt, ag, ba, uq; ln for layered models, env\n"
10404 " for random environments\n"
10405 " -a, --analysis <type> avg (default), node, sys, chain, tran, prob, cdf,\n"
10406 " states, sample, normconst, bounds, ... (comma list)\n"
10407 " -o, --output <fmt> readable (default) | json | jsimg (export the\n"
10408 " model as a JMT simulation document)\n"
10409 " --method <name> algorithm within the chosen solver\n"
10410 " --samples <n> simulation run length (ssa, ldes); default 10000\n"
10411 " --seed <n> random seed; default 23000\n"
10412 " -v, --verbosity <lvl> silent | standard | debug; debug turns on\n"
10413 " the solver console, a running progress log\n"
10414 " --find-solver [m] which solvers and methods can analyze this model,\n"
10415 " optionally only those answering measure <m>\n"
10416 " (avg, tran, cdf, prob, sample, ...); reports and exits\n"
10417 " --find-solver-all [m] the same, keeping the refused pairs and the\n"
10418 " reason each was refused\n"
10419 " -h, --help this message\n"
10420 " --help-all every option, solver by solver\n"
10421 " -V, --version version string\n"
10422 " --install environment check: which optional backends\n"
10423 " (Java/JMT, LQNS, qnsolver, SageMath) are reachable\n"
10424 "\n"
10425 "Examples:\n"
10426 " line-cli -f model.json solve, letting auto pick the solver\n"
10427 " line-cli -f model.json -s mva -a avg mean queue lengths, MVA\n"
10428 " line-cli -f model.lqnx -s ln solve a layered model\n"
10429 " line-cli -f model.json -s ssa --samples 1e6 -o json\n"
10430 " line-cli -f model.json --find-solver what can solve this model\n"
10431 " line-cli -f model.json --find-solver cdf ... and return a passage-time law\n"
10432 "\n"
10433 "Each solver has flags of its own (tolerances, horizons, engine choices):\n"
10434 "run `line-cli --help-all` for the full reference.\n",
10435 kVersion);
10436}
10437
10438void print_help() {
10439 std::printf(
10440 "LINE multiprecision solver (C++), version %s\n"
10441 "\n"
10442 "Usage: line-cli [OPTIONS]\n"
10443 " cat model.json | line-cli -i json [OPTIONS]\n"
10444 " line-cli model.lqnx [OPTIONS]\n"
10445 "\n"
10446 "Options (flag-compatible with jline.cli.LineCLI):\n"
10447 " -f, --file <path> model file; stdin when omitted (json only)\n"
10448 " -i, --input <fmt> input format: json | jsim | jsimg | jsimw |\n"
10449 " lqnx | xml | srvn | pnml. Without it a .lqnx or\n"
10450 " .xml path is read as a layered model, a .in\n"
10451 " path (or a .lqn one not opening with '<') as\n"
10452 " a layered model in the LQNS SRVN text, a\n"
10453 " .jsim/.jsimg/.jsimw path as a JMT simulation\n"
10454 " document, a .pnml path as a place/transition\n"
10455 " net (ISO/IEC 15909-2), and anything else as a\n"
10456 " Network model.json. The three jsim spellings\n"
10457 " name ONE format, as they do in JMT\n"
10458 " -o, --output <fmt> output format: readable | json | jsimg\n"
10459 " (| layers, lqnx for layered models).\n"
10460 " jsimg exports the model as a JMT simulation\n"
10461 " document and solves nothing -- the write half\n"
10462 " of -i jsim|jsimg|jsimw, and the same three\n"
10463 " spellings are accepted. --seed, --samples and\n"
10464 " --tspan set the controls its header carries\n"
10465 " json is honoured by EVERY analysis, not only\n"
10466 " -a avg: each answers under a key named after\n"
10467 " its -a, with the arithmetic and the resolved\n"
10468 " method beside it, and every index inside a\n"
10469 " payload is 0-based against the tables' 1-based\n"
10470 " columns (each payload states its indexBase)\n"
10471 " -s, --solver <name> Network: auto, mva, nc, ctmc, mam, ba, ssa, fluid, uq,\n"
10472 " ldes (the SSJ discrete-event engine, run as a\n"
10473 " subprocess on common/ldes or common/ldes.jar),\n"
10474 " jmt (the Java Modelling Tools engine),\n"
10475 " lqns (its qns methods, the external qnsolver\n"
10476 " binary)\n"
10477 " layered: auto, ln, ln.mva, ln.comom, lqns,\n"
10478 " ldes (the native in-process LN simulator, a\n"
10479 " sample path of the layered model itself and\n"
10480 " not a decomposition into layers; it takes\n"
10481 " --samples and --seed, and NOT the --ldes-*\n"
10482 " family, which configures the subprocess\n"
10483 " engine that answers -s ldes on a Network)\n"
10484 " environment: env (an Environment model.json)\n"
10485 " -a, --analysis <type> analysis: avg, node, sys, chain, nodechain,\n"
10486 " stage, cache, item, fj, prob, marg, cdf, cdf-passt,\n"
10487 " perct-respt, tran, tran-cdf-respt,\n"
10488 " tran-cdf-passt, tranprob, tranreward,\n"
10489 " reward-value, normconst, gen, states, sample,\n"
10490 " reward, sens, first-passt, odes, statevec,\n"
10491 " var, tranvar, busyperiod,\n"
10492 " jacobian, aoi, internals, bounds, posterior,\n"
10493 " interval. A COMMA LIST runs several in order\n"
10494 " (`-a avg,sys`), emitting one -o json envelope\n"
10495 " per analysis rather than one merged object.\n"
10496 " The JAR CLI's own spellings are accepted as\n"
10497 " aliases -- cdf-respt, prob-sys-aggr, tran-avg,\n"
10498 " generator, reward-steady, all -- and collapse\n"
10499 " onto the arm that already answers them whole:\n"
10500 " -a prob reports getProbSys, getProbSysAggr and\n"
10501 " the per-station pair together, -a sample walks\n"
10502 " all four samplers at once, and -a stage IS\n"
10503 " -a avg on a Network (one implicit stage).\n"
10504 " Which names a\n"
10505 " solver serves is stated by its own refusal;\n"
10506 " ssa serves avg, prob and sample (prob and\n"
10507 " sample run the SERIAL engine whatever -m\n"
10508 " said, since the NRM simulates counts rather\n"
10509 " than the state encoding)\n"
10510 " -v, --verbosity <lvl> silent | standard | debug; debug turns on\n"
10511 " the solver console, a running progress log\n"
10512 " of every solver run\n"
10513 " -d, --seed <n> random seed (SSA, nc --method cftp); default\n"
10514 " 23000. -d is the JAR CLI's spelling\n"
10515 " --warmupfrac <f> SSA: leading fraction of the path discarded\n"
10516 " before the means are taken, in [0,1); unset,\n"
10517 " 0.2 on a model with an open class, else 0\n"
10518 " --pstar <p> fluid: exponent of the p-norm smoothing of the\n"
10519 " drift; without it the hard min() is integrated\n"
10520 " --fluid-boundary <m> fluid (Petri dae): reflect (default) reports a\n"
10521 " fluid place level held at a bound as the bound;\n"
10522 " diffusion as a reflected Brownian motion's mean\n"
10523 " --nonmkv <method> ctmc, mam, fluid, ag: bernstein (default) fits a\n"
10524 " phase-type to a non-Markovian law; none skips it\n"
10525 " --busyperiod <n,..> -a busyperiod: the orders wanted; default 1\n"
10526 " --busyperiod-subnet <i,..>\n"
10527 " -a busyperiod: the 1-based stations forming the\n"
10528 " subnetwork. Required -- a busy period is defined\n"
10529 " for a NAMED set and no default can choose one\n"
10530 " --method <name> algorithm within the solver\n"
10531 " --samples <n> simulation run length (SSA); default 10000.\n"
10532 " Accepts 1e6 as well as 1000000. A simulation\n"
10533 " figure is only a measurement WITH this number,\n"
10534 " which is why the SSA banner reports it back.\n"
10535 " Also the draw count of nc --method cftp,\n"
10536 " default 1e5, where the draw is the answer.\n"
10537 " --mdd-tol <x> level-iteration tolerance of ctmc --method mdd;\n"
10538 " default 1e-12. NOT --tol: that iteration is an\n"
10539 " inner solve whose fixed point is checked\n"
10540 " against the population invariant at 1e-6, so a\n"
10541 " solver-sized tolerance stops short of it\n"
10542 " --mdd-maxiter <n> coupled sweeps before ctmc --method mdd is\n"
10543 " declared non-convergent; default 500\n"
10544 " --level <n> hierarchy level of the ba pbh/bjbh/cbh/sib\n"
10545 " families; default 2\n"
10546 " --qrf-params <j> JSON (inline or a path) with the QRF blocking\n"
10547 " tables of -s ba --method qrf.bas|qrf.rsrd:\n"
10548 " f, MR, BB, MM, MM1, ZZ and optionally F, the\n"
10549 " fields sn_to_qrf_params assembles. ZM is\n"
10550 " derived from ZZ. There is no default: assuming\n"
10551 " no blocking puts the bound ~31x farther from\n"
10552 " exact, so its absence is refused\n"
10553 " --qrf-alpha <j> JSON (nstations x N) load-dependent scaling of\n"
10554 " the ba qrf.mmi.ld, qrf.mmi.linear and qrf.rsrd\n"
10555 " arms; default all ones\n"
10556 " --tol <x> convergence tolerance (mva, nc, mam, ag, fluid)\n"
10557 " --iter_tol <x> outer-loop tolerance (mva, nc, fluid)\n"
10558 " --iter_max <n> iteration cap (mva, nc, mam, ag, fluid)\n"
10559 " --max-states <n> truncation level of an OPEN agent's queue-length\n"
10560 " dimension (ag), options.config.maxStates;\n"
10561 " default 100. A closed class is bounded by its\n"
10562 " own population instead, and --method inapinf\n"
10563 " ignores the level and solves the open agents on\n"
10564 " the infinite state space\n"
10565 " --fork-join <arm> which fork-join transform the mean-value fixed\n"
10566 " point takes (mva, nc): default|mmt|fjt is the\n"
10567 " MMT transform, ht|heidelberger-trivedi the\n"
10568 " Heidelberger-Trivedi one, which is CLOSED\n"
10569 " models only and a different answer to the same\n"
10570 " model rather than a faster route to one\n"
10571 " --cutoff <n|matrix> open jobs per class in the CTMC state space;\n"
10572 " a matrix is per (station,class), '1,1,0;3,3,0;0,0,3'\n"
10573 " (ctmc); without it the reference's\n"
10574 " ceil(6000^(1/(M*K))) is used and reported.\n"
10575 " Also the level truncation of mam -a prob,\n"
10576 " whose open queue has no bound of its own\n"
10577 " --fj-accuracy <n> FJ_codes truncation C of the queue-length\n"
10578 " difference between the two fork-join\n"
10579 " branches (mam, homogeneous fork-join);\n"
10580 " default 100, larger is more accurate\n"
10581 " --fj-tmode <mode> how that approximation solves for its T\n"
10582 " matrix: NARE (default) or Sylvester\n"
10583 " --sync-phases <n> synchronous phase expansion of mam\n"
10584 " --method sync.faster|sync.slower; 0 (default)\n"
10585 " solves without expansion, n >= 1 needs a\n"
10586 " deterministic service and tightens both bounds\n"
10587 " --mapmsg-quantization <n>\n"
10588 " patience regimes of the mam MAP/M/s+G reneging\n"
10589 " analyzer; default 81, at least 2\n"
10590 " --timescale <mode> auto (default), discrete or continuous: how\n"
10591 " -s mam reads the time scale. auto lets the\n"
10592 " distributions decide; discrete raises rather\n"
10593 " than solve a model that mixes lattice and\n"
10594 " non-lattice laws. The slot is --slotlength\n"
10595 " --tspan <t0>:<t1> transient horizon (ctmc -a tranprob,\n"
10596 " mam -a tran, fluid); a bare <t1> starts at 0.\n"
10597 " For the CTMC and MAM there is no default:\n"
10598 " pi(t) on an unstated horizon is not a\n"
10599 " quantity. For the fluid solver it bounds the\n"
10600 " integration, and is what -s fluid --method kp\n"
10601 " reports its covariance AT\n"
10602 " -n, --node <n> 1-based stateful node a state query is\n"
10603 " labelled by (ctmc -a tranprob, -a sample and\n"
10604 " ssa -a sample), the\n"
10605 " queue mam -a prob reports, or the station\n"
10606 " mva -a marg reports; without it the\n"
10607 " whole network is reported (the MAM queries\n"
10608 " take the model's only Queue)\n"
10609 " -c, --class <r> 1-based job class of mva -a marg; without it\n"
10610 " every class is reported\n"
10611 " NOTE ON THE INDEX BASE: -n and -c are 1-BASED\n"
10612 " here, as every station index this CLI takes\n"
10613 " is, and 0-BASED in jline.cli.LineCLI, which\n"
10614 " indexes as Java does. The short spellings are\n"
10615 " accepted so one command line parses in both,\n"
10616 " but the SAME number names a different node --\n"
10617 " the two bridges (cpp_dispatch, jar_dispatch)\n"
10618 " each convert for their own CLI\n"
10619 " --marg-states <ns> comma-separated job counts the mva -a marg\n"
10620 " curve is evaluated at, the reference's\n"
10621 " state_m; without it each law takes its own\n"
10622 " default range (0..N_r closed, mean + 5 sigma\n"
10623 " Poisson, the 1e-10 tail geometric)\n"
10624 " --notation <form> scalar (default) | matrix, the form the ODE\n"
10625 " export writes (fluid -a odes only)\n"
10626 " --symbolic <b> computer-algebra backend of fluid -a\n"
10627 " jacobian: auto (default, searches for a\n"
10628 " line-sage-rest service), a URL, an image\n"
10629 " name, or none to differentiate locally\n"
10630 " --equilibria also ask the backend for the solutions of\n"
10631 " f(x) = 0 (fluid -a jacobian). Needs a\n"
10632 " backend: solving is not differentiating\n"
10633 " --cdf-algorithm <a> exact (default, pfqn_stdf) | rd\n"
10634 " (pfqn_stdf_heur), how the sojourn law is\n"
10635 " inverted (nc -a cdf only)\n"
10636 " --perm-engine <e> exact (default, Ryser) | spm | bethe | heur |\n"
10637 " huberlaw | adapart, the permanent estimator\n"
10638 " of nc -a sysmarg. The five approximations\n"
10639 " refuse a demand matrix with a zero entry;\n"
10640 " spm is the saddle point, whose cost does not\n"
10641 " grow with the population\n"
10642 " --env-init <p,...> initial probability of each environment stage,\n"
10643 " options.config.env_init of -s env --method\n"
10644 " occupation; default the first stage. --tspan\n"
10645 " <t1> sets its horizon, unbounded without it\n"
10646 " --tran-points <n> points on the uniform transient grid the ENV\n"
10647 " mean-field coupling sums its stage exit\n"
10648 " metrics over (env only); default 1001\n"
10649 " --state <n,...> the ENCODED state row of the node named by\n"
10650 " --node that -a prob asks about, i.e.\n"
10651 " getProb(node, state)'s second argument; without\n"
10652 " it the query is about the model's default\n"
10653 " initial state\n"
10654 " --events <n> length of ONE sampled trajectory (-a sample);\n"
10655 " default 1000. NOT --samples, which is a\n"
10656 " solver's run length\n"
10657 " --timestep <dt> fixed output step of a transient CTMC solve\n"
10658 " (-a tranprob, -a tranreward), options.timestep\n"
10659 " of ctmc_transient.m; without it the grid is the\n"
10660 " integrator's own adaptive one. It changes WHERE\n"
10661 " the solution is reported, not how it is\n"
10662 " computed: the grid points are read off the same\n"
10663 " interpolant\n"
10664 " --transient-method <m> ode (default) or fau, how the CTMC forward\n"
10665 " equation is advanced over the output grid\n"
10666 " --fau-epsilon <e> fau: probability mass the grid may discard\n"
10667 " --fau-delta <d> fau: occupancy below which a state is dropped\n"
10668 " --rate-sched <j> -s ctmc -a tran: time-varying rates, a JSON array\n"
10669 " (inline or a file) of {station, class, tgrid,\n"
10670 " rates[, nominal]}; station and class by name or\n"
10671 " 1-based index. The rate of that pair follows the\n"
10672 " piecewise-linear schedule, held at its end values\n"
10673 " outside tgrid (options.config.rate_sched)\n"
10674 " --ctmc-tv-ngrid <n> grid size of the --rate-sched propagator (100)\n"
10675 " --percentiles <p,..> levels getPerctRespT is read at (-s mam),\n"
10676 " as fractions (0.9) or percents (90); default\n"
10677 " 0.50,0.90,0.95,0.99, the reference's pers_stored\n"
10678 " --reward-name <nm> which declared reward -a reward-value returns\n"
10679 " the value function of. REQUIRED there and never\n"
10680 " defaulted: two rewards have different value\n"
10681 " functions, and picking one would mislabel it\n"
10682 " --map-env <mode> auto (the default) | off, whether a model whose ONLY\n"
10683 " unsupported features are MAP/MMPP2/MMAP/MPH is\n"
10684 " solved through a random-environment image of the\n"
10685 " modulating chain instead of being refused. Not a\n"
10686 " -s env knob: it applies to mva, nc and fluid,\n"
10687 " which is where the refusal it replaces comes\n"
10688 " from. off restores that plain refusal\n"
10689 " --map-env-method <m> auto (default) | dec | avg | meanfield, the\n"
10690 " recombination the image is solved with. auto\n"
10691 " picks from the environment timescale; meanfield\n"
10692 " needs a stage backend and is refused by name\n"
10693 " where there is none\n"
10694 " --map-env-maxstages <n> cap on the stages map2renv draws from the\n"
10695 " modulating chain; 0, the default, takes every\n"
10696 " phase. Beyond the JAR, which has no such cap\n"
10697 " -p, --port <n> run as a solve SERVER on this port, speaking\n"
10698 " LineWebSocketServer's protocol: one WebSocket\n"
10699 " text message per connection, its first line the\n"
10700 " comma-separated argument list and its remainder\n"
10701 " the model document; the CLI's output comes back\n"
10702 " as one text message. Plaintext, one connection\n"
10703 " at a time, and -f is refused beside it\n"
10704 " -m, --maxreq <n> quit after serving n requests; without it the\n"
10705 " server runs until interrupted\n"
10706 " -h, --help the short message: the flags a first run needs\n"
10707 " --help-all this message, every option solver by solver\n"
10708 " -V, --version version string\n"
10709 " --install environment check: report which optional backends\n"
10710 " (Java/JMT, LQNS, qnsolver, SageMath) are reachable\n"
10711 "\n"
10712 "Layered models (-i lqnx|xml) additionally take:\n"
10713 " --layer-solver <s> solver run in each layer: mva (default)|nc|\n"
10714 " fluid|ssa|mam|ctmc|ldes|jmt, the C++ spelling of the\n"
10715 " reference's\n"
10716 " factory argument: LN(model, @(m) MVA(m))\n"
10717 " against LN(model, @(m) Fluid(m)). They converge\n"
10718 " to DIFFERENT fixed points, not to the same one\n"
10719 " by different routes, because each layer's\n"
10720 " results feed the next outer iteration's demands.\n"
10721 " fluid is double only and refuses a layer with a\n"
10722 " fork; ssa, ldes and jmt are NOISY, so the outer\n"
10723 " loop switches to the Robbins-Monro / Polyak-Ruppert\n"
10724 " controller, and --samples/--seed set their run\n"
10725 " length and BASE seed (layer e of iteration it runs\n"
10726 " at seed+(it-1)*nlayers+e). ldes is the native\n"
10727 " engine in process; jmt runs JSIM per layer\n"
10728 " --method <name> the LN method: default | srvn | srvn.ph |\n"
10729 " srvn.cs | flat | flat.cs | flat.ph | moment3 |\n"
10730 " mw.upper | mw.lower. srvn*/flat* choose the\n"
10731 " layering (one submodel per server, or one for\n"
10732 " all) and the encoding (.cs routing, .ph composed\n"
10733 " phase-type law); default is srvn. moment3 fits an APH to each layer's\n"
10734 " response-time CDF and convolves along the entry,\n"
10735 " which is what makes -a cdf possible; mw.*\n"
10736 " reports Majumdar-Woodside box BOUNDS on\n"
10737 " throughput and processor utilization and solves\n"
10738 " no layer at all (every other metric is NaN)\n"
10739 " --ln-transient <m> coupled (default) | decoupled, how -a tran\n"
10740 " couples the layers. decoupled freezes the\n"
10741 " inter-layer demands at the fixed point; coupled\n"
10742 " relaxes time-varying demands through the fluid\n"
10743 " rate schedule until the trajectories settle\n"
10744 " --ln-transient-channels <c> both (default) | thinkt | callservt,\n"
10745 " which inter-layer coupling the relaxation\n"
10746 " injects, for isolating one channel's share\n"
10747 " --sens-method <m> auto (default) | exact | fd, the branch each\n"
10748 " LAYER's sensitivity table takes (-a sens);\n"
10749 " the same three under -s nc -a sens\n"
10750 " --sens-scheme <s> forward (default) | central, the difference\n"
10751 " quotient of the fd branch\n"
10752 " --sens-step <h> relative rate perturbation of the fd branch,\n"
10753 " in (0,1); default 1e-4, or 1e-2 for ssa layers\n"
10754 " --no-interlocking disable the interlocking correction\n"
10755 " --interlock-method <m> ilrate (default) | refpath | none, how\n"
10756 " the interlock is tracked; refpath merges the\n"
10757 " callers that are one reference task's\n"
10758 " customers into one chain (srvn.cs only, it\n"
10759 " falls back to ilrate elsewhere, with a warning)\n"
10760 " --interlock-maxpaths <n> refpath refuses a layer carrying more\n"
10761 " reference routes into it; default 32\n"
10762 " --interlock-refpath-scope <s> merging (default) | all, whether\n"
10763 " refpath also rewrites a lone-caller layer\n"
10764 " --method nlp (alias qdamva, qd-amva) build NO layers: root\n"
10765 " the QD-AMVA laws of the whole model as one\n"
10766 " nonlinear program (-a avg only)\n"
10767 " --lld <STATION=EXPR> a smooth rate multiplier in n for a\n"
10768 " processor or task, read by nlp; repeatable\n"
10769 " --nlp-alpha <a> softmin sharpness for min(n, m); default 20\n"
10770 " --nlp-warm <k> warm sweeps the Newton ladder starts from; 0\n"
10771 " --nlp-maxiter <k> budget of the fallback descent; default 3000\n"
10772 " --nlp-fork <f> refuse (default) | exp, how nlp prices an\n"
10773 " AND-fork: E[max] of exponential branches\n"
10774 " --warmstart <w> nlp | mol, seed a LAYERED method's fixed\n"
10775 " point from the program or the Method of\n"
10776 " Layers; not with srvn.ph/flat.ph or nlp\n"
10777 " --mol-iter-max <k>, --mol-iter-tol <t>, --mol-relax-factor <w>\n"
10778 " lqn_mol's settings for --warmstart mol;\n"
10779 " defaults 200, 1e-6, 0.5\n"
10780 " --repeat <k> re-solve k times, report the best wall clock\n"
10781 " -o layers dump every layer's stations, classes, rates\n"
10782 " and routing instead of the AvgTable\n"
10783 " -a takes avg (getAvgTable), tran (getTranAvg, needs --tspan and fluid\n"
10784 " layers), sens (getSensitivityTable) and cdf (getCdfRespT, moment3).\n"
10785 " --iter_max and --iter_tol set the outer LN loop; --tol does not apply,\n"
10786 " and neither does any Network solver token.\n"
10787 "\n"
10788 "The external LQNS binary (-s lqns) additionally takes:\n"
10789 " --method <name> default | lqns | srvn | exactmva |\n"
10790 " srvn.exactmva | sim | lqsim | lqns.default.\n"
10791 " sim and lqsim run lqsim, the SIMULATOR;\n"
10792 " lqns.default is lqns with no pragma at all,\n"
10793 " which is a different fixed point and not a\n"
10794 " synonym for default\n"
10795 " --multiserver <p> conway|rolia|zhou|suri|reiser|schmidt|default\n"
10796 " (= rolia), the -Pmultiserver= pragma. Not\n"
10797 " passed to lqsim, which has no MVA to configure\n"
10798 " --samples <n> lqsim run length (-A); default 10000\n"
10799 " --timeout <s> kill the child after s seconds; without it the\n"
10800 " wrapper waits\n"
10801 " --keep keep the working directory with model.lqnx and\n"
10802 " model.lqxo instead of removing it\n"
10803 " --verbose echo the command line and the binary's output\n"
10804 " --remote[-url <u>] solve on a host running lqns-rest instead of\n"
10805 " locally; -url implies --remote. LINE ships no\n"
10806 " LQNS binary, so this is the other way to reach\n"
10807 " one\n"
10808 " -a takes avg only: lqns computes no transient, no sensitivity and no\n"
10809 " response-time distribution. QLen is the element utilization, Util its\n"
10810 " processor utilization per server, RespT its phase-1 service time;\n"
10811 " ResidT and ArvR print NaN because lqns reports neither.\n"
10812 "\n"
10813 "The external qnsolver binary (-s lqns on a Network) additionally takes:\n"
10814 " --method <name> default | qns (both = rolia) | qns.conway |\n"
10815 " qns.rolia | qns.zhou | qns.reiser | qns.suri |\n"
10816 " qns.schmidt. The multiserver approximation,\n"
10817 " passed as qnsolver -m and only\n"
10818 " when the model HAS a multiserver station. suri\n"
10819 " and schmidt are lqns approximations: they reach\n"
10820 " the tool only on the non-product-form closed\n"
10821 " branch below, and a multiserver model on the\n"
10822 " qnsolver branch refuses them by name\n"
10823 " --multiserver <p> the same choice under its config spelling;\n"
10824 " --method wins when it names one\n"
10825 " --timeout <s> kill the child after s seconds; without it the\n"
10826 " wrapper waits\n"
10827 " --keep keep the working directory with model.jmva and\n"
10828 " result.jmva instead of removing it\n"
10829 " -a takes avg only. The model is marshalled to the JMVA interchange\n"
10830 " format at CHAIN level and the chain results are de-aggregated back to\n"
10831 " classes, so only Queue, Delay and Source stations are expressible; any\n"
10832 " other station is refused rather than dropped. A closed model that is\n"
10833 " NOT product-form is converted with QN2LQN and delegated to lqns, as the\n"
10834 " reference does, so it also needs the lqns binary; class priorities are\n"
10835 " outside that conversion and are refused. --arith is double only, since\n"
10836 " the results arrive as the text an external binary printed.\n"
10837 "\n"
10838 "The Java Modelling Tools wrapper (-s jmt) additionally takes:\n"
10839 " --jmt-replications <n> independent JSIM runs the transient ensemble\n"
10840 " of -a tran and -a tranprob averages over\n"
10841 " (options.config.replications, default 10),\n"
10842 " replication k seeded seed+k-1. A single path\n"
10843 " is NOT E[N](t); --method jsim is one run\n"
10844 "\n"
10845 "Discrete-event simulation (-s ldes) additionally takes:\n"
10846 " --ldes-tranfilter <f> warmup filter: mser5 (default), fixed, none\n"
10847 " --ldes-warmupfrac <x> fraction the fixed filter discards (0.2)\n"
10848 " --ldes-cimethod <m> CI estimator: obm (default), bm, spectral,\n"
10849 " none\n"
10850 " --ldes-cnvgon stop on relative precision instead of on\n"
10851 " the --samples budget\n"
10852 " --ldes-cnvgtol <x> that precision target (0.05); implies\n"
10853 " --ldes-cnvgon\n"
10854 " --slotted run the analytical solver on a discrete\n"
10855 " (slotted) time scale; SolverNC routes to the\n"
10856 " discrete-time product form and refuses a model\n"
10857 " outside it\n"
10858 " --slotlength <x> the slot in model time units; implies --slotted\n"
10859 " --ldes-slotted run on a discrete (slotted) time scale; a\n"
10860 " sample off the lattice is an error, never\n"
10861 " rounded\n"
10862 " --ldes-slotlength <x> the slot (1.0); implies --ldes-slotted\n"
10863 " --ldes-replications <n> independent runs, averaged. A single path\n"
10864 " is NOT E[N](t): -a tran over an ensemble\n"
10865 " mean needs this\n"
10866 " --ldes-numthreads <n> workers for those replications\n"
10867 " --ldes-maxtime <s> wall-clock budget; the engine stops early\n"
10868 " and reports stopping=max_time\n"
10869 " --ldes-initsol <v,..> warm-start placement, station-major\n"
10870 " [st0_cl0, st0_cl1, ...]. Add --ldes-tranfilter\n"
10871 " fixed --ldes-warmupfrac 0 to reproduce\n"
10872 " initFromSolver, which assumes the placement\n"
10873 " is already a steady state\n"
10874 " --ldes-rest-url <u> solve on an LDES REST server instead of a\n"
10875 " local binary; same wire format, same numbers\n"
10876 " The model.json is forwarded to the engine BYTE FOR BYTE, so a model\n"
10877 " this port cannot itself parse (a cache with retrieval, an SPN, a\n"
10878 " polling server) is simulated exactly as the MATLAB and Python clients\n"
10879 " simulate it. -a avg, tran, cdf (the empirical response-time law),\n"
10880 " sample, reward and prob (the histogram's residence time at the declared\n"
10881 " per-class counts; --node with --state overrides one station) are\n"
10882 " served. --arith is double only.\n"
10883 "\n"
10884 "Uncertainty quantification (-s uq) additionally takes:\n"
10885 " --uq-solver <s> the engine run at each design point: mva, nc,\n"
10886 " mam, ba, ctmc, fluid or ssa. REQUIRED: UQ\n"
10887 " computes nothing itself, and defaulting it\n"
10888 " would attribute the numbers to an engine the\n"
10889 " caller never chose\n"
10890 " A model whose service or arrival process is a Prior is a FAMILY of\n"
10891 " models. UQ discretizes each Prior, solves the tensor product of the\n"
10892 " alternatives, and reports the prior-weighted expectation. Under -s uq\n"
10893 " three flags describe the DESIGN and not the engine: --method is\n"
10894 " quadrature (default, and the alias of discrete) or montecarlo,\n"
10895 " --samples the nodes per continuous Prior (11), --seed the Monte Carlo\n"
10896 " stream. The stage solver therefore keeps its own sample count and its\n"
10897 " own seed; --tol, --iter_tol, --iter_max and --cutoff pass through to\n"
10898 " it, since UQ has no convergence of its own. -a posterior prints every\n"
10899 " design point, its weight and the means it substituted, which is what\n"
10900 " says whether the expectation averaged two nearby models or two very\n"
10901 " different ones. Every other solver REFUSES a model carrying a Prior\n"
10902 " rather than lowering it to its mixture moments.\n"
10903 " -a interval answers the OTHER epistemic question, in which a\n"
10904 " parameter is bounded but not distributed: it drops the weights and\n"
10905 " keeps the endpoints. On a single-class closed model of LI\n"
10906 " single-server queues and delays it is the EXACT hull of MVA over the\n"
10907 " demand box (2*(m+2) MVA calls, no design solved at all); otherwise it\n"
10908 " falls back to the range over the solved design points, which for a\n"
10909 " continuous Prior lies strictly inside the true range. The table says\n"
10910 " which, and the fallback warns on stderr: a range that is not an\n"
10911 " enclosure must not read like one.\n"
10912 "\n"
10913 "Random environments (-s env) read an Environment model.json, whose\n"
10914 " stages each hold a Network and whose transitions carry the stage\n"
10915 " holding times. The stages are solved TRANSIENTLY and coupled: each\n"
10916 " stage starts from the queue lengths the previous one left, and the\n"
10917 " reported means are the per-stage sojourn averages blended by the\n"
10918 " environment probabilities. --method selects the coupling: meanfield\n"
10919 " (default, the reference's, carries the marginal means across a\n"
10920 " switch) or statevec|blend (carries the whole joint distribution).\n"
10921 " meanfield solves each stage with the fluid transient and is double\n"
10922 " only; statevec uniformizes a CTMC and takes the whole --arith ladder.\n"
10923 " --method avg|dec asks instead for a closed-form limit, which carries\n"
10924 " nothing across a switch and iterates nothing: avg solves ONE model\n"
10925 " whose modulated rates are their probEnv-weighted averages (exact as\n"
10926 " the environment gets fast), dec solves each stage in steady state and\n"
10927 " blends by probEnv (exact as it gets slow). A model with an\n"
10928 " environment-declared node breakdown is read from the nodeFailures\n"
10929 " block, in the expanded or the one-stage macro form.\n"
10930 " --tspan bounds the stage horizon and --tran-points its grid; --iter_tol\n"
10931 " and --iter_max drive the fixed point. RespT and ArvR print as nan\n"
10932 " because ENV computes neither -- the reference returns them as NaN too,\n"
10933 " and ResidT carries QLen/Tput.\n"
10934 "\n"
10935 "Additions specific to this port:\n"
10936 " --arith <mode> double (default) | exact | real:<digits>\n"
10937 " --list-api list the API functions ported so far\n"
10938 " --generate <spec> draw a random model and print it, solving\n"
10939 " nothing. <spec> is a JSON object inline, a\n"
10940 " path to one, or `-` for standard input.\n"
10941 " {\"kind\":\"network\"} draws a flat model and\n"
10942 " prints the line-model JSON document; its keys\n"
10943 " are seed, name, queues, delays, openClasses,\n"
10944 " closedClasses, schedStrat, routingStrat,\n"
10945 " distribution, cclassJobLoad,\n"
10946 " varyingServiceRates, multiServerQueues,\n"
10947 " randomCSNodes, multiChainCS and topology\n"
10948 " (rand|cyclic). A `delays` of -1, the default,\n"
10949 " lets the generator split the stations itself.\n"
10950 " {\"kind\":\"layered\"} draws an LQN and prints\n"
10951 " the .lqnx document; its keys are seed, name,\n"
10952 " clients, levels, tasks, processors,\n"
10953 " populationRange, thinkTimeRange,\n"
10954 " taskMultiRange, procMultiRange,\n"
10955 " hostDemandRange, synchCallRange (each a\n"
10956 " [lo,hi] pair), taskInfProbability and\n"
10957 " procInfProbability. Both draw from\n"
10958 " java.util.Random at `seed`, so a seeded spec\n"
10959 " reproduces; --arith is not read, since a draw\n"
10960 " is a double whatever the model is solved at.\n"
10961 " --api <name> invoke one API function directly\n"
10962 " --args <path> JSON arguments for --api; stdin when omitted\n"
10963 "\n"
10964 "Solvers and what they honour. Every model-solving solver reads -a avg,\n"
10965 "-f/-i and --method; nothing else is wired, so tolerances, iteration\n"
10966 "caps, seeds and sample counts keep their SolverOptions defaults rather\n"
10967 "than being invented here. mva and nc additionally read -a prob -- mva\n"
10968 "fits a binomial to its own means, nc returns the exact product-form\n"
10969 "probability, so the two disagree by construction. mva also reads -a\n"
10970 "marg, @SolverMVA's getProbMarg: P(n jobs of class r at station i) for\n"
10971 "every (station, class) pair, narrowed by --node / --class and evaluated\n"
10972 "at --marg-states. A closed class takes the Schmidt binomial fitted to\n"
10973 "Q(i,r); an open one takes the station's exact BCMP marginal (Poisson at\n"
10974 "an infinite server, multinomial-geometric at a queue). Both solvers read\n"
10975 "-a normconst, getProbNormConstAggr: nc reports the constant its solve\n"
10976 "already formed, mva RE-ENTERS its analyzer at method='exact', since only\n"
10977 "the exact recursion carries a G -- a model whose branch forms none, an\n"
10978 "open or mixed one above all, reports nan, as the reference does. nc also\n"
10979 "reads -a cdf,\n"
10980 "@SolverNC's getCdfRespT: the whole response-time law per (station,\n"
10981 "class) on one logarithmic grid, FCFS stations only, with\n"
10982 "--cdf-algorithm exact (pfqn_stdf, the default) or rd (pfqn_stdf_heur).\n"
10983 "nc reads -a sens as well, @NetworkSolver's getSensitivityTable: the\n"
10984 "derivative of each row's means with respect to its own service rate,\n"
10985 "selected with --sens-method / --sens-scheme / --sens-step. NC is one of\n"
10986 "the two engines whose exact branch differentiates the product-form\n"
10987 "recursion analytically, so auto takes it wherever the model is in its\n"
10988 "scope (single-server queues plus delays, not mixed) and falls back to\n"
10989 "finite differences elsewhere. NOT the -s ctmc -a sens analysis, which\n"
10990 "is getSensitivityRanking, a ranking of rate perturbations and not a\n"
10991 "table of derivatives.\n"
10992 "-a node is @NetworkSolver's getAvgNodeTable and is served by mva, nc,\n"
10993 "mam, ba and ctmc, the model solvers whose runner returns the station\n"
10994 "AvgResult it is built from. It is a DIFFERENT INDEX SPACE from -a avg,\n"
10995 "not a relabelling: the AvgTable has one row per STATION, so a\n"
10996 "ClassSwitch, Router, Fork, Join or Sink never appears in it, yet jobs\n"
10997 "flow through all of them. QLen, Util, RespT and ResidT are the station\n"
10998 "numbers scattered to their node indices and zero elsewhere -- a node\n"
10999 "that is not a station holds no jobs -- while ArvR and Tput are\n"
11000 "recomputed for every node by sn_get_node_arvr_from_tput and\n"
11001 "sn_get_node_tput_from_tput. The reference's finite-capacity-region\n"
11002 "pseudo-node rows are NOT emitted: this port's AvgResult carries no\n"
11003 "per-region queue length or utilization to fill them with.\n"
11004 "\n"
11005 "auto picks the engine\n"
11006 "with chooseSolverHeur and prints the name it picked; a branch selecting\n"
11007 "JMT or LDES refuses by name rather than substituting another. ctmc reads\n"
11008 "--cutoff and ports -a avg, prob, gen, states, tranprob, sample, reward,\n"
11009 "cdf, first-passt and sens, which are @SolverCTMC's\n"
11010 "getProbSys/getProbSysAggr and the per-station getProb/getProbAggr,\n"
11011 "getInfGen, getStateSpace, getTranProbSysAggr, sampleSys, getAvgReward,\n"
11012 "getCdfRespT, getCdfFirstPassT (state sets via --passage-from and\n"
11013 "--passage-into, as 1-based space rows '3,5' or state rows '0,2;1,1'),\n"
11014 "first-passt-moments (-a firstpasstmom, the same two sets plus\n"
11015 "--passage-orders: exact moments by one linear solve per order, so a\n"
11016 "variance or a skewness costs no truncated curve; both take --passage-taboo,\n"
11017 "a TABOO set C in the same syntax: entering it first loses the passage, the\n"
11018 "curve becomes defective and the moments conditional on B before C, and\n"
11019 "firstpasst also takes --passage-tset 't1,t2,...,inf' for its grid)\n"
11020 "and getSensitivityRanking. Its --method also takes mdd, which holds the\n"
11021 "reachable set in a decision diagram and solves K coupled level-CTMCs\n"
11022 "instead of the |S|-state generator; it serves -a avg only, having no\n"
11023 "explicit chain to answer the rest from. nc's --method also takes cftp /\n"
11024 "cftp.approx, which draw iid states from the exact stationary law of a\n"
11025 "closed single-class product-form network by coupling from the past;\n"
11026 "they serve -a avg only and their rows carry Monte Carlo error.\n"
11027 "mam ports -a avg,\n"
11028 "prob, cdf, tran and internals, which are @SolverMAM's getProb and\n"
11029 "getProbMarg (the joint (level, phase) law of the queue and its\n"
11030 "per-class marginals, truncated at --cutoff when the model is open),\n"
11031 "getCdfRespT with getPerctRespT beside it, getTranAvg over --tspan (the\n"
11032 "reference forces method ldqbd there, so --method does not select it),\n"
11033 "and getMAMResult, the M/G/1-type internals of a single queue. fluid\n"
11034 "additionally reads -a tran, -a prob, -a cdf, -a var and -a aoi, which\n"
11035 "are @SolverFLD's getTranAvg (the metrics along the trajectory, over\n"
11036 "--tspan or, without one, the horizon the reference's own adaptive loop\n"
11037 "converges at), getProbAggr (a law FITTED to the fluid means, so it does\n"
11038 "not agree with the mva or nc answer by construction), getCdfRespT (the\n"
11039 "response-time law per station and class, read off a marked-fluid\n"
11040 "integration started from the steady state), getMoments/getTranAvgVar\n"
11041 "and getAvgAoI with getCdfAoI beside it -- the last needing method mfq\n"
11042 "and the Source/Queue/Sink topology the age laws are defined for. It\n"
11043 "further reads -a odes, which is\n"
11044 "@SolverFLD/exportODEs, and --notation for the form it writes, and -a\n"
11045 "jacobian, which is @SolverFLD/getJacobian: d f_i / d x_j of the drift,\n"
11046 "differentiated locally over the structure of the system, with the\n"
11047 "equilibria beside it under --equilibria, which needs the\n"
11048 "line-sage-rest backend --symbolic names. Only the smooth methods have\n"
11049 "a Jacobian: min(n,S) has none where the regime switches, so the\n"
11050 "min-scaled drifts are refused by the factor that carries the kink.\n"
11051 "nc, ba and ctmc run\n"
11052 "under every --arith backend, nc's cftp method excepted: its sampler\n"
11053 "works in the log domain, so it refuses 'exact' by name rather than\n"
11054 "answering in a field it does not live in. mdd does run under 'exact'\n"
11055 "(its level solve drops Householder for a rational least squares there),\n"
11056 "but the LEVEL AGGREGATION is still an approximation away from product\n"
11057 "form: exact arithmetic pins the fixed point, not the model. mam is\n"
11058 "double only (its phase-type fitter\n"
11059 "needs transcendental arithmetic) and so is ssa (its sample path is\n"
11060 "generated from exponential clocks) and fluid (LSODA). ba reports a\n"
11061 "BOUND, not an estimate, and ssa a simulation carrying Monte Carlo\n"
11062 "error; neither is comparable with an exact solver except as such.\n"
11063 "\n"
11064 "Arithmetic: 'exact' computes in arbitrary-precision rationals and\n"
11065 "reports numerator and denominator alongside the double value; 'real'\n"
11066 "computes in fixed high-precision binary floating point, at 50, 100 or\n"
11067 "200 digits (a request in between is rounded up to the next tier).\n"
11068 "\n"
11069 "--api arguments are a JSON object keyed by the MATLAB parameter names,\n"
11070 "e.g. {\"L\": [[0.6,0.4]], \"N\": [2,1], \"Z\": [1,0.5]}: a 2-D array is a\n"
11071 "matrix (row-major), a 1-D array a row vector, a bare number a scalar.\n"
11072 "A JSON number is read as its shortest round-tripping decimal, so 0.6 is\n"
11073 "3/5 in exact arithmetic; pass a string such as \"1/3\" for anything else.\n",
11074 kVersion);
11075}
11076
11077/**
11078 * `--install`: the environment check, the C++ twin of MATLAB's `lineInstall`,
11079 * the JAR's `jline.cli.LineInstall` and Python's `line-install`.
11080 *
11081 * NOTHING IT LOOKS FOR IS REQUIRED. The C++ edition is header-only and its
11082 * native solvers stand alone, so every dependency probed here backs one
11083 * optional wrapper or backend. A miss is therefore a warning on stderr that
11084 * names the solvers it disables and how to install it, never an error: the
11085 * point of the command is to tell a fresh checkout which solvers it can
11086 * actually reach, not to refuse to run.
11087 *
11088 * @return true when nothing warned
11089 */
11090bool install_check() {
11091 bool has_warnings = false;
11092 // stdout is block-buffered when the check is piped or redirected while
11093 // stderr is not, so an unflushed progress line would surface AFTER the
11094 // warning it belongs to. Flush before every warning so the transcript reads
11095 // in the order the checks ran.
11096 const auto warn = [&](const std::string& text) {
11097 std::fflush(stdout);
11098 std::fprintf(stderr, "%s\n", text.c_str());
11099 std::fflush(stderr);
11100 has_warnings = true;
11101 };
11102
11103 std::printf("Checking LINE (C++)...\n");
11104 std::printf(" line-cli %s\n", kVersion);
11105
11106 std::printf("Checking Java runtime (JMT wrapper)...\n");
11107 const std::string java = line::jmt::detail::find_java();
11108 if (java.empty()) {
11109 warn("WARNING: no Java runtime was found in LINE_JAVA, JAVA_HOME or on PATH, so the "
11110 "JMT wrapper (-s jmt) cannot run. Install a JRE 8 or later.");
11111 } else {
11112 std::printf(" %s\n", java.c_str());
11113 }
11114
11115 std::printf("Checking JMT...\n");
11116 const std::string jmt_dir = line::jmt::detail::jmt_jar_path();
11117 if (jmt_dir.empty()) {
11118 // REPORTS, DOES NOT ACQUIRE: `jmt_jar_path` never downloads, and this
11119 // command must not either. An environment check that fetched 31 MiB as
11120 // a side effect of being asked would be changing the environment it is
11121 // reporting on, so the download is named here and performed by the
11122 // solver call that needs it.
11123 warn("WARNING: JMT.jar was not found, this is required by the JMT wrapper. Download it "
11124 "from https://line-solver.sourceforge.net/latest/JMT.jar into common/, or point "
11125 "LINE_JMT_DIR at the folder holding it, or set LINE_JMT_DOWNLOAD=1 to let the first "
11126 "JMT solve fetch and verify it.");
11127 } else {
11128 std::printf(" %s/JMT.jar\n", jmt_dir.c_str());
11129 }
11130
11131 std::printf("Checking LQNS...\n");
11133 const std::string banner = line::lqns::lqns_version();
11134 if (banner.empty())
11135 warn("WARNING: lqns is not installed, this is required by the LQNS wrapper (-s lqns) "
11136 "for layered models. Download it at: https://github.com/layeredqueuing/dist");
11137 else
11138 warn("WARNING: the installed lqns is too old for LINE, which needs release 6 or "
11139 "later; it reports '" + banner + "'. Upgrade it from: "
11140 "https://github.com/layeredqueuing/dist");
11141 } else {
11142 std::printf(" %s\n", line::lqns::lqns_version().c_str());
11143 }
11144
11145 std::printf("Checking qnsolver...\n");
11147 warn("WARNING: qnsolver is not installed, this is required by the qns methods of the "
11148 "LQNS wrapper (-s lqns on a Network). "
11149 "It ships with LQNS: https://github.com/layeredqueuing/dist");
11150 else
11151 std::printf(" qnsolver found\n"); // every other check reports its finding, so this one does too
11152
11153 std::printf("Checking symbolic backend (line-sage-rest)...\n");
11155 warn(std::string("WARNING: Docker is not available, so the SageMath symbolic backend "
11156 "cannot start. It is the only computer algebra system this edition "
11157 "reaches and is required by the symbolic methods of "
11158 "SolverCTMC/SolverFluid. Install Docker, then run: "
11159 "docker run -d -p 8080:8080 ") + line::sym::SYM_DOCKER_IMAGE);
11160 else {
11161 const std::string image = line::sym::sym_find_image();
11162 if (image.empty())
11163 warn(std::string("WARNING: the line-sage-rest image is not present locally, this may be "
11164 "required by some LINE methods. Pull it with: "
11165 "docker run -d -p 8080:8080 ") + line::sym::SYM_DOCKER_IMAGE);
11166 else
11167 std::printf(" %s (present locally)\n", image.c_str()); // as the JAR's check reports it
11168 }
11169
11170 if (has_warnings)
11171 std::printf("Completed. LINE has warnings.\n");
11172 else
11173 std::printf("Success. LINE is ready to use.\n");
11174 return !has_warnings;
11175}
11176
11177void list_api() {
11178 const auto& reg = line::api_registry();
11179 std::printf("%-24s %-8s %-24s %s\n", "function", "domain", "arithmetic", "ported from");
11180 for (const auto& e : reg) {
11181 std::string modes;
11182 for (std::size_t k = 0; k < e.arith.size(); ++k) {
11183 if (k) modes += ",";
11184 modes += line::arith_name(e.arith[k]);
11185 }
11186 std::printf("%-24s %-8s %-24s %s\n", e.name.c_str(), e.domain.c_str(), modes.c_str(),
11187 e.reference.c_str());
11188 }
11189 std::printf("\n%zu of ~480 API functions ported.\n", reg.size());
11190}
11191
11192/**
11193 * Read the --api argument object: from the file named by --args, or from stdin
11194 * when --args is absent. A parse failure names the source and the position, so
11195 * a malformed file is a legible error rather than an empty argument set.
11196 */
11197line::reg::Json read_api_args(const std::string& path) {
11198 std::string text;
11199 std::string source;
11200 if (path.empty()) {
11201 source = "standard input";
11202 text.assign(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
11203 } else {
11204 source = "'" + path + "'";
11205 std::ifstream in(path.c_str());
11206 if (!in) throw line::InputError("cannot open the --args file " + source);
11207 text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
11208 }
11209 // No --args and nothing on stdin means the caller forgot the arguments.
11210 if (text.find_first_not_of(" \t\r\n") == std::string::npos)
11211 throw line::InputError("no --api arguments given: pass --args <path> or a JSON object on "
11212 "standard input (read from " +
11213 source + ")");
11214 try {
11215 return line::reg::Json::parse(text);
11216 } catch (const line::reg::Json::parse_error& e) {
11217 throw line::InputError("malformed --api arguments in " + source + ": " + e.what());
11218 }
11219}
11220
11221/**
11222 * Read the --generate specification: inline when it starts with `{`, from
11223 * standard input when it is `-` or absent, and from a file otherwise.
11224 *
11225 * INLINE IS THE POINT. A host that already holds the spec as a string (the R
11226 * binding does, and so does an MCP tool) would otherwise have to write a
11227 * temporary file for a dozen numbers, and a `line_capi_run` caller has no
11228 * standard input to pipe it through either.
11229 */
11230line::reg::Json read_generate_spec(const std::string& arg) {
11231 std::string text;
11232 std::string source;
11233 const std::string::size_type first = arg.find_first_not_of(" \t\r\n");
11234 if (first != std::string::npos && arg[first] == '{') {
11235 source = "the --generate argument";
11236 text = arg;
11237 } else if (arg.empty() || arg == "-") {
11238 source = "standard input";
11239 text.assign(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
11240 } else {
11241 source = "'" + arg + "'";
11242 std::ifstream in(arg.c_str());
11243 if (!in) throw line::InputError("cannot open the --generate spec file " + source);
11244 text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
11245 }
11246 if (text.find_first_not_of(" \t\r\n") == std::string::npos)
11247 throw line::InputError("no --generate specification given: pass a JSON object inline, a "
11248 "path to one, or `-` for standard input (read from " +
11249 source + ")");
11250 try {
11251 return line::reg::Json::parse(text);
11252 } catch (const line::reg::Json::parse_error& e) {
11253 throw line::InputError("malformed --generate specification in " + source + ": " + e.what());
11254 }
11255}
11256
11257/** Read one integer spec key, refusing a value that is not a whole number. */
11258long gen_int(const line::reg::Json& j, const char* key, long dflt) {
11259 if (!j.contains(key) || j.at(key).is_null()) return dflt;
11260 const line::reg::Json& v = j.at(key);
11261 if (!v.is_number())
11262 throw line::InputError(std::string("--generate: '") + key + "' must be a number");
11263 const double d = v.get<double>();
11264 if (d != std::floor(d))
11265 throw line::InputError(std::string("--generate: '") + key + "' must be a whole number");
11266 return static_cast<long>(d);
11267}
11268
11269/** Read one real spec key. */
11270double gen_real(const line::reg::Json& j, const char* key, double dflt) {
11271 if (!j.contains(key) || j.at(key).is_null()) return dflt;
11272 const line::reg::Json& v = j.at(key);
11273 if (!v.is_number())
11274 throw line::InputError(std::string("--generate: '") + key + "' must be a number");
11275 return v.get<double>();
11276}
11277
11278/** Read one boolean spec key. */
11279bool gen_bool(const line::reg::Json& j, const char* key, bool dflt) {
11280 if (!j.contains(key) || j.at(key).is_null()) return dflt;
11281 const line::reg::Json& v = j.at(key);
11282 if (!v.is_boolean())
11283 throw line::InputError(std::string("--generate: '") + key + "' must be true or false");
11284 return v.get<bool>();
11285}
11286
11287/** Read one string spec key. */
11288std::string gen_str(const line::reg::Json& j, const char* key, const std::string& dflt) {
11289 if (!j.contains(key) || j.at(key).is_null()) return dflt;
11290 const line::reg::Json& v = j.at(key);
11291 if (!v.is_string())
11292 throw line::InputError(std::string("--generate: '") + key + "' must be a string");
11293 return v.get<std::string>();
11294}
11295
11296/** Read a two-element `[lo, hi]` range key. */
11297line::gen::Range gen_range(const line::reg::Json& j, const char* key, const line::gen::Range& dflt) {
11298 if (!j.contains(key) || j.at(key).is_null()) return dflt;
11299 const line::reg::Json& v = j.at(key);
11300 if (!v.is_array() || v.size() != 2 || !v[0].is_number() || !v[1].is_number())
11301 throw line::InputError(std::string("--generate: '") + key +
11302 "' must be a two-element array of numbers");
11303 return line::gen::Range(v[0].get<double>(), v[1].get<double>());
11304}
11305
11306/**
11307 * Refuse a spec key that is not one this kind reads.
11308 *
11309 * A TYPO IS A SILENT WRONG ANSWER OTHERWISE: `"openClases": 2` would generate a
11310 * closed model and the caller would have no way to tell it was not asked for.
11311 */
11312void gen_check_keys(const line::reg::Json& j, const char* const* allowed, std::size_t n) {
11313 for (line::reg::Json::const_iterator it = j.begin(); it != j.end(); ++it) {
11314 bool ok = false;
11315 for (std::size_t k = 0; k < n && !ok; ++k) ok = it.key() == allowed[k];
11316 if (!ok) {
11317 std::string names;
11318 for (std::size_t k = 0; k < n; ++k) {
11319 if (k) names += ", ";
11320 names += allowed[k];
11321 }
11322 throw line::InputError("--generate: unknown key '" + it.key() +
11323 "'; this kind reads: " + names);
11324 }
11325 }
11326}
11327
11328/**
11329 * The JAR CLI's `-a` spelling, mapped onto this port's token.
11330 *
11331 * `jline.cli.LineCLI` names its analyses with hyphenated, getter-shaped tokens
11332 * (`cdf-respt`, `prob-sys-aggr`, `tran-avg`) and this port names them by the
11333 * question (`cdf`, `prob`, `tran`), because one arm here answers what the JAR
11334 * splits across several: `-a prob` reports `getProbSys`, `getProbSysAggr` and
11335 * the per-station `getProb`/`getProbAggr` in ONE table, and `-a sample` walks
11336 * `sampleSys`, `sampleSysAggr` and the per-node pair in one trajectory. Both
11337 * spellings are therefore accepted and the JAR's collapse onto the arm that
11338 * already contains the answer -- a script written against the JAR CLI keeps
11339 * working, and nothing here is renamed to make that true.
11340 *
11341 * AN UNKNOWN METHOD NAME IS RETURNED UNCHANGED, not rejected here: the per-solver
11342 * whitelists downstream refuse by name and say which analyses that solver
11343 * serves, which is a better message than a table of every token in the CLI.
11344 */
11345std::string normalize_analysis(const std::string& a) {
11346 // Same question, different spelling.
11347 if (a == "cdf-respt" || a == "cdfrespt") return "cdf";
11348 if (a == "cdf-passt" || a == "cdfpasst") return "cdfpasst";
11349 if (a == "first-passt" || a == "cdf-firstpasst" || a == "cdffirstpasst") return "firstpasst";
11350 if (a == "first-passt-moments" || a == "firstpasst-moments" || a == "firstpasstmoments")
11351 return "firstpasstmom";
11352 if (a == "perct-respt" || a == "perctrespt") return "perct";
11353 if (a == "tran-avg" || a == "tranavg") return "tran";
11354 if (a == "tran-cdf-respt" || a == "trancdfrespt") return "trancdf";
11355 if (a == "tran-cdf-passt" || a == "trancdfpasst") return "trancdfpasst";
11356 if (a == "tran-prob" || a == "tranprob-sys-aggr") return "tranprob";
11357 if (a == "generator") return "gen";
11358 if (a == "state-space" || a == "statespace") return "states";
11359 if (a == "reward-steady" || a == "rewardsteady") return "reward";
11360 if (a == "reward-value" || a == "rewardvalue") return "rewardvalue";
11361 if (a == "node-chain" || a == "node-chain-table") return "nodechain";
11362 if (a == "fj-table" || a == "avg-fj" || a == "fjavgt") return "fj";
11363 // The JAR's four probability getters and its four samplers, each answered
11364 // whole by one arm here. Collapsing them is not a loss: the arm emits every
11365 // one of the four, so a caller asking for the aggregate receives it beside
11366 // the joint rather than instead of it.
11367 if (a == "prob-aggr" || a == "prob-sys" || a == "prob-sys-aggr") return "prob";
11368 if (a == "prob-marg" || a == "probmarg") return "marg";
11369 if (a == "prob-sys-marg" || a == "probsysmarg" || a == "sys-marg") return "sysmarg";
11370 if (a == "sample-aggr" || a == "sample-sys" || a == "sample-sys-aggr") return "sample";
11371 // `getStageTable` IS `getAvgTable` on a Network model, in the reference and
11372 // in the JAR both: a network has one implicit stage, and only an
11373 // Environment has several. Mapped here rather than given an arm of its own,
11374 // because an arm would be a second name for one table and free to drift
11375 // from it.
11376 if (a == "stage") return "avg";
11377 return a;
11378}
11379
11380/** `-a` split on commas, each token normalized; never empty. */
11381std::vector<std::string> analysis_list(const std::string& spec) {
11382 std::vector<std::string> out;
11383 std::size_t at = 0;
11384 while (at <= spec.size()) {
11385 const std::size_t comma = spec.find(',', at);
11386 std::string tok =
11387 spec.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11388 // A stray space around a comma is a typo, not a different analysis.
11389 while (!tok.empty() && std::isspace(static_cast<unsigned char>(tok.front())))
11390 tok.erase(tok.begin());
11391 while (!tok.empty() && std::isspace(static_cast<unsigned char>(tok.back())))
11392 tok.pop_back();
11393 if (tok.empty())
11394 throw line::InputError("-a takes a comma-separated list of analyses and one entry of '" +
11395 spec + "' is empty");
11396 // `all` is the JAR's composite of the station table and the system one,
11397 // expanded HERE rather than inside a solver arm so every downstream
11398 // whitelist sees the two analyses it already knows.
11399 if (normalize_analysis(tok) == "all") {
11400 out.push_back("avg");
11401 out.push_back("sys");
11402 } else {
11403 out.push_back(normalize_analysis(tok));
11404 }
11405 if (comma == std::string::npos) break;
11406 at = comma + 1;
11407 }
11408 if (out.empty()) throw line::InputError("-a takes at least one analysis");
11409 return out;
11410}
11411
11412struct Options {
11413 std::string file, input = "json", output = "readable", solver = "auto", analysis = "avg";
11414 std::string arith = "double", api, args;
11415 bool help = false, help_all = false, version = false, list = false;
11416 /**
11417 * `--find-solver [metric]`: report which solvers and methods can analyze the
11418 * model named by -f, and exit without solving it.
11419 *
11420 * `find_solver_all` is `--find-solver-all`, which keeps the refused pairs
11421 * and the reason each was refused. The report is arithmetic-independent --
11422 * it asks feature sets and shapes, not numbers -- so it always reads the
11423 * model at double and ignores --arith.
11424 */
11425 bool find_solver = false, find_solver_all = false;
11426 std::string find_solver_metric;
11427 /** `--install`: run the environment check and exit, solving nothing. */
11428 bool install = false;
11429 /**
11430 * `--generate <spec>`: draw a random model from a JSON specification and
11431 * print it, solving nothing.
11432 *
11433 * The spec is taken INLINE when it starts with `{`, from standard input
11434 * when it is `-` or empty, and from a file otherwise, so a host that has
11435 * the spec in a string never has to stage a file for it. `generate_given`
11436 * rather than a non-empty test, because `--generate ""` means "read stdin"
11437 * and is not the same as not passing the flag.
11438 */
11439 std::string generate;
11440 bool generate_given = false;
11441 /**
11442 * Whether -i was actually passed.
11443 *
11444 * Without it a `.lqnx` path could not be recognised: `input` defaults to
11445 * json, and a defaulted json is indistinguishable from an explicit one, so
11446 * the extension sniff below would either never fire or would override a
11447 * caller who said `-i json` deliberately.
11448 */
11449 bool input_given = false;
11450 /**
11451 * `-p/--port` and `-m/--maxreq`: server mode.
11452 *
11453 * `port == 0` is "not given" and not "port 0": binding port 0 asks the
11454 * kernel for an ephemeral one, which a caller who typed no port did not
11455 * ask for. `maxreq == 0` is unbounded, matching the JAR's documented
11456 * "quit after this many requests" with no cap by default.
11457 */
11458 int port = 0;
11459 int maxreq = 0;
11460 Knobs knobs;
11461};
11462
11463/** Whether `file` ends in one of JMT's three simulation-document extensions. */
11464bool has_jsim_extension(const std::string& file) {
11465 const std::string::size_type dot = file.find_last_of('.');
11466 if (dot == std::string::npos) return false;
11467 std::string ext = file.substr(dot + 1);
11468 for (std::size_t i = 0; i < ext.size(); ++i)
11469 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
11470 return ext == "jsim" || ext == "jsimg" || ext == "jsimw";
11471}
11472
11473/** Whether `file` ends in the PNML extension. */
11474bool has_pnml_extension(const std::string& file) {
11475 const std::string::size_type dot = file.find_last_of('.');
11476 if (dot == std::string::npos) return false;
11477 std::string ext = file.substr(dot + 1);
11478 for (std::size_t i = 0; i < ext.size(); ++i)
11479 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
11480 return ext == "pnml";
11481}
11482
11483/** Whether `file` ends in one of the layered model's XML extensions (`.lqn` also holds SRVN text, see is_srvn_path). */
11484bool has_lqn_extension(const std::string& file) {
11485 const std::string::size_type dot = file.find_last_of('.');
11486 if (dot == std::string::npos) return false;
11487 std::string ext = file.substr(dot + 1);
11488 for (std::size_t i = 0; i < ext.size(); ++i)
11489 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
11490 return ext == "lqnx" || ext == "xml" || ext == "lqn";
11491}
11492
11493Options parse_args(int argc, char** argv) {
11494 Options o;
11495 for (int i = 1; i < argc; ++i) {
11496 std::string a = argv[i];
11497 auto next = [&](const char* what) -> std::string {
11498 if (i + 1 >= argc) throw line::InputError(std::string("missing value after ") + what);
11499 const char* v = argv[++i];
11500 // a flag carrying a config key with listed values takes only those (doc/line-config.schema.json)
11502 if (k && !line::solvers::config_value_ok(*k, v))
11503 throw line::InputError(line::ErrorKind{"ConfigInvalid"}, std::string(what) + " (options.config." + k->name + ") expects one of " +
11504 k->values + "; got '" + v + "'");
11505 return v;
11506 };
11507 if (a == "-h" || a == "--help") o.help = true;
11508 else if (a == "--help-all" || a == "--help-full") o.help_all = true;
11509 else if (a == "-V" || a == "--version") o.version = true;
11510 else if (a == "--install") o.install = true;
11511 else if (a == "--list-api") o.list = true;
11512 else if (a == "--generate") {
11513 // The value is OPTIONAL: a bare --generate reads the spec from
11514 // standard input, as --api does with its --args.
11515 o.generate_given = true;
11516 // A lone `-` IS the value (it names standard input); anything
11517 // else beginning with `-` is the next flag.
11518 if (i + 1 < argc && (argv[i + 1][0] != '-' || argv[i + 1][1] == '\0'))
11519 o.generate = argv[++i];
11520 }
11521 else if (a == "--find-solver" || a == "--find-method" || a == "--help-model") {
11522 o.find_solver = true;
11523 // The metric is OPTIONAL, so it is taken only when the next token is
11524 // not itself a flag: `--find-solver -f m.json` must not swallow -f.
11525 if (i + 1 < argc && argv[i + 1][0] != '-') o.find_solver_metric = argv[++i];
11526 } else if (a == "--find-solver-all" || a == "--find-method-all") {
11527 o.find_solver = true;
11528 o.find_solver_all = true;
11529 if (i + 1 < argc && argv[i + 1][0] != '-') o.find_solver_metric = argv[++i];
11530 }
11531 else if (a == "-f" || a == "--file") o.file = next("-f");
11532 else if (a == "-i" || a == "--input") { o.input = next("-i"); o.input_given = true; }
11533 else if (a == "-o" || a == "--output") o.output = next("-o");
11534 else if (a == "-s" || a == "--solver") o.solver = next("-s");
11535 else if (a == "-a" || a == "--analysis") o.analysis = next("-a");
11536 else if (a == "--arith") o.arith = next("--arith");
11537 else if (a == "-p" || a == "--port") {
11538 const std::string v = next("-p");
11539 const long n = std::atol(v.c_str());
11540 if (n < 1 || n > 65535)
11541 throw line::InputError("-p takes a TCP port in 1..65535 (got '" + v + "')");
11542 o.port = static_cast<int>(n);
11543 } else if (a == "-m" || a == "--maxreq") {
11544 const std::string v = next("-m");
11545 const long n = std::atol(v.c_str());
11546 if (n < 1)
11547 throw line::InputError(
11548 "-m is the number of requests the server serves before quitting and must be "
11549 "positive; omit it to serve indefinitely (got '" + v + "')");
11550 o.maxreq = static_cast<int>(n);
11551 }
11552 else if (a == "--api") o.api = next("--api");
11553 else if (a == "--args") o.args = next("--args");
11554 else if (a == "--method") o.knobs.method = next("--method");
11555 else if (a == "--qrf-params") o.knobs.qrf_params = next("--qrf-params");
11556 else if (a == "--qrf-alpha") o.knobs.qrf_alpha = next("--qrf-alpha");
11557 else if (a == "--level") {
11558 const std::string v = next("--level");
11559 const int lv = std::atoi(v.c_str());
11560 if (lv < 1) throw line::InputError("--level must be a positive integer (got '" + v + "')");
11561 o.knobs.level = lv;
11562 }
11563 else if (a == "--samples") {
11564 const std::string v = next("--samples");
11565 const double d = std::atof(v.c_str()); // accepts 1e6 as well as 1000000
11566 if (!(d >= 1.0))
11567 throw line::InputError("--samples must be a positive count (got '" + v + "')");
11568 o.knobs.samples = static_cast<std::size_t>(d);
11569 } else if (a == "-d" || a == "--seed") {
11570 const std::string v = next("--seed");
11571 o.knobs.seed = std::strtoul(v.c_str(), nullptr, 10);
11572 if (o.knobs.seed == 0)
11573 throw line::InputError("--seed must be a positive integer (got '" + v + "')");
11574 } else if (a == "--warmupfrac") {
11575 const std::string v = next("--warmupfrac");
11576 const double f = std::atof(v.c_str());
11577 if (!(f >= 0.0 && f < 1.0))
11578 throw line::InputError(
11579 "--warmupfrac is the fraction of the path discarded before the means are "
11580 "taken and must lie in [0,1) (got '" + v + "')");
11581 o.knobs.warmupfrac = f;
11582 } else if (a == "--pstar") {
11583 const std::string v = next("--pstar");
11584 const double ps = std::atof(v.c_str());
11585 if (!(ps > 0.0))
11586 throw line::InputError(
11587 "--pstar is the exponent of the fluid p-norm smoothing and must be positive "
11588 "(got '" + v + "')");
11589 o.knobs.pstar = ps;
11590 } else if (a == "--fluid-boundary") {
11591 const std::string v = next("--fluid-boundary");
11592 if (v != "reflect" && v != "diffusion")
11593 throw line::InputError(line::ErrorKind{"ConfigInvalid"},
11594 "--fluid-boundary (options.config.fluid_boundary) is 'reflect' (a fluid level held at a "
11595 "bound is the bound) or 'diffusion' (the mean of a reflected Brownian motion); got '" + v +
11596 "'");
11597 o.knobs.fluid_boundary = v;
11598 } else if (a == "--nonmkv") {
11599 const std::string v = next("--nonmkv");
11600 line::api::nonmkv_enabled(v); // refuses a name other than bernstein or none
11601 o.knobs.nonmkv = v;
11602 } else if (a == "--busyperiod" || a == "--busyperiod-subnet") {
11603 // Same all-or-nothing parse as --marg-states: an entry dropped from
11604 // the list is a DIFFERENT report, not a shorter one.
11605 const bool orders = (a == "--busyperiod");
11606 const std::string v = next(orders ? "--busyperiod" : "--busyperiod-subnet");
11607 std::vector<std::size_t>& into = orders ? o.knobs.busy_orders : o.knobs.busy_subnet;
11608 std::size_t at = 0;
11609 while (at <= v.size()) {
11610 const std::size_t comma = v.find(',', at);
11611 const std::string tok =
11612 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11613 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos ||
11614 std::atol(tok.c_str()) < 1)
11615 throw line::InputError(
11616 std::string(orders ? "--busyperiod takes a comma-separated list of "
11617 "positive orders"
11618 : "--busyperiod-subnet takes a comma-separated list of "
11619 "1-based station indexes") +
11620 " (got '" + v + "')");
11621 into.push_back(static_cast<std::size_t>(std::atol(tok.c_str())));
11622 if (comma == std::string::npos) break;
11623 at = comma + 1;
11624 }
11625 } else if (a == "--tol") o.knobs.tol = std::atof(next("--tol").c_str());
11626 else if (a == "--iter_tol") o.knobs.iter_tol = std::atof(next("--iter_tol").c_str());
11627 else if (a == "--iter_max") o.knobs.iter_max = std::atoi(next("--iter_max").c_str());
11628 else if (a == "--max-states") {
11629 const std::string v = next("--max-states");
11630 const long long n = std::atoll(v.c_str());
11631 if (n <= 0)
11632 throw line::InputError(
11633 "--max-states truncates an open agent's queue-length dimension and takes a "
11634 "positive state count (got '" + v + "')");
11635 o.knobs.max_states = n;
11636 }
11637 else if (a == "--multiserver") o.knobs.multiserver = next("--multiserver");
11638 else if (a == "--fork-join" || a == "--fork_join")
11639 o.knobs.fork_join = next("--fork-join");
11640 else if (a == "--tran-points" || a == "--tran_points") {
11641 const std::string v = next("--tran-points");
11642 const long n = std::atol(v.c_str());
11643 if (n < 2)
11644 throw line::InputError(
11645 "--tran-points is the number of points on the transient grid and needs at "
11646 "least two, a start and an end (got '" + v + "')");
11647 o.knobs.tran_points = static_cast<std::size_t>(n);
11648 }
11649 else if (a == "--mdd-tol" || a == "--mdd_tol") {
11650 const std::string v = next("--mdd-tol");
11651 const double d = std::atof(v.c_str());
11652 if (!(d > 0.0))
11653 throw line::InputError("--mdd-tol must be a positive tolerance (got '" + v + "')");
11654 o.knobs.mdd_tol = d;
11655 } else if (a == "--mdd-maxiter" || a == "--mdd_maxiter") {
11656 const std::string v = next("--mdd-maxiter");
11657 const long n = std::atol(v.c_str());
11658 if (n < 1)
11659 throw line::InputError(
11660 "--mdd-maxiter must be a positive sweep count (got '" + v + "')");
11661 o.knobs.mdd_maxiter = static_cast<int>(n);
11662 }
11663 else if (a == "--fj-accuracy") {
11664 const std::string v = next("--fj-accuracy");
11665 const long n = std::atol(v.c_str());
11666 if (n < 1)
11667 throw line::InputError(
11668 "--fj-accuracy is the FJ_codes truncation C of the queue-length difference "
11669 "between the two fork-join branches and must be at least 1 (got '" + v + "')");
11670 o.knobs.fj_accuracy = static_cast<int>(n);
11671 } else if (a == "--fj-tmode") {
11672 const std::string v = next("--fj-tmode");
11673 if (v != "NARE" && v != "Sylvester")
11674 throw line::InputError(
11675 "--fj-tmode selects how computeT.m solves for the T matrix and is 'NARE' (the "
11676 "Riccati route, the default) or 'Sylvester' (the fixed-point iteration); got '" +
11677 v + "'");
11678 o.knobs.fj_tmode = v;
11679 } else if (a == "--sync-phases") {
11680 const std::string v = next("--sync-phases");
11681 char* end = nullptr;
11682 const long n = std::strtol(v.c_str(), &end, 10);
11683 if (v.empty() || end == nullptr || *end != '\0' || n < 0)
11684 throw line::InputError(
11685 "--sync-phases is the number of phases of the synchronous phase expansion and "
11686 "must be a non-negative integer (got '" + v + "')");
11687 o.knobs.sync_phases = static_cast<int>(n);
11688 } else if (a == "--mapmsg-quantization") {
11689 const std::string v = next("--mapmsg-quantization");
11690 char* end = nullptr;
11691 const long n = std::strtol(v.c_str(), &end, 10);
11692 if (v.empty() || end == nullptr || *end != '\0' || n < 2)
11693 throw line::InputError(
11694 "--mapmsg-quantization is the number of patience regimes of the MAP/M/s+G "
11695 "reneging analyzer and must be an integer of at least 2 (got '" + v + "')");
11696 o.knobs.mapmsg_quantization = static_cast<int>(n);
11697 } else if (a == "--timescale") {
11698 const std::string v = next("--timescale");
11699 if (v != "auto" && v != "discrete" && v != "continuous")
11700 throw line::InputError(
11701 "--timescale decides whether the model is read on a slot lattice and is "
11702 "'auto' (the default), 'discrete' or 'continuous'; got '" + v + "'");
11703 o.knobs.timescale = v;
11704 }
11705 else if (a == "--force") {
11706 o.knobs.force = true;
11707 }
11708 else if (a == "--cutoff") {
11709 const std::string v = next("--cutoff");
11710 if (v.find(',') != std::string::npos || v.find(';') != std::string::npos) {
11711 o.knobs.cutoff_mat = parse_cutoff_matrix(v);
11712 if (o.knobs.cutoff_mat.empty())
11713 throw line::InputError(
11714 "--cutoff takes a number or a per-(station,class) matrix written "
11715 "'r1c1,r1c2;r2c1,r2c2' (got '" + v + "')");
11716 } else {
11717 const double d = std::atof(v.c_str());
11718 if (!(d >= 1.0))
11719 throw line::InputError(
11720 "--cutoff must be a positive job count per open class (got '" + v + "')");
11721 o.knobs.cutoff = d;
11722 }
11723 } else if (a == "--tspan" || a == "--timespan") {
11724 const std::string v = next("--tspan");
11725 // BOTH SEPARATORS. This port has always written the horizon
11726 // `t0:t1`, and `jline.cli.LineCLI --timespan` has always written it
11727 // `t0,t1`; accepting only one made a command line that names a
11728 // horizon unportable between the two CLIs even after the flag names
11729 // were reconciled.
11730 std::string::size_type sep = v.find(':');
11731 if (sep == std::string::npos) sep = v.find(',');
11732 // A bare value is the END of the horizon and starts at 0, which is
11733 // what a transient from the initial state means; `t0:t1` states both.
11734 const double lo = sep == std::string::npos ? 0.0 : std::atof(v.substr(0, sep).c_str());
11735 const double hi = std::atof(
11736 (sep == std::string::npos ? v : v.substr(sep + 1)).c_str());
11737 // An INFINITE horizon is refused here rather than integrated to:
11738 // pi(t) on [0, Inf) is the stationary vector, which -a avg reports.
11739 if (!(hi > lo) || !(lo >= 0.0) || !std::isfinite(hi))
11740 throw line::InputError(
11741 "--tspan must be a finite horizon 0 <= t0 < t1, given as <t1>, <t0>:<t1> or "
11742 "<t0>,<t1> (got '" + v + "')");
11743 o.knobs.t0 = lo;
11744 o.knobs.t1 = hi;
11745 } else if (a == "-n" || a == "--node") {
11746 const std::string v = next("--node");
11747 const long n = std::atol(v.c_str());
11748 if (n < 1)
11749 throw line::InputError("--node must be a positive 1-based node index (got '" + v +
11750 "')");
11751 o.knobs.node = static_cast<std::size_t>(n);
11752 } else if (a == "-c" || a == "--class") {
11753 const std::string v = next("--class");
11754 const long c = std::atol(v.c_str());
11755 if (c < 1)
11756 throw line::InputError("--class must be a positive 1-based class index (got '" + v +
11757 "')");
11758 o.knobs.jobclass = static_cast<std::size_t>(c);
11759 } else if (a == "--marg-states" || a == "--marg_states") {
11760 // Every entry must parse: a dropped one shortens the curve silently.
11761 const std::string v = next("--marg-states");
11762 std::size_t at = 0;
11763 while (at <= v.size()) {
11764 const std::size_t comma = v.find(',', at);
11765 const std::string tok =
11766 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11767 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos)
11768 throw line::InputError(
11769 "--marg-states takes a comma-separated list of non-negative job counts "
11770 "(got '" + v + "')");
11771 o.knobs.marg_states.push_back(std::atol(tok.c_str()));
11772 if (comma == std::string::npos) break;
11773 at = comma + 1;
11774 }
11775 } else if (a == "--state") {
11776 // Same all-or-nothing discipline as --marg-states: a state vector
11777 // with one entry dropped is a DIFFERENT state, not a shorter one,
11778 // and the length is checked against the node's own space later.
11779 const std::string v = next("--state");
11780 std::size_t at = 0;
11781 while (at <= v.size()) {
11782 const std::size_t comma = v.find(',', at);
11783 const std::string tok =
11784 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11785 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos)
11786 throw line::InputError(
11787 "--state is the state vector -a prob asks about and takes a "
11788 "comma-separated list of non-negative counts (got '" + v + "')");
11789 o.knobs.state.push_back(std::atol(tok.c_str()));
11790 if (comma == std::string::npos) break;
11791 at = comma + 1;
11792 }
11793 } else if (a == "--events") {
11794 const std::string v = next("--events");
11795 const double d = std::atof(v.c_str()); // accepts 5e3 as well as 5000
11796 if (!(d >= 1.0))
11797 throw line::InputError(
11798 "--events is the length of a sampled trajectory and must be a positive event "
11799 "count (got '" + v + "')");
11800 o.knobs.events = static_cast<std::size_t>(d);
11801 } else if (a == "--timestep") {
11802 const std::string v = next("--timestep");
11803 const double d = std::atof(v.c_str());
11804 if (!(d > 0.0) || !std::isfinite(d))
11805 throw line::InputError(
11806 "--timestep is the fixed output step of a transient analysis and must be a "
11807 "positive finite time (got '" + v + "')");
11808 o.knobs.timestep = d;
11809 } else if (a == "--percentiles") {
11810 const std::string v = next("--percentiles");
11811 std::size_t at = 0;
11812 while (at <= v.size()) {
11813 const std::size_t comma = v.find(',', at);
11814 const std::string tok =
11815 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11816 if (tok.empty())
11817 throw line::InputError(
11818 "--percentiles takes a comma-separated list of levels (got '" + v + "')");
11819 double p = std::atof(tok.c_str());
11820 // A LEVEL ABOVE 1 IS A PERCENT, below it a probability. The JAR
11821 // documents `--percentiles 50,90,95,99` and MATLAB stores
11822 // `pers_stored` as fractions, so both spellings reach this port
11823 // and the magnitude is what tells them apart. 1 itself is read
11824 // as the fraction: P(T <= t) = 1 is a level, 1% is not one
11825 // anybody asks for beside 50, 90 and 99.
11826 if (p > 1.0) p /= 100.0;
11827 if (!(p > 0.0) || !(p < 1.0))
11828 throw line::InputError(
11829 "--percentiles levels lie strictly inside (0,1) as fractions or (0,100) "
11830 "as percents; the 100th percentile of an unbounded law is not finite "
11831 "(got '" + tok + "')");
11832 o.knobs.percentiles.push_back(p);
11833 if (comma == std::string::npos) break;
11834 at = comma + 1;
11835 }
11836 } else if (a == "--reward-name" || a == "--reward_name") {
11837 o.knobs.reward_name = next("--reward-name");
11838 } else if (a == "--notation") {
11839 const std::string v = next("--notation");
11840 // The exporter validates the name; it is not defaulted here, so an
11841 // unrecognised notation is refused rather than answered with scalar.
11842 o.knobs.notation = v;
11843 } else if (a == "--symbolic") {
11844 // `sym_resolve` validates the name: auto, none, a URL or an image.
11845 // It is not defaulted here, so `--symbolic none` stays local rather
11846 // than being read as "not given" and searching anyway.
11847 o.knobs.symbolic = next("--symbolic");
11848 } else if (a == "--equilibria") o.knobs.equilibria = true;
11849 else if (a == "--perm-engine") {
11850 // The analyzer validates the name; an unrecognised engine is
11851 // refused rather than answered with exact.
11852 o.knobs.method_perm = next("--perm-engine");
11853 } else if (a == "--transient-method") {
11854 // The analyzer validates the name, so an unrecognised one is
11855 // refused rather than answered with the default.
11856 o.knobs.transient_method = next("--transient-method");
11857 } else if (a == "--rate-sched") {
11858 o.knobs.rate_sched = next("--rate-sched");
11859 } else if (a == "--ctmc-tv-ngrid") {
11860 const std::string v = next("--ctmc-tv-ngrid");
11861 const long n = std::atol(v.c_str());
11862 if (n < 2)
11863 throw line::InputError("--ctmc-tv-ngrid is the grid size of the rate_sched "
11864 "propagator and must be an integer >= 2 (got '" + v + "')");
11865 o.knobs.ctmc_tv_ngrid = static_cast<std::size_t>(n);
11866 } else if (a == "--fau-epsilon") {
11867 const std::string v = next("--fau-epsilon");
11868 const double d = std::atof(v.c_str());
11869 if (!(d > 0.0) || !std::isfinite(d))
11870 throw line::InputError(
11871 "--fau-epsilon is the probability mass the transient grid may discard and "
11872 "must be a positive finite number (got '" + v + "')");
11873 o.knobs.fau_epsilon = d;
11874 } else if (a == "--fau-delta") {
11875 const std::string v = next("--fau-delta");
11876 const double d = std::atof(v.c_str());
11877 if (!(d >= 0.0) || !std::isfinite(d))
11878 throw line::InputError(
11879 "--fau-delta is the occupancy below which a state is dropped and must be a "
11880 "nonnegative finite number (got '" + v + "')");
11881 o.knobs.fau_delta = d;
11882 } else if (a == "--cdf-algorithm") {
11883 // The analyzer validates the name; it is not defaulted here, so an
11884 // unrecognised algorithm is refused rather than answered with exact.
11885 o.knobs.cdf_algorithm = next("--cdf-algorithm");
11886 } else if (a == "--passage-from") o.knobs.passage_from = next("--passage-from");
11887 else if (a == "--passage-into") o.knobs.passage_into = next("--passage-into");
11888 else if (a == "--passage-method") o.knobs.passage_method = next("--passage-method");
11889 else if (a == "--passage-taboo") o.knobs.passage_taboo = next("--passage-taboo");
11890 else if (a == "--passage-tset") {
11891 o.knobs.passage_tset = next("--passage-tset");
11892 if (o.knobs.passage_tset.empty())
11893 throw line::InputError("--passage-tset needs at least one time ('t1,t2,...')");
11894 }
11895 else if (a == "--passage-orders")
11896 o.knobs.passage_orders = static_cast<std::size_t>(std::stoul(next("--passage-orders")));
11897 else if (a == "--no-interlocking") o.knobs.no_interlocking = true;
11898 else if (a == "--interlock-method") o.knobs.interlock_method = next("--interlock-method");
11899 else if (a == "--interlock-maxpaths")
11900 o.knobs.interlock_maxpaths = std::stod(next("--interlock-maxpaths"));
11901 else if (a == "--interlock-refpath-scope")
11902 o.knobs.interlock_refpath_scope = next("--interlock-refpath-scope");
11903 else if (a == "--lld") {
11904 // STATION=EXPR, repeatable; the expression is checked when the program is built
11905 const std::string v = next("--lld");
11906 const std::size_t eq = v.find('=');
11907 if (eq == std::string::npos || eq == 0 || eq + 1 == v.size())
11908 throw line::InputError("--lld takes STATION=EXPR, a processor or task name and an "
11909 "expression in n (got '" + v + "')");
11910 const std::string st = v.substr(0, eq);
11911 if (o.knobs.lld.count(st))
11912 throw line::InputError("--lld names station '" + st + "' twice");
11913 o.knobs.lld[st] = v.substr(eq + 1);
11914 } else if (a == "--nlp-alpha") {
11915 const std::string v = next("--nlp-alpha");
11916 const double x = std::atof(v.c_str());
11917 if (!(x > 0.0) || !std::isfinite(x))
11918 throw line::InputError("--nlp-alpha is the softmin sharpness and must be a positive finite "
11919 "number (got '" + v + "')");
11920 o.knobs.nlp_alpha = x;
11921 } else if (a == "--nlp-warm") {
11922 const long w = std::stol(next("--nlp-warm"));
11923 if (w < 0) throw line::InputError("--nlp-warm counts warm-start sweeps and must be >= 0");
11924 o.knobs.nlp_warm = w;
11925 } else if (a == "--nlp-maxiter") {
11926 const long m = std::stol(next("--nlp-maxiter"));
11927 if (m < 0) throw line::InputError("--nlp-maxiter is an iteration budget and must be >= 0");
11928 o.knobs.nlp_maxiter = m;
11929 } else if (a == "--nlp-fork") o.knobs.nlp_fork = next("--nlp-fork");
11930 else if (a == "--warmstart") o.knobs.warmstart = next("--warmstart");
11931 else if (a == "--mol-iter-max") {
11932 const long m = std::stol(next("--mol-iter-max"));
11933 if (m < 0) throw line::InputError("--mol-iter-max is an iteration budget and must be >= 0");
11934 o.knobs.mol_iter_max = m;
11935 } else if (a == "--mol-iter-tol") {
11936 const std::string v = next("--mol-iter-tol");
11937 const double x = std::atof(v.c_str());
11938 if (!(x > 0.0) || !std::isfinite(x))
11939 throw line::InputError("--mol-iter-tol must be a positive finite number (got '" + v + "')");
11940 o.knobs.mol_iter_tol = x;
11941 } else if (a == "--mol-relax-factor") {
11942 const std::string v = next("--mol-relax-factor");
11943 const double x = std::atof(v.c_str());
11944 if (!(x > 0.0) || !(x <= 1.0))
11945 throw line::InputError("--mol-relax-factor must lie in (0, 1] (got '" + v + "')");
11946 o.knobs.mol_relax_factor = x;
11947 } else if (a == "--layer-solver") o.knobs.layer_solver = next("--layer-solver");
11948 else if (a == "--stage-solver") o.knobs.stage_solver = next("--stage-solver");
11949 else if (a == "--env-init") o.knobs.env_init = next("--env-init");
11950 else if (a == "--map-env") o.knobs.map_env = next("--map-env");
11951 else if (a == "--map-env-method") o.knobs.map_env_method = next("--map-env-method");
11952 else if (a == "--map-env-maxstages")
11953 o.knobs.map_env_maxstages = static_cast<std::size_t>(std::stoul(next("--map-env-maxstages")));
11954 else if (a == "--ln-transient") o.knobs.ln_transient = next("--ln-transient");
11955 else if (a == "--ln-transient-channels")
11956 o.knobs.ln_transient_channels = next("--ln-transient-channels");
11957 else if (a == "--sens-method") o.knobs.sens_method = next("--sens-method");
11958 else if (a == "--sens-scheme") o.knobs.sens_scheme = next("--sens-scheme");
11959 else if (a == "--sens-step") {
11960 const std::string v = next("--sens-step");
11961 const double h = std::atof(v.c_str());
11962 if (!(h > 0.0) || !(h < 1.0))
11963 throw line::InputError(
11964 "--sens-step is the RELATIVE rate perturbation and must lie in (0,1) (got '" +
11965 v + "')");
11966 o.knobs.sens_step = h;
11967 }
11968 else if (a == "--uq-solver") o.knobs.uq_solver = next("--uq-solver");
11969 else if (a == "--keep") o.knobs.keep = true;
11970 else if (a == "--verbose") o.knobs.verbose = true;
11971 else if (a == "--remote") o.knobs.remote = true;
11972 else if (a == "--remote-url") {
11973 // Implies --remote: a URL given and then ignored because the flag
11974 // was forgotten would solve LOCALLY and report nothing about it.
11975 o.knobs.remote_url = next("--remote-url");
11976 o.knobs.remote = true;
11977 }
11978 else if (a == "--timeout") {
11979 const std::string v = next("--timeout");
11980 const long s = std::atol(v.c_str());
11981 if (s < 1)
11982 throw line::InputError("--timeout is a deadline in seconds and must be positive "
11983 "(got '" + v + "')");
11984 o.knobs.timeout_seconds = static_cast<int>(s);
11985 }
11986 else if (a == "--repeat") {
11987 const std::string v = next("--repeat");
11988 const long n = std::atol(v.c_str());
11989 if (n < 1)
11990 throw line::InputError("--repeat must be a positive run count (got '" + v + "')");
11991 o.knobs.repeat = static_cast<int>(n);
11992 }
11993 // ---- the simulator's own knobs ------------------------------------
11994 else if (a == "--ldes-tranfilter") {
11995 const std::string v = next("--ldes-tranfilter");
11996 if (v != "mser5" && v != "fixed" && v != "none")
11997 throw line::InputError(
11998 "--ldes-tranfilter selects the warmup filter and is mser5, fixed or none (got '" +
11999 v + "')");
12000 o.knobs.ldes_tranfilter = v;
12001 }
12002 else if (a == "--ldes-warmupfrac") {
12003 const std::string v = next("--ldes-warmupfrac");
12004 const double d = std::atof(v.c_str());
12005 if (!(d >= 0.0 && d < 1.0))
12006 throw line::InputError(
12007 "--ldes-warmupfrac is the fraction of the run the fixed filter discards and "
12008 "lies in [0,1) (got '" + v + "')");
12009 o.knobs.ldes_warmupfrac = d;
12010 }
12011 else if (a == "--ldes-cimethod") {
12012 const std::string v = next("--ldes-cimethod");
12013 if (v != "obm" && v != "bm" && v != "spectral" && v != "none")
12014 throw line::InputError(
12015 "--ldes-cimethod selects the confidence-interval estimator and is obm, bm, "
12016 "spectral or none (got '" + v + "')");
12017 o.knobs.ldes_cimethod = v;
12018 }
12019 else if (a == "--ldes-cnvgon") o.knobs.ldes_cnvgon = true;
12020 else if (a == "--ldes-cnvgtol") {
12021 // Implies --ldes-cnvgon: a tolerance given and then ignored because
12022 // the switch was forgotten would run the full budget and say nothing.
12023 const std::string v = next("--ldes-cnvgtol");
12024 const double d = std::atof(v.c_str());
12025 if (!(d > 0.0 && d < 1.0))
12026 throw line::InputError(
12027 "--ldes-cnvgtol is a RELATIVE precision target and lies in (0,1) (got '" + v +
12028 "')");
12029 o.knobs.ldes_cnvgtol = d;
12030 o.knobs.ldes_cnvgon = true;
12031 }
12032 else if (a == "--ldes-slotted") o.knobs.ldes_slotted = true;
12033 else if (a == "--slotted") o.knobs.slotted = true;
12034 else if (a == "--slotlength") {
12035 // Implies --slotted, as --ldes-slotlength does for the simulator.
12036 const std::string v = next("--slotlength");
12037 const double d = std::atof(v.c_str());
12038 if (!(d > 0.0))
12039 throw line::InputError(
12040 "--slotlength is the slot of the discrete time scale and must be positive "
12041 "(got '" + v + "')");
12042 o.knobs.slotlength = d;
12043 o.knobs.slotted = true;
12044 }
12045 else if (a == "--ldes-slotlength") {
12046 // Implies --ldes-slotted, as --slotlength does on the engine's CLI.
12047 const std::string v = next("--ldes-slotlength");
12048 const double d = std::atof(v.c_str());
12049 if (!(d > 0.0))
12050 throw line::InputError(
12051 "--ldes-slotlength is the slot of the discrete time scale and must be positive "
12052 "(got '" + v + "')");
12053 o.knobs.ldes_slotlength = d;
12054 o.knobs.ldes_slotted = true;
12055 }
12056 else if (a == "--jmt-replications") {
12057 const std::string v = next("--jmt-replications");
12058 const long n = std::atol(v.c_str());
12059 if (n < 1)
12060 throw line::InputError(
12061 "--jmt-replications is a positive count of independent runs (got '" + v + "')");
12062 o.knobs.jmt_replications = static_cast<int>(n);
12063 }
12064 else if (a == "--ldes-replications") {
12065 const std::string v = next("--ldes-replications");
12066 const long n = std::atol(v.c_str());
12067 if (n < 1)
12068 throw line::InputError(
12069 "--ldes-replications is a positive count of independent runs (got '" + v + "')");
12070 o.knobs.ldes_replications = static_cast<int>(n);
12071 }
12072 else if (a == "--ldes-numthreads") {
12073 const std::string v = next("--ldes-numthreads");
12074 const long n = std::atol(v.c_str());
12075 if (n < 1)
12076 throw line::InputError(
12077 "--ldes-numthreads is a positive worker count (got '" + v + "')");
12078 o.knobs.ldes_numthreads = static_cast<int>(n);
12079 }
12080 else if (a == "--ldes-maxtime") {
12081 const std::string v = next("--ldes-maxtime");
12082 const double d = std::atof(v.c_str());
12083 if (!(d > 0.0))
12084 throw line::InputError(
12085 "--ldes-maxtime is a wall-clock budget in seconds and must be positive (got '" +
12086 v + "')");
12087 o.knobs.ldes_maxtime = d;
12088 }
12089 else if (a == "--ldes-initsol") {
12090 // A STATION-MAJOR placement, [st0_cl0, st0_cl1, ...]; the engine
12091 // reads it as the initial state and skips its default placement.
12092 const std::string v = next("--ldes-initsol");
12093 std::size_t b = 0;
12094 while (b <= v.size()) {
12095 const std::size_t e = v.find(',', b);
12096 const std::string tok =
12097 v.substr(b, e == std::string::npos ? std::string::npos : e - b);
12098 if (tok.empty())
12099 throw line::InputError(
12100 "--ldes-initsol is a comma-separated placement with no empty entry (got '" +
12101 v + "')");
12102 o.knobs.ldes_initsol.push_back(std::atof(tok.c_str()));
12103 if (e == std::string::npos) break;
12104 b = e + 1;
12105 }
12106 }
12107 else if (a == "--ldes-rest-url") o.knobs.ldes_rest_url = next("--ldes-rest-url");
12108 else if (a == "-v" || a == "--verbosity") {
12109 const std::string v = next(a.c_str());
12110 // The JAR names two levels and this port's help documents three;
12111 // all five spellings are accepted, and an unknown one is refused
12112 // rather than read as `standard`, which would silence nothing while
12113 // reporting that it had.
12114 if (v != "silent" && v != "standard" && v != "normal" && v != "debug" &&
12115 v != "verbose")
12116 throw line::InputError(
12117 "-v takes silent, standard (the JAR spells it normal) or debug; got '" + v +
12118 "'");
12119 o.knobs.verbosity = (v == "normal") ? "standard" : v;
12120 }
12121 else if (!a.empty() && a[0] == '-')
12122 throw line::InputError("unknown option: " + a);
12123 else
12124 o.file = a;
12125 }
12126 return o;
12127}
12128
12129} // namespace
12130
12131/**
12132 * `--find-solver`: which solvers and methods can analyze the model named by -f.
12133 *
12134 * It reports rather than solves, so it stops before the solver ladder in
12135 * `solve_model_dispatch` and before every knob that describes a run. The answer
12136 * is arithmetic-independent -- `auto_find_solver` asks feature sets, shapes and
12137 * gates, never numbers -- so the model is read at double whatever --arith says,
12138 * and a caller who passed one is told rather than silently obeyed.
12139 *
12140 * The layered path is not covered: `auto_find_solver` narrows the flat Network
12141 * families, and a LayeredNetwork's are `ln` and `lqns`, which this port reaches
12142 * through `solve_lqn_dispatch` and not through an AUTO of its own.
12143 */
12144int find_solver_report(const Options& o) {
12145 // A MODEL ON STDIN IS A MODEL. This used to refuse anything but `-f`, which
12146 // made the report the one part of the vocabulary a piped model could not
12147 // reach -- and `read_model` has read stdin on the empty path all along.
12148 if (o.file.empty() && !g_stdin_loaded)
12149 throw line::InputError("--find-solver reports on a model; name one with -f");
12150 const bool layered = o.file.empty()
12151 ? line::io::is_layered_json_text(stdin_model_text())
12152 : (has_lqn_extension(o.file) || line::lqn::is_srvn_path(o.file) ||
12154 if (!o.input_given && layered)
12156 "--find-solver reports on a flat Network model; a layered one is solved by -s ln "
12157 "and -s lqns, which this port reaches through the -i lqnx path");
12158 if (o.output != "readable" && o.output != "json")
12159 throw line::InputError("unknown -o '" + o.output +
12160 "'; --find-solver reports as: readable, json");
12161 line::qn::Network<double> net = read_model<double>(o.file);
12162 const std::vector<line::autosolver::SolverCandidate> rows = line::autosolver::auto_find_solver(
12163 net.get_struct(), o.find_solver_metric, o.find_solver_all);
12164 if (o.output != "json") {
12165 std::printf("%s", line::autosolver::auto_find_solver_table(rows).c_str());
12166 return 0;
12167 }
12168 // THE STRUCTURED ROWS EXIST ALREADY AND HAD NO WAY OUT. auto_find_solver
12169 // returns solver, method, runnable, the method class, the metrics and the
12170 // reason for a refusal, and the fixed-width table was the only rendering,
12171 // so every caller that wanted the reason had to parse columns. All five
12172 // front ends and the MCP `find_solver` tool benefit from this arm.
12173 line::reg::Json j = line::reg::Json::object();
12174 line::reg::Json cands = line::reg::Json::array();
12175 for (std::size_t i = 0; i < rows.size(); ++i) {
12176 line::reg::Json c = line::reg::Json::object();
12177 c["solver"] = rows[i].solver;
12178 c["method"] = rows[i].method;
12179 c["runnable"] = rows[i].runnable;
12180 c["methodClass"] = rows[i].method_class;
12181 c["reason"] = rows[i].reason;
12182 // metrics is already the comma-joined string the table prints; iterating
12183 // it pushed one character code per element.
12184 c["metrics"] = rows[i].metrics;
12185 cands.push_back(c);
12186 }
12187 line::reg::Json body = line::reg::Json::object();
12188 body["type"] = "FindSolver";
12189 body["candidates"] = cands;
12190 j["findSolver"] = body;
12191 emit_document(dump_document(j));
12192 return 0;
12193}
12194
12195/**
12196 * Everything one invocation does once the arguments are in hand.
12197 *
12198 * SPLIT OUT OF `main` FOR SERVER MODE, which runs it once per request with a
12199 * different `-f` and a captured stdout. Keeping one body means a request served
12200 * over the socket takes exactly the path the same command line takes at the
12201 * shell -- the failure this avoids is a server that answers slightly differently
12202 * from the CLI it is supposed to BE.
12203 */
12204int run_invocation(Options o) {
12205 // Validated on every path, not only --api: an unrecognised --arith is a
12206 // caller error whatever else the invocation asks for.
12207 line::reg::parse_arith(o.arith);
12208 if (!o.api.empty()) {
12209 if (o.output != "readable" && o.output != "json")
12210 throw line::InputError("unknown -o '" + o.output +
12211 "'; accepted forms are: readable, json");
12212 const line::reg::Json result =
12213 line::reg::api_invoke(o.api, o.arith, read_api_args(o.args));
12214 if (o.output == "json")
12215 emit_document(dump_document(result, 2));
12216 else
12217 std::printf("%s", line::reg::api_render_readable(result).c_str());
12218 return 0;
12219 }
12220 // A .lqnx/.xml path with no -i is taken as a layered model, so naming
12221 // the file is enough to solve it. An explicit -i always wins, so a
12222 // caller who says -i json about an oddly-named file still gets json.
12223 if (!o.input_given && has_lqn_extension(o.file)) o.input = "lqnx";
12224 // The LQNS SRVN text: `.in` always, `.lqn` unless the file opens with '<' (then it is lqnx).
12225 if (!o.input_given && !o.file.empty() && line::lqn::is_srvn_path(o.file)) o.input = "srvn";
12226 // The same courtesy for a JMT document: naming the file is enough.
12227 if (!o.input_given && has_jsim_extension(o.file)) o.input = "jsimg";
12228 // And for a PNML document.
12229 if (!o.input_given && has_pnml_extension(o.file)) o.input = "pnml";
12230 // A model.json can carry EITHER model kind, and `linemodel_save` writes
12231 // `.json` for both, so the extension cannot decide it. The content can:
12232 // a `LayeredNetwork` type takes the layered path whatever `-i` says,
12233 // because handing it to the Network reader only produces "model type
12234 // 'LayeredNetwork' is not a Network" one frame further down.
12235 // THE SNIFF NOW COVERS A PIPED DOCUMENT. It took a PATH and was
12236 // therefore skipped entirely on stdin, so a LayeredNetwork arriving on
12237 // the standard input was handed to the Network reader and refused with
12238 // "the model-solving path ports -s mva, nc, ..." -- a message about the
12239 // solver, for a model the CLI could read perfectly well from a file.
12240 // Staging the text to a temporary file would have worked and would cost
12241 // a file per solve, which a host calling this in a sweep notices.
12242 const bool sniffable = o.input != "lqnx" && o.input != "xml" && o.input != "srvn" && o.input != "pnml" &&
12243 o.input.compare(0, 4, "jsim") != 0;
12244 if (sniffable && !o.file.empty() && !has_pnml_extension(o.file) &&
12245 !has_jsim_extension(o.file) && line::io::is_layered_json(o.file))
12246 o.input = "lqnx";
12247 else if (sniffable && o.file.empty() && g_stdin_loaded &&
12248 line::io::is_layered_json_text(stdin_model_text()))
12249 o.input = "lqnx";
12250 if (o.input == "lqnx" || o.input == "xml" || o.input == "srvn") {
12251 // `-i srvn` is read by TRANSLATION to .lqnx (lqn::srvn2lqnx), and every layered reader
12252 // recognises a `.in` / non-XML `.lqn` path itself. Only an SRVN file under another
12253 // extension is staged, as its translation, to a temporary document the arms can read.
12254 std::string lqn_file = o.file;
12255 std::string staged;
12256 if (o.input == "srvn" && !o.file.empty() && !line::lqn::is_srvn_path(o.file)) {
12257 const std::string doc = line::lqn::srvn_file_to_lqnx(o.file);
12258 const char* tmpdir = std::getenv("TMPDIR");
12259 std::string path = std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + "/line-cli-srvn-XXXXXX";
12260 std::vector<char> buf(path.begin(), path.end());
12261 buf.push_back('\0');
12262 const int fd = ::mkstemp(&buf[0]);
12263 if (fd < 0) throw line::InputError(line::ErrorKind{"File"}, "cannot stage the SRVN translation of " + o.file);
12264 ::close(fd);
12265 staged.assign(&buf[0]);
12266 std::ofstream(staged.c_str(), std::ios::binary) << doc;
12267 lqn_file = staged;
12268 }
12269 struct Unstage {
12270 const std::string& p;
12271 ~Unstage() { if (!p.empty()) std::remove(p.c_str()); }
12272 } unstage{staged};
12273 if (o.output != "readable" && o.output != "json" && o.output != "layers")
12274 throw line::InputError("unknown -o '" + o.output +
12275 "'; accepted forms on the layered path are: readable, "
12276 "json, layers");
12277 // ONE ENVELOPE PER ANALYSIS, in the order asked for. `-a avg,sens`
12278 // is the JAR's comma list, and the JAR merges the results into one
12279 // JSON object; here each arm prints as it computes, so the multi
12280 // form emits a sequence of the SAME envelopes a single `-a` emits
12281 // (JSON Lines). That keeps one parser for both spellings, where a
12282 // merged object would need a second one for the multi case alone.
12283 const std::vector<std::string> as = analysis_list(o.analysis);
12284 for (std::size_t i = 0; i + 1 < as.size(); ++i) {
12285 const int rc = solve_lqn_dispatch(o.arith, o.solver, as[i], o.output, lqn_file,
12286 o.knobs);
12287 if (rc != 0) return rc;
12288 poll_interrupt();
12289 }
12290 return solve_lqn_dispatch(o.arith, o.solver, as.back(), o.output, lqn_file, o.knobs);
12291 }
12292 // `-i jsim | jsimg | jsimw`: a JMT simulation document, read by
12293 // `read_jsim`. The three extensions name ONE format -- JMT writes the
12294 // same `<sim>` document under all three -- and are accepted separately
12295 // because the JAR CLI accepts them separately and a script naming the
12296 // wrong one is not describing a different model.
12297 if (o.input == "jsim" || o.input == "jsimg" || o.input == "jsimw")
12298 g_jsim_input = true;
12299 else if (o.input == "pnml")
12300 g_pnml_input = true;
12301 else if (o.input != "json")
12303 "the model-solving path reads -i json for a Network model, -i jsim|jsimg|jsimw "
12304 "for a JMT simulation document, -i pnml for a place/transition net and "
12305 "-i lqnx|xml|srvn for a layered one (got '" + o.input + "')");
12306 // `-o json` IS WIRED FOR EVERY ANALYSIS: every arm emits a payload
12307 // (`emit_analysis`) under a key named after its own `-a`, so a caller can
12308 // tell which question was answered. An arm added without a payload must
12309 // refuse `-o json`, not ignore it. On `-a avg` the solver BANNER still
12310 // precedes the object on stdout, as the JAR's does; it contains no brace,
12311 // so the wrapper's first-`{` scan is unaffected. The other arms print no
12312 // banner: what it carried -- the arithmetic, the resolved method, the
12313 // state count and the cutoff -- is inside the payload, where a host reads
12314 // it as data instead of scraping it from a line above.
12315 // `-o jsimg`: EXPORT the model as a JMT simulation document instead of
12316 // solving it. It is the missing half of a pair -- `-i jsimg` has read one
12317 // back all along -- and the C++ twin of MATLAB's `JMTIO`, the JAR's
12318 // `QN2JSIMG` and Python's `qn2jsimg`. The three spellings name ONE
12319 // format, as they do on `-i` and in JMT.
12320 if (o.output == "jsim" || o.output == "jsimg" || o.output == "jsimw") {
12321 line::qn::Network<double> net = read_model<double>(o.file);
12324 wopt.file_name = "model";
12325 wopt.log_path = sn.log_path;
12326 // The simulation controls the header carries are the ones a solve
12327 // would have written into it, so an exported document run through
12328 // JMT by hand reproduces `-s jmt` rather than the writer defaults.
12329 if (o.knobs.seed) wopt.seed = static_cast<long>(o.knobs.seed);
12330 if (o.knobs.samples) wopt.max_samples = static_cast<double>(o.knobs.samples);
12331 if (o.knobs.t1 >= 0.0) wopt.max_simulated_time = o.knobs.t1;
12332 emit_document(line::io::jmt_write_jsim<double>(sn, wopt));
12333 return 0;
12334 }
12335 if (o.output != "readable" && o.output != "json")
12336 throw line::InputError("unknown -o '" + o.output +
12337 "'; accepted forms are: readable, json, jsimg");
12338 g_json_output = (o.output == "json");
12339 // See the layered path above for why a comma list emits one envelope
12340 // per analysis rather than one merged object.
12341 const std::vector<std::string> as = analysis_list(o.analysis);
12342 for (std::size_t i = 0; i + 1 < as.size(); ++i) {
12343 const int rc = solve_model_dispatch(o.arith, o.solver, as[i], o.file, o.knobs);
12344 if (rc != 0) return rc;
12345 poll_interrupt();
12346 }
12347 return solve_model_dispatch(o.arith, o.solver, as.back(), o.file, o.knobs);
12348}
12349
12350/**
12351 * Redirect a file descriptor into a temporary file for a scope, and give back
12352 * what was written to it.
12353 *
12354 * A TEMPORARY FILE AND NOT A PIPE, which was already the choice here and is
12355 * worth keeping deliberately: a pipe holds 64 KB and nobody is draining it, so
12356 * a solve that prints more than that would deadlock against its own output.
12357 *
12358 * IT IS AN OBJECT so that the restore happens on the way out of the scope
12359 * whatever happens inside it. The server's version below did the same dance
12360 * inline and could not have survived a throw between the dup2 and the restore,
12361 * which is survivable in a one-shot process and is a leaked file descriptor
12362 * per call in a host that calls this in a loop.
12363 */
12365 public:
12366 explicit FdCapture(int fd) : fd_(fd), saved_(-1), tfd_(-1) {
12367 if (fd_ < 0) return; // "do not capture", so the object is inert
12368 const char* tmpdir = std::getenv("TMPDIR");
12369 std::string path =
12370 std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + "/line-cli-cap-XXXXXX";
12371 std::vector<char> buf(path.begin(), path.end());
12372 buf.push_back('\0');
12373 tfd_ = ::mkstemp(&buf[0]);
12374 if (tfd_ < 0) return;
12375 path_.assign(&buf[0]);
12376 std::fflush(fd_ == 1 ? stdout : stderr);
12377 saved_ = ::dup(fd_);
12378 ::dup2(tfd_, fd_);
12379 }
12380
12381 // restore() closes the descriptor; only take() removes the backing file, so a
12382 // capture abandoned by a throw would leave one behind. The server calls
12383 // run_invocation_captured once per request, so that is a leak per FAILED
12384 // request, which is the same defect the JAR CLI had in its model staging.
12385 // take() clears path_ after removing, so this is a no-op on the normal path.
12387 restore();
12388 if (!path_.empty()) {
12389 std::remove(path_.c_str());
12390 path_.clear();
12391 }
12392 }
12393
12394 /** Restore the descriptor and read back what was written. Idempotent. */
12395 std::string take() {
12396 restore();
12397 if (path_.empty()) return std::string();
12398 std::ifstream in(path_.c_str());
12399 std::string out((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
12400 in.close();
12401 std::remove(path_.c_str());
12402 path_.clear();
12403 return out;
12404 }
12405
12406 bool active() const { return saved_ >= 0; }
12407
12408 private:
12409 void restore() {
12410 if (saved_ < 0) return;
12411 std::fflush(fd_ == 1 ? stdout : stderr);
12412 ::dup2(saved_, fd_);
12413 ::close(saved_);
12414 saved_ = -1;
12415 if (tfd_ >= 0) {
12416 ::close(tfd_);
12417 tfd_ = -1;
12418 }
12419 }
12420 FdCapture(const FdCapture&);
12421 FdCapture& operator=(const FdCapture&);
12422
12423 int fd_;
12424 int saved_;
12425 int tfd_;
12426 std::string path_;
12427};
12428
12429/**
12430 * Run one invocation with stdout captured, and return what it printed.
12431 *
12432 * SERVER MODE'S ONE PIECE OF MACHINERY. Every arm of this CLI writes its answer
12433 * with `printf`, which is the right thing for a command-line tool and leaves a
12434 * server nothing to send. Redirecting fd 1 around the call means the arms need
12435 * no server-aware variant and cannot drift from the command-line behaviour;
12436 * stderr is deliberately NOT captured, so a warning still reaches the operator's
12437 * console rather than being folded into the client's answer.
12438 */
12439std::string run_invocation_captured(Options o, int& rc) {
12440 // EVERY REQUEST STARTS FROM A CLEAN SLATE. Without this a request carrying
12441 // `-i jsimg` left g_jsim_input true and poisoned every later request on the
12442 // same server, which is a live bug in shipped code rather than a
12443 // hypothetical one.
12444 reset_invocation_state(false);
12445 FdCapture cap(1);
12446 if (!cap.active()) {
12447 rc = 3;
12448 return "line-cli: cannot create a capture file for the response\n";
12449 }
12450 std::string err;
12451 try {
12452 rc = run_invocation(o);
12453 } catch (const line::Error& e) {
12454 rc = 2;
12455 err = std::string("line-cli: [") + e.id() + "] " + e.what() + "\n";
12456 } catch (const std::exception& e) {
12457 rc = 3;
12458 err = std::string("line-cli: unexpected failure: ") + e.what() + "\n";
12459 }
12460 std::string out = cap.take();
12461 // THE ERROR IS THE ANSWER when the solve failed: a client that receives an
12462 // empty message cannot tell a refusal from a model with no rows.
12463 return err.empty() ? out : out + err;
12464}
12465
12466/**
12467 * `-p/--port`: serve solve requests over a WebSocket, as `LineWebSocketServer`
12468 * does.
12469 *
12470 * THE PROTOCOL IS THE JAR's, unchanged: one text message per connection, whose
12471 * FIRST LINE is the comma-separated argument list and whose remainder is the
12472 * model document. The JAR overwrites the first two arguments with `--file` and
12473 * the path it staged the document at, so the client's own first two tokens are
12474 * placeholders; the same substitution happens here, which is what lets an
12475 * existing client talk to this server without knowing which binary answered.
12476 */
12477int run_server(const Options& base) {
12478 line::ws::Server server(base.port);
12479 std::printf("--------------------------------------------------------------------\n");
12480 std::printf("LINE Solver - Command Line Interface (C++)\n");
12481 std::printf("Copyright (c) 2012-2026, QORE Lab, Imperial College London\n");
12482 std::printf("Version %s. All rights reserved.\n", kVersion);
12483 std::printf("--------------------------------------------------------------------\n");
12484 std::printf("Running in server mode on port %d.\n", base.port);
12485 if (base.maxreq)
12486 std::printf("Quitting after %d request(s).\n", base.maxreq);
12487 std::fflush(stdout);
12488
12489 int served = 0;
12490 while (base.maxreq == 0 || served < base.maxreq) {
12491 const bool ok = server.serve_one([&](const std::string& msg) -> std::string {
12492 const std::string::size_type nl = msg.find('\n');
12493 if (nl == std::string::npos)
12494 return "line-cli: the request's first line is the argument list and its "
12495 "remainder is the model document; this message has no newline\n";
12496 const std::string argline = msg.substr(0, nl);
12497 const std::string model = msg.substr(nl + 1);
12498
12499 const char* tmpdir = std::getenv("TMPDIR");
12500 std::string path =
12501 std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + "/line-cli-req-XXXXXX";
12502 std::vector<char> nb(path.begin(), path.end());
12503 nb.push_back('\0');
12504 const int mfd = ::mkstemp(&nb[0]);
12505 if (mfd < 0) return "line-cli: cannot stage the client model\n";
12506 ::close(mfd);
12507 path.assign(&nb[0]);
12508 {
12509 std::ofstream mf(path.c_str());
12510 mf << model;
12511 }
12512
12513 // The argument list, with the first two tokens replaced by the
12514 // staged path exactly as `LineWebSocketServer.onMessage` replaces
12515 // them. A list SHORTER than two is the client's error and is
12516 // reported rather than padded, since padding would solve the
12517 // default model instead of the one it sent.
12518 std::vector<std::string> toks;
12519 std::string::size_type at = 0;
12520 while (at <= argline.size()) {
12521 const std::string::size_type comma = argline.find(',', at);
12522 toks.push_back(argline.substr(
12523 at, comma == std::string::npos ? std::string::npos : comma - at));
12524 if (comma == std::string::npos) break;
12525 at = comma + 1;
12526 }
12527 std::string result;
12528 if (toks.size() < 2) {
12529 result = "line-cli: the argument list needs at least two tokens; the first two "
12530 "are replaced by --file and the staged model path\n";
12531 } else {
12532 toks[0] = "--file";
12533 toks[1] = path;
12534 std::vector<char*> argv;
12535 std::vector<std::string> store;
12536 store.push_back("line-cli");
12537 for (std::size_t i = 0; i < toks.size(); ++i) store.push_back(toks[i]);
12538 for (std::size_t i = 0; i < store.size(); ++i)
12539 argv.push_back(const_cast<char*>(store[i].c_str()));
12540 int rc = 0;
12541 try {
12542 Options ro = parse_args(static_cast<int>(argv.size()), &argv[0]);
12543 // The server's own flags never travel into a request: a
12544 // client that sent `-p` would otherwise make the server
12545 // recurse into a second listener on the same process.
12546 ro.port = 0;
12547 ro.maxreq = 0;
12548 result = run_invocation_captured(ro, rc);
12549 } catch (const line::Error& e) {
12550 result = std::string("line-cli: [") + e.id() + "] " + e.what() + "\n";
12551 }
12552 }
12553 std::remove(path.c_str());
12554 return result;
12555 });
12556 // A dropped client is not a reason to stop serving, and it does not
12557 // count against --maxreq either: the JAR counts REQUESTS, and a peer
12558 // that vanished before sending one made none.
12559 if (ok) ++served;
12560 }
12561 return 0;
12562}
12563
12564/**
12565 * `--generate`: draw a random model and print it, solving nothing.
12566 *
12567 * `{"kind":"network", ...}` emits the `line-model` JSON document that every
12568 * reader in the project already takes, so the generated model round-trips
12569 * through `-f` and through `load_model` in R and Python without a second
12570 * format. `{"kind":"layered", ...}` emits the `.lqnx` document instead,
12571 * because that is the only interchange the layered readers all share; it is
12572 * plain text on standard output rather than a collected JSON document, since
12573 * an XML string is not one.
12574 *
12575 * THE ARITHMETIC IS ALWAYS double. Both generators draw from
12576 * `java.util.Random`, whose output is a double by definition, so generating at
12577 * `--arith exact` would fabricate an exactness the draw never had. A caller who
12578 * wants the model solved in another arithmetic reads it back with that --arith.
12579 */
12580int run_generate(const Options& o) {
12581 const line::reg::Json spec = read_generate_spec(o.generate);
12582 if (!spec.is_object())
12583 throw line::InputError("--generate takes a JSON object, e.g. "
12584 "{\"kind\":\"network\",\"queues\":3,\"closedClasses\":1}");
12585 const std::string kind = gen_str(spec, "kind", "network");
12586 if (kind == "network") {
12587 static const char* const keys[] = {
12588 "kind", "seed", "name", "queues", "delays", "openClasses", "closedClasses",
12589 "schedStrat", "routingStrat", "distribution", "cclassJobLoad", "varyingServiceRates",
12590 "multiServerQueues", "randomCSNodes", "multiChainCS", "topology"};
12591 gen_check_keys(spec, keys, sizeof(keys) / sizeof(keys[0]));
12593 if (spec.contains("seed")) g.set_seed(gen_int(spec, "seed", 0));
12594 g.set_model_name(gen_str(spec, "name", "nw"));
12595 g.set_sched_strat(gen_str(spec, "schedStrat", "randomize"));
12596 g.set_routing_strat(gen_str(spec, "routingStrat", "randomize"));
12597 g.set_distribution(gen_str(spec, "distribution", "randomize"));
12598 g.set_cclass_job_load(gen_str(spec, "cclassJobLoad", "randomize"));
12599 g.set_varying_service_rates(gen_bool(spec, "varyingServiceRates", false));
12600 g.set_multi_server_queues(gen_bool(spec, "multiServerQueues", false));
12601 g.set_random_cs_nodes(gen_bool(spec, "randomCSNodes", false));
12602 g.set_multi_chain_cs(gen_bool(spec, "multiChainCS", false));
12603 const std::string topo = gen_str(spec, "topology", "rand");
12604 if (topo == "rand")
12606 else if (topo == "cyclic")
12608 else
12609 throw line::InputError("--generate: unknown topology '" + topo +
12610 "'; accepted forms are: rand, cyclic");
12611 // `delays` defaults to -1, which is the generator's "decide for me":
12612 // it then draws the split between queues and delays itself.
12613 const long nq = gen_int(spec, "queues", 1);
12614 const long nd = gen_int(spec, "delays", -1);
12615 const long no = gen_int(spec, "openClasses", 0);
12616 const long nc = gen_int(spec, "closedClasses", 1);
12617 line::qn::Network<double> m = g.generate(static_cast<int>(nq), static_cast<int>(nd),
12618 static_cast<int>(no), static_cast<int>(nc));
12619 emit_document(line::io::network_json_envelope(m.get_struct()).dump(2));
12620 return 0;
12621 }
12622 if (kind == "layered") {
12623 static const char* const keys[] = {
12624 "kind", "seed", "name", "clients", "levels", "tasks", "processors", "populationRange",
12625 "thinkTimeRange", "taskMultiRange", "procMultiRange", "hostDemandRange",
12626 "synchCallRange", "taskInfProbability", "procInfProbability"};
12627 gen_check_keys(spec, keys, sizeof(keys) / sizeof(keys[0]));
12629 if (spec.contains("seed")) g.set_seed(gen_int(spec, "seed", 0));
12630 g.set_model_name(gen_str(spec, "name", "lnw"));
12631 g.set_population_range(gen_range(spec, "populationRange", line::gen::Range(1, 1)));
12632 g.set_think_time_range(gen_range(spec, "thinkTimeRange", line::gen::Range(1, 1)));
12633 g.set_task_multi_range(gen_range(spec, "taskMultiRange", line::gen::Range(1, 1)));
12634 g.set_proc_multi_range(gen_range(spec, "procMultiRange", line::gen::Range(1, 1)));
12635 g.set_host_demand_range(gen_range(spec, "hostDemandRange", line::gen::Range(1, 1)));
12636 g.set_synch_call_range(gen_range(spec, "synchCallRange", line::gen::Range(1, 1)));
12637 g.set_task_inf_probability(gen_real(spec, "taskInfProbability", 0.0));
12638 g.set_proc_inf_probability(gen_real(spec, "procInfProbability", 0.0));
12639 const long ncl = gen_int(spec, "clients", 1);
12640 const long nlv = gen_int(spec, "levels", 1);
12641 const long ntk = gen_int(spec, "tasks", 1);
12642 const long npr = gen_int(spec, "processors", 1);
12644 g.generate(static_cast<int>(ncl), static_cast<int>(nlv), static_cast<int>(ntk),
12645 static_cast<int>(npr));
12646 // BUILT BEFORE IT IS PRINTED, so a model the finalizer refuses is a
12647 // failure here rather than a document that only fails when read back.
12648 b.build();
12649 std::printf("%s\n", line::lqn::lqnx_to_string(b.model(), g.model_name()).c_str());
12650 return 0;
12651 }
12652 throw line::InputError("--generate: unknown kind '" + kind +
12653 "'; accepted forms are: network, layered");
12654}
12655
12656/**
12657 * Everything `main` did, with the arguments already in hand.
12658 *
12659 * SPLIT OUT SO A HOST CAN CALL IT. The binary's `main` is now ten lines in its
12660 * own translation unit, and this is the body both it and the C ABI run, so a
12661 * command line and an in-process call take the same path by construction
12662 * rather than by review.
12663 *
12664 * `no_args` replaces the `argc == 1` that decided the brief help, because an
12665 * in-process caller has no argv[0] to count and would otherwise never see it.
12666 */
12667int dispatch(const Options& in, bool allow_server, bool no_args) {
12668 {
12669 Options o = in;
12670 // Before any arm runs: `read_model` raises the priority warning and has
12671 // no knobs of its own to read the level from.
12672 if (!o.knobs.verbosity.empty()) g_verbosity = o.knobs.verbosity;
12673 // Solver console: set for the whole process, so that the model compile
12674 // narrates too -- LineConsole::writes() falls back to the session level
12675 // when no run is open, and reading the model happens before any solver
12676 // exists. `-v debug` (and its `verbose` spelling) is the ONLY way in.
12678 g_verbosity == "silent" ? line::util::VerboseLevel::SILENT
12679 : (g_verbosity == "debug" || g_verbosity == "verbose")
12682 if (o.help_all) {
12683 print_help();
12684 return 0;
12685 }
12686 if (o.help || no_args) {
12687 print_brief_help();
12688 return 0;
12689 }
12690 if (o.version) {
12691 std::printf("line-cli %s\n", kVersion);
12692 return 0;
12693 }
12694 // A warning is not a failure: every dependency the check probes is
12695 // optional, so the command exits 0 whenever the check itself ran.
12696 if (o.install) {
12697 install_check();
12698 return 0;
12699 }
12700 if (o.list) {
12701 list_api();
12702 return 0;
12703 }
12704 // BEFORE the model-reading path, and not inside it: --generate PRODUCES
12705 // a model rather than consuming one, so -f is neither required nor read.
12706 if (o.generate_given) return run_generate(o);
12707 if (o.find_solver) return find_solver_report(o);
12708 if (o.port) {
12709 // A HOST DOES NOT GET A SERVER BY ACCIDENT. `-p` blocks the calling
12710 // thread and opens a listening socket, so a host forwarding a
12711 // user's argument string would otherwise be handing that user both.
12712 if (!allow_server)
12713 throw line::InputError(
12714 "-p runs the solver as a server, which this caller did not permit");
12715 // `-f` and `-p` together would be two model sources for one run;
12716 // the request carries the model in server mode, so a file named on
12717 // the command line could only be ignored.
12718 if (!o.file.empty())
12719 throw line::InputError(
12720 "-p runs the solver as a server, where each request carries its own model; "
12721 "-f names a model on the command line and the two cannot both be the source");
12722 return run_server(o);
12723 }
12724 return run_invocation(o);
12725 }
12726}
12727
12728namespace line {
12729namespace cli {
12730
12731Response run(const Request& req) {
12732 Response res;
12733 reset_invocation_state(false);
12734 g_interrupt_cb = req.interrupt;
12735 g_interrupt_user = req.interrupt_user;
12736 if (req.has_stdin) set_stdin_override(req.stdin_text);
12737
12738 std::vector<std::string> docs;
12739 if (req.collect_json) g_json_sink = &docs;
12740
12741 // argv[0] IS SYNTHESISED HERE. parse_args skips it, as every argv parser
12742 // does, and a host that passed its own would have its first real argument
12743 // eaten. Requiring the host to prepend one would be requiring it to know
12744 // that, which is exactly the kind of knowledge a boundary should absorb.
12745 std::vector<std::string> store;
12746 store.push_back("line-cli");
12747 for (std::size_t i = 0; i < req.argv.size(); ++i) store.push_back(req.argv[i]);
12748 std::vector<char*> argv;
12749 for (std::size_t i = 0; i < store.size(); ++i) argv.push_back(&store[i][0]);
12750
12751 {
12752 // THE CAPTURES ENCLOSE THE try/catch, NOT JUST THE CALL. A failure
12753 // message the arms would have written to fd 2 is therefore captured
12754 // too, rather than escaping to the host's console -- which for R means
12755 // escaping to a terminal the user may not even be looking at. They are
12756 // plain scoped objects taking a sentinel fd for "do not capture",
12757 // because a std::optional here is the same thing with a longer name.
12758 FdCapture out(req.capture_stdout ? 1 : -1);
12759 FdCapture err(req.capture_stderr ? 2 : -1);
12760 try {
12761 res.exit_code = dispatch(parse_args(static_cast<int>(argv.size()), &argv[0]),
12762 req.allow_server, req.argv.empty());
12763 } catch (const InterruptRequested& e) {
12764 res.exit_code = 2;
12765 res.error = e.what();
12766 res.error_id = "line:interrupt";
12767 res.line_error_id = e.id();
12768 } catch (const line::Error& e) {
12769 res.exit_code = 2;
12770 res.error = e.what();
12772 res.line_error_id = e.id();
12773 } catch (const std::exception& e) {
12774 res.exit_code = 3;
12775 res.error = std::string("unexpected failure: ") + e.what();
12776 res.error_id = "line:internal";
12777 }
12778 if (req.capture_stdout) res.text = out.take();
12779 if (req.capture_stderr) res.diagnostics = err.take();
12780 }
12781
12782 g_json_sink = nullptr;
12783 g_interrupt_cb = nullptr;
12784 g_interrupt_user = nullptr;
12785 res.documents = docs;
12786 reset_invocation_state(false);
12787 return res;
12788}
12789
12790int main_body(int argc, char** argv) {
12791 try {
12792 return dispatch(parse_args(argc, argv), true, argc == 1);
12793 } catch (const line::Error& e) {
12794 std::fprintf(stderr, "line-cli: [%s] %s\n", e.id().c_str(), e.what());
12795 return 2;
12796 } catch (const std::exception& e) {
12797 std::fprintf(stderr, "line-cli: unexpected failure: %s\n", e.what());
12798 return 3;
12799 }
12800}
12801
12802} // namespace cli
12803} // namespace line
The -s ag entry point: gates, fixed point, mean measures.
Direct invocation of a single API function from named JSON arguments.
SolverAUTO.listValidMethods: the method names THIS MODEL can actually run.
Redirect a file descriptor into a temporary file for a scope, and give back what was written to it.
FdCapture(int fd)
bool active() const
std::string take()
Restore the descriptor and read back what was written.
Base error for the multiprecision C++ port.
Definition error.h:73
const std::string & id() const noexcept
The full identifier, e.g.
Definition error.h:78
std::string category() const
unsupported, numerical, input, external, internal, or empty for LINE:Error.
Definition error.h:81
std::string kind() const
The <kind> part, or empty for LINE:Error.
Definition error.h:88
A tool, binary, service or bridge LINE depends on is missing or failed: LINE:external:<kind>.
Definition error.h:124
Malformed or inconsistent input (dimensions, negative populations, ...): LINE:input:<kind>.
Definition error.h:103
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
The algorithm cannot produce a trustworthy answer on this instance: LINE:numerical:<kind>.
Definition error.h:110
A refusal: the feature, method or analysis is not covered: LINE:unsupported:<kind>.
Definition error.h:117
const EnvStage< T > & stage(std::size_t e) const
std::size_t nstages() const
A random lqn::LqnBuilder<T> source, configured once and then drawn from.
void set_host_demand_range(const Range &r)
Activity host demands.
lqn::LqnBuilder< T > generate(int num_clients, int num_levels, int num_tasks, int num_processors)
void set_proc_inf_probability(double p)
Probability that a processor is infinite-server rather than PS.
void set_synch_call_range(const Range &r)
Mean number of synchronous calls on a call arc.
void set_model_name(const std::string &nm)
The name of the generated model.
void set_population_range(const Range &r)
Reference-task populations.
void set_think_time_range(const Range &r)
Client think times.
void set_proc_multi_range(const Range &r)
Processor multiplicities, for the processors that are not infinite-server.
void set_task_multi_range(const Range &r)
Task multiplicities, for the tasks that are not infinite-server.
void set_task_inf_probability(double p)
Probability that a server task is infinite-server rather than FCFS.
void set_seed(long long seed)
Reseed the single stream every draw comes from.
A random qn::Network<T> source, configured once and then drawn from.
void set_cclass_job_load(const std::string &load)
high, medium, low, or randomize: the population band of a closed class.
void set_model_name(const std::string &nm)
The name given to the generated model.
void set_seed(long long seed)
Reseed the single stream every draw comes from, as MATLAB rng(seed).
void set_topology(TopologyKind kind)
Pick one of the two shipped topology generators; validated like set_topology_fcn.
void set_distribution(const std::string &d)
Exp, Erlang, HyperExp, or randomize.
void set_multi_chain_cs(bool v)
Classes are partitioned into random chains and switching is confined to a chain, instead of the defau...
void set_sched_strat(const std::string &strat)
fcfs, ps, inf, lcfs, lcfspr, siro, sjf, ljf, sept, lept, or randomize.
void set_random_cs_nodes(bool v)
Each link gets a ClassSwitch node inserted on a coin flip.
qn::Network< T > generate(int num_queues, int num_delays, int num_oclass, int num_cclass)
The full form.
void set_routing_strat(const std::string &strat)
Probabilities, Random, or randomize.
void set_varying_service_rates(bool v)
Service means spread over 2^-6 .
void set_multi_server_queues(bool v)
Queues get 1..40 servers instead of exactly one.
LnSensTable< T > get_sensitivity_table(const sens::SensOptions &sopt)
Port of @SolverLN/getSensitivityTable: solve the ensemble, then concatenate each layer solver's own t...
Definition solver_ln.h:735
LnTranSolution get_tran_avg()
Port of @SolverLN/getTranAvg: the block-diagonal aggregate transient.
Definition solver_ln.h:712
std::size_t nlayers() const
Definition solver_ln.h:819
std::vector< LnCdf > get_cdf_respt()
Port of @SolverLN/getCdfRespT: the per-entry response-time distribution.
Definition solver_ln.h:669
LnSolution< T > get_ensemble_avg()
Port of getEnsembleAvg: run the iteration and aggregate onto LQN elements.
Definition solver_ln.h:600
const std::vector< qn::Layer< T > > & layers() const
Definition solver_ln.h:820
LqnStruct< T > build() const
Flatten into the struct SolverLN consumes.
const LqnModel< T > & model() const
The layered model solved by lqns or lqsim.
static bool is_stochastic_method(const std::string &method)
Only the lqsim methods draw random numbers.
static std::string version()
The version banner of the local binary, empty when there is none.
A layer network: everything a NetworkStruct holds, plus the LQN back-mapping.
Definition qn_layer.h:52
A network plus its refreshed NetworkStruct.
std::size_t nvars_of(std::size_t ind) const
Total local-variable width of node ind (1-based).
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 nof_nodes() const
std::map< std::pair< std::size_t, std::size_t >, Matrix< T > > P
P[(r,s)] is an (nnodes x nnodes) block; absent means all zero.
std::size_t phases_of(std::size_t ist, std::size_t r) const
sn.phases(i,r): the order of the process representation.
std::vector< Reward > reward
std::vector< std::size_t > stateful_nodes
1-based node indices, ascending
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< std::vector< bool > > disabled
std::map< std::size_t, CacheParam< T > > nodeparam
Cache parameters by 1-based NODE index; only Cache nodes have an entry.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::size_t > station_to_node
(nstations) 1-based node index
std::vector< Region > regions
A queueing network under construction.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
NetworkStruct< T > & raw_struct()
The struct WITHOUT refreshing it, for a caller that is still building.
RAII guard: opens a run on construction, closes it on scope exit.
static void reset()
Forget any open run (used after an interrupted solve).
static void set_verbose(VerboseLevel level)
Set the session verbosity.
A listening socket; one connection is served at a time.
Definition websocket.h:189
bool serve_one(Fn serve)
Accept one connection, complete the handshake, read ONE text message and hand it to serve; send what ...
Definition websocket.h:225
The CLI's argument vector, reachable in-process.
The keys a LINE solver accepts under options.config, in all four codebases.
Docker primitives for the backends that legitimately ship an image.
The SolverENV entry surface: a port of the analyzer selection that @@SolverENV/SolverENV....
Reader for the LINE model.json interchange of an ENVIRONMENT model into an env::Environment<T>.
The exception types the port throws.
Port of @@SolverFLD/getJacobian, all four of its outputs.
The fluid solver's outermost entry point: @@SolverFLD/runAnalyzer.m's method resolution over solver_f...
The log-driven half of SolverJMT: linkAndLog, parseLogs, parseTranState, parseTranRespT,...
Port of @@JMTIO: a refreshed NetworkStruct written out as a JMT .jsimg simulation model.
Read a JMT .jsim / .jsimg / .jsimw model into a qn::Network.
Random layered-queueing-network generation: the C++ twin of MATLAB @LayeredNetworkGenerator,...
The NATIVE LDES engine for LAYERED (LQN) models, the C++ twin of jline/solvers/ldes/handlers/Solver_s...
int dispatch(const Options &in, bool allow_server, bool no_args)
Everything main did, with the arguments already in hand.
int run_invocation(Options o)
Everything one invocation does once the arguments are in hand.
#define LINE_CLI_TABLE_LADDER(FN)
int find_solver_report(const Options &o)
--find-solver: which solvers and methods can analyze the model named by -f.
int run_generate(const Options &o)
--generate: draw a random model and print it, solving nothing.
std::string run_invocation_captured(Options o, int &rc)
Run one invocation with stdout captured, and return what it printed.
int run_server(const Options &base)
-p/--port: serve solve requests over a WebSocket, as LineWebSocketServer does.
Running progress log of a LINE solver run (the "solver console").
model.json with type: "LayeredNetwork" -> LqnStruct, via LqnBuilder.
.lqnx -> LqnStruct, a port of matlab/src/lang/layered/@LayeredNetwork/parseXML.m followed by ....
LqnModel -> .lqnx, a port of matlab/src/lang/layered/@LayeredNetwork/writeXML.m.
The flat-Network path of @@SolverLQNS: its qns methods, served by qnsolver of the RADS/LQNS distribut...
Port of @NetworkSolver/mapEnvApprox.m: the solver-agnostic random-environment approximation of a netw...
The stage solvers map_env_approx injects, one per runner that can be a caller.
Boundary marshalling for host bindings (MATLAB MEX, pybind11, the JSON CLI).
Classification of a solution method, as printed in the solver banner: "<accuracy>,...
mva::AvgResult< T > solver_ag_run_analyzer(const qn::NetworkStruct< T > &L, const AgOptions &opt)
SolverAG.runAnalyzer: the converged agents as mean measures.
Definition ag_dispatch.h:34
Matrix< T > sn_declared_marginal(const qn::NetworkStruct< T > &sn)
The (nstations x nclasses) per-class job counts of the model's OWN state.
Definition sn_state.h:85
bool nonmkv_enabled(const std::string &method)
Resolve options.config.nonmkv to NonmarkovOptions::enabled.
AutoEnvChoice auto_choose_env_solver(const std::string &method, AutoMode mode=AutoMode::HEUR)
The Environment arm of chooseSolverHeur / chooseSolverExact / chooseSolverSim.
std::string auto_find_solver_table(const std::vector< SolverCandidate > &rows)
The rows as an aligned text table, the form the CLI and a console caller want.
AutoLayeredChoice auto_choose_layered_solver(const std::string &method, bool has_cache_task, AutoMode mode=AutoMode::HEUR)
chooseLayeredSolver plus the LayeredNetwork arm of chooseAvgSolverHeur.
const char * auto_env_stage_solver(AutoEnv s)
The EnvOptions::stage_solver (and --stage-solver) name of a candidate ensemble.
const char * auto_solver_name(AutoSolver s)
AutoToken auto_resolve_token(const std::string &raw, const qn::NetworkStruct< T > &sn)
resolveMethodToken with its third arm: an intent, a family[.submethod], or else a BARE method name,...
std::vector< AutoSolver > auto_proposed_solvers(const qn::NetworkStruct< T > &sn, const std::string &method, AutoMode mode)
delegate's proposed order: the chosen solver, then every feasible candidate in slot order.
AutoChoice auto_choose_solver_mode(const qn::NetworkStruct< T > &sn, const std::string &method, AutoMode mode)
chooseSolver: the selection mode picks the ranking, and every mode but the two learned ones keeps the...
std::vector< SolverCandidate > auto_find_solver(const qn::NetworkStruct< T > &sn, const std::string &metric=std::string(), bool show_all=false)
SolverAUTO.findSolver: which solvers and solver methods can analyze this model, and for the ones that...
const char * auto_env_name(AutoEnv s)
const char * auto_layered_name(AutoLayered s)
The layered names are the CLI's own tokens, because that is what the choice is spent on: ln....
BaBounds< T > ba_bounds(const qn::NetworkStruct< T > &L, const BaOptions &opt)
Port of SolverBA.getBounds.
mva::AvgResult< T > solver_ba_run_analyzer(const qn::NetworkStruct< T > &L, const BaOptions &opt_in)
Port of @@SolverBA/runAnalyzer.m for the lang='matlab' path.
std::string resolve_method(const std::string &method)
Port of runAnalyzer's method aliases: default is the geometric upper bound, bare auto is the AUTO com...
int main_body(int argc, char **argv)
The binary's main, so line_cli_main.cpp stays ten lines.
Response run(const Request &req)
Run one invocation and return what it produced.
Matrix< T > solver_ctmc_sample(const NetworkStruct< T > &sn, const CtmcSamplePath< T > &path, std::size_t ind)
Port of @@SolverCTMC/sample: the walk restricted to ONE stateful node's local block.
Matrix< T > solver_ctmc_sample_aggr(const NetworkStruct< T > &sn, const CtmcSamplePath< T > &path, std::size_t ind)
Port of @@SolverCTMC/sampleAggr: one node's per-class counts over time.
std::vector< CdfCurve< T > > solver_ctmc_cdf_sys_respt(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of @@SolverCTMC/getCdfSysRespT.m: the per-chain SYSTEM response-time CDF, indexed by chain.
T solver_ctmc_jointaggr(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const NetState< T > &state)
Port of solver_ctmc_jointaggr: P(the network holds exactly these per-class counts),...
CtmcTranProb< T > ctmc_get_tran_prob_sys(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr)
Port of @@SolverCTMC/getTranProbSys.m: pi(t), labelled by the whole network state with its phases.
CtmcSamplePath< T > solver_ctmc_sample_sys(const NetworkStruct< T > &sn, const CtmcOptions &opt_in, std::size_t nevents, unsigned long seed=23000)
Port of @@SolverCTMC/sampleSys: a marked walk on the whole network state.
CtmcTranProb< T > ctmc_get_tran_prob_sys_aggr(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr)
Port of @@SolverCTMC/getTranProbSysAggr.m: pi(t), labelled by the network's per-(station,...
mva::AvgResult< T > solver_ctmc_avg_table(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const std::string &method)
Port of @@SolverCTMC/runAnalyzer.m's result assembly: solve, then apply the metric filter @@NetworkSo...
CtmcMddSolution< T > solver_ctmc_mdd_analyzer(const NetworkStruct< T > &sn, const CtmcOptions &opt, const mdd::MddMcdOptions &mcdopt=mdd::MddMcdOptions())
Solve with the mdd method.
CtmcGenerator< T > ctmc_get_infgen(const NetworkStruct< T > &sn, const CtmcSolution< T > &d)
@@SolverCTMC/getInfGen.m, a pure alias of getGenerator in the reference.
std::vector< T > solver_ctmc_margaggr(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const NetState< T > &state)
Port of solver_ctmc_margaggr: per STATION, P(that station holds exactly these per-class counts).
std::vector< CtmcSensRank< T > > solver_ctmc_sensitivity_ranking(const NetworkStruct< T > &sn, const CtmcOptions &opt, const std::vector< CtmcSensParam< T > > &params, const std::vector< T > &reward)
Port of @@SolverCTMC/getSensitivityRanking: rank parameters by influence.
std::vector< std::vector< CdfCurve< T > > > solver_ctmc_cdf_respt(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of @@SolverCTMC/getCdfRespT.m: the per-(station, class) response-time CDF, indexed [ist-1][r-1].
std::vector< T > solver_ctmc_marg(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const NetState< T > &state)
Port of solver_ctmc_marg: per STATION, P(that station is in exactly its local slice of state),...
std::vector< T > solver_ctmc_avg_reward(const NetworkStruct< T > &sn, const CtmcOptions &opt, std::vector< std::string > *names=nullptr)
Port of @@SolverCTMC/getAvgReward: the steady-state expected rewards.
CtmcStateSpace< T > ctmc_get_state_space(const NetworkStruct< T > &, const CtmcSolution< T > &d)
Port of @@SolverCTMC/getStateSpace.m.
CtmcTransient< T > solver_ctmc_transient_analyzer(const NetworkStruct< T > &sn_arg, const CtmcOptions &opt, const T &t0, const T &t1, const std::vector< T > &grid=std::vector< T >())
Port of solver_ctmc_transient_analyzer.m.
CtmcFirstPassageMoments< T > ctmc_firstpasst_moments(const NetworkStruct< T > &, const CtmcSolution< T > &d, const Matrix< double > &A, const Matrix< double > &B, std::size_t nmax, const Matrix< double > &C)
Port of @@SolverCTMC/getFirstPassTMoments.m: moments of order 1..nmax of the first passage time from ...
T solver_ctmc_joint(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const NetState< T > &state)
Port of solver_ctmc_joint: P(the network is in exactly state).
Matrix< T > ctmc_get_state_space_aggr(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of @@SolverCTMC/getStateSpaceAggr.m: the per-(station, class) job counts of every state,...
CtmcFirstPassage ctmc_cdf_firstpasst(const NetworkStruct< T > &, const CtmcSolution< T > &d, const Matrix< double > &A, const Matrix< double > &B, const Matrix< double > &C, std::vector< double > tset, const std::string &method="expm")
Port of @@SolverCTMC/getCdfFirstPassT.m: the distribution of the FIRST PASSAGE TIME from state set A ...
CtmcSolution< T > solver_ctmc_analyzer(const NetworkStruct< T > &sn_in, const CtmcOptions &opt)
Port of solver_ctmc_analyzer.m plus the fork-join wrapper of @@SolverCTMC/runAnalyzer....
CtmcAnySolution< T > solver_ctmc_analyzer_any(const NetworkStruct< T > &sn, const CtmcOptions &opt)
The entry point a caller who does not know which path a model needs should use: pick the WAITQ walk w...
CtmcTranProb< T > ctmc_get_tran_prob(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr, std::size_t ind)
Port of @@SolverCTMC/getTranProb.m: pi(t) over the whole chain, labelled by one node's local state.
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...
CtmcTranProb< T > ctmc_get_tran_prob_aggr(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr, std::size_t ind)
Port of @@SolverCTMC/getTranProbAggr.m: pi(t), labelled by one node's per-class job counts.
std::vector< std::vector< T > > solver_ctmc_tran_reward(const NetworkStruct< T > &sn, const CtmcOptions &opt, const T &t0, const T &t1, std::vector< T > *tout=nullptr, std::vector< std::string > *names=nullptr)
Port of @@SolverCTMC/getTranReward: E[r(X(t))] = sum_s pi_t(s) r(s).
CtmcReward< T > solver_ctmc_reward(const NetworkStruct< T > &sn, const CtmcOptions &opt, std::size_t tmax=1000)
Port of solver_ctmc_reward.m.
Matrix< T > solver_ctmc_sample_sys_aggr(const NetworkStruct< T > &sn, const CtmcSamplePath< T > &path)
Port of @@SolverCTMC/sampleSysAggr: the same walk, reported as per-(station, class) job counts rather...
EnvStatevecSolution< T > solver_env_statevec(Environment< T > &e, const EnvStatevecOptions< T > &o)
Solve in one call, for a caller with no use for the solver object.
EnvAnalyzerSolution< T > solver_env(Environment< T > &e, const EnvOptions &o)
SolverENV.init's analyzer selection: solve the environment with the coupling o.method names.
FluidKpTransient solver_fluid_tran_avg_var(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of @@SolverFLD/getTranAvgVar: the queue-length VARIANCE along the trajectory,...
Definition fluid_kp.h:1070
double aoi_cdf(const AoiMe &me, double t)
getCdfAoI: F(t) = 1 - S(t) with S the survival function of the age law.
Definition fluid_aoi.h:677
double fluid_prob_aggr(const qn::NetworkStruct< T > &sn, const FluidSolution &sol, std::size_t ist, double *logp_out=nullptr)
Port of @@SolverFLD/getProbAggr: the probability that station ist holds the marginal population of th...
AoiTopology aoi_is_aoi(const qn::NetworkStruct< T > &sn)
Port of aoi_is_aoi.m.
Definition fluid_aoi.h:102
std::string solver_fluid_export_odes(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, const std::string &notation="scalar", const std::string &model_name="model")
Port of @@SolverFLD/exportODEs.m, which hands options.method to solver_fluid_symodes UNRESOLVED: defa...
FluidJacobian fluid_jacobian(const FluidSymSystem &sys, const FluidSymbolicOptions &opt=FluidSymbolicOptions())
Jacobian, drift and equilibria of the mean-field vector field.
FluidSolution solver_fluid_run_analyzer(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, qn::NetworkStruct< T > *sn_out=nullptr, qn::NetworkStruct< T > *refreshed_out=nullptr, solvers::CacheMetrics< T > *cache_out=nullptr)
Port of @@SolverFLD/runAnalyzer.m: resolve the method, route to the function the reference routes to,...
std::vector< std::vector< FluidPassage > > solver_fluid_cdf_respt(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, std::size_t points=201)
Port of @@SolverFLD/getCdfRespT: the response-time law of every (station, class) pair,...
FluidSymSystem fluid_symodes(const qn::NetworkStruct< T > &sn, const std::string &method_in, double pstar, const std::vector< double > &init_sol)
Build the symbolic system of sn under opt.method.
std::vector< FluidTranPoint > solver_fluid_run_transient(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, std::size_t points=101)
-a tran / @@SolverFLD/getTranAvg with the method HONOURED, which is the one place the reference does ...
bool is_layered_json(const std::string &path)
True when a file is a LayeredNetwork model.json rather than an .lqnx.
std::string jsim_stage_stdin(const std::string &text)
Write a piped XML model document to a temporary file and return its path.
qn::Network< T > read_network_json(const std::string &path)
Parse a model.json file into a qn::Network<T>.
qn::Network< T > read_jsim(const std::string &path, const std::string &name=std::string())
Read a JSIM document into a Network.
lqn::LqnModel< T > read_lqnx_model(const std::string &path)
Read a .lqnx file into the model-level LqnModel, which write_lqnx accepts back.
Definition lqnx_io.h:39
lqn::LqnStruct< T > read_layered_model(const std::string &path)
Read a layered model from any of its interchanges: the LINE model.json, the LQNS ....
std::string jmt_write_jsim(const qn::NetworkStruct< T > &sn, const JmtWriteOptions &opt)
Port of @@JMTIO/writeJSIM.m: serialize sn as a JMT .jsimg document.
env::Environment< T > build_environment_from_json(const detail::json &root)
Build an env::Environment<T> from a parsed model.json envelope.
env::Environment< T > read_environment_json(const std::string &path)
Parse a model.json file into an env::Environment<T>.
qn::Network< T > build_network_from_json(const detail::json &root)
Build a qn::Network<T> from a parsed model.json envelope.
bool docker_daemon_available()
True if the Docker daemon is reachable.
detail::json network_json_envelope(const qn::NetworkStruct< T > &sn)
The complete model.json envelope: {format, version, model}.
const char * error_id(const Error &e)
Stable identifier for an error, for mexErrMsgIdAndTxt and for mapping to a host exception class.
Definition marshal.h:146
bool is_layered_json_text(const std::string &text)
True when a DOCUMENT ALREADY IN MEMORY is a LayeredNetwork model.json.
qn::Network< T > pnml_load(const std::string &path, const std::string &net_id=std::string())
Read one net of a PNML place/transition document into a Network.
Definition pnml.h:492
JmtReplication< T > jmt_transient_replications(const qn::NetworkStruct< T > &sn, const JmtOptions &opt)
Port of the transient ensemble of @@SolverJMT/runAnalyzer.m (default over a finite timespan).
Definition jmt_logs.h:912
JmtSysTrace< T > jmt_sample_sys_aggr(const qn::NetworkStruct< T > &sn, std::size_t num_events, const JmtOptions &opt)
Port of sampleSysAggr: every station's trajectory on one time grid.
Definition jmt_logs.h:501
JmtNodeTrace< T > jmt_sample_aggr(const qn::NetworkStruct< T > &sn, std::size_t node, std::size_t num_events, const JmtOptions &opt)
Port of sampleAggr: the queue-length trajectory of one node.
Definition jmt_logs.h:450
std::vector< std::string > jmt_list_valid_methods()
Port of SolverJMT.listValidMethods.
std::size_t jmt_transient_replication_count(const JmtOptions &opt, const std::string &what)
How many JSIM runs a transient estimate is over: the gate the two transient arms share.
Definition jmt_logs.h:677
JmtResult< T > solver_jmt_run_analyzer(const qn::NetworkStruct< T > &sn, const JmtOptions &opt_in)
Port of @@SolverJMT/runAnalyzer.m, the jsim and jmva arms.
std::map< std::pair< std::size_t, std::size_t >, std::vector< std::pair< double, double > > > jmt_get_cdf_resp_t(const qn::NetworkStruct< T > &sn, const JmtOptions &opt, bool seed_from_steady=true)
Port of getCdfRespT: the empirical response-time distribution per (station, class),...
Definition jmt_logs.h:573
std::string jmt_removed_method_refusal(const std::string &method)
The migration sentence for a method name SolverJMT no longer has; empty for any other name.
std::pair< std::vector< double >, std::vector< std::vector< double > > > jmt_get_tran_prob_aggr(const qn::NetworkStruct< T > &sn, std::size_t station, const JmtOptions &opt, std::vector< std::vector< double > > &states_out)
Port of getTranProbAggr: the transient distribution of one station's aggregate state,...
Definition jmt_logs.h:706
JmtProbAggr jmt_prob_aggr(const qn::NetworkStruct< T > &sn, const JmtOptions &opt, std::size_t target_station=0, const std::vector< double > &target=std::vector< double >())
Port of getProbAggr and getProbSysAggr, both off ONE instrumented run.
Definition jmt_logs.h:816
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:235
const char * event_to_text(EventType e)
Definition lang_types.h:141
LnColumn
A column of the shared layered average table, as line-cli prints it.
bool ln_defined(const lqn::LqnStruct< T > &lsn, const LnResult &r, std::size_t i, LnColumn c)
Does the element at i HAVE the quantity in column c?
std::vector< LnEntryCdf > ldes_ln_cdf_respt(const LnResult &r, std::size_t nentries)
The empirical response time CDF of every ENTRY, the getCdfRespTLN of the other codebases: one [F(t),...
solvers::FjMetrics< double > ldes_fj_metrics(const qn::NetworkStruct< T > &sn, const LdesResult &r)
ldesParseForkJoin: the engine's "forkJoin" rows mapped onto the Fork/Join pairs of sn by (fork name,...
engine::LnResult ldes_ln_engine_solve(const lqn::LqnStruct< T > &lsn, const LdesOptions &o)
Simulate a layered model in process.
LdesResult solver_ldes_text(const std::string &doc, const LdesOptions &o, const std::vector< std::string > &extra_flags=std::vector< std::string >())
Runs one LDES simulation on a model.json DOCUMENT and parses its result.
bool ldes_is_available()
True when this machine can run the engine at all, by either image.
Definition ldes_probe.h:253
std::string fmt(const char *f, double v)
bool in(const std::string &s, std::initializer_list< const char * > set)
const std::string * get(const Element *el, const std::string &key)
el.get(key), with Python's None as a null pointer.
Definition srvn_writer.h:62
std::string name(const Element *el, const std::string &key="name")
std::string srvn_file_to_lqnx(const std::string &path)
Translate the SRVN file path to .lqnx text, model name = its basename without extension.
LqnModel< T > read_srvn_model(const std::string &path)
Read an SRVN file (.in, or a .lqn that is not XML) into the intermediate LqnModel,...
std::string lqnx_to_string(const LqnModel< T > &m, const std::string &model_name=std::string("LQN"), bool use_abstract_names=false)
The .lqnx document as a STRING, for a caller with no file to write to – the CLI's model-generation mo...
Definition lqn_writer.h:666
bool is_srvn_path(const std::string &path)
Whether path should be read as SRVN: a .in file, or a .lqn file that is not XML.
mva::AvgResult< T > solve_network_run_analyzer(const qn::NetworkStruct< T > &L, const QnsOptions &opt)
Port of @@SolverLQNS/runAnalyzerNetwork.m and solver_qns.m: SolverLQNS on a flat Network.
const std::string & lqns_version()
The version banner of the local lqns, empty when there is none.
Definition lqns_probe.h:49
bool lqns_is_available()
True when lqns is installed AND is a release this port speaks.
Definition lqns_probe.h:69
bool qnsolver_is_available()
Port of SolverLQNS.hasQnsolver: a native qnsolver binary on the PATH.
std::vector< T > mam_percentiles_from_cdf(const RespTCdf< T > &cdf, const std::vector< double > &pcts)
Port of the CDF path of @@SolverMAM/getPerctRespT.m: linear interpolation of the response-time CDF at...
TranResult< T > solver_mam_get_tran_avg(const qn::NetworkStruct< T > &L, const MamOptions &opt_in)
Port of @@SolverMAM/getTranAvg.m: transient queue length, utilization and throughput.
std::vector< RespTCdf< T > > solver_mam_get_cdf_respt(const qn::NetworkStruct< T > &L, const MamOptions &opt)
@@SolverMAM/getCdfRespT.m: the response-time CDF per class.
mva::AvgResult< T > solver_mam_run_analyzer(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of @@SolverMAM/runAnalyzer.m for the lang='matlab' path: solve, then apply the metric filter @@N...
ProbTable< T > solver_mam_get_prob(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::size_t node, const mva::AvgResult< T > &avg)
@@SolverMAM/getProb.m: the joint (level, phase) table at a node.
std::vector< std::vector< T > > solver_mam_get_perct_respt(const qn::NetworkStruct< T > &L, const MamOptions &opt, const std::vector< double > &percentiles)
@@SolverMAM/getPerctRespT.m: response-time percentiles per class.
bool mam_has_fj_percentiles(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Whether getPerctRespT reads the FJ_codes table rather than inverting a CDF.
std::vector< T > solver_mam_get_prob_marg(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::size_t ist, std::size_t jobclass, const mva::AvgResult< T > &avg)
@@SolverMAM/getProbMarg.m: P(n jobs of one class) at a station.
qsys::BmapM1Result< T > solver_mam_get_mam_result(const qn::NetworkStruct< T > &L)
@@SolverMAM/getMAMResult.m: the M/G/1-type internals of a single queue.
const std::map< std::string, std::vector< std::string > > & table()
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
AggrResult< T > solver_mva_get_prob_aggr(const qn::NetworkStruct< T > &L, const AvgResult< T > &avg, std::size_t ist, const std::string &method="default")
T solver_mva_get_prob_norm_const_aggr(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of @@SolverMVA/getProbNormConstAggr.m: log G.
std::string resolve_method(const qn::NetworkStruct< T > &L, const std::string &method)
Port of SolverMVA.resolveMethod: the feature-driven default -> rqna upgrade for a bursty single-class...
MargResult< T > solver_mva_get_prob_marg(const qn::NetworkStruct< T > &L, const AvgResult< T > &avg, std::size_t ist, std::size_t r, const std::vector< long > &states, const std::string &method="default")
Port of @@SolverMVA/getProbMarg.m: P(n jobs of class r at station i) for the states in states (or the...
AggrResult< T > solver_mva_get_prob_sys_aggr(const qn::NetworkStruct< T > &L, const AvgResult< T > &avg, const std::string &method="default")
Port of @@SolverMVA/getProbSysAggr.m: the joint probability of the model's whole state across all sta...
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...
AvgResult< T > solver_mva_run_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt_in, const Matrix< T > &init_sol)
Port of @@SolverMVA/runAnalyzer.m for the lang='matlab' path: gate, solve, convert,...
NcCftpSolution< T > solver_nc_cftp(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const NcCftpOptions &cftpopt)
Solve with the cftp / cftp.approx method.
mva::AvgResult< T > solver_nc_run_analyzer(const qn::NetworkStruct< T > &L_in, const NcSolverOptions &opt_in)
T solver_nc_getprob_sys_marg(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const std::vector< int > &nvec, const std::string &engine="exact")
Port of @@SolverNC/getProbSysMarg.m.
T solver_nc_getprob_sys_aggr(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir)
Port of @@SolverNC/getProbSysAggr.m.
NcSolution< T > solver_nc_cftp_solution(const NcCftpSolution< T > &s)
A cftp solve in the shape every other SolverNC analyzer returns, so the runner's metric filter applie...
T solver_nc_joint(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir, double *lG_out)
Port of solver_nc_joint.m: the probability of the WHOLE system state.
std::vector< double > solver_nc_busyp(const qn::NetworkStruct< T > &sn, const std::vector< std::size_t > &subnet, const std::vector< std::size_t > &orders)
Mean busy period of order n for a set of stations.
NcQueueLengthDist< T > solver_nc_getprob_marg(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, std::size_t ist)
Port of @@SolverNC/getProbMarg.m: the TOTAL queue-length distribution.
NcMargResult< T > solver_nc_margaggr(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir, double lG)
Port of solver_nc_margaggr.m.
bool is_stochastic_method(const std::string &method)
Port of SolverNC.isStochasticMethod.
NcSolution< T > solver_nc_solve(const qn::NetworkStruct< T > &L_in, const NcSolverOptions &opt_in)
The gates, the multiserver conversion and the dispatch of @@SolverNC/runAnalyzer.m,...
CdfRespTResult< T > solver_nc_cdf_respt(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of @@SolverNC/getCdfRespT.m.
NcMargResult< T > solver_nc_marg(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir, double lG)
Port of solver_nc_marg.m: the DETAILED marginal, which weighs the station's internal arrangement and ...
std::vector< std::vector< int > > MarginalState
A state, as this port expresses it: nir[i][r] jobs of class r at station i.
mva::AvgResult< T > solver_nc_avg_table(const qn::NetworkStruct< T > &L_in, const NcSolution< T > &d, const std::string &origmethod)
Port of @@SolverNC/runAnalyzer.m for the lang='matlab' path: solve, then apply the metric filter @@Ne...
std::string solver_nc_cftp_supports(const qn::NetworkStruct< T > &sn)
The cftp model-class gate as a public predicate.
NcldMethod
The load-dependent methods this port dispatches.
Definition pfqn_ncld.h:87
std::vector< std::vector< int > > multichoose_rows(int n, int k)
All n-vectors of nonnegative integers summing to k, in MATLAB multichoose(n,k) order.
bool ncld_method_try(const std::string &s, NcldMethod &out)
Map a method name to its enum; false when the name is not one of them.
Definition pfqn_ncld.h:109
FeatureSet fluid_feature_set(const std::string &method)
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:131
FeatureSet nc_feature_set(const std::string &method)
SolverNC.getFeatureSet, 48 names, transcribed unchanged.
FeatureSet mva_feature_set(const std::string &raw_method)
SolverMVA.getFeatureSet, plus getMethodFeatureSet's per-method deltas.
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
nlohmann::json Json
Definition api_json.h:63
std::string api_render_readable(const Json &result)
Human-readable rendering of the object api_invoke returns, for -o readable.
ArithSpec parse_arith(const std::string &text)
Parse –arith.
Json api_invoke(const std::string &name, const std::string &arith, const Json &args)
Invoke one API function.
SensTable< T > solver_sensitivity_table(qn::NetworkStruct< T > &sn, const SensOptions &opt, bool exact_available, const std::function< mva::MvaSolution< T >()> &solve)
Build the sensitivity table of sn under solve.
void put(Matrix< double > &A, std::size_t r0, std::size_t c0, const Matrix< double > &S)
A(r0:, c0:) = S.
Definition mg1.h:927
double dot(const std::vector< double > &a, const std::vector< double > &b)
The inner product of a row vector with a column held as a vector.
Definition mg1.h:240
MapEnvDecision needs_map_env(const qn::FeatureSet &declared, const qn::NetworkStruct< T > &sn, const MapEnvConfig &cfg=MapEnvConfig())
needsMapEnv: does this model need the environment image, and would the image make it solvable?
std::vector< std::string > chain_class_labels(const qn::NetworkStruct< T > &sn)
(ClassA ClassB), the JobClasses column: which classes a chain holds.
std::vector< bool > node_table_hidden_classes(const line::qn::NetworkStruct< T > &sn)
The classes getAvgNodeTable omits: the auxiliary retrieval classes that Cache.setRetrievalSystem mint...
env::EnvStageAvgFn< double > nc_stage_fn(const nc::NcSolverOptions &opt)
NC stages, bound to the caller's own NcSolverOptions.
SysResult< T > solver_get_avg_sys(const qn::NetworkStruct< T > &sn, const mva::AvgResult< T > &r)
Port of @@NetworkSolver/getAvgSys.m.
FjMetrics< double > fj_metrics_to_double(const FjMetrics< T > &in)
The same metrics in double precision, for the facade and the CLI.
Definition fj_metrics.h:90
line::mva::AvgResult< T > avg_result_from_sim(const line::qn::NetworkStruct< T > &sn, const line::Matrix< double > &QN, const line::Matrix< double > &UN, const line::Matrix< double > &RN, const line::Matrix< double > &TN, const std::vector< double > &CN, const std::vector< double > &XN, const std::string &method)
The station AvgResult of a solver whose runner returns its own solution type, i.e.
NodeMetrics< T > node_metrics(const line::qn::NetworkStruct< T > &sn, const line::mva::AvgResult< T > &r)
bool config_value_ok(const ConfigKey &k, const char *value)
Whether value is admissible for k: always true when the key lists no values.
mva::AvgResult< T > run_avg(const qn::NetworkStruct< T > &sn, const std::string &solver, const qn::FeatureSet &declared, const MapEnvConfig &cfg, Run run, StageFn stage_fn, const std::string &requested_method="default")
The getAvg funnel: run the model, or its environment image when the ONLY thing in the way is a non-re...
Definition map_env.h:332
ChainResult< T > solver_get_avg_node_chain(const qn::NetworkStruct< T > &sn, const Matrix< T > &QNn, const Matrix< T > &UNn, const Matrix< T > &RNn, const Matrix< T > &WNn, const Matrix< T > &ANn, const Matrix< T > &TNn)
Port of @@NetworkSolver/getAvgNodeChain.m: the NODE table aggregated by chain.
ChainResult< T > solver_get_avg_chain(const qn::NetworkStruct< T > &sn, const mva::AvgResult< T > &r)
Port of @@NetworkSolver/getAvgChain.m: the station table aggregated by chain.
mva::AvgResult< T > map_env_approx(const qn::NetworkStruct< T > &sn, const std::string &solver, const MapEnvConfig &cfg, StageFn stage_fn, const std::string &requested_method="default")
mapEnvApprox: solve the model through the random-environment image of its non-renewal processes.
Definition map_env.h:186
const ConfigKey * config_key_for_flag(const char *flag)
The config key a line-cli flag carries; nullptr when the flag carries none.
std::vector< std::vector< DefaultCdfCurve > > solver_default_cdf_respt(const qn::NetworkStruct< T > &sn, const Matrix< T > &RN)
The NetworkSolver base-class response-time CDF: an exponential law with the right mean per (station,...
std::vector< std::string > chain_names(std::size_t nchains)
Chain1, Chain2, ... – the reference's own chain labels.
env::EnvStageAvgFn< double > fluid_stage_fn(const fluid::FluidOptions &opt)
Fluid stages, bound to the caller's own FluidOptions.
env::EnvStageAvgFn< double > mva_stage_fn(const mva::MvaOptions &opt)
MVA stages, bound to the caller's own MvaOptions.
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....
SsaSamplePath< T > ssa_sample_node(const qn::NetworkStruct< T > &sn, const SsaSerialRun< T > &r, std::size_t ind)
sample(node) and sampleAggr(node): the same trajectory, one node's block.
SsaProbReport solver_ssa_prob(const qn::NetworkStruct< T > &sn_arg, const SsaSerialOptions &opt_in)
The whole -a prob report over the model's DEFAULT INITIAL STATE, which is the state SolverCTMC's own ...
SsaSamplePath< T > ssa_sample_sys(const qn::NetworkStruct< T > &sn, const SsaSerialRun< T > &r)
sampleSys and sampleSysAggr: the trajectory itself.
SsaSolution solver_ssa(const qn::NetworkStruct< T > &sn, const SsaOptions &opt, std::vector< SsaCacheRatio > *cache=nullptr)
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...
void ssa_cdf_respt_refuse()
getCdfRespT: refused, and the refusal is the ANSWER rather than a gap.
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.
std::string sym_find_image()
const char *const SYM_DOCKER_IMAGE
Image serving the symbolic REST API.
Definition sym_engines.h:72
UqSolution< T > solver_uq_run_analyzer(qn::Network< T > &net, const UqStageSolver< T > &stage, const UqOptions &opt=UqOptions())
UQ.runAnalyzer as a free call: expand, solve every design point, aggregate.
Definition solver_uq.h:576
UqInterval< T > uq_interval_run(qn::Network< T > &net, const UqStageSolver< T > &stage, const UqOptions &opt=UqOptions())
getInterval from the model, running the ensemble ONLY when it is needed.
Definition solver_uq.h:976
std::vector< PriorSite< T > > uq_detect_priors(const qn::NetworkStruct< T > &sn)
UQ.detectPriors: find every Prior, in node order and then class order.
Definition solver_uq.h:198
UqStageSolver< T > uq_stage_solver(const UqStageOptions &o)
The stage solver named by o.solver.
std::string method_type(const std::string &solvername, const std::string &method)
Banner classification of a solution method.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
const char * arith_name(Arith a)
Definition registry.h:28
const std::vector< ApiEntry > & api_registry()
The registry is a function-local static, not a global object, so there is no static-initialization or...
Definition registry.h:49
Random queueing-network generation: the C++ twin of MATLAB @NetworkGenerator, the JAR jline....
Reader for the LINE model.json interchange (a Network model) into a qn::Network<T> built through the ...
Number-type abstraction for the templated API port.
PNML (ISO/IEC 15909-2) place/transition nets, read and written.
Coverage registry of the C++ port.
Ports of matlab/src/api/sn/sn_get_node_arvr_from_tput.m and sn_get_node_tput_from_tput....
Ports of matlab/src/api/sn/sn_get_state_aggr.m and sn_is_state_valid.m.
The SolverAUTO chooser: which solver a model is handed to.
The SolverBA class surface: @@SolverBA/runAnalyzer.m, listValidMethods, getBounds and getBoundsTable.
The CHAIN-level and SYSTEM-level views of a solved model.
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
Port of @@SolverCTMC/getCdfRespT.m and @@SolverCTMC/getCdfSysRespT.m: the exact distribution of the r...
The remaining @@SolverCTMC accessors: getGenerator / getInfGen, getStateSpace / getStateSpaceAggr and...
The mdd method of SolverCTMC: stationary analysis of a closed single-class network whose state space ...
The SolverCTMC probability family: solver_ctmc_joint, _jointaggr, _marg, _margaggr,...
Port of solver_ctmc_reward.m and the @@SolverCTMC reward surface (runRewardAnalyzer,...
Port of the @@SolverCTMC sampling surface: sample, sampleAggr, sampleSys, sampleSysAggr.
Port of @@SolverCTMC/getSensitivity and getSensitivityRanking: the parametric sensitivity of a steady...
Port of solver_ctmc_fcr_waitq.m: the reachability-built generator of a model whose finite capacity re...
The base-class fallback for a response-time CDF.
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
Port of SolverJMT, the Java Modelling Tools client.
Port of SolverLDES, the discrete-event simulator, as its C++ client.
SolverLN: layered decomposition of a layered queueing network.
SolverLQNS: the layered model solved by the external lqns / lqsim binaries.
The SolverMAM class surface: @@SolverMAM/runAnalyzer.m and the gates around it.
The state-probability half of the SolverMVA class surface.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
Port of @SolverNC/getAvgBusyPeriod.m and of the Python-native SolverNC.getAvgBusyPeriod: the mean bus...
Port of @@SolverNC/getCdfRespT.m, and of its two aliases getSjrnT and sjrnT.
The cftp and cftp.approx methods of SolverNC: stationary analysis of a closed single-class product-fo...
The state-probability half of the SolverNC class surface: ports of solver_nc_marg....
The SolverNC class surface: @@SolverNC/runAnalyzer.m and the gates around it.
The NODE-indexed view of a station result, behind getAvgNodeTable.
Performance sensitivities with respect to service rates.
The SolverSSA queries that are not the average table: getProb, getProbAggr, getProbSys,...
SRVN input format (the LQNS .in / .lqn text grammar) for layered models, the C++ twin of python/line_...
The SolverSSA entry surface: a port of @@SolverSSA/runAnalyzer.m's method whitelist,...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
std::string method
'default', 'inap', 'inaprc', 'inapinf' or the vestigial 'exact'.
Definition ag_types.h:51
int iter_max
SolverOptions('AG') lowers this from the global 1000 to 100.
Definition ag_types.h:57
double tol
Convergence tolerance of the reversed-rate fixed point.
Definition ag_types.h:54
std::size_t max_states
Truncation level of an OPEN agent's queue-length dimension.
Definition ag_types.h:65
std::string nonmkv
options.config.nonmkv: 'bernstein' (default) runs sn_nonmarkov_toph, 'none' skips it; any other name ...
Definition ag_types.h:76
What a ranking resolved to, and what it had to skip to get there.
std::string method
The method the choice was gated on: "" for the default, "exact".
std::vector< AutoSolver > skipped
Slots that outranked solver and have no engine in this port.
std::vector< AutoEnv > retry
delegate.m's proposed list: the chosen ensemble, then every candidate in slot order (MVA,...
std::vector< AutoEnv > skipped
std::vector< AutoLayered > skipped
resolveMethodToken, minus the unqualified-algorithm-name arm.
std::string submethod
the method handed to the family, "default" when bare
std::string family
empty when is_intent
Port of SolverBA.getBounds: the {lower,upper} bracket of a family.
std::vector< std::vector< bool > > keep
getBoundsTable's row filter, (M x K): whether the (station, class) pair earns a row.
Matrix< T > Qupper
(M x K), all-NaN on a side the family lacks
options.config.qrf_params, the blocking tables the BAS and RS-RD arms need.
std::vector< std::vector< int > > MM1
(MR x M) extended order
std::vector< std::vector< int > > MM
(MR x 2) blocking order
std::vector< std::vector< int > > BB
(MR x M) blocking state
int MR
number of blocking configurations
int f
finite-capacity queue, 1-based as in the reference
std::vector< int > ZZ
(MR) blocked count per config
std::vector< int > F
(M) capacity; empty takes sn.cap
The options SolverBA reads.
int level
options.level: the hierarchy level of pbh/bjbh/cbh/sib.
std::string method
Bound method; default resolves to gb.upper in the runner.
Matrix< double > qrf_alpha
options.config.qrf_alpha, the (nstations x N) load-dependent scaling of the two load-dependent QRF ar...
One invocation: the argument vector, plus what a pipe would have carried.
Definition cli_run.h:49
bool collect_json
Collect the JSON documents as they are emitted.
Definition cli_run.h:68
void * interrupt_user
Definition cli_run.h:100
int(*) interrupt(void *user)
Polled between analyses and at each document emission; non-zero aborts.
Definition cli_run.h:99
std::vector< std::string > argv
The argument vector WITHOUT argv[0], e.g.
Definition cli_run.h:55
std::string stdin_text
The model document, as if piped to the binary's stdin.
Definition cli_run.h:64
bool capture_stderr
Redirect fd 2 for the duration of the call into Response::diagnostics.
Definition cli_run.h:72
bool capture_stdout
Redirect fd 1 for the duration of the call into Response::text.
Definition cli_run.h:70
bool allow_server
Permit -p, which serves until its request budget is exhausted.
Definition cli_run.h:81
What one invocation produced.
Definition cli_run.h:104
std::string line_error_id
The cross-codebase identifier of error, LINE:<category>:<kind> or the unclassified LINE:Error (_kb/17...
Definition cli_run.h:147
int exit_code
The CLI's own exit status: 0 ok, 2 line::Error, 3 other.
Definition cli_run.h:106
std::string error_id
line::io::error_id's stable identifier for error.
Definition cli_run.h:140
std::vector< std::string > documents
Each JSON document the run emitted, in emission order, AS EMITTED.
Definition cli_run.h:124
std::string error
Empty on success; the what() of the exception otherwise.
Definition cli_run.h:132
std::string text
Everything written to fd 1, with the collected documents still in it.
Definition cli_run.h:127
std::string diagnostics
Everything written to fd 2: warnings, and the failure message.
Definition cli_run.h:129
A CTMC solve routed to whichever path the model's region rules require.
std::vector< T > parked
mean parked jobs per class; empty off the WAITQ path
The answer of @@SolverCTMC/getFirstPassTMoments.m.
std::vector< T > p
(nstates) probability of entering B (before C), one row per state
Matrix< T > mall
(nstates x nmax), one row per starting state
std::vector< std::size_t > source
resolved 0-based rows; empty = conditional stationary
std::vector< std::size_t > target
resolved 0-based rows
std::vector< std::size_t > taboo
resolved 0-based rows of C minus B; empty = none
std::vector< T > m
(nmax) moments for a passage started uniformly in A
The answer of @@SolverCTMC/getCdfFirstPassT.m: the [F(t), t] curve with its grid, density and resolve...
[infGen, eventFilt, ev] of @@SolverCTMC/getGenerator.m.
std::vector< std::vector< Matrix< T > > > preempt_filt
std::vector< Matrix< T > > filt
eventFilt: filt[a] holds only what synchronization sync[a] contributed, so sum_a filt[a] is the off-d...
std::vector< Sync< T > > sync
ev, the reference's sn.sync
std::vector< std::vector< Matrix< T > > > start_filt
The DERIVED START/PREEMPT filtrations, indexed [station-1][class-1].
Matrix< T > Q
the infinitesimal generator
What one mdd solve produces beside the means, i.e.
long long num_states
|S|, counted in the diagram without ever listing a state.
int iters
Coupled fixed-point sweeps performed.
bool no_aggregation
True certifies the answer is exact structurally; see the file header.
std::string encoding
Which local encoding was picked, "np" or "ps".
std::vector< std::size_t > level_sizes
|M_k| per paper level; their sum is what the diagram actually holds.
The SolverCTMC knobs this port honours.
bool force
options.force: downgrade the memory pre-gate's refusal to a warning.
std::vector< std::vector< std::size_t > > cutoff_mat
options.cutoff AS A (station x class) MATRIX, or empty.
double fau_delta
options.config.fau_delta: occupancy below which a state is dropped.
double cutoff
< 0 = not given
double timestep
options.timestep: the FIXED OUTPUT STEP of a transient analysis.
std::string nonmkv
options.config.nonmkv: 'bernstein' (default) runs sn_nonmarkov_toph, 'none' skips it; any other name ...
std::string transient_method
options.config.transient_method: "ode" (the default) integrates the forward equation,...
std::vector< CtmcRateSched > rate_sched
options.config.rate_sched: the TIME-INHOMOGENEOUS transient.
std::size_t ctmc_tv_ngrid
options.config.ctmc_tv_ngrid: uniform grid size of the rate_sched propagator.
double fau_epsilon
options.config.fau_epsilon: total probability mass the whole grid may discard under "fau".
bool keep_filtration
Keep the per-synchronization EVENT FILTRATION alongside Q.
One entry of options.config.rate_sched: the rate of (station, class) follows the piecewise-linear sch...
std::vector< double > rates
std::vector< double > tgrid
What the reward analyzer produces, per declared reward.
std::vector< Matrix< T > > V
V[r] is (Tmax+1 x nstates).
std::vector< T > t
iteration index / q
Matrix< T > state_space_aggr
the rows the reward saw
std::vector< std::string > names
One sampled trajectory of the chain.
std::vector< std::size_t > event
synchronization that fired to LEAVE it
std::vector< std::size_t > state
0-based index into chain.space
std::vector< T > t
time at which each state was ENTERED
The scalar parameter a sensitivity is taken with respect to.
std::function< void(NetworkStruct< T > &, double)> set
Apply theta to a COPY of the struct; the original is never mutated.
Everything one CTMC solve produces.
std::string warning
Set when the chain is a reducible mixture solved from an invented seed.
std::vector< std::size_t > cutoff
the per-class cutoff actually used
std::vector< T > pi
stationary distribution over chain.space
[stateSpace, localStateSpace] of @@SolverCTMC/getStateSpace.m.
std::vector< std::size_t > node_width
column width of each stateful node's block
std::vector< Matrix< T > > local
localStateSpace{f}: one matrix per stateful node, its DISTINCT local rows in first-appearance order.
Matrix< T > flat
the blocks concatenated, as MATLAB returns them
The time-dependent answer of one getTranProb* query.
Matrix< T > pit
(ntimes x nstates) occupancy over the solved chain
std::vector< T > t
The reference returns Pi_t = [t, pi_t], one matrix with time glued on as column 1.
Matrix< T > labels
(nstates x width) the state descriptor the query asked for
What one transient CTMC solve produces.
What the ENV entry reports: the environment-blended metrics, and the whole result of whichever coupli...
std::string method
What ran: meanfield, statevec, or the limit avg / dec.
EnvLimitSolution limit
Populated on the closed-form limit path (avg, dec, occupation); limit.occupation / limit....
EnvStatevecSolution< T > statevec
Populated on the state-vector path.
bool converged
True on the limit path: a closed form has converged by construction.
solvers::CacheMetrics< T > cache
The environment-blended cache surface, whichever coupling produced it.
int iterations
ZERO ON THE LIMIT PATH, and that is the answer rather than a gap: a limit reads the environment once ...
std::vector< double > occupation
occupation only (getStageOccupation): the expected time spent in each stage over the horizon,...
std::vector< double > occupation_weight
Options of SolverENV.
Definition solver_env.h:120
std::string method
The inter-stage coupling: meanfield is the reference's default.
Definition solver_env.h:143
std::vector< double > env_init
Method occupation: options.config.env_init, the initial probability of each stage.
Definition solver_env.h:167
std::string stage_solver
Which solver runs each FLAT stage: fluid (the fluid transient), ctmc (the enumerated chain),...
Definition solver_env.h:137
fluid::FluidOptions stage
Options handed to each stage solver.
Definition solver_env.h:147
double stage_cutoff
options.cutoff of a CTMC stage, read only when stage_solver is ctmc.
Definition solver_env.h:152
double timespan_end
options.timespan(2) of the inner solver: the transient horizon.
Definition solver_env.h:154
double env_timespan_start
Method occupation: SolverENV's OWN options.timespan, [t0, t1].
Definition solver_env.h:161
std::size_t tran_points
Points on a UNIFORM transient grid, used only where stage_grid declines to build one (a stage whose h...
Definition solver_env.h:180
std::vector< double > g
Definition fluid_aoi.h:255
std::vector< double > h
Definition fluid_aoi.h:257
Matrix< double > A
Definition fluid_aoi.h:256
std::string system_type
"bufferless" or "singlebuffer"
Definition fluid_aoi.h:265
What the AoI gate found, when it matches.
Definition fluid_aoi.h:84
The four outputs of @@SolverFLD/getJacobian.
std::string engine
sage or local, whichever produced J
std::vector< std::string > rhs
the drift, one expression per variable
std::vector< std::vector< std::string > > J
J[i][j] = d f_i / d x_j.
bool has_equilibria
the backend answered the equilibria request
std::vector< std::map< std::string, std::string > > equilibria
Nonnegative solutions of f(x) = 0 together with the conserved invariants (fluid_conservation_constrai...
std::vector< std::string > vars
state variable names
The transient the covariance equation produces, i.e.
Definition fluid_kp.h:120
Matrix< double > Sigma
state-level covariance, on range(D)
Matrix< double > QStd
per station and class queue-length variance
Controls, defaulting to SolverOptions('Fluid') in the reference.
double pstar
exponent of the 'pnorm' smoothing
double iter_tol
>0 stops early when the moved-mass ratio falls below it; 0 runs to iter_max, as the reference does
double tol
absolute and relative tolerance handed to the integrator
std::string fluid_boundary
options.config.fluid_boundary: how the Petri dae route reports a fluid place level held at a bound,...
std::string nonmkv
options.config.nonmkv: 'bernstein' (default) runs sn_nonmarkov_toph, 'none' skips it; any other name ...
std::size_t iter_max
cap on outer integrations
bool pstar_set
Opt in to the p-norm under matrix/default too, which is what options.config.pstar does in MATLAB,...
What the analyzer returns, in the same shape as the MVA solver's result.
bool has_moments
result.solverSpecific.moments: set only by minnormal and refined.
std::vector< double > XN
bool has_aoi
result.solverSpecific.aoiResults: set only by the AoI branch of mfq, where the age laws,...
FluidMomentReport moments
std::string warning
The reference's line_warning text, verbatim, empty when it did not warn.
std::vector< double > xvec
the converged fluid state
solvers::FjMetrics< double > fj
self.result.FJ, set by the fork-join arm (fluid_fork_join_run); empty otherwise.
std::vector< double > CN
The symbolic system, in whichever of the two forms the method implies.
Backend selection, mirroring options.config.symbolic and its timeout.
bool equilibria
The reference's nargout >= 4: ask the backend to solve f(x) = 0.
std::string backend
auto to search, a URL, an image name, or none to stay local.
A closed interval the generator samples from.
The simulation controls the JSIM header carries, MATLAB's JMTIO properties.
Definition jmt_writer.h:69
std::string log_path
model.getLogPath, the logPath attribute
Definition jmt_writer.h:71
std::string file_name
base name; the header echoes it plus .jsimg
Definition jmt_writer.h:70
The per-class queue-length trajectory of one node, plus its event stream.
Definition jmt_logs.h:227
The options of one JMT solve, SolverOptions('JMT') restricted to what is read.
Definition solver_jmt.h:121
int replications
options.config.replications: the independent JSIM runs the transient ensemble of default averages ove...
Definition solver_jmt.h:137
std::string method
default | jsim | jmva | jmva.<alg>
Definition solver_jmt.h:122
bool keep
keep the scratch directory after the solve
Definition solver_jmt.h:125
double samples
samples per measure; raised to 5000 below
Definition solver_jmt.h:123
What jmt_prob_aggr reports: the system probability and the per-station ones.
Definition jmt_logs.h:779
std::vector< bool > station_seen
the same, per station
Definition jmt_logs.h:783
bool sys_seen
whether the joint state occurred at all
Definition jmt_logs.h:782
double sys
P(the whole network is in the declared state).
Definition jmt_logs.h:780
std::vector< double > station
P(station i holds its declared per-class counts).
Definition jmt_logs.h:781
Transient averages over independent replications, on one time grid.
Definition jmt_logs.h:880
std::vector< double > t
Definition jmt_logs.h:881
std::vector< std::vector< std::vector< double > > > UNt
Definition jmt_logs.h:884
std::size_t valid
Replications that produced a usable trajectory.
Definition jmt_logs.h:887
std::vector< std::vector< std::vector< double > > > TNt
Definition jmt_logs.h:885
std::vector< std::vector< std::vector< double > > > QNt
QNt[ist-1][r] over t.
Definition jmt_logs.h:883
The result of a JMT solve: the shared AvgResult plus what only JMT reports.
Definition solver_jmt.h:759
std::map< std::size_t, std::vector< T > > cache_hit_prob
Per Cache node (1-based node index), the per-class hit probability.
Definition solver_jmt.h:768
mva::AvgResult< T > avg
Definition solver_jmt.h:760
Matrix< T > ACI
confidence half-widths, empty when disabled
Definition solver_jmt.h:761
Matrix< T > DropRateNfcr
(nregions x nclasses) carried and lost rate
Definition solver_jmt.h:763
The system trajectory: one per-class block per station, on a common grid.
Definition jmt_logs.h:483
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double CoarseTol
Definition lang_types.h:766
The knobs of one LDES run.
double warmupfrac
–warmupfrac, only for tranfilter=fixed
bool verbose
echo the resolved command line before running it
double cnvgtol
–cnvgtol
std::string rest_url
Base URL of an LDES REST server (the imperialqore/ldes container).
long seed
–seed; -1 requests a random stream
bool slotted
–slotted, run on the slot lattice
bool cnvgon
–cnvgon
std::string cimethod
–cimethod: obm | bm | spectral | none
double slot_length
–slotlength
int replications
–replications; 0 = not given (one path)
std::size_t events
0 = not given; overrides samples when set
std::string tranfilter
–tranfilter: mser5 | fixed | none
std::vector< double > init_sol
–initsol, the warm-start placement as a STATION-MAJOR vector [st0_cl0, st0_cl1, .....
int numthreads
–numthreads; 0 = not given
std::size_t samples
-s, service-completion budget
double timeout
–maxtime, a COOPERATIVE wall-clock budget the event loop polls.
bool has_timespan
true when [t0,t1] was set: a TRANSIENT run
One ldes-result document, parsed.
std::vector< std::string > fj_forks
Fork and Join node NAMES of each pair, ordered by fork node then join node; empty without a pair.
std::map< std::string, LdesCacheMetrics > cache_metrics
Per-cache metrics, keyed by the Cache NODE name.
Matrix< double > FJTput
getAvgFJ, FJQLen = FJTput * FJRespT
Matrix< double > TNCI
Matrix< double > WNfcr
std::vector< std::vector< Matrix< double > > > QNt
[STATION][class] -> (npoints x 2), columns [value, time].
Matrix< double > XN
(1 x nclasses), per-class visits and system tput
std::vector< std::vector< std::vector< double > > > respTimeSamples
[station][class] -> the per-job response times the engine recorded.
Matrix< double > FJQLen
Matrix< double > UNfcr
Matrix< double > FJRespT
Matrix< double > WeightNfcr
Matrix< double > QNfcr
Matrix< double > UN
Matrix< double > DropRateNfcr
Matrix< double > histogram_space
The exact joint-state residence-time histogram (–export-histogram).
std::string stopping_reason
convergence | max_events | max_sim_events | max_time.
std::vector< std::vector< Matrix< double > > > UNt
std::vector< std::string > class_names
Matrix< double > TN
Matrix< double > UNCI
Matrix< double > RN
std::string engine
"native" or "jar": which runner produced these numbers.
Matrix< double > QNCI
Matrix< double > CN
Matrix< double > RNfcr
Matrix< double > WN
Matrix< double > AN
long long total_simulated_events
Matrix< double > MemOccNfcr
Matrix< double > TNfcr
std::vector< std::string > station_names
Matrix< double > DropRateJoin
quorum-Join sibling drops, Join rows only
std::vector< std::string > fj_joins
bool timed_out
True when the HARD subprocess bound fired, not the cooperative one.
Matrix< double > histogram_time
Matrix< double > RNCI
std::vector< double > t
the time vector, empty on a steady-state run
Matrix< double > WNCI
Matrix< double > QN
std::vector< std::vector< Matrix< double > > > TNt
Matrix< double > ANfcr
Matrix< double > ANCI
The layered result: per element, the mean measures.
Matrix< double > WLN
Residence time per element, the ResidT column: the time an activity holds ITS HOST PROCESSOR per visi...
std::vector< std::vector< double > > entry_resp_samples
Every per-request ENTRY response time observed, one vector per entry in LOCAL index space (0....
double simulated_time
Length of the MEASURED span, from the end of warmup to the end of the run.
long long completions
Reference-task cycles completed in the measured span.
Options of SolverLN.
Definition solver_ln.h:275
getSensitivityTable of the ensemble: the layer tables under a Layer column.
Definition solver_ln.h:485
std::vector< Row > rows
Definition solver_ln.h:490
std::string method
The summary label: the common branch, or "mixed" when they differ.
Definition solver_ln.h:494
The LQN-level answer, indexed by element 1..nidx.
Definition solver_ln.h:420
std::vector< T > WN
Definition solver_ln.h:421
std::vector< bool > defined_W
Definition solver_ln.h:422
std::vector< bool > defined_U
Definition solver_ln.h:422
std::string warning
A warning the analysis raised, verbatim, empty when it raised none (method='nlp' off a root).
Definition solver_ln.h:433
std::vector< T > QN
Definition solver_ln.h:421
std::vector< bool > defined_R
Definition solver_ln.h:422
std::vector< T > RN
Definition solver_ln.h:421
std::vector< bool > defined_Q
Definition solver_ln.h:422
bool is_bound
True when the numbers are a BOUND (method = mw.upper / mw.lower) rather than the fixed point.
Definition solver_ln.h:431
std::vector< T > UN
Definition solver_ln.h:421
std::vector< bool > defined_T
Definition solver_ln.h:422
std::vector< T > TN
Definition solver_ln.h:421
The layered transient: one block per layer, plus how it was produced.
Definition solver_ln.h:476
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:411
std::vector< LqnElement > type
(nidx+1)
Definition lqn_struct.h:214
std::vector< bool > iscache
(tshift+ntasks+1)
Definition lqn_struct.h:267
std::size_t nentries
Definition lqn_struct.h:209
std::vector< std::string > names
(nidx+1) declared name
Definition lqn_struct.h:212
std::vector< bool > isref
(tshift+ntasks+1)
Definition lqn_struct.h:266
Knobs of the wrapper, the subset of SolverOptions that reaches lqns.
Definition solver_lqns.h:81
The six measures, on the element index space, with a defined mask each.
std::vector< bool > defined_U
std::vector< T > UN
std::vector< bool > defined_W
std::vector< bool > defined_Q
std::vector< T > RN
std::vector< bool > defined_R
std::vector< T > TN
std::vector< T > QN
std::vector< bool > defined_T
std::vector< T > WN
SolverLQNS.defaultOptions on a flat Network plus the knobs the JMVA document carries.
int timeout
Seconds before a hung qnsolver is killed; not positive waits forever.
bool keep
options.keep: leave the scratch directory behind, to inspect what was sent.
std::string multiserver
options.config.multiserver.
std::string method
default, qns or qns.NAME; see qns_methods().
The options SolverMAM reads.
Definition mam_types.h:30
std::size_t mapmsg_quantization
options.config.mapmsg_quantization: the number of regimes the MAP/M/s+G reneging analyzer quantizes t...
Definition mam_types.h:128
int iter_max
SolverOptions('MAM') lowers this from the global 1000 to 100.
Definition mam_types.h:34
double timespan_start
options.timespan: the transient horizon.
Definition mam_types.h:142
std::string timescale
options.config.timescale: "auto", "discrete" or "continuous".
Definition mam_types.h:134
std::string nonmkv
options.config.nonmkv: 'bernstein' (default) runs sn_nonmarkov_toph, 'none' skips it; any other name ...
Definition mam_types.h:61
std::size_t fj_accuracy
options.config.fj_accuracy: the FJ_codes truncation C of the queue-length DIFFERENCE between the two ...
Definition mam_types.h:109
std::string method
Definition mam_types.h:31
std::size_t cutoff
options.cutoff: the level truncation getProb / getProbMarg use for an OPEN model, where the queue len...
Definition mam_types.h:81
std::string fj_tmode
options.config.fj_tmode: which route computeT.m takes to the T matrix, 'NARE' (Riccati,...
Definition mam_types.h:115
std::size_t sync_phases
options.config.sync_phases: the synchronous phase expansion of the 'sync.faster' / 'sync....
Definition mam_types.h:122
double slotlength
options.config.slotlength: the slot in model time units.
Definition mam_types.h:136
The joint (level, phase) table getProb returns: rows levels, cols phases.
What getTranAvg returns: queue length, utilization and throughput curves.
Knobs of the level iteration in mdd_mcd.
Definition mdd_types.h:212
double tol
Convergence tolerance on the level marginals.
Definition mdd_types.h:214
int maxiter
Maximum coupled sweeps before the iteration is declared non-convergent.
Definition mdd_types.h:216
Port of @@SolverMVA/getProbAggr.m: P(n1 jobs of class 1, n2 of class 2, ...) at station ist for the m...
The metrics getAvg returns, after filtering.
std::shared_ptr< qn::NetworkStruct< T > > refreshed_struct
The struct whose cache self-switch carries the CONVERGED hit/miss split, filled by the cacheqn branch...
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
std::string warning
The reference's own warning text, verbatim, empty when it did not warn.
Matrix< T > UN
utilization
solvers::FjMetrics< T > fj
self.result.FJ: the fork-join section metrics FJQLen, FJRespT and FJTput, filled by the solvers the f...
std::vector< T > listcost
(h) mean storage cost held by each cache list, K_j = sum_i sigma_i pi_ij, filled only by the NC cache...
std::optional< double > lognormconst
@@SolverNC/getProbNormConstAggr, i.e.
std::optional< bool > converged
Whether the fixed point met its tolerance, empty when the handler reports none.
Matrix< T > WN
residence time, per job
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
solvers::CacheMetrics< T > cache
What the cache branches observed, EMPTY on a model with no Cache node and on every solver that does n...
Matrix< T > AN
arrival rate
A marginal distribution and its logarithm, over the states asked for.
std::vector< T > logP
The options SolverMVA reads.
Definition mva_types.h:32
std::string multiserver
Definition mva_types.h:37
std::string method
Definition mva_types.h:33
std::string fork_join
options.config.fork_join: which fork-join arm the fixed point takes.
Definition mva_types.h:52
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:97
std::vector< T > X
Definition mva_types.h:99
std::vector< T > C
Definition mva_types.h:99
The response-time distributions, station by class.
std::vector< std::vector< Matrix< T > > > RD
std::string warning
Non-empty when the reference WARNS AND RETURNS EMPTY rather than computing: today only "applies only ...
std::vector< T > tset
the shared evaluation grid
The knobs of one perfect-sampling run.
std::size_t samples
Number of iid stationary draws, SolverOptions('NC').samples by default.
unsigned long seed
Stream seed, so a row is reproducible within this port.
What one cftp solve produces beside the means.
std::vector< long > horizon
(samples) per-draw coalescence horizon, or the mixing steps of M_A.
std::vector< std::vector< int > > distinct_states
The distinct sampled states, aligned with paggr.
What the marginal analyzers return: one probability per station.
std::vector< T > P
(M) probability that station i holds its given vector
The [Q,U,R,T,C,X,lG] of the reference, plus the algorithm that ran.
Definition nc_types.h:113
std::string actualmethod
Definition nc_types.h:115
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
double slotlength
options.config.slotlength, the slot length in model time units.
Definition nc_types.h:108
std::string multiserver
options.config.multiserver: how a finite multiserver station is represented.
Definition nc_types.h:63
std::string fork_join
options.config.fork_join: which fork-join arm the shared fixed point takes on a model with a Fork.
Definition nc_types.h:51
double tol
options.tol
Definition nc_types.h:35
std::size_t samples
options.samples, read by the estimators
Definition nc_types.h:64
unsigned long seed
options.seed, read by the estimators
Definition nc_types.h:65
bool slotted
options.config.slotted.
Definition nc_types.h:106
std::string cdf_algorithm
options.config.algorithm for getCdfRespT: 'exact' selects pfqn_stdf, 'rd' the heuristic pfqn_stdf_heu...
Definition nc_types.h:90
double iter_tol
options.iter_tol, the eta stopping test
Definition nc_types.h:36
What State.toMarginal returns for one station and one state row.
Definition state.h:52
std::vector< T > nir
jobs per class
Definition state.h:54
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2734
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2735
Everything qsys_bmapm1 returns, mirroring the MATLAB result struct.
Definition qsys_bmapm1.h:62
T q
uniformization constant actually used
Definition qsys_bmapm1.h:66
T drift
stable iff strictly negative
Definition qsys_bmapm1.h:72
T pi0
probability the system is empty
Definition qsys_bmapm1.h:75
Matrix< T > levelProb
level probabilities, row n = pi_n
Definition qsys_bmapm1.h:74
T lambda
mean arrival rate, theta (sum_k k D_k) e
Definition qsys_bmapm1.h:64
std::vector< T > theta
stationary vector of the BMAP phase process
Definition qsys_bmapm1.h:63
T rho
offered load lambda/mu
Definition qsys_bmapm1.h:65
double decayRate
measured pi_(n+1)/pi_n; NaN when unmeasurable
Definition qsys_bmapm1.h:73
std::vector< T > alpha
stationary vector of A
Definition qsys_bmapm1.h:70
The name-value contract of getSensitivityTable.
bool simulation
True when the callback is a simulator, which widens the default step.
double step
Relative step of the rate perturbation; negative selects the default.
std::string scheme
forward | central
std::string method
auto | exact | fd
One (station, class) row of the table.
What the table carries, plus the branch that produced it.
std::vector< SensRow< T > > rows
std::string method
"exact" or "fd", the branch actually taken
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 > delayedhitqlen
(n) mean secondary requests waiting on the in-flight fetch of each item, and the same including the r...
std::vector< double > itemcap
(h) capacity of each list
std::vector< double > itemsize
(n) storage cost per item, EMPTY without setItemSizes
std::vector< T > hitprob
(K) TRUE hit fraction, EMPTY = not computed
std::vector< T > delayedprob
(K) delayed-hit fraction, EMPTY off a retrieval system
Matrix< T > hitproblist
(K x h) per-list hit fraction, EMPTY = not computed
std::size_t node
1-based node index of the Cache
std::vector< T > latency
(K) expected latency per read request (hits wait 0), EMPTY = not computed
std::vector< T > listcost
(h) mean storage cost held by each list
Matrix< T > itemprob
(n x h+1), column 0 = miss; EMPTY = not computed
std::vector< T > delayedhitqlenfull
std::string name
The Cache node's NAME, which is what a cross-language payload must key on.
The station- or node-level table aggregated by chain.
Matrix< T > TN
(rows x nchains), rows = stations or nodes
One options.config key; an empty placement means "not implemented there".
const char * values
comma-separated admissible values; empty when any string is taken
self.result.FJ: one row per Fork/Join pair, one column per class.
Definition fj_metrics.h:43
std::vector< std::size_t > forks
1-based fork node of each pair
Definition fj_metrics.h:44
Matrix< T > Tput
FJTput, P x R.
Definition fj_metrics.h:48
bool empty() const
No pair to report: a model without Fork/Join, or a solver that does not compute them.
Definition fj_metrics.h:52
Matrix< T > QLen
FJQLen, P x R.
Definition fj_metrics.h:46
std::size_t npairs() const
Definition fj_metrics.h:53
std::vector< std::size_t > joins
1-based join node of each pair
Definition fj_metrics.h:45
Matrix< T > RespT
FJRespT, P x R.
Definition fj_metrics.h:47
The caller-facing map_env knobs, options.config.map_env and friends.
std::size_t max_stages
0 = the transform's own default cap
The station table scattered to the NODE index space, plus the two flow columns the reference recomput...
@@NetworkSolver/getAvgSys: one response time and one throughput per chain.
std::vector< T > XN
(nchains) system throughput at the reference station
std::vector< T > CN
(nchains) system response time, i.e. the cycle time
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:71
std::size_t samples
Reaction firings to simulate; options.samples in the reference.
Definition ssa_types.h:75
double warmupfrac
options.config.warmupfrac: the leading fraction of the path discarded before the means are taken.
Definition ssa_types.h:96
std::string method
default and nrm both select the Next Reaction Method here.
Definition ssa_types.h:73
unsigned long seed
options.seed; LINE's own default is 23000.
Definition ssa_types.h:77
double cutoff
options.cutoff: the bound on an open class's jobs at a station, which the serial and replicated engin...
Definition ssa_types.h:114
The four probabilities -a prob reports, over one requested state.
SsaProbResult sys_aggr
getProbSys, getProbSysAggr
std::vector< SsaProbResult > aggr
getProb, getProbAggr, per station
std::vector< SsaProbResult > marg
One trajectory, in the shape the reference's sampleSys returns it.
std::vector< double > t
the event times, increasing
Matrix< T > aggr
the same, as per-(stateful, class) counts
std::vector< std::size_t > event
which synchronization fired
Matrix< T > state
per event: the state OCCUPIED until then
The serial engine's knobs: SsaOptions plus the three the serial path reads and the NRM has no use for...
The serial analyzer's return: the metric table, the path, and the stream.
std::shared_ptr< const qn::NetworkStruct< T > > solved_sn
The struct the path was simulated on when it differs from the caller's (ctmc_mapc widens a multiserve...
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 > UN
Definition ssa_types.h:148
Matrix< double > RN
Definition ssa_types.h:148
double simulated_time
Simulated time the metrics are averaged over; the reference's totalTime.
Definition ssa_types.h:161
Matrix< double > TN
Definition ssa_types.h:148
std::size_t samples
Reaction firings actually performed.
Definition ssa_types.h:163
Matrix< double > QN
Definition ssa_types.h:148
std::string method
The concrete algorithm, as the reference's method.
Definition ssa_types.h:159
UQ.getInterval: the RANGE of every metric over the support of the Priors.
Definition solver_uq.h:728
std::string method
mvainterval or sampled.
Definition solver_uq.h:738
bool exact
True when the interval is the attained hull rather than a sampled range.
Definition solver_uq.h:736
Matrix< T > Qlo
(nstations x nclasses) lower and upper endpoints of each metric.
Definition solver_uq.h:730
std::string why
On the sampled path, the condition that disqualified the exact one.
Definition solver_uq.h:740
T Xlo
System throughput and total response time; the EXACT path only.
Definition solver_uq.h:732
UQ.defaultOptions plus the stream the Monte Carlo design draws from.
Definition solver_uq.h:86
std::string method
default | dd | quad | mc, MATLAB UQ.listValidMethods.
Definition solver_uq.h:93
std::size_t samples
Nodes per continuous Prior, or design points under mc; the reference's options.samples,...
Definition solver_uq.h:100
unsigned long seed
The Monte Carlo stream; unread by a quadrature design, which draws nothing.
Definition solver_uq.h:102
What solver_uq_run_analyzer returns.
Definition solver_uq.h:126
std::vector< T > weights
The design weights, summing to 1.
Definition solver_uq.h:132
std::string method
The RESOLVED discretization method: quad or mc.
Definition solver_uq.h:138
std::vector< mva::AvgResult< T > > points
The result at each design point, in design order.
Definition solver_uq.h:130
std::vector< PriorSite< T > > sites
Where the Priors were found.
Definition solver_uq.h:136
std::vector< UqDesignPoint< T > > design
The alternatives each point substituted, one per site.
Definition solver_uq.h:134
mva::AvgResult< T > avg
The prior-weighted expectation of every metric, (nstations x nclasses).
Definition solver_uq.h:128
The inner solver's knobs, carried through untranslated.
Definition uq_dispatch.h:65
double tol
< 0 = not given
Definition uq_dispatch.h:70
double cutoff
ctmc open-population cutoff; < 0 = not given
Definition uq_dispatch.h:75
std::string solver
mva | nc | mam | ba | ctmc | fluid | ssa.
Definition uq_dispatch.h:67
Resolves the symbolic backend to use, and owns the container that serves it.
The stage solver of SolverUQ, named rather than passed.
Minimal RFC 6455 WebSocket server, enough to serve LineWebSocketServer's protocol.