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 <sstream>
30#include <string>
31#include <vector>
32
40#include "line/io/jmt_writer.h"
41#include "line/io/jsim_reader.h"
43#include "line/io/pnml.h"
49#include "line/num/number.h"
51#include "line/reg/registry.h"
93#include "line/cli/cli_run.h"
94#include "line/io/marshal.h"
95#include "line/util/error.h"
96#include "line/util/websocket.h"
97
98namespace {
99
100// THE PRODUCT VERSION, and it must track the other six sites. `-V`, the help banner
101// and `--install` all print it, so a stale value here makes the tree report two
102// different versions depending on which entry point is asked. It sat at 0.1.0
103// while everything else said 3.0.8, because `release/post-release.sh` bumped a
104// list this file was not on; it is on that list now, and on its straggler scan.
105const char* kVersion = "3.0.8";
106
107/**
108 * Whether `-a avg` prints its table as JSON, i.e. `-o json`.
109 *
110 * A FILE-SCOPE FLAG, and the one place in this port that has one. The library
111 * under `include/line/` keeps no global mutable state, deliberately, so a
112 * pybind11 or MEX host can call it reentrantly; `line_cli.cpp` is a `main()`
113 * translation unit and the output format is a property of ONE process
114 * invocation, decided before any solve begins and never changed after. The
115 * alternative is threading a bool through `solve_model_{mva,nc,mam,ba}`,
116 * `solve_ctmc_avg`, `solve_fluid_avg` and `solve_ssa_avg`, three of which do
117 * not take the `Knobs` struct at all, to carry a value none of them varies.
118 */
119bool g_json_output = false;
120
121/**
122 * Where a JSON document goes when a host is collecting them, and nothing when
123 * no host is.
124 *
125 * IT HOLDS STRINGS AND NOT PARSED VALUES, which is the whole reason the sink
126 * sits at the emission sites rather than around them. The eleven sites use
127 * three different indents (`dump()`, `dump(1)`, `dump(2)`) and print their
128 * numbers at full precision precisely so a cross-codebase diff is not capped;
129 * parsing here and re-dumping in the host would silently reformat both, and the
130 * reformatting would stay invisible until someone diffed a golden. Storing the
131 * bytes the CLI printed is also what makes the byte-identity test possible: a
132 * host's document must appear VERBATIM in the binary's stdout.
133 */
134std::vector<std::string>* g_json_sink = nullptr;
135
136/** A host asked, through the C ABI, for the run to stop. */
137struct InterruptRequested : line::Error {
138 explicit InterruptRequested(const std::string& what) : line::Error(what) {}
139};
140
141int (*g_interrupt_cb)(void*) = nullptr;
142void* g_interrupt_user = nullptr;
143
144/**
145 * Poll the host's interrupt, if one was installed.
146 *
147 * Called where the CLI is between two units of work and holds nothing that a
148 * throw would leak: after a document is emitted, and between the analyses of a
149 * comma-separated `-a` list. It deliberately does NOT reach inside a solve.
150 */
151void poll_interrupt() {
152 if (g_interrupt_cb != nullptr && g_interrupt_cb(g_interrupt_user) != 0)
153 throw InterruptRequested("the run was interrupted by the host");
154}
155
156/**
157 * Print one JSON document, and record it when a host is collecting.
158 *
159 * The printf is unchanged and unconditional, so `line-cli` prints exactly what
160 * it printed before this sink existed. That is the acceptance test for this
161 * whole change: with `g_json_sink == nullptr` the binary must be byte-identical.
162 */
163void emit_document(const std::string& text) {
164 if (g_json_sink != nullptr) g_json_sink->push_back(text);
165 std::printf("%s\n", text.c_str());
166 poll_interrupt();
167}
168
169/**
170 * Replace every non-finite number in a document with the spelling this wire
171 * gives one.
172 *
173 * JSON HAS NO INFINITE LITERAL, and nlohmann serialises any non-finite double
174 * as `null`. That is correct for a NaN -- `null` is how this wire says "no
175 * value", and it is what the readable table's `nan` means -- and wrong for an
176 * infinity, which is an ANSWER rather than the absence of one. A queue at or
177 * past its stability boundary has an infinite mean queue length and response
178 * time; a host that reads that `null` as "not computed", or as zero, inverts
179 * the conclusion it draws from it.
180 *
181 * The spelling is the JAR's. `LineCLI.appendDoubleArray` states it and says
182 * "line-cli emits the identical form", which until this function it did not:
183 * an infinity rides as the STRING "Infinity" / "-Infinity". Every other reader
184 * in the project already accepts that spelling -- MATLAB `linemodel_load`,
185 * Python `linemodel_io`, the R `from_wire_num` -- so this only stops line-cli
186 * from being the one writer that disagreed with all of them.
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 * Solve a Network model.json with SolverMVA and print its AvgTable.
215 *
216 * The columns and their order -- Station, JobClass, then QLen, Util, RespT,
217 * ResidT, ArvR, Tput -- are the ones `parity/compare_parity.py` parses, so the
218 * printed table is directly diffable against the MATLAB and Python rows. A
219 * (station, class) pair with no presence at all (every metric zero) is dropped,
220 * as `getAvgTable` drops its unvisited rows.
221 */
222/**
223 * The solver knobs the CLI exposes, gathered so the dispatcher can check them
224 * against the solver actually chosen.
225 *
226 * A sentinel means "not given" rather than a default, because the distinction
227 * is what lets an option be REFUSED for a solver that has no such setting
228 * instead of being accepted and dropped. That silent acceptance is the defect
229 * this struct exists to prevent: `--samples 1e6` was previously taken without
230 * complaint and the run still used 10000.
231 */
232struct Knobs {
233 std::string method;
234 double tol = -1.0; // < 0 = not given
235 double iter_tol = -1.0;
236 int iter_max = -1;
237 // --max-states, `options.config.maxStates`: the truncation level of an OPEN
238 // agent's queue-length dimension in SolverAG. < 0 = not given, so AgOptions
239 // keeps its own 100. It is a TRUNCATION and therefore part of the ANSWER,
240 // not a budget: a run truncated at 100 that the caller asked to truncate at
241 // 500 is a different number reported as theirs, which is why it travels
242 // rather than being dropped the way the execution backends are.
243 long long max_states = -1;
244 // --multiserver, the AMVA rule that decides WHICH algorithm serves a
245 // multiserver model. Empty = not given, so the solver keeps its default:
246 // a rule this CLI silently dropped made every delegated solve answer under
247 // 'default' while reporting the caller's choice.
248 std::string multiserver;
249 // --fork-join, `options.config.fork_join`: WHICH fork-join arm the shared
250 // fixed point takes on a model with a Fork. Empty = not given, so the solver
251 // keeps 'default' (the MMT transform); 'ht' is Heidelberger-Trivedi, which
252 // is a different answer to the same model rather than a faster one.
253 std::string fork_join;
254 // The solver console has NO knob of its own: it IS VerboseLevel::DEBUG,
255 // so `-v debug` is what asks for the running progress log.
256 std::size_t samples = 0; // 0 = not given
257 unsigned long seed = 0;
258 double cutoff = -1.0; // < 0 = not given
259 // --cutoff AS A MATRIX, `r1c1,r1c2;r2c1,r2c2`: the reference's
260 // `options.cutoff` is a (station x class) table wherever a model needs a
261 // different truncation per station, and reading only the scalar form left
262 // `atof` silently taking the first number, i.e. truncating every station at
263 // the first station's first class. Empty = not given.
264 std::vector<std::vector<std::size_t>> cutoff_mat;
265 /** True when `--cutoff` was given in EITHER spelling. */
266 bool has_cutoff() const { return cutoff >= 0.0 || !cutoff_mat.empty(); }
267 // --force, `options.force`: downgrade SolverCTMC's memory pre-gate from a
268 // refusal to a warning. The gate exists because the alternative to refusing
269 // is the OOM killer, so this is opt-in and never a default.
270 bool force = false;
271 // --fj-accuracy / --fj-tmode, `options.config.fj_accuracy` and
272 // `options.config.fj_tmode` of solver_mam_fj.m. The first is the FJ_codes
273 // truncation C of the queue-length difference between the two branches and
274 // is the accuracy knob of that approximation; the second picks the route to
275 // the T matrix. Kept apart from --tol/--iter_max for the same reason
276 // --mdd-tol is: C is a STATE-SPACE bound, not a convergence threshold.
277 int fj_accuracy = 0; // 0 = not given
278 std::string fj_tmode; // empty = not given
279 // --timescale, `options.config.timescale` of sn_is_discrete_time.m. "auto"
280 // lets the distributions decide whether the model is slotted; "discrete"
281 // and "continuous" force the reading, the first raising when the model
282 // mixes lattice and non-lattice laws rather than solving the wrong time
283 // scale. The slot itself comes from the SHARED `--slotlength` below, the
284 // same one SolverNC's discrete product form reads.
285 std::string timescale; // empty = not given
286 // --mdd-tol / --mdd-maxiter, the level iteration of `-s ctmc --method mdd`.
287 // They are DELIBERATELY not --tol / --iter_tol: that iteration is an INNER
288 // numerical solve whose fixed point is verified against the model's
289 // population invariant at 1e-6, so an AMVA-sized tolerance converges short
290 // of it and trips the guard. The reference keeps them apart for the same
291 // reason (options.config.mdd_tol, not options.iter_tol).
292 double mdd_tol = -1.0; // < 0 = not given
293 int mdd_maxiter = 0; // 0 = not given
294 // --level, the hierarchy level of the pbh/bjbh/cbh/sib families.
295 // 0 = not given, so BaOptions keeps its 2.
296 int level = 0;
297 // --busyperiod / --busyperiod-subnet, the orders and the subnetwork of
298 // `-a busyperiod`. The flag names are `ldes_cli`'s, so ONE spelling drives
299 // the transform (solver_nc_busyp) and the simulation. The subnetwork has no
300 // default -- a busy period is defined for a named set of stations and
301 // choosing one here would answer about a subnetwork the caller never
302 // named -- while the orders default to the ordinary busy period, 1.
303 std::vector<std::size_t> busy_orders;
304 std::vector<std::size_t> busy_subnet;
305 // --qrf-params / --qrf-alpha, the blocking parameterisation and the
306 // load-dependent scaling of the QRF arms of SolverBA. Both are JSON, given
307 // inline or as a path; empty = not given.
308 std::string qrf_params;
309 std::string qrf_alpha;
310 // --tspan, the horizon the transient CTMC analyses integrate over. There is
311 // no default: pi(t) on an unstated horizon is not a quantity, and picking
312 // one here would answer a question the caller did not ask.
313 double t0 = 0.0, t1 = -1.0; // t1 < 0 = not given
314 std::size_t node = 0; // --node, 1-based; 0 = not given
315 // --class and --marg-states, the second and third arguments of
316 // `@@SolverMVA/getProbMarg`. The class is 1-based and 0 = not given, i.e.
317 // every class; the state list is the reference's `state_m` and empty = not
318 // given, i.e. the default range each of the three laws picks for itself.
319 // They are NOT folded into --node: a marginal is indexed by a PAIR, and one
320 // flag carrying both would make "station 2" and "class 2" the same token.
321 std::size_t jobclass = 0; // --class, 1-based; 0 = not given
322 std::vector<long> marg_states; // --marg-states; empty = not given
323 // --warmupfrac, `options.config.warmupfrac`: the leading fraction of an SSA
324 // path discarded before the means are taken. The JAR CLI has carried it
325 // since the SSA branch existed and this port did not, so a delegated solve
326 // asking for a warmup discard silently kept the whole transient.
327 // < 0 = not given, so the engine keeps its own 0.
328 double warmupfrac = -1.0;
329 // --pstar, `options.config.pstar`: the exponent of the fluid p-norm
330 // smoothing (Ruuskanen et al., PEVA 151 (2021), eq. (26)). It selects the
331 // DRIFT the matrix method integrates, so a solve that never receives it
332 // returns the unsmoothed mean-field fixed point under the caller's choice.
333 // < 0 = not given.
334 double pstar = -1.0;
335 // --notation, which form of the exported ODE document is wanted; empty = not
336 // given, and only `-a odes` has one.
337 std::string notation;
338 // --cdf-algorithm, `options.config.algorithm` of `@@SolverNC/getCdfRespT`:
339 // 'exact' is the pfqn_stdf sojourn-time inversion, 'rd' the pfqn_stdf_heur
340 // reduction. Empty = not given, so the solver keeps the reference's 'exact'.
341 // It is NOT --method: the ladder that computes the constants and the
342 // algorithm that inverts the sojourn law are separate choices, and folding
343 // them would make one name silently select the other.
344 std::string cdf_algorithm;
345 // -s ctmc -a firstpasst: the two state sets of `getCdfFirstPassT(A, B)`.
346 // Each is either a 1-based index list into the state space ("3,5") or
347 // semicolon-separated state rows ("0,2;1,1"), resolved against the space
348 // the engine enumerates -- rows travel across the boundary because the two
349 // enumerations need not order (or even purge) states identically.
350 std::string passage_from;
351 std::string passage_into;
352 // "expm" (default) or "lt", `options.config.passage_method` of the reference
353 std::string passage_method;
354 // -s ctmc -a firstpasstmom: the highest moment order, `nmax` of
355 // `getFirstPassTMoments(A, B, nmax)`. 0 means the reference's default of 3.
356 std::size_t passage_orders = 0;
357 // --perm-engine, the permanent estimator of `@@SolverNC/getProbSysMarg.m`:
358 // 'exact' is Ryser's expansion with column multiplicities, and 'spm',
359 // 'bethe', 'heur', 'huberlaw', 'adapart' are the five approximations. It is
360 // NOT --method: the ladder that computes the normalizing constant and the
361 // estimator that evaluates the permanent are separate choices. The five
362 // approximations REFUSE a demand matrix with a structural zero rather than
363 // flooring it, since they need full support. 'spm' is the only one that does
364 // not expand the matrix to order sum(N), so it is the one whose cost does
365 // not grow with the population and whose error falls as it grows.
366 std::string method_perm = "exact";
367 // --symbolic, `options.config.symbolic` of `@@SolverFLD/getJacobian`: auto
368 // to search for a line-sage-rest backend, a URL, an image name, or none to
369 // stay with the locally differentiated Jacobian. Empty = not given, i.e.
370 // auto. --equilibria is the reference's fourth output, which is REQUESTED
371 // and not implied: solving f(x) = 0 needs the backend, so implying it would
372 // turn a Jacobian this port answers on its own into one that fails without
373 // a container.
374 std::string symbolic;
375 bool equilibria = false;
376 // ---- the layered path's own knobs, on the same not-given discipline ----
377 // They are refused on the Network path rather than dropped, exactly as the
378 // ones above are refused for a solver that has no such setting.
379 bool no_interlocking = false; // --no-interlocking was passed
380 // --interlock-method / --interlock-maxpaths / --interlock-refpath-scope, the
381 // config.interlock_* of SolverLN. Empty / negative = not given.
382 std::string interlock_method;
383 double interlock_maxpaths = -1.0;
384 std::string interlock_refpath_scope;
385 bool interlock_knobs_given() const {
386 return !interlock_method.empty() || interlock_maxpaths >= 0.0 ||
387 !interlock_refpath_scope.empty();
388 }
389 int repeat = 0; // --repeat; 0 = not given, i.e. one run
390 std::string layer_solver; // --layer-solver; empty = not given
391 /**
392 * `--stage-solver`, which solver runs each stage of an ENVIRONMENT.
393 *
394 * IT IS NOT `--layer-solver`, and folding the two would be wrong: a LAYER of
395 * an LQN is solved in STEADY STATE and a STAGE of a random environment is
396 * solved TRANSIENTLY, so their admissible solver sets are different sets for
397 * different reasons. Empty = not given, i.e. the coupling's own default
398 * (fluid for the mean-field one, ctmc for the state-vector one).
399 */
400 std::string stage_solver;
401 /*
402 * --map-env / --map-env-method / --map-env-maxstages: the random-environment
403 * FALLBACK for a model whose only unsupported feature is a non-renewal
404 * process. Unlike `--stage-solver` these are not `-s env` knobs: they apply
405 * to whichever solver was asked for, because the fallback is decided above
406 * the runner, in the `getAvg` funnel that `run_avg_engine` plays here.
407 * Empty / 0 = not given.
408 */
409 std::string map_env;
410 std::string map_env_method;
411 std::size_t map_env_maxstages = 0;
412 // --ln-transient / --ln-transient-channels, the coupling of the layered
413 // transient and which inter-layer channels it injects. Empty = not given.
414 std::string ln_transient;
415 std::string ln_transient_channels;
416 // --sens-method / --sens-scheme / --sens-step, the name-value contract of
417 // getSensitivityTable. They are NOT --method: --method names the LN update
418 // (default / moment3 / mw.*), and the branch that differentiates it is a
419 // separate choice. Empty / <= 0 = not given.
420 std::string sens_method;
421 std::string sens_scheme;
422 double sens_step = -1.0;
423 // --uq-solver, the engine SolverUQ runs at each design point. Empty is not
424 // a default here but a missing argument: UQ computes nothing itself, so
425 // there is no engine to fall back to, and `-s uq` without it is refused.
426 std::string uq_solver;
427 // --tran-points, the resolution of the uniform transient grid the ENV
428 // mean-field coupling sums its exit metrics over. It is that coupling's
429 // accuracy knob, not a cosmetic one: the sum is a Riemann-Stieltjes
430 // quadrature against the holding-time CDF, so the answer moves with the
431 // grid. 0 = not given, i.e. EnvOptions' own default.
432 std::size_t tran_points = 0;
433 // ---- the LQNS wrapper's own knobs -------------------------------------
434 // `options.keep` and `options.verbose` of the reference wrapper, plus the
435 // two that pick a REMOTE lqns. They apply to `-s lqns` alone and are
436 // refused elsewhere: nothing else in this CLI runs a child process whose
437 // working directory a caller might want to inspect.
438 bool keep = false;
439 bool verbose = false;
440 bool remote = false;
441 std::string remote_url; // empty = not given, i.e. the wrapper's default
442 int timeout_seconds = 0; // 0 = not given, i.e. no deadline
443 // ---- the simulator's own knobs, all `--ldes-*` -------------------------
444 // They are PREFIXED rather than folded into the shared names because they
445 // are an external engine's settings and not this port's: --ldes-tranfilter
446 // is the warmup filter of a simulation run and has nothing to do with
447 // --tol, and an unprefixed --warmupfrac would read as a knob every solver
448 // has. Same not-given discipline as the rest -- empty, <= 0 or false means
449 // not given -- so the engine keeps its own default and the command line
450 // stays minimal, which is what an older AOT native binary can still parse.
451 std::string ldes_tranfilter; // mser5 | fixed | none
452 double ldes_warmupfrac = -1.0;
453 std::string ldes_cimethod; // obm | bm | spectral | none
454 bool ldes_cnvgon = false;
455 double ldes_cnvgtol = -1.0;
456 bool ldes_slotted = false;
457 double ldes_slotlength = -1.0;
458 /**
459 * `--slotted` / `--slotlength`: the discrete time scale for the
460 * ANALYTICAL solvers, distinct from the `--ldes-*` pair above, which
461 * configures the simulator. SolverNC reads it and routes to the
462 * discrete-time product form.
463 */
464 bool slotted = false;
465 double slotlength = -1.0;
466 int ldes_replications = 0;
467 int ldes_numthreads = 0;
468 double ldes_maxtime = -1.0;
469 std::vector<double> ldes_initsol; // station-major warm-start placement
470 std::string ldes_rest_url;
471 /**
472 * `--jmt-replications`, SolverJMT's `options.config.replications`: the JSIM runs the transient
473 * ensemble of -a tran / -a tranprob averages over. 0 keeps the engine default of 10.
474 */
475 int jmt_replications = 0;
476 // ---- the JAR CLI's own five, ported so `line-cli` answers every question
477 // `jline.cli.LineCLI` answers ------------------------------------------
478 /**
479 * `--state`, the state vector `-a prob` asks about.
480 *
481 * WITHOUT IT THE QUERY IS ABOUT THE MODEL'S DEFAULT INITIAL STATE, which is
482 * what this CLI reported before and remains the default. The JAR takes an
483 * explicit one because `getProb(node, state)` is a different question from
484 * `getProb(node)`: the second names a state the model already holds, the
485 * first names any state of the node's own space, and a caller sweeping a
486 * marginal law needs the first. Empty = not given.
487 */
488 std::vector<long> state;
489 /**
490 * `--events`, the length of a sampled trajectory, `options.samples` of
491 * `@@SolverSSA/sample`. It is NOT `--samples`: the JAR keeps them apart
492 * because `--samples` is a simulation run length or a Monte Carlo draw
493 * count and reaches a solver's options, while this is the number of EVENTS
494 * one `sample` call walks. Folding them would make `-s ssa -a avg --events`
495 * silently lengthen the run. 0 = not given, i.e. the reference's 1000.
496 */
497 std::size_t events = 0;
498 /**
499 * `--timestep`, the fixed output step of a transient analysis. Without it
500 * the grid is whatever the integrator chose, which is the reference's
501 * adaptive default; with it the trajectory is resampled onto a uniform
502 * lattice of that step, which is what a caller diffing two transients needs.
503 * <= 0 = not given.
504 */
505 double timestep = -1.0;
506 /**
507 * `--transient-method`, `options.config.transient_method` of
508 * `solver_ctmc_transient_analyzer.m`: "ode" integrates the forward
509 * equation, "fau" marches fast adaptive uniformization over the output
510 * grid. Empty = not given, so the solver keeps the reference's "ode".
511 *
512 * It is NOT `--method`: the state-space path and the way the forward
513 * equation is advanced on it are separate choices, and the reference keeps
514 * this one out of its valid-method list because it changes no stationary
515 * answer.
516 */
517 std::string transient_method;
518 /**
519 * `--fau-epsilon` and `--fau-delta`, the two tolerances of the "fau"
520 * transient: the total probability mass the grid may discard, and the
521 * occupancy below which a state is dropped from the support. <= 0 = not
522 * given, i.e. the reference's 1e-6 and 1e-12.
523 */
524 double fau_epsilon = -1.0;
525 double fau_delta = -1.0;
526 /**
527 * `--rate-sched`, `options.config.rate_sched` of
528 * `solver_ctmc_transient_analyzer.m`: a JSON array (inline, or the path of a
529 * file holding it) of {station, class, tgrid, rates[, nominal]} objects,
530 * with station and class given by name or 1-based index. Empty = not given.
531 */
532 std::string rate_sched;
533 /// `--ctmc-tv-ngrid`, `options.config.ctmc_tv_ngrid`; 0 = not given (100).
534 std::size_t ctmc_tv_ngrid = 0;
535 /**
536 * `--percentiles`, the levels `getPerctRespT` is read at. Empty = not
537 * given, i.e. the reference's `pers_stored` {0.50, 0.90, 0.95, 0.99}.
538 * Accepted as fractions (0.9) or as percents (90), told apart by magnitude:
539 * a level above 1 cannot be a probability.
540 */
541 std::vector<double> percentiles;
542 /**
543 * `-v/--verbosity`: silent | standard | debug.
544 *
545 * IT WAS ACCEPTED AND DISCARDED, which is the silent-acceptance defect this
546 * struct exists to prevent one layer up: a caller who asked for `silent`
547 * still received every warning the arms print on stderr. It gates them now,
548 * and nothing else -- the tables on stdout are the answer and are printed
549 * whatever the level, exactly as the JAR prints them.
550 */
551 std::string verbosity;
552 /**
553 * `--reward-name`, which declared reward `-a reward-value` returns the
554 * value function of. Empty = not given, and `-a reward-value` without it is
555 * refused rather than defaulted to the first reward: the value functions of
556 * two rewards are different objects, and picking one silently would label
557 * the wrong matrix with the caller's question.
558 */
559 std::string reward_name;
560};
561
562/**
563 * The three `--map-env*` knobs, as the gate wants them.
564 *
565 * One helper rather than three assignments per arm, for the reason the Knobs
566 * struct itself exists: the fallback is decided in every `-a avg` ladder and in
567 * `run_avg_engine`, and an arm that read a different subset of the three would
568 * accept a different model for the same command line.
569 */
570inline line::solvers::MapEnvConfig map_env_config(const Knobs& k) {
572 if (!k.map_env.empty()) c.mode = k.map_env;
573 if (!k.map_env_method.empty()) c.method = k.map_env_method;
574 if (k.map_env_maxstages) c.max_stages = k.map_env_maxstages;
575 return c;
576}
577
578/**
579 * `--cutoff` written as a per-(station,class) matrix, `';'`-separated rows of
580 * `','`-separated non-negative counts. Empty on anything that is not one.
581 *
582 * The spelling is the JAR CLI's, so one example pins one string for both
583 * engines. A zero entry is legal and means the station may not hold that class
584 * at all, which is how the reference bounds a queue in the classes it does not
585 * serve.
586 */
587std::vector<std::vector<std::size_t>> parse_cutoff_matrix(const std::string& s) {
588 std::vector<std::vector<std::size_t>> out;
589 std::string::size_type pos = 0;
590 while (pos <= s.size()) {
591 const std::string::size_type semi = s.find(';', pos);
592 const std::string row = s.substr(pos, semi == std::string::npos ? std::string::npos
593 : semi - pos);
594 std::vector<std::size_t> cells;
595 std::string::size_type cp = 0;
596 while (cp <= row.size()) {
597 const std::string::size_type comma = row.find(',', cp);
598 const std::string cell = row.substr(cp, comma == std::string::npos ? std::string::npos
599 : comma - cp);
600 if (cell.empty()) return std::vector<std::vector<std::size_t>>();
601 for (std::string::size_type i = 0; i < cell.size(); ++i)
602 if (!std::isdigit(static_cast<unsigned char>(cell[i])))
603 return std::vector<std::vector<std::size_t>>();
604 cells.push_back(static_cast<std::size_t>(std::atol(cell.c_str())));
605 if (comma == std::string::npos) break;
606 cp = comma + 1;
607 }
608 if (cells.empty()) return std::vector<std::vector<std::size_t>>();
609 if (!out.empty() && cells.size() != out[0].size())
610 return std::vector<std::vector<std::size_t>>();
611 out.push_back(cells);
612 if (semi == std::string::npos) break;
613 pos = semi + 1;
614 }
615 return out;
616}
617
618/**
619 * The model text piped on stdin, drained ONCE and kept.
620 *
621 * `-s auto` parses the model twice: once to choose an engine and once for the
622 * engine to solve. A stream can only be drained once, so without this buffer the
623 * second parse would see nothing and the chooser would be unusable on a piped
624 * model. It is deliberately not a function template -- a static local inside one
625 * would be per-instantiation, and the two parses need not share an arithmetic.
626 */
627std::string g_stdin_buf;
628bool g_stdin_loaded = false;
629
630/**
631 * The model text a host supplied INSTEAD of the process's standard input.
632 *
633 * A library has no standard input worth reading: R's is the user's terminal,
634 * and draining it would hang the session. When a host passes a document it is
635 * installed here, and `stdin_model_text` returns it without touching std::cin.
636 */
637void set_stdin_override(const std::string& text) {
638 g_stdin_buf = text;
639 g_stdin_loaded = true;
640}
641
642const std::string& stdin_model_text() {
643 if (!g_stdin_loaded) {
644 g_stdin_buf.assign(std::istreambuf_iterator<char>(std::cin),
645 std::istreambuf_iterator<char>());
646 g_stdin_loaded = true;
647 }
648 return g_stdin_buf;
649}
650
651/**
652 * Whether `-i` selected a JMT document rather than a model.json.
653 *
654 * A FILE-SCOPE FLAG AND NOT A PARAMETER, because `read_model` is called from
655 * every solver arm and threading the format through all of them would touch
656 * fifty signatures to carry one bit that main already knows. It is set once,
657 * before any arm runs, and never again.
658 */
659bool g_jsim_input = false;
660
661/**
662 * Whether `-i` selected a PNML place/transition net.
663 *
664 * A second flag rather than a format string for the same reason `g_jsim_input`
665 * is one: `read_model` is called from every solver arm, and the three input
666 * kinds it can be handed are mutually exclusive, so two booleans say what a
667 * threaded enum would and touch no signature. Set once in main.
668 */
669bool g_pnml_input = false;
670
671/**
672 * `-v/--verbosity`, hoisted to file scope for the same reason as `g_jsim_input`.
673 *
674 * The knobs carry it too, but the priority warning below is raised where the
675 * model is READ rather than inside a solver arm, and that function receives no
676 * knobs. Set once in main, before any arm runs.
677 */
678std::string g_verbosity = "standard";
679
680/**
681 * Put every piece of process state back where a fresh process would have it.
682 *
683 * THESE ARE DEFECTS AND NOT HYGIENE, and one of them is live in shipped code:
684 * `run_invocation_captured` serves one request per connection on the same
685 * process, so a request carrying `-i jsimg` used to set `g_jsim_input` and
686 * every later request on that server took the JSIM reader for a model.json.
687 * The header on `g_jsim_input` states the assumption exactly -- "It is set
688 * once, before any arm runs, and never again" -- which is true of a process
689 * that runs one invocation and exits, and false of every other caller.
690 *
691 * The stdin buffer is reset only when no host override is pending, because a
692 * host that just supplied a document means it for the call about to run.
693 */
694void reset_invocation_state(bool keep_stdin_override) {
695 g_json_output = false;
696 g_jsim_input = false;
697 g_pnml_input = false;
698 g_verbosity = "standard";
699 if (!keep_stdin_override) {
700 g_stdin_buf.clear();
701 g_stdin_loaded = false;
702 }
703 // The console keeps the level of the LAST run, so a `-v debug` call would
704 // narrate the one after it. LineConsole::reset() drops any open run; the
705 // level is set again from g_verbosity by every entry point below.
707}
708
709/**
710 * Say so when class priorities were declared and no station will read them.
711 *
712 * The twin of the block at the tail of MATLAB's `@MNetwork/refreshStruct.m` and
713 * of `Network.refreshStruct` in the JAR. It is a WARNING and not a refusal: a
714 * priority at a station whose discipline ignores one is a legitimate model --
715 * `prio_hol_open` is built on it -- and only becomes a defect when NO station
716 * reads it, at which point the metrics are not the priority ones the caller is
717 * about to read them as. Priority-awareness is a property of the declared
718 * policy and is never inferred from the data; see network_struct.h.
719 */
720template <class T>
721void warn_priorities_ignored(line::qn::Network<T>& net) {
722 if (g_verbosity == "silent") return;
723 // `raw_struct` and not `get_struct`: the priorities and the disciplines are
724 // written when the classes and stations are added, and nothing in the
725 // refresh chain touches either, so reading them here costs no refresh.
726 if (!net.raw_struct().priorities_ignored()) return;
727 std::fprintf(stderr,
728 "Warning: Priority classes are specified but no priority-aware scheduling "
729 "policy (PSPRIO, DPSPRIO, GPSPRIO, HOL, FCFSPRIO, FCFSPRPRIO, FCFSPIPRIO, "
730 "LCFSPRIO, LCFSPRPRIO, LCFSPIPRIO, SRPTPRIO) is used in the model. "
731 "Priorities will be ignored.\n");
732}
733
734/** Read the model named by `file`, or stdin when it is empty. */
735template <class T>
736line::qn::Network<T> read_model(const std::string& file) {
737 if (g_pnml_input) {
738 // The PNML reader walks a DOM and has no stdin form, so a piped document
739 // is staged to a temporary file, as the JSIM arm below does.
740 if (!file.empty()) {
742 warn_priorities_ignored(net);
743 return net;
744 }
745 const std::string tmp = line::io::jsim_stage_stdin(stdin_model_text());
747 std::remove(tmp.c_str());
748 warn_priorities_ignored(net);
749 return net;
750 }
751 if (g_jsim_input) {
752 // The JSIM reader walks a DOM and has no stdin form, so a piped JMT
753 // document is staged to a temporary file rather than refused: `cat
754 // model.jsimg | line-cli -i jsimg` is how the JAR CLI is used and the
755 // Docker image documents it.
756 if (!file.empty()) {
758 warn_priorities_ignored(net);
759 return net;
760 }
761 const std::string tmp = line::io::jsim_stage_stdin(stdin_model_text());
763 std::remove(tmp.c_str());
764 warn_priorities_ignored(net);
765 return net;
766 }
767 if (!file.empty()) {
769 warn_priorities_ignored(net);
770 return net;
771 }
772 std::istringstream in(stdin_model_text());
773 line::io::detail::json root;
774 in >> root;
776 warn_priorities_ignored(net);
777 return net;
778}
779
780/** Read the Environment model.json named by `file`, or stdin when it is empty. */
781template <class T>
782line::env::Environment<T> read_env_model(const std::string& file) {
783 if (!file.empty()) return line::io::read_environment_json<T>(file);
784 std::istringstream in(stdin_model_text());
785 line::io::detail::json root;
786 in >> root;
788}
789
790/** One row of an average table, already reduced to double. */
791struct AvgRow {
792 double q, u, r, w, a, t;
793};
794
795/**
796 * Render an average table, as text or as the JSON the hosts parse.
797 *
798 * THE ONE PLACE `-o json` IS HONOURED, reached by every solver arm through a
799 * per-(station,class) accessor. The SSA and fluid arms used to hand-roll their
800 * own printer -- their solution types are `SsaSolution`/`FluidSolution`, not
801 * `mva::AvgResult<T>` -- and neither consulted `g_json_output`, so `-o json` was
802 * ACCEPTED AND IGNORED for `-s ssa` and `-s fluid`: the caller got the readable
803 * table with no brace in it, which is what the Python `lang='cpp'` bridge hit as
804 * "no JSON object found in solver output". Accepting a flag and not applying it
805 * is the silent-acceptance defect this CLI refuses everywhere else, so the
806 * rendering is shared rather than reimplemented per solution type.
807 *
808 * THE JSON FORM IS THE JAR's, key for key. `jline.cli.LineCLI -a avg -o json`
809 * emits {"avg": {"type": "AvgTable", "Station": [...], "JobClass": [...],
810 * "QLen": [...], ...}}, column-oriented, and the Python wrapper's
811 * `station_matrices_via_jar` parses exactly that shape. Emitting anything else
812 * would force a second parser into the wrapper for a table that is the same
813 * table, so the host can treat `lang='cpp'` and `lang='java'` as one transport
814 * with two binaries. The JAR's `data` key (the rendered text) is NOT reproduced:
815 * no caller reads it, and a second rendering of the same numbers is one more
816 * thing that can disagree with the first.
817 *
818 * The rows are also byte-identical across the solvers that share it, which
819 * matters more than it looks: `parity/compare_parity.py` diffs these tables
820 * column by column, and a solver whose printer drifted by a space would read as
821 * a parity failure in a harness that is supposed to be measuring the numbers.
822 */
823template <class Get>
824void emit_avg_table_named(const std::vector<std::string>& stations,
825 const std::vector<std::string>& classes, const char* arith,
826 const std::string& method, Get get, const line::reg::Json& extra,
827 const line::reg::Json& envelope) {
828 line::reg::Json rows = line::reg::Json::object();
829 for (const char* key : {"Station", "JobClass", "QLen", "Util", "RespT", "ResidT", "ArvR",
830 "Tput"})
831 rows[key] = line::reg::Json::array();
832 rows["type"] = "AvgTable";
833
834 if (!g_json_output)
835 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s\n", "Station", "JobClass", "QLen",
836 "Util", "RespT", "ResidT", "ArvR", "Tput");
837 for (std::size_t i = 0; i < stations.size(); ++i) {
838 for (std::size_t c = 0; c < classes.size(); ++c) {
839 const AvgRow v = get(i, c);
840 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)
841 continue;
842 if (!g_json_output) {
843 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
844 stations[i].c_str(), classes[c].c_str(), v.q, v.u, v.r, v.w, v.a, v.t);
845 continue;
846 }
847 // FULL PRECISION on the JSON path, against the readable path's
848 // %12.6g. The table is for a human to read; the JSON is for a host
849 // to compare against another codebase's answer, and six digits
850 // would cap any parity check at six digits.
851 rows["Station"].push_back(stations[i]);
852 rows["JobClass"].push_back(classes[c]);
853 rows["QLen"].push_back(v.q);
854 rows["Util"].push_back(v.u);
855 rows["RespT"].push_back(v.r);
856 rows["ResidT"].push_back(v.w);
857 rows["ArvR"].push_back(v.a);
858 rows["Tput"].push_back(v.t);
859 }
860 }
861 if (g_json_output) {
862 // `extra` NESTS INSIDE the "avg" payload, for emit_analysis's reason: a
863 // solver-specific key at envelope level can collide with an analysis's
864 // own name, and a per-list cost qualifies THIS answer.
865 for (line::reg::Json::const_iterator it = extra.begin(); it != extra.end(); ++it)
866 rows[it.key()] = it.value();
867 line::reg::Json out = line::reg::Json::object();
868 out["avg"] = rows;
869 out["arith"] = arith;
870 out["method"] = method;
871 // `envelope`, unlike `extra`, sits BESIDE "avg" rather than inside it.
872 // Provenance of the solve as a whole belongs there: the iteration count
873 // and the convergence flag qualify the answer, not any one row, and a
874 // host reads them where the JAR CLI puts them.
875 for (line::reg::Json::const_iterator it = envelope.begin(); it != envelope.end(); ++it)
876 out[it.key()] = it.value();
877 emit_document(dump_document(out));
878 }
879}
880
881/**
882 * The same table, labelled from a struct.
883 *
884 * THE NAMES, NOT THE STRUCT, are what the printer needs, and one solver has no
885 * struct to give it: `-s ldes` forwards the model document to an external engine
886 * and is labelled from the names that engine reports, precisely so a model this
887 * port cannot itself parse still prints the same table. Everything else calls
888 * this overload and nothing about those rows changes.
889 */
890template <class T, class Get>
891void emit_avg_table(const line::qn::NetworkStruct<T>& sn, const std::string& method, Get get,
892 const line::reg::Json& extra = line::reg::Json::object(),
893 const line::reg::Json& envelope = line::reg::Json::object()) {
894 std::vector<std::string> stations, classes;
895 stations.reserve(sn.nstations);
896 classes.reserve(sn.nclasses);
897 for (std::size_t i = 0; i < sn.nstations; ++i) stations.push_back(sn.stations[i].name);
898 for (std::size_t c = 0; c < sn.nclasses; ++c) classes.push_back(sn.classes[c].name);
899 emit_avg_table_named(stations, classes, line::num_traits<T>::name(), method, get, extra,
900 envelope);
901}
902
903/** NaN as this arithmetic spells it, the reference's "not computed" marker. */
904template <class T>
905T cache_nan() {
906 return line::num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
907}
908
909/** `v[r]` when the vector reaches r, NaN otherwise -- the reference's nanGetAt. */
910template <class T>
911double cache_at(const std::vector<T>& v, std::size_t r) {
912 if (r >= v.size()) return std::numeric_limits<double>::quiet_NaN();
914}
915
916/**
917 * The readable cache rows of a solve that reports beside the AvgTable.
918 *
919 * A Cache answers in (node, read class) and in (node, read class, list), which
920 * the AvgTable cannot carry, so a caller that only got the AvgTable was told
921 * nothing about the cache it asked about. `-a cache` has its own richer table
922 * built from the model's Source rates; this one reports the surface alone and
923 * is what the `-s env` arm uses, where there is no single model whose Source
924 * rate describes the blend.
925 *
926 * ABSENT PRINTS NaN, NEVER 0: an empty field is a coupling that did not measure
927 * the quantity, and a zero would read as a cache that never hits.
928 */
929template <class T>
930void print_cache_rows(const line::qn::NetworkStruct<T>& sn,
931 const line::solvers::CacheMetrics<T>& cache) {
932 if (cache.empty()) return;
933 const double dnan = std::numeric_limits<double>::quiet_NaN();
934 std::printf("\n%-12s %-12s %5s %12s %12s %12s\n", "Cache", "JobClass", "List", "HitProb",
935 "DelayedHitP", "MissProb");
936 for (std::size_t c = 0; c < cache.caches.size(); ++c) {
937 const line::solvers::CacheNodeMetrics<T>& m = cache.caches[c];
938 const typename std::map<std::size_t, line::qn::CacheParam<T> >::const_iterator it =
939 sn.nodeparam.find(m.node);
940 if (it == sn.nodeparam.end()) continue;
941 const std::vector<std::size_t>& hitclass = it->second.hitclass;
942 for (std::size_t cl = 0; cl < sn.nclasses; ++cl) {
943 // A READ CLASS IS ONE WITH A HIT CLASS DEFINED, the same rule the
944 // `-a cache` table applies; every other class has no ratio of its
945 // own and gets no row rather than a row of zeros.
946 if (cl >= hitclass.size() || hitclass[cl] == 0) continue;
947 const double ph = cache_at(m.hitprob, cl), pm = cache_at(m.missprob, cl),
948 pd = cache_at(m.delayedprob, cl);
949 std::printf("%-12s %-12s %5d %12.6g %12.6g %12.6g\n", m.name.c_str(),
950 sn.classes[cl].name.c_str(), 0, ph, pd, pm);
951 for (std::size_t l = 0; l < m.hitproblist.cols() && cl < m.hitproblist.rows(); ++l)
952 std::printf("%-12s %-12s %5d %12.6g %12.6g %12.6g\n", m.name.c_str(),
953 sn.classes[cl].name.c_str(), static_cast<int>(l + 1),
954 line::num_traits<T>::to_double(m.hitproblist(cl, l)), dnan, dnan);
955 }
956 }
957}
958
959/**
960 * Print one analysis that is NOT the average table, as the JSON a host parses.
961 *
962 * ONE ENVELOPE FOR EVERY ANALYSIS: the payload sits under a key named after the
963 * `-a` it answers, and the arithmetic and the resolved method sit beside it,
964 * exactly as `emit_avg_table` places them beside "avg". A host therefore reads
965 * the provenance the same way whatever it asked for, and a caller that asked for
966 * `-a cdf` and received a "states" key knows the answer is not its own instead of
967 * misreading the numbers as its own.
968 *
969 * `method` MAY BE EMPTY, in which case the key is omitted rather than filled
970 * with the requested name: `-a reward` returns rewards and no solved chain, so
971 * there is no resolved method to report, and echoing back "default" would claim
972 * a resolution that never happened.
973 *
974 * EVERY INDEX IN A PAYLOAD IS 0-BASED, against the readable tables' 1-based
975 * columns, and each payload carries `indexBase` so a host cannot get it wrong
976 * silently. The two conventions are deliberate: the table is read by a human
977 * diffing it against MATLAB, whose indices start at 1, while the JSON is
978 * consumed by code that will index a numpy array or a std::vector with it.
979 */
980template <class T>
981void emit_analysis(const char* key, const line::reg::Json& payload, const std::string& method,
982 const line::reg::Json& extra = line::reg::Json::object()) {
983 line::reg::Json body = payload;
984 // `extra` GOES INSIDE THE PAYLOAD, not beside it. At envelope level a
985 // solver-specific key can collide with the analysis's own name -- the CTMC
986 // state count is "states" and so is the `-a states` payload, and the merge
987 // silently replaced the whole answer with the integer 3. Nesting it makes
988 // that class of collision unrepresentable, and it is where the value belongs
989 // anyway: a cutoff qualifies THIS answer.
990 for (line::reg::Json::const_iterator it = extra.begin(); it != extra.end(); ++it)
991 body[it.key()] = it.value();
992 line::reg::Json out = line::reg::Json::object();
993 out[key] = body;
994 out["arith"] = line::num_traits<T>::name();
995 if (!method.empty()) out["method"] = method;
996 emit_document(dump_document(out));
997}
998
999/** A Matrix as a row-major array of arrays, each entry reduced to double. */
1000template <class T>
1001line::reg::Json matrix_json(const line::Matrix<T>& M) {
1002 line::reg::Json rows = line::reg::Json::array();
1003 for (std::size_t i = 0; i < M.rows(); ++i) {
1004 line::reg::Json row = line::reg::Json::array();
1005 for (std::size_t j = 0; j < M.cols(); ++j)
1006 row.push_back(line::num_traits<T>::to_double(M(i, j)));
1007 rows.push_back(row);
1008 }
1009 return rows;
1010}
1011
1012/** A vector of field elements as a JSON array of doubles. */
1013template <class T>
1014line::reg::Json vector_json(const std::vector<T>& v) {
1015 line::reg::Json a = line::reg::Json::array();
1016 for (std::size_t i = 0; i < v.size(); ++i) a.push_back(line::num_traits<T>::to_double(v[i]));
1017 return a;
1018}
1019
1020/** A vector of sizes as a JSON array, unchanged: a width is not a measurement. */
1021line::reg::Json index_json(const std::vector<std::size_t>& v) {
1022 line::reg::Json a = line::reg::Json::array();
1023 for (std::size_t i = 0; i < v.size(); ++i) a.push_back(v[i]);
1024 return a;
1025}
1026
1027/**
1028 * The per-Cache result block, as `-a avg` carries it inside the "avg" payload.
1029 *
1030 * PER-CACHE RESULTS RIDE WITH THE AVG TABLE, because a cache's hit, miss and
1031 * delayed-hit fractions are a SOLVER RESULT and a host that solves through this
1032 * CLI has no other way to get them back onto its own Cache node. Emitted per
1033 * node, and each vector is OMITTED when the solver computed none: absent must
1034 * CLEAR the host's copy, and a zero-filled vector would instead assert that
1035 * nothing hits.
1036 *
1037 * FACTORED OUT of `print_avg_table` because the SSA and Fluid arms build the
1038 * table themselves rather than through it, and so carried no block at all:
1039 * `CPPLINE.restoreCacheResults` then cleared MATLAB's Cache node, refreshed the
1040 * visits, and reported link()'s offered 1/2-1/2 for a split both engines had
1041 * measured. On cache_replc_rr that is hit 1.0 against 1.1246 (fluid) and
1042 * 1.1460 (ssa). A second copy of this serialization here would be free to drop
1043 * a field again, so there is one.
1044 */
1045template <class T>
1046line::reg::Json cache_extra_json(const line::solvers::CacheMetrics<T>& cache) {
1047 line::reg::Json caches = line::reg::Json::array();
1048 for (std::size_t c = 0; c < cache.caches.size(); ++c) {
1049 const line::solvers::CacheNodeMetrics<T>& m = cache.caches[c];
1050 line::reg::Json e = line::reg::Json::object();
1051 // NAME FIRST, because the index is not portable. `node` is an index
1052 // into THIS process's node order, which is not the model.json
1053 // declaration order: on retrieval_simple the JSON declares Source,
1054 // Cache, Queue, Sink and this struct holds Source, Queue, Sink,
1055 // Cache, so the Cache is 2 to the host and 4 here. A host matching
1056 // on the index wrote onto its Sink, found no Cache and silently kept
1057 // the PREVIOUS solver's numbers. `node` stays for provenance.
1058 e["name"] = m.name;
1059 e["node"] = m.node;
1060 if (!m.hitprob.empty()) e["HitProb"] = vector_json(m.hitprob);
1061 if (!m.missprob.empty()) e["MissProb"] = vector_json(m.missprob);
1062 if (!m.delayedprob.empty()) e["DelayedHitProb"] = vector_json(m.delayedprob);
1063 if (!m.latency.empty()) e["ResidT"] = vector_json(m.latency);
1064 if (!m.listcost.empty()) e["ListCost"] = vector_json(m.listcost);
1065 // The per-list breakdown, (K x h). Carried because it is an ANSWER a
1066 // host cannot rebuild from HitProb: only the row SUM is HitProb, and
1067 // which list served the hit is what a replacement policy is judged on.
1068 if (m.hitproblist.rows() > 0) e["HitProbList"] = matrix_json(m.hitproblist);
1069 if (!m.delayedhitqlen.empty()) {
1070 e["DelayedHitQLen"] = vector_json(m.delayedhitqlen);
1071 e["DelayedHitQLenFull"] = vector_json(m.delayedhitqlenfull);
1072 }
1073 caches.push_back(e);
1074 }
1075 return caches;
1076}
1077
1078/**
1079 * Print an AvgResult as the parity table.
1080 *
1081 * Shared by every solver that returns `mva::AvgResult` -- MVA, NC, MAM and BA
1082 * -- so the rows stay byte-identical across them.
1083 */
1084template <class T>
1085void print_avg_table(const line::qn::NetworkStruct<T>& sn, const line::mva::AvgResult<T>& r,
1086 const line::reg::Json& extra_in = line::reg::Json::object()) {
1087 // THE JSON FORM IS THE JAR's, key for key. `jline.cli.LineCLI -a avg -o json`
1088 // emits {"avg": {"type": "AvgTable", "Station": [...], "JobClass": [...],
1089 // "QLen": [...], ...}}, column-oriented, and the Python wrapper's
1090 // `station_matrices_via_jar` parses exactly that shape. Emitting anything
1091 // else here would force a second parser into the wrapper for a table that is
1092 // the same table, so the host can treat `lang='cpp'` and `lang='java'` as one
1093 // transport with two binaries. The JAR's `data` key (the rendered text) is
1094 // NOT reproduced: no caller reads it, and a second rendering of the same
1095 // numbers is one more thing that can disagree with the first.
1096 // A CARRIED WARNING IS ONLY A WARNING IF SOMEONE SEES IT. `AvgResult`
1097 // carries the reference's text verbatim for the cases where the answer is
1098 // usable but not one the reference stands behind (the SJN starvation cap,
1099 // immediate feedback approximated as re-queueing), and until now no CLI
1100 // path printed it -- so the table looked authoritative exactly where the
1101 // reference declines. On STDERR, not stdout: stdout is the table a parity
1102 // harness diffs and the JSON a wrapper parses, and a warning line in either
1103 // would be read as data. Python's `lang='cpp'` bridge re-raises anything on
1104 // stderr as a Python warning, so it reaches that caller too.
1105 if (!r.warning.empty())
1106 std::fprintf(stderr, "Warning: %s\n", r.warning.c_str());
1107
1108 // Mean per-list cache storage cost, the ListCost column of getAvgCacheTable.
1109 // Present only on a cache model carrying item sizes, and omitted entirely
1110 // otherwise rather than emitted empty, so a host can test for the key.
1111 line::reg::Json extra = line::reg::Json::object();
1112 if (!r.listcost.empty()) {
1113 line::reg::Json lc = line::reg::Json::array();
1114 for (std::size_t j = 0; j < r.listcost.size(); ++j)
1115 lc.push_back(line::num_traits<T>::to_double(r.listcost[j]));
1116 extra["ListCost"] = lc;
1117 }
1118
1119 // WHAT THE CALLER ADDS, merged rather than replaced. The JMT arm carries the
1120 // finite-capacity-region rows this way: they are metric rows past the last
1121 // station, and the station table drops them, so without a channel of their
1122 // own a host reading `-a avg` cannot see a region at all (MATLAB's
1123 // `getAvgNodeTable` then filtered the FCR node out for having no numbers).
1124 if (extra_in.is_object())
1125 for (line::reg::Json::const_iterator it = extra_in.begin(); it != extra_in.end(); ++it)
1126 extra[it.key()] = it.value();
1127
1128 // See `cache_extra_json`: the split is a solver result and the host has no
1129 // other channel back to its own Cache node.
1130 if (!r.cache.empty()) extra["Cache"] = cache_extra_json<T>(r.cache);
1131
1132 // Provenance of the solve, beside "avg" and keyed as the JAR CLI keys it, so
1133 // one host-side reduction reads both backends. `converged` is OMITTED when
1134 // the handler reports none: absent is not false, and there the count is the
1135 // signal a caller may fall back on.
1136 line::reg::Json envelope = line::reg::Json::object();
1137 envelope["iter"] = r.iter;
1138 if (r.converged.has_value()) envelope["converged"] = r.converged.value();
1139 // `@SolverNC/getProbNormConstAggr`, keyed as the reference names the field it
1140 // stores it in. OMITTED for every solver that computes no constant: log G = 0
1141 // is the constant of an empty network, so a zero here would be a claim.
1142 if (r.lognormconst.has_value()) envelope["logNormConstAggr"] = r.lognormconst.value();
1143
1144 emit_avg_table<T>(sn, r.actualmethod, [&](std::size_t i, std::size_t c) {
1145 AvgRow row;
1146 row.q = line::num_traits<T>::to_double(r.QN(i, c));
1147 row.u = line::num_traits<T>::to_double(r.UN(i, c));
1148 row.r = line::num_traits<T>::to_double(r.RN(i, c));
1149 row.w = line::num_traits<T>::to_double(r.WN(i, c));
1150 row.a = line::num_traits<T>::to_double(r.AN(i, c));
1151 row.t = line::num_traits<T>::to_double(r.TN(i, c));
1152 return row;
1153 }, extra, envelope);
1154}
1155
1156template <class T>
1157int solve_model_mva(const std::string& file, const Knobs& k) {
1158 line::qn::Network<T> net = read_model<T>(file);
1160 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1161 if (k.tol >= 0.0) opt.tol = k.tol;
1162 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
1163 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1164 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
1165 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
1166 line::Matrix<T> init;
1167 // The map_env funnel, the same one `run_avg_engine` applies to the node,
1168 // sys, chain and nodechain views: `-a avg` and `-a node` must accept the
1169 // same models for the same command line. Compile-time because the driver is
1170 // double-only -- see the note on the nc arm of `run_avg_engine`.
1172 if constexpr (std::is_same_v<T, double>) {
1174 net.get_struct(), "SolverMVA",
1176 net.get_struct()),
1177 map_env_config(k),
1178 [&opt, &init](const line::qn::NetworkStruct<double>& m) {
1179 return line::mva::solver_mva_run_analyzer(m, opt, init);
1180 },
1182 } else {
1183 r = line::mva::solver_mva_run_analyzer(net.get_struct(), opt, init);
1184 }
1185 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1186
1187 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1188 r.actualmethod.c_str(),
1189 line::util::method_type("MVA", r.actualmethod).c_str());
1190 print_avg_table<T>(sn, r);
1191 return 0;
1192}
1193
1194
1195/**
1196 * `-s jmt -a tran`: `getTranAvg`, the transient ensemble of `default` over a finite timespan.
1197 *
1198 * A SINGLE SAMPLE PATH IS NOT E[N](t). There is no time ergodicity at a fixed
1199 * t, so the mean is estimated over independent replications -- `--jmt-replications`
1200 * of them, which is what `options.config.replications` names in the reference --
1201 * replication k seeded `seed + k - 1` and resampled onto the union of their event
1202 * grids. An explicit `--method jsim` is ONE run. `jmt_transient_replications`
1203 * states the horizon and method rules itself, so they are not restated here.
1204 *
1205 * The payload is the SAME `TranAvgTable` the CTMC, fluid, MAM and LDES
1206 * transients emit, key for key, so one host reader serves every solver that
1207 * answers `-a tran`.
1208 */
1209int solve_model_jmt_tran(const line::qn::NetworkStruct<double>& sn,
1210 const line::jmt::JmtOptions& o, const Knobs& k) {
1212 const std::size_t reps = line::jmt::jmt_transient_replication_count(o, "getTranAvg");
1213 const std::size_t M = sn.nstations, K = sn.nclasses, nt = r.t.size();
1214
1215 if (g_json_output) {
1216 line::reg::Json p = line::reg::Json::object();
1217 p["type"] = "TranAvgTable";
1218 p["indexBase"] = 0;
1219 p["t0"] = k.t0 >= 0.0 ? k.t0 : 0.0;
1220 p["t1"] = o.max_simulated_time;
1221 // HOW MANY PATHS THE MEAN IS OVER is part of the answer on a simulated
1222 // transient: a replication that produced no log contributes nothing, so
1223 // the count asked for and the count averaged are not the same number.
1224 p["replications"] = r.valid;
1225 p["replicationsRequested"] = reps;
1226 line::reg::Json curves = line::reg::Json::array();
1227 for (std::size_t i = 0; i < M && i < r.QNt.size(); ++i)
1228 for (std::size_t c = 0; c < K && c < r.QNt[i].size(); ++c) {
1229 // A disabled pair is OMITTED, not sent as zeros, as on the CTMC
1230 // arm: the host turns an absent curve into the disabled
1231 // handle's NaN, where zeros would read as a genuinely idle
1232 // station.
1233 if (sn.disabled[i][c]) continue;
1234 line::reg::Json e = line::reg::Json::object();
1235 e["Station"] = sn.stations[i].name;
1236 e["JobClass"] = sn.classes[c].name;
1237 e["station"] = i;
1238 e["jobclass"] = c;
1239 line::reg::Json tt = line::reg::Json::array(), q = line::reg::Json::array(),
1240 u = line::reg::Json::array(), x = line::reg::Json::array();
1241 for (std::size_t j = 0; j < nt; ++j) {
1242 tt.push_back(r.t[j]);
1243 q.push_back(r.QNt[i][c][j]);
1244 u.push_back(r.UNt[i][c][j]);
1245 x.push_back(r.TNt[i][c][j]);
1246 }
1247 e["t"] = tt;
1248 e["QLen"] = q;
1249 e["Util"] = u;
1250 e["Tput"] = x;
1251 curves.push_back(e);
1252 }
1253 p["curves"] = curves;
1254 // ALWAYS jsim: every replication is a simulated path, and JMVA computes
1255 // means from a product form and logs no trajectory at all, so naming the
1256 // requested method would name an engine that never ran.
1257 emit_analysis<double>("tran", p, std::string("jsim"));
1258 return 0;
1259 }
1260 std::printf("SolverJMT arith=double method=jsim\n");
1261 std::printf("TranAvg times=%zu replications=%zu/%zu\n", nt, r.valid, reps);
1262 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
1263 "Tput");
1264 for (std::size_t i = 0; i < M && i < r.QNt.size(); ++i)
1265 for (std::size_t c = 0; c < K && c < r.QNt[i].size(); ++c) {
1266 if (sn.disabled[i][c]) continue;
1267 for (std::size_t j = 0; j < nt; ++j)
1268 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
1269 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), r.t[j],
1270 r.QNt[i][c][j], r.UNt[i][c][j], r.TNt[i][c][j]);
1271 }
1272 return 0;
1273}
1274
1275/**
1276 * `-s jmt -a tranprob`: `getTranProbAggr`, pi(t) over ONE station's aggregate
1277 * state, estimated over independent replications.
1278 *
1279 * THE AGGREGATE LAW ONLY, and refused rather than aliased for the detailed one:
1280 * a JMT log records per-class job counts at a node and nothing about the buffer
1281 * order or the service phase, so the encoding `getTranProb` is a law over is
1282 * not observed at all -- the same rule the `-a prob` arm states.
1283 *
1284 * `--node` names the station, defaulting to the first one: the estimate is
1285 * over one station's own state, not over the network's, so there is no system
1286 * view to fall back to here.
1287 */
1288int solve_model_jmt_tranprob(const line::qn::NetworkStruct<double>& sn,
1289 const line::jmt::JmtOptions& o, const Knobs& k) {
1290 std::size_t station = 1;
1291 if (k.node) {
1292 // OUT OF RANGE IS REFUSED, not silently folded onto station 1: falling
1293 // back to the default would answer a question about a different node
1294 // than the one named, which is the silent-wrong-answer failure the
1295 // whole Knobs struct exists to prevent.
1296 if (k.node > sn.nodes.size() || sn.nodes[k.node - 1].station == 0)
1297 throw line::InputError(
1298 "--node " + std::to_string(k.node) +
1299 " is not a station, so it holds no per-class job count to take a law over");
1300 station = sn.nodes[k.node - 1].station;
1301 }
1302 const std::size_t reps = line::jmt::jmt_transient_replication_count(o, "getTranProbAggr");
1303 std::vector<std::vector<double> > states;
1304 const std::pair<std::vector<double>, std::vector<std::vector<double> > > r =
1305 line::jmt::jmt_get_tran_prob_aggr(sn, station, o, states);
1306 const std::vector<double>& t = r.first;
1307 const std::vector<std::vector<double> >& pit = r.second;
1308
1309 if (g_json_output) {
1310 line::reg::Json p = line::reg::Json::object();
1311 p["type"] = "TranProb";
1312 p["indexBase"] = 0;
1313 p["scope"] = sn.stations[station - 1].name;
1314 p["node"] = sn.station_to_node[station - 1] - 1;
1315 p["station"] = station - 1;
1316 p["replications"] = reps;
1317 line::reg::Json span = line::reg::Json::array();
1318 // THE HORIZON IS PART OF THE ANSWER, as on the CTMC arm: pi(t) over an
1319 // unstated span is not a quantity, and here it is also what makes the
1320 // replications' grids commensurable in the first place.
1321 span.push_back(k.t0 >= 0.0 ? k.t0 : 0.0);
1322 span.push_back(o.max_simulated_time);
1323 p["tspan"] = span;
1324 p["t"] = vector_json(t);
1325 // ONLY THE AGGREGATE LABELS, and the key says so: a host reading
1326 // `labels` here would be reading the detailed law of another solver.
1327 line::reg::Json labels = line::reg::Json::array();
1328 for (std::size_t s = 0; s < states.size(); ++s) labels.push_back(vector_json(states[s]));
1329 p["labelsAggr"] = labels;
1330 line::reg::Json rows = line::reg::Json::array();
1331 for (std::size_t g = 0; g < pit.size(); ++g) rows.push_back(vector_json(pit[g]));
1332 p["pitAggr"] = rows;
1333 emit_analysis<double>("tranprob", p, std::string("jsim"));
1334 return 0;
1335 }
1336 std::printf("SolverJMT arith=double method=jsim\n");
1337 std::printf("TranProb times=%zu tspan=%g:%g scope=%s replications=%zu\n", t.size(),
1338 k.t0 >= 0.0 ? k.t0 : 0.0, o.max_simulated_time,
1339 sn.stations[station - 1].name.c_str(), reps);
1340 // The labels come FIRST and the occupancy after, as on the CTMC arm: pi(t)
1341 // is a row per time over columns that mean nothing until the state they
1342 // index is named.
1343 std::printf("%8s %s\n", "State", "Aggregate");
1344 for (std::size_t s = 0; s < states.size(); ++s) {
1345 std::printf("%8zu ", s + 1);
1346 for (std::size_t c = 0; c < states[s].size(); ++c) std::printf(" %g", states[s][c]);
1347 std::printf("\n");
1348 }
1349 std::printf("%14s", "Time");
1350 for (std::size_t s = 0; s < states.size(); ++s) std::printf(" %12zu", s + 1);
1351 std::printf("\n");
1352 for (std::size_t g = 0; g < t.size(); ++g) {
1353 std::printf("%14.8g", t[g]);
1354 for (std::size_t s = 0; s < states.size(); ++s)
1355 std::printf(" %12.6g", s < pit[g].size() ? pit[g][s] : 0.0);
1356 std::printf("\n");
1357 }
1358 return 0;
1359}
1360
1361/**
1362 * `-s jmt -a sample`: `sampleSysAggr`, one simulated trajectory; with `--node`,
1363 * that node's own block (`sampleAggr`) instead.
1364 *
1365 * `--events` NAMES THE TRUNCATION, and it is a truncation rather than a
1366 * stopping rule: JMT's `maxEvents` is global and cannot be asked for a count at
1367 * one node, so what comes back is a prefix of the run the engine produced --
1368 * exactly the caveat `jmt_sample_aggr` carries. `--samples` stands in when
1369 * `--events` is absent, as on the CTMC arm, and neither bounds the engine.
1370 */
1371int solve_model_jmt_sample(const line::qn::NetworkStruct<double>& sn,
1372 const line::jmt::JmtOptions& o, const Knobs& k) {
1373 const std::size_t nevents = k.events ? k.events : 0;
1374 const std::size_t K = sn.nclasses;
1375
1376 if (k.node) {
1377 if (k.node > sn.nodes.size() || sn.nodes[k.node - 1].station == 0)
1378 throw line::InputError(
1379 "--node " + std::to_string(k.node) +
1380 " is not a station, so no per-class job count is logged for it");
1382 line::jmt::jmt_sample_aggr(sn, k.node, nevents, o);
1383 if (g_json_output) {
1384 line::reg::Json p = line::reg::Json::object();
1385 p["type"] = "SamplePath";
1386 p["indexBase"] = 0;
1387 p["scope"] = sn.nodes[k.node - 1].name;
1388 p["node"] = k.node - 1;
1389 p["events"] = nevents;
1390 p["seed"] = o.seed;
1391 p["drawn"] = tr.t.size();
1392 p["t"] = vector_json(tr.t);
1393 line::reg::Json st = line::reg::Json::array();
1394 for (std::size_t j = 0; j < tr.qlen.size(); ++j) st.push_back(vector_json(tr.qlen[j]));
1395 p["state"] = st;
1396 line::reg::Json cols = line::reg::Json::array();
1397 for (std::size_t c = 0; c < K; ++c) cols.push_back(sn.classes[c].name);
1398 p["columns"] = cols;
1399 emit_analysis<double>("sample", p, std::string("jsim"));
1400 return 0;
1401 }
1402 std::printf("SolverJMT arith=double method=jsim\n");
1403 std::printf("%14s %s\n", "Time", ("NodeState (" + sn.nodes[k.node - 1].name +
1404 ", per class)").c_str());
1405 for (std::size_t j = 0; j < tr.t.size(); ++j) {
1406 std::printf("%14.8g ", tr.t[j]);
1407 for (std::size_t c = 0; c < K && c < tr.qlen[j].size(); ++c)
1408 std::printf(" %g", tr.qlen[j][c]);
1409 std::printf("\n");
1410 }
1411 return 0;
1412 }
1413
1415 const std::size_t M = sn.nstations, n = tr.t.size();
1416 if (g_json_output) {
1417 line::reg::Json p = line::reg::Json::object();
1418 p["type"] = "SamplePath";
1419 p["indexBase"] = 0;
1420 p["scope"] = "(system)";
1421 p["events"] = nevents;
1422 p["seed"] = o.seed;
1423 p["drawn"] = n;
1424 p["t"] = vector_json(tr.t);
1425 line::reg::Json st = line::reg::Json::array();
1426 for (std::size_t j = 0; j < n; ++j) {
1427 line::reg::Json row = line::reg::Json::array();
1428 for (std::size_t i = 0; i < M; ++i)
1429 for (std::size_t c = 0; c < K; ++c)
1430 row.push_back(i < tr.state.size() && j < tr.state[i].size() &&
1431 c < tr.state[i][j].size()
1432 ? tr.state[i][j][c]
1433 : 0.0);
1434 st.push_back(row);
1435 }
1436 p["state"] = st;
1437 line::reg::Json cols = line::reg::Json::array();
1438 for (std::size_t i = 0; i < M; ++i)
1439 for (std::size_t c = 0; c < K; ++c)
1440 cols.push_back(sn.stations[i].name + "," + sn.classes[c].name);
1441 p["columns"] = cols;
1442 emit_analysis<double>("sample", p, std::string("jsim"));
1443 return 0;
1444 }
1445 std::printf("SolverJMT arith=double method=jsim\n");
1446 std::printf("%14s %s\n", "Time", "SysState (station-major, per class)");
1447 for (std::size_t j = 0; j < n; ++j) {
1448 std::printf("%14.8g ", tr.t[j]);
1449 for (std::size_t i = 0; i < M; ++i)
1450 for (std::size_t c = 0; c < K; ++c)
1451 std::printf(" %g", i < tr.state.size() && j < tr.state[i].size() &&
1452 c < tr.state[i][j].size()
1453 ? tr.state[i][j][c]
1454 : 0.0);
1455 std::printf("\n");
1456 }
1457 return 0;
1458}
1459
1460/**
1461 * Solve a Network model.json with SolverJMT: write the JMT document, run the
1462 * external engine, print the same table every other solver prints.
1463 *
1464 * DOUBLE ONLY, and refused rather than relabelled under another arithmetic:
1465 * JMT simulates in double and reports in double, so an answer tagged
1466 * `real:128` would name a precision that never touched the computation. The
1467 * same rule the LDES arm applies, for the same reason.
1468 */
1469int solve_model_jmt(const std::string& file, const Knobs& k, const std::string& analysis) {
1470 line::qn::Network<double> net = read_model<double>(file);
1472
1474 if (!k.method.empty() && k.method != "default") o.method = k.method;
1475 if (k.samples > 0) o.samples = static_cast<double>(k.samples);
1476 if (k.seed != 0) o.seed = static_cast<long>(k.seed);
1477 o.keep = k.keep;
1478 if (k.t1 >= 0.0) o.max_simulated_time = k.t1;
1479 // `--jmt-replications` IS THE ENSEMBLE SIZE of the two arms that average over independent
1480 // runs, `options.config.replications`; the gate above accepts it for `-a tran` and `-a tranprob`.
1481 if (k.jmt_replications > 0) o.replications = k.jmt_replications;
1482 // AN EXPLICIT `jsim` IS ONE RUN, not the ensemble, so a count given with it would be dropped.
1483 if (k.jmt_replications > 0 && o.method == "jsim")
1485 "--jmt-replications sizes the transient ensemble of --method default; --method jsim "
1486 "is a single JSIM run, so the count would be ignored rather than honoured");
1487 o.verbose = k.verbose;
1488
1489 if (analysis == "tran") return solve_model_jmt_tran(sn, o, k);
1490 if (analysis == "tranprob") return solve_model_jmt_tranprob(sn, o, k);
1491 if (analysis == "sample") return solve_model_jmt_sample(sn, o, k);
1492
1493 // `-a prob`: `getProbAggr` per station and `getProbSysAggr`, weighed off ONE
1494 // instrumented run. The DETAILED pair -- `getProb` and `getProbSys` -- has
1495 // no counterpart here and is OMITTED rather than aliased to the aggregate:
1496 // a JMT log records per-class job counts at a node and nothing about the
1497 // buffer order or the service phase, so the encoding those two are
1498 // probabilities of is not observed at all.
1499 if (analysis == "prob") {
1500 std::size_t target = 0;
1501 if (k.node) {
1502 if (k.node <= sn.nodes.size()) target = sn.nodes[k.node - 1].station;
1503 if (target == 0)
1504 throw line::InputError(
1505 "--node " + std::to_string(k.node) +
1506 " is not a station, so it holds no per-class job count to ask about");
1507 }
1509 sn, o, target, std::vector<double>(k.state.begin(), k.state.end()));
1510 if (g_json_output) {
1511 line::reg::Json p = line::reg::Json::object();
1512 p["type"] = "ProbAggr";
1513 p["indexBase"] = 0;
1514 p["ProbSysAggr"] = r.sys;
1515 // WHETHER THE STATE OCCURRED AT ALL, beside the number. On an exact
1516 // solver a zero probability is a property of the model; on a
1517 // simulation it is far more often a property of the run length, and
1518 // a caller cannot tell the two apart from the zero alone.
1519 p["SysStateSeen"] = r.sys_seen;
1520 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
1521 sv = line::reg::Json::array();
1522 for (std::size_t i = 0; i < sn.nstations; ++i) {
1523 st.push_back(sn.stations[i].name);
1524 pa.push_back(r.station[i]);
1525 sv.push_back(static_cast<bool>(r.station_seen[i]));
1526 }
1527 p["Station"] = st;
1528 p["ProbAggr"] = pa;
1529 p["StateSeen"] = sv;
1530 // ALWAYS jsim, whatever `--method` asked for: this answer is read
1531 // off a simulated trajectory, and JMVA computes means from a
1532 // product form and logs nothing, so labelling it with the
1533 // requested method would name an engine that never ran.
1534 emit_analysis<double>("prob", p, std::string("jsim"));
1535 return 0;
1536 }
1537 std::printf("SolverJMT arith=double method=jsim\n");
1538 std::printf("ProbSysAggr %.10g%s\n", r.sys, r.sys_seen ? "" : " (state never observed)");
1539 std::printf("%-16s %14s\n", "Station", "ProbAggr");
1540 for (std::size_t i = 0; i < sn.nstations; ++i)
1541 std::printf("%-16s %14.10g%s\n", sn.stations[i].name.c_str(), r.station[i],
1542 r.station_seen[i] ? "" : " (state never observed)");
1543 return 0;
1544 }
1545
1546 if (analysis == "cdf" || analysis == "trancdf" || analysis == "trancdfpasst") {
1547 // -a cdf preloads the rounded steady-state queue lengths, the seeded
1548 // getCdfRespT pipeline; the transient names start from the default
1549 // initial state, getTranCdfRespT's contract
1550 const std::map<std::pair<std::size_t, std::size_t>,
1551 std::vector<std::pair<double, double> > >
1552 rd = line::jmt::jmt_get_cdf_resp_t(sn, o, analysis == "cdf");
1553 line::reg::Json cdf = line::reg::Json::object();
1554 for (std::map<std::pair<std::size_t, std::size_t>,
1555 std::vector<std::pair<double, double> > >::const_iterator it = rd.begin();
1556 it != rd.end(); ++it) {
1557 line::reg::Json rows = line::reg::Json::array();
1558 for (std::size_t i = 0; i < it->second.size(); ++i) {
1559 line::reg::Json row = line::reg::Json::array();
1560 row.push_back(it->second[i].first);
1561 row.push_back(it->second[i].second);
1562 rows.push_back(row);
1563 }
1564 const std::string key =
1565 sn.nodes[sn.station_to_node[it->first.first - 1] - 1].name + "/" +
1566 sn.classes[it->first.second - 1].name;
1567 cdf[key] = rows;
1568 if (!g_json_output)
1569 std::printf("%-24s %8zu points respT(max)=%.6g\n", key.c_str(),
1570 it->second.size(), it->second.back().second);
1571 }
1572 if (g_json_output) {
1573 line::reg::Json out = line::reg::Json::object();
1574 out["cdf"] = cdf;
1575 emit_document(dump_document(out, 2));
1576 }
1577 return 0;
1578 }
1579
1581 std::printf("SolverJMT arith=double method=%s\n", r.avg.actualmethod.c_str());
1582
1583 // The metric matrices carry `nregions` EXTRA rows past the stations; the
1584 // station table takes the first `nstations` of them and the regions are
1585 // reported separately, as the LDES arm reports its own FCR block.
1587 if (table.QN.rows() > sn.nstations) {
1588 const std::size_t M = sn.nstations, K = sn.nclasses;
1589 line::Matrix<double>* dst[6] = {&table.QN, &table.UN, &table.RN,
1590 &table.TN, &table.AN, &table.WN};
1591 const line::Matrix<double>* src[6] = {&r.avg.QN, &r.avg.UN, &r.avg.RN,
1592 &r.avg.TN, &r.avg.AN, &r.avg.WN};
1593 for (int m = 0; m < 6; ++m) {
1594 line::Matrix<double> t(M, K, 0.0);
1595 for (std::size_t i = 0; i < M; ++i)
1596 for (std::size_t c = 0; c < K; ++c) t(i, c) = (*src[m])(i, c);
1597 *dst[m] = t;
1598 }
1599 }
1600 // THE REGION ROWS TRAVEL BESIDE THE STATION TABLE, in the same shape the
1601 // node view prints them: a host solving through `-a avg` has no other way to
1602 // reach them, and MATLAB's `getAvgNodeTable` needs `result.Avg` to carry
1603 // `nstations + nregions` rows or it filters the FCR node out for having no
1604 // numbers at all (fcr_mm1waitq[M2C], 'row FCR1 missing').
1605 line::reg::Json fcr_extra = line::reg::Json::object();
1606 if (r.avg.QN.rows() > sn.nstations && sn.regions.size() > 0) {
1607 const std::size_t M = sn.nstations, K = sn.nclasses;
1608 line::reg::Json names = line::reg::Json::array();
1609 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array();
1610 line::reg::Json rt = line::reg::Json::array(), w = line::reg::Json::array();
1611 line::reg::Json a = line::reg::Json::array(), t = line::reg::Json::array();
1612 for (std::size_t f = 0; f < sn.regions.size() && M + f < r.avg.QN.rows(); ++f) {
1613 names.push_back(f < sn.regions.size() && !sn.regions[f].name.empty()
1614 ? sn.regions[f].name
1615 : "FCR" + std::to_string(f + 1));
1616 for (std::size_t c = 0; c < K; ++c) {
1617 q.push_back(r.avg.QN(M + f, c));
1618 u.push_back(r.avg.UN(M + f, c));
1619 rt.push_back(r.avg.RN(M + f, c));
1620 w.push_back(r.avg.WN(M + f, c));
1621 a.push_back(r.avg.AN(M + f, c));
1622 t.push_back(r.avg.TN(M + f, c));
1623 }
1624 }
1625 fcr_extra["Region"] = names;
1626 fcr_extra["QLen"] = q;
1627 fcr_extra["Util"] = u;
1628 fcr_extra["RespT"] = rt;
1629 fcr_extra["ResidT"] = w;
1630 fcr_extra["ArvR"] = a;
1631 fcr_extra["Tput"] = t;
1632 }
1633 line::reg::Json avg_extra = line::reg::Json::object();
1634 if (!fcr_extra.empty()) avg_extra["FCR"] = fcr_extra;
1635 // THE CONFIDENCE INTERVALS ARE PART OF A SIMULATED MEAN. JMT reports a
1636 // half-width per measure at `--confint`, `JmtResult` carries them, and both
1637 // consumers used to take `r.avg` and drop them -- so the one solver here
1638 // whose answer is an estimate was the one quoting a point estimate as if it
1639 // were exact. Same block, same keys and same reason as
1640 // `solve_model_ldes_avg`'s, so one host reader serves both simulators.
1641 line::reg::Json ci = line::reg::Json::object();
1642 if (!r.QCI.empty()) ci["QNCI"] = matrix_json<double>(r.QCI);
1643 if (!r.UCI.empty()) ci["UNCI"] = matrix_json<double>(r.UCI);
1644 if (!r.RCI.empty()) ci["RNCI"] = matrix_json<double>(r.RCI);
1645 if (!r.TCI.empty()) ci["TNCI"] = matrix_json<double>(r.TCI);
1646 if (!r.ACI.empty()) ci["ANCI"] = matrix_json<double>(r.ACI);
1647 if (!ci.empty()) avg_extra["CI"] = ci;
1648 print_avg_table<double>(sn, table, avg_extra);
1649
1650 // The readable rendering prints the half-widths beside the table rather
1651 // than inside it: the columns are the same six every solver prints, and a
1652 // JMVA run (which computes no interval) would leave them blank.
1653 if (!g_json_output && !r.QCI.empty()) {
1654 std::printf("%-16s %-14s %12s %12s %12s %12s %12s\n", "Station", "JobClass", "QLenCI",
1655 "UtilCI", "RespTCI", "TputCI", "ArvRCI");
1656 for (std::size_t i = 0; i < sn.nstations && i < r.QCI.rows(); ++i)
1657 for (std::size_t c = 0; c < sn.nclasses; ++c)
1658 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g\n",
1659 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), r.QCI(i, c),
1660 r.UCI.empty() ? 0.0 : r.UCI(i, c), r.RCI.empty() ? 0.0 : r.RCI(i, c),
1661 r.TCI.empty() ? 0.0 : r.TCI(i, c),
1662 r.ACI.empty() ? 0.0 : r.ACI(i, c));
1663 }
1664
1665 if (!g_json_output && r.TNfcr.rows() > 0) {
1666 std::printf("%-16s %-14s %12s %12s\n", "Region", "JobClass", "Tput", "DropRate");
1667 for (std::size_t f = 0; f < r.TNfcr.rows(); ++f)
1668 for (std::size_t c = 0; c < sn.nclasses; ++c)
1669 // THE REGION'S DECLARED NAME, not the JSIM document's internal
1670 // `FCRegion<n>` label: that spelling is what the writer puts in
1671 // the exported model, and reporting it back renamed the user's
1672 // own region (`FCR1` in every other codebase's node table).
1673 std::printf("%-16s %-14s %12.6g %12.6g\n",
1674 (f < sn.regions.size() && !sn.regions[f].name.empty()
1675 ? sn.regions[f].name
1676 : "FCR" + std::to_string(f + 1))
1677 .c_str(),
1678 sn.classes[c].name.c_str(), r.TNfcr(f, c), r.DropRateNfcr(f, c));
1679 }
1680 if (!g_json_output && !r.cache_hit_prob.empty())
1681 for (std::map<std::size_t, std::vector<double> >::const_iterator it =
1682 r.cache_hit_prob.begin();
1683 it != r.cache_hit_prob.end(); ++it)
1684 for (std::size_t c = 0; c < it->second.size(); ++c)
1685 std::printf("%-16s %-14s hitProb=%12.6g\n", sn.nodes[it->first - 1].name.c_str(),
1686 sn.classes[c].name.c_str(), it->second[c]);
1687 return 0;
1688}
1689
1690/**
1691 * `-s nc -a avg` under `--method cftp` / `cftp.approx`: the AvgTable from
1692 * perfect sampling.
1693 *
1694 * THE BANNER NAMES THE RUN LENGTH because these numbers carry Monte Carlo error
1695 * and are not comparable to an exact solver's at solver tolerance. The mean
1696 * coalescence horizon travels with it: it is the cost the exact sampler paid,
1697 * has no a-priori bound, and is the one number that says whether the draw was
1698 * cheap or the model is nearly saturated. No `lognormconst=`: the sampler
1699 * yields no normalizing constant.
1700 */
1701template <class T>
1702int solve_nc_cftp_avg(const line::qn::NetworkStruct<T>& sn, const line::nc::NcSolverOptions& opt,
1703 const line::nc::NcCftpOptions& cftpopt) {
1706 "the cftp methods draw random states and form the station balance functions in the "
1707 "log domain, neither of which exists in exact rational arithmetic; rerun with --arith "
1708 "double or --arith real");
1709 } else {
1710 // The run's own gates, in the run's order: the structural predicate
1711 // first, then the declared feature set.
1712 const std::string why = line::nc::solver_nc_cftp_supports(sn);
1713 if (!why.empty()) throw line::UnsupportedError(why);
1715 const line::nc::NcCftpSolution<T> s = line::nc::solver_nc_cftp(sn, opt, cftpopt);
1716 const line::mva::AvgResult<T> r =
1718 double horizon = 0.0;
1719 for (std::size_t i = 0; i < s.horizon.size(); ++i)
1720 horizon += static_cast<double>(s.horizon[i]);
1721 if (!s.horizon.empty()) horizon /= static_cast<double>(s.horizon.size());
1722 std::printf(
1723 "SolverNC arith=%s method=%s type=%s samples=%zu seed=%lu distinct=%zu "
1724 "meanhorizon=%.6g\n",
1726 line::util::method_type("NC", s.actualmethod).c_str(), cftpopt.samples,
1727 cftpopt.seed, s.distinct_states.size(), horizon);
1728 print_avg_table<T>(sn, r);
1729 return 0;
1730 }
1731}
1732
1733/**
1734 * Solve a Network model.json with SolverNC and print the same table.
1735 *
1736 * `--samples` and `--seed` are wired because NC has stochastic methods that read
1737 * them ('mci', 'imci', 'ls', 'is', 'sampling' and 'mcmc'), and a run length nobody
1738 * can set is a method nobody can drive. `highvar` still has no flag and keeps its
1739 * SolverOptions('NC') default rather than being invented here.
1740 */
1741template <class T>
1742int solve_model_nc(const std::string& file, const Knobs& k) {
1743 line::qn::Network<T> net = read_model<T>(file);
1745 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1746 if (k.tol >= 0.0) opt.tol = k.tol;
1747 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
1748 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1749 if (k.samples) opt.samples = k.samples;
1750 if (k.seed) opt.seed = k.seed;
1751 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
1752 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
1753 if (k.slotted) opt.slotted = true;
1754 if (k.slotlength > 0.0) {
1755 opt.slotted = true;
1756 opt.slotlength = k.slotlength;
1757 }
1758 // The perfect sampler reports its run length beside the table, so it is
1759 // served by its own arm. The dispatcher has already refused every analysis
1760 // but `avg` for it.
1761 if (opt.method == "cftp" || opt.method == "cftp.approx") {
1763 cftpopt.samples = opt.samples;
1764 cftpopt.seed = opt.seed;
1765 return solve_nc_cftp_avg<T>(net.get_struct(), opt, cftpopt);
1766 }
1767 // The map_env funnel, as on the mva arm above and for the same reason.
1769 if constexpr (std::is_same_v<T, double>) {
1771 net.get_struct(), "SolverNC", line::qn::nc_feature_set(opt.method), map_env_config(k),
1772 [&opt](const line::qn::NetworkStruct<double>& m) {
1773 return line::nc::solver_nc_run_analyzer(m, opt);
1774 },
1776 } else {
1778 }
1779 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1780
1781 // `lognormconst=` rides ON THE BANNER LINE rather than on one of its own,
1782 // as the fluid arm's `iters=` does: the banner is provenance and a new line
1783 // between it and the table is one more thing a table parser has to skip.
1784 // It is `getProbNormConstAggr`, which no other `-a` reports.
1785 std::printf("SolverNC arith=%s method=%s type=%s lognormconst=%.10g\n",
1787 line::util::method_type("NC", r.actualmethod).c_str(),
1788 r.lognormconst.has_value() ? r.lognormconst.value() : 0.0);
1789 print_avg_table<T>(sn, r);
1790 return 0;
1791}
1792
1793/**
1794 * Solve a Network model.json with SolverMAM and print the same table.
1795 *
1796 * DOUBLE ONLY, and refused by name otherwise in the dispatcher: the analyzer
1797 * fits phase-type representations (aph_fit), which static_asserts on
1798 * transcendental arithmetic, so an exact instantiation would fail to COMPILE
1799 * rather than refuse at run time. `MamOptions`' tol, iter_max, space_max and
1800 * preserveDet keep their SolverOptions('MAM') defaults.
1801 */
1802template <class T>
1803int solve_model_mam(const std::string& file, const Knobs& k) {
1804 line::qn::Network<T> net = read_model<T>(file);
1806 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1807 if (k.tol >= 0.0) opt.tol = k.tol;
1808 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1810 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1811
1812 std::printf("SolverMAM arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1813 r.actualmethod.c_str(),
1814 line::util::method_type("MAM", r.actualmethod).c_str());
1815 print_avg_table<T>(sn, r);
1816 return 0;
1817}
1818
1819/**
1820 * Solve a Network model.json with SolverAG and print the same table.
1821 *
1822 * The RCAT/INAP arm, reachable from the CLI rather than API-only. It was the
1823 * one engine `run_avg_engine` already served for `-a node` and its three
1824 * sibling views while `-a avg` -- the view every parity row and every wrapper
1825 * asks for first -- refused the token outright, so `-s ag` reported an argument
1826 * error where the solver was present and working.
1827 *
1828 * `--method` picks between inap, inaprc, inapinf and exact; `default`
1829 * resolves to inap inside the analyzer, as it does in every codebase. `--tol`
1830 * and `--iter_max` are the fixed point's, matching -s mam.
1831 */
1832/**
1833 * The AgOptions a command line asks for.
1834 *
1835 * ONE PLACE, because there are three call sites (-a avg, -a cdf and the
1836 * run_avg_engine views) and a knob added to only some of them is a knob that
1837 * works or not depending on which view was asked for.
1838 */
1839void apply_ag_knobs(const Knobs& k, line::ag::AgOptions& opt) {
1840 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1841 if (k.tol >= 0.0) opt.tol = k.tol;
1842 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1843 if (k.max_states > 0) opt.max_states = static_cast<std::size_t>(k.max_states);
1844}
1845
1846template <class T>
1847int solve_model_ag(const std::string& file, const Knobs& k) {
1848 line::qn::Network<T> net = read_model<T>(file);
1850 apply_ag_knobs(k, opt);
1852 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1853
1854 std::printf("SolverAG arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1855 r.actualmethod.c_str(),
1856 line::util::method_type("AG", r.actualmethod).c_str());
1857 print_avg_table<T>(sn, r);
1858 return 0;
1859}
1860
1861/**
1862 * The station a per-node MAM query is about: `--node` when given, and otherwise
1863 * the model's only Queue.
1864 *
1865 * Defaulting is legitimate here and nowhere else: `getProb`, `getProbMarg` and
1866 * `getMAMResult` all run `require_single_queue`, so a model that reaches them
1867 * has exactly ONE queue and there is nothing to choose. `--node` is still
1868 * accepted, because naming the node one means is clearer than relying on that.
1869 */
1870template <class T>
1871std::size_t mam_query_node(const line::qn::NetworkStruct<T>& sn, const Knobs& k) {
1872 if (k.node) return k.node;
1873 for (std::size_t a = 0; a < sn.nof_nodes(); ++a)
1874 if (sn.nodes[a].nodetype == line::qn::NodeType::Queue) return a + 1;
1875 throw line::InputError(
1876 "the MAM per-node analyses report a queue's internals and this model has no Queue node");
1877}
1878
1879/** The MamOptions every `-s mam` entry point builds from the CLI knobs. */
1880line::mam::MamOptions mam_options(const Knobs& k) {
1882 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1883 if (k.tol >= 0.0) opt.tol = k.tol;
1884 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1885 if (k.cutoff >= 0.0) opt.cutoff = static_cast<std::size_t>(k.cutoff);
1886 if (k.fj_accuracy > 0) opt.fj_accuracy = static_cast<std::size_t>(k.fj_accuracy);
1887 if (!k.fj_tmode.empty()) opt.fj_tmode = k.fj_tmode;
1888 if (!k.timescale.empty()) opt.timescale = k.timescale;
1889 if (k.slotlength > 0.0) opt.slotlength = k.slotlength;
1890 if (k.t1 >= 0.0) {
1891 opt.timespan_start = k.t0;
1892 opt.timespan_end = k.t1;
1893 }
1894 return opt;
1895}
1896
1897/**
1898 * `-s mam -a prob`: `@@SolverMAM/getProb` and `@@SolverMAM/getProbMarg`.
1899 *
1900 * BOTH, in one answer, because they are two views of the same queue-length law:
1901 * the joint (level, phase) table the first returns, and the per-class marginal
1902 * P(n jobs of class r) the second does. Reporting only one would leave the other
1903 * unreachable again, which is the state this wiring closes.
1904 *
1905 * `--cutoff` is the level truncation an OPEN model needs -- its queue length is
1906 * unbounded, so the table has to stop somewhere -- and is passed straight
1907 * through as `options.cutoff`; a closed model bounds itself by its population
1908 * and ignores it.
1909 */
1910template <class T>
1911int solve_model_mam_prob(const std::string& file, const Knobs& k) {
1912 line::qn::Network<T> net = read_model<T>(file);
1913 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1914 const line::mam::MamOptions opt = mam_options(k);
1915 // The reference's getters read `self.getAvg()` first, so the solve comes
1916 // before the query and a caller cannot reach them on an unsolved model.
1918 const std::size_t node = mam_query_node<T>(sn, k);
1919 const std::size_t ist = sn.nodes[node - 1].station;
1920 const line::mam::ProbTable<T> P = line::mam::solver_mam_get_prob(sn, opt, node, avg);
1921 std::vector<std::vector<T> > marg(sn.nclasses);
1922 for (std::size_t r = 0; r < sn.nclasses; ++r)
1923 marg[r] = line::mam::solver_mam_get_prob_marg(sn, opt, ist, r + 1, avg);
1924
1925 if (g_json_output) {
1926 line::reg::Json p = line::reg::Json::object();
1927 p["type"] = "ProbTable";
1928 p["indexBase"] = 0;
1929 p["node"] = node - 1;
1930 p["Node"] = sn.nodes[node - 1].name;
1931 p["levels"] = P.P.rows();
1932 p["phases"] = P.P.cols();
1933 line::reg::Json joint = line::reg::Json::array();
1934 for (std::size_t n = 0; n < P.P.rows(); ++n) {
1935 line::reg::Json row = line::reg::Json::array();
1936 for (std::size_t j = 0; j < P.P.cols(); ++j)
1937 row.push_back(line::num_traits<T>::to_double(P.P(n, j)));
1938 joint.push_back(row);
1939 }
1940 p["joint"] = joint;
1941 line::reg::Json mj = line::reg::Json::array();
1942 for (std::size_t r = 0; r < sn.nclasses; ++r) {
1943 line::reg::Json e = line::reg::Json::object();
1944 e["JobClass"] = sn.classes[r].name;
1945 e["jobclass"] = r;
1946 e["P"] = vector_json(marg[r]);
1947 mj.push_back(e);
1948 }
1949 p["marginal"] = mj;
1950 emit_analysis<T>("prob", p, avg.actualmethod);
1951 return 0;
1952 }
1953 std::printf("SolverMAM arith=%s method=%s node=%s levels=%zu phases=%zu\n",
1955 sn.nodes[node - 1].name.c_str(), P.P.rows(), P.P.cols());
1956 std::printf("%-8s %-8s %16s\n", "Level", "Phase", "Prob");
1957 for (std::size_t n = 0; n < P.P.rows(); ++n)
1958 for (std::size_t j = 0; j < P.P.cols(); ++j)
1959 std::printf("%-8zu %-8zu %16.10g\n", n, j + 1,
1961 std::printf("%-14s %-8s %16s\n", "JobClass", "Jobs", "Prob");
1962 for (std::size_t r = 0; r < sn.nclasses; ++r)
1963 for (std::size_t n = 0; n < marg[r].size(); ++n)
1964 std::printf("%-14s %-8zu %16.10g\n", sn.classes[r].name.c_str(), n,
1965 line::num_traits<T>::to_double(marg[r][n]));
1966 return 0;
1967}
1968
1969/**
1970 * The levels `getPerctRespT` is read at: `--percentiles`, or the reference's
1971 * `pers_stored` when the caller named none.
1972 */
1973std::vector<double> percentile_levels(const Knobs& k) {
1974 if (!k.percentiles.empty()) return k.percentiles;
1975 std::vector<double> pcts;
1976 pcts.push_back(0.50);
1977 pcts.push_back(0.90);
1978 pcts.push_back(0.95);
1979 pcts.push_back(0.99);
1980 return pcts;
1981}
1982
1983/**
1984 * `-s mam -a cdf`: `@@SolverMAM/getCdfRespT` (and its aliases getSjrnT / sjrnT),
1985 * with `@@SolverMAM/getPerctRespT` beside it.
1986 *
1987 * THE PERCENTILE LEVELS COME FROM `--percentiles`, defaulting to the
1988 * {0.50, 0.90, 0.95, 0.99} that `solver_mam_fj.m` stores as `pers_stored`: the
1989 * reference takes them as an argument to `getPerctRespT`, and the flag is that
1990 * argument. On the CDF path they are a READING of the same curve --
1991 * `mam_percentiles_from_cdf` inverts the CDF that is printed above them -- so a
1992 * level the caller names costs nothing beyond the inversion.
1993 *
1994 * A FORK-JOIN MODEL HAS NO CDF HERE, and that is the reference's structure
1995 * rather than a gap in this port: `getPerctRespT.m` reads the table
1996 * `solver_mam_fj.m` stored, and its own fallback comment records that
1997 * `getCdfRespT` is "not available for this model". The two are separate methods
1998 * in MATLAB and only this CLI bundles them, so the fork-join case prints the
1999 * percentiles alone and says why. The condition is tested explicitly, not
2000 * discovered by catching the CDF's refusal: a caught exception cannot tell "no
2001 * fork-join route exists" from "the passage-time engine failed on this model".
2002 */
2003template <class T>
2004int solve_model_mam_cdf(const std::string& file, const Knobs& k, const char* key,
2005 const char* type) {
2006 line::qn::Network<T> net = read_model<T>(file);
2007 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2008 const line::mam::MamOptions opt = mam_options(k);
2009 const std::vector<double> pcts = percentile_levels(k);
2010
2011 if (line::mam::mam_has_fj_percentiles(sn, opt)) {
2012 const std::vector<std::vector<T> > perc =
2014 if (g_json_output) {
2015 line::reg::Json p = line::reg::Json::object();
2016 p["type"] = "PerctRespT";
2017 p["indexBase"] = 0;
2018 p["source"] = "fjcodes";
2019 line::reg::Json arr = line::reg::Json::array();
2020 for (std::size_t r = 0; r < perc.size(); ++r) {
2021 line::reg::Json e = line::reg::Json::object();
2022 e["JobClass"] = sn.classes[r].name;
2023 e["jobclass"] = r;
2024 e["percentileLevels"] = pcts;
2025 e["percentiles"] = vector_json(perc[r]);
2026 arr.push_back(e);
2027 }
2028 p["respt"] = arr;
2029 emit_analysis<T>(key, p, opt.method);
2030 return 0;
2031 }
2032 std::printf("SolverMAM arith=%s method=%s classes=%zu\n", line::num_traits<T>::name(),
2033 opt.method.c_str(), sn.nclasses);
2034 std::printf("# the response-time CDF has no fork-join route in the reference; these are "
2035 "the FJ_codes percentiles getPerctRespT reads\n");
2036 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
2037 for (std::size_t r = 0; r < perc.size(); ++r)
2038 for (std::size_t j = 0; j < perc[r].size(); ++j)
2039 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
2040 line::num_traits<T>::to_double(perc[r][j]));
2041 return 0;
2042 }
2043
2044 const std::vector<line::mam::RespTCdf<T> > rd = line::mam::solver_mam_get_cdf_respt(sn, opt);
2045 std::vector<std::vector<T> > perc;
2046 for (std::size_t r = 0; r < rd.size(); ++r)
2047 perc.push_back(line::mam::mam_percentiles_from_cdf(rd[r], pcts));
2048
2049 if (g_json_output) {
2050 line::reg::Json p = line::reg::Json::object();
2051 p["type"] = type;
2052 p["indexBase"] = 0;
2053 line::reg::Json arr = line::reg::Json::array();
2054 for (std::size_t r = 0; r < rd.size(); ++r) {
2055 // An empty curve is not a degenerate one: the class has no passage
2056 // through a queue here, and it is OMITTED rather than sent as a flat
2057 // zero law.
2058 if (rd[r].X.empty()) continue;
2059 line::reg::Json e = line::reg::Json::object();
2060 e["JobClass"] = sn.classes[r].name;
2061 e["jobclass"] = r;
2062 e["t"] = vector_json(rd[r].X);
2063 e["F"] = vector_json(rd[r].F);
2064 e["percentileLevels"] = pcts;
2065 e["percentiles"] = vector_json(perc[r]);
2066 arr.push_back(e);
2067 }
2068 p["respt"] = arr;
2069 emit_analysis<T>(key, p, opt.method);
2070 return 0;
2071 }
2072 std::printf("SolverMAM arith=%s method=%s classes=%zu\n", line::num_traits<T>::name(),
2073 opt.method.c_str(), sn.nclasses);
2074 std::printf("%-14s %14s %14s\n", "JobClass", "Time", "F(t)");
2075 for (std::size_t r = 0; r < rd.size(); ++r)
2076 for (std::size_t j = 0; j < rd[r].X.size(); ++j)
2077 std::printf("%-14s %14.8g %14.10g\n", sn.classes[r].name.c_str(),
2079 line::num_traits<T>::to_double(rd[r].F[j]));
2080 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
2081 for (std::size_t r = 0; r < perc.size(); ++r)
2082 for (std::size_t j = 0; j < perc[r].size(); ++j)
2083 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
2084 line::num_traits<T>::to_double(perc[r][j]));
2085 return 0;
2086}
2087
2088/**
2089 * `-s mam -a perct-respt`: `@@SolverMAM/getPerctRespT` ALONE.
2090 *
2091 * The percentiles ride beside the curve under `-a cdf` as well, and this arm
2092 * exists because they are a separate METHOD in the reference and a separate
2093 * answer to a caller: a service-level question ("what is the 99th percentile")
2094 * wants four numbers, not the whole law printed above them. On a fork-join
2095 * model it is the only route -- `getCdfRespT` has none there -- and on every
2096 * other model the levels are inverted from the CDF the other arm prints, so
2097 * the two never disagree.
2098 */
2099template <class T>
2100int solve_model_mam_perct(const std::string& file, const Knobs& k) {
2101 line::qn::Network<T> net = read_model<T>(file);
2102 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2103 const line::mam::MamOptions opt = mam_options(k);
2104 const std::vector<double> pcts = percentile_levels(k);
2105
2106 std::vector<std::vector<T> > perc;
2107 const bool fj = line::mam::mam_has_fj_percentiles(sn, opt);
2108 if (fj) {
2109 perc = line::mam::solver_mam_get_perct_respt(sn, opt, pcts);
2110 } else {
2111 const std::vector<line::mam::RespTCdf<T> > rd =
2113 for (std::size_t r = 0; r < rd.size(); ++r)
2114 perc.push_back(line::mam::mam_percentiles_from_cdf(rd[r], pcts));
2115 }
2116
2117 if (g_json_output) {
2118 line::reg::Json p = line::reg::Json::object();
2119 p["type"] = "PerctRespT";
2120 p["indexBase"] = 0;
2121 p["source"] = fj ? "fjcodes" : "cdf";
2122 line::reg::Json arr = line::reg::Json::array();
2123 for (std::size_t r = 0; r < perc.size(); ++r) {
2124 if (perc[r].empty()) continue;
2125 line::reg::Json e = line::reg::Json::object();
2126 e["JobClass"] = sn.classes[r].name;
2127 e["jobclass"] = r;
2128 e["percentileLevels"] = pcts;
2129 e["percentiles"] = vector_json(perc[r]);
2130 arr.push_back(e);
2131 }
2132 p["respt"] = arr;
2133 emit_analysis<T>("perct", p, opt.method);
2134 return 0;
2135 }
2136 std::printf("SolverMAM arith=%s method=%s classes=%zu source=%s\n",
2137 line::num_traits<T>::name(), opt.method.c_str(), sn.nclasses,
2138 fj ? "fjcodes" : "cdf");
2139 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
2140 for (std::size_t r = 0; r < perc.size(); ++r)
2141 for (std::size_t j = 0; j < perc[r].size(); ++j)
2142 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
2143 line::num_traits<T>::to_double(perc[r][j]));
2144 return 0;
2145}
2146
2147/**
2148 * `-s mam -a tran`: `@@SolverMAM/getTranAvg`.
2149 *
2150 * The reference FORCES `options.method = 'ldqbd'` before delegating, so the
2151 * transient path is not the caller's method and `--method` does not select it;
2152 * which engine runs -- the Laplace-domain transient QBD or the QBD fast path --
2153 * is decided from the model by `mam_transient_qbd_applicable`. `--tspan` is
2154 * required: a transient curve on an unstated horizon is not a quantity.
2155 */
2156template <class T>
2157int solve_model_mam_tran(const std::string& file, const Knobs& k) {
2158 line::qn::Network<T> net = read_model<T>(file);
2159 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2160 const line::mam::MamOptions opt = mam_options(k);
2162
2163 if (g_json_output) {
2164 line::reg::Json p = line::reg::Json::object();
2165 p["type"] = "TranAvgTable";
2166 p["indexBase"] = 0;
2167 p["t0"] = opt.timespan_start;
2168 p["t1"] = opt.timespan_end;
2169 line::reg::Json arr = line::reg::Json::array();
2170 for (std::size_t i = 0; i < tr.Qt.size(); ++i)
2171 for (std::size_t r = 0; r < tr.Qt[i].size(); ++r) {
2172 if (tr.Qt[i][r].times.empty()) continue;
2173 line::reg::Json e = line::reg::Json::object();
2174 e["Station"] = sn.stations[i].name;
2175 e["JobClass"] = sn.classes[r].name;
2176 e["station"] = i;
2177 e["jobclass"] = r;
2178 e["t"] = tr.Qt[i][r].times;
2179 e["QLen"] = vector_json(tr.Qt[i][r].values);
2180 e["Util"] = vector_json(tr.Ut[i][r].values);
2181 e["Tput"] = vector_json(tr.Tt[i][r].values);
2182 arr.push_back(e);
2183 }
2184 p["curves"] = arr;
2185 emit_analysis<T>("tran", p, "ldqbd");
2186 return 0;
2187 }
2188 std::printf("SolverMAM arith=%s method=ldqbd tspan=[%g,%g]\n", line::num_traits<T>::name(),
2189 opt.timespan_start, opt.timespan_end);
2190 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
2191 "Tput");
2192 for (std::size_t i = 0; i < tr.Qt.size(); ++i)
2193 for (std::size_t r = 0; r < tr.Qt[i].size(); ++r)
2194 for (std::size_t j = 0; j < tr.Qt[i][r].times.size(); ++j)
2195 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
2196 sn.stations[i].name.c_str(), sn.classes[r].name.c_str(),
2197 tr.Qt[i][r].times[j],
2198 line::num_traits<T>::to_double(tr.Qt[i][r].values[j]),
2199 line::num_traits<T>::to_double(tr.Ut[i][r].values[j]),
2200 line::num_traits<T>::to_double(tr.Tt[i][r].values[j]));
2201 return 0;
2202}
2203
2204/**
2205 * `-s mam -a internals`: `@@SolverMAM/getMAMResult`, the M/G/1-type internals of
2206 * a single queue.
2207 *
2208 * It is NOT a metric table and is deliberately not folded into `-a avg`: the
2209 * answer is the matrix-analytic machinery itself -- the randomized blocks, the
2210 * G matrix, the drift, the decay rate, the level probabilities -- which is what
2211 * a caller checking a queue's stability or tail decay asks for, and what a
2212 * cross-codebase comparison of the QBD assembly needs to see.
2213 */
2214template <class T>
2215int solve_model_mam_internals(const std::string& file, const Knobs& k) {
2216 line::qn::Network<T> net = read_model<T>(file);
2217 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2218 (void)k;
2220 const double q = line::num_traits<T>::to_double(r.q);
2221
2222 if (g_json_output) {
2223 line::reg::Json p = line::reg::Json::object();
2224 p["type"] = "MAMResult";
2225 p["indexBase"] = 0;
2226 p["lambda"] = line::num_traits<T>::to_double(r.lambda);
2227 p["rho"] = line::num_traits<T>::to_double(r.rho);
2228 p["uniformization"] = q;
2229 p["drift"] = line::num_traits<T>::to_double(r.drift);
2230 p["decayRate"] = r.decayRate;
2231 p["pi0"] = line::num_traits<T>::to_double(r.pi0);
2235 p["truncLevel"] = r.truncLevel;
2236 p["truncError"] = r.truncError;
2237 p["gConverged"] = r.gConverged;
2238 p["theta"] = vector_json(r.theta);
2239 p["alpha"] = vector_json(r.alpha);
2240 line::reg::Json lv = line::reg::Json::array();
2241 for (std::size_t n = 0; n < r.levelProb.rows(); ++n) {
2242 line::reg::Json row = line::reg::Json::array();
2243 for (std::size_t j = 0; j < r.levelProb.cols(); ++j)
2244 row.push_back(line::num_traits<T>::to_double(r.levelProb(n, j)));
2245 lv.push_back(row);
2246 }
2247 p["levelProb"] = lv;
2248 emit_analysis<T>("internals", p, std::string());
2249 return 0;
2250 }
2251 std::printf("SolverMAM arith=%s stations=%zu\n", line::num_traits<T>::name(), sn.nstations);
2252 std::printf("lambda=%.10g rho=%.10g q=%.10g drift=%.10g decayRate=%.10g\n",
2255 std::printf("pi0=%.10g QLen=%.10g Util=%.10g Tput=%.10g truncLevel=%zu truncError=%.3g "
2256 "gConverged=%s\n",
2261 r.gConverged ? "yes" : "no");
2262 std::printf("%-8s %16s\n", "Level", "Prob");
2263 for (std::size_t n = 0; n < r.levelProb.rows(); ++n) {
2264 double s = 0.0;
2265 for (std::size_t j = 0; j < r.levelProb.cols(); ++j)
2267 std::printf("%-8zu %16.10g\n", n, s);
2268 }
2269 return 0;
2270}
2271
2272/**
2273 * Solve a Network model.json with SolverBA and print the same table.
2274 *
2275 * A BOUND, not an estimate: `--method` names which side of which hierarchy is
2276 * wanted (`aba.upper`, `gb.lower`, ...) and `default` resolves to `gb.upper`,
2277 * as in the reference. `options.level` keeps its default of 2. The table is
2278 * therefore not comparable with an exact solver's row except as a bracket,
2279 * which is why the parity row is separate.
2280 */
2281/** A JSON flag value, given inline or as the path of a file holding it. */
2282line::reg::Json read_json_arg(const std::string& spec, const char* flag) {
2283 std::string text = spec;
2284 const std::size_t at = spec.find_first_not_of(" \t\r\n");
2285 if (at == std::string::npos || (spec[at] != '{' && spec[at] != '[')) {
2286 std::ifstream in(spec.c_str());
2287 if (!in)
2288 throw line::InputError(std::string(flag) + " is neither inline JSON nor a readable "
2289 "file (got '" + spec + "')");
2290 text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
2291 }
2292 try {
2293 return line::reg::Json::parse(text);
2294 } catch (const line::reg::Json::parse_error& e) {
2295 throw line::InputError(std::string("malformed ") + flag + " JSON: " + e.what());
2296 }
2297}
2298
2299/** A JSON array of arrays as an integer table. */
2300std::vector<std::vector<int> > json_int_table(const line::reg::Json& j, const char* name) {
2301 if (!j.is_array())
2302 throw line::InputError(std::string("--qrf-params ") + name + " must be an array of rows");
2303 std::vector<std::vector<int> > out;
2304 for (std::size_t m = 0; m < j.size(); ++m) {
2305 if (!j[m].is_array())
2306 throw line::InputError(std::string("--qrf-params ") + name + " must be an array of "
2307 "rows");
2308 std::vector<int> row;
2309 for (std::size_t c = 0; c < j[m].size(); ++c) row.push_back(j[m][c].get<int>());
2310 out.push_back(row);
2311 }
2312 return out;
2313}
2314
2315/**
2316 * Decode `--qrf-params` into `BaOptions::qrf_params`.
2317 *
2318 * Required: f, MR, BB, MM, MM1, ZZ, exactly what `sn_to_qrf_params` demands.
2319 * F is optional and falls back to sn.cap. ZM is DERIVED from ZZ and a supplied
2320 * one is ignored: it is max(ZZ) by definition, and a larger one empties the
2321 * polytope through THM3I instead of failing cleanly.
2322 */
2323void decode_qrf_params(const std::string& spec, line::ba::BaOptions& opt) {
2324 const line::reg::Json j = read_json_arg(spec, "--qrf-params");
2325 if (!j.is_object()) throw line::InputError("--qrf-params must be a JSON object");
2326 const char* required[] = {"f", "MR", "BB", "MM", "MM1", "ZZ"};
2327 std::string missing;
2328 for (std::size_t i = 0; i < 6; ++i)
2329 if (!j.contains(required[i]))
2330 missing += (missing.empty() ? "" : ", ") + std::string(required[i]);
2331 if (!missing.empty())
2332 throw line::InputError("--qrf-params is missing the field(s) " + missing +
2333 "; required are f, MR, BB, MM, MM1, ZZ (F is optional, ZM is "
2334 "derived from ZZ)");
2336 p.supplied = true;
2337 p.f = j["f"].get<int>();
2338 p.MR = j["MR"].get<int>();
2339 p.BB = json_int_table(j["BB"], "BB");
2340 p.MM = json_int_table(j["MM"], "MM");
2341 p.MM1 = json_int_table(j["MM1"], "MM1");
2342 if (!j["ZZ"].is_array()) throw line::InputError("--qrf-params ZZ must be an array");
2343 for (std::size_t i = 0; i < j["ZZ"].size(); ++i) p.ZZ.push_back(j["ZZ"][i].get<int>());
2344 if (j.contains("F"))
2345 for (std::size_t i = 0; i < j["F"].size(); ++i) p.F.push_back(j["F"][i].get<int>());
2346 opt.qrf_params = p;
2347}
2348
2349/** Decode `--qrf-alpha`, the (nstations x N) load-dependent scaling. */
2350void decode_qrf_alpha(const std::string& spec, line::ba::BaOptions& opt) {
2351 const line::reg::Json j = read_json_arg(spec, "--qrf-alpha");
2352 if (!j.is_array() || j.empty() || !j[0].is_array())
2353 throw line::InputError("--qrf-alpha must be a JSON array of rows, one per station");
2354 line::Matrix<double> a(j.size(), j[0].size(), 0.0);
2355 for (std::size_t i = 0; i < j.size(); ++i) {
2356 if (!j[i].is_array() || j[i].size() != j[0].size())
2357 throw line::InputError("--qrf-alpha rows must all have the same length");
2358 for (std::size_t n = 0; n < j[i].size(); ++n) a(i, n) = j[i][n].get<double>();
2359 }
2360 opt.qrf_alpha = a;
2361}
2362
2363/** The knobs the two SolverBA arms share. */
2364void apply_ba_knobs(const Knobs& k, line::ba::BaOptions& opt) {
2365 if (!k.method.empty() && k.method != "default") opt.method = k.method;
2366 if (k.level > 0) opt.level = k.level;
2367 if (!k.qrf_params.empty()) decode_qrf_params(k.qrf_params, opt);
2368 if (!k.qrf_alpha.empty()) decode_qrf_alpha(k.qrf_alpha, opt);
2369}
2370
2371template <class T>
2372int solve_model_ba(const std::string& file, const Knobs& k) {
2373 line::qn::Network<T> net = read_model<T>(file);
2375 apply_ba_knobs(k, opt);
2377 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2378
2379 std::printf("SolverBA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
2380 r.actualmethod.c_str(),
2381 line::util::method_type("BA", r.actualmethod).c_str());
2382 print_avg_table<T>(sn, r);
2383 return 0;
2384}
2385
2386/**
2387 * `-s ba -a bounds`: `SolverBA.getBounds` and its `getBoundsTable` row filter.
2388 *
2389 * NOT THE SAME ANSWER AS `-a avg`, which is why it is its own analysis. `-a avg`
2390 * reports ONE side of ONE family -- whichever `--method` named -- so a caller who
2391 * wants the bracket has to run the solver twice and know which two method names
2392 * pair up. `getBounds` takes the family (the method's prefix before the first
2393 * dot) and re-runs both sides under the caller's FULL option set, so a
2394 * hierarchical family tightens with `--level` as it should.
2395 *
2396 * A ONE-SIDED FAMILY REPORTS NaN ON THE SIDE IT LACKS, never zero: `cub` is
2397 * upper-only and `mbjb`/`ldac` are lower-only, and a zero there would read as
2398 * a lower bound of zero rather than as the absence of one.
2399 */
2400template <class T>
2401int solve_model_ba_bounds(const std::string& file, const Knobs& k) {
2402 line::qn::Network<T> net = read_model<T>(file);
2404 apply_ba_knobs(k, opt);
2405 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2406 const line::ba::BaBounds<T> b = line::ba::ba_bounds(sn, opt);
2407 const std::string am = line::ba::resolve_method(opt.method);
2408
2409 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
2410 if (g_json_output) {
2411 line::reg::Json p = line::reg::Json::object();
2412 p["type"] = "BoundsTable";
2413 p["indexBase"] = 0;
2414 p["family"] = am.substr(0, am.find('.'));
2415 p["hasLower"] = b.has_lower;
2416 p["hasUpper"] = b.has_upper;
2417 for (const char* key : {"Station", "JobClass", "QLower", "QUpper", "TLower", "TUpper"})
2418 p[key] = line::reg::Json::array();
2419 for (std::size_t i = 0; i < sn.nstations; ++i)
2420 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2421 if (!b.keep[i][c]) continue;
2422 p["Station"].push_back(sn.stations[i].name);
2423 p["JobClass"].push_back(sn.classes[c].name);
2424 p["QLower"].push_back(d(b.Qlower(i, c)));
2425 p["QUpper"].push_back(d(b.Qupper(i, c)));
2426 p["TLower"].push_back(d(b.Tlower(i, c)));
2427 p["TUpper"].push_back(d(b.Tupper(i, c)));
2428 }
2429 emit_analysis<T>("bounds", p, am);
2430 return 0;
2431 }
2432 std::printf("SolverBA arith=%s method=%s type=%s family=%s sides=%s\n",
2433 line::num_traits<T>::name(), am.c_str(),
2434 line::util::method_type("BA", am).c_str(), am.substr(0, am.find('.')).c_str(),
2435 b.has_lower ? (b.has_upper ? "lower,upper" : "lower") : "upper");
2436 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "QLower", "QUpper",
2437 "TLower", "TUpper");
2438 for (std::size_t i = 0; i < sn.nstations; ++i)
2439 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2440 if (!b.keep[i][c]) continue;
2441 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", sn.stations[i].name.c_str(),
2442 sn.classes[c].name.c_str(), d(b.Qlower(i, c)), d(b.Qupper(i, c)),
2443 d(b.Tlower(i, c)), d(b.Tupper(i, c)));
2444 }
2445 return 0;
2446}
2447
2448/**
2449 * `-s lqns` on a flat Network: its qns methods, the model handed to the external
2450 * `qnsolver` binary (or, closed and not product-form, through QN2LQN to lqns).
2451 *
2452 * The only wrapper on the model-solving path. The banner names it separately
2453 * from the table so a reader can tell an independent tool's numbers from the
2454 * port's own -- which is the whole reason the wrapper exists.
2455 */
2456template <class T>
2457int solve_model_lqns_network(const std::string& file, const Knobs& k) {
2458 line::qn::Network<T> net = read_model<T>(file);
2460 if (!k.method.empty()) opt.method = k.method;
2461 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
2462 // --samples is deliberately NOT forwarded: `options.samples` reaches the
2463 // JMVA document as `maxSamples`, which is JMT's Monte Carlo cap and which
2464 // qnsolver reads past. The shared knob ladder below refuses it for every
2465 // solver that draws nothing, and this path is one of them.
2466 opt.timeout = k.timeout_seconds;
2467 opt.keep = k.keep;
2469 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2470
2471 if (!g_json_output)
2472 std::printf("SolverLQNS arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
2473 r.actualmethod.c_str(),
2474 line::util::method_type("LQNS", r.actualmethod).c_str());
2475 print_avg_table<T>(sn, r);
2476 return 0;
2477}
2478
2479/**
2480 * Emit the base-class exponential response-time CDF fallback, the same
2481 * `CdfRespT` document every real distributional arm emits.
2482 *
2483 * This is `@@NetworkSolver/getCdfRespT.m`: the solvers without a
2484 * distributional result of their own (MVA, LQNS, BA, AG) inherit an exponential
2485 * law with the right mean in the reference, and refusing `-a cdf` for them
2486 * diverged from it. The curve says nothing about the tail; the banner names
2487 * the solver so the caller knows which mean it wraps.
2488 */
2489template <class T>
2490int emit_default_cdf(const char* solver_name, const line::qn::NetworkStruct<T>& sn,
2491 const line::mva::AvgResult<T>& r) {
2492 const std::vector<std::vector<line::solvers::DefaultCdfCurve>> RD =
2494 if (g_json_output) {
2495 line::reg::Json p = line::reg::Json::object();
2496 p["type"] = "CdfRespT";
2497 p["indexBase"] = 0;
2498 line::reg::Json arr = line::reg::Json::array();
2499 for (std::size_t i = 0; i < RD.size(); ++i)
2500 for (std::size_t c = 0; c < RD[i].size(); ++c) {
2501 if (RD[i][c].t.empty()) continue;
2502 line::reg::Json e = line::reg::Json::object();
2503 e["Station"] = sn.stations[i].name;
2504 e["JobClass"] = sn.classes[c].name;
2505 e["station"] = i;
2506 e["jobclass"] = c;
2507 e["t"] = line::reg::Json(RD[i][c].t);
2508 e["F"] = line::reg::Json(RD[i][c].F);
2509 arr.push_back(e);
2510 }
2511 p["respt"] = arr;
2512 emit_analysis<T>("cdf", p, std::string());
2513 return 0;
2514 }
2515 std::printf("%s arith=%s method=%s (exponential fallback with the solver's mean)\n",
2516 solver_name, line::num_traits<T>::name(), r.actualmethod.c_str());
2517 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
2518 for (std::size_t i = 0; i < RD.size(); ++i)
2519 for (std::size_t c = 0; c < RD[i].size(); ++c)
2520 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
2521 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
2522 sn.classes[c].name.c_str(), RD[i][c].t[j], RD[i][c].F[j]);
2523 return 0;
2524}
2525
2526/** `-s mva -a cdf`: the inherited exponential fallback over the MVA means. */
2527template <class T>
2528int solve_model_mva_cdf(const std::string& file, const Knobs& k) {
2529 line::qn::Network<T> net = read_model<T>(file);
2531 if (!k.method.empty() && k.method != "default") opt.method = k.method;
2532 if (k.tol >= 0.0) opt.tol = k.tol;
2533 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
2534 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
2535 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
2536 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
2537 line::Matrix<T> init;
2539 return emit_default_cdf<T>("SolverMVA", net.get_struct(), r);
2540}
2541
2542/** `-s ag -a cdf`: the inherited exponential fallback over the RCAT means. */
2543template <class T>
2544int solve_model_ag_cdf(const std::string& file, const Knobs& k) {
2545 line::qn::Network<T> net = read_model<T>(file);
2547 apply_ag_knobs(k, opt);
2549 return emit_default_cdf<T>("SolverAG", net.get_struct(), r);
2550}
2551
2552/** `-s ba -a cdf`: the inherited exponential fallback over the bound means. */
2553template <class T>
2554int solve_model_ba_cdf(const std::string& file, const Knobs& k) {
2555 line::qn::Network<T> net = read_model<T>(file);
2557 apply_ba_knobs(k, opt);
2559 return emit_default_cdf<T>("SolverBA", net.get_struct(), r);
2560}
2561
2562/** `-s lqns -a cdf` on a flat Network: the inherited exponential fallback over qnsolver's means. */
2563int solve_model_lqns_network_cdf(const std::string& file, const Knobs& k) {
2564 line::qn::Network<double> net = read_model<double>(file);
2566 if (!k.method.empty()) opt.method = k.method;
2567 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
2568 opt.timeout = k.timeout_seconds;
2569 opt.keep = k.keep;
2572 return emit_default_cdf<double>("SolverLQNS", net.get_struct(), r);
2573}
2574
2575
2576/**
2577 * `-a interval`: the support-only range, the reference's `getIntervalTable`.
2578 *
2579 * A SEPARATE FUNCTION because it is printed before the ensemble exists: on the
2580 * exact path there is no design to solve, so the interval arrives without a
2581 * `UqSolution` beside it.
2582 */
2583template <class T>
2584int print_uq_interval(const line::qn::NetworkStruct<T>& sn, const line::uq::UqInterval<T>& iv,
2585 const std::string& stage, std::size_t npriors) {
2586 auto v = [](const line::Matrix<T>& M, std::size_t i, std::size_t c) {
2587 return M.empty() ? 0.0 : line::num_traits<T>::to_double(M(i, c));
2588 };
2589 line::reg::Json p = line::reg::Json::object();
2590 p["type"] = "IntervalTable";
2591 p["indexBase"] = 0;
2592 p["exact"] = iv.exact;
2593 p["intervalMethod"] = iv.method;
2594 if (!iv.why.empty()) p["why"] = iv.why;
2595 if (iv.has_totals) {
2596 p["X"] = {line::num_traits<T>::to_double(iv.Xlo),
2598 p["Rtot"] = {line::num_traits<T>::to_double(iv.Rtot_lo),
2600 }
2601 for (const char* key : {"Station", "JobClass", "QLen_lo", "QLen_up", "Util_lo", "Util_up",
2602 "RespT_lo", "RespT_up", "Tput_lo", "Tput_up"})
2603 p[key] = line::reg::Json::array();
2604 if (!g_json_output) {
2605 std::printf("SolverUQ arith=%s interval=%s exact=%s priors=%zu stage=%s\n",
2606 line::num_traits<T>::name(), iv.method.c_str(), iv.exact ? "yes" : "no",
2607 npriors, stage.c_str());
2608 // A RANGE THAT IS NOT AN ENCLOSURE MUST SAY SO. The sampled path
2609 // spans the design points only, so on a continuous Prior it lies
2610 // strictly inside the true range; printing it beside an exact hull
2611 // without the reason would make the two indistinguishable.
2612 if (!iv.exact)
2613 std::fprintf(stderr,
2614 "Warning: exact interval MVA does not apply (%s); the range below is "
2615 "over the solved design points and is not an enclosure.\n",
2616 iv.why.c_str());
2617 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s %12s %12s\n", "Station",
2618 "JobClass", "QLen_lo", "QLen_up", "Util_lo", "Util_up", "RespT_lo",
2619 "RespT_up", "Tput_lo", "Tput_up");
2620 }
2621 for (std::size_t i = 0; i < sn.nstations; ++i)
2622 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2623 // The reference's row filter: an upper endpoint of zero on every
2624 // presence metric means the class never visits the station.
2625 if (v(iv.Qup, i, c) <= 0.0 && v(iv.Uup, i, c) <= 0.0 && v(iv.Tup, i, c) <= 0.0)
2626 continue;
2627 if (!g_json_output) {
2628 std::printf(
2629 "%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
2630 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), v(iv.Qlo, i, c),
2631 v(iv.Qup, i, c), v(iv.Ulo, i, c), v(iv.Uup, i, c), v(iv.Rlo, i, c),
2632 v(iv.Rup, i, c), v(iv.Tlo, i, c), v(iv.Tup, i, c));
2633 continue;
2634 }
2635 p["Station"].push_back(sn.stations[i].name);
2636 p["JobClass"].push_back(sn.classes[c].name);
2637 p["QLen_lo"].push_back(v(iv.Qlo, i, c));
2638 p["QLen_up"].push_back(v(iv.Qup, i, c));
2639 p["Util_lo"].push_back(v(iv.Ulo, i, c));
2640 p["Util_up"].push_back(v(iv.Uup, i, c));
2641 p["RespT_lo"].push_back(v(iv.Rlo, i, c));
2642 p["RespT_up"].push_back(v(iv.Rup, i, c));
2643 p["Tput_lo"].push_back(v(iv.Tlo, i, c));
2644 p["Tput_up"].push_back(v(iv.Tup, i, c));
2645 }
2646 if (g_json_output) emit_analysis<T>("interval", p, iv.method);
2647 return 0;
2648}
2649
2650/**
2651 * Solve a Network model.json carrying a Prior with SolverUQ.
2652 *
2653 * TWO FLAGS MEAN SOMETHING ELSE HERE, and both are UQ's own rather than the
2654 * stage solver's: `--method` names the DESIGN (quad or mc, the
2655 * reference's `options.method`), and `--samples` the number of nodes per
2656 * continuous Prior (`options.samples`, 11 by default and not the simulation
2657 * default, since each node is a full solver run). The engine that runs at each
2658 * point is `--uq-solver`, and it keeps its own defaults for everything except
2659 * the convergence knobs, which UQ does not have and therefore passes through.
2660 * A caller who wants an SSA run length AND a UQ design cannot state both, so
2661 * the SSA stage keeps its default sample count; that is stated in --help rather
2662 * than resolved by giving one flag two meanings.
2663 *
2664 * `-a posterior` prints the design itself -- every point, its weight and its
2665 * metrics -- because the expectation alone hides whether it averaged two nearby
2666 * models or two wildly different ones, and that spread IS the answer to an
2667 * uncertainty question.
2668 */
2669template <class T>
2670int solve_model_uq(const std::string& file, const Knobs& k, const std::string& analysis) {
2671 line::qn::Network<T> net = read_model<T>(file);
2673 if (!k.method.empty()) opt.method = k.method;
2674 if (k.samples) opt.samples = k.samples;
2675 if (k.seed) opt.seed = k.seed;
2677 so.solver = k.uq_solver;
2678 so.tol = k.tol;
2679 so.iter_tol = k.iter_tol;
2680 so.iter_max = k.iter_max;
2681 so.cutoff = k.cutoff;
2682
2683 if (analysis == "interval") {
2684 // BEFORE THE ENSEMBLE, because the exact path does not need one: it is
2685 // 2*(m+2) MVA calls over the demand box, and the reference's
2686 // `getInterval` likewise reaches `intervalByMVA` without touching
2687 // `self.results`. Only the sampling fallback solves the design.
2688 const line::uq::UqInterval<T> iv =
2690 const line::qn::NetworkStruct<T>& isn = net.get_struct();
2691 return print_uq_interval<T>(isn, iv, so.solver, line::uq::uq_detect_priors(isn).size());
2692 }
2693
2694 const line::uq::UqSolution<T> r =
2696 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2697
2698 if (analysis == "avg") {
2699 if (!g_json_output)
2700 std::printf("SolverUQ arith=%s design=%s points=%zu priors=%zu stage=%s method=%s\n",
2701 line::num_traits<T>::name(), r.method.c_str(), r.points.size(),
2702 r.sites.size(), so.solver.c_str(), r.avg.actualmethod.c_str());
2703 print_avg_table<T>(sn, r.avg);
2704 return 0;
2705 }
2706
2707 // -a posterior: the per-design-point table, the reference's getPosteriorTable.
2708 line::reg::Json p = line::reg::Json::object();
2709 p["type"] = "PosteriorTable";
2710 p["indexBase"] = 0;
2711 p["design"] = r.method;
2712 p["stage"] = so.solver;
2713 line::reg::Json sites = line::reg::Json::array();
2714 for (std::size_t l = 0; l < r.sites.size(); ++l) {
2715 line::reg::Json s = line::reg::Json::object();
2716 s["node"] = sn.nodes[r.sites[l].node - 1].name;
2717 s["class"] = sn.classes[r.sites[l].cls - 1].name;
2718 s["kind"] = r.sites[l].arrival ? "arrival" : "service";
2719 sites.push_back(s);
2720 }
2721 p["priors"] = sites;
2722 for (const char* key : {"Point", "Weight", "Station", "JobClass", "QLen", "Util", "RespT",
2723 "Tput"})
2724 p[key] = line::reg::Json::array();
2725 line::reg::Json means = line::reg::Json::array();
2726
2727 if (!g_json_output) {
2728 std::printf("SolverUQ arith=%s design=%s points=%zu priors=%zu stage=%s\n",
2729 line::num_traits<T>::name(), r.method.c_str(), r.points.size(), r.sites.size(),
2730 so.solver.c_str());
2731 // THE SUBSTITUTED MEANS ARE PROVENANCE, not decoration: a design point
2732 // is a model, and this line is what says which one.
2733 std::printf("%-6s %12s substituted means\n", "Point", "Weight");
2734 for (std::size_t e = 0; e < r.points.size(); ++e) {
2735 std::printf("%-6zu %12.6g ", e + 1, line::num_traits<T>::to_double(r.weights[e]));
2736 for (std::size_t l = 0; l < r.sites.size(); ++l)
2737 std::printf(" %s@%s=%.6g", sn.nodes[r.sites[l].node - 1].name.c_str(),
2738 sn.classes[r.sites[l].cls - 1].name.c_str(),
2739 line::num_traits<T>::to_double(r.design[e].dists[l].mean));
2740 std::printf("\n");
2741 }
2742 std::printf("%-6s %12s %-16s %-14s %12s %12s %12s %12s\n", "Point", "Weight", "Station",
2743 "JobClass", "QLen", "Util", "RespT", "Tput");
2744 }
2745 for (std::size_t e = 0; e < r.points.size(); ++e) {
2746 line::reg::Json row = line::reg::Json::array();
2747 for (std::size_t l = 0; l < r.sites.size(); ++l)
2748 row.push_back(line::num_traits<T>::to_double(r.design[e].dists[l].mean));
2749 means.push_back(row);
2750 const line::mva::AvgResult<T>& a = r.points[e];
2751 for (std::size_t i = 0; i < sn.nstations; ++i)
2752 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2753 const double q = line::num_traits<T>::to_double(a.QN(i, c));
2754 const double u = line::num_traits<T>::to_double(a.UN(i, c));
2755 const double rr = line::num_traits<T>::to_double(a.RN(i, c));
2756 const double t = line::num_traits<T>::to_double(a.TN(i, c));
2757 // getPosteriorTable's row filter, the PRESENCE metrics only: a
2758 // response time alone does not put a class at a station, and
2759 // reading it as presence kept rows the reference drops.
2760 if (q <= 0.0 && u <= 0.0 && t <= 0.0) continue;
2761 if (!g_json_output) {
2762 std::printf("%-6zu %12.6g %-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", e + 1,
2764 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), q, u, rr,
2765 t);
2766 continue;
2767 }
2768 p["Point"].push_back(e);
2769 p["Weight"].push_back(line::num_traits<T>::to_double(r.weights[e]));
2770 p["Station"].push_back(sn.stations[i].name);
2771 p["JobClass"].push_back(sn.classes[c].name);
2772 p["QLen"].push_back(q);
2773 p["Util"].push_back(u);
2774 p["RespT"].push_back(rr);
2775 p["Tput"].push_back(t);
2776 }
2777 }
2778 if (g_json_output) {
2779 p["substitutedMean"] = means;
2780 emit_analysis<T>("posterior", p, r.avg.actualmethod);
2781 }
2782 return 0;
2783}
2784
2785/**
2786 * The banner every `-s ctmc` analysis opens with.
2787 *
2788 * It carries the state count and the cutoff because a CTMC number is not the
2789 * model's number without them: the space is what was enumerated, and on an open
2790 * model the cutoff is what truncated it.
2791 */
2792template <class T>
2793void print_ctmc_banner(const line::ctmc::CtmcSolution<T>& d) {
2794 std::size_t cut = 0;
2795 for (std::size_t i = 0; i < d.cutoff.size(); ++i) cut = std::max(cut, d.cutoff[i]);
2796 if (cut)
2797 std::printf("SolverCTMC arith=%s method=%s type=%s states=%zu cutoff=%zu\n",
2799 line::util::method_type("CTMC", d.actualmethod).c_str(), d.chain.space.size(),
2800 cut);
2801 else
2802 std::printf("SolverCTMC arith=%s method=%s type=%s states=%zu\n",
2804 line::util::method_type("CTMC", d.actualmethod).c_str(), d.chain.space.size());
2805 // The library never writes to stderr, so an unseeded reducible mixture is
2806 // reported here or not at all -- and it is the one case where the printed
2807 // distribution is not the model's.
2808 if (!d.warning.empty()) std::fprintf(stderr, "warning: %s\n", d.warning.c_str());
2809}
2810
2811/**
2812 * The banner's own content as JSON, added to every `-s ctmc` payload.
2813 *
2814 * A CTMC number is not the model's number without them, which is why the
2815 * readable banner carries them: the space is what was actually enumerated, and
2816 * on an open model the cutoff is what truncated it. A host that reported the
2817 * occupancy of a truncated chain as the model's would be reporting a different
2818 * model's answer, so the two travel with the payload rather than only above it.
2819 */
2820template <class T>
2821line::reg::Json ctmc_meta(const line::ctmc::CtmcSolution<T>& d) {
2822 line::reg::Json m = line::reg::Json::object();
2823 m["states"] = d.chain.space.size();
2824 std::size_t cut = 0;
2825 for (std::size_t i = 0; i < d.cutoff.size(); ++i) cut = std::max(cut, d.cutoff[i]);
2826 if (cut) m["cutoff"] = cut;
2827 return m;
2828}
2829
2830/**
2831 * `-a avg`: the AvgTable, on whichever path the model's regions require.
2832 *
2833 * `solver_ctmc_analyzer_any` and not `solver_ctmc_analyzer`, so a WAITQ region
2834 * reaches the augmented walk that carries its token FIFO instead of the
2835 * lattice analyzer, which refuses it. Every other region rule keeps refusing.
2836 */
2837template <class T>
2838int solve_ctmc_avg(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
2841 print_ctmc_banner<T>(a.sol);
2842 print_avg_table<T>(sn, r);
2843 // A PARKED JOB IS IN NO QLen COLUMN: it left its station and sits in the
2844 // region's FIFO, so the model's population balances only once this is read
2845 // alongside the table rather than instead of it.
2846 if (a.waitq) {
2847 std::printf("%-16s %-14s %12s\n", "Region", "JobClass", "Parked");
2848 for (std::size_t c = 0; c < sn.nclasses && c < a.parked.size(); ++c)
2849 std::printf("%-16s %-14s %12.6g\n", "(all)", sn.classes[c].name.c_str(),
2851 }
2852 return 0;
2853}
2854
2855/**
2856 * `-a avg` under `--method mdd`: the AvgTable from the level aggregation.
2857 *
2858 * A SEPARATE ARM, and not a branch inside `solve_ctmc_avg`, because the method
2859 * never forms the |S|-state generator: there is no state space to print a state
2860 * count from and no cutoff to report, so the banner is the diagram's own -- the
2861 * cardinality it counts symbolically, what the levels actually hold, and whether
2862 * the answer is certified exact. Reporting the generator banner here would name
2863 * a chain that was never built.
2864 */
2865template <class T>
2866int solve_ctmc_mdd_avg(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
2867 const line::mdd::MddMcdOptions& mcdopt) {
2868 // EVERY BACKEND, `exact` included: the level solve picks Householder QR in
2869 // floating point and `line::lstsq` under exact arithmetic, so nothing here
2870 // takes a square root that a rational field has no answer for.
2873 d.avg = s.avg;
2876 std::size_t held = 0;
2877 for (std::size_t k = 0; k < s.level_sizes.size(); ++k) held += s.level_sizes[k];
2878 std::printf(
2879 "SolverCTMC arith=%s method=%s type=%s states=%lld held=%zu levels=%zu iters=%d "
2880 "encoding=%s exact=%s\n",
2882 line::util::method_type("CTMC", s.actualmethod).c_str(), s.num_states, held,
2883 s.level_sizes.size(), s.iters, s.encoding.c_str(),
2884 // "certified" is not "exact": a product-form model is exact however
2885 // much its diagram shares, so the false case says only that the
2886 // STRUCTURAL test did not fire.
2887 s.no_aggregation ? "certified" : "product-form-only");
2888 print_avg_table<T>(sn, r);
2889 return 0;
2890}
2891
2892/**
2893 * `-a prob`: the four SolverCTMC probability queries over the model's default
2894 * initial state -- `getProbSys`, `getProbSysAggr`, and `getProb`/`getProbAggr`
2895 * per station.
2896 *
2897 * ALL FOUR AND NOT ONE, because the joint and the aggregate answer different
2898 * questions and the pair is what makes either readable: `getProbSys` is the
2899 * probability of exactly that state, phases and buffer arrangement included,
2900 * while `getProbSysAggr` sums over every arrangement realizing the same
2901 * per-class counts. On a single-phase model with no buffer ordering the two
2902 * coincide, and where they do not the ratio is what the encoding added.
2903 *
2904 * NOT THE SAME NUMBERS AS `-s mva -a prob` OR `-s nc -a prob`, and that is the
2905 * point of having all three: MVA fits a binomial to its own means and NC takes a
2906 * ratio of normalizing constants under the product form, whereas these are the
2907 * stationary law of the chain itself and are exact for any model the chain
2908 * represents, product-form or not.
2909 */
2910template <class T>
2911int solve_ctmc_prob(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
2912 const Knobs& k) {
2915 if (!line::ctmc::analyzer_detail::default_init_state(sn, init))
2917 "-a prob reports the probability of the model's DEFAULT INITIAL STATE, and this "
2918 "model's initial marking admits no state; check the class populations against their "
2919 "reference stations");
2920 // `--state` is `getProb(node, state)`'s second argument: the ENCODED ROW of
2921 // one stateful node's own state space, not a per-class job count. The
2922 // reference substitutes it into `sn.state{node}` and leaves every other
2923 // node at its default, which is what happens here -- so the station
2924 // marginals below are the requested state's, and the two system
2925 // probabilities are that state's joined with the rest of the default
2926 // marking, exactly as `setState` followed by `getProbSys` reports it.
2927 if (!k.state.empty()) {
2928 if (!k.node)
2929 throw line::InputError(
2930 "--state is the state of ONE node and needs --node to say which; a bare state "
2931 "vector cannot be matched against a network whose nodes have different widths");
2932 const std::size_t isf = sn.stateful_index(k.node);
2933 if (isf == 0)
2934 throw line::InputError("--node " + std::to_string(k.node) +
2935 " is not a stateful node, so it holds no state to ask about");
2936 // THE WIDTH MUST MATCH EXACTLY, and a short vector is refused rather
2937 // than padded. Every row of a node's block is stored at the node's
2938 // widest encoding, so a padded row IS a state -- just not the one the
2939 // caller named: on an FCFS queue holding two jobs the encoding is
2940 // (buffer, in-service phase count) = (1,1), and padding `--state 2` to
2941 // (0,2) names a state the chain never visits, which would answer 0
2942 // where the caller expected the marginal. The width is reported so the
2943 // next attempt can be right.
2944 const std::size_t w = d.chain.space.empty() ? k.state.size()
2945 : d.chain.space[0].local[isf - 1].size();
2946 if (k.state.size() != w)
2947 throw line::InputError(
2948 "--state has " + std::to_string(k.state.size()) + " entries but node " +
2949 std::to_string(k.node) + " encodes its state in " + std::to_string(w) +
2950 "; getProb(node, state) takes the node's whole encoded row, and a shorter one "
2951 "padded with zeros is a different state rather than a partial one (use -a marg "
2952 "for a per-class job-count marginal)");
2953 std::vector<T> row(w, line::num_traits<T>::from_int(0));
2954 for (std::size_t i = 0; i < k.state.size(); ++i)
2955 row[i] = line::num_traits<T>::from_int(k.state[i]);
2956 init.local[isf - 1] = row;
2957 }
2958 const T psys = line::ctmc::solver_ctmc_joint(sn, d, init);
2959 const T psysaggr = line::ctmc::solver_ctmc_jointaggr(sn, d, init);
2960 const std::vector<T> pmarg = line::ctmc::solver_ctmc_marg(sn, d, init);
2961 const std::vector<T> pmargaggr = line::ctmc::solver_ctmc_margaggr(sn, d, init);
2962
2963 if (g_json_output) {
2964 line::reg::Json p = line::reg::Json::object();
2965 p["type"] = "ProbAggr";
2966 p["indexBase"] = 0;
2967 p["ProbSys"] = line::num_traits<T>::to_double(psys);
2968 p["ProbSysAggr"] = line::num_traits<T>::to_double(psysaggr);
2969 line::reg::Json st = line::reg::Json::array(), pm = line::reg::Json::array(),
2970 pa = line::reg::Json::array();
2971 for (std::size_t i = 0; i < sn.nstations; ++i) {
2972 st.push_back(sn.stations[i].name);
2973 pm.push_back(line::num_traits<T>::to_double(pmarg[i]));
2974 pa.push_back(line::num_traits<T>::to_double(pmargaggr[i]));
2975 }
2976 p["Station"] = st;
2977 p["Prob"] = pm;
2978 p["ProbAggr"] = pa;
2979 emit_analysis<T>("prob", p, d.actualmethod, ctmc_meta<T>(d));
2980 return 0;
2981 }
2982 print_ctmc_banner<T>(d);
2983 std::printf("ProbSys %.10g\n", line::num_traits<T>::to_double(psys));
2984 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(psysaggr));
2985 std::printf("%-16s %14s %14s\n", "Station", "Prob", "ProbAggr");
2986 for (std::size_t i = 0; i < sn.nstations; ++i)
2987 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(),
2989 line::num_traits<T>::to_double(pmargaggr[i]));
2990 return 0;
2991}
2992
2993/**
2994 * `-a gen`: `getInfGen`, as Q in sparse triplets plus the synchronization list
2995 * its event filtration is indexed by.
2996 *
2997 * Q IS PRINTED SPARSELY. A generator's rows hold one entry per enabled
2998 * transition and the space can run to thousands of states, so the dense form
2999 * would be quadratic in a quantity that is linear in the model.
3000 */
3001/**
3002 * The derived START/PREEMPT filtration as one JSON block per (station, class),
3003 * each carrying its own 0-based Station and Class beside the usual From/To/Rate
3004 * triplets. An all-zero block is omitted: on a model with no preemption that is
3005 * every block of the PREEMPT filtration.
3006 */
3007template <class T>
3008line::reg::Json aux_filt_json(const std::vector<std::vector<line::Matrix<T> > >& filt,
3009 std::size_t n) {
3010 line::reg::Json blocks = line::reg::Json::array();
3011 for (std::size_t i = 0; i < filt.size(); ++i)
3012 for (std::size_t r = 0; r < filt[i].size(); ++r) {
3013 line::reg::Json bfrom = line::reg::Json::array(), bto = line::reg::Json::array(),
3014 brate = line::reg::Json::array();
3015 for (std::size_t a = 0; a < n; ++a)
3016 for (std::size_t b = 0; b < n; ++b) {
3017 const double q = line::num_traits<T>::to_double(filt[i][r](a, b));
3018 if (q == 0.0) continue;
3019 bfrom.push_back(a);
3020 bto.push_back(b);
3021 brate.push_back(q);
3022 }
3023 if (bfrom.empty()) continue;
3024 line::reg::Json e = line::reg::Json::object();
3025 e["Station"] = i;
3026 e["Class"] = r;
3027 e["From"] = bfrom;
3028 e["To"] = bto;
3029 e["Rate"] = brate;
3030 blocks.push_back(e);
3031 }
3032 return blocks;
3033}
3034
3035template <class T>
3036int solve_ctmc_gen(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3038 o.keep_filtration = true; // the filtration is half of what getInfGen returns
3041 const std::size_t n = g.Q.rows();
3042 std::size_t nnz = 0;
3043 for (std::size_t i = 0; i < n; ++i)
3044 for (std::size_t j = 0; j < n; ++j)
3045 if (line::num_traits<T>::to_double(g.Q(i, j)) != 0.0) ++nnz;
3046 if (g_json_output) {
3047 line::reg::Json p = line::reg::Json::object();
3048 p["type"] = "InfGen";
3049 p["indexBase"] = 0;
3050 p["size"] = n;
3051 p["nnz"] = nnz;
3052 // SPARSE HERE TOO, for the reason the table is: a generator has one entry
3053 // per enabled transition, so the dense form is quadratic in a quantity
3054 // that is linear in the model. A host rebuilds Q with one scatter.
3055 line::reg::Json from = line::reg::Json::array(), to = line::reg::Json::array(),
3056 rate = line::reg::Json::array();
3057 for (std::size_t i = 0; i < n; ++i)
3058 for (std::size_t j = 0; j < n; ++j) {
3059 const double q = line::num_traits<T>::to_double(g.Q(i, j));
3060 if (q == 0.0) continue;
3061 from.push_back(i);
3062 to.push_back(j);
3063 rate.push_back(q);
3064 }
3065 p["From"] = from;
3066 p["To"] = to;
3067 p["Rate"] = rate;
3068 // The synchronization list is HALF of what getInfGen returns: without it
3069 // the filtration's index has no meaning, so it travels with Q. Its node
3070 // and class are the port's own 1-BASED indices, with 0 the LOCAL dummy
3071 // -- `indexBase` above governs the state indices, which is where a host
3072 // would otherwise be indexing a chain by an off-by-one.
3073 line::reg::Json sync = line::reg::Json::array();
3074 for (std::size_t a = 0; a < g.sync.size() && a < g.filt.size(); ++a) {
3075 // THE FILTER ITSELF, not only its nnz as the readable table reports:
3076 // `eventFilt` is half of what getInfGen returns, and a host handed
3077 // only Q cannot recover which synchronization contributed a rate --
3078 // Q's entries have already summed every one of them.
3079 std::size_t fnz = 0;
3080 line::reg::Json ffrom = line::reg::Json::array(), fto = line::reg::Json::array(),
3081 frate = line::reg::Json::array();
3082 for (std::size_t i = 0; i < n; ++i)
3083 for (std::size_t j = 0; j < n; ++j) {
3084 const double q = line::num_traits<T>::to_double(g.filt[a](i, j));
3085 if (q == 0.0) continue;
3086 ++fnz;
3087 ffrom.push_back(i);
3088 fto.push_back(j);
3089 frate.push_back(q);
3090 }
3091 line::reg::Json e = line::reg::Json::object();
3092 e["From"] = ffrom;
3093 e["To"] = fto;
3094 e["Rate"] = frate;
3095 e["activeEvent"] = line::lang::event_to_text(g.sync[a].active.event);
3096 e["activeNode"] = g.sync[a].active.node;
3097 e["activeClass"] = g.sync[a].active.cls;
3098 e["passiveEvent"] = line::lang::event_to_text(g.sync[a].passive.event);
3099 e["passiveNode"] = g.sync[a].passive.node;
3100 e["passiveClass"] = g.sync[a].passive.cls;
3101 e["nnz"] = fnz;
3102 sync.push_back(e);
3103 }
3104 p["sync"] = sync;
3105 // The DERIVED filtrations travel under their own keys, one block per
3106 // (station, class), because they are NOT synchronizations: a START
3107 // rides on an arc `sync` already carries, so folding them in would make
3108 // a host summing the filtration double-count the generator.
3109 p["startFilt"] = aux_filt_json<T>(g.start_filt, n);
3110 p["preemptFilt"] = aux_filt_json<T>(g.preempt_filt, n);
3111 emit_analysis<T>("gen", p, d.actualmethod, ctmc_meta<T>(d));
3112 return 0;
3113 }
3114 print_ctmc_banner<T>(d);
3115 std::printf("InfGen events=%zu nnz=%zu\n", g.sync.size(), nnz);
3116 std::printf("%8s %8s %16s\n", "From", "To", "Rate");
3117 for (std::size_t i = 0; i < n; ++i)
3118 for (std::size_t j = 0; j < n; ++j) {
3119 const double q = line::num_traits<T>::to_double(g.Q(i, j));
3120 if (q != 0.0) std::printf("%8zu %8zu %16.10g\n", i + 1, j + 1, q);
3121 }
3122 std::printf("%6s %-10s %6s %6s %-10s %6s %6s %8s\n", "Event", "ActEvent", "ActNode", "ActCls",
3123 "PasEvent", "PasNode", "PasCls", "Nnz");
3124 for (std::size_t a = 0; a < g.sync.size() && a < g.filt.size(); ++a) {
3125 std::size_t fnz = 0;
3126 for (std::size_t i = 0; i < n; ++i)
3127 for (std::size_t j = 0; j < n; ++j)
3128 if (line::num_traits<T>::to_double(g.filt[a](i, j)) != 0.0) ++fnz;
3129 std::printf("%6zu %-10s %6zu %6zu %-10s %6zu %6zu %8zu\n", a + 1,
3130 line::lang::event_to_text(g.sync[a].active.event), g.sync[a].active.node,
3131 g.sync[a].active.cls, line::lang::event_to_text(g.sync[a].passive.event),
3132 g.sync[a].passive.node, g.sync[a].passive.cls, fnz);
3133 }
3134 return 0;
3135}
3136
3137/** `-a states`: `getStateSpace` and `getStateSpaceAggr`, side by side. */
3138template <class T>
3139int solve_ctmc_states(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3143 if (g_json_output) {
3144 line::reg::Json p = line::reg::Json::object();
3145 p["type"] = "StateSpace";
3146 p["indexBase"] = 0;
3147 // The per-node widths are the ONLY thing that makes a flat row decodable:
3148 // they are where one stateful node's block ends and the next begins.
3149 p["NodeWidths"] = index_json(s.node_width);
3150 p["space"] = matrix_json(s.flat);
3151 p["spaceAggr"] = matrix_json(A);
3152 // `localStateSpace` of `[stateSpace, localStateSpace] = getStateSpace`.
3153 // One entry per stateful node, in stateful-node order, holding that
3154 // node's distinct local rows. A host with only the flat space can
3155 // return the second output as a SINGLE cell holding the whole space,
3156 // which is what `getStateSpace.m` does under `lang='cpp'`, and a caller
3157 // indexing it per node then reads the global space for every node.
3158 line::reg::Json loc = line::reg::Json::array();
3159 for (std::size_t f = 0; f < s.local.size(); ++f) loc.push_back(matrix_json(s.local[f]));
3160 p["localSpace"] = loc;
3161 // pi travels with the space because a row of the space is not an answer:
3162 // the pair (state, probability) is, and reading them from two invocations
3163 // would risk pairing one solve's states with another solve's law.
3164 p["pi"] = vector_json(d.pi);
3165 emit_analysis<T>("states", p, d.actualmethod, ctmc_meta<T>(d));
3166 return 0;
3167 }
3168 print_ctmc_banner<T>(d);
3169 // The per-node widths are printed because the flat row is only decodable
3170 // with them: they are where one node's block ends and the next begins.
3171 std::printf("NodeWidths");
3172 for (std::size_t f = 0; f < s.node_width.size(); ++f) std::printf(" %zu", s.node_width[f]);
3173 std::printf("\n");
3174 std::printf("%8s %12s %s\n", "State", "Prob", "Detailed | Aggregate");
3175 for (std::size_t i = 0; i < s.flat.rows(); ++i) {
3176 std::printf("%8zu %12.6g ", i + 1, line::num_traits<T>::to_double(d.pi[i]));
3177 for (std::size_t c = 0; c < s.flat.cols(); ++c)
3178 std::printf(" %g", line::num_traits<T>::to_double(s.flat(i, c)));
3179 std::printf(" |");
3180 for (std::size_t c = 0; c < A.cols(); ++c)
3181 std::printf(" %g", line::num_traits<T>::to_double(A(i, c)));
3182 std::printf("\n");
3183 }
3184 return 0;
3185}
3186
3187/**
3188 * `-a sens`: `getSensitivityRanking` over the exponential service rates.
3189 *
3190 * THE REWARD IS STATED, NOT INFERRED. The reference makes the caller supply one,
3191 * and a model.json carries no reward function, so the CLI has to name the reward
3192 * it ranks against or the numbers mean nothing: it is the mean number of jobs at
3193 * the QUEUEING stations, which excludes the Source (whose column is the infinite
3194 * reservoir) and the Delay stations (whose population is think time, not work).
3195 *
3196 * Only an exponential service pair becomes a parameter. Perturbing any other
3197 * distribution would mean replacing it with an exponential of the new rate,
3198 * which changes the model's shape rather than one of its parameters; those pairs
3199 * are listed as skipped rather than silently differenced.
3200 */
3201template <class T>
3202int solve_ctmc_sens(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3203 const std::size_t M = sn.nstations, K = sn.nclasses;
3206
3207 std::vector<bool> queueing(M, false);
3208 for (std::size_t i = 0; i < M; ++i)
3209 queueing[i] = sn.stations[i].nodetype != line::lang::NodeType::Source &&
3212
3213 std::vector<T> reward(d.chain.space.size(), line::num_traits<T>::from_int(0));
3214 for (std::size_t s = 0; s < reward.size(); ++s)
3215 for (std::size_t i = 0; i < M; ++i)
3216 if (queueing[i])
3217 for (std::size_t c = 0; c < K; ++c) reward[s] += A(s, i * K + c);
3218
3219 std::vector<line::ctmc::CtmcSensParam<T> > params;
3220 std::vector<std::string> skipped;
3221 for (std::size_t i = 0; i < M; ++i) {
3222 if (sn.stations[i].nodetype == line::lang::NodeType::Source) continue;
3223 for (std::size_t c = 0; c < K; ++c) {
3224 if (sn.disabled[i][c]) continue;
3225 const double mu = line::num_traits<T>::to_double(sn.rates(i, c));
3226 if (!(mu > 0.0)) continue;
3227 const std::string nm =
3228 "mu(" + sn.stations[i].name + "," + sn.classes[c].name + ")";
3229 if (sn.service[i][c].type != line::lang::ProcessType::EXP) {
3230 skipped.push_back(nm);
3231 continue;
3232 }
3234 p.name = nm;
3235 p.value = mu;
3236 p.set = [i, c](line::qn::NetworkStruct<T>& s, double v) {
3238 s.refresh_rates();
3239 };
3240 params.push_back(p);
3241 }
3242 }
3243 if (params.empty())
3245 "-a sens found no exponential service rate to differentiate: every enabled "
3246 "(station, class) pair carries a non-exponential distribution, and replacing one with "
3247 "an exponential of the perturbed rate would change the model rather than a parameter "
3248 "of it");
3249
3250 const std::vector<line::ctmc::CtmcSensRank<T> > rank =
3251 line::ctmc::solver_ctmc_sensitivity_ranking(sn, opt, params, reward);
3252 if (g_json_output) {
3253 line::reg::Json p = line::reg::Json::object();
3254 p["type"] = "SensRanking";
3255 // THE REWARD IS STATED, NOT INFERRED, on this path too: a ranking is a
3256 // ranking against something, and a host that read these numbers without
3257 // knowing what was differentiated would be reading a sensitivity of an
3258 // unnamed functional.
3259 p["reward"] = "mean number of jobs at the queueing stations";
3260 line::reg::Json par = line::reg::Json::array(), val = line::reg::Json::array(),
3261 S = line::reg::Json::array(), SS = line::reg::Json::array();
3262 for (std::size_t l = 0; l < rank.size(); ++l) {
3263 par.push_back(rank[l].parameter);
3264 val.push_back(rank[l].value);
3265 S.push_back(line::num_traits<T>::to_double(rank[l].S));
3266 // null, not NaN and not 0: a zero mean reward makes the scaled form
3267 // undefined, which is a property of the model, and JSON has no NaN.
3268 if (rank[l].scaled_valid)
3269 SS.push_back(line::num_traits<T>::to_double(rank[l].SS));
3270 else
3271 SS.push_back(line::reg::Json());
3272 }
3273 p["Parameter"] = par;
3274 p["Value"] = val;
3275 p["Sens"] = S;
3276 p["ScaledSens"] = SS;
3277 line::reg::Json sk = line::reg::Json::array();
3278 for (std::size_t l = 0; l < skipped.size(); ++l) sk.push_back(skipped[l]);
3279 p["Skipped"] = sk;
3280 emit_analysis<T>("sens", p, d.actualmethod, ctmc_meta<T>(d));
3281 return 0;
3282 }
3283 print_ctmc_banner<T>(d);
3284 std::printf("Reward mean number of jobs at the queueing stations\n");
3285 std::printf("%-28s %14s %16s %16s\n", "Parameter", "Value", "Sens", "ScaledSens");
3286 for (std::size_t l = 0; l < rank.size(); ++l) {
3287 if (rank[l].scaled_valid)
3288 std::printf("%-28s %14.6g %16.8g %16.8g\n", rank[l].parameter.c_str(), rank[l].value,
3290 line::num_traits<T>::to_double(rank[l].SS));
3291 else
3292 // MATLAB reports NaN here; the reason is printed instead, since a
3293 // zero mean reward is a property of the model and not a failure.
3294 std::printf("%-28s %14.6g %16.8g %16s\n", rank[l].parameter.c_str(), rank[l].value,
3295 line::num_traits<T>::to_double(rank[l].S), "undefined(E[r]=0)");
3296 }
3297 for (std::size_t l = 0; l < skipped.size(); ++l)
3298 std::printf("Skipped %s: service is not exponential\n", skipped[l].c_str());
3299 return 0;
3300}
3301
3302/** `-a reward`: `getAvgReward`, the steady-state expectation of each reward. */
3303template <class T>
3304int solve_ctmc_reward(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3305 std::vector<std::string> names;
3306 const std::vector<T> r = line::ctmc::solver_ctmc_avg_reward(sn, opt, &names);
3307 if (g_json_output) {
3308 line::reg::Json p = line::reg::Json::object();
3309 p["type"] = "AvgReward";
3310 line::reg::Json nm = line::reg::Json::array();
3311 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
3312 p["Reward"] = nm;
3313 p["E"] = vector_json(r);
3314 // NO "method": this arm returns the expectations and no solved chain, so
3315 // there is no resolved method to report and none is invented.
3316 emit_analysis<T>("reward", p, std::string());
3317 return 0;
3318 }
3319 std::printf("SolverCTMC arith=%s rewards=%zu\n", line::num_traits<T>::name(), r.size());
3320 std::printf("%-28s %16s\n", "Reward", "E[r]");
3321 for (std::size_t l = 0; l < r.size(); ++l)
3322 std::printf("%-28s %16.10g\n", names[l].c_str(), line::num_traits<T>::to_double(r[l]));
3323 return 0;
3324}
3325
3326/**
3327 * `-a reward-value`: `@@SolverCTMC/getRewardValueFunction`, V^k(s).
3328 *
3329 * A DIFFERENT OBJECT FROM BOTH `-a reward` AND `-a tranreward`. `-a reward`
3330 * returns one number per reward, the steady-state E[r]; `-a tranreward` the
3331 * expected rate along a horizon; this returns the VALUE FUNCTION of ONE named
3332 * reward -- the reward accumulated over k uniformized steps, from every state
3333 * of the chain, as a (Tmax+1 x nstates) matrix. It is the object a policy
3334 * evaluation reads, and it is indexed by state, not by station.
3335 *
3336 * `--reward-name` IS REQUIRED and not defaulted to the first declared reward:
3337 * the value functions of two rewards are different matrices, and labelling one
3338 * with the caller's question would be a wrong answer rather than a missing one.
3339 */
3340template <class T>
3341int solve_ctmc_reward_value(const line::qn::NetworkStruct<T>& sn,
3342 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3343 if (k.reward_name.empty())
3344 throw line::InputError(
3345 "-a reward-value returns the value function of ONE reward and needs --reward-name to "
3346 "say which; -a reward returns the steady-state expectation of every declared reward");
3348 std::size_t which = rr.names.size();
3349 for (std::size_t l = 0; l < rr.names.size(); ++l)
3350 if (rr.names[l] == k.reward_name) which = l;
3351 if (which == rr.names.size()) {
3352 std::string avail;
3353 for (std::size_t l = 0; l < rr.names.size(); ++l)
3354 avail += (l ? ", " : "") + rr.names[l];
3355 throw line::InputError("--reward-name '" + k.reward_name +
3356 "' is not declared by this model; it declares: " +
3357 (avail.empty() ? std::string("(none)") : avail));
3358 }
3359 const line::Matrix<T>& V = rr.V[which];
3360
3361 if (g_json_output) {
3362 line::reg::Json p = line::reg::Json::object();
3363 p["type"] = "RewardValueFunction";
3364 p["indexBase"] = 0;
3365 p["Reward"] = rr.names[which];
3366 p["steps"] = V.rows();
3367 p["states"] = V.cols();
3368 p["t"] = vector_json(rr.t);
3369 p["V"] = matrix_json(V);
3370 p["stateSpaceAggr"] = matrix_json(rr.state_space_aggr);
3371 emit_analysis<T>("rewardvalue", p, std::string());
3372 return 0;
3373 }
3374 std::printf("SolverCTMC arith=%s reward=%s steps=%zu states=%zu\n",
3375 line::num_traits<T>::name(), rr.names[which].c_str(), V.rows(), V.cols());
3376 std::printf("%-10s %-10s %20s\n", "Step", "State", "V");
3377 for (std::size_t i = 0; i < V.rows(); ++i)
3378 for (std::size_t j = 0; j < V.cols(); ++j)
3379 std::printf("%-10zu %-10zu %20.10g\n", i, j, line::num_traits<T>::to_double(V(i, j)));
3380 return 0;
3381}
3382
3383/**
3384 * `-a tranreward`: `getTranReward`, E[r(X(t))] over the --tspan horizon.
3385 *
3386 * A DIFFERENT QUANTITY FROM `-a reward`, not a formatting of it. `-a reward`
3387 * returns the steady-state expectation, one number per reward; this returns the
3388 * expected reward RATE along the trajectory, which converges to that number but
3389 * is not it at any finite t. It is also not the accumulated reward `V`, which
3390 * the same header computes and which diverges -- the header calls that the
3391 * easiest mistake to make here, so the two are kept on separate flags.
3392 *
3393 * THE HORIZON IS REQUIRED, as it is for `-a tranprob`: E[r(X(t))] on an
3394 * unstated horizon is not a quantity, and the reference refuses an infinite one
3395 * rather than picking a bound.
3396 */
3397template <class T>
3398int solve_ctmc_tran_reward(const line::qn::NetworkStruct<T>& sn,
3399 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3401 (void)sn; (void)opt; (void)k;
3403 "-a tranreward integrates the forward equation, which needs transcendental "
3404 "arithmetic; rerun with --arith double or --arith real");
3405 } else {
3406 if (k.t1 < 0.0)
3407 throw line::InputError(
3408 "-a tranreward integrates E[r(X(t))] and needs a horizon: pass --tspan <t0>:<t1>");
3409 std::vector<std::string> names;
3410 std::vector<T> t;
3411 const std::vector<std::vector<T> > r = line::ctmc::solver_ctmc_tran_reward(
3412 sn, opt, line::num_traits<T>::from_double(k.t0),
3413 line::num_traits<T>::from_double(k.t1), &t, &names);
3414 if (g_json_output) {
3415 line::reg::Json p = line::reg::Json::object();
3416 p["type"] = "TranReward";
3417 p["indexBase"] = 0;
3418 line::reg::Json nm = line::reg::Json::array();
3419 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
3420 p["Reward"] = nm;
3421 p["t"] = vector_json<T>(t);
3422 line::reg::Json e = line::reg::Json::array();
3423 for (std::size_t l = 0; l < r.size(); ++l) e.push_back(vector_json<T>(r[l]));
3424 p["E"] = e;
3425 emit_analysis<T>("tranreward", p, std::string());
3426 return 0;
3427 }
3428 std::printf("SolverCTMC arith=%s rewards=%zu points=%zu tspan=[%g,%g]\n",
3429 line::num_traits<T>::name(), r.size(), t.size(), k.t0, k.t1);
3430 std::printf("%-16s", "t");
3431 for (std::size_t l = 0; l < names.size(); ++l) std::printf(" %16s", names[l].c_str());
3432 std::printf("\n");
3433 for (std::size_t i = 0; i < t.size(); ++i) {
3434 std::printf("%-16.10g", line::num_traits<T>::to_double(t[i]));
3435 for (std::size_t l = 0; l < r.size(); ++l)
3436 std::printf(" %16.10g", line::num_traits<T>::to_double(r[l][i]));
3437 std::printf("\n");
3438 }
3439 return 0;
3440 }
3441}
3442
3443/**
3444 * `-a tranprob`: pi(t) over the --tspan horizon, labelled by the whole network
3445 * (`getTranProbSys` / `getTranProbSysAggr`) or by one node when `--node` names
3446 * it (`getTranProb` / `getTranProbAggr`).
3447 *
3448 * The forward equation is integrated ONCE and both label sets are taken off the
3449 * same `CtmcTransient`. Calling the (sn, opt, ...) overloads twice would solve
3450 * and integrate the chain twice for two views of one answer.
3451 */
3452template <class T>
3453int solve_ctmc_tranprob(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
3454 const Knobs& k) {
3455 if (k.t1 < 0.0)
3456 throw line::InputError(
3457 "-a tranprob integrates pi(t) and needs a horizon: pass --tspan <t0>:<t1>");
3460 const line::ctmc::CtmcTranProb<T> det =
3461 k.node ? line::ctmc::ctmc_get_tran_prob(sn, tr, k.node)
3462 : line::ctmc::ctmc_get_tran_prob_sys(sn, tr);
3463 const line::ctmc::CtmcTranProb<T> agg =
3464 k.node ? line::ctmc::ctmc_get_tran_prob_aggr(sn, tr, k.node)
3465 : line::ctmc::ctmc_get_tran_prob_sys_aggr(sn, tr);
3466 if (g_json_output) {
3467 line::reg::Json p = line::reg::Json::object();
3468 p["type"] = "TranProb";
3469 p["indexBase"] = 0;
3470 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
3471 if (k.node) p["node"] = k.node - 1;
3472 line::reg::Json span = line::reg::Json::array();
3473 span.push_back(k.t0);
3474 span.push_back(k.t1);
3475 // THE HORIZON IS PART OF THE ANSWER: pi(t) on an unstated span is not a
3476 // quantity, and a host that took the last row for "the" occupancy without
3477 // it would be quoting a time it does not know.
3478 p["tspan"] = span;
3479 p["t"] = vector_json(det.t);
3480 // BOTH VIEWS COME OFF ONE INTEGRATION, as on the readable path: the
3481 // detailed labels answer getTranProb(Sys) and the aggregate ones
3482 // getTranProb(Sys)Aggr, and a host asking for both would otherwise
3483 // integrate the same forward equation twice for one answer.
3484 p["labels"] = matrix_json(det.labels);
3485 p["labelsAggr"] = matrix_json(agg.labels);
3486 p["pit"] = matrix_json(det.pit);
3487 p["pitAggr"] = matrix_json(agg.pit);
3488 emit_analysis<T>("tranprob", p, tr.chain.actualmethod, ctmc_meta<T>(tr.chain));
3489 return 0;
3490 }
3491 print_ctmc_banner<T>(tr.chain);
3492 std::printf("TranProb times=%zu tspan=%g:%g scope=%s\n", det.t.size(), k.t0, k.t1,
3493 k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
3494 // The labels come FIRST and the occupancy after, because pi(t) is a row per
3495 // time over columns that mean nothing until the state they index is named.
3496 std::printf("%8s %s\n", "State", "Detailed | Aggregate");
3497 for (std::size_t s = 0; s < det.labels.rows(); ++s) {
3498 std::printf("%8zu ", s + 1);
3499 for (std::size_t c = 0; c < det.labels.cols(); ++c)
3500 std::printf(" %g", line::num_traits<T>::to_double(det.labels(s, c)));
3501 std::printf(" |");
3502 for (std::size_t c = 0; c < agg.labels.cols(); ++c)
3503 std::printf(" %g", line::num_traits<T>::to_double(agg.labels(s, c)));
3504 std::printf("\n");
3505 }
3506 std::printf("%14s", "Time");
3507 for (std::size_t s = 0; s < det.pit.cols(); ++s) std::printf(" %12zu", s + 1);
3508 std::printf("\n");
3509 for (std::size_t i = 0; i < det.t.size(); ++i) {
3510 std::printf("%14.8g", line::num_traits<T>::to_double(det.t[i]));
3511 for (std::size_t s = 0; s < det.pit.cols(); ++s)
3512 std::printf(" %12.6g", line::num_traits<T>::to_double(det.pit(i, s)));
3513 std::printf("\n");
3514 }
3515 return 0;
3516}
3517
3518/**
3519 * `-s ctmc -a tran`: `getTranAvg`, the transient MEANS Q(t), U(t) and X(t) over
3520 * the --tspan horizon.
3521 *
3522 * NOT `-a tranprob`, AND THE TWO ARE NOT REDUCIBLE TO ONE ANOTHER FOR A CALLER.
3523 * `tranprob` sends pi(t) with the labels that index it, from which a host COULD
3524 * form these means -- and that is exactly the computation that must not happen
3525 * in a host: the utilization is not a linear functional of the labels (the PS
3526 * and DPS shares divide by the state's own total, and every other discipline
3527 * takes min(n_k, c)/c with the reference's warning attached), so a host folding
3528 * pi(t) itself would be reimplementing `solver_ctmc_transient_analyzer`'s
3529 * discipline switch and would diverge from it silently. The analyzer already
3530 * computes all three trajectories on the way to pi(t); this arm reports them.
3531 *
3532 * The payload is the SAME `TranAvgTable` the fluid and MAM transients emit, key
3533 * for key, so one host reader serves every solver that answers `-a tran`.
3534 */
3535template <class T>
3536int solve_ctmc_tran(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
3537 const Knobs& k) {
3538 if (k.t1 < 0.0)
3539 throw line::InputError(
3540 "-a tran integrates the forward equation and needs a horizon: pass --tspan <t0>:<t1>");
3543 const std::size_t M = sn.nstations, K = sn.nclasses, nt = tr.t.size();
3544
3545 if (g_json_output) {
3546 line::reg::Json p = line::reg::Json::object();
3547 p["type"] = "TranAvgTable";
3548 p["indexBase"] = 0;
3549 p["t0"] = k.t0;
3550 p["t1"] = k.t1;
3551 line::reg::Json ts = line::reg::Json::array();
3552 for (std::size_t j = 0; j < nt; ++j) ts.push_back(line::num_traits<T>::to_double(tr.t[j]));
3553 line::reg::Json arr = line::reg::Json::array();
3554 for (std::size_t i = 0; i < M; ++i)
3555 for (std::size_t c = 0; c < K; ++c) {
3556 // A DISABLED PAIR IS OMITTED, not sent as zeros: the reference
3557 // leaves its cell empty and the host turns an absent curve into
3558 // the disabled handle's NaN. Zeros would read as a station that
3559 // is genuinely idle for that class.
3560 if (sn.disabled[i][c]) continue;
3561 line::reg::Json e = line::reg::Json::object();
3562 e["Station"] = sn.stations[i].name;
3563 e["JobClass"] = sn.classes[c].name;
3564 e["station"] = i;
3565 e["jobclass"] = c;
3566 e["t"] = ts;
3567 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array(),
3568 x = line::reg::Json::array();
3569 for (std::size_t j = 0; j < nt; ++j) {
3570 q.push_back(line::num_traits<T>::to_double(tr.QNt[i][c][j]));
3571 u.push_back(line::num_traits<T>::to_double(tr.UNt[i][c][j]));
3572 x.push_back(line::num_traits<T>::to_double(tr.TNt[i][c][j]));
3573 }
3574 e["QLen"] = q;
3575 e["Util"] = u;
3576 e["Tput"] = x;
3577 arr.push_back(e);
3578 }
3579 p["curves"] = arr;
3580 emit_analysis<T>("tran", p, tr.chain.actualmethod, ctmc_meta<T>(tr.chain));
3581 return 0;
3582 }
3583 print_ctmc_banner<T>(tr.chain);
3584 std::printf("TranAvg times=%zu tspan=%g:%g\n", nt, k.t0, k.t1);
3585 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
3586 "Tput");
3587 for (std::size_t i = 0; i < M; ++i)
3588 for (std::size_t c = 0; c < K; ++c) {
3589 if (sn.disabled[i][c]) continue;
3590 for (std::size_t j = 0; j < nt; ++j)
3591 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
3592 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(),
3594 line::num_traits<T>::to_double(tr.QNt[i][c][j]),
3595 line::num_traits<T>::to_double(tr.UNt[i][c][j]),
3596 line::num_traits<T>::to_double(tr.TNt[i][c][j]));
3597 }
3598 return 0;
3599}
3600
3601/**
3602 * `-a sample`: `sampleSys` and `sampleSysAggr`, one marked trajectory; with
3603 * `--node`, also that node's own block (`sample`) and per-class counts
3604 * (`sampleAggr`), which is the view MATLAB's per-node sampler returns.
3605 */
3606template <class T>
3607int solve_ctmc_sample(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
3608 const Knobs& k) {
3609 // `--events` NAMES THIS NUMBER, `--samples` only stands in for it. The
3610 // JAR keeps the two apart because a sampled trajectory is walked for a
3611 // number of EVENTS while `--samples` is a solver-wide run length, so a
3612 // caller who set the run length and then asked for a trajectory would
3613 // silently get one of that length. Both are honoured, --events first,
3614 // and the reference default of 1000 stands when neither is given.
3615 const std::size_t nevents = k.events ? k.events : (k.samples ? k.samples : 1000);
3616 const unsigned long seed = k.seed ? k.seed : 23000;
3618 line::ctmc::solver_ctmc_sample_sys<T>(sn, opt, nevents, seed);
3620 line::Matrix<T> L, LA;
3621 if (k.node) {
3622 L = line::ctmc::solver_ctmc_sample(sn, path, k.node);
3623 LA = line::ctmc::solver_ctmc_sample_aggr(sn, path, k.node);
3624 }
3625 if (g_json_output) {
3626 line::reg::Json p = line::reg::Json::object();
3627 p["type"] = "SamplePath";
3628 p["indexBase"] = 0;
3629 // The seed and the requested length are part of the ANSWER, as on the SSA
3630 // path: two runs are the same trace only if both are stated.
3631 p["events"] = nevents;
3632 p["seed"] = seed;
3633 p["drawn"] = path.state.size();
3634 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
3635 if (k.node) p["node"] = k.node - 1;
3636 p["t"] = vector_json(path.t);
3637 p["state"] = index_json(path.state);
3638 line::reg::Json ev = line::reg::Json::array();
3639 for (std::size_t i = 0; i < path.state.size(); ++i) {
3640 // null where the readable table prints "absorb": the walk left no
3641 // state, so there is no synchronization index, and any integer here
3642 // would name an event that did not fire.
3643 if (i < path.event.size() && path.event[i] != static_cast<std::size_t>(-1))
3644 ev.push_back(path.event[i]);
3645 else
3646 ev.push_back(line::reg::Json());
3647 }
3648 p["event"] = ev;
3649 p["sysAggr"] = matrix_json(A);
3650 // THE STATE SPACE TRAVELS WITH THE TRAJECTORY, so a host can turn the
3651 // visited indices into the states themselves without enumerating the
3652 // chain a second time in another process -- which would also be a second
3653 // chance for the two enumerations to disagree while looking paired.
3654 {
3657 p["space"] = matrix_json(s.flat);
3658 p["NodeWidths"] = index_json(s.node_width);
3659 }
3660 if (k.node) {
3661 p["nodeState"] = matrix_json(L);
3662 p["nodeAggr"] = matrix_json(LA);
3663 }
3664 emit_analysis<T>("sample", p, path.chain.actualmethod, ctmc_meta<T>(path.chain));
3665 return 0;
3666 }
3667 // The seed and the requested length are part of the ANSWER, as on the SSA
3668 // path: two runs are the same trace only if both are stated.
3669 std::printf("SolverCTMC arith=%s states=%zu events=%zu seed=%lu drawn=%zu scope=%s\n",
3670 line::num_traits<T>::name(), path.chain.chain.space.size(), nevents, seed,
3671 path.state.size(), k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
3672 std::printf("%14s %8s %8s %s\n", "Time", "State", "Event",
3673 k.node ? "SysAggregate | NodeState | NodeAggregate" : "SysAggregate");
3674 for (std::size_t i = 0; i < path.state.size(); ++i) {
3675 const std::size_t ev = i < path.event.size() ? path.event[i] : static_cast<std::size_t>(-1);
3676 std::printf("%14.8g %8zu ", line::num_traits<T>::to_double(path.t[i]), path.state[i] + 1);
3677 if (ev == static_cast<std::size_t>(-1))
3678 std::printf("%8s ", "absorb");
3679 else
3680 std::printf("%8zu ", ev + 1);
3681 for (std::size_t c = 0; c < A.cols(); ++c)
3682 std::printf(" %g", line::num_traits<T>::to_double(A(i, c)));
3683 if (k.node) {
3684 std::printf(" |");
3685 for (std::size_t c = 0; c < L.cols(); ++c)
3686 std::printf(" %g", line::num_traits<T>::to_double(L(i, c)));
3687 std::printf(" |");
3688 for (std::size_t c = 0; c < LA.cols(); ++c)
3689 std::printf(" %g", line::num_traits<T>::to_double(LA(i, c)));
3690 }
3691 std::printf("\n");
3692 }
3693 return 0;
3694}
3695
3696/** `-a cdf`: `getCdfRespT` per (station, class), and `getCdfSysRespT` per chain. */
3697template <class T>
3698int solve_ctmc_cdf(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3699 const std::vector<std::vector<line::ctmc::CdfCurve<T> > > RD =
3701 const std::vector<line::ctmc::CdfCurve<T> > RS = line::ctmc::solver_ctmc_cdf_sys_respt(sn, opt);
3702 if (g_json_output) {
3703 line::reg::Json p = line::reg::Json::object();
3704 p["type"] = "CdfRespT";
3705 p["chains"] = sn.nchains;
3706 // ONE OBJECT PER CURVE rather than four parallel columns: the grids are
3707 // per-pair and of different lengths, so a column-oriented form would have
3708 // to repeat the station and the class on every sample and leave the host
3709 // to re-group them.
3710 line::reg::Json rd = line::reg::Json::array();
3711 for (std::size_t i = 0; i < RD.size(); ++i)
3712 for (std::size_t c = 0; c < RD[i].size(); ++c) {
3713 // An empty curve is not a degenerate one: the chain never visits
3714 // that pair, so there is no arrival event to condition on, and it
3715 // is OMITTED rather than sent as a flat zero law.
3716 if (RD[i][c].empty()) continue;
3717 line::reg::Json e = line::reg::Json::object();
3718 e["Station"] = sn.stations[i].name;
3719 e["JobClass"] = sn.classes[c].name;
3720 e["station"] = i;
3721 e["jobclass"] = c;
3722 e["t"] = vector_json(RD[i][c].t);
3723 e["F"] = vector_json(RD[i][c].F);
3724 rd.push_back(e);
3725 }
3726 p["respt"] = rd;
3727 line::reg::Json rs = line::reg::Json::array();
3728 for (std::size_t c = 0; c < RS.size(); ++c) {
3729 if (RS[c].empty()) continue;
3730 line::reg::Json e = line::reg::Json::object();
3731 e["chain"] = c;
3732 e["t"] = vector_json(RS[c].t);
3733 e["F"] = vector_json(RS[c].F);
3734 rs.push_back(e);
3735 }
3736 p["sysrespt"] = rs;
3737 p["indexBase"] = 0;
3738 // NO "method", as on the reward arm: the response-time laws are computed
3739 // from tagged chains this function does not return, so there is no
3740 // resolved method to report and the requested one is not it.
3741 emit_analysis<T>("cdf", p, std::string());
3742 return 0;
3743 }
3744 std::printf("SolverCTMC arith=%s chains=%zu\n", line::num_traits<T>::name(), sn.nchains);
3745 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
3746 for (std::size_t i = 0; i < RD.size(); ++i)
3747 for (std::size_t c = 0; c < RD[i].size(); ++c) {
3748 // An empty curve is not a degenerate one: the chain never visits
3749 // that pair, so there is no arrival event to condition on.
3750 if (RD[i][c].empty()) continue;
3751 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
3752 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
3753 sn.classes[c].name.c_str(),
3754 line::num_traits<T>::to_double(RD[i][c].t[j]),
3755 line::num_traits<T>::to_double(RD[i][c].F[j]));
3756 }
3757 std::printf("%-16s %-14s %14s %14s\n", "System", "Chain", "Time", "F(t)");
3758 for (std::size_t c = 0; c < RS.size(); ++c) {
3759 if (RS[c].empty()) continue;
3760 for (std::size_t j = 0; j < RS[c].t.size(); ++j)
3761 std::printf("%-16s %-14zu %14.8g %14.10g\n", "(system)", c + 1,
3763 line::num_traits<T>::to_double(RS[c].F[j]));
3764 }
3765 return 0;
3766}
3767
3768/**
3769 * A `--passage-from`/`--passage-into` spec as a Matrix<double>: either a flat
3770 * comma list ("3,5", one row the resolver reads as 1-based indices when every
3771 * entry is one) or semicolon-separated state rows ("0,2;1,1"), which resolve
3772 * against the enumerated space by content.
3773 */
3774inline line::Matrix<double> parse_passage_set(const std::string& spec, const char* flag) {
3775 if (spec.empty()) return line::Matrix<double>(0, 0);
3776 std::vector<std::vector<double>> rows;
3777 std::size_t pos = 0;
3778 while (pos <= spec.size()) {
3779 std::size_t semi = spec.find(';', pos);
3780 if (semi == std::string::npos) semi = spec.size();
3781 std::string rowtxt = spec.substr(pos, semi - pos);
3782 std::vector<double> row;
3783 std::size_t p2 = 0;
3784 while (p2 <= rowtxt.size()) {
3785 std::size_t comma = rowtxt.find(',', p2);
3786 if (comma == std::string::npos) comma = rowtxt.size();
3787 std::string cell = rowtxt.substr(p2, comma - p2);
3788 if (!cell.empty()) {
3789 char* endp = 0;
3790 const double v = std::strtod(cell.c_str(), &endp);
3791 if (endp == cell.c_str() || *endp != '\0')
3792 throw line::InputError(std::string(flag) + ": '" + cell +
3793 "' is not a number");
3794 row.push_back(v);
3795 }
3796 p2 = comma + 1;
3797 }
3798 if (!row.empty()) rows.push_back(row);
3799 pos = semi + 1;
3800 }
3801 if (rows.empty()) return line::Matrix<double>(0, 0);
3802 for (std::size_t i = 1; i < rows.size(); ++i)
3803 if (rows[i].size() != rows[0].size())
3804 throw line::InputError(std::string(flag) +
3805 ": every state row must have the same width");
3806 line::Matrix<double> out(rows.size(), rows[0].size());
3807 for (std::size_t i = 0; i < rows.size(); ++i)
3808 for (std::size_t j = 0; j < rows[i].size(); ++j) out(i, j) = rows[i][j];
3809 return out;
3810}
3811
3812/**
3813 * `-s ctmc -a firstpasst`: `@@SolverCTMC/getCdfFirstPassT(A, B)`, the first
3814 * passage time between two state sets the caller names. `--passage-into` is
3815 * required; an empty `--passage-from` starts from the conditional stationary
3816 * law on the complement of the target, as the reference does.
3817 */
3818template <class T>
3819int solve_ctmc_firstpasst(const line::qn::NetworkStruct<T>& sn,
3820 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3821 if (k.passage_into.empty())
3822 throw line::InputError(
3823 "-a firstpasst times the passage INTO a state set and needs --passage-into; name it "
3824 "as 1-based rows of the state space ('3,5') or as state rows ('0,2;1,1')");
3825 const line::Matrix<double> A = parse_passage_set(k.passage_from, "--passage-from");
3826 const line::Matrix<double> B = parse_passage_set(k.passage_into, "--passage-into");
3827 const std::string method = k.passage_method.empty() ? "expm" : k.passage_method;
3828
3830 line::ctmc::ctmc_cdf_firstpasst<T>(sn, opt, A, B, method);
3831
3832 if (g_json_output) {
3833 line::reg::Json p = line::reg::Json::object();
3834 p["type"] = "CdfFirstPassT";
3835 p["indexBase"] = 0;
3836 p["t"] = line::reg::Json(fp.t);
3837 p["F"] = line::reg::Json(fp.F);
3838 p["f"] = line::reg::Json(fp.f);
3839 line::reg::Json src = line::reg::Json::array(), tgt = line::reg::Json::array();
3840 for (std::size_t i : fp.source) src.push_back(static_cast<double>(i));
3841 for (std::size_t i : fp.target) tgt.push_back(static_cast<double>(i));
3842 p["source"] = src;
3843 p["target"] = tgt;
3844 emit_analysis<T>("firstpasst", p, method);
3845 return 0;
3846 }
3847 std::printf("SolverCTMC arith=%s method=%s getCdfFirstPassT\n",
3848 line::num_traits<T>::name(), method.c_str());
3849 std::printf("source states: %zu%s, target states: %zu\n", fp.source.size(),
3850 fp.source.empty() ? " (conditional stationary law)" : "", fp.target.size());
3851 std::printf("%14s %14s %14s\n", "Time", "F(t)", "f(t)");
3852 for (std::size_t j = 0; j < fp.t.size(); j += 111)
3853 std::printf("%14.8g %14.10g %14.10g\n", fp.t[j], fp.F[j], fp.f[j]);
3854 std::printf("%14.8g %14.10g %14.10g\n", fp.t.back(), fp.F.back(), fp.f.back());
3855 return 0;
3856}
3857
3858/**
3859 * `-s ctmc -a firstpasstmom`: `@@SolverCTMC/getFirstPassTMoments(A, B, nmax)`,
3860 * the moments of the same passage `firstpasst` gives the curve of.
3861 *
3862 * These are EXACT and cost one linear solve per order, so a caller who wants a
3863 * variance or a skewness should ask for them here rather than integrate the
3864 * truncated curve the other arm returns. `--passage-orders` is the nmax; it
3865 * defaults to the reference's 3.
3866 */
3867template <class T>
3868int solve_ctmc_firstpasst_moments(const line::qn::NetworkStruct<T>& sn,
3869 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3870 if (k.passage_into.empty())
3871 throw line::InputError(
3872 "-a firstpasstmom times the passage INTO a state set and needs --passage-into; name "
3873 "it as 1-based rows of the state space ('3,5') or as state rows ('0,2;1,1')");
3874 const line::Matrix<double> A = parse_passage_set(k.passage_from, "--passage-from");
3875 const line::Matrix<double> B = parse_passage_set(k.passage_into, "--passage-into");
3876 const std::size_t nmax = (k.passage_orders > 0) ? k.passage_orders : 3;
3877
3879 line::ctmc::ctmc_firstpasst_moments<T>(sn, opt, A, B, nmax);
3880
3881 if (g_json_output) {
3882 line::reg::Json p = line::reg::Json::object();
3883 p["type"] = "FirstPassTMoments";
3884 p["indexBase"] = 0;
3885 line::reg::Json m = line::reg::Json::array();
3886 for (std::size_t i = 0; i < fm.m.size(); ++i)
3887 m.push_back(line::num_traits<T>::to_double(fm.m[i]));
3888 p["m"] = m;
3889 line::reg::Json mall = line::reg::Json::array();
3890 for (std::size_t i = 0; i < fm.mall.rows(); ++i) {
3891 line::reg::Json row = line::reg::Json::array();
3892 for (std::size_t j = 0; j < fm.mall.cols(); ++j)
3893 row.push_back(line::num_traits<T>::to_double(fm.mall(i, j)));
3894 mall.push_back(row);
3895 }
3896 p["mall"] = mall;
3897 line::reg::Json src = line::reg::Json::array(), tgt = line::reg::Json::array();
3898 for (std::size_t i : fm.source) src.push_back(static_cast<double>(i));
3899 for (std::size_t i : fm.target) tgt.push_back(static_cast<double>(i));
3900 p["source"] = src;
3901 p["target"] = tgt;
3902 emit_analysis<T>("firstpasstmom", p, "moments");
3903 return 0;
3904 }
3905 std::printf("SolverCTMC arith=%s getFirstPassTMoments\n", line::num_traits<T>::name());
3906 std::printf("source states: %zu%s, target states: %zu\n", fm.source.size(),
3907 fm.source.empty() ? " (conditional stationary law)" : "", fm.target.size());
3908 std::printf("%8s %20s\n", "Order", "Moment");
3909 for (std::size_t i = 0; i < fm.m.size(); ++i)
3910 std::printf("%8zu %20.10g\n", i + 1, line::num_traits<T>::to_double(fm.m[i]));
3911 return 0;
3912}
3913
3914/**
3915 * Solve a Network model.json with SolverCTMC and print what `-a` asked for.
3916 *
3917 * EXACT, and the only ported solver that is exact on a non-product-form model:
3918 * it enumerates the state space and solves pi Q = 0, so the numbers are the
3919 * chain's own and not an approximation of them. The price is the state space,
3920 * which is why an OPEN model needs `--cutoff`: without a bound on the open
3921 * population the chain is infinite. The banner reports the cutoff that was used,
3922 * because a truncated chain's answer is not the model's answer without it.
3923 *
3924 * Every arithmetic backend runs the stationary analyses: the generator assembly
3925 * and the stationary solve are field operations throughout, so `--arith exact`
3926 * returns the exact rational stationary law of a chain with rational rates. The
3927 * transient ones refuse by name under exact, since a forward integration, an
3928 * exponential clock and a matrix exponential are all transcendental.
3929 */
3930/**
3931 * `--rate-sched`: the JSON schedule, inline or from a file, resolved against the
3932 * model so that a station or class may be named rather than numbered.
3933 */
3934template <class T>
3935std::vector<line::ctmc::CtmcRateSched> parse_rate_sched(const std::string& arg,
3936 const line::qn::NetworkStruct<T>& sn) {
3937 std::string text = arg;
3938 const std::size_t first = arg.find_first_not_of(" \t\r\n");
3939 if (first == std::string::npos || (arg[first] != '[' && arg[first] != '{')) {
3940 std::ifstream in(arg.c_str());
3941 if (!in)
3942 throw line::InputError("--rate-sched: '" + arg +
3943 "' is neither inline JSON nor a readable file");
3944 std::ostringstream ss;
3945 ss << in.rdbuf();
3946 text = ss.str();
3947 }
3948 line::io::detail::json root;
3949 try {
3950 root = line::io::detail::json::parse(text);
3951 } catch (const std::exception& e) {
3952 throw line::InputError(std::string("--rate-sched: not valid JSON: ") + e.what());
3953 }
3954 if (root.is_object()) root = line::io::detail::json::array({root});
3955 if (!root.is_array() || root.empty())
3956 throw line::InputError("--rate-sched: expected a non-empty array of schedule objects");
3957
3958 const auto resolve = [](const line::io::detail::json& v, const std::vector<std::string>& names,
3959 const char* what) -> std::size_t {
3960 if (v.is_number_integer()) {
3961 const long long i = v.get<long long>();
3962 if (i < 1 || static_cast<std::size_t>(i) > names.size())
3963 throw line::InputError(std::string("--rate-sched: ") + what + " index " +
3964 std::to_string(i) + " is out of range (1-based)");
3965 return static_cast<std::size_t>(i);
3966 }
3967 if (v.is_string()) {
3968 const std::string nm = v.get<std::string>();
3969 for (std::size_t i = 0; i < names.size(); ++i)
3970 if (names[i] == nm) return i + 1;
3971 throw line::InputError(std::string("--rate-sched: no ") + what + " named '" + nm + "'");
3972 }
3973 throw line::InputError(std::string("--rate-sched: ") + what +
3974 " must be a name or a 1-based index");
3975 };
3976 std::vector<std::string> stations, classes;
3977 for (std::size_t i = 0; i < sn.stations.size(); ++i)
3978 stations.push_back(sn.nodes[sn.station_to_node[i] - 1].name);
3979 for (const auto& c : sn.classes) classes.push_back(c.name);
3980
3981 std::vector<line::ctmc::CtmcRateSched> out;
3982 for (const auto& e : root) {
3983 if (!e.is_object() || !e.contains("station") || !e.contains("class") ||
3984 !e.contains("tgrid") || !e.contains("rates"))
3985 throw line::InputError(
3986 "--rate-sched: every entry needs station, class, tgrid and rates");
3988 rs.station = resolve(e.at("station"), stations, "station");
3989 rs.cls = resolve(e.at("class"), classes, "class");
3990 rs.tgrid = e.at("tgrid").get<std::vector<double>>();
3991 rs.rates = e.at("rates").get<std::vector<double>>();
3992 if (e.contains("nominal") && !e.at("nominal").is_null())
3993 rs.nominal = e.at("nominal").get<double>();
3994 out.push_back(rs);
3995 }
3996 return out;
3997}
3998
3999template <class T>
4000int solve_model_ctmc(const std::string& file, const Knobs& k, const std::string& analysis) {
4001 line::qn::Network<T> net = read_model<T>(file);
4003 if (!k.method.empty()) opt.method = k.method;
4004 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
4005 opt.cutoff_mat = k.cutoff_mat;
4006 opt.force = k.force;
4007 if (k.timestep > 0.0) opt.timestep = k.timestep; // `--timestep`, the fixed output grid
4008 // `--transient-method` and its two tolerances, `options.config` of the
4009 // reference's transient analyzer. The analyzer validates the name.
4010 if (!k.transient_method.empty()) opt.transient_method = k.transient_method;
4011 if (k.fau_epsilon > 0.0) opt.fau_epsilon = k.fau_epsilon;
4012 if (k.fau_delta >= 0.0) opt.fau_delta = k.fau_delta;
4013 if (k.ctmc_tv_ngrid > 0) opt.ctmc_tv_ngrid = k.ctmc_tv_ngrid;
4014 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4015 if (!k.rate_sched.empty()) opt.rate_sched = parse_rate_sched(k.rate_sched, sn);
4016
4017 // The method that never builds the generator is served by its own analyzer,
4018 // ahead of every state-space path below. The dispatcher has already refused
4019 // every analysis but `avg` for it, since it produces no state space, no
4020 // filtration and no trajectory to answer one from.
4021 if (opt.method == "mdd") {
4023 if (k.mdd_tol > 0.0) mcdopt.tol = k.mdd_tol;
4024 if (k.mdd_maxiter > 0) mcdopt.maxiter = k.mdd_maxiter;
4025 return solve_ctmc_mdd_avg<T>(sn, opt, mcdopt);
4026 }
4027
4028 // The field-arithmetic analyses: every step from the generator to the answer
4029 // is a ring operation, so exact returns the exact rational quantity.
4030 if (analysis == "avg") return solve_ctmc_avg<T>(sn, opt);
4031 if (analysis == "prob") return solve_ctmc_prob<T>(sn, opt, k);
4032 if (analysis == "gen") return solve_ctmc_gen<T>(sn, opt);
4033 if (analysis == "states") return solve_ctmc_states<T>(sn, opt);
4034 if (analysis == "sens") return solve_ctmc_sens<T>(sn, opt);
4035 if (analysis == "reward") return solve_ctmc_reward<T>(sn, opt);
4036 if (analysis == "rewardvalue") return solve_ctmc_reward_value<T>(sn, opt, k);
4037
4038 // The rest integrate a forward equation, draw an exponential clock or take a
4039 // matrix exponential, none of which exists in a rational field. Their
4040 // static_asserts are behind if-constexpr so the refusal is a message rather
4041 // than a compile error in the exact instantiation.
4044 "the -a " + analysis +
4045 " analysis integrates the forward equation, draws exponential clocks or takes a matrix "
4046 "exponential, none of which exists in exact rational arithmetic; rerun with --arith "
4047 "double or --arith real");
4048 } else {
4049 if (analysis == "tran") return solve_ctmc_tran<T>(sn, opt, k);
4050 if (analysis == "tranprob") return solve_ctmc_tranprob<T>(sn, opt, k);
4051 if (analysis == "tranreward") return solve_ctmc_tran_reward<T>(sn, opt, k);
4052 if (analysis == "sample") return solve_ctmc_sample<T>(sn, opt, k);
4053 if (analysis == "firstpasst") return solve_ctmc_firstpasst<T>(sn, opt, k);
4054 if (analysis == "firstpasstmom") return solve_ctmc_firstpasst_moments<T>(sn, opt, k);
4055 return solve_ctmc_cdf<T>(sn, opt); // the dispatcher admitted no other name
4056 }
4057}
4058
4059/**
4060 * `-s ssa -a prob`: the four SolverSSA probability queries over the model's
4061 * DEFAULT INITIAL STATE, the same state `-s ctmc -a prob` reports on.
4062 *
4063 * THE PAIR IS THE POINT. The CTMC answer is the stationary law of the chain and
4064 * this one is a time average of a finite sample path, so running both on a model
4065 * small enough for the chain measures the simulation error directly instead of
4066 * inferring it. The banner therefore carries the run length and the seed, as
4067 * every simulated number on this CLI does.
4068 *
4069 * `seen` TRAVELS WITH EACH PROBABILITY because a zero here has two meanings: the
4070 * path visited the state and left immediately, or it never got there at all. The
4071 * reference warns on the second; a machine-readable answer has to carry the
4072 * distinction rather than print it.
4073 */
4074template <class T>
4075int solve_model_ssa_prob(const std::string& file, const Knobs& k) {
4076 line::qn::Network<T> net = read_model<T>(file);
4077 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4079 // `serial`, whatever `-m` said, and for the reference's own reason: the NRM
4080 // simulates per-(node, class, phase) counts rather than the state ENCODING,
4081 // so it has no row to compare a requested state against.
4082 // `@@SolverSSA/getProb.m` rewrites the method the same way.
4083 opt.method = "serial";
4084 if (k.samples) opt.samples = k.samples;
4085 if (k.seed) opt.seed = k.seed;
4086 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
4087 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
4089
4090 if (g_json_output) {
4091 line::reg::Json p = line::reg::Json::object();
4092 p["type"] = "ProbAggr";
4093 p["indexBase"] = 0;
4094 p["samples"] = r.samples;
4095 p["seed"] = r.seed;
4096 p["ProbSys"] = r.sys.prob;
4097 p["ProbSysAggr"] = r.sys_aggr.prob;
4098 p["ProbSysSeen"] = r.sys.seen;
4099 p["ProbSysAggrSeen"] = r.sys_aggr.seen;
4100 line::reg::Json st = line::reg::Json::array(), pm = line::reg::Json::array(),
4101 pa = line::reg::Json::array(), sm = line::reg::Json::array();
4102 for (std::size_t i = 0; i < sn.nstations; ++i) {
4103 st.push_back(sn.stations[i].name);
4104 pm.push_back(r.marg[i].prob);
4105 pa.push_back(r.aggr[i].prob);
4106 sm.push_back(r.marg[i].seen);
4107 }
4108 p["Station"] = st;
4109 p["Prob"] = pm;
4110 p["ProbAggr"] = pa;
4111 p["Seen"] = sm;
4112 emit_analysis<T>("prob", p, "serial");
4113 return 0;
4114 }
4115 std::printf("SolverSSA arith=%s method=serial samples=%zu seed=%lu time=%.6g\n",
4117 std::printf("ProbSys = %.8g%s\n", r.sys.prob, r.sys.seen ? "" : " (state never visited)");
4118 std::printf("ProbSysAggr = %.8g%s\n", r.sys_aggr.prob,
4119 r.sys_aggr.seen ? "" : " (state never visited)");
4120 std::printf("%-20s %14s %14s\n", "Station", "Prob", "ProbAggr");
4121 for (std::size_t i = 0; i < sn.nstations; ++i)
4122 std::printf("%-20s %14.8g %14.8g\n", sn.stations[i].name.c_str(), r.marg[i].prob,
4123 r.aggr[i].prob);
4124 return 0;
4125}
4126
4127/**
4128 * `-s ssa -a sample`: `sampleSys` and `sampleSysAggr`, one simulated trajectory;
4129 * with `--node`, also that node's own block (`sample`) and per-class counts
4130 * (`sampleAggr`).
4131 *
4132 * The SAME shape `-s ctmc -a sample` emits, deliberately: the CTMC sampler walks
4133 * the jump chain of an enumerated generator and this one walks the network's own
4134 * encoding, and a host that can read one trajectory should be able to read the
4135 * other. The event column indexes the synchronization list, so the two are
4136 * comparable only within a solver -- which is why it is printed and not
4137 * interpreted here.
4138 */
4139template <class T>
4140int solve_model_ssa_sample(const std::string& file, const Knobs& k) {
4141 line::qn::Network<T> net = read_model<T>(file);
4142 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4144 opt.method = "serial";
4145 opt.samples = k.events ? k.events : (k.samples ? k.samples : 1000); // see -a sample above
4146 if (k.seed) opt.seed = k.seed;
4147 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
4148 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
4152 if (k.node) nodep = line::ssa::ssa_sample_node(sn, sim.run, k.node);
4153
4154 if (g_json_output) {
4155 line::reg::Json p = line::reg::Json::object();
4156 p["type"] = "SamplePath";
4157 p["indexBase"] = 0;
4158 p["events"] = opt.samples;
4159 p["seed"] = sys.seed;
4160 p["drawn"] = sys.t.size();
4161 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
4162 if (k.node) p["node"] = k.node - 1;
4163 p["t"] = vector_json(sys.t);
4164 p["event"] = index_json(sys.event);
4165 p["state"] = matrix_json(sys.state);
4166 // THE BLOCK BOUNDARIES TRAVEL, as under `-s ctmc -a sample`. A row of
4167 // `state` is the stateful nodes' local encodings laid end to end, and
4168 // `sampleSys` reports them one node at a time; a host that had to guess
4169 // the widths would cut the row at the wrong columns on any model with a
4170 // phase-type service, and read a phase index as a job count.
4171 {
4172 line::reg::Json w = line::reg::Json::array();
4173 for (std::size_t f = 0; f < sn.stateful_nodes.size(); ++f)
4174 w.push_back(sim.run.space.empty() ? 0 : sim.run.space[0].local[f].size());
4175 p["NodeWidths"] = w;
4176 }
4177 p["sysAggr"] = matrix_json(sys.aggr);
4178 if (k.node) {
4179 p["nodeState"] = matrix_json(nodep.state);
4180 p["nodeAggr"] = matrix_json(nodep.aggr);
4181 }
4182 emit_analysis<T>("sample", p, "serial");
4183 return 0;
4184 }
4185 std::printf("SolverSSA arith=%s method=serial events=%zu seed=%lu drawn=%zu scope=%s\n",
4186 line::num_traits<T>::name(), opt.samples, sys.seed, sys.t.size(),
4187 k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
4188 std::printf("%14s %8s %s\n", "Time", "Event",
4189 k.node ? "SysAggregate | NodeState | NodeAggregate" : "SysAggregate");
4190 for (std::size_t i = 0; i < sys.t.size(); ++i) {
4191 std::printf("%14.8g %8zu ", sys.t[i], sys.event[i]);
4192 for (std::size_t c = 0; c < sys.aggr.cols(); ++c)
4193 std::printf(" %g", line::num_traits<T>::to_double(sys.aggr(i, c)));
4194 if (k.node) {
4195 std::printf(" |");
4196 for (std::size_t c = 0; c < nodep.state.cols(); ++c)
4197 std::printf(" %g", line::num_traits<T>::to_double(nodep.state(i, c)));
4198 std::printf(" |");
4199 for (std::size_t c = 0; c < nodep.aggr.cols(); ++c)
4200 std::printf(" %g", line::num_traits<T>::to_double(nodep.aggr(i, c)));
4201 }
4202 std::printf("\n");
4203 }
4204 return 0;
4205}
4206
4207/**
4208 * A `Matrix<double>` read as this arithmetic's matrix.
4209 *
4210 * SSA and Fluid return plain-double solutions whatever `T` the CLI was asked
4211 * for, so every shared routine that takes a `Matrix<T>` -- the residence-time
4212 * conversion, the chain aggregation -- needs this one lift. Both are refused
4213 * outside `--arith double` anyway, so it is a type bridge and not a precision
4214 * claim.
4215 */
4216template <class T>
4217line::Matrix<T> to_matrix(const line::Matrix<double>& m) {
4219 for (std::size_t i = 0; i < m.rows(); ++i)
4220 for (std::size_t j = 0; j < m.cols(); ++j)
4221 out(i, j) = line::num_traits<T>::from_double(m(i, j));
4222 return out;
4223}
4224
4225/**
4226 * Solve a Network model.json with SolverSSA and print the same table.
4227 *
4228 * A SIMULATION: its numbers carry Monte Carlo error, so the row is compared
4229 * against the other codebases' SSA rows and not against an exact solver's.
4230 *
4231 * `--samples` and `--seed` set the run length and the stream; without them the
4232 * defaults are 10000 firings and seed 23000, which on a two-station model is
4233 * roughly 3300 job cycles and lands a few percent from the analytical answer.
4234 * Diffing THAT against an exact solver reads as a defect and is not one: a
4235 * measured -3.12% at 1e4 on an M/M/1 falls to +0.06% at 2.56e6. The banner
4236 * therefore carries both numbers, so a row quoting an SSA figure carries the
4237 * conditions that produced it. Double only, refused by name in the dispatcher.
4238 *
4239 * `-m` REACHES THE SOLVER'S OWN DISPATCHER, `ssa::solver_ssa`, and not one
4240 * engine's entry. Calling `solver_ssa_nrm_analyzer` here would run the NRM
4241 * whatever `-m` said, so `-m serial` would silently answer with a different
4242 * estimator than the one asked for -- and the three names the NRM entry cannot
4243 * serve (`serial`, `para`, `parallel`) are all methods the library honours.
4244 */
4245template <class T>
4246int solve_model_ssa(const std::string& file, const Knobs& k) {
4247 line::qn::Network<T> net = read_model<T>(file);
4249 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4250 if (k.samples) opt.samples = k.samples;
4251 if (k.seed) opt.seed = k.seed;
4252 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
4253 // The cache write-back is COLLECTED HERE, not only on the `-a node` path:
4254 // the realized hit and miss shares are what the sample path measured, and
4255 // `-a avg` is the arm MATLAB's lang='cpp' bridge calls. Without it
4256 // `CPPLINE.restoreCacheResults` found no block, cleared the Cache node and
4257 // refreshed the visits back to link()'s offered 1/2-1/2.
4258 std::vector<line::ssa::SsaCacheRatio> cacheratio;
4259 const line::ssa::SsaSolution r = line::ssa::solver_ssa(net.get_struct(), opt, &cacheratio);
4260 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4261
4262 // The seed and the sample count are part of the ANSWER, not of the
4263 // invocation: two runs of a simulation are the same measurement only if
4264 // both are stated, so the banner carries them and a parity row that quotes
4265 // an SSA number carries them with it.
4266 //
4267 // `r.method` is the engine that ACTUALLY ran, never `k.method`: `-m
4268 // parallel` on an NRM-eligible model reports `nrm`, because that is what
4269 // produced the numbers below and the banner may not claim otherwise.
4270 std::printf("SolverSSA arith=%s method=%s type=%s samples=%zu seed=%lu time=%.6g\n",
4271 line::num_traits<T>::name(), r.method.c_str(),
4272 line::util::method_type("SSA", r.method).c_str(), r.samples, opt.seed,
4273 r.simulated_time);
4274 // ResidT IS NOT RespT UNLESS EVERY STATION IS VISITED ONCE PER CYCLE.
4275 // `sn_get_residt_from_respt` is the reference's own per-visit -> per-job
4276 // conversion and a pure function of `sn` and RN, so a solver that reports no
4277 // residence time of its own still owes the caller this one: reporting RespT
4278 // in its place was a factor of 3 out on sdroute_closed and 17 on Queue1 of
4279 // init_state_ps, both multi-visit closed models.
4280 //
4281 // TAKEN ON THE MEASURED CACHE SPLIT, not on the offered one: the visits this
4282 // conversion divides by are a function of the routing, and a cache's routing
4283 // is a RESULT. On tut06_cache_lru_zipf the base struct still carried
4284 // `link()`'s even hit/miss share, so both classes came back at exactly half
4285 // their response time (0.1 and 0.5 against 0.16475 and 0.17625) -- a number
4286 // that is not the residence time of any model.
4288 const line::Matrix<T> WN = line::mva::sn_get_residt_from_respt<T>(snw, to_matrix<T>(r.RN));
4289 line::reg::Json extra = line::reg::Json::object();
4291 if (!cache.empty()) extra["Cache"] = cache_extra_json<T>(cache);
4292 // ArvR IS NOT Tput, and reading it off the throughput column was wrong
4293 // wherever the two differ -- most visibly at a JOIN, which takes in one
4294 // sibling per branch and fires once per parent, so its arrival rate is the
4295 // fork degree times its throughput. `ssa_fj_foldback` already divides the
4296 // Join's QLen by the DERIVED rate to get its response time, so reporting
4297 // Tput in the ArvR column left the printed row self-inconsistent
4298 // (0.62372/1.02229 is 0.610, not the 0.3038 beside it). Taken on the
4299 // measured-cache-split struct for the same reason ResidT is.
4300 const line::Matrix<T> AN = line::mva::sn_get_arvr_from_tput<T>(snw, to_matrix<T>(r.TN));
4301 emit_avg_table<T>(sn, r.method, [&](std::size_t i, std::size_t c) {
4302 AvgRow row;
4303 row.q = r.QN(i, c);
4304 row.u = r.UN(i, c);
4305 row.r = r.RN(i, c);
4306 row.w = line::num_traits<T>::to_double(WN(i, c));
4307 row.t = r.TN(i, c);
4308 // A Source has no arrivals TO ITSELF, so its ArvR is 0 while its Tput is
4309 // the arrival rate.
4310 row.a = sn.stations[i].sched == line::lang::SchedStrategy::EXT
4311 ? 0.0
4312 : line::num_traits<T>::to_double(AN(i, c));
4313 return row;
4314 }, extra);
4315 return 0;
4316}
4317
4318/** The knobs the fluid solver reads, in one place so every fluid arm reads the
4319 * same set: an arm that quietly dropped one would answer a different model. */
4320inline line::fluid::FluidOptions fluid_options(const Knobs& k) {
4322 if (!k.method.empty()) opt.method = k.method;
4323 if (k.tol >= 0.0) opt.tol = k.tol;
4324 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4325 if (k.iter_max >= 0) opt.iter_max = static_cast<std::size_t>(k.iter_max);
4326 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
4327 // `pstar_set` is what makes the smoothing active under a method that did
4328 // not ask for it by name, exactly as `options.config.pstar` does in the
4329 // reference: setting the exponent alone would leave `-a avg` integrating
4330 // the hard-min drift while reporting the caller's choice.
4331 if (k.pstar > 0.0) {
4332 opt.pstar = k.pstar;
4333 opt.pstar_set = true;
4334 }
4335 return opt;
4336}
4337
4338/**
4339 * Solve a Network model.json with the fluid solver and print the same table as
4340 * the MVA path, so the parity harness can diff the two rows unchanged.
4341 *
4342 * ResidT and ArvR are reported as the per-visit response time and the
4343 * throughput: the fluid analyzer works at station level and, unlike the MVA
4344 * runner, has no chain-visit conversion behind it. That is what MATLAB's
4345 * fluid `getAvgTable` shows for these columns on a single-visit model.
4346 *
4347 * Only `double` reaches here -- the drift is integrated by LSODA -- so the
4348 * other backends are refused by name in `solve_model_dispatch` rather than
4349 * being narrowed silently.
4350 */
4351template <class T>
4352int solve_model_fluid(const std::string& file, const Knobs& k) {
4353 line::qn::Network<T> net = read_model<T>(file);
4354 const line::fluid::FluidOptions opt = fluid_options(k);
4355 // THE map_env FUNNEL, asked directly rather than through `run_avg`: this
4356 // runner returns a `FluidSolution` and not an `AvgResult`, so the arm cannot
4357 // pass itself as the `Run` callable. The decision and the config are the
4358 // ones `run_avg` would have used, so `-a avg` and the four
4359 // `run_avg_engine` views accept the same models for the same command line.
4360 // No arithmetic guard is needed here: this arm is instantiated at `double`
4361 // alone, the dispatcher having refused the other backends by name.
4362 {
4363 const line::solvers::MapEnvConfig mecfg = map_env_config(k);
4365 net.get_struct(), mecfg)
4366 .needed) {
4368 net.get_struct(), "SolverFluid", mecfg, line::solvers::fluid_stage_fn(opt),
4369 opt.method);
4370 std::printf("SolverFluid arith=%s method=%s type=%s iters=%d\n",
4372 line::util::method_type("FLD", rme.actualmethod).c_str(), rme.iter);
4373 print_avg_table<T>(net.get_struct(), rme);
4374 return 0;
4375 }
4376 }
4377 // `solver_fluid_run_analyzer` is runAnalyzer's resolution over the analyzer, so a
4378 // Cache model reaches the rmf branch, a DPS model the closing drift, and
4379 // anything the moment closure accepts reaches `minnormal`.
4380 // The converged hit/miss split is COLLECTED, for the reason the SSA arm
4381 // above collects its own: `-a avg` is what MATLAB's lang='cpp' bridge calls,
4382 // and a missing block there CLEARS the host's Cache node.
4384 // THE REFRESHED STRUCT IS TAKEN, not dropped: on a cache model the routing
4385 // the analyzer converged to carries the ACTUAL hit/miss split, where
4386 // `net.get_struct()` still carries link()'s offered one. The arrival rates
4387 // below are read off that routing, so the offered split reported 0.5/0.5
4388 // where the model converged to 0.4/0.6 (cache_replc_routing).
4391 net.get_struct(), opt, static_cast<line::qn::NetworkStruct<T>*>(nullptr),
4392 &refreshed, &cache);
4393 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4394 const line::qn::NetworkStruct<T>& snflow =
4395 refreshed.nstations == sn.nstations && refreshed.nclasses == sn.nclasses ? refreshed : sn;
4396
4397 std::printf("SolverFluid arith=%s method=%s type=%s iters=%zu\n", line::num_traits<T>::name(),
4398 r.method.c_str(),
4399 line::util::method_type("FLD", r.method).c_str(), r.iters);
4400 // The residence-time conversion the SSA arm above applies, for the same
4401 // reason: neither analyzer produces a per-job residence time, and
4402 // `sn_get_residt_from_respt` derives one from RN and the visit ratios.
4403 const line::Matrix<T> WN = line::mva::sn_get_residt_from_respt<T>(sn, to_matrix<T>(r.RN));
4404 // THE ARRIVAL RATE IS A FLOW, NOT A COPY OF THE THROUGHPUT. `runAnalyzer`
4405 // takes it from `sn_get_arvr_from_tput`, i.e. from the class-expanded
4406 // routing, and the two agree only where every job a station serves it also
4407 // completes. They part on a station a job LEAVES by another route: on
4408 // cache_replc_routing the fluid solution puts zero throughput on the two
4409 // Delay stations while 0.4 and 0.6 arrive at them, so copying the
4410 // throughput made both rows all-zero and the table dropped them.
4411 const line::Matrix<T> AN = line::mva::sn_get_arvr_from_tput<T>(snflow, to_matrix<T>(r.TN));
4412 line::reg::Json extra = line::reg::Json::object();
4413 if (!cache.empty()) extra["Cache"] = cache_extra_json<T>(cache);
4414 emit_avg_table<T>(sn, r.method, [&](std::size_t i, std::size_t c) {
4415 AvgRow row;
4416 row.q = r.QN(i, c);
4417 row.u = r.UN(i, c);
4418 row.r = r.RN(i, c);
4419 row.w = line::num_traits<T>::to_double(WN(i, c));
4420 row.t = r.TN(i, c);
4421 // A Source has no arrivals TO ITSELF, so its ArvR is 0 while its Tput is
4422 // the arrival rate -- the same rule the SSA arm above applies. Without it
4423 // the two CLI paths disagreed on one column of the same model:
4424 // gallery_mm1 reported Source ArvR 0 under -s mva and 1 under -s fluid.
4425 row.a = sn.stations[i].sched == line::lang::SchedStrategy::EXT
4426 ? 0.0
4427 : line::num_traits<T>::to_double(AN(i, c));
4428 return row;
4429 }, extra);
4430 return 0;
4431}
4432
4433/**
4434 * `-s fluid -a statevec`: the converged FLUID STATE VECTOR, `result.odeStateVec`.
4435 *
4436 * NOT A METRIC AND NOT INDEXED LIKE ONE. The ODE state carries one coordinate
4437 * per (station, class, PHASE), so a two-phase Erlang service contributes two
4438 * entries where the AvgTable contributes one number, and the sum over a
4439 * station's phases is its mean queue length. It is what a caller needs to
4440 * restart an integration, to seed another solver, or to read the phase
4441 * occupancy the means average away -- which is why the JAR exposes it as its
4442 * own `-a statevec` rather than as a column.
4443 *
4444 * The index layout is `fluid_state_layout`'s and is emitted BESIDE the vector,
4445 * because a bare list of numbers cannot be related back to a station without it.
4446 */
4447template <class T>
4448int solve_model_fluid_statevec(const std::string& file, const Knobs& k) {
4449 line::qn::Network<T> net = read_model<T>(file);
4450 const line::fluid::FluidOptions opt = fluid_options(k);
4452 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4453 if (r.xvec.empty())
4455 "-a statevec reports the converged ODE state and this solve produced none; the rmf "
4456 "and closing branches integrate a drift and fill it, so a branch that returns means "
4457 "directly has no state vector to report");
4458
4459 // The (station, class, phase) each coordinate belongs to, in the order the
4460 // ODE state is laid out: station-major, then class, then phase.
4461 std::vector<std::size_t> ist, cls, phs;
4462 for (std::size_t i = 0; i < sn.nstations; ++i)
4463 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4464 const std::size_t np = sn.phases_of(i + 1, c + 1);
4465 for (std::size_t j = 0; j < np; ++j) {
4466 ist.push_back(i);
4467 cls.push_back(c);
4468 phs.push_back(j);
4469 }
4470 }
4471 // The layout is a CLAIM about the solver's state ordering, so it is checked
4472 // rather than asserted in a comment: a mismatch means the labels below would
4473 // name the wrong station, which is worse than no labels at all.
4474 const bool labelled = ist.size() == r.xvec.size();
4475
4476 if (g_json_output) {
4477 line::reg::Json p = line::reg::Json::object();
4478 p["type"] = "FluidStateVec";
4479 p["indexBase"] = 0;
4480 p["xvec"] = vector_json(r.xvec);
4481 p["labelled"] = labelled;
4482 if (labelled) {
4483 line::reg::Json st = line::reg::Json::array(), cl = line::reg::Json::array(),
4484 ph = line::reg::Json::array();
4485 for (std::size_t j = 0; j < ist.size(); ++j) {
4486 st.push_back(sn.stations[ist[j]].name);
4487 cl.push_back(sn.classes[cls[j]].name);
4488 ph.push_back(phs[j]);
4489 }
4490 p["Station"] = st;
4491 p["JobClass"] = cl;
4492 p["Phase"] = ph;
4493 }
4494 emit_analysis<T>("statevec", p, r.method);
4495 return 0;
4496 }
4497 std::printf("SolverFluid arith=%s method=%s coords=%zu\n", line::num_traits<T>::name(),
4498 r.method.c_str(), r.xvec.size());
4499 if (!labelled) {
4500 std::printf("# the ODE state is %zu wide and the (station, class, phase) layout accounts "
4501 "for %zu; the coordinates are printed unlabelled\n",
4502 r.xvec.size(), ist.size());
4503 std::printf("%-10s %20s\n", "Index", "x");
4504 for (std::size_t j = 0; j < r.xvec.size(); ++j)
4505 std::printf("%-10zu %20.10g\n", j, r.xvec[j]);
4506 return 0;
4507 }
4508 std::printf("%-16s %-14s %-8s %20s\n", "Station", "JobClass", "Phase", "x");
4509 for (std::size_t j = 0; j < r.xvec.size(); ++j)
4510 std::printf("%-16s %-14s %-8zu %20.10g\n", sn.stations[ist[j]].name.c_str(),
4511 sn.classes[cls[j]].name.c_str(), phs[j], r.xvec[j]);
4512 return 0;
4513}
4514
4515/**
4516 * Solve an ENVIRONMENT model.json with SolverENV and print the same average
4517 * table every other arm prints.
4518 *
4519 * THE COLUMNS ARE THE REFERENCE'S, INCLUDING THE TWO IT LEAVES EMPTY.
4520 * `@@SolverENV/getEnsembleAvg` returns Q, U and T from the coupling, sets
4521 * `WNclass = QNclass ./ TNclass` and returns `RNclass` and `ANclass` as NaN --
4522 * ENV blends per-stage metrics over the environment process and computes no
4523 * response time or arrival rate at all. Printing Q/T under RespT here would
4524 * invent a number the reference declines to give, so RespT and ArvR are NaN and
4525 * ResidT carries the Little's-law ratio, exactly as MATLAB's table does.
4526 *
4527 * The station and class names come from stage 1. The mean-field coupling
4528 * already refuses an environment whose stages disagree on the station or class
4529 * count, so any stage names the same rows.
4530 */
4531/**
4532 * Run the coupling `o.method` names, at the arithmetic the caller asked for.
4533 *
4534 * WHY THIS IS NOT JUST `env::solver_env`. That entry instantiates BOTH
4535 * couplings, and the mean-field one solves each stage with the fluid transient
4536 * -- LSODA, hence double. Calling it at `Rational` does not merely give a worse
4537 * answer, it does not compile (`sqrt` on a rational), so the template below
4538 * carries only the state-vector coupling and the double overload beside it
4539 * carries the full dispatch. The dispatcher has already refused `-s env
4540 * --arith exact` without `--method statevec`, so a non-double run reaching here HAS
4541 * asked for the state-vector coupling and gets it, banner included.
4542 */
4543template <class T>
4545 const line::env::EnvOptions& o) {
4547 out.statevec =
4548 line::env::solver_env_statevec(e, line::env::dispatch_detail::env_statevec_options<T>(o));
4549 line::env::dispatch_detail::env_take_statevec(out);
4550 return out;
4551}
4552
4553/** The double case, where both couplings are available. */
4555 const line::env::EnvOptions& o) {
4556 return line::env::solver_env(e, o);
4557}
4558
4559template <class T>
4560int solve_model_env(const std::string& file, const Knobs& k) {
4561 line::env::Environment<T> e = read_env_model<T>(file);
4563 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4564 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4565 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
4566 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
4567 if (k.tran_points) opt.tran_points = k.tran_points;
4568 if (k.tol >= 0.0) opt.stage.tol = k.tol;
4569 // THE STAGE SOLVER FOLLOWS THE COUPLING, because each coupling has exactly
4570 // one. `EnvOptions::stage_solver` defaults to `fluid`, which is what the
4571 // mean-field coupling needs (a transient mean) and what the state-vector one
4572 // refuses (it needs an enumerated generator and state space, which only
4573 // SolverCTMC exposes). Leaving the default in place made every
4574 // `--method statevec` run die on "stage solver 'fluid' is not available",
4575 // with no flag to fix it. This is not a silent fallback: there is one
4576 // admissible stage solver per coupling, and picking the other would be the
4577 // error.
4578 if (opt.method == "statevec") opt.stage_solver = "ctmc";
4579 // `--stage-solver` OVERRIDES that default, and only the mean-field coupling
4580 // has a choice to make: it needs a transient mean, which both the fluid
4581 // analyzer and the enumerated CTMC produce, and the two are different
4582 // models rather than two routes to one answer (a chain holds whole jobs).
4583 // An ensemble built on SolverCTMC stages therefore has to say so, or the
4584 // engine answers the fluid ensemble under its name -- which is what the
4585 // hosts refused lang='cpp' for.
4586 if (!k.stage_solver.empty()) opt.stage_solver = k.stage_solver;
4587 if (k.cutoff >= 0.0) opt.stage_cutoff = k.cutoff;
4588
4589 // UNQUALIFIED, so the double overload above wins for T = double: naming the
4590 // template explicitly would send every arithmetic to the state-vector
4591 // coupling and quietly ignore `--method meanfield`.
4592 const line::env::EnvAnalyzerSolution<T> r = env_run(e, opt);
4593 const line::qn::NetworkStruct<T>& sn = e.stage(0).model;
4594
4595 // The horizon and the grid are part of the ANSWER on this path, for the
4596 // same reason the seed and the sample count are on the SSA one: the
4597 // mean-field exit metrics are a quadrature, and two runs are the same
4598 // measurement only if both knobs are stated.
4599 // `points` is the mean-field quadrature's and is printed only there: the
4600 // state-vector coupling carries the whole joint law across a switch and
4601 // never sums over that grid, so reporting a grid it did not use would
4602 // describe a computation that did not happen.
4603 // A closed-form limit reads neither knob: it solves each stage, or one
4604 // rate-averaged model, in STEADY STATE, so a horizon and an iteration count
4605 // would describe a transient and a fixed point that never ran.
4606 if (r.method == "avg" || r.method == "dec")
4607 std::printf("SolverENV arith=%s method=%s stages=%zu (closed-form limit)\n",
4608 line::num_traits<T>::name(), r.method.c_str(), e.nstages());
4609 else if (r.method == "statevec")
4610 std::printf("SolverENV arith=%s method=%s stages=%zu horizon=%.6g iters=%d%s\n",
4612 r.iterations, r.converged ? "" : " (NOT CONVERGED)");
4613 else
4614 // Every remaining method -- meanfield, and the smp and statedep runs of
4615 // the same analyzer -- sums the same quadrature, so all of them report
4616 // the grid it was summed over.
4617 std::printf("SolverENV arith=%s method=%s stages=%zu horizon=%.6g points=%zu iters=%d%s\n",
4619 opt.tran_points, r.iterations, r.converged ? "" : " (NOT CONVERGED)");
4620 // A Cache answers on the NODE and in three index spaces, none of them
4621 // (station, class), so the AvgTable cannot carry it and an environment
4622 // solve that computed one used to drop it here. It rides in the JSON
4623 // envelope for a host and prints as its own rows for a reader.
4624 line::reg::Json extra = line::reg::Json::object();
4625 if (!r.cache.empty()) extra["Cache"] = cache_extra_json<T>(r.cache);
4626 emit_avg_table<T>(
4627 sn, r.method,
4628 [&](std::size_t i, std::size_t c) {
4629 AvgRow row;
4630 row.q = line::num_traits<T>::to_double(r.QN(i, c));
4631 row.u = line::num_traits<T>::to_double(r.UN(i, c));
4632 row.t = line::num_traits<T>::to_double(r.TN(i, c));
4633 row.r = std::numeric_limits<double>::quiet_NaN();
4634 row.a = std::numeric_limits<double>::quiet_NaN();
4635 row.w = row.q / row.t;
4636 return row;
4637 },
4638 extra);
4639 if (!g_json_output) print_cache_rows<T>(sn, r.cache);
4640 return 0;
4641}
4642
4643/**
4644 * `-a var`: the SECOND moment of the queue length, which only the fluid solver
4645 * has and only through two of its methods.
4646 *
4647 * `minnormal` and `refined` report the STATIONARY covariance of the linear noise
4648 * approximation (`@@SolverFLD/getMoments`), `kp` the covariance integrated along
4649 * the trajectory (`@@SolverFLD/getTranAvgVar`); this prints the per-station,
4650 * per-class variance and its standard deviation, plus, on the JSON path, the full
4651 * state covariance so that cross-station terms survive rather than only the
4652 * per-block totals. Every other method carries a first moment only and is refused
4653 * by name -- a variance of zero would be a claim, not an absence.
4654 */
4655template <class T>
4656int solve_model_fluid_var(const std::string& file, const Knobs& k) {
4657 line::qn::Network<T> net = read_model<T>(file);
4658 const line::fluid::FluidOptions opt = fluid_options(k);
4659 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4661 if (!r.has_moments)
4663 "-a var needs a fluid method that computes a second moment: 'minnormal', 'refined' or "
4664 "'dae' for the stationary covariance, 'kp' for the covariance along the trajectory. "
4665 "The '" +
4666 r.method + "' method integrates the mean only");
4667
4668 if (g_json_output) {
4669 line::reg::Json p = line::reg::Json::object();
4670 p["type"] = "QueueLengthVariance";
4671 p["indexBase"] = 0;
4672 p["Station"] = line::reg::Json::array();
4673 p["JobClass"] = line::reg::Json::array();
4674 p["QVar"] = line::reg::Json::array();
4675 p["QStd"] = line::reg::Json::array();
4676 for (std::size_t i = 0; i < sn.nstations; ++i)
4677 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4678 if (r.moments.QVar(i, c) == 0.0) continue;
4679 p["Station"].push_back(sn.stations[i].name);
4680 p["JobClass"].push_back(sn.classes[c].name);
4681 p["QVar"].push_back(r.moments.QVar(i, c));
4682 p["QStd"].push_back(r.moments.QStd(i, c));
4683 }
4684 p["Sigma"] = matrix_json<double>(r.moments.Sigma);
4685 emit_analysis<T>("var", p, r.method);
4686 return 0;
4687 }
4688 std::printf("SolverFluid arith=%s method=%s second moment\n", line::num_traits<T>::name(),
4689 r.method.c_str());
4690 std::printf("%-16s %-14s %12s %12s\n", "Station", "JobClass", "QVar", "QStd");
4691 for (std::size_t i = 0; i < sn.nstations; ++i)
4692 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4693 if (r.moments.QVar(i, c) == 0.0) continue;
4694 std::printf("%-16s %-14s %12.6g %12.6g\n", sn.stations[i].name.c_str(),
4695 sn.classes[c].name.c_str(), r.moments.QVar(i, c), r.moments.QStd(i, c));
4696 }
4697 return 0;
4698}
4699
4700/**
4701 * `-a odes`: `@@SolverFLD/exportODEs`, the drift itself rather than its fixed
4702 * point.
4703 *
4704 * The output is the LaTeX document the reference writes, printed to stdout so
4705 * that it can be redirected. It carries a machine-readable comment header
4706 * naming every state variable and every event, which is what makes the document
4707 * diffable against MATLAB's rather than only readable.
4708 */
4709template <class T>
4710int solve_model_fluid_odes(const std::string& file, const Knobs& k) {
4711 line::qn::Network<T> net = read_model<T>(file);
4713 if (!k.method.empty()) opt.method = k.method;
4714 if (k.tol >= 0.0) opt.tol = k.tol;
4715 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4716 // `--notation` REACHES THE EXPORTER, and is not fixed at "scalar" here: the
4717 // matrix form is a different document of the same drift, and hardcoding one
4718 // while accepting a flag naming the other is the silent-acceptance defect
4719 // this CLI refuses everywhere else. An unrecognised name is refused by
4720 // `export_odes_latex` itself rather than defaulted.
4721 const std::string notation = k.notation.empty() ? "scalar" : k.notation;
4722 const std::string tex = line::fluid::solver_fluid_export_odes(sn, opt, notation, sn.name);
4723 if (g_json_output) {
4724 line::reg::Json p = line::reg::Json::object();
4725 p["type"] = "ODEs";
4726 p["notation"] = notation;
4727 // The document goes in a STRING VALUE, escaped by the JSON writer: it is
4728 // LaTeX, so it carries backslashes and newlines that a host reading raw
4729 // stdout would have to re-parse out of the surrounding table.
4730 p["latex"] = tex;
4731 emit_analysis<T>("odes", p,
4732 line::fluid::detail::fluid_resolve_method(sn, opt.method, opt));
4733 return 0;
4734 }
4735 std::printf("%s\n", tex.c_str());
4736 return 0;
4737}
4738
4739/**
4740 * `-a jacobian`: `@@SolverFLD/getJacobian`, d f_i / d x_j of the mean-field
4741 * drift, with the equilibria beside it when they are asked for.
4742 *
4743 * This is the fixed point's LOCAL BEHAVIOUR, which no integration reports: the
4744 * eigenvalues of J tell a stable fixed point from a limit cycle and give the
4745 * rate at which the fluid approximation converges to it.
4746 *
4747 * The method must be a smooth one. `fluid_symbolic_drift` refuses the min-scaled
4748 * drifts by the factor that carries the kink, before any backend is contacted.
4749 */
4750template <class T>
4751int solve_model_fluid_jacobian(const std::string& file, const Knobs& k) {
4752 line::qn::Network<T> net = read_model<T>(file);
4754 if (!k.method.empty()) opt.method = k.method;
4755 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4756 // pstar REACHES THE SYSTEM ONLY UNDER `pnorm`, which is the rule the
4757 // integrator and the exporter both follow: `matrix` and `default` leave it
4758 // at zero, selecting the hard min. That is why they have no Jacobian here
4759 // and `pnorm` does, and it is the reference's rule too -- MATLAB reads
4760 // options.config.pstar, which is unset unless asked for.
4761 std::string m = opt.method;
4762 if (m.compare(0, 4, "fld.") == 0) m = m.substr(4);
4764 sn, opt.method, (m == "pnorm" || opt.pstar_set) ? opt.pstar : 0.0, std::vector<double>());
4766 if (!k.symbolic.empty()) symopt.backend = k.symbolic;
4767 symopt.equilibria = k.equilibria;
4769
4770 if (g_json_output) {
4771 line::reg::Json p = line::reg::Json::object();
4772 p["type"] = "Jacobian";
4773 p["engine"] = jac.engine;
4774 p["vars"] = line::reg::Json(jac.vars);
4775 p["rhs"] = line::reg::Json(jac.rhs);
4776 line::reg::Json rows = line::reg::Json::array();
4777 for (std::size_t i = 0; i < jac.J.size(); ++i) rows.push_back(line::reg::Json(jac.J[i]));
4778 p["jacobian"] = rows;
4779 // `hasEquilibria` separates "asked and answered with none" from "never
4780 // asked"; an empty list alone would read as "this system has none".
4781 p["hasEquilibria"] = jac.has_equilibria;
4782 line::reg::Json eqs = line::reg::Json::array();
4783 for (std::size_t e = 0; e < jac.equilibria.size(); ++e) {
4784 line::reg::Json one = line::reg::Json::object();
4785 for (std::map<std::string, std::string>::const_iterator it = jac.equilibria[e].begin();
4786 it != jac.equilibria[e].end(); ++it)
4787 one[it->first] = it->second;
4788 eqs.push_back(one);
4789 }
4790 p["equilibria"] = eqs;
4791 emit_analysis<T>("jacobian", p,
4792 line::fluid::detail::fluid_resolve_method(sn, opt.method, opt));
4793 return 0;
4794 }
4795
4796 std::printf("engine=%s states=%zu\n", jac.engine.c_str(), jac.vars.size());
4797 for (std::size_t i = 0; i < jac.rhs.size(); ++i)
4798 std::printf("d%s/dt = %s\n", jac.vars[i].c_str(), jac.rhs[i].c_str());
4799 for (std::size_t i = 0; i < jac.J.size(); ++i)
4800 for (std::size_t j = 0; j < jac.J[i].size(); ++j) {
4801 // A structurally zero entry is printed, not skipped: a reader must be
4802 // able to tell a zero derivative from a row this port never emitted.
4803 std::printf("J[%s,%s] = %s\n", jac.vars[i].c_str(), jac.vars[j].c_str(),
4804 jac.J[i][j].c_str());
4805 }
4806 if (jac.has_equilibria) {
4807 if (jac.equilibria.empty())
4808 std::printf("equilibria: none in closed form (the solve found none, which is not a "
4809 "proof that none exist)\n");
4810 for (std::size_t e = 0; e < jac.equilibria.size(); ++e)
4811 for (std::map<std::string, std::string>::const_iterator it = jac.equilibria[e].begin();
4812 it != jac.equilibria[e].end(); ++it)
4813 std::printf("equilibrium %zu: %s = %s\n", e + 1, it->first.c_str(),
4814 it->second.c_str());
4815 }
4816 return 0;
4817}
4818
4819/**
4820 * `-s fluid -a tranvar`: `@@SolverFLD/getTranAvgVar`, the queue-length VARIANCE
4821 * along the trajectory, plus the full state covariance at each time point.
4822 *
4823 * NOT `-a var`, which reports the STATIONARY covariance of `minnormal` /
4824 * `refined` -- one number per (station, class) at the fixed point. This is the
4825 * diffusion limit of Ko and Pender integrated alongside the fluid limit, so it
4826 * has a value at every t, and only `--method kp` produces it. Asking any other
4827 * method for it is an error rather than a misleading zero, which is the header's
4828 * own rule and is left to the header to enforce so the two flags cannot drift.
4829 */
4830template <class T>
4831int solve_model_fluid_tranvar(const std::string& file, const Knobs& k) {
4832 line::qn::Network<T> net = read_model<T>(file);
4833 const line::fluid::FluidOptions opt = fluid_options(k);
4834 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4836 if (tr.t.empty())
4837 throw line::UnsupportedError("-a tranvar produced no trajectory points");
4838
4839 if (g_json_output) {
4840 line::reg::Json p = line::reg::Json::object();
4841 p["type"] = "TranAvgVarTable";
4842 p["indexBase"] = 0;
4843 p["t0"] = tr.t.front();
4844 p["t1"] = tr.t.back();
4845 line::reg::Json ts = line::reg::Json::array();
4846 for (std::size_t j = 0; j < tr.t.size(); ++j) ts.push_back(tr.t[j]);
4847 p["t"] = ts;
4848 line::reg::Json arr = line::reg::Json::array();
4849 for (std::size_t i = 0; i < sn.nstations; ++i)
4850 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4851 if (sn.disabled[i][c]) continue;
4852 line::reg::Json e = line::reg::Json::object();
4853 e["Station"] = sn.stations[i].name;
4854 e["JobClass"] = sn.classes[c].name;
4855 e["station"] = i;
4856 e["jobclass"] = c;
4857 line::reg::Json v = line::reg::Json::array();
4858 for (std::size_t j = 0; j < tr.QVar.size(); ++j) v.push_back(tr.QVar[j](i, c));
4859 e["QVar"] = v;
4860 arr.push_back(e);
4861 }
4862 p["curves"] = arr;
4863 // The full covariance is the answer's other half: the per-pair variances
4864 // are its diagonal, and a caller asking for the diffusion limit wants the
4865 // off-diagonal correlations the limit is about.
4866 line::reg::Json sig = line::reg::Json::array();
4867 for (std::size_t j = 0; j < tr.Sigma.size(); ++j)
4868 sig.push_back(matrix_json<double>(tr.Sigma[j]));
4869 p["Sigma"] = sig;
4870 emit_analysis<T>("tranvar", p, "kp");
4871 return 0;
4872 }
4873 std::printf("SolverFluid arith=%s method=kp tspan=[%g,%g] points=%zu dim=%zu\n",
4874 line::num_traits<T>::name(), tr.t.front(), tr.t.back(), tr.t.size(),
4875 tr.Sigma.empty() ? std::size_t(0) : tr.Sigma.front().rows());
4876 std::printf("%-16s %-14s %12s %12s %12s\n", "Station", "JobClass", "Time", "QVar", "QStd");
4877 for (std::size_t i = 0; i < sn.nstations; ++i)
4878 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4879 if (sn.disabled[i][c]) continue;
4880 for (std::size_t j = 0; j < tr.QVar.size(); ++j) {
4881 const double var = tr.QVar[j](i, c);
4882 std::printf("%-16s %-14s %12.6g %12.6g %12.6g\n", sn.stations[i].name.c_str(),
4883 sn.classes[c].name.c_str(), tr.t[j], var,
4884 var >= 0.0 ? std::sqrt(var) : std::numeric_limits<double>::quiet_NaN());
4885 }
4886 }
4887 return 0;
4888}
4889
4890/**
4891 * `-s fluid -a tran`: `@@SolverFLD/getTranAvg`, the metrics ALONG the
4892 * trajectory rather than at its fixed point.
4893 *
4894 * `--tspan` is optional here and required on the MAM arm, and the difference is
4895 * the reference's: `options.timespan` defaults to [0, Inf] for the fluid solver,
4896 * which does not mean "integrate forever" but "integrate until the state stops
4897 * moving" -- `solver_fluid_tran_avg` reproduces the horizon that adaptive loop
4898 * converges at. A caller that names a horizon gets exactly that one.
4899 *
4900 * The reference forces `closing` for a transient (the matrix and smoothed
4901 * variants are steady-state devices) and warns when it does; the port forces it
4902 * too, and the banner names the method that actually integrated. `dae` is the
4903 * one exception the reference itself makes -- it has a trajectory of its own,
4904 * with conservation carried as an algebraic equation -- so
4905 * `solver_fluid_run_transient` routes it rather than substituting the
4906 * first-order drift under its name.
4907 */
4908template <class T>
4909int solve_model_fluid_tran(const std::string& file, const Knobs& k) {
4910 line::qn::Network<T> net = read_model<T>(file);
4911 const line::fluid::FluidOptions opt = fluid_options(k);
4912 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4913 const std::vector<line::fluid::FluidTranPoint> tr =
4915 if (tr.empty()) throw line::UnsupportedError("-a tran produced no trajectory points");
4916
4917 if (g_json_output) {
4918 line::reg::Json p = line::reg::Json::object();
4919 p["type"] = "TranAvgTable";
4920 p["indexBase"] = 0;
4921 p["t0"] = 0.0;
4922 p["t1"] = tr.back().t;
4923 line::reg::Json ts = line::reg::Json::array();
4924 for (std::size_t j = 0; j < tr.size(); ++j) ts.push_back(tr[j].t);
4925 line::reg::Json arr = line::reg::Json::array();
4926 for (std::size_t i = 0; i < sn.nstations; ++i)
4927 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4928 if (sn.disabled[i][c]) continue;
4929 line::reg::Json e = line::reg::Json::object();
4930 e["Station"] = sn.stations[i].name;
4931 e["JobClass"] = sn.classes[c].name;
4932 e["station"] = i;
4933 e["jobclass"] = c;
4934 e["t"] = ts;
4935 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array(),
4936 x = line::reg::Json::array();
4937 for (std::size_t j = 0; j < tr.size(); ++j) {
4938 q.push_back(tr[j].QN(i, c));
4939 u.push_back(tr[j].UN(i, c));
4940 x.push_back(tr[j].TN(i, c));
4941 }
4942 e["QLen"] = q;
4943 e["Util"] = u;
4944 e["Tput"] = x;
4945 arr.push_back(e);
4946 }
4947 p["curves"] = arr;
4948 emit_analysis<T>("tran", p, "closing");
4949 return 0;
4950 }
4951 std::printf("SolverFluid arith=%s method=closing tspan=[0,%g] points=%zu\n",
4952 line::num_traits<T>::name(), tr.back().t, tr.size());
4953 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
4954 "Tput");
4955 for (std::size_t i = 0; i < sn.nstations; ++i)
4956 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4957 if (sn.disabled[i][c]) continue;
4958 for (std::size_t j = 0; j < tr.size(); ++j)
4959 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
4960 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), tr[j].t,
4961 tr[j].QN(i, c), tr[j].UN(i, c), tr[j].TN(i, c));
4962 }
4963 return 0;
4964}
4965
4966/**
4967 * `-s fluid -a prob`: `@@SolverFLD/getProbAggr`, the probability that a station
4968 * holds the marginal population of the model's default state.
4969 *
4970 * IT IS NOT THE MVA ARM'S ANSWER AND IS NOT MEANT TO BE. The fluid solver has
4971 * no state space, so the law is fitted to the means it does produce -- a
4972 * binomial per closed class, the BCMP marginal per open one -- and under a
4973 * moment closure it is instead the multivariate normal the closure supplies,
4974 * correlation between the classes included. Two solvers disagreeing here is the
4975 * approximation showing, not a defect.
4976 *
4977 * The log-probability is reported beside it because the fitted law underflows
4978 * on a large population, where the linear value is 0 and the log one is not.
4979 */
4980template <class T>
4981int solve_model_fluid_prob(const std::string& file, const Knobs& k) {
4982 line::qn::Network<T> net = read_model<T>(file);
4983 const line::fluid::FluidOptions opt = fluid_options(k);
4984 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4986
4987 std::vector<double> pr(sn.nstations, 0.0), lg(sn.nstations, 0.0);
4988 for (std::size_t i = 0; i < sn.nstations; ++i)
4989 pr[i] = line::fluid::fluid_prob_aggr(sn, r, i + 1, &lg[i]);
4990
4991 if (g_json_output) {
4992 line::reg::Json p = line::reg::Json::object();
4993 p["type"] = "ProbAggr";
4994 p["indexBase"] = 0;
4995 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
4996 lp = line::reg::Json::array();
4997 for (std::size_t i = 0; i < sn.nstations; ++i) {
4998 st.push_back(sn.stations[i].name);
4999 pa.push_back(pr[i]);
5000 lp.push_back(lg[i]);
5001 }
5002 p["Station"] = st;
5003 p["ProbAggr"] = pa;
5004 p["logProbAggr"] = lp;
5005 emit_analysis<T>("prob", p, r.method);
5006 return 0;
5007 }
5008 std::printf("SolverFluid arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5009 r.method.c_str(), line::util::method_type("FLD", r.method).c_str());
5010 std::printf("%-16s %14s %14s\n", "Station", "ProbAggr", "logProbAggr");
5011 for (std::size_t i = 0; i < sn.nstations; ++i)
5012 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(), pr[i], lg[i]);
5013 return 0;
5014}
5015
5016/**
5017 * `-s fluid -a cdf`: `@@SolverFLD/getCdfRespT`, the WHOLE response-time law per
5018 * (station, class) and not only its mean.
5019 *
5020 * The law is read off a second integration in which the jobs present at the
5021 * steady state are MARKED and followed to their departure, so it is the
5022 * stationary response-time distribution of the fluid model. The solve that
5023 * produces the state to mark in is run here, as the reference runs it: its
5024 * `getCdfRespT` clears the cached result and re-runs `getAvg` first.
5025 */
5026template <class T>
5027int solve_model_fluid_cdf(const std::string& file, const Knobs& k) {
5028 line::qn::Network<T> net = read_model<T>(file);
5029 const line::fluid::FluidOptions opt = fluid_options(k);
5030 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5031 const std::vector<std::vector<line::fluid::FluidPassage> > RD =
5033
5034 if (g_json_output) {
5035 line::reg::Json p = line::reg::Json::object();
5036 p["type"] = "CdfRespT";
5037 p["indexBase"] = 0;
5038 line::reg::Json arr = line::reg::Json::array();
5039 for (std::size_t i = 0; i < RD.size(); ++i)
5040 for (std::size_t c = 0; c < RD[i].size(); ++c) {
5041 // An empty curve is an ABSENT law -- a Source, or a class the
5042 // station does not serve -- and is omitted rather than sent as a
5043 // degenerate one.
5044 if (RD[i][c].t.empty()) continue;
5045 line::reg::Json e = line::reg::Json::object();
5046 e["Station"] = sn.stations[i].name;
5047 e["JobClass"] = sn.classes[c].name;
5048 e["station"] = i;
5049 e["jobclass"] = c;
5050 e["t"] = line::reg::Json(RD[i][c].t);
5051 e["F"] = line::reg::Json(RD[i][c].cdf);
5052 arr.push_back(e);
5053 }
5054 p["respt"] = arr;
5055 emit_analysis<T>("cdf", p, std::string());
5056 return 0;
5057 }
5058 std::printf("SolverFluid arith=%s\n", line::num_traits<T>::name());
5059 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
5060 for (std::size_t i = 0; i < RD.size(); ++i)
5061 for (std::size_t c = 0; c < RD[i].size(); ++c)
5062 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
5063 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
5064 sn.classes[c].name.c_str(), RD[i][c].t[j], RD[i][c].cdf[j]);
5065 return 0;
5066}
5067
5068/**
5069 * `-s fluid -a aoi`: `@@SolverFLD/getAvgAoI` and `getCdfAoI` in one answer, the
5070 * Age of Information and Peak AoI laws of a status-update system.
5071 *
5072 * ONLY THE `mfq` METHOD HAS THEM, and only on the topology the age laws are
5073 * defined for: one open class through Source -> Queue -> Sink, a single server,
5074 * capacity 1 (bufferless) or 2 (single buffer), FCFS/LCFS/LCFSPR. The topology
5075 * is tested first so a model that is not one is told WHICH condition it fails
5076 * rather than being handed a number computed for a different system.
5077 *
5078 * `--method` may only say `mfq` here: silently overriding a caller who asked for
5079 * another method would report the age laws under a method that does not produce
5080 * them.
5081 */
5082template <class T>
5083int solve_model_fluid_aoi(const std::string& file, const Knobs& k) {
5084 line::qn::Network<T> net = read_model<T>(file);
5085 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5087 if (!top.ok)
5089 "-a aoi reports the age of a status-update system and needs the topology the age laws "
5090 "are defined for: " +
5091 (top.error.empty() ? std::string("this model is not one") : top.error));
5092 line::fluid::FluidOptions opt = fluid_options(k);
5093 const std::string requested = line::fluid::detail::fluid_unqualify(opt.method);
5094 if (requested != "default" && requested != "mfq")
5096 "-a aoi is the AoI branch of the 'mfq' method; '" + requested +
5097 "' integrates the mean-field drift and carries no age process");
5098 opt.method = "mfq";
5100 if (!r.has_aoi)
5102 "-a aoi: the 'mfq' method did not take its AoI branch on this model");
5103
5104 // The grid `getCdfAoI` builds when the caller names no time points: five
5105 // mean ages, which covers the bulk of both laws.
5106 const double base = (std::isfinite(r.aoi.aoi.mean) && r.aoi.aoi.mean > 0.0) ? r.aoi.aoi.mean : 1.0;
5107 const std::size_t np = 200;
5108 std::vector<double> tv(np), fa(np), fp(np);
5109 for (std::size_t j = 0; j < np; ++j) {
5110 tv[j] = 5.0 * base * static_cast<double>(j) / static_cast<double>(np - 1);
5111 fa[j] = line::fluid::aoi_cdf(r.aoi.aoi, tv[j]);
5112 fp[j] = line::fluid::aoi_cdf(r.aoi.paoi, tv[j]);
5113 }
5114 const double asd = std::sqrt(std::max(0.0, r.aoi.aoi.var));
5115 const double psd = std::sqrt(std::max(0.0, r.aoi.paoi.var));
5116
5117 if (g_json_output) {
5118 line::reg::Json p = line::reg::Json::object();
5119 p["type"] = "AoI";
5120 p["systemType"] = r.aoi.system_type;
5121 p["preemption"] = r.aoi.preemption;
5122 p["AoIMean"] = r.aoi.aoi.mean;
5123 p["AoIVar"] = r.aoi.aoi.var;
5124 p["AoIStd"] = asd;
5125 p["PAoIMean"] = r.aoi.paoi.mean;
5126 p["PAoIVar"] = r.aoi.paoi.var;
5127 p["PAoIStd"] = psd;
5128 p["t"] = line::reg::Json(tv);
5129 p["AoICdf"] = line::reg::Json(fa);
5130 p["PAoICdf"] = line::reg::Json(fp);
5131 // The (g, A, h) DENSITY triples, not only the curve evaluated above.
5132 // `getCdfAoI` takes an optional t_values, and a caller who names their
5133 // own grid cannot be served from a fixed 200-point one; with the triple
5134 // they evaluate the same law at their own abscissae. `solve_mfq_aoi`
5135 // normalizes g so that g*expm(A t)*h is the density, so the survival
5136 // function carries an extra inv(A) -- the MATLAB getter's own note.
5137 p["AoI_g"] = line::reg::Json(r.aoi.aoi.g);
5138 p["AoI_A"] = matrix_json(r.aoi.aoi.A);
5139 p["AoI_h"] = line::reg::Json(r.aoi.aoi.h);
5140 p["PAoI_g"] = line::reg::Json(r.aoi.paoi.g);
5141 p["PAoI_A"] = matrix_json(r.aoi.paoi.A);
5142 p["PAoI_h"] = line::reg::Json(r.aoi.paoi.h);
5143 emit_analysis<T>("aoi", p, r.method);
5144 return 0;
5145 }
5146 std::printf("SolverFluid arith=%s method=mfq system=%s preemption=%.6g\n",
5148 std::printf("%-8s %14s %14s %14s\n", "Metric", "Mean", "Var", "Std");
5149 std::printf("%-8s %14.10g %14.10g %14.10g\n", "AoI", r.aoi.aoi.mean, r.aoi.aoi.var, asd);
5150 std::printf("%-8s %14.10g %14.10g %14.10g\n", "PAoI", r.aoi.paoi.mean, r.aoi.paoi.var, psd);
5151 std::printf("%14s %14s %14s\n", "Time", "F_AoI(t)", "F_PAoI(t)");
5152 for (std::size_t j = 0; j < np; ++j)
5153 std::printf("%14.8g %14.10g %14.10g\n", tv[j], fa[j], fp[j]);
5154 return 0;
5155}
5156
5157/**
5158 * Solve a Network model.json and print its aggregate state probabilities:
5159 * getProbSysAggr (the whole-system joint) and getProbAggr per station, over the
5160 * model's default initial state. These fit the MVA means and so need logarithms;
5161 * under exact/Rational they refuse by name.
5162 */
5163template <class T>
5164int solve_model_prob(const std::string& file, const Knobs& k) {
5167 "the -a prob analysis fits a binomial/product-form law and needs transcendental "
5168 "arithmetic; rerun with --arith double or --arith real");
5169 } else {
5170 line::qn::Network<T> net = read_model<T>(file);
5172 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5173 if (k.tol >= 0.0) opt.tol = k.tol;
5174 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5175 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5176 line::Matrix<T> init;
5178 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5179 const line::mva::AggrResult<T> ps =
5181 if (g_json_output) {
5182 line::reg::Json p = line::reg::Json::object();
5183 p["type"] = "ProbAggr";
5184 p["indexBase"] = 0;
5185 p["ProbSysAggr"] = line::num_traits<T>::to_double(ps.P);
5186 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array();
5187 for (std::size_t i = 0; i < sn.nstations; ++i) {
5188 st.push_back(sn.stations[i].name);
5189 pa.push_back(line::num_traits<T>::to_double(
5190 line::mva::solver_mva_get_prob_aggr(sn, r, i + 1, opt.method).P));
5191 }
5192 p["Station"] = st;
5193 p["ProbAggr"] = pa;
5194 emit_analysis<T>("prob", p, r.actualmethod);
5195 return 0;
5196 }
5197 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5198 r.actualmethod.c_str(),
5199 line::util::method_type("MVA", r.actualmethod).c_str());
5200 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(ps.P));
5201 std::printf("%-16s %14s\n", "Station", "ProbAggr");
5202 for (std::size_t i = 0; i < sn.nstations; ++i) {
5203 const line::mva::AggrResult<T> pa =
5205 std::printf("%-16s %14.10g\n", sn.stations[i].name.c_str(),
5207 }
5208 return 0;
5209 }
5210}
5211
5212/**
5213 * `-s mva -a marg`: `@@SolverMVA/getProbMarg`, P(n jobs of class r at station i).
5214 *
5215 * THE WHOLE GRID BY DEFAULT, one curve per (station, class): the reference takes
5216 * the station and the class as arguments and this CLI has no notion of a
5217 * "current" pair, so reporting every pair is the only reading that answers the
5218 * method rather than a choice this file would be making on the caller's behalf.
5219 * `--node` and `--class` narrow it to one node's station and one class, and
5220 * `--marg-states` is the reference's third argument `state_m`: the n values to
5221 * report, in place of the default range each case picks for itself (0..N_r for a
5222 * closed class, mean + 5 sigma for a Poisson, the 1e-10 tail for a geometric).
5223 *
5224 * A NODE THAT IS NOT A STATION IS AN ERROR, not an empty answer: a queue-length
5225 * law at a ClassSwitch is not a quantity, and defaulting to the whole network
5226 * after the caller narrowed it would report more than was asked for.
5227 */
5228template <class T>
5229int solve_model_marg(const std::string& file, const Knobs& k) {
5232 "the -a marg analysis fits a binomial / Poisson / geometric law and needs "
5233 "transcendental arithmetic; rerun with --arith double or --arith real");
5234 } else {
5235 line::qn::Network<T> net = read_model<T>(file);
5237 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5238 if (k.tol >= 0.0) opt.tol = k.tol;
5239 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5240 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5241 line::Matrix<T> init;
5243 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5244
5245 std::vector<std::size_t> ists; // 1-based station indices to report
5246 if (k.node) {
5247 if (k.node > sn.nof_nodes())
5248 throw line::InputError("--node " + std::to_string(k.node) +
5249 " exceeds the number of nodes in the model (" +
5250 std::to_string(sn.nof_nodes()) + ")");
5251 const std::size_t ist = sn.nodes[k.node - 1].station;
5252 if (!ist)
5253 throw line::InputError("--node " + std::to_string(k.node) + " ('" +
5254 sn.nodes[k.node - 1].name +
5255 "') is not a station, and a queue-length distribution is "
5256 "reported per station");
5257 ists.push_back(ist);
5258 } else {
5259 for (std::size_t i = 0; i < sn.nstations; ++i) ists.push_back(i + 1);
5260 }
5261 std::vector<std::size_t> rs; // 1-based class indices to report
5262 if (k.jobclass) {
5263 if (k.jobclass > sn.nclasses)
5264 throw line::InputError("--class " + std::to_string(k.jobclass) +
5265 " exceeds the number of classes in the model");
5266 rs.push_back(k.jobclass);
5267 } else {
5268 for (std::size_t c = 0; c < sn.nclasses; ++c) rs.push_back(c + 1);
5269 }
5270
5271 // Every curve first: a pair the reference refuses must not leave a
5272 // banner and a column header standing above an answer that never came.
5273 std::vector<line::mva::MargResult<T> > curves;
5274 for (std::size_t a = 0; a < ists.size(); ++a)
5275 for (std::size_t b = 0; b < rs.size(); ++b)
5276 curves.push_back(line::mva::solver_mva_get_prob_marg(sn, r, ists[a], rs[b],
5277 k.marg_states, opt.method));
5278
5279 if (g_json_output) {
5280 line::reg::Json p = line::reg::Json::object();
5281 p["type"] = "ProbMarg";
5282 p["indexBase"] = 0;
5283 line::reg::Json arr = line::reg::Json::array();
5284 for (std::size_t a = 0, q = 0; a < ists.size(); ++a)
5285 for (std::size_t b = 0; b < rs.size(); ++b, ++q) {
5286 const line::mva::MargResult<T>& m = curves[q];
5287 line::reg::Json e = line::reg::Json::object();
5288 e["station"] = ists[a] - 1;
5289 e["Station"] = sn.stations[ists[a] - 1].name;
5290 e["jobclass"] = rs[b] - 1;
5291 e["JobClass"] = sn.classes[rs[b] - 1].name;
5292 line::reg::Json jobs = line::reg::Json::array();
5293 for (std::size_t n = 0; n < m.P.size(); ++n)
5294 jobs.push_back(k.marg_states.empty() ? static_cast<long>(n)
5295 : k.marg_states[n]);
5296 e["Jobs"] = jobs;
5297 e["P"] = vector_json(m.P);
5298 e["logP"] = vector_json(m.logP);
5299 arr.push_back(e);
5300 }
5301 p["marginal"] = arr;
5302 emit_analysis<T>("marg", p, r.actualmethod);
5303 return 0;
5304 }
5305 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5306 r.actualmethod.c_str(),
5307 line::util::method_type("MVA", r.actualmethod).c_str());
5308 std::printf("%-16s %-14s %-8s %16s\n", "Station", "JobClass", "Jobs", "ProbMarg");
5309 for (std::size_t a = 0, q = 0; a < ists.size(); ++a)
5310 for (std::size_t b = 0; b < rs.size(); ++b, ++q) {
5311 const line::mva::MargResult<T>& m = curves[q];
5312 for (std::size_t n = 0; n < m.P.size(); ++n)
5313 std::printf("%-16s %-14s %-8ld %16.10g\n",
5314 sn.stations[ists[a] - 1].name.c_str(),
5315 sn.classes[rs[b] - 1].name.c_str(),
5316 k.marg_states.empty() ? static_cast<long>(n) : k.marg_states[n],
5318 }
5319 return 0;
5320 }
5321}
5322
5323/**
5324 * `-a normconst`: `@@SolverMVA/getProbNormConstAggr` and `@@SolverNC`'s.
5325 *
5326 * ONE ANALYSIS, TWO SOLVERS, AND THEY DO NOT COMPUTE IT THE SAME WAY. The NC
5327 * arm reads the constant its own solve already formed. The MVA arm RE-ENTERS the
5328 * analyzer at method='exact', as the reference does, because only the exact MVA
5329 * recursion carries a G: an AMVA solve has none, and reporting the requested
5330 * method's number would attribute the constant to an algorithm that never
5331 * produced one. That re-entry is why this is a separate `-a` and not a field on
5332 * the `-s mva` banner, where it would charge every average solve for a second
5333 * exact one.
5334 *
5335 * WHAT A MODEL WITH NO CONSTANT REPORTS IS THE ANALYZER'S OWN ANSWER, not a
5336 * substitution made here, and the two cases differ: the branches that form no G
5337 * at all -- MVAC, the LCFS chain -- set lG to NaN at the source and print nan,
5338 * while the open-queue closed forms report lG = 0 exactly as
5339 * solver_mva_qsys_analyzer.m:54,96,235 does. Neither is edited on the way out.
5340 */
5341/**
5342 * Mean busy period of a named subnetwork, Daduna (J. ACM 35(3), 1988).
5343 *
5344 * The transform `solver_nc_busyp` has been in the port since it landed, and
5345 * `ldes_cli` has answered `--busyperiod` all along, so the ONLY thing between
5346 * a caller and the analytical form was a `-a` token: asking this CLI for a busy
5347 * period meant simulating a quantity there is a closed form for.
5348 *
5349 * `--busyperiod-subnet` is 1-BASED, as every station index this CLI takes is,
5350 * and is required: a busy period is defined for a NAMED set of stations and
5351 * defaulting it would answer about a subnetwork the caller never chose. The
5352 * orders default to 1, the ordinary busy period.
5353 */
5354template <class T>
5355int solve_model_nc_busyp(const std::string& file, const Knobs& k) {
5356 if (k.busy_subnet.empty())
5357 throw line::InputError(
5358 "-a busyperiod needs --busyperiod-subnet: the busy period is defined for a named "
5359 "subnetwork of stations, and no default can choose one");
5360 line::qn::Network<T> net = read_model<T>(file);
5361 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5362 std::vector<std::size_t> subnet;
5363 for (std::size_t t = 0; t < k.busy_subnet.size(); ++t) {
5364 if (k.busy_subnet[t] > sn.nstations)
5365 throw line::InputError("--busyperiod-subnet names station " +
5366 std::to_string(k.busy_subnet[t]) + ", beyond the model's " +
5367 std::to_string(sn.nstations));
5368 subnet.push_back(k.busy_subnet[t] - 1);
5369 }
5370 std::vector<std::size_t> orders = k.busy_orders;
5371 if (orders.empty()) orders.push_back(1);
5372 const std::vector<double> b = line::nc::solver_nc_busyp(sn, subnet, orders);
5373
5374 if (g_json_output) {
5375 line::reg::Json p = line::reg::Json::object();
5376 p["type"] = "BusyPeriod";
5377 p["indexBase"] = 0;
5378 line::reg::Json sj = line::reg::Json::array();
5379 for (std::size_t t = 0; t < subnet.size(); ++t) sj.push_back(subnet[t]);
5380 line::reg::Json oj = line::reg::Json::array();
5381 for (std::size_t t = 0; t < orders.size(); ++t) oj.push_back(orders[t]);
5382 line::reg::Json bj = line::reg::Json::array();
5383 for (std::size_t t = 0; t < b.size(); ++t) bj.push_back(b[t]);
5384 p["subnet"] = sj;
5385 p["orders"] = oj;
5386 p["b"] = bj;
5387 emit_analysis<T>("busyperiod", p, "daduna");
5388 return 0;
5389 }
5390 std::printf("SolverNC arith=%s busy period, subnetwork {", line::num_traits<T>::name());
5391 for (std::size_t t = 0; t < k.busy_subnet.size(); ++t)
5392 std::printf("%s%zu", t ? "," : "", k.busy_subnet[t]);
5393 std::printf("}\n");
5394 for (std::size_t t = 0; t < orders.size(); ++t)
5395 std::printf(" order %zu %.10g\n", orders[t], b[t]);
5396 return 0;
5397}
5398
5399template <class T>
5400int solve_model_normconst(const std::string& file, const Knobs& k, const std::string& solver) {
5401 line::qn::Network<T> net = read_model<T>(file);
5402 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5403 double lG = 0.0;
5404 std::string method;
5405 if (solver == "nc") {
5407 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5408 if (k.tol >= 0.0) opt.tol = k.tol;
5409 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5410 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5412 lG = r.lognormconst.has_value() ? r.lognormconst.value()
5413 : std::numeric_limits<double>::quiet_NaN();
5414 method = r.actualmethod;
5415 } else {
5417 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5418 if (k.tol >= 0.0) opt.tol = k.tol;
5419 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5420 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5422 method = "exact";
5423 }
5424 if (g_json_output) {
5425 line::reg::Json p = line::reg::Json::object();
5426 p["type"] = "NormConst";
5427 p["indexBase"] = 0;
5428 // A NaN rides as JSON null, which is the table's nan on the wire.
5429 p["logNormConstAggr"] = lG;
5430 emit_analysis<T>("normconst", p, method);
5431 return 0;
5432 }
5433 std::printf("Solver%s arith=%s method=%s lognormconst=%.10g\n",
5434 solver == "nc" ? "NC" : "MVA", line::num_traits<T>::name(), method.c_str(), lG);
5435 return 0;
5436}
5437
5438/** The model's declared per-class placement, in `solver_nc_*`'s own container. */
5439template <class T>
5440line::nc::MarginalState nc_declared_marginal(const line::qn::NetworkStruct<T>& sn) {
5442 line::nc::MarginalState out(sn.nstations, std::vector<int>(sn.nclasses, 0));
5443 for (std::size_t i = 0; i < sn.nstations; ++i)
5444 for (std::size_t r = 0; r < sn.nclasses; ++r)
5445 out[i][r] = static_cast<int>(std::llround(line::num_traits<T>::to_double(nir(i, r))));
5446 return out;
5447}
5448
5449/**
5450 * The same two probabilities under SolverNC: `@@SolverNC/getProbSysAggr.m` and
5451 * `@@SolverNC/getProbAggr.m`, over the model's declared state.
5452 *
5453 * NOT THE SAME NUMBERS AS `-s mva -a prob`, and that is the point of having
5454 * both. SolverMVA fits a binomial to its own means (Schmidt 1997); these are a
5455 * ratio of normalizing constants and are the product-form model's own
5456 * probabilities exactly. A closed model therefore reports different figures
5457 * under the two solvers, and the NC ones are the reference.
5458 */
5459template <class T>
5460int solve_model_nc_prob(const std::string& file, const Knobs& k) {
5463 "the -s nc -a prob analysis exponentiates a difference of log normalizing constants "
5464 "and needs transcendental arithmetic; rerun with --arith double or --arith real");
5465 } else {
5466 line::qn::Network<T> net = read_model<T>(file);
5468 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5469 if (k.tol >= 0.0) opt.tol = k.tol;
5470 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5471 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5472 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5473
5474 // The state is the MODEL'S OWN, which `model.json` now carries: a
5475 // stateful node's declared row is decoded to the per-class counts the
5476 // reference reads with `State.toMarginal(sn, ist, state{isf})`. Where a
5477 // station declares none, the default marking is rebuilt for it -- every
5478 // closed class at its reference station -- which is what `initDefault`
5479 // would have put there.
5480 line::nc::MarginalState nir = nc_declared_marginal<T>(sn);
5481 // `--state` is `getProb(node, state)`'s second argument, decoded the way
5482 // the reference decodes it: it substitutes the row into `sn.state{isf}`
5483 // and takes `State.toMarginal` of the result, so what reaches the
5484 // probability is that node's PER-CLASS COUNTS and every other node's
5485 // declared ones. Passing the row through untouched would treat an
5486 // encoding as a job vector, and on a station with phase-type service the
5487 // two differ in both width and meaning.
5488 if (!k.state.empty()) {
5489 if (!k.node)
5490 throw line::InputError(
5491 "--state is the state of ONE node and needs --node to say which");
5492 const std::size_t ist =
5493 k.node <= sn.nodes.size() ? sn.nodes[k.node - 1].station : 0;
5494 if (ist == 0)
5495 throw line::InputError("--node " + std::to_string(k.node) +
5496 " is not a station, so it has no queue-length state");
5497 std::vector<std::size_t> ph(sn.nclasses, 1), shift(sn.nclasses, 0);
5498 std::size_t w = 0;
5499 for (std::size_t c = 0; c < sn.nclasses; ++c) {
5500 ph[c] = sn.phases_of(ist, c + 1);
5501 shift[c] = w;
5502 w += ph[c];
5503 }
5504 std::vector<T> row(k.state.size());
5505 for (std::size_t i = 0; i < k.state.size(); ++i)
5506 row[i] = line::num_traits<T>::from_int(k.state[i]);
5507 const line::qn::Marginal<T> m =
5508 line::qn::to_marginal(sn, ist, row, ph, shift, sn.nvars_of(k.node));
5509 for (std::size_t c = 0; c < sn.nclasses; ++c)
5510 nir[ist - 1][c] =
5511 static_cast<int>(std::llround(line::num_traits<T>::to_double(m.nir[c])));
5512 }
5513
5514 // THE SOLVE'S lG IS NOT THIS lG, and handing it over here was wrong.
5515 // `solver_nc_solve` normalizes the SEIDMANN-REDUCED model -- a
5516 // multiserver station enters as demand/c with the residual folded into
5517 // the delay -- while the probability identity F_i G_{-i} / G needs the
5518 // constant of the load-dependent lattice mu(n) = min(n, c) that F_i and
5519 // G_{-i} are themselves computed on. Mixing the two scaled every
5520 // probability of a model with a multiserver station by one common
5521 // factor: on the 2-job Delay -> PS -> PS(c=2) chain the three stations
5522 // came back 0.17225 / 0.68900 / 0.32536 against the exact 0.18 / 0.72 /
5523 // 0.34, and the error is invisible on a single-server model because
5524 // there the two constants coincide. `solver_nc_margaggr` computes its
5525 // own, once, for every station -- which is the reference's
5526 // `logNormConstAggr` caching, not a per-station resolve.
5527 // THREE OF THE FOUR VIEWS NEED A LOAD-DEPENDENT NORMALIZING CONSTANT
5528 // over the WHOLE network, and `pfqn_ncld` computes it with the caller's
5529 // own method (`solver_nc_margaggr.m:53`, `solver_nc_marg.m:35`,
5530 // `solver_nc_joint.m:32`). A method name with no load-dependent
5531 // algorithm behind it -- the stochastic estimators `ls`, `mci`, `imci`,
5532 // `mcmc`, and the load-INDEPENDENT exact ones such as `comom` -- has
5533 // nothing to run there, and MATLAB refuses `getProb`, `getProbSys` and
5534 // `getProbAggr` under such a name with `pfqn_ncld`'s own message
5535 // (`pfqn_ncld.m:210`). `solver_nc_jointaggr` is the one that escapes:
5536 // its lG comes from `solver_nc` and its per-station factors are
5537 // SINGLE-STATION, which `pfqn_ncld` answers from a degenerate return
5538 // before it ever looks at the name. So the three are OMITTED rather
5539 // than faked or allowed to fail the whole analysis, exactly as
5540 // `-s mva -a prob` omits the detailed pair it has no counterpart for;
5541 // `ProbSysAggr` is still reported, which is what the caller asked for.
5543 const bool has_ld = line::pfqn::ncld_method_try(opt.method, ldm);
5544
5546 T pjoint = line::num_traits<T>::from_int(0);
5547 if (has_ld) {
5548 mr = line::nc::solver_nc_margaggr(sn, opt, nir,
5549 std::numeric_limits<double>::quiet_NaN());
5550 // THE DETAILED PAIR TOO, because `getProb` and `getProbSys` are not
5551 // the aggregate ones with rounding: `solver_nc_marg` and
5552 // `solver_nc_joint` carry the class-within-chain split
5553 // `lg0_i - lG0_i` that the aggregate pair sums out, so on a
5554 // multichain model they are different numbers rather than the same
5555 // one to more places.
5556 mdet = line::nc::solver_nc_marg(sn, opt, nir,
5557 std::numeric_limits<double>::quiet_NaN());
5558 pjoint = line::nc::solver_nc_joint<T>(sn, opt, nir, nullptr);
5559 }
5560 const T ps = line::nc::solver_nc_getprob_sys_aggr(sn, opt, nir);
5561
5563 const std::string am = (opt.method == "default" && !d.actualmethod.empty() &&
5564 d.actualmethod != "default")
5565 ? "default/" + d.actualmethod
5566 : d.actualmethod;
5567 if (g_json_output) {
5568 line::reg::Json p = line::reg::Json::object();
5569 p["type"] = "ProbAggr";
5570 p["indexBase"] = 0;
5571 p["ProbSysAggr"] = line::num_traits<T>::to_double(ps);
5572 line::reg::Json st = line::reg::Json::array();
5573 for (std::size_t i = 0; i < sn.nstations; ++i) st.push_back(sn.stations[i].name);
5574 p["Station"] = st;
5575 if (has_ld) {
5576 p["ProbSys"] = line::num_traits<T>::to_double(pjoint);
5577 line::reg::Json pa = line::reg::Json::array(), pm = line::reg::Json::array();
5578 for (std::size_t i = 0; i < sn.nstations; ++i) {
5579 pa.push_back(line::num_traits<T>::to_double(mr.P[i]));
5580 pm.push_back(line::num_traits<T>::to_double(mdet.P[i]));
5581 }
5582 p["ProbAggr"] = pa;
5583 p["Prob"] = pm;
5584 }
5585 emit_analysis<T>("prob", p, am);
5586 return 0;
5587 }
5588 std::printf("SolverNC arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5589 am.c_str(), line::util::method_type("NC", am).c_str());
5590 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(ps));
5591 if (!has_ld) {
5592 std::printf("ProbSys, Prob and ProbAggr need a load-dependent normalizing constant, "
5593 "which method '%s' does not compute\n",
5594 opt.method.c_str());
5595 return 0;
5596 }
5597 std::printf("ProbSys %.10g\n", line::num_traits<T>::to_double(pjoint));
5598 std::printf("%-16s %14s %14s\n", "Station", "Prob", "ProbAggr");
5599 for (std::size_t i = 0; i < sn.nstations; ++i)
5600 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(),
5603 return 0;
5604 }
5605}
5606
5607/**
5608 * `-s nc -a sysmarg`: `@@SolverNC/getProbSysMarg.m`, the JOINT law of the
5609 * per-station total queue lengths.
5610 *
5611 * NEITHER `-a prob` NOR `-a marg`, and the three are worth telling apart.
5612 * `-a prob` fixes the PER-CLASS population of every station and is a product
5613 * form; `-a marg` is this law marginalized down to ONE station; this arm is the
5614 * joint over all of them, with the classes summed out. Each value is the sum of
5615 * `-a prob` over the whole fibre of per-class tables with these row sums, and
5616 * that fibre grows combinatorially, so it is evaluated as a permanent of the
5617 * demand matrix replicated once per job (Ryser 1963) rather than enumerated.
5618 *
5619 * The whole lattice of total states is swept, so the printed column sums to one
5620 * and the sweep pays for the normalizing constant once.
5621 */
5622template <class T>
5623int solve_model_nc_sysmarg(const std::string& file, const Knobs& k) {
5624 line::qn::Network<T> net = read_model<T>(file);
5626 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5627 if (k.tol >= 0.0) opt.tol = k.tol;
5628 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5629
5630 double Ntot = 0.0;
5631 for (std::size_t r = 0; r < sn.nclasses; ++r) {
5632 const double pop = sn.classes[r].population;
5633 if (!std::isfinite(pop))
5635 "getProbSysMarg requires a closed model: the joint law of the total queue lengths "
5636 "is not defined when a class has an infinite population");
5637 Ntot += pop;
5638 }
5639 const std::vector<std::vector<int> > states = line::pfqn::multichoose_rows(
5640 static_cast<int>(sn.nstations), static_cast<int>(std::llround(Ntot)));
5641
5642 std::vector<double> P(states.size(), 0.0);
5643 for (std::size_t j = 0; j < states.size(); ++j)
5645 line::nc::solver_nc_getprob_sys_marg(sn, opt, states[j], k.method_perm));
5646
5647 if (g_json_output) {
5648 line::reg::Json p = line::reg::Json::object();
5649 p["type"] = "ProbSysMarg";
5650 p["indexBase"] = 0;
5651 p["engine"] = k.method_perm;
5652 line::reg::Json st = line::reg::Json::array(), arr = line::reg::Json::array();
5653 for (std::size_t i = 0; i < sn.nstations; ++i) st.push_back(sn.stations[i].name);
5654 for (std::size_t j = 0; j < states.size(); ++j) {
5655 line::reg::Json e = line::reg::Json::object();
5656 line::reg::Json n = line::reg::Json::array();
5657 for (std::size_t i = 0; i < sn.nstations; ++i) n.push_back(states[j][i]);
5658 e["n"] = n;
5659 e["P"] = P[j];
5660 arr.push_back(e);
5661 }
5662 p["Station"] = st;
5663 p["states"] = arr;
5664 emit_analysis<T>("sysmarg", p, opt.method);
5665 return 0;
5666 }
5667 std::printf("SolverNC arith=%s method=%s engine=%s\n", line::num_traits<T>::name(),
5668 opt.method.c_str(), k.method_perm.c_str());
5669 for (std::size_t i = 0; i < sn.nstations; ++i)
5670 std::printf("%10s", sn.stations[i].name.c_str());
5671 std::printf(" %14s\n", "ProbSysMarg");
5672 double total = 0.0;
5673 for (std::size_t j = 0; j < states.size(); ++j) {
5674 for (std::size_t i = 0; i < sn.nstations; ++i) std::printf("%10d", states[j][i]);
5675 std::printf(" %14.10g\n", P[j]);
5676 total += P[j];
5677 }
5678 std::printf("%*s %14.10g\n", static_cast<int>(10 * sn.nstations), "sum", total);
5679 return 0;
5680}
5681
5682/**
5683 * `-s nc -a marg`: `@@SolverNC/getProbMarg.m`, the TOTAL queue-length law.
5684 *
5685 * NOT THE SAME QUANTITY AS `-s mva -a marg`, although the reference gives both
5686 * methods the same name. SolverMVA's getProbMarg is per (station, CLASS) and is
5687 * a binomial / Poisson / geometric fitted to the solver's own means; SolverNC's
5688 * is the TOTAL number of jobs at a station, summed over classes, and is exact --
5689 * a ratio of normalizing constants, obtained either from one `pfqn_procomom`
5690 * solve (`--method comom`) or by summing the aggregate marginal over the
5691 * per-class partitions of n. `--class` and `--marg-states` are therefore refused
5692 * for it rather than ignored: this law has no class argument and its support is
5693 * 0..sum(N), which the model fixes.
5694 *
5695 * Both P and log P are reported. The log is not a formatting of the other: the
5696 * enumeration forms it first and a probability that underflows to 0 in double
5697 * still has a finite log, so dropping it would lose the only number left.
5698 */
5699template <class T>
5700int solve_model_nc_marg(const std::string& file, const Knobs& k) {
5701 line::qn::Network<T> net = read_model<T>(file);
5703 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5704 if (k.tol >= 0.0) opt.tol = k.tol;
5705 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5706 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5707 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5708
5709 std::vector<std::size_t> ists; // 1-based station indices to report
5710 if (k.node) {
5711 if (k.node > sn.nof_nodes())
5712 throw line::InputError("--node " + std::to_string(k.node) +
5713 " exceeds the number of nodes in the model (" +
5714 std::to_string(sn.nof_nodes()) + ")");
5715 const std::size_t ist = sn.nodes[k.node - 1].station;
5716 if (!ist)
5717 throw line::InputError("--node " + std::to_string(k.node) + " ('" +
5718 sn.nodes[k.node - 1].name +
5719 "') is not a station, and a queue-length distribution is "
5720 "reported per station");
5721 ists.push_back(ist);
5722 } else {
5723 for (std::size_t i = 0; i < sn.nstations; ++i) ists.push_back(i + 1);
5724 }
5725
5726 // Every curve first, for solve_model_marg's reason: a station the reference
5727 // refuses must not leave a header standing above an answer that never came.
5728 std::vector<line::nc::NcQueueLengthDist<T> > curves;
5729 for (std::size_t a = 0; a < ists.size(); ++a)
5730 curves.push_back(line::nc::solver_nc_getprob_marg(sn, opt, ists[a]));
5731
5732 if (g_json_output) {
5733 line::reg::Json p = line::reg::Json::object();
5734 p["type"] = "ProbMargAggr";
5735 p["indexBase"] = 0;
5736 line::reg::Json arr = line::reg::Json::array();
5737 for (std::size_t a = 0; a < ists.size(); ++a) {
5738 line::reg::Json e = line::reg::Json::object();
5739 e["station"] = ists[a] - 1;
5740 e["Station"] = sn.stations[ists[a] - 1].name;
5741 e["P"] = vector_json<T>(curves[a].P);
5742 e["logP"] = vector_json<T>(curves[a].logP);
5743 arr.push_back(e);
5744 }
5745 p["curves"] = arr;
5746 emit_analysis<T>("marg", p, opt.method);
5747 return 0;
5748 }
5749 std::printf("SolverNC arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5750 opt.method.c_str(), line::util::method_type("NC", opt.method).c_str());
5751 for (std::size_t a = 0; a < ists.size(); ++a) {
5752 std::printf("%-16s %-8s %14s %14s\n", "Station", "n", "P", "logP");
5753 for (std::size_t n = 0; n < curves[a].P.size(); ++n)
5754 std::printf("%-16s %-8zu %14.10g %14.10g\n", sn.stations[ists[a] - 1].name.c_str(), n,
5755 line::num_traits<T>::to_double(curves[a].P[n]),
5756 line::num_traits<T>::to_double(curves[a].logP[n]));
5757 }
5758 return 0;
5759}
5760
5761/**
5762 * `-s nc -a cdf`: `@@SolverNC/getCdfRespT.m` and its aliases `getSjrnT`/`sjrnT`.
5763 *
5764 * THE WHOLE LAW, NOT ITS MEAN. `-a avg` reports E[R]; this reports F(t) per
5765 * (station, class) on one shared logarithmic grid, so a percentile or a tail
5766 * probability can be read off it. The algorithm is `pfqn_stdf` (`--method-cdf
5767 * exact`, the default) or the `pfqn_stdf_heur` reduction (`rd`), selected
5768 * through `options.config.algorithm` exactly as in the reference.
5769 *
5770 * FCFS ONLY, and the reference says so by WARNING and returning an empty
5771 * result rather than raising: the sojourn law of a processor-sharing or
5772 * infinite-server station is not the one this inversion computes. That warning
5773 * is carried through to stderr here and the analysis reports no curve, which is
5774 * distinguishable from a curve that is flat.
5775 */
5776template <class T>
5777int solve_model_nc_cdf(const std::string& file, const Knobs& k) {
5780 "the -s nc -a cdf analysis evaluates the sojourn law on a logarithmic time grid and "
5781 "inverts a generating function; it needs transcendental arithmetic, so rerun with "
5782 "--arith double or --arith real");
5783 } else {
5784 line::qn::Network<T> net = read_model<T>(file);
5786 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5787 if (k.tol >= 0.0) opt.tol = k.tol;
5788 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5789 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5790 if (!k.cdf_algorithm.empty()) opt.cdf_algorithm = k.cdf_algorithm;
5791 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5793 if (!r.warning.empty()) std::fprintf(stderr, "warning: %s\n", r.warning.c_str());
5794
5795 if (g_json_output) {
5796 line::reg::Json p = line::reg::Json::object();
5797 p["type"] = "CdfRespT";
5798 p["indexBase"] = 0;
5799 p["algorithm"] = opt.cdf_algorithm;
5800 // ONE OBJECT PER CURVE, as on the CTMC arm: the pairs that carry a
5801 // law are a subset of the grid, so a column form would leave the
5802 // host to re-group them.
5803 line::reg::Json rd = line::reg::Json::array();
5804 for (std::size_t i = 0; i < r.RD.size(); ++i)
5805 for (std::size_t c = 0; c < r.RD[i].size(); ++c) {
5806 // An empty entry is an ABSENT law -- the station is not FCFS
5807 // or does not serve the class -- and is omitted rather than
5808 // sent as a degenerate one.
5809 if (r.RD[i][c].empty()) continue;
5810 line::reg::Json e = line::reg::Json::object();
5811 e["Station"] = sn.stations[i].name;
5812 e["JobClass"] = sn.classes[c].name;
5813 e["station"] = i;
5814 e["jobclass"] = c;
5815 line::reg::Json tt = line::reg::Json::array(), ff = line::reg::Json::array();
5816 for (std::size_t j = 0; j < r.RD[i][c].rows(); ++j) {
5817 ff.push_back(line::num_traits<T>::to_double(r.RD[i][c](j, 0)));
5818 tt.push_back(line::num_traits<T>::to_double(r.RD[i][c](j, 1)));
5819 }
5820 e["t"] = tt;
5821 e["F"] = ff;
5822 rd.push_back(e);
5823 }
5824 p["respt"] = rd;
5825 p["tset"] = vector_json(r.tset);
5826 if (!r.warning.empty()) p["warning"] = r.warning;
5827 // NO "method": the law comes from the sojourn-time inversion and not
5828 // from the normalizing-constant ladder, so the requested method name
5829 // would not be the algorithm that produced these numbers.
5830 emit_analysis<T>("cdf", p, std::string());
5831 return 0;
5832 }
5833 std::printf("SolverNC arith=%s algorithm=%s grid=%zu\n", line::num_traits<T>::name(),
5834 opt.cdf_algorithm.c_str(), r.tset.size());
5835 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
5836 for (std::size_t i = 0; i < r.RD.size(); ++i)
5837 for (std::size_t c = 0; c < r.RD[i].size(); ++c) {
5838 if (r.RD[i][c].empty()) continue;
5839 for (std::size_t j = 0; j < r.RD[i][c].rows(); ++j)
5840 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
5841 sn.classes[c].name.c_str(),
5842 line::num_traits<T>::to_double(r.RD[i][c](j, 1)),
5843 line::num_traits<T>::to_double(r.RD[i][c](j, 0)));
5844 }
5845 return 0;
5846 }
5847}
5848
5849/**
5850 * The clean-up a sensitivity table applies before printing, for a quantity that
5851 * may legitimately be NEGATIVE.
5852 *
5853 * `ln_sanitize` below tests `x <= FineTol`, which is right for a queue length or
5854 * a utilization -- every metric it was written for is nonnegative, so that test
5855 * reads as "negligible". A DERIVATIVE is not: raising a service rate lowers the
5856 * response time, the queue length and the utilization, so the whole sensitivity
5857 * table is negative by construction and the unsigned test would print an exact
5858 * zero for every one of those columns. Only the MAGNITUDE decides negligibility
5859 * here. The NC and the layered tables share it, which is why it sits above both.
5860 */
5861double sens_sanitize_signed(double x) {
5862 if (std::fabs(x) <= line::lang::GlobalConstants::FineTol) return 0.0;
5863 return x;
5864}
5865
5866/**
5867 * `-s nc -a sens`: `@@NetworkSolver/getSensitivityTable.m` under SolverNC.
5868 *
5869 * ONE ROW PER (station, class) carrying dTput/dRate, dRespT/dRate, dQLen/dRate
5870 * and dUtil/dRate, i.e. the derivative of that row's means with respect to that
5871 * row's service RATE. Two branches produce them and the banner names the one
5872 * that ran: `exact` differentiates the product-form recursion analytically
5873 * (`pfqn_sens` at chain level for a closed model, the closed-form BCMP
5874 * derivatives for an open one), `fd` re-solves rate-perturbed copies of the
5875 * model with THIS solver and forms the quotient.
5876 *
5877 * NC IS ONE OF THE TWO ENGINES THAT CAN TAKE THE EXACT BRANCH, which is what
5878 * `@@SolverNC/supportsExactSensitivity.m` returns true for, so `auto` resolves
5879 * to `exact` whenever the model is in its scope (single-server queues plus
5880 * delays, not mixed) and only falls back to differences outside it. Asking for
5881 * `--sens-method exact` outside that scope is refused by name rather than
5882 * silently downgraded: the two branches answer to different precision.
5883 *
5884 * The struct is COPIED rather than referenced because the fd branch writes a
5885 * scaled service process into it between solves; `net.get_struct()` hands out a
5886 * const reference to the model's own, which must not move under the caller.
5887 */
5888template <class T>
5889int solve_model_nc_sens(const std::string& file, const Knobs& k) {
5890 line::qn::Network<T> net = read_model<T>(file);
5892 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5893 if (k.tol >= 0.0) opt.tol = k.tol;
5894 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5895 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5897
5899 if (!k.sens_method.empty()) so.method = k.sens_method;
5900 if (!k.sens_scheme.empty()) so.scheme = k.sens_scheme;
5901 if (k.sens_step > 0.0) so.step = k.sens_step;
5902 so.simulation = false; // the normalizing-constant path is deterministic
5903
5904 // `getAvg`, not the raw analyzer: the reference's difference quotient is
5905 // taken on the metrics the solver reports, which are the filtered ones.
5907 sn, so, /*exact_available=*/true, [&sn, &opt]() {
5910 s.Q = a.QN;
5911 s.U = a.UN;
5912 s.R = a.RN;
5913 s.Tp = a.TN;
5914 s.C = a.CN;
5915 s.X = a.XN;
5916 s.method = a.actualmethod;
5917 s.iter = a.iter;
5918 return s;
5919 });
5920
5921 if (g_json_output) {
5922 line::reg::Json p = line::reg::Json::object();
5923 p["type"] = "SensitivityTable";
5924 p["indexBase"] = 0;
5925 p["branch"] = tbl.method;
5926 line::reg::Json rows = line::reg::Json::array();
5927 for (const line::sens::SensRow<T>& r : tbl.rows) {
5928 line::reg::Json o = line::reg::Json::object();
5929 o["Station"] = r.station;
5930 o["JobClass"] = r.jobclass;
5931 o["dTput_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dTput));
5932 o["dRespT_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dRespT));
5933 o["dQLen_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dQLen));
5934 o["dUtil_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dUtil));
5935 rows.push_back(o);
5936 }
5937 p["rows"] = rows;
5938 // The branch rides in "branch" because it is not a normalizing-constant
5939 // method name. The method slot is EMPTY on the exact branch, which
5940 // differentiates the recursion in closed form and never runs a solve:
5941 // naming the NC method there would attribute the numbers to an
5942 // algorithm that did not produce them.
5943 emit_analysis<T>("sens", p, tbl.method == "fd" ? opt.method : std::string());
5944 return 0;
5945 }
5946
5947 std::printf("SolverNC arith=%s branch=%s method=%s rows=%zu\n", line::num_traits<T>::name(),
5948 tbl.method.c_str(), tbl.method == "fd" ? opt.method.c_str() : "-",
5949 tbl.rows.size());
5950 std::printf("%-16s %-14s %14s %14s %14s %14s\n", "Station", "JobClass", "dTput_dRate",
5951 "dRespT_dRate", "dQLen_dRate", "dUtil_dRate");
5952 for (const line::sens::SensRow<T>& r : tbl.rows)
5953 std::printf("%-16s %-14s %14.6g %14.6g %14.6g %14.6g\n", r.station.c_str(),
5954 r.jobclass.c_str(),
5955 sens_sanitize_signed(line::num_traits<T>::to_double(r.dTput)),
5956 sens_sanitize_signed(line::num_traits<T>::to_double(r.dRespT)),
5957 sens_sanitize_signed(line::num_traits<T>::to_double(r.dQLen)),
5958 sens_sanitize_signed(line::num_traits<T>::to_double(r.dUtil)));
5959 return 0;
5960}
5961
5962/** The @@Solver method each `-a` stands for, which is what the chooser keys on. */
5963// ===================== SolverLDES, the simulator ==========================
5964
5965/**
5966 * The knobs one `-s ldes` invocation resolves to.
5967 *
5968 * `--samples` is the SERVICE-COMPLETION budget the engine stops at, `--seed` the
5969 * stream, and both are part of the answer rather than of the invocation, which is
5970 * why the banner carries them. Everything else is `--ldes-*`.
5971 *
5972 * A `--ldes-initsol` placement is NOT accompanied by a forced `fixed` warmup
5973 * filter here, deliberately: `initFromSolver` sets `tranfilter='fixed'` with
5974 * `warmupfrac=0` because the placement it computes IS a steady state, while a
5975 * placement handed in on the command line may equally be the start of a
5976 * transient. Pass `--ldes-tranfilter fixed --ldes-warmupfrac 0` alongside it to
5977 * reproduce `initFromSolver` exactly.
5978 */
5979inline line::ldes::LdesOptions ldes_options(const Knobs& k) {
5981 if (k.samples) o.samples = k.samples;
5982 if (k.seed) o.seed = static_cast<long>(k.seed);
5983 if (!k.method.empty()) o.method = k.method;
5984 if (!k.ldes_tranfilter.empty()) o.tranfilter = k.ldes_tranfilter;
5985 if (k.ldes_warmupfrac >= 0.0) o.warmupfrac = k.ldes_warmupfrac;
5986 if (!k.ldes_cimethod.empty()) o.cimethod = k.ldes_cimethod;
5987 if (k.ldes_cnvgon) o.cnvgon = true;
5988 if (k.ldes_cnvgtol > 0.0) o.cnvgtol = k.ldes_cnvgtol;
5989 if (k.ldes_slotted) o.slotted = true;
5990 if (k.ldes_slotlength > 0.0) {
5991 o.slotted = true;
5992 o.slot_length = k.ldes_slotlength;
5993 }
5994 if (k.ldes_replications > 0) o.replications = k.ldes_replications;
5995 if (k.ldes_numthreads > 0) o.numthreads = k.ldes_numthreads;
5996 if (k.ldes_maxtime > 0.0) o.timeout = k.ldes_maxtime;
5997 if (!k.ldes_initsol.empty()) o.init_sol = k.ldes_initsol;
5998 if (!k.ldes_rest_url.empty()) o.rest_url = k.ldes_rest_url;
5999 o.verbose = k.verbose;
6000 return o;
6001}
6002
6003/**
6004 * The model.json text the engine is handed.
6005 *
6006 * FORWARDED BYTE FOR BYTE, and never through `read_model`: the reader is scoped
6007 * to the subset the analytical solvers need, and a round trip through it would
6008 * degrade exactly the models LDES exists for. `-a reward` is the one arm that
6009 * also parses the document, because a reward DECLARATION is what it needs.
6010 */
6011inline std::string ldes_document(const std::string& file) {
6012 return file.empty() ? stdin_model_text() : line::ldes::detail::read_file(file);
6013}
6014
6015/** A reported entry, or 0 where the engine reported no such row. */
6016inline double ldes_at(const line::Matrix<double>& M, std::size_t i, std::size_t j) {
6017 return i < M.rows() && j < M.cols() ? M(i, j) : 0.0;
6018}
6019
6020/**
6021 * One LDES run.
6022 *
6023 * A HARD TIMEOUT IS A REFUSAL HERE, not an empty result. The two other clients
6024 * return an empty result flagged `timedOut` and warn, which suits a caller that
6025 * can inspect the flag; a CLI's caller reads a table, and a table of zeros that
6026 * means "the run was killed" is the silent-wrong-number outcome this CLI refuses
6027 * everywhere else.
6028 */
6029inline line::ldes::LdesResult ldes_run(const std::string& file,
6030 const line::ldes::LdesOptions& o,
6031 const std::vector<std::string>& extra) {
6032 const line::ldes::LdesResult r = line::ldes::solver_ldes_text(ldes_document(file), o, extra);
6033 if (r.timed_out)
6034 throw line::NumericError(
6035 "SolverLDES exceeded its wall-clock budget (--ldes-maxtime) and was terminated before "
6036 "it wrote a result; raise the budget or lower --samples");
6037 if (r.station_names.empty())
6038 throw line::NumericError(
6039 "SolverLDES: the engine reported no station names, so its metrics cannot be labelled; "
6040 "the run produced no result document");
6041 return r;
6042}
6043
6044/**
6045 * The provenance line every LDES arm prints first.
6046 *
6047 * `engine=` is not decoration: the AOT native image and the jar are two builds of
6048 * one engine and the first can lag the sources, so a number quoted from an LDES
6049 * run has to say which produced it. `stopping=` is the reason the run ended --
6050 * a `max_events` stop at a low `--samples` is a wide confidence interval and a
6051 * `max_time` one is a truncated run, and neither is visible in the means.
6052 */
6053inline void ldes_banner(const line::ldes::LdesResult& r, const line::ldes::LdesOptions& o) {
6054 std::printf("SolverLDES arith=double method=%s type=%s engine=%s samples=%zu seed=%ld "
6055 "time=%.6g events=%lld stopping=%s\n",
6056 r.method.c_str(), line::util::method_type("LDES", r.method).c_str(),
6057 r.engine.c_str(), o.events ? o.events : o.samples, o.seed, r.runtime,
6059}
6060
6061/** The envelope keys that qualify an LDES solve as a whole. */
6062inline line::reg::Json ldes_envelope(const line::ldes::LdesResult& r,
6063 const line::ldes::LdesOptions& o) {
6064 line::reg::Json e = line::reg::Json::object();
6065 e["engine"] = r.engine;
6066 e["samples"] = o.events ? o.events : o.samples;
6067 e["seed"] = o.seed;
6068 e["converged"] = r.converged;
6069 e["stoppingReason"] = r.stopping_reason;
6070 e["totalSimulatedEvents"] = r.total_simulated_events;
6071 e["runtime"] = r.runtime;
6072 return e;
6073}
6074
6075/**
6076 * `-s ldes -a avg`: the steady-state table, the engine's `getAvg`.
6077 *
6078 * THE FINITE-CAPACITY-REGION ROWS DO NOT JOIN THE STATION TABLE, unlike MATLAB's
6079 * `getAvgTable`, which appends them after the stations. A region is not a station
6080 * and the "Station" column of this CLI's table is read by a parity harness that
6081 * pairs rows with another codebase's stations; a region row there would pair with
6082 * nothing. They are printed as their own table and carried under `avg.fcr`, which
6083 * is the same information without the collision.
6084 */
6085int solve_model_ldes_avg(const std::string& file, const Knobs& k) {
6086 const line::ldes::LdesOptions o = ldes_options(k);
6087 const line::ldes::LdesResult r = ldes_run(file, o, std::vector<std::string>());
6088 if (!g_json_output) ldes_banner(r, o);
6089
6090 line::reg::Json extra = line::reg::Json::object();
6091 // The confidence intervals are the half-widths the engine reports, one per
6092 // metric; a simulation that quoted a mean without them would be quoting a
6093 // point estimate as if it were exact.
6094 line::reg::Json ci = line::reg::Json::object();
6095 if (!r.QNCI.empty()) ci["QNCI"] = matrix_json<double>(r.QNCI);
6096 if (!r.UNCI.empty()) ci["UNCI"] = matrix_json<double>(r.UNCI);
6097 if (!r.RNCI.empty()) ci["RNCI"] = matrix_json<double>(r.RNCI);
6098 if (!r.TNCI.empty()) ci["TNCI"] = matrix_json<double>(r.TNCI);
6099 if (!r.ANCI.empty()) ci["ANCI"] = matrix_json<double>(r.ANCI);
6100 if (!r.WNCI.empty()) ci["WNCI"] = matrix_json<double>(r.WNCI);
6101 if (!ci.empty()) extra["CI"] = ci;
6102 if (!r.QNfcr.empty()) {
6103 line::reg::Json f = line::reg::Json::object();
6104 f["nregions"] = r.nregions;
6105 f["QNfcr"] = matrix_json<double>(r.QNfcr);
6106 f["RNfcr"] = matrix_json<double>(r.RNfcr);
6107 f["TNfcr"] = matrix_json<double>(r.TNfcr);
6108 f["WNfcr"] = matrix_json<double>(r.WNfcr);
6109 if (!r.WeightNfcr.empty()) f["WeightNfcr"] = matrix_json<double>(r.WeightNfcr);
6110 if (!r.MemOccNfcr.empty()) f["MemOccNfcr"] = matrix_json<double>(r.MemOccNfcr);
6111 if (!r.DropRateNfcr.empty()) f["DropRateNfcr"] = matrix_json<double>(r.DropRateNfcr);
6112 extra["fcr"] = f;
6113 }
6114 if (!r.DropRateJoin.empty()) extra["DropRateJoin"] = matrix_json<double>(r.DropRateJoin);
6115 if (!r.cache_metrics.empty()) {
6116 line::reg::Json cm = line::reg::Json::object();
6117 for (std::map<std::string, line::ldes::LdesCacheMetrics>::const_iterator it =
6118 r.cache_metrics.begin();
6119 it != r.cache_metrics.end(); ++it) {
6120 line::reg::Json c = line::reg::Json::object();
6121 if (!it->second.hit.empty()) c["hit"] = matrix_json<double>(it->second.hit);
6122 if (!it->second.delayed.empty()) c["delayed"] = matrix_json<double>(it->second.delayed);
6123 if (!it->second.miss.empty()) c["miss"] = matrix_json<double>(it->second.miss);
6124 if (!it->second.latency.empty()) c["latency"] = matrix_json<double>(it->second.latency);
6125 if (!it->second.hitList.empty()) c["hitList"] = matrix_json<double>(it->second.hitList);
6126 if (!it->second.itemProb.empty())
6127 c["itemProb"] = matrix_json<double>(it->second.itemProb);
6128 if (!it->second.listCost.empty())
6129 c["listCost"] = matrix_json<double>(it->second.listCost);
6130 cm[it->first] = c;
6131 }
6132 extra["cacheMetrics"] = cm;
6133 }
6134
6135 // THE RESIDENCE TIME IS DERIVED HERE, not taken from the engine. The engine
6136 // reports WN = RN because it counts one visit per station, which is only
6137 // true when every visit ratio is 1; `getAvg.m:204` therefore discards the
6138 // WN a solver returned and recomputes `sn_get_residt_from_respt(sn, RN)`,
6139 // and every other C++ solver already routes through the same helper. On
6140 // cqn_repairmen, whose Queue1 is visited 0.3 times per cycle, the engine's
6141 // WN came out 11.8136 against the reference's 3.5205 -- the response time
6142 // reported as if the station were visited once.
6143 //
6144 // MATCHED BY NAME. The engine's station order is its own; a Cache or a
6145 // Source can sit at a different index in the struct, and pairing the two
6146 // off positionally would scale one station's time by another's visits.
6147 line::Matrix<double> WNd(r.station_names.size(), r.class_names.size(), 0.0);
6148 {
6149 line::qn::Network<double> net = read_model<double>(file);
6151 std::vector<std::size_t> st_of(r.station_names.size(), 0); // 1-based, 0 = unmatched
6152 for (std::size_t i = 0; i < r.station_names.size(); ++i)
6153 for (std::size_t j = 0; j < sn.nstations; ++j)
6154 if (sn.stations[j].name == r.station_names[i]) { st_of[i] = j + 1; break; }
6155 std::vector<std::size_t> cl_of(r.class_names.size(), 0);
6156 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6157 for (std::size_t k = 0; k < sn.nclasses; ++k)
6158 if (sn.classes[k].name == r.class_names[c]) { cl_of[c] = k + 1; break; }
6159 line::Matrix<double> RNs(sn.nstations, sn.nclasses, 0.0);
6160 for (std::size_t i = 0; i < r.station_names.size(); ++i)
6161 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6162 if (st_of[i] && cl_of[c]) RNs(st_of[i] - 1, cl_of[c] - 1) = ldes_at(r.RN, i, c);
6164 for (std::size_t i = 0; i < r.station_names.size(); ++i)
6165 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6166 WNd(i, c) = (st_of[i] && cl_of[c]) ? WNs(st_of[i] - 1, cl_of[c] - 1)
6167 : ldes_at(r.WN, i, c);
6168 }
6169
6170 emit_avg_table_named(r.station_names, r.class_names, "double", r.method,
6171 [&](std::size_t i, std::size_t c) {
6172 AvgRow v;
6173 v.q = ldes_at(r.QN, i, c);
6174 v.u = ldes_at(r.UN, i, c);
6175 v.r = ldes_at(r.RN, i, c);
6176 v.w = WNd(i, c);
6177 v.a = ldes_at(r.AN, i, c);
6178 v.t = ldes_at(r.TN, i, c);
6179 return v;
6180 },
6181 extra, ldes_envelope(r, o));
6182
6183 if (!g_json_output && !r.QNfcr.empty()) {
6184 std::printf("%-16s %-14s %12s %12s %12s %12s %12s\n", "Region", "JobClass", "QLen", "RespT",
6185 "Tput", "Weight", "MemOcc");
6186 for (std::size_t i = 0; i < r.QNfcr.rows(); ++i)
6187 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6188 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g\n",
6189 ("Region" + std::to_string(i + 1)).c_str(), r.class_names[c].c_str(),
6190 ldes_at(r.QNfcr, i, c), ldes_at(r.RNfcr, i, c),
6191 ldes_at(r.TNfcr, i, c), ldes_at(r.WeightNfcr, i, c),
6192 ldes_at(r.MemOccNfcr, i, c));
6193 }
6194 if (!g_json_output && !r.cache_metrics.empty()) {
6195 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Cache", "JobClass", "Hit", "Delayed",
6196 "Miss", "Latency");
6197 for (std::map<std::string, line::ldes::LdesCacheMetrics>::const_iterator it =
6198 r.cache_metrics.begin();
6199 it != r.cache_metrics.end(); ++it)
6200 for (std::size_t c = 0; c < r.class_names.size(); ++c)
6201 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", it->first.c_str(),
6202 r.class_names[c].c_str(), ldes_at(it->second.hit, 0, c),
6203 ldes_at(it->second.delayed, 0, c), ldes_at(it->second.miss, 0, c),
6204 ldes_at(it->second.latency, 0, c));
6205 }
6206 return 0;
6207}
6208
6209/**
6210 * `-s ldes -a tran`: `getTranAvg`, the per-bucket QNt / UNt / TNt series over
6211 * `--tspan`.
6212 *
6213 * THE HORIZON IS REQUIRED. `options.timespan` is what turns the engine's run into
6214 * a transient one, and there is no default: a trajectory over an unstated horizon
6215 * is not a quantity. A SINGLE PATH IS NOT E[N](t) either -- there is no time
6216 * ergodicity at fixed t -- so `--ldes-replications` is how an ensemble mean is
6217 * asked for, exactly as `runAnalyzer.m` passes `--replications` for the same
6218 * reason.
6219 *
6220 * The series are indexed by STATION, as `LDESResultIO` writes them
6221 * (`result.QNt = new Matrix[numStations][numClasses]`).
6222 */
6223int solve_model_ldes_tran(const std::string& file, const Knobs& k) {
6224 line::ldes::LdesOptions o = ldes_options(k);
6225 o.has_timespan = true;
6226 o.t0 = k.t0;
6227 o.t1 = k.t1;
6228 std::vector<std::string> extra;
6229 extra.push_back("--trajectory");
6230 const line::ldes::LdesResult r = ldes_run(file, o, extra);
6231 if (r.t.empty() || r.QNt.empty())
6232 throw line::NumericError(
6233 "SolverLDES -a tran produced no trajectory: the engine ran but recorded no bucket over "
6234 "[" + line::ldes::detail::shortest(k.t0) + "," +
6235 line::ldes::detail::shortest(k.t1) + "]");
6236
6237 if (g_json_output) {
6238 line::reg::Json p = line::reg::Json::object();
6239 p["type"] = "TranAvgTable";
6240 p["indexBase"] = 0;
6241 p["t0"] = k.t0;
6242 p["t1"] = k.t1;
6243 line::reg::Json curves = line::reg::Json::array();
6244 for (std::size_t i = 0; i < r.QNt.size(); ++i)
6245 for (std::size_t c = 0; c < r.QNt[i].size(); ++c) {
6246 // An empty series is an ABSENT one -- the class does not visit
6247 // the station -- and is omitted rather than sent as a flat zero.
6248 if (r.QNt[i][c].empty()) continue;
6249 line::reg::Json e = line::reg::Json::object();
6250 e["Station"] = i < r.station_names.size() ? r.station_names[i]
6251 : "Station" + std::to_string(i);
6252 e["JobClass"] =
6253 c < r.class_names.size() ? r.class_names[c] : "Class" + std::to_string(c);
6254 e["station"] = i;
6255 e["jobclass"] = c;
6256 line::reg::Json tt = line::reg::Json::array(), q = line::reg::Json::array(),
6257 u = line::reg::Json::array(), x = line::reg::Json::array();
6258 for (std::size_t j = 0; j < r.QNt[i][c].rows(); ++j) {
6259 tt.push_back(r.QNt[i][c](j, 1));
6260 q.push_back(r.QNt[i][c](j, 0));
6261 }
6262 if (i < r.UNt.size() && c < r.UNt[i].size())
6263 for (std::size_t j = 0; j < r.UNt[i][c].rows(); ++j)
6264 u.push_back(r.UNt[i][c](j, 0));
6265 if (i < r.TNt.size() && c < r.TNt[i].size())
6266 for (std::size_t j = 0; j < r.TNt[i][c].rows(); ++j)
6267 x.push_back(r.TNt[i][c](j, 0));
6268 e["t"] = tt;
6269 e["QLen"] = q;
6270 e["Util"] = u;
6271 e["Tput"] = x;
6272 curves.push_back(e);
6273 }
6274 p["curves"] = curves;
6275 p["tset"] = vector_json(r.t);
6276 // The envelope is built ONCE into a local: `begin()` and `end()` taken
6277 // from two different temporaries are iterators into two different
6278 // objects, which is undefined behaviour and not a style point.
6279 const line::reg::Json env = ldes_envelope(r, o);
6280 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
6281 p[it.key()] = it.value();
6282 emit_analysis<double>("tran", p, r.method);
6283 return 0;
6284 }
6285 ldes_banner(r, o);
6286 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
6287 "Tput");
6288 for (std::size_t i = 0; i < r.QNt.size(); ++i)
6289 for (std::size_t c = 0; c < r.QNt[i].size(); ++c) {
6290 if (r.QNt[i][c].empty()) continue;
6291 for (std::size_t j = 0; j < r.QNt[i][c].rows(); ++j) {
6292 const bool hu = i < r.UNt.size() && c < r.UNt[i].size() &&
6293 j < r.UNt[i][c].rows();
6294 const bool hx = i < r.TNt.size() && c < r.TNt[i].size() &&
6295 j < r.TNt[i][c].rows();
6296 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
6297 (i < r.station_names.size() ? r.station_names[i].c_str() : "?"),
6298 (c < r.class_names.size() ? r.class_names[c].c_str() : "?"),
6299 r.QNt[i][c](j, 1), r.QNt[i][c](j, 0),
6300 hu ? r.UNt[i][c](j, 0) : 0.0, hx ? r.TNt[i][c](j, 0) : 0.0);
6301 }
6302 }
6303 return 0;
6304}
6305
6306/**
6307 * `-s ldes -a cdf`: `getCdfRespT`, the EMPIRICAL response-time law.
6308 *
6309 * A SIMULATOR MUST REPORT WHAT IT OBSERVED. The base solver's fallback fabricates
6310 * an exponential law with the right mean, which says nothing about the tail; the
6311 * engine records every per-job response time under `--respt-samples`, and the
6312 * curve here is the ecdf of those samples with repeated observations collapsed to
6313 * their largest F, exactly as `@@SolverLDES/getCdfRespT.m` builds it.
6314 *
6315 * `--respt-samples` POSTDATES the prebuilt AOT image, which is why the runner
6316 * order flips for it (see `ldes_runners`): on that image the flag is accepted and
6317 * ignored, and the arm would refuse for want of samples that were never asked for.
6318 */
6319int solve_model_ldes_cdf(const std::string& file, const Knobs& k, const char* key,
6320 const char* type) {
6321 const line::ldes::LdesOptions o = ldes_options(k);
6322 std::vector<std::string> extra;
6323 extra.push_back("--respt-samples");
6324 const line::ldes::LdesResult r = ldes_run(file, o, extra);
6325 if (r.respTimeSamples.empty())
6326 throw line::NumericError(
6327 "SolverLDES -a cdf needs the per-job response times the engine records under "
6328 "--respt-samples and the run returned none; raise --samples so completions are "
6329 "observed at all");
6330
6331 // The ecdf of each (station, class): sorted observations, F = i/n, and one
6332 // pair per DISTINCT value carrying the largest F at it.
6333 std::vector<std::vector<std::vector<double>>> tt(r.respTimeSamples.size()), ff(
6334 r.respTimeSamples.size());
6335 for (std::size_t i = 0; i < r.respTimeSamples.size(); ++i) {
6336 tt[i].resize(r.respTimeSamples[i].size());
6337 ff[i].resize(r.respTimeSamples[i].size());
6338 for (std::size_t c = 0; c < r.respTimeSamples[i].size(); ++c) {
6339 std::vector<double> x = r.respTimeSamples[i][c];
6340 if (x.empty()) continue;
6341 std::sort(x.begin(), x.end());
6342 const double n = static_cast<double>(x.size());
6343 for (std::size_t j = 0; j < x.size(); ++j) {
6344 if (j + 1 < x.size() && x[j + 1] == x[j]) continue;
6345 tt[i][c].push_back(x[j]);
6346 ff[i][c].push_back(static_cast<double>(j + 1) / n);
6347 }
6348 }
6349 }
6350
6351 if (g_json_output) {
6352 line::reg::Json p = line::reg::Json::object();
6353 p["type"] = type;
6354 p["indexBase"] = 0;
6355 p["algorithm"] = "empirical";
6356 line::reg::Json rd = line::reg::Json::array();
6357 for (std::size_t i = 0; i < tt.size(); ++i)
6358 for (std::size_t c = 0; c < tt[i].size(); ++c) {
6359 if (tt[i][c].empty()) continue;
6360 line::reg::Json e = line::reg::Json::object();
6361 e["Station"] = i < r.station_names.size() ? r.station_names[i]
6362 : "Station" + std::to_string(i);
6363 e["JobClass"] =
6364 c < r.class_names.size() ? r.class_names[c] : "Class" + std::to_string(c);
6365 e["station"] = i;
6366 e["jobclass"] = c;
6367 e["t"] = vector_json(tt[i][c]);
6368 e["F"] = vector_json(ff[i][c]);
6369 e["samples"] = r.respTimeSamples[i][c].size();
6370 rd.push_back(e);
6371 }
6372 p["respt"] = rd;
6373 emit_analysis<double>(key, p, std::string());
6374 return 0;
6375 }
6376 ldes_banner(r, o);
6377 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
6378 for (std::size_t i = 0; i < tt.size(); ++i)
6379 for (std::size_t c = 0; c < tt[i].size(); ++c)
6380 for (std::size_t j = 0; j < tt[i][c].size(); ++j)
6381 std::printf("%-16s %-14s %14.8g %14.10g\n",
6382 (i < r.station_names.size() ? r.station_names[i].c_str() : "?"),
6383 (c < r.class_names.size() ? r.class_names[c].c_str() : "?"),
6384 tt[i][c][j], ff[i][c][j]);
6385 return 0;
6386}
6387
6388/**
6389 * `-s ldes -a sample`: `sampleSys` / `sampleSysAggr`, one simulated trajectory.
6390 *
6391 * The horizon is `[0, --samples]`, which is `runTransientJson`'s: the event budget
6392 * doubles as the transient horizon there because the engine ignores the budget in
6393 * transient mode, so the number the caller gave has to name the horizon or name
6394 * nothing. `--tspan` names a horizon in its own right and belongs to `-a tran`.
6395 *
6396 * The state is the per-class queue length at each station, which is why there is
6397 * no separate `sampleSysAggr` column: an LDES trajectory is ALREADY per class, so
6398 * the aggregate view is the row sum and the reference's two getters return the
6399 * same data with one flag flipped.
6400 */
6401int solve_model_ldes_sample(const std::string& file, const Knobs& k) {
6402 line::ldes::LdesOptions o = ldes_options(k);
6403 o.has_timespan = true;
6404 o.t0 = 0.0;
6405 o.t1 = static_cast<double>(o.events ? o.events : o.samples);
6406 std::vector<std::string> extra;
6407 extra.push_back("--trajectory");
6408 const line::ldes::LdesResult r = ldes_run(file, o, extra);
6409 if (r.t.empty() || r.QNt.empty())
6410 throw line::NumericError(
6411 "SolverLDES -a sample produced no trajectory over [0," +
6412 line::ldes::detail::shortest(o.t1) + "]");
6413
6414 const std::size_t M = r.QNt.size(), K = r.class_names.size(), n = r.t.size();
6415 if (g_json_output) {
6416 line::reg::Json p = line::reg::Json::object();
6417 p["type"] = "SamplePath";
6418 p["indexBase"] = 0;
6419 p["scope"] = "(system)";
6420 p["drawn"] = n;
6421 p["t"] = vector_json(r.t);
6422 line::reg::Json st = line::reg::Json::array();
6423 for (std::size_t j = 0; j < n; ++j) {
6424 line::reg::Json row = line::reg::Json::array();
6425 for (std::size_t i = 0; i < M; ++i)
6426 for (std::size_t c = 0; c < K; ++c)
6427 row.push_back(c < r.QNt[i].size() && j < r.QNt[i][c].rows()
6428 ? r.QNt[i][c](j, 0)
6429 : 0.0);
6430 st.push_back(row);
6431 }
6432 p["state"] = st;
6433 line::reg::Json cols = line::reg::Json::array();
6434 for (std::size_t i = 0; i < M; ++i)
6435 for (std::size_t c = 0; c < K; ++c)
6436 cols.push_back((i < r.station_names.size() ? r.station_names[i] : "?") + "," +
6437 (c < K ? r.class_names[c] : "?"));
6438 p["columns"] = cols;
6439 // The envelope is built ONCE into a local: `begin()` and `end()` taken
6440 // from two different temporaries are iterators into two different
6441 // objects, which is undefined behaviour and not a style point.
6442 const line::reg::Json env = ldes_envelope(r, o);
6443 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
6444 p[it.key()] = it.value();
6445 emit_analysis<double>("sample", p, r.method);
6446 return 0;
6447 }
6448 ldes_banner(r, o);
6449 std::printf("%14s %s\n", "Time", "SysState (station-major, per class)");
6450 for (std::size_t j = 0; j < n; ++j) {
6451 std::printf("%14.8g ", r.t[j]);
6452 for (std::size_t i = 0; i < M; ++i)
6453 for (std::size_t c = 0; c < K; ++c)
6454 std::printf(" %g", c < r.QNt[i].size() && j < r.QNt[i][c].rows()
6455 ? r.QNt[i][c](j, 0)
6456 : 0.0);
6457 std::printf("\n");
6458 }
6459 return 0;
6460}
6461
6462/**
6463 * `-s ldes -a reward`: `getAvgReward`, E[r] on the EXACT joint-state histogram.
6464 *
6465 * The engine exports, under `--export-histogram`, the residence time of every
6466 * joint state it visited, in the aggregate layout `ctmc_state_space_aggr` builds.
6467 * The rewards are then evaluated here, state by state, so E[r] = sum_s (t_s /
6468 * sum t) r(state_s) is correct for a NONLINEAR reward too -- which is the whole
6469 * point of the histogram over the means: E[n^2] cannot be recovered from E[n].
6470 *
6471 * THIS IS THE ONE ARM THAT ALSO PARSES THE DOCUMENT, because a reward is a
6472 * DECLARATION and the declarations live in the model, not in the result. A model
6473 * outside this port's reader therefore reaches every other LDES arm and not this
6474 * one, and says so by the reader's own refusal.
6475 */
6476int solve_model_ldes_reward(const std::string& file, const Knobs& k) {
6477 line::qn::Network<double> net = read_model<double>(file);
6479 if (sn.reward.empty())
6480 throw line::InputError(
6481 "-s ldes -a reward needs a reward declared on the model (set_reward(name, fn), the "
6482 "`rewards` block of model.json); there is nothing to average");
6483
6484 const line::ldes::LdesOptions o = ldes_options(k);
6485 std::vector<std::string> extra;
6486 extra.push_back("--export-histogram");
6487 const line::ldes::LdesResult r = ldes_run(file, o, extra);
6489 throw line::NumericError(
6490 "SolverLDES -a reward needs the joint-state residence-time histogram the engine "
6491 "exports under --export-histogram and the run returned none");
6492
6493 double total = 0.0;
6494 for (std::size_t s = 0; s < r.histogram_time.rows(); ++s)
6495 for (std::size_t c = 0; c < r.histogram_time.cols(); ++c) total += r.histogram_time(s, c);
6496 if (!(total > 0.0))
6497 throw line::NumericError(
6498 "SolverLDES -a reward: the state histogram carries no residence time, so no state "
6499 "distribution can be formed from it");
6500
6501 const std::size_t ns = r.histogram_space.rows(), w = r.histogram_space.cols();
6502 std::vector<double> E(sn.reward.size(), 0.0);
6503 std::vector<std::string> names(sn.reward.size());
6504 for (std::size_t l = 0; l < sn.reward.size(); ++l) {
6505 names[l] = sn.reward[l].name;
6506 for (std::size_t s = 0; s < ns; ++s) {
6507 std::vector<double> row(w);
6508 for (std::size_t c = 0; c < w; ++c) row[c] = r.histogram_space(s, c);
6509 const double t = s < r.histogram_time.rows() && r.histogram_time.cols() > 0
6510 ? r.histogram_time(s, 0)
6511 : 0.0;
6512 E[l] += (t / total) * sn.reward[l].fn(row);
6513 }
6514 }
6515
6516 if (g_json_output) {
6517 line::reg::Json p = line::reg::Json::object();
6518 p["type"] = "AvgReward";
6519 line::reg::Json nm = line::reg::Json::array();
6520 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
6521 p["Reward"] = nm;
6522 p["E"] = vector_json(E);
6523 p["states"] = ns;
6524 // The envelope is built ONCE into a local: `begin()` and `end()` taken
6525 // from two different temporaries are iterators into two different
6526 // objects, which is undefined behaviour and not a style point.
6527 const line::reg::Json env = ldes_envelope(r, o);
6528 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
6529 p[it.key()] = it.value();
6530 // NO "method": these are expectations over an empirical distribution and
6531 // no chain was solved, so there is no resolved method to report.
6532 emit_analysis<double>("reward", p, std::string());
6533 return 0;
6534 }
6535 ldes_banner(r, o);
6536 std::printf("%-28s %16s\n", "Reward", "E[r]");
6537 for (std::size_t l = 0; l < E.size(); ++l)
6538 std::printf("%-28s %16.10g\n", names[l].c_str(), E[l]);
6539 return 0;
6540}
6541
6542/**
6543 * `-s ldes -a prob`: `getProb`, `getProbAggr`, `getProbSys` and `getProbSysAggr`,
6544 * all four read off the joint-state histogram of ONE run, as
6545 * `@@SolverLDES/getProb*.m` does through `ldesHistProb`.
6546 *
6547 * THE STATE IS THE MODEL'S DECLARED ONE (`api::sn_declared_marginal`, the same
6548 * per-class counts `-s jmt -a prob` weighs against), and `--node` with `--state`
6549 * overrides one station's counts, which is `getProbAggr(node, state)`'s second
6550 * argument. THE DETAILED AND THE AGGREGATE QUERIES COINCIDE HERE, and that is the
6551 * reference's definition rather than a shortcut: the histogram records per-class
6552 * counts per station and nothing about buffer order or phase, so MATLAB's
6553 * `getProb` marginalizes its state with `State.toMarginal` before matching, and
6554 * `getProbSysAggr` is literally `getProbSys`. Each probability is the residence
6555 * time of the matching histogram rows over the total, and `seen` says whether any
6556 * row matched at all, since on a simulation a zero is usually the run length.
6557 *
6558 * A Source holds no jobs, so it takes no part in the system match (its histogram
6559 * columns are zero, as the reference's own match would find them).
6560 */
6561int solve_model_ldes_prob(const std::string& file, const Knobs& k) {
6562 line::qn::Network<double> net = read_model<double>(file);
6564 const std::size_t M = sn.nstations, R = sn.nclasses;
6565
6567 if (!k.state.empty()) {
6568 std::size_t ist = 0;
6569 if (k.node && k.node <= sn.nodes.size()) ist = sn.nodes[k.node - 1].station;
6570 if (ist == 0)
6571 throw line::InputError("--node " + std::to_string(k.node) +
6572 " is not a station; the LDES state histogram records station "
6573 "queue lengths only");
6574 if (k.state.size() != R)
6575 throw line::InputError("-s ldes -a prob takes --state as one job count per class (" +
6576 std::to_string(R) + "), got " + std::to_string(k.state.size()));
6577 for (std::size_t r = 0; r < R; ++r) nir(ist - 1, r) = static_cast<double>(k.state[r]);
6578 }
6579
6580 const line::ldes::LdesOptions o = ldes_options(k);
6581 std::vector<std::string> extra;
6582 extra.push_back("--export-histogram");
6583 const line::ldes::LdesResult res = ldes_run(file, o, extra);
6584 const line::Matrix<double>& space = res.histogram_space;
6585 const line::Matrix<double>& tm = res.histogram_time;
6586 if (space.empty() || tm.empty())
6587 throw line::NumericError(
6588 "SolverLDES -a prob needs the joint-state residence-time histogram the engine exports "
6589 "under --export-histogram and the run returned none");
6590 if (space.cols() < M * R)
6591 throw line::NumericError("SolverLDES: the state histogram holds " +
6592 std::to_string(space.cols()) + " columns, fewer than the " +
6593 std::to_string(M * R) + " of " + std::to_string(M) +
6594 " stations by " + std::to_string(R) + " classes");
6595
6596 std::vector<double> dwell(space.rows(), 0.0);
6597 double total = 0.0;
6598 for (std::size_t s = 0; s < space.rows() && s < tm.rows(); ++s)
6599 for (std::size_t c = 0; c < tm.cols(); ++c) dwell[s] += tm(s, c);
6600 for (double d : dwell) total += d;
6601 if (!(total > 0.0))
6602 throw line::NumericError(
6603 "SolverLDES -a prob: the state histogram carries no residence time, so no state "
6604 "probability can be formed from it");
6605
6606 // ldesHistProb's match: |space - target| < 1e-9 on the station's R columns.
6607 auto matches = [&](std::size_t s, std::size_t i) {
6608 for (std::size_t r = 0; r < R; ++r)
6609 if (!(std::abs(space(s, i * R + r) - nir(i, r)) < 1e-9)) return false;
6610 return true;
6611 };
6612 std::vector<double> prob(M, 0.0);
6613 std::vector<bool> seen(M, false);
6614 double psys = 0.0;
6615 bool sys_seen = false;
6616 for (std::size_t s = 0; s < space.rows(); ++s) {
6617 bool all = true;
6618 for (std::size_t i = 0; i < M; ++i) {
6619 const bool src = sn.nodes[sn.station_to_node[i] - 1].nodetype == line::lang::NodeType::Source;
6620 if (matches(s, i)) {
6621 prob[i] += dwell[s];
6622 seen[i] = true;
6623 } else if (!src) {
6624 all = false;
6625 }
6626 }
6627 if (all) {
6628 psys += dwell[s];
6629 sys_seen = true;
6630 }
6631 }
6632 for (double& p : prob) p /= total;
6633 psys /= total;
6634
6635 if (g_json_output) {
6636 line::reg::Json p = line::reg::Json::object();
6637 p["type"] = "ProbAggr";
6638 p["indexBase"] = 0;
6639 p["ProbSys"] = psys;
6640 p["ProbSysAggr"] = psys;
6641 p["SysStateSeen"] = sys_seen;
6642 p["states"] = space.rows();
6643 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
6644 sv = line::reg::Json::array();
6645 for (std::size_t i = 0; i < M; ++i) {
6646 st.push_back(sn.stations[i].name);
6647 pa.push_back(prob[i]);
6648 sv.push_back(static_cast<bool>(seen[i]));
6649 }
6650 p["Station"] = st;
6651 p["Prob"] = pa;
6652 p["ProbAggr"] = pa;
6653 p["StateSeen"] = sv;
6654 emit_analysis<double>("prob", p, res.method);
6655 return 0;
6656 }
6657 ldes_banner(res, o);
6658 std::printf("ProbSys = ProbSysAggr %.10g%s\n", psys, sys_seen ? "" : " (state never observed)");
6659 std::printf("%-16s %14s\n", "Station", "Prob=ProbAggr");
6660 for (std::size_t i = 0; i < M; ++i)
6661 std::printf("%-16s %14.10g%s\n", sn.stations[i].name.c_str(), prob[i],
6662 seen[i] ? "" : " (state never observed)");
6663 return 0;
6664}
6665
6666/**
6667 * The `-s ldes` entry: one analysis per getter of the reference's surface.
6668 *
6669 * `-a prob` is `solve_model_ldes_prob`: the four getProb queries read off the
6670 * engine's joint-state histogram at the model's DECLARED state.
6671 */
6672int solve_model_ldes(const std::string& file, const Knobs& k, const std::string& analysis) {
6673 if (analysis == "avg") return solve_model_ldes_avg(file, k);
6674 if (analysis == "tran") return solve_model_ldes_tran(file, k);
6675 // THE SAME ECDF UNDER FOUR NAMES, which is the reference's own structure and
6676 // not a shortcut here: `@@SolverLDES/getCdfRespT`, `getTranCdfRespT` and
6677 // `getTranCdfPassT` all read `respTimeSamples`, and the JAR's
6678 // getTranCdfPassT is literally `return getTranCdfRespT(R)`. A simulator
6679 // observes one per-job passage and there is no second measurement to make;
6680 // emitting the curve under the key the caller asked for is what tells them
6681 // apart, and the payload's `type` says which question it answers.
6682 if (analysis == "cdf") return solve_model_ldes_cdf(file, k, "cdf", "CdfRespT");
6683 if (analysis == "cdfpasst") return solve_model_ldes_cdf(file, k, "cdfpasst", "CdfPassT");
6684 if (analysis == "trancdf") return solve_model_ldes_cdf(file, k, "trancdf", "TranCdfRespT");
6685 if (analysis == "trancdfpasst")
6686 return solve_model_ldes_cdf(file, k, "trancdfpasst", "TranCdfPassT");
6687 if (analysis == "sample") return solve_model_ldes_sample(file, k);
6688 if (analysis == "reward") return solve_model_ldes_reward(file, k);
6689 if (analysis == "prob") return solve_model_ldes_prob(file, k);
6691 "SolverLDES ports -a avg (getAvg) and its four views -a node, -a sys, -a chain and "
6692 "-a nodechain, -a tran (getTranAvg), -a cdf / cdf-passt / "
6693 "tran-cdf-respt / tran-cdf-passt (the empirical passage law, one measurement under the "
6694 "four names the reference gives it), -a sample (sampleSys), -a reward "
6695 "(getAvgReward) and -a prob (getProb, getProbAggr, getProbSys, getProbSysAggr); got '" +
6696 analysis + "'");
6697}
6698
6699std::string auto_getter_of_analysis(const std::string& analysis) {
6700 if (analysis == "prob") return "getProbSysAggr";
6701 if (analysis == "marg") return "getProbMarg";
6702 if (analysis == "sysmarg") return "getProbSysMarg";
6703 if (analysis == "normconst") return "getProbNormConstAggr";
6704 if (analysis == "tranprob") return "getTranProbSysAggr";
6705 if (analysis == "sample") return "sampleSys";
6706 if (analysis == "cdf") return "getCdfRespT";
6707 if (analysis == "gen") return "getInfGen";
6708 if (analysis == "states") return "getStateSpace";
6709 if (analysis == "reward") return "getAvgReward";
6710 if (analysis == "sens") return "getSensitivityTable";
6711 if (analysis == "tran") return "getTranAvg";
6712 if (analysis == "internals") return "getMAMResult";
6713 // `-a bounds` and `-a tranreward` deliberately keep the AvgTable getter:
6714 // `getBoundsTable` and `getTranReward` are absent from chooseSolverHeur's
6715 // own method lists, so naming them here would ask the chooser about a getter
6716 // it does not rank. Each is served by exactly one engine anyway (BA and
6717 // CTMC), which refuses by name when `-s auto` sends the run elsewhere.
6718 if (analysis == "node") return "getAvgNodeTable";
6719 return "getAvgTable";
6720}
6721
6722/**
6723 * What `-s auto` resolved to: the engines to try, in order, and the method the
6724 * first of them runs.
6725 */
6726struct AutoPlan {
6727 std::vector<std::string> order; ///< CLI solver method names, chosen first
6728 std::string method; ///< the method the chosen engine runs, "" for its default
6729 std::string note; ///< what the ranking preferred and this port cannot build
6730};
6731
6732/** The CLI method name of a method family, or a refusal naming what the family needs. */
6733std::string auto_cli_token_of_family(const std::string& fam) {
6734 if (fam == "mva" || fam == "nc" || fam == "ctmc" || fam == "mam" || fam == "ag" ||
6735 fam == "ssa" || fam == "ba" || fam == "uq" || fam == "env")
6736 return fam;
6737 if (fam == "fld") return "fluid";
6738 if (fam == "ldes") {
6739 // The engine is not built here, it is RUN here, so the family resolves
6740 // whenever the machine has one and refuses -- naming what is missing --
6741 // when it does not, rather than diverting to a different simulator.
6744 "--method ldes names the discrete-event engine, and no engine was found beside "
6745 "this binary (common/ldes or common/ldes.jar, or $LINE_LDES_DIR); -s ssa is the "
6746 "simulator this port builds in process");
6747 return "ldes";
6748 }
6749 if (fam == "jmt") {
6750 // IT IS WRAPPED NOW. This refused the token outright until the JMT
6751 // client landed, and stayed behind: `-s jmt` drives jsim and jmva
6752 // through `solver_jmt_run_analyzer`, so refusing `--method jmt` denied
6753 // under `-s auto` what the very same binary answers under `-s jmt`.
6754 // Availability is left to the wrapper, which names what is missing (a
6755 // JVM, common/JMT.jar or a REST endpoint) rather than guessing here.
6756 return "jmt";
6757 }
6758 // On a flat Network the lqns family's methods are the qns ones, served by
6759 // `qnsolver` through `solve_model_lqns_network`.
6760 if (fam == "lqns") return "lqns";
6761 if (fam == "ln")
6763 "--method ln names the layered solver, which takes a LayeredNetwork: pass the model "
6764 "as -i lqnx -s ln rather than as a Network");
6765 throw line::InputError("SolverAUTO: no engine stands behind method family '" + fam + "'");
6766}
6767
6768/**
6769 * Is this model.json an Environment envelope rather than a Network?
6770 *
6771 * `-s auto` has to know before it parses: the two readers take different
6772 * documents, and the reference's chooser has an Environment arm that is
6773 * unreachable if every auto run is assumed to hold a Network. A malformed
6774 * document answers false, so the real reader reports the parse error.
6775 */
6776bool model_is_environment(const std::string& file) {
6777 try {
6778 line::io::detail::json root;
6779 if (file.empty()) {
6780 std::istringstream in(stdin_model_text());
6781 in >> root;
6782 } else {
6783 std::ifstream in(file.c_str());
6784 if (!in) return false;
6785 in >> root;
6786 }
6787 // The envelope may wrap the model, exactly as build_environment_from_json
6788 // unwraps it; `type` is what that reader keys on, so this reads the same
6789 // field rather than a second convention of its own.
6790 if (!root.is_object()) return false;
6791 const line::io::detail::json& model = root.contains("model") ? root.at("model") : root;
6792 return model.is_object() && model.value("type", std::string()) == "Environment";
6793 } catch (...) {
6794 return false;
6795 }
6796}
6797
6798/**
6799 * `-s auto`: which engine answers, by `chooseSolver.m` through solver_auto.h.
6800 *
6801 * THREE THINGS DECIDE, in the reference's own order. `--method` is resolved
6802 * first, because a method FAMILY ('nc', 'nc.comom') names the engine outright
6803 * and bypasses every ranking, while a selection INTENT ('exact', 'sim', 'fast',
6804 * 'accurate') picks which ranking runs. Then the model class: an Environment
6805 * envelope takes the Environment arm, a Network the Network one. Then the
6806 * GETTER the caller asked for, since the reference keys the ranking on the
6807 * metric family and not on the model alone.
6808 *
6809 * THE ORDER IS A RETRY LIST, not a single name. `delegate.m` tries the chosen
6810 * solver and then every feasible candidate, so a refusal moves to the next
6811 * engine instead of ending the run; the caller sees which one answered.
6812 *
6813 * The choice reads structure only -- traits, feature sets, product form,
6814 * populations -- so it is made in the arithmetic the run will use and costs one
6815 * extra parse of the model, nothing more.
6816 */
6817template <class T>
6818AutoPlan choose_auto_plan(const std::string& file, const std::string& analysis,
6819 const std::string& method_token) {
6821 AutoPlan plan;
6822 if (!tok.is_intent) {
6823 plan.order.push_back(auto_cli_token_of_family(tok.family));
6824 if (tok.submethod != "default") plan.method = tok.submethod;
6825 return plan;
6826 }
6827
6828 const std::string getter = auto_getter_of_analysis(analysis);
6829 if (model_is_environment(file)) {
6832 plan.order.push_back("env");
6833 for (std::size_t i = 0; i < ec.skipped.size(); ++i)
6834 plan.note += std::string(i ? ", " : " (the ranking preferred ") +
6836 if (!ec.skipped.empty()) plan.note += ", which this port does not build)";
6837 return plan;
6838 }
6839
6840 line::qn::Network<T> net = read_model<T>(file);
6843 plan.method = c.method;
6844 const std::vector<line::autosolver::AutoSolver> proposed =
6846 for (std::size_t i = 0; i < proposed.size(); ++i)
6847 plan.order.push_back(line::autosolver::auto_solver_name(proposed[i]));
6848 for (std::size_t i = 0; i < c.skipped.size(); ++i)
6849 plan.note += std::string(i ? ", " : " (the ranking preferred ") +
6851 if (!c.skipped.empty()) plan.note += ", which this port does not build)";
6852 return plan;
6853}
6854
6855AutoPlan choose_auto_plan_dispatch(const std::string& arith, const std::string& file,
6856 const std::string& analysis, const std::string& method_token) {
6857 if (arith == "exact") return choose_auto_plan<line::Rational>(file, analysis, method_token);
6858 if (arith == "real:16") return choose_auto_plan<line::Real<16> >(file, analysis, method_token);
6859 if (arith == "real" || arith == "real:32")
6860 return choose_auto_plan<line::Real<32> >(file, analysis, method_token);
6861 if (arith == "real:64") return choose_auto_plan<line::Real<64> >(file, analysis, method_token);
6862 if (arith == "real:128")
6863 return choose_auto_plan<line::Real<128> >(file, analysis, method_token);
6864 if (arith == "real:256")
6865 return choose_auto_plan<line::Real<256> >(file, analysis, method_token);
6866 return choose_auto_plan<double>(file, analysis, method_token);
6867}
6868
6869/**
6870 * The numeric clean-up SolverLN.getAvgTable applies before printing.
6871 *
6872 * It is reproduced here because the reference's reported table IS the
6873 * sanitized one, so a row-by-row comparison against it has to compare like
6874 * with like. Two rules: snap a value to one decimal place when it is already
6875 * within CoarseTol of it relatively, and snap anything at or below FineTol to
6876 * zero. The second rule is what turns the residual queue length of a chain of
6877 * Immediate classes -- of order 1e-8 by construction, since Immediate has rate
6878 * 1e8 -- into the exact zero the reference prints.
6879 */
6880double ln_sanitize(double x) {
6881 const double r = std::round(x * 10.0);
6882 if (std::fabs(x * 10.0 - r) < line::lang::GlobalConstants::CoarseTol * x * 10.0) x = r / 10.0;
6883 if (x <= line::lang::GlobalConstants::FineTol) x = 0.0;
6884 return x;
6885}
6886
6887const char* ln_element_kind(const line::lqn::LqnStruct<double>& l, std::size_t i) {
6888 switch (l.type[i]) {
6889 case line::lang::LqnElement::HOST: return "Processor";
6890 case line::lang::LqnElement::TASK: return l.isref[i] ? "RefTask" : "Task";
6891 case line::lang::LqnElement::ENTRY: return "Entry";
6892 default: return "Activity";
6893 }
6894}
6895
6896/**
6897 * Print every layer's stations, classes and routing, in a form a MATLAB dump of
6898 * `solver.ensemble{k}` can be diffed against line by line.
6899 *
6900 * A layered result that is close but not equal across codebases is almost never
6901 * a difference in the MVA call; it is a layer that was BUILT differently -- a
6902 * class that is present in one and not the other, a population, a routing
6903 * probability. Comparing the final AvgTable cannot tell those apart, so the
6904 * structure has to be observable directly.
6905 */
6906template <class T>
6907void ln_dump_layers(const line::ln::SolverLN<T>& solver) {
6908 using namespace line;
6909 const std::vector<qn::Layer<T> >& ens = solver.layers();
6910 for (std::size_t k = 0; k < ens.size(); ++k) {
6911 const qn::Layer<T>& L = ens[k];
6912 std::printf("LAYER %zu %s nstations=%zu nclasses=%zu nchains=%zu\n", k + 1, L.name.c_str(),
6913 L.stations.size(), L.classes.size(), L.nchains);
6914 for (std::size_t i = 0; i < L.stations.size(); ++i)
6915 std::printf(" STATION %zu %s sched=%s nservers=%g\n", i + 1, L.stations[i].name.c_str(),
6916 lang::sched_to_text(L.stations[i].sched), L.stations[i].nservers);
6917 for (std::size_t r = 0; r < L.classes.size(); ++r)
6918 std::printf(" CLASS %zu %s pop=%.17g refstat=%zu completes=%d\n", r + 1,
6919 L.classes[r].name.c_str(), L.classes[r].population, L.classes[r].refstat,
6920 int(L.classes[r].completes));
6921 for (std::size_t i = 0; i < L.stations.size(); ++i)
6922 for (std::size_t r = 0; r < L.classes.size(); ++r) {
6923 if (L.disabled.empty() || L.disabled[i][r]) continue;
6924 std::printf(" RATE %s %s %.17g scv=%.17g\n", L.stations[i].name.c_str(),
6925 L.classes[r].name.c_str(), num_traits<T>::to_double(L.rates(i, r)),
6926 num_traits<T>::to_double(L.scv(i, r)));
6927 }
6928 for (const auto& kv : L.P) {
6929 const Matrix<T>& B = kv.second;
6930 for (std::size_t i = 0; i < B.rows(); ++i)
6931 for (std::size_t j = 0; j < B.cols(); ++j) {
6932 const double p = num_traits<T>::to_double(B(i, j));
6933 if (p == 0.0) continue;
6934 std::printf(" ROUTE %s->%s %s->%s %.17g\n",
6935 L.classes[kv.first.first - 1].name.c_str(),
6936 L.classes[kv.first.second - 1].name.c_str(),
6937 L.nodes[i].name.c_str(), L.nodes[j].name.c_str(), p);
6938 }
6939 }
6940 }
6941}
6942
6943/**
6944 * Solve a .lqnx layered queueing network with SolverLN and print its AvgTable.
6945 *
6946 * The output is one row per LQN element, with its queue length, utilization,
6947 * response time, residence time and throughput, in the same element order as
6948 * MATLAB's getAvgTable, so a row-by-row numeric comparison against the
6949 * reference is a plain diff.
6950 *
6951 * `--repeat` re-runs the whole solve K times and reports the best wall-clock
6952 * time, which is what the arithmetic-backend benchmark reads.
6953 */
6954/** The LnOptions a set of CLI knobs describes; arithmetic-independent. */
6955inline line::ln::LnOptions ln_options_from(const Knobs& k) {
6956 using namespace line;
6957 // The reference's own LnOptions defaults stand unless the caller overrode
6958 // them; the CLI does not restate them, so an untouched knob keeps whatever
6959 // SolverLN itself considers default.
6961 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
6962 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
6963 if (k.no_interlocking) opt.interlocking = false;
6964 if (!k.interlock_method.empty()) opt.interlock_method = k.interlock_method;
6965 if (k.interlock_maxpaths >= 0.0) opt.interlock_maxpaths = k.interlock_maxpaths;
6966 if (!k.interlock_refpath_scope.empty()) opt.interlock_refpath_scope = k.interlock_refpath_scope;
6967 if (!k.layer_solver.empty()) opt.layer_solver = k.layer_solver;
6968 if (!k.method.empty()) opt.method = k.method;
6969 if (k.samples) opt.layer_ssa.samples = k.samples;
6970 if (k.seed) opt.layer_ssa.seed = k.seed;
6971 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
6972 if (k.tran_points) opt.tran_points = k.tran_points;
6973 if (!k.ln_transient.empty()) opt.ln_transient = k.ln_transient;
6974 if (!k.ln_transient_channels.empty()) opt.ln_transient_channels = k.ln_transient_channels;
6975 return opt;
6976}
6977
6978/** The engine name the banner reports, which is never a hardcoded MVA. */
6979inline const char* ln_layer_engine_name(const std::string& layer_solver) {
6980 if (layer_solver == "fluid") return "Fluid";
6981 if (layer_solver == "nc") return "NC";
6982 if (layer_solver == "ssa") return "SSA";
6983 return "MVA";
6984}
6985
6986/**
6987 * `-a tran`: the layered transient, one block of time series per layer.
6988 *
6989 * The blocks are printed rather than the block-diagonal cell array the
6990 * reference assembles, because the off-diagonal blocks of that array are empty
6991 * by construction and each layer keeps its own grid.
6992 */
6993template <class T>
6994int run_ln_tran(const std::string& file, const std::string& output, const Knobs& k) {
6995 using namespace line;
6996 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
6997 ln::LnOptions opt = ln_options_from(k);
6998 ln::SolverLN<T> solver(model, opt);
6999 const ln::LnTranSolution tr = solver.get_tran_avg();
7000 const std::vector<qn::Layer<T>>& layers = solver.layers();
7001
7002 if (output == "json") {
7003 reg::Json j = reg::Json::object();
7004 j["model"] = file;
7005 j["arith"] = num_traits<T>::name();
7006 j["mode"] = tr.mode;
7007 j["iterations"] = tr.iterations;
7008 j["gap"] = ln_sanitize(tr.gap);
7009 reg::Json ls = reg::Json::array();
7010 for (std::size_t e = 0; e < tr.layers.size(); ++e) {
7011 reg::Json le = reg::Json::object();
7012 le["layer"] = layers[e].name;
7013 reg::Json tt = reg::Json::array();
7014 for (double t : tr.layers[e].t) tt.push_back(ln_sanitize(t));
7015 le["t"] = tt;
7016 reg::Json series = reg::Json::array();
7017 for (std::size_t i = 0; i < tr.layers[e].QN.size(); ++i)
7018 for (std::size_t r = 0; r < tr.layers[e].QN[i].size(); ++r) {
7019 reg::Json s = reg::Json::object();
7020 s["station"] = layers[e].stations[i].name;
7021 s["class"] = layers[e].classes[r].name;
7022 auto arr = [&](const std::vector<double>& v) {
7023 reg::Json a = reg::Json::array();
7024 for (double x : v) a.push_back(ln_sanitize(x));
7025 return a;
7026 };
7027 s["QLen"] = arr(tr.layers[e].QN[i][r]);
7028 s["Util"] = arr(tr.layers[e].UN[i][r]);
7029 s["Tput"] = arr(tr.layers[e].TN[i][r]);
7030 series.push_back(s);
7031 }
7032 le["series"] = series;
7033 ls.push_back(le);
7034 }
7035 j["layers"] = ls;
7036 emit_document(dump_document(j, 1));
7037 return 0;
7038 }
7039
7040 std::printf("SolverLN(Solver%s) getTranAvg arith=%s mode=%s layers=%zu iterations=%ld gap=%.3e\n",
7041 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), tr.mode.c_str(),
7042 tr.layers.size(), tr.iterations, tr.gap);
7043 for (std::size_t e = 0; e < tr.layers.size(); ++e) {
7044 const std::vector<double>& t = tr.layers[e].t;
7045 if (t.empty()) continue;
7046 std::printf("\nLayer %s (%zu points on [%.6g, %.6g])\n", layers[e].name.c_str(), t.size(),
7047 t.front(), t.back());
7048 std::printf("%-30s %-24s %12s %12s %12s %12s\n", "Station", "JobClass", "QLen(0)",
7049 "QLen(end)", "Util(end)", "Tput(end)");
7050 for (std::size_t i = 0; i < tr.layers[e].QN.size(); ++i)
7051 for (std::size_t r = 0; r < tr.layers[e].QN[i].size(); ++r) {
7052 const std::vector<double>& q = tr.layers[e].QN[i][r];
7053 if (q.empty()) continue;
7054 std::printf("%-30s %-24s %12.6g %12.6g %12.6g %12.6g\n",
7055 layers[e].stations[i].name.c_str(), layers[e].classes[r].name.c_str(),
7056 ln_sanitize(q.front()), ln_sanitize(q.back()),
7057 ln_sanitize(tr.layers[e].UN[i][r].back()),
7058 ln_sanitize(tr.layers[e].TN[i][r].back()));
7059 }
7060 }
7061 return 0;
7062}
7063
7064/** `-a sens`: the layer sensitivity tables under a leading Layer column. */
7065template <class T>
7066int run_ln_sens(const std::string& file, const std::string& output, const Knobs& k) {
7067 using namespace line;
7068 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
7069 ln::LnOptions opt = ln_options_from(k);
7070 ln::SolverLN<T> solver(model, opt);
7072 if (!k.sens_method.empty()) so.method = k.sens_method;
7073 if (!k.sens_scheme.empty()) so.scheme = k.sens_scheme;
7074 if (k.sens_step > 0.0) so.step = k.sens_step;
7075 const ln::LnSensTable<T> tbl = solver.get_sensitivity_table(so);
7076
7077 if (output == "json") {
7078 reg::Json j = reg::Json::object();
7079 j["model"] = file;
7080 j["arith"] = num_traits<T>::name();
7081 j["method"] = tbl.method;
7082 reg::Json rows = reg::Json::array();
7083 for (const auto& r : tbl.rows) {
7084 reg::Json o = reg::Json::object();
7085 o["Layer"] = r.layer;
7086 o["Station"] = r.station;
7087 o["JobClass"] = r.jobclass;
7088 o["dTput_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dTput));
7089 o["dRespT_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dRespT));
7090 o["dQLen_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dQLen));
7091 o["dUtil_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dUtil));
7092 rows.push_back(o);
7093 }
7094 j["rows"] = rows;
7095 emit_document(dump_document(j, 1));
7096 return 0;
7097 }
7098
7099 std::printf("SolverLN(Solver%s) getSensitivityTable arith=%s method=%s rows=%zu\n",
7100 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), tbl.method.c_str(),
7101 tbl.rows.size());
7102 std::printf("%-28s %-28s %-20s %14s %14s %14s %14s\n", "Layer", "Station", "JobClass",
7103 "dTput_dRate", "dRespT_dRate", "dQLen_dRate", "dUtil_dRate");
7104 for (const auto& r : tbl.rows)
7105 std::printf("%-28s %-28s %-20s %14.6g %14.6g %14.6g %14.6g\n", r.layer.c_str(),
7106 r.station.c_str(), r.jobclass.c_str(),
7107 sens_sanitize_signed(num_traits<T>::to_double(r.dTput)),
7108 sens_sanitize_signed(num_traits<T>::to_double(r.dRespT)),
7109 sens_sanitize_signed(num_traits<T>::to_double(r.dQLen)),
7110 sens_sanitize_signed(num_traits<T>::to_double(r.dUtil)));
7111 return 0;
7112}
7113
7114/** `-a cdf`: the per-entry response-time distribution of the moment3 method. */
7115template <class T>
7116int run_ln_cdf(const std::string& file, const std::string& output, const Knobs& k) {
7117 using namespace line;
7118 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
7119 ln::LnOptions opt = ln_options_from(k);
7120 // getCdfRespT.m runs the ensemble under moment3 whatever the caller asked
7121 // for, and restores the method afterwards: the mean-based update forms no
7122 // distribution at all, so there is nothing else to report.
7123 opt.method = "moment3";
7124 ln::SolverLN<T> solver(model, opt);
7125 const std::vector<ln::LnCdf> cdf = solver.get_cdf_respt();
7127
7128 if (output == "json") {
7129 reg::Json j = reg::Json::object();
7130 j["model"] = file;
7131 j["arith"] = num_traits<T>::name();
7132 reg::Json rows = reg::Json::array();
7133 for (std::size_t e = 1; e <= model.nentries && e < cdf.size(); ++e) {
7134 reg::Json o = reg::Json::object();
7135 o["entry"] = names.names[model.eshift + e];
7136 reg::Json tt = reg::Json::array(), ff = reg::Json::array();
7137 for (std::size_t p = 0; p < cdf[e].t.size(); ++p) {
7138 tt.push_back(ln_sanitize(cdf[e].t[p]));
7139 ff.push_back(ln_sanitize(cdf[e].cdf[p]));
7140 }
7141 o["t"] = tt;
7142 o["F"] = ff;
7143 rows.push_back(o);
7144 }
7145 j["entries"] = rows;
7146 emit_document(dump_document(j, 1));
7147 return 0;
7148 }
7149
7150 std::printf("SolverLN(Solver%s) getCdfRespT arith=%s method=moment3 entries=%zu\n",
7151 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), model.nentries);
7152 for (std::size_t e = 1; e <= model.nentries && e < cdf.size(); ++e) {
7153 if (cdf[e].t.empty()) {
7154 std::printf("%-40s (no distribution: the entry has no fitted term)\n",
7155 names.names[model.eshift + e].c_str());
7156 continue;
7157 }
7158 // Quartiles, which is what a CDF is read for; the full grid goes to JSON.
7159 auto quantile = [&](double p) {
7160 for (std::size_t i = 0; i < cdf[e].cdf.size(); ++i)
7161 if (cdf[e].cdf[i] >= p) return cdf[e].t[i];
7162 return cdf[e].t.back();
7163 };
7164 std::printf("%-40s p25=%12.6g p50=%12.6g p75=%12.6g p95=%12.6g points=%zu\n",
7165 names.names[model.eshift + e].c_str(), ln_sanitize(quantile(0.25)),
7166 ln_sanitize(quantile(0.50)), ln_sanitize(quantile(0.75)),
7167 ln_sanitize(quantile(0.95)), cdf[e].t.size());
7168 }
7169 return 0;
7170}
7171
7172template <class T>
7173int run_ln(const std::string& file, const std::string& output, const Knobs& k) {
7174 using namespace line;
7175 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
7176
7177 ln::LnOptions opt = ln_options_from(k);
7178 const int repeat = k.repeat > 0 ? k.repeat : 1;
7179
7180 double best = 1e300;
7182 std::size_t nlayers = 0;
7183 for (int rep = 0; rep < repeat; ++rep) {
7184 const auto t0 = std::chrono::steady_clock::now();
7185 ln::SolverLN<T> solver(model, opt);
7186 sol = solver.get_ensemble_avg();
7187 nlayers = solver.nlayers();
7188 if (output == "layers") {
7189 ln_dump_layers(solver);
7190 return 0;
7191 }
7192 const auto t1 = std::chrono::steady_clock::now();
7193 best = std::min(best, std::chrono::duration<double>(t1 - t0).count());
7194 }
7195
7196 // element names and kinds are arithmetic-independent, so read them once
7198
7199 if (output == "json") {
7200 reg::Json j = reg::Json::object();
7201 j["model"] = file;
7202 j["arith"] = num_traits<T>::name();
7203 j["layers"] = nlayers;
7204 j["iterations"] = sol.iterations;
7205 j["converged"] = sol.converged;
7206 j["seconds"] = best;
7207 reg::Json rows = reg::Json::array();
7208 for (std::size_t i = 1; i <= model.nidx; ++i) {
7209 reg::Json r = reg::Json::object();
7210 r["node"] = names.names[i];
7211 r["type"] = ln_element_kind(names, i);
7212 auto put = [&](const char* key, const std::vector<T>& v, const std::vector<bool>& d) {
7213 if (d[i]) r[key] = ln_sanitize(num_traits<T>::to_double(v[i]));
7214 else if (sol.is_bound) r[key] = 0.0; // see the note in the table below
7215 else r[key] = nullptr;
7216 };
7217 put("QLen", sol.QN, sol.defined_Q);
7218 put("Util", sol.UN, sol.defined_U);
7219 put("RespT", sol.RN, sol.defined_R);
7220 put("ResidT", sol.WN, sol.defined_W);
7221 put("Tput", sol.TN, sol.defined_T);
7222 rows.push_back(r);
7223 }
7224 j["rows"] = rows;
7225 emit_document(dump_document(j, 1));
7226 return 0;
7227 }
7228
7229 // The banner names the LAYER solver actually used, not a hardcoded MVA: the
7230 // two converge to different fixed points, so a reader (or a parity row) that
7231 // cannot tell them apart is reading numbers it cannot attribute.
7232 std::printf(
7233 "SolverLN(Solver%s) arith=%s type=%s layers=%zu iterations=%d converged=%d time=%.4fs\n",
7234 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(),
7235 line::util::method_type("LN", opt.method).c_str(), nlayers, sol.iterations,
7236 int(sol.converged), best);
7237 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
7238 "RespT", "ResidT", "Tput");
7239 for (std::size_t i = 1; i <= model.nidx; ++i) {
7240 // NaN IS THE UNDEFINED MARKER EVERYWHERE EXCEPT UNDER A BOUND. MATLAB's
7241 // getAvgTable prints NaN for a measure the element does not have (a
7242 // processor has no queue length), and this reproduces that. A BOUND is
7243 // the one case where the reference prints 0 instead: `mw.*` defines
7244 // throughput and processor utilization only, and both MATLAB and the JAR
7245 // report the rest as zero rather than as absent (the JAR maps the NaN
7246 // explicitly, SolverLN.java:3294-3299). The `defined_*` flags still say
7247 // undefined to any caller of the API; only the printed table follows the
7248 // reference, so that a numeric parity row compares like with like.
7249 //
7250 // This table is for a human; a cross-codebase comparison must read the
7251 // `-o json` above, which carries the raw double, and quantize it itself.
7252 auto fmt = [&](const std::vector<T>& v, const std::vector<bool>& d, char* buf) {
7253 if (!d[i] && sol.is_bound) std::snprintf(buf, 24, "%12.6g", 0.0);
7254 else if (!d[i]) std::snprintf(buf, 24, "%12s", "NaN");
7255 else std::snprintf(buf, 24, "%12.6g", ln_sanitize(num_traits<T>::to_double(v[i])));
7256 };
7257 char q[24], u[24], rr[24], w[24], t[24];
7258 fmt(sol.QN, sol.defined_Q, q);
7259 fmt(sol.UN, sol.defined_U, u);
7260 fmt(sol.RN, sol.defined_R, rr);
7261 fmt(sol.WN, sol.defined_W, w);
7262 fmt(sol.TN, sol.defined_T, t);
7263 std::printf("%-62s %-10s %s %s %s %s %s\n", names.names[i].c_str(),
7264 ln_element_kind(names, i), q, u, rr, w, t);
7265 }
7266 return 0;
7267}
7268
7269/**
7270 * `-s ldes`: the layered model simulated directly, by the IN-PROCESS engine.
7271 *
7272 * IT IS NOT THE SUBPROCESS `-s ldes` OF THE FLAT PATH. That arm hands a
7273 * `model.json` to `common/ldes`, and the engine behind that wire refuses a
7274 * layered document outright ("LDES currently supports Network models only"), so
7275 * there is nothing to forward. This arm calls `ldes_ln_engine_solve` in process
7276 * -- the C++ twin of `Solver_ssj_ln.java` -- which simulates entries,
7277 * activities, task threads and synchronous calls directly instead of
7278 * decomposing the model into layers. So it is not a noisier route to `-s ln`:
7279 * SolverLN's decomposition is an APPROXIMATION and this is a sample path of the
7280 * model itself, which is what makes it the reference the layered solvers are
7281 * checked against.
7282 *
7283 * THE NaN MASK OF THE LAYERED TABLE IS PART OF THE ANSWER, and it belongs to
7284 * the table rather than to whichever solver filled it: a processor has no queue
7285 * length, a task no response time, an entry no residence, and `-s ln` and
7286 * `-s lqns` both print NaN there. This engine MEASURES more than that -- a
7287 * processor's completion rate is sitting in `LnResult::TLN` -- and printing it
7288 * under a column the other two arms leave empty would make one column mean
7289 * different things depending on who filled it. That is the divergence the JAR
7290 * removed from `getLNAvgTable` on 2026-08-21, and masking here rather than in
7291 * the engine keeps it removed on this side too. Nothing is discarded: the
7292 * unmasked measurements stay on `LnResult` for a programmatic caller, and only
7293 * the shared table is masked, so that it can be diffed row by row.
7294 *
7295 * THE NUMBERS ARE NOT `ln_sanitize`d, for the reason `run_lqns` states below:
7296 * that helper snaps to a tenth and floors at FineTol, which is right for a
7297 * fixed point this port computed and wrong for a measurement it made. A
7298 * simulated utilization of 1e-9 is a rare event that was observed, not a
7299 * negligible residue of an iteration, and the JAR's own layered LDES table
7300 * reports the raw estimate too -- so snapping here would put the two out of
7301 * step on exactly the models a parity row is read on.
7302 */
7303/**
7304 * `-i lqnx -s ldes -a cdf`: the SIMULATED response time distribution per entry.
7305 *
7306 * The measured counterpart of `run_ln_cdf`, which fits an APH to three moments of
7307 * a fluid passage time and convolves. Here the engine timed every invocation from
7308 * the instant the request reached the entry to its reply -- the interval `RLN`
7309 * averages -- so this law's mean reproduces that row and its tail is observed
7310 * rather than extrapolated.
7311 */
7312int run_ln_ldes_cdf(const std::string& file, const std::string& output, const Knobs& k) {
7313 using namespace line;
7316 if (k.samples) o.samples = k.samples;
7317 if (k.seed) o.seed = static_cast<long>(k.seed);
7318
7320
7321 // The per-entry ecdf, through the same API function the other codebases
7322 // call getCdfRespTLN.
7323 const std::vector<ldes::engine::LnEntryCdf> cdf =
7325 std::vector<std::vector<double> > tt(model.nentries), ff(model.nentries);
7326 for (std::size_t e = 0; e < model.nentries; ++e) {
7327 tt[e] = cdf[e].t;
7328 ff[e] = cdf[e].F;
7329 }
7330
7331 if (output == "json") {
7332 reg::Json j = reg::Json::object();
7333 j["model"] = file;
7334 j["engine"] = "native-ln";
7335 reg::Json rows = reg::Json::array();
7336 for (std::size_t e = 0; e < model.nentries; ++e) {
7337 reg::Json ob = reg::Json::object();
7338 ob["entry"] = model.names[model.eshift + e + 1];
7339 reg::Json at = reg::Json::array(), af = reg::Json::array();
7340 for (std::size_t i = 0; i < tt[e].size(); ++i) {
7341 at.push_back(tt[e][i]);
7342 af.push_back(ff[e][i]);
7343 }
7344 ob["t"] = at;
7345 ob["F"] = af;
7346 ob["observations"] = static_cast<double>(
7347 e < r.entry_resp_samples.size() ? r.entry_resp_samples[e].size() : 0);
7348 rows.push_back(ob);
7349 }
7350 j["entries"] = rows;
7351 emit_document(dump_document(j, 1));
7352 return 0;
7353 }
7354
7355 std::printf("SolverLDES(native LN engine) getCdfRespT entries=%zu\n", model.nentries);
7356 for (std::size_t e = 0; e < model.nentries; ++e) {
7357 if (tt[e].empty()) {
7358 std::printf("%-40s (no observation)\n", model.names[model.eshift + e + 1].c_str());
7359 continue;
7360 }
7361 // Quartiles plus p95, which is what a measured law is read for.
7362 struct Q {
7363 const std::vector<double>& t;
7364 const std::vector<double>& f;
7365 double operator()(double p) const {
7366 for (std::size_t i = 0; i < f.size(); ++i)
7367 if (f[i] >= p) return t[i];
7368 return t.back();
7369 }
7370 } q = {tt[e], ff[e]};
7371 std::printf("%-40s p25=%12.6g p50=%12.6g p75=%12.6g p95=%12.6g n=%zu\n",
7372 model.names[model.eshift + e + 1].c_str(), q(0.25), q(0.50), q(0.75),
7373 q(0.95), r.entry_resp_samples[e].size());
7374 }
7375 return 0;
7376}
7377
7378int run_ln_ldes(const std::string& file, const std::string& output, const Knobs& k) {
7379 using namespace line;
7381
7382 // The engine reads THREE settings and no more (`o.samples`, `o.events`,
7383 // `o.seed`); the dispatcher refuses the rest of the --ldes-* family rather
7384 // than letting this function drop them silently.
7386 if (k.samples) o.samples = k.samples;
7387 if (k.seed) o.seed = static_cast<long>(k.seed);
7388
7389 // --repeat is a TIMING loop and stays honest here only because the stream is
7390 // seeded: every replication of one command walks the same sample path, so
7391 // the table is that run's table and `time=` is the only thing that varies.
7392 // This is why -s lqns refuses the flag and this arm does not -- lqsim seeds
7393 // itself, so re-running it would report different numbers under one banner.
7394 const int repeat = k.repeat > 0 ? k.repeat : 1;
7395 double best = 1e300;
7397 for (int rep = 0; rep < repeat; ++rep) {
7398 const auto t0 = std::chrono::steady_clock::now();
7399 r = ldes::ldes_ln_engine_solve(model, o);
7400 const auto t1 = std::chrono::steady_clock::now();
7401 best = std::min(best, std::chrono::duration<double>(t1 - t0).count());
7402 }
7403
7404 // The mask lives beside the data it masks (`ldes::engine::ln_defined`), so
7405 // that the doctest suite can pin it against `LnSolution::defined_*` without
7406 // reaching into this file. Spelling it here instead would put the rule that
7407 // decides what the table MEANS inside a printer.
7408 typedef ldes::engine::LnColumn Col;
7409 const auto def = [&](std::size_t i, Col c) {
7410 return ldes::engine::ln_defined(model, r, i, c);
7411 };
7412
7413 if (output == "json") {
7414 reg::Json j = reg::Json::object();
7415 j["model"] = file;
7416 j["arith"] = "double";
7417 j["solver"] = "ldes";
7418 // WHICH ENGINE ANSWERED, on the same grounds the flat arm's `engine=` is
7419 // not decoration: `-s ldes` names two different simulators depending on
7420 // whether the model is layered, and a number quoted from one must not be
7421 // read as the other's.
7422 j["engine"] = "native-ln";
7423 j["samples"] = o.samples;
7424 j["seed"] = o.seed;
7425 j["simulatedTime"] = r.simulated_time;
7426 j["completions"] = r.completions;
7427 j["seconds"] = best;
7428 reg::Json rows = reg::Json::array();
7429 for (std::size_t i = 1; i <= model.nidx; ++i) {
7430 reg::Json row = reg::Json::object();
7431 row["node"] = model.names[i];
7432 row["type"] = ln_element_kind(model, i);
7433 auto put = [&](const char* key, double v, bool defined) {
7434 if (defined) row[key] = v;
7435 else row[key] = nullptr;
7436 };
7437 put("QLen", r.QLN(i, 0), def(i, Col::QLen));
7438 put("Util", r.ULN(i, 0), def(i, Col::Util));
7439 put("RespT", r.RLN(i, 0), def(i, Col::RespT));
7440 put("ResidT", r.WLN(i, 0), def(i, Col::ResidT));
7441 put("Tput", r.TLN(i, 0), def(i, Col::Tput));
7442 rows.push_back(row);
7443 }
7444 j["rows"] = rows;
7445 emit_document(dump_document(j, 1));
7446 return 0;
7447 }
7448
7449 std::printf("SolverLDES(native LN engine) arith=double type=%s samples=%zu seed=%ld "
7450 "simtime=%.6g completions=%lld time=%.4fs\n",
7451 line::util::method_type("LDES", o.method).c_str(), o.samples, o.seed,
7452 r.simulated_time, r.completions, best);
7453 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
7454 "RespT", "ResidT", "Tput");
7455 for (std::size_t i = 1; i <= model.nidx; ++i) {
7456 // Six digits, as on the two tables around it, so the three arms diff.
7457 auto fmt = [&](double v, bool defined, char* buf) {
7458 if (!defined) std::snprintf(buf, 24, "%12s", "NaN");
7459 else std::snprintf(buf, 24, "%12.6g", v);
7460 };
7461 char q[24], u[24], rr[24], w[24], t[24];
7462 fmt(r.QLN(i, 0), def(i, Col::QLen), q);
7463 fmt(r.ULN(i, 0), def(i, Col::Util), u);
7464 fmt(r.RLN(i, 0), def(i, Col::RespT), rr);
7465 fmt(r.WLN(i, 0), def(i, Col::ResidT), w);
7466 fmt(r.TLN(i, 0), def(i, Col::Tput), t);
7467 std::printf("%-62s %-10s %s %s %s %s %s\n", model.names[i].c_str(),
7468 ln_element_kind(model, i), q, u, rr, w, t);
7469 }
7470 return 0;
7471}
7472
7473/**
7474 * `-s lqns`: the same layered model, solved by the external binary.
7475 *
7476 * The table has the SAME columns as run_ln's so that the two can be diffed row
7477 * by row, but the numbers are not sanitized the same way: `ln_sanitize` also
7478 * snaps anything at or below FineTol to zero, which is right for a fixed point
7479 * this port computed and wrong for a measurement it did not -- lqns reports no
7480 * residence time and no arrival rate at all, and a zero there would read as a
7481 * computed zero. Those two columns print NaN, and only the snap-to-tenth of the
7482 * reference's getAvgTable is applied.
7483 */
7484template <class T>
7485int run_lqns(const std::string& file, const std::string& output, const Knobs& k) {
7486 using namespace line;
7487 const lqn::LqnModel<T> model = lqn::read_lqnx_model<T>(file);
7488
7490 if (!k.method.empty()) opt.method = k.method;
7491 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
7492 if (k.samples) opt.samples = static_cast<double>(k.samples);
7493 opt.verbose = k.verbose;
7494 opt.keep = k.keep;
7495 opt.remote = k.remote;
7496 if (!k.remote_url.empty()) opt.remote_url = k.remote_url;
7497 opt.timeout_seconds = k.timeout_seconds;
7498
7499 lqns::SolverLQNS<T> solver(model, opt);
7500 const lqns::LqnsSolution<T> sol = solver.get_ensemble_avg();
7501 const lqn::LqnStruct<T>& sn = solver.get_struct();
7503
7504 if (output == "json") {
7505 reg::Json j = reg::Json::object();
7506 j["model"] = file;
7507 j["arith"] = num_traits<T>::name();
7508 j["solver"] = lqns::SolverLQNS<T>::is_stochastic_method(opt.method) ? "lqsim" : "lqns";
7509 j["method"] = opt.method;
7510 j["iterations"] = sol.iterations;
7511 j["seconds"] = solver.runtime();
7512 reg::Json rows = reg::Json::array();
7513 for (std::size_t i = 1; i <= sn.nidx; ++i) {
7514 reg::Json r = reg::Json::object();
7515 r["node"] = names.names[i];
7516 r["type"] = ln_element_kind(names, i);
7517 auto put = [&](const char* key, const std::vector<T>& v, const std::vector<bool>& d) {
7518 if (d[i]) r[key] = lqns::detail::snap_to_tenth(num_traits<T>::to_double(v[i]));
7519 else r[key] = nullptr;
7520 };
7521 put("QLen", sol.QN, sol.defined_Q);
7522 put("Util", sol.UN, sol.defined_U);
7523 put("RespT", sol.RN, sol.defined_R);
7524 put("ResidT", sol.WN, sol.defined_W);
7525 put("Tput", sol.TN, sol.defined_T);
7526 rows.push_back(r);
7527 }
7528 j["rows"] = rows;
7529 emit_document(dump_document(j, 1));
7530 return 0;
7531 }
7532
7533 std::printf("SolverLQNS(%s) arith=%s type=%s iterations=%d time=%.4fs\n",
7535 line::util::method_type("LQNS", opt.method).c_str(), sol.iterations,
7536 solver.runtime());
7537 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
7538 "RespT", "ResidT", "Tput");
7539 for (std::size_t i = 1; i <= sn.nidx; ++i) {
7540 // Six digits, as on the SolverLN table above and for the same reason.
7541 auto fmt = [&](const std::vector<T>& v, const std::vector<bool>& d, char* buf) {
7542 if (!d[i]) std::snprintf(buf, 24, "%12s", "NaN");
7543 else
7544 std::snprintf(buf, 24, "%12.6g",
7545 lqns::detail::snap_to_tenth(num_traits<T>::to_double(v[i])));
7546 };
7547 char q[24], u[24], rr[24], w[24], t[24];
7548 fmt(sol.QN, sol.defined_Q, q);
7549 fmt(sol.UN, sol.defined_U, u);
7550 fmt(sol.RN, sol.defined_R, rr);
7551 fmt(sol.WN, sol.defined_W, w);
7552 fmt(sol.TN, sol.defined_T, t);
7553 std::printf("%-62s %-10s %s %s %s %s %s\n", names.names[i].c_str(),
7554 ln_element_kind(names, i), q, u, rr, w, t);
7555 }
7556 return 0;
7557}
7558
7559/**
7560 * `-i lqnx|xml`: the layered path, sibling to solve_model_dispatch.
7561 *
7562 * `-s auto` CONSULTS THE LAYERED ARM of the chooser, which is keyed on the
7563 * metric and on one trait of the model: a cache task promotes the NC layer
7564 * engine, because the cache layer is where NC beats MVA. LQNS leads two of
7565 * those rankings and IS wrapped, so `-s auto` selects it wherever the binary is
7566 * installed -- which makes the choice machine-dependent, exactly as
7567 * `chooseAvgSolverHeur.m` makes it, since LINE ships no LQNS binary. The banner
7568 * always names what answered.
7569 *
7570 * THE TOKENS NAME THE LAYER ENGINE, as the Java CLI's do: `ln.mva` runs the
7571 * layers under SolverMVA and `ln.comom` under SolverNC. The bare `ln` keeps the
7572 * MVA layers this port has always given it -- the Java CLI reads it as NC, but
7573 * changing it here would re-baseline every existing layered result under a token
7574 * whose meaning nothing in this tree states -- so `ln.comom` is the way to ask
7575 * for NC layers. `lqns` is not one of them: it does not solve LAYERS at all, it
7576 * hands the whole model to another program, so it takes the wrapper's own knobs
7577 * and none of SolverLN's.
7578 */
7579/** The solver method name as the console names it, e.g. "mva" -> "MVA". */
7580std::string upper_tag(const std::string& s) {
7581 std::string out = s;
7582 for (std::size_t i = 0; i < out.size(); ++i)
7583 out[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(out[i])));
7584 return out;
7585}
7586
7587int solve_lqn_dispatch(const std::string& arith, const std::string& solver,
7588 const std::string& analysis, const std::string& output,
7589 const std::string& file, const Knobs& k) {
7590 if (file.empty())
7591 throw line::InputError(
7592 "a layered model is read from a file: pass -f <model.lqnx> or -f <model.json> (neither "
7593 "layered reader has a stdin form)");
7594 if (analysis != "avg" && analysis != "tran" && analysis != "sens" && analysis != "cdf")
7596 "the layered path ports -a avg (getAvgTable), -a tran (getTranAvg), -a sens "
7597 "(getSensitivityTable) and -a cdf (getCdfRespT); got '" + analysis + "'");
7598 const std::string s = solver.empty() ? "auto" : solver;
7599 if (s != "auto" && s != "ln" && s != "ln.mva" && s != "ln.comom" && s != "lqns" &&
7600 s != "ldes")
7602 "the layered path takes -s ln, ln.mva, ln.comom, ldes, lqns or auto (got '" + s +
7603 "'); a Network solver cannot be applied to a LayeredNetwork directly");
7604
7605 // Solver console: the layered path has its own dispatcher and never
7606 // reaches solve_model_dispatch, so it opens the narrated run here. The
7607 // guard's destructor closes it, on an exception too.
7608 line::util::LineConsole::Run consoleRun(upper_tag(s), "", true);
7609
7610 Knobs kk = k;
7611 // What will actually answer: the token, or what `-s auto` resolves to.
7612 std::string engine = s;
7613 // `-s auto` with no explicit layer engine consults chooseLayeredSolver. An
7614 // explicit --layer-solver is a choice the caller already made, so the
7615 // chooser does not overrule it.
7616 if (s == "auto" && kk.layer_solver.empty()) {
7617 std::string getter = "getAvgTable";
7618 if (analysis == "tran") getter = "getTranAvg";
7619 else if (analysis == "cdf") getter = "getCdfRespT";
7620 else if (analysis == "sens") getter = "getSensitivityTable";
7621 // `iscache` is the one trait the ranking reads, and it costs one parse
7622 // of the .lqnx: the same document the solve parses again below.
7624 bool has_cache_task = false;
7625 for (std::size_t i = 0; i < probe.iscache.size(); ++i)
7626 if (probe.iscache[i]) has_cache_task = true;
7628 line::autosolver::auto_choose_layered_solver(getter, has_cache_task);
7629 const std::string token = line::autosolver::auto_layered_name(lc.solver);
7630 if (token == "lqns") engine = "lqns";
7631 else if (token == "ln.comom") kk.layer_solver = "nc";
7632 else if (token == "ln.fld") kk.layer_solver = "fluid";
7633 else kk.layer_solver = "mva";
7634 std::string note;
7635 for (std::size_t i = 0; i < lc.skipped.size(); ++i)
7636 note += std::string(i ? ", " : " (the ranking preferred ") +
7638 if (!lc.skipped.empty()) note += ", which is not available here)";
7639 std::printf("SolverAUTO selected %s%s\n", token.c_str(), note.c_str());
7640 }
7641
7642 // ---- the wrapper, BEFORE the SolverLN knob ladder ---------------------
7643 // It is a wrapper, not a layer engine: --samples is the lqsim run length
7644 // rather than a simulated LAYER's, and --keep, --remote and --timeout
7645 // describe a child process no SolverLN run has. Asking the ladder below
7646 // about them would answer for the wrong solver.
7647 if (engine == "lqns") {
7648 if (analysis != "avg")
7650 "SolverLQNS reports the mean table its binary computes; it has no transient, no "
7651 "sensitivity and no response-time distribution here, so it takes -a avg (got '" +
7652 analysis + "')");
7653 if (!kk.layer_solver.empty())
7654 throw line::InputError(
7655 "--layer-solver names the engine SolverLN runs on each layer; -s lqns solves no "
7656 "layers, it hands the whole model to the lqns binary");
7657 if (output == "layers")
7659 "-o layers dumps the stations and routing SolverLN BUILT from the model; lqns "
7660 "builds its own submodels inside another process and this port never sees them");
7661 if (kk.iter_tol >= 0.0 || kk.iter_max > 0)
7663 "--iter_tol and --iter_max are SolverLN's layer-iteration knobs; lqns runs its own "
7664 "iteration and takes neither (its --iteration-limit is unreliable as of 6.2.27, "
7665 "which is why the reference stopped passing it)");
7666 if (kk.seed)
7668 "--seed sets the stream of a simulator this port drives; lqsim seeds itself and "
7669 "the wrapper passes no seed, exactly as the reference does not");
7670 if (kk.repeat > 1)
7672 "--repeat times a solve by re-running it; re-running lqsim would report a "
7673 "different answer under the same banner");
7674 if (kk.no_interlocking || kk.interlock_knobs_given() || !kk.ln_transient.empty() || !kk.ln_transient_channels.empty() ||
7675 !kk.sens_method.empty() || !kk.sens_scheme.empty() || kk.sens_step > 0.0)
7677 "--no-interlocking, --interlock-*, --ln-transient*, and --sens-* are SolverLN options; -s lqns "
7678 "has none of them");
7679 if (arith != "double")
7681 "SolverLQNS reads a result file another program wrote in decimal double "
7682 "precision; there is no higher precision to carry, so rerun with --arith double "
7683 "(got '" + arith + "')");
7684 // The Network-only knobs the SolverLN ladder below refuses are refused
7685 // HERE TOO. This branch returns before that ladder runs, so a knob left
7686 // out of it is silently DROPPED rather than refused -- and which branch
7687 // a bare `.lqnx` path takes depends on whether an lqns binary is
7688 // installed, so the same command line would be refused on one machine
7689 // and quietly ignored on another.
7690 if (kk.has_cutoff())
7692 "--cutoff bounds the open population of a CTMC state space and applies to -s "
7693 "ctmc; the layered path enumerates no states");
7694 if (kk.node)
7696 "--node selects the stateful node a CTMC query is labelled by; the layered path "
7697 "reports every LQN element");
7698 return run_lqns<double>(file, output, kk);
7699 }
7700 if (k.keep || k.verbose || k.remote || !k.remote_url.empty() || k.timeout_seconds)
7702 "--keep, --verbose, --remote, --remote-url and --timeout describe the child process "
7703 "of an external solver and apply to -s lqns only");
7704 // ---- the native LN simulator, BEFORE the SolverLN knob ladder ----------
7705 // It solves no LAYERS, so the ladder below asks its questions of a
7706 // decomposition this arm never builds: --iter_tol and --iter_max bound a
7707 // fixed point it does not iterate, --layer-solver names an engine it does
7708 // not run, and --samples and --seed -- which the ladder refuses outright
7709 // unless a layer is simulated -- are precisely this arm's two settings.
7710 if (engine == "ldes") {
7711 if (analysis != "avg" && analysis != "cdf")
7713 "the native LN engine measures a sample path: it takes -a avg for the mean table "
7714 "and -a cdf for the per-entry response time distribution, and has no transient "
7715 "and no sensitivity here (got '" + analysis + "')");
7716 if (arith != "double")
7718 "the native LN engine accumulates its estimators in double, so there is no higher "
7719 "precision to carry; rerun with --arith double (got '" + arith + "')");
7720 if (output == "layers")
7722 "-o layers dumps the stations and routing SolverLN BUILT from the model; -s ldes "
7723 "simulates the layered semantics directly and builds no submodels");
7724 if (!kk.layer_solver.empty())
7725 throw line::InputError(
7726 "--layer-solver names the engine SolverLN runs on each layer; -s ldes solves no "
7727 "layers, it simulates entries, activities and calls directly");
7728 if (kk.iter_tol >= 0.0 || kk.iter_max > 0 || kk.no_interlocking || kk.interlock_knobs_given())
7730 "--iter_tol, --iter_max, --no-interlocking and --interlock-* are SolverLN's layer-iteration "
7731 "knobs; a simulated sample path converges by run length, which is --samples");
7732 if (!kk.ln_transient.empty() || !kk.ln_transient_channels.empty() ||
7733 !kk.sens_method.empty() || !kk.sens_scheme.empty() || kk.sens_step > 0.0)
7735 "--ln-transient* and --sens-* are SolverLN options; -s ldes has none of them");
7736 if (!kk.method.empty() && kk.method != "default")
7738 "--method on the layered path names the LN UPDATE (default, moment3, mw.*); "
7739 "-s ldes performs no update, it simulates the model (got '" + kk.method + "')");
7740 // The --ldes-* family is the SUBPROCESS engine's settings. The native LN
7741 // engine reads samples, events and seed and nothing else, so a warmup
7742 // filter or a CI estimator passed here would be DROPPED rather than
7743 // honoured -- and a dropped `--ldes-tranfilter none` reads as a run with
7744 // no warmup removal that in fact removed one.
7745 if (!kk.ldes_tranfilter.empty() || kk.ldes_warmupfrac >= 0.0 ||
7746 !kk.ldes_cimethod.empty() || kk.ldes_cnvgon || kk.ldes_cnvgtol > 0.0 ||
7747 kk.ldes_slotted || kk.ldes_slotlength > 0.0 || kk.ldes_replications > 0 ||
7748 kk.ldes_numthreads > 0 || kk.ldes_maxtime > 0.0 || !kk.ldes_initsol.empty() ||
7749 !kk.ldes_rest_url.empty())
7751 "the --ldes-* flags configure the SUBPROCESS engine that answers -s ldes on a "
7752 "Network (warmup filter, CI estimator, slot lattice, replications, warm-start "
7753 "placement); the native LN engine behind -i lqnx -s ldes reads --samples and "
7754 "--seed only");
7755 // The Network-only knobs the SolverLN ladder refuses below are refused
7756 // HERE TOO: this branch returns before that ladder runs, so a knob left
7757 // out of it would be silently dropped rather than refused.
7758 if (kk.has_cutoff())
7760 "--cutoff bounds the open population of a CTMC state space and applies to -s "
7761 "ctmc; the layered path enumerates no states");
7762 if (kk.node)
7764 "--node selects the stateful node a CTMC query is labelled by; the layered path "
7765 "reports every LQN element");
7766 if (kk.t1 >= 0.0)
7768 "--tspan sets the horizon of a transient analysis; the native LN engine runs to "
7769 "a completion budget, which is --samples");
7770 if (kk.tol >= 0.0)
7772 "--tol is not an LDES option; the run length is set with --samples");
7773 if (analysis == "cdf") return run_ln_ldes_cdf(file, output, kk);
7774 return run_ln_ldes(file, output, kk);
7775 }
7776 // The solver method name and --layer-solver name the same choice, so they may not
7777 // disagree: silently letting one win would report the other in the banner.
7778 if (s == "ln.comom") {
7779 if (!kk.layer_solver.empty() && kk.layer_solver != "nc")
7780 throw line::InputError("-s ln.comom already selects NC layers, but --layer-solver says '" +
7781 kk.layer_solver + "'");
7782 kk.layer_solver = "nc";
7783 } else if (s == "ln.mva") {
7784 if (!kk.layer_solver.empty() && kk.layer_solver != "mva")
7785 throw line::InputError("-s ln.mva already selects MVA layers, but --layer-solver says '" +
7786 kk.layer_solver + "'");
7787 kk.layer_solver = "mva";
7788 }
7789 // --layer-solver is this port's own flag, the C++ spelling of the
7790 // reference's solver FACTORY: `LN(model, @(m) MVA(m))` against
7791 // `LN(model, @(m) Fluid(m))`. They converge to DIFFERENT fixed points.
7792 if (!kk.layer_solver.empty() && kk.layer_solver != "mva" && kk.layer_solver != "nc" &&
7793 kk.layer_solver != "fluid" && kk.layer_solver != "ssa")
7794 throw line::InputError("--layer-solver takes mva, nc, fluid or ssa (got '" +
7795 kk.layer_solver + "')");
7796 // ---- knobs the layered path does not have are refused, not dropped ----
7797 if ((k.samples || k.seed) && kk.layer_solver != "ssa")
7799 "--samples and --seed set the run length and the stream of a SIMULATED layer; the "
7800 "layered path draws no random numbers unless --layer-solver ssa is in force");
7801 if (k.has_cutoff())
7803 "--cutoff bounds the open population of a CTMC state space and applies to -s ctmc; the "
7804 "layered path enumerates no states");
7805 if (k.t1 >= 0.0 && analysis != "tran")
7807 "--tspan sets the horizon of a transient analysis and applies to the layered path "
7808 "only with -a tran");
7809 if (analysis == "tran" && !(k.t1 >= 0.0))
7810 throw line::InputError(
7811 "-a tran integrates each layer's drift and needs a horizon: pass --tspan <t0>:<t1>");
7812 if (k.node)
7814 "--node selects the stateful node a CTMC query is labelled by; the layered path "
7815 "reports every LQN element");
7816 if (k.tol >= 0.0)
7818 "--tol is not a SolverLN option (LnOptions carries iter_tol and iter_max); "
7819 "use --iter_tol");
7820 // --method now names the LN UPDATE, which is a different question from the
7821 // layer engine: `moment3` reports a distribution the default never forms,
7822 // and the two bound requests report a bound instead of a fixed point.
7823 if (!k.method.empty() && k.method != "default" && k.method != "moment3" &&
7824 k.method != "mw.upper" && k.method != "mw.lower")
7826 "--method on the layered path takes default, moment3, mw.upper or mw.lower "
7827 "(got '" + k.method + "'); the per-layer engine is chosen with --layer-solver");
7828 if ((k.method == "mw.upper" || k.method == "mw.lower") && analysis != "avg")
7830 "--method mw.* reports a throughput and utilization BOUND and solves no layer, so "
7831 "it has no transient, no sensitivity and no response-time law; use -a avg");
7832 if (!k.sens_method.empty() && analysis != "sens")
7833 throw line::UnsupportedError("--sens-method applies to -a sens");
7834 if (!k.ln_transient.empty() && analysis != "tran")
7835 throw line::UnsupportedError("--ln-transient applies to -a tran");
7836
7837 if (analysis == "tran") {
7838 if (arith != "double")
7840 "the layered transient integrates each layer's drift with LSODA, which is double "
7841 "precision by construction; rerun with --arith double (got '" + arith + "')");
7842 return run_ln_tran<double>(file, output, kk);
7843 }
7844 if (analysis == "cdf") {
7845 if (arith != "double")
7847 "-a cdf fits an APH to a fluid passage time, integrated by LSODA in double "
7848 "precision; rerun with --arith double (got '" + arith + "')");
7849 return run_ln_cdf<double>(file, output, kk);
7850 }
7851 if (analysis == "sens") {
7852 if (arith == "double") return run_ln_sens<double>(file, output, kk);
7853 if (arith == "exact") return run_ln_sens<line::Rational>(file, output, kk);
7854 if (arith == "real:16") return run_ln_sens<line::Real<16> >(file, output, kk);
7855 if (arith == "real" || arith == "real:32")
7856 return run_ln_sens<line::Real<32> >(file, output, kk);
7857 if (arith == "real:64") return run_ln_sens<line::Real<64> >(file, output, kk);
7858 if (arith == "real:128") return run_ln_sens<line::Real<128> >(file, output, kk);
7859 if (arith == "real:256") return run_ln_sens<line::Real<256> >(file, output, kk);
7860 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
7861 }
7862
7863 if (arith == "double") return run_ln<double>(file, output, kk);
7864 if (arith == "exact") return run_ln<line::Rational>(file, output, kk);
7865 // precision-ladder rationale (real:16 rung): see _kb/14-cpp-multiprecision.md
7866 if (arith == "real:16") return run_ln<line::Real<16> >(file, output, kk);
7867 if (arith == "real" || arith == "real:32") return run_ln<line::Real<32> >(file, output, kk);
7868 if (arith == "real:64") return run_ln<line::Real<64> >(file, output, kk);
7869 if (arith == "real:128") return run_ln<line::Real<128> >(file, output, kk);
7870 if (arith == "real:256") return run_ln<line::Real<256> >(file, output, kk);
7871 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
7872}
7873
7874/**
7875 * `-a node`: `@@NetworkSolver/getAvgNodeTable`, the means per NODE.
7876 *
7877 * A DIFFERENT INDEX SPACE FROM `-a avg`, not a relabelling of it. The AvgTable
7878 * is indexed by STATION, so a ClassSwitch, a Router, a Fork, a Join and a Sink
7879 * are absent from it entirely -- they hold no jobs, so they have no row -- yet
7880 * jobs flow through them and the flow is what a caller sizing a link or an
7881 * interconnect needs. This table has one row per node and reports the arrival
7882 * rate and the throughput at every one of them, which is the only place those
7883 * two numbers exist for a non-station node.
7884 *
7885 * QLen, Util, RespT and ResidT ARE the station numbers, scattered to the node
7886 * indices and left at zero elsewhere; that is the reference's own construction
7887 * and not a gap, because a node that is not a station holds no jobs and serves
7888 * nothing. ArvR and Tput are the two the reference recomputes, through
7889 * `sn_get_node_arvr_from_tput` and `sn_get_node_tput_from_tput`.
7890 *
7891 * THE F REGION PSEUDO-NODE ROWS OF THE REFERENCE ARE NOT EMITTED. MATLAB
7892 * appends one row per finite-capacity region, filled from `result.Avg` rows
7893 * M+1..M+F; the C++ `AvgResult` carries no per-region queue length or
7894 * utilization, so those rows have no data source here and are omitted rather
7895 * than fabricated as zeros, which would read as an empty region.
7896 */
7897/* `avg_result_from_sim` MOVED to line/solvers/solver_node_tables.h, where the
7898 * example corpus can reach it too: four `cache_replc_*` twins print the NODE
7899 * table their references print, and it is the same view of the same numbers.
7900 * Named unqualified below, as it was when it was defined here. */
7902
7903/**
7904 * The LDES result document mapped onto the station AvgResult, BY NAME.
7905 *
7906 * The engine reports its own station and class order, and pairing the two off
7907 * positionally would put one station's numbers on another's row; `-a avg`
7908 * already matches by name for exactly that reason and this is the same match.
7909 * `WN` is recomputed rather than taken, for the reason the `-a avg` arm states:
7910 * the engine counts one visit per station, so its residence time is the response
7911 * time whenever a visit ratio is not 1.
7912 *
7913 * REGION ROWS RIDE PAST THE STATIONS, in the (M+F) layout `jmt_map_measures`
7914 * already uses, so a caller reads both engines' finite-capacity rows the same
7915 * way.
7916 */
7917inline line::mva::AvgResult<double> avg_result_from_ldes(
7919 const std::size_t M = sn.nstations, K = sn.nclasses;
7920 const std::size_t F = a.QNfcr.empty() ? 0 : a.QNfcr.rows();
7922 line::Matrix<double>* dst[6] = {&r.QN, &r.UN, &r.RN, &r.TN, &r.AN, &r.WN};
7923 const line::Matrix<double>* src[6] = {&a.QN, &a.UN, &a.RN, &a.TN, &a.AN, &a.WN};
7924 const line::Matrix<double>* fcr[6] = {&a.QNfcr, &a.UNfcr, &a.RNfcr,
7925 &a.TNfcr, &a.ANfcr, &a.WNfcr};
7926 std::vector<std::size_t> st_of(a.station_names.size(), 0); // 1-based, 0 = unmatched
7927 for (std::size_t i = 0; i < a.station_names.size(); ++i)
7928 for (std::size_t j = 0; j < M; ++j)
7929 if (sn.stations[j].name == a.station_names[i]) { st_of[i] = j + 1; break; }
7930 std::vector<std::size_t> cl_of(a.class_names.size(), 0);
7931 for (std::size_t c = 0; c < a.class_names.size(); ++c)
7932 for (std::size_t j = 0; j < K; ++j)
7933 if (sn.classes[j].name == a.class_names[c]) { cl_of[c] = j + 1; break; }
7934 for (int m = 0; m < 6; ++m) {
7935 *dst[m] = line::Matrix<double>(M + F, K, 0.0);
7936 for (std::size_t i = 0; i < a.station_names.size(); ++i)
7937 for (std::size_t c = 0; c < a.class_names.size(); ++c)
7938 if (st_of[i] && cl_of[c])
7939 (*dst[m])(st_of[i] - 1, cl_of[c] - 1) = ldes_at(*src[m], i, c);
7940 for (std::size_t f = 0; f < F; ++f)
7941 for (std::size_t c = 0; c < a.class_names.size(); ++c)
7942 if (cl_of[c]) (*dst[m])(M + f, cl_of[c] - 1) = ldes_at(*fcr[m], f, c);
7943 }
7944 line::Matrix<double> RNs(M, K, 0.0);
7945 for (std::size_t i = 0; i < M; ++i)
7946 for (std::size_t c = 0; c < K; ++c) RNs(i, c) = r.RN(i, c);
7948 for (std::size_t i = 0; i < M; ++i)
7949 for (std::size_t c = 0; c < K; ++c) r.WN(i, c) = WNs(i, c);
7950 for (std::size_t c = 0; c < K; ++c) {
7951 r.CN.push_back(ldes_at(a.CN, 0, c));
7952 r.XN.push_back(ldes_at(a.XN, 0, c));
7953 }
7954 r.method = a.method;
7955 r.actualmethod = a.method;
7956 return r;
7957}
7958
7959/**
7960 * Solve for the station AvgResult with the named engine.
7961 *
7962 * SHARED BY EVERY @@NetworkSolver TABLE THAT IS NOT THE AvgTable -- `-a node`,
7963 * `-a sys`, `-a chain`, `-a nodechain` -- because each of them is a VIEW of the
7964 * same solved result and differs only in how it is indexed and aggregated.
7965 * Giving each arm its own engine ladder would let the five drift apart in which
7966 * knobs they honour, which is exactly the silent divergence the Knobs struct
7967 * exists to prevent one layer up.
7968 *
7969 * SSA and Fluid reach it through `avg_result_from_sim`, and only under
7970 * `--arith double`: both integrate transcendental quantities (exponential
7971 * clocks, an LSODA drift), so the dispatcher refuses the other backends by name
7972 * before the ladder is entered, exactly as their own `-a avg` arms do.
7973 *
7974 * THE TWO WRAPPERS ARE HERE FOR THE SAME REASON THE TWO SIMULATORS ARE.
7975 * `getAvgNodeTable` is @@NetworkSolver's and is a VIEW of whatever AvgResult a
7976 * solver returned; JMT and LDES both return one (`JmtResult::avg`, the LDES
7977 * result document), so refusing them the four views said "no C++ simulation
7978 * engine" about engines this port drives. `file` is what LDES needs and the
7979 * others ignore: it hands the model DOCUMENT to an external process rather
7980 * than reading the struct.
7981 */
7982template <class T>
7983line::mva::AvgResult<T> run_avg_engine(const line::qn::NetworkStruct<T>& sn, const Knobs& k,
7984 const std::string& s, std::string& banner,
7985 std::string* suffix = nullptr,
7986 const std::string* file = nullptr) {
7988 if (s == "jmt" || s == "ldes") {
7989 // COMPILE-TIME, as for the two simulators: both wrappers report in
7990 // double and the dispatcher has already refused every other arithmetic
7991 // by name, so the discarded branch is unreachable rather than narrowed.
7992 if constexpr (std::is_same_v<T, double>) {
7993 if (s == "jmt") {
7995 if (!k.method.empty() && k.method != "default") o.method = k.method;
7996 if (k.samples > 0) o.samples = static_cast<double>(k.samples);
7997 if (k.seed != 0) o.seed = static_cast<long>(k.seed);
7998 o.keep = k.keep;
7999 if (k.t1 >= 0.0) o.max_simulated_time = k.t1;
8000 o.verbose = k.verbose;
8002 r = a.avg;
8003 banner = "SolverJMT";
8004 // THE SEED IS PART OF THE ANSWER, as it is for SSA: two runs of
8005 // a simulation are the same measurement only if both state it.
8006 if (suffix) {
8007 char buf[128];
8008 std::snprintf(buf, sizeof(buf), " samples=%g seed=%ld", o.samples, o.seed);
8009 *suffix = buf;
8010 }
8011 } else {
8012 const line::ldes::LdesOptions o = ldes_options(k);
8013 const line::ldes::LdesResult a =
8014 ldes_run(file ? *file : std::string(), o, std::vector<std::string>());
8015 r = avg_result_from_ldes(sn, a);
8016 banner = "SolverLDES";
8017 if (suffix) {
8018 char buf[160];
8019 std::snprintf(buf, sizeof(buf), " engine=%s samples=%zu seed=%ld",
8020 a.engine.empty() ? "?" : a.engine.c_str(), o.samples, o.seed);
8021 *suffix = buf;
8022 }
8023 }
8024 } else {
8025 throw line::UnsupportedError("-s " + s + " runs under --arith double only");
8026 }
8027 } else if (s == "ssa" || s == "fluid") {
8028 // COMPILE-TIME, not just run-time: both runners static_assert on
8029 // transcendental arithmetic inside (an exponential clock, a square root
8030 // in the Cox refit), so instantiating them at Rational is a hard error
8031 // and not a refusal. The dispatcher has already rejected every arith
8032 // but double by name, so the discarded branch is unreachable rather
8033 // than silently narrowed.
8034 if constexpr (std::is_same_v<T, double>) {
8035 if (s == "ssa") {
8037 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8038 if (k.samples) opt.samples = k.samples;
8039 if (k.seed) opt.seed = k.seed;
8040 // The cache write-back rides beside the metric table, for the
8041 // reason `node_metrics` states: the realized hit and miss shares
8042 // are what the simulation MEASURED, and without them the node
8043 // table falls back to the split `link()` offered.
8044 std::vector<line::ssa::SsaCacheRatio> cache;
8046 // ResidT and ArvR are derived from the VISITS, and a cache's
8047 // split is routing, so both are taken on the struct carrying the
8048 // measured hit/miss shares rather than on `link()`'s even offer.
8049 r = avg_result_from_sim<T>(line::ssa::sn_with_ssa_cache_split<T>(sn, cache),
8050 a.QN, a.UN, a.RN, a.TN, a.CN, a.XN, a.method);
8052 // THE SEED AND THE SAMPLE COUNT ARE PART OF THE ANSWER, not of
8053 // the invocation, so they ride in the banner here as they do in
8054 // `-a avg`: two runs of a simulation are the same measurement
8055 // only if both are stated, and a parity row that quotes one of
8056 // these tables has to carry them with it.
8057 banner = "SolverSSA";
8058 // APPENDED, NOT PREFIXED. Every consumer recognises a banner by
8059 // `Solver<name> arith=`, so a fact wedged between the two makes
8060 // the table belong to no solver at all -- which is how the
8061 // parity harness lost the whole SSA section.
8062 if (suffix) {
8063 char buf[128];
8064 std::snprintf(buf, sizeof(buf), " samples=%zu seed=%lu time=%.6g", a.samples,
8065 static_cast<unsigned long>(opt.seed), a.simulated_time);
8066 *suffix = buf;
8067 }
8068 } else {
8069 const line::fluid::FluidOptions opt = fluid_options(k);
8070 // THE map_env FALLBACK IS ASKED FOR HERE RATHER THAN THROUGH
8071 // `run_avg`: the fluid runner returns a `FluidSolution` and not
8072 // an `AvgResult`, so this arm cannot pass itself as the `Run`
8073 // callable. It asks `needs_map_env` the same question with the
8074 // same config instead of carrying a second copy of the decision.
8075 const line::solvers::MapEnvConfig mecfg = map_env_config(k);
8077 mecfg)
8078 .needed) {
8079 r = line::solvers::map_env_approx<T>(sn, "SolverFluid", mecfg,
8081 opt.method);
8082 banner = "SolverFluid";
8083 if (suffix) *suffix = " map_env=" + r.actualmethod;
8084 return r;
8085 }
8086 // The cache decomposition renormalizes the self-switch at the
8087 // converged split; `-a node` needs that struct or it reports the
8088 // 1/2-1/2 `link()` left behind (see `node_metrics`).
8090 // The null must be typed: a bare `nullptr` is `std::nullptr_t`
8091 // and blocks deduction of T from the fourth argument.
8092 // THE SPLIT ITSELF IS TAKEN TOO, not only the struct it
8093 // renormalized: `node_metrics` prefers the stated split over the
8094 // visit ratios, and `-a cache` is built from nothing else.
8097 sn, opt, static_cast<line::qn::NetworkStruct<T>*>(nullptr), &refreshed, &cache);
8098 // Non-empty only where the cache branch ran, which is the same
8099 // test `node_metrics` makes on the pointer. IT IS ALSO THE
8100 // STRUCT THE ARRIVAL RATE IS READ FROM: that column is derived
8101 // from the class-expanded routing, and the base struct still
8102 // holds the 1/2-1/2 self-switch `link()` offered, so deriving it
8103 // there reports 0.5/0.5 where cache_replc_routing's Delay1 sees
8104 // 0.4/0.6. Every other column is indexed by station and is the
8105 // same in both structs.
8106 const bool has_ref = !refreshed.nodes.empty();
8107 r = avg_result_from_sim<T>(has_ref ? refreshed : sn, a.QN, a.UN, a.RN, a.TN, a.CN,
8108 a.XN, a.method);
8109 if (has_ref) r.refreshed_struct.reset(new line::qn::NetworkStruct<T>(refreshed));
8110 r.cache = cache;
8111 r.iter = static_cast<int>(a.iters);
8112 banner = "SolverFluid";
8113 if (suffix) {
8114 char buf[64];
8115 std::snprintf(buf, sizeof(buf), " iters=%zu", a.iters);
8116 *suffix = buf;
8117 }
8118 }
8119 } else {
8120 throw line::UnsupportedError("-s " + s + " runs under --arith double only");
8121 }
8122 } else if (s == "nc") {
8124 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8125 if (k.tol >= 0.0) opt.tol = k.tol;
8126 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
8127 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
8128 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
8129 // The map_env fallback, wired here for the reason this engine exists at
8130 // all: these views ARE the `getAvg` funnel of this CLI, so a view that
8131 // refused a MAP model the `-a avg` arm accepts would answer a different
8132 // model for the same command line.
8133 // COMPILE-TIME because the driver is double-only: `map_env_approx`
8134 // refuses `T != double` at run time, but its body still has to compile,
8135 // and an `EnvStageAvgFn<double>` does not convert to an
8136 // `EnvStageAvgFn<Rational>`.
8137 if constexpr (std::is_same_v<T, double>) {
8139 sn, "SolverNC", line::qn::nc_feature_set(opt.method), map_env_config(k),
8142 },
8144 } else {
8146 }
8147 banner = "SolverNC";
8148 } else if (s == "mam") {
8150 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8151 if (k.tol >= 0.0) opt.tol = k.tol;
8152 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
8154 banner = "SolverMAM";
8155 } else if (s == "ag") {
8157 apply_ag_knobs(k, opt);
8159 banner = "SolverAG";
8160 } else if (s == "ba") {
8162 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8164 banner = "SolverBA";
8165 } else if (s == "ctmc") {
8167 if (!k.method.empty()) opt.method = k.method;
8168 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
8169 opt.cutoff_mat = k.cutoff_mat;
8170 opt.force = k.force;
8173 banner = "SolverCTMC";
8174 } else {
8176 if (!k.method.empty() && k.method != "default") opt.method = k.method;
8177 if (k.tol >= 0.0) opt.tol = k.tol;
8178 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
8179 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
8180 // `-a avg` has honoured --multiserver since the flag existed; these
8181 // views are the SAME solve indexed differently, so ignoring it here made
8182 // `-a chain` answer a different model than `-a avg` for the same command
8183 // line. Measured on cqn_repairmen_multi, where softmin and the default
8184 // rule differ by 39% on the Delay queue length.
8185 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
8186 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
8187 line::Matrix<T> init;
8188 // Same funnel, same compile-time guard as the nc arm above.
8189 if constexpr (std::is_same_v<T, double>) {
8191 sn, "SolverMVA",
8193 map_env_config(k),
8194 [&opt, &init](const line::qn::NetworkStruct<double>& m) {
8195 return line::mva::solver_mva_run_analyzer(m, opt, init);
8196 },
8198 } else {
8200 }
8201 banner = "SolverMVA";
8202 }
8203 return r;
8204}
8205
8206/* `NodeMetrics` / `node_metrics` MOVED to line/solvers/solver_node_tables.h;
8207 * see the note above `run_avg_engine`. Used unchanged by both arms below. */
8210
8211template <class T>
8212int solve_model_node(const std::string& file, const Knobs& k, const std::string& s) {
8213 line::qn::Network<T> net = read_model<T>(file);
8215 std::string banner, suffix;
8216 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8217
8218 const std::size_t I = sn.nodes.size(), R = sn.nclasses;
8219 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
8220 const line::Matrix<T>&QNn = nm.QN, &UNn = nm.UN, &RNn = nm.RN, &WNn = nm.WN, &ANn = nm.AN,
8221 &TNn = nm.TN;
8222
8223 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
8224 // A FINITE CAPACITY REGION IS NOT A NODE, and the reference still prints it
8225 // in this table: `getAvgNodeTable` appends one row per region past the
8226 // nodes, because a WAITQ region holds jobs that are in no station's QLen and
8227 // the model's population only balances once they are read. The rows ride
8228 // past the stations in the returned AvgResult -- the (M+F) layout both
8229 // wrappers report -- and no analytical solver fills them, so this block is
8230 // empty for every engine that does not measure a region.
8231 //
8232 // Util AND ArvR ARE NaN, NOT ZERO. A region has no server to be busy and no
8233 // arrival process of its own; the reference reports both as missing, and a
8234 // zero there would be a number the run never measured.
8235 const std::size_t F =
8236 r.QN.rows() > sn.nstations ? r.QN.rows() - sn.nstations : static_cast<std::size_t>(0);
8237 const double region_nan = std::numeric_limits<double>::quiet_NaN();
8238 auto region_name = [&](std::size_t f) {
8239 return (f < sn.regions.size() && !sn.regions[f].name.empty())
8240 ? sn.regions[f].name
8241 : "FCR" + std::to_string(f + 1);
8242 };
8243 // The reference's own filter, the region twin of the all-zero row test: a
8244 // region no job ever entered is absent rather than a row of zeros.
8245 auto region_empty = [&](std::size_t f, std::size_t c) {
8246 return !(d(r.QN(sn.nstations + f, c)) > 0.0 || d(r.RN(sn.nstations + f, c)) > 0.0 ||
8247 d(r.TN(sn.nstations + f, c)) > 0.0);
8248 };
8249 if (g_json_output) {
8250 // The banner under `-o json` too, for print_chain_table's reason: the
8251 // envelope names the arithmetic and the method but never the SOLVER.
8252 std::printf("%s arith=%s method=%s nodes=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
8253 r.actualmethod.c_str(), I, suffix.c_str());
8254 line::reg::Json p = line::reg::Json::object();
8255 p["type"] = "AvgNodeTable";
8256 p["indexBase"] = 0;
8257 for (const char* key : {"Node", "JobClass", "QLen", "Util", "RespT", "ResidT", "ArvR",
8258 "Tput"})
8259 p[key] = line::reg::Json::array();
8260 for (std::size_t i = 0; i < I; ++i)
8261 for (std::size_t c = 0; c < R; ++c) {
8262 // The reference's own row filter: a node a class never reaches
8263 // is absent, not a row of zeros, exactly as in the AvgTable.
8264 if (d(QNn(i, c)) == 0.0 && d(UNn(i, c)) == 0.0 && d(RNn(i, c)) == 0.0 &&
8265 d(WNn(i, c)) == 0.0 && d(ANn(i, c)) == 0.0 && d(TNn(i, c)) == 0.0)
8266 continue;
8267 p["Node"].push_back(sn.nodes[i].name);
8268 p["JobClass"].push_back(sn.classes[c].name);
8269 p["QLen"].push_back(d(QNn(i, c)));
8270 p["Util"].push_back(d(UNn(i, c)));
8271 p["RespT"].push_back(d(RNn(i, c)));
8272 p["ResidT"].push_back(d(WNn(i, c)));
8273 p["ArvR"].push_back(d(ANn(i, c)));
8274 p["Tput"].push_back(d(TNn(i, c)));
8275 }
8276 for (std::size_t f = 0; f < F; ++f)
8277 for (std::size_t c = 0; c < R; ++c) {
8278 if (region_empty(f, c)) continue;
8279 p["Node"].push_back(region_name(f));
8280 p["JobClass"].push_back(sn.classes[c].name);
8281 p["QLen"].push_back(d(r.QN(sn.nstations + f, c)));
8282 p["Util"].push_back(region_nan);
8283 p["RespT"].push_back(d(r.RN(sn.nstations + f, c)));
8284 p["ResidT"].push_back(d(r.WN(sn.nstations + f, c)));
8285 p["ArvR"].push_back(region_nan);
8286 p["Tput"].push_back(d(r.TN(sn.nstations + f, c)));
8287 }
8288 emit_analysis<T>("node", p, r.actualmethod);
8289 return 0;
8290 }
8291 std::printf("%s arith=%s method=%s nodes=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
8292 r.actualmethod.c_str(), I, suffix.c_str());
8293 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s\n", "Node", "JobClass", "QLen", "Util",
8294 "RespT", "ResidT", "ArvR", "Tput");
8295 for (std::size_t i = 0; i < I; ++i)
8296 for (std::size_t c = 0; c < R; ++c) {
8297 if (d(QNn(i, c)) == 0.0 && d(UNn(i, c)) == 0.0 && d(RNn(i, c)) == 0.0 &&
8298 d(WNn(i, c)) == 0.0 && d(ANn(i, c)) == 0.0 && d(TNn(i, c)) == 0.0)
8299 continue;
8300 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
8301 sn.nodes[i].name.c_str(), sn.classes[c].name.c_str(), d(QNn(i, c)),
8302 d(UNn(i, c)), d(RNn(i, c)), d(WNn(i, c)), d(ANn(i, c)), d(TNn(i, c)));
8303 }
8304 for (std::size_t f = 0; f < F; ++f)
8305 for (std::size_t c = 0; c < R; ++c) {
8306 if (region_empty(f, c)) continue;
8307 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
8308 region_name(f).c_str(), sn.classes[c].name.c_str(),
8309 d(r.QN(sn.nstations + f, c)), region_nan, d(r.RN(sn.nstations + f, c)),
8310 d(r.WN(sn.nstations + f, c)), region_nan, d(r.TN(sn.nstations + f, c)));
8311 }
8312 return 0;
8313}
8314
8315/**
8316 * `-a cache`: `@@NetworkSolver/getAvgCacheTable`, per Cache node and READ class.
8317 *
8318 * ONE TOTAL ROW PER (node, read class), plus one row per cache list where the
8319 * solver reported a per-list breakdown and the cache has more than one list.
8320 * The `List` column tells them apart: 0 is the total over every list, l is
8321 * list l. Only the total row carries the delayed-hit and miss columns, because
8322 * a miss is a property of the cache and not of any one list.
8323 *
8324 * THE ArvR COLUMN IS THE RETRIEVAL FLOW, `arvr * (missprob + delayedprob)`, on
8325 * the total row and the raw read rate on a list row. That is the reference's
8326 * choice and it is Little-consistent with ResidT: the residence time reported
8327 * beside it is the expected retrieval latency, which only the requests that
8328 * actually retrieve wait for.
8329 *
8330 * A READ CLASS IS ONE WITH A HIT CLASS DEFINED. A class that never reads the
8331 * cache has no row at all rather than a row of zeros, which is the same rule
8332 * the AvgTable applies to a class that never visits a station.
8333 */
8334template <class T>
8335int solve_model_cache(const std::string& file, const Knobs& k, const std::string& s) {
8336 line::qn::Network<T> net = read_model<T>(file);
8338 std::string banner, suffix;
8339 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8340 if (r.cache.empty())
8342 "-a cache reports the per-Cache hit and miss table and this model has no Cache node, "
8343 "or the solver that ran analyzes none; SolverNC's cache branches are what fill it");
8344
8345 // The read-class arrival rate is that class's SOURCE throughput: every read
8346 // request enters the cache, so this holds across solvers, including the
8347 // simulators where a delayed hit is not folded into the hit throughput.
8348 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
8349 std::size_t srcnode = 0;
8350 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
8351 if (sn.nodes[i].nodetype == line::lang::NodeType::Source) { srcnode = i + 1; break; }
8352
8353 struct Row {
8354 std::string node, cls;
8355 double list, listcap, items, hitp, dhitp, missp, hitr, dhitr, missr, arvr, residt, cost;
8356 };
8357 std::vector<Row> rows;
8358 const double dnan = std::numeric_limits<double>::quiet_NaN();
8359
8360 for (std::size_t c = 0; c < r.cache.caches.size(); ++c) {
8361 const line::solvers::CacheNodeMetrics<T>& m = r.cache.caches[c];
8362 const typename std::map<std::size_t, line::qn::CacheParam<T> >::const_iterator it =
8363 sn.nodeparam.find(m.node);
8364 if (it == sn.nodeparam.end()) continue;
8365 const std::vector<std::size_t>& hitclass = it->second.hitclass;
8366 const std::size_t h = m.itemcap.size();
8367 double totcap = 0.0;
8368 for (std::size_t l = 0; l < h; ++l) totcap += m.itemcap[l];
8369 double totcost = dnan;
8370 if (!m.listcost.empty()) {
8371 totcost = 0.0;
8372 for (std::size_t l = 0; l < m.listcost.size(); ++l)
8374 }
8375
8376 for (std::size_t cl = 0; cl < sn.nclasses; ++cl) {
8377 if (cl >= hitclass.size() || hitclass[cl] == 0) continue; // not a read class
8378 double ph = cache_at(m.hitprob, cl), pm = cache_at(m.missprob, cl),
8379 pd = cache_at(m.delayedprob, cl);
8380 if (std::isnan(ph) && std::isnan(pm) && std::isnan(pd)) continue;
8381 if (std::isnan(ph)) ph = 0.0;
8382 if (std::isnan(pm)) pm = 0.0;
8383 if (std::isnan(pd)) pd = 0.0;
8384 const double arvr =
8385 srcnode ? line::num_traits<T>::to_double(nm.TN(srcnode - 1, cl)) : 0.0;
8386 const double lat = cache_at(m.latency, cl);
8387
8388 Row t;
8389 t.node = sn.nodes[m.node - 1].name;
8390 t.cls = sn.classes[cl].name;
8391 t.list = 0;
8392 t.listcap = totcap;
8393 t.items = static_cast<double>(m.nitems);
8394 t.hitp = ph;
8395 t.dhitp = pd;
8396 t.missp = pm;
8397 t.hitr = arvr * ph;
8398 t.dhitr = arvr * pd;
8399 t.missr = arvr * pm;
8400 t.arvr = arvr * (pm + pd);
8401 t.residt = lat;
8402 t.cost = totcost;
8403 rows.push_back(t);
8404
8405 // Per-list rows, only where a genuine multi-list breakdown exists.
8406 bool any = false;
8407 if (h > 1 && cl < m.hitproblist.rows())
8408 for (std::size_t l = 0; l < m.hitproblist.cols(); ++l)
8409 if (!std::isnan(line::num_traits<T>::to_double(m.hitproblist(cl, l))))
8410 any = true;
8411 if (!any) continue;
8412 for (std::size_t l = 0; l < h; ++l) {
8413 double phl = l < m.hitproblist.cols()
8415 : dnan;
8416 if (std::isnan(phl)) phl = 0.0;
8417 Row u;
8418 u.node = t.node;
8419 u.cls = t.cls;
8420 u.list = static_cast<double>(l + 1);
8421 u.listcap = m.itemcap[l];
8422 u.items = t.items;
8423 u.hitp = phl;
8424 u.dhitp = dnan;
8425 u.missp = dnan;
8426 u.hitr = arvr * phl;
8427 u.dhitr = dnan;
8428 u.missr = dnan;
8429 u.arvr = arvr;
8430 u.residt = dnan;
8431 u.cost = l < m.listcost.size()
8433 : dnan;
8434 rows.push_back(u);
8435 }
8436 }
8437 }
8438
8439 if (g_json_output) {
8440 line::reg::Json p = line::reg::Json::object();
8441 p["type"] = "AvgCacheTable";
8442 p["indexBase"] = 0;
8443 for (const char* key : {"Node", "JobClass", "List", "ListCap", "Items", "HitProb",
8444 "DelayedHitProb", "MissProb", "HitRate", "DelayedHitRate",
8445 "MissRate", "ArvR", "ResidT", "ListCost"})
8446 p[key] = line::reg::Json::array();
8447 for (std::size_t i = 0; i < rows.size(); ++i) {
8448 p["Node"].push_back(rows[i].node);
8449 p["JobClass"].push_back(rows[i].cls);
8450 p["List"].push_back(rows[i].list);
8451 p["ListCap"].push_back(rows[i].listcap);
8452 p["Items"].push_back(rows[i].items);
8453 p["HitProb"].push_back(rows[i].hitp);
8454 p["DelayedHitProb"].push_back(rows[i].dhitp);
8455 p["MissProb"].push_back(rows[i].missp);
8456 p["HitRate"].push_back(rows[i].hitr);
8457 p["DelayedHitRate"].push_back(rows[i].dhitr);
8458 p["MissRate"].push_back(rows[i].missr);
8459 p["ArvR"].push_back(rows[i].arvr);
8460 p["ResidT"].push_back(rows[i].residt);
8461 p["ListCost"].push_back(rows[i].cost);
8462 }
8463 emit_analysis<T>("cache", p, r.actualmethod);
8464 return 0;
8465 }
8466 std::printf("%s arith=%s method=%s caches=%zu\n", banner.c_str(), line::num_traits<T>::name(),
8467 r.actualmethod.c_str(), r.cache.caches.size());
8468 std::printf("%-14s %-12s %5s %8s %6s %10s %10s %10s %10s %10s %10s %10s %10s %10s\n", "Node",
8469 "JobClass", "List", "ListCap", "Items", "HitProb", "DHitProb", "MissProb",
8470 "HitRate", "DHitRate", "MissRate", "ArvR", "ResidT", "ListCost");
8471 for (std::size_t i = 0; i < rows.size(); ++i)
8472 std::printf(
8473 "%-14s %-12s %5g %8g %6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g "
8474 "%10.6g\n",
8475 rows[i].node.c_str(), rows[i].cls.c_str(), rows[i].list, rows[i].listcap,
8476 rows[i].items, rows[i].hitp, rows[i].dhitp, rows[i].missp, rows[i].hitr,
8477 rows[i].dhitr, rows[i].missr, rows[i].arvr, rows[i].residt, rows[i].cost);
8478 return 0;
8479}
8480
8481/**
8482 * `-a item`: `@@NetworkSolver/getAvgItemTable`, one row per (Cache, item, list).
8483 *
8484 * THE PER-ITEM OCCUPANCY, which only a solver that computes a genuine per-item
8485 * distribution has: the exact NC cache recursions and the delayed-hit retrieval
8486 * algorithms. Every other branch measures the aggregate hit probability and
8487 * never forms the item law, and the arm refuses rather than filling the column
8488 * with the uniform guess that would reproduce the same aggregate.
8489 *
8490 * `Cost` is `Size * Prob`, so summing it over the items of a list reproduces
8491 * that list's ListCost in the cache table -- which is what makes the two tables
8492 * checkable against each other.
8493 */
8494template <class T>
8495int solve_model_item(const std::string& file, const Knobs& k, const std::string& s) {
8496 line::qn::Network<T> net = read_model<T>(file);
8498 std::string banner, suffix;
8499 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8500 if (r.cache.empty())
8502 "-a item reports the per-item cache occupancy and this model has no Cache node, or "
8503 "the solver that ran analyzes none");
8504
8505 const double dnan = std::numeric_limits<double>::quiet_NaN();
8506 struct Row {
8507 std::string node;
8508 double item, list, listcap, size, prob, cost, dhq, dhqf;
8509 };
8510 std::vector<Row> rows;
8511 for (std::size_t c = 0; c < r.cache.caches.size(); ++c) {
8512 const line::solvers::CacheNodeMetrics<T>& m = r.cache.caches[c];
8513 const std::size_t h = m.itemcap.size();
8514 // EITHER measurement earns the item its rows. A solver may form the
8515 // per-item occupancy (the NC and MVA cache recursions) or the per-item
8516 // delayed-hit queue length (the exact chain) and not the other, and
8517 // requiring both would drop the CTMC's whole table.
8518 if (h == 0 || (m.itemprob.rows() == 0 && m.delayedhitqlen.empty())) continue;
8519 const std::size_t nit =
8520 m.itemprob.rows() > 0 ? m.itemprob.rows() : m.delayedhitqlen.size();
8521 for (std::size_t i = 0; i < nit; ++i)
8522 for (std::size_t l = 0; l < h; ++l) {
8523 Row t;
8524 t.node = sn.nodes[m.node - 1].name;
8525 t.item = static_cast<double>(i + 1);
8526 t.list = static_cast<double>(l + 1);
8527 t.listcap = m.itemcap[l];
8528 t.size = i < m.itemsize.size() ? m.itemsize[i] : dnan;
8529 // Column 0 of `itemprob` is the MISS column, so list l is column
8530 // l+1; reading it as l would report every item one list too low.
8531 t.prob = (l + 1) < m.itemprob.cols()
8533 : dnan;
8534 t.cost = t.size * t.prob;
8535 t.dhq = i < m.delayedhitqlen.size()
8537 : dnan;
8538 t.dhqf = i < m.delayedhitqlenfull.size()
8540 : dnan;
8541 rows.push_back(t);
8542 }
8543 }
8544 if (rows.empty())
8546 "-a item needs a per-item occupancy law and this solve produced none; the NC/MVA "
8547 "cache recursions (isolated and integrated alike) and the delayed-hit retrieval "
8548 "algorithms compute the embedded one, SolverCTMC the time-weighted one, and the "
8549 "simulators none");
8550
8551 if (g_json_output) {
8552 line::reg::Json p = line::reg::Json::object();
8553 p["type"] = "AvgItemTable";
8554 p["indexBase"] = 0;
8555 for (const char* key : {"Node", "Item", "List", "ListCap", "Size", "Prob", "Cost",
8556 "DelayedHitQLen", "DelayedHitQLenFull"})
8557 p[key] = line::reg::Json::array();
8558 for (std::size_t i = 0; i < rows.size(); ++i) {
8559 p["Node"].push_back(rows[i].node);
8560 p["Item"].push_back(rows[i].item);
8561 p["List"].push_back(rows[i].list);
8562 p["ListCap"].push_back(rows[i].listcap);
8563 p["Size"].push_back(rows[i].size);
8564 p["Prob"].push_back(rows[i].prob);
8565 p["Cost"].push_back(rows[i].cost);
8566 p["DelayedHitQLen"].push_back(rows[i].dhq);
8567 p["DelayedHitQLenFull"].push_back(rows[i].dhqf);
8568 }
8569 emit_analysis<T>("item", p, r.actualmethod);
8570 return 0;
8571 }
8572 std::printf("%s arith=%s method=%s rows=%zu\n", banner.c_str(), line::num_traits<T>::name(),
8573 r.actualmethod.c_str(), rows.size());
8574 std::printf("%-14s %6s %6s %8s %10s %12s %12s %14s %18s\n", "Node", "Item", "List", "ListCap",
8575 "Size", "Prob", "Cost", "DelayedHitQLen", "DelayedHitQLenFull");
8576 for (std::size_t i = 0; i < rows.size(); ++i)
8577 std::printf("%-14s %6g %6g %8g %10g %12.8g %12.8g %14.8g %18.8g\n", rows[i].node.c_str(),
8578 rows[i].item, rows[i].list, rows[i].listcap, rows[i].size, rows[i].prob,
8579 rows[i].cost, rows[i].dhq, rows[i].dhqf);
8580 return 0;
8581}
8582
8583/**
8584 * `-a sys`: `@@NetworkSolver/getAvgSysTable`, one row per CHAIN.
8585 *
8586 * SysRespT is the chain's CYCLE TIME and SysTput the flow that completes it,
8587 * both measured at the chain's reference station -- not a column of the
8588 * AvgTable summed up. On a closed chain the two are tied by Little's law and
8589 * the table is the standard capacity-planning view: N = X * R.
8590 */
8591template <class T>
8592int solve_model_sys(const std::string& file, const Knobs& k, const std::string& s) {
8593 line::qn::Network<T> net = read_model<T>(file);
8595 std::string banner, suffix;
8596 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8598 const std::vector<std::string> cn = line::solvers::chain_names(sn.nchains);
8599 const std::vector<std::string> cc = line::solvers::chain_class_labels<T>(sn);
8600 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
8601
8602 if (g_json_output) {
8603 // The banner under `-o json` too, for print_chain_table's reason: the
8604 // envelope names the arithmetic and the method but never the SOLVER.
8605 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
8606 r.actualmethod.c_str(), cn.size(), suffix.c_str());
8607 line::reg::Json p = line::reg::Json::object();
8608 p["type"] = "AvgSysTable";
8609 p["indexBase"] = 0;
8610 for (const char* key : {"Chain", "JobClasses", "SysRespT", "SysTput"})
8611 p[key] = line::reg::Json::array();
8612 for (std::size_t c = 0; c < sn.nchains; ++c) {
8613 p["Chain"].push_back(cn[c]);
8614 p["JobClasses"].push_back(cc[c]);
8615 p["SysRespT"].push_back(d(sys.CN[c]));
8616 p["SysTput"].push_back(d(sys.XN[c]));
8617 }
8618 emit_analysis<T>("sys", p, r.actualmethod);
8619 return 0;
8620 }
8621 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner.c_str(),
8622 line::num_traits<T>::name(), r.actualmethod.c_str(), sn.nchains, suffix.c_str());
8623 std::printf("%-10s %-24s %14s %14s\n", "Chain", "JobClasses", "SysRespT", "SysTput");
8624 for (std::size_t c = 0; c < sn.nchains; ++c)
8625 std::printf("%-10s %-24s %14.6g %14.6g\n", cn[c].c_str(), cc[c].c_str(), d(sys.CN[c]),
8626 d(sys.XN[c]));
8627 return 0;
8628}
8629
8630/** Render a station- or node-level chain table, as text or as the host's JSON. */
8631template <class T>
8632void print_chain_table(const char* key, const char* type, const char* rowlabel,
8633 const std::vector<std::string>& rows,
8634 const std::vector<std::string>& chains,
8635 const std::vector<std::string>& classes,
8636 const line::solvers::ChainResult<T>& t, const std::string& method,
8637 const char* banner, const char* arith, const char* suffix = "") {
8638 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
8639 if (g_json_output) {
8640 // THE BANNER IS PRINTED UNDER `-o json` TOO, as the AvgTable path does:
8641 // the envelope names the arithmetic and the method but never the
8642 // SOLVER, so a host that pairs a table with another codebase's by the
8643 // solver in its banner cannot attribute a bannerless one at all. Its
8644 // absence here made `-o json` unusable for these four analyses.
8645 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner, arith, method.c_str(),
8646 chains.size(), suffix);
8647 line::reg::Json p = line::reg::Json::object();
8648 p["type"] = type;
8649 p["indexBase"] = 0;
8650 for (const char* c : {rowlabel, "Chain", "JobClasses", "QLen", "Util", "RespT", "ResidT",
8651 "ArvR", "Tput"})
8652 p[c] = line::reg::Json::array();
8653 // ROW-MAJOR OVER (row, chain), the reference's `(ist-1)*C+c` ordering,
8654 // so a host reading the two codebases' tables side by side indexes them
8655 // the same way.
8656 for (std::size_t i = 0; i < rows.size(); ++i)
8657 for (std::size_t c = 0; c < chains.size(); ++c) {
8658 p[rowlabel].push_back(rows[i]);
8659 p["Chain"].push_back(chains[c]);
8660 p["JobClasses"].push_back(classes[c]);
8661 p["QLen"].push_back(d(t.QN(i, c)));
8662 p["Util"].push_back(d(t.UN(i, c)));
8663 p["RespT"].push_back(d(t.RN(i, c)));
8664 p["ResidT"].push_back(d(t.WN(i, c)));
8665 p["ArvR"].push_back(d(t.AN(i, c)));
8666 p["Tput"].push_back(d(t.TN(i, c)));
8667 }
8668 emit_analysis<T>(key, p, method);
8669 return;
8670 }
8671 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner, arith, method.c_str(),
8672 chains.size(), suffix);
8673 std::printf("%-16s %-10s %-20s %12s %12s %12s %12s %12s %12s\n", rowlabel, "Chain",
8674 "JobClasses", "QLen", "Util", "RespT", "ResidT", "ArvR", "Tput");
8675 for (std::size_t i = 0; i < rows.size(); ++i)
8676 for (std::size_t c = 0; c < chains.size(); ++c)
8677 std::printf("%-16s %-10s %-20s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
8678 rows[i].c_str(), chains[c].c_str(), classes[c].c_str(), d(t.QN(i, c)),
8679 d(t.UN(i, c)), d(t.RN(i, c)), d(t.WN(i, c)), d(t.AN(i, c)),
8680 d(t.TN(i, c)));
8681}
8682
8683/**
8684 * `-a chain`: `@@NetworkSolver/getAvgChainTable`, the station table by CHAIN.
8685 *
8686 * EVERY ROW IS EMITTED, including the all-zero ones, unlike the AvgTable and the
8687 * AvgNodeTable. The reference builds this table with a full (M x C) grid and no
8688 * row filter, and a chain that is absent from a station is information -- it is
8689 * the shape of the routing -- where a class absent from a station in the
8690 * AvgTable is only the class's own scope.
8691 */
8692template <class T>
8693int solve_model_chain(const std::string& file, const Knobs& k, const std::string& s) {
8694 line::qn::Network<T> net = read_model<T>(file);
8696 std::string banner, suffix;
8697 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8699 std::vector<std::string> rows;
8700 for (std::size_t i = 0; i < sn.nstations; ++i) rows.push_back(sn.stations[i].name);
8701 print_chain_table<T>("chain", "AvgChainTable", "Station", rows,
8704 banner.c_str(), line::num_traits<T>::name(), suffix.c_str());
8705 return 0;
8706}
8707
8708/** `-a nodechain`: `@@NetworkSolver/getAvgNodeChainTable`, the node table by CHAIN. */
8709template <class T>
8710int solve_model_nodechain(const std::string& file, const Knobs& k, const std::string& s) {
8711 line::qn::Network<T> net = read_model<T>(file);
8713 std::string banner, suffix;
8714 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
8715 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
8717 line::solvers::solver_get_avg_node_chain<T>(sn, nm.QN, nm.UN, nm.RN, nm.WN, nm.AN, nm.TN);
8718 std::vector<std::string> rows;
8719 for (std::size_t i = 0; i < sn.nodes.size(); ++i) rows.push_back(sn.nodes[i].name);
8720 print_chain_table<T>("nodechain", "AvgNodeChainTable", "Node", rows,
8723 banner.c_str(), line::num_traits<T>::name(), suffix.c_str());
8724 return 0;
8725}
8726
8727/** The four @@NetworkSolver tables that are VIEWS of one solved AvgResult. */
8728inline bool is_avg_view(const std::string& analysis) {
8729 return analysis == "node" || analysis == "sys" || analysis == "chain" ||
8730 analysis == "nodechain";
8731}
8732
8733/**
8734 * Dispatch one of those four views at a fixed arithmetic.
8735 *
8736 * The wrapper arms reach the views through this rather than through the shared
8737 * ladder further down: they return before it, having validated their own knobs
8738 * against an engine the ladder knows nothing about. The arithmetic is fixed
8739 * because both wrappers report in double and have already refused every other
8740 * backend by name.
8741 */
8742template <class T>
8743int solve_avg_view(const std::string& file, const Knobs& k, const std::string& s,
8744 const std::string& analysis) {
8745 if (analysis == "node") return solve_model_node<T>(file, k, s);
8746 if (analysis == "sys") return solve_model_sys<T>(file, k, s);
8747 if (analysis == "chain") return solve_model_chain<T>(file, k, s);
8748 return solve_model_nodechain<T>(file, k, s);
8749}
8750
8751int solve_model_dispatch(const std::string& arith, const std::string& solver,
8752 const std::string& analysis, const std::string& file, const Knobs& k) {
8753 std::string s = solver.empty() ? "auto" : solver;
8754 if (s != "mva" && s != "auto" && s != "fluid" && s != "fld" && s != "nc" && s != "mam" &&
8755 s != "ag" && s != "ba" && s != "ssa" && s != "ctmc" && s != "uq" && s != "env" &&
8756 s != "lqns" && s != "ldes" && s != "jmt")
8758 "the model-solving path ports -s mva, nc, ctmc, mam, ag, ba, ssa, fluid, ldes, jmt, "
8759 "uq, env and lqns (got '" + s + "'); other solvers remain API-only");
8760 // Every solver on this path but JMT and LQNS runs in-process, so the flags
8761 // that describe an external solver's child process have nothing to act on.
8762 // Those two do run one, and both write a scratch directory --keep names:
8763 // `solve_model_jmt` forwards it to JmtOptions.keep, which is what leaves
8764 // model.jsim behind, and refusing it here made the one document a parity
8765 // difference has to be read from unobtainable.
8766 if ((k.verbose && s != "ldes") || k.remote || !k.remote_url.empty() ||
8767 (k.keep && s != "lqns" && s != "jmt"))
8769 "--keep, --verbose, --remote and --remote-url describe the child process of an "
8770 "external solver; on this path -s jmt and -s lqns run one and take --keep, -s ldes "
8771 "runs one and takes --verbose (which echoes the resolved engine command line), and "
8772 "no path takes --remote or --remote-url");
8773 if (k.timeout_seconds && s != "lqns")
8775 "--timeout is the deadline of an external solver's child process; on this path only "
8776 "-s lqns runs one");
8777 // --fork-join names an arm of the fork-join FIXED POINT, which only the
8778 // mean-value arms drive: a simulator walks the fork on its sample path and
8779 // a CTMC enumerates it, so neither has a transform to choose. Refused by
8780 // name rather than ignored, which would report the default arm's numbers
8781 // under the caller's choice.
8782 if (!k.fork_join.empty() && s != "mva" && s != "nc")
8784 "--fork-join selects the fork-join transform of the shared mean-value fixed point "
8785 "and is read by -s mva and -s nc; -s " + s +
8786 " either simulates or enumerates the fork and applies no transform");
8787 // REFUSED BY NAME RATHER THAN IGNORED, on the same grounds as --fork-join
8788 // above: a knob silently dropped reports the DEFAULT arm's numbers under
8789 // the caller's choice, which is the one outcome stating the flag exists to
8790 // rule out.
8791 if (k.warmupfrac >= 0.0 && s != "ssa")
8793 "--warmupfrac discards a leading fraction of a SIMULATED path before the means are "
8794 "taken and is read by -s ssa; -s " + s +
8795 " has no path to discard (the LDES engine takes --ldes-warmupfrac)");
8796 if (k.pstar > 0.0 && s != "fluid" && s != "fld")
8798 "--pstar is the exponent of the fluid p-norm smoothing of the drift and is read by "
8799 "-s fluid; -s " + s + " integrates no drift");
8800 if ((!k.busy_orders.empty() || !k.busy_subnet.empty()) && analysis != "busyperiod")
8802 "--busyperiod and --busyperiod-subnet name the orders and the subnetwork of "
8803 "-a busyperiod; got -a " + analysis);
8804 // Solver console: this dispatcher is the single point every model-solving
8805 // arm passes through, so the narrated run is opened here and closed by the
8806 // guard's destructor -- on an exception too, so a failed analysis still
8807 // reports what it had reached. The model name is not known before the file
8808 // is read, so the header names the file's model once the struct compiles.
8809 line::util::LineConsole::Run consoleRun(upper_tag(s), "", true);
8810
8811 // ---- SolverUQ, BEFORE the per-solver knob ladder ----------------------
8812 // It is a wrapper, not an engine: `--method`, `--samples` and `--seed`
8813 // describe its DESIGN and the convergence knobs belong to whatever
8814 // `--uq-solver` names, so the ladder below -- which asks "does THIS solver
8815 // have a sample count" -- answers about the wrong solver here.
8816 if (s == "uq") {
8817 if (analysis != "avg" && analysis != "posterior" && analysis != "interval")
8819 "SolverUQ ports -a avg (the prior-weighted expectation), -a posterior (the "
8820 "per-design-point table) and -a interval (the support-only range); got '" +
8821 analysis + "'");
8822 if (k.uq_solver.empty())
8823 throw line::InputError(
8824 "-s uq needs --uq-solver: UQ computes nothing itself, it expands the Prior and "
8825 "runs another solver at each design point (the C++ spelling of UQ(model, "
8826 "@SolverMVA)). Naming one here by default would attribute the numbers to an "
8827 "engine the caller never chose");
8828 if (k.t1 >= 0.0 || k.node || !k.notation.empty())
8830 "--tspan, --node and --notation name a transient horizon, a stateful node and an "
8831 "ODE document; SolverUQ reports steady-state means over a design of models and "
8832 "has none of the three");
8833 if (k.no_interlocking || k.interlock_knobs_given() || k.repeat > 0 || !k.layer_solver.empty())
8835 "--no-interlocking, --interlock-*, --repeat and --layer-solver are options of the layered solver "
8836 "and apply to -i lqnx; a Network model has no layers to interlock");
8837 if (arith == "double") return solve_model_uq<double>(file, k, analysis);
8838 if (arith == "exact") return solve_model_uq<line::Rational>(file, k, analysis);
8839 if (arith == "real:16") return solve_model_uq<line::Real<16> >(file, k, analysis);
8840 if (arith == "real" || arith == "real:32")
8841 return solve_model_uq<line::Real<32> >(file, k, analysis);
8842 if (arith == "real:64") return solve_model_uq<line::Real<64> >(file, k, analysis);
8843 if (arith == "real:128") return solve_model_uq<line::Real<128> >(file, k, analysis);
8844 if (arith == "real:256") return solve_model_uq<line::Real<256> >(file, k, analysis);
8845 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8846 }
8847 // ---- SolverENV, also BEFORE the per-solver knob ladder ----------------
8848 // It reads a DIFFERENT MODEL TYPE: an Environment envelope, whose stages
8849 // each hold a Network. The ladder below asks its questions of one Network
8850 // model -- `--cutoff` per open class, `--node` on a stateful node -- and
8851 // the model-level knobs it would validate belong to the STAGE solver here,
8852 // so ENV states its own refusals and routes around it.
8853 if (s == "env") {
8854 if (analysis != "avg")
8856 "SolverENV reports -a avg, the environment-blended means; getEnsembleAvg is its "
8857 "only metric entry in the reference too (got '" + analysis + "')");
8858 if (k.samples || k.seed)
8860 "--samples and --seed describe a simulation; SolverENV iterates a fixed point over "
8861 "transient stage solves and draws nothing");
8862 // `--cutoff` IS ADMITTED WITH `--stage-solver ctmc`, and only then: every
8863 // stage is then enumerated, and an open stage's chain has to be
8864 // truncated somewhere. It stays refused for a fluid ensemble, which
8865 // enumerates nothing.
8866 if (k.has_cutoff() && k.stage_solver != "ctmc")
8868 "--cutoff bounds the open population of an enumerated state space and applies to "
8869 "-s env only beside --stage-solver ctmc; the fluid stages of this ensemble "
8870 "enumerate no states");
8871 if (k.node || !k.notation.empty())
8873 "--node and --notation name a stateful node and an ODE document of ONE network; "
8874 "an Environment holds a network per stage and "
8875 "SolverENV reports the blend over them");
8876 if (k.no_interlocking || k.interlock_knobs_given() || k.repeat > 0 || !k.layer_solver.empty())
8878 "--no-interlocking, --interlock-*, --repeat and --layer-solver are options of the layered solver "
8879 "and apply to -i lqnx; an Environment has stages, not layers");
8880 if (k.t0 != 0.0)
8882 "--tspan on the ENV path states the transient HORIZON each stage solve integrates "
8883 "to, and every stage starts from its entry state at 0; a nonzero t0 would name a "
8884 "start the coupling has no state for");
8885 // The mean-field coupling integrates the stage drift with LSODA, which
8886 // is double; the state-vector one uniformizes a CTMC and carries the
8887 // whole ladder. Narrowing silently would report an `exact` banner over
8888 // a double solve, so the refusal names the coupling that decided it.
8889 // `blend` is a spelling of `default` since 2026-09-13 (it named the
8890 // state-vector coupling before that), so it normalizes to `meanfield`
8891 // here and every gate below reads it as such without repeating it.
8892 const std::string coupling =
8893 (k.method.empty() || k.method == "default" || k.method == "mean"
8894 || k.method == "blend" || k.method == "blending")
8895 ? "meanfield"
8896 : k.method;
8897 if (k.tran_points && coupling == "statevec")
8899 "--tran-points is the mean-field coupling's quadrature grid; the state-vector "
8900 "coupling carries the whole joint law across a switch and sums over no such grid, "
8901 "so the value would be accepted and never used");
8902 // `statedep` is a C++-API method and not a file one: it needs a rate
8903 // function PER ARC (`Environment::set_env_rate_reset`), which is a
8904 // function of the stage exit metrics and has no representation in
8905 // model.json -- the reference cannot serialize `resetEnvRatesFun`
8906 // either. Reaching it from a file would find no hook and refuse deeper
8907 // in, with a message about an environment the caller never wrote.
8908 if (coupling == "statedep")
8910 "--method statedep makes each environment transition depend on the state its "
8911 "stage is left in, through a rate function per arc that no model.json can carry "
8912 "(the reference cannot serialize resetEnvRatesFun either); it is reachable from "
8913 "the C++ API, through Environment::set_env_rate_reset");
8914 if ((k.tran_points || k.t1 >= 0.0) && (coupling == "avg" || coupling == "dec"))
8916 "--tran-points and --tspan state the grid and the horizon of a TRANSIENT stage "
8917 "solve; the closed-form limits --method avg and --method dec solve in steady "
8918 "state and carry nothing across a switch, so both would be accepted and never "
8919 "used");
8920 if (arith != "double" && coupling != "statevec")
8922 "SolverENV solves a stage with the fluid analyzer on every method but the "
8923 "state-vector one -- the mean-field coupling transiently, the avg and dec limits "
8924 "in steady state -- and that analyzer is LSODA's and therefore double; --arith " +
8925 arith +
8926 " reaches ENV only through the state-vector coupling (--method statevec)");
8927 if (arith == "double") return solve_model_env<double>(file, k);
8928 if (arith == "exact") return solve_model_env<line::Rational>(file, k);
8929 if (arith == "real:16") return solve_model_env<line::Real<16> >(file, k);
8930 if (arith == "real" || arith == "real:32")
8931 return solve_model_env<line::Real<32> >(file, k);
8932 if (arith == "real:64") return solve_model_env<line::Real<64> >(file, k);
8933 if (arith == "real:128") return solve_model_env<line::Real<128> >(file, k);
8934 if (arith == "real:256") return solve_model_env<line::Real<256> >(file, k);
8935 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8936 }
8937 if (!k.uq_solver.empty())
8939 "--uq-solver names the engine SolverUQ runs at each design point and applies to -s uq; "
8940 "'" + s + "' solves one model, not a design of them");
8941 if (k.tran_points)
8943 "--tran-points is the resolution of the transient grid SolverENV sums its stage exit "
8944 "metrics over and applies to -s env; '" + s + "' has no such quadrature");
8945 // ---- SolverLDES, BEFORE the per-solver knob ladder --------------------
8946 // It hands the model DOCUMENT to an external engine instead of reading it,
8947 // so the ladder below -- which validates knobs against a parsed struct --
8948 // asks its questions of a model this path never parses. LDES states its own
8949 // refusals here for the same reason UQ and ENV state theirs.
8950 const bool ldes_knob = !k.ldes_tranfilter.empty() || k.ldes_warmupfrac >= 0.0 ||
8951 !k.ldes_cimethod.empty() || k.ldes_cnvgon || k.ldes_slotted ||
8952 k.ldes_replications > 0 || k.ldes_numthreads > 0 ||
8953 k.ldes_maxtime > 0.0 || !k.ldes_initsol.empty() ||
8954 !k.ldes_rest_url.empty();
8955 if (ldes_knob && s != "ldes" && s != "auto")
8957 "the --ldes-* flags are the discrete-event engine's own settings (warmup filter, "
8958 "confidence-interval estimator, slot lattice, replications, warm-start placement) and "
8959 "apply to -s ldes; '" + s + "' has none of them");
8960 if (k.jmt_replications > 0 && s != "jmt")
8962 "--jmt-replications is SolverJMT's transient ensemble size (-s jmt -a tran / -a "
8963 "tranprob); '" + s + "' has none (-s ldes takes --ldes-replications)");
8964 if (s == "jmt") {
8965 if (arith != "double")
8967 "SolverJMT is a client of the Java Modelling Tools engine, which simulates in "
8968 "double and reports in double; --arith " + arith +
8969 " would label a double answer with an arithmetic that never touched it");
8970 // THE LIST AND `auto_family_metrics("jmt")` ARE ONE ANSWER, and they
8971 // had drifted: the family table advertised tran, tranprob and sample
8972 // while this gate refused all three, so a caller following
8973 // `--find-solver` was refused by name. `jmt_logs.h` implements every
8974 // one of them (`jmt_transient_replications`, `jmt_get_tran_prob_aggr`,
8975 // `jmt_sample_aggr`, `jmt_sample_sys_aggr`); what was missing was the
8976 // way in.
8977 if (analysis != "avg" && analysis != "cdf" && analysis != "trancdf" &&
8978 analysis != "trancdfpasst" && analysis != "prob" && analysis != "tran" &&
8979 analysis != "tranprob" && analysis != "sample" && !is_avg_view(analysis))
8981 "-s jmt reports -a avg (the JSIM or JMVA mean table), its four views -a node, "
8982 "-a sys, -a chain and -a nodechain, -a cdf (the empirical response-time law read "
8983 "back from the JMT logs, preloaded at the rounded steady-state queue lengths), "
8984 "-a tran-cdf-respt and -a tran-cdf-passt (the same logged run from the default "
8985 "initial state, so the samples cover the transient), -a prob (the time each "
8986 "declared state is held for along the logged trajectory), -a tran (the transient "
8987 "means over --jmt-replications independent replications), -a tranprob (the same "
8988 "replications' aggregate state law at one station) and -a sample (one logged "
8989 "trajectory); the DETAILED laws -a states and getTranProb have no counterpart, "
8990 "since a JMT log records per-class job counts and nothing about the buffer order "
8991 "or the service phase");
8992 {
8993 const std::string removed = line::jmt::jmt_removed_method_refusal(k.method);
8994 if (!removed.empty()) throw line::UnsupportedError(removed);
8995 }
8996 const std::vector<std::string> valid = line::jmt::jmt_list_valid_methods();
8997 if (!k.method.empty() &&
8998 std::find(valid.begin(), valid.end(), k.method) == valid.end())
9000 "SolverJMT methods are default, jsim and the jmva family (jmva, jmva.amva, "
9001 "jmva.mva, jmva.recal, jmva.comom, jmva.chow, jmva.bs, jmva.aql, jmva.lin, "
9002 "jmva.dmlin); got '" + k.method + "'");
9003 // `--iter_max` MEANS NOTHING TO JMT. It used to be the replication count of `-a tran` and
9004 // `-a tranprob`; that count is now `options.config.replications`, spelt --jmt-replications.
9005 const bool jmt_replicated = analysis == "tran" || analysis == "tranprob";
9006 if (k.iter_max >= 0)
9008 "--iter_max is not a SolverJMT option: the number of independent replications -a "
9009 "tran and -a tranprob average over is --jmt-replications (options.config."
9010 "replications, default 10)");
9011 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || !k.multiserver.empty())
9013 "--tol, --iter_tol and --multiserver configure a fixed-point iteration; JSIM "
9014 "simulates a sample path and JMVA takes its tolerance from the exported document. "
9015 "The simulation's stopping rule is --samples");
9016 if (k.jmt_replications > 0 && !jmt_replicated)
9018 "--jmt-replications sizes the transient ensemble of -a tran and -a tranprob; "
9019 "steady-state JSIM (-a " + analysis + ") is one run and reads no replication count");
9020 if (k.has_cutoff())
9022 "--cutoff truncates an enumerated state space; JMT enumerates none");
9023 // `--node` NAMES ONE STATION, on the three arms that answer about one.
9024 // On `-a prob` it says which station `--state` overrides, the second
9025 // argument of `getProbAggr(node, state_a)`; on `-a tranprob` it is the
9026 // station whose aggregate law is estimated; on `-a sample` it is the
9027 // node whose own trajectory is returned instead of the system's. Every
9028 // other JMT arm reports a table over all stations and classes.
9029 const bool jmt_node_arm =
9030 analysis == "prob" || analysis == "tranprob" || analysis == "sample";
9031 if ((k.node && !jmt_node_arm) || k.jobclass || !k.marg_states.empty())
9033 "--node, --class and --marg-states select the marginal law of one (node, class); "
9034 "the JMT arms report tables over every station and class, and --node applies to "
9035 "-a prob (which station --state overrides), -a tranprob (whose state law) and "
9036 "-a sample (whose trajectory)");
9037 if (!k.state.empty() && analysis != "prob")
9039 "--state names the state a probability is asked about and applies to -a prob");
9040 if (!k.state.empty() && !k.node)
9042 "--state is the per-class job count of ONE station and needs --node to say "
9043 "which; a bare count vector cannot be matched against a whole network");
9044 if (!k.notation.empty() || !k.symbolic.empty() || k.equilibria)
9046 "--notation, --symbolic and --equilibria describe an exported ODE document; JMT "
9047 "integrates no ODE");
9048 if (!k.cdf_algorithm.empty())
9050 "--cdf-algorithm selects between the two sojourn-time INVERSIONS of -s nc; the "
9051 "JMT response-time law is the ecdf of the passages its loggers recorded and is "
9052 "not computed from a transform");
9053 if (is_avg_view(analysis)) return solve_avg_view<double>(file, k, "jmt", analysis);
9054 return solve_model_jmt(file, k, analysis);
9055 }
9056 if (s == "ldes") {
9057 if (arith != "double")
9059 "SolverLDES is a client of the SSJ engine, which simulates in double and reports "
9060 "in double; --arith " + arith +
9061 " would label a double answer with an arithmetic that never touched it");
9062 // 'parallel' asks the engine for INDEPENDENT REPLICATIONS and the mean
9063 // over them, which is what its parallel analyzer is; it is not a second
9064 // engine. It resolves to a replication count here, taking --ldes-
9065 // replications when given and 8 otherwise -- the same default the SSA
9066 // parallel analyzer uses. Mirrors SolverLDES.listValidMethods in every
9067 // codebase, which advertises exactly {default, para, parallel}, 'para'
9068 // being the short spelling of 'parallel' as in SolverSSA.
9069 if (!k.method.empty() && k.method != "default" && k.method != "para" && k.method != "parallel")
9071 "SolverLDES has the methods 'default' and 'parallel' (alias 'para'; listValidMethods returns "
9072 "exactly those in every codebase); got '" + k.method + "'");
9073
9074 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0 || !k.multiserver.empty())
9076 "--tol, --iter_tol, --iter_max and --multiserver are the knobs of a fixed-point "
9077 "iteration; LDES simulates a sample path and iterates nothing. Its stopping rule "
9078 "is --samples, or --ldes-cnvgon with --ldes-cnvgtol");
9079 if (k.has_cutoff())
9081 "--cutoff truncates an enumerated state space; a simulator visits the states the "
9082 "sample path reaches and enumerates none");
9083 if (k.jobclass || !k.marg_states.empty() || (k.node && analysis != "prob"))
9085 "--class and --marg-states select a marginal law of one (node, class), and --node "
9086 "applies to -a prob beside --state; the other LDES arms report tables over every "
9087 "station and class");
9088 if (!k.state.empty() && (analysis != "prob" || !k.node))
9090 "--state is getProbAggr(node, state)'s per-class job count of ONE station: it "
9091 "applies to -s ldes -a prob and needs --node to say which");
9092 if (!k.notation.empty() || !k.symbolic.empty() || k.equilibria)
9094 "--notation, --symbolic and --equilibria describe an exported ODE document; LDES "
9095 "integrates no ODE");
9096 if (!k.cdf_algorithm.empty())
9098 "--cdf-algorithm selects between the two sojourn-time INVERSIONS of -s nc; the "
9099 "LDES response-time law is the ecdf of the samples the engine recorded and is not "
9100 "computed from a transform");
9101 if (k.no_interlocking || k.interlock_knobs_given() || k.repeat > 0 || !k.layer_solver.empty())
9103 "--no-interlocking, --interlock-*, --repeat and --layer-solver are options of the layered solver "
9104 "and apply to -i lqnx; the LDES layered path runs in the JAR's own ensemble "
9105 "backend and has no JSON interface to reach from here");
9106 if (!k.sens_method.empty() || !k.sens_scheme.empty() || k.sens_step >= 0.0)
9108 "--sens-method, --sens-scheme and --sens-step configure the layered sensitivity "
9109 "table; LDES reports no sensitivity");
9110 if (analysis == "tran" && !(k.t1 >= 0.0))
9111 throw line::InputError(
9112 "-s ldes -a tran needs --tspan <t1> or --tspan <t0>:<t1>: a trajectory over an unstated "
9113 "horizon is not a quantity, and the engine only records buckets once a timespan "
9114 "makes the run transient");
9115 if (k.t1 >= 0.0 && analysis != "tran")
9117 "--tspan names the horizon of -a tran; -a sample runs over [0, --samples], which "
9118 "is the horizon runTransientJson uses, and the other arms are steady state");
9119 // `-a tran-cdf-*` NAMES THE TRANSIENT LAW AND IS THE STEADY-STATE ONE
9120 // HERE, because a simulator has only the samples it observed: the
9121 // reference's `getTranCdfRespT` reads the same `respTimeSamples` its
9122 // `getCdfRespT` reads, and its `getTranCdfPassT` is a one-line delegation
9123 // to `getTranCdfRespT`. Warned rather than refused, so a script written
9124 // against the JAR runs and its author is told what the curve is.
9125 if ((analysis == "trancdf" || analysis == "trancdfpasst") &&
9126 k.verbosity != "silent")
9127 std::fprintf(stderr,
9128 "Warning: -a %s is the ecdf of the per-job response times the run "
9129 "observed, the same curve -a cdf reports; the reference's LDES "
9130 "getTranCdfRespT reads the same samples\n",
9131 analysis.c_str());
9132 // method='parallel' resolved to its replication count, after every
9133 // other knob has been validated against the caller's own Knobs.
9134 Knobs kldes = k;
9135 if ((kldes.method == "parallel" || kldes.method == "para") && kldes.ldes_replications <= 1)
9136 kldes.ldes_replications = 8;
9137 if (is_avg_view(analysis)) return solve_avg_view<double>(file, kldes, "ldes", analysis);
9138 return solve_model_ldes(file, kldes, analysis);
9139 }
9140 // SolverAUTO resolves to a real engine BEFORE the knob checks below, so a
9141 // model the chooser sends to SSA accepts --samples and one it sends to CTMC
9142 // accepts --cutoff: the checks must see the solver that will actually run.
9143 if (s == "auto") {
9144 // `chooseSolverHeur` picks an engine from a GETTER, and the age laws are
9145 // not one of its getters: a model whose table it would send to MVA does
9146 // not thereby have an AoI answer. Refusing by name here beats letting
9147 // the chosen engine refuse an analysis it was never asked about.
9148 if (analysis == "aoi")
9150 "-a aoi is the AoI branch of the fluid 'mfq' method and no other engine reports "
9151 "it, so SolverAUTO does not choose for it: ask for it by name with -s fluid");
9152 const AutoPlan plan = choose_auto_plan_dispatch(arith, file, analysis, k.method);
9153 // `delegate.m` runs the chosen solver and, when it fails, every other
9154 // feasible candidate in slot order. Reproduced here by re-entering this
9155 // dispatch with a CONCRETE method name, so each attempt is validated against
9156 // the knobs of the solver that will actually run it. Only the first
9157 // attempt carries the method the ranking gated on ('exact'): a method
9158 // name is a solver's own vocabulary and does not travel to the next.
9159 if (plan.order.size() == 1) {
9160 // A forced method name (a method family, or the Environment envelope)
9161 // leaves ONE solver, and its own diagnostic is then the whole
9162 // story: the reference rethrows it rather than wrapping it.
9163 Knobs kk = k;
9164 kk.method = plan.method;
9165 std::printf("SolverAUTO selected %s%s\n", plan.order[0].c_str(), plan.note.c_str());
9166 return solve_model_dispatch(arith, plan.order[0], analysis, file, kk);
9167 }
9168 std::string first_error;
9169 for (std::size_t i = 0; i < plan.order.size(); ++i) {
9170 Knobs kk = k;
9171 kk.method = (i == 0) ? plan.method : std::string();
9172 if (i == 0)
9173 std::printf("SolverAUTO selected %s%s\n", plan.order[i].c_str(),
9174 plan.note.c_str());
9175 else
9176 std::printf("SolverAUTO retrying with %s\n", plan.order[i].c_str());
9177 try {
9178 return solve_model_dispatch(arith, plan.order[i], analysis, file, kk);
9179 } catch (const line::UnsupportedError& e) {
9180 if (first_error.empty()) first_error = plan.order[i] + ": " + e.what();
9181 std::printf("SolverAUTO: %s cannot serve this run (%s)\n", plan.order[i].c_str(),
9182 e.what());
9183 }
9184 }
9186 "SolverAUTO: every candidate refused this run. The chosen engine reported -- " +
9187 first_error);
9188 }
9189 // ---- knobs the CHOSEN solver does not have are refused, not dropped ----
9190 // Accepting an option and discarding it is the one place the port would
9191 // answer a question it was not asked: the caller believes a value the
9192 // solver never saw. Every refusal below names the option and the solver.
9193 const bool is_sim = (s == "ssa");
9194 // `-s ctmc -a sample` walks the chain with an exponential clock, so it has a
9195 // run length and a stream in the same sense a simulation does; every other
9196 // CTMC analysis is a solve and still refuses both.
9197 // The NC cftp methods draw iid stationary states, so they too have a run
9198 // length and a stream, the NC `options.samples` / `options.seed`. They are
9199 // named here rather than through is_stochastic_method, which classifies
9200 // them as SolverCTMC did and is deliberately unchanged by their move.
9201 const bool is_cftp =
9202 (s == "nc" && (k.method == "cftp" || k.method == "cftp.approx"));
9203 // NC IS NOT ONE SOLVER HERE: its Monte Carlo integrators, logistic sampler,
9204 // importance-sampling estimators and Chen-O'Cinneide MCMC all read a run
9205 // length and a stream, which is why solve_model_nc wires k.samples and
9206 // k.seed into NcSolverOptions and says so in its own doc comment. This
9207 // guard contradicted that file for every NC method, so a caller naming one
9208 // of those methods was told 'nc' has no sample count -- the refusal that
9209 // made the statepr_sys_aggr_large [M2C] parity row skip, its MATLAB source
9210 // being NC(model,'seed',23000,'method','ls','samples',10000).
9211 // is_stochastic_method is the port of SolverNC.isStochasticMethod and
9212 // tokenizes on '.' and '/', so 'nc.ls' and 'default/imci' classify alike.
9213 // A method nobody named stays refused: 'default' RESOLVES to a stochastic
9214 // method only at runtime, and accepting a knob on the strength of what a
9215 // name might become is the same discard this block exists to prevent.
9216 const bool nc_draws = (s == "nc" && line::nc::is_stochastic_method(k.method));
9217 const bool draws_samples =
9218 is_sim || (s == "ctmc" && analysis == "sample") || is_cftp || nc_draws;
9219 if (k.samples && !draws_samples)
9220 throw line::UnsupportedError("--samples applies to the simulation solver (-s ssa), to "
9221 "-s ctmc -a sample and to the stochastic -s nc methods "
9222 "(mci, imci, ls, is, sampling, mcmc, cftp, cftp.approx); '" +
9223 s + "' has no sample count");
9224 if (k.seed && !draws_samples)
9225 throw line::UnsupportedError("--seed applies to the simulation solver (-s ssa), to "
9226 "-s ctmc -a sample and to the stochastic -s nc methods "
9227 "(mci, imci, ls, is, sampling, mcmc, cftp, cftp.approx); '" +
9228 s + "' draws no random numbers");
9229 // --mdd-tol / --mdd-maxiter drive the level iteration, which only the mdd
9230 // method runs. Accepting them elsewhere would let a caller believe a
9231 // tolerance was applied to a solve that has no iteration in it.
9232 if ((k.mdd_tol > 0.0 || k.mdd_maxiter > 0) && !(s == "ctmc" && k.method == "mdd"))
9234 "--mdd-tol and --mdd-maxiter set the coupled level iteration of -s ctmc --method mdd; "
9235 "'" + s + " / " + (k.method.empty() ? std::string("default") : k.method) +
9236 "' iterates no levels");
9237 // --tspan names a transient horizon, which only the CTMC transient analyses
9238 // have; accepting it elsewhere would let a caller believe a horizon was used.
9239 // The fluid solver integrates a forward equation too, and its horizon is what
9240 // `kp` reports its covariance AT, so --tspan reaches it as well; every other
9241 // fluid method restarts from its own end state until the moved mass stops
9242 // changing, and a horizon there caps that iteration rather than naming a time.
9243 if (k.t1 >= 0.0 &&
9244 !(s == "ctmc" &&
9245 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")) &&
9246 !(s == "fluid" || s == "fld") && !(s == "mam" && analysis == "tran"))
9248 "--tspan sets the horizon of a transient analysis and applies to -s ctmc -a tran, "
9249 "-a tranprob and -a tranreward, to -s mam -a tran and to -s fluid; '" + s + " / " +
9250 analysis + "' integrates no forward equation");
9251 // --node narrows an answer to one node's block of the state, which only the
9252 // per-node CTMC queries and the per-node MAM queue-length law have; every
9253 // other analysis reports the whole network.
9254 // `-s ctmc -a prob` takes it TOGETHER WITH --state and only then: the arm
9255 // reports every station's marginal, so a bare --node would narrow nothing,
9256 // while --state names a row of ONE node's own space and needs --node to say
9257 // whose.
9258 if (k.node && !(s == "ctmc" && (analysis == "tranprob" || analysis == "sample")) &&
9259 !(s == "ctmc" && analysis == "prob" && !k.state.empty()) &&
9260 !(s == "nc" && analysis == "prob" && !k.state.empty()) &&
9261 !(s == "ssa" && analysis == "sample") && !(s == "mam" && analysis == "prob") &&
9262 !((s == "mva" || s == "nc") && analysis == "marg"))
9264 "--node selects the stateful node a state query is labelled by and applies to -s ctmc "
9265 "-a tranprob and -a sample, to -s ctmc|nc -a prob beside --state, to -s ssa -a sample, "
9266 "to -s mam -a prob and to -s mva|nc -a marg; '" + s + " / " + analysis +
9267 "' reports the whole network");
9268 // --class and --marg-states are the remaining two arguments of getProbMarg,
9269 // and nothing else in the surface takes either: every other analysis reports
9270 // all classes, and no other law is evaluated at a caller-chosen job count.
9271 if ((k.jobclass || !k.marg_states.empty()) && !(s == "mva" && analysis == "marg"))
9273 "--class and --marg-states are the job class and the state list of getProbMarg and "
9274 "apply to -s mva -a marg; '" + s + " / " + analysis +
9275 "' reports every class over its own range");
9276 // --notation selects which document the ODE export writes, and only the
9277 // export writes one; every other analysis reports numbers, which have no
9278 // notation to choose.
9279 if (!k.notation.empty() && !((s == "fluid" || s == "fld") && analysis == "odes"))
9281 "--notation selects the form of the exported ODE document and applies to -s fluid -a "
9282 "odes; '" + s + " / " + analysis + "' exports no equations");
9283 // --symbolic and --equilibria select the computer-algebra backend and ask it
9284 // to solve f(x) = 0; only the Jacobian consults one.
9285 if ((!k.symbolic.empty() || k.equilibria) &&
9286 !((s == "fluid" || s == "fld") && analysis == "jacobian"))
9288 "--symbolic selects the computer-algebra backend and --equilibria asks it for the "
9289 "solutions of f(x) = 0; both apply to -s fluid -a jacobian, and '" + s + " / " +
9290 analysis + "' consults no backend");
9291 // ---- the five JAR knobs, each refused where it would be accepted and never
9292 // read. Same discipline as every knob above: a caller who passed one to an
9293 // arm that does not consult it would believe a setting had been applied.
9294 // `-s jmt -a prob` reads it too, and reads it DIFFERENTLY: an exact chain is
9295 // indexed by the ENCODED row of a node's own state space, while a JMT log
9296 // records per-class job counts and nothing else, so there the vector is
9297 // `getProbAggr(node, state_a)`'s per-class count. Both are "the state this
9298 // probability is about"; which encoding it is in follows the solver.
9299 if (!k.state.empty() &&
9300 !(analysis == "prob" && (s == "ctmc" || s == "auto" || s == "jmt" || s == "nc")))
9302 "--state names the state `getProb(node, state)` asks about and applies to -a prob "
9303 "under -s ctmc, -s nc and -s jmt, the arms whose answer is indexed by a state; '" + s +
9304 " / " + analysis + "' reports a mean or a law over all of them");
9305 if (k.events && analysis != "sample")
9307 "--events is the length of ONE sampled trajectory and applies to -a sample; use "
9308 "--samples for a solver's run length ('" + s + " / " + analysis + "')");
9309 if (!k.percentiles.empty() && !(s == "mam" && (analysis == "cdf" || analysis == "cdfpasst" ||
9310 analysis == "perct")))
9312 "--percentiles names the levels getPerctRespT is read at and applies to -s mam -a "
9313 "perct-respt (and to the percentiles printed beside -a cdf); '" + s + " / " +
9314 analysis + "' inverts no response-time law");
9315 if (!k.reward_name.empty() && analysis != "rewardvalue")
9317 "--reward-name selects which declared reward -a reward-value returns the value "
9318 "function of; -a reward returns every reward's steady-state expectation and needs no "
9319 "name ('" + s + " / " + analysis + "')");
9320 if (k.timestep > 0.0 &&
9321 !(s == "ctmc" &&
9322 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")))
9324 "--timestep is the fixed output grid of a transient CTMC solve, `options.timestep` of "
9325 "ctmc_transient.m, and applies to -s ctmc -a tran, -a tranprob and -a tranreward; the "
9326 "fluid "
9327 "and simulated transients report the points their own integrator or engine produced "
9328 "('" + s + " / " + analysis + "')");
9329 if ((!k.rate_sched.empty() || k.ctmc_tv_ngrid > 0) && !(s == "ctmc" && analysis == "tran"))
9331 "--rate-sched (and --ctmc-tv-ngrid) makes the CTMC generator time-inhomogeneous, "
9332 "`options.config.rate_sched` of solver_ctmc_transient_analyzer.m, and applies to "
9333 "-s ctmc -a tran only ('" + s + " / " + analysis + "')");
9334 if ((!k.transient_method.empty() || k.fau_epsilon > 0.0 || k.fau_delta >= 0.0) &&
9335 !(s == "ctmc" &&
9336 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")))
9338 "--transient-method (and --fau-epsilon / --fau-delta) selects how the CTMC forward "
9339 "equation is advanced, `options.config.transient_method` of "
9340 "solver_ctmc_transient_analyzer.m, and applies to -s ctmc -a tran, -a tranprob and "
9341 "-a tranreward; every other analysis solves no forward equation ('" +
9342 s + " / " + analysis + "')");
9343 // --cdf-algorithm selects how the sojourn law is inverted, which only the NC
9344 // response-time distribution does; the CTMC one is read off tagged chains
9345 // and has no such choice.
9346 if (!k.cdf_algorithm.empty() && !(s == "nc" && analysis == "cdf"))
9348 "--cdf-algorithm selects the sojourn-time inversion of the NC response-time "
9349 "distribution and applies to -s nc -a cdf; '" + s + " / " + analysis +
9350 "' inverts no generating function");
9351 // The passage flags name the two state sets of getCdfFirstPassT and of
9352 // getFirstPassTMoments, which are the only two arms that time a state-set
9353 // passage. --passage-method selects the inversion and so belongs to the
9354 // curve alone; the moments involve no inversion at all.
9355 const bool passage_arm =
9356 (s == "ctmc" && (analysis == "firstpasst" || analysis == "firstpasstmom"));
9357 if ((!k.passage_from.empty() || !k.passage_into.empty() || k.passage_orders > 0) &&
9358 !passage_arm)
9360 "--passage-from, --passage-into and --passage-orders name the state sets and the "
9361 "moment order of -s ctmc -a firstpasst / firstpasstmom; '" + s + " / " + analysis +
9362 "' times no state-set passage");
9363 if (!k.passage_method.empty() && !(s == "ctmc" && analysis == "firstpasst"))
9365 "--passage-method selects the transform inversion of -s ctmc -a firstpasst; '" + s +
9366 " / " + analysis + "' inverts none (the moments arm solves for them directly)");
9367 // --perm-engine selects the permanent estimator, which only the NC joint law
9368 // of the per-station totals uses; nothing else in the tree evaluates one.
9369 if (k.method_perm != "exact" && !(s == "nc" && analysis == "sysmarg"))
9371 "--perm-engine selects the permanent estimator of the NC joint total-queue-length "
9372 "law and applies to -s nc -a sysmarg; '" + s + " / " + analysis +
9373 "' evaluates no permanent");
9374 // The layered path's own knobs, refused here for the same reason every other
9375 // knob above is: a caller who passed one to a Network solve would believe a
9376 // setting was applied that no Network solver has. --sens-* is the exception:
9377 // getSensitivityTable is a @@NetworkSolver method, so it selects the branch
9378 // of `-s nc -a sens` as much as of the layered one.
9379 const bool nc_sens = s == "nc" && analysis == "sens";
9380 if (!nc_sens && (!k.sens_method.empty() || !k.sens_scheme.empty() || k.sens_step > 0.0))
9382 "--sens-method, --sens-scheme and --sens-step select the branch of a sensitivity "
9383 "table and apply to -i lqnx -a sens or to -s nc -a sens; '" + s + " / " + analysis +
9384 "' differentiates nothing");
9385 if (k.no_interlocking || k.interlock_knobs_given() || k.repeat > 0 || !k.layer_solver.empty() ||
9386 !k.ln_transient.empty() || !k.ln_transient_channels.empty())
9388 "--no-interlocking, --interlock-*, --repeat, --layer-solver and --ln-transient* are options "
9389 "of the layered solver and apply to -i lqnx; a Network model has no layers to "
9390 "interlock");
9391 if (s == "ba" && (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0))
9393 "--tol, --iter_tol and --iter_max do not apply to -s ba: a bound is a closed form, "
9394 "with nothing to converge");
9395 // Silent acceptance is the defect these guard against: a caller who passed
9396 // a QRF table to another solver would believe a parameterisation was
9397 // applied that nothing read.
9398 if (s != "ba" && (!k.qrf_params.empty() || !k.qrf_alpha.empty()))
9400 "--qrf-params and --qrf-alpha parameterise the QRF reduction bounds and apply to "
9401 "-s ba; '" + s + "' solves no reduction program");
9402 if (s != "ba" && k.level > 0)
9404 "--level is the hierarchy level of the SolverBA bound families and applies to -s ba; "
9405 "'" + s + "' has no bound hierarchy");
9406 if (is_sim && (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0))
9408 "--tol, --iter_tol and --iter_max do not apply to -s ssa: a sample path is not an "
9409 "iteration; use --samples to set its length");
9410 if (s == "mam" && k.iter_tol >= 0.0)
9412 "--iter_tol is not a SolverMAM option (MamOptions carries tol and iter_max); "
9413 "use --tol");
9414 if (s == "ag" && k.iter_tol >= 0.0)
9416 "--iter_tol is not a SolverAG option (AgOptions carries tol and iter_max, the "
9417 "tolerance and the sweep budget of the reversed-rate fixed point); use --tol");
9418 // --max-states BOUNDS AN OPEN AGENT'S QUEUE-LENGTH DIMENSION, and only the
9419 // RCAT agents have one: a closed class is bounded by its own population
9420 // instead, and 'inapinf' ignores the level entirely and solves the open
9421 // agents on the infinite state space. Accepting it elsewhere would be the
9422 // silent-acceptance defect these guards exist for -- a caller who passed it
9423 // to -s ctmc would believe a truncation applied that nothing truncated.
9424 if (k.max_states >= 0 && s != "ag")
9426 "--max-states truncates the queue-length dimension of a SolverAG agent and applies "
9427 "to -s ag; '" + s + "' truncates no agent (use --cutoff for a CTMC state space)");
9428 // The cutoff BOUNDS A STATE SPACE, and only SolverCTMC and the MAM
9429 // queue-length law have one -- the latter because an OPEN queue's level
9430 // process is unbounded and `getProb` has to stop somewhere. Accepting it
9431 // elsewhere would be the silent-acceptance defect: a caller who passed it to
9432 // -s mva would believe the answer was truncated when nothing truncated it.
9433 if (k.has_cutoff() && s != "ctmc" && !(s == "mam" && analysis == "prob") && s != "env")
9435 "--cutoff bounds the open population of a CTMC state space, and the level truncation "
9436 "of -s mam -a prob; '" + s + " / " + analysis + "' enumerates no states");
9437 // THE MATRIX SPELLING IS NARROWER THAN THE SCALAR ONE. Only the CTMC state
9438 // space is enumerated per station, so only it can honour a per-station
9439 // bound; the MAM level truncation and an environment's stage cutoff are one
9440 // number each. Refused rather than reduced to a maximum, because that
9441 // silently answers a LARGER chain than the caller asked for.
9442 if (!k.cutoff_mat.empty() && s != "ctmc")
9444 "--cutoff as a per-(station,class) matrix bounds an enumerated state space per "
9445 "station and applies to -s ctmc; '" + s + "' takes one number");
9446 // `--stage-solver` names the solver each STAGE of an environment is run
9447 // with, and nothing else has stages: a layer of an LQN takes
9448 // `--layer-solver`, which is a different set for a different reason (a
9449 // layer is solved in steady state, a stage transiently).
9450 if (!k.stage_solver.empty() && s != "env")
9452 "--stage-solver names the solver each stage of a random environment is run with and "
9453 "applies to -s env; '" + s + "' has no stages");
9454 if (!k.stage_solver.empty() && k.stage_solver != "fluid" && k.stage_solver != "ctmc" &&
9455 k.stage_solver != "mam")
9457 "--stage-solver '" + k.stage_solver +
9458 "' is not available: the environment coupling needs a TRANSIENT stage solve, and only "
9459 "the fluid analyzer, the enumerated CTMC and the flattened LD-QBD provide one in this "
9460 "port");
9461 // The three `--map-env` knobs. Refused beside `-s ba`, whose bounds the
9462 // environment image does not bracket, and beside `-s env`, which IS the
9463 // environment solver and has a model with stages already.
9464 if (!k.map_env.empty() && k.map_env != "default" && k.map_env != "off")
9465 throw line::UnsupportedError("--map-env '" + k.map_env +
9466 "' is not a value: use 'default' or 'off'");
9467 if (!k.map_env_method.empty() && k.map_env_method != "auto" && k.map_env_method != "dec" &&
9468 k.map_env_method != "avg" && k.map_env_method != "meanfield")
9470 "--map-env-method '" + k.map_env_method +
9471 "' is not an environment recombination: use 'auto', 'meanfield', 'dec' or 'avg'");
9472 if ((!k.map_env.empty() || !k.map_env_method.empty() || k.map_env_maxstages) &&
9473 (s == "ba" || s == "env"))
9475 "--map-env* asks a solver to fall back on a random-environment image of a non-renewal "
9476 "process; '-s " + s +
9477 "' takes no such fallback (a bound must not be computed on an approximation, and the "
9478 "environment solver already takes a model with stages)");
9479 // `mam` IS THE STATE-VECTOR COUPLING'S BACKEND ONLY. The mean-field one
9480 // carries marginal means and reads them off a transient mean the LD-QBD
9481 // reduction does not produce; accepting it there would run the CTMC
9482 // ensemble under the MAM name.
9483 if (k.stage_solver == "mam" && k.method != "statevec")
9485 "--stage-solver mam applies to -s env --method statevec: the LD-QBD backend flattens "
9486 "its blocks into a generator the state-vector coupling propagates a distribution "
9487 "across, and the mean-field coupling carries marginal MEANS instead");
9488 // The two FJ_codes knobs configure ONE analyzer, solver_mam_fj, which the
9489 // MAM dispatch reaches on a homogeneous fork-join model. Accepting them
9490 // anywhere else would let a caller believe an accuracy setting had been
9491 // honoured by a solver that never read it.
9492 if ((k.fj_accuracy > 0 || !k.fj_tmode.empty()) && s != "mam")
9494 "--fj-accuracy and --fj-tmode configure the FJ_codes fork-join approximation of "
9495 "solver_mam_fj.m and apply to -s mam; '" + s + "' does not run it");
9496 // --timescale gates the slotted branch of the MAM dispatch alone. SolverNC
9497 // has its own discrete product form and takes --slotted for it, so a
9498 // caller that names a time scale for any other solver is told rather than
9499 // silently answered on the continuous one.
9500 if (!k.timescale.empty() && s != "mam")
9502 "--timescale selects the time scale of the MAM discrete-time path and applies to "
9503 "-s mam; '" + s + "' does not read it (SolverNC takes --slotted)");
9504 // ---- `-a node`, ahead of the per-solver whitelists ---------------------
9505 // getAvgNodeTable is @@NetworkSolver's, not any one solver's: it is the
9506 // station table scattered to the node index space plus the two flow columns
9507 // recomputed from it, so every solver that produces an AvgResult can answer
9508 // it and none of them needs its own arm.
9509 // `-a node`, `-a sys`, `-a chain` and `-a nodechain` are the four
9510 // @@NetworkSolver tables that are VIEWS of one solved AvgResult -- scattered
9511 // to nodes, aggregated to chains, or reduced to the reference station -- so
9512 // they share the engine whitelist and the arithmetic ladder. Adding a
9513 // per-arm copy of either would let the four drift on which solvers and
9514 // which arithmetics they accept, for tables built from the same numbers.
9515 // `-s ssa` and `-s fluid` return their own solution types rather than an
9516 // AvgResult; `run_avg_engine` bridges them (`avg_result_from_sim`), so the
9517 // reference's rule holds here too -- a solver that reports an AvgTable
9518 // reports its four views. The two external wrappers obey the same rule and
9519 // are dispatched in their own arms above, which run before this one: they
9520 // validate knobs the ladder here knows nothing about, and LDES is handed the
9521 // model DOCUMENT rather than a parsed struct.
9522 if (analysis == "node" || analysis == "sys" || analysis == "chain" ||
9523 analysis == "nodechain" || analysis == "cache" || analysis == "item") {
9524 const bool is_sim_engine = (s == "ssa" || s == "fluid");
9525 // The two cache tables are read off the SAME solved result, so they
9526 // belong to the same group; SolverNC is the only engine here whose
9527 // branches fill `AvgResult::cache`, and the arms say so when it is
9528 // empty rather than being whitelisted to nc alone -- `-s auto` on a
9529 // cache model resolves to nc, and refusing the token would refuse the
9530 // model.
9531 //
9532 // BOTH SIMULATORS ARE ADMITTED TO `-a cache`, and only there.
9533 // `run_avg_engine` fills `r.cache` for `-s ssa` from
9534 // `cache_metrics_of_ssa` -- the realized hit, delayed-hit and miss
9535 // SHARES the sample path measured -- and for `-s fluid` from the
9536 // cacheqn decomposition's converged split, which is the same quantity
9537 // its refreshed struct is renormalized at. Both are how
9538 // `SSA(model).getAvgCacheTable()` and `Fluid(model).getAvgCacheTable()`
9539 // answer in the reference, which reads them off the node the analyzer
9540 // wrote. `-a item` stays refused for both: the per-item occupancy is a
9541 // recursion of the
9542 // NC/MVA cache branches and no simulator forms it, so admitting it
9543 // would report an empty table for a quantity that was never measured.
9544 const bool cache_table = (analysis == "cache" || analysis == "item");
9545 const bool sim_cache_ok = (analysis == "cache");
9546 // `-a cache` and `-a item` are the CACHE tables, which RCAT does not
9547 // form -- so `ag` joins the AvgResult views and not those two.
9548 const bool ag_view = (s == "ag" && !cache_table);
9549 if ((s != "mva" && s != "auto" && s != "nc" && s != "mam" && s != "ba" && s != "ctmc" &&
9550 !ag_view && !is_sim_engine) ||
9551 (cache_table && is_sim_engine && !sim_cache_ok))
9553 // `-a node`'s own wording is kept verbatim: it is the message a
9554 // caller has been reading since the arm existed, and the group
9555 // it now shares does not change what it says.
9556 (analysis == "node"
9557 ? std::string("-a node reports the per-node table of -s mva, nc, mam, ag, ba, "
9558 "ctmc, ssa, fluid, jmt, ldes and auto; '")
9559 : "-a " + analysis +
9560 " is a view of the station AvgResult and is reported by -s mva, nc, "
9561 "mam, " + (analysis == "item" ? "" : "ag, ") + "ba, ctmc" +
9562 (analysis == "item" ? "" : ", ssa, fluid, jmt, ldes") + " and auto; '") +
9563 s + "' does not return the station AvgResult it is built from");
9564 // The same refusal their own `-a avg` arms raise, for the same reason:
9565 // an SSA sample path is generated from exponential clocks and a fluid
9566 // trajectory is integrated by LSODA, so neither is carried by an exact
9567 // or an extended-precision backend. Refused BY NAME here rather than
9568 // narrowed silently in the ladder below.
9569 if (is_sim_engine && arith != "double")
9571 "-a " + analysis + " under -s " + s +
9572 " is read off a " + (s == "ssa" ? "sample path" : "fluid trajectory") +
9573 ", which is transcendental; rerun with --arith double (got '" + arith + "')");
9574 const std::string eng = (s == "auto") ? std::string("mva") : s;
9575#define LINE_CLI_TABLE_LADDER(FN) \
9576 do { \
9577 if (arith == "double") return FN<double>(file, k, eng); \
9578 if (arith == "exact") return FN<line::Rational>(file, k, eng); \
9579 if (arith == "real:16") return FN<line::Real<16> >(file, k, eng); \
9580 if (arith == "real" || arith == "real:32") return FN<line::Real<32> >(file, k, eng); \
9581 if (arith == "real:64") return FN<line::Real<64> >(file, k, eng); \
9582 if (arith == "real:128") return FN<line::Real<128> >(file, k, eng); \
9583 if (arith == "real:256") return FN<line::Real<256> >(file, k, eng); \
9584 } while (0)
9585 if (analysis == "node") LINE_CLI_TABLE_LADDER(solve_model_node);
9586 if (analysis == "sys") LINE_CLI_TABLE_LADDER(solve_model_sys);
9587 if (analysis == "chain") LINE_CLI_TABLE_LADDER(solve_model_chain);
9588 if (analysis == "nodechain") LINE_CLI_TABLE_LADDER(solve_model_nodechain);
9589 if (analysis == "cache") LINE_CLI_TABLE_LADDER(solve_model_cache);
9590 if (analysis == "item") LINE_CLI_TABLE_LADDER(solve_model_item);
9591#undef LINE_CLI_TABLE_LADDER
9592 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9593 }
9594 if (s == "ctmc") {
9595 // The CTMC surface the port reaches: the AvgTable, plus the @@SolverCTMC
9596 // methods that are not means -- the generator and the state space
9597 // (getInfGen, getStateSpace), the transient occupancy (getTranProbSysAggr),
9598 // a marked trajectory (sampleSys), the declared rewards (getAvgReward),
9599 // the response-time laws (getCdfRespT, getCdfSysRespT) and the parametric
9600 // sensitivity (getSensitivityRanking).
9601 if (analysis != "avg" && analysis != "prob" && analysis != "gen" && analysis != "states" &&
9602 analysis != "tran" && analysis != "tranprob" && analysis != "tranreward" &&
9603 analysis != "sample" && analysis != "reward" && analysis != "rewardvalue" &&
9604 analysis != "cdf" && analysis != "sens" && analysis != "firstpasst" &&
9605 analysis != "firstpasstmom")
9607 "the CTMC solver ports -a avg, node, sys, chain, nodechain, prob, gen, states, "
9608 "tran, tranprob, tranreward, sample, reward, reward-value, cdf, first-passt, "
9609 "first-passt-moments and sens (got '" + analysis + "')");
9610 // The generator-free method answers MEANS and nothing else: mdd holds
9611 // the reachable set in a diagram, so there is no state space to list, no
9612 // filtration to split and no trajectory to walk. Refused by name rather
9613 // than served from the enumerated chain, which would report an answer
9614 // under a method that did not produce it.
9615 if (k.method == "mdd" && analysis != "avg")
9617 "the '" + k.method +
9618 "' method never builds the explicit generator, so it serves -a avg only (got '" +
9619 analysis + "'); use --method default for the state-space analyses");
9620 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0)
9622 "--tol, --iter_tol and --iter_max do not apply to -s ctmc: the stationary vector "
9623 "is obtained by a direct solve of pi Q = 0, with nothing to converge. The mdd "
9624 "method's level iteration has --mdd-tol and --mdd-maxiter of its own");
9625 // Every step from the generator to the means is a field operation, so
9626 // there is no arithmetic to refuse: exact returns the exact rational
9627 // stationary law. The transient analyses refuse inside, by name.
9628 if (arith == "double") return solve_model_ctmc<double>(file, k, analysis);
9629 if (arith == "exact") return solve_model_ctmc<line::Rational>(file, k, analysis);
9630 if (arith == "real:16") return solve_model_ctmc<line::Real<16> >(file, k, analysis);
9631 if (arith == "real" || arith == "real:32")
9632 return solve_model_ctmc<line::Real<32> >(file, k, analysis);
9633 if (arith == "real:64") return solve_model_ctmc<line::Real<64> >(file, k, analysis);
9634 if (arith == "real:128") return solve_model_ctmc<line::Real<128> >(file, k, analysis);
9635 if (arith == "real:256") return solve_model_ctmc<line::Real<256> >(file, k, analysis);
9636 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9637 }
9638
9639 // ---- the solvers that carry their own arithmetic restriction -----------
9640 // Each refuses BY NAME rather than being narrowed silently, and the reason
9641 // is the solver's own, not a limitation of the CLI.
9642 if (s == "mam") {
9643 // The @@SolverMAM surface the port reaches: the AvgTable, the
9644 // queue-length law (getProb / getProbMarg), the response-time law
9645 // (getCdfRespT and its getSjrnT / sjrnT aliases, plus getPerctRespT),
9646 // the transient means (getTranAvg) and the M/G/1-type internals of the
9647 // queue (getMAMResult).
9648 if (analysis != "avg" && analysis != "prob" && analysis != "cdf" &&
9649 analysis != "cdfpasst" && analysis != "perct" && analysis != "tran" &&
9650 analysis != "internals")
9652 "the MAM solver ports -a avg, node, prob, cdf, cdf-passt, perct-respt, tran and "
9653 "internals (got '" + analysis + "')");
9654 if (arith != "double")
9656 "the MAM solver fits phase-type representations, whose fitter requires "
9657 "transcendental arithmetic; rerun with --arith double (got '" + arith + "')");
9658 if (analysis == "tran" && k.t1 < 0.0)
9660 "-s mam -a tran integrates the transient queue length over a horizon and there is "
9661 "no default for it; pass --tspan t0 t1");
9662 if (analysis == "prob") return solve_model_mam_prob<double>(file, k);
9663 if (analysis == "cdf") return solve_model_mam_cdf<double>(file, k, "cdf", "CdfRespT");
9664 // `getCdfPassT` IS `getCdfRespT` here, and that is the reference's
9665 // construction rather than an alias invented in the CLI: SolverMAM.java
9666 // computes both from the SAME `solver_mam_passage_time(sn, sn.proc,
9667 // options)` call. It is emitted under its own key so a caller that
9668 // asked the passage-time question is answered it, and the payload's
9669 // `type` records which of the two names produced the curve.
9670 if (analysis == "cdfpasst")
9671 return solve_model_mam_cdf<double>(file, k, "cdfpasst", "CdfPassT");
9672 if (analysis == "perct") return solve_model_mam_perct<double>(file, k);
9673 if (analysis == "tran") return solve_model_mam_tran<double>(file, k);
9674 if (analysis == "internals") return solve_model_mam_internals<double>(file, k);
9675 return solve_model_mam<double>(file, k);
9676 }
9677 if (s == "ssa") {
9678 // `-a cdf` is refused BY NAME rather than falling into the generic
9679 // message, because the refusal is the reference's own answer and not a
9680 // port gap: `@@SolverSSA/getCdfRespT.m` raises the same error, since SSA
9681 // samples state trajectories and not per-job sojourn times.
9682 if (analysis == "cdf") line::ssa::ssa_cdf_respt_refuse();
9683 if (analysis != "avg" && analysis != "prob" && analysis != "sample")
9684 throw line::UnsupportedError("the SSA solver ports -a avg, -a prob and -a sample (got '" +
9685 analysis + "')");
9686 if (arith != "double")
9688 "an SSA sample path is generated from exponential clocks, which are "
9689 "transcendental; rerun with --arith double (got '" + arith + "')");
9690 if (analysis == "prob") return solve_model_ssa_prob<double>(file, k);
9691 if (analysis == "sample") return solve_model_ssa_sample<double>(file, k);
9692 return solve_model_ssa<double>(file, k);
9693 }
9694 if (s == "nc") {
9695 if (analysis != "avg" && analysis != "prob" && analysis != "marg" &&
9696 analysis != "sysmarg" && analysis != "cdf" && analysis != "sens" &&
9697 analysis != "normconst" && analysis != "busyperiod")
9699 "the NC solver ports -a avg, -a node, -a prob, -a marg, -a sysmarg, -a cdf, "
9700 "-a sens, -a normconst and -a busyperiod (got '" + analysis + "')");
9701 // The perfect sampler answers MEANS from iid stationary draws and yields
9702 // no normalizing constant, so none of the other NC analyses has anything
9703 // to be computed from. Refused by name rather than served by a different
9704 // NC route under the caller's method.
9705 if ((k.method == "cftp" || k.method == "cftp.approx") && analysis != "avg")
9707 "the '" + k.method +
9708 "' method draws stationary states and yields no normalizing constant, so it "
9709 "serves -a avg only (got '" + analysis + "'); use another --method for the "
9710 "other NC analyses");
9711 if (analysis == "busyperiod") {
9712 if (arith == "double") return solve_model_nc_busyp<double>(file, k);
9713 if (arith == "exact") return solve_model_nc_busyp<line::Rational>(file, k);
9714 if (arith == "real:16") return solve_model_nc_busyp<line::Real<16> >(file, k);
9715 if (arith == "real" || arith == "real:32")
9716 return solve_model_nc_busyp<line::Real<32> >(file, k);
9717 if (arith == "real:64") return solve_model_nc_busyp<line::Real<64> >(file, k);
9718 if (arith == "real:128") return solve_model_nc_busyp<line::Real<128> >(file, k);
9719 if (arith == "real:256") return solve_model_nc_busyp<line::Real<256> >(file, k);
9720 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9721 }
9722 if (analysis == "sysmarg") {
9723 if (arith == "double") return solve_model_nc_sysmarg<double>(file, k);
9724 if (arith == "exact") return solve_model_nc_sysmarg<line::Rational>(file, k);
9725 if (arith == "real:16") return solve_model_nc_sysmarg<line::Real<16> >(file, k);
9726 if (arith == "real" || arith == "real:32")
9727 return solve_model_nc_sysmarg<line::Real<32> >(file, k);
9728 if (arith == "real:64") return solve_model_nc_sysmarg<line::Real<64> >(file, k);
9729 if (arith == "real:128") return solve_model_nc_sysmarg<line::Real<128> >(file, k);
9730 if (arith == "real:256") return solve_model_nc_sysmarg<line::Real<256> >(file, k);
9731 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9732 }
9733 if (analysis == "marg") {
9734 if (arith == "double") return solve_model_nc_marg<double>(file, k);
9735 if (arith == "exact") return solve_model_nc_marg<line::Rational>(file, k);
9736 if (arith == "real:16") return solve_model_nc_marg<line::Real<16> >(file, k);
9737 if (arith == "real" || arith == "real:32")
9738 return solve_model_nc_marg<line::Real<32> >(file, k);
9739 if (arith == "real:64") return solve_model_nc_marg<line::Real<64> >(file, k);
9740 if (arith == "real:128") return solve_model_nc_marg<line::Real<128> >(file, k);
9741 if (arith == "real:256") return solve_model_nc_marg<line::Real<256> >(file, k);
9742 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9743 }
9744 // Served under NC too so `-s auto -a normconst`, which chooseSolverHeur
9745 // sends to NC on a product-form model, lands on an arm that answers.
9746 if (analysis == "normconst") {
9747 if (arith == "double") return solve_model_normconst<double>(file, k, s);
9748 if (arith == "exact") return solve_model_normconst<line::Rational>(file, k, s);
9749 if (arith == "real:16") return solve_model_normconst<line::Real<16> >(file, k, s);
9750 if (arith == "real" || arith == "real:32")
9751 return solve_model_normconst<line::Real<32> >(file, k, s);
9752 if (arith == "real:64") return solve_model_normconst<line::Real<64> >(file, k, s);
9753 if (arith == "real:128") return solve_model_normconst<line::Real<128> >(file, k, s);
9754 if (arith == "real:256") return solve_model_normconst<line::Real<256> >(file, k, s);
9755 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9756 }
9757 if (analysis == "sens") {
9758 if (arith == "double") return solve_model_nc_sens<double>(file, k);
9759 if (arith == "exact") return solve_model_nc_sens<line::Rational>(file, k);
9760 if (arith == "real:16") return solve_model_nc_sens<line::Real<16> >(file, k);
9761 if (arith == "real" || arith == "real:32")
9762 return solve_model_nc_sens<line::Real<32> >(file, k);
9763 if (arith == "real:64") return solve_model_nc_sens<line::Real<64> >(file, k);
9764 if (arith == "real:128") return solve_model_nc_sens<line::Real<128> >(file, k);
9765 if (arith == "real:256") return solve_model_nc_sens<line::Real<256> >(file, k);
9766 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9767 }
9768 if (analysis == "cdf") {
9769 if (arith == "double") return solve_model_nc_cdf<double>(file, k);
9770 if (arith == "exact") return solve_model_nc_cdf<line::Rational>(file, k);
9771 if (arith == "real:16") return solve_model_nc_cdf<line::Real<16> >(file, k);
9772 if (arith == "real" || arith == "real:32")
9773 return solve_model_nc_cdf<line::Real<32> >(file, k);
9774 if (arith == "real:64") return solve_model_nc_cdf<line::Real<64> >(file, k);
9775 if (arith == "real:128") return solve_model_nc_cdf<line::Real<128> >(file, k);
9776 if (arith == "real:256") return solve_model_nc_cdf<line::Real<256> >(file, k);
9777 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9778 }
9779 if (analysis == "prob") {
9780 if (arith == "double") return solve_model_nc_prob<double>(file, k);
9781 if (arith == "exact") return solve_model_nc_prob<line::Rational>(file, k);
9782 if (arith == "real:16") return solve_model_nc_prob<line::Real<16> >(file, k);
9783 if (arith == "real" || arith == "real:32")
9784 return solve_model_nc_prob<line::Real<32> >(file, k);
9785 if (arith == "real:64") return solve_model_nc_prob<line::Real<64> >(file, k);
9786 if (arith == "real:128") return solve_model_nc_prob<line::Real<128> >(file, k);
9787 if (arith == "real:256") return solve_model_nc_prob<line::Real<256> >(file, k);
9788 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9789 }
9790 if (arith == "double") return solve_model_nc<double>(file, k);
9791 if (arith == "exact") return solve_model_nc<line::Rational>(file, k);
9792 if (arith == "real:16") return solve_model_nc<line::Real<16> >(file, k);
9793 if (arith == "real" || arith == "real:32") return solve_model_nc<line::Real<32> >(file, k);
9794 if (arith == "real:64") return solve_model_nc<line::Real<64> >(file, k);
9795 if (arith == "real:128") return solve_model_nc<line::Real<128> >(file, k);
9796 if (arith == "real:256") return solve_model_nc<line::Real<256> >(file, k);
9797 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9798 }
9799 if (s == "ag") {
9800 // The @@SolverAG surface the port reaches: the AvgTable and its four
9801 // views, plus -a cdf as the inherited base-class exponential fallback.
9802 // RCAT converges a fixed point over the synchronization rates and
9803 // reports mean measures; it forms no state probability and no
9804 // transient, so there is nothing else to expose.
9805 if (analysis != "avg" && analysis != "cdf")
9807 "the AG solver ports -a avg (with its views -a node, -a sys, -a chain and "
9808 "-a nodechain) and -a cdf, the inherited exponential fallback: RCAT converges a "
9809 "fixed point over the synchronization rates and reports mean measures, forming no "
9810 "state probability or transient (got '" + analysis + "')");
9811 if (analysis == "cdf") {
9812 if (arith == "double") return solve_model_ag_cdf<double>(file, k);
9813 if (arith == "exact") return solve_model_ag_cdf<line::Rational>(file, k);
9814 if (arith == "real:16") return solve_model_ag_cdf<line::Real<16> >(file, k);
9815 if (arith == "real" || arith == "real:32")
9816 return solve_model_ag_cdf<line::Real<32> >(file, k);
9817 if (arith == "real:64") return solve_model_ag_cdf<line::Real<64> >(file, k);
9818 if (arith == "real:128") return solve_model_ag_cdf<line::Real<128> >(file, k);
9819 if (arith == "real:256") return solve_model_ag_cdf<line::Real<256> >(file, k);
9820 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9821 }
9822 if (arith == "double") return solve_model_ag<double>(file, k);
9823 if (arith == "exact") return solve_model_ag<line::Rational>(file, k);
9824 if (arith == "real:16") return solve_model_ag<line::Real<16> >(file, k);
9825 if (arith == "real" || arith == "real:32") return solve_model_ag<line::Real<32> >(file, k);
9826 if (arith == "real:64") return solve_model_ag<line::Real<64> >(file, k);
9827 if (arith == "real:128") return solve_model_ag<line::Real<128> >(file, k);
9828 if (arith == "real:256") return solve_model_ag<line::Real<256> >(file, k);
9829 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9830 }
9831 if (s == "ba") {
9832 if (analysis != "avg" && analysis != "bounds" && analysis != "cdf")
9833 throw line::UnsupportedError("the BA solver ports -a avg, -a node, -a bounds and "
9834 "-a cdf, the inherited exponential fallback (got '" +
9835 analysis + "')");
9836 if (analysis == "cdf") {
9837 if (arith == "double") return solve_model_ba_cdf<double>(file, k);
9838 if (arith == "exact") return solve_model_ba_cdf<line::Rational>(file, k);
9839 if (arith == "real:16") return solve_model_ba_cdf<line::Real<16> >(file, k);
9840 if (arith == "real" || arith == "real:32")
9841 return solve_model_ba_cdf<line::Real<32> >(file, k);
9842 if (arith == "real:64") return solve_model_ba_cdf<line::Real<64> >(file, k);
9843 if (arith == "real:128") return solve_model_ba_cdf<line::Real<128> >(file, k);
9844 if (arith == "real:256") return solve_model_ba_cdf<line::Real<256> >(file, k);
9845 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9846 }
9847 if (analysis == "bounds") {
9848 if (arith == "double") return solve_model_ba_bounds<double>(file, k);
9849 if (arith == "exact") return solve_model_ba_bounds<line::Rational>(file, k);
9850 if (arith == "real:16") return solve_model_ba_bounds<line::Real<16> >(file, k);
9851 if (arith == "real" || arith == "real:32")
9852 return solve_model_ba_bounds<line::Real<32> >(file, k);
9853 if (arith == "real:64") return solve_model_ba_bounds<line::Real<64> >(file, k);
9854 if (arith == "real:128") return solve_model_ba_bounds<line::Real<128> >(file, k);
9855 if (arith == "real:256") return solve_model_ba_bounds<line::Real<256> >(file, k);
9856 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9857 }
9858 if (arith == "double") return solve_model_ba<double>(file, k);
9859 if (arith == "exact") return solve_model_ba<line::Rational>(file, k);
9860 if (arith == "real:16") return solve_model_ba<line::Real<16> >(file, k);
9861 if (arith == "real" || arith == "real:32") return solve_model_ba<line::Real<32> >(file, k);
9862 if (arith == "real:64") return solve_model_ba<line::Real<64> >(file, k);
9863 if (arith == "real:128") return solve_model_ba<line::Real<128> >(file, k);
9864 if (arith == "real:256") return solve_model_ba<line::Real<256> >(file, k);
9865 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9866 }
9867 if (s == "lqns") {
9868 if (analysis != "avg" && analysis != "cdf")
9870 "SolverLQNS on a Network reports -a avg and -a cdf, the inherited exponential "
9871 "fallback: "
9872 "qnsolver returns one chain-level table of means and computes no state "
9873 "probability and no transient (got '" + analysis + "')");
9874 // The numbers arrive as the decimal text qnsolver printed, so every
9875 // digit past double is one this port invented; the ladder is refused
9876 // rather than run at a width the answer does not have.
9877 if (arith != "double")
9879 "SolverLQNS reads its results back as the decimal text an external binary printed, "
9880 "which is double at best; --arith " + arith +
9881 " would report a precision the tool never produced");
9882 if (analysis == "cdf") return solve_model_lqns_network_cdf(file, k);
9883 return solve_model_lqns_network<double>(file, k);
9884 }
9885 if (s == "fluid" || s == "fld") {
9886 if (analysis != "avg" && analysis != "odes" && analysis != "var" &&
9887 analysis != "tranvar" && analysis != "jacobian" && analysis != "tran" &&
9888 analysis != "prob" && analysis != "cdf" && analysis != "aoi" &&
9889 analysis != "statevec")
9891 "the fluid solver ports -a avg, -a tran, -a tranvar, -a prob, -a cdf, -a aoi, "
9892 "-a odes, -a statevec, -a var and -a jacobian (got '" + analysis + "')");
9893 // The drift is integrated by LSODA, whose coefficients assume double;
9894 // a higher-precision request is refused rather than quietly narrowed.
9895 if (arith != "double")
9897 "the fluid solver integrates its drift with LSODA, which is double precision by "
9898 "construction; rerun with --arith double (got '" + arith + "')");
9899 if (analysis == "odes") return solve_model_fluid_odes<double>(file, k);
9900 if (analysis == "statevec") return solve_model_fluid_statevec<double>(file, k);
9901 if (analysis == "jacobian") return solve_model_fluid_jacobian<double>(file, k);
9902 if (analysis == "var") return solve_model_fluid_var<double>(file, k);
9903 if (analysis == "tranvar") return solve_model_fluid_tranvar<double>(file, k);
9904 if (analysis == "tran") return solve_model_fluid_tran<double>(file, k);
9905 if (analysis == "prob") return solve_model_fluid_prob<double>(file, k);
9906 if (analysis == "cdf") return solve_model_fluid_cdf<double>(file, k);
9907 if (analysis == "aoi") return solve_model_fluid_aoi<double>(file, k);
9908 return solve_model_fluid<double>(file, k);
9909 }
9910 if (analysis == "prob") {
9911 if (arith == "double") return solve_model_prob<double>(file, k);
9912 if (arith == "exact") return solve_model_prob<line::Rational>(file, k);
9913 if (arith == "real:16") return solve_model_prob<line::Real<16> >(file, k);
9914 if (arith == "real" || arith == "real:32") return solve_model_prob<line::Real<32> >(file, k);
9915 if (arith == "real:64") return solve_model_prob<line::Real<64> >(file, k);
9916 if (arith == "real:128") return solve_model_prob<line::Real<128> >(file, k);
9917 if (arith == "real:256") return solve_model_prob<line::Real<256> >(file, k);
9918 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9919 }
9920 if (analysis == "marg") {
9921 if (arith == "double") return solve_model_marg<double>(file, k);
9922 if (arith == "exact") return solve_model_marg<line::Rational>(file, k);
9923 if (arith == "real:16") return solve_model_marg<line::Real<16> >(file, k);
9924 if (arith == "real" || arith == "real:32") return solve_model_marg<line::Real<32> >(file, k);
9925 if (arith == "real:64") return solve_model_marg<line::Real<64> >(file, k);
9926 if (arith == "real:128") return solve_model_marg<line::Real<128> >(file, k);
9927 if (arith == "real:256") return solve_model_marg<line::Real<256> >(file, k);
9928 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9929 }
9930 if (analysis == "normconst") {
9931 if (arith == "double") return solve_model_normconst<double>(file, k, s);
9932 if (arith == "exact") return solve_model_normconst<line::Rational>(file, k, s);
9933 if (arith == "real:16") return solve_model_normconst<line::Real<16> >(file, k, s);
9934 if (arith == "real" || arith == "real:32")
9935 return solve_model_normconst<line::Real<32> >(file, k, s);
9936 if (arith == "real:64") return solve_model_normconst<line::Real<64> >(file, k, s);
9937 if (arith == "real:128") return solve_model_normconst<line::Real<128> >(file, k, s);
9938 if (arith == "real:256") return solve_model_normconst<line::Real<256> >(file, k, s);
9939 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9940 }
9941 if (analysis == "cdf") {
9942 // The inherited base-class exponential fallback over the MVA means,
9943 // as @@NetworkSolver/getCdfRespT.m serves it for SolverMVA
9944 if (arith == "double") return solve_model_mva_cdf<double>(file, k);
9945 if (arith == "exact") return solve_model_mva_cdf<line::Rational>(file, k);
9946 if (arith == "real:16") return solve_model_mva_cdf<line::Real<16> >(file, k);
9947 if (arith == "real" || arith == "real:32")
9948 return solve_model_mva_cdf<line::Real<32> >(file, k);
9949 if (arith == "real:64") return solve_model_mva_cdf<line::Real<64> >(file, k);
9950 if (arith == "real:128") return solve_model_mva_cdf<line::Real<128> >(file, k);
9951 if (arith == "real:256") return solve_model_mva_cdf<line::Real<256> >(file, k);
9952 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
9953 }
9954 if (analysis != "avg")
9956 "the model-solving path ports -a avg, -a node, -a prob, -a marg, -a normconst and "
9957 "-a cdf (got '" +
9958 analysis + "')");
9959 // The mvaDispatch ladder's transcendental-only analyzers (open-queue closed
9960 // forms, DPS-exact, Marie, size-based) refuse by name under exact/real via
9961 // their if-constexpr guards, so the field-arithmetic branches (product-form
9962 // MVA, LD scaling) stay exact while a transcendental model is refused rather
9963 // than silently degraded.
9964 if (arith == "double") return solve_model_mva<double>(file, k);
9965 if (arith == "exact") return solve_model_mva<line::Rational>(file, k);
9966 if (arith == "real:16") return solve_model_mva<line::Real<16> >(file, k);
9967 if (arith == "real" || arith == "real:32") return solve_model_mva<line::Real<32> >(file, k);
9968 if (arith == "real:64") return solve_model_mva<line::Real<64> >(file, k);
9969 if (arith == "real:128") return solve_model_mva<line::Real<128> >(file, k);
9970 if (arith == "real:256") return solve_model_mva<line::Real<256> >(file, k);
9971 throw line::InputError(
9972 "--arith '" + arith +
9973 "' is not a model-solve backend; use double, exact or real:<16|32|64|128|256>");
9974}
9975
9976/**
9977 * The no-argument and `-h` message: the flags a first run actually needs.
9978 *
9979 * The full reference below is ~460 lines, which is not a thing a human reads at
9980 * a prompt; it stays one flag away under `--help-all` rather than being the
9981 * first thing the binary says.
9982 */
9983void print_brief_help() {
9984 std::printf(
9985 "LINE solver (C++), version %s\n"
9986 "\n"
9987 "Usage: line-cli -f <model> [-s <solver>] [-a <analysis>] [-o <format>]\n"
9988 " cat model.json | line-cli\n"
9989 "\n"
9990 "Common options:\n"
9991 " -f, --file <path> model file: .json (network), .lqnx (layered),\n"
9992 " .jsimg (JMT), .pnml (Petri net); stdin if omitted\n"
9993 " -s, --solver <name> auto (default), mva, nc, ctmc, mam, fluid, ssa,\n"
9994 " ldes, jmt, ag, ba, uq; ln for layered models, env\n"
9995 " for random environments\n"
9996 " -a, --analysis <type> avg (default), node, sys, chain, tran, prob, cdf,\n"
9997 " states, sample, normconst, bounds, ... (comma list)\n"
9998 " -o, --output <fmt> readable (default) | json | jsimg (export the\n"
9999 " model as a JMT simulation document)\n"
10000 " --method <name> algorithm within the chosen solver\n"
10001 " --samples <n> simulation run length (ssa, ldes); default 10000\n"
10002 " --seed <n> random seed; default 23000\n"
10003 " -v, --verbosity <lvl> silent | standard | debug; debug turns on\n"
10004 " the solver console, a running progress log\n"
10005 " --find-solver [m] which solvers and methods can analyze this model,\n"
10006 " optionally only those answering measure <m>\n"
10007 " (avg, tran, cdf, prob, sample, ...); reports and exits\n"
10008 " --find-solver-all [m] the same, keeping the refused pairs and the\n"
10009 " reason each was refused\n"
10010 " -h, --help this message\n"
10011 " --help-all every option, solver by solver\n"
10012 " -V, --version version string\n"
10013 " --install environment check: which optional backends\n"
10014 " (Java/JMT, LQNS, qnsolver, SageMath) are reachable\n"
10015 "\n"
10016 "Examples:\n"
10017 " line-cli -f model.json solve, letting auto pick the solver\n"
10018 " line-cli -f model.json -s mva -a avg mean queue lengths, MVA\n"
10019 " line-cli -f model.lqnx -s ln solve a layered model\n"
10020 " line-cli -f model.json -s ssa --samples 1e6 -o json\n"
10021 " line-cli -f model.json --find-solver what can solve this model\n"
10022 " line-cli -f model.json --find-solver cdf ... and return a passage-time law\n"
10023 "\n"
10024 "Each solver has flags of its own (tolerances, horizons, engine choices):\n"
10025 "run `line-cli --help-all` for the full reference.\n",
10026 kVersion);
10027}
10028
10029void print_help() {
10030 std::printf(
10031 "LINE multiprecision solver (C++), version %s\n"
10032 "\n"
10033 "Usage: line-cli [OPTIONS]\n"
10034 " cat model.json | line-cli -i json [OPTIONS]\n"
10035 " line-cli model.lqnx [OPTIONS]\n"
10036 "\n"
10037 "Options (flag-compatible with jline.cli.LineCLI):\n"
10038 " -f, --file <path> model file; stdin when omitted (json only)\n"
10039 " -i, --input <fmt> input format: json | jsim | jsimg | jsimw |\n"
10040 " lqnx | xml | pnml. Without it a .lqnx or .xml\n"
10041 " path is read as a layered model, a\n"
10042 " .jsim/.jsimg/.jsimw path as a JMT simulation\n"
10043 " document, a .pnml path as a place/transition\n"
10044 " net (ISO/IEC 15909-2), and anything else as a\n"
10045 " Network model.json. The three jsim spellings\n"
10046 " name ONE format, as they do in JMT\n"
10047 " -o, --output <fmt> output format: readable | json | jsimg\n"
10048 " (| layers, lqnx for layered models).\n"
10049 " jsimg exports the model as a JMT simulation\n"
10050 " document and solves nothing -- the write half\n"
10051 " of -i jsim|jsimg|jsimw, and the same three\n"
10052 " spellings are accepted. --seed, --samples and\n"
10053 " --tspan set the controls its header carries\n"
10054 " json is honoured by EVERY analysis, not only\n"
10055 " -a avg: each answers under a key named after\n"
10056 " its -a, with the arithmetic and the resolved\n"
10057 " method beside it, and every index inside a\n"
10058 " payload is 0-based against the tables' 1-based\n"
10059 " columns (each payload states its indexBase)\n"
10060 " -s, --solver <name> Network: auto, mva, nc, ctmc, mam, ba, ssa, fluid, uq,\n"
10061 " ldes (the SSJ discrete-event engine, run as a\n"
10062 " subprocess on common/ldes or common/ldes.jar),\n"
10063 " jmt (the Java Modelling Tools engine),\n"
10064 " lqns (its qns methods, the external qnsolver\n"
10065 " binary)\n"
10066 " layered: auto, ln, ln.mva, ln.comom, lqns,\n"
10067 " ldes (the native in-process LN simulator, a\n"
10068 " sample path of the layered model itself and\n"
10069 " not a decomposition into layers; it takes\n"
10070 " --samples and --seed, and NOT the --ldes-*\n"
10071 " family, which configures the subprocess\n"
10072 " engine that answers -s ldes on a Network)\n"
10073 " environment: env (an Environment model.json)\n"
10074 " -a, --analysis <type> analysis: avg, node, sys, chain, nodechain,\n"
10075 " stage, cache, item, prob, marg, cdf, cdf-passt,\n"
10076 " perct-respt, tran, tran-cdf-respt,\n"
10077 " tran-cdf-passt, tranprob, tranreward,\n"
10078 " reward-value, normconst, gen, states, sample,\n"
10079 " reward, sens, first-passt, odes, statevec,\n"
10080 " var, tranvar, busyperiod,\n"
10081 " jacobian, aoi, internals, bounds, posterior,\n"
10082 " interval. A COMMA LIST runs several in order\n"
10083 " (`-a avg,sys`), emitting one -o json envelope\n"
10084 " per analysis rather than one merged object.\n"
10085 " The JAR CLI's own spellings are accepted as\n"
10086 " aliases -- cdf-respt, prob-sys-aggr, tran-avg,\n"
10087 " generator, reward-steady, all -- and collapse\n"
10088 " onto the arm that already answers them whole:\n"
10089 " -a prob reports getProbSys, getProbSysAggr and\n"
10090 " the per-station pair together, -a sample walks\n"
10091 " all four samplers at once, and -a stage IS\n"
10092 " -a avg on a Network (one implicit stage).\n"
10093 " Which names a\n"
10094 " solver serves is stated by its own refusal;\n"
10095 " ssa serves avg, prob and sample (prob and\n"
10096 " sample run the SERIAL engine whatever -m\n"
10097 " said, since the NRM simulates counts rather\n"
10098 " than the state encoding)\n"
10099 " -v, --verbosity <lvl> silent | standard | debug; debug turns on\n"
10100 " the solver console, a running progress log\n"
10101 " of every solver run\n"
10102 " -d, --seed <n> random seed (SSA, nc --method cftp); default\n"
10103 " 23000. -d is the JAR CLI's spelling\n"
10104 " --warmupfrac <f> SSA: leading fraction of the path discarded\n"
10105 " before the means are taken, in [0,1)\n"
10106 " --pstar <p> fluid: exponent of the p-norm smoothing of the\n"
10107 " drift; without it the hard min() is integrated\n"
10108 " --busyperiod <n,..> -a busyperiod: the orders wanted; default 1\n"
10109 " --busyperiod-subnet <i,..>\n"
10110 " -a busyperiod: the 1-based stations forming the\n"
10111 " subnetwork. Required -- a busy period is defined\n"
10112 " for a NAMED set and no default can choose one\n"
10113 " --method <name> algorithm within the solver\n"
10114 " --samples <n> simulation run length (SSA); default 10000.\n"
10115 " Accepts 1e6 as well as 1000000. A simulation\n"
10116 " figure is only a measurement WITH this number,\n"
10117 " which is why the SSA banner reports it back.\n"
10118 " Also the draw count of nc --method cftp,\n"
10119 " default 1e5, where the draw is the answer.\n"
10120 " --mdd-tol <x> level-iteration tolerance of ctmc --method mdd;\n"
10121 " default 1e-12. NOT --tol: that iteration is an\n"
10122 " inner solve whose fixed point is checked\n"
10123 " against the population invariant at 1e-6, so a\n"
10124 " solver-sized tolerance stops short of it\n"
10125 " --mdd-maxiter <n> coupled sweeps before ctmc --method mdd is\n"
10126 " declared non-convergent; default 500\n"
10127 " --level <n> hierarchy level of the ba pbh/bjbh/cbh/sib\n"
10128 " families; default 2\n"
10129 " --qrf-params <j> JSON (inline or a path) with the QRF blocking\n"
10130 " tables of -s ba --method qrf.bas|qrf.rsrd:\n"
10131 " f, MR, BB, MM, MM1, ZZ and optionally F, the\n"
10132 " fields sn_to_qrf_params assembles. ZM is\n"
10133 " derived from ZZ. There is no default: assuming\n"
10134 " no blocking puts the bound ~31x farther from\n"
10135 " exact, so its absence is refused\n"
10136 " --qrf-alpha <j> JSON (nstations x N) load-dependent scaling of\n"
10137 " the ba qrf.mmi.ld, qrf.mmi.linear and qrf.rsrd\n"
10138 " arms; default all ones\n"
10139 " --tol <x> convergence tolerance (mva, nc, mam, ag, fluid)\n"
10140 " --iter_tol <x> outer-loop tolerance (mva, nc, fluid)\n"
10141 " --iter_max <n> iteration cap (mva, nc, mam, ag, fluid)\n"
10142 " --max-states <n> truncation level of an OPEN agent's queue-length\n"
10143 " dimension (ag), options.config.maxStates;\n"
10144 " default 100. A closed class is bounded by its\n"
10145 " own population instead, and --method inapinf\n"
10146 " ignores the level and solves the open agents on\n"
10147 " the infinite state space\n"
10148 " --fork-join <arm> which fork-join transform the mean-value fixed\n"
10149 " point takes (mva, nc): default|mmt|fjt is the\n"
10150 " MMT transform, ht|heidelberger-trivedi the\n"
10151 " Heidelberger-Trivedi one, which is CLOSED\n"
10152 " models only and a different answer to the same\n"
10153 " model rather than a faster route to one\n"
10154 " --cutoff <n|matrix> open jobs per class in the CTMC state space;\n"
10155 " a matrix is per (station,class), '1,1,0;3,3,0;0,0,3'\n"
10156 " (ctmc); without it the reference's\n"
10157 " ceil(6000^(1/(M*K))) is used and reported.\n"
10158 " Also the level truncation of mam -a prob,\n"
10159 " whose open queue has no bound of its own\n"
10160 " --fj-accuracy <n> FJ_codes truncation C of the queue-length\n"
10161 " difference between the two fork-join\n"
10162 " branches (mam, homogeneous fork-join);\n"
10163 " default 100, larger is more accurate\n"
10164 " --fj-tmode <mode> how that approximation solves for its T\n"
10165 " matrix: NARE (default) or Sylves\n"
10166 " --timescale <mode> auto (default), discrete or continuous: how\n"
10167 " -s mam reads the time scale. auto lets the\n"
10168 " distributions decide; discrete raises rather\n"
10169 " than solve a model that mixes lattice and\n"
10170 " non-lattice laws. The slot is --slotlength\n"
10171 " --tspan <t0>:<t1> transient horizon (ctmc -a tranprob,\n"
10172 " mam -a tran, fluid); a bare <t1> starts at 0.\n"
10173 " For the CTMC and MAM there is no default:\n"
10174 " pi(t) on an unstated horizon is not a\n"
10175 " quantity. For the fluid solver it bounds the\n"
10176 " integration, and is what -s fluid --method kp\n"
10177 " reports its covariance AT\n"
10178 " -n, --node <n> 1-based stateful node a state query is\n"
10179 " labelled by (ctmc -a tranprob, -a sample and\n"
10180 " ssa -a sample), the\n"
10181 " queue mam -a prob reports, or the station\n"
10182 " mva -a marg reports; without it the\n"
10183 " whole network is reported (the MAM queries\n"
10184 " take the model's only Queue)\n"
10185 " -c, --class <r> 1-based job class of mva -a marg; without it\n"
10186 " every class is reported\n"
10187 " NOTE ON THE INDEX BASE: -n and -c are 1-BASED\n"
10188 " here, as every station index this CLI takes\n"
10189 " is, and 0-BASED in jline.cli.LineCLI, which\n"
10190 " indexes as Java does. The short spellings are\n"
10191 " accepted so one command line parses in both,\n"
10192 " but the SAME number names a different node --\n"
10193 " the two bridges (cpp_dispatch, jar_dispatch)\n"
10194 " each convert for their own CLI\n"
10195 " --marg-states <ns> comma-separated job counts the mva -a marg\n"
10196 " curve is evaluated at, the reference's\n"
10197 " state_m; without it each law takes its own\n"
10198 " default range (0..N_r closed, mean + 5 sigma\n"
10199 " Poisson, the 1e-10 tail geometric)\n"
10200 " --notation <form> scalar (default) | matrix, the form the ODE\n"
10201 " export writes (fluid -a odes only)\n"
10202 " --symbolic <b> computer-algebra backend of fluid -a\n"
10203 " jacobian: auto (default, searches for a\n"
10204 " line-sage-rest service), a URL, an image\n"
10205 " name, or none to differentiate locally\n"
10206 " --equilibria also ask the backend for the solutions of\n"
10207 " f(x) = 0 (fluid -a jacobian). Needs a\n"
10208 " backend: solving is not differentiating\n"
10209 " --cdf-algorithm <a> exact (default, pfqn_stdf) | rd\n"
10210 " (pfqn_stdf_heur), how the sojourn law is\n"
10211 " inverted (nc -a cdf only)\n"
10212 " --perm-engine <e> exact (default, Ryser) | spm | bethe | heur |\n"
10213 " huberlaw | adapart, the permanent estimator\n"
10214 " of nc -a sysmarg. The five approximations\n"
10215 " refuse a demand matrix with a zero entry;\n"
10216 " spm is the saddle point, whose cost does not\n"
10217 " grow with the population\n"
10218 " --tran-points <n> points on the uniform transient grid the ENV\n"
10219 " mean-field coupling sums its stage exit\n"
10220 " metrics over (env only); default 1001\n"
10221 " --state <n,...> the ENCODED state row of the node named by\n"
10222 " --node that -a prob asks about, i.e.\n"
10223 " getProb(node, state)'s second argument; without\n"
10224 " it the query is about the model's default\n"
10225 " initial state\n"
10226 " --events <n> length of ONE sampled trajectory (-a sample);\n"
10227 " default 1000. NOT --samples, which is a\n"
10228 " solver's run length\n"
10229 " --timestep <dt> fixed output step of a transient CTMC solve\n"
10230 " (-a tranprob, -a tranreward), options.timestep\n"
10231 " of ctmc_transient.m; without it the grid is the\n"
10232 " integrator's own adaptive one. It changes WHERE\n"
10233 " the solution is reported, not how it is\n"
10234 " computed: the grid points are read off the same\n"
10235 " interpolant\n"
10236 " --transient-method <m> ode (default) or fau, how the CTMC forward\n"
10237 " equation is advanced over the output grid\n"
10238 " --fau-epsilon <e> fau: probability mass the grid may discard\n"
10239 " --fau-delta <d> fau: occupancy below which a state is dropped\n"
10240 " --rate-sched <j> -s ctmc -a tran: time-varying rates, a JSON array\n"
10241 " (inline or a file) of {station, class, tgrid,\n"
10242 " rates[, nominal]}; station and class by name or\n"
10243 " 1-based index. The rate of that pair follows the\n"
10244 " piecewise-linear schedule, held at its end values\n"
10245 " outside tgrid (options.config.rate_sched)\n"
10246 " --ctmc-tv-ngrid <n> grid size of the --rate-sched propagator (100)\n"
10247 " --percentiles <p,..> levels getPerctRespT is read at (-s mam),\n"
10248 " as fractions (0.9) or percents (90); default\n"
10249 " 0.50,0.90,0.95,0.99, the reference's pers_stored\n"
10250 " --reward-name <nm> which declared reward -a reward-value returns\n"
10251 " the value function of. REQUIRED there and never\n"
10252 " defaulted: two rewards have different value\n"
10253 " functions, and picking one would mislabel it\n"
10254 " --map-env <mode> default (on) | off, whether a model whose ONLY\n"
10255 " unsupported features are MAP/MMPP2/MMAP/MPH is\n"
10256 " solved through a random-environment image of the\n"
10257 " modulating chain instead of being refused. Not a\n"
10258 " -s env knob: it applies to mva, nc and fluid,\n"
10259 " which is where the refusal it replaces comes\n"
10260 " from. off restores that plain refusal\n"
10261 " --map-env-method <m> auto (default) | dec | avg | meanfield, the\n"
10262 " recombination the image is solved with. auto\n"
10263 " picks from the environment timescale; meanfield\n"
10264 " needs a stage backend and is refused by name\n"
10265 " where there is none\n"
10266 " --map-env-maxstages <n> cap on the stages map2renv draws from the\n"
10267 " modulating chain; 0, the default, takes every\n"
10268 " phase. Beyond the JAR, which has no such cap\n"
10269 " -p, --port <n> run as a solve SERVER on this port, speaking\n"
10270 " LineWebSocketServer's protocol: one WebSocket\n"
10271 " text message per connection, its first line the\n"
10272 " comma-separated argument list and its remainder\n"
10273 " the model document; the CLI's output comes back\n"
10274 " as one text message. Plaintext, one connection\n"
10275 " at a time, and -f is refused beside it\n"
10276 " -m, --maxreq <n> quit after serving n requests; without it the\n"
10277 " server runs until interrupted\n"
10278 " -h, --help the short message: the flags a first run needs\n"
10279 " --help-all this message, every option solver by solver\n"
10280 " -V, --version version string\n"
10281 " --install environment check: report which optional backends\n"
10282 " (Java/JMT, LQNS, qnsolver, SageMath) are reachable\n"
10283 "\n"
10284 "Layered models (-i lqnx|xml) additionally take:\n"
10285 " --layer-solver <s> solver run in each layer: mva (default)|nc|\n"
10286 " fluid|ssa, the C++ spelling of the reference's\n"
10287 " factory argument: LN(model, @(m) MVA(m))\n"
10288 " against LN(model, @(m) Fluid(m)). They converge\n"
10289 " to DIFFERENT fixed points, not to the same one\n"
10290 " by different routes, because each layer's\n"
10291 " results feed the next outer iteration's demands.\n"
10292 " fluid is double only and refuses a layer with a\n"
10293 " fork; ssa is NOISY, so the outer loop switches\n"
10294 " to the Robbins-Monro / Polyak-Ruppert controller\n"
10295 " --method <name> the LN update: default | moment3 | mw.upper |\n"
10296 " mw.lower. moment3 fits an APH to each layer's\n"
10297 " response-time CDF and convolves along the entry,\n"
10298 " which is what makes -a cdf possible; mw.*\n"
10299 " reports Majumdar-Woodside box BOUNDS on\n"
10300 " throughput and processor utilization and solves\n"
10301 " no layer at all (every other metric is NaN)\n"
10302 " --ln-transient <m> coupled (default) | decoupled, how -a tran\n"
10303 " couples the layers. decoupled freezes the\n"
10304 " inter-layer demands at the fixed point; coupled\n"
10305 " relaxes time-varying demands through the fluid\n"
10306 " rate schedule until the trajectories settle\n"
10307 " --ln-transient-channels <c> both (default) | thinkt | callservt,\n"
10308 " which inter-layer coupling the relaxation\n"
10309 " injects, for isolating one channel's share\n"
10310 " --sens-method <m> auto (default) | exact | fd, the branch each\n"
10311 " LAYER's sensitivity table takes (-a sens);\n"
10312 " the same three under -s nc -a sens\n"
10313 " --sens-scheme <s> forward (default) | central, the difference\n"
10314 " quotient of the fd branch\n"
10315 " --sens-step <h> relative rate perturbation of the fd branch,\n"
10316 " in (0,1); default 1e-4, or 1e-2 for ssa layers\n"
10317 " --no-interlocking disable the interlocking correction\n"
10318 " --interlock-method <m> ilrate (default) | refpath | none, how\n"
10319 " the interlock is tracked; refpath merges the\n"
10320 " callers that are one reference task's\n"
10321 " customers into one chain (srvn.cs only, it\n"
10322 " falls back to ilrate elsewhere, with a warning)\n"
10323 " --interlock-maxpaths <n> refpath refuses a layer carrying more\n"
10324 " reference routes into it; default 32\n"
10325 " --interlock-refpath-scope <s> merging (default) | all, whether\n"
10326 " refpath also rewrites a lone-caller layer\n"
10327 " --repeat <k> re-solve k times, report the best wall clock\n"
10328 " -o layers dump every layer's stations, classes, rates\n"
10329 " and routing instead of the AvgTable\n"
10330 " -a takes avg (getAvgTable), tran (getTranAvg, needs --tspan and fluid\n"
10331 " layers), sens (getSensitivityTable) and cdf (getCdfRespT, moment3).\n"
10332 " --iter_max and --iter_tol set the outer LN loop; --tol does not apply,\n"
10333 " and neither does any Network solver token.\n"
10334 "\n"
10335 "The external LQNS binary (-s lqns) additionally takes:\n"
10336 " --method <name> default | lqns | srvn | exactmva |\n"
10337 " srvn.exactmva | sim | lqsim | lqns.default.\n"
10338 " sim and lqsim run lqsim, the SIMULATOR;\n"
10339 " lqns.default is lqns with no pragma at all,\n"
10340 " which is a different fixed point and not a\n"
10341 " synonym for default\n"
10342 " --multiserver <p> conway|rolia|zhou|suri|reiser|schmidt|default\n"
10343 " (= rolia), the -Pmultiserver= pragma. Not\n"
10344 " passed to lqsim, which has no MVA to configure\n"
10345 " --samples <n> lqsim run length (-A); default 10000\n"
10346 " --timeout <s> kill the child after s seconds; without it the\n"
10347 " wrapper waits\n"
10348 " --keep keep the working directory with model.lqnx and\n"
10349 " model.lqxo instead of removing it\n"
10350 " --verbose echo the command line and the binary's output\n"
10351 " --remote[-url <u>] solve on a host running lqns-rest instead of\n"
10352 " locally; -url implies --remote. LINE ships no\n"
10353 " LQNS binary, so this is the other way to reach\n"
10354 " one\n"
10355 " -a takes avg only: lqns computes no transient, no sensitivity and no\n"
10356 " response-time distribution. QLen is the element utilization, Util its\n"
10357 " processor utilization per server, RespT its phase-1 service time;\n"
10358 " ResidT and ArvR print NaN because lqns reports neither.\n"
10359 "\n"
10360 "The external qnsolver binary (-s lqns on a Network) additionally takes:\n"
10361 " --method <name> default | qns (both = rolia) | qns.conway |\n"
10362 " qns.rolia | qns.zhou | qns.reiser | qns.suri |\n"
10363 " qns.schmidt. The multiserver approximation,\n"
10364 " passed as qnsolver -m and only\n"
10365 " when the model HAS a multiserver station. suri\n"
10366 " and schmidt are lqns approximations: they reach\n"
10367 " the tool only on the non-product-form closed\n"
10368 " branch below, and a multiserver model on the\n"
10369 " qnsolver branch refuses them by name\n"
10370 " --multiserver <p> the same choice under its config spelling;\n"
10371 " --method wins when it names one\n"
10372 " --timeout <s> kill the child after s seconds; without it the\n"
10373 " wrapper waits\n"
10374 " --keep keep the working directory with model.jmva and\n"
10375 " result.jmva instead of removing it\n"
10376 " -a takes avg only. The model is marshalled to the JMVA interchange\n"
10377 " format at CHAIN level and the chain results are de-aggregated back to\n"
10378 " classes, so only Queue, Delay and Source stations are expressible; any\n"
10379 " other station is refused rather than dropped. A closed model that is\n"
10380 " NOT product-form is converted with QN2LQN and delegated to lqns, as the\n"
10381 " reference does, so it also needs the lqns binary; class priorities are\n"
10382 " outside that conversion and are refused. --arith is double only, since\n"
10383 " the results arrive as the text an external binary printed.\n"
10384 "\n"
10385 "The Java Modelling Tools wrapper (-s jmt) additionally takes:\n"
10386 " --jmt-replications <n> independent JSIM runs the transient ensemble\n"
10387 " of -a tran and -a tranprob averages over\n"
10388 " (options.config.replications, default 10),\n"
10389 " replication k seeded seed+k-1. A single path\n"
10390 " is NOT E[N](t); --method jsim is one run\n"
10391 "\n"
10392 "Discrete-event simulation (-s ldes) additionally takes:\n"
10393 " --ldes-tranfilter <f> warmup filter: mser5 (default), fixed, none\n"
10394 " --ldes-warmupfrac <x> fraction the fixed filter discards (0.2)\n"
10395 " --ldes-cimethod <m> CI estimator: obm (default), bm, spectral,\n"
10396 " none\n"
10397 " --ldes-cnvgon stop on relative precision instead of on\n"
10398 " the --samples budget\n"
10399 " --ldes-cnvgtol <x> that precision target (0.05); implies\n"
10400 " --ldes-cnvgon\n"
10401 " --slotted run the analytical solver on a discrete\n"
10402 " (slotted) time scale; SolverNC routes to the\n"
10403 " discrete-time product form and refuses a model\n"
10404 " outside it\n"
10405 " --slotlength <x> the slot in model time units; implies --slotted\n"
10406 " --ldes-slotted run on a discrete (slotted) time scale; a\n"
10407 " sample off the lattice is an error, never\n"
10408 " rounded\n"
10409 " --ldes-slotlength <x> the slot (1.0); implies --ldes-slotted\n"
10410 " --ldes-replications <n> independent runs, averaged. A single path\n"
10411 " is NOT E[N](t): -a tran over an ensemble\n"
10412 " mean needs this\n"
10413 " --ldes-numthreads <n> workers for those replications\n"
10414 " --ldes-maxtime <s> wall-clock budget; the engine stops early\n"
10415 " and reports stopping=max_time\n"
10416 " --ldes-initsol <v,..> warm-start placement, station-major\n"
10417 " [st0_cl0, st0_cl1, ...]. Add --ldes-tranfilter\n"
10418 " fixed --ldes-warmupfrac 0 to reproduce\n"
10419 " initFromSolver, which assumes the placement\n"
10420 " is already a steady state\n"
10421 " --ldes-rest-url <u> solve on an LDES REST server instead of a\n"
10422 " local binary; same wire format, same numbers\n"
10423 " The model.json is forwarded to the engine BYTE FOR BYTE, so a model\n"
10424 " this port cannot itself parse (a cache with retrieval, an SPN, a\n"
10425 " polling server) is simulated exactly as the MATLAB and Python clients\n"
10426 " simulate it. -a avg, tran, cdf (the empirical response-time law),\n"
10427 " sample, reward and prob (the histogram's residence time at the declared\n"
10428 " per-class counts; --node with --state overrides one station) are\n"
10429 " served. --arith is double only.\n"
10430 "\n"
10431 "Uncertainty quantification (-s uq) additionally takes:\n"
10432 " --uq-solver <s> the engine run at each design point: mva, nc,\n"
10433 " mam, ba, ctmc, fluid or ssa. REQUIRED: UQ\n"
10434 " computes nothing itself, and defaulting it\n"
10435 " would attribute the numbers to an engine the\n"
10436 " caller never chose\n"
10437 " A model whose service or arrival process is a Prior is a FAMILY of\n"
10438 " models. UQ discretizes each Prior, solves the tensor product of the\n"
10439 " alternatives, and reports the prior-weighted expectation. Under -s uq\n"
10440 " three flags describe the DESIGN and not the engine: --method is\n"
10441 " quadrature (default, and the alias of discrete) or montecarlo,\n"
10442 " --samples the nodes per continuous Prior (11), --seed the Monte Carlo\n"
10443 " stream. The stage solver therefore keeps its own sample count and its\n"
10444 " own seed; --tol, --iter_tol, --iter_max and --cutoff pass through to\n"
10445 " it, since UQ has no convergence of its own. -a posterior prints every\n"
10446 " design point, its weight and the means it substituted, which is what\n"
10447 " says whether the expectation averaged two nearby models or two very\n"
10448 " different ones. Every other solver REFUSES a model carrying a Prior\n"
10449 " rather than lowering it to its mixture moments.\n"
10450 " -a interval answers the OTHER epistemic question, in which a\n"
10451 " parameter is bounded but not distributed: it drops the weights and\n"
10452 " keeps the endpoints. On a single-class closed model of LI\n"
10453 " single-server queues and delays it is the EXACT hull of MVA over the\n"
10454 " demand box (2*(m+2) MVA calls, no design solved at all); otherwise it\n"
10455 " falls back to the range over the solved design points, which for a\n"
10456 " continuous Prior lies strictly inside the true range. The table says\n"
10457 " which, and the fallback warns on stderr: a range that is not an\n"
10458 " enclosure must not read like one.\n"
10459 "\n"
10460 "Random environments (-s env) read an Environment model.json, whose\n"
10461 " stages each hold a Network and whose transitions carry the stage\n"
10462 " holding times. The stages are solved TRANSIENTLY and coupled: each\n"
10463 " stage starts from the queue lengths the previous one left, and the\n"
10464 " reported means are the per-stage sojourn averages blended by the\n"
10465 " environment probabilities. --method selects the coupling: meanfield\n"
10466 " (default, the reference's, carries the marginal means across a\n"
10467 " switch) or statevec|blend (carries the whole joint distribution).\n"
10468 " meanfield solves each stage with the fluid transient and is double\n"
10469 " only; statevec uniformizes a CTMC and takes the whole --arith ladder.\n"
10470 " --method avg|dec asks instead for a closed-form limit, which carries\n"
10471 " nothing across a switch and iterates nothing: avg solves ONE model\n"
10472 " whose modulated rates are their probEnv-weighted averages (exact as\n"
10473 " the environment gets fast), dec solves each stage in steady state and\n"
10474 " blends by probEnv (exact as it gets slow). A model with an\n"
10475 " environment-declared node breakdown is read from the nodeFailures\n"
10476 " block, in the expanded or the one-stage macro form.\n"
10477 " --tspan bounds the stage horizon and --tran-points its grid; --iter_tol\n"
10478 " and --iter_max drive the fixed point. RespT and ArvR print as nan\n"
10479 " because ENV computes neither -- the reference returns them as NaN too,\n"
10480 " and ResidT carries QLen/Tput.\n"
10481 "\n"
10482 "Additions specific to this port:\n"
10483 " --arith <mode> double (default) | exact | real:<digits>\n"
10484 " --list-api list the API functions ported so far\n"
10485 " --generate <spec> draw a random model and print it, solving\n"
10486 " nothing. <spec> is a JSON object inline, a\n"
10487 " path to one, or `-` for standard input.\n"
10488 " {\"kind\":\"network\"} draws a flat model and\n"
10489 " prints the line-model JSON document; its keys\n"
10490 " are seed, name, queues, delays, openClasses,\n"
10491 " closedClasses, schedStrat, routingStrat,\n"
10492 " distribution, cclassJobLoad,\n"
10493 " varyingServiceRates, multiServerQueues,\n"
10494 " randomCSNodes, multiChainCS and topology\n"
10495 " (rand|cyclic). A `delays` of -1, the default,\n"
10496 " lets the generator split the stations itself.\n"
10497 " {\"kind\":\"layered\"} draws an LQN and prints\n"
10498 " the .lqnx document; its keys are seed, name,\n"
10499 " clients, levels, tasks, processors,\n"
10500 " populationRange, thinkTimeRange,\n"
10501 " taskMultiRange, procMultiRange,\n"
10502 " hostDemandRange, synchCallRange (each a\n"
10503 " [lo,hi] pair), taskInfProbability and\n"
10504 " procInfProbability. Both draw from\n"
10505 " java.util.Random at `seed`, so a seeded spec\n"
10506 " reproduces; --arith is not read, since a draw\n"
10507 " is a double whatever the model is solved at.\n"
10508 " --api <name> invoke one API function directly\n"
10509 " --args <path> JSON arguments for --api; stdin when omitted\n"
10510 "\n"
10511 "Solvers and what they honour. Every model-solving solver reads -a avg,\n"
10512 "-f/-i and --method; nothing else is wired, so tolerances, iteration\n"
10513 "caps, seeds and sample counts keep their SolverOptions defaults rather\n"
10514 "than being invented here. mva and nc additionally read -a prob -- mva\n"
10515 "fits a binomial to its own means, nc returns the exact product-form\n"
10516 "probability, so the two disagree by construction. mva also reads -a\n"
10517 "marg, @SolverMVA's getProbMarg: P(n jobs of class r at station i) for\n"
10518 "every (station, class) pair, narrowed by --node / --class and evaluated\n"
10519 "at --marg-states. A closed class takes the Schmidt binomial fitted to\n"
10520 "Q(i,r); an open one takes the station's exact BCMP marginal (Poisson at\n"
10521 "an infinite server, multinomial-geometric at a queue). Both solvers read\n"
10522 "-a normconst, getProbNormConstAggr: nc reports the constant its solve\n"
10523 "already formed, mva RE-ENTERS its analyzer at method='exact', since only\n"
10524 "the exact recursion carries a G -- a model whose branch forms none, an\n"
10525 "open or mixed one above all, reports nan, as the reference does. nc also\n"
10526 "reads -a cdf,\n"
10527 "@SolverNC's getCdfRespT: the whole response-time law per (station,\n"
10528 "class) on one logarithmic grid, FCFS stations only, with\n"
10529 "--cdf-algorithm exact (pfqn_stdf, the default) or rd (pfqn_stdf_heur).\n"
10530 "nc reads -a sens as well, @NetworkSolver's getSensitivityTable: the\n"
10531 "derivative of each row's means with respect to its own service rate,\n"
10532 "selected with --sens-method / --sens-scheme / --sens-step. NC is one of\n"
10533 "the two engines whose exact branch differentiates the product-form\n"
10534 "recursion analytically, so auto takes it wherever the model is in its\n"
10535 "scope (single-server queues plus delays, not mixed) and falls back to\n"
10536 "finite differences elsewhere. NOT the -s ctmc -a sens analysis, which\n"
10537 "is getSensitivityRanking, a ranking of rate perturbations and not a\n"
10538 "table of derivatives.\n"
10539 "-a node is @NetworkSolver's getAvgNodeTable and is served by mva, nc,\n"
10540 "mam, ba and ctmc, the model solvers whose runner returns the station\n"
10541 "AvgResult it is built from. It is a DIFFERENT INDEX SPACE from -a avg,\n"
10542 "not a relabelling: the AvgTable has one row per STATION, so a\n"
10543 "ClassSwitch, Router, Fork, Join or Sink never appears in it, yet jobs\n"
10544 "flow through all of them. QLen, Util, RespT and ResidT are the station\n"
10545 "numbers scattered to their node indices and zero elsewhere -- a node\n"
10546 "that is not a station holds no jobs -- while ArvR and Tput are\n"
10547 "recomputed for every node by sn_get_node_arvr_from_tput and\n"
10548 "sn_get_node_tput_from_tput. The reference's finite-capacity-region\n"
10549 "pseudo-node rows are NOT emitted: this port's AvgResult carries no\n"
10550 "per-region queue length or utilization to fill them with.\n"
10551 "\n"
10552 "auto picks the engine\n"
10553 "with chooseSolverHeur and prints the name it picked; a branch selecting\n"
10554 "JMT or LDES refuses by name rather than substituting another. ctmc reads\n"
10555 "--cutoff and ports -a avg, prob, gen, states, tranprob, sample, reward,\n"
10556 "cdf, first-passt and sens, which are @SolverCTMC's\n"
10557 "getProbSys/getProbSysAggr and the per-station getProb/getProbAggr,\n"
10558 "getInfGen, getStateSpace, getTranProbSysAggr, sampleSys, getAvgReward,\n"
10559 "getCdfRespT, getCdfFirstPassT (state sets via --passage-from and\n"
10560 "--passage-into, as 1-based space rows '3,5' or state rows '0,2;1,1'),\n"
10561 "first-passt-moments (-a firstpasstmom, the same two sets plus\n"
10562 "--passage-orders: exact moments by one linear solve per order, so a\n"
10563 "variance or a skewness costs no truncated curve)\n"
10564 "and getSensitivityRanking. Its --method also takes mdd, which holds the\n"
10565 "reachable set in a decision diagram and solves K coupled level-CTMCs\n"
10566 "instead of the |S|-state generator; it serves -a avg only, having no\n"
10567 "explicit chain to answer the rest from. nc's --method also takes cftp /\n"
10568 "cftp.approx, which draw iid states from the exact stationary law of a\n"
10569 "closed single-class product-form network by coupling from the past;\n"
10570 "they serve -a avg only and their rows carry Monte Carlo error.\n"
10571 "mam ports -a avg,\n"
10572 "prob, cdf, tran and internals, which are @SolverMAM's getProb and\n"
10573 "getProbMarg (the joint (level, phase) law of the queue and its\n"
10574 "per-class marginals, truncated at --cutoff when the model is open),\n"
10575 "getCdfRespT with getPerctRespT beside it, getTranAvg over --tspan (the\n"
10576 "reference forces method ldqbd there, so --method does not select it),\n"
10577 "and getMAMResult, the M/G/1-type internals of a single queue. fluid\n"
10578 "additionally reads -a tran, -a prob, -a cdf, -a var and -a aoi, which\n"
10579 "are @SolverFLD's getTranAvg (the metrics along the trajectory, over\n"
10580 "--tspan or, without one, the horizon the reference's own adaptive loop\n"
10581 "converges at), getProbAggr (a law FITTED to the fluid means, so it does\n"
10582 "not agree with the mva or nc answer by construction), getCdfRespT (the\n"
10583 "response-time law per station and class, read off a marked-fluid\n"
10584 "integration started from the steady state), getMoments/getTranAvgVar\n"
10585 "and getAvgAoI with getCdfAoI beside it -- the last needing method mfq\n"
10586 "and the Source/Queue/Sink topology the age laws are defined for. It\n"
10587 "further reads -a odes, which is\n"
10588 "@SolverFLD/exportODEs, and --notation for the form it writes, and -a\n"
10589 "jacobian, which is @SolverFLD/getJacobian: d f_i / d x_j of the drift,\n"
10590 "differentiated locally over the structure of the system, with the\n"
10591 "equilibria beside it under --equilibria, which needs the\n"
10592 "line-sage-rest backend --symbolic names. Only the smooth methods have\n"
10593 "a Jacobian: min(n,S) has none where the regime switches, so the\n"
10594 "min-scaled drifts are refused by the factor that carries the kink.\n"
10595 "nc, ba and ctmc run\n"
10596 "under every --arith backend, nc's cftp method excepted: its sampler\n"
10597 "works in the log domain, so it refuses 'exact' by name rather than\n"
10598 "answering in a field it does not live in. mdd does run under 'exact'\n"
10599 "(its level solve drops Householder for a rational least squares there),\n"
10600 "but the LEVEL AGGREGATION is still an approximation away from product\n"
10601 "form: exact arithmetic pins the fixed point, not the model. mam is\n"
10602 "double only (its phase-type fitter\n"
10603 "needs transcendental arithmetic) and so is ssa (its sample path is\n"
10604 "generated from exponential clocks) and fluid (LSODA). ba reports a\n"
10605 "BOUND, not an estimate, and ssa a simulation carrying Monte Carlo\n"
10606 "error; neither is comparable with an exact solver except as such.\n"
10607 "\n"
10608 "Arithmetic: 'exact' computes in arbitrary-precision rationals and\n"
10609 "reports numerator and denominator alongside the double value; 'real'\n"
10610 "computes in fixed high-precision binary floating point, at 50, 100 or\n"
10611 "200 digits (a request in between is rounded up to the next tier).\n"
10612 "\n"
10613 "--api arguments are a JSON object keyed by the MATLAB parameter names,\n"
10614 "e.g. {\"L\": [[0.6,0.4]], \"N\": [2,1], \"Z\": [1,0.5]}: a 2-D array is a\n"
10615 "matrix (row-major), a 1-D array a row vector, a bare number a scalar.\n"
10616 "A JSON number is read as its shortest round-tripping decimal, so 0.6 is\n"
10617 "3/5 in exact arithmetic; pass a string such as \"1/3\" for anything else.\n",
10618 kVersion);
10619}
10620
10621/**
10622 * `--install`: the environment check, the C++ twin of MATLAB's `lineInstall`,
10623 * the JAR's `jline.cli.LineInstall` and Python's `line-install`.
10624 *
10625 * NOTHING IT LOOKS FOR IS REQUIRED. The C++ edition is header-only and its
10626 * native solvers stand alone, so every dependency probed here backs one
10627 * optional wrapper or backend. A miss is therefore a warning on stderr that
10628 * names the solvers it disables and how to install it, never an error: the
10629 * point of the command is to tell a fresh checkout which solvers it can
10630 * actually reach, not to refuse to run.
10631 *
10632 * @return true when nothing warned
10633 */
10634bool install_check() {
10635 bool has_warnings = false;
10636 // stdout is block-buffered when the check is piped or redirected while
10637 // stderr is not, so an unflushed progress line would surface AFTER the
10638 // warning it belongs to. Flush before every warning so the transcript reads
10639 // in the order the checks ran.
10640 const auto warn = [&](const std::string& text) {
10641 std::fflush(stdout);
10642 std::fprintf(stderr, "%s\n", text.c_str());
10643 std::fflush(stderr);
10644 has_warnings = true;
10645 };
10646
10647 std::printf("Checking LINE (C++)...\n");
10648 std::printf(" line-cli %s\n", kVersion);
10649
10650 std::printf("Checking Java runtime (JMT wrapper)...\n");
10651 const std::string java = line::jmt::detail::find_java();
10652 if (java.empty()) {
10653 warn("WARNING: no Java runtime was found in LINE_JAVA, JAVA_HOME or on PATH, so the "
10654 "JMT wrapper (-s jmt) cannot run. Install a JRE 8 or later.");
10655 } else {
10656 std::printf(" %s\n", java.c_str());
10657 }
10658
10659 std::printf("Checking JMT...\n");
10660 const std::string jmt_dir = line::jmt::detail::jmt_jar_path();
10661 if (jmt_dir.empty()) {
10662 // REPORTS, DOES NOT ACQUIRE: `jmt_jar_path` never downloads, and this
10663 // command must not either. An environment check that fetched 31 MiB as
10664 // a side effect of being asked would be changing the environment it is
10665 // reporting on, so the download is named here and performed by the
10666 // solver call that needs it.
10667 warn("WARNING: JMT.jar was not found, this is required by the JMT wrapper. Download it "
10668 "from https://line-solver.sourceforge.net/latest/JMT.jar into common/, or point "
10669 "LINE_JMT_DIR at the folder holding it, or set LINE_JMT_DOWNLOAD=1 to let the first "
10670 "JMT solve fetch and verify it.");
10671 } else {
10672 std::printf(" %s/JMT.jar\n", jmt_dir.c_str());
10673 }
10674
10675 std::printf("Checking LQNS...\n");
10677 const std::string banner = line::lqns::lqns_version();
10678 if (banner.empty())
10679 warn("WARNING: lqns is not installed, this is required by the LQNS wrapper (-s lqns) "
10680 "for layered models. Download it at: https://github.com/layeredqueuing/dist");
10681 else
10682 warn("WARNING: the installed lqns is too old for LINE, which needs release 6 or "
10683 "later; it reports '" + banner + "'. Upgrade it from: "
10684 "https://github.com/layeredqueuing/dist");
10685 } else {
10686 std::printf(" %s\n", line::lqns::lqns_version().c_str());
10687 }
10688
10689 std::printf("Checking qnsolver...\n");
10691 warn("WARNING: qnsolver is not installed, this is required by the qns methods of the "
10692 "LQNS wrapper (-s lqns on a Network). "
10693 "It ships with LQNS: https://github.com/layeredqueuing/dist");
10694
10695 std::printf("Checking symbolic backend (line-sage-rest)...\n");
10697 warn(std::string("WARNING: Docker is not available, so the SageMath symbolic backend "
10698 "cannot start. It is the only computer algebra system this edition "
10699 "reaches and is required by the symbolic methods of "
10700 "SolverCTMC/SolverFluid. Install Docker, then run: "
10701 "docker run -d -p 8080:8080 ") + line::sym::SYM_DOCKER_IMAGE);
10702 else if (line::sym::sym_find_image().empty())
10703 warn(std::string("WARNING: the line-sage-rest image is not present locally, this may be "
10704 "required by some LINE methods. Pull it with: "
10705 "docker run -d -p 8080:8080 ") + line::sym::SYM_DOCKER_IMAGE);
10706
10707 if (has_warnings)
10708 std::printf("Completed. LINE has warnings.\n");
10709 else
10710 std::printf("Success. LINE is ready to use.\n");
10711 return !has_warnings;
10712}
10713
10714void list_api() {
10715 const auto& reg = line::api_registry();
10716 std::printf("%-24s %-8s %-24s %s\n", "function", "domain", "arithmetic", "ported from");
10717 for (const auto& e : reg) {
10718 std::string modes;
10719 for (std::size_t k = 0; k < e.arith.size(); ++k) {
10720 if (k) modes += ",";
10721 modes += line::arith_name(e.arith[k]);
10722 }
10723 std::printf("%-24s %-8s %-24s %s\n", e.name.c_str(), e.domain.c_str(), modes.c_str(),
10724 e.reference.c_str());
10725 }
10726 std::printf("\n%zu of ~480 API functions ported.\n", reg.size());
10727}
10728
10729/**
10730 * Read the --api argument object: from the file named by --args, or from stdin
10731 * when --args is absent. A parse failure names the source and the position, so
10732 * a malformed file is a legible error rather than an empty argument set.
10733 */
10734line::reg::Json read_api_args(const std::string& path) {
10735 std::string text;
10736 std::string source;
10737 if (path.empty()) {
10738 source = "standard input";
10739 text.assign(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
10740 } else {
10741 source = "'" + path + "'";
10742 std::ifstream in(path.c_str());
10743 if (!in) throw line::InputError("cannot open the --args file " + source);
10744 text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
10745 }
10746 // No --args and nothing on stdin means the caller forgot the arguments.
10747 if (text.find_first_not_of(" \t\r\n") == std::string::npos)
10748 throw line::InputError("no --api arguments given: pass --args <path> or a JSON object on "
10749 "standard input (read from " +
10750 source + ")");
10751 try {
10752 return line::reg::Json::parse(text);
10753 } catch (const line::reg::Json::parse_error& e) {
10754 throw line::InputError("malformed --api arguments in " + source + ": " + e.what());
10755 }
10756}
10757
10758/**
10759 * Read the --generate specification: inline when it starts with `{`, from
10760 * standard input when it is `-` or absent, and from a file otherwise.
10761 *
10762 * INLINE IS THE POINT. A host that already holds the spec as a string (the R
10763 * binding does, and so does an MCP tool) would otherwise have to write a
10764 * temporary file for a dozen numbers, and a `line_capi_run` caller has no
10765 * standard input to pipe it through either.
10766 */
10767line::reg::Json read_generate_spec(const std::string& arg) {
10768 std::string text;
10769 std::string source;
10770 const std::string::size_type first = arg.find_first_not_of(" \t\r\n");
10771 if (first != std::string::npos && arg[first] == '{') {
10772 source = "the --generate argument";
10773 text = arg;
10774 } else if (arg.empty() || arg == "-") {
10775 source = "standard input";
10776 text.assign(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
10777 } else {
10778 source = "'" + arg + "'";
10779 std::ifstream in(arg.c_str());
10780 if (!in) throw line::InputError("cannot open the --generate spec file " + source);
10781 text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
10782 }
10783 if (text.find_first_not_of(" \t\r\n") == std::string::npos)
10784 throw line::InputError("no --generate specification given: pass a JSON object inline, a "
10785 "path to one, or `-` for standard input (read from " +
10786 source + ")");
10787 try {
10788 return line::reg::Json::parse(text);
10789 } catch (const line::reg::Json::parse_error& e) {
10790 throw line::InputError("malformed --generate specification in " + source + ": " + e.what());
10791 }
10792}
10793
10794/** Read one integer spec key, refusing a value that is not a whole number. */
10795long gen_int(const line::reg::Json& j, const char* key, long dflt) {
10796 if (!j.contains(key) || j.at(key).is_null()) return dflt;
10797 const line::reg::Json& v = j.at(key);
10798 if (!v.is_number())
10799 throw line::InputError(std::string("--generate: '") + key + "' must be a number");
10800 const double d = v.get<double>();
10801 if (d != std::floor(d))
10802 throw line::InputError(std::string("--generate: '") + key + "' must be a whole number");
10803 return static_cast<long>(d);
10804}
10805
10806/** Read one real spec key. */
10807double gen_real(const line::reg::Json& j, const char* key, double dflt) {
10808 if (!j.contains(key) || j.at(key).is_null()) return dflt;
10809 const line::reg::Json& v = j.at(key);
10810 if (!v.is_number())
10811 throw line::InputError(std::string("--generate: '") + key + "' must be a number");
10812 return v.get<double>();
10813}
10814
10815/** Read one boolean spec key. */
10816bool gen_bool(const line::reg::Json& j, const char* key, bool dflt) {
10817 if (!j.contains(key) || j.at(key).is_null()) return dflt;
10818 const line::reg::Json& v = j.at(key);
10819 if (!v.is_boolean())
10820 throw line::InputError(std::string("--generate: '") + key + "' must be true or false");
10821 return v.get<bool>();
10822}
10823
10824/** Read one string spec key. */
10825std::string gen_str(const line::reg::Json& j, const char* key, const std::string& dflt) {
10826 if (!j.contains(key) || j.at(key).is_null()) return dflt;
10827 const line::reg::Json& v = j.at(key);
10828 if (!v.is_string())
10829 throw line::InputError(std::string("--generate: '") + key + "' must be a string");
10830 return v.get<std::string>();
10831}
10832
10833/** Read a two-element `[lo, hi]` range key. */
10834line::gen::Range gen_range(const line::reg::Json& j, const char* key, const line::gen::Range& dflt) {
10835 if (!j.contains(key) || j.at(key).is_null()) return dflt;
10836 const line::reg::Json& v = j.at(key);
10837 if (!v.is_array() || v.size() != 2 || !v[0].is_number() || !v[1].is_number())
10838 throw line::InputError(std::string("--generate: '") + key +
10839 "' must be a two-element array of numbers");
10840 return line::gen::Range(v[0].get<double>(), v[1].get<double>());
10841}
10842
10843/**
10844 * Refuse a spec key that is not one this kind reads.
10845 *
10846 * A TYPO IS A SILENT WRONG ANSWER OTHERWISE: `"openClases": 2` would generate a
10847 * closed model and the caller would have no way to tell it was not asked for.
10848 */
10849void gen_check_keys(const line::reg::Json& j, const char* const* allowed, std::size_t n) {
10850 for (line::reg::Json::const_iterator it = j.begin(); it != j.end(); ++it) {
10851 bool ok = false;
10852 for (std::size_t k = 0; k < n && !ok; ++k) ok = it.key() == allowed[k];
10853 if (!ok) {
10854 std::string names;
10855 for (std::size_t k = 0; k < n; ++k) {
10856 if (k) names += ", ";
10857 names += allowed[k];
10858 }
10859 throw line::InputError("--generate: unknown key '" + it.key() +
10860 "'; this kind reads: " + names);
10861 }
10862 }
10863}
10864
10865/**
10866 * The JAR CLI's `-a` spelling, mapped onto this port's token.
10867 *
10868 * `jline.cli.LineCLI` names its analyses with hyphenated, getter-shaped tokens
10869 * (`cdf-respt`, `prob-sys-aggr`, `tran-avg`) and this port names them by the
10870 * question (`cdf`, `prob`, `tran`), because one arm here answers what the JAR
10871 * splits across several: `-a prob` reports `getProbSys`, `getProbSysAggr` and
10872 * the per-station `getProb`/`getProbAggr` in ONE table, and `-a sample` walks
10873 * `sampleSys`, `sampleSysAggr` and the per-node pair in one trajectory. Both
10874 * spellings are therefore accepted and the JAR's collapse onto the arm that
10875 * already contains the answer -- a script written against the JAR CLI keeps
10876 * working, and nothing here is renamed to make that true.
10877 *
10878 * AN UNKNOWN METHOD NAME IS RETURNED UNCHANGED, not rejected here: the per-solver
10879 * whitelists downstream refuse by name and say which analyses that solver
10880 * serves, which is a better message than a table of every token in the CLI.
10881 */
10882std::string normalize_analysis(const std::string& a) {
10883 // Same question, different spelling.
10884 if (a == "cdf-respt" || a == "cdfrespt") return "cdf";
10885 if (a == "cdf-passt" || a == "cdfpasst") return "cdfpasst";
10886 if (a == "first-passt" || a == "cdf-firstpasst" || a == "cdffirstpasst") return "firstpasst";
10887 if (a == "first-passt-moments" || a == "firstpasst-moments" || a == "firstpasstmoments")
10888 return "firstpasstmom";
10889 if (a == "perct-respt" || a == "perctrespt") return "perct";
10890 if (a == "tran-avg" || a == "tranavg") return "tran";
10891 if (a == "tran-cdf-respt" || a == "trancdfrespt") return "trancdf";
10892 if (a == "tran-cdf-passt" || a == "trancdfpasst") return "trancdfpasst";
10893 if (a == "tran-prob" || a == "tranprob-sys-aggr") return "tranprob";
10894 if (a == "generator") return "gen";
10895 if (a == "state-space" || a == "statespace") return "states";
10896 if (a == "reward-steady" || a == "rewardsteady") return "reward";
10897 if (a == "reward-value" || a == "rewardvalue") return "rewardvalue";
10898 if (a == "node-chain" || a == "node-chain-table") return "nodechain";
10899 // The JAR's four probability getters and its four samplers, each answered
10900 // whole by one arm here. Collapsing them is not a loss: the arm emits every
10901 // one of the four, so a caller asking for the aggregate receives it beside
10902 // the joint rather than instead of it.
10903 if (a == "prob-aggr" || a == "prob-sys" || a == "prob-sys-aggr") return "prob";
10904 if (a == "prob-marg" || a == "probmarg") return "marg";
10905 if (a == "prob-sys-marg" || a == "probsysmarg" || a == "sys-marg") return "sysmarg";
10906 if (a == "sample-aggr" || a == "sample-sys" || a == "sample-sys-aggr") return "sample";
10907 // `getStageTable` IS `getAvgTable` on a Network model, in the reference and
10908 // in the JAR both: a network has one implicit stage, and only an
10909 // Environment has several. Mapped here rather than given an arm of its own,
10910 // because an arm would be a second name for one table and free to drift
10911 // from it.
10912 if (a == "stage") return "avg";
10913 return a;
10914}
10915
10916/** `-a` split on commas, each token normalized; never empty. */
10917std::vector<std::string> analysis_list(const std::string& spec) {
10918 std::vector<std::string> out;
10919 std::size_t at = 0;
10920 while (at <= spec.size()) {
10921 const std::size_t comma = spec.find(',', at);
10922 std::string tok =
10923 spec.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
10924 // A stray space around a comma is a typo, not a different analysis.
10925 while (!tok.empty() && std::isspace(static_cast<unsigned char>(tok.front())))
10926 tok.erase(tok.begin());
10927 while (!tok.empty() && std::isspace(static_cast<unsigned char>(tok.back())))
10928 tok.pop_back();
10929 if (tok.empty())
10930 throw line::InputError("-a takes a comma-separated list of analyses and one entry of '" +
10931 spec + "' is empty");
10932 // `all` is the JAR's composite of the station table and the system one,
10933 // expanded HERE rather than inside a solver arm so every downstream
10934 // whitelist sees the two analyses it already knows.
10935 if (normalize_analysis(tok) == "all") {
10936 out.push_back("avg");
10937 out.push_back("sys");
10938 } else {
10939 out.push_back(normalize_analysis(tok));
10940 }
10941 if (comma == std::string::npos) break;
10942 at = comma + 1;
10943 }
10944 if (out.empty()) throw line::InputError("-a takes at least one analysis");
10945 return out;
10946}
10947
10948struct Options {
10949 std::string file, input = "json", output = "readable", solver = "auto", analysis = "avg";
10950 std::string arith = "double", api, args;
10951 bool help = false, help_all = false, version = false, list = false;
10952 /**
10953 * `--find-solver [metric]`: report which solvers and methods can analyze the
10954 * model named by -f, and exit without solving it.
10955 *
10956 * `find_solver_all` is `--find-solver-all`, which keeps the refused pairs
10957 * and the reason each was refused. The report is arithmetic-independent --
10958 * it asks feature sets and shapes, not numbers -- so it always reads the
10959 * model at double and ignores --arith.
10960 */
10961 bool find_solver = false, find_solver_all = false;
10962 std::string find_solver_metric;
10963 /** `--install`: run the environment check and exit, solving nothing. */
10964 bool install = false;
10965 /**
10966 * `--generate <spec>`: draw a random model from a JSON specification and
10967 * print it, solving nothing.
10968 *
10969 * The spec is taken INLINE when it starts with `{`, from standard input
10970 * when it is `-` or empty, and from a file otherwise, so a host that has
10971 * the spec in a string never has to stage a file for it. `generate_given`
10972 * rather than a non-empty test, because `--generate ""` means "read stdin"
10973 * and is not the same as not passing the flag.
10974 */
10975 std::string generate;
10976 bool generate_given = false;
10977 /**
10978 * Whether -i was actually passed.
10979 *
10980 * Without it a `.lqnx` path could not be recognised: `input` defaults to
10981 * json, and a defaulted json is indistinguishable from an explicit one, so
10982 * the extension sniff below would either never fire or would override a
10983 * caller who said `-i json` deliberately.
10984 */
10985 bool input_given = false;
10986 /**
10987 * `-p/--port` and `-m/--maxreq`: server mode.
10988 *
10989 * `port == 0` is "not given" and not "port 0": binding port 0 asks the
10990 * kernel for an ephemeral one, which a caller who typed no port did not
10991 * ask for. `maxreq == 0` is unbounded, matching the JAR's documented
10992 * "quit after this many requests" with no cap by default.
10993 */
10994 int port = 0;
10995 int maxreq = 0;
10996 Knobs knobs;
10997};
10998
10999/** Whether `file` ends in one of JMT's three simulation-document extensions. */
11000bool has_jsim_extension(const std::string& file) {
11001 const std::string::size_type dot = file.find_last_of('.');
11002 if (dot == std::string::npos) return false;
11003 std::string ext = file.substr(dot + 1);
11004 for (std::size_t i = 0; i < ext.size(); ++i)
11005 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
11006 return ext == "jsim" || ext == "jsimg" || ext == "jsimw";
11007}
11008
11009/** Whether `file` ends in the PNML extension. */
11010bool has_pnml_extension(const std::string& file) {
11011 const std::string::size_type dot = file.find_last_of('.');
11012 if (dot == std::string::npos) return false;
11013 std::string ext = file.substr(dot + 1);
11014 for (std::size_t i = 0; i < ext.size(); ++i)
11015 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
11016 return ext == "pnml";
11017}
11018
11019/** Whether `file` ends in one of the layered model's two extensions. */
11020bool has_lqn_extension(const std::string& file) {
11021 const std::string::size_type dot = file.find_last_of('.');
11022 if (dot == std::string::npos) return false;
11023 std::string ext = file.substr(dot + 1);
11024 for (std::size_t i = 0; i < ext.size(); ++i)
11025 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
11026 return ext == "lqnx" || ext == "xml";
11027}
11028
11029Options parse_args(int argc, char** argv) {
11030 Options o;
11031 for (int i = 1; i < argc; ++i) {
11032 std::string a = argv[i];
11033 auto next = [&](const char* what) -> std::string {
11034 if (i + 1 >= argc) throw line::InputError(std::string("missing value after ") + what);
11035 return argv[++i];
11036 };
11037 if (a == "-h" || a == "--help") o.help = true;
11038 else if (a == "--help-all" || a == "--help-full") o.help_all = true;
11039 else if (a == "-V" || a == "--version") o.version = true;
11040 else if (a == "--install") o.install = true;
11041 else if (a == "--list-api") o.list = true;
11042 else if (a == "--generate") {
11043 // The value is OPTIONAL: a bare --generate reads the spec from
11044 // standard input, as --api does with its --args.
11045 o.generate_given = true;
11046 // A lone `-` IS the value (it names standard input); anything
11047 // else beginning with `-` is the next flag.
11048 if (i + 1 < argc && (argv[i + 1][0] != '-' || argv[i + 1][1] == '\0'))
11049 o.generate = argv[++i];
11050 }
11051 else if (a == "--find-solver" || a == "--find-method" || a == "--help-model") {
11052 o.find_solver = true;
11053 // The metric is OPTIONAL, so it is taken only when the next token is
11054 // not itself a flag: `--find-solver -f m.json` must not swallow -f.
11055 if (i + 1 < argc && argv[i + 1][0] != '-') o.find_solver_metric = argv[++i];
11056 } else if (a == "--find-solver-all" || a == "--find-method-all") {
11057 o.find_solver = true;
11058 o.find_solver_all = true;
11059 if (i + 1 < argc && argv[i + 1][0] != '-') o.find_solver_metric = argv[++i];
11060 }
11061 else if (a == "-f" || a == "--file") o.file = next("-f");
11062 else if (a == "-i" || a == "--input") { o.input = next("-i"); o.input_given = true; }
11063 else if (a == "-o" || a == "--output") o.output = next("-o");
11064 else if (a == "-s" || a == "--solver") o.solver = next("-s");
11065 else if (a == "-a" || a == "--analysis") o.analysis = next("-a");
11066 else if (a == "--arith") o.arith = next("--arith");
11067 else if (a == "-p" || a == "--port") {
11068 const std::string v = next("-p");
11069 const long n = std::atol(v.c_str());
11070 if (n < 1 || n > 65535)
11071 throw line::InputError("-p takes a TCP port in 1..65535 (got '" + v + "')");
11072 o.port = static_cast<int>(n);
11073 } else if (a == "-m" || a == "--maxreq") {
11074 const std::string v = next("-m");
11075 const long n = std::atol(v.c_str());
11076 if (n < 1)
11077 throw line::InputError(
11078 "-m is the number of requests the server serves before quitting and must be "
11079 "positive; omit it to serve indefinitely (got '" + v + "')");
11080 o.maxreq = static_cast<int>(n);
11081 }
11082 else if (a == "--api") o.api = next("--api");
11083 else if (a == "--args") o.args = next("--args");
11084 else if (a == "--method") o.knobs.method = next("--method");
11085 else if (a == "--qrf-params") o.knobs.qrf_params = next("--qrf-params");
11086 else if (a == "--qrf-alpha") o.knobs.qrf_alpha = next("--qrf-alpha");
11087 else if (a == "--level") {
11088 const std::string v = next("--level");
11089 const int lv = std::atoi(v.c_str());
11090 if (lv < 1) throw line::InputError("--level must be a positive integer (got '" + v + "')");
11091 o.knobs.level = lv;
11092 }
11093 else if (a == "--samples") {
11094 const std::string v = next("--samples");
11095 const double d = std::atof(v.c_str()); // accepts 1e6 as well as 1000000
11096 if (!(d >= 1.0))
11097 throw line::InputError("--samples must be a positive count (got '" + v + "')");
11098 o.knobs.samples = static_cast<std::size_t>(d);
11099 } else if (a == "-d" || a == "--seed") {
11100 const std::string v = next("--seed");
11101 o.knobs.seed = std::strtoul(v.c_str(), nullptr, 10);
11102 if (o.knobs.seed == 0)
11103 throw line::InputError("--seed must be a positive integer (got '" + v + "')");
11104 } else if (a == "--warmupfrac") {
11105 const std::string v = next("--warmupfrac");
11106 const double f = std::atof(v.c_str());
11107 if (!(f >= 0.0 && f < 1.0))
11108 throw line::InputError(
11109 "--warmupfrac is the fraction of the path discarded before the means are "
11110 "taken and must lie in [0,1) (got '" + v + "')");
11111 o.knobs.warmupfrac = f;
11112 } else if (a == "--pstar") {
11113 const std::string v = next("--pstar");
11114 const double ps = std::atof(v.c_str());
11115 if (!(ps > 0.0))
11116 throw line::InputError(
11117 "--pstar is the exponent of the fluid p-norm smoothing and must be positive "
11118 "(got '" + v + "')");
11119 o.knobs.pstar = ps;
11120 } else if (a == "--busyperiod" || a == "--busyperiod-subnet") {
11121 // Same all-or-nothing parse as --marg-states: an entry dropped from
11122 // the list is a DIFFERENT report, not a shorter one.
11123 const bool orders = (a == "--busyperiod");
11124 const std::string v = next(orders ? "--busyperiod" : "--busyperiod-subnet");
11125 std::vector<std::size_t>& into = orders ? o.knobs.busy_orders : o.knobs.busy_subnet;
11126 std::size_t at = 0;
11127 while (at <= v.size()) {
11128 const std::size_t comma = v.find(',', at);
11129 const std::string tok =
11130 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11131 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos ||
11132 std::atol(tok.c_str()) < 1)
11133 throw line::InputError(
11134 std::string(orders ? "--busyperiod takes a comma-separated list of "
11135 "positive orders"
11136 : "--busyperiod-subnet takes a comma-separated list of "
11137 "1-based station indexes") +
11138 " (got '" + v + "')");
11139 into.push_back(static_cast<std::size_t>(std::atol(tok.c_str())));
11140 if (comma == std::string::npos) break;
11141 at = comma + 1;
11142 }
11143 } else if (a == "--tol") o.knobs.tol = std::atof(next("--tol").c_str());
11144 else if (a == "--iter_tol") o.knobs.iter_tol = std::atof(next("--iter_tol").c_str());
11145 else if (a == "--iter_max") o.knobs.iter_max = std::atoi(next("--iter_max").c_str());
11146 else if (a == "--max-states") {
11147 const std::string v = next("--max-states");
11148 const long long n = std::atoll(v.c_str());
11149 if (n <= 0)
11150 throw line::InputError(
11151 "--max-states truncates an open agent's queue-length dimension and takes a "
11152 "positive state count (got '" + v + "')");
11153 o.knobs.max_states = n;
11154 }
11155 else if (a == "--multiserver") o.knobs.multiserver = next("--multiserver");
11156 else if (a == "--fork-join" || a == "--fork_join")
11157 o.knobs.fork_join = next("--fork-join");
11158 else if (a == "--tran-points" || a == "--tran_points") {
11159 const std::string v = next("--tran-points");
11160 const long n = std::atol(v.c_str());
11161 if (n < 2)
11162 throw line::InputError(
11163 "--tran-points is the number of points on the transient grid and needs at "
11164 "least two, a start and an end (got '" + v + "')");
11165 o.knobs.tran_points = static_cast<std::size_t>(n);
11166 }
11167 else if (a == "--mdd-tol" || a == "--mdd_tol") {
11168 const std::string v = next("--mdd-tol");
11169 const double d = std::atof(v.c_str());
11170 if (!(d > 0.0))
11171 throw line::InputError("--mdd-tol must be a positive tolerance (got '" + v + "')");
11172 o.knobs.mdd_tol = d;
11173 } else if (a == "--mdd-maxiter" || a == "--mdd_maxiter") {
11174 const std::string v = next("--mdd-maxiter");
11175 const long n = std::atol(v.c_str());
11176 if (n < 1)
11177 throw line::InputError(
11178 "--mdd-maxiter must be a positive sweep count (got '" + v + "')");
11179 o.knobs.mdd_maxiter = static_cast<int>(n);
11180 }
11181 else if (a == "--fj-accuracy") {
11182 const std::string v = next("--fj-accuracy");
11183 const long n = std::atol(v.c_str());
11184 if (n < 1)
11185 throw line::InputError(
11186 "--fj-accuracy is the FJ_codes truncation C of the queue-length difference "
11187 "between the two fork-join branches and must be at least 1 (got '" + v + "')");
11188 o.knobs.fj_accuracy = static_cast<int>(n);
11189 } else if (a == "--fj-tmode") {
11190 const std::string v = next("--fj-tmode");
11191 if (v != "NARE" && v != "Sylves")
11192 throw line::InputError(
11193 "--fj-tmode selects how computeT.m solves for the T matrix and is 'NARE' (the "
11194 "Riccati route, the default) or 'Sylves' (the fixed-point iteration); got '" +
11195 v + "'");
11196 o.knobs.fj_tmode = v;
11197 } else if (a == "--timescale") {
11198 const std::string v = next("--timescale");
11199 if (v != "auto" && v != "discrete" && v != "continuous")
11200 throw line::InputError(
11201 "--timescale decides whether the model is read on a slot lattice and is "
11202 "'auto' (the default), 'discrete' or 'continuous'; got '" + v + "'");
11203 o.knobs.timescale = v;
11204 }
11205 else if (a == "--force") {
11206 o.knobs.force = true;
11207 }
11208 else if (a == "--cutoff") {
11209 const std::string v = next("--cutoff");
11210 if (v.find(',') != std::string::npos || v.find(';') != std::string::npos) {
11211 o.knobs.cutoff_mat = parse_cutoff_matrix(v);
11212 if (o.knobs.cutoff_mat.empty())
11213 throw line::InputError(
11214 "--cutoff takes a number or a per-(station,class) matrix written "
11215 "'r1c1,r1c2;r2c1,r2c2' (got '" + v + "')");
11216 } else {
11217 const double d = std::atof(v.c_str());
11218 if (!(d >= 1.0))
11219 throw line::InputError(
11220 "--cutoff must be a positive job count per open class (got '" + v + "')");
11221 o.knobs.cutoff = d;
11222 }
11223 } else if (a == "--tspan" || a == "--timespan") {
11224 const std::string v = next("--tspan");
11225 // BOTH SEPARATORS. This port has always written the horizon
11226 // `t0:t1`, and `jline.cli.LineCLI --timespan` has always written it
11227 // `t0,t1`; accepting only one made a command line that names a
11228 // horizon unportable between the two CLIs even after the flag names
11229 // were reconciled.
11230 std::string::size_type sep = v.find(':');
11231 if (sep == std::string::npos) sep = v.find(',');
11232 // A bare value is the END of the horizon and starts at 0, which is
11233 // what a transient from the initial state means; `t0:t1` states both.
11234 const double lo = sep == std::string::npos ? 0.0 : std::atof(v.substr(0, sep).c_str());
11235 const double hi = std::atof(
11236 (sep == std::string::npos ? v : v.substr(sep + 1)).c_str());
11237 // An INFINITE horizon is refused here rather than integrated to:
11238 // pi(t) on [0, Inf) is the stationary vector, which -a avg reports.
11239 if (!(hi > lo) || !(lo >= 0.0) || !std::isfinite(hi))
11240 throw line::InputError(
11241 "--tspan must be a finite horizon 0 <= t0 < t1, given as <t1>, <t0>:<t1> or "
11242 "<t0>,<t1> (got '" + v + "')");
11243 o.knobs.t0 = lo;
11244 o.knobs.t1 = hi;
11245 } else if (a == "-n" || a == "--node") {
11246 const std::string v = next("--node");
11247 const long n = std::atol(v.c_str());
11248 if (n < 1)
11249 throw line::InputError("--node must be a positive 1-based node index (got '" + v +
11250 "')");
11251 o.knobs.node = static_cast<std::size_t>(n);
11252 } else if (a == "-c" || a == "--class") {
11253 const std::string v = next("--class");
11254 const long c = std::atol(v.c_str());
11255 if (c < 1)
11256 throw line::InputError("--class must be a positive 1-based class index (got '" + v +
11257 "')");
11258 o.knobs.jobclass = static_cast<std::size_t>(c);
11259 } else if (a == "--marg-states" || a == "--marg_states") {
11260 // Every entry must parse: a dropped one shortens the curve silently.
11261 const std::string v = next("--marg-states");
11262 std::size_t at = 0;
11263 while (at <= v.size()) {
11264 const std::size_t comma = v.find(',', at);
11265 const std::string tok =
11266 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11267 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos)
11268 throw line::InputError(
11269 "--marg-states takes a comma-separated list of non-negative job counts "
11270 "(got '" + v + "')");
11271 o.knobs.marg_states.push_back(std::atol(tok.c_str()));
11272 if (comma == std::string::npos) break;
11273 at = comma + 1;
11274 }
11275 } else if (a == "--state") {
11276 // Same all-or-nothing discipline as --marg-states: a state vector
11277 // with one entry dropped is a DIFFERENT state, not a shorter one,
11278 // and the length is checked against the node's own space later.
11279 const std::string v = next("--state");
11280 std::size_t at = 0;
11281 while (at <= v.size()) {
11282 const std::size_t comma = v.find(',', at);
11283 const std::string tok =
11284 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11285 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos)
11286 throw line::InputError(
11287 "--state is the state vector -a prob asks about and takes a "
11288 "comma-separated list of non-negative counts (got '" + v + "')");
11289 o.knobs.state.push_back(std::atol(tok.c_str()));
11290 if (comma == std::string::npos) break;
11291 at = comma + 1;
11292 }
11293 } else if (a == "--events") {
11294 const std::string v = next("--events");
11295 const double d = std::atof(v.c_str()); // accepts 5e3 as well as 5000
11296 if (!(d >= 1.0))
11297 throw line::InputError(
11298 "--events is the length of a sampled trajectory and must be a positive event "
11299 "count (got '" + v + "')");
11300 o.knobs.events = static_cast<std::size_t>(d);
11301 } else if (a == "--timestep") {
11302 const std::string v = next("--timestep");
11303 const double d = std::atof(v.c_str());
11304 if (!(d > 0.0) || !std::isfinite(d))
11305 throw line::InputError(
11306 "--timestep is the fixed output step of a transient analysis and must be a "
11307 "positive finite time (got '" + v + "')");
11308 o.knobs.timestep = d;
11309 } else if (a == "--percentiles") {
11310 const std::string v = next("--percentiles");
11311 std::size_t at = 0;
11312 while (at <= v.size()) {
11313 const std::size_t comma = v.find(',', at);
11314 const std::string tok =
11315 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
11316 if (tok.empty())
11317 throw line::InputError(
11318 "--percentiles takes a comma-separated list of levels (got '" + v + "')");
11319 double p = std::atof(tok.c_str());
11320 // A LEVEL ABOVE 1 IS A PERCENT, below it a probability. The JAR
11321 // documents `--percentiles 50,90,95,99` and MATLAB stores
11322 // `pers_stored` as fractions, so both spellings reach this port
11323 // and the magnitude is what tells them apart. 1 itself is read
11324 // as the fraction: P(T <= t) = 1 is a level, 1% is not one
11325 // anybody asks for beside 50, 90 and 99.
11326 if (p > 1.0) p /= 100.0;
11327 if (!(p > 0.0) || !(p < 1.0))
11328 throw line::InputError(
11329 "--percentiles levels lie strictly inside (0,1) as fractions or (0,100) "
11330 "as percents; the 100th percentile of an unbounded law is not finite "
11331 "(got '" + tok + "')");
11332 o.knobs.percentiles.push_back(p);
11333 if (comma == std::string::npos) break;
11334 at = comma + 1;
11335 }
11336 } else if (a == "--reward-name" || a == "--reward_name") {
11337 o.knobs.reward_name = next("--reward-name");
11338 } else if (a == "--notation") {
11339 const std::string v = next("--notation");
11340 // The exporter validates the name; it is not defaulted here, so an
11341 // unrecognised notation is refused rather than answered with scalar.
11342 o.knobs.notation = v;
11343 } else if (a == "--symbolic") {
11344 // `sym_resolve` validates the name: auto, none, a URL or an image.
11345 // It is not defaulted here, so `--symbolic none` stays local rather
11346 // than being read as "not given" and searching anyway.
11347 o.knobs.symbolic = next("--symbolic");
11348 } else if (a == "--equilibria") o.knobs.equilibria = true;
11349 else if (a == "--perm-engine") {
11350 // The analyzer validates the name; an unrecognised engine is
11351 // refused rather than answered with exact.
11352 o.knobs.method_perm = next("--perm-engine");
11353 } else if (a == "--transient-method") {
11354 // The analyzer validates the name, so an unrecognised one is
11355 // refused rather than answered with the default.
11356 o.knobs.transient_method = next("--transient-method");
11357 } else if (a == "--rate-sched") {
11358 o.knobs.rate_sched = next("--rate-sched");
11359 } else if (a == "--ctmc-tv-ngrid") {
11360 const std::string v = next("--ctmc-tv-ngrid");
11361 const long n = std::atol(v.c_str());
11362 if (n < 2)
11363 throw line::InputError("--ctmc-tv-ngrid is the grid size of the rate_sched "
11364 "propagator and must be an integer >= 2 (got '" + v + "')");
11365 o.knobs.ctmc_tv_ngrid = static_cast<std::size_t>(n);
11366 } else if (a == "--fau-epsilon") {
11367 const std::string v = next("--fau-epsilon");
11368 const double d = std::atof(v.c_str());
11369 if (!(d > 0.0) || !std::isfinite(d))
11370 throw line::InputError(
11371 "--fau-epsilon is the probability mass the transient grid may discard and "
11372 "must be a positive finite number (got '" + v + "')");
11373 o.knobs.fau_epsilon = d;
11374 } else if (a == "--fau-delta") {
11375 const std::string v = next("--fau-delta");
11376 const double d = std::atof(v.c_str());
11377 if (!(d >= 0.0) || !std::isfinite(d))
11378 throw line::InputError(
11379 "--fau-delta is the occupancy below which a state is dropped and must be a "
11380 "nonnegative finite number (got '" + v + "')");
11381 o.knobs.fau_delta = d;
11382 } else if (a == "--cdf-algorithm") {
11383 // The analyzer validates the name; it is not defaulted here, so an
11384 // unrecognised algorithm is refused rather than answered with exact.
11385 o.knobs.cdf_algorithm = next("--cdf-algorithm");
11386 } else if (a == "--passage-from") o.knobs.passage_from = next("--passage-from");
11387 else if (a == "--passage-into") o.knobs.passage_into = next("--passage-into");
11388 else if (a == "--passage-method") o.knobs.passage_method = next("--passage-method");
11389 else if (a == "--passage-orders")
11390 o.knobs.passage_orders = static_cast<std::size_t>(std::stoul(next("--passage-orders")));
11391 else if (a == "--no-interlocking") o.knobs.no_interlocking = true;
11392 else if (a == "--interlock-method") o.knobs.interlock_method = next("--interlock-method");
11393 else if (a == "--interlock-maxpaths")
11394 o.knobs.interlock_maxpaths = std::stod(next("--interlock-maxpaths"));
11395 else if (a == "--interlock-refpath-scope")
11396 o.knobs.interlock_refpath_scope = next("--interlock-refpath-scope");
11397 else if (a == "--layer-solver") o.knobs.layer_solver = next("--layer-solver");
11398 else if (a == "--stage-solver") o.knobs.stage_solver = next("--stage-solver");
11399 else if (a == "--map-env") o.knobs.map_env = next("--map-env");
11400 else if (a == "--map-env-method") o.knobs.map_env_method = next("--map-env-method");
11401 else if (a == "--map-env-maxstages")
11402 o.knobs.map_env_maxstages = static_cast<std::size_t>(std::stoul(next("--map-env-maxstages")));
11403 else if (a == "--ln-transient") o.knobs.ln_transient = next("--ln-transient");
11404 else if (a == "--ln-transient-channels")
11405 o.knobs.ln_transient_channels = next("--ln-transient-channels");
11406 else if (a == "--sens-method") o.knobs.sens_method = next("--sens-method");
11407 else if (a == "--sens-scheme") o.knobs.sens_scheme = next("--sens-scheme");
11408 else if (a == "--sens-step") {
11409 const std::string v = next("--sens-step");
11410 const double h = std::atof(v.c_str());
11411 if (!(h > 0.0) || !(h < 1.0))
11412 throw line::InputError(
11413 "--sens-step is the RELATIVE rate perturbation and must lie in (0,1) (got '" +
11414 v + "')");
11415 o.knobs.sens_step = h;
11416 }
11417 else if (a == "--uq-solver") o.knobs.uq_solver = next("--uq-solver");
11418 else if (a == "--keep") o.knobs.keep = true;
11419 else if (a == "--verbose") o.knobs.verbose = true;
11420 else if (a == "--remote") o.knobs.remote = true;
11421 else if (a == "--remote-url") {
11422 // Implies --remote: a URL given and then ignored because the flag
11423 // was forgotten would solve LOCALLY and report nothing about it.
11424 o.knobs.remote_url = next("--remote-url");
11425 o.knobs.remote = true;
11426 }
11427 else if (a == "--timeout") {
11428 const std::string v = next("--timeout");
11429 const long s = std::atol(v.c_str());
11430 if (s < 1)
11431 throw line::InputError("--timeout is a deadline in seconds and must be positive "
11432 "(got '" + v + "')");
11433 o.knobs.timeout_seconds = static_cast<int>(s);
11434 }
11435 else if (a == "--repeat") {
11436 const std::string v = next("--repeat");
11437 const long n = std::atol(v.c_str());
11438 if (n < 1)
11439 throw line::InputError("--repeat must be a positive run count (got '" + v + "')");
11440 o.knobs.repeat = static_cast<int>(n);
11441 }
11442 // ---- the simulator's own knobs ------------------------------------
11443 else if (a == "--ldes-tranfilter") {
11444 const std::string v = next("--ldes-tranfilter");
11445 if (v != "mser5" && v != "fixed" && v != "none")
11446 throw line::InputError(
11447 "--ldes-tranfilter selects the warmup filter and is mser5, fixed or none (got '" +
11448 v + "')");
11449 o.knobs.ldes_tranfilter = v;
11450 }
11451 else if (a == "--ldes-warmupfrac") {
11452 const std::string v = next("--ldes-warmupfrac");
11453 const double d = std::atof(v.c_str());
11454 if (!(d >= 0.0 && d < 1.0))
11455 throw line::InputError(
11456 "--ldes-warmupfrac is the fraction of the run the fixed filter discards and "
11457 "lies in [0,1) (got '" + v + "')");
11458 o.knobs.ldes_warmupfrac = d;
11459 }
11460 else if (a == "--ldes-cimethod") {
11461 const std::string v = next("--ldes-cimethod");
11462 if (v != "obm" && v != "bm" && v != "spectral" && v != "none")
11463 throw line::InputError(
11464 "--ldes-cimethod selects the confidence-interval estimator and is obm, bm, "
11465 "spectral or none (got '" + v + "')");
11466 o.knobs.ldes_cimethod = v;
11467 }
11468 else if (a == "--ldes-cnvgon") o.knobs.ldes_cnvgon = true;
11469 else if (a == "--ldes-cnvgtol") {
11470 // Implies --ldes-cnvgon: a tolerance given and then ignored because
11471 // the switch was forgotten would run the full budget and say nothing.
11472 const std::string v = next("--ldes-cnvgtol");
11473 const double d = std::atof(v.c_str());
11474 if (!(d > 0.0 && d < 1.0))
11475 throw line::InputError(
11476 "--ldes-cnvgtol is a RELATIVE precision target and lies in (0,1) (got '" + v +
11477 "')");
11478 o.knobs.ldes_cnvgtol = d;
11479 o.knobs.ldes_cnvgon = true;
11480 }
11481 else if (a == "--ldes-slotted") o.knobs.ldes_slotted = true;
11482 else if (a == "--slotted") o.knobs.slotted = true;
11483 else if (a == "--slotlength") {
11484 // Implies --slotted, as --ldes-slotlength does for the simulator.
11485 const std::string v = next("--slotlength");
11486 const double d = std::atof(v.c_str());
11487 if (!(d > 0.0))
11488 throw line::InputError(
11489 "--slotlength is the slot of the discrete time scale and must be positive "
11490 "(got '" + v + "')");
11491 o.knobs.slotlength = d;
11492 o.knobs.slotted = true;
11493 }
11494 else if (a == "--ldes-slotlength") {
11495 // Implies --ldes-slotted, as --slotlength does on the engine's CLI.
11496 const std::string v = next("--ldes-slotlength");
11497 const double d = std::atof(v.c_str());
11498 if (!(d > 0.0))
11499 throw line::InputError(
11500 "--ldes-slotlength is the slot of the discrete time scale and must be positive "
11501 "(got '" + v + "')");
11502 o.knobs.ldes_slotlength = d;
11503 o.knobs.ldes_slotted = true;
11504 }
11505 else if (a == "--jmt-replications") {
11506 const std::string v = next("--jmt-replications");
11507 const long n = std::atol(v.c_str());
11508 if (n < 1)
11509 throw line::InputError(
11510 "--jmt-replications is a positive count of independent runs (got '" + v + "')");
11511 o.knobs.jmt_replications = static_cast<int>(n);
11512 }
11513 else if (a == "--ldes-replications") {
11514 const std::string v = next("--ldes-replications");
11515 const long n = std::atol(v.c_str());
11516 if (n < 1)
11517 throw line::InputError(
11518 "--ldes-replications is a positive count of independent runs (got '" + v + "')");
11519 o.knobs.ldes_replications = static_cast<int>(n);
11520 }
11521 else if (a == "--ldes-numthreads") {
11522 const std::string v = next("--ldes-numthreads");
11523 const long n = std::atol(v.c_str());
11524 if (n < 1)
11525 throw line::InputError(
11526 "--ldes-numthreads is a positive worker count (got '" + v + "')");
11527 o.knobs.ldes_numthreads = static_cast<int>(n);
11528 }
11529 else if (a == "--ldes-maxtime") {
11530 const std::string v = next("--ldes-maxtime");
11531 const double d = std::atof(v.c_str());
11532 if (!(d > 0.0))
11533 throw line::InputError(
11534 "--ldes-maxtime is a wall-clock budget in seconds and must be positive (got '" +
11535 v + "')");
11536 o.knobs.ldes_maxtime = d;
11537 }
11538 else if (a == "--ldes-initsol") {
11539 // A STATION-MAJOR placement, [st0_cl0, st0_cl1, ...]; the engine
11540 // reads it as the initial state and skips its default placement.
11541 const std::string v = next("--ldes-initsol");
11542 std::size_t b = 0;
11543 while (b <= v.size()) {
11544 const std::size_t e = v.find(',', b);
11545 const std::string tok =
11546 v.substr(b, e == std::string::npos ? std::string::npos : e - b);
11547 if (tok.empty())
11548 throw line::InputError(
11549 "--ldes-initsol is a comma-separated placement with no empty entry (got '" +
11550 v + "')");
11551 o.knobs.ldes_initsol.push_back(std::atof(tok.c_str()));
11552 if (e == std::string::npos) break;
11553 b = e + 1;
11554 }
11555 }
11556 else if (a == "--ldes-rest-url") o.knobs.ldes_rest_url = next("--ldes-rest-url");
11557 else if (a == "-v" || a == "--verbosity") {
11558 const std::string v = next(a.c_str());
11559 // The JAR names two levels and this port's help documents three;
11560 // all five spellings are accepted, and an unknown one is refused
11561 // rather than read as `standard`, which would silence nothing while
11562 // reporting that it had.
11563 if (v != "silent" && v != "standard" && v != "normal" && v != "debug" &&
11564 v != "verbose")
11565 throw line::InputError(
11566 "-v takes silent, standard (the JAR spells it normal) or debug; got '" + v +
11567 "'");
11568 o.knobs.verbosity = (v == "normal") ? "standard" : v;
11569 }
11570 else if (!a.empty() && a[0] == '-')
11571 throw line::InputError("unknown option: " + a);
11572 else
11573 o.file = a;
11574 }
11575 return o;
11576}
11577
11578} // namespace
11579
11580/**
11581 * Everything one invocation does once the arguments are in hand.
11582 *
11583 * SPLIT OUT OF `main` FOR SERVER MODE, which runs it once per request with a
11584 * different `-f` and a captured stdout. Keeping one body means a request served
11585 * over the socket takes exactly the path the same command line takes at the
11586 * shell -- the failure this avoids is a server that answers slightly differently
11587 * from the CLI it is supposed to BE.
11588 */
11589/**
11590 * `--find-solver`: which solvers and methods can analyze the model named by -f.
11591 *
11592 * It reports rather than solves, so it stops before the solver ladder in
11593 * `solve_model_dispatch` and before every knob that describes a run. The answer
11594 * is arithmetic-independent -- `auto_find_solver` asks feature sets, shapes and
11595 * gates, never numbers -- so the model is read at double whatever --arith says,
11596 * and a caller who passed one is told rather than silently obeyed.
11597 *
11598 * The layered path is not covered: `auto_find_solver` narrows the flat Network
11599 * families, and a LayeredNetwork's are `ln` and `lqns`, which this port reaches
11600 * through `solve_lqn_dispatch` and not through an AUTO of its own.
11601 */
11602int find_solver_report(const Options& o) {
11603 // A MODEL ON STDIN IS A MODEL. This used to refuse anything but `-f`, which
11604 // made the report the one part of the vocabulary a piped model could not
11605 // reach -- and `read_model` has read stdin on the empty path all along.
11606 if (o.file.empty() && !g_stdin_loaded)
11607 throw line::InputError("--find-solver reports on a model; name one with -f");
11608 const bool layered = o.file.empty()
11609 ? line::io::is_layered_json_text(stdin_model_text())
11610 : (has_lqn_extension(o.file) || line::io::is_layered_json(o.file));
11611 if (!o.input_given && layered)
11613 "--find-solver reports on a flat Network model; a layered one is solved by -s ln "
11614 "and -s lqns, which this port reaches through the -i lqnx path");
11615 if (o.output != "readable" && o.output != "json")
11616 throw line::InputError("unknown -o '" + o.output +
11617 "'; --find-solver reports as: readable, json");
11618 line::qn::Network<double> net = read_model<double>(o.file);
11619 const std::vector<line::autosolver::SolverCandidate> rows = line::autosolver::auto_find_solver(
11620 net.get_struct(), o.find_solver_metric, o.find_solver_all);
11621 if (o.output != "json") {
11622 std::printf("%s", line::autosolver::auto_find_solver_table(rows).c_str());
11623 return 0;
11624 }
11625 // THE STRUCTURED ROWS EXIST ALREADY AND HAD NO WAY OUT. auto_find_solver
11626 // returns solver, method, runnable, the method class, the metrics and the
11627 // reason for a refusal, and the fixed-width table was the only rendering,
11628 // so every caller that wanted the reason had to parse columns. All five
11629 // front ends and the MCP `find_solver` tool benefit from this arm.
11630 line::reg::Json j = line::reg::Json::object();
11631 line::reg::Json cands = line::reg::Json::array();
11632 for (std::size_t i = 0; i < rows.size(); ++i) {
11633 line::reg::Json c = line::reg::Json::object();
11634 c["solver"] = rows[i].solver;
11635 c["method"] = rows[i].method;
11636 c["runnable"] = rows[i].runnable;
11637 c["methodClass"] = rows[i].method_class;
11638 c["reason"] = rows[i].reason;
11639 line::reg::Json ms = line::reg::Json::array();
11640 for (std::size_t k = 0; k < rows[i].metrics.size(); ++k) ms.push_back(rows[i].metrics[k]);
11641 c["metrics"] = ms;
11642 cands.push_back(c);
11643 }
11644 line::reg::Json body = line::reg::Json::object();
11645 body["type"] = "FindSolver";
11646 body["candidates"] = cands;
11647 j["findSolver"] = body;
11648 emit_document(dump_document(j));
11649 return 0;
11650}
11651
11652int run_invocation(Options o) {
11653 // Validated on every path, not only --api: an unrecognised --arith is a
11654 // caller error whatever else the invocation asks for.
11655 line::reg::parse_arith(o.arith);
11656 if (!o.api.empty()) {
11657 if (o.output != "readable" && o.output != "json")
11658 throw line::InputError("unknown -o '" + o.output +
11659 "'; accepted forms are: readable, json");
11660 const line::reg::Json result =
11661 line::reg::api_invoke(o.api, o.arith, read_api_args(o.args));
11662 if (o.output == "json")
11663 emit_document(dump_document(result, 2));
11664 else
11665 std::printf("%s", line::reg::api_render_readable(result).c_str());
11666 return 0;
11667 }
11668 // A .lqnx/.xml path with no -i is taken as a layered model, so naming
11669 // the file is enough to solve it. An explicit -i always wins, so a
11670 // caller who says -i json about an oddly-named file still gets json.
11671 if (!o.input_given && has_lqn_extension(o.file)) o.input = "lqnx";
11672 // The same courtesy for a JMT document: naming the file is enough.
11673 if (!o.input_given && has_jsim_extension(o.file)) o.input = "jsimg";
11674 // And for a PNML document.
11675 if (!o.input_given && has_pnml_extension(o.file)) o.input = "pnml";
11676 // A model.json can carry EITHER model kind, and `linemodel_save` writes
11677 // `.json` for both, so the extension cannot decide it. The content can:
11678 // a `LayeredNetwork` type takes the layered path whatever `-i` says,
11679 // because handing it to the Network reader only produces "model type
11680 // 'LayeredNetwork' is not a Network" one frame further down.
11681 // THE SNIFF NOW COVERS A PIPED DOCUMENT. It took a PATH and was
11682 // therefore skipped entirely on stdin, so a LayeredNetwork arriving on
11683 // the standard input was handed to the Network reader and refused with
11684 // "the model-solving path ports -s mva, nc, ..." -- a message about the
11685 // solver, for a model the CLI could read perfectly well from a file.
11686 // Staging the text to a temporary file would have worked and would cost
11687 // a file per solve, which a host calling this in a sweep notices.
11688 const bool sniffable = o.input != "lqnx" && o.input != "xml" && o.input != "pnml" &&
11689 o.input.compare(0, 4, "jsim") != 0;
11690 if (sniffable && !o.file.empty() && !has_pnml_extension(o.file) &&
11691 !has_jsim_extension(o.file) && line::io::is_layered_json(o.file))
11692 o.input = "lqnx";
11693 else if (sniffable && o.file.empty() && g_stdin_loaded &&
11694 line::io::is_layered_json_text(stdin_model_text()))
11695 o.input = "lqnx";
11696 if (o.input == "lqnx" || o.input == "xml") {
11697 if (o.output != "readable" && o.output != "json" && o.output != "layers")
11698 throw line::InputError("unknown -o '" + o.output +
11699 "'; accepted forms on the layered path are: readable, "
11700 "json, layers");
11701 // ONE ENVELOPE PER ANALYSIS, in the order asked for. `-a avg,sens`
11702 // is the JAR's comma list, and the JAR merges the results into one
11703 // JSON object; here each arm prints as it computes, so the multi
11704 // form emits a sequence of the SAME envelopes a single `-a` emits
11705 // (JSON Lines). That keeps one parser for both spellings, where a
11706 // merged object would need a second one for the multi case alone.
11707 const std::vector<std::string> as = analysis_list(o.analysis);
11708 for (std::size_t i = 0; i + 1 < as.size(); ++i) {
11709 const int rc = solve_lqn_dispatch(o.arith, o.solver, as[i], o.output, o.file,
11710 o.knobs);
11711 if (rc != 0) return rc;
11712 poll_interrupt();
11713 }
11714 return solve_lqn_dispatch(o.arith, o.solver, as.back(), o.output, o.file, o.knobs);
11715 }
11716 // `-i jsim | jsimg | jsimw`: a JMT simulation document, read by
11717 // `read_jsim`. The three extensions name ONE format -- JMT writes the
11718 // same `<sim>` document under all three -- and are accepted separately
11719 // because the JAR CLI accepts them separately and a script naming the
11720 // wrong one is not describing a different model.
11721 if (o.input == "jsim" || o.input == "jsimg" || o.input == "jsimw")
11722 g_jsim_input = true;
11723 else if (o.input == "pnml")
11724 g_pnml_input = true;
11725 else if (o.input != "json")
11727 "the model-solving path reads -i json for a Network model, -i jsim|jsimg|jsimw "
11728 "for a JMT simulation document, -i pnml for a place/transition net and "
11729 "-i lqnx|xml for a layered one (got '" + o.input + "')");
11730 // `-o json` IS WIRED FOR EVERY ANALYSIS, not only `-a avg`. It was
11731 // avg-only, and the restriction was real rather than nominal: each other
11732 // arm printed its own readable shape and nothing else, so accepting
11733 // `json` for one would have handed a caller a table it asked to receive
11734 // as JSON -- the silent-acceptance failure the Knobs struct exists to
11735 // prevent, one layer up. The refusal is therefore removed only now that
11736 // every arm emits a payload (`emit_analysis`), and each answers under a
11737 // key named after its own `-a` so a caller can tell which question was
11738 // answered. On `-a avg` the solver BANNER still precedes the object on
11739 // stdout, as the JAR's does; it contains no brace, so the wrapper's
11740 // first-`{` scan is unaffected. The other arms print no banner: what it
11741 // carried -- the arithmetic, the resolved method, the state count and the
11742 // cutoff -- is inside the payload, where a host reads it as data instead
11743 // of scraping it from a line above.
11744 // `-o jsimg`: EXPORT the model as a JMT simulation document instead of
11745 // solving it. It is the missing half of a pair -- `-i jsimg` has read
11746 // one back all along -- and the C++ twin of MATLAB's `JMTIO`, the JAR's
11747 // `QN2JSIMG` and Python's `qn2jsimg`. Until now `jmt_write_jsim` was
11748 // reachable only from inside the solver's own scratch directory, which
11749 // `--keep` leaves behind, so exporting a model meant solving it first.
11750 //
11751 // The three spellings name ONE format, as they do on `-i` and in JMT.
11752 if (o.output == "jsim" || o.output == "jsimg" || o.output == "jsimw") {
11753 line::qn::Network<double> net = read_model<double>(o.file);
11756 wopt.file_name = "model";
11757 wopt.log_path = sn.log_path;
11758 // The simulation controls the header carries are the ones a solve
11759 // would have written into it, so an exported document run through
11760 // JMT by hand reproduces `-s jmt` rather than the writer defaults.
11761 if (o.knobs.seed) wopt.seed = static_cast<long>(o.knobs.seed);
11762 if (o.knobs.samples) wopt.max_samples = static_cast<double>(o.knobs.samples);
11763 if (o.knobs.t1 >= 0.0) wopt.max_simulated_time = o.knobs.t1;
11764 emit_document(line::io::jmt_write_jsim<double>(sn, wopt));
11765 return 0;
11766 }
11767 if (o.output != "readable" && o.output != "json")
11768 throw line::InputError("unknown -o '" + o.output +
11769 "'; accepted forms are: readable, json, jsimg");
11770 g_json_output = (o.output == "json");
11771 // See the layered path above for why a comma list emits one envelope
11772 // per analysis rather than one merged object.
11773 const std::vector<std::string> as = analysis_list(o.analysis);
11774 for (std::size_t i = 0; i + 1 < as.size(); ++i) {
11775 const int rc = solve_model_dispatch(o.arith, o.solver, as[i], o.file, o.knobs);
11776 if (rc != 0) return rc;
11777 poll_interrupt();
11778 }
11779 return solve_model_dispatch(o.arith, o.solver, as.back(), o.file, o.knobs);
11780}
11781
11782/**
11783 * Redirect a file descriptor into a temporary file for a scope, and give back
11784 * what was written to it.
11785 *
11786 * A TEMPORARY FILE AND NOT A PIPE, which was already the choice here and is
11787 * worth keeping deliberately: a pipe holds 64 KB and nobody is draining it, so
11788 * a solve that prints more than that would deadlock against its own output.
11789 *
11790 * IT IS AN OBJECT so that the restore happens on the way out of the scope
11791 * whatever happens inside it. The server's version below did the same dance
11792 * inline and could not have survived a throw between the dup2 and the restore,
11793 * which is survivable in a one-shot process and is a leaked file descriptor
11794 * per call in a host that calls this in a loop.
11795 */
11797 public:
11798 explicit FdCapture(int fd) : fd_(fd), saved_(-1), tfd_(-1) {
11799 if (fd_ < 0) return; // "do not capture", so the object is inert
11800 const char* tmpdir = std::getenv("TMPDIR");
11801 std::string path =
11802 std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + "/line-cli-cap-XXXXXX";
11803 std::vector<char> buf(path.begin(), path.end());
11804 buf.push_back('\0');
11805 tfd_ = ::mkstemp(&buf[0]);
11806 if (tfd_ < 0) return;
11807 path_.assign(&buf[0]);
11808 std::fflush(fd_ == 1 ? stdout : stderr);
11809 saved_ = ::dup(fd_);
11810 ::dup2(tfd_, fd_);
11811 }
11812
11813 // restore() closes the descriptor; only take() removes the backing file, so a
11814 // capture abandoned by a throw would leave one behind. The server calls
11815 // run_invocation_captured once per request, so that is a leak per FAILED
11816 // request, which is the same defect the JAR CLI had in its model staging.
11817 // take() clears path_ after removing, so this is a no-op on the normal path.
11819 restore();
11820 if (!path_.empty()) {
11821 std::remove(path_.c_str());
11822 path_.clear();
11823 }
11824 }
11825
11826 /** Restore the descriptor and read back what was written. Idempotent. */
11827 std::string take() {
11828 restore();
11829 if (path_.empty()) return std::string();
11830 std::ifstream in(path_.c_str());
11831 std::string out((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
11832 in.close();
11833 std::remove(path_.c_str());
11834 path_.clear();
11835 return out;
11836 }
11837
11838 bool active() const { return saved_ >= 0; }
11839
11840 private:
11841 void restore() {
11842 if (saved_ < 0) return;
11843 std::fflush(fd_ == 1 ? stdout : stderr);
11844 ::dup2(saved_, fd_);
11845 ::close(saved_);
11846 saved_ = -1;
11847 if (tfd_ >= 0) {
11848 ::close(tfd_);
11849 tfd_ = -1;
11850 }
11851 }
11852 FdCapture(const FdCapture&);
11853 FdCapture& operator=(const FdCapture&);
11854
11855 int fd_;
11856 int saved_;
11857 int tfd_;
11858 std::string path_;
11859};
11860
11861/**
11862 * Run one invocation with stdout captured, and return what it printed.
11863 *
11864 * SERVER MODE'S ONE PIECE OF MACHINERY. Every arm of this CLI writes its answer
11865 * with `printf`, which is the right thing for a command-line tool and leaves a
11866 * server nothing to send. Redirecting fd 1 around the call means the arms need
11867 * no server-aware variant and cannot drift from the command-line behaviour;
11868 * stderr is deliberately NOT captured, so a warning still reaches the operator's
11869 * console rather than being folded into the client's answer.
11870 */
11871std::string run_invocation_captured(Options o, int& rc) {
11872 // EVERY REQUEST STARTS FROM A CLEAN SLATE. Without this a request carrying
11873 // `-i jsimg` left g_jsim_input true and poisoned every later request on the
11874 // same server, which is a live bug in shipped code rather than a
11875 // hypothetical one.
11876 reset_invocation_state(false);
11877 FdCapture cap(1);
11878 if (!cap.active()) {
11879 rc = 3;
11880 return "line-cli: cannot create a capture file for the response\n";
11881 }
11882 std::string err;
11883 try {
11884 rc = run_invocation(o);
11885 } catch (const line::Error& e) {
11886 rc = 2;
11887 err = std::string("line-cli: ") + e.what() + "\n";
11888 } catch (const std::exception& e) {
11889 rc = 3;
11890 err = std::string("line-cli: unexpected failure: ") + e.what() + "\n";
11891 }
11892 std::string out = cap.take();
11893 // THE ERROR IS THE ANSWER when the solve failed: a client that receives an
11894 // empty message cannot tell a refusal from a model with no rows.
11895 return err.empty() ? out : out + err;
11896}
11897
11898/**
11899 * `-p/--port`: serve solve requests over a WebSocket, as `LineWebSocketServer`
11900 * does.
11901 *
11902 * THE PROTOCOL IS THE JAR's, unchanged: one text message per connection, whose
11903 * FIRST LINE is the comma-separated argument list and whose remainder is the
11904 * model document. The JAR overwrites the first two arguments with `--file` and
11905 * the path it staged the document at, so the client's own first two tokens are
11906 * placeholders; the same substitution happens here, which is what lets an
11907 * existing client talk to this server without knowing which binary answered.
11908 */
11909int run_server(const Options& base) {
11910 line::ws::Server server(base.port);
11911 std::printf("--------------------------------------------------------------------\n");
11912 std::printf("LINE Solver - Command Line Interface (C++)\n");
11913 std::printf("Copyright (c) 2012-2026, QORE Lab, Imperial College London\n");
11914 std::printf("Version %s. All rights reserved.\n", kVersion);
11915 std::printf("--------------------------------------------------------------------\n");
11916 std::printf("Running in server mode on port %d.\n", base.port);
11917 if (base.maxreq)
11918 std::printf("Quitting after %d request(s).\n", base.maxreq);
11919 std::fflush(stdout);
11920
11921 int served = 0;
11922 while (base.maxreq == 0 || served < base.maxreq) {
11923 const bool ok = server.serve_one([&](const std::string& msg) -> std::string {
11924 const std::string::size_type nl = msg.find('\n');
11925 if (nl == std::string::npos)
11926 return "line-cli: the request's first line is the argument list and its "
11927 "remainder is the model document; this message has no newline\n";
11928 const std::string argline = msg.substr(0, nl);
11929 const std::string model = msg.substr(nl + 1);
11930
11931 const char* tmpdir = std::getenv("TMPDIR");
11932 std::string path =
11933 std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + "/line-cli-req-XXXXXX";
11934 std::vector<char> nb(path.begin(), path.end());
11935 nb.push_back('\0');
11936 const int mfd = ::mkstemp(&nb[0]);
11937 if (mfd < 0) return "line-cli: cannot stage the client model\n";
11938 ::close(mfd);
11939 path.assign(&nb[0]);
11940 {
11941 std::ofstream mf(path.c_str());
11942 mf << model;
11943 }
11944
11945 // The argument list, with the first two tokens replaced by the
11946 // staged path exactly as `LineWebSocketServer.onMessage` replaces
11947 // them. A list SHORTER than two is the client's error and is
11948 // reported rather than padded, since padding would solve the
11949 // default model instead of the one it sent.
11950 std::vector<std::string> toks;
11951 std::string::size_type at = 0;
11952 while (at <= argline.size()) {
11953 const std::string::size_type comma = argline.find(',', at);
11954 toks.push_back(argline.substr(
11955 at, comma == std::string::npos ? std::string::npos : comma - at));
11956 if (comma == std::string::npos) break;
11957 at = comma + 1;
11958 }
11959 std::string result;
11960 if (toks.size() < 2) {
11961 result = "line-cli: the argument list needs at least two tokens; the first two "
11962 "are replaced by --file and the staged model path\n";
11963 } else {
11964 toks[0] = "--file";
11965 toks[1] = path;
11966 std::vector<char*> argv;
11967 std::vector<std::string> store;
11968 store.push_back("line-cli");
11969 for (std::size_t i = 0; i < toks.size(); ++i) store.push_back(toks[i]);
11970 for (std::size_t i = 0; i < store.size(); ++i)
11971 argv.push_back(const_cast<char*>(store[i].c_str()));
11972 int rc = 0;
11973 try {
11974 Options ro = parse_args(static_cast<int>(argv.size()), &argv[0]);
11975 // The server's own flags never travel into a request: a
11976 // client that sent `-p` would otherwise make the server
11977 // recurse into a second listener on the same process.
11978 ro.port = 0;
11979 ro.maxreq = 0;
11980 result = run_invocation_captured(ro, rc);
11981 } catch (const line::Error& e) {
11982 result = std::string("line-cli: ") + e.what() + "\n";
11983 }
11984 }
11985 std::remove(path.c_str());
11986 return result;
11987 });
11988 // A dropped client is not a reason to stop serving, and it does not
11989 // count against --maxreq either: the JAR counts REQUESTS, and a peer
11990 // that vanished before sending one made none.
11991 if (ok) ++served;
11992 }
11993 return 0;
11994}
11995
11996/**
11997 * `--generate`: draw a random model and print it, solving nothing.
11998 *
11999 * `{"kind":"network", ...}` emits the `line-model` JSON document that every
12000 * reader in the project already takes, so the generated model round-trips
12001 * through `-f` and through `load_model` in R and Python without a second
12002 * format. `{"kind":"layered", ...}` emits the `.lqnx` document instead,
12003 * because that is the only interchange the layered readers all share; it is
12004 * plain text on standard output rather than a collected JSON document, since
12005 * an XML string is not one.
12006 *
12007 * THE ARITHMETIC IS ALWAYS double. Both generators draw from
12008 * `java.util.Random`, whose output is a double by definition, so generating at
12009 * `--arith exact` would fabricate an exactness the draw never had. A caller who
12010 * wants the model solved in another arithmetic reads it back with that --arith.
12011 */
12012int run_generate(const Options& o) {
12013 const line::reg::Json spec = read_generate_spec(o.generate);
12014 if (!spec.is_object())
12015 throw line::InputError("--generate takes a JSON object, e.g. "
12016 "{\"kind\":\"network\",\"queues\":3,\"closedClasses\":1}");
12017 const std::string kind = gen_str(spec, "kind", "network");
12018 if (kind == "network") {
12019 static const char* const keys[] = {
12020 "kind", "seed", "name", "queues", "delays", "openClasses", "closedClasses",
12021 "schedStrat", "routingStrat", "distribution", "cclassJobLoad", "varyingServiceRates",
12022 "multiServerQueues", "randomCSNodes", "multiChainCS", "topology"};
12023 gen_check_keys(spec, keys, sizeof(keys) / sizeof(keys[0]));
12025 if (spec.contains("seed")) g.set_seed(gen_int(spec, "seed", 0));
12026 g.set_model_name(gen_str(spec, "name", "nw"));
12027 g.set_sched_strat(gen_str(spec, "schedStrat", "randomize"));
12028 g.set_routing_strat(gen_str(spec, "routingStrat", "randomize"));
12029 g.set_distribution(gen_str(spec, "distribution", "randomize"));
12030 g.set_cclass_job_load(gen_str(spec, "cclassJobLoad", "randomize"));
12031 g.set_varying_service_rates(gen_bool(spec, "varyingServiceRates", false));
12032 g.set_multi_server_queues(gen_bool(spec, "multiServerQueues", false));
12033 g.set_random_cs_nodes(gen_bool(spec, "randomCSNodes", false));
12034 g.set_multi_chain_cs(gen_bool(spec, "multiChainCS", false));
12035 const std::string topo = gen_str(spec, "topology", "rand");
12036 if (topo == "rand")
12038 else if (topo == "cyclic")
12040 else
12041 throw line::InputError("--generate: unknown topology '" + topo +
12042 "'; accepted forms are: rand, cyclic");
12043 // `delays` defaults to -1, which is the generator's "decide for me":
12044 // it then draws the split between queues and delays itself.
12045 const long nq = gen_int(spec, "queues", 1);
12046 const long nd = gen_int(spec, "delays", -1);
12047 const long no = gen_int(spec, "openClasses", 0);
12048 const long nc = gen_int(spec, "closedClasses", 1);
12049 line::qn::Network<double> m = g.generate(static_cast<int>(nq), static_cast<int>(nd),
12050 static_cast<int>(no), static_cast<int>(nc));
12051 emit_document(line::io::network_json_envelope(m.get_struct()).dump(2));
12052 return 0;
12053 }
12054 if (kind == "layered") {
12055 static const char* const keys[] = {
12056 "kind", "seed", "name", "clients", "levels", "tasks", "processors", "populationRange",
12057 "thinkTimeRange", "taskMultiRange", "procMultiRange", "hostDemandRange",
12058 "synchCallRange", "taskInfProbability", "procInfProbability"};
12059 gen_check_keys(spec, keys, sizeof(keys) / sizeof(keys[0]));
12061 if (spec.contains("seed")) g.set_seed(gen_int(spec, "seed", 0));
12062 g.set_model_name(gen_str(spec, "name", "lnw"));
12063 g.set_population_range(gen_range(spec, "populationRange", line::gen::Range(1, 1)));
12064 g.set_think_time_range(gen_range(spec, "thinkTimeRange", line::gen::Range(1, 1)));
12065 g.set_task_multi_range(gen_range(spec, "taskMultiRange", line::gen::Range(1, 1)));
12066 g.set_proc_multi_range(gen_range(spec, "procMultiRange", line::gen::Range(1, 1)));
12067 g.set_host_demand_range(gen_range(spec, "hostDemandRange", line::gen::Range(1, 1)));
12068 g.set_synch_call_range(gen_range(spec, "synchCallRange", line::gen::Range(1, 1)));
12069 g.set_task_inf_probability(gen_real(spec, "taskInfProbability", 0.0));
12070 g.set_proc_inf_probability(gen_real(spec, "procInfProbability", 0.0));
12071 const long ncl = gen_int(spec, "clients", 1);
12072 const long nlv = gen_int(spec, "levels", 1);
12073 const long ntk = gen_int(spec, "tasks", 1);
12074 const long npr = gen_int(spec, "processors", 1);
12076 g.generate(static_cast<int>(ncl), static_cast<int>(nlv), static_cast<int>(ntk),
12077 static_cast<int>(npr));
12078 // BUILT BEFORE IT IS PRINTED, so a model the finalizer refuses is a
12079 // failure here rather than a document that only fails when read back.
12080 b.build();
12081 std::printf("%s\n", line::lqn::lqnx_to_string(b.model(), g.model_name()).c_str());
12082 return 0;
12083 }
12084 throw line::InputError("--generate: unknown kind '" + kind +
12085 "'; accepted forms are: network, layered");
12086}
12087
12088/**
12089 * Everything `main` did, with the arguments already in hand.
12090 *
12091 * SPLIT OUT SO A HOST CAN CALL IT. The binary's `main` is now ten lines in its
12092 * own translation unit, and this is the body both it and the C ABI run, so a
12093 * command line and an in-process call take the same path by construction
12094 * rather than by review.
12095 *
12096 * `no_args` replaces the `argc == 1` that decided the brief help, because an
12097 * in-process caller has no argv[0] to count and would otherwise never see it.
12098 */
12099int dispatch(const Options& in, bool allow_server, bool no_args) {
12100 {
12101 Options o = in;
12102 // Before any arm runs: `read_model` raises the priority warning and has
12103 // no knobs of its own to read the level from.
12104 if (!o.knobs.verbosity.empty()) g_verbosity = o.knobs.verbosity;
12105 // Solver console: set for the whole process, so that the model compile
12106 // narrates too -- LineConsole::writes() falls back to the session level
12107 // when no run is open, and reading the model happens before any solver
12108 // exists. `-v debug` (and its `verbose` spelling) is the ONLY way in.
12110 g_verbosity == "silent" ? line::util::VerboseLevel::SILENT
12111 : (g_verbosity == "debug" || g_verbosity == "verbose")
12114 if (o.help_all) {
12115 print_help();
12116 return 0;
12117 }
12118 if (o.help || no_args) {
12119 print_brief_help();
12120 return 0;
12121 }
12122 if (o.version) {
12123 std::printf("line-cli %s\n", kVersion);
12124 return 0;
12125 }
12126 // A warning is not a failure: every dependency the check probes is
12127 // optional, so the command exits 0 whenever the check itself ran.
12128 if (o.install) {
12129 install_check();
12130 return 0;
12131 }
12132 if (o.list) {
12133 list_api();
12134 return 0;
12135 }
12136 // BEFORE the model-reading path, and not inside it: --generate PRODUCES
12137 // a model rather than consuming one, so -f is neither required nor read.
12138 if (o.generate_given) return run_generate(o);
12139 if (o.find_solver) return find_solver_report(o);
12140 if (o.port) {
12141 // A HOST DOES NOT GET A SERVER BY ACCIDENT. `-p` blocks the calling
12142 // thread and opens a listening socket, so a host forwarding a
12143 // user's argument string would otherwise be handing that user both.
12144 if (!allow_server)
12145 throw line::InputError(
12146 "-p runs the solver as a server, which this caller did not permit");
12147 // `-f` and `-p` together would be two model sources for one run;
12148 // the request carries the model in server mode, so a file named on
12149 // the command line could only be ignored.
12150 if (!o.file.empty())
12151 throw line::InputError(
12152 "-p runs the solver as a server, where each request carries its own model; "
12153 "-f names a model on the command line and the two cannot both be the source");
12154 return run_server(o);
12155 }
12156 return run_invocation(o);
12157 }
12158}
12159
12160namespace line {
12161namespace cli {
12162
12163Response run(const Request& req) {
12164 Response res;
12165 reset_invocation_state(false);
12166 g_interrupt_cb = req.interrupt;
12167 g_interrupt_user = req.interrupt_user;
12168 if (req.has_stdin) set_stdin_override(req.stdin_text);
12169
12170 std::vector<std::string> docs;
12171 if (req.collect_json) g_json_sink = &docs;
12172
12173 // argv[0] IS SYNTHESISED HERE. parse_args skips it, as every argv parser
12174 // does, and a host that passed its own would have its first real argument
12175 // eaten. Requiring the host to prepend one would be requiring it to know
12176 // that, which is exactly the kind of knowledge a boundary should absorb.
12177 std::vector<std::string> store;
12178 store.push_back("line-cli");
12179 for (std::size_t i = 0; i < req.argv.size(); ++i) store.push_back(req.argv[i]);
12180 std::vector<char*> argv;
12181 for (std::size_t i = 0; i < store.size(); ++i) argv.push_back(&store[i][0]);
12182
12183 {
12184 // THE CAPTURES ENCLOSE THE try/catch, NOT JUST THE CALL. A failure
12185 // message the arms would have written to fd 2 is therefore captured
12186 // too, rather than escaping to the host's console -- which for R means
12187 // escaping to a terminal the user may not even be looking at. They are
12188 // plain scoped objects taking a sentinel fd for "do not capture",
12189 // because a std::optional here is the same thing with a longer name.
12190 FdCapture out(req.capture_stdout ? 1 : -1);
12191 FdCapture err(req.capture_stderr ? 2 : -1);
12192 try {
12193 res.exit_code = dispatch(parse_args(static_cast<int>(argv.size()), &argv[0]),
12194 req.allow_server, req.argv.empty());
12195 } catch (const InterruptRequested& e) {
12196 res.exit_code = 2;
12197 res.error = e.what();
12198 res.error_id = "line:interrupt";
12199 } catch (const line::Error& e) {
12200 res.exit_code = 2;
12201 res.error = e.what();
12203 } catch (const std::exception& e) {
12204 res.exit_code = 3;
12205 res.error = std::string("unexpected failure: ") + e.what();
12206 res.error_id = "line:internal";
12207 }
12208 if (req.capture_stdout) res.text = out.take();
12209 if (req.capture_stderr) res.diagnostics = err.take();
12210 }
12211
12212 g_json_sink = nullptr;
12213 g_interrupt_cb = nullptr;
12214 g_interrupt_user = nullptr;
12215 res.documents = docs;
12216 reset_invocation_state(false);
12217 return res;
12218}
12219
12220int main_body(int argc, char** argv) {
12221 try {
12222 return dispatch(parse_args(argc, argv), true, argc == 1);
12223 } catch (const line::Error& e) {
12224 std::fprintf(stderr, "line-cli: %s\n", e.what());
12225 return 2;
12226 } catch (const std::exception& e) {
12227 std::fprintf(stderr, "line-cli: unexpected failure: %s\n", e.what());
12228 return 3;
12229 }
12230}
12231
12232} // namespace cli
12233} // 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:31
Malformed or inconsistent input (dimensions, negative populations, ...).
Definition error.h:37
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 proceed on this instance (singular matrix, ...).
Definition error.h:43
Requested feature or arithmetic mode is not ported yet.
Definition error.h:49
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.
void set_topology(TopologyKind kind)
Pick one of the two shipped topology generators.
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:610
LnTranSolution get_tran_avg()
Port of @SolverLN/getTranAvg: the block-diagonal aggregate transient.
Definition solver_ln.h:591
std::size_t nlayers() const
Definition solver_ln.h:688
std::vector< LnCdf > get_cdf_respt()
Port of @SolverLN/getCdfRespT: the per-entry response-time distribution.
Definition solver_ln.h:552
LnSolution< T > get_ensemble_avg()
Port of getEnsembleAvg: run the iteration and aggregate onto LQN elements.
Definition solver_ln.h:528
const std::vector< qn::Layer< T > > & layers() const
Definition solver_ln.h:689
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.
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)
#define LINE_CLI_TABLE_LADDER(FN)
int find_solver_report(const Options &o)
Everything one invocation does once the arguments are in hand.
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
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_solver_name(AutoSolver s)
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)
AutoToken auto_resolve_token(const std::string &raw)
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.
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,...
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...
CtmcTransient< T > solver_ctmc_transient_analyzer(const NetworkStruct< T > &sn, const CtmcOptions &opt, const T &t0, const T &t1, const std::vector< T > &grid=std::vector< T >())
Port of solver_ctmc_transient_analyzer.m.
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.
CtmcFirstPassage ctmc_cdf_firstpasst(const NetworkStruct< T > &, const CtmcSolution< T > &d, const Matrix< double > &A, const Matrix< double > &B, const std::string &method="expm")
Port of @@SolverCTMC/getCdfFirstPassT.m: the distribution of the FIRST PASSAGE TIME from state set A ...
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.
CtmcFirstPassageMoments< T > ctmc_firstpasst_moments(const NetworkStruct< T > &, const CtmcSolution< T > &d, const Matrix< double > &A, const Matrix< double > &B, std::size_t nmax=3)
Port of @@SolverCTMC/getFirstPassTMoments.m: moments of order 1..nmax of the first passage time from ...
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 at the runner's own method resolution, so the exported system is the...
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 ...
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:349
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::LqnStruct< T > read_layered_model(const std::string &path)
Read a layered model from either interchange: the LINE model.json or the LQNS .lqnx.
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:490
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:906
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:498
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:671
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:567
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:700
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:810
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
const char * event_to_text(EventType e)
Definition lang_types.h:137
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),...
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:245
LqnModel< T > read_lqnx_model(const std::string &path)
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:550
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.
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)
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)
SolverFLD.getFeatureSet, transcribed, MINUS what the requested method cannot evaluate – the port of @...
Marginal< T > to_marginal(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< T > &state_i, const std::vector< std::size_t > &phasesz, const std::vector< std::size_t > &phaseshift, std::size_t nvar=0)
Port of State.toMarginal for a STATION, one state row at a time.
Definition state.h:130
FeatureSet nc_feature_set(const std::string &method)
SolverNC.getFeatureSet, 48 names, transcribed unchanged.
FeatureSet mva_feature_set(const std::string &raw_method)
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.
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.
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)
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:327
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
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....
SsaProbReport solver_ssa_prob(const qn::NetworkStruct< T > &sn, const SsaSerialOptions &opt)
The whole -a prob report over the model's DEFAULT INITIAL STATE, which is the state SolverCTMC's own ...
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.
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)
@@SolverSSA/runAnalyzer itself: the engine the method selects, then the result assembly the reference...
qn::NetworkStruct< T > sn_with_ssa_cache_split(const qn::NetworkStruct< T > &base, const std::vector< SsaCacheRatio > &cache)
The struct with the cache split the SIMULATION MEASURED, visits rebuilt.
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:574
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:974
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:196
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,...
The SolverSSA entry surface: a port of @@SolverSSA/runAnalyzer.m's method whitelist,...
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
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 > 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
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.
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< 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...
std::vector< double > F
CDF at t, clamped to [0, 1].
std::vector< double > t
the grid, 1000 points to the horizon
std::vector< std::size_t > target
resolved 0-based rows
std::vector< std::size_t > source
resolved 0-based rows; empty = conditional stationary
std::vector< double > f
density at t
[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 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.
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 ...
Options of SolverENV.
Definition solver_env.h:113
std::string method
The inter-stage coupling: meanfield is the reference's default.
Definition solver_env.h:125
std::string stage_solver
Which solver runs each FLAT stage: the fluid transient or the enumerated CTMC.
Definition solver_env.h:123
fluid::FluidOptions stage
Options handed to each stage solver.
Definition solver_env.h:129
double stage_cutoff
options.cutoff of a CTMC stage, read only when stage_solver is ctmc.
Definition solver_env.h:134
double timespan_end
options.timespan(2) of the inner solver: the transient horizon.
Definition solver_env.h:136
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:149
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
Solutions of f(x) = 0, each a variable -> expression map.
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::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::vector< double > xvec
the converged fluid state
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:68
std::string log_path
model.getLogPath, the logPath attribute
Definition jmt_writer.h:70
std::string file_name
base name; the header echoes it plus .jsimg
Definition jmt_writer.h:69
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:119
int replications
options.config.replications: the independent JSIM runs the transient ensemble of default averages ove...
Definition solver_jmt.h:134
std::string method
default | jsim | jmva | jmva.<alg>
Definition solver_jmt.h:120
bool keep
keep the scratch directory after the solve
Definition solver_jmt.h:123
double samples
samples per measure; raised to 5000 below
Definition solver_jmt.h:121
What jmt_prob_aggr reports: the system probability and the per-station ones.
Definition jmt_logs.h:773
std::vector< bool > station_seen
the same, per station
Definition jmt_logs.h:777
bool sys_seen
whether the joint state occurred at all
Definition jmt_logs.h:776
double sys
P(the whole network is in the declared state).
Definition jmt_logs.h:774
std::vector< double > station
P(station i holds its declared per-class counts).
Definition jmt_logs.h:775
Transient averages over independent replications, on one time grid.
Definition jmt_logs.h:874
std::vector< double > t
Definition jmt_logs.h:875
std::vector< std::vector< std::vector< double > > > UNt
Definition jmt_logs.h:878
std::size_t valid
Replications that produced a usable trajectory.
Definition jmt_logs.h:881
std::vector< std::vector< std::vector< double > > > TNt
Definition jmt_logs.h:879
std::vector< std::vector< std::vector< double > > > QNt
QNt[ist-1][r] over t.
Definition jmt_logs.h:877
The result of a JMT solve: the shared AvgResult plus what only JMT reports.
Definition solver_jmt.h:749
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:755
mva::AvgResult< T > avg
Definition solver_jmt.h:750
Matrix< T > ACI
confidence half-widths, empty when disabled
Definition solver_jmt.h:751
Matrix< T > DropRateNfcr
(nregions x nclasses) carried and lost rate
Definition solver_jmt.h:753
The system trajectory: one per-class block per station, on a common grid.
Definition jmt_logs.h:480
static Distrib exp_rate(const T &r)
Definition lang_types.h:945
static constexpr double FineTol
Definition lang_types.h:760
static constexpr double CoarseTol
Definition lang_types.h:761
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::map< std::string, LdesCacheMetrics > cache_metrics
Per-cache metrics, keyed by the Cache NODE name.
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 > UNfcr
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
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....
Options of SolverLN.
Definition solver_ln.h:266
getSensitivityTable of the ensemble: the layer tables under a Layer column.
Definition solver_ln.h:434
std::vector< Row > rows
Definition solver_ln.h:439
std::string method
The summary label: the common branch, or "mixed" when they differ.
Definition solver_ln.h:443
The LQN-level answer, indexed by element 1..nidx.
Definition solver_ln.h:371
std::vector< T > WN
Definition solver_ln.h:372
std::vector< bool > defined_W
Definition solver_ln.h:373
std::vector< bool > defined_U
Definition solver_ln.h:373
std::vector< T > QN
Definition solver_ln.h:372
std::vector< bool > defined_R
Definition solver_ln.h:373
std::vector< T > RN
Definition solver_ln.h:372
std::vector< bool > defined_Q
Definition solver_ln.h:373
bool is_bound
True when the numbers are a BOUND (method = mw.upper / mw.lower) rather than the fixed point.
Definition solver_ln.h:382
std::vector< T > UN
Definition solver_ln.h:372
std::vector< bool > defined_T
Definition solver_ln.h:373
std::vector< T > TN
Definition solver_ln.h:372
The layered transient: one block per layer, plus how it was produced.
Definition solver_ln.h:425
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:409
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:80
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
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:124
std::string timescale
options.config.timescale: "auto", "discrete" or "continuous".
Definition mam_types.h:116
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:104
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:76
std::string fj_tmode
options.config.fj_tmode: which route computeT.m takes to the T matrix, 'NARE' (Riccati,...
Definition mam_types.h:110
double slotlength
options.config.slotlength: the slot in model time units.
Definition mam_types.h:118
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
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:31
std::string multiserver
Definition mva_types.h:36
std::string method
Definition mva_types.h:32
std::string fork_join
options.config.fork_join: which fork-join arm the fixed point takes.
Definition mva_types.h:51
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
std::vector< T > C
Definition mva_types.h:98
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:51
std::vector< T > nir
jobs per class
Definition state.h:53
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2157
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2158
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 retrieval latency, 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
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:69
std::size_t samples
Reaction firings to simulate; options.samples in the reference.
Definition ssa_types.h:73
double warmupfrac
options.config.warmupfrac: the leading fraction of the path discarded before the means are taken.
Definition ssa_types.h:88
std::string method
default and nrm both select the Next Reaction Method here.
Definition ssa_types.h:71
unsigned long seed
options.seed; LINE's own default is 23000.
Definition ssa_types.h:75
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...
double cutoff
< 0 = the reference's automatic value
The serial analyzer's return: the metric table, the path, and the stream.
What the analyzer returns, in the same shape as the MVA and fluid results.
Definition ssa_types.h:101
std::vector< double > XN
Definition ssa_types.h:103
std::vector< double > CN
Definition ssa_types.h:103
Matrix< double > UN
Definition ssa_types.h:102
Matrix< double > RN
Definition ssa_types.h:102
double simulated_time
Simulated time the metrics are averaged over; the reference's totalTime.
Definition ssa_types.h:115
Matrix< double > TN
Definition ssa_types.h:102
std::size_t samples
Reaction firings actually performed.
Definition ssa_types.h:117
Matrix< double > QN
Definition ssa_types.h:102
std::string method
The concrete algorithm, as the reference's method.
Definition ssa_types.h:113
UQ.getInterval: the RANGE of every metric over the support of the Priors.
Definition solver_uq.h:726
std::string method
mvainterval or sampled.
Definition solver_uq.h:736
bool exact
True when the interval is the attained hull rather than a sampled range.
Definition solver_uq.h:734
Matrix< T > Qlo
(nstations x nclasses) lower and upper endpoints of each metric.
Definition solver_uq.h:728
std::string why
On the sampled path, the condition that disqualified the exact one.
Definition solver_uq.h:738
T Xlo
System throughput and total response time; the EXACT path only.
Definition solver_uq.h:730
UQ.defaultOptions plus the stream the Monte Carlo design draws from.
Definition solver_uq.h:85
std::string method
default | dd | quad | mc, MATLAB UQ.listValidMethods.
Definition solver_uq.h:92
std::size_t samples
Nodes per continuous Prior, or design points under mc; the reference's options.samples,...
Definition solver_uq.h:99
unsigned long seed
The Monte Carlo stream; unread by a quadrature design, which draws nothing.
Definition solver_uq.h:101
What solver_uq_run_analyzer returns.
Definition solver_uq.h:125
std::vector< T > weights
The design weights, summing to 1.
Definition solver_uq.h:131
std::string method
The RESOLVED discretization method: quad or mc.
Definition solver_uq.h:137
std::vector< mva::AvgResult< T > > points
The result at each design point, in design order.
Definition solver_uq.h:129
std::vector< PriorSite< T > > sites
Where the Priors were found.
Definition solver_uq.h:135
std::vector< UqDesignPoint< T > > design
The alternatives each point substituted, one per site.
Definition solver_uq.h:133
mva::AvgResult< T > avg
The prior-weighted expectation of every metric, (nstations x nclasses).
Definition solver_uq.h:127
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.