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/jsim_reader.h"
42#include "line/io/pnml.h"
45#include "line/num/number.h"
47#include "line/reg/registry.h"
87#include "line/util/error.h"
88#include "line/util/websocket.h"
89
90namespace {
91
92const char* kVersion = "0.1.0";
93
94/**
95 * Whether `-a avg` prints its table as JSON, i.e. `-o json`.
96 *
97 * A FILE-SCOPE FLAG, and the one place in this port that has one. The library
98 * under `include/line/` keeps no global mutable state, deliberately, so a
99 * pybind11 or MEX host can call it reentrantly; `line_cli.cpp` is a `main()`
100 * translation unit and the output format is a property of ONE process
101 * invocation, decided before any solve begins and never changed after. The
102 * alternative is threading a bool through `solve_model_{mva,nc,mam,ba}`,
103 * `solve_ctmc_avg`, `solve_fluid_avg` and `solve_ssa_avg`, three of which do
104 * not take the `Knobs` struct at all, to carry a value none of them varies.
105 */
106bool g_json_output = false;
107
108/**
109 * Solve a Network model.json with SolverMVA and print its AvgTable.
110 *
111 * The columns and their order -- Station, JobClass, then QLen, Util, RespT,
112 * ResidT, ArvR, Tput -- are the ones `parity/compare_parity.py` parses, so the
113 * printed table is directly diffable against the MATLAB and Python rows. A
114 * (station, class) pair with no presence at all (every metric zero) is dropped,
115 * as `getAvgTable` drops its unvisited rows.
116 */
117/**
118 * The solver knobs the CLI exposes, gathered so the dispatcher can check them
119 * against the solver actually chosen.
120 *
121 * A sentinel means "not given" rather than a default, because the distinction
122 * is what lets an option be REFUSED for a solver that has no such setting
123 * instead of being accepted and dropped. That silent acceptance is the defect
124 * this struct exists to prevent: `--samples 1e6` was previously taken without
125 * complaint and the run still used 10000.
126 */
127struct Knobs {
128 std::string method;
129 double tol = -1.0; // < 0 = not given
130 double iter_tol = -1.0;
131 int iter_max = -1;
132 // --max-states, `options.config.maxStates`: the truncation level of an OPEN
133 // agent's queue-length dimension in SolverAG. < 0 = not given, so AgOptions
134 // keeps its own 100. It is a TRUNCATION and therefore part of the ANSWER,
135 // not a budget: a run truncated at 100 that the caller asked to truncate at
136 // 500 is a different number reported as theirs, which is why it travels
137 // rather than being dropped the way the execution backends are.
138 long long max_states = -1;
139 // --multiserver, the AMVA rule that decides WHICH algorithm serves a
140 // multiserver model. Empty = not given, so the solver keeps its default:
141 // a rule this CLI silently dropped made every delegated solve answer under
142 // 'default' while reporting the caller's choice.
143 std::string multiserver;
144 // --fork-join, `options.config.fork_join`: WHICH fork-join arm the shared
145 // fixed point takes on a model with a Fork. Empty = not given, so the solver
146 // keeps 'default' (the MMT transform); 'ht' is Heidelberger-Trivedi, which
147 // is a different answer to the same model rather than a faster one.
148 std::string fork_join;
149 // The solver console has NO knob of its own: it IS VerboseLevel::DEBUG,
150 // so `-v debug` is what asks for the running progress log.
151 std::size_t samples = 0; // 0 = not given
152 unsigned long seed = 0;
153 double cutoff = -1.0; // < 0 = not given
154 // --cutoff AS A MATRIX, `r1c1,r1c2;r2c1,r2c2`: the reference's
155 // `options.cutoff` is a (station x class) table wherever a model needs a
156 // different truncation per station, and reading only the scalar form left
157 // `atof` silently taking the first number, i.e. truncating every station at
158 // the first station's first class. Empty = not given.
159 std::vector<std::vector<std::size_t>> cutoff_mat;
160 /** True when `--cutoff` was given in EITHER spelling. */
161 bool has_cutoff() const { return cutoff >= 0.0 || !cutoff_mat.empty(); }
162 // --force, `options.force`: downgrade SolverCTMC's memory pre-gate from a
163 // refusal to a warning. The gate exists because the alternative to refusing
164 // is the OOM killer, so this is opt-in and never a default.
165 bool force = false;
166 // --fj-accuracy / --fj-tmode, `options.config.fj_accuracy` and
167 // `options.config.fj_tmode` of solver_mam_fj.m. The first is the FJ_codes
168 // truncation C of the queue-length difference between the two branches and
169 // is the accuracy knob of that approximation; the second picks the route to
170 // the T matrix. Kept apart from --tol/--iter_max for the same reason
171 // --mdd-tol is: C is a STATE-SPACE bound, not a convergence threshold.
172 int fj_accuracy = 0; // 0 = not given
173 std::string fj_tmode; // empty = not given
174 // --timescale, `options.config.timescale` of sn_is_discrete_time.m. "auto"
175 // lets the distributions decide whether the model is slotted; "discrete"
176 // and "continuous" force the reading, the first raising when the model
177 // mixes lattice and non-lattice laws rather than solving the wrong time
178 // scale. The slot itself comes from the SHARED `--slotlength` below, the
179 // same one SolverNC's discrete product form reads.
180 std::string timescale; // empty = not given
181 // --mdd-tol / --mdd-maxiter, the level iteration of `-s ctmc --method mdd`.
182 // They are DELIBERATELY not --tol / --iter_tol: that iteration is an INNER
183 // numerical solve whose fixed point is verified against the model's
184 // population invariant at 1e-6, so an AMVA-sized tolerance converges short
185 // of it and trips the guard. The reference keeps them apart for the same
186 // reason (options.config.mdd_tol, not options.iter_tol).
187 double mdd_tol = -1.0; // < 0 = not given
188 int mdd_maxiter = 0; // 0 = not given
189 // --level, the hierarchy level of the pbh/cbh/sib families and the
190 // iteration count of pbk/bjbk. 0 = not given, so BaOptions keeps its 2.
191 int level = 0;
192 // --busyperiod / --busyperiod-subnet, the orders and the subnetwork of
193 // `-a busyperiod`. The flag names are `ldes_cli`'s, so ONE spelling drives
194 // the transform (solver_nc_busyp) and the simulation. The subnetwork has no
195 // default -- a busy period is defined for a named set of stations and
196 // choosing one here would answer about a subnetwork the caller never
197 // named -- while the orders default to the ordinary busy period, 1.
198 std::vector<std::size_t> busy_orders;
199 std::vector<std::size_t> busy_subnet;
200 // --qrf-params / --qrf-alpha, the blocking parameterisation and the
201 // load-dependent scaling of the QRF arms of SolverBA. Both are JSON, given
202 // inline or as a path; empty = not given.
203 std::string qrf_params;
204 std::string qrf_alpha;
205 // --tspan, the horizon the transient CTMC analyses integrate over. There is
206 // no default: pi(t) on an unstated horizon is not a quantity, and picking
207 // one here would answer a question the caller did not ask.
208 double t0 = 0.0, t1 = -1.0; // t1 < 0 = not given
209 std::size_t node = 0; // --node, 1-based; 0 = not given
210 // --class and --marg-states, the second and third arguments of
211 // `@@SolverMVA/getProbMarg`. The class is 1-based and 0 = not given, i.e.
212 // every class; the state list is the reference's `state_m` and empty = not
213 // given, i.e. the default range each of the three laws picks for itself.
214 // They are NOT folded into --node: a marginal is indexed by a PAIR, and one
215 // flag carrying both would make "station 2" and "class 2" the same token.
216 std::size_t jobclass = 0; // --class, 1-based; 0 = not given
217 std::vector<long> marg_states; // --marg-states; empty = not given
218 // --warmupfrac, `options.config.warmupfrac`: the leading fraction of an SSA
219 // path discarded before the means are taken. The JAR CLI has carried it
220 // since the SSA branch existed and this port did not, so a delegated solve
221 // asking for a warmup discard silently kept the whole transient.
222 // < 0 = not given, so the engine keeps its own 0.
223 double warmupfrac = -1.0;
224 // --pstar, `options.config.pstar`: the exponent of the fluid p-norm
225 // smoothing (Ruuskanen et al., PEVA 151 (2021), eq. (26)). It selects the
226 // DRIFT the matrix method integrates, so a solve that never receives it
227 // returns the unsmoothed mean-field fixed point under the caller's choice.
228 // < 0 = not given.
229 double pstar = -1.0;
230 // --notation, which form of the exported ODE document is wanted; empty = not
231 // given, and only `-a odes` has one.
232 std::string notation;
233 // --cdf-algorithm, `options.config.algorithm` of `@@SolverNC/getCdfRespT`:
234 // 'exact' is the pfqn_stdf sojourn-time inversion, 'rd' the pfqn_stdf_heur
235 // reduction. Empty = not given, so the solver keeps the reference's 'exact'.
236 // It is NOT --method: the ladder that computes the constants and the
237 // algorithm that inverts the sojourn law are separate choices, and folding
238 // them would make one name silently select the other.
239 std::string cdf_algorithm;
240 // -s ctmc -a firstpasst: the two state sets of `getCdfFirstPassT(A, B)`.
241 // Each is either a 1-based index list into the state space ("3,5") or
242 // semicolon-separated state rows ("0,2;1,1"), resolved against the space
243 // the engine enumerates -- rows travel across the boundary because the two
244 // enumerations need not order (or even purge) states identically.
245 std::string passage_from;
246 std::string passage_into;
247 // "expm" (default) or "lt", `options.config.passage_method` of the reference
248 std::string passage_method;
249 // -s ctmc -a firstpasstmom: the highest moment order, `nmax` of
250 // `getFirstPassTMoments(A, B, nmax)`. 0 means the reference's default of 3.
251 std::size_t passage_orders = 0;
252 // --perm-engine, the permanent estimator of `@@SolverNC/getProbSysMarg.m`:
253 // 'exact' is Ryser's expansion with column multiplicities, and 'spm',
254 // 'bethe', 'heur', 'huberlaw', 'adapart' are the five approximations. It is
255 // NOT --method: the ladder that computes the normalizing constant and the
256 // estimator that evaluates the permanent are separate choices. The five
257 // approximations REFUSE a demand matrix with a structural zero rather than
258 // flooring it, since they need full support. 'spm' is the only one that does
259 // not expand the matrix to order sum(N), so it is the one whose cost does
260 // not grow with the population and whose error falls as it grows.
261 std::string method_perm = "exact";
262 // --symbolic, `options.config.symbolic` of `@@SolverFLD/getJacobian`: auto
263 // to search for a line-sage-rest backend, a URL, an image name, or none to
264 // stay with the locally differentiated Jacobian. Empty = not given, i.e.
265 // auto. --equilibria is the reference's fourth output, which is REQUESTED
266 // and not implied: solving f(x) = 0 needs the backend, so implying it would
267 // turn a Jacobian this port answers on its own into one that fails without
268 // a container.
269 std::string symbolic;
270 bool equilibria = false;
271 // ---- the layered path's own knobs, on the same not-given discipline ----
272 // They are refused on the Network path rather than dropped, exactly as the
273 // ones above are refused for a solver that has no such setting.
274 bool no_interlocking = false; // --no-interlocking was passed
275 int repeat = 0; // --repeat; 0 = not given, i.e. one run
276 std::string layer_solver; // --layer-solver; empty = not given
277 /**
278 * `--stage-solver`, which solver runs each stage of an ENVIRONMENT.
279 *
280 * IT IS NOT `--layer-solver`, and folding the two would be wrong: a LAYER of
281 * an LQN is solved in STEADY STATE and a STAGE of a random environment is
282 * solved TRANSIENTLY, so their admissible solver sets are different sets for
283 * different reasons. Empty = not given, i.e. the coupling's own default
284 * (fluid for the mean-field one, ctmc for the state-vector one).
285 */
286 std::string stage_solver;
287 // --ln-transient / --ln-transient-channels, the coupling of the layered
288 // transient and which inter-layer channels it injects. Empty = not given.
289 std::string ln_transient;
290 std::string ln_transient_channels;
291 // --sens-method / --sens-scheme / --sens-step, the name-value contract of
292 // getSensitivityTable. They are NOT --method: --method names the LN update
293 // (default / moment3 / mwba.*), and the branch that differentiates it is a
294 // separate choice. Empty / <= 0 = not given.
295 std::string sens_method;
296 std::string sens_scheme;
297 double sens_step = -1.0;
298 // --uq-solver, the engine SolverUQ runs at each design point. Empty is not
299 // a default here but a missing argument: UQ computes nothing itself, so
300 // there is no engine to fall back to, and `-s uq` without it is refused.
301 std::string uq_solver;
302 // --tran-points, the resolution of the uniform transient grid the ENV
303 // mean-field coupling sums its exit metrics over. It is that coupling's
304 // accuracy knob, not a cosmetic one: the sum is a Riemann-Stieltjes
305 // quadrature against the holding-time CDF, so the answer moves with the
306 // grid. 0 = not given, i.e. EnvOptions' own default.
307 std::size_t tran_points = 0;
308 // ---- the LQNS wrapper's own knobs -------------------------------------
309 // `options.keep` and `options.verbose` of the reference wrapper, plus the
310 // two that pick a REMOTE lqns. They apply to `-s lqns` alone and are
311 // refused elsewhere: nothing else in this CLI runs a child process whose
312 // working directory a caller might want to inspect.
313 bool keep = false;
314 bool verbose = false;
315 bool remote = false;
316 std::string remote_url; // empty = not given, i.e. the wrapper's default
317 int timeout_seconds = 0; // 0 = not given, i.e. no deadline
318 // ---- the simulator's own knobs, all `--ldes-*` -------------------------
319 // They are PREFIXED rather than folded into the shared names because they
320 // are an external engine's settings and not this port's: --ldes-tranfilter
321 // is the warmup filter of a simulation run and has nothing to do with
322 // --tol, and an unprefixed --warmupfrac would read as a knob every solver
323 // has. Same not-given discipline as the rest -- empty, <= 0 or false means
324 // not given -- so the engine keeps its own default and the command line
325 // stays minimal, which is what an older AOT native binary can still parse.
326 std::string ldes_tranfilter; // mser5 | fixed | none
327 double ldes_warmupfrac = -1.0;
328 std::string ldes_cimethod; // obm | bm | spectral | none
329 bool ldes_cnvgon = false;
330 double ldes_cnvgtol = -1.0;
331 bool ldes_slotted = false;
332 double ldes_slotlength = -1.0;
333 /**
334 * `--slotted` / `--slotlength`: the discrete time scale for the
335 * ANALYTICAL solvers, distinct from the `--ldes-*` pair above, which
336 * configures the simulator. SolverNC reads it and routes to the
337 * discrete-time product form.
338 */
339 bool slotted = false;
340 double slotlength = -1.0;
341 int ldes_replications = 0;
342 int ldes_numthreads = 0;
343 double ldes_maxtime = -1.0;
344 std::vector<double> ldes_initsol; // station-major warm-start placement
345 std::string ldes_rest_url;
346 // ---- the JAR CLI's own five, ported so `line-cli` answers every question
347 // `jline.cli.LineCLI` answers ------------------------------------------
348 /**
349 * `--state`, the state vector `-a prob` asks about.
350 *
351 * WITHOUT IT THE QUERY IS ABOUT THE MODEL'S DEFAULT INITIAL STATE, which is
352 * what this CLI reported before and remains the default. The JAR takes an
353 * explicit one because `getProb(node, state)` is a different question from
354 * `getProb(node)`: the second names a state the model already holds, the
355 * first names any state of the node's own space, and a caller sweeping a
356 * marginal law needs the first. Empty = not given.
357 */
358 std::vector<long> state;
359 /**
360 * `--events`, the length of a sampled trajectory, `options.samples` of
361 * `@@SolverSSA/sample`. It is NOT `--samples`: the JAR keeps them apart
362 * because `--samples` is a simulation run length or a Monte Carlo draw
363 * count and reaches a solver's options, while this is the number of EVENTS
364 * one `sample` call walks. Folding them would make `-s ssa -a avg --events`
365 * silently lengthen the run. 0 = not given, i.e. the reference's 1000.
366 */
367 std::size_t events = 0;
368 /**
369 * `--timestep`, the fixed output step of a transient analysis. Without it
370 * the grid is whatever the integrator chose, which is the reference's
371 * adaptive default; with it the trajectory is resampled onto a uniform
372 * lattice of that step, which is what a caller diffing two transients needs.
373 * <= 0 = not given.
374 */
375 double timestep = -1.0;
376 /**
377 * `--transient-method`, `options.config.transient_method` of
378 * `solver_ctmc_transient_analyzer.m`: "ode" integrates the forward
379 * equation, "fau" marches fast adaptive uniformization over the output
380 * grid. Empty = not given, so the solver keeps the reference's "ode".
381 *
382 * It is NOT `--method`: the state-space path and the way the forward
383 * equation is advanced on it are separate choices, and the reference keeps
384 * this one out of its valid-method list because it changes no stationary
385 * answer.
386 */
387 std::string transient_method;
388 /**
389 * `--fau-epsilon` and `--fau-delta`, the two tolerances of the "fau"
390 * transient: the total probability mass the grid may discard, and the
391 * occupancy below which a state is dropped from the support. <= 0 = not
392 * given, i.e. the reference's 1e-6 and 1e-12.
393 */
394 double fau_epsilon = -1.0;
395 double fau_delta = -1.0;
396 /**
397 * `--percentiles`, the levels `getPerctRespT` is read at. Empty = not
398 * given, i.e. the reference's `pers_stored` {0.50, 0.90, 0.95, 0.99}.
399 * Accepted as fractions (0.9) or as percents (90), told apart by magnitude:
400 * a level above 1 cannot be a probability.
401 */
402 std::vector<double> percentiles;
403 /**
404 * `-v/--verbosity`: silent | standard | debug.
405 *
406 * IT WAS ACCEPTED AND DISCARDED, which is the silent-acceptance defect this
407 * struct exists to prevent one layer up: a caller who asked for `silent`
408 * still received every warning the arms print on stderr. It gates them now,
409 * and nothing else -- the tables on stdout are the answer and are printed
410 * whatever the level, exactly as the JAR prints them.
411 */
412 std::string verbosity;
413 /**
414 * `--reward-name`, which declared reward `-a reward-value` returns the
415 * value function of. Empty = not given, and `-a reward-value` without it is
416 * refused rather than defaulted to the first reward: the value functions of
417 * two rewards are different objects, and picking one silently would label
418 * the wrong matrix with the caller's question.
419 */
420 std::string reward_name;
421};
422
423/**
424 * `--cutoff` written as a per-(station,class) matrix, `';'`-separated rows of
425 * `','`-separated non-negative counts. Empty on anything that is not one.
426 *
427 * The spelling is the JAR CLI's, so one example pins one string for both
428 * engines. A zero entry is legal and means the station may not hold that class
429 * at all, which is how the reference bounds a queue in the classes it does not
430 * serve.
431 */
432std::vector<std::vector<std::size_t>> parse_cutoff_matrix(const std::string& s) {
433 std::vector<std::vector<std::size_t>> out;
434 std::string::size_type pos = 0;
435 while (pos <= s.size()) {
436 const std::string::size_type semi = s.find(';', pos);
437 const std::string row = s.substr(pos, semi == std::string::npos ? std::string::npos
438 : semi - pos);
439 std::vector<std::size_t> cells;
440 std::string::size_type cp = 0;
441 while (cp <= row.size()) {
442 const std::string::size_type comma = row.find(',', cp);
443 const std::string cell = row.substr(cp, comma == std::string::npos ? std::string::npos
444 : comma - cp);
445 if (cell.empty()) return std::vector<std::vector<std::size_t>>();
446 for (std::string::size_type i = 0; i < cell.size(); ++i)
447 if (!std::isdigit(static_cast<unsigned char>(cell[i])))
448 return std::vector<std::vector<std::size_t>>();
449 cells.push_back(static_cast<std::size_t>(std::atol(cell.c_str())));
450 if (comma == std::string::npos) break;
451 cp = comma + 1;
452 }
453 if (cells.empty()) return std::vector<std::vector<std::size_t>>();
454 if (!out.empty() && cells.size() != out[0].size())
455 return std::vector<std::vector<std::size_t>>();
456 out.push_back(cells);
457 if (semi == std::string::npos) break;
458 pos = semi + 1;
459 }
460 return out;
461}
462
463/**
464 * The model text piped on stdin, drained ONCE and kept.
465 *
466 * `-s auto` parses the model twice: once to choose an engine and once for the
467 * engine to solve. A stream can only be drained once, so without this buffer the
468 * second parse would see nothing and the chooser would be unusable on a piped
469 * model. It is deliberately not a function template -- a static local inside one
470 * would be per-instantiation, and the two parses need not share an arithmetic.
471 */
472const std::string& stdin_model_text() {
473 static std::string buf;
474 static bool loaded = false;
475 if (!loaded) {
476 buf.assign(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
477 loaded = true;
478 }
479 return buf;
480}
481
482/**
483 * Whether `-i` selected a JMT document rather than a model.json.
484 *
485 * A FILE-SCOPE FLAG AND NOT A PARAMETER, because `read_model` is called from
486 * every solver arm and threading the format through all of them would touch
487 * fifty signatures to carry one bit that main already knows. It is set once,
488 * before any arm runs, and never again.
489 */
490bool g_jsim_input = false;
491
492/**
493 * Whether `-i` selected a PNML place/transition net.
494 *
495 * A second flag rather than a format string for the same reason `g_jsim_input`
496 * is one: `read_model` is called from every solver arm, and the three input
497 * kinds it can be handed are mutually exclusive, so two booleans say what a
498 * threaded enum would and touch no signature. Set once in main.
499 */
500bool g_pnml_input = false;
501
502/**
503 * `-v/--verbosity`, hoisted to file scope for the same reason as `g_jsim_input`.
504 *
505 * The knobs carry it too, but the priority warning below is raised where the
506 * model is READ rather than inside a solver arm, and that function receives no
507 * knobs. Set once in main, before any arm runs.
508 */
509std::string g_verbosity = "standard";
510
511/**
512 * Say so when class priorities were declared and no station will read them.
513 *
514 * The twin of the block at the tail of MATLAB's `@MNetwork/refreshStruct.m` and
515 * of `Network.refreshStruct` in the JAR. It is a WARNING and not a refusal: a
516 * priority at a station whose discipline ignores one is a legitimate model --
517 * `prio_hol_open` is built on it -- and only becomes a defect when NO station
518 * reads it, at which point the metrics are not the priority ones the caller is
519 * about to read them as. Priority-awareness is a property of the declared
520 * policy and is never inferred from the data; see network_struct.h.
521 */
522template <class T>
523void warn_priorities_ignored(line::qn::Network<T>& net) {
524 if (g_verbosity == "silent") return;
525 // `raw_struct` and not `get_struct`: the priorities and the disciplines are
526 // written when the classes and stations are added, and nothing in the
527 // refresh chain touches either, so reading them here costs no refresh.
528 if (!net.raw_struct().priorities_ignored()) return;
529 std::fprintf(stderr,
530 "Warning: Priority classes are specified but no priority-aware scheduling "
531 "policy (PSPRIO, DPSPRIO, GPSPRIO, HOL, FCFSPRIO, FCFSPRPRIO, FCFSPIPRIO, "
532 "LCFSPRIO, LCFSPRPRIO, LCFSPIPRIO, SRPTPRIO) is used in the model. "
533 "Priorities will be ignored.\n");
534}
535
536/** Read the model named by `file`, or stdin when it is empty. */
537template <class T>
538line::qn::Network<T> read_model(const std::string& file) {
539 if (g_pnml_input) {
540 // The PNML reader walks a DOM and has no stdin form, so a piped document
541 // is staged to a temporary file, as the JSIM arm below does.
542 if (!file.empty()) {
544 warn_priorities_ignored(net);
545 return net;
546 }
547 const std::string tmp = line::io::jsim_stage_stdin(stdin_model_text());
549 std::remove(tmp.c_str());
550 warn_priorities_ignored(net);
551 return net;
552 }
553 if (g_jsim_input) {
554 // The JSIM reader walks a DOM and has no stdin form, so a piped JMT
555 // document is staged to a temporary file rather than refused: `cat
556 // model.jsimg | line-cli -i jsimg` is how the JAR CLI is used and the
557 // Docker image documents it.
558 if (!file.empty()) {
560 warn_priorities_ignored(net);
561 return net;
562 }
563 const std::string tmp = line::io::jsim_stage_stdin(stdin_model_text());
565 std::remove(tmp.c_str());
566 warn_priorities_ignored(net);
567 return net;
568 }
569 if (!file.empty()) {
571 warn_priorities_ignored(net);
572 return net;
573 }
574 std::istringstream in(stdin_model_text());
575 line::io::detail::json root;
576 in >> root;
578 warn_priorities_ignored(net);
579 return net;
580}
581
582/** Read the Environment model.json named by `file`, or stdin when it is empty. */
583template <class T>
584line::env::Environment<T> read_env_model(const std::string& file) {
585 if (!file.empty()) return line::io::read_environment_json<T>(file);
586 std::istringstream in(stdin_model_text());
587 line::io::detail::json root;
588 in >> root;
590}
591
592/** One row of an average table, already reduced to double. */
593struct AvgRow {
594 double q, u, r, w, a, t;
595};
596
597/**
598 * Render an average table, as text or as the JSON the hosts parse.
599 *
600 * THE ONE PLACE `-o json` IS HONOURED, reached by every solver arm through a
601 * per-(station,class) accessor. The SSA and fluid arms used to hand-roll their
602 * own printer -- their solution types are `SsaSolution`/`FluidSolution`, not
603 * `mva::AvgResult<T>` -- and neither consulted `g_json_output`, so `-o json` was
604 * ACCEPTED AND IGNORED for `-s ssa` and `-s fluid`: the caller got the readable
605 * table with no brace in it, which is what the Python `lang='cpp'` bridge hit as
606 * "no JSON object found in solver output". Accepting a flag and not applying it
607 * is the silent-acceptance defect this CLI refuses everywhere else, so the
608 * rendering is shared rather than reimplemented per solution type.
609 *
610 * THE JSON FORM IS THE JAR's, key for key. `jline.cli.LineCLI -a avg -o json`
611 * emits {"avg": {"type": "AvgTable", "Station": [...], "JobClass": [...],
612 * "QLen": [...], ...}}, column-oriented, and the Python wrapper's
613 * `station_matrices_via_jar` parses exactly that shape. Emitting anything else
614 * would force a second parser into the wrapper for a table that is the same
615 * table, so the host can treat `lang='cpp'` and `lang='java'` as one transport
616 * with two binaries. The JAR's `data` key (the rendered text) is NOT reproduced:
617 * no caller reads it, and a second rendering of the same numbers is one more
618 * thing that can disagree with the first.
619 *
620 * The rows are also byte-identical across the solvers that share it, which
621 * matters more than it looks: `parity/compare_parity.py` diffs these tables
622 * column by column, and a solver whose printer drifted by a space would read as
623 * a parity failure in a harness that is supposed to be measuring the numbers.
624 */
625template <class Get>
626void emit_avg_table_named(const std::vector<std::string>& stations,
627 const std::vector<std::string>& classes, const char* arith,
628 const std::string& method, Get get, const line::reg::Json& extra,
629 const line::reg::Json& envelope) {
630 line::reg::Json rows = line::reg::Json::object();
631 for (const char* key : {"Station", "JobClass", "QLen", "Util", "RespT", "ResidT", "ArvR",
632 "Tput"})
633 rows[key] = line::reg::Json::array();
634 rows["type"] = "AvgTable";
635
636 if (!g_json_output)
637 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s\n", "Station", "JobClass", "QLen",
638 "Util", "RespT", "ResidT", "ArvR", "Tput");
639 for (std::size_t i = 0; i < stations.size(); ++i) {
640 for (std::size_t c = 0; c < classes.size(); ++c) {
641 const AvgRow v = get(i, c);
642 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)
643 continue;
644 if (!g_json_output) {
645 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
646 stations[i].c_str(), classes[c].c_str(), v.q, v.u, v.r, v.w, v.a, v.t);
647 continue;
648 }
649 // FULL PRECISION on the JSON path, against the readable path's
650 // %12.6g. The table is for a human to read; the JSON is for a host
651 // to compare against another codebase's answer, and six digits
652 // would cap any parity check at six digits.
653 rows["Station"].push_back(stations[i]);
654 rows["JobClass"].push_back(classes[c]);
655 rows["QLen"].push_back(v.q);
656 rows["Util"].push_back(v.u);
657 rows["RespT"].push_back(v.r);
658 rows["ResidT"].push_back(v.w);
659 rows["ArvR"].push_back(v.a);
660 rows["Tput"].push_back(v.t);
661 }
662 }
663 if (g_json_output) {
664 // `extra` NESTS INSIDE the "avg" payload, for emit_analysis's reason: a
665 // solver-specific key at envelope level can collide with an analysis's
666 // own name, and a per-list cost qualifies THIS answer.
667 for (line::reg::Json::const_iterator it = extra.begin(); it != extra.end(); ++it)
668 rows[it.key()] = it.value();
669 line::reg::Json out = line::reg::Json::object();
670 out["avg"] = rows;
671 out["arith"] = arith;
672 out["method"] = method;
673 // `envelope`, unlike `extra`, sits BESIDE "avg" rather than inside it.
674 // Provenance of the solve as a whole belongs there: the iteration count
675 // and the convergence flag qualify the answer, not any one row, and a
676 // host reads them where the JAR CLI puts them.
677 for (line::reg::Json::const_iterator it = envelope.begin(); it != envelope.end(); ++it)
678 out[it.key()] = it.value();
679 std::printf("%s\n", out.dump().c_str());
680 }
681}
682
683/**
684 * The same table, labelled from a struct.
685 *
686 * THE NAMES, NOT THE STRUCT, are what the printer needs, and one solver has no
687 * struct to give it: `-s ldes` forwards the model document to an external engine
688 * and is labelled from the names that engine reports, precisely so a model this
689 * port cannot itself parse still prints the same table. Everything else calls
690 * this overload and nothing about those rows changes.
691 */
692template <class T, class Get>
693void emit_avg_table(const line::qn::NetworkStruct<T>& sn, const std::string& method, Get get,
694 const line::reg::Json& extra = line::reg::Json::object(),
695 const line::reg::Json& envelope = line::reg::Json::object()) {
696 std::vector<std::string> stations, classes;
697 stations.reserve(sn.nstations);
698 classes.reserve(sn.nclasses);
699 for (std::size_t i = 0; i < sn.nstations; ++i) stations.push_back(sn.stations[i].name);
700 for (std::size_t c = 0; c < sn.nclasses; ++c) classes.push_back(sn.classes[c].name);
701 emit_avg_table_named(stations, classes, line::num_traits<T>::name(), method, get, extra,
702 envelope);
703}
704
705/**
706 * Print one analysis that is NOT the average table, as the JSON a host parses.
707 *
708 * ONE ENVELOPE FOR EVERY ANALYSIS: the payload sits under a key named after the
709 * `-a` it answers, and the arithmetic and the resolved method sit beside it,
710 * exactly as `emit_avg_table` places them beside "avg". A host therefore reads
711 * the provenance the same way whatever it asked for, and a caller that asked for
712 * `-a cdf` and received a "states" key knows the answer is not its own instead of
713 * misreading the numbers as its own.
714 *
715 * `method` MAY BE EMPTY, in which case the key is omitted rather than filled
716 * with the requested name: `-a reward` returns rewards and no solved chain, so
717 * there is no resolved method to report, and echoing back "default" would claim
718 * a resolution that never happened.
719 *
720 * EVERY INDEX IN A PAYLOAD IS 0-BASED, against the readable tables' 1-based
721 * columns, and each payload carries `indexBase` so a host cannot get it wrong
722 * silently. The two conventions are deliberate: the table is read by a human
723 * diffing it against MATLAB, whose indices start at 1, while the JSON is
724 * consumed by code that will index a numpy array or a std::vector with it.
725 */
726template <class T>
727void emit_analysis(const char* key, const line::reg::Json& payload, const std::string& method,
728 const line::reg::Json& extra = line::reg::Json::object()) {
729 line::reg::Json body = payload;
730 // `extra` GOES INSIDE THE PAYLOAD, not beside it. At envelope level a
731 // solver-specific key can collide with the analysis's own name -- the CTMC
732 // state count is "states" and so is the `-a states` payload, and the merge
733 // silently replaced the whole answer with the integer 3. Nesting it makes
734 // that class of collision unrepresentable, and it is where the value belongs
735 // anyway: a cutoff qualifies THIS answer.
736 for (line::reg::Json::const_iterator it = extra.begin(); it != extra.end(); ++it)
737 body[it.key()] = it.value();
738 line::reg::Json out = line::reg::Json::object();
739 out[key] = body;
740 out["arith"] = line::num_traits<T>::name();
741 if (!method.empty()) out["method"] = method;
742 std::printf("%s\n", out.dump().c_str());
743}
744
745/** A Matrix as a row-major array of arrays, each entry reduced to double. */
746template <class T>
747line::reg::Json matrix_json(const line::Matrix<T>& M) {
748 line::reg::Json rows = line::reg::Json::array();
749 for (std::size_t i = 0; i < M.rows(); ++i) {
750 line::reg::Json row = line::reg::Json::array();
751 for (std::size_t j = 0; j < M.cols(); ++j)
752 row.push_back(line::num_traits<T>::to_double(M(i, j)));
753 rows.push_back(row);
754 }
755 return rows;
756}
757
758/** A vector of field elements as a JSON array of doubles. */
759template <class T>
760line::reg::Json vector_json(const std::vector<T>& v) {
761 line::reg::Json a = line::reg::Json::array();
762 for (std::size_t i = 0; i < v.size(); ++i) a.push_back(line::num_traits<T>::to_double(v[i]));
763 return a;
764}
765
766/** A vector of sizes as a JSON array, unchanged: a width is not a measurement. */
767line::reg::Json index_json(const std::vector<std::size_t>& v) {
768 line::reg::Json a = line::reg::Json::array();
769 for (std::size_t i = 0; i < v.size(); ++i) a.push_back(v[i]);
770 return a;
771}
772
773/**
774 * The per-Cache result block, as `-a avg` carries it inside the "avg" payload.
775 *
776 * PER-CACHE RESULTS RIDE WITH THE AVG TABLE, because a cache's hit, miss and
777 * delayed-hit fractions are a SOLVER RESULT and a host that solves through this
778 * CLI has no other way to get them back onto its own Cache node. Emitted per
779 * node, and each vector is OMITTED when the solver computed none: absent must
780 * CLEAR the host's copy, and a zero-filled vector would instead assert that
781 * nothing hits.
782 *
783 * FACTORED OUT of `print_avg_table` because the SSA and Fluid arms build the
784 * table themselves rather than through it, and so carried no block at all:
785 * `CPPLINE.restoreCacheResults` then cleared MATLAB's Cache node, refreshed the
786 * visits, and reported link()'s offered 1/2-1/2 for a split both engines had
787 * measured. On cache_replc_rr that is hit 1.0 against 1.1246 (fluid) and
788 * 1.1460 (ssa). A second copy of this serialization here would be free to drop
789 * a field again, so there is one.
790 */
791template <class T>
792line::reg::Json cache_extra_json(const line::solvers::CacheMetrics<T>& cache) {
793 line::reg::Json caches = line::reg::Json::array();
794 for (std::size_t c = 0; c < cache.caches.size(); ++c) {
795 const line::solvers::CacheNodeMetrics<T>& m = cache.caches[c];
796 line::reg::Json e = line::reg::Json::object();
797 // NAME FIRST, because the index is not portable. `node` is an index
798 // into THIS process's node order, which is not the model.json
799 // declaration order: on retrieval_simple the JSON declares Source,
800 // Cache, Queue, Sink and this struct holds Source, Queue, Sink,
801 // Cache, so the Cache is 2 to the host and 4 here. A host matching
802 // on the index wrote onto its Sink, found no Cache and silently kept
803 // the PREVIOUS solver's numbers. `node` stays for provenance.
804 e["name"] = m.name;
805 e["node"] = m.node;
806 if (!m.hitprob.empty()) e["HitProb"] = vector_json(m.hitprob);
807 if (!m.missprob.empty()) e["MissProb"] = vector_json(m.missprob);
808 if (!m.delayedprob.empty()) e["DelayedHitProb"] = vector_json(m.delayedprob);
809 if (!m.latency.empty()) e["ResidT"] = vector_json(m.latency);
810 if (!m.listcost.empty()) e["ListCost"] = vector_json(m.listcost);
811 if (!m.delayedhitqlen.empty()) {
812 e["DelayedHitQLen"] = vector_json(m.delayedhitqlen);
813 e["DelayedHitQLenFull"] = vector_json(m.delayedhitqlenfull);
814 }
815 caches.push_back(e);
816 }
817 return caches;
818}
819
820/**
821 * Print an AvgResult as the parity table.
822 *
823 * Shared by every solver that returns `mva::AvgResult` -- MVA, NC, MAM and BA
824 * -- so the rows stay byte-identical across them.
825 */
826template <class T>
827void print_avg_table(const line::qn::NetworkStruct<T>& sn, const line::mva::AvgResult<T>& r,
828 const line::reg::Json& extra_in = line::reg::Json::object()) {
829 // THE JSON FORM IS THE JAR's, key for key. `jline.cli.LineCLI -a avg -o json`
830 // emits {"avg": {"type": "AvgTable", "Station": [...], "JobClass": [...],
831 // "QLen": [...], ...}}, column-oriented, and the Python wrapper's
832 // `station_matrices_via_jar` parses exactly that shape. Emitting anything
833 // else here would force a second parser into the wrapper for a table that is
834 // the same table, so the host can treat `lang='cpp'` and `lang='java'` as one
835 // transport with two binaries. The JAR's `data` key (the rendered text) is
836 // NOT reproduced: no caller reads it, and a second rendering of the same
837 // numbers is one more thing that can disagree with the first.
838 // A CARRIED WARNING IS ONLY A WARNING IF SOMEONE SEES IT. `AvgResult`
839 // carries the reference's text verbatim for the cases where the answer is
840 // usable but not one the reference stands behind (the SJN starvation cap,
841 // immediate feedback approximated as re-queueing), and until now no CLI
842 // path printed it -- so the table looked authoritative exactly where the
843 // reference declines. On STDERR, not stdout: stdout is the table a parity
844 // harness diffs and the JSON a wrapper parses, and a warning line in either
845 // would be read as data. Python's `lang='cpp'` bridge re-raises anything on
846 // stderr as a Python warning, so it reaches that caller too.
847 if (!r.warning.empty())
848 std::fprintf(stderr, "Warning: %s\n", r.warning.c_str());
849
850 // Mean per-list cache storage cost, the ListCost column of getAvgCacheTable.
851 // Present only on a cache model carrying item sizes, and omitted entirely
852 // otherwise rather than emitted empty, so a host can test for the key.
853 line::reg::Json extra = line::reg::Json::object();
854 if (!r.listcost.empty()) {
855 line::reg::Json lc = line::reg::Json::array();
856 for (std::size_t j = 0; j < r.listcost.size(); ++j)
857 lc.push_back(line::num_traits<T>::to_double(r.listcost[j]));
858 extra["ListCost"] = lc;
859 }
860
861 // WHAT THE CALLER ADDS, merged rather than replaced. The JMT arm carries the
862 // finite-capacity-region rows this way: they are metric rows past the last
863 // station, and the station table drops them, so without a channel of their
864 // own a host reading `-a avg` cannot see a region at all (MATLAB's
865 // `getAvgNodeTable` then filtered the FCR node out for having no numbers).
866 if (extra_in.is_object())
867 for (line::reg::Json::const_iterator it = extra_in.begin(); it != extra_in.end(); ++it)
868 extra[it.key()] = it.value();
869
870 // See `cache_extra_json`: the split is a solver result and the host has no
871 // other channel back to its own Cache node.
872 if (!r.cache.empty()) extra["Cache"] = cache_extra_json<T>(r.cache);
873
874 // Provenance of the solve, beside "avg" and keyed as the JAR CLI keys it, so
875 // one host-side reduction reads both backends. `converged` is OMITTED when
876 // the handler reports none: absent is not false, and there the count is the
877 // signal a caller may fall back on.
878 line::reg::Json envelope = line::reg::Json::object();
879 envelope["iter"] = r.iter;
880 if (r.converged.has_value()) envelope["converged"] = r.converged.value();
881 // `@SolverNC/getProbNormConstAggr`, keyed as the reference names the field it
882 // stores it in. OMITTED for every solver that computes no constant: log G = 0
883 // is the constant of an empty network, so a zero here would be a claim.
884 if (r.lognormconst.has_value()) envelope["logNormConstAggr"] = r.lognormconst.value();
885
886 emit_avg_table<T>(sn, r.actualmethod, [&](std::size_t i, std::size_t c) {
887 AvgRow row;
888 row.q = line::num_traits<T>::to_double(r.QN(i, c));
889 row.u = line::num_traits<T>::to_double(r.UN(i, c));
890 row.r = line::num_traits<T>::to_double(r.RN(i, c));
891 row.w = line::num_traits<T>::to_double(r.WN(i, c));
892 row.a = line::num_traits<T>::to_double(r.AN(i, c));
893 row.t = line::num_traits<T>::to_double(r.TN(i, c));
894 return row;
895 }, extra, envelope);
896}
897
898template <class T>
899int solve_model_mva(const std::string& file, const Knobs& k) {
900 line::qn::Network<T> net = read_model<T>(file);
902 if (!k.method.empty() && k.method != "default") opt.method = k.method;
903 if (k.tol >= 0.0) opt.tol = k.tol;
904 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
905 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
906 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
907 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
908 line::Matrix<T> init;
911 const line::qn::NetworkStruct<T>& sn = net.get_struct();
912
913 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
914 r.actualmethod.c_str(),
915 line::util::method_type("MVA", r.actualmethod).c_str());
916 print_avg_table<T>(sn, r);
917 return 0;
918}
919
920
921/**
922 * Solve a Network model.json with SolverJMT: write the JMT document, run the
923 * external engine, print the same table every other solver prints.
924 *
925 * DOUBLE ONLY, and refused rather than relabelled under another arithmetic:
926 * JMT simulates in double and reports in double, so an answer tagged
927 * `real:128` would name a precision that never touched the computation. The
928 * same rule the LDES arm applies, for the same reason.
929 */
930int solve_model_jmt(const std::string& file, const Knobs& k, const std::string& analysis) {
931 line::qn::Network<double> net = read_model<double>(file);
933
935 if (!k.method.empty() && k.method != "default") o.method = k.method;
936 if (k.samples > 0) o.samples = static_cast<double>(k.samples);
937 if (k.seed != 0) o.seed = static_cast<long>(k.seed);
938 o.keep = k.keep;
939 if (k.t1 >= 0.0) o.max_simulated_time = k.t1;
940 o.verbose = k.verbose;
941
942 // `-a prob`: `getProbAggr` per station and `getProbSysAggr`, weighed off ONE
943 // instrumented run. The DETAILED pair -- `getProb` and `getProbSys` -- has
944 // no counterpart here and is OMITTED rather than aliased to the aggregate:
945 // a JMT log records per-class job counts at a node and nothing about the
946 // buffer order or the service phase, so the encoding those two are
947 // probabilities of is not observed at all.
948 if (analysis == "prob") {
949 std::size_t target = 0;
950 if (k.node) {
951 if (k.node <= sn.nodes.size()) target = sn.nodes[k.node - 1].station;
952 if (target == 0)
953 throw line::InputError(
954 "--node " + std::to_string(k.node) +
955 " is not a station, so it holds no per-class job count to ask about");
956 }
958 sn, o, target, std::vector<double>(k.state.begin(), k.state.end()));
959 if (g_json_output) {
960 line::reg::Json p = line::reg::Json::object();
961 p["type"] = "ProbAggr";
962 p["indexBase"] = 0;
963 p["ProbSysAggr"] = r.sys;
964 // WHETHER THE STATE OCCURRED AT ALL, beside the number. On an exact
965 // solver a zero probability is a property of the model; on a
966 // simulation it is far more often a property of the run length, and
967 // a caller cannot tell the two apart from the zero alone.
968 p["SysStateSeen"] = r.sys_seen;
969 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
970 sv = line::reg::Json::array();
971 for (std::size_t i = 0; i < sn.nstations; ++i) {
972 st.push_back(sn.stations[i].name);
973 pa.push_back(r.station[i]);
974 sv.push_back(static_cast<bool>(r.station_seen[i]));
975 }
976 p["Station"] = st;
977 p["ProbAggr"] = pa;
978 p["StateSeen"] = sv;
979 // ALWAYS jsim, whatever `--method` asked for: this answer is read
980 // off a simulated trajectory, and JMVA computes means from a
981 // product form and logs nothing, so labelling it with the
982 // requested method would name an engine that never ran.
983 emit_analysis<double>("prob", p, std::string("jsim"));
984 return 0;
985 }
986 std::printf("SolverJMT arith=double method=jsim\n");
987 std::printf("ProbSysAggr %.10g%s\n", r.sys, r.sys_seen ? "" : " (state never observed)");
988 std::printf("%-16s %14s\n", "Station", "ProbAggr");
989 for (std::size_t i = 0; i < sn.nstations; ++i)
990 std::printf("%-16s %14.10g%s\n", sn.stations[i].name.c_str(), r.station[i],
991 r.station_seen[i] ? "" : " (state never observed)");
992 return 0;
993 }
994
995 if (analysis == "cdf" || analysis == "trancdf" || analysis == "trancdfpasst") {
996 // -a cdf preloads the rounded steady-state queue lengths, the seeded
997 // getCdfRespT pipeline; the transient names start from the default
998 // initial state, getTranCdfRespT's contract
999 const std::map<std::pair<std::size_t, std::size_t>,
1000 std::vector<std::pair<double, double> > >
1001 rd = line::jmt::jmt_get_cdf_resp_t(sn, o, analysis == "cdf");
1002 line::reg::Json cdf = line::reg::Json::object();
1003 for (std::map<std::pair<std::size_t, std::size_t>,
1004 std::vector<std::pair<double, double> > >::const_iterator it = rd.begin();
1005 it != rd.end(); ++it) {
1006 line::reg::Json rows = line::reg::Json::array();
1007 for (std::size_t i = 0; i < it->second.size(); ++i) {
1008 line::reg::Json row = line::reg::Json::array();
1009 row.push_back(it->second[i].first);
1010 row.push_back(it->second[i].second);
1011 rows.push_back(row);
1012 }
1013 const std::string key =
1014 sn.nodes[sn.station_to_node[it->first.first - 1] - 1].name + "/" +
1015 sn.classes[it->first.second - 1].name;
1016 cdf[key] = rows;
1017 if (!g_json_output)
1018 std::printf("%-24s %8zu points respT(max)=%.6g\n", key.c_str(),
1019 it->second.size(), it->second.back().second);
1020 }
1021 if (g_json_output) {
1022 line::reg::Json out = line::reg::Json::object();
1023 out["cdf"] = cdf;
1024 std::printf("%s\n", out.dump(2).c_str());
1025 }
1026 return 0;
1027 }
1028
1030 std::printf("SolverJMT arith=double method=%s\n", r.avg.actualmethod.c_str());
1031
1032 // The metric matrices carry `nregions` EXTRA rows past the stations; the
1033 // station table takes the first `nstations` of them and the regions are
1034 // reported separately, as the LDES arm reports its own FCR block.
1036 if (table.QN.rows() > sn.nstations) {
1037 const std::size_t M = sn.nstations, K = sn.nclasses;
1038 line::Matrix<double>* dst[6] = {&table.QN, &table.UN, &table.RN,
1039 &table.TN, &table.AN, &table.WN};
1040 const line::Matrix<double>* src[6] = {&r.avg.QN, &r.avg.UN, &r.avg.RN,
1041 &r.avg.TN, &r.avg.AN, &r.avg.WN};
1042 for (int m = 0; m < 6; ++m) {
1043 line::Matrix<double> t(M, K, 0.0);
1044 for (std::size_t i = 0; i < M; ++i)
1045 for (std::size_t c = 0; c < K; ++c) t(i, c) = (*src[m])(i, c);
1046 *dst[m] = t;
1047 }
1048 }
1049 // THE REGION ROWS TRAVEL BESIDE THE STATION TABLE, in the same shape the
1050 // node view prints them: a host solving through `-a avg` has no other way to
1051 // reach them, and MATLAB's `getAvgNodeTable` needs `result.Avg` to carry
1052 // `nstations + nregions` rows or it filters the FCR node out for having no
1053 // numbers at all (fcr_mm1waitq[M2C], 'row FCR1 missing').
1054 line::reg::Json fcr_extra = line::reg::Json::object();
1055 if (r.avg.QN.rows() > sn.nstations && sn.regions.size() > 0) {
1056 const std::size_t M = sn.nstations, K = sn.nclasses;
1057 line::reg::Json names = line::reg::Json::array();
1058 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array();
1059 line::reg::Json rt = line::reg::Json::array(), w = line::reg::Json::array();
1060 line::reg::Json a = line::reg::Json::array(), t = line::reg::Json::array();
1061 for (std::size_t f = 0; f < sn.regions.size() && M + f < r.avg.QN.rows(); ++f) {
1062 names.push_back(f < sn.regions.size() && !sn.regions[f].name.empty()
1063 ? sn.regions[f].name
1064 : "FCR" + std::to_string(f + 1));
1065 for (std::size_t c = 0; c < K; ++c) {
1066 q.push_back(r.avg.QN(M + f, c));
1067 u.push_back(r.avg.UN(M + f, c));
1068 rt.push_back(r.avg.RN(M + f, c));
1069 w.push_back(r.avg.WN(M + f, c));
1070 a.push_back(r.avg.AN(M + f, c));
1071 t.push_back(r.avg.TN(M + f, c));
1072 }
1073 }
1074 fcr_extra["Region"] = names;
1075 fcr_extra["QLen"] = q;
1076 fcr_extra["Util"] = u;
1077 fcr_extra["RespT"] = rt;
1078 fcr_extra["ResidT"] = w;
1079 fcr_extra["ArvR"] = a;
1080 fcr_extra["Tput"] = t;
1081 }
1082 line::reg::Json avg_extra = line::reg::Json::object();
1083 if (!fcr_extra.empty()) avg_extra["FCR"] = fcr_extra;
1084 print_avg_table<double>(sn, table, avg_extra);
1085
1086 if (!g_json_output && r.TNfcr.rows() > 0) {
1087 std::printf("%-16s %-14s %12s %12s\n", "Region", "JobClass", "Tput", "DropRate");
1088 for (std::size_t f = 0; f < r.TNfcr.rows(); ++f)
1089 for (std::size_t c = 0; c < sn.nclasses; ++c)
1090 // THE REGION'S DECLARED NAME, not the JSIM document's internal
1091 // `FCRegion<n>` label: that spelling is what the writer puts in
1092 // the exported model, and reporting it back renamed the user's
1093 // own region (`FCR1` in every other codebase's node table).
1094 std::printf("%-16s %-14s %12.6g %12.6g\n",
1095 (f < sn.regions.size() && !sn.regions[f].name.empty()
1096 ? sn.regions[f].name
1097 : "FCR" + std::to_string(f + 1))
1098 .c_str(),
1099 sn.classes[c].name.c_str(), r.TNfcr(f, c), r.DropRateNfcr(f, c));
1100 }
1101 if (!g_json_output && !r.cache_hit_prob.empty())
1102 for (std::map<std::size_t, std::vector<double> >::const_iterator it =
1103 r.cache_hit_prob.begin();
1104 it != r.cache_hit_prob.end(); ++it)
1105 for (std::size_t c = 0; c < it->second.size(); ++c)
1106 std::printf("%-16s %-14s hitProb=%12.6g\n", sn.nodes[it->first - 1].name.c_str(),
1107 sn.classes[c].name.c_str(), it->second[c]);
1108 return 0;
1109}
1110
1111/**
1112 * Solve a Network model.json with SolverNC and print the same table.
1113 *
1114 * `--samples` and `--seed` are wired because NC has stochastic methods that read
1115 * them ('mci', 'imci', 'ls', 'is', 'sampling' and 'mcmc'), and a run length nobody
1116 * can set is a method nobody can drive. `highvar` still has no flag and keeps its
1117 * SolverOptions('NC') default rather than being invented here.
1118 */
1119template <class T>
1120int solve_model_nc(const std::string& file, const Knobs& k) {
1121 line::qn::Network<T> net = read_model<T>(file);
1123 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1124 if (k.tol >= 0.0) opt.tol = k.tol;
1125 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
1126 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1127 if (k.samples) opt.samples = k.samples;
1128 if (k.seed) opt.seed = k.seed;
1129 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
1130 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
1131 if (k.slotted) opt.slotted = true;
1132 if (k.slotlength > 0.0) {
1133 opt.slotted = true;
1134 opt.slotlength = k.slotlength;
1135 }
1137 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1138
1139 // `lognormconst=` rides ON THE BANNER LINE rather than on one of its own,
1140 // as the fluid arm's `iters=` does: the banner is provenance and a new line
1141 // between it and the table is one more thing a table parser has to skip.
1142 // It is `getProbNormConstAggr`, which no other `-a` reports.
1143 std::printf("SolverNC arith=%s method=%s type=%s lognormconst=%.10g\n",
1145 line::util::method_type("NC", r.actualmethod).c_str(),
1146 r.lognormconst.has_value() ? r.lognormconst.value() : 0.0);
1147 print_avg_table<T>(sn, r);
1148 return 0;
1149}
1150
1151/**
1152 * Solve a Network model.json with SolverMAM and print the same table.
1153 *
1154 * DOUBLE ONLY, and refused by name otherwise in the dispatcher: the analyzer
1155 * fits phase-type representations (aph_fit), which static_asserts on
1156 * transcendental arithmetic, so an exact instantiation would fail to COMPILE
1157 * rather than refuse at run time. `MamOptions`' tol, iter_max, space_max and
1158 * preserveDet keep their SolverOptions('MAM') defaults.
1159 */
1160template <class T>
1161int solve_model_mam(const std::string& file, const Knobs& k) {
1162 line::qn::Network<T> net = read_model<T>(file);
1164 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1165 if (k.tol >= 0.0) opt.tol = k.tol;
1166 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1168 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1169
1170 std::printf("SolverMAM arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1171 r.actualmethod.c_str(),
1172 line::util::method_type("MAM", r.actualmethod).c_str());
1173 print_avg_table<T>(sn, r);
1174 return 0;
1175}
1176
1177/**
1178 * Solve a Network model.json with SolverAG and print the same table.
1179 *
1180 * The RCAT/INAP arm, reachable from the CLI rather than API-only. It was the
1181 * one engine `run_avg_engine` already served for `-a node` and its three
1182 * sibling views while `-a avg` -- the view every parity row and every wrapper
1183 * asks for first -- refused the token outright, so `-s ag` reported an argument
1184 * error where the solver was present and working.
1185 *
1186 * `--method` picks between inap, inapplus, inapinf and exact; `default`
1187 * resolves to inap inside the analyzer, as it does in every codebase. `--tol`
1188 * and `--iter_max` are the fixed point's, matching -s mam.
1189 */
1190/**
1191 * The AgOptions a command line asks for.
1192 *
1193 * ONE PLACE, because there are three call sites (-a avg, -a cdf and the
1194 * run_avg_engine views) and a knob added to only some of them is a knob that
1195 * works or not depending on which view was asked for.
1196 */
1197void apply_ag_knobs(const Knobs& k, line::ag::AgOptions& opt) {
1198 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1199 if (k.tol >= 0.0) opt.tol = k.tol;
1200 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1201 if (k.max_states > 0) opt.max_states = static_cast<std::size_t>(k.max_states);
1202}
1203
1204template <class T>
1205int solve_model_ag(const std::string& file, const Knobs& k) {
1206 line::qn::Network<T> net = read_model<T>(file);
1208 apply_ag_knobs(k, opt);
1210 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1211
1212 std::printf("SolverAG arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1213 r.actualmethod.c_str(),
1214 line::util::method_type("AG", r.actualmethod).c_str());
1215 print_avg_table<T>(sn, r);
1216 return 0;
1217}
1218
1219/**
1220 * The station a per-node MAM query is about: `--node` when given, and otherwise
1221 * the model's only Queue.
1222 *
1223 * Defaulting is legitimate here and nowhere else: `getProb`, `getProbMarg` and
1224 * `getMAMResult` all run `require_single_queue`, so a model that reaches them
1225 * has exactly ONE queue and there is nothing to choose. `--node` is still
1226 * accepted, because naming the node one means is clearer than relying on that.
1227 */
1228template <class T>
1229std::size_t mam_query_node(const line::qn::NetworkStruct<T>& sn, const Knobs& k) {
1230 if (k.node) return k.node;
1231 for (std::size_t a = 0; a < sn.nof_nodes(); ++a)
1232 if (sn.nodes[a].nodetype == line::qn::NodeType::Queue) return a + 1;
1233 throw line::InputError(
1234 "the MAM per-node analyses report a queue's internals and this model has no Queue node");
1235}
1236
1237/** The MamOptions every `-s mam` entry point builds from the CLI knobs. */
1238line::mam::MamOptions mam_options(const Knobs& k) {
1240 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1241 if (k.tol >= 0.0) opt.tol = k.tol;
1242 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1243 if (k.cutoff >= 0.0) opt.cutoff = static_cast<std::size_t>(k.cutoff);
1244 if (k.fj_accuracy > 0) opt.fj_accuracy = static_cast<std::size_t>(k.fj_accuracy);
1245 if (!k.fj_tmode.empty()) opt.fj_tmode = k.fj_tmode;
1246 if (!k.timescale.empty()) opt.timescale = k.timescale;
1247 if (k.slotlength > 0.0) opt.slotlength = k.slotlength;
1248 if (k.t1 >= 0.0) {
1249 opt.timespan_start = k.t0;
1250 opt.timespan_end = k.t1;
1251 }
1252 return opt;
1253}
1254
1255/**
1256 * `-s mam -a prob`: `@@SolverMAM/getProb` and `@@SolverMAM/getProbMarg`.
1257 *
1258 * BOTH, in one answer, because they are two views of the same queue-length law:
1259 * the joint (level, phase) table the first returns, and the per-class marginal
1260 * P(n jobs of class r) the second does. Reporting only one would leave the other
1261 * unreachable again, which is the state this wiring closes.
1262 *
1263 * `--cutoff` is the level truncation an OPEN model needs -- its queue length is
1264 * unbounded, so the table has to stop somewhere -- and is passed straight
1265 * through as `options.cutoff`; a closed model bounds itself by its population
1266 * and ignores it.
1267 */
1268template <class T>
1269int solve_model_mam_prob(const std::string& file, const Knobs& k) {
1270 line::qn::Network<T> net = read_model<T>(file);
1271 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1272 const line::mam::MamOptions opt = mam_options(k);
1273 // The reference's getters read `self.getAvg()` first, so the solve comes
1274 // before the query and a caller cannot reach them on an unsolved model.
1276 const std::size_t node = mam_query_node<T>(sn, k);
1277 const std::size_t ist = sn.nodes[node - 1].station;
1278 const line::mam::ProbTable<T> P = line::mam::solver_mam_get_prob(sn, opt, node, avg);
1279 std::vector<std::vector<T> > marg(sn.nclasses);
1280 for (std::size_t r = 0; r < sn.nclasses; ++r)
1281 marg[r] = line::mam::solver_mam_get_prob_marg(sn, opt, ist, r + 1, avg);
1282
1283 if (g_json_output) {
1284 line::reg::Json p = line::reg::Json::object();
1285 p["type"] = "ProbTable";
1286 p["indexBase"] = 0;
1287 p["node"] = node - 1;
1288 p["Node"] = sn.nodes[node - 1].name;
1289 p["levels"] = P.P.rows();
1290 p["phases"] = P.P.cols();
1291 line::reg::Json joint = line::reg::Json::array();
1292 for (std::size_t n = 0; n < P.P.rows(); ++n) {
1293 line::reg::Json row = line::reg::Json::array();
1294 for (std::size_t j = 0; j < P.P.cols(); ++j)
1295 row.push_back(line::num_traits<T>::to_double(P.P(n, j)));
1296 joint.push_back(row);
1297 }
1298 p["joint"] = joint;
1299 line::reg::Json mj = line::reg::Json::array();
1300 for (std::size_t r = 0; r < sn.nclasses; ++r) {
1301 line::reg::Json e = line::reg::Json::object();
1302 e["JobClass"] = sn.classes[r].name;
1303 e["jobclass"] = r;
1304 e["P"] = vector_json(marg[r]);
1305 mj.push_back(e);
1306 }
1307 p["marginal"] = mj;
1308 emit_analysis<T>("prob", p, avg.actualmethod);
1309 return 0;
1310 }
1311 std::printf("SolverMAM arith=%s method=%s node=%s levels=%zu phases=%zu\n",
1313 sn.nodes[node - 1].name.c_str(), P.P.rows(), P.P.cols());
1314 std::printf("%-8s %-8s %16s\n", "Level", "Phase", "Prob");
1315 for (std::size_t n = 0; n < P.P.rows(); ++n)
1316 for (std::size_t j = 0; j < P.P.cols(); ++j)
1317 std::printf("%-8zu %-8zu %16.10g\n", n, j + 1,
1319 std::printf("%-14s %-8s %16s\n", "JobClass", "Jobs", "Prob");
1320 for (std::size_t r = 0; r < sn.nclasses; ++r)
1321 for (std::size_t n = 0; n < marg[r].size(); ++n)
1322 std::printf("%-14s %-8zu %16.10g\n", sn.classes[r].name.c_str(), n,
1323 line::num_traits<T>::to_double(marg[r][n]));
1324 return 0;
1325}
1326
1327/**
1328 * The levels `getPerctRespT` is read at: `--percentiles`, or the reference's
1329 * `pers_stored` when the caller named none.
1330 */
1331std::vector<double> percentile_levels(const Knobs& k) {
1332 if (!k.percentiles.empty()) return k.percentiles;
1333 std::vector<double> pcts;
1334 pcts.push_back(0.50);
1335 pcts.push_back(0.90);
1336 pcts.push_back(0.95);
1337 pcts.push_back(0.99);
1338 return pcts;
1339}
1340
1341/**
1342 * `-s mam -a cdf`: `@@SolverMAM/getCdfRespT` (and its aliases getSjrnT / sjrnT),
1343 * with `@@SolverMAM/getPerctRespT` beside it.
1344 *
1345 * THE PERCENTILE LEVELS COME FROM `--percentiles`, defaulting to the
1346 * {0.50, 0.90, 0.95, 0.99} that `solver_mam_fj.m` stores as `pers_stored`: the
1347 * reference takes them as an argument to `getPerctRespT`, and the flag is that
1348 * argument. On the CDF path they are a READING of the same curve --
1349 * `mam_percentiles_from_cdf` inverts the CDF that is printed above them -- so a
1350 * level the caller names costs nothing beyond the inversion.
1351 *
1352 * A FORK-JOIN MODEL HAS NO CDF HERE, and that is the reference's structure
1353 * rather than a gap in this port: `getPerctRespT.m` reads the table
1354 * `solver_mam_fj.m` stored, and its own fallback comment records that
1355 * `getCdfRespT` is "not available for this model". The two are separate methods
1356 * in MATLAB and only this CLI bundles them, so the fork-join case prints the
1357 * percentiles alone and says why. The condition is tested explicitly, not
1358 * discovered by catching the CDF's refusal: a caught exception cannot tell "no
1359 * fork-join route exists" from "the passage-time engine failed on this model".
1360 */
1361template <class T>
1362int solve_model_mam_cdf(const std::string& file, const Knobs& k, const char* key,
1363 const char* type) {
1364 line::qn::Network<T> net = read_model<T>(file);
1365 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1366 const line::mam::MamOptions opt = mam_options(k);
1367 const std::vector<double> pcts = percentile_levels(k);
1368
1369 if (line::mam::mam_has_fj_percentiles(sn, opt)) {
1370 const std::vector<std::vector<T> > perc =
1372 if (g_json_output) {
1373 line::reg::Json p = line::reg::Json::object();
1374 p["type"] = "PerctRespT";
1375 p["indexBase"] = 0;
1376 p["source"] = "fjcodes";
1377 line::reg::Json arr = line::reg::Json::array();
1378 for (std::size_t r = 0; r < perc.size(); ++r) {
1379 line::reg::Json e = line::reg::Json::object();
1380 e["JobClass"] = sn.classes[r].name;
1381 e["jobclass"] = r;
1382 e["percentileLevels"] = pcts;
1383 e["percentiles"] = vector_json(perc[r]);
1384 arr.push_back(e);
1385 }
1386 p["respt"] = arr;
1387 emit_analysis<T>(key, p, opt.method);
1388 return 0;
1389 }
1390 std::printf("SolverMAM arith=%s method=%s classes=%zu\n", line::num_traits<T>::name(),
1391 opt.method.c_str(), sn.nclasses);
1392 std::printf("# the response-time CDF has no fork-join route in the reference; these are "
1393 "the FJ_codes percentiles getPerctRespT reads\n");
1394 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
1395 for (std::size_t r = 0; r < perc.size(); ++r)
1396 for (std::size_t j = 0; j < perc[r].size(); ++j)
1397 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
1398 line::num_traits<T>::to_double(perc[r][j]));
1399 return 0;
1400 }
1401
1402 const std::vector<line::mam::RespTCdf<T> > rd = line::mam::solver_mam_get_cdf_respt(sn, opt);
1403 std::vector<std::vector<T> > perc;
1404 for (std::size_t r = 0; r < rd.size(); ++r)
1405 perc.push_back(line::mam::mam_percentiles_from_cdf(rd[r], pcts));
1406
1407 if (g_json_output) {
1408 line::reg::Json p = line::reg::Json::object();
1409 p["type"] = type;
1410 p["indexBase"] = 0;
1411 line::reg::Json arr = line::reg::Json::array();
1412 for (std::size_t r = 0; r < rd.size(); ++r) {
1413 // An empty curve is not a degenerate one: the class has no passage
1414 // through a queue here, and it is OMITTED rather than sent as a flat
1415 // zero law.
1416 if (rd[r].X.empty()) continue;
1417 line::reg::Json e = line::reg::Json::object();
1418 e["JobClass"] = sn.classes[r].name;
1419 e["jobclass"] = r;
1420 e["t"] = vector_json(rd[r].X);
1421 e["F"] = vector_json(rd[r].F);
1422 e["percentileLevels"] = pcts;
1423 e["percentiles"] = vector_json(perc[r]);
1424 arr.push_back(e);
1425 }
1426 p["respt"] = arr;
1427 emit_analysis<T>(key, p, opt.method);
1428 return 0;
1429 }
1430 std::printf("SolverMAM arith=%s method=%s classes=%zu\n", line::num_traits<T>::name(),
1431 opt.method.c_str(), sn.nclasses);
1432 std::printf("%-14s %14s %14s\n", "JobClass", "Time", "F(t)");
1433 for (std::size_t r = 0; r < rd.size(); ++r)
1434 for (std::size_t j = 0; j < rd[r].X.size(); ++j)
1435 std::printf("%-14s %14.8g %14.10g\n", sn.classes[r].name.c_str(),
1437 line::num_traits<T>::to_double(rd[r].F[j]));
1438 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
1439 for (std::size_t r = 0; r < perc.size(); ++r)
1440 for (std::size_t j = 0; j < perc[r].size(); ++j)
1441 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
1442 line::num_traits<T>::to_double(perc[r][j]));
1443 return 0;
1444}
1445
1446/**
1447 * `-s mam -a perct-respt`: `@@SolverMAM/getPerctRespT` ALONE.
1448 *
1449 * The percentiles ride beside the curve under `-a cdf` as well, and this arm
1450 * exists because they are a separate METHOD in the reference and a separate
1451 * answer to a caller: a service-level question ("what is the 99th percentile")
1452 * wants four numbers, not the whole law printed above them. On a fork-join
1453 * model it is the only route -- `getCdfRespT` has none there -- and on every
1454 * other model the levels are inverted from the CDF the other arm prints, so
1455 * the two never disagree.
1456 */
1457template <class T>
1458int solve_model_mam_perct(const std::string& file, const Knobs& k) {
1459 line::qn::Network<T> net = read_model<T>(file);
1460 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1461 const line::mam::MamOptions opt = mam_options(k);
1462 const std::vector<double> pcts = percentile_levels(k);
1463
1464 std::vector<std::vector<T> > perc;
1465 const bool fj = line::mam::mam_has_fj_percentiles(sn, opt);
1466 if (fj) {
1467 perc = line::mam::solver_mam_get_perct_respt(sn, opt, pcts);
1468 } else {
1469 const std::vector<line::mam::RespTCdf<T> > rd =
1471 for (std::size_t r = 0; r < rd.size(); ++r)
1472 perc.push_back(line::mam::mam_percentiles_from_cdf(rd[r], pcts));
1473 }
1474
1475 if (g_json_output) {
1476 line::reg::Json p = line::reg::Json::object();
1477 p["type"] = "PerctRespT";
1478 p["indexBase"] = 0;
1479 p["source"] = fj ? "fjcodes" : "cdf";
1480 line::reg::Json arr = line::reg::Json::array();
1481 for (std::size_t r = 0; r < perc.size(); ++r) {
1482 if (perc[r].empty()) continue;
1483 line::reg::Json e = line::reg::Json::object();
1484 e["JobClass"] = sn.classes[r].name;
1485 e["jobclass"] = r;
1486 e["percentileLevels"] = pcts;
1487 e["percentiles"] = vector_json(perc[r]);
1488 arr.push_back(e);
1489 }
1490 p["respt"] = arr;
1491 emit_analysis<T>("perct", p, opt.method);
1492 return 0;
1493 }
1494 std::printf("SolverMAM arith=%s method=%s classes=%zu source=%s\n",
1495 line::num_traits<T>::name(), opt.method.c_str(), sn.nclasses,
1496 fj ? "fjcodes" : "cdf");
1497 std::printf("%-14s %14s %14s\n", "JobClass", "Percentile", "RespT");
1498 for (std::size_t r = 0; r < perc.size(); ++r)
1499 for (std::size_t j = 0; j < perc[r].size(); ++j)
1500 std::printf("%-14s %14.4g %14.10g\n", sn.classes[r].name.c_str(), pcts[j],
1501 line::num_traits<T>::to_double(perc[r][j]));
1502 return 0;
1503}
1504
1505/**
1506 * `-s mam -a tran`: `@@SolverMAM/getTranAvg`.
1507 *
1508 * The reference FORCES `options.method = 'ldqbd'` before delegating, so the
1509 * transient path is not the caller's method and `--method` does not select it;
1510 * which engine runs -- the Laplace-domain transient QBD or the QBD fast path --
1511 * is decided from the model by `mam_transient_qbd_applicable`. `--tspan` is
1512 * required: a transient curve on an unstated horizon is not a quantity.
1513 */
1514template <class T>
1515int solve_model_mam_tran(const std::string& file, const Knobs& k) {
1516 line::qn::Network<T> net = read_model<T>(file);
1517 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1518 const line::mam::MamOptions opt = mam_options(k);
1520
1521 if (g_json_output) {
1522 line::reg::Json p = line::reg::Json::object();
1523 p["type"] = "TranAvgTable";
1524 p["indexBase"] = 0;
1525 p["t0"] = opt.timespan_start;
1526 p["t1"] = opt.timespan_end;
1527 line::reg::Json arr = line::reg::Json::array();
1528 for (std::size_t i = 0; i < tr.Qt.size(); ++i)
1529 for (std::size_t r = 0; r < tr.Qt[i].size(); ++r) {
1530 if (tr.Qt[i][r].times.empty()) continue;
1531 line::reg::Json e = line::reg::Json::object();
1532 e["Station"] = sn.stations[i].name;
1533 e["JobClass"] = sn.classes[r].name;
1534 e["station"] = i;
1535 e["jobclass"] = r;
1536 e["t"] = tr.Qt[i][r].times;
1537 e["QLen"] = vector_json(tr.Qt[i][r].values);
1538 e["Util"] = vector_json(tr.Ut[i][r].values);
1539 e["Tput"] = vector_json(tr.Tt[i][r].values);
1540 arr.push_back(e);
1541 }
1542 p["curves"] = arr;
1543 emit_analysis<T>("tran", p, "ldqbd");
1544 return 0;
1545 }
1546 std::printf("SolverMAM arith=%s method=ldqbd tspan=[%g,%g]\n", line::num_traits<T>::name(),
1547 opt.timespan_start, opt.timespan_end);
1548 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
1549 "Tput");
1550 for (std::size_t i = 0; i < tr.Qt.size(); ++i)
1551 for (std::size_t r = 0; r < tr.Qt[i].size(); ++r)
1552 for (std::size_t j = 0; j < tr.Qt[i][r].times.size(); ++j)
1553 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
1554 sn.stations[i].name.c_str(), sn.classes[r].name.c_str(),
1555 tr.Qt[i][r].times[j],
1556 line::num_traits<T>::to_double(tr.Qt[i][r].values[j]),
1557 line::num_traits<T>::to_double(tr.Ut[i][r].values[j]),
1558 line::num_traits<T>::to_double(tr.Tt[i][r].values[j]));
1559 return 0;
1560}
1561
1562/**
1563 * `-s mam -a internals`: `@@SolverMAM/getMAMResult`, the M/G/1-type internals of
1564 * a single queue.
1565 *
1566 * It is NOT a metric table and is deliberately not folded into `-a avg`: the
1567 * answer is the matrix-analytic machinery itself -- the randomized blocks, the
1568 * G matrix, the drift, the decay rate, the level probabilities -- which is what
1569 * a caller checking a queue's stability or tail decay asks for, and what a
1570 * cross-codebase comparison of the QBD assembly needs to see.
1571 */
1572template <class T>
1573int solve_model_mam_internals(const std::string& file, const Knobs& k) {
1574 line::qn::Network<T> net = read_model<T>(file);
1575 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1576 (void)k;
1578 const double q = line::num_traits<T>::to_double(r.q);
1579
1580 if (g_json_output) {
1581 line::reg::Json p = line::reg::Json::object();
1582 p["type"] = "MAMResult";
1583 p["indexBase"] = 0;
1584 p["lambda"] = line::num_traits<T>::to_double(r.lambda);
1585 p["rho"] = line::num_traits<T>::to_double(r.rho);
1586 p["uniformization"] = q;
1587 p["drift"] = line::num_traits<T>::to_double(r.drift);
1588 p["decayRate"] = r.decayRate;
1589 p["pi0"] = line::num_traits<T>::to_double(r.pi0);
1593 p["truncLevel"] = r.truncLevel;
1594 p["truncError"] = r.truncError;
1595 p["gConverged"] = r.gConverged;
1596 p["theta"] = vector_json(r.theta);
1597 p["alpha"] = vector_json(r.alpha);
1598 line::reg::Json lv = line::reg::Json::array();
1599 for (std::size_t n = 0; n < r.levelProb.rows(); ++n) {
1600 line::reg::Json row = line::reg::Json::array();
1601 for (std::size_t j = 0; j < r.levelProb.cols(); ++j)
1602 row.push_back(line::num_traits<T>::to_double(r.levelProb(n, j)));
1603 lv.push_back(row);
1604 }
1605 p["levelProb"] = lv;
1606 emit_analysis<T>("internals", p, std::string());
1607 return 0;
1608 }
1609 std::printf("SolverMAM arith=%s stations=%zu\n", line::num_traits<T>::name(), sn.nstations);
1610 std::printf("lambda=%.10g rho=%.10g q=%.10g drift=%.10g decayRate=%.10g\n",
1613 std::printf("pi0=%.10g QLen=%.10g Util=%.10g Tput=%.10g truncLevel=%zu truncError=%.3g "
1614 "gConverged=%s\n",
1619 r.gConverged ? "yes" : "no");
1620 std::printf("%-8s %16s\n", "Level", "Prob");
1621 for (std::size_t n = 0; n < r.levelProb.rows(); ++n) {
1622 double s = 0.0;
1623 for (std::size_t j = 0; j < r.levelProb.cols(); ++j)
1625 std::printf("%-8zu %16.10g\n", n, s);
1626 }
1627 return 0;
1628}
1629
1630/**
1631 * Solve a Network model.json with SolverBA and print the same table.
1632 *
1633 * A BOUND, not an estimate: `--method` names which side of which hierarchy is
1634 * wanted (`aba.upper`, `gb.lower`, ...) and `default` resolves to `gb.upper`,
1635 * as in the reference. `options.level` keeps its default of 2. The table is
1636 * therefore not comparable with an exact solver's row except as a bracket,
1637 * which is why the parity row is separate.
1638 */
1639/** A JSON flag value, given inline or as the path of a file holding it. */
1640line::reg::Json read_json_arg(const std::string& spec, const char* flag) {
1641 std::string text = spec;
1642 const std::size_t at = spec.find_first_not_of(" \t\r\n");
1643 if (at == std::string::npos || (spec[at] != '{' && spec[at] != '[')) {
1644 std::ifstream in(spec.c_str());
1645 if (!in)
1646 throw line::InputError(std::string(flag) + " is neither inline JSON nor a readable "
1647 "file (got '" + spec + "')");
1648 text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
1649 }
1650 try {
1651 return line::reg::Json::parse(text);
1652 } catch (const line::reg::Json::parse_error& e) {
1653 throw line::InputError(std::string("malformed ") + flag + " JSON: " + e.what());
1654 }
1655}
1656
1657/** A JSON array of arrays as an integer table. */
1658std::vector<std::vector<int> > json_int_table(const line::reg::Json& j, const char* name) {
1659 if (!j.is_array())
1660 throw line::InputError(std::string("--qrf-params ") + name + " must be an array of rows");
1661 std::vector<std::vector<int> > out;
1662 for (std::size_t m = 0; m < j.size(); ++m) {
1663 if (!j[m].is_array())
1664 throw line::InputError(std::string("--qrf-params ") + name + " must be an array of "
1665 "rows");
1666 std::vector<int> row;
1667 for (std::size_t c = 0; c < j[m].size(); ++c) row.push_back(j[m][c].get<int>());
1668 out.push_back(row);
1669 }
1670 return out;
1671}
1672
1673/**
1674 * Decode `--qrf-params` into `BaOptions::qrf_params`.
1675 *
1676 * Required: f, MR, BB, MM, MM1, ZZ, exactly what `sn_to_qrf_params` demands.
1677 * F is optional and falls back to sn.cap. ZM is DERIVED from ZZ and a supplied
1678 * one is ignored: it is max(ZZ) by definition, and a larger one empties the
1679 * polytope through THM3I instead of failing cleanly.
1680 */
1681void decode_qrf_params(const std::string& spec, line::ba::BaOptions& opt) {
1682 const line::reg::Json j = read_json_arg(spec, "--qrf-params");
1683 if (!j.is_object()) throw line::InputError("--qrf-params must be a JSON object");
1684 const char* required[] = {"f", "MR", "BB", "MM", "MM1", "ZZ"};
1685 std::string missing;
1686 for (std::size_t i = 0; i < 6; ++i)
1687 if (!j.contains(required[i]))
1688 missing += (missing.empty() ? "" : ", ") + std::string(required[i]);
1689 if (!missing.empty())
1690 throw line::InputError("--qrf-params is missing the field(s) " + missing +
1691 "; required are f, MR, BB, MM, MM1, ZZ (F is optional, ZM is "
1692 "derived from ZZ)");
1694 p.supplied = true;
1695 p.f = j["f"].get<int>();
1696 p.MR = j["MR"].get<int>();
1697 p.BB = json_int_table(j["BB"], "BB");
1698 p.MM = json_int_table(j["MM"], "MM");
1699 p.MM1 = json_int_table(j["MM1"], "MM1");
1700 if (!j["ZZ"].is_array()) throw line::InputError("--qrf-params ZZ must be an array");
1701 for (std::size_t i = 0; i < j["ZZ"].size(); ++i) p.ZZ.push_back(j["ZZ"][i].get<int>());
1702 if (j.contains("F"))
1703 for (std::size_t i = 0; i < j["F"].size(); ++i) p.F.push_back(j["F"][i].get<int>());
1704 opt.qrf_params = p;
1705}
1706
1707/** Decode `--qrf-alpha`, the (nstations x N) load-dependent scaling. */
1708void decode_qrf_alpha(const std::string& spec, line::ba::BaOptions& opt) {
1709 const line::reg::Json j = read_json_arg(spec, "--qrf-alpha");
1710 if (!j.is_array() || j.empty() || !j[0].is_array())
1711 throw line::InputError("--qrf-alpha must be a JSON array of rows, one per station");
1712 line::Matrix<double> a(j.size(), j[0].size(), 0.0);
1713 for (std::size_t i = 0; i < j.size(); ++i) {
1714 if (!j[i].is_array() || j[i].size() != j[0].size())
1715 throw line::InputError("--qrf-alpha rows must all have the same length");
1716 for (std::size_t n = 0; n < j[i].size(); ++n) a(i, n) = j[i][n].get<double>();
1717 }
1718 opt.qrf_alpha = a;
1719}
1720
1721/** The knobs the two SolverBA arms share. */
1722void apply_ba_knobs(const Knobs& k, line::ba::BaOptions& opt) {
1723 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1724 if (k.level > 0) opt.level = k.level;
1725 if (!k.qrf_params.empty()) decode_qrf_params(k.qrf_params, opt);
1726 if (!k.qrf_alpha.empty()) decode_qrf_alpha(k.qrf_alpha, opt);
1727}
1728
1729template <class T>
1730int solve_model_ba(const std::string& file, const Knobs& k) {
1731 line::qn::Network<T> net = read_model<T>(file);
1733 apply_ba_knobs(k, opt);
1735 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1736
1737 std::printf("SolverBA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1738 r.actualmethod.c_str(),
1739 line::util::method_type("BA", r.actualmethod).c_str());
1740 print_avg_table<T>(sn, r);
1741 return 0;
1742}
1743
1744/**
1745 * `-s ba -a bounds`: `SolverBA.getBounds` and its `getBoundsTable` row filter.
1746 *
1747 * NOT THE SAME ANSWER AS `-a avg`, which is why it is its own analysis. `-a avg`
1748 * reports ONE side of ONE family -- whichever `--method` named -- so a caller who
1749 * wants the bracket has to run the solver twice and know which two method names
1750 * pair up. `getBounds` takes the family (the method's prefix before the first
1751 * dot) and re-runs both sides under the caller's FULL option set, so a
1752 * hierarchical family tightens with `--level` as it should.
1753 *
1754 * A ONE-SIDED FAMILY REPORTS NaN ON THE SIDE IT LACKS, never zero: `cub` is
1755 * upper-only and `mbjb`/`ldbcmp` are lower-only, and a zero there would read as
1756 * a lower bound of zero rather than as the absence of one.
1757 */
1758template <class T>
1759int solve_model_ba_bounds(const std::string& file, const Knobs& k) {
1760 line::qn::Network<T> net = read_model<T>(file);
1762 apply_ba_knobs(k, opt);
1763 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1764 const line::ba::BaBounds<T> b = line::ba::ba_bounds(sn, opt);
1765 const std::string am = line::ba::resolve_method(opt.method);
1766
1767 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
1768 if (g_json_output) {
1769 line::reg::Json p = line::reg::Json::object();
1770 p["type"] = "BoundsTable";
1771 p["indexBase"] = 0;
1772 p["family"] = am.substr(0, am.find('.'));
1773 p["hasLower"] = b.has_lower;
1774 p["hasUpper"] = b.has_upper;
1775 for (const char* key : {"Station", "JobClass", "QLower", "QUpper", "TLower", "TUpper"})
1776 p[key] = line::reg::Json::array();
1777 for (std::size_t i = 0; i < sn.nstations; ++i)
1778 for (std::size_t c = 0; c < sn.nclasses; ++c) {
1779 if (!b.keep[i][c]) continue;
1780 p["Station"].push_back(sn.stations[i].name);
1781 p["JobClass"].push_back(sn.classes[c].name);
1782 p["QLower"].push_back(d(b.Qlower(i, c)));
1783 p["QUpper"].push_back(d(b.Qupper(i, c)));
1784 p["TLower"].push_back(d(b.Tlower(i, c)));
1785 p["TUpper"].push_back(d(b.Tupper(i, c)));
1786 }
1787 emit_analysis<T>("bounds", p, am);
1788 return 0;
1789 }
1790 std::printf("SolverBA arith=%s method=%s type=%s family=%s sides=%s\n",
1791 line::num_traits<T>::name(), am.c_str(),
1792 line::util::method_type("BA", am).c_str(), am.substr(0, am.find('.')).c_str(),
1793 b.has_lower ? (b.has_upper ? "lower,upper" : "lower") : "upper");
1794 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "QLower", "QUpper",
1795 "TLower", "TUpper");
1796 for (std::size_t i = 0; i < sn.nstations; ++i)
1797 for (std::size_t c = 0; c < sn.nclasses; ++c) {
1798 if (!b.keep[i][c]) continue;
1799 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", sn.stations[i].name.c_str(),
1800 sn.classes[c].name.c_str(), d(b.Qlower(i, c)), d(b.Qupper(i, c)),
1801 d(b.Tlower(i, c)), d(b.Tupper(i, c)));
1802 }
1803 return 0;
1804}
1805
1806/**
1807 * `-s qns`: the model handed to the external `qnsolver` binary.
1808 *
1809 * The only wrapper on the model-solving path. The banner names it separately
1810 * from the table so a reader can tell an independent tool's numbers from the
1811 * port's own -- which is the whole reason the wrapper exists.
1812 */
1813template <class T>
1814int solve_model_qns(const std::string& file, const Knobs& k) {
1815 line::qn::Network<T> net = read_model<T>(file);
1817 if (!k.method.empty()) opt.method = k.method;
1818 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
1819 // --samples is deliberately NOT forwarded: `options.samples` reaches the
1820 // JMVA document as `maxSamples`, which is JMT's Monte Carlo cap and which
1821 // qnsolver reads past. The shared knob ladder below refuses it for every
1822 // solver that draws nothing, and QNS is one of them.
1823 opt.timeout = k.timeout_seconds;
1824 opt.keep = k.keep;
1826 const line::qn::NetworkStruct<T>& sn = net.get_struct();
1827
1828 if (!g_json_output)
1829 std::printf("SolverQNS arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
1830 r.actualmethod.c_str(),
1831 line::util::method_type("QNS", r.actualmethod).c_str());
1832 print_avg_table<T>(sn, r);
1833 return 0;
1834}
1835
1836/**
1837 * Emit the base-class exponential response-time CDF fallback, the same
1838 * `CdfRespT` document every real distributional arm emits.
1839 *
1840 * This is `@@NetworkSolver/getCdfRespT.m`: the solvers without a
1841 * distributional result of their own (MVA, QNS, BA, AG) inherit an exponential
1842 * law with the right mean in the reference, and refusing `-a cdf` for them
1843 * diverged from it. The curve says nothing about the tail; the banner names
1844 * the solver so the caller knows which mean it wraps.
1845 */
1846template <class T>
1847int emit_default_cdf(const char* solver_name, const line::qn::NetworkStruct<T>& sn,
1848 const line::mva::AvgResult<T>& r) {
1849 const std::vector<std::vector<line::solvers::DefaultCdfCurve>> RD =
1851 if (g_json_output) {
1852 line::reg::Json p = line::reg::Json::object();
1853 p["type"] = "CdfRespT";
1854 p["indexBase"] = 0;
1855 line::reg::Json arr = line::reg::Json::array();
1856 for (std::size_t i = 0; i < RD.size(); ++i)
1857 for (std::size_t c = 0; c < RD[i].size(); ++c) {
1858 if (RD[i][c].t.empty()) continue;
1859 line::reg::Json e = line::reg::Json::object();
1860 e["Station"] = sn.stations[i].name;
1861 e["JobClass"] = sn.classes[c].name;
1862 e["station"] = i;
1863 e["jobclass"] = c;
1864 e["t"] = line::reg::Json(RD[i][c].t);
1865 e["F"] = line::reg::Json(RD[i][c].F);
1866 arr.push_back(e);
1867 }
1868 p["respt"] = arr;
1869 emit_analysis<T>("cdf", p, std::string());
1870 return 0;
1871 }
1872 std::printf("%s arith=%s method=%s (exponential fallback with the solver's mean)\n",
1873 solver_name, line::num_traits<T>::name(), r.actualmethod.c_str());
1874 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
1875 for (std::size_t i = 0; i < RD.size(); ++i)
1876 for (std::size_t c = 0; c < RD[i].size(); ++c)
1877 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
1878 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
1879 sn.classes[c].name.c_str(), RD[i][c].t[j], RD[i][c].F[j]);
1880 return 0;
1881}
1882
1883/** `-s mva -a cdf`: the inherited exponential fallback over the MVA means. */
1884template <class T>
1885int solve_model_mva_cdf(const std::string& file, const Knobs& k) {
1886 line::qn::Network<T> net = read_model<T>(file);
1888 if (!k.method.empty() && k.method != "default") opt.method = k.method;
1889 if (k.tol >= 0.0) opt.tol = k.tol;
1890 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
1891 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
1892 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
1893 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
1894 line::Matrix<T> init;
1896 return emit_default_cdf<T>("SolverMVA", net.get_struct(), r);
1897}
1898
1899/** `-s ag -a cdf`: the inherited exponential fallback over the RCAT means. */
1900template <class T>
1901int solve_model_ag_cdf(const std::string& file, const Knobs& k) {
1902 line::qn::Network<T> net = read_model<T>(file);
1904 apply_ag_knobs(k, opt);
1906 return emit_default_cdf<T>("SolverAG", net.get_struct(), r);
1907}
1908
1909/** `-s ba -a cdf`: the inherited exponential fallback over the bound means. */
1910template <class T>
1911int solve_model_ba_cdf(const std::string& file, const Knobs& k) {
1912 line::qn::Network<T> net = read_model<T>(file);
1914 apply_ba_knobs(k, opt);
1916 return emit_default_cdf<T>("SolverBA", net.get_struct(), r);
1917}
1918
1919/** `-s qns -a cdf`: the inherited exponential fallback over qnsolver's means. */
1920int solve_model_qns_cdf(const std::string& file, const Knobs& k) {
1921 line::qn::Network<double> net = read_model<double>(file);
1923 if (!k.method.empty()) opt.method = k.method;
1924 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
1925 opt.timeout = k.timeout_seconds;
1926 opt.keep = k.keep;
1928 return emit_default_cdf<double>("SolverQNS", net.get_struct(), r);
1929}
1930
1931
1932/**
1933 * `-a interval`: the support-only range, the reference's `getIntervalTable`.
1934 *
1935 * A SEPARATE FUNCTION because it is printed before the ensemble exists: on the
1936 * exact path there is no design to solve, so the interval arrives without a
1937 * `UqSolution` beside it.
1938 */
1939template <class T>
1940int print_uq_interval(const line::qn::NetworkStruct<T>& sn, const line::uq::UqInterval<T>& iv,
1941 const std::string& stage, std::size_t npriors) {
1942 auto v = [](const line::Matrix<T>& M, std::size_t i, std::size_t c) {
1943 return M.empty() ? 0.0 : line::num_traits<T>::to_double(M(i, c));
1944 };
1945 line::reg::Json p = line::reg::Json::object();
1946 p["type"] = "IntervalTable";
1947 p["indexBase"] = 0;
1948 p["exact"] = iv.exact;
1949 p["intervalMethod"] = iv.method;
1950 if (!iv.why.empty()) p["why"] = iv.why;
1951 if (iv.has_totals) {
1952 p["X"] = {line::num_traits<T>::to_double(iv.Xlo),
1954 p["Rtot"] = {line::num_traits<T>::to_double(iv.Rtot_lo),
1956 }
1957 for (const char* key : {"Station", "JobClass", "QLen_lo", "QLen_up", "Util_lo", "Util_up",
1958 "RespT_lo", "RespT_up", "Tput_lo", "Tput_up"})
1959 p[key] = line::reg::Json::array();
1960 if (!g_json_output) {
1961 std::printf("SolverUQ arith=%s interval=%s exact=%s priors=%zu stage=%s\n",
1962 line::num_traits<T>::name(), iv.method.c_str(), iv.exact ? "yes" : "no",
1963 npriors, stage.c_str());
1964 // A RANGE THAT IS NOT AN ENCLOSURE MUST SAY SO. The sampled path
1965 // spans the design points only, so on a continuous Prior it lies
1966 // strictly inside the true range; printing it beside an exact hull
1967 // without the reason would make the two indistinguishable.
1968 if (!iv.exact)
1969 std::fprintf(stderr,
1970 "Warning: exact interval MVA does not apply (%s); the range below is "
1971 "over the solved design points and is not an enclosure.\n",
1972 iv.why.c_str());
1973 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s %12s %12s\n", "Station",
1974 "JobClass", "QLen_lo", "QLen_up", "Util_lo", "Util_up", "RespT_lo",
1975 "RespT_up", "Tput_lo", "Tput_up");
1976 }
1977 for (std::size_t i = 0; i < sn.nstations; ++i)
1978 for (std::size_t c = 0; c < sn.nclasses; ++c) {
1979 // The reference's row filter: an upper endpoint of zero on every
1980 // presence metric means the class never visits the station.
1981 if (v(iv.Qup, i, c) <= 0.0 && v(iv.Uup, i, c) <= 0.0 && v(iv.Tup, i, c) <= 0.0)
1982 continue;
1983 if (!g_json_output) {
1984 std::printf(
1985 "%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
1986 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), v(iv.Qlo, i, c),
1987 v(iv.Qup, i, c), v(iv.Ulo, i, c), v(iv.Uup, i, c), v(iv.Rlo, i, c),
1988 v(iv.Rup, i, c), v(iv.Tlo, i, c), v(iv.Tup, i, c));
1989 continue;
1990 }
1991 p["Station"].push_back(sn.stations[i].name);
1992 p["JobClass"].push_back(sn.classes[c].name);
1993 p["QLen_lo"].push_back(v(iv.Qlo, i, c));
1994 p["QLen_up"].push_back(v(iv.Qup, i, c));
1995 p["Util_lo"].push_back(v(iv.Ulo, i, c));
1996 p["Util_up"].push_back(v(iv.Uup, i, c));
1997 p["RespT_lo"].push_back(v(iv.Rlo, i, c));
1998 p["RespT_up"].push_back(v(iv.Rup, i, c));
1999 p["Tput_lo"].push_back(v(iv.Tlo, i, c));
2000 p["Tput_up"].push_back(v(iv.Tup, i, c));
2001 }
2002 if (g_json_output) emit_analysis<T>("interval", p, iv.method);
2003 return 0;
2004}
2005
2006/**
2007 * Solve a Network model.json carrying a Prior with SolverUQ.
2008 *
2009 * TWO FLAGS MEAN SOMETHING ELSE HERE, and both are UQ's own rather than the
2010 * stage solver's: `--method` names the DESIGN (quadrature or montecarlo, the
2011 * reference's `options.method`), and `--samples` the number of nodes per
2012 * continuous Prior (`options.samples`, 11 by default and not the simulation
2013 * default, since each node is a full solver run). The engine that runs at each
2014 * point is `--uq-solver`, and it keeps its own defaults for everything except
2015 * the convergence knobs, which UQ does not have and therefore passes through.
2016 * A caller who wants an SSA run length AND a UQ design cannot state both, so
2017 * the SSA stage keeps its default sample count; that is stated in --help rather
2018 * than resolved by giving one flag two meanings.
2019 *
2020 * `-a posterior` prints the design itself -- every point, its weight and its
2021 * metrics -- because the expectation alone hides whether it averaged two nearby
2022 * models or two wildly different ones, and that spread IS the answer to an
2023 * uncertainty question.
2024 */
2025template <class T>
2026int solve_model_uq(const std::string& file, const Knobs& k, const std::string& analysis) {
2027 line::qn::Network<T> net = read_model<T>(file);
2029 if (!k.method.empty()) opt.method = k.method;
2030 if (k.samples) opt.samples = k.samples;
2031 if (k.seed) opt.seed = k.seed;
2033 so.solver = k.uq_solver;
2034 so.tol = k.tol;
2035 so.iter_tol = k.iter_tol;
2036 so.iter_max = k.iter_max;
2037 so.cutoff = k.cutoff;
2038
2039 if (analysis == "interval") {
2040 // BEFORE THE ENSEMBLE, because the exact path does not need one: it is
2041 // 2*(m+2) MVA calls over the demand box, and the reference's
2042 // `getInterval` likewise reaches `intervalByMVA` without touching
2043 // `self.results`. Only the sampling fallback solves the design.
2044 const line::uq::UqInterval<T> iv =
2046 const line::qn::NetworkStruct<T>& isn = net.get_struct();
2047 return print_uq_interval<T>(isn, iv, so.solver, line::uq::uq_detect_priors(isn).size());
2048 }
2049
2050 const line::uq::UqSolution<T> r =
2052 const line::qn::NetworkStruct<T>& sn = net.get_struct();
2053
2054 if (analysis == "avg") {
2055 if (!g_json_output)
2056 std::printf("SolverUQ arith=%s design=%s points=%zu priors=%zu stage=%s method=%s\n",
2057 line::num_traits<T>::name(), r.method.c_str(), r.points.size(),
2058 r.sites.size(), so.solver.c_str(), r.avg.actualmethod.c_str());
2059 print_avg_table<T>(sn, r.avg);
2060 return 0;
2061 }
2062
2063 // -a posterior: the per-design-point table, the reference's getPosteriorTable.
2064 line::reg::Json p = line::reg::Json::object();
2065 p["type"] = "PosteriorTable";
2066 p["indexBase"] = 0;
2067 p["design"] = r.method;
2068 p["stage"] = so.solver;
2069 line::reg::Json sites = line::reg::Json::array();
2070 for (std::size_t l = 0; l < r.sites.size(); ++l) {
2071 line::reg::Json s = line::reg::Json::object();
2072 s["node"] = sn.nodes[r.sites[l].node - 1].name;
2073 s["class"] = sn.classes[r.sites[l].cls - 1].name;
2074 s["kind"] = r.sites[l].arrival ? "arrival" : "service";
2075 sites.push_back(s);
2076 }
2077 p["priors"] = sites;
2078 for (const char* key : {"Point", "Weight", "Station", "JobClass", "QLen", "Util", "RespT",
2079 "Tput"})
2080 p[key] = line::reg::Json::array();
2081 line::reg::Json means = line::reg::Json::array();
2082
2083 if (!g_json_output) {
2084 std::printf("SolverUQ arith=%s design=%s points=%zu priors=%zu stage=%s\n",
2085 line::num_traits<T>::name(), r.method.c_str(), r.points.size(), r.sites.size(),
2086 so.solver.c_str());
2087 // THE SUBSTITUTED MEANS ARE PROVENANCE, not decoration: a design point
2088 // is a model, and this line is what says which one.
2089 std::printf("%-6s %12s substituted means\n", "Point", "Weight");
2090 for (std::size_t e = 0; e < r.points.size(); ++e) {
2091 std::printf("%-6zu %12.6g ", e + 1, line::num_traits<T>::to_double(r.weights[e]));
2092 for (std::size_t l = 0; l < r.sites.size(); ++l)
2093 std::printf(" %s@%s=%.6g", sn.nodes[r.sites[l].node - 1].name.c_str(),
2094 sn.classes[r.sites[l].cls - 1].name.c_str(),
2095 line::num_traits<T>::to_double(r.design[e].dists[l].mean));
2096 std::printf("\n");
2097 }
2098 std::printf("%-6s %12s %-16s %-14s %12s %12s %12s %12s\n", "Point", "Weight", "Station",
2099 "JobClass", "QLen", "Util", "RespT", "Tput");
2100 }
2101 for (std::size_t e = 0; e < r.points.size(); ++e) {
2102 line::reg::Json row = line::reg::Json::array();
2103 for (std::size_t l = 0; l < r.sites.size(); ++l)
2104 row.push_back(line::num_traits<T>::to_double(r.design[e].dists[l].mean));
2105 means.push_back(row);
2106 const line::mva::AvgResult<T>& a = r.points[e];
2107 for (std::size_t i = 0; i < sn.nstations; ++i)
2108 for (std::size_t c = 0; c < sn.nclasses; ++c) {
2109 const double q = line::num_traits<T>::to_double(a.QN(i, c));
2110 const double u = line::num_traits<T>::to_double(a.UN(i, c));
2111 const double rr = line::num_traits<T>::to_double(a.RN(i, c));
2112 const double t = line::num_traits<T>::to_double(a.TN(i, c));
2113 // getPosteriorTable's row filter, the PRESENCE metrics only: a
2114 // response time alone does not put a class at a station, and
2115 // reading it as presence kept rows the reference drops.
2116 if (q <= 0.0 && u <= 0.0 && t <= 0.0) continue;
2117 if (!g_json_output) {
2118 std::printf("%-6zu %12.6g %-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", e + 1,
2120 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), q, u, rr,
2121 t);
2122 continue;
2123 }
2124 p["Point"].push_back(e);
2125 p["Weight"].push_back(line::num_traits<T>::to_double(r.weights[e]));
2126 p["Station"].push_back(sn.stations[i].name);
2127 p["JobClass"].push_back(sn.classes[c].name);
2128 p["QLen"].push_back(q);
2129 p["Util"].push_back(u);
2130 p["RespT"].push_back(rr);
2131 p["Tput"].push_back(t);
2132 }
2133 }
2134 if (g_json_output) {
2135 p["substitutedMean"] = means;
2136 emit_analysis<T>("posterior", p, r.avg.actualmethod);
2137 }
2138 return 0;
2139}
2140
2141/**
2142 * The banner every `-s ctmc` analysis opens with.
2143 *
2144 * It carries the state count and the cutoff because a CTMC number is not the
2145 * model's number without them: the space is what was enumerated, and on an open
2146 * model the cutoff is what truncated it.
2147 */
2148template <class T>
2149void print_ctmc_banner(const line::ctmc::CtmcSolution<T>& d) {
2150 std::size_t cut = 0;
2151 for (std::size_t i = 0; i < d.cutoff.size(); ++i) cut = std::max(cut, d.cutoff[i]);
2152 if (cut)
2153 std::printf("SolverCTMC arith=%s method=%s type=%s states=%zu cutoff=%zu\n",
2155 line::util::method_type("CTMC", d.actualmethod).c_str(), d.chain.space.size(),
2156 cut);
2157 else
2158 std::printf("SolverCTMC arith=%s method=%s type=%s states=%zu\n",
2160 line::util::method_type("CTMC", d.actualmethod).c_str(), d.chain.space.size());
2161 // The library never writes to stderr, so an unseeded reducible mixture is
2162 // reported here or not at all -- and it is the one case where the printed
2163 // distribution is not the model's.
2164 if (!d.warning.empty()) std::fprintf(stderr, "warning: %s\n", d.warning.c_str());
2165}
2166
2167/**
2168 * The banner's own content as JSON, added to every `-s ctmc` payload.
2169 *
2170 * A CTMC number is not the model's number without them, which is why the
2171 * readable banner carries them: the space is what was actually enumerated, and
2172 * on an open model the cutoff is what truncated it. A host that reported the
2173 * occupancy of a truncated chain as the model's would be reporting a different
2174 * model's answer, so the two travel with the payload rather than only above it.
2175 */
2176template <class T>
2177line::reg::Json ctmc_meta(const line::ctmc::CtmcSolution<T>& d) {
2178 line::reg::Json m = line::reg::Json::object();
2179 m["states"] = d.chain.space.size();
2180 std::size_t cut = 0;
2181 for (std::size_t i = 0; i < d.cutoff.size(); ++i) cut = std::max(cut, d.cutoff[i]);
2182 if (cut) m["cutoff"] = cut;
2183 return m;
2184}
2185
2186/**
2187 * `-a avg`: the AvgTable, on whichever path the model's regions require.
2188 *
2189 * `solver_ctmc_analyzer_any` and not `solver_ctmc_analyzer`, so a WAITQ region
2190 * reaches the augmented walk that carries its token FIFO instead of the
2191 * lattice analyzer, which refuses it. Every other region rule keeps refusing.
2192 */
2193template <class T>
2194int solve_ctmc_avg(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
2197 print_ctmc_banner<T>(a.sol);
2198 print_avg_table<T>(sn, r);
2199 // A PARKED JOB IS IN NO QLen COLUMN: it left its station and sits in the
2200 // region's FIFO, so the model's population balances only once this is read
2201 // alongside the table rather than instead of it.
2202 if (a.waitq) {
2203 std::printf("%-16s %-14s %12s\n", "Region", "JobClass", "Parked");
2204 for (std::size_t c = 0; c < sn.nclasses && c < a.parked.size(); ++c)
2205 std::printf("%-16s %-14s %12.6g\n", "(all)", sn.classes[c].name.c_str(),
2207 }
2208 return 0;
2209}
2210
2211/**
2212 * `-a avg` under `--method mdd`: the AvgTable from the level aggregation.
2213 *
2214 * A SEPARATE ARM, and not a branch inside `solve_ctmc_avg`, because the method
2215 * never forms the |S|-state generator: there is no state space to print a state
2216 * count from and no cutoff to report, so the banner is the diagram's own -- the
2217 * cardinality it counts symbolically, what the levels actually hold, and whether
2218 * the answer is certified exact. Reporting the generator banner here would name
2219 * a chain that was never built.
2220 */
2221template <class T>
2222int solve_ctmc_mdd_avg(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
2223 const line::mdd::MddMcdOptions& mcdopt) {
2224 // EVERY BACKEND, `exact` included: the level solve picks Householder QR in
2225 // floating point and `line::lstsq` under exact arithmetic, so nothing here
2226 // takes a square root that a rational field has no answer for.
2229 d.avg = s.avg;
2232 std::size_t held = 0;
2233 for (std::size_t k = 0; k < s.level_sizes.size(); ++k) held += s.level_sizes[k];
2234 std::printf(
2235 "SolverCTMC arith=%s method=%s type=%s states=%lld held=%zu levels=%zu iters=%d "
2236 "encoding=%s exact=%s\n",
2238 line::util::method_type("CTMC", s.actualmethod).c_str(), s.num_states, held,
2239 s.level_sizes.size(), s.iters, s.encoding.c_str(),
2240 // "certified" is not "exact": a product-form model is exact however
2241 // much its diagram shares, so the false case says only that the
2242 // STRUCTURAL test did not fire.
2243 s.no_aggregation ? "certified" : "product-form-only");
2244 print_avg_table<T>(sn, r);
2245 return 0;
2246}
2247
2248/**
2249 * `-a avg` under `--method cftp` / `cftp.approx`: the AvgTable from perfect
2250 * sampling.
2251 *
2252 * THE BANNER NAMES THE RUN LENGTH because these numbers carry Monte Carlo error
2253 * and are not comparable to an exact solver's at solver tolerance. The mean
2254 * coalescence horizon travels with it: it is the cost the exact sampler paid,
2255 * has no a-priori bound, and is the one number that says whether the draw was
2256 * cheap or the model is nearly saturated.
2257 */
2258template <class T>
2259int solve_ctmc_cftp_avg(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
2260 const line::ctmc::CtmcCftpOptions& cftpopt) {
2263 "the cftp methods draw random states and form the station balance functions in the "
2264 "log domain, neither of which exists in exact rational arithmetic; rerun with --arith "
2265 "double or --arith real");
2266 } else {
2268 line::ctmc::solver_ctmc_cftp(sn, opt, cftpopt);
2270 d.avg = s.avg;
2273 double horizon = 0.0;
2274 for (std::size_t i = 0; i < s.horizon.size(); ++i)
2275 horizon += static_cast<double>(s.horizon[i]);
2276 if (!s.horizon.empty()) horizon /= static_cast<double>(s.horizon.size());
2277 std::printf(
2278 "SolverCTMC arith=%s method=%s type=%s samples=%zu seed=%lu distinct=%zu "
2279 "meanhorizon=%.6g\n",
2281 line::util::method_type("CTMC", s.actualmethod).c_str(), cftpopt.samples,
2282 cftpopt.seed, s.distinct_states.size(), horizon);
2283 print_avg_table<T>(sn, r);
2284 return 0;
2285 }
2286}
2287
2288/**
2289 * `-a prob`: the four SolverCTMC probability queries over the model's default
2290 * initial state -- `getProbSys`, `getProbSysAggr`, and `getProb`/`getProbAggr`
2291 * per station.
2292 *
2293 * ALL FOUR AND NOT ONE, because the joint and the aggregate answer different
2294 * questions and the pair is what makes either readable: `getProbSys` is the
2295 * probability of exactly that state, phases and buffer arrangement included,
2296 * while `getProbSysAggr` sums over every arrangement realizing the same
2297 * per-class counts. On a single-phase model with no buffer ordering the two
2298 * coincide, and where they do not the ratio is what the encoding added.
2299 *
2300 * NOT THE SAME NUMBERS AS `-s mva -a prob` OR `-s nc -a prob`, and that is the
2301 * point of having all three: MVA fits a binomial to its own means and NC takes a
2302 * ratio of normalizing constants under the product form, whereas these are the
2303 * stationary law of the chain itself and are exact for any model the chain
2304 * represents, product-form or not.
2305 */
2306template <class T>
2307int solve_ctmc_prob(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
2308 const Knobs& k) {
2311 if (!line::ctmc::analyzer_detail::default_init_state(sn, init))
2313 "-a prob reports the probability of the model's DEFAULT INITIAL STATE, and this "
2314 "model's initial marking admits no state; check the class populations against their "
2315 "reference stations");
2316 // `--state` is `getProb(node, state)`'s second argument: the ENCODED ROW of
2317 // one stateful node's own state space, not a per-class job count. The
2318 // reference substitutes it into `sn.state{node}` and leaves every other
2319 // node at its default, which is what happens here -- so the station
2320 // marginals below are the requested state's, and the two system
2321 // probabilities are that state's joined with the rest of the default
2322 // marking, exactly as `setState` followed by `getProbSys` reports it.
2323 if (!k.state.empty()) {
2324 if (!k.node)
2325 throw line::InputError(
2326 "--state is the state of ONE node and needs --node to say which; a bare state "
2327 "vector cannot be matched against a network whose nodes have different widths");
2328 const std::size_t isf = sn.stateful_index(k.node);
2329 if (isf == 0)
2330 throw line::InputError("--node " + std::to_string(k.node) +
2331 " is not a stateful node, so it holds no state to ask about");
2332 // THE WIDTH MUST MATCH EXACTLY, and a short vector is refused rather
2333 // than padded. Every row of a node's block is stored at the node's
2334 // widest encoding, so a padded row IS a state -- just not the one the
2335 // caller named: on an FCFS queue holding two jobs the encoding is
2336 // (buffer, in-service phase count) = (1,1), and padding `--state 2` to
2337 // (0,2) names a state the chain never visits, which would answer 0
2338 // where the caller expected the marginal. The width is reported so the
2339 // next attempt can be right.
2340 const std::size_t w = d.chain.space.empty() ? k.state.size()
2341 : d.chain.space[0].local[isf - 1].size();
2342 if (k.state.size() != w)
2343 throw line::InputError(
2344 "--state has " + std::to_string(k.state.size()) + " entries but node " +
2345 std::to_string(k.node) + " encodes its state in " + std::to_string(w) +
2346 "; getProb(node, state) takes the node's whole encoded row, and a shorter one "
2347 "padded with zeros is a different state rather than a partial one (use -a marg "
2348 "for a per-class job-count marginal)");
2349 std::vector<T> row(w, line::num_traits<T>::from_int(0));
2350 for (std::size_t i = 0; i < k.state.size(); ++i)
2351 row[i] = line::num_traits<T>::from_int(k.state[i]);
2352 init.local[isf - 1] = row;
2353 }
2354 const T psys = line::ctmc::solver_ctmc_joint(sn, d, init);
2355 const T psysaggr = line::ctmc::solver_ctmc_jointaggr(sn, d, init);
2356 const std::vector<T> pmarg = line::ctmc::solver_ctmc_marg(sn, d, init);
2357 const std::vector<T> pmargaggr = line::ctmc::solver_ctmc_margaggr(sn, d, init);
2358
2359 if (g_json_output) {
2360 line::reg::Json p = line::reg::Json::object();
2361 p["type"] = "ProbAggr";
2362 p["indexBase"] = 0;
2363 p["ProbSys"] = line::num_traits<T>::to_double(psys);
2364 p["ProbSysAggr"] = line::num_traits<T>::to_double(psysaggr);
2365 line::reg::Json st = line::reg::Json::array(), pm = line::reg::Json::array(),
2366 pa = line::reg::Json::array();
2367 for (std::size_t i = 0; i < sn.nstations; ++i) {
2368 st.push_back(sn.stations[i].name);
2369 pm.push_back(line::num_traits<T>::to_double(pmarg[i]));
2370 pa.push_back(line::num_traits<T>::to_double(pmargaggr[i]));
2371 }
2372 p["Station"] = st;
2373 p["Prob"] = pm;
2374 p["ProbAggr"] = pa;
2375 emit_analysis<T>("prob", p, d.actualmethod, ctmc_meta<T>(d));
2376 return 0;
2377 }
2378 print_ctmc_banner<T>(d);
2379 std::printf("ProbSys %.10g\n", line::num_traits<T>::to_double(psys));
2380 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(psysaggr));
2381 std::printf("%-16s %14s %14s\n", "Station", "Prob", "ProbAggr");
2382 for (std::size_t i = 0; i < sn.nstations; ++i)
2383 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(),
2385 line::num_traits<T>::to_double(pmargaggr[i]));
2386 return 0;
2387}
2388
2389/**
2390 * `-a gen`: `getInfGen`, as Q in sparse triplets plus the synchronization list
2391 * its event filtration is indexed by.
2392 *
2393 * Q IS PRINTED SPARSELY. A generator's rows hold one entry per enabled
2394 * transition and the space can run to thousands of states, so the dense form
2395 * would be quadratic in a quantity that is linear in the model.
2396 */
2397/**
2398 * The derived START/PREEMPT filtration as one JSON block per (station, class),
2399 * each carrying its own 0-based Station and Class beside the usual From/To/Rate
2400 * triplets. An all-zero block is omitted: on a model with no preemption that is
2401 * every block of the PREEMPT filtration.
2402 */
2403template <class T>
2404line::reg::Json aux_filt_json(const std::vector<std::vector<line::Matrix<T> > >& filt,
2405 std::size_t n) {
2406 line::reg::Json blocks = line::reg::Json::array();
2407 for (std::size_t i = 0; i < filt.size(); ++i)
2408 for (std::size_t r = 0; r < filt[i].size(); ++r) {
2409 line::reg::Json bfrom = line::reg::Json::array(), bto = line::reg::Json::array(),
2410 brate = line::reg::Json::array();
2411 for (std::size_t a = 0; a < n; ++a)
2412 for (std::size_t b = 0; b < n; ++b) {
2413 const double q = line::num_traits<T>::to_double(filt[i][r](a, b));
2414 if (q == 0.0) continue;
2415 bfrom.push_back(a);
2416 bto.push_back(b);
2417 brate.push_back(q);
2418 }
2419 if (bfrom.empty()) continue;
2420 line::reg::Json e = line::reg::Json::object();
2421 e["Station"] = i;
2422 e["Class"] = r;
2423 e["From"] = bfrom;
2424 e["To"] = bto;
2425 e["Rate"] = brate;
2426 blocks.push_back(e);
2427 }
2428 return blocks;
2429}
2430
2431template <class T>
2432int solve_ctmc_gen(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
2434 o.keep_filtration = true; // the filtration is half of what getInfGen returns
2437 const std::size_t n = g.Q.rows();
2438 std::size_t nnz = 0;
2439 for (std::size_t i = 0; i < n; ++i)
2440 for (std::size_t j = 0; j < n; ++j)
2441 if (line::num_traits<T>::to_double(g.Q(i, j)) != 0.0) ++nnz;
2442 if (g_json_output) {
2443 line::reg::Json p = line::reg::Json::object();
2444 p["type"] = "InfGen";
2445 p["indexBase"] = 0;
2446 p["size"] = n;
2447 p["nnz"] = nnz;
2448 // SPARSE HERE TOO, for the reason the table is: a generator has one entry
2449 // per enabled transition, so the dense form is quadratic in a quantity
2450 // that is linear in the model. A host rebuilds Q with one scatter.
2451 line::reg::Json from = line::reg::Json::array(), to = line::reg::Json::array(),
2452 rate = line::reg::Json::array();
2453 for (std::size_t i = 0; i < n; ++i)
2454 for (std::size_t j = 0; j < n; ++j) {
2455 const double q = line::num_traits<T>::to_double(g.Q(i, j));
2456 if (q == 0.0) continue;
2457 from.push_back(i);
2458 to.push_back(j);
2459 rate.push_back(q);
2460 }
2461 p["From"] = from;
2462 p["To"] = to;
2463 p["Rate"] = rate;
2464 // The synchronization list is HALF of what getInfGen returns: without it
2465 // the filtration's index has no meaning, so it travels with Q. Its node
2466 // and class are the port's own 1-BASED indices, with 0 the LOCAL dummy
2467 // -- `indexBase` above governs the state indices, which is where a host
2468 // would otherwise be indexing a chain by an off-by-one.
2469 line::reg::Json sync = line::reg::Json::array();
2470 for (std::size_t a = 0; a < g.sync.size() && a < g.filt.size(); ++a) {
2471 // THE FILTER ITSELF, not only its nnz as the readable table reports:
2472 // `eventFilt` is half of what getInfGen returns, and a host handed
2473 // only Q cannot recover which synchronization contributed a rate --
2474 // Q's entries have already summed every one of them.
2475 std::size_t fnz = 0;
2476 line::reg::Json ffrom = line::reg::Json::array(), fto = line::reg::Json::array(),
2477 frate = line::reg::Json::array();
2478 for (std::size_t i = 0; i < n; ++i)
2479 for (std::size_t j = 0; j < n; ++j) {
2480 const double q = line::num_traits<T>::to_double(g.filt[a](i, j));
2481 if (q == 0.0) continue;
2482 ++fnz;
2483 ffrom.push_back(i);
2484 fto.push_back(j);
2485 frate.push_back(q);
2486 }
2487 line::reg::Json e = line::reg::Json::object();
2488 e["From"] = ffrom;
2489 e["To"] = fto;
2490 e["Rate"] = frate;
2491 e["activeEvent"] = line::lang::event_to_text(g.sync[a].active.event);
2492 e["activeNode"] = g.sync[a].active.node;
2493 e["activeClass"] = g.sync[a].active.cls;
2494 e["passiveEvent"] = line::lang::event_to_text(g.sync[a].passive.event);
2495 e["passiveNode"] = g.sync[a].passive.node;
2496 e["passiveClass"] = g.sync[a].passive.cls;
2497 e["nnz"] = fnz;
2498 sync.push_back(e);
2499 }
2500 p["sync"] = sync;
2501 // The DERIVED filtrations travel under their own keys, one block per
2502 // (station, class), because they are NOT synchronizations: a START
2503 // rides on an arc `sync` already carries, so folding them in would make
2504 // a host summing the filtration double-count the generator.
2505 p["startFilt"] = aux_filt_json<T>(g.start_filt, n);
2506 p["preemptFilt"] = aux_filt_json<T>(g.preempt_filt, n);
2507 emit_analysis<T>("gen", p, d.actualmethod, ctmc_meta<T>(d));
2508 return 0;
2509 }
2510 print_ctmc_banner<T>(d);
2511 std::printf("InfGen events=%zu nnz=%zu\n", g.sync.size(), nnz);
2512 std::printf("%8s %8s %16s\n", "From", "To", "Rate");
2513 for (std::size_t i = 0; i < n; ++i)
2514 for (std::size_t j = 0; j < n; ++j) {
2515 const double q = line::num_traits<T>::to_double(g.Q(i, j));
2516 if (q != 0.0) std::printf("%8zu %8zu %16.10g\n", i + 1, j + 1, q);
2517 }
2518 std::printf("%6s %-10s %6s %6s %-10s %6s %6s %8s\n", "Event", "ActEvent", "ActNode", "ActCls",
2519 "PasEvent", "PasNode", "PasCls", "Nnz");
2520 for (std::size_t a = 0; a < g.sync.size() && a < g.filt.size(); ++a) {
2521 std::size_t fnz = 0;
2522 for (std::size_t i = 0; i < n; ++i)
2523 for (std::size_t j = 0; j < n; ++j)
2524 if (line::num_traits<T>::to_double(g.filt[a](i, j)) != 0.0) ++fnz;
2525 std::printf("%6zu %-10s %6zu %6zu %-10s %6zu %6zu %8zu\n", a + 1,
2526 line::lang::event_to_text(g.sync[a].active.event), g.sync[a].active.node,
2527 g.sync[a].active.cls, line::lang::event_to_text(g.sync[a].passive.event),
2528 g.sync[a].passive.node, g.sync[a].passive.cls, fnz);
2529 }
2530 return 0;
2531}
2532
2533/** `-a states`: `getStateSpace` and `getStateSpaceAggr`, side by side. */
2534template <class T>
2535int solve_ctmc_states(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
2539 if (g_json_output) {
2540 line::reg::Json p = line::reg::Json::object();
2541 p["type"] = "StateSpace";
2542 p["indexBase"] = 0;
2543 // The per-node widths are the ONLY thing that makes a flat row decodable:
2544 // they are where one stateful node's block ends and the next begins.
2545 p["NodeWidths"] = index_json(s.node_width);
2546 p["space"] = matrix_json(s.flat);
2547 p["spaceAggr"] = matrix_json(A);
2548 // `localStateSpace` of `[stateSpace, localStateSpace] = getStateSpace`.
2549 // One entry per stateful node, in stateful-node order, holding that
2550 // node's distinct local rows. A host with only the flat space can
2551 // return the second output as a SINGLE cell holding the whole space,
2552 // which is what `getStateSpace.m` does under `lang='cpp'`, and a caller
2553 // indexing it per node then reads the global space for every node.
2554 line::reg::Json loc = line::reg::Json::array();
2555 for (std::size_t f = 0; f < s.local.size(); ++f) loc.push_back(matrix_json(s.local[f]));
2556 p["localSpace"] = loc;
2557 // pi travels with the space because a row of the space is not an answer:
2558 // the pair (state, probability) is, and reading them from two invocations
2559 // would risk pairing one solve's states with another solve's law.
2560 p["pi"] = vector_json(d.pi);
2561 emit_analysis<T>("states", p, d.actualmethod, ctmc_meta<T>(d));
2562 return 0;
2563 }
2564 print_ctmc_banner<T>(d);
2565 // The per-node widths are printed because the flat row is only decodable
2566 // with them: they are where one node's block ends and the next begins.
2567 std::printf("NodeWidths");
2568 for (std::size_t f = 0; f < s.node_width.size(); ++f) std::printf(" %zu", s.node_width[f]);
2569 std::printf("\n");
2570 std::printf("%8s %12s %s\n", "State", "Prob", "Detailed | Aggregate");
2571 for (std::size_t i = 0; i < s.flat.rows(); ++i) {
2572 std::printf("%8zu %12.6g ", i + 1, line::num_traits<T>::to_double(d.pi[i]));
2573 for (std::size_t c = 0; c < s.flat.cols(); ++c)
2574 std::printf(" %g", line::num_traits<T>::to_double(s.flat(i, c)));
2575 std::printf(" |");
2576 for (std::size_t c = 0; c < A.cols(); ++c)
2577 std::printf(" %g", line::num_traits<T>::to_double(A(i, c)));
2578 std::printf("\n");
2579 }
2580 return 0;
2581}
2582
2583/**
2584 * `-a sens`: `getSensitivityRanking` over the exponential service rates.
2585 *
2586 * THE REWARD IS STATED, NOT INFERRED. The reference makes the caller supply one,
2587 * and a model.json carries no reward function, so the CLI has to name the reward
2588 * it ranks against or the numbers mean nothing: it is the mean number of jobs at
2589 * the QUEUEING stations, which excludes the Source (whose column is the infinite
2590 * reservoir) and the Delay stations (whose population is think time, not work).
2591 *
2592 * Only an exponential service pair becomes a parameter. Perturbing any other
2593 * distribution would mean replacing it with an exponential of the new rate,
2594 * which changes the model's shape rather than one of its parameters; those pairs
2595 * are listed as skipped rather than silently differenced.
2596 */
2597template <class T>
2598int solve_ctmc_sens(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
2599 const std::size_t M = sn.nstations, K = sn.nclasses;
2602
2603 std::vector<bool> queueing(M, false);
2604 for (std::size_t i = 0; i < M; ++i)
2605 queueing[i] = sn.stations[i].nodetype != line::lang::NodeType::Source &&
2608
2609 std::vector<T> reward(d.chain.space.size(), line::num_traits<T>::from_int(0));
2610 for (std::size_t s = 0; s < reward.size(); ++s)
2611 for (std::size_t i = 0; i < M; ++i)
2612 if (queueing[i])
2613 for (std::size_t c = 0; c < K; ++c) reward[s] += A(s, i * K + c);
2614
2615 std::vector<line::ctmc::CtmcSensParam<T> > params;
2616 std::vector<std::string> skipped;
2617 for (std::size_t i = 0; i < M; ++i) {
2618 if (sn.stations[i].nodetype == line::lang::NodeType::Source) continue;
2619 for (std::size_t c = 0; c < K; ++c) {
2620 if (sn.disabled[i][c]) continue;
2621 const double mu = line::num_traits<T>::to_double(sn.rates(i, c));
2622 if (!(mu > 0.0)) continue;
2623 const std::string nm =
2624 "mu(" + sn.stations[i].name + "," + sn.classes[c].name + ")";
2625 if (sn.service[i][c].type != line::lang::ProcessType::EXP) {
2626 skipped.push_back(nm);
2627 continue;
2628 }
2630 p.name = nm;
2631 p.value = mu;
2632 p.set = [i, c](line::qn::NetworkStruct<T>& s, double v) {
2634 s.refresh_rates();
2635 };
2636 params.push_back(p);
2637 }
2638 }
2639 if (params.empty())
2641 "-a sens found no exponential service rate to differentiate: every enabled "
2642 "(station, class) pair carries a non-exponential distribution, and replacing one with "
2643 "an exponential of the perturbed rate would change the model rather than a parameter "
2644 "of it");
2645
2646 const std::vector<line::ctmc::CtmcSensRank<T> > rank =
2647 line::ctmc::solver_ctmc_sensitivity_ranking(sn, opt, params, reward);
2648 if (g_json_output) {
2649 line::reg::Json p = line::reg::Json::object();
2650 p["type"] = "SensRanking";
2651 // THE REWARD IS STATED, NOT INFERRED, on this path too: a ranking is a
2652 // ranking against something, and a host that read these numbers without
2653 // knowing what was differentiated would be reading a sensitivity of an
2654 // unnamed functional.
2655 p["reward"] = "mean number of jobs at the queueing stations";
2656 line::reg::Json par = line::reg::Json::array(), val = line::reg::Json::array(),
2657 S = line::reg::Json::array(), SS = line::reg::Json::array();
2658 for (std::size_t l = 0; l < rank.size(); ++l) {
2659 par.push_back(rank[l].parameter);
2660 val.push_back(rank[l].value);
2661 S.push_back(line::num_traits<T>::to_double(rank[l].S));
2662 // null, not NaN and not 0: a zero mean reward makes the scaled form
2663 // undefined, which is a property of the model, and JSON has no NaN.
2664 if (rank[l].scaled_valid)
2665 SS.push_back(line::num_traits<T>::to_double(rank[l].SS));
2666 else
2667 SS.push_back(line::reg::Json());
2668 }
2669 p["Parameter"] = par;
2670 p["Value"] = val;
2671 p["Sens"] = S;
2672 p["ScaledSens"] = SS;
2673 line::reg::Json sk = line::reg::Json::array();
2674 for (std::size_t l = 0; l < skipped.size(); ++l) sk.push_back(skipped[l]);
2675 p["Skipped"] = sk;
2676 emit_analysis<T>("sens", p, d.actualmethod, ctmc_meta<T>(d));
2677 return 0;
2678 }
2679 print_ctmc_banner<T>(d);
2680 std::printf("Reward mean number of jobs at the queueing stations\n");
2681 std::printf("%-28s %14s %16s %16s\n", "Parameter", "Value", "Sens", "ScaledSens");
2682 for (std::size_t l = 0; l < rank.size(); ++l) {
2683 if (rank[l].scaled_valid)
2684 std::printf("%-28s %14.6g %16.8g %16.8g\n", rank[l].parameter.c_str(), rank[l].value,
2686 line::num_traits<T>::to_double(rank[l].SS));
2687 else
2688 // MATLAB reports NaN here; the reason is printed instead, since a
2689 // zero mean reward is a property of the model and not a failure.
2690 std::printf("%-28s %14.6g %16.8g %16s\n", rank[l].parameter.c_str(), rank[l].value,
2691 line::num_traits<T>::to_double(rank[l].S), "undefined(E[r]=0)");
2692 }
2693 for (std::size_t l = 0; l < skipped.size(); ++l)
2694 std::printf("Skipped %s: service is not exponential\n", skipped[l].c_str());
2695 return 0;
2696}
2697
2698/** `-a reward`: `getAvgReward`, the steady-state expectation of each reward. */
2699template <class T>
2700int solve_ctmc_reward(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
2701 std::vector<std::string> names;
2702 const std::vector<T> r = line::ctmc::solver_ctmc_avg_reward(sn, opt, &names);
2703 if (g_json_output) {
2704 line::reg::Json p = line::reg::Json::object();
2705 p["type"] = "AvgReward";
2706 line::reg::Json nm = line::reg::Json::array();
2707 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
2708 p["Reward"] = nm;
2709 p["E"] = vector_json(r);
2710 // NO "method": this arm returns the expectations and no solved chain, so
2711 // there is no resolved method to report and none is invented.
2712 emit_analysis<T>("reward", p, std::string());
2713 return 0;
2714 }
2715 std::printf("SolverCTMC arith=%s rewards=%zu\n", line::num_traits<T>::name(), r.size());
2716 std::printf("%-28s %16s\n", "Reward", "E[r]");
2717 for (std::size_t l = 0; l < r.size(); ++l)
2718 std::printf("%-28s %16.10g\n", names[l].c_str(), line::num_traits<T>::to_double(r[l]));
2719 return 0;
2720}
2721
2722/**
2723 * `-a reward-value`: `@@SolverCTMC/getRewardValueFunction`, V^k(s).
2724 *
2725 * A DIFFERENT OBJECT FROM BOTH `-a reward` AND `-a tranreward`. `-a reward`
2726 * returns one number per reward, the steady-state E[r]; `-a tranreward` the
2727 * expected rate along a horizon; this returns the VALUE FUNCTION of ONE named
2728 * reward -- the reward accumulated over k uniformized steps, from every state
2729 * of the chain, as a (Tmax+1 x nstates) matrix. It is the object a policy
2730 * evaluation reads, and it is indexed by state, not by station.
2731 *
2732 * `--reward-name` IS REQUIRED and not defaulted to the first declared reward:
2733 * the value functions of two rewards are different matrices, and labelling one
2734 * with the caller's question would be a wrong answer rather than a missing one.
2735 */
2736template <class T>
2737int solve_ctmc_reward_value(const line::qn::NetworkStruct<T>& sn,
2738 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
2739 if (k.reward_name.empty())
2740 throw line::InputError(
2741 "-a reward-value returns the value function of ONE reward and needs --reward-name to "
2742 "say which; -a reward returns the steady-state expectation of every declared reward");
2744 std::size_t which = rr.names.size();
2745 for (std::size_t l = 0; l < rr.names.size(); ++l)
2746 if (rr.names[l] == k.reward_name) which = l;
2747 if (which == rr.names.size()) {
2748 std::string avail;
2749 for (std::size_t l = 0; l < rr.names.size(); ++l)
2750 avail += (l ? ", " : "") + rr.names[l];
2751 throw line::InputError("--reward-name '" + k.reward_name +
2752 "' is not declared by this model; it declares: " +
2753 (avail.empty() ? std::string("(none)") : avail));
2754 }
2755 const line::Matrix<T>& V = rr.V[which];
2756
2757 if (g_json_output) {
2758 line::reg::Json p = line::reg::Json::object();
2759 p["type"] = "RewardValueFunction";
2760 p["indexBase"] = 0;
2761 p["Reward"] = rr.names[which];
2762 p["steps"] = V.rows();
2763 p["states"] = V.cols();
2764 p["t"] = vector_json(rr.t);
2765 p["V"] = matrix_json(V);
2766 p["stateSpaceAggr"] = matrix_json(rr.state_space_aggr);
2767 emit_analysis<T>("rewardvalue", p, std::string());
2768 return 0;
2769 }
2770 std::printf("SolverCTMC arith=%s reward=%s steps=%zu states=%zu\n",
2771 line::num_traits<T>::name(), rr.names[which].c_str(), V.rows(), V.cols());
2772 std::printf("%-10s %-10s %20s\n", "Step", "State", "V");
2773 for (std::size_t i = 0; i < V.rows(); ++i)
2774 for (std::size_t j = 0; j < V.cols(); ++j)
2775 std::printf("%-10zu %-10zu %20.10g\n", i, j, line::num_traits<T>::to_double(V(i, j)));
2776 return 0;
2777}
2778
2779/**
2780 * `-a tranreward`: `getTranReward`, E[r(X(t))] over the --tspan horizon.
2781 *
2782 * A DIFFERENT QUANTITY FROM `-a reward`, not a formatting of it. `-a reward`
2783 * returns the steady-state expectation, one number per reward; this returns the
2784 * expected reward RATE along the trajectory, which converges to that number but
2785 * is not it at any finite t. It is also not the accumulated reward `V`, which
2786 * the same header computes and which diverges -- the header calls that the
2787 * easiest mistake to make here, so the two are kept on separate flags.
2788 *
2789 * THE HORIZON IS REQUIRED, as it is for `-a tranprob`: E[r(X(t))] on an
2790 * unstated horizon is not a quantity, and the reference refuses an infinite one
2791 * rather than picking a bound.
2792 */
2793template <class T>
2794int solve_ctmc_tran_reward(const line::qn::NetworkStruct<T>& sn,
2795 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
2797 (void)sn; (void)opt; (void)k;
2799 "-a tranreward integrates the forward equation, which needs transcendental "
2800 "arithmetic; rerun with --arith double or --arith real");
2801 } else {
2802 if (k.t1 < 0.0)
2803 throw line::InputError(
2804 "-a tranreward integrates E[r(X(t))] and needs a horizon: pass --tspan <t0>:<t1>");
2805 std::vector<std::string> names;
2806 std::vector<T> t;
2807 const std::vector<std::vector<T> > r = line::ctmc::solver_ctmc_tran_reward(
2808 sn, opt, line::num_traits<T>::from_double(k.t0),
2809 line::num_traits<T>::from_double(k.t1), &t, &names);
2810 if (g_json_output) {
2811 line::reg::Json p = line::reg::Json::object();
2812 p["type"] = "TranReward";
2813 p["indexBase"] = 0;
2814 line::reg::Json nm = line::reg::Json::array();
2815 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
2816 p["Reward"] = nm;
2817 p["t"] = vector_json<T>(t);
2818 line::reg::Json e = line::reg::Json::array();
2819 for (std::size_t l = 0; l < r.size(); ++l) e.push_back(vector_json<T>(r[l]));
2820 p["E"] = e;
2821 emit_analysis<T>("tranreward", p, std::string());
2822 return 0;
2823 }
2824 std::printf("SolverCTMC arith=%s rewards=%zu points=%zu tspan=[%g,%g]\n",
2825 line::num_traits<T>::name(), r.size(), t.size(), k.t0, k.t1);
2826 std::printf("%-16s", "t");
2827 for (std::size_t l = 0; l < names.size(); ++l) std::printf(" %16s", names[l].c_str());
2828 std::printf("\n");
2829 for (std::size_t i = 0; i < t.size(); ++i) {
2830 std::printf("%-16.10g", line::num_traits<T>::to_double(t[i]));
2831 for (std::size_t l = 0; l < r.size(); ++l)
2832 std::printf(" %16.10g", line::num_traits<T>::to_double(r[l][i]));
2833 std::printf("\n");
2834 }
2835 return 0;
2836 }
2837}
2838
2839/**
2840 * `-a tranprob`: pi(t) over the --tspan horizon, labelled by the whole network
2841 * (`getTranProbSys` / `getTranProbSysAggr`) or by one node when `--node` names
2842 * it (`getTranProb` / `getTranProbAggr`).
2843 *
2844 * The forward equation is integrated ONCE and both label sets are taken off the
2845 * same `CtmcTransient`. Calling the (sn, opt, ...) overloads twice would solve
2846 * and integrate the chain twice for two views of one answer.
2847 */
2848template <class T>
2849int solve_ctmc_tranprob(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
2850 const Knobs& k) {
2851 if (k.t1 < 0.0)
2852 throw line::InputError(
2853 "-a tranprob integrates pi(t) and needs a horizon: pass --tspan <t0>:<t1>");
2856 const line::ctmc::CtmcTranProb<T> det =
2857 k.node ? line::ctmc::ctmc_get_tran_prob(sn, tr, k.node)
2858 : line::ctmc::ctmc_get_tran_prob_sys(sn, tr);
2859 const line::ctmc::CtmcTranProb<T> agg =
2860 k.node ? line::ctmc::ctmc_get_tran_prob_aggr(sn, tr, k.node)
2861 : line::ctmc::ctmc_get_tran_prob_sys_aggr(sn, tr);
2862 if (g_json_output) {
2863 line::reg::Json p = line::reg::Json::object();
2864 p["type"] = "TranProb";
2865 p["indexBase"] = 0;
2866 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
2867 if (k.node) p["node"] = k.node - 1;
2868 line::reg::Json span = line::reg::Json::array();
2869 span.push_back(k.t0);
2870 span.push_back(k.t1);
2871 // THE HORIZON IS PART OF THE ANSWER: pi(t) on an unstated span is not a
2872 // quantity, and a host that took the last row for "the" occupancy without
2873 // it would be quoting a time it does not know.
2874 p["tspan"] = span;
2875 p["t"] = vector_json(det.t);
2876 // BOTH VIEWS COME OFF ONE INTEGRATION, as on the readable path: the
2877 // detailed labels answer getTranProb(Sys) and the aggregate ones
2878 // getTranProb(Sys)Aggr, and a host asking for both would otherwise
2879 // integrate the same forward equation twice for one answer.
2880 p["labels"] = matrix_json(det.labels);
2881 p["labelsAggr"] = matrix_json(agg.labels);
2882 p["pit"] = matrix_json(det.pit);
2883 p["pitAggr"] = matrix_json(agg.pit);
2884 emit_analysis<T>("tranprob", p, tr.chain.actualmethod, ctmc_meta<T>(tr.chain));
2885 return 0;
2886 }
2887 print_ctmc_banner<T>(tr.chain);
2888 std::printf("TranProb times=%zu tspan=%g:%g scope=%s\n", det.t.size(), k.t0, k.t1,
2889 k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
2890 // The labels come FIRST and the occupancy after, because pi(t) is a row per
2891 // time over columns that mean nothing until the state they index is named.
2892 std::printf("%8s %s\n", "State", "Detailed | Aggregate");
2893 for (std::size_t s = 0; s < det.labels.rows(); ++s) {
2894 std::printf("%8zu ", s + 1);
2895 for (std::size_t c = 0; c < det.labels.cols(); ++c)
2896 std::printf(" %g", line::num_traits<T>::to_double(det.labels(s, c)));
2897 std::printf(" |");
2898 for (std::size_t c = 0; c < agg.labels.cols(); ++c)
2899 std::printf(" %g", line::num_traits<T>::to_double(agg.labels(s, c)));
2900 std::printf("\n");
2901 }
2902 std::printf("%14s", "Time");
2903 for (std::size_t s = 0; s < det.pit.cols(); ++s) std::printf(" %12zu", s + 1);
2904 std::printf("\n");
2905 for (std::size_t i = 0; i < det.t.size(); ++i) {
2906 std::printf("%14.8g", line::num_traits<T>::to_double(det.t[i]));
2907 for (std::size_t s = 0; s < det.pit.cols(); ++s)
2908 std::printf(" %12.6g", line::num_traits<T>::to_double(det.pit(i, s)));
2909 std::printf("\n");
2910 }
2911 return 0;
2912}
2913
2914/**
2915 * `-s ctmc -a tran`: `getTranAvg`, the transient MEANS Q(t), U(t) and X(t) over
2916 * the --tspan horizon.
2917 *
2918 * NOT `-a tranprob`, AND THE TWO ARE NOT REDUCIBLE TO ONE ANOTHER FOR A CALLER.
2919 * `tranprob` sends pi(t) with the labels that index it, from which a host COULD
2920 * form these means -- and that is exactly the computation that must not happen
2921 * in a host: the utilization is not a linear functional of the labels (the PS
2922 * and DPS shares divide by the state's own total, and every other discipline
2923 * takes min(n_k, c)/c with the reference's warning attached), so a host folding
2924 * pi(t) itself would be reimplementing `solver_ctmc_transient_analyzer`'s
2925 * discipline switch and would diverge from it silently. The analyzer already
2926 * computes all three trajectories on the way to pi(t); this arm reports them.
2927 *
2928 * The payload is the SAME `TranAvgTable` the fluid and MAM transients emit, key
2929 * for key, so one host reader serves every solver that answers `-a tran`.
2930 */
2931template <class T>
2932int solve_ctmc_tran(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
2933 const Knobs& k) {
2934 if (k.t1 < 0.0)
2935 throw line::InputError(
2936 "-a tran integrates the forward equation and needs a horizon: pass --tspan <t0>:<t1>");
2939 const std::size_t M = sn.nstations, K = sn.nclasses, nt = tr.t.size();
2940
2941 if (g_json_output) {
2942 line::reg::Json p = line::reg::Json::object();
2943 p["type"] = "TranAvgTable";
2944 p["indexBase"] = 0;
2945 p["t0"] = k.t0;
2946 p["t1"] = k.t1;
2947 line::reg::Json ts = line::reg::Json::array();
2948 for (std::size_t j = 0; j < nt; ++j) ts.push_back(line::num_traits<T>::to_double(tr.t[j]));
2949 line::reg::Json arr = line::reg::Json::array();
2950 for (std::size_t i = 0; i < M; ++i)
2951 for (std::size_t c = 0; c < K; ++c) {
2952 // A DISABLED PAIR IS OMITTED, not sent as zeros: the reference
2953 // leaves its cell empty and the host turns an absent curve into
2954 // the disabled handle's NaN. Zeros would read as a station that
2955 // is genuinely idle for that class.
2956 if (sn.disabled[i][c]) continue;
2957 line::reg::Json e = line::reg::Json::object();
2958 e["Station"] = sn.stations[i].name;
2959 e["JobClass"] = sn.classes[c].name;
2960 e["station"] = i;
2961 e["jobclass"] = c;
2962 e["t"] = ts;
2963 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array(),
2964 x = line::reg::Json::array();
2965 for (std::size_t j = 0; j < nt; ++j) {
2966 q.push_back(line::num_traits<T>::to_double(tr.QNt[i][c][j]));
2967 u.push_back(line::num_traits<T>::to_double(tr.UNt[i][c][j]));
2968 x.push_back(line::num_traits<T>::to_double(tr.TNt[i][c][j]));
2969 }
2970 e["QLen"] = q;
2971 e["Util"] = u;
2972 e["Tput"] = x;
2973 arr.push_back(e);
2974 }
2975 p["curves"] = arr;
2976 emit_analysis<T>("tran", p, tr.chain.actualmethod, ctmc_meta<T>(tr.chain));
2977 return 0;
2978 }
2979 print_ctmc_banner<T>(tr.chain);
2980 std::printf("TranAvg times=%zu tspan=%g:%g\n", nt, k.t0, k.t1);
2981 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
2982 "Tput");
2983 for (std::size_t i = 0; i < M; ++i)
2984 for (std::size_t c = 0; c < K; ++c) {
2985 if (sn.disabled[i][c]) continue;
2986 for (std::size_t j = 0; j < nt; ++j)
2987 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
2988 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(),
2990 line::num_traits<T>::to_double(tr.QNt[i][c][j]),
2991 line::num_traits<T>::to_double(tr.UNt[i][c][j]),
2992 line::num_traits<T>::to_double(tr.TNt[i][c][j]));
2993 }
2994 return 0;
2995}
2996
2997/**
2998 * `-a sample`: `sampleSys` and `sampleSysAggr`, one marked trajectory; with
2999 * `--node`, also that node's own block (`sample`) and per-class counts
3000 * (`sampleAggr`), which is the view MATLAB's per-node sampler returns.
3001 */
3002template <class T>
3003int solve_ctmc_sample(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt,
3004 const Knobs& k) {
3005 // `--events` NAMES THIS NUMBER, `--samples` only stands in for it. The
3006 // JAR keeps the two apart because a sampled trajectory is walked for a
3007 // number of EVENTS while `--samples` is a solver-wide run length, so a
3008 // caller who set the run length and then asked for a trajectory would
3009 // silently get one of that length. Both are honoured, --events first,
3010 // and the reference default of 1000 stands when neither is given.
3011 const std::size_t nevents = k.events ? k.events : (k.samples ? k.samples : 1000);
3012 const unsigned long seed = k.seed ? k.seed : 23000;
3014 line::ctmc::solver_ctmc_sample_sys<T>(sn, opt, nevents, seed);
3016 line::Matrix<T> L, LA;
3017 if (k.node) {
3018 L = line::ctmc::solver_ctmc_sample(sn, path, k.node);
3019 LA = line::ctmc::solver_ctmc_sample_aggr(sn, path, k.node);
3020 }
3021 if (g_json_output) {
3022 line::reg::Json p = line::reg::Json::object();
3023 p["type"] = "SamplePath";
3024 p["indexBase"] = 0;
3025 // The seed and the requested length are part of the ANSWER, as on the SSA
3026 // path: two runs are the same trace only if both are stated.
3027 p["events"] = nevents;
3028 p["seed"] = seed;
3029 p["drawn"] = path.state.size();
3030 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
3031 if (k.node) p["node"] = k.node - 1;
3032 p["t"] = vector_json(path.t);
3033 p["state"] = index_json(path.state);
3034 line::reg::Json ev = line::reg::Json::array();
3035 for (std::size_t i = 0; i < path.state.size(); ++i) {
3036 // null where the readable table prints "absorb": the walk left no
3037 // state, so there is no synchronization index, and any integer here
3038 // would name an event that did not fire.
3039 if (i < path.event.size() && path.event[i] != static_cast<std::size_t>(-1))
3040 ev.push_back(path.event[i]);
3041 else
3042 ev.push_back(line::reg::Json());
3043 }
3044 p["event"] = ev;
3045 p["sysAggr"] = matrix_json(A);
3046 // THE STATE SPACE TRAVELS WITH THE TRAJECTORY, so a host can turn the
3047 // visited indices into the states themselves without enumerating the
3048 // chain a second time in another process -- which would also be a second
3049 // chance for the two enumerations to disagree while looking paired.
3050 {
3053 p["space"] = matrix_json(s.flat);
3054 p["NodeWidths"] = index_json(s.node_width);
3055 }
3056 if (k.node) {
3057 p["nodeState"] = matrix_json(L);
3058 p["nodeAggr"] = matrix_json(LA);
3059 }
3060 emit_analysis<T>("sample", p, path.chain.actualmethod, ctmc_meta<T>(path.chain));
3061 return 0;
3062 }
3063 // The seed and the requested length are part of the ANSWER, as on the SSA
3064 // path: two runs are the same trace only if both are stated.
3065 std::printf("SolverCTMC arith=%s states=%zu events=%zu seed=%lu drawn=%zu scope=%s\n",
3066 line::num_traits<T>::name(), path.chain.chain.space.size(), nevents, seed,
3067 path.state.size(), k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
3068 std::printf("%14s %8s %8s %s\n", "Time", "State", "Event",
3069 k.node ? "SysAggregate | NodeState | NodeAggregate" : "SysAggregate");
3070 for (std::size_t i = 0; i < path.state.size(); ++i) {
3071 const std::size_t ev = i < path.event.size() ? path.event[i] : static_cast<std::size_t>(-1);
3072 std::printf("%14.8g %8zu ", line::num_traits<T>::to_double(path.t[i]), path.state[i] + 1);
3073 if (ev == static_cast<std::size_t>(-1))
3074 std::printf("%8s ", "absorb");
3075 else
3076 std::printf("%8zu ", ev + 1);
3077 for (std::size_t c = 0; c < A.cols(); ++c)
3078 std::printf(" %g", line::num_traits<T>::to_double(A(i, c)));
3079 if (k.node) {
3080 std::printf(" |");
3081 for (std::size_t c = 0; c < L.cols(); ++c)
3082 std::printf(" %g", line::num_traits<T>::to_double(L(i, c)));
3083 std::printf(" |");
3084 for (std::size_t c = 0; c < LA.cols(); ++c)
3085 std::printf(" %g", line::num_traits<T>::to_double(LA(i, c)));
3086 }
3087 std::printf("\n");
3088 }
3089 return 0;
3090}
3091
3092/** `-a cdf`: `getCdfRespT` per (station, class), and `getCdfSysRespT` per chain. */
3093template <class T>
3094int solve_ctmc_cdf(const line::qn::NetworkStruct<T>& sn, const line::ctmc::CtmcOptions& opt) {
3095 const std::vector<std::vector<line::ctmc::CdfCurve<T> > > RD =
3097 const std::vector<line::ctmc::CdfCurve<T> > RS = line::ctmc::solver_ctmc_cdf_sys_respt(sn, opt);
3098 if (g_json_output) {
3099 line::reg::Json p = line::reg::Json::object();
3100 p["type"] = "CdfRespT";
3101 p["chains"] = sn.nchains;
3102 // ONE OBJECT PER CURVE rather than four parallel columns: the grids are
3103 // per-pair and of different lengths, so a column-oriented form would have
3104 // to repeat the station and the class on every sample and leave the host
3105 // to re-group them.
3106 line::reg::Json rd = line::reg::Json::array();
3107 for (std::size_t i = 0; i < RD.size(); ++i)
3108 for (std::size_t c = 0; c < RD[i].size(); ++c) {
3109 // An empty curve is not a degenerate one: the chain never visits
3110 // that pair, so there is no arrival event to condition on, and it
3111 // is OMITTED rather than sent as a flat zero law.
3112 if (RD[i][c].empty()) continue;
3113 line::reg::Json e = line::reg::Json::object();
3114 e["Station"] = sn.stations[i].name;
3115 e["JobClass"] = sn.classes[c].name;
3116 e["station"] = i;
3117 e["jobclass"] = c;
3118 e["t"] = vector_json(RD[i][c].t);
3119 e["F"] = vector_json(RD[i][c].F);
3120 rd.push_back(e);
3121 }
3122 p["respt"] = rd;
3123 line::reg::Json rs = line::reg::Json::array();
3124 for (std::size_t c = 0; c < RS.size(); ++c) {
3125 if (RS[c].empty()) continue;
3126 line::reg::Json e = line::reg::Json::object();
3127 e["chain"] = c;
3128 e["t"] = vector_json(RS[c].t);
3129 e["F"] = vector_json(RS[c].F);
3130 rs.push_back(e);
3131 }
3132 p["sysrespt"] = rs;
3133 p["indexBase"] = 0;
3134 // NO "method", as on the reward arm: the response-time laws are computed
3135 // from tagged chains this function does not return, so there is no
3136 // resolved method to report and the requested one is not it.
3137 emit_analysis<T>("cdf", p, std::string());
3138 return 0;
3139 }
3140 std::printf("SolverCTMC arith=%s chains=%zu\n", line::num_traits<T>::name(), sn.nchains);
3141 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
3142 for (std::size_t i = 0; i < RD.size(); ++i)
3143 for (std::size_t c = 0; c < RD[i].size(); ++c) {
3144 // An empty curve is not a degenerate one: the chain never visits
3145 // that pair, so there is no arrival event to condition on.
3146 if (RD[i][c].empty()) continue;
3147 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
3148 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
3149 sn.classes[c].name.c_str(),
3150 line::num_traits<T>::to_double(RD[i][c].t[j]),
3151 line::num_traits<T>::to_double(RD[i][c].F[j]));
3152 }
3153 std::printf("%-16s %-14s %14s %14s\n", "System", "Chain", "Time", "F(t)");
3154 for (std::size_t c = 0; c < RS.size(); ++c) {
3155 if (RS[c].empty()) continue;
3156 for (std::size_t j = 0; j < RS[c].t.size(); ++j)
3157 std::printf("%-16s %-14zu %14.8g %14.10g\n", "(system)", c + 1,
3159 line::num_traits<T>::to_double(RS[c].F[j]));
3160 }
3161 return 0;
3162}
3163
3164/**
3165 * A `--passage-from`/`--passage-into` spec as a Matrix<double>: either a flat
3166 * comma list ("3,5", one row the resolver reads as 1-based indices when every
3167 * entry is one) or semicolon-separated state rows ("0,2;1,1"), which resolve
3168 * against the enumerated space by content.
3169 */
3170inline line::Matrix<double> parse_passage_set(const std::string& spec, const char* flag) {
3171 if (spec.empty()) return line::Matrix<double>(0, 0);
3172 std::vector<std::vector<double>> rows;
3173 std::size_t pos = 0;
3174 while (pos <= spec.size()) {
3175 std::size_t semi = spec.find(';', pos);
3176 if (semi == std::string::npos) semi = spec.size();
3177 std::string rowtxt = spec.substr(pos, semi - pos);
3178 std::vector<double> row;
3179 std::size_t p2 = 0;
3180 while (p2 <= rowtxt.size()) {
3181 std::size_t comma = rowtxt.find(',', p2);
3182 if (comma == std::string::npos) comma = rowtxt.size();
3183 std::string cell = rowtxt.substr(p2, comma - p2);
3184 if (!cell.empty()) {
3185 char* endp = 0;
3186 const double v = std::strtod(cell.c_str(), &endp);
3187 if (endp == cell.c_str() || *endp != '\0')
3188 throw line::InputError(std::string(flag) + ": '" + cell +
3189 "' is not a number");
3190 row.push_back(v);
3191 }
3192 p2 = comma + 1;
3193 }
3194 if (!row.empty()) rows.push_back(row);
3195 pos = semi + 1;
3196 }
3197 if (rows.empty()) return line::Matrix<double>(0, 0);
3198 for (std::size_t i = 1; i < rows.size(); ++i)
3199 if (rows[i].size() != rows[0].size())
3200 throw line::InputError(std::string(flag) +
3201 ": every state row must have the same width");
3202 line::Matrix<double> out(rows.size(), rows[0].size());
3203 for (std::size_t i = 0; i < rows.size(); ++i)
3204 for (std::size_t j = 0; j < rows[i].size(); ++j) out(i, j) = rows[i][j];
3205 return out;
3206}
3207
3208/**
3209 * `-s ctmc -a firstpasst`: `@@SolverCTMC/getCdfFirstPassT(A, B)`, the first
3210 * passage time between two state sets the caller names. `--passage-into` is
3211 * required; an empty `--passage-from` starts from the conditional stationary
3212 * law on the complement of the target, as the reference does.
3213 */
3214template <class T>
3215int solve_ctmc_firstpasst(const line::qn::NetworkStruct<T>& sn,
3216 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3217 if (k.passage_into.empty())
3218 throw line::InputError(
3219 "-a firstpasst times the passage INTO a state set and needs --passage-into; name it "
3220 "as 1-based rows of the state space ('3,5') or as state rows ('0,2;1,1')");
3221 const line::Matrix<double> A = parse_passage_set(k.passage_from, "--passage-from");
3222 const line::Matrix<double> B = parse_passage_set(k.passage_into, "--passage-into");
3223 const std::string method = k.passage_method.empty() ? "expm" : k.passage_method;
3224
3226 line::ctmc::ctmc_cdf_firstpasst<T>(sn, opt, A, B, method);
3227
3228 if (g_json_output) {
3229 line::reg::Json p = line::reg::Json::object();
3230 p["type"] = "CdfFirstPassT";
3231 p["indexBase"] = 0;
3232 p["t"] = line::reg::Json(fp.t);
3233 p["F"] = line::reg::Json(fp.F);
3234 p["f"] = line::reg::Json(fp.f);
3235 line::reg::Json src = line::reg::Json::array(), tgt = line::reg::Json::array();
3236 for (std::size_t i : fp.source) src.push_back(static_cast<double>(i));
3237 for (std::size_t i : fp.target) tgt.push_back(static_cast<double>(i));
3238 p["source"] = src;
3239 p["target"] = tgt;
3240 emit_analysis<T>("firstpasst", p, method);
3241 return 0;
3242 }
3243 std::printf("SolverCTMC arith=%s method=%s getCdfFirstPassT\n",
3244 line::num_traits<T>::name(), method.c_str());
3245 std::printf("source states: %zu%s, target states: %zu\n", fp.source.size(),
3246 fp.source.empty() ? " (conditional stationary law)" : "", fp.target.size());
3247 std::printf("%14s %14s %14s\n", "Time", "F(t)", "f(t)");
3248 for (std::size_t j = 0; j < fp.t.size(); j += 111)
3249 std::printf("%14.8g %14.10g %14.10g\n", fp.t[j], fp.F[j], fp.f[j]);
3250 std::printf("%14.8g %14.10g %14.10g\n", fp.t.back(), fp.F.back(), fp.f.back());
3251 return 0;
3252}
3253
3254/**
3255 * `-s ctmc -a firstpasstmom`: `@@SolverCTMC/getFirstPassTMoments(A, B, nmax)`,
3256 * the moments of the same passage `firstpasst` gives the curve of.
3257 *
3258 * These are EXACT and cost one linear solve per order, so a caller who wants a
3259 * variance or a skewness should ask for them here rather than integrate the
3260 * truncated curve the other arm returns. `--passage-orders` is the nmax; it
3261 * defaults to the reference's 3.
3262 */
3263template <class T>
3264int solve_ctmc_firstpasst_moments(const line::qn::NetworkStruct<T>& sn,
3265 const line::ctmc::CtmcOptions& opt, const Knobs& k) {
3266 if (k.passage_into.empty())
3267 throw line::InputError(
3268 "-a firstpasstmom times the passage INTO a state set and needs --passage-into; name "
3269 "it as 1-based rows of the state space ('3,5') or as state rows ('0,2;1,1')");
3270 const line::Matrix<double> A = parse_passage_set(k.passage_from, "--passage-from");
3271 const line::Matrix<double> B = parse_passage_set(k.passage_into, "--passage-into");
3272 const std::size_t nmax = (k.passage_orders > 0) ? k.passage_orders : 3;
3273
3275 line::ctmc::ctmc_firstpasst_moments<T>(sn, opt, A, B, nmax);
3276
3277 if (g_json_output) {
3278 line::reg::Json p = line::reg::Json::object();
3279 p["type"] = "FirstPassTMoments";
3280 p["indexBase"] = 0;
3281 line::reg::Json m = line::reg::Json::array();
3282 for (std::size_t i = 0; i < fm.m.size(); ++i)
3283 m.push_back(line::num_traits<T>::to_double(fm.m[i]));
3284 p["m"] = m;
3285 line::reg::Json mall = line::reg::Json::array();
3286 for (std::size_t i = 0; i < fm.mall.rows(); ++i) {
3287 line::reg::Json row = line::reg::Json::array();
3288 for (std::size_t j = 0; j < fm.mall.cols(); ++j)
3289 row.push_back(line::num_traits<T>::to_double(fm.mall(i, j)));
3290 mall.push_back(row);
3291 }
3292 p["mall"] = mall;
3293 line::reg::Json src = line::reg::Json::array(), tgt = line::reg::Json::array();
3294 for (std::size_t i : fm.source) src.push_back(static_cast<double>(i));
3295 for (std::size_t i : fm.target) tgt.push_back(static_cast<double>(i));
3296 p["source"] = src;
3297 p["target"] = tgt;
3298 emit_analysis<T>("firstpasstmom", p, "moments");
3299 return 0;
3300 }
3301 std::printf("SolverCTMC arith=%s getFirstPassTMoments\n", line::num_traits<T>::name());
3302 std::printf("source states: %zu%s, target states: %zu\n", fm.source.size(),
3303 fm.source.empty() ? " (conditional stationary law)" : "", fm.target.size());
3304 std::printf("%8s %20s\n", "Order", "Moment");
3305 for (std::size_t i = 0; i < fm.m.size(); ++i)
3306 std::printf("%8zu %20.10g\n", i + 1, line::num_traits<T>::to_double(fm.m[i]));
3307 return 0;
3308}
3309
3310/**
3311 * Solve a Network model.json with SolverCTMC and print what `-a` asked for.
3312 *
3313 * EXACT, and the only ported solver that is exact on a non-product-form model:
3314 * it enumerates the state space and solves pi Q = 0, so the numbers are the
3315 * chain's own and not an approximation of them. The price is the state space,
3316 * which is why an OPEN model needs `--cutoff`: without a bound on the open
3317 * population the chain is infinite. The banner reports the cutoff that was used,
3318 * because a truncated chain's answer is not the model's answer without it.
3319 *
3320 * Every arithmetic backend runs the stationary analyses: the generator assembly
3321 * and the stationary solve are field operations throughout, so `--arith exact`
3322 * returns the exact rational stationary law of a chain with rational rates. The
3323 * transient ones refuse by name under exact, since a forward integration, an
3324 * exponential clock and a matrix exponential are all transcendental.
3325 */
3326template <class T>
3327int solve_model_ctmc(const std::string& file, const Knobs& k, const std::string& analysis) {
3328 line::qn::Network<T> net = read_model<T>(file);
3330 if (!k.method.empty()) opt.method = k.method;
3331 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
3332 opt.cutoff_mat = k.cutoff_mat;
3333 opt.force = k.force;
3334 if (k.timestep > 0.0) opt.timestep = k.timestep; // `--timestep`, the fixed output grid
3335 // `--transient-method` and its two tolerances, `options.config` of the
3336 // reference's transient analyzer. The analyzer validates the name.
3337 if (!k.transient_method.empty()) opt.transient_method = k.transient_method;
3338 if (k.fau_epsilon > 0.0) opt.fau_epsilon = k.fau_epsilon;
3339 if (k.fau_delta >= 0.0) opt.fau_delta = k.fau_delta;
3340 const line::qn::NetworkStruct<T>& sn = net.get_struct();
3341
3342 // The two methods that never build the generator are served by their own
3343 // analyzers, ahead of every state-space path below. The dispatcher has
3344 // already refused every analysis but `avg` for them, since neither produces
3345 // a state space, a filtration or a trajectory to answer one from.
3346 if (opt.method == "mdd") {
3348 if (k.mdd_tol > 0.0) mcdopt.tol = k.mdd_tol;
3349 if (k.mdd_maxiter > 0) mcdopt.maxiter = k.mdd_maxiter;
3350 return solve_ctmc_mdd_avg<T>(sn, opt, mcdopt);
3351 }
3352 if (opt.method == "cftp" || opt.method == "cftp.approx") {
3354 if (k.samples) cftpopt.samples = k.samples;
3355 if (k.seed) cftpopt.seed = k.seed;
3356 return solve_ctmc_cftp_avg<T>(sn, opt, cftpopt);
3357 }
3358
3359 // The field-arithmetic analyses: every step from the generator to the answer
3360 // is a ring operation, so exact returns the exact rational quantity.
3361 if (analysis == "avg") return solve_ctmc_avg<T>(sn, opt);
3362 if (analysis == "prob") return solve_ctmc_prob<T>(sn, opt, k);
3363 if (analysis == "gen") return solve_ctmc_gen<T>(sn, opt);
3364 if (analysis == "states") return solve_ctmc_states<T>(sn, opt);
3365 if (analysis == "sens") return solve_ctmc_sens<T>(sn, opt);
3366 if (analysis == "reward") return solve_ctmc_reward<T>(sn, opt);
3367 if (analysis == "rewardvalue") return solve_ctmc_reward_value<T>(sn, opt, k);
3368
3369 // The rest integrate a forward equation, draw an exponential clock or take a
3370 // matrix exponential, none of which exists in a rational field. Their
3371 // static_asserts are behind if-constexpr so the refusal is a message rather
3372 // than a compile error in the exact instantiation.
3375 "the -a " + analysis +
3376 " analysis integrates the forward equation, draws exponential clocks or takes a matrix "
3377 "exponential, none of which exists in exact rational arithmetic; rerun with --arith "
3378 "double or --arith real");
3379 } else {
3380 if (analysis == "tran") return solve_ctmc_tran<T>(sn, opt, k);
3381 if (analysis == "tranprob") return solve_ctmc_tranprob<T>(sn, opt, k);
3382 if (analysis == "tranreward") return solve_ctmc_tran_reward<T>(sn, opt, k);
3383 if (analysis == "sample") return solve_ctmc_sample<T>(sn, opt, k);
3384 if (analysis == "firstpasst") return solve_ctmc_firstpasst<T>(sn, opt, k);
3385 if (analysis == "firstpasstmom") return solve_ctmc_firstpasst_moments<T>(sn, opt, k);
3386 return solve_ctmc_cdf<T>(sn, opt); // the dispatcher admitted no other name
3387 }
3388}
3389
3390/**
3391 * `-s ssa -a prob`: the four SolverSSA probability queries over the model's
3392 * DEFAULT INITIAL STATE, the same state `-s ctmc -a prob` reports on.
3393 *
3394 * THE PAIR IS THE POINT. The CTMC answer is the stationary law of the chain and
3395 * this one is a time average of a finite sample path, so running both on a model
3396 * small enough for the chain measures the simulation error directly instead of
3397 * inferring it. The banner therefore carries the run length and the seed, as
3398 * every simulated number on this CLI does.
3399 *
3400 * `seen` TRAVELS WITH EACH PROBABILITY because a zero here has two meanings: the
3401 * path visited the state and left immediately, or it never got there at all. The
3402 * reference warns on the second; a machine-readable answer has to carry the
3403 * distinction rather than print it.
3404 */
3405template <class T>
3406int solve_model_ssa_prob(const std::string& file, const Knobs& k) {
3407 line::qn::Network<T> net = read_model<T>(file);
3408 const line::qn::NetworkStruct<T>& sn = net.get_struct();
3410 // `serial`, whatever `-m` said, and for the reference's own reason: the NRM
3411 // simulates per-(node, class, phase) counts rather than the state ENCODING,
3412 // so it has no row to compare a requested state against.
3413 // `@@SolverSSA/getProb.m` rewrites the method the same way.
3414 opt.method = "serial";
3415 if (k.samples) opt.samples = k.samples;
3416 if (k.seed) opt.seed = k.seed;
3417 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
3418 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
3420
3421 if (g_json_output) {
3422 line::reg::Json p = line::reg::Json::object();
3423 p["type"] = "ProbAggr";
3424 p["indexBase"] = 0;
3425 p["samples"] = r.samples;
3426 p["seed"] = r.seed;
3427 p["ProbSys"] = r.sys.prob;
3428 p["ProbSysAggr"] = r.sys_aggr.prob;
3429 p["ProbSysSeen"] = r.sys.seen;
3430 p["ProbSysAggrSeen"] = r.sys_aggr.seen;
3431 line::reg::Json st = line::reg::Json::array(), pm = line::reg::Json::array(),
3432 pa = line::reg::Json::array(), sm = line::reg::Json::array();
3433 for (std::size_t i = 0; i < sn.nstations; ++i) {
3434 st.push_back(sn.stations[i].name);
3435 pm.push_back(r.marg[i].prob);
3436 pa.push_back(r.aggr[i].prob);
3437 sm.push_back(r.marg[i].seen);
3438 }
3439 p["Station"] = st;
3440 p["Prob"] = pm;
3441 p["ProbAggr"] = pa;
3442 p["Seen"] = sm;
3443 emit_analysis<T>("prob", p, "serial");
3444 return 0;
3445 }
3446 std::printf("SolverSSA arith=%s method=serial samples=%zu seed=%lu time=%.6g\n",
3448 std::printf("ProbSys = %.8g%s\n", r.sys.prob, r.sys.seen ? "" : " (state never visited)");
3449 std::printf("ProbSysAggr = %.8g%s\n", r.sys_aggr.prob,
3450 r.sys_aggr.seen ? "" : " (state never visited)");
3451 std::printf("%-20s %14s %14s\n", "Station", "Prob", "ProbAggr");
3452 for (std::size_t i = 0; i < sn.nstations; ++i)
3453 std::printf("%-20s %14.8g %14.8g\n", sn.stations[i].name.c_str(), r.marg[i].prob,
3454 r.aggr[i].prob);
3455 return 0;
3456}
3457
3458/**
3459 * `-s ssa -a sample`: `sampleSys` and `sampleSysAggr`, one simulated trajectory;
3460 * with `--node`, also that node's own block (`sample`) and per-class counts
3461 * (`sampleAggr`).
3462 *
3463 * The SAME shape `-s ctmc -a sample` emits, deliberately: the CTMC sampler walks
3464 * the jump chain of an enumerated generator and this one walks the network's own
3465 * encoding, and a host that can read one trajectory should be able to read the
3466 * other. The event column indexes the synchronization list, so the two are
3467 * comparable only within a solver -- which is why it is printed and not
3468 * interpreted here.
3469 */
3470template <class T>
3471int solve_model_ssa_sample(const std::string& file, const Knobs& k) {
3472 line::qn::Network<T> net = read_model<T>(file);
3473 const line::qn::NetworkStruct<T>& sn = net.get_struct();
3475 opt.method = "serial";
3476 opt.samples = k.events ? k.events : (k.samples ? k.samples : 1000); // see -a sample above
3477 if (k.seed) opt.seed = k.seed;
3478 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
3479 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
3483 if (k.node) nodep = line::ssa::ssa_sample_node(sn, sim.run, k.node);
3484
3485 if (g_json_output) {
3486 line::reg::Json p = line::reg::Json::object();
3487 p["type"] = "SamplePath";
3488 p["indexBase"] = 0;
3489 p["events"] = opt.samples;
3490 p["seed"] = sys.seed;
3491 p["drawn"] = sys.t.size();
3492 p["scope"] = k.node ? sn.nodes[k.node - 1].name : std::string("(system)");
3493 if (k.node) p["node"] = k.node - 1;
3494 p["t"] = vector_json(sys.t);
3495 p["event"] = index_json(sys.event);
3496 p["state"] = matrix_json(sys.state);
3497 // THE BLOCK BOUNDARIES TRAVEL, as under `-s ctmc -a sample`. A row of
3498 // `state` is the stateful nodes' local encodings laid end to end, and
3499 // `sampleSys` reports them one node at a time; a host that had to guess
3500 // the widths would cut the row at the wrong columns on any model with a
3501 // phase-type service, and read a phase index as a job count.
3502 {
3503 line::reg::Json w = line::reg::Json::array();
3504 for (std::size_t f = 0; f < sn.stateful_nodes.size(); ++f)
3505 w.push_back(sim.run.space.empty() ? 0 : sim.run.space[0].local[f].size());
3506 p["NodeWidths"] = w;
3507 }
3508 p["sysAggr"] = matrix_json(sys.aggr);
3509 if (k.node) {
3510 p["nodeState"] = matrix_json(nodep.state);
3511 p["nodeAggr"] = matrix_json(nodep.aggr);
3512 }
3513 emit_analysis<T>("sample", p, "serial");
3514 return 0;
3515 }
3516 std::printf("SolverSSA arith=%s method=serial events=%zu seed=%lu drawn=%zu scope=%s\n",
3517 line::num_traits<T>::name(), opt.samples, sys.seed, sys.t.size(),
3518 k.node ? sn.nodes[k.node - 1].name.c_str() : "(system)");
3519 std::printf("%14s %8s %s\n", "Time", "Event",
3520 k.node ? "SysAggregate | NodeState | NodeAggregate" : "SysAggregate");
3521 for (std::size_t i = 0; i < sys.t.size(); ++i) {
3522 std::printf("%14.8g %8zu ", sys.t[i], sys.event[i]);
3523 for (std::size_t c = 0; c < sys.aggr.cols(); ++c)
3524 std::printf(" %g", line::num_traits<T>::to_double(sys.aggr(i, c)));
3525 if (k.node) {
3526 std::printf(" |");
3527 for (std::size_t c = 0; c < nodep.state.cols(); ++c)
3528 std::printf(" %g", line::num_traits<T>::to_double(nodep.state(i, c)));
3529 std::printf(" |");
3530 for (std::size_t c = 0; c < nodep.aggr.cols(); ++c)
3531 std::printf(" %g", line::num_traits<T>::to_double(nodep.aggr(i, c)));
3532 }
3533 std::printf("\n");
3534 }
3535 return 0;
3536}
3537
3538/**
3539 * A `Matrix<double>` read as this arithmetic's matrix.
3540 *
3541 * SSA and Fluid return plain-double solutions whatever `T` the CLI was asked
3542 * for, so every shared routine that takes a `Matrix<T>` -- the residence-time
3543 * conversion, the chain aggregation -- needs this one lift. Both are refused
3544 * outside `--arith double` anyway, so it is a type bridge and not a precision
3545 * claim.
3546 */
3547template <class T>
3548line::Matrix<T> to_matrix(const line::Matrix<double>& m) {
3550 for (std::size_t i = 0; i < m.rows(); ++i)
3551 for (std::size_t j = 0; j < m.cols(); ++j)
3552 out(i, j) = line::num_traits<T>::from_double(m(i, j));
3553 return out;
3554}
3555
3556/**
3557 * Solve a Network model.json with SolverSSA and print the same table.
3558 *
3559 * A SIMULATION: its numbers carry Monte Carlo error, so the row is compared
3560 * against the other codebases' SSA rows and not against an exact solver's.
3561 *
3562 * `--samples` and `--seed` set the run length and the stream; without them the
3563 * defaults are 10000 firings and seed 23000, which on a two-station model is
3564 * roughly 3300 job cycles and lands a few percent from the analytical answer.
3565 * Diffing THAT against an exact solver reads as a defect and is not one: a
3566 * measured -3.12% at 1e4 on an M/M/1 falls to +0.06% at 2.56e6. The banner
3567 * therefore carries both numbers, so a row quoting an SSA figure carries the
3568 * conditions that produced it. Double only, refused by name in the dispatcher.
3569 *
3570 * `-m` REACHES THE SOLVER'S OWN DISPATCHER, `ssa::solver_ssa`, and not one
3571 * engine's entry. Calling `solver_ssa_nrm_analyzer` here would run the NRM
3572 * whatever `-m` said, so `-m serial` would silently answer with a different
3573 * estimator than the one asked for -- and the three names the NRM entry cannot
3574 * serve (`serial`, `para`, `parallel`) are all methods the library honours.
3575 */
3576template <class T>
3577int solve_model_ssa(const std::string& file, const Knobs& k) {
3578 line::qn::Network<T> net = read_model<T>(file);
3580 if (!k.method.empty() && k.method != "default") opt.method = k.method;
3581 if (k.samples) opt.samples = k.samples;
3582 if (k.seed) opt.seed = k.seed;
3583 if (k.warmupfrac >= 0.0) opt.warmupfrac = k.warmupfrac;
3584 // The cache write-back is COLLECTED HERE, not only on the `-a node` path:
3585 // the realized hit and miss shares are what the sample path measured, and
3586 // `-a avg` is the arm MATLAB's lang='cpp' bridge calls. Without it
3587 // `CPPLINE.restoreCacheResults` found no block, cleared the Cache node and
3588 // refreshed the visits back to link()'s offered 1/2-1/2.
3589 std::vector<line::ssa::SsaCacheRatio> cacheratio;
3590 const line::ssa::SsaSolution r = line::ssa::solver_ssa(net.get_struct(), opt, &cacheratio);
3591 const line::qn::NetworkStruct<T>& sn = net.get_struct();
3592
3593 // The seed and the sample count are part of the ANSWER, not of the
3594 // invocation: two runs of a simulation are the same measurement only if
3595 // both are stated, so the banner carries them and a parity row that quotes
3596 // an SSA number carries them with it.
3597 //
3598 // `r.method` is the engine that ACTUALLY ran, never `k.method`: `-m
3599 // parallel` on an NRM-eligible model reports `nrm`, because that is what
3600 // produced the numbers below and the banner may not claim otherwise.
3601 std::printf("SolverSSA arith=%s method=%s type=%s samples=%zu seed=%lu time=%.6g\n",
3602 line::num_traits<T>::name(), r.method.c_str(),
3603 line::util::method_type("SSA", r.method).c_str(), r.samples, opt.seed,
3604 r.simulated_time);
3605 // ResidT IS NOT RespT UNLESS EVERY STATION IS VISITED ONCE PER CYCLE.
3606 // `sn_get_residt_from_respt` is the reference's own per-visit -> per-job
3607 // conversion and a pure function of `sn` and RN, so a solver that reports no
3608 // residence time of its own still owes the caller this one: reporting RespT
3609 // in its place was a factor of 3 out on sdroute_closed and 17 on Queue1 of
3610 // init_state_ps, both multi-visit closed models.
3611 //
3612 // TAKEN ON THE MEASURED CACHE SPLIT, not on the offered one: the visits this
3613 // conversion divides by are a function of the routing, and a cache's routing
3614 // is a RESULT. On tut06_cache_lru_zipf the base struct still carried
3615 // `link()`'s even hit/miss share, so both classes came back at exactly half
3616 // their response time (0.1 and 0.5 against 0.16475 and 0.17625) -- a number
3617 // that is not the residence time of any model.
3619 const line::Matrix<T> WN = line::mva::sn_get_residt_from_respt<T>(snw, to_matrix<T>(r.RN));
3620 line::reg::Json extra = line::reg::Json::object();
3622 if (!cache.empty()) extra["Cache"] = cache_extra_json<T>(cache);
3623 // ArvR IS NOT Tput, and reading it off the throughput column was wrong
3624 // wherever the two differ -- most visibly at a JOIN, which takes in one
3625 // sibling per branch and fires once per parent, so its arrival rate is the
3626 // fork degree times its throughput. `ssa_fj_foldback` already divides the
3627 // Join's QLen by the DERIVED rate to get its response time, so reporting
3628 // Tput in the ArvR column left the printed row self-inconsistent
3629 // (0.62372/1.02229 is 0.610, not the 0.3038 beside it). Taken on the
3630 // measured-cache-split struct for the same reason ResidT is.
3631 const line::Matrix<T> AN = line::mva::sn_get_arvr_from_tput<T>(snw, to_matrix<T>(r.TN));
3632 emit_avg_table<T>(sn, r.method, [&](std::size_t i, std::size_t c) {
3633 AvgRow row;
3634 row.q = r.QN(i, c);
3635 row.u = r.UN(i, c);
3636 row.r = r.RN(i, c);
3637 row.w = line::num_traits<T>::to_double(WN(i, c));
3638 row.t = r.TN(i, c);
3639 // A Source has no arrivals TO ITSELF, so its ArvR is 0 while its Tput is
3640 // the arrival rate.
3641 row.a = sn.stations[i].sched == line::lang::SchedStrategy::EXT
3642 ? 0.0
3643 : line::num_traits<T>::to_double(AN(i, c));
3644 return row;
3645 }, extra);
3646 return 0;
3647}
3648
3649/** The knobs the fluid solver reads, in one place so every fluid arm reads the
3650 * same set: an arm that quietly dropped one would answer a different model. */
3651inline line::fluid::FluidOptions fluid_options(const Knobs& k) {
3653 if (!k.method.empty()) opt.method = k.method;
3654 if (k.tol >= 0.0) opt.tol = k.tol;
3655 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
3656 if (k.iter_max >= 0) opt.iter_max = static_cast<std::size_t>(k.iter_max);
3657 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
3658 // `pstar_set` is what makes the smoothing active under a method that did
3659 // not ask for it by name, exactly as `options.config.pstar` does in the
3660 // reference: setting the exponent alone would leave `-a avg` integrating
3661 // the hard-min drift while reporting the caller's choice.
3662 if (k.pstar > 0.0) {
3663 opt.pstar = k.pstar;
3664 opt.pstar_set = true;
3665 }
3666 return opt;
3667}
3668
3669/**
3670 * Solve a Network model.json with the fluid solver and print the same table as
3671 * the MVA path, so the parity harness can diff the two rows unchanged.
3672 *
3673 * ResidT and ArvR are reported as the per-visit response time and the
3674 * throughput: the fluid analyzer works at station level and, unlike the MVA
3675 * runner, has no chain-visit conversion behind it. That is what MATLAB's
3676 * fluid `getAvgTable` shows for these columns on a single-visit model.
3677 *
3678 * Only `double` reaches here -- the drift is integrated by LSODA -- so the
3679 * other backends are refused by name in `solve_model_dispatch` rather than
3680 * being narrowed silently.
3681 */
3682template <class T>
3683int solve_model_fluid(const std::string& file, const Knobs& k) {
3684 line::qn::Network<T> net = read_model<T>(file);
3685 const line::fluid::FluidOptions opt = fluid_options(k);
3686 // `solver_fluid_run_analyzer` is runAnalyzer's resolution over the analyzer, so a
3687 // Cache model reaches the rmf branch, a DPS model the closing drift, and
3688 // anything the moment closure accepts reaches `minnormal`.
3689 // The converged hit/miss split is COLLECTED, for the reason the SSA arm
3690 // above collects its own: `-a avg` is what MATLAB's lang='cpp' bridge calls,
3691 // and a missing block there CLEARS the host's Cache node.
3693 // THE REFRESHED STRUCT IS TAKEN, not dropped: on a cache model the routing
3694 // the analyzer converged to carries the ACTUAL hit/miss split, where
3695 // `net.get_struct()` still carries link()'s offered one. The arrival rates
3696 // below are read off that routing, so the offered split reported 0.5/0.5
3697 // where the model converged to 0.4/0.6 (cache_replc_routing).
3700 net.get_struct(), opt, static_cast<line::qn::NetworkStruct<T>*>(nullptr),
3701 &refreshed, &cache);
3702 const line::qn::NetworkStruct<T>& sn = net.get_struct();
3703 const line::qn::NetworkStruct<T>& snflow =
3704 refreshed.nstations == sn.nstations && refreshed.nclasses == sn.nclasses ? refreshed : sn;
3705
3706 std::printf("SolverFluid arith=%s method=%s type=%s iters=%zu\n", line::num_traits<T>::name(),
3707 r.method.c_str(),
3708 line::util::method_type("FLD", r.method).c_str(), r.iters);
3709 // The residence-time conversion the SSA arm above applies, for the same
3710 // reason: neither analyzer produces a per-job residence time, and
3711 // `sn_get_residt_from_respt` derives one from RN and the visit ratios.
3712 const line::Matrix<T> WN = line::mva::sn_get_residt_from_respt<T>(sn, to_matrix<T>(r.RN));
3713 // THE ARRIVAL RATE IS A FLOW, NOT A COPY OF THE THROUGHPUT. `runAnalyzer`
3714 // takes it from `sn_get_arvr_from_tput`, i.e. from the class-expanded
3715 // routing, and the two agree only where every job a station serves it also
3716 // completes. They part on a station a job LEAVES by another route: on
3717 // cache_replc_routing the fluid solution puts zero throughput on the two
3718 // Delay stations while 0.4 and 0.6 arrive at them, so copying the
3719 // throughput made both rows all-zero and the table dropped them.
3720 const line::Matrix<T> AN = line::mva::sn_get_arvr_from_tput<T>(snflow, to_matrix<T>(r.TN));
3721 line::reg::Json extra = line::reg::Json::object();
3722 if (!cache.empty()) extra["Cache"] = cache_extra_json<T>(cache);
3723 emit_avg_table<T>(sn, r.method, [&](std::size_t i, std::size_t c) {
3724 AvgRow row;
3725 row.q = r.QN(i, c);
3726 row.u = r.UN(i, c);
3727 row.r = r.RN(i, c);
3728 row.w = line::num_traits<T>::to_double(WN(i, c));
3729 row.t = r.TN(i, c);
3730 // A Source has no arrivals TO ITSELF, so its ArvR is 0 while its Tput is
3731 // the arrival rate -- the same rule the SSA arm above applies. Without it
3732 // the two CLI paths disagreed on one column of the same model:
3733 // gallery_mm1 reported Source ArvR 0 under -s mva and 1 under -s fluid.
3734 row.a = sn.stations[i].sched == line::lang::SchedStrategy::EXT
3735 ? 0.0
3736 : line::num_traits<T>::to_double(AN(i, c));
3737 return row;
3738 }, extra);
3739 return 0;
3740}
3741
3742/**
3743 * `-s fluid -a statevec`: the converged FLUID STATE VECTOR, `result.odeStateVec`.
3744 *
3745 * NOT A METRIC AND NOT INDEXED LIKE ONE. The ODE state carries one coordinate
3746 * per (station, class, PHASE), so a two-phase Erlang service contributes two
3747 * entries where the AvgTable contributes one number, and the sum over a
3748 * station's phases is its mean queue length. It is what a caller needs to
3749 * restart an integration, to seed another solver, or to read the phase
3750 * occupancy the means average away -- which is why the JAR exposes it as its
3751 * own `-a statevec` rather than as a column.
3752 *
3753 * The index layout is `fluid_state_layout`'s and is emitted BESIDE the vector,
3754 * because a bare list of numbers cannot be related back to a station without it.
3755 */
3756template <class T>
3757int solve_model_fluid_statevec(const std::string& file, const Knobs& k) {
3758 line::qn::Network<T> net = read_model<T>(file);
3759 const line::fluid::FluidOptions opt = fluid_options(k);
3761 const line::qn::NetworkStruct<T>& sn = net.get_struct();
3762 if (r.xvec.empty())
3764 "-a statevec reports the converged ODE state and this solve produced none; the rmf "
3765 "and closing branches integrate a drift and fill it, so a branch that returns means "
3766 "directly has no state vector to report");
3767
3768 // The (station, class, phase) each coordinate belongs to, in the order the
3769 // ODE state is laid out: station-major, then class, then phase.
3770 std::vector<std::size_t> ist, cls, phs;
3771 for (std::size_t i = 0; i < sn.nstations; ++i)
3772 for (std::size_t c = 0; c < sn.nclasses; ++c) {
3773 const std::size_t np = sn.phases_of(i + 1, c + 1);
3774 for (std::size_t j = 0; j < np; ++j) {
3775 ist.push_back(i);
3776 cls.push_back(c);
3777 phs.push_back(j);
3778 }
3779 }
3780 // The layout is a CLAIM about the solver's state ordering, so it is checked
3781 // rather than asserted in a comment: a mismatch means the labels below would
3782 // name the wrong station, which is worse than no labels at all.
3783 const bool labelled = ist.size() == r.xvec.size();
3784
3785 if (g_json_output) {
3786 line::reg::Json p = line::reg::Json::object();
3787 p["type"] = "FluidStateVec";
3788 p["indexBase"] = 0;
3789 p["xvec"] = vector_json(r.xvec);
3790 p["labelled"] = labelled;
3791 if (labelled) {
3792 line::reg::Json st = line::reg::Json::array(), cl = line::reg::Json::array(),
3793 ph = line::reg::Json::array();
3794 for (std::size_t j = 0; j < ist.size(); ++j) {
3795 st.push_back(sn.stations[ist[j]].name);
3796 cl.push_back(sn.classes[cls[j]].name);
3797 ph.push_back(phs[j]);
3798 }
3799 p["Station"] = st;
3800 p["JobClass"] = cl;
3801 p["Phase"] = ph;
3802 }
3803 emit_analysis<T>("statevec", p, r.method);
3804 return 0;
3805 }
3806 std::printf("SolverFluid arith=%s method=%s coords=%zu\n", line::num_traits<T>::name(),
3807 r.method.c_str(), r.xvec.size());
3808 if (!labelled) {
3809 std::printf("# the ODE state is %zu wide and the (station, class, phase) layout accounts "
3810 "for %zu; the coordinates are printed unlabelled\n",
3811 r.xvec.size(), ist.size());
3812 std::printf("%-10s %20s\n", "Index", "x");
3813 for (std::size_t j = 0; j < r.xvec.size(); ++j)
3814 std::printf("%-10zu %20.10g\n", j, r.xvec[j]);
3815 return 0;
3816 }
3817 std::printf("%-16s %-14s %-8s %20s\n", "Station", "JobClass", "Phase", "x");
3818 for (std::size_t j = 0; j < r.xvec.size(); ++j)
3819 std::printf("%-16s %-14s %-8zu %20.10g\n", sn.stations[ist[j]].name.c_str(),
3820 sn.classes[cls[j]].name.c_str(), phs[j], r.xvec[j]);
3821 return 0;
3822}
3823
3824/**
3825 * Solve an ENVIRONMENT model.json with SolverENV and print the same average
3826 * table every other arm prints.
3827 *
3828 * THE COLUMNS ARE THE REFERENCE'S, INCLUDING THE TWO IT LEAVES EMPTY.
3829 * `@@SolverENV/getEnsembleAvg` returns Q, U and T from the coupling, sets
3830 * `WNclass = QNclass ./ TNclass` and returns `RNclass` and `ANclass` as NaN --
3831 * ENV blends per-stage metrics over the environment process and computes no
3832 * response time or arrival rate at all. Printing Q/T under RespT here would
3833 * invent a number the reference declines to give, so RespT and ArvR are NaN and
3834 * ResidT carries the Little's-law ratio, exactly as MATLAB's table does.
3835 *
3836 * The station and class names come from stage 1. The mean-field coupling
3837 * already refuses an environment whose stages disagree on the station or class
3838 * count, so any stage names the same rows.
3839 */
3840/**
3841 * Run the coupling `o.method` names, at the arithmetic the caller asked for.
3842 *
3843 * WHY THIS IS NOT JUST `env::solver_env`. That entry instantiates BOTH
3844 * couplings, and the mean-field one solves each stage with the fluid transient
3845 * -- LSODA, hence double. Calling it at `Rational` does not merely give a worse
3846 * answer, it does not compile (`sqrt` on a rational), so the template below
3847 * carries only the state-vector coupling and the double overload beside it
3848 * carries the full dispatch. The dispatcher has already refused `-s env
3849 * --arith exact` without `--method statevec`, so a non-double run reaching here HAS
3850 * asked for the state-vector coupling and gets it, banner included.
3851 */
3852template <class T>
3854 const line::env::EnvOptions& o) {
3856 out.statevec =
3857 line::env::solver_env_statevec(e, line::env::dispatch_detail::env_statevec_options<T>(o));
3858 line::env::dispatch_detail::env_take_statevec(out);
3859 return out;
3860}
3861
3862/** The double case, where both couplings are available. */
3864 const line::env::EnvOptions& o) {
3865 return line::env::solver_env(e, o);
3866}
3867
3868template <class T>
3869int solve_model_env(const std::string& file, const Knobs& k) {
3870 line::env::Environment<T> e = read_env_model<T>(file);
3872 if (!k.method.empty() && k.method != "default") opt.method = k.method;
3873 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
3874 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
3875 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
3876 if (k.tran_points) opt.tran_points = k.tran_points;
3877 if (k.tol >= 0.0) opt.stage.tol = k.tol;
3878 // THE STAGE SOLVER FOLLOWS THE COUPLING, because each coupling has exactly
3879 // one. `EnvOptions::stage_solver` defaults to `fluid`, which is what the
3880 // mean-field coupling needs (a transient mean) and what the state-vector one
3881 // refuses (it needs an enumerated generator and state space, which only
3882 // SolverCTMC exposes). Leaving the default in place made every
3883 // `--method statevec` run die on "stage solver 'fluid' is not available",
3884 // with no flag to fix it. This is not a silent fallback: there is one
3885 // admissible stage solver per coupling, and picking the other would be the
3886 // error.
3887 if (opt.method == "statevec" || opt.method == "blend") opt.stage_solver = "ctmc";
3888 // `--stage-solver` OVERRIDES that default, and only the mean-field coupling
3889 // has a choice to make: it needs a transient mean, which both the fluid
3890 // analyzer and the enumerated CTMC produce, and the two are different
3891 // models rather than two routes to one answer (a chain holds whole jobs).
3892 // An ensemble built on SolverCTMC stages therefore has to say so, or the
3893 // engine answers the fluid ensemble under its name -- which is what the
3894 // hosts refused lang='cpp' for.
3895 if (!k.stage_solver.empty()) opt.stage_solver = k.stage_solver;
3896 if (k.cutoff >= 0.0) opt.stage_cutoff = k.cutoff;
3897
3898 // UNQUALIFIED, so the double overload above wins for T = double: naming the
3899 // template explicitly would send every arithmetic to the state-vector
3900 // coupling and quietly ignore `--method meanfield`.
3901 const line::env::EnvAnalyzerSolution<T> r = env_run(e, opt);
3902 const line::qn::NetworkStruct<T>& sn = e.stage(0).model;
3903
3904 // The horizon and the grid are part of the ANSWER on this path, for the
3905 // same reason the seed and the sample count are on the SSA one: the
3906 // mean-field exit metrics are a quadrature, and two runs are the same
3907 // measurement only if both knobs are stated.
3908 // `points` is the mean-field quadrature's and is printed only there: the
3909 // state-vector coupling carries the whole joint law across a switch and
3910 // never sums over that grid, so reporting a grid it did not use would
3911 // describe a computation that did not happen.
3912 // A closed-form limit reads neither knob: it solves each stage, or one
3913 // rate-averaged model, in STEADY STATE, so a horizon and an iteration count
3914 // would describe a transient and a fixed point that never ran.
3915 if (r.method == "avg" || r.method == "dec")
3916 std::printf("SolverENV arith=%s method=%s stages=%zu (closed-form limit)\n",
3917 line::num_traits<T>::name(), r.method.c_str(), e.nstages());
3918 else if (r.method == "statevec")
3919 std::printf("SolverENV arith=%s method=%s stages=%zu horizon=%.6g iters=%d%s\n",
3921 r.iterations, r.converged ? "" : " (NOT CONVERGED)");
3922 else
3923 // Every remaining method -- meanfield, and the smp and statedep runs of
3924 // the same analyzer -- sums the same quadrature, so all of them report
3925 // the grid it was summed over.
3926 std::printf("SolverENV arith=%s method=%s stages=%zu horizon=%.6g points=%zu iters=%d%s\n",
3928 opt.tran_points, r.iterations, r.converged ? "" : " (NOT CONVERGED)");
3929 emit_avg_table<T>(sn, r.method, [&](std::size_t i, std::size_t c) {
3930 AvgRow row;
3931 row.q = line::num_traits<T>::to_double(r.QN(i, c));
3932 row.u = line::num_traits<T>::to_double(r.UN(i, c));
3933 row.t = line::num_traits<T>::to_double(r.TN(i, c));
3934 row.r = std::numeric_limits<double>::quiet_NaN();
3935 row.a = std::numeric_limits<double>::quiet_NaN();
3936 row.w = row.q / row.t;
3937 return row;
3938 });
3939 return 0;
3940}
3941
3942/**
3943 * `-a var`: the SECOND moment of the queue length, which only the fluid solver
3944 * has and only through two of its methods.
3945 *
3946 * `minnormal` and `refined` report the STATIONARY covariance of the linear noise
3947 * approximation (`@@SolverFLD/getMoments`), `kp` the covariance integrated along
3948 * the trajectory (`@@SolverFLD/getTranAvgVar`); this prints the per-station,
3949 * per-class variance and its standard deviation, plus, on the JSON path, the full
3950 * state covariance so that cross-station terms survive rather than only the
3951 * per-block totals. Every other method carries a first moment only and is refused
3952 * by name -- a variance of zero would be a claim, not an absence.
3953 */
3954template <class T>
3955int solve_model_fluid_var(const std::string& file, const Knobs& k) {
3956 line::qn::Network<T> net = read_model<T>(file);
3957 const line::fluid::FluidOptions opt = fluid_options(k);
3958 const line::qn::NetworkStruct<T>& sn = net.get_struct();
3960 if (!r.has_moments)
3962 "-a var needs a fluid method that computes a second moment: 'minnormal', 'refined' or "
3963 "'dae' for the stationary covariance, 'kp' for the covariance along the trajectory. "
3964 "The '" +
3965 r.method + "' method integrates the mean only");
3966
3967 if (g_json_output) {
3968 line::reg::Json p = line::reg::Json::object();
3969 p["type"] = "QueueLengthVariance";
3970 p["indexBase"] = 0;
3971 p["Station"] = line::reg::Json::array();
3972 p["JobClass"] = line::reg::Json::array();
3973 p["QVar"] = line::reg::Json::array();
3974 p["QStd"] = line::reg::Json::array();
3975 for (std::size_t i = 0; i < sn.nstations; ++i)
3976 for (std::size_t c = 0; c < sn.nclasses; ++c) {
3977 if (r.moments.QVar(i, c) == 0.0) continue;
3978 p["Station"].push_back(sn.stations[i].name);
3979 p["JobClass"].push_back(sn.classes[c].name);
3980 p["QVar"].push_back(r.moments.QVar(i, c));
3981 p["QStd"].push_back(r.moments.QStd(i, c));
3982 }
3983 p["Sigma"] = matrix_json<double>(r.moments.Sigma);
3984 emit_analysis<T>("var", p, r.method);
3985 return 0;
3986 }
3987 std::printf("SolverFluid arith=%s method=%s second moment\n", line::num_traits<T>::name(),
3988 r.method.c_str());
3989 std::printf("%-16s %-14s %12s %12s\n", "Station", "JobClass", "QVar", "QStd");
3990 for (std::size_t i = 0; i < sn.nstations; ++i)
3991 for (std::size_t c = 0; c < sn.nclasses; ++c) {
3992 if (r.moments.QVar(i, c) == 0.0) continue;
3993 std::printf("%-16s %-14s %12.6g %12.6g\n", sn.stations[i].name.c_str(),
3994 sn.classes[c].name.c_str(), r.moments.QVar(i, c), r.moments.QStd(i, c));
3995 }
3996 return 0;
3997}
3998
3999/**
4000 * `-a odes`: `@@SolverFLD/exportODEs`, the drift itself rather than its fixed
4001 * point.
4002 *
4003 * The output is the LaTeX document the reference writes, printed to stdout so
4004 * that it can be redirected. It carries a machine-readable comment header
4005 * naming every state variable and every event, which is what makes the document
4006 * diffable against MATLAB's rather than only readable.
4007 */
4008template <class T>
4009int solve_model_fluid_odes(const std::string& file, const Knobs& k) {
4010 line::qn::Network<T> net = read_model<T>(file);
4012 if (!k.method.empty()) opt.method = k.method;
4013 if (k.tol >= 0.0) opt.tol = k.tol;
4014 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4015 // `--notation` REACHES THE EXPORTER, and is not fixed at "scalar" here: the
4016 // matrix form is a different document of the same drift, and hardcoding one
4017 // while accepting a flag naming the other is the silent-acceptance defect
4018 // this CLI refuses everywhere else. An unrecognised name is refused by
4019 // `export_odes_latex` itself rather than defaulted.
4020 const std::string notation = k.notation.empty() ? "scalar" : k.notation;
4021 const std::string tex = line::fluid::solver_fluid_export_odes(sn, opt, notation, sn.name);
4022 if (g_json_output) {
4023 line::reg::Json p = line::reg::Json::object();
4024 p["type"] = "ODEs";
4025 p["notation"] = notation;
4026 // The document goes in a STRING VALUE, escaped by the JSON writer: it is
4027 // LaTeX, so it carries backslashes and newlines that a host reading raw
4028 // stdout would have to re-parse out of the surrounding table.
4029 p["latex"] = tex;
4030 emit_analysis<T>("odes", p,
4031 line::fluid::detail::fluid_resolve_method(sn, opt.method, opt));
4032 return 0;
4033 }
4034 std::printf("%s\n", tex.c_str());
4035 return 0;
4036}
4037
4038/**
4039 * `-a jacobian`: `@@SolverFLD/getJacobian`, d f_i / d x_j of the mean-field
4040 * drift, with the equilibria beside it when they are asked for.
4041 *
4042 * This is the fixed point's LOCAL BEHAVIOUR, which no integration reports: the
4043 * eigenvalues of J tell a stable fixed point from a limit cycle and give the
4044 * rate at which the fluid approximation converges to it.
4045 *
4046 * The method must be a smooth one. `fluid_symbolic_drift` refuses the min-scaled
4047 * drifts by the factor that carries the kink, before any backend is contacted.
4048 */
4049template <class T>
4050int solve_model_fluid_jacobian(const std::string& file, const Knobs& k) {
4051 line::qn::Network<T> net = read_model<T>(file);
4053 if (!k.method.empty()) opt.method = k.method;
4054 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4055 // pstar REACHES THE SYSTEM ONLY UNDER `pnorm`, which is the rule the
4056 // integrator and the exporter both follow: `matrix` and `default` leave it
4057 // at zero, selecting the hard min. That is why they have no Jacobian here
4058 // and `pnorm` does, and it is the reference's rule too -- MATLAB reads
4059 // options.config.pstar, which is unset unless asked for.
4060 std::string m = opt.method;
4061 if (m.compare(0, 6, "fluid.") == 0) m = m.substr(6);
4063 sn, opt.method, (m == "pnorm" || opt.pstar_set) ? opt.pstar : 0.0, std::vector<double>());
4065 if (!k.symbolic.empty()) symopt.backend = k.symbolic;
4066 symopt.equilibria = k.equilibria;
4068
4069 if (g_json_output) {
4070 line::reg::Json p = line::reg::Json::object();
4071 p["type"] = "Jacobian";
4072 p["engine"] = jac.engine;
4073 p["vars"] = line::reg::Json(jac.vars);
4074 p["rhs"] = line::reg::Json(jac.rhs);
4075 line::reg::Json rows = line::reg::Json::array();
4076 for (std::size_t i = 0; i < jac.J.size(); ++i) rows.push_back(line::reg::Json(jac.J[i]));
4077 p["jacobian"] = rows;
4078 // `hasEquilibria` separates "asked and answered with none" from "never
4079 // asked"; an empty list alone would read as "this system has none".
4080 p["hasEquilibria"] = jac.has_equilibria;
4081 line::reg::Json eqs = line::reg::Json::array();
4082 for (std::size_t e = 0; e < jac.equilibria.size(); ++e) {
4083 line::reg::Json one = line::reg::Json::object();
4084 for (std::map<std::string, std::string>::const_iterator it = jac.equilibria[e].begin();
4085 it != jac.equilibria[e].end(); ++it)
4086 one[it->first] = it->second;
4087 eqs.push_back(one);
4088 }
4089 p["equilibria"] = eqs;
4090 emit_analysis<T>("jacobian", p,
4091 line::fluid::detail::fluid_resolve_method(sn, opt.method, opt));
4092 return 0;
4093 }
4094
4095 std::printf("engine=%s states=%zu\n", jac.engine.c_str(), jac.vars.size());
4096 for (std::size_t i = 0; i < jac.rhs.size(); ++i)
4097 std::printf("d%s/dt = %s\n", jac.vars[i].c_str(), jac.rhs[i].c_str());
4098 for (std::size_t i = 0; i < jac.J.size(); ++i)
4099 for (std::size_t j = 0; j < jac.J[i].size(); ++j) {
4100 // A structurally zero entry is printed, not skipped: a reader must be
4101 // able to tell a zero derivative from a row this port never emitted.
4102 std::printf("J[%s,%s] = %s\n", jac.vars[i].c_str(), jac.vars[j].c_str(),
4103 jac.J[i][j].c_str());
4104 }
4105 if (jac.has_equilibria) {
4106 if (jac.equilibria.empty())
4107 std::printf("equilibria: none in closed form (the solve found none, which is not a "
4108 "proof that none exist)\n");
4109 for (std::size_t e = 0; e < jac.equilibria.size(); ++e)
4110 for (std::map<std::string, std::string>::const_iterator it = jac.equilibria[e].begin();
4111 it != jac.equilibria[e].end(); ++it)
4112 std::printf("equilibrium %zu: %s = %s\n", e + 1, it->first.c_str(),
4113 it->second.c_str());
4114 }
4115 return 0;
4116}
4117
4118/**
4119 * `-s fluid -a tranvar`: `@@SolverFLD/getTranAvgVar`, the queue-length VARIANCE
4120 * along the trajectory, plus the full state covariance at each time point.
4121 *
4122 * NOT `-a var`, which reports the STATIONARY covariance of `minnormal` /
4123 * `refined` -- one number per (station, class) at the fixed point. This is the
4124 * diffusion limit of Ko and Pender integrated alongside the fluid limit, so it
4125 * has a value at every t, and only `--method kp` produces it. Asking any other
4126 * method for it is an error rather than a misleading zero, which is the header's
4127 * own rule and is left to the header to enforce so the two flags cannot drift.
4128 */
4129template <class T>
4130int solve_model_fluid_tranvar(const std::string& file, const Knobs& k) {
4131 line::qn::Network<T> net = read_model<T>(file);
4132 const line::fluid::FluidOptions opt = fluid_options(k);
4133 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4135 if (tr.t.empty())
4136 throw line::UnsupportedError("-a tranvar produced no trajectory points");
4137
4138 if (g_json_output) {
4139 line::reg::Json p = line::reg::Json::object();
4140 p["type"] = "TranAvgVarTable";
4141 p["indexBase"] = 0;
4142 p["t0"] = tr.t.front();
4143 p["t1"] = tr.t.back();
4144 line::reg::Json ts = line::reg::Json::array();
4145 for (std::size_t j = 0; j < tr.t.size(); ++j) ts.push_back(tr.t[j]);
4146 p["t"] = ts;
4147 line::reg::Json arr = line::reg::Json::array();
4148 for (std::size_t i = 0; i < sn.nstations; ++i)
4149 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4150 if (sn.disabled[i][c]) continue;
4151 line::reg::Json e = line::reg::Json::object();
4152 e["Station"] = sn.stations[i].name;
4153 e["JobClass"] = sn.classes[c].name;
4154 e["station"] = i;
4155 e["jobclass"] = c;
4156 line::reg::Json v = line::reg::Json::array();
4157 for (std::size_t j = 0; j < tr.QVar.size(); ++j) v.push_back(tr.QVar[j](i, c));
4158 e["QVar"] = v;
4159 arr.push_back(e);
4160 }
4161 p["curves"] = arr;
4162 // The full covariance is the answer's other half: the per-pair variances
4163 // are its diagonal, and a caller asking for the diffusion limit wants the
4164 // off-diagonal correlations the limit is about.
4165 line::reg::Json sig = line::reg::Json::array();
4166 for (std::size_t j = 0; j < tr.Sigma.size(); ++j)
4167 sig.push_back(matrix_json<double>(tr.Sigma[j]));
4168 p["Sigma"] = sig;
4169 emit_analysis<T>("tranvar", p, "kp");
4170 return 0;
4171 }
4172 std::printf("SolverFluid arith=%s method=kp tspan=[%g,%g] points=%zu dim=%zu\n",
4173 line::num_traits<T>::name(), tr.t.front(), tr.t.back(), tr.t.size(),
4174 tr.Sigma.empty() ? std::size_t(0) : tr.Sigma.front().rows());
4175 std::printf("%-16s %-14s %12s %12s %12s\n", "Station", "JobClass", "Time", "QVar", "QStd");
4176 for (std::size_t i = 0; i < sn.nstations; ++i)
4177 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4178 if (sn.disabled[i][c]) continue;
4179 for (std::size_t j = 0; j < tr.QVar.size(); ++j) {
4180 const double var = tr.QVar[j](i, c);
4181 std::printf("%-16s %-14s %12.6g %12.6g %12.6g\n", sn.stations[i].name.c_str(),
4182 sn.classes[c].name.c_str(), tr.t[j], var,
4183 var >= 0.0 ? std::sqrt(var) : std::numeric_limits<double>::quiet_NaN());
4184 }
4185 }
4186 return 0;
4187}
4188
4189/**
4190 * `-s fluid -a tran`: `@@SolverFLD/getTranAvg`, the metrics ALONG the
4191 * trajectory rather than at its fixed point.
4192 *
4193 * `--tspan` is optional here and required on the MAM arm, and the difference is
4194 * the reference's: `options.timespan` defaults to [0, Inf] for the fluid solver,
4195 * which does not mean "integrate forever" but "integrate until the state stops
4196 * moving" -- `solver_fluid_tran_avg` reproduces the horizon that adaptive loop
4197 * converges at. A caller that names a horizon gets exactly that one.
4198 *
4199 * The reference forces `closing` for a transient (the matrix and smoothed
4200 * variants are steady-state devices) and warns when it does; the port forces it
4201 * too, and the banner names the method that actually integrated. `dae` is the
4202 * one exception the reference itself makes -- it has a trajectory of its own,
4203 * with conservation carried as an algebraic equation -- so
4204 * `solver_fluid_run_transient` routes it rather than substituting the
4205 * first-order drift under its name.
4206 */
4207template <class T>
4208int solve_model_fluid_tran(const std::string& file, const Knobs& k) {
4209 line::qn::Network<T> net = read_model<T>(file);
4210 const line::fluid::FluidOptions opt = fluid_options(k);
4211 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4212 const std::vector<line::fluid::FluidTranPoint> tr =
4214 if (tr.empty()) throw line::UnsupportedError("-a tran produced no trajectory points");
4215
4216 if (g_json_output) {
4217 line::reg::Json p = line::reg::Json::object();
4218 p["type"] = "TranAvgTable";
4219 p["indexBase"] = 0;
4220 p["t0"] = 0.0;
4221 p["t1"] = tr.back().t;
4222 line::reg::Json ts = line::reg::Json::array();
4223 for (std::size_t j = 0; j < tr.size(); ++j) ts.push_back(tr[j].t);
4224 line::reg::Json arr = line::reg::Json::array();
4225 for (std::size_t i = 0; i < sn.nstations; ++i)
4226 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4227 if (sn.disabled[i][c]) continue;
4228 line::reg::Json e = line::reg::Json::object();
4229 e["Station"] = sn.stations[i].name;
4230 e["JobClass"] = sn.classes[c].name;
4231 e["station"] = i;
4232 e["jobclass"] = c;
4233 e["t"] = ts;
4234 line::reg::Json q = line::reg::Json::array(), u = line::reg::Json::array(),
4235 x = line::reg::Json::array();
4236 for (std::size_t j = 0; j < tr.size(); ++j) {
4237 q.push_back(tr[j].QN(i, c));
4238 u.push_back(tr[j].UN(i, c));
4239 x.push_back(tr[j].TN(i, c));
4240 }
4241 e["QLen"] = q;
4242 e["Util"] = u;
4243 e["Tput"] = x;
4244 arr.push_back(e);
4245 }
4246 p["curves"] = arr;
4247 emit_analysis<T>("tran", p, "closing");
4248 return 0;
4249 }
4250 std::printf("SolverFluid arith=%s method=closing tspan=[0,%g] points=%zu\n",
4251 line::num_traits<T>::name(), tr.back().t, tr.size());
4252 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
4253 "Tput");
4254 for (std::size_t i = 0; i < sn.nstations; ++i)
4255 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4256 if (sn.disabled[i][c]) continue;
4257 for (std::size_t j = 0; j < tr.size(); ++j)
4258 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
4259 sn.stations[i].name.c_str(), sn.classes[c].name.c_str(), tr[j].t,
4260 tr[j].QN(i, c), tr[j].UN(i, c), tr[j].TN(i, c));
4261 }
4262 return 0;
4263}
4264
4265/**
4266 * `-s fluid -a prob`: `@@SolverFLD/getProbAggr`, the probability that a station
4267 * holds the marginal population of the model's default state.
4268 *
4269 * IT IS NOT THE MVA ARM'S ANSWER AND IS NOT MEANT TO BE. The fluid solver has
4270 * no state space, so the law is fitted to the means it does produce -- a
4271 * binomial per closed class, the BCMP marginal per open one -- and under a
4272 * moment closure it is instead the multivariate normal the closure supplies,
4273 * correlation between the classes included. Two solvers disagreeing here is the
4274 * approximation showing, not a defect.
4275 *
4276 * The log-probability is reported beside it because the fitted law underflows
4277 * on a large population, where the linear value is 0 and the log one is not.
4278 */
4279template <class T>
4280int solve_model_fluid_prob(const std::string& file, const Knobs& k) {
4281 line::qn::Network<T> net = read_model<T>(file);
4282 const line::fluid::FluidOptions opt = fluid_options(k);
4283 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4285
4286 std::vector<double> pr(sn.nstations, 0.0), lg(sn.nstations, 0.0);
4287 for (std::size_t i = 0; i < sn.nstations; ++i)
4288 pr[i] = line::fluid::fluid_prob_aggr(sn, r, i + 1, &lg[i]);
4289
4290 if (g_json_output) {
4291 line::reg::Json p = line::reg::Json::object();
4292 p["type"] = "ProbAggr";
4293 p["indexBase"] = 0;
4294 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
4295 lp = line::reg::Json::array();
4296 for (std::size_t i = 0; i < sn.nstations; ++i) {
4297 st.push_back(sn.stations[i].name);
4298 pa.push_back(pr[i]);
4299 lp.push_back(lg[i]);
4300 }
4301 p["Station"] = st;
4302 p["ProbAggr"] = pa;
4303 p["logProbAggr"] = lp;
4304 emit_analysis<T>("prob", p, r.method);
4305 return 0;
4306 }
4307 std::printf("SolverFluid arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
4308 r.method.c_str(), line::util::method_type("FLD", r.method).c_str());
4309 std::printf("%-16s %14s %14s\n", "Station", "ProbAggr", "logProbAggr");
4310 for (std::size_t i = 0; i < sn.nstations; ++i)
4311 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(), pr[i], lg[i]);
4312 return 0;
4313}
4314
4315/**
4316 * `-s fluid -a cdf`: `@@SolverFLD/getCdfRespT`, the WHOLE response-time law per
4317 * (station, class) and not only its mean.
4318 *
4319 * The law is read off a second integration in which the jobs present at the
4320 * steady state are MARKED and followed to their departure, so it is the
4321 * stationary response-time distribution of the fluid model. The solve that
4322 * produces the state to mark in is run here, as the reference runs it: its
4323 * `getCdfRespT` clears the cached result and re-runs `getAvg` first.
4324 */
4325template <class T>
4326int solve_model_fluid_cdf(const std::string& file, const Knobs& k) {
4327 line::qn::Network<T> net = read_model<T>(file);
4328 const line::fluid::FluidOptions opt = fluid_options(k);
4329 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4330 const std::vector<std::vector<line::fluid::FluidPassage> > RD =
4332
4333 if (g_json_output) {
4334 line::reg::Json p = line::reg::Json::object();
4335 p["type"] = "CdfRespT";
4336 p["indexBase"] = 0;
4337 line::reg::Json arr = line::reg::Json::array();
4338 for (std::size_t i = 0; i < RD.size(); ++i)
4339 for (std::size_t c = 0; c < RD[i].size(); ++c) {
4340 // An empty curve is an ABSENT law -- a Source, or a class the
4341 // station does not serve -- and is omitted rather than sent as a
4342 // degenerate one.
4343 if (RD[i][c].t.empty()) continue;
4344 line::reg::Json e = line::reg::Json::object();
4345 e["Station"] = sn.stations[i].name;
4346 e["JobClass"] = sn.classes[c].name;
4347 e["station"] = i;
4348 e["jobclass"] = c;
4349 e["t"] = line::reg::Json(RD[i][c].t);
4350 e["F"] = line::reg::Json(RD[i][c].cdf);
4351 arr.push_back(e);
4352 }
4353 p["respt"] = arr;
4354 emit_analysis<T>("cdf", p, std::string());
4355 return 0;
4356 }
4357 std::printf("SolverFluid arith=%s\n", line::num_traits<T>::name());
4358 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
4359 for (std::size_t i = 0; i < RD.size(); ++i)
4360 for (std::size_t c = 0; c < RD[i].size(); ++c)
4361 for (std::size_t j = 0; j < RD[i][c].t.size(); ++j)
4362 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
4363 sn.classes[c].name.c_str(), RD[i][c].t[j], RD[i][c].cdf[j]);
4364 return 0;
4365}
4366
4367/**
4368 * `-s fluid -a aoi`: `@@SolverFLD/getAvgAoI` and `getCdfAoI` in one answer, the
4369 * Age of Information and Peak AoI laws of a status-update system.
4370 *
4371 * ONLY THE `mfq` METHOD HAS THEM, and only on the topology the age laws are
4372 * defined for: one open class through Source -> Queue -> Sink, a single server,
4373 * capacity 1 (bufferless) or 2 (single buffer), FCFS/LCFS/LCFSPR. The topology
4374 * is tested first so a model that is not one is told WHICH condition it fails
4375 * rather than being handed a number computed for a different system.
4376 *
4377 * `--method` may only say `mfq` here: silently overriding a caller who asked for
4378 * another method would report the age laws under a method that does not produce
4379 * them.
4380 */
4381template <class T>
4382int solve_model_fluid_aoi(const std::string& file, const Knobs& k) {
4383 line::qn::Network<T> net = read_model<T>(file);
4384 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4386 if (!top.ok)
4388 "-a aoi reports the age of a status-update system and needs the topology the age laws "
4389 "are defined for: " +
4390 (top.error.empty() ? std::string("this model is not one") : top.error));
4391 line::fluid::FluidOptions opt = fluid_options(k);
4392 const std::string requested = line::fluid::detail::fluid_unqualify(opt.method);
4393 if (requested != "default" && requested != "mfq")
4395 "-a aoi is the AoI branch of the 'mfq' method; '" + requested +
4396 "' integrates the mean-field drift and carries no age process");
4397 opt.method = "mfq";
4399 if (!r.has_aoi)
4401 "-a aoi: the 'mfq' method did not take its AoI branch on this model");
4402
4403 // The grid `getCdfAoI` builds when the caller names no time points: five
4404 // mean ages, which covers the bulk of both laws.
4405 const double base = (std::isfinite(r.aoi.aoi.mean) && r.aoi.aoi.mean > 0.0) ? r.aoi.aoi.mean : 1.0;
4406 const std::size_t np = 200;
4407 std::vector<double> tv(np), fa(np), fp(np);
4408 for (std::size_t j = 0; j < np; ++j) {
4409 tv[j] = 5.0 * base * static_cast<double>(j) / static_cast<double>(np - 1);
4410 fa[j] = line::fluid::aoi_cdf(r.aoi.aoi, tv[j]);
4411 fp[j] = line::fluid::aoi_cdf(r.aoi.paoi, tv[j]);
4412 }
4413 const double asd = std::sqrt(std::max(0.0, r.aoi.aoi.var));
4414 const double psd = std::sqrt(std::max(0.0, r.aoi.paoi.var));
4415
4416 if (g_json_output) {
4417 line::reg::Json p = line::reg::Json::object();
4418 p["type"] = "AoI";
4419 p["systemType"] = r.aoi.system_type;
4420 p["preemption"] = r.aoi.preemption;
4421 p["AoIMean"] = r.aoi.aoi.mean;
4422 p["AoIVar"] = r.aoi.aoi.var;
4423 p["AoIStd"] = asd;
4424 p["PAoIMean"] = r.aoi.paoi.mean;
4425 p["PAoIVar"] = r.aoi.paoi.var;
4426 p["PAoIStd"] = psd;
4427 p["t"] = line::reg::Json(tv);
4428 p["AoICdf"] = line::reg::Json(fa);
4429 p["PAoICdf"] = line::reg::Json(fp);
4430 // The (g, A, h) DENSITY triples, not only the curve evaluated above.
4431 // `getCdfAoI` takes an optional t_values, and a caller who names their
4432 // own grid cannot be served from a fixed 200-point one; with the triple
4433 // they evaluate the same law at their own abscissae. `solve_mfq_aoi`
4434 // normalizes g so that g*expm(A t)*h is the density, so the survival
4435 // function carries an extra inv(A) -- the MATLAB getter's own note.
4436 p["AoI_g"] = line::reg::Json(r.aoi.aoi.g);
4437 p["AoI_A"] = matrix_json(r.aoi.aoi.A);
4438 p["AoI_h"] = line::reg::Json(r.aoi.aoi.h);
4439 p["PAoI_g"] = line::reg::Json(r.aoi.paoi.g);
4440 p["PAoI_A"] = matrix_json(r.aoi.paoi.A);
4441 p["PAoI_h"] = line::reg::Json(r.aoi.paoi.h);
4442 emit_analysis<T>("aoi", p, r.method);
4443 return 0;
4444 }
4445 std::printf("SolverFluid arith=%s method=mfq system=%s preemption=%.6g\n",
4447 std::printf("%-8s %14s %14s %14s\n", "Metric", "Mean", "Var", "Std");
4448 std::printf("%-8s %14.10g %14.10g %14.10g\n", "AoI", r.aoi.aoi.mean, r.aoi.aoi.var, asd);
4449 std::printf("%-8s %14.10g %14.10g %14.10g\n", "PAoI", r.aoi.paoi.mean, r.aoi.paoi.var, psd);
4450 std::printf("%14s %14s %14s\n", "Time", "F_AoI(t)", "F_PAoI(t)");
4451 for (std::size_t j = 0; j < np; ++j)
4452 std::printf("%14.8g %14.10g %14.10g\n", tv[j], fa[j], fp[j]);
4453 return 0;
4454}
4455
4456/**
4457 * Solve a Network model.json and print its aggregate state probabilities:
4458 * getProbSysAggr (the whole-system joint) and getProbAggr per station, over the
4459 * model's default initial state. These fit the MVA means and so need logarithms;
4460 * under exact/Rational they refuse by name.
4461 */
4462template <class T>
4463int solve_model_prob(const std::string& file, const Knobs& k) {
4466 "the -a prob analysis fits a binomial/product-form law and needs transcendental "
4467 "arithmetic; rerun with --arith double or --arith real");
4468 } else {
4469 line::qn::Network<T> net = read_model<T>(file);
4471 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4472 if (k.tol >= 0.0) opt.tol = k.tol;
4473 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4474 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
4475 line::Matrix<T> init;
4477 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4478 const line::mva::AggrResult<T> ps =
4480 if (g_json_output) {
4481 line::reg::Json p = line::reg::Json::object();
4482 p["type"] = "ProbAggr";
4483 p["indexBase"] = 0;
4484 p["ProbSysAggr"] = line::num_traits<T>::to_double(ps.P);
4485 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array();
4486 for (std::size_t i = 0; i < sn.nstations; ++i) {
4487 st.push_back(sn.stations[i].name);
4488 pa.push_back(line::num_traits<T>::to_double(
4489 line::mva::solver_mva_get_prob_aggr(sn, r, i + 1, opt.method).P));
4490 }
4491 p["Station"] = st;
4492 p["ProbAggr"] = pa;
4493 emit_analysis<T>("prob", p, r.actualmethod);
4494 return 0;
4495 }
4496 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
4497 r.actualmethod.c_str(),
4498 line::util::method_type("MVA", r.actualmethod).c_str());
4499 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(ps.P));
4500 std::printf("%-16s %14s\n", "Station", "ProbAggr");
4501 for (std::size_t i = 0; i < sn.nstations; ++i) {
4502 const line::mva::AggrResult<T> pa =
4504 std::printf("%-16s %14.10g\n", sn.stations[i].name.c_str(),
4506 }
4507 return 0;
4508 }
4509}
4510
4511/**
4512 * `-s mva -a marg`: `@@SolverMVA/getProbMarg`, P(n jobs of class r at station i).
4513 *
4514 * THE WHOLE GRID BY DEFAULT, one curve per (station, class): the reference takes
4515 * the station and the class as arguments and this CLI has no notion of a
4516 * "current" pair, so reporting every pair is the only reading that answers the
4517 * method rather than a choice this file would be making on the caller's behalf.
4518 * `--node` and `--class` narrow it to one node's station and one class, and
4519 * `--marg-states` is the reference's third argument `state_m`: the n values to
4520 * report, in place of the default range each case picks for itself (0..N_r for a
4521 * closed class, mean + 5 sigma for a Poisson, the 1e-10 tail for a geometric).
4522 *
4523 * A NODE THAT IS NOT A STATION IS AN ERROR, not an empty answer: a queue-length
4524 * law at a ClassSwitch is not a quantity, and defaulting to the whole network
4525 * after the caller narrowed it would report more than was asked for.
4526 */
4527template <class T>
4528int solve_model_marg(const std::string& file, const Knobs& k) {
4531 "the -a marg analysis fits a binomial / Poisson / geometric law and needs "
4532 "transcendental arithmetic; rerun with --arith double or --arith real");
4533 } else {
4534 line::qn::Network<T> net = read_model<T>(file);
4536 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4537 if (k.tol >= 0.0) opt.tol = k.tol;
4538 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4539 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
4540 line::Matrix<T> init;
4542 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4543
4544 std::vector<std::size_t> ists; // 1-based station indices to report
4545 if (k.node) {
4546 if (k.node > sn.nof_nodes())
4547 throw line::InputError("--node " + std::to_string(k.node) +
4548 " exceeds the number of nodes in the model (" +
4549 std::to_string(sn.nof_nodes()) + ")");
4550 const std::size_t ist = sn.nodes[k.node - 1].station;
4551 if (!ist)
4552 throw line::InputError("--node " + std::to_string(k.node) + " ('" +
4553 sn.nodes[k.node - 1].name +
4554 "') is not a station, and a queue-length distribution is "
4555 "reported per station");
4556 ists.push_back(ist);
4557 } else {
4558 for (std::size_t i = 0; i < sn.nstations; ++i) ists.push_back(i + 1);
4559 }
4560 std::vector<std::size_t> rs; // 1-based class indices to report
4561 if (k.jobclass) {
4562 if (k.jobclass > sn.nclasses)
4563 throw line::InputError("--class " + std::to_string(k.jobclass) +
4564 " exceeds the number of classes in the model");
4565 rs.push_back(k.jobclass);
4566 } else {
4567 for (std::size_t c = 0; c < sn.nclasses; ++c) rs.push_back(c + 1);
4568 }
4569
4570 // Every curve first: a pair the reference refuses must not leave a
4571 // banner and a column header standing above an answer that never came.
4572 std::vector<line::mva::MargResult<T> > curves;
4573 for (std::size_t a = 0; a < ists.size(); ++a)
4574 for (std::size_t b = 0; b < rs.size(); ++b)
4575 curves.push_back(line::mva::solver_mva_get_prob_marg(sn, r, ists[a], rs[b],
4576 k.marg_states, opt.method));
4577
4578 if (g_json_output) {
4579 line::reg::Json p = line::reg::Json::object();
4580 p["type"] = "ProbMarg";
4581 p["indexBase"] = 0;
4582 line::reg::Json arr = line::reg::Json::array();
4583 for (std::size_t a = 0, q = 0; a < ists.size(); ++a)
4584 for (std::size_t b = 0; b < rs.size(); ++b, ++q) {
4585 const line::mva::MargResult<T>& m = curves[q];
4586 line::reg::Json e = line::reg::Json::object();
4587 e["station"] = ists[a] - 1;
4588 e["Station"] = sn.stations[ists[a] - 1].name;
4589 e["jobclass"] = rs[b] - 1;
4590 e["JobClass"] = sn.classes[rs[b] - 1].name;
4591 line::reg::Json jobs = line::reg::Json::array();
4592 for (std::size_t n = 0; n < m.P.size(); ++n)
4593 jobs.push_back(k.marg_states.empty() ? static_cast<long>(n)
4594 : k.marg_states[n]);
4595 e["Jobs"] = jobs;
4596 e["P"] = vector_json(m.P);
4597 e["logP"] = vector_json(m.logP);
4598 arr.push_back(e);
4599 }
4600 p["marginal"] = arr;
4601 emit_analysis<T>("marg", p, r.actualmethod);
4602 return 0;
4603 }
4604 std::printf("SolverMVA arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
4605 r.actualmethod.c_str(),
4606 line::util::method_type("MVA", r.actualmethod).c_str());
4607 std::printf("%-16s %-14s %-8s %16s\n", "Station", "JobClass", "Jobs", "ProbMarg");
4608 for (std::size_t a = 0, q = 0; a < ists.size(); ++a)
4609 for (std::size_t b = 0; b < rs.size(); ++b, ++q) {
4610 const line::mva::MargResult<T>& m = curves[q];
4611 for (std::size_t n = 0; n < m.P.size(); ++n)
4612 std::printf("%-16s %-14s %-8ld %16.10g\n",
4613 sn.stations[ists[a] - 1].name.c_str(),
4614 sn.classes[rs[b] - 1].name.c_str(),
4615 k.marg_states.empty() ? static_cast<long>(n) : k.marg_states[n],
4617 }
4618 return 0;
4619 }
4620}
4621
4622/**
4623 * `-a normconst`: `@@SolverMVA/getProbNormConstAggr` and `@@SolverNC`'s.
4624 *
4625 * ONE ANALYSIS, TWO SOLVERS, AND THEY DO NOT COMPUTE IT THE SAME WAY. The NC
4626 * arm reads the constant its own solve already formed. The MVA arm RE-ENTERS the
4627 * analyzer at method='exact', as the reference does, because only the exact MVA
4628 * recursion carries a G: an AMVA solve has none, and reporting the requested
4629 * method's number would attribute the constant to an algorithm that never
4630 * produced one. That re-entry is why this is a separate `-a` and not a field on
4631 * the `-s mva` banner, where it would charge every average solve for a second
4632 * exact one.
4633 *
4634 * WHAT A MODEL WITH NO CONSTANT REPORTS IS THE ANALYZER'S OWN ANSWER, not a
4635 * substitution made here, and the two cases differ: the branches that form no G
4636 * at all -- MVAC, the LCFS chain -- set lG to NaN at the source and print nan,
4637 * while the open-queue closed forms report lG = 0 exactly as
4638 * solver_mva_qsys_analyzer.m:54,96,235 does. Neither is edited on the way out.
4639 */
4640/**
4641 * Mean busy period of a named subnetwork, Daduna (J. ACM 35(3), 1988).
4642 *
4643 * The transform `solver_nc_busyp` has been in the port since it landed, and
4644 * `ldes_cli` has answered `--busyperiod` all along, so the ONLY thing between
4645 * a caller and the analytical form was a `-a` token: asking this CLI for a busy
4646 * period meant simulating a quantity there is a closed form for.
4647 *
4648 * `--busyperiod-subnet` is 1-BASED, as every station index this CLI takes is,
4649 * and is required: a busy period is defined for a NAMED set of stations and
4650 * defaulting it would answer about a subnetwork the caller never chose. The
4651 * orders default to 1, the ordinary busy period.
4652 */
4653template <class T>
4654int solve_model_nc_busyp(const std::string& file, const Knobs& k) {
4655 if (k.busy_subnet.empty())
4656 throw line::InputError(
4657 "-a busyperiod needs --busyperiod-subnet: the busy period is defined for a named "
4658 "subnetwork of stations, and no default can choose one");
4659 line::qn::Network<T> net = read_model<T>(file);
4660 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4661 std::vector<std::size_t> subnet;
4662 for (std::size_t t = 0; t < k.busy_subnet.size(); ++t) {
4663 if (k.busy_subnet[t] > sn.nstations)
4664 throw line::InputError("--busyperiod-subnet names station " +
4665 std::to_string(k.busy_subnet[t]) + ", beyond the model's " +
4666 std::to_string(sn.nstations));
4667 subnet.push_back(k.busy_subnet[t] - 1);
4668 }
4669 std::vector<std::size_t> orders = k.busy_orders;
4670 if (orders.empty()) orders.push_back(1);
4671 const std::vector<double> b = line::nc::solver_nc_busyp(sn, subnet, orders);
4672
4673 if (g_json_output) {
4674 line::reg::Json p = line::reg::Json::object();
4675 p["type"] = "BusyPeriod";
4676 p["indexBase"] = 0;
4677 line::reg::Json sj = line::reg::Json::array();
4678 for (std::size_t t = 0; t < subnet.size(); ++t) sj.push_back(subnet[t]);
4679 line::reg::Json oj = line::reg::Json::array();
4680 for (std::size_t t = 0; t < orders.size(); ++t) oj.push_back(orders[t]);
4681 line::reg::Json bj = line::reg::Json::array();
4682 for (std::size_t t = 0; t < b.size(); ++t) bj.push_back(b[t]);
4683 p["subnet"] = sj;
4684 p["orders"] = oj;
4685 p["b"] = bj;
4686 emit_analysis<T>("busyperiod", p, "daduna");
4687 return 0;
4688 }
4689 std::printf("SolverNC arith=%s busy period, subnetwork {", line::num_traits<T>::name());
4690 for (std::size_t t = 0; t < k.busy_subnet.size(); ++t)
4691 std::printf("%s%zu", t ? "," : "", k.busy_subnet[t]);
4692 std::printf("}\n");
4693 for (std::size_t t = 0; t < orders.size(); ++t)
4694 std::printf(" order %zu %.10g\n", orders[t], b[t]);
4695 return 0;
4696}
4697
4698template <class T>
4699int solve_model_normconst(const std::string& file, const Knobs& k, const std::string& solver) {
4700 line::qn::Network<T> net = read_model<T>(file);
4701 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4702 double lG = 0.0;
4703 std::string method;
4704 if (solver == "nc") {
4706 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4707 if (k.tol >= 0.0) opt.tol = k.tol;
4708 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4709 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
4711 lG = r.lognormconst.has_value() ? r.lognormconst.value()
4712 : std::numeric_limits<double>::quiet_NaN();
4713 method = r.actualmethod;
4714 } else {
4716 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4717 if (k.tol >= 0.0) opt.tol = k.tol;
4718 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4719 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
4721 method = "exact";
4722 }
4723 if (g_json_output) {
4724 line::reg::Json p = line::reg::Json::object();
4725 p["type"] = "NormConst";
4726 p["indexBase"] = 0;
4727 // A NaN rides as JSON null, which is the table's nan on the wire.
4728 p["logNormConstAggr"] = lG;
4729 emit_analysis<T>("normconst", p, method);
4730 return 0;
4731 }
4732 std::printf("Solver%s arith=%s method=%s lognormconst=%.10g\n",
4733 solver == "nc" ? "NC" : "MVA", line::num_traits<T>::name(), method.c_str(), lG);
4734 return 0;
4735}
4736
4737/** The model's declared per-class placement, in `solver_nc_*`'s own container. */
4738template <class T>
4739line::nc::MarginalState nc_declared_marginal(const line::qn::NetworkStruct<T>& sn) {
4741 line::nc::MarginalState out(sn.nstations, std::vector<int>(sn.nclasses, 0));
4742 for (std::size_t i = 0; i < sn.nstations; ++i)
4743 for (std::size_t r = 0; r < sn.nclasses; ++r)
4744 out[i][r] = static_cast<int>(std::llround(line::num_traits<T>::to_double(nir(i, r))));
4745 return out;
4746}
4747
4748/**
4749 * The same two probabilities under SolverNC: `@@SolverNC/getProbSysAggr.m` and
4750 * `@@SolverNC/getProbAggr.m`, over the model's declared state.
4751 *
4752 * NOT THE SAME NUMBERS AS `-s mva -a prob`, and that is the point of having
4753 * both. SolverMVA fits a binomial to its own means (Schmidt 1997); these are a
4754 * ratio of normalizing constants and are the product-form model's own
4755 * probabilities exactly. A closed model therefore reports different figures
4756 * under the two solvers, and the NC ones are the reference.
4757 */
4758template <class T>
4759int solve_model_nc_prob(const std::string& file, const Knobs& k) {
4762 "the -s nc -a prob analysis exponentiates a difference of log normalizing constants "
4763 "and needs transcendental arithmetic; rerun with --arith double or --arith real");
4764 } else {
4765 line::qn::Network<T> net = read_model<T>(file);
4767 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4768 if (k.tol >= 0.0) opt.tol = k.tol;
4769 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4770 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
4771 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4772
4773 // The state is the MODEL'S OWN, which `model.json` now carries: a
4774 // stateful node's declared row is decoded to the per-class counts the
4775 // reference reads with `State.toMarginal(sn, ist, state{isf})`. Where a
4776 // station declares none, the default marking is rebuilt for it -- every
4777 // closed class at its reference station -- which is what `initDefault`
4778 // would have put there.
4779 line::nc::MarginalState nir = nc_declared_marginal<T>(sn);
4780 // `--state` is `getProb(node, state)`'s second argument, decoded the way
4781 // the reference decodes it: it substitutes the row into `sn.state{isf}`
4782 // and takes `State.toMarginal` of the result, so what reaches the
4783 // probability is that node's PER-CLASS COUNTS and every other node's
4784 // declared ones. Passing the row through untouched would treat an
4785 // encoding as a job vector, and on a station with phase-type service the
4786 // two differ in both width and meaning.
4787 if (!k.state.empty()) {
4788 if (!k.node)
4789 throw line::InputError(
4790 "--state is the state of ONE node and needs --node to say which");
4791 const std::size_t ist =
4792 k.node <= sn.nodes.size() ? sn.nodes[k.node - 1].station : 0;
4793 if (ist == 0)
4794 throw line::InputError("--node " + std::to_string(k.node) +
4795 " is not a station, so it has no queue-length state");
4796 std::vector<std::size_t> ph(sn.nclasses, 1), shift(sn.nclasses, 0);
4797 std::size_t w = 0;
4798 for (std::size_t c = 0; c < sn.nclasses; ++c) {
4799 ph[c] = sn.phases_of(ist, c + 1);
4800 shift[c] = w;
4801 w += ph[c];
4802 }
4803 std::vector<T> row(k.state.size());
4804 for (std::size_t i = 0; i < k.state.size(); ++i)
4805 row[i] = line::num_traits<T>::from_int(k.state[i]);
4806 const line::qn::Marginal<T> m =
4807 line::qn::to_marginal(sn, ist, row, ph, shift, sn.nvars_of(k.node));
4808 for (std::size_t c = 0; c < sn.nclasses; ++c)
4809 nir[ist - 1][c] =
4810 static_cast<int>(std::llround(line::num_traits<T>::to_double(m.nir[c])));
4811 }
4812
4813 // THE SOLVE'S lG IS NOT THIS lG, and handing it over here was wrong.
4814 // `solver_nc_solve` normalizes the SEIDMANN-REDUCED model -- a
4815 // multiserver station enters as demand/c with the residual folded into
4816 // the delay -- while the probability identity F_i G_{-i} / G needs the
4817 // constant of the load-dependent lattice mu(n) = min(n, c) that F_i and
4818 // G_{-i} are themselves computed on. Mixing the two scaled every
4819 // probability of a model with a multiserver station by one common
4820 // factor: on the 2-job Delay -> PS -> PS(c=2) chain the three stations
4821 // came back 0.17225 / 0.68900 / 0.32536 against the exact 0.18 / 0.72 /
4822 // 0.34, and the error is invisible on a single-server model because
4823 // there the two constants coincide. `solver_nc_margaggr` computes its
4824 // own, once, for every station -- which is the reference's
4825 // `logNormConstAggr` caching, not a per-station resolve.
4827 sn, opt, nir, std::numeric_limits<double>::quiet_NaN());
4828 const T ps = line::nc::solver_nc_getprob_sys_aggr(sn, opt, nir);
4829 // THE DETAILED PAIR TOO, because `getProb` and `getProbSys` are not the
4830 // aggregate ones with rounding: `solver_nc_marg` and `solver_nc_joint`
4831 // carry the class-within-chain split `lg0_i - lG0_i` that the aggregate
4832 // pair sums out, so on a multichain model they are different numbers
4833 // rather than the same one to more places.
4835 sn, opt, nir, std::numeric_limits<double>::quiet_NaN());
4836 const T pjoint = line::nc::solver_nc_joint<T>(sn, opt, nir, nullptr);
4837
4839 const std::string am = (opt.method == "default" && !d.actualmethod.empty() &&
4840 d.actualmethod != "default")
4841 ? "default/" + d.actualmethod
4842 : d.actualmethod;
4843 if (g_json_output) {
4844 line::reg::Json p = line::reg::Json::object();
4845 p["type"] = "ProbAggr";
4846 p["indexBase"] = 0;
4847 p["ProbSysAggr"] = line::num_traits<T>::to_double(ps);
4848 p["ProbSys"] = line::num_traits<T>::to_double(pjoint);
4849 line::reg::Json st = line::reg::Json::array(), pa = line::reg::Json::array(),
4850 pm = line::reg::Json::array();
4851 for (std::size_t i = 0; i < sn.nstations; ++i) {
4852 st.push_back(sn.stations[i].name);
4853 pa.push_back(line::num_traits<T>::to_double(mr.P[i]));
4854 pm.push_back(line::num_traits<T>::to_double(mdet.P[i]));
4855 }
4856 p["Station"] = st;
4857 p["ProbAggr"] = pa;
4858 p["Prob"] = pm;
4859 emit_analysis<T>("prob", p, am);
4860 return 0;
4861 }
4862 std::printf("SolverNC arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
4863 am.c_str(), line::util::method_type("NC", am).c_str());
4864 std::printf("ProbSysAggr %.10g\n", line::num_traits<T>::to_double(ps));
4865 std::printf("ProbSys %.10g\n", line::num_traits<T>::to_double(pjoint));
4866 std::printf("%-16s %14s %14s\n", "Station", "Prob", "ProbAggr");
4867 for (std::size_t i = 0; i < sn.nstations; ++i)
4868 std::printf("%-16s %14.10g %14.10g\n", sn.stations[i].name.c_str(),
4871 return 0;
4872 }
4873}
4874
4875/**
4876 * `-s nc -a sysmarg`: `@@SolverNC/getProbSysMarg.m`, the JOINT law of the
4877 * per-station total queue lengths.
4878 *
4879 * NEITHER `-a prob` NOR `-a marg`, and the three are worth telling apart.
4880 * `-a prob` fixes the PER-CLASS population of every station and is a product
4881 * form; `-a marg` is this law marginalized down to ONE station; this arm is the
4882 * joint over all of them, with the classes summed out. Each value is the sum of
4883 * `-a prob` over the whole fibre of per-class tables with these row sums, and
4884 * that fibre grows combinatorially, so it is evaluated as a permanent of the
4885 * demand matrix replicated once per job (Ryser 1963) rather than enumerated.
4886 *
4887 * The whole lattice of total states is swept, so the printed column sums to one
4888 * and the sweep pays for the normalizing constant once.
4889 */
4890template <class T>
4891int solve_model_nc_sysmarg(const std::string& file, const Knobs& k) {
4892 line::qn::Network<T> net = read_model<T>(file);
4894 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4895 if (k.tol >= 0.0) opt.tol = k.tol;
4896 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4897
4898 double Ntot = 0.0;
4899 for (std::size_t r = 0; r < sn.nclasses; ++r) {
4900 const double pop = sn.classes[r].population;
4901 if (!std::isfinite(pop))
4903 "getProbSysMarg requires a closed model: the joint law of the total queue lengths "
4904 "is not defined when a class has an infinite population");
4905 Ntot += pop;
4906 }
4907 const std::vector<std::vector<int> > states = line::pfqn::multichoose_rows(
4908 static_cast<int>(sn.nstations), static_cast<int>(std::llround(Ntot)));
4909
4910 std::vector<double> P(states.size(), 0.0);
4911 for (std::size_t j = 0; j < states.size(); ++j)
4913 line::nc::solver_nc_getprob_sys_marg(sn, opt, states[j], k.method_perm));
4914
4915 if (g_json_output) {
4916 line::reg::Json p = line::reg::Json::object();
4917 p["type"] = "ProbSysMarg";
4918 p["indexBase"] = 0;
4919 p["engine"] = k.method_perm;
4920 line::reg::Json st = line::reg::Json::array(), arr = line::reg::Json::array();
4921 for (std::size_t i = 0; i < sn.nstations; ++i) st.push_back(sn.stations[i].name);
4922 for (std::size_t j = 0; j < states.size(); ++j) {
4923 line::reg::Json e = line::reg::Json::object();
4924 line::reg::Json n = line::reg::Json::array();
4925 for (std::size_t i = 0; i < sn.nstations; ++i) n.push_back(states[j][i]);
4926 e["n"] = n;
4927 e["P"] = P[j];
4928 arr.push_back(e);
4929 }
4930 p["Station"] = st;
4931 p["states"] = arr;
4932 emit_analysis<T>("sysmarg", p, opt.method);
4933 return 0;
4934 }
4935 std::printf("SolverNC arith=%s method=%s engine=%s\n", line::num_traits<T>::name(),
4936 opt.method.c_str(), k.method_perm.c_str());
4937 for (std::size_t i = 0; i < sn.nstations; ++i)
4938 std::printf("%10s", sn.stations[i].name.c_str());
4939 std::printf(" %14s\n", "ProbSysMarg");
4940 double total = 0.0;
4941 for (std::size_t j = 0; j < states.size(); ++j) {
4942 for (std::size_t i = 0; i < sn.nstations; ++i) std::printf("%10d", states[j][i]);
4943 std::printf(" %14.10g\n", P[j]);
4944 total += P[j];
4945 }
4946 std::printf("%*s %14.10g\n", static_cast<int>(10 * sn.nstations), "sum", total);
4947 return 0;
4948}
4949
4950/**
4951 * `-s nc -a marg`: `@@SolverNC/getProbMarg.m`, the TOTAL queue-length law.
4952 *
4953 * NOT THE SAME QUANTITY AS `-s mva -a marg`, although the reference gives both
4954 * methods the same name. SolverMVA's getProbMarg is per (station, CLASS) and is
4955 * a binomial / Poisson / geometric fitted to the solver's own means; SolverNC's
4956 * is the TOTAL number of jobs at a station, summed over classes, and is exact --
4957 * a ratio of normalizing constants, obtained either from one `pfqn_procomom`
4958 * solve (`--method comom`) or by summing the aggregate marginal over the
4959 * per-class partitions of n. `--class` and `--marg-states` are therefore refused
4960 * for it rather than ignored: this law has no class argument and its support is
4961 * 0..sum(N), which the model fixes.
4962 *
4963 * Both P and log P are reported. The log is not a formatting of the other: the
4964 * enumeration forms it first and a probability that underflows to 0 in double
4965 * still has a finite log, so dropping it would lose the only number left.
4966 */
4967template <class T>
4968int solve_model_nc_marg(const std::string& file, const Knobs& k) {
4969 line::qn::Network<T> net = read_model<T>(file);
4971 if (!k.method.empty() && k.method != "default") opt.method = k.method;
4972 if (k.tol >= 0.0) opt.tol = k.tol;
4973 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
4974 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
4975 const line::qn::NetworkStruct<T>& sn = net.get_struct();
4976
4977 std::vector<std::size_t> ists; // 1-based station indices to report
4978 if (k.node) {
4979 if (k.node > sn.nof_nodes())
4980 throw line::InputError("--node " + std::to_string(k.node) +
4981 " exceeds the number of nodes in the model (" +
4982 std::to_string(sn.nof_nodes()) + ")");
4983 const std::size_t ist = sn.nodes[k.node - 1].station;
4984 if (!ist)
4985 throw line::InputError("--node " + std::to_string(k.node) + " ('" +
4986 sn.nodes[k.node - 1].name +
4987 "') is not a station, and a queue-length distribution is "
4988 "reported per station");
4989 ists.push_back(ist);
4990 } else {
4991 for (std::size_t i = 0; i < sn.nstations; ++i) ists.push_back(i + 1);
4992 }
4993
4994 // Every curve first, for solve_model_marg's reason: a station the reference
4995 // refuses must not leave a header standing above an answer that never came.
4996 std::vector<line::nc::NcQueueLengthDist<T> > curves;
4997 for (std::size_t a = 0; a < ists.size(); ++a)
4998 curves.push_back(line::nc::solver_nc_getprob_marg(sn, opt, ists[a]));
4999
5000 if (g_json_output) {
5001 line::reg::Json p = line::reg::Json::object();
5002 p["type"] = "ProbMargAggr";
5003 p["indexBase"] = 0;
5004 line::reg::Json arr = line::reg::Json::array();
5005 for (std::size_t a = 0; a < ists.size(); ++a) {
5006 line::reg::Json e = line::reg::Json::object();
5007 e["station"] = ists[a] - 1;
5008 e["Station"] = sn.stations[ists[a] - 1].name;
5009 e["P"] = vector_json<T>(curves[a].P);
5010 e["logP"] = vector_json<T>(curves[a].logP);
5011 arr.push_back(e);
5012 }
5013 p["curves"] = arr;
5014 emit_analysis<T>("marg", p, opt.method);
5015 return 0;
5016 }
5017 std::printf("SolverNC arith=%s method=%s type=%s\n", line::num_traits<T>::name(),
5018 opt.method.c_str(), line::util::method_type("NC", opt.method).c_str());
5019 for (std::size_t a = 0; a < ists.size(); ++a) {
5020 std::printf("%-16s %-8s %14s %14s\n", "Station", "n", "P", "logP");
5021 for (std::size_t n = 0; n < curves[a].P.size(); ++n)
5022 std::printf("%-16s %-8zu %14.10g %14.10g\n", sn.stations[ists[a] - 1].name.c_str(), n,
5023 line::num_traits<T>::to_double(curves[a].P[n]),
5024 line::num_traits<T>::to_double(curves[a].logP[n]));
5025 }
5026 return 0;
5027}
5028
5029/**
5030 * `-s nc -a cdf`: `@@SolverNC/getCdfRespT.m` and its aliases `getSjrnT`/`sjrnT`.
5031 *
5032 * THE WHOLE LAW, NOT ITS MEAN. `-a avg` reports E[R]; this reports F(t) per
5033 * (station, class) on one shared logarithmic grid, so a percentile or a tail
5034 * probability can be read off it. The algorithm is `pfqn_stdf` (`--method-cdf
5035 * exact`, the default) or the `pfqn_stdf_heur` reduction (`rd`), selected
5036 * through `options.config.algorithm` exactly as in the reference.
5037 *
5038 * FCFS ONLY, and the reference says so by WARNING and returning an empty
5039 * result rather than raising: the sojourn law of a processor-sharing or
5040 * infinite-server station is not the one this inversion computes. That warning
5041 * is carried through to stderr here and the analysis reports no curve, which is
5042 * distinguishable from a curve that is flat.
5043 */
5044template <class T>
5045int solve_model_nc_cdf(const std::string& file, const Knobs& k) {
5048 "the -s nc -a cdf analysis evaluates the sojourn law on a logarithmic time grid and "
5049 "inverts a generating function; it needs transcendental arithmetic, so rerun with "
5050 "--arith double or --arith real");
5051 } else {
5052 line::qn::Network<T> net = read_model<T>(file);
5054 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5055 if (k.tol >= 0.0) opt.tol = k.tol;
5056 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5057 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5058 if (!k.cdf_algorithm.empty()) opt.cdf_algorithm = k.cdf_algorithm;
5059 const line::qn::NetworkStruct<T>& sn = net.get_struct();
5061 if (!r.warning.empty()) std::fprintf(stderr, "warning: %s\n", r.warning.c_str());
5062
5063 if (g_json_output) {
5064 line::reg::Json p = line::reg::Json::object();
5065 p["type"] = "CdfRespT";
5066 p["indexBase"] = 0;
5067 p["algorithm"] = opt.cdf_algorithm;
5068 // ONE OBJECT PER CURVE, as on the CTMC arm: the pairs that carry a
5069 // law are a subset of the grid, so a column form would leave the
5070 // host to re-group them.
5071 line::reg::Json rd = line::reg::Json::array();
5072 for (std::size_t i = 0; i < r.RD.size(); ++i)
5073 for (std::size_t c = 0; c < r.RD[i].size(); ++c) {
5074 // An empty entry is an ABSENT law -- the station is not FCFS
5075 // or does not serve the class -- and is omitted rather than
5076 // sent as a degenerate one.
5077 if (r.RD[i][c].empty()) continue;
5078 line::reg::Json e = line::reg::Json::object();
5079 e["Station"] = sn.stations[i].name;
5080 e["JobClass"] = sn.classes[c].name;
5081 e["station"] = i;
5082 e["jobclass"] = c;
5083 line::reg::Json tt = line::reg::Json::array(), ff = line::reg::Json::array();
5084 for (std::size_t j = 0; j < r.RD[i][c].rows(); ++j) {
5085 ff.push_back(line::num_traits<T>::to_double(r.RD[i][c](j, 0)));
5086 tt.push_back(line::num_traits<T>::to_double(r.RD[i][c](j, 1)));
5087 }
5088 e["t"] = tt;
5089 e["F"] = ff;
5090 rd.push_back(e);
5091 }
5092 p["respt"] = rd;
5093 p["tset"] = vector_json(r.tset);
5094 if (!r.warning.empty()) p["warning"] = r.warning;
5095 // NO "method": the law comes from the sojourn-time inversion and not
5096 // from the normalizing-constant ladder, so the requested method name
5097 // would not be the algorithm that produced these numbers.
5098 emit_analysis<T>("cdf", p, std::string());
5099 return 0;
5100 }
5101 std::printf("SolverNC arith=%s algorithm=%s grid=%zu\n", line::num_traits<T>::name(),
5102 opt.cdf_algorithm.c_str(), r.tset.size());
5103 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
5104 for (std::size_t i = 0; i < r.RD.size(); ++i)
5105 for (std::size_t c = 0; c < r.RD[i].size(); ++c) {
5106 if (r.RD[i][c].empty()) continue;
5107 for (std::size_t j = 0; j < r.RD[i][c].rows(); ++j)
5108 std::printf("%-16s %-14s %14.8g %14.10g\n", sn.stations[i].name.c_str(),
5109 sn.classes[c].name.c_str(),
5110 line::num_traits<T>::to_double(r.RD[i][c](j, 1)),
5111 line::num_traits<T>::to_double(r.RD[i][c](j, 0)));
5112 }
5113 return 0;
5114 }
5115}
5116
5117/**
5118 * The clean-up a sensitivity table applies before printing, for a quantity that
5119 * may legitimately be NEGATIVE.
5120 *
5121 * `ln_sanitize` below tests `x <= FineTol`, which is right for a queue length or
5122 * a utilization -- every metric it was written for is nonnegative, so that test
5123 * reads as "negligible". A DERIVATIVE is not: raising a service rate lowers the
5124 * response time, the queue length and the utilization, so the whole sensitivity
5125 * table is negative by construction and the unsigned test would print an exact
5126 * zero for every one of those columns. Only the MAGNITUDE decides negligibility
5127 * here. The NC and the layered tables share it, which is why it sits above both.
5128 */
5129double sens_sanitize_signed(double x) {
5130 if (std::fabs(x) <= line::lang::GlobalConstants::FineTol) return 0.0;
5131 return x;
5132}
5133
5134/**
5135 * `-s nc -a sens`: `@@NetworkSolver/getSensitivityTable.m` under SolverNC.
5136 *
5137 * ONE ROW PER (station, class) carrying dTput/dRate, dRespT/dRate, dQLen/dRate
5138 * and dUtil/dRate, i.e. the derivative of that row's means with respect to that
5139 * row's service RATE. Two branches produce them and the banner names the one
5140 * that ran: `exact` differentiates the product-form recursion analytically
5141 * (`pfqn_sens` at chain level for a closed model, the closed-form BCMP
5142 * derivatives for an open one), `fd` re-solves rate-perturbed copies of the
5143 * model with THIS solver and forms the quotient.
5144 *
5145 * NC IS ONE OF THE TWO ENGINES THAT CAN TAKE THE EXACT BRANCH, which is what
5146 * `@@SolverNC/supportsExactSensitivity.m` returns true for, so `auto` resolves
5147 * to `exact` whenever the model is in its scope (single-server queues plus
5148 * delays, not mixed) and only falls back to differences outside it. Asking for
5149 * `--sens-method exact` outside that scope is refused by name rather than
5150 * silently downgraded: the two branches answer to different precision.
5151 *
5152 * The struct is COPIED rather than referenced because the fd branch writes a
5153 * scaled service process into it between solves; `net.get_struct()` hands out a
5154 * const reference to the model's own, which must not move under the caller.
5155 */
5156template <class T>
5157int solve_model_nc_sens(const std::string& file, const Knobs& k) {
5158 line::qn::Network<T> net = read_model<T>(file);
5160 if (!k.method.empty() && k.method != "default") opt.method = k.method;
5161 if (k.tol >= 0.0) opt.tol = k.tol;
5162 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
5163 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
5165
5167 if (!k.sens_method.empty()) so.method = k.sens_method;
5168 if (!k.sens_scheme.empty()) so.scheme = k.sens_scheme;
5169 if (k.sens_step > 0.0) so.step = k.sens_step;
5170 so.simulation = false; // the normalizing-constant path is deterministic
5171
5172 // `getAvg`, not the raw analyzer: the reference's difference quotient is
5173 // taken on the metrics the solver reports, which are the filtered ones.
5175 sn, so, /*exact_available=*/true, [&sn, &opt]() {
5178 s.Q = a.QN;
5179 s.U = a.UN;
5180 s.R = a.RN;
5181 s.Tp = a.TN;
5182 s.C = a.CN;
5183 s.X = a.XN;
5184 s.method = a.actualmethod;
5185 s.iter = a.iter;
5186 return s;
5187 });
5188
5189 if (g_json_output) {
5190 line::reg::Json p = line::reg::Json::object();
5191 p["type"] = "SensitivityTable";
5192 p["indexBase"] = 0;
5193 p["branch"] = tbl.method;
5194 line::reg::Json rows = line::reg::Json::array();
5195 for (const line::sens::SensRow<T>& r : tbl.rows) {
5196 line::reg::Json o = line::reg::Json::object();
5197 o["Station"] = r.station;
5198 o["JobClass"] = r.jobclass;
5199 o["dTput_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dTput));
5200 o["dRespT_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dRespT));
5201 o["dQLen_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dQLen));
5202 o["dUtil_dRate"] = sens_sanitize_signed(line::num_traits<T>::to_double(r.dUtil));
5203 rows.push_back(o);
5204 }
5205 p["rows"] = rows;
5206 // The branch rides in "branch" because it is not a normalizing-constant
5207 // method name. The method slot is EMPTY on the exact branch, which
5208 // differentiates the recursion in closed form and never runs a solve:
5209 // naming the NC method there would attribute the numbers to an
5210 // algorithm that did not produce them.
5211 emit_analysis<T>("sens", p, tbl.method == "fd" ? opt.method : std::string());
5212 return 0;
5213 }
5214
5215 std::printf("SolverNC arith=%s branch=%s method=%s rows=%zu\n", line::num_traits<T>::name(),
5216 tbl.method.c_str(), tbl.method == "fd" ? opt.method.c_str() : "-",
5217 tbl.rows.size());
5218 std::printf("%-16s %-14s %14s %14s %14s %14s\n", "Station", "JobClass", "dTput_dRate",
5219 "dRespT_dRate", "dQLen_dRate", "dUtil_dRate");
5220 for (const line::sens::SensRow<T>& r : tbl.rows)
5221 std::printf("%-16s %-14s %14.6g %14.6g %14.6g %14.6g\n", r.station.c_str(),
5222 r.jobclass.c_str(),
5223 sens_sanitize_signed(line::num_traits<T>::to_double(r.dTput)),
5224 sens_sanitize_signed(line::num_traits<T>::to_double(r.dRespT)),
5225 sens_sanitize_signed(line::num_traits<T>::to_double(r.dQLen)),
5226 sens_sanitize_signed(line::num_traits<T>::to_double(r.dUtil)));
5227 return 0;
5228}
5229
5230/** The @@Solver method each `-a` stands for, which is what the chooser keys on. */
5231// ===================== SolverLDES, the simulator ==========================
5232
5233/**
5234 * The knobs one `-s ldes` invocation resolves to.
5235 *
5236 * `--samples` is the SERVICE-COMPLETION budget the engine stops at, `--seed` the
5237 * stream, and both are part of the answer rather than of the invocation, which is
5238 * why the banner carries them. Everything else is `--ldes-*`.
5239 *
5240 * A `--ldes-initsol` placement is NOT accompanied by a forced `fixed` warmup
5241 * filter here, deliberately: `initFromSolver` sets `tranfilter='fixed'` with
5242 * `warmupfrac=0` because the placement it computes IS a steady state, while a
5243 * placement handed in on the command line may equally be the start of a
5244 * transient. Pass `--ldes-tranfilter fixed --ldes-warmupfrac 0` alongside it to
5245 * reproduce `initFromSolver` exactly.
5246 */
5247inline line::ldes::LdesOptions ldes_options(const Knobs& k) {
5249 if (k.samples) o.samples = k.samples;
5250 if (k.seed) o.seed = static_cast<long>(k.seed);
5251 if (!k.method.empty()) o.method = k.method;
5252 if (!k.ldes_tranfilter.empty()) o.tranfilter = k.ldes_tranfilter;
5253 if (k.ldes_warmupfrac >= 0.0) o.warmupfrac = k.ldes_warmupfrac;
5254 if (!k.ldes_cimethod.empty()) o.cimethod = k.ldes_cimethod;
5255 if (k.ldes_cnvgon) o.cnvgon = true;
5256 if (k.ldes_cnvgtol > 0.0) o.cnvgtol = k.ldes_cnvgtol;
5257 if (k.ldes_slotted) o.slotted = true;
5258 if (k.ldes_slotlength > 0.0) {
5259 o.slotted = true;
5260 o.slot_length = k.ldes_slotlength;
5261 }
5262 if (k.ldes_replications > 0) o.replications = k.ldes_replications;
5263 if (k.ldes_numthreads > 0) o.numthreads = k.ldes_numthreads;
5264 if (k.ldes_maxtime > 0.0) o.timeout = k.ldes_maxtime;
5265 if (!k.ldes_initsol.empty()) o.init_sol = k.ldes_initsol;
5266 if (!k.ldes_rest_url.empty()) o.rest_url = k.ldes_rest_url;
5267 o.verbose = k.verbose;
5268 return o;
5269}
5270
5271/**
5272 * The model.json text the engine is handed.
5273 *
5274 * FORWARDED BYTE FOR BYTE, and never through `read_model`: the reader is scoped
5275 * to the subset the analytical solvers need, and a round trip through it would
5276 * degrade exactly the models LDES exists for. `-a reward` is the one arm that
5277 * also parses the document, because a reward DECLARATION is what it needs.
5278 */
5279inline std::string ldes_document(const std::string& file) {
5280 return file.empty() ? stdin_model_text() : line::ldes::detail::read_file(file);
5281}
5282
5283/** A reported entry, or 0 where the engine reported no such row. */
5284inline double ldes_at(const line::Matrix<double>& M, std::size_t i, std::size_t j) {
5285 return i < M.rows() && j < M.cols() ? M(i, j) : 0.0;
5286}
5287
5288/**
5289 * One LDES run.
5290 *
5291 * A HARD TIMEOUT IS A REFUSAL HERE, not an empty result. The two other clients
5292 * return an empty result flagged `timedOut` and warn, which suits a caller that
5293 * can inspect the flag; a CLI's caller reads a table, and a table of zeros that
5294 * means "the run was killed" is the silent-wrong-number outcome this CLI refuses
5295 * everywhere else.
5296 */
5297inline line::ldes::LdesResult ldes_run(const std::string& file,
5298 const line::ldes::LdesOptions& o,
5299 const std::vector<std::string>& extra) {
5300 const line::ldes::LdesResult r = line::ldes::solver_ldes_text(ldes_document(file), o, extra);
5301 if (r.timed_out)
5302 throw line::NumericError(
5303 "SolverLDES exceeded its wall-clock budget (--ldes-maxtime) and was terminated before "
5304 "it wrote a result; raise the budget or lower --samples");
5305 if (r.station_names.empty())
5306 throw line::NumericError(
5307 "SolverLDES: the engine reported no station names, so its metrics cannot be labelled; "
5308 "the run produced no result document");
5309 return r;
5310}
5311
5312/**
5313 * The provenance line every LDES arm prints first.
5314 *
5315 * `engine=` is not decoration: the AOT native image and the jar are two builds of
5316 * one engine and the first can lag the sources, so a number quoted from an LDES
5317 * run has to say which produced it. `stopping=` is the reason the run ended --
5318 * a `max_events` stop at a low `--samples` is a wide confidence interval and a
5319 * `max_time` one is a truncated run, and neither is visible in the means.
5320 */
5321inline void ldes_banner(const line::ldes::LdesResult& r, const line::ldes::LdesOptions& o) {
5322 std::printf("SolverLDES arith=double method=%s type=%s engine=%s samples=%zu seed=%ld "
5323 "time=%.6g events=%lld stopping=%s\n",
5324 r.method.c_str(), line::util::method_type("LDES", r.method).c_str(),
5325 r.engine.c_str(), o.events ? o.events : o.samples, o.seed, r.runtime,
5327}
5328
5329/** The envelope keys that qualify an LDES solve as a whole. */
5330inline line::reg::Json ldes_envelope(const line::ldes::LdesResult& r,
5331 const line::ldes::LdesOptions& o) {
5332 line::reg::Json e = line::reg::Json::object();
5333 e["engine"] = r.engine;
5334 e["samples"] = o.events ? o.events : o.samples;
5335 e["seed"] = o.seed;
5336 e["converged"] = r.converged;
5337 e["stoppingReason"] = r.stopping_reason;
5338 e["totalSimulatedEvents"] = r.total_simulated_events;
5339 e["runtime"] = r.runtime;
5340 return e;
5341}
5342
5343/**
5344 * `-s ldes -a avg`: the steady-state table, the engine's `getAvg`.
5345 *
5346 * THE FINITE-CAPACITY-REGION ROWS DO NOT JOIN THE STATION TABLE, unlike MATLAB's
5347 * `getAvgTable`, which appends them after the stations. A region is not a station
5348 * and the "Station" column of this CLI's table is read by a parity harness that
5349 * pairs rows with another codebase's stations; a region row there would pair with
5350 * nothing. They are printed as their own table and carried under `avg.fcr`, which
5351 * is the same information without the collision.
5352 */
5353int solve_model_ldes_avg(const std::string& file, const Knobs& k) {
5354 const line::ldes::LdesOptions o = ldes_options(k);
5355 const line::ldes::LdesResult r = ldes_run(file, o, std::vector<std::string>());
5356 if (!g_json_output) ldes_banner(r, o);
5357
5358 line::reg::Json extra = line::reg::Json::object();
5359 // The confidence intervals are the half-widths the engine reports, one per
5360 // metric; a simulation that quoted a mean without them would be quoting a
5361 // point estimate as if it were exact.
5362 line::reg::Json ci = line::reg::Json::object();
5363 if (!r.QNCI.empty()) ci["QNCI"] = matrix_json<double>(r.QNCI);
5364 if (!r.UNCI.empty()) ci["UNCI"] = matrix_json<double>(r.UNCI);
5365 if (!r.RNCI.empty()) ci["RNCI"] = matrix_json<double>(r.RNCI);
5366 if (!r.TNCI.empty()) ci["TNCI"] = matrix_json<double>(r.TNCI);
5367 if (!r.ANCI.empty()) ci["ANCI"] = matrix_json<double>(r.ANCI);
5368 if (!r.WNCI.empty()) ci["WNCI"] = matrix_json<double>(r.WNCI);
5369 if (!ci.empty()) extra["CI"] = ci;
5370 if (!r.QNfcr.empty()) {
5371 line::reg::Json f = line::reg::Json::object();
5372 f["nregions"] = r.nregions;
5373 f["QNfcr"] = matrix_json<double>(r.QNfcr);
5374 f["RNfcr"] = matrix_json<double>(r.RNfcr);
5375 f["TNfcr"] = matrix_json<double>(r.TNfcr);
5376 f["WNfcr"] = matrix_json<double>(r.WNfcr);
5377 if (!r.WeightNfcr.empty()) f["WeightNfcr"] = matrix_json<double>(r.WeightNfcr);
5378 if (!r.MemOccNfcr.empty()) f["MemOccNfcr"] = matrix_json<double>(r.MemOccNfcr);
5379 if (!r.DropRateNfcr.empty()) f["DropRateNfcr"] = matrix_json<double>(r.DropRateNfcr);
5380 extra["fcr"] = f;
5381 }
5382 if (!r.DropRateJoin.empty()) extra["DropRateJoin"] = matrix_json<double>(r.DropRateJoin);
5383 if (!r.cache_metrics.empty()) {
5384 line::reg::Json cm = line::reg::Json::object();
5385 for (std::map<std::string, line::ldes::LdesCacheMetrics>::const_iterator it =
5386 r.cache_metrics.begin();
5387 it != r.cache_metrics.end(); ++it) {
5388 line::reg::Json c = line::reg::Json::object();
5389 if (!it->second.hit.empty()) c["hit"] = matrix_json<double>(it->second.hit);
5390 if (!it->second.delayed.empty()) c["delayed"] = matrix_json<double>(it->second.delayed);
5391 if (!it->second.miss.empty()) c["miss"] = matrix_json<double>(it->second.miss);
5392 if (!it->second.latency.empty()) c["latency"] = matrix_json<double>(it->second.latency);
5393 if (!it->second.hitList.empty()) c["hitList"] = matrix_json<double>(it->second.hitList);
5394 if (!it->second.itemProb.empty())
5395 c["itemProb"] = matrix_json<double>(it->second.itemProb);
5396 if (!it->second.listCost.empty())
5397 c["listCost"] = matrix_json<double>(it->second.listCost);
5398 cm[it->first] = c;
5399 }
5400 extra["cacheMetrics"] = cm;
5401 }
5402
5403 // THE RESIDENCE TIME IS DERIVED HERE, not taken from the engine. The engine
5404 // reports WN = RN because it counts one visit per station, which is only
5405 // true when every visit ratio is 1; `getAvg.m:204` therefore discards the
5406 // WN a solver returned and recomputes `sn_get_residt_from_respt(sn, RN)`,
5407 // and every other C++ solver already routes through the same helper. On
5408 // cqn_repairmen, whose Queue1 is visited 0.3 times per cycle, the engine's
5409 // WN came out 11.8136 against the reference's 3.5205 -- the response time
5410 // reported as if the station were visited once.
5411 //
5412 // MATCHED BY NAME. The engine's station order is its own; a Cache or a
5413 // Source can sit at a different index in the struct, and pairing the two
5414 // off positionally would scale one station's time by another's visits.
5415 line::Matrix<double> WNd(r.station_names.size(), r.class_names.size(), 0.0);
5416 {
5417 line::qn::Network<double> net = read_model<double>(file);
5419 std::vector<std::size_t> st_of(r.station_names.size(), 0); // 1-based, 0 = unmatched
5420 for (std::size_t i = 0; i < r.station_names.size(); ++i)
5421 for (std::size_t j = 0; j < sn.nstations; ++j)
5422 if (sn.stations[j].name == r.station_names[i]) { st_of[i] = j + 1; break; }
5423 std::vector<std::size_t> cl_of(r.class_names.size(), 0);
5424 for (std::size_t c = 0; c < r.class_names.size(); ++c)
5425 for (std::size_t k = 0; k < sn.nclasses; ++k)
5426 if (sn.classes[k].name == r.class_names[c]) { cl_of[c] = k + 1; break; }
5427 line::Matrix<double> RNs(sn.nstations, sn.nclasses, 0.0);
5428 for (std::size_t i = 0; i < r.station_names.size(); ++i)
5429 for (std::size_t c = 0; c < r.class_names.size(); ++c)
5430 if (st_of[i] && cl_of[c]) RNs(st_of[i] - 1, cl_of[c] - 1) = ldes_at(r.RN, i, c);
5432 for (std::size_t i = 0; i < r.station_names.size(); ++i)
5433 for (std::size_t c = 0; c < r.class_names.size(); ++c)
5434 WNd(i, c) = (st_of[i] && cl_of[c]) ? WNs(st_of[i] - 1, cl_of[c] - 1)
5435 : ldes_at(r.WN, i, c);
5436 }
5437
5438 emit_avg_table_named(r.station_names, r.class_names, "double", r.method,
5439 [&](std::size_t i, std::size_t c) {
5440 AvgRow v;
5441 v.q = ldes_at(r.QN, i, c);
5442 v.u = ldes_at(r.UN, i, c);
5443 v.r = ldes_at(r.RN, i, c);
5444 v.w = WNd(i, c);
5445 v.a = ldes_at(r.AN, i, c);
5446 v.t = ldes_at(r.TN, i, c);
5447 return v;
5448 },
5449 extra, ldes_envelope(r, o));
5450
5451 if (!g_json_output && !r.QNfcr.empty()) {
5452 std::printf("%-16s %-14s %12s %12s %12s %12s %12s\n", "Region", "JobClass", "QLen", "RespT",
5453 "Tput", "Weight", "MemOcc");
5454 for (std::size_t i = 0; i < r.QNfcr.rows(); ++i)
5455 for (std::size_t c = 0; c < r.class_names.size(); ++c)
5456 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g\n",
5457 ("Region" + std::to_string(i + 1)).c_str(), r.class_names[c].c_str(),
5458 ldes_at(r.QNfcr, i, c), ldes_at(r.RNfcr, i, c),
5459 ldes_at(r.TNfcr, i, c), ldes_at(r.WeightNfcr, i, c),
5460 ldes_at(r.MemOccNfcr, i, c));
5461 }
5462 if (!g_json_output && !r.cache_metrics.empty()) {
5463 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Cache", "JobClass", "Hit", "Delayed",
5464 "Miss", "Latency");
5465 for (std::map<std::string, line::ldes::LdesCacheMetrics>::const_iterator it =
5466 r.cache_metrics.begin();
5467 it != r.cache_metrics.end(); ++it)
5468 for (std::size_t c = 0; c < r.class_names.size(); ++c)
5469 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n", it->first.c_str(),
5470 r.class_names[c].c_str(), ldes_at(it->second.hit, 0, c),
5471 ldes_at(it->second.delayed, 0, c), ldes_at(it->second.miss, 0, c),
5472 ldes_at(it->second.latency, 0, c));
5473 }
5474 return 0;
5475}
5476
5477/**
5478 * `-s ldes -a tran`: `getTranAvg`, the per-bucket QNt / UNt / TNt series over
5479 * `--tspan`.
5480 *
5481 * THE HORIZON IS REQUIRED. `options.timespan` is what turns the engine's run into
5482 * a transient one, and there is no default: a trajectory over an unstated horizon
5483 * is not a quantity. A SINGLE PATH IS NOT E[N](t) either -- there is no time
5484 * ergodicity at fixed t -- so `--ldes-replications` is how an ensemble mean is
5485 * asked for, exactly as `runAnalyzer.m` passes `--replications` for the same
5486 * reason.
5487 *
5488 * The series are indexed by STATION, as `LDESResultIO` writes them
5489 * (`result.QNt = new Matrix[numStations][numClasses]`).
5490 */
5491int solve_model_ldes_tran(const std::string& file, const Knobs& k) {
5492 line::ldes::LdesOptions o = ldes_options(k);
5493 o.has_timespan = true;
5494 o.t0 = k.t0;
5495 o.t1 = k.t1;
5496 std::vector<std::string> extra;
5497 extra.push_back("--trajectory");
5498 const line::ldes::LdesResult r = ldes_run(file, o, extra);
5499 if (r.t.empty() || r.QNt.empty())
5500 throw line::NumericError(
5501 "SolverLDES -a tran produced no trajectory: the engine ran but recorded no bucket over "
5502 "[" + line::ldes::detail::shortest(k.t0) + "," +
5503 line::ldes::detail::shortest(k.t1) + "]");
5504
5505 if (g_json_output) {
5506 line::reg::Json p = line::reg::Json::object();
5507 p["type"] = "TranAvgTable";
5508 p["indexBase"] = 0;
5509 p["t0"] = k.t0;
5510 p["t1"] = k.t1;
5511 line::reg::Json curves = line::reg::Json::array();
5512 for (std::size_t i = 0; i < r.QNt.size(); ++i)
5513 for (std::size_t c = 0; c < r.QNt[i].size(); ++c) {
5514 // An empty series is an ABSENT one -- the class does not visit
5515 // the station -- and is omitted rather than sent as a flat zero.
5516 if (r.QNt[i][c].empty()) continue;
5517 line::reg::Json e = line::reg::Json::object();
5518 e["Station"] = i < r.station_names.size() ? r.station_names[i]
5519 : "Station" + std::to_string(i);
5520 e["JobClass"] =
5521 c < r.class_names.size() ? r.class_names[c] : "Class" + std::to_string(c);
5522 e["station"] = i;
5523 e["jobclass"] = c;
5524 line::reg::Json tt = line::reg::Json::array(), q = line::reg::Json::array(),
5525 u = line::reg::Json::array(), x = line::reg::Json::array();
5526 for (std::size_t j = 0; j < r.QNt[i][c].rows(); ++j) {
5527 tt.push_back(r.QNt[i][c](j, 1));
5528 q.push_back(r.QNt[i][c](j, 0));
5529 }
5530 if (i < r.UNt.size() && c < r.UNt[i].size())
5531 for (std::size_t j = 0; j < r.UNt[i][c].rows(); ++j)
5532 u.push_back(r.UNt[i][c](j, 0));
5533 if (i < r.TNt.size() && c < r.TNt[i].size())
5534 for (std::size_t j = 0; j < r.TNt[i][c].rows(); ++j)
5535 x.push_back(r.TNt[i][c](j, 0));
5536 e["t"] = tt;
5537 e["QLen"] = q;
5538 e["Util"] = u;
5539 e["Tput"] = x;
5540 curves.push_back(e);
5541 }
5542 p["curves"] = curves;
5543 p["tset"] = vector_json(r.t);
5544 // The envelope is built ONCE into a local: `begin()` and `end()` taken
5545 // from two different temporaries are iterators into two different
5546 // objects, which is undefined behaviour and not a style point.
5547 const line::reg::Json env = ldes_envelope(r, o);
5548 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
5549 p[it.key()] = it.value();
5550 emit_analysis<double>("tran", p, r.method);
5551 return 0;
5552 }
5553 ldes_banner(r, o);
5554 std::printf("%-16s %-14s %12s %12s %12s %12s\n", "Station", "JobClass", "Time", "QLen", "Util",
5555 "Tput");
5556 for (std::size_t i = 0; i < r.QNt.size(); ++i)
5557 for (std::size_t c = 0; c < r.QNt[i].size(); ++c) {
5558 if (r.QNt[i][c].empty()) continue;
5559 for (std::size_t j = 0; j < r.QNt[i][c].rows(); ++j) {
5560 const bool hu = i < r.UNt.size() && c < r.UNt[i].size() &&
5561 j < r.UNt[i][c].rows();
5562 const bool hx = i < r.TNt.size() && c < r.TNt[i].size() &&
5563 j < r.TNt[i][c].rows();
5564 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g\n",
5565 (i < r.station_names.size() ? r.station_names[i].c_str() : "?"),
5566 (c < r.class_names.size() ? r.class_names[c].c_str() : "?"),
5567 r.QNt[i][c](j, 1), r.QNt[i][c](j, 0),
5568 hu ? r.UNt[i][c](j, 0) : 0.0, hx ? r.TNt[i][c](j, 0) : 0.0);
5569 }
5570 }
5571 return 0;
5572}
5573
5574/**
5575 * `-s ldes -a cdf`: `getCdfRespT`, the EMPIRICAL response-time law.
5576 *
5577 * A SIMULATOR MUST REPORT WHAT IT OBSERVED. The base solver's fallback fabricates
5578 * an exponential law with the right mean, which says nothing about the tail; the
5579 * engine records every per-job response time under `--respt-samples`, and the
5580 * curve here is the ecdf of those samples with repeated observations collapsed to
5581 * their largest F, exactly as `@@SolverLDES/getCdfRespT.m` builds it.
5582 *
5583 * `--respt-samples` POSTDATES the prebuilt AOT image, which is why the runner
5584 * order flips for it (see `ldes_runners`): on that image the flag is accepted and
5585 * ignored, and the arm would refuse for want of samples that were never asked for.
5586 */
5587int solve_model_ldes_cdf(const std::string& file, const Knobs& k, const char* key,
5588 const char* type) {
5589 const line::ldes::LdesOptions o = ldes_options(k);
5590 std::vector<std::string> extra;
5591 extra.push_back("--respt-samples");
5592 const line::ldes::LdesResult r = ldes_run(file, o, extra);
5593 if (r.respTimeSamples.empty())
5594 throw line::NumericError(
5595 "SolverLDES -a cdf needs the per-job response times the engine records under "
5596 "--respt-samples and the run returned none; raise --samples so completions are "
5597 "observed at all");
5598
5599 // The ecdf of each (station, class): sorted observations, F = i/n, and one
5600 // pair per DISTINCT value carrying the largest F at it.
5601 std::vector<std::vector<std::vector<double>>> tt(r.respTimeSamples.size()), ff(
5602 r.respTimeSamples.size());
5603 for (std::size_t i = 0; i < r.respTimeSamples.size(); ++i) {
5604 tt[i].resize(r.respTimeSamples[i].size());
5605 ff[i].resize(r.respTimeSamples[i].size());
5606 for (std::size_t c = 0; c < r.respTimeSamples[i].size(); ++c) {
5607 std::vector<double> x = r.respTimeSamples[i][c];
5608 if (x.empty()) continue;
5609 std::sort(x.begin(), x.end());
5610 const double n = static_cast<double>(x.size());
5611 for (std::size_t j = 0; j < x.size(); ++j) {
5612 if (j + 1 < x.size() && x[j + 1] == x[j]) continue;
5613 tt[i][c].push_back(x[j]);
5614 ff[i][c].push_back(static_cast<double>(j + 1) / n);
5615 }
5616 }
5617 }
5618
5619 if (g_json_output) {
5620 line::reg::Json p = line::reg::Json::object();
5621 p["type"] = type;
5622 p["indexBase"] = 0;
5623 p["algorithm"] = "empirical";
5624 line::reg::Json rd = line::reg::Json::array();
5625 for (std::size_t i = 0; i < tt.size(); ++i)
5626 for (std::size_t c = 0; c < tt[i].size(); ++c) {
5627 if (tt[i][c].empty()) continue;
5628 line::reg::Json e = line::reg::Json::object();
5629 e["Station"] = i < r.station_names.size() ? r.station_names[i]
5630 : "Station" + std::to_string(i);
5631 e["JobClass"] =
5632 c < r.class_names.size() ? r.class_names[c] : "Class" + std::to_string(c);
5633 e["station"] = i;
5634 e["jobclass"] = c;
5635 e["t"] = vector_json(tt[i][c]);
5636 e["F"] = vector_json(ff[i][c]);
5637 e["samples"] = r.respTimeSamples[i][c].size();
5638 rd.push_back(e);
5639 }
5640 p["respt"] = rd;
5641 emit_analysis<double>(key, p, std::string());
5642 return 0;
5643 }
5644 ldes_banner(r, o);
5645 std::printf("%-16s %-14s %14s %14s\n", "Station", "JobClass", "Time", "F(t)");
5646 for (std::size_t i = 0; i < tt.size(); ++i)
5647 for (std::size_t c = 0; c < tt[i].size(); ++c)
5648 for (std::size_t j = 0; j < tt[i][c].size(); ++j)
5649 std::printf("%-16s %-14s %14.8g %14.10g\n",
5650 (i < r.station_names.size() ? r.station_names[i].c_str() : "?"),
5651 (c < r.class_names.size() ? r.class_names[c].c_str() : "?"),
5652 tt[i][c][j], ff[i][c][j]);
5653 return 0;
5654}
5655
5656/**
5657 * `-s ldes -a sample`: `sampleSys` / `sampleSysAggr`, one simulated trajectory.
5658 *
5659 * The horizon is `[0, --samples]`, which is `runTransientJson`'s: the event budget
5660 * doubles as the transient horizon there because the engine ignores the budget in
5661 * transient mode, so the number the caller gave has to name the horizon or name
5662 * nothing. `--tspan` names a horizon in its own right and belongs to `-a tran`.
5663 *
5664 * The state is the per-class queue length at each station, which is why there is
5665 * no separate `sampleSysAggr` column: an LDES trajectory is ALREADY per class, so
5666 * the aggregate view is the row sum and the reference's two getters return the
5667 * same data with one flag flipped.
5668 */
5669int solve_model_ldes_sample(const std::string& file, const Knobs& k) {
5670 line::ldes::LdesOptions o = ldes_options(k);
5671 o.has_timespan = true;
5672 o.t0 = 0.0;
5673 o.t1 = static_cast<double>(o.events ? o.events : o.samples);
5674 std::vector<std::string> extra;
5675 extra.push_back("--trajectory");
5676 const line::ldes::LdesResult r = ldes_run(file, o, extra);
5677 if (r.t.empty() || r.QNt.empty())
5678 throw line::NumericError(
5679 "SolverLDES -a sample produced no trajectory over [0," +
5680 line::ldes::detail::shortest(o.t1) + "]");
5681
5682 const std::size_t M = r.QNt.size(), K = r.class_names.size(), n = r.t.size();
5683 if (g_json_output) {
5684 line::reg::Json p = line::reg::Json::object();
5685 p["type"] = "SamplePath";
5686 p["indexBase"] = 0;
5687 p["scope"] = "(system)";
5688 p["drawn"] = n;
5689 p["t"] = vector_json(r.t);
5690 line::reg::Json st = line::reg::Json::array();
5691 for (std::size_t j = 0; j < n; ++j) {
5692 line::reg::Json row = line::reg::Json::array();
5693 for (std::size_t i = 0; i < M; ++i)
5694 for (std::size_t c = 0; c < K; ++c)
5695 row.push_back(c < r.QNt[i].size() && j < r.QNt[i][c].rows()
5696 ? r.QNt[i][c](j, 0)
5697 : 0.0);
5698 st.push_back(row);
5699 }
5700 p["state"] = st;
5701 line::reg::Json cols = line::reg::Json::array();
5702 for (std::size_t i = 0; i < M; ++i)
5703 for (std::size_t c = 0; c < K; ++c)
5704 cols.push_back((i < r.station_names.size() ? r.station_names[i] : "?") + "," +
5705 (c < K ? r.class_names[c] : "?"));
5706 p["columns"] = cols;
5707 // The envelope is built ONCE into a local: `begin()` and `end()` taken
5708 // from two different temporaries are iterators into two different
5709 // objects, which is undefined behaviour and not a style point.
5710 const line::reg::Json env = ldes_envelope(r, o);
5711 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
5712 p[it.key()] = it.value();
5713 emit_analysis<double>("sample", p, r.method);
5714 return 0;
5715 }
5716 ldes_banner(r, o);
5717 std::printf("%14s %s\n", "Time", "SysState (station-major, per class)");
5718 for (std::size_t j = 0; j < n; ++j) {
5719 std::printf("%14.8g ", r.t[j]);
5720 for (std::size_t i = 0; i < M; ++i)
5721 for (std::size_t c = 0; c < K; ++c)
5722 std::printf(" %g", c < r.QNt[i].size() && j < r.QNt[i][c].rows()
5723 ? r.QNt[i][c](j, 0)
5724 : 0.0);
5725 std::printf("\n");
5726 }
5727 return 0;
5728}
5729
5730/**
5731 * `-s ldes -a reward`: `getAvgReward`, E[r] on the EXACT joint-state histogram.
5732 *
5733 * The engine exports, under `--export-histogram`, the residence time of every
5734 * joint state it visited, in the aggregate layout `ctmc_state_space_aggr` builds.
5735 * The rewards are then evaluated here, state by state, so E[r] = sum_s (t_s /
5736 * sum t) r(state_s) is correct for a NONLINEAR reward too -- which is the whole
5737 * point of the histogram over the means: E[n^2] cannot be recovered from E[n].
5738 *
5739 * THIS IS THE ONE ARM THAT ALSO PARSES THE DOCUMENT, because a reward is a
5740 * DECLARATION and the declarations live in the model, not in the result. A model
5741 * outside this port's reader therefore reaches every other LDES arm and not this
5742 * one, and says so by the reader's own refusal.
5743 */
5744int solve_model_ldes_reward(const std::string& file, const Knobs& k) {
5745 line::qn::Network<double> net = read_model<double>(file);
5747 if (sn.reward.empty())
5748 throw line::InputError(
5749 "-s ldes -a reward needs a reward declared on the model (set_reward(name, fn), the "
5750 "`rewards` block of model.json); there is nothing to average");
5751
5752 const line::ldes::LdesOptions o = ldes_options(k);
5753 std::vector<std::string> extra;
5754 extra.push_back("--export-histogram");
5755 const line::ldes::LdesResult r = ldes_run(file, o, extra);
5757 throw line::NumericError(
5758 "SolverLDES -a reward needs the joint-state residence-time histogram the engine "
5759 "exports under --export-histogram and the run returned none");
5760
5761 double total = 0.0;
5762 for (std::size_t s = 0; s < r.histogram_time.rows(); ++s)
5763 for (std::size_t c = 0; c < r.histogram_time.cols(); ++c) total += r.histogram_time(s, c);
5764 if (!(total > 0.0))
5765 throw line::NumericError(
5766 "SolverLDES -a reward: the state histogram carries no residence time, so no state "
5767 "distribution can be formed from it");
5768
5769 const std::size_t ns = r.histogram_space.rows(), w = r.histogram_space.cols();
5770 std::vector<double> E(sn.reward.size(), 0.0);
5771 std::vector<std::string> names(sn.reward.size());
5772 for (std::size_t l = 0; l < sn.reward.size(); ++l) {
5773 names[l] = sn.reward[l].name;
5774 for (std::size_t s = 0; s < ns; ++s) {
5775 std::vector<double> row(w);
5776 for (std::size_t c = 0; c < w; ++c) row[c] = r.histogram_space(s, c);
5777 const double t = s < r.histogram_time.rows() && r.histogram_time.cols() > 0
5778 ? r.histogram_time(s, 0)
5779 : 0.0;
5780 E[l] += (t / total) * sn.reward[l].fn(row);
5781 }
5782 }
5783
5784 if (g_json_output) {
5785 line::reg::Json p = line::reg::Json::object();
5786 p["type"] = "AvgReward";
5787 line::reg::Json nm = line::reg::Json::array();
5788 for (std::size_t l = 0; l < names.size(); ++l) nm.push_back(names[l]);
5789 p["Reward"] = nm;
5790 p["E"] = vector_json(E);
5791 p["states"] = ns;
5792 // The envelope is built ONCE into a local: `begin()` and `end()` taken
5793 // from two different temporaries are iterators into two different
5794 // objects, which is undefined behaviour and not a style point.
5795 const line::reg::Json env = ldes_envelope(r, o);
5796 for (line::reg::Json::const_iterator it = env.begin(); it != env.end(); ++it)
5797 p[it.key()] = it.value();
5798 // NO "method": these are expectations over an empirical distribution and
5799 // no chain was solved, so there is no resolved method to report.
5800 emit_analysis<double>("reward", p, std::string());
5801 return 0;
5802 }
5803 ldes_banner(r, o);
5804 std::printf("%-28s %16s\n", "Reward", "E[r]");
5805 for (std::size_t l = 0; l < E.size(); ++l)
5806 std::printf("%-28s %16.10g\n", names[l].c_str(), E[l]);
5807 return 0;
5808}
5809
5810/**
5811 * The `-s ldes` entry: one analysis per getter of the reference's surface.
5812 *
5813 * `-a prob` IS REFUSED BY NAME, and the refusal is about this port's model layer
5814 * rather than about LDES. `getProb` / `getProbSys` weigh the simulated trajectory
5815 * by how long it spent in the model's CURRENT state, `sn.state{isf}` per stateful
5816 * node; `qn::NetworkStruct` carries no such row -- a Cache's `initstate`, a
5817 * Place's `initmarking` and the `stateprior`/`statespace` pair are declarations of
5818 * a different thing -- so there is no state to weigh against and any answer would
5819 * be about a state the caller never named.
5820 */
5821int solve_model_ldes(const std::string& file, const Knobs& k, const std::string& analysis) {
5822 if (analysis == "avg") return solve_model_ldes_avg(file, k);
5823 if (analysis == "tran") return solve_model_ldes_tran(file, k);
5824 // THE SAME ECDF UNDER FOUR NAMES, which is the reference's own structure and
5825 // not a shortcut here: `@@SolverLDES/getCdfRespT`, `getTranCdfRespT` and
5826 // `getTranCdfPassT` all read `respTimeSamples`, and the JAR's
5827 // getTranCdfPassT is literally `return getTranCdfRespT(R)`. A simulator
5828 // observes one per-job passage and there is no second measurement to make;
5829 // emitting the curve under the key the caller asked for is what tells them
5830 // apart, and the payload's `type` says which question it answers.
5831 if (analysis == "cdf") return solve_model_ldes_cdf(file, k, "cdf", "CdfRespT");
5832 if (analysis == "cdfpasst") return solve_model_ldes_cdf(file, k, "cdfpasst", "CdfPassT");
5833 if (analysis == "trancdf") return solve_model_ldes_cdf(file, k, "trancdf", "TranCdfRespT");
5834 if (analysis == "trancdfpasst")
5835 return solve_model_ldes_cdf(file, k, "trancdfpasst", "TranCdfPassT");
5836 if (analysis == "sample") return solve_model_ldes_sample(file, k);
5837 if (analysis == "reward") return solve_model_ldes_reward(file, k);
5838 if (analysis == "prob")
5840 "-s ldes -a prob is not ported: getProb/getProbSys weigh the trajectory by the time "
5841 "spent in the model's CURRENT state, and the C++ NetworkStruct carries no such state "
5842 "row to compare against (only a Cache initstate, a Place initmarking and the "
5843 "statePrior/space pair). Use -s ctmc -a prob for an exact marginal, or -s ssa -a prob "
5844 "for a simulated one");
5846 "SolverLDES ports -a avg (getAvg) and its four views -a node, -a sys, -a chain and "
5847 "-a nodechain, -a tran (getTranAvg), -a cdf / cdf-passt / "
5848 "tran-cdf-respt / tran-cdf-passt (the empirical passage law, one measurement under the "
5849 "four names the reference gives it), -a sample (sampleSys) and -a reward "
5850 "(getAvgReward); got '" + analysis + "'");
5851}
5852
5853std::string auto_getter_of_analysis(const std::string& analysis) {
5854 if (analysis == "prob") return "getProbSysAggr";
5855 if (analysis == "marg") return "getProbMarg";
5856 if (analysis == "sysmarg") return "getProbSysMarg";
5857 if (analysis == "normconst") return "getProbNormConstAggr";
5858 if (analysis == "tranprob") return "getTranProbSysAggr";
5859 if (analysis == "sample") return "sampleSys";
5860 if (analysis == "cdf") return "getCdfRespT";
5861 if (analysis == "gen") return "getInfGen";
5862 if (analysis == "states") return "getStateSpace";
5863 if (analysis == "reward") return "getAvgReward";
5864 if (analysis == "sens") return "getSensitivityTable";
5865 if (analysis == "tran") return "getTranAvg";
5866 if (analysis == "internals") return "getMAMResult";
5867 // `-a bounds` and `-a tranreward` deliberately keep the AvgTable getter:
5868 // `getBoundsTable` and `getTranReward` are absent from chooseSolverHeur's
5869 // own method lists, so naming them here would ask the chooser about a getter
5870 // it does not rank. Each is served by exactly one engine anyway (BA and
5871 // CTMC), which refuses by name when `-s auto` sends the run elsewhere.
5872 if (analysis == "node") return "getAvgNodeTable";
5873 return "getAvgTable";
5874}
5875
5876/**
5877 * What `-s auto` resolved to: the engines to try, in order, and the method the
5878 * first of them runs.
5879 */
5880struct AutoPlan {
5881 std::vector<std::string> order; ///< CLI solver method names, chosen first
5882 std::string method; ///< the method the chosen engine runs, "" for its default
5883 std::string note; ///< what the ranking preferred and this port cannot build
5884};
5885
5886/** The CLI method name of a method family, or a refusal naming what the family needs. */
5887std::string auto_cli_token_of_family(const std::string& fam) {
5888 if (fam == "mva" || fam == "nc" || fam == "ctmc" || fam == "mam" || fam == "ag" ||
5889 fam == "ssa" || fam == "ba" || fam == "uq" || fam == "env")
5890 return fam;
5891 if (fam == "fluid") return "fluid";
5892 if (fam == "ldes") {
5893 // The engine is not built here, it is RUN here, so the family resolves
5894 // whenever the machine has one and refuses -- naming what is missing --
5895 // when it does not, rather than diverting to a different simulator.
5898 "--method ldes names the discrete-event engine, and no engine was found beside "
5899 "this binary (common/ldes or common/ldes.jar, or $LINE_LDES_DIR); -s ssa is the "
5900 "simulator this port builds in process");
5901 return "ldes";
5902 }
5903 if (fam == "jmt") {
5904 // IT IS WRAPPED NOW. This refused the token outright until the JMT
5905 // client landed, and stayed behind: `-s jmt` drives jsim and jmva
5906 // through `solver_jmt_run_analyzer`, so refusing `--method jmt` denied
5907 // under `-s auto` what the very same binary answers under `-s jmt`.
5908 // Availability is left to the wrapper, which names what is missing (a
5909 // JVM, common/JMT.jar or a REST endpoint) rather than guessing here.
5910 return "jmt";
5911 }
5912 if (fam == "qns") return "qns";
5913 if (fam == "lqns")
5915 "--method lqns names the external layered binary, which solves a LayeredNetwork and "
5916 "not a Network; reach it as -i lqnx -s lqns");
5917 if (fam == "ln")
5919 "--method ln names the layered solver, which takes a LayeredNetwork: pass the model "
5920 "as -i lqnx -s ln rather than as a Network");
5921 throw line::InputError("SolverAUTO: no engine stands behind method family '" + fam + "'");
5922}
5923
5924/**
5925 * Is this model.json an Environment envelope rather than a Network?
5926 *
5927 * `-s auto` has to know before it parses: the two readers take different
5928 * documents, and the reference's chooser has an Environment arm that is
5929 * unreachable if every auto run is assumed to hold a Network. A malformed
5930 * document answers false, so the real reader reports the parse error.
5931 */
5932bool model_is_environment(const std::string& file) {
5933 try {
5934 line::io::detail::json root;
5935 if (file.empty()) {
5936 std::istringstream in(stdin_model_text());
5937 in >> root;
5938 } else {
5939 std::ifstream in(file.c_str());
5940 if (!in) return false;
5941 in >> root;
5942 }
5943 // The envelope may wrap the model, exactly as build_environment_from_json
5944 // unwraps it; `type` is what that reader keys on, so this reads the same
5945 // field rather than a second convention of its own.
5946 if (!root.is_object()) return false;
5947 const line::io::detail::json& model = root.contains("model") ? root.at("model") : root;
5948 return model.is_object() && model.value("type", std::string()) == "Environment";
5949 } catch (...) {
5950 return false;
5951 }
5952}
5953
5954/**
5955 * `-s auto`: which engine answers, by `chooseSolver.m` through solver_auto.h.
5956 *
5957 * THREE THINGS DECIDE, in the reference's own order. `--method` is resolved
5958 * first, because a method FAMILY ('nc', 'nc.comom') names the engine outright
5959 * and bypasses every ranking, while a selection INTENT ('exact', 'sim', 'fast',
5960 * 'accurate') picks which ranking runs. Then the model class: an Environment
5961 * envelope takes the Environment arm, a Network the Network one. Then the
5962 * GETTER the caller asked for, since the reference keys the ranking on the
5963 * metric family and not on the model alone.
5964 *
5965 * THE ORDER IS A RETRY LIST, not a single name. `delegate.m` tries the chosen
5966 * solver and then every feasible candidate, so a refusal moves to the next
5967 * engine instead of ending the run; the caller sees which one answered.
5968 *
5969 * The choice reads structure only -- traits, feature sets, product form,
5970 * populations -- so it is made in the arithmetic the run will use and costs one
5971 * extra parse of the model, nothing more.
5972 */
5973template <class T>
5974AutoPlan choose_auto_plan(const std::string& file, const std::string& analysis,
5975 const std::string& method_token) {
5977 AutoPlan plan;
5978 if (!tok.is_intent) {
5979 plan.order.push_back(auto_cli_token_of_family(tok.family));
5980 if (tok.submethod != "default") plan.method = tok.submethod;
5981 return plan;
5982 }
5983
5984 const std::string getter = auto_getter_of_analysis(analysis);
5985 if (model_is_environment(file)) {
5988 plan.order.push_back("env");
5989 for (std::size_t i = 0; i < ec.skipped.size(); ++i)
5990 plan.note += std::string(i ? ", " : " (the ranking preferred ") +
5992 if (!ec.skipped.empty()) plan.note += ", which this port does not build)";
5993 return plan;
5994 }
5995
5996 line::qn::Network<T> net = read_model<T>(file);
5999 plan.method = c.method;
6000 const std::vector<line::autosolver::AutoSolver> proposed =
6002 for (std::size_t i = 0; i < proposed.size(); ++i)
6003 plan.order.push_back(line::autosolver::auto_solver_name(proposed[i]));
6004 for (std::size_t i = 0; i < c.skipped.size(); ++i)
6005 plan.note += std::string(i ? ", " : " (the ranking preferred ") +
6007 if (!c.skipped.empty()) plan.note += ", which this port does not build)";
6008 return plan;
6009}
6010
6011AutoPlan choose_auto_plan_dispatch(const std::string& arith, const std::string& file,
6012 const std::string& analysis, const std::string& method_token) {
6013 if (arith == "exact") return choose_auto_plan<line::Rational>(file, analysis, method_token);
6014 if (arith == "real:16") return choose_auto_plan<line::Real<16> >(file, analysis, method_token);
6015 if (arith == "real" || arith == "real:32")
6016 return choose_auto_plan<line::Real<32> >(file, analysis, method_token);
6017 if (arith == "real:64") return choose_auto_plan<line::Real<64> >(file, analysis, method_token);
6018 if (arith == "real:128")
6019 return choose_auto_plan<line::Real<128> >(file, analysis, method_token);
6020 if (arith == "real:256")
6021 return choose_auto_plan<line::Real<256> >(file, analysis, method_token);
6022 return choose_auto_plan<double>(file, analysis, method_token);
6023}
6024
6025/**
6026 * The numeric clean-up SolverLN.getAvgTable applies before printing.
6027 *
6028 * It is reproduced here because the reference's reported table IS the
6029 * sanitized one, so a row-by-row comparison against it has to compare like
6030 * with like. Two rules: snap a value to one decimal place when it is already
6031 * within CoarseTol of it relatively, and snap anything at or below FineTol to
6032 * zero. The second rule is what turns the residual queue length of a chain of
6033 * Immediate classes -- of order 1e-8 by construction, since Immediate has rate
6034 * 1e8 -- into the exact zero the reference prints.
6035 */
6036double ln_sanitize(double x) {
6037 const double r = std::round(x * 10.0);
6038 if (std::fabs(x * 10.0 - r) < line::lang::GlobalConstants::CoarseTol * x * 10.0) x = r / 10.0;
6039 if (x <= line::lang::GlobalConstants::FineTol) x = 0.0;
6040 return x;
6041}
6042
6043const char* ln_element_kind(const line::lqn::LqnStruct<double>& l, std::size_t i) {
6044 switch (l.type[i]) {
6045 case line::lang::LqnElement::HOST: return "Processor";
6046 case line::lang::LqnElement::TASK: return l.isref[i] ? "RefTask" : "Task";
6047 case line::lang::LqnElement::ENTRY: return "Entry";
6048 default: return "Activity";
6049 }
6050}
6051
6052/**
6053 * Print every layer's stations, classes and routing, in a form a MATLAB dump of
6054 * `solver.ensemble{k}` can be diffed against line by line.
6055 *
6056 * A layered result that is close but not equal across codebases is almost never
6057 * a difference in the MVA call; it is a layer that was BUILT differently -- a
6058 * class that is present in one and not the other, a population, a routing
6059 * probability. Comparing the final AvgTable cannot tell those apart, so the
6060 * structure has to be observable directly.
6061 */
6062template <class T>
6063void ln_dump_layers(const line::ln::SolverLN<T>& solver) {
6064 using namespace line;
6065 const std::vector<qn::Layer<T> >& ens = solver.layers();
6066 for (std::size_t k = 0; k < ens.size(); ++k) {
6067 const qn::Layer<T>& L = ens[k];
6068 std::printf("LAYER %zu %s nstations=%zu nclasses=%zu nchains=%zu\n", k + 1, L.name.c_str(),
6069 L.stations.size(), L.classes.size(), L.nchains);
6070 for (std::size_t i = 0; i < L.stations.size(); ++i)
6071 std::printf(" STATION %zu %s sched=%s nservers=%g\n", i + 1, L.stations[i].name.c_str(),
6072 lang::sched_to_text(L.stations[i].sched), L.stations[i].nservers);
6073 for (std::size_t r = 0; r < L.classes.size(); ++r)
6074 std::printf(" CLASS %zu %s pop=%.17g refstat=%zu completes=%d\n", r + 1,
6075 L.classes[r].name.c_str(), L.classes[r].population, L.classes[r].refstat,
6076 int(L.classes[r].completes));
6077 for (std::size_t i = 0; i < L.stations.size(); ++i)
6078 for (std::size_t r = 0; r < L.classes.size(); ++r) {
6079 if (L.disabled.empty() || L.disabled[i][r]) continue;
6080 std::printf(" RATE %s %s %.17g scv=%.17g\n", L.stations[i].name.c_str(),
6081 L.classes[r].name.c_str(), num_traits<T>::to_double(L.rates(i, r)),
6082 num_traits<T>::to_double(L.scv(i, r)));
6083 }
6084 for (const auto& kv : L.P) {
6085 const Matrix<T>& B = kv.second;
6086 for (std::size_t i = 0; i < B.rows(); ++i)
6087 for (std::size_t j = 0; j < B.cols(); ++j) {
6088 const double p = num_traits<T>::to_double(B(i, j));
6089 if (p == 0.0) continue;
6090 std::printf(" ROUTE %s->%s %s->%s %.17g\n",
6091 L.classes[kv.first.first - 1].name.c_str(),
6092 L.classes[kv.first.second - 1].name.c_str(),
6093 L.nodes[i].name.c_str(), L.nodes[j].name.c_str(), p);
6094 }
6095 }
6096 }
6097}
6098
6099/**
6100 * Solve a .lqnx layered queueing network with SolverLN and print its AvgTable.
6101 *
6102 * The output is one row per LQN element, with its queue length, utilization,
6103 * response time, residence time and throughput, in the same element order as
6104 * MATLAB's getAvgTable, so a row-by-row numeric comparison against the
6105 * reference is a plain diff.
6106 *
6107 * `--repeat` re-runs the whole solve K times and reports the best wall-clock
6108 * time, which is what the arithmetic-backend benchmark reads.
6109 */
6110/** The LnOptions a set of CLI knobs describes; arithmetic-independent. */
6111inline line::ln::LnOptions ln_options_from(const Knobs& k) {
6112 using namespace line;
6113 // The reference's own LnOptions defaults stand unless the caller overrode
6114 // them; the CLI does not restate them, so an untouched knob keeps whatever
6115 // SolverLN itself considers default.
6117 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
6118 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
6119 if (k.no_interlocking) opt.interlocking = false;
6120 if (!k.layer_solver.empty()) opt.layer_solver = k.layer_solver;
6121 if (!k.method.empty()) opt.method = k.method;
6122 if (k.samples) opt.layer_ssa.samples = k.samples;
6123 if (k.seed) opt.layer_ssa.seed = k.seed;
6124 if (k.t1 >= 0.0) opt.timespan_end = k.t1;
6125 if (k.tran_points) opt.tran_points = k.tran_points;
6126 if (!k.ln_transient.empty()) opt.ln_transient = k.ln_transient;
6127 if (!k.ln_transient_channels.empty()) opt.ln_transient_channels = k.ln_transient_channels;
6128 return opt;
6129}
6130
6131/** The engine name the banner reports, which is never a hardcoded MVA. */
6132inline const char* ln_layer_engine_name(const std::string& layer_solver) {
6133 if (layer_solver == "fluid") return "Fluid";
6134 if (layer_solver == "nc") return "NC";
6135 if (layer_solver == "ssa") return "SSA";
6136 return "MVA";
6137}
6138
6139/**
6140 * `-a tran`: the layered transient, one block of time series per layer.
6141 *
6142 * The blocks are printed rather than the block-diagonal cell array the
6143 * reference assembles, because the off-diagonal blocks of that array are empty
6144 * by construction and each layer keeps its own grid.
6145 */
6146template <class T>
6147int run_ln_tran(const std::string& file, const std::string& output, const Knobs& k) {
6148 using namespace line;
6149 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
6150 ln::LnOptions opt = ln_options_from(k);
6151 ln::SolverLN<T> solver(model, opt);
6152 const ln::LnTranSolution tr = solver.get_tran_avg();
6153 const std::vector<qn::Layer<T>>& layers = solver.layers();
6154
6155 if (output == "json") {
6156 reg::Json j = reg::Json::object();
6157 j["model"] = file;
6158 j["arith"] = num_traits<T>::name();
6159 j["mode"] = tr.mode;
6160 j["iterations"] = tr.iterations;
6161 j["gap"] = ln_sanitize(tr.gap);
6162 reg::Json ls = reg::Json::array();
6163 for (std::size_t e = 0; e < tr.layers.size(); ++e) {
6164 reg::Json le = reg::Json::object();
6165 le["layer"] = layers[e].name;
6166 reg::Json tt = reg::Json::array();
6167 for (double t : tr.layers[e].t) tt.push_back(ln_sanitize(t));
6168 le["t"] = tt;
6169 reg::Json series = reg::Json::array();
6170 for (std::size_t i = 0; i < tr.layers[e].QN.size(); ++i)
6171 for (std::size_t r = 0; r < tr.layers[e].QN[i].size(); ++r) {
6172 reg::Json s = reg::Json::object();
6173 s["station"] = layers[e].stations[i].name;
6174 s["class"] = layers[e].classes[r].name;
6175 auto arr = [&](const std::vector<double>& v) {
6176 reg::Json a = reg::Json::array();
6177 for (double x : v) a.push_back(ln_sanitize(x));
6178 return a;
6179 };
6180 s["QLen"] = arr(tr.layers[e].QN[i][r]);
6181 s["Util"] = arr(tr.layers[e].UN[i][r]);
6182 s["Tput"] = arr(tr.layers[e].TN[i][r]);
6183 series.push_back(s);
6184 }
6185 le["series"] = series;
6186 ls.push_back(le);
6187 }
6188 j["layers"] = ls;
6189 std::printf("%s\n", j.dump(1).c_str());
6190 return 0;
6191 }
6192
6193 std::printf("SolverLN(Solver%s) getTranAvg arith=%s mode=%s layers=%zu iterations=%ld gap=%.3e\n",
6194 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), tr.mode.c_str(),
6195 tr.layers.size(), tr.iterations, tr.gap);
6196 for (std::size_t e = 0; e < tr.layers.size(); ++e) {
6197 const std::vector<double>& t = tr.layers[e].t;
6198 if (t.empty()) continue;
6199 std::printf("\nLayer %s (%zu points on [%.6g, %.6g])\n", layers[e].name.c_str(), t.size(),
6200 t.front(), t.back());
6201 std::printf("%-30s %-24s %12s %12s %12s %12s\n", "Station", "JobClass", "QLen(0)",
6202 "QLen(end)", "Util(end)", "Tput(end)");
6203 for (std::size_t i = 0; i < tr.layers[e].QN.size(); ++i)
6204 for (std::size_t r = 0; r < tr.layers[e].QN[i].size(); ++r) {
6205 const std::vector<double>& q = tr.layers[e].QN[i][r];
6206 if (q.empty()) continue;
6207 std::printf("%-30s %-24s %12.6g %12.6g %12.6g %12.6g\n",
6208 layers[e].stations[i].name.c_str(), layers[e].classes[r].name.c_str(),
6209 ln_sanitize(q.front()), ln_sanitize(q.back()),
6210 ln_sanitize(tr.layers[e].UN[i][r].back()),
6211 ln_sanitize(tr.layers[e].TN[i][r].back()));
6212 }
6213 }
6214 return 0;
6215}
6216
6217/** `-a sens`: the layer sensitivity tables under a leading Layer column. */
6218template <class T>
6219int run_ln_sens(const std::string& file, const std::string& output, const Knobs& k) {
6220 using namespace line;
6221 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
6222 ln::LnOptions opt = ln_options_from(k);
6223 ln::SolverLN<T> solver(model, opt);
6225 if (!k.sens_method.empty()) so.method = k.sens_method;
6226 if (!k.sens_scheme.empty()) so.scheme = k.sens_scheme;
6227 if (k.sens_step > 0.0) so.step = k.sens_step;
6228 const ln::LnSensTable<T> tbl = solver.get_sensitivity_table(so);
6229
6230 if (output == "json") {
6231 reg::Json j = reg::Json::object();
6232 j["model"] = file;
6233 j["arith"] = num_traits<T>::name();
6234 j["method"] = tbl.method;
6235 reg::Json rows = reg::Json::array();
6236 for (const auto& r : tbl.rows) {
6237 reg::Json o = reg::Json::object();
6238 o["Layer"] = r.layer;
6239 o["Station"] = r.station;
6240 o["JobClass"] = r.jobclass;
6241 o["dTput_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dTput));
6242 o["dRespT_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dRespT));
6243 o["dQLen_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dQLen));
6244 o["dUtil_dRate"] = sens_sanitize_signed(num_traits<T>::to_double(r.dUtil));
6245 rows.push_back(o);
6246 }
6247 j["rows"] = rows;
6248 std::printf("%s\n", j.dump(1).c_str());
6249 return 0;
6250 }
6251
6252 std::printf("SolverLN(Solver%s) getSensitivityTable arith=%s method=%s rows=%zu\n",
6253 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), tbl.method.c_str(),
6254 tbl.rows.size());
6255 std::printf("%-28s %-28s %-20s %14s %14s %14s %14s\n", "Layer", "Station", "JobClass",
6256 "dTput_dRate", "dRespT_dRate", "dQLen_dRate", "dUtil_dRate");
6257 for (const auto& r : tbl.rows)
6258 std::printf("%-28s %-28s %-20s %14.6g %14.6g %14.6g %14.6g\n", r.layer.c_str(),
6259 r.station.c_str(), r.jobclass.c_str(),
6260 sens_sanitize_signed(num_traits<T>::to_double(r.dTput)),
6261 sens_sanitize_signed(num_traits<T>::to_double(r.dRespT)),
6262 sens_sanitize_signed(num_traits<T>::to_double(r.dQLen)),
6263 sens_sanitize_signed(num_traits<T>::to_double(r.dUtil)));
6264 return 0;
6265}
6266
6267/** `-a cdf`: the per-entry response-time distribution of the moment3 method. */
6268template <class T>
6269int run_ln_cdf(const std::string& file, const std::string& output, const Knobs& k) {
6270 using namespace line;
6271 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
6272 ln::LnOptions opt = ln_options_from(k);
6273 // getCdfRespT.m runs the ensemble under moment3 whatever the caller asked
6274 // for, and restores the method afterwards: the mean-based update forms no
6275 // distribution at all, so there is nothing else to report.
6276 opt.method = "moment3";
6277 ln::SolverLN<T> solver(model, opt);
6278 const std::vector<ln::LnCdf> cdf = solver.get_cdf_respt();
6280
6281 if (output == "json") {
6282 reg::Json j = reg::Json::object();
6283 j["model"] = file;
6284 j["arith"] = num_traits<T>::name();
6285 reg::Json rows = reg::Json::array();
6286 for (std::size_t e = 1; e <= model.nentries && e < cdf.size(); ++e) {
6287 reg::Json o = reg::Json::object();
6288 o["entry"] = names.names[model.eshift + e];
6289 reg::Json tt = reg::Json::array(), ff = reg::Json::array();
6290 for (std::size_t p = 0; p < cdf[e].t.size(); ++p) {
6291 tt.push_back(ln_sanitize(cdf[e].t[p]));
6292 ff.push_back(ln_sanitize(cdf[e].cdf[p]));
6293 }
6294 o["t"] = tt;
6295 o["F"] = ff;
6296 rows.push_back(o);
6297 }
6298 j["entries"] = rows;
6299 std::printf("%s\n", j.dump(1).c_str());
6300 return 0;
6301 }
6302
6303 std::printf("SolverLN(Solver%s) getCdfRespT arith=%s method=moment3 entries=%zu\n",
6304 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(), model.nentries);
6305 for (std::size_t e = 1; e <= model.nentries && e < cdf.size(); ++e) {
6306 if (cdf[e].t.empty()) {
6307 std::printf("%-40s (no distribution: the entry has no fitted term)\n",
6308 names.names[model.eshift + e].c_str());
6309 continue;
6310 }
6311 // Quartiles, which is what a CDF is read for; the full grid goes to JSON.
6312 auto quantile = [&](double p) {
6313 for (std::size_t i = 0; i < cdf[e].cdf.size(); ++i)
6314 if (cdf[e].cdf[i] >= p) return cdf[e].t[i];
6315 return cdf[e].t.back();
6316 };
6317 std::printf("%-40s p25=%12.6g p50=%12.6g p75=%12.6g p95=%12.6g points=%zu\n",
6318 names.names[model.eshift + e].c_str(), ln_sanitize(quantile(0.25)),
6319 ln_sanitize(quantile(0.50)), ln_sanitize(quantile(0.75)),
6320 ln_sanitize(quantile(0.95)), cdf[e].t.size());
6321 }
6322 return 0;
6323}
6324
6325template <class T>
6326int run_ln(const std::string& file, const std::string& output, const Knobs& k) {
6327 using namespace line;
6328 const lqn::LqnStruct<T> model = io::read_layered_model<T>(file);
6329
6330 ln::LnOptions opt = ln_options_from(k);
6331 const int repeat = k.repeat > 0 ? k.repeat : 1;
6332
6333 double best = 1e300;
6335 std::size_t nlayers = 0;
6336 for (int rep = 0; rep < repeat; ++rep) {
6337 const auto t0 = std::chrono::steady_clock::now();
6338 ln::SolverLN<T> solver(model, opt);
6339 sol = solver.get_ensemble_avg();
6340 nlayers = solver.nlayers();
6341 if (output == "layers") {
6342 ln_dump_layers(solver);
6343 return 0;
6344 }
6345 const auto t1 = std::chrono::steady_clock::now();
6346 best = std::min(best, std::chrono::duration<double>(t1 - t0).count());
6347 }
6348
6349 // element names and kinds are arithmetic-independent, so read them once
6351
6352 if (output == "json") {
6353 reg::Json j = reg::Json::object();
6354 j["model"] = file;
6355 j["arith"] = num_traits<T>::name();
6356 j["layers"] = nlayers;
6357 j["iterations"] = sol.iterations;
6358 j["converged"] = sol.converged;
6359 j["seconds"] = best;
6360 reg::Json rows = reg::Json::array();
6361 for (std::size_t i = 1; i <= model.nidx; ++i) {
6362 reg::Json r = reg::Json::object();
6363 r["node"] = names.names[i];
6364 r["type"] = ln_element_kind(names, i);
6365 auto put = [&](const char* key, const std::vector<T>& v, const std::vector<bool>& d) {
6366 if (d[i]) r[key] = ln_sanitize(num_traits<T>::to_double(v[i]));
6367 else if (sol.is_bound) r[key] = 0.0; // see the note in the table below
6368 else r[key] = nullptr;
6369 };
6370 put("QLen", sol.QN, sol.defined_Q);
6371 put("Util", sol.UN, sol.defined_U);
6372 put("RespT", sol.RN, sol.defined_R);
6373 put("ResidT", sol.WN, sol.defined_W);
6374 put("Tput", sol.TN, sol.defined_T);
6375 rows.push_back(r);
6376 }
6377 j["rows"] = rows;
6378 std::printf("%s\n", j.dump(1).c_str());
6379 return 0;
6380 }
6381
6382 // The banner names the LAYER solver actually used, not a hardcoded MVA: the
6383 // two converge to different fixed points, so a reader (or a parity row) that
6384 // cannot tell them apart is reading numbers it cannot attribute.
6385 std::printf(
6386 "SolverLN(Solver%s) arith=%s type=%s layers=%zu iterations=%d converged=%d time=%.4fs\n",
6387 ln_layer_engine_name(opt.layer_solver), num_traits<T>::name(),
6388 line::util::method_type("LN", opt.method).c_str(), nlayers, sol.iterations,
6389 int(sol.converged), best);
6390 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
6391 "RespT", "ResidT", "Tput");
6392 for (std::size_t i = 1; i <= model.nidx; ++i) {
6393 // NaN IS THE UNDEFINED MARKER EVERYWHERE EXCEPT UNDER A BOUND. MATLAB's
6394 // getAvgTable prints NaN for a measure the element does not have (a
6395 // processor has no queue length), and this reproduces that. A BOUND is
6396 // the one case where the reference prints 0 instead: `mwba.*` defines
6397 // throughput and processor utilization only, and both MATLAB and the JAR
6398 // report the rest as zero rather than as absent (the JAR maps the NaN
6399 // explicitly, SolverLN.java:3294-3299). The `defined_*` flags still say
6400 // undefined to any caller of the API; only the printed table follows the
6401 // reference, so that a numeric parity row compares like with like.
6402 //
6403 // This table is for a human; a cross-codebase comparison must read the
6404 // `-o json` above, which carries the raw double, and quantize it itself.
6405 auto fmt = [&](const std::vector<T>& v, const std::vector<bool>& d, char* buf) {
6406 if (!d[i] && sol.is_bound) std::snprintf(buf, 24, "%12.6g", 0.0);
6407 else if (!d[i]) std::snprintf(buf, 24, "%12s", "NaN");
6408 else std::snprintf(buf, 24, "%12.6g", ln_sanitize(num_traits<T>::to_double(v[i])));
6409 };
6410 char q[24], u[24], rr[24], w[24], t[24];
6411 fmt(sol.QN, sol.defined_Q, q);
6412 fmt(sol.UN, sol.defined_U, u);
6413 fmt(sol.RN, sol.defined_R, rr);
6414 fmt(sol.WN, sol.defined_W, w);
6415 fmt(sol.TN, sol.defined_T, t);
6416 std::printf("%-62s %-10s %s %s %s %s %s\n", names.names[i].c_str(),
6417 ln_element_kind(names, i), q, u, rr, w, t);
6418 }
6419 return 0;
6420}
6421
6422/**
6423 * `-s ldes`: the layered model simulated directly, by the IN-PROCESS engine.
6424 *
6425 * IT IS NOT THE SUBPROCESS `-s ldes` OF THE FLAT PATH. That arm hands a
6426 * `model.json` to `common/ldes`, and the engine behind that wire refuses a
6427 * layered document outright ("LDES currently supports Network models only"), so
6428 * there is nothing to forward. This arm calls `ldes_ln_engine_solve` in process
6429 * -- the C++ twin of `Solver_ssj_ln.java` -- which simulates entries,
6430 * activities, task threads and synchronous calls directly instead of
6431 * decomposing the model into layers. So it is not a noisier route to `-s ln`:
6432 * SolverLN's decomposition is an APPROXIMATION and this is a sample path of the
6433 * model itself, which is what makes it the reference the layered solvers are
6434 * checked against.
6435 *
6436 * THE NaN MASK OF THE LAYERED TABLE IS PART OF THE ANSWER, and it belongs to
6437 * the table rather than to whichever solver filled it: a processor has no queue
6438 * length, a task no response time, an entry no residence, and `-s ln` and
6439 * `-s lqns` both print NaN there. This engine MEASURES more than that -- a
6440 * processor's completion rate is sitting in `LnResult::TLN` -- and printing it
6441 * under a column the other two arms leave empty would make one column mean
6442 * different things depending on who filled it. That is the divergence the JAR
6443 * removed from `getLNAvgTable` on 2026-08-21, and masking here rather than in
6444 * the engine keeps it removed on this side too. Nothing is discarded: the
6445 * unmasked measurements stay on `LnResult` for a programmatic caller, and only
6446 * the shared table is masked, so that it can be diffed row by row.
6447 *
6448 * THE NUMBERS ARE NOT `ln_sanitize`d, for the reason `run_lqns` states below:
6449 * that helper snaps to a tenth and floors at FineTol, which is right for a
6450 * fixed point this port computed and wrong for a measurement it made. A
6451 * simulated utilization of 1e-9 is a rare event that was observed, not a
6452 * negligible residue of an iteration, and the JAR's own layered LDES table
6453 * reports the raw estimate too -- so snapping here would put the two out of
6454 * step on exactly the models a parity row is read on.
6455 */
6456/**
6457 * `-i lqnx -s ldes -a cdf`: the SIMULATED response time distribution per entry.
6458 *
6459 * The measured counterpart of `run_ln_cdf`, which fits an APH to three moments of
6460 * a fluid passage time and convolves. Here the engine timed every invocation from
6461 * the instant the request reached the entry to its reply -- the interval `RLN`
6462 * averages -- so this law's mean reproduces that row and its tail is observed
6463 * rather than extrapolated.
6464 */
6465int run_ln_ldes_cdf(const std::string& file, const std::string& output, const Knobs& k) {
6466 using namespace line;
6469 if (k.samples) o.samples = k.samples;
6470 if (k.seed) o.seed = static_cast<long>(k.seed);
6471
6473
6474 // The per-entry ecdf, through the same API function the other codebases
6475 // call getCdfRespTLN.
6476 const std::vector<ldes::engine::LnEntryCdf> cdf =
6478 std::vector<std::vector<double> > tt(model.nentries), ff(model.nentries);
6479 for (std::size_t e = 0; e < model.nentries; ++e) {
6480 tt[e] = cdf[e].t;
6481 ff[e] = cdf[e].F;
6482 }
6483
6484 if (output == "json") {
6485 reg::Json j = reg::Json::object();
6486 j["model"] = file;
6487 j["engine"] = "native-ln";
6488 reg::Json rows = reg::Json::array();
6489 for (std::size_t e = 0; e < model.nentries; ++e) {
6490 reg::Json ob = reg::Json::object();
6491 ob["entry"] = model.names[model.eshift + e + 1];
6492 reg::Json at = reg::Json::array(), af = reg::Json::array();
6493 for (std::size_t i = 0; i < tt[e].size(); ++i) {
6494 at.push_back(tt[e][i]);
6495 af.push_back(ff[e][i]);
6496 }
6497 ob["t"] = at;
6498 ob["F"] = af;
6499 ob["observations"] = static_cast<double>(
6500 e < r.entry_resp_samples.size() ? r.entry_resp_samples[e].size() : 0);
6501 rows.push_back(ob);
6502 }
6503 j["entries"] = rows;
6504 std::printf("%s\n", j.dump(1).c_str());
6505 return 0;
6506 }
6507
6508 std::printf("SolverLDES(native LN engine) getCdfRespT entries=%zu\n", model.nentries);
6509 for (std::size_t e = 0; e < model.nentries; ++e) {
6510 if (tt[e].empty()) {
6511 std::printf("%-40s (no observation)\n", model.names[model.eshift + e + 1].c_str());
6512 continue;
6513 }
6514 // Quartiles plus p95, which is what a measured law is read for.
6515 struct Q {
6516 const std::vector<double>& t;
6517 const std::vector<double>& f;
6518 double operator()(double p) const {
6519 for (std::size_t i = 0; i < f.size(); ++i)
6520 if (f[i] >= p) return t[i];
6521 return t.back();
6522 }
6523 } q = {tt[e], ff[e]};
6524 std::printf("%-40s p25=%12.6g p50=%12.6g p75=%12.6g p95=%12.6g n=%zu\n",
6525 model.names[model.eshift + e + 1].c_str(), q(0.25), q(0.50), q(0.75),
6526 q(0.95), r.entry_resp_samples[e].size());
6527 }
6528 return 0;
6529}
6530
6531int run_ln_ldes(const std::string& file, const std::string& output, const Knobs& k) {
6532 using namespace line;
6534
6535 // The engine reads THREE settings and no more (`o.samples`, `o.events`,
6536 // `o.seed`); the dispatcher refuses the rest of the --ldes-* family rather
6537 // than letting this function drop them silently.
6539 if (k.samples) o.samples = k.samples;
6540 if (k.seed) o.seed = static_cast<long>(k.seed);
6541
6542 // --repeat is a TIMING loop and stays honest here only because the stream is
6543 // seeded: every replication of one command walks the same sample path, so
6544 // the table is that run's table and `time=` is the only thing that varies.
6545 // This is why -s lqns refuses the flag and this arm does not -- lqsim seeds
6546 // itself, so re-running it would report different numbers under one banner.
6547 const int repeat = k.repeat > 0 ? k.repeat : 1;
6548 double best = 1e300;
6550 for (int rep = 0; rep < repeat; ++rep) {
6551 const auto t0 = std::chrono::steady_clock::now();
6552 r = ldes::ldes_ln_engine_solve(model, o);
6553 const auto t1 = std::chrono::steady_clock::now();
6554 best = std::min(best, std::chrono::duration<double>(t1 - t0).count());
6555 }
6556
6557 // The mask lives beside the data it masks (`ldes::engine::ln_defined`), so
6558 // that the doctest suite can pin it against `LnSolution::defined_*` without
6559 // reaching into this file. Spelling it here instead would put the rule that
6560 // decides what the table MEANS inside a printer.
6561 typedef ldes::engine::LnColumn Col;
6562 const auto def = [&](std::size_t i, Col c) {
6563 return ldes::engine::ln_defined(model, r, i, c);
6564 };
6565
6566 if (output == "json") {
6567 reg::Json j = reg::Json::object();
6568 j["model"] = file;
6569 j["arith"] = "double";
6570 j["solver"] = "ldes";
6571 // WHICH ENGINE ANSWERED, on the same grounds the flat arm's `engine=` is
6572 // not decoration: `-s ldes` names two different simulators depending on
6573 // whether the model is layered, and a number quoted from one must not be
6574 // read as the other's.
6575 j["engine"] = "native-ln";
6576 j["samples"] = o.samples;
6577 j["seed"] = o.seed;
6578 j["simulatedTime"] = r.simulated_time;
6579 j["completions"] = r.completions;
6580 j["seconds"] = best;
6581 reg::Json rows = reg::Json::array();
6582 for (std::size_t i = 1; i <= model.nidx; ++i) {
6583 reg::Json row = reg::Json::object();
6584 row["node"] = model.names[i];
6585 row["type"] = ln_element_kind(model, i);
6586 auto put = [&](const char* key, double v, bool defined) {
6587 if (defined) row[key] = v;
6588 else row[key] = nullptr;
6589 };
6590 put("QLen", r.QLN(i, 0), def(i, Col::QLen));
6591 put("Util", r.ULN(i, 0), def(i, Col::Util));
6592 put("RespT", r.RLN(i, 0), def(i, Col::RespT));
6593 put("ResidT", r.WLN(i, 0), def(i, Col::ResidT));
6594 put("Tput", r.TLN(i, 0), def(i, Col::Tput));
6595 rows.push_back(row);
6596 }
6597 j["rows"] = rows;
6598 std::printf("%s\n", j.dump(1).c_str());
6599 return 0;
6600 }
6601
6602 std::printf("SolverLDES(native LN engine) arith=double type=%s samples=%zu seed=%ld "
6603 "simtime=%.6g completions=%lld time=%.4fs\n",
6604 line::util::method_type("LDES", o.method).c_str(), o.samples, o.seed,
6605 r.simulated_time, r.completions, best);
6606 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
6607 "RespT", "ResidT", "Tput");
6608 for (std::size_t i = 1; i <= model.nidx; ++i) {
6609 // Six digits, as on the two tables around it, so the three arms diff.
6610 auto fmt = [&](double v, bool defined, char* buf) {
6611 if (!defined) std::snprintf(buf, 24, "%12s", "NaN");
6612 else std::snprintf(buf, 24, "%12.6g", v);
6613 };
6614 char q[24], u[24], rr[24], w[24], t[24];
6615 fmt(r.QLN(i, 0), def(i, Col::QLen), q);
6616 fmt(r.ULN(i, 0), def(i, Col::Util), u);
6617 fmt(r.RLN(i, 0), def(i, Col::RespT), rr);
6618 fmt(r.WLN(i, 0), def(i, Col::ResidT), w);
6619 fmt(r.TLN(i, 0), def(i, Col::Tput), t);
6620 std::printf("%-62s %-10s %s %s %s %s %s\n", model.names[i].c_str(),
6621 ln_element_kind(model, i), q, u, rr, w, t);
6622 }
6623 return 0;
6624}
6625
6626/**
6627 * `-s lqns`: the same layered model, solved by the external binary.
6628 *
6629 * The table has the SAME columns as run_ln's so that the two can be diffed row
6630 * by row, but the numbers are not sanitized the same way: `ln_sanitize` also
6631 * snaps anything at or below FineTol to zero, which is right for a fixed point
6632 * this port computed and wrong for a measurement it did not -- lqns reports no
6633 * residence time and no arrival rate at all, and a zero there would read as a
6634 * computed zero. Those two columns print NaN, and only the snap-to-tenth of the
6635 * reference's getAvgTable is applied.
6636 */
6637template <class T>
6638int run_lqns(const std::string& file, const std::string& output, const Knobs& k) {
6639 using namespace line;
6640 const lqn::LqnModel<T> model = lqn::read_lqnx_model<T>(file);
6641
6643 if (!k.method.empty()) opt.method = k.method;
6644 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
6645 if (k.samples) opt.samples = static_cast<double>(k.samples);
6646 opt.verbose = k.verbose;
6647 opt.keep = k.keep;
6648 opt.remote = k.remote;
6649 if (!k.remote_url.empty()) opt.remote_url = k.remote_url;
6650 opt.timeout_seconds = k.timeout_seconds;
6651
6652 lqns::SolverLQNS<T> solver(model, opt);
6653 const lqns::LqnsSolution<T> sol = solver.get_ensemble_avg();
6654 const lqn::LqnStruct<T>& sn = solver.get_struct();
6656
6657 if (output == "json") {
6658 reg::Json j = reg::Json::object();
6659 j["model"] = file;
6660 j["arith"] = num_traits<T>::name();
6661 j["solver"] = lqns::SolverLQNS<T>::is_stochastic_method(opt.method) ? "lqsim" : "lqns";
6662 j["method"] = opt.method;
6663 j["iterations"] = sol.iterations;
6664 j["seconds"] = solver.runtime();
6665 reg::Json rows = reg::Json::array();
6666 for (std::size_t i = 1; i <= sn.nidx; ++i) {
6667 reg::Json r = reg::Json::object();
6668 r["node"] = names.names[i];
6669 r["type"] = ln_element_kind(names, i);
6670 auto put = [&](const char* key, const std::vector<T>& v, const std::vector<bool>& d) {
6671 if (d[i]) r[key] = lqns::detail::snap_to_tenth(num_traits<T>::to_double(v[i]));
6672 else r[key] = nullptr;
6673 };
6674 put("QLen", sol.QN, sol.defined_Q);
6675 put("Util", sol.UN, sol.defined_U);
6676 put("RespT", sol.RN, sol.defined_R);
6677 put("ResidT", sol.WN, sol.defined_W);
6678 put("Tput", sol.TN, sol.defined_T);
6679 rows.push_back(r);
6680 }
6681 j["rows"] = rows;
6682 std::printf("%s\n", j.dump(1).c_str());
6683 return 0;
6684 }
6685
6686 std::printf("SolverLQNS(%s) arith=%s type=%s iterations=%d time=%.4fs\n",
6688 line::util::method_type("LQNS", opt.method).c_str(), sol.iterations,
6689 solver.runtime());
6690 std::printf("%-62s %-10s %12s %12s %12s %12s %12s\n", "Node", "NodeType", "QLen", "Util",
6691 "RespT", "ResidT", "Tput");
6692 for (std::size_t i = 1; i <= sn.nidx; ++i) {
6693 // Six digits, as on the SolverLN table above and for the same reason.
6694 auto fmt = [&](const std::vector<T>& v, const std::vector<bool>& d, char* buf) {
6695 if (!d[i]) std::snprintf(buf, 24, "%12s", "NaN");
6696 else
6697 std::snprintf(buf, 24, "%12.6g",
6698 lqns::detail::snap_to_tenth(num_traits<T>::to_double(v[i])));
6699 };
6700 char q[24], u[24], rr[24], w[24], t[24];
6701 fmt(sol.QN, sol.defined_Q, q);
6702 fmt(sol.UN, sol.defined_U, u);
6703 fmt(sol.RN, sol.defined_R, rr);
6704 fmt(sol.WN, sol.defined_W, w);
6705 fmt(sol.TN, sol.defined_T, t);
6706 std::printf("%-62s %-10s %s %s %s %s %s\n", names.names[i].c_str(),
6707 ln_element_kind(names, i), q, u, rr, w, t);
6708 }
6709 return 0;
6710}
6711
6712/**
6713 * `-i lqnx|xml`: the layered path, sibling to solve_model_dispatch.
6714 *
6715 * `-s auto` CONSULTS THE LAYERED ARM of the chooser, which is keyed on the
6716 * metric and on one trait of the model: a cache task promotes the NC layer
6717 * engine, because the cache layer is where NC beats MVA. LQNS leads two of
6718 * those rankings and IS wrapped, so `-s auto` selects it wherever the binary is
6719 * installed -- which makes the choice machine-dependent, exactly as
6720 * `chooseAvgSolverHeur.m` makes it, since LINE ships no LQNS binary. The banner
6721 * always names what answered.
6722 *
6723 * THE TOKENS NAME THE LAYER ENGINE, as the Java CLI's do: `ln.mva` runs the
6724 * layers under SolverMVA and `ln.comom` under SolverNC. The bare `ln` keeps the
6725 * MVA layers this port has always given it -- the Java CLI reads it as NC, but
6726 * changing it here would re-baseline every existing layered result under a token
6727 * whose meaning nothing in this tree states -- so `ln.comom` is the way to ask
6728 * for NC layers. `lqns` is not one of them: it does not solve LAYERS at all, it
6729 * hands the whole model to another program, so it takes the wrapper's own knobs
6730 * and none of SolverLN's.
6731 */
6732/** The solver method name as the console names it, e.g. "mva" -> "MVA". */
6733std::string upper_tag(const std::string& s) {
6734 std::string out = s;
6735 for (std::size_t i = 0; i < out.size(); ++i)
6736 out[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(out[i])));
6737 return out;
6738}
6739
6740int solve_lqn_dispatch(const std::string& arith, const std::string& solver,
6741 const std::string& analysis, const std::string& output,
6742 const std::string& file, const Knobs& k) {
6743 if (file.empty())
6744 throw line::InputError(
6745 "a layered model is read from a file: pass -f <model.lqnx> or -f <model.json> (neither "
6746 "layered reader has a stdin form)");
6747 if (analysis != "avg" && analysis != "tran" && analysis != "sens" && analysis != "cdf")
6749 "the layered path ports -a avg (getAvgTable), -a tran (getTranAvg), -a sens "
6750 "(getSensitivityTable) and -a cdf (getCdfRespT); got '" + analysis + "'");
6751 const std::string s = solver.empty() ? "auto" : solver;
6752 if (s != "auto" && s != "ln" && s != "ln.mva" && s != "ln.comom" && s != "lqns" &&
6753 s != "ldes")
6755 "the layered path takes -s ln, ln.mva, ln.comom, ldes, lqns or auto (got '" + s +
6756 "'); a Network solver cannot be applied to a LayeredNetwork directly");
6757
6758 // Solver console: the layered path has its own dispatcher and never
6759 // reaches solve_model_dispatch, so it opens the narrated run here. The
6760 // guard's destructor closes it, on an exception too.
6761 line::util::LineConsole::Run consoleRun(upper_tag(s), "", true);
6762
6763 Knobs kk = k;
6764 // What will actually answer: the token, or what `-s auto` resolves to.
6765 std::string engine = s;
6766 // `-s auto` with no explicit layer engine consults chooseLayeredSolver. An
6767 // explicit --layer-solver is a choice the caller already made, so the
6768 // chooser does not overrule it.
6769 if (s == "auto" && kk.layer_solver.empty()) {
6770 std::string getter = "getAvgTable";
6771 if (analysis == "tran") getter = "getTranAvg";
6772 else if (analysis == "cdf") getter = "getCdfRespT";
6773 else if (analysis == "sens") getter = "getSensitivityTable";
6774 // `iscache` is the one trait the ranking reads, and it costs one parse
6775 // of the .lqnx: the same document the solve parses again below.
6777 bool has_cache_task = false;
6778 for (std::size_t i = 0; i < probe.iscache.size(); ++i)
6779 if (probe.iscache[i]) has_cache_task = true;
6781 line::autosolver::auto_choose_layered_solver(getter, has_cache_task);
6782 const std::string token = line::autosolver::auto_layered_name(lc.solver);
6783 if (token == "lqns") engine = "lqns";
6784 else if (token == "ln.comom") kk.layer_solver = "nc";
6785 else if (token == "ln.fluid") kk.layer_solver = "fluid";
6786 else kk.layer_solver = "mva";
6787 std::string note;
6788 for (std::size_t i = 0; i < lc.skipped.size(); ++i)
6789 note += std::string(i ? ", " : " (the ranking preferred ") +
6791 if (!lc.skipped.empty()) note += ", which is not available here)";
6792 std::printf("SolverAUTO selected %s%s\n", token.c_str(), note.c_str());
6793 }
6794
6795 // ---- the wrapper, BEFORE the SolverLN knob ladder ---------------------
6796 // It is a wrapper, not a layer engine: --samples is the lqsim run length
6797 // rather than a simulated LAYER's, and --keep, --remote and --timeout
6798 // describe a child process no SolverLN run has. Asking the ladder below
6799 // about them would answer for the wrong solver.
6800 if (engine == "lqns") {
6801 if (analysis != "avg")
6803 "SolverLQNS reports the mean table its binary computes; it has no transient, no "
6804 "sensitivity and no response-time distribution here, so it takes -a avg (got '" +
6805 analysis + "')");
6806 if (!kk.layer_solver.empty())
6807 throw line::InputError(
6808 "--layer-solver names the engine SolverLN runs on each layer; -s lqns solves no "
6809 "layers, it hands the whole model to the lqns binary");
6810 if (output == "layers")
6812 "-o layers dumps the stations and routing SolverLN BUILT from the model; lqns "
6813 "builds its own submodels inside another process and this port never sees them");
6814 if (kk.iter_tol >= 0.0 || kk.iter_max > 0)
6816 "--iter_tol and --iter_max are SolverLN's layer-iteration knobs; lqns runs its own "
6817 "iteration and takes neither (its --iteration-limit is unreliable as of 6.2.27, "
6818 "which is why the reference stopped passing it)");
6819 if (kk.seed)
6821 "--seed sets the stream of a simulator this port drives; lqsim seeds itself and "
6822 "the wrapper passes no seed, exactly as the reference does not");
6823 if (kk.repeat > 1)
6825 "--repeat times a solve by re-running it; re-running lqsim would report a "
6826 "different answer under the same banner");
6827 if (kk.no_interlocking || !kk.ln_transient.empty() || !kk.ln_transient_channels.empty() ||
6828 !kk.sens_method.empty() || !kk.sens_scheme.empty() || kk.sens_step > 0.0)
6830 "--no-interlocking, --ln-transient*, and --sens-* are SolverLN options; -s lqns "
6831 "has none of them");
6832 if (arith != "double")
6834 "SolverLQNS reads a result file another program wrote in decimal double "
6835 "precision; there is no higher precision to carry, so rerun with --arith double "
6836 "(got '" + arith + "')");
6837 // The Network-only knobs the SolverLN ladder below refuses are refused
6838 // HERE TOO. This branch returns before that ladder runs, so a knob left
6839 // out of it is silently DROPPED rather than refused -- and which branch
6840 // a bare `.lqnx` path takes depends on whether an lqns binary is
6841 // installed, so the same command line would be refused on one machine
6842 // and quietly ignored on another.
6843 if (kk.has_cutoff())
6845 "--cutoff bounds the open population of a CTMC state space and applies to -s "
6846 "ctmc; the layered path enumerates no states");
6847 if (kk.node)
6849 "--node selects the stateful node a CTMC query is labelled by; the layered path "
6850 "reports every LQN element");
6851 return run_lqns<double>(file, output, kk);
6852 }
6853 if (k.keep || k.verbose || k.remote || !k.remote_url.empty() || k.timeout_seconds)
6855 "--keep, --verbose, --remote, --remote-url and --timeout describe the child process "
6856 "of an external solver and apply to -s lqns only");
6857 // ---- the native LN simulator, BEFORE the SolverLN knob ladder ----------
6858 // It solves no LAYERS, so the ladder below asks its questions of a
6859 // decomposition this arm never builds: --iter_tol and --iter_max bound a
6860 // fixed point it does not iterate, --layer-solver names an engine it does
6861 // not run, and --samples and --seed -- which the ladder refuses outright
6862 // unless a layer is simulated -- are precisely this arm's two settings.
6863 if (engine == "ldes") {
6864 if (analysis != "avg" && analysis != "cdf")
6866 "the native LN engine measures a sample path: it takes -a avg for the mean table "
6867 "and -a cdf for the per-entry response time distribution, and has no transient "
6868 "and no sensitivity here (got '" + analysis + "')");
6869 if (arith != "double")
6871 "the native LN engine accumulates its estimators in double, so there is no higher "
6872 "precision to carry; rerun with --arith double (got '" + arith + "')");
6873 if (output == "layers")
6875 "-o layers dumps the stations and routing SolverLN BUILT from the model; -s ldes "
6876 "simulates the layered semantics directly and builds no submodels");
6877 if (!kk.layer_solver.empty())
6878 throw line::InputError(
6879 "--layer-solver names the engine SolverLN runs on each layer; -s ldes solves no "
6880 "layers, it simulates entries, activities and calls directly");
6881 if (kk.iter_tol >= 0.0 || kk.iter_max > 0 || kk.no_interlocking)
6883 "--iter_tol, --iter_max and --no-interlocking are SolverLN's layer-iteration "
6884 "knobs; a simulated sample path converges by run length, which is --samples");
6885 if (!kk.ln_transient.empty() || !kk.ln_transient_channels.empty() ||
6886 !kk.sens_method.empty() || !kk.sens_scheme.empty() || kk.sens_step > 0.0)
6888 "--ln-transient* and --sens-* are SolverLN options; -s ldes has none of them");
6889 if (!kk.method.empty() && kk.method != "default")
6891 "--method on the layered path names the LN UPDATE (default, moment3, mwba.*); "
6892 "-s ldes performs no update, it simulates the model (got '" + kk.method + "')");
6893 // The --ldes-* family is the SUBPROCESS engine's settings. The native LN
6894 // engine reads samples, events and seed and nothing else, so a warmup
6895 // filter or a CI estimator passed here would be DROPPED rather than
6896 // honoured -- and a dropped `--ldes-tranfilter none` reads as a run with
6897 // no warmup removal that in fact removed one.
6898 if (!kk.ldes_tranfilter.empty() || kk.ldes_warmupfrac >= 0.0 ||
6899 !kk.ldes_cimethod.empty() || kk.ldes_cnvgon || kk.ldes_cnvgtol > 0.0 ||
6900 kk.ldes_slotted || kk.ldes_slotlength > 0.0 || kk.ldes_replications > 0 ||
6901 kk.ldes_numthreads > 0 || kk.ldes_maxtime > 0.0 || !kk.ldes_initsol.empty() ||
6902 !kk.ldes_rest_url.empty())
6904 "the --ldes-* flags configure the SUBPROCESS engine that answers -s ldes on a "
6905 "Network (warmup filter, CI estimator, slot lattice, replications, warm-start "
6906 "placement); the native LN engine behind -i lqnx -s ldes reads --samples and "
6907 "--seed only");
6908 // The Network-only knobs the SolverLN ladder refuses below are refused
6909 // HERE TOO: this branch returns before that ladder runs, so a knob left
6910 // out of it would be silently dropped rather than refused.
6911 if (kk.has_cutoff())
6913 "--cutoff bounds the open population of a CTMC state space and applies to -s "
6914 "ctmc; the layered path enumerates no states");
6915 if (kk.node)
6917 "--node selects the stateful node a CTMC query is labelled by; the layered path "
6918 "reports every LQN element");
6919 if (kk.t1 >= 0.0)
6921 "--tspan sets the horizon of a transient analysis; the native LN engine runs to "
6922 "a completion budget, which is --samples");
6923 if (kk.tol >= 0.0)
6925 "--tol is not an LDES option; the run length is set with --samples");
6926 if (analysis == "cdf") return run_ln_ldes_cdf(file, output, kk);
6927 return run_ln_ldes(file, output, kk);
6928 }
6929 // The solver method name and --layer-solver name the same choice, so they may not
6930 // disagree: silently letting one win would report the other in the banner.
6931 if (s == "ln.comom") {
6932 if (!kk.layer_solver.empty() && kk.layer_solver != "nc")
6933 throw line::InputError("-s ln.comom already selects NC layers, but --layer-solver says '" +
6934 kk.layer_solver + "'");
6935 kk.layer_solver = "nc";
6936 } else if (s == "ln.mva") {
6937 if (!kk.layer_solver.empty() && kk.layer_solver != "mva")
6938 throw line::InputError("-s ln.mva already selects MVA layers, but --layer-solver says '" +
6939 kk.layer_solver + "'");
6940 kk.layer_solver = "mva";
6941 }
6942 // --layer-solver is this port's own flag, the C++ spelling of the
6943 // reference's solver FACTORY: `LN(model, @(m) MVA(m))` against
6944 // `LN(model, @(m) Fluid(m))`. They converge to DIFFERENT fixed points.
6945 if (!kk.layer_solver.empty() && kk.layer_solver != "mva" && kk.layer_solver != "nc" &&
6946 kk.layer_solver != "fluid" && kk.layer_solver != "ssa")
6947 throw line::InputError("--layer-solver takes mva, nc, fluid or ssa (got '" +
6948 kk.layer_solver + "')");
6949 // ---- knobs the layered path does not have are refused, not dropped ----
6950 if ((k.samples || k.seed) && kk.layer_solver != "ssa")
6952 "--samples and --seed set the run length and the stream of a SIMULATED layer; the "
6953 "layered path draws no random numbers unless --layer-solver ssa is in force");
6954 if (k.has_cutoff())
6956 "--cutoff bounds the open population of a CTMC state space and applies to -s ctmc; the "
6957 "layered path enumerates no states");
6958 if (k.t1 >= 0.0 && analysis != "tran")
6960 "--tspan sets the horizon of a transient analysis and applies to the layered path "
6961 "only with -a tran");
6962 if (analysis == "tran" && !(k.t1 >= 0.0))
6963 throw line::InputError(
6964 "-a tran integrates each layer's drift and needs a horizon: pass --tspan <t0>:<t1>");
6965 if (k.node)
6967 "--node selects the stateful node a CTMC query is labelled by; the layered path "
6968 "reports every LQN element");
6969 if (k.tol >= 0.0)
6971 "--tol is not a SolverLN option (LnOptions carries iter_tol and iter_max); "
6972 "use --iter_tol");
6973 // --method now names the LN UPDATE, which is a different question from the
6974 // layer engine: `moment3` reports a distribution the default never forms,
6975 // and the two bound requests report a bound instead of a fixed point.
6976 if (!k.method.empty() && k.method != "default" && k.method != "moment3" &&
6977 k.method != "mwba.upper" && k.method != "mwba.lower")
6979 "--method on the layered path takes default, moment3, mwba.upper or mwba.lower "
6980 "(got '" + k.method + "'); the per-layer engine is chosen with --layer-solver");
6981 if ((k.method == "mwba.upper" || k.method == "mwba.lower") && analysis != "avg")
6983 "--method mwba.* reports a throughput and utilization BOUND and solves no layer, so "
6984 "it has no transient, no sensitivity and no response-time law; use -a avg");
6985 if (!k.sens_method.empty() && analysis != "sens")
6986 throw line::UnsupportedError("--sens-method applies to -a sens");
6987 if (!k.ln_transient.empty() && analysis != "tran")
6988 throw line::UnsupportedError("--ln-transient applies to -a tran");
6989
6990 if (analysis == "tran") {
6991 if (arith != "double")
6993 "the layered transient integrates each layer's drift with LSODA, which is double "
6994 "precision by construction; rerun with --arith double (got '" + arith + "')");
6995 return run_ln_tran<double>(file, output, kk);
6996 }
6997 if (analysis == "cdf") {
6998 if (arith != "double")
7000 "-a cdf fits an APH to a fluid passage time, integrated by LSODA in double "
7001 "precision; rerun with --arith double (got '" + arith + "')");
7002 return run_ln_cdf<double>(file, output, kk);
7003 }
7004 if (analysis == "sens") {
7005 if (arith == "double") return run_ln_sens<double>(file, output, kk);
7006 if (arith == "exact") return run_ln_sens<line::Rational>(file, output, kk);
7007 if (arith == "real:16") return run_ln_sens<line::Real<16> >(file, output, kk);
7008 if (arith == "real" || arith == "real:32")
7009 return run_ln_sens<line::Real<32> >(file, output, kk);
7010 if (arith == "real:64") return run_ln_sens<line::Real<64> >(file, output, kk);
7011 if (arith == "real:128") return run_ln_sens<line::Real<128> >(file, output, kk);
7012 if (arith == "real:256") return run_ln_sens<line::Real<256> >(file, output, kk);
7013 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
7014 }
7015
7016 if (arith == "double") return run_ln<double>(file, output, kk);
7017 if (arith == "exact") return run_ln<line::Rational>(file, output, kk);
7018 // precision-ladder rationale (real:16 rung): see _kb/14-cpp-multiprecision.md
7019 if (arith == "real:16") return run_ln<line::Real<16> >(file, output, kk);
7020 if (arith == "real" || arith == "real:32") return run_ln<line::Real<32> >(file, output, kk);
7021 if (arith == "real:64") return run_ln<line::Real<64> >(file, output, kk);
7022 if (arith == "real:128") return run_ln<line::Real<128> >(file, output, kk);
7023 if (arith == "real:256") return run_ln<line::Real<256> >(file, output, kk);
7024 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
7025}
7026
7027/**
7028 * `-a node`: `@@NetworkSolver/getAvgNodeTable`, the means per NODE.
7029 *
7030 * A DIFFERENT INDEX SPACE FROM `-a avg`, not a relabelling of it. The AvgTable
7031 * is indexed by STATION, so a ClassSwitch, a Router, a Fork, a Join and a Sink
7032 * are absent from it entirely -- they hold no jobs, so they have no row -- yet
7033 * jobs flow through them and the flow is what a caller sizing a link or an
7034 * interconnect needs. This table has one row per node and reports the arrival
7035 * rate and the throughput at every one of them, which is the only place those
7036 * two numbers exist for a non-station node.
7037 *
7038 * QLen, Util, RespT and ResidT ARE the station numbers, scattered to the node
7039 * indices and left at zero elsewhere; that is the reference's own construction
7040 * and not a gap, because a node that is not a station holds no jobs and serves
7041 * nothing. ArvR and Tput are the two the reference recomputes, through
7042 * `sn_get_node_arvr_from_tput` and `sn_get_node_tput_from_tput`.
7043 *
7044 * THE F REGION PSEUDO-NODE ROWS OF THE REFERENCE ARE NOT EMITTED. MATLAB
7045 * appends one row per finite-capacity region, filled from `result.Avg` rows
7046 * M+1..M+F; the C++ `AvgResult` carries no per-region queue length or
7047 * utilization, so those rows have no data source here and are omitted rather
7048 * than fabricated as zeros, which would read as an empty region.
7049 */
7050/* `avg_result_from_sim` MOVED to line/solvers/solver_node_tables.h, where the
7051 * example corpus can reach it too: four `cache_replc_*` twins print the NODE
7052 * table their references print, and it is the same view of the same numbers.
7053 * Named unqualified below, as it was when it was defined here. */
7055
7056/**
7057 * The LDES result document mapped onto the station AvgResult, BY NAME.
7058 *
7059 * The engine reports its own station and class order, and pairing the two off
7060 * positionally would put one station's numbers on another's row; `-a avg`
7061 * already matches by name for exactly that reason and this is the same match.
7062 * `WN` is recomputed rather than taken, for the reason the `-a avg` arm states:
7063 * the engine counts one visit per station, so its residence time is the response
7064 * time whenever a visit ratio is not 1.
7065 *
7066 * REGION ROWS RIDE PAST THE STATIONS, in the (M+F) layout `jmt_map_measures`
7067 * already uses, so a caller reads both engines' finite-capacity rows the same
7068 * way.
7069 */
7070inline line::mva::AvgResult<double> avg_result_from_ldes(
7072 const std::size_t M = sn.nstations, K = sn.nclasses;
7073 const std::size_t F = a.QNfcr.empty() ? 0 : a.QNfcr.rows();
7075 line::Matrix<double>* dst[6] = {&r.QN, &r.UN, &r.RN, &r.TN, &r.AN, &r.WN};
7076 const line::Matrix<double>* src[6] = {&a.QN, &a.UN, &a.RN, &a.TN, &a.AN, &a.WN};
7077 const line::Matrix<double>* fcr[6] = {&a.QNfcr, &a.UNfcr, &a.RNfcr,
7078 &a.TNfcr, &a.ANfcr, &a.WNfcr};
7079 std::vector<std::size_t> st_of(a.station_names.size(), 0); // 1-based, 0 = unmatched
7080 for (std::size_t i = 0; i < a.station_names.size(); ++i)
7081 for (std::size_t j = 0; j < M; ++j)
7082 if (sn.stations[j].name == a.station_names[i]) { st_of[i] = j + 1; break; }
7083 std::vector<std::size_t> cl_of(a.class_names.size(), 0);
7084 for (std::size_t c = 0; c < a.class_names.size(); ++c)
7085 for (std::size_t j = 0; j < K; ++j)
7086 if (sn.classes[j].name == a.class_names[c]) { cl_of[c] = j + 1; break; }
7087 for (int m = 0; m < 6; ++m) {
7088 *dst[m] = line::Matrix<double>(M + F, K, 0.0);
7089 for (std::size_t i = 0; i < a.station_names.size(); ++i)
7090 for (std::size_t c = 0; c < a.class_names.size(); ++c)
7091 if (st_of[i] && cl_of[c])
7092 (*dst[m])(st_of[i] - 1, cl_of[c] - 1) = ldes_at(*src[m], i, c);
7093 for (std::size_t f = 0; f < F; ++f)
7094 for (std::size_t c = 0; c < a.class_names.size(); ++c)
7095 if (cl_of[c]) (*dst[m])(M + f, cl_of[c] - 1) = ldes_at(*fcr[m], f, c);
7096 }
7097 line::Matrix<double> RNs(M, K, 0.0);
7098 for (std::size_t i = 0; i < M; ++i)
7099 for (std::size_t c = 0; c < K; ++c) RNs(i, c) = r.RN(i, c);
7101 for (std::size_t i = 0; i < M; ++i)
7102 for (std::size_t c = 0; c < K; ++c) r.WN(i, c) = WNs(i, c);
7103 for (std::size_t c = 0; c < K; ++c) {
7104 r.CN.push_back(ldes_at(a.CN, 0, c));
7105 r.XN.push_back(ldes_at(a.XN, 0, c));
7106 }
7107 r.method = a.method;
7108 r.actualmethod = a.method;
7109 return r;
7110}
7111
7112/**
7113 * Solve for the station AvgResult with the named engine.
7114 *
7115 * SHARED BY EVERY @@NetworkSolver TABLE THAT IS NOT THE AvgTable -- `-a node`,
7116 * `-a sys`, `-a chain`, `-a nodechain` -- because each of them is a VIEW of the
7117 * same solved result and differs only in how it is indexed and aggregated.
7118 * Giving each arm its own engine ladder would let the five drift apart in which
7119 * knobs they honour, which is exactly the silent divergence the Knobs struct
7120 * exists to prevent one layer up.
7121 *
7122 * SSA and Fluid reach it through `avg_result_from_sim`, and only under
7123 * `--arith double`: both integrate transcendental quantities (exponential
7124 * clocks, an LSODA drift), so the dispatcher refuses the other backends by name
7125 * before the ladder is entered, exactly as their own `-a avg` arms do.
7126 *
7127 * THE TWO WRAPPERS ARE HERE FOR THE SAME REASON THE TWO SIMULATORS ARE.
7128 * `getAvgNodeTable` is @@NetworkSolver's and is a VIEW of whatever AvgResult a
7129 * solver returned; JMT and LDES both return one (`JmtResult::avg`, the LDES
7130 * result document), so refusing them the four views said "no C++ simulation
7131 * engine" about engines this port drives. `file` is what LDES needs and the
7132 * others ignore: it hands the model DOCUMENT to an external process rather
7133 * than reading the struct.
7134 */
7135template <class T>
7136line::mva::AvgResult<T> run_avg_engine(const line::qn::NetworkStruct<T>& sn, const Knobs& k,
7137 const std::string& s, std::string& banner,
7138 std::string* suffix = nullptr,
7139 const std::string* file = nullptr) {
7141 if (s == "jmt" || s == "ldes") {
7142 // COMPILE-TIME, as for the two simulators: both wrappers report in
7143 // double and the dispatcher has already refused every other arithmetic
7144 // by name, so the discarded branch is unreachable rather than narrowed.
7145 if constexpr (std::is_same_v<T, double>) {
7146 if (s == "jmt") {
7148 if (!k.method.empty() && k.method != "default") o.method = k.method;
7149 if (k.samples > 0) o.samples = static_cast<double>(k.samples);
7150 if (k.seed != 0) o.seed = static_cast<long>(k.seed);
7151 o.keep = k.keep;
7152 if (k.t1 >= 0.0) o.max_simulated_time = k.t1;
7153 o.verbose = k.verbose;
7155 r = a.avg;
7156 banner = "SolverJMT";
7157 // THE SEED IS PART OF THE ANSWER, as it is for SSA: two runs of
7158 // a simulation are the same measurement only if both state it.
7159 if (suffix) {
7160 char buf[128];
7161 std::snprintf(buf, sizeof(buf), " samples=%g seed=%ld", o.samples, o.seed);
7162 *suffix = buf;
7163 }
7164 } else {
7165 const line::ldes::LdesOptions o = ldes_options(k);
7166 const line::ldes::LdesResult a =
7167 ldes_run(file ? *file : std::string(), o, std::vector<std::string>());
7168 r = avg_result_from_ldes(sn, a);
7169 banner = "SolverLDES";
7170 if (suffix) {
7171 char buf[160];
7172 std::snprintf(buf, sizeof(buf), " engine=%s samples=%zu seed=%ld",
7173 a.engine.empty() ? "?" : a.engine.c_str(), o.samples, o.seed);
7174 *suffix = buf;
7175 }
7176 }
7177 } else {
7178 throw line::UnsupportedError("-s " + s + " runs under --arith double only");
7179 }
7180 } else if (s == "ssa" || s == "fluid") {
7181 // COMPILE-TIME, not just run-time: both runners static_assert on
7182 // transcendental arithmetic inside (an exponential clock, a square root
7183 // in the Cox refit), so instantiating them at Rational is a hard error
7184 // and not a refusal. The dispatcher has already rejected every arith
7185 // but double by name, so the discarded branch is unreachable rather
7186 // than silently narrowed.
7187 if constexpr (std::is_same_v<T, double>) {
7188 if (s == "ssa") {
7190 if (!k.method.empty() && k.method != "default") opt.method = k.method;
7191 if (k.samples) opt.samples = k.samples;
7192 if (k.seed) opt.seed = k.seed;
7193 // The cache write-back rides beside the metric table, for the
7194 // reason `node_metrics` states: the realized hit and miss shares
7195 // are what the simulation MEASURED, and without them the node
7196 // table falls back to the split `link()` offered.
7197 std::vector<line::ssa::SsaCacheRatio> cache;
7199 // ResidT and ArvR are derived from the VISITS, and a cache's
7200 // split is routing, so both are taken on the struct carrying the
7201 // measured hit/miss shares rather than on `link()`'s even offer.
7202 r = avg_result_from_sim<T>(line::ssa::sn_with_ssa_cache_split<T>(sn, cache),
7203 a.QN, a.UN, a.RN, a.TN, a.CN, a.XN, a.method);
7205 // THE SEED AND THE SAMPLE COUNT ARE PART OF THE ANSWER, not of
7206 // the invocation, so they ride in the banner here as they do in
7207 // `-a avg`: two runs of a simulation are the same measurement
7208 // only if both are stated, and a parity row that quotes one of
7209 // these tables has to carry them with it.
7210 banner = "SolverSSA";
7211 // APPENDED, NOT PREFIXED. Every consumer recognises a banner by
7212 // `Solver<name> arith=`, so a fact wedged between the two makes
7213 // the table belong to no solver at all -- which is how the
7214 // parity harness lost the whole SSA section.
7215 if (suffix) {
7216 char buf[128];
7217 std::snprintf(buf, sizeof(buf), " samples=%zu seed=%lu time=%.6g", a.samples,
7218 static_cast<unsigned long>(opt.seed), a.simulated_time);
7219 *suffix = buf;
7220 }
7221 } else {
7222 const line::fluid::FluidOptions opt = fluid_options(k);
7223 // The cache decomposition renormalizes the self-switch at the
7224 // converged split; `-a node` needs that struct or it reports the
7225 // 1/2-1/2 `link()` left behind (see `node_metrics`).
7227 // The null must be typed: a bare `nullptr` is `std::nullptr_t`
7228 // and blocks deduction of T from the fourth argument.
7229 // THE SPLIT ITSELF IS TAKEN TOO, not only the struct it
7230 // renormalized: `node_metrics` prefers the stated split over the
7231 // visit ratios, and `-a cache` is built from nothing else.
7234 sn, opt, static_cast<line::qn::NetworkStruct<T>*>(nullptr), &refreshed, &cache);
7235 // Non-empty only where the cache branch ran, which is the same
7236 // test `node_metrics` makes on the pointer. IT IS ALSO THE
7237 // STRUCT THE ARRIVAL RATE IS READ FROM: that column is derived
7238 // from the class-expanded routing, and the base struct still
7239 // holds the 1/2-1/2 self-switch `link()` offered, so deriving it
7240 // there reports 0.5/0.5 where cache_replc_routing's Delay1 sees
7241 // 0.4/0.6. Every other column is indexed by station and is the
7242 // same in both structs.
7243 const bool has_ref = !refreshed.nodes.empty();
7244 r = avg_result_from_sim<T>(has_ref ? refreshed : sn, a.QN, a.UN, a.RN, a.TN, a.CN,
7245 a.XN, a.method);
7246 if (has_ref) r.refreshed_struct.reset(new line::qn::NetworkStruct<T>(refreshed));
7247 r.cache = cache;
7248 r.iter = static_cast<int>(a.iters);
7249 banner = "SolverFluid";
7250 if (suffix) {
7251 char buf[64];
7252 std::snprintf(buf, sizeof(buf), " iters=%zu", a.iters);
7253 *suffix = buf;
7254 }
7255 }
7256 } else {
7257 throw line::UnsupportedError("-s " + s + " runs under --arith double only");
7258 }
7259 } else if (s == "nc") {
7261 if (!k.method.empty() && k.method != "default") opt.method = k.method;
7262 if (k.tol >= 0.0) opt.tol = k.tol;
7263 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
7264 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
7265 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
7267 banner = "SolverNC";
7268 } else if (s == "mam") {
7270 if (!k.method.empty() && k.method != "default") opt.method = k.method;
7271 if (k.tol >= 0.0) opt.tol = k.tol;
7272 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
7274 banner = "SolverMAM";
7275 } else if (s == "ag") {
7277 apply_ag_knobs(k, opt);
7279 banner = "SolverAG";
7280 } else if (s == "ba") {
7282 if (!k.method.empty() && k.method != "default") opt.method = k.method;
7284 banner = "SolverBA";
7285 } else if (s == "ctmc") {
7287 if (!k.method.empty()) opt.method = k.method;
7288 if (k.cutoff >= 0.0) opt.cutoff = k.cutoff;
7289 opt.cutoff_mat = k.cutoff_mat;
7290 opt.force = k.force;
7293 banner = "SolverCTMC";
7294 } else {
7296 if (!k.method.empty() && k.method != "default") opt.method = k.method;
7297 if (k.tol >= 0.0) opt.tol = k.tol;
7298 if (k.iter_tol >= 0.0) opt.iter_tol = k.iter_tol;
7299 if (k.iter_max >= 0) opt.iter_max = k.iter_max;
7300 // `-a avg` has honoured --multiserver since the flag existed; these
7301 // views are the SAME solve indexed differently, so ignoring it here made
7302 // `-a chain` answer a different model than `-a avg` for the same command
7303 // line. Measured on cqn_repairmen_multi, where softmin and the default
7304 // rule differ by 39% on the Delay queue length.
7305 if (!k.multiserver.empty()) opt.multiserver = k.multiserver;
7306 if (!k.fork_join.empty()) opt.fork_join = k.fork_join;
7307 line::Matrix<T> init;
7309 banner = "SolverMVA";
7310 }
7311 return r;
7312}
7313
7314/* `NodeMetrics` / `node_metrics` MOVED to line/solvers/solver_node_tables.h;
7315 * see the note above `run_avg_engine`. Used unchanged by both arms below. */
7318
7319template <class T>
7320int solve_model_node(const std::string& file, const Knobs& k, const std::string& s) {
7321 line::qn::Network<T> net = read_model<T>(file);
7323 std::string banner, suffix;
7324 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
7325
7326 const std::size_t I = sn.nodes.size(), R = sn.nclasses;
7327 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
7328 const line::Matrix<T>&QNn = nm.QN, &UNn = nm.UN, &RNn = nm.RN, &WNn = nm.WN, &ANn = nm.AN,
7329 &TNn = nm.TN;
7330
7331 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
7332 // A FINITE CAPACITY REGION IS NOT A NODE, and the reference still prints it
7333 // in this table: `getAvgNodeTable` appends one row per region past the
7334 // nodes, because a WAITQ region holds jobs that are in no station's QLen and
7335 // the model's population only balances once they are read. The rows ride
7336 // past the stations in the returned AvgResult -- the (M+F) layout both
7337 // wrappers report -- and no analytical solver fills them, so this block is
7338 // empty for every engine that does not measure a region.
7339 //
7340 // Util AND ArvR ARE NaN, NOT ZERO. A region has no server to be busy and no
7341 // arrival process of its own; the reference reports both as missing, and a
7342 // zero there would be a number the run never measured.
7343 const std::size_t F =
7344 r.QN.rows() > sn.nstations ? r.QN.rows() - sn.nstations : static_cast<std::size_t>(0);
7345 const double region_nan = std::numeric_limits<double>::quiet_NaN();
7346 auto region_name = [&](std::size_t f) {
7347 return (f < sn.regions.size() && !sn.regions[f].name.empty())
7348 ? sn.regions[f].name
7349 : "FCR" + std::to_string(f + 1);
7350 };
7351 // The reference's own filter, the region twin of the all-zero row test: a
7352 // region no job ever entered is absent rather than a row of zeros.
7353 auto region_empty = [&](std::size_t f, std::size_t c) {
7354 return !(d(r.QN(sn.nstations + f, c)) > 0.0 || d(r.RN(sn.nstations + f, c)) > 0.0 ||
7355 d(r.TN(sn.nstations + f, c)) > 0.0);
7356 };
7357 if (g_json_output) {
7358 // The banner under `-o json` too, for print_chain_table's reason: the
7359 // envelope names the arithmetic and the method but never the SOLVER.
7360 std::printf("%s arith=%s method=%s nodes=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
7361 r.actualmethod.c_str(), I, suffix.c_str());
7362 line::reg::Json p = line::reg::Json::object();
7363 p["type"] = "AvgNodeTable";
7364 p["indexBase"] = 0;
7365 for (const char* key : {"Node", "JobClass", "QLen", "Util", "RespT", "ResidT", "ArvR",
7366 "Tput"})
7367 p[key] = line::reg::Json::array();
7368 for (std::size_t i = 0; i < I; ++i)
7369 for (std::size_t c = 0; c < R; ++c) {
7370 // The reference's own row filter: a node a class never reaches
7371 // is absent, not a row of zeros, exactly as in the AvgTable.
7372 if (d(QNn(i, c)) == 0.0 && d(UNn(i, c)) == 0.0 && d(RNn(i, c)) == 0.0 &&
7373 d(WNn(i, c)) == 0.0 && d(ANn(i, c)) == 0.0 && d(TNn(i, c)) == 0.0)
7374 continue;
7375 p["Node"].push_back(sn.nodes[i].name);
7376 p["JobClass"].push_back(sn.classes[c].name);
7377 p["QLen"].push_back(d(QNn(i, c)));
7378 p["Util"].push_back(d(UNn(i, c)));
7379 p["RespT"].push_back(d(RNn(i, c)));
7380 p["ResidT"].push_back(d(WNn(i, c)));
7381 p["ArvR"].push_back(d(ANn(i, c)));
7382 p["Tput"].push_back(d(TNn(i, c)));
7383 }
7384 for (std::size_t f = 0; f < F; ++f)
7385 for (std::size_t c = 0; c < R; ++c) {
7386 if (region_empty(f, c)) continue;
7387 p["Node"].push_back(region_name(f));
7388 p["JobClass"].push_back(sn.classes[c].name);
7389 p["QLen"].push_back(d(r.QN(sn.nstations + f, c)));
7390 p["Util"].push_back(region_nan);
7391 p["RespT"].push_back(d(r.RN(sn.nstations + f, c)));
7392 p["ResidT"].push_back(d(r.WN(sn.nstations + f, c)));
7393 p["ArvR"].push_back(region_nan);
7394 p["Tput"].push_back(d(r.TN(sn.nstations + f, c)));
7395 }
7396 emit_analysis<T>("node", p, r.actualmethod);
7397 return 0;
7398 }
7399 std::printf("%s arith=%s method=%s nodes=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
7400 r.actualmethod.c_str(), I, suffix.c_str());
7401 std::printf("%-16s %-14s %12s %12s %12s %12s %12s %12s\n", "Node", "JobClass", "QLen", "Util",
7402 "RespT", "ResidT", "ArvR", "Tput");
7403 for (std::size_t i = 0; i < I; ++i)
7404 for (std::size_t c = 0; c < R; ++c) {
7405 if (d(QNn(i, c)) == 0.0 && d(UNn(i, c)) == 0.0 && d(RNn(i, c)) == 0.0 &&
7406 d(WNn(i, c)) == 0.0 && d(ANn(i, c)) == 0.0 && d(TNn(i, c)) == 0.0)
7407 continue;
7408 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
7409 sn.nodes[i].name.c_str(), sn.classes[c].name.c_str(), d(QNn(i, c)),
7410 d(UNn(i, c)), d(RNn(i, c)), d(WNn(i, c)), d(ANn(i, c)), d(TNn(i, c)));
7411 }
7412 for (std::size_t f = 0; f < F; ++f)
7413 for (std::size_t c = 0; c < R; ++c) {
7414 if (region_empty(f, c)) continue;
7415 std::printf("%-16s %-14s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
7416 region_name(f).c_str(), sn.classes[c].name.c_str(),
7417 d(r.QN(sn.nstations + f, c)), region_nan, d(r.RN(sn.nstations + f, c)),
7418 d(r.WN(sn.nstations + f, c)), region_nan, d(r.TN(sn.nstations + f, c)));
7419 }
7420 return 0;
7421}
7422
7423/** NaN as this arithmetic spells it, the reference's "not computed" marker. */
7424template <class T>
7425T cache_nan() {
7426 return line::num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
7427}
7428
7429/** `v[r]` when the vector reaches r, NaN otherwise -- the reference's nanGetAt. */
7430template <class T>
7431double cache_at(const std::vector<T>& v, std::size_t r) {
7432 if (r >= v.size()) return std::numeric_limits<double>::quiet_NaN();
7433 return line::num_traits<T>::to_double(v[r]);
7434}
7435
7436/**
7437 * `-a cache`: `@@NetworkSolver/getAvgCacheTable`, per Cache node and READ class.
7438 *
7439 * ONE TOTAL ROW PER (node, read class), plus one row per cache list where the
7440 * solver reported a per-list breakdown and the cache has more than one list.
7441 * The `List` column tells them apart: 0 is the total over every list, l is
7442 * list l. Only the total row carries the delayed-hit and miss columns, because
7443 * a miss is a property of the cache and not of any one list.
7444 *
7445 * THE ArvR COLUMN IS THE RETRIEVAL FLOW, `arvr * (missprob + delayedprob)`, on
7446 * the total row and the raw read rate on a list row. That is the reference's
7447 * choice and it is Little-consistent with ResidT: the residence time reported
7448 * beside it is the expected retrieval latency, which only the requests that
7449 * actually retrieve wait for.
7450 *
7451 * A READ CLASS IS ONE WITH A HIT CLASS DEFINED. A class that never reads the
7452 * cache has no row at all rather than a row of zeros, which is the same rule
7453 * the AvgTable applies to a class that never visits a station.
7454 */
7455template <class T>
7456int solve_model_cache(const std::string& file, const Knobs& k, const std::string& s) {
7457 line::qn::Network<T> net = read_model<T>(file);
7459 std::string banner, suffix;
7460 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
7461 if (r.cache.empty())
7463 "-a cache reports the per-Cache hit and miss table and this model has no Cache node, "
7464 "or the solver that ran analyzes none; SolverNC's cache branches are what fill it");
7465
7466 // The read-class arrival rate is that class's SOURCE throughput: every read
7467 // request enters the cache, so this holds across solvers, including the
7468 // simulators where a delayed hit is not folded into the hit throughput.
7469 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
7470 std::size_t srcnode = 0;
7471 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
7472 if (sn.nodes[i].nodetype == line::lang::NodeType::Source) { srcnode = i + 1; break; }
7473
7474 struct Row {
7475 std::string node, cls;
7476 double list, listcap, items, hitp, dhitp, missp, hitr, dhitr, missr, arvr, residt, cost;
7477 };
7478 std::vector<Row> rows;
7479 const double dnan = std::numeric_limits<double>::quiet_NaN();
7480
7481 for (std::size_t c = 0; c < r.cache.caches.size(); ++c) {
7482 const line::solvers::CacheNodeMetrics<T>& m = r.cache.caches[c];
7483 const typename std::map<std::size_t, line::qn::CacheParam<T> >::const_iterator it =
7484 sn.nodeparam.find(m.node);
7485 if (it == sn.nodeparam.end()) continue;
7486 const std::vector<std::size_t>& hitclass = it->second.hitclass;
7487 const std::size_t h = m.itemcap.size();
7488 double totcap = 0.0;
7489 for (std::size_t l = 0; l < h; ++l) totcap += m.itemcap[l];
7490 double totcost = dnan;
7491 if (!m.listcost.empty()) {
7492 totcost = 0.0;
7493 for (std::size_t l = 0; l < m.listcost.size(); ++l)
7495 }
7496
7497 for (std::size_t cl = 0; cl < sn.nclasses; ++cl) {
7498 if (cl >= hitclass.size() || hitclass[cl] == 0) continue; // not a read class
7499 double ph = cache_at(m.hitprob, cl), pm = cache_at(m.missprob, cl),
7500 pd = cache_at(m.delayedprob, cl);
7501 if (std::isnan(ph) && std::isnan(pm) && std::isnan(pd)) continue;
7502 if (std::isnan(ph)) ph = 0.0;
7503 if (std::isnan(pm)) pm = 0.0;
7504 if (std::isnan(pd)) pd = 0.0;
7505 const double arvr =
7506 srcnode ? line::num_traits<T>::to_double(nm.TN(srcnode - 1, cl)) : 0.0;
7507 const double lat = cache_at(m.latency, cl);
7508
7509 Row t;
7510 t.node = sn.nodes[m.node - 1].name;
7511 t.cls = sn.classes[cl].name;
7512 t.list = 0;
7513 t.listcap = totcap;
7514 t.items = static_cast<double>(m.nitems);
7515 t.hitp = ph;
7516 t.dhitp = pd;
7517 t.missp = pm;
7518 t.hitr = arvr * ph;
7519 t.dhitr = arvr * pd;
7520 t.missr = arvr * pm;
7521 t.arvr = arvr * (pm + pd);
7522 t.residt = lat;
7523 t.cost = totcost;
7524 rows.push_back(t);
7525
7526 // Per-list rows, only where a genuine multi-list breakdown exists.
7527 bool any = false;
7528 if (h > 1 && cl < m.hitproblist.rows())
7529 for (std::size_t l = 0; l < m.hitproblist.cols(); ++l)
7530 if (!std::isnan(line::num_traits<T>::to_double(m.hitproblist(cl, l))))
7531 any = true;
7532 if (!any) continue;
7533 for (std::size_t l = 0; l < h; ++l) {
7534 double phl = l < m.hitproblist.cols()
7536 : dnan;
7537 if (std::isnan(phl)) phl = 0.0;
7538 Row u;
7539 u.node = t.node;
7540 u.cls = t.cls;
7541 u.list = static_cast<double>(l + 1);
7542 u.listcap = m.itemcap[l];
7543 u.items = t.items;
7544 u.hitp = phl;
7545 u.dhitp = dnan;
7546 u.missp = dnan;
7547 u.hitr = arvr * phl;
7548 u.dhitr = dnan;
7549 u.missr = dnan;
7550 u.arvr = arvr;
7551 u.residt = dnan;
7552 u.cost = l < m.listcost.size()
7554 : dnan;
7555 rows.push_back(u);
7556 }
7557 }
7558 }
7559
7560 if (g_json_output) {
7561 line::reg::Json p = line::reg::Json::object();
7562 p["type"] = "AvgCacheTable";
7563 p["indexBase"] = 0;
7564 for (const char* key : {"Node", "JobClass", "List", "ListCap", "Items", "HitProb",
7565 "DelayedHitProb", "MissProb", "HitRate", "DelayedHitRate",
7566 "MissRate", "ArvR", "ResidT", "ListCost"})
7567 p[key] = line::reg::Json::array();
7568 for (std::size_t i = 0; i < rows.size(); ++i) {
7569 p["Node"].push_back(rows[i].node);
7570 p["JobClass"].push_back(rows[i].cls);
7571 p["List"].push_back(rows[i].list);
7572 p["ListCap"].push_back(rows[i].listcap);
7573 p["Items"].push_back(rows[i].items);
7574 p["HitProb"].push_back(rows[i].hitp);
7575 p["DelayedHitProb"].push_back(rows[i].dhitp);
7576 p["MissProb"].push_back(rows[i].missp);
7577 p["HitRate"].push_back(rows[i].hitr);
7578 p["DelayedHitRate"].push_back(rows[i].dhitr);
7579 p["MissRate"].push_back(rows[i].missr);
7580 p["ArvR"].push_back(rows[i].arvr);
7581 p["ResidT"].push_back(rows[i].residt);
7582 p["ListCost"].push_back(rows[i].cost);
7583 }
7584 emit_analysis<T>("cache", p, r.actualmethod);
7585 return 0;
7586 }
7587 std::printf("%s arith=%s method=%s caches=%zu\n", banner.c_str(), line::num_traits<T>::name(),
7588 r.actualmethod.c_str(), r.cache.caches.size());
7589 std::printf("%-14s %-12s %5s %8s %6s %10s %10s %10s %10s %10s %10s %10s %10s %10s\n", "Node",
7590 "JobClass", "List", "ListCap", "Items", "HitProb", "DHitProb", "MissProb",
7591 "HitRate", "DHitRate", "MissRate", "ArvR", "ResidT", "ListCost");
7592 for (std::size_t i = 0; i < rows.size(); ++i)
7593 std::printf(
7594 "%-14s %-12s %5g %8g %6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g %10.6g "
7595 "%10.6g\n",
7596 rows[i].node.c_str(), rows[i].cls.c_str(), rows[i].list, rows[i].listcap,
7597 rows[i].items, rows[i].hitp, rows[i].dhitp, rows[i].missp, rows[i].hitr,
7598 rows[i].dhitr, rows[i].missr, rows[i].arvr, rows[i].residt, rows[i].cost);
7599 return 0;
7600}
7601
7602/**
7603 * `-a item`: `@@NetworkSolver/getAvgItemTable`, one row per (Cache, item, list).
7604 *
7605 * THE PER-ITEM OCCUPANCY, which only a solver that computes a genuine per-item
7606 * distribution has: the exact NC cache recursions and the delayed-hit retrieval
7607 * algorithms. Every other branch measures the aggregate hit probability and
7608 * never forms the item law, and the arm refuses rather than filling the column
7609 * with the uniform guess that would reproduce the same aggregate.
7610 *
7611 * `Cost` is `Size * Prob`, so summing it over the items of a list reproduces
7612 * that list's ListCost in the cache table -- which is what makes the two tables
7613 * checkable against each other.
7614 */
7615template <class T>
7616int solve_model_item(const std::string& file, const Knobs& k, const std::string& s) {
7617 line::qn::Network<T> net = read_model<T>(file);
7619 std::string banner, suffix;
7620 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
7621 if (r.cache.empty())
7623 "-a item reports the per-item cache occupancy and this model has no Cache node, or "
7624 "the solver that ran analyzes none");
7625
7626 const double dnan = std::numeric_limits<double>::quiet_NaN();
7627 struct Row {
7628 std::string node;
7629 double item, list, listcap, size, prob, cost, dhq, dhqf;
7630 };
7631 std::vector<Row> rows;
7632 for (std::size_t c = 0; c < r.cache.caches.size(); ++c) {
7633 const line::solvers::CacheNodeMetrics<T>& m = r.cache.caches[c];
7634 const std::size_t h = m.itemcap.size();
7635 // EITHER measurement earns the item its rows. A solver may form the
7636 // per-item occupancy (the NC and MVA cache recursions) or the per-item
7637 // delayed-hit queue length (the exact chain) and not the other, and
7638 // requiring both would drop the CTMC's whole table.
7639 if (h == 0 || (m.itemprob.rows() == 0 && m.delayedhitqlen.empty())) continue;
7640 const std::size_t nit =
7641 m.itemprob.rows() > 0 ? m.itemprob.rows() : m.delayedhitqlen.size();
7642 for (std::size_t i = 0; i < nit; ++i)
7643 for (std::size_t l = 0; l < h; ++l) {
7644 Row t;
7645 t.node = sn.nodes[m.node - 1].name;
7646 t.item = static_cast<double>(i + 1);
7647 t.list = static_cast<double>(l + 1);
7648 t.listcap = m.itemcap[l];
7649 t.size = i < m.itemsize.size() ? m.itemsize[i] : dnan;
7650 // Column 0 of `itemprob` is the MISS column, so list l is column
7651 // l+1; reading it as l would report every item one list too low.
7652 t.prob = (l + 1) < m.itemprob.cols()
7654 : dnan;
7655 t.cost = t.size * t.prob;
7656 t.dhq = i < m.delayedhitqlen.size()
7658 : dnan;
7659 t.dhqf = i < m.delayedhitqlenfull.size()
7661 : dnan;
7662 rows.push_back(t);
7663 }
7664 }
7665 if (rows.empty())
7667 "-a item needs a per-item occupancy law and this solve produced none; the NC/MVA "
7668 "cache recursions (isolated and integrated alike) and the delayed-hit retrieval "
7669 "algorithms compute the embedded one, SolverCTMC the time-weighted one, and the "
7670 "simulators none");
7671
7672 if (g_json_output) {
7673 line::reg::Json p = line::reg::Json::object();
7674 p["type"] = "AvgItemTable";
7675 p["indexBase"] = 0;
7676 for (const char* key : {"Node", "Item", "List", "ListCap", "Size", "Prob", "Cost",
7677 "DelayedHitQLen", "DelayedHitQLenFull"})
7678 p[key] = line::reg::Json::array();
7679 for (std::size_t i = 0; i < rows.size(); ++i) {
7680 p["Node"].push_back(rows[i].node);
7681 p["Item"].push_back(rows[i].item);
7682 p["List"].push_back(rows[i].list);
7683 p["ListCap"].push_back(rows[i].listcap);
7684 p["Size"].push_back(rows[i].size);
7685 p["Prob"].push_back(rows[i].prob);
7686 p["Cost"].push_back(rows[i].cost);
7687 p["DelayedHitQLen"].push_back(rows[i].dhq);
7688 p["DelayedHitQLenFull"].push_back(rows[i].dhqf);
7689 }
7690 emit_analysis<T>("item", p, r.actualmethod);
7691 return 0;
7692 }
7693 std::printf("%s arith=%s method=%s rows=%zu\n", banner.c_str(), line::num_traits<T>::name(),
7694 r.actualmethod.c_str(), rows.size());
7695 std::printf("%-14s %6s %6s %8s %10s %12s %12s %14s %18s\n", "Node", "Item", "List", "ListCap",
7696 "Size", "Prob", "Cost", "DelayedHitQLen", "DelayedHitQLenFull");
7697 for (std::size_t i = 0; i < rows.size(); ++i)
7698 std::printf("%-14s %6g %6g %8g %10g %12.8g %12.8g %14.8g %18.8g\n", rows[i].node.c_str(),
7699 rows[i].item, rows[i].list, rows[i].listcap, rows[i].size, rows[i].prob,
7700 rows[i].cost, rows[i].dhq, rows[i].dhqf);
7701 return 0;
7702}
7703
7704/**
7705 * `-a sys`: `@@NetworkSolver/getAvgSysTable`, one row per CHAIN.
7706 *
7707 * SysRespT is the chain's CYCLE TIME and SysTput the flow that completes it,
7708 * both measured at the chain's reference station -- not a column of the
7709 * AvgTable summed up. On a closed chain the two are tied by Little's law and
7710 * the table is the standard capacity-planning view: N = X * R.
7711 */
7712template <class T>
7713int solve_model_sys(const std::string& file, const Knobs& k, const std::string& s) {
7714 line::qn::Network<T> net = read_model<T>(file);
7716 std::string banner, suffix;
7717 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
7719 const std::vector<std::string> cn = line::solvers::chain_names(sn.nchains);
7720 const std::vector<std::string> cc = line::solvers::chain_class_labels<T>(sn);
7721 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
7722
7723 if (g_json_output) {
7724 // The banner under `-o json` too, for print_chain_table's reason: the
7725 // envelope names the arithmetic and the method but never the SOLVER.
7726 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner.c_str(), line::num_traits<T>::name(),
7727 r.actualmethod.c_str(), cn.size(), suffix.c_str());
7728 line::reg::Json p = line::reg::Json::object();
7729 p["type"] = "AvgSysTable";
7730 p["indexBase"] = 0;
7731 for (const char* key : {"Chain", "JobClasses", "SysRespT", "SysTput"})
7732 p[key] = line::reg::Json::array();
7733 for (std::size_t c = 0; c < sn.nchains; ++c) {
7734 p["Chain"].push_back(cn[c]);
7735 p["JobClasses"].push_back(cc[c]);
7736 p["SysRespT"].push_back(d(sys.CN[c]));
7737 p["SysTput"].push_back(d(sys.XN[c]));
7738 }
7739 emit_analysis<T>("sys", p, r.actualmethod);
7740 return 0;
7741 }
7742 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner.c_str(),
7743 line::num_traits<T>::name(), r.actualmethod.c_str(), sn.nchains, suffix.c_str());
7744 std::printf("%-10s %-24s %14s %14s\n", "Chain", "JobClasses", "SysRespT", "SysTput");
7745 for (std::size_t c = 0; c < sn.nchains; ++c)
7746 std::printf("%-10s %-24s %14.6g %14.6g\n", cn[c].c_str(), cc[c].c_str(), d(sys.CN[c]),
7747 d(sys.XN[c]));
7748 return 0;
7749}
7750
7751/** Render a station- or node-level chain table, as text or as the host's JSON. */
7752template <class T>
7753void print_chain_table(const char* key, const char* type, const char* rowlabel,
7754 const std::vector<std::string>& rows,
7755 const std::vector<std::string>& chains,
7756 const std::vector<std::string>& classes,
7757 const line::solvers::ChainResult<T>& t, const std::string& method,
7758 const char* banner, const char* arith, const char* suffix = "") {
7759 auto d = [](const T& v) { return line::num_traits<T>::to_double(v); };
7760 if (g_json_output) {
7761 // THE BANNER IS PRINTED UNDER `-o json` TOO, as the AvgTable path does:
7762 // the envelope names the arithmetic and the method but never the
7763 // SOLVER, so a host that pairs a table with another codebase's by the
7764 // solver in its banner cannot attribute a bannerless one at all. Its
7765 // absence here made `-o json` unusable for these four analyses.
7766 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner, arith, method.c_str(),
7767 chains.size(), suffix);
7768 line::reg::Json p = line::reg::Json::object();
7769 p["type"] = type;
7770 p["indexBase"] = 0;
7771 for (const char* c : {rowlabel, "Chain", "JobClasses", "QLen", "Util", "RespT", "ResidT",
7772 "ArvR", "Tput"})
7773 p[c] = line::reg::Json::array();
7774 // ROW-MAJOR OVER (row, chain), the reference's `(ist-1)*C+c` ordering,
7775 // so a host reading the two codebases' tables side by side indexes them
7776 // the same way.
7777 for (std::size_t i = 0; i < rows.size(); ++i)
7778 for (std::size_t c = 0; c < chains.size(); ++c) {
7779 p[rowlabel].push_back(rows[i]);
7780 p["Chain"].push_back(chains[c]);
7781 p["JobClasses"].push_back(classes[c]);
7782 p["QLen"].push_back(d(t.QN(i, c)));
7783 p["Util"].push_back(d(t.UN(i, c)));
7784 p["RespT"].push_back(d(t.RN(i, c)));
7785 p["ResidT"].push_back(d(t.WN(i, c)));
7786 p["ArvR"].push_back(d(t.AN(i, c)));
7787 p["Tput"].push_back(d(t.TN(i, c)));
7788 }
7789 emit_analysis<T>(key, p, method);
7790 return;
7791 }
7792 std::printf("%s arith=%s method=%s chains=%zu%s\n", banner, arith, method.c_str(),
7793 chains.size(), suffix);
7794 std::printf("%-16s %-10s %-20s %12s %12s %12s %12s %12s %12s\n", rowlabel, "Chain",
7795 "JobClasses", "QLen", "Util", "RespT", "ResidT", "ArvR", "Tput");
7796 for (std::size_t i = 0; i < rows.size(); ++i)
7797 for (std::size_t c = 0; c < chains.size(); ++c)
7798 std::printf("%-16s %-10s %-20s %12.6g %12.6g %12.6g %12.6g %12.6g %12.6g\n",
7799 rows[i].c_str(), chains[c].c_str(), classes[c].c_str(), d(t.QN(i, c)),
7800 d(t.UN(i, c)), d(t.RN(i, c)), d(t.WN(i, c)), d(t.AN(i, c)),
7801 d(t.TN(i, c)));
7802}
7803
7804/**
7805 * `-a chain`: `@@NetworkSolver/getAvgChainTable`, the station table by CHAIN.
7806 *
7807 * EVERY ROW IS EMITTED, including the all-zero ones, unlike the AvgTable and the
7808 * AvgNodeTable. The reference builds this table with a full (M x C) grid and no
7809 * row filter, and a chain that is absent from a station is information -- it is
7810 * the shape of the routing -- where a class absent from a station in the
7811 * AvgTable is only the class's own scope.
7812 */
7813template <class T>
7814int solve_model_chain(const std::string& file, const Knobs& k, const std::string& s) {
7815 line::qn::Network<T> net = read_model<T>(file);
7817 std::string banner, suffix;
7818 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
7820 std::vector<std::string> rows;
7821 for (std::size_t i = 0; i < sn.nstations; ++i) rows.push_back(sn.stations[i].name);
7822 print_chain_table<T>("chain", "AvgChainTable", "Station", rows,
7825 banner.c_str(), line::num_traits<T>::name(), suffix.c_str());
7826 return 0;
7827}
7828
7829/** `-a nodechain`: `@@NetworkSolver/getAvgNodeChainTable`, the node table by CHAIN. */
7830template <class T>
7831int solve_model_nodechain(const std::string& file, const Knobs& k, const std::string& s) {
7832 line::qn::Network<T> net = read_model<T>(file);
7834 std::string banner, suffix;
7835 const line::mva::AvgResult<T> r = run_avg_engine<T>(sn, k, s, banner, &suffix, &file);
7836 const NodeMetrics<T> nm = node_metrics<T>(sn, r);
7838 line::solvers::solver_get_avg_node_chain<T>(sn, nm.QN, nm.UN, nm.RN, nm.WN, nm.AN, nm.TN);
7839 std::vector<std::string> rows;
7840 for (std::size_t i = 0; i < sn.nodes.size(); ++i) rows.push_back(sn.nodes[i].name);
7841 print_chain_table<T>("nodechain", "AvgNodeChainTable", "Node", rows,
7844 banner.c_str(), line::num_traits<T>::name(), suffix.c_str());
7845 return 0;
7846}
7847
7848/** The four @@NetworkSolver tables that are VIEWS of one solved AvgResult. */
7849inline bool is_avg_view(const std::string& analysis) {
7850 return analysis == "node" || analysis == "sys" || analysis == "chain" ||
7851 analysis == "nodechain";
7852}
7853
7854/**
7855 * Dispatch one of those four views at a fixed arithmetic.
7856 *
7857 * The wrapper arms reach the views through this rather than through the shared
7858 * ladder further down: they return before it, having validated their own knobs
7859 * against an engine the ladder knows nothing about. The arithmetic is fixed
7860 * because both wrappers report in double and have already refused every other
7861 * backend by name.
7862 */
7863template <class T>
7864int solve_avg_view(const std::string& file, const Knobs& k, const std::string& s,
7865 const std::string& analysis) {
7866 if (analysis == "node") return solve_model_node<T>(file, k, s);
7867 if (analysis == "sys") return solve_model_sys<T>(file, k, s);
7868 if (analysis == "chain") return solve_model_chain<T>(file, k, s);
7869 return solve_model_nodechain<T>(file, k, s);
7870}
7871
7872int solve_model_dispatch(const std::string& arith, const std::string& solver,
7873 const std::string& analysis, const std::string& file, const Knobs& k) {
7874 std::string s = solver.empty() ? "auto" : solver;
7875 if (s != "mva" && s != "auto" && s != "fluid" && s != "fld" && s != "nc" && s != "mam" &&
7876 s != "ag" && s != "ba" && s != "ssa" && s != "ctmc" && s != "uq" && s != "env" &&
7877 s != "qns" && s != "ldes" && s != "jmt")
7879 "the model-solving path ports -s mva, nc, ctmc, mam, ag, ba, ssa, fluid, ldes, jmt, "
7880 "uq, env and qns (got '" + s + "'); other solvers remain API-only");
7881 // Every solver on this path but JMT and QNS runs in-process, so the flags
7882 // that describe an external solver's child process have nothing to act on.
7883 // Those two do run one, and both write a scratch directory --keep names:
7884 // `solve_model_jmt` forwards it to JmtOptions.keep, which is what leaves
7885 // model.jsim behind, and refusing it here made the one document a parity
7886 // difference has to be read from unobtainable.
7887 if ((k.verbose && s != "ldes") || k.remote || !k.remote_url.empty() ||
7888 (k.keep && s != "qns" && s != "jmt"))
7890 "--keep, --verbose, --remote and --remote-url describe the child process of an "
7891 "external solver; on this path -s jmt and -s qns run one and take --keep, -s ldes "
7892 "runs one and takes --verbose (which echoes the resolved engine command line), and "
7893 "no path takes --remote or --remote-url");
7894 if (k.timeout_seconds && s != "qns")
7896 "--timeout is the deadline of an external solver's child process; on this path only "
7897 "-s qns runs one");
7898 // --fork-join names an arm of the fork-join FIXED POINT, which only the
7899 // mean-value arms drive: a simulator walks the fork on its sample path and
7900 // a CTMC enumerates it, so neither has a transform to choose. Refused by
7901 // name rather than ignored, which would report the default arm's numbers
7902 // under the caller's choice.
7903 if (!k.fork_join.empty() && s != "mva" && s != "nc")
7905 "--fork-join selects the fork-join transform of the shared mean-value fixed point "
7906 "and is read by -s mva and -s nc; -s " + s +
7907 " either simulates or enumerates the fork and applies no transform");
7908 // REFUSED BY NAME RATHER THAN IGNORED, on the same grounds as --fork-join
7909 // above: a knob silently dropped reports the DEFAULT arm's numbers under
7910 // the caller's choice, which is the one outcome stating the flag exists to
7911 // rule out.
7912 if (k.warmupfrac >= 0.0 && s != "ssa")
7914 "--warmupfrac discards a leading fraction of a SIMULATED path before the means are "
7915 "taken and is read by -s ssa; -s " + s +
7916 " has no path to discard (the LDES engine takes --ldes-warmupfrac)");
7917 if (k.pstar > 0.0 && s != "fluid" && s != "fld")
7919 "--pstar is the exponent of the fluid p-norm smoothing of the drift and is read by "
7920 "-s fluid; -s " + s + " integrates no drift");
7921 if ((!k.busy_orders.empty() || !k.busy_subnet.empty()) && analysis != "busyperiod")
7923 "--busyperiod and --busyperiod-subnet name the orders and the subnetwork of "
7924 "-a busyperiod; got -a " + analysis);
7925 // Solver console: this dispatcher is the single point every model-solving
7926 // arm passes through, so the narrated run is opened here and closed by the
7927 // guard's destructor -- on an exception too, so a failed analysis still
7928 // reports what it had reached. The model name is not known before the file
7929 // is read, so the header names the file's model once the struct compiles.
7930 line::util::LineConsole::Run consoleRun(upper_tag(s), "", true);
7931
7932 // ---- SolverUQ, BEFORE the per-solver knob ladder ----------------------
7933 // It is a wrapper, not an engine: `--method`, `--samples` and `--seed`
7934 // describe its DESIGN and the convergence knobs belong to whatever
7935 // `--uq-solver` names, so the ladder below -- which asks "does THIS solver
7936 // have a sample count" -- answers about the wrong solver here.
7937 if (s == "uq") {
7938 if (analysis != "avg" && analysis != "posterior" && analysis != "interval")
7940 "SolverUQ ports -a avg (the prior-weighted expectation), -a posterior (the "
7941 "per-design-point table) and -a interval (the support-only range); got '" +
7942 analysis + "'");
7943 if (k.uq_solver.empty())
7944 throw line::InputError(
7945 "-s uq needs --uq-solver: UQ computes nothing itself, it expands the Prior and "
7946 "runs another solver at each design point (the C++ spelling of UQ(model, "
7947 "@SolverMVA)). Naming one here by default would attribute the numbers to an "
7948 "engine the caller never chose");
7949 if (k.t1 >= 0.0 || k.node || !k.notation.empty())
7951 "--tspan, --node and --notation name a transient horizon, a stateful node and an "
7952 "ODE document; SolverUQ reports steady-state means over a design of models and "
7953 "has none of the three");
7954 if (k.no_interlocking || k.repeat > 0 || !k.layer_solver.empty())
7956 "--no-interlocking, --repeat and --layer-solver are options of the layered solver "
7957 "and apply to -i lqnx; a Network model has no layers to interlock");
7958 if (arith == "double") return solve_model_uq<double>(file, k, analysis);
7959 if (arith == "exact") return solve_model_uq<line::Rational>(file, k, analysis);
7960 if (arith == "real:16") return solve_model_uq<line::Real<16> >(file, k, analysis);
7961 if (arith == "real" || arith == "real:32")
7962 return solve_model_uq<line::Real<32> >(file, k, analysis);
7963 if (arith == "real:64") return solve_model_uq<line::Real<64> >(file, k, analysis);
7964 if (arith == "real:128") return solve_model_uq<line::Real<128> >(file, k, analysis);
7965 if (arith == "real:256") return solve_model_uq<line::Real<256> >(file, k, analysis);
7966 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
7967 }
7968 // ---- SolverENV, also BEFORE the per-solver knob ladder ----------------
7969 // It reads a DIFFERENT MODEL TYPE: an Environment envelope, whose stages
7970 // each hold a Network. The ladder below asks its questions of one Network
7971 // model -- `--cutoff` per open class, `--node` on a stateful node -- and
7972 // the model-level knobs it would validate belong to the STAGE solver here,
7973 // so ENV states its own refusals and routes around it.
7974 if (s == "env") {
7975 if (analysis != "avg")
7977 "SolverENV reports -a avg, the environment-blended means; getEnsembleAvg is its "
7978 "only metric entry in the reference too (got '" + analysis + "')");
7979 if (k.samples || k.seed)
7981 "--samples and --seed describe a simulation; SolverENV iterates a fixed point over "
7982 "transient stage solves and draws nothing");
7983 // `--cutoff` IS ADMITTED WITH `--stage-solver ctmc`, and only then: every
7984 // stage is then enumerated, and an open stage's chain has to be
7985 // truncated somewhere. It stays refused for a fluid ensemble, which
7986 // enumerates nothing.
7987 if (k.has_cutoff() && k.stage_solver != "ctmc")
7989 "--cutoff bounds the open population of an enumerated state space and applies to "
7990 "-s env only beside --stage-solver ctmc; the fluid stages of this ensemble "
7991 "enumerate no states");
7992 if (k.node || !k.notation.empty())
7994 "--node and --notation name a stateful node and an ODE document of ONE network; "
7995 "an Environment holds a network per stage and "
7996 "SolverENV reports the blend over them");
7997 if (k.no_interlocking || k.repeat > 0 || !k.layer_solver.empty())
7999 "--no-interlocking, --repeat and --layer-solver are options of the layered solver "
8000 "and apply to -i lqnx; an Environment has stages, not layers");
8001 if (k.t0 != 0.0)
8003 "--tspan on the ENV path states the transient HORIZON each stage solve integrates "
8004 "to, and every stage starts from its entry state at 0; a nonzero t0 would name a "
8005 "start the coupling has no state for");
8006 // The mean-field coupling integrates the stage drift with LSODA, which
8007 // is double; the state-vector one uniformizes a CTMC and carries the
8008 // whole ladder. Narrowing silently would report an `exact` banner over
8009 // a double solve, so the refusal names the coupling that decided it.
8010 const std::string coupling =
8011 (k.method.empty() || k.method == "default") ? "meanfield" : k.method;
8012 if (k.tran_points && (coupling == "statevec" || coupling == "blend"))
8014 "--tran-points is the mean-field coupling's quadrature grid; the state-vector "
8015 "coupling carries the whole joint law across a switch and sums over no such grid, "
8016 "so the value would be accepted and never used");
8017 // `statedep` is a C++-API method and not a file one: it needs a rate
8018 // function PER ARC (`Environment::set_env_rate_reset`), which is a
8019 // function of the stage exit metrics and has no representation in
8020 // model.json -- the reference cannot serialize `resetEnvRatesFun`
8021 // either. Reaching it from a file would find no hook and refuse deeper
8022 // in, with a message about an environment the caller never wrote.
8023 if (coupling == "statedep")
8025 "--method statedep makes each environment transition depend on the state its "
8026 "stage is left in, through a rate function per arc that no model.json can carry "
8027 "(the reference cannot serialize resetEnvRatesFun either); it is reachable from "
8028 "the C++ API, through Environment::set_env_rate_reset");
8029 if ((k.tran_points || k.t1 >= 0.0) && (coupling == "avg" || coupling == "dec"))
8031 "--tran-points and --tspan state the grid and the horizon of a TRANSIENT stage "
8032 "solve; the closed-form limits --method avg and --method dec solve in steady "
8033 "state and carry nothing across a switch, so both would be accepted and never "
8034 "used");
8035 if (arith != "double" && coupling != "statevec" && coupling != "blend")
8037 "SolverENV solves a stage with the fluid analyzer on every method but the "
8038 "state-vector one -- the mean-field coupling transiently, the avg and dec limits "
8039 "in steady state -- and that analyzer is LSODA's and therefore double; --arith " +
8040 arith +
8041 " reaches ENV only through the state-vector coupling (--method statevec or --method blend)");
8042 if (arith == "double") return solve_model_env<double>(file, k);
8043 if (arith == "exact") return solve_model_env<line::Rational>(file, k);
8044 if (arith == "real:16") return solve_model_env<line::Real<16> >(file, k);
8045 if (arith == "real" || arith == "real:32")
8046 return solve_model_env<line::Real<32> >(file, k);
8047 if (arith == "real:64") return solve_model_env<line::Real<64> >(file, k);
8048 if (arith == "real:128") return solve_model_env<line::Real<128> >(file, k);
8049 if (arith == "real:256") return solve_model_env<line::Real<256> >(file, k);
8050 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8051 }
8052 if (!k.uq_solver.empty())
8054 "--uq-solver names the engine SolverUQ runs at each design point and applies to -s uq; "
8055 "'" + s + "' solves one model, not a design of them");
8056 if (k.tran_points)
8058 "--tran-points is the resolution of the transient grid SolverENV sums its stage exit "
8059 "metrics over and applies to -s env; '" + s + "' has no such quadrature");
8060 // ---- SolverLDES, BEFORE the per-solver knob ladder --------------------
8061 // It hands the model DOCUMENT to an external engine instead of reading it,
8062 // so the ladder below -- which validates knobs against a parsed struct --
8063 // asks its questions of a model this path never parses. LDES states its own
8064 // refusals here for the same reason UQ and ENV state theirs.
8065 const bool ldes_knob = !k.ldes_tranfilter.empty() || k.ldes_warmupfrac >= 0.0 ||
8066 !k.ldes_cimethod.empty() || k.ldes_cnvgon || k.ldes_slotted ||
8067 k.ldes_replications > 0 || k.ldes_numthreads > 0 ||
8068 k.ldes_maxtime > 0.0 || !k.ldes_initsol.empty() ||
8069 !k.ldes_rest_url.empty();
8070 if (ldes_knob && s != "ldes" && s != "auto")
8072 "the --ldes-* flags are the discrete-event engine's own settings (warmup filter, "
8073 "confidence-interval estimator, slot lattice, replications, warm-start placement) and "
8074 "apply to -s ldes; '" + s + "' has none of them");
8075 if (s == "jmt") {
8076 if (arith != "double")
8078 "SolverJMT is a client of the Java Modelling Tools engine, which simulates in "
8079 "double and reports in double; --arith " + arith +
8080 " would label a double answer with an arithmetic that never touched it");
8081 if (analysis != "avg" && analysis != "cdf" && analysis != "trancdf" &&
8082 analysis != "trancdfpasst" && analysis != "prob" && !is_avg_view(analysis))
8084 "-s jmt reports -a avg (the JSIM or JMVA mean table), its four views -a node, "
8085 "-a sys, -a chain and -a nodechain, -a cdf (the empirical response-time law read "
8086 "back from the JMT logs, preloaded at the rounded steady-state queue lengths), "
8087 "-a tran-cdf-respt and -a tran-cdf-passt (the same logged run from the default "
8088 "initial state, so the samples cover the transient) and -a prob (the time each "
8089 "declared state is held for along the logged trajectory); -a tranprob needs one "
8090 "run per replication and is not exposed here");
8091 const std::vector<std::string> valid = line::jmt::jmt_list_valid_methods();
8092 if (!k.method.empty() &&
8093 std::find(valid.begin(), valid.end(), k.method) == valid.end())
8095 "SolverJMT methods are default, jsim and the jmva family (jmva, jmva.amva, "
8096 "jmva.mva, jmva.recal, jmva.comom, jmva.chow, jmva.bs, jmva.aql, jmva.lin, "
8097 "jmva.dmlin); got '" + k.method + "'");
8098 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0 || !k.multiserver.empty())
8100 "--tol, --iter_tol, --iter_max and --multiserver configure a fixed-point "
8101 "iteration; JSIM simulates a sample path and JMVA takes its tolerance from the "
8102 "exported document. The simulation's stopping rule is --samples");
8103 if (k.has_cutoff())
8105 "--cutoff truncates an enumerated state space; JMT enumerates none");
8106 // `--node` NAMES THE STATION `--state` OVERRIDES, and nothing else: the
8107 // JMT arms report tables over every station and class, and `-a prob`
8108 // does too. The pair is `getProbAggr(node, state_a)`'s two arguments,
8109 // where the reference substitutes the given counts into that station's
8110 // row and leaves every other station at its declared state.
8111 if ((k.node && analysis != "prob") || k.jobclass || !k.marg_states.empty())
8113 "--node, --class and --marg-states select the marginal law of one (node, class); "
8114 "the JMT arms report tables over every station and class, and -a prob takes "
8115 "--node only to say which station --state overrides");
8116 if (!k.state.empty() && analysis != "prob")
8118 "--state names the state a probability is asked about and applies to -a prob");
8119 if (!k.state.empty() && !k.node)
8121 "--state is the per-class job count of ONE station and needs --node to say "
8122 "which; a bare count vector cannot be matched against a whole network");
8123 if (!k.notation.empty() || !k.symbolic.empty() || k.equilibria)
8125 "--notation, --symbolic and --equilibria describe an exported ODE document; JMT "
8126 "integrates no ODE");
8127 if (!k.cdf_algorithm.empty())
8129 "--cdf-algorithm selects between the two sojourn-time INVERSIONS of -s nc; the "
8130 "JMT response-time law is the ecdf of the passages its loggers recorded and is "
8131 "not computed from a transform");
8132 if (is_avg_view(analysis)) return solve_avg_view<double>(file, k, "jmt", analysis);
8133 return solve_model_jmt(file, k, analysis);
8134 }
8135 if (s == "ldes") {
8136 if (arith != "double")
8138 "SolverLDES is a client of the SSJ engine, which simulates in double and reports "
8139 "in double; --arith " + arith +
8140 " would label a double answer with an arithmetic that never touched it");
8141 // 'parallel' asks the engine for INDEPENDENT REPLICATIONS and the mean
8142 // over them, which is what its parallel analyzer is; it is not a second
8143 // engine. It resolves to a replication count here, taking --ldes-
8144 // replications when given and 8 otherwise -- the same default the SSA
8145 // parallel analyzer uses. Mirrors SolverLDES.listValidMethods in every
8146 // codebase, which advertises exactly {default, parallel}.
8147 if (!k.method.empty() && k.method != "default" && k.method != "parallel")
8149 "SolverLDES has two methods, 'default' and 'parallel' (listValidMethods returns "
8150 "exactly those in every codebase); got '" + k.method + "'");
8151
8152 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0 || !k.multiserver.empty())
8154 "--tol, --iter_tol, --iter_max and --multiserver are the knobs of a fixed-point "
8155 "iteration; LDES simulates a sample path and iterates nothing. Its stopping rule "
8156 "is --samples, or --ldes-cnvgon with --ldes-cnvgtol");
8157 if (k.has_cutoff())
8159 "--cutoff truncates an enumerated state space; a simulator visits the states the "
8160 "sample path reaches and enumerates none");
8161 if (k.node || k.jobclass || !k.marg_states.empty())
8163 "--node, --class and --marg-states select a marginal law of one (node, class); the "
8164 "LDES arms report tables over every station and class, and -a prob is refused for "
8165 "want of a target state");
8166 if (!k.notation.empty() || !k.symbolic.empty() || k.equilibria)
8168 "--notation, --symbolic and --equilibria describe an exported ODE document; LDES "
8169 "integrates no ODE");
8170 if (!k.cdf_algorithm.empty())
8172 "--cdf-algorithm selects between the two sojourn-time INVERSIONS of -s nc; the "
8173 "LDES response-time law is the ecdf of the samples the engine recorded and is not "
8174 "computed from a transform");
8175 if (k.no_interlocking || k.repeat > 0 || !k.layer_solver.empty())
8177 "--no-interlocking, --repeat and --layer-solver are options of the layered solver "
8178 "and apply to -i lqnx; the LDES layered path runs in the JAR's own ensemble "
8179 "backend and has no JSON interface to reach from here");
8180 if (!k.sens_method.empty() || !k.sens_scheme.empty() || k.sens_step >= 0.0)
8182 "--sens-method, --sens-scheme and --sens-step configure the layered sensitivity "
8183 "table; LDES reports no sensitivity");
8184 if (analysis == "tran" && !(k.t1 >= 0.0))
8185 throw line::InputError(
8186 "-s ldes -a tran needs --tspan <t1> or --tspan <t0>:<t1>: a trajectory over an unstated "
8187 "horizon is not a quantity, and the engine only records buckets once a timespan "
8188 "makes the run transient");
8189 if (k.t1 >= 0.0 && analysis != "tran")
8191 "--tspan names the horizon of -a tran; -a sample runs over [0, --samples], which "
8192 "is the horizon runTransientJson uses, and the other arms are steady state");
8193 // `-a tran-cdf-*` NAMES THE TRANSIENT LAW AND IS THE STEADY-STATE ONE
8194 // HERE, because a simulator has only the samples it observed: the
8195 // reference's `getTranCdfRespT` reads the same `respTimeSamples` its
8196 // `getCdfRespT` reads, and its `getTranCdfPassT` is a one-line delegation
8197 // to `getTranCdfRespT`. Warned rather than refused, so a script written
8198 // against the JAR runs and its author is told what the curve is.
8199 if ((analysis == "trancdf" || analysis == "trancdfpasst") &&
8200 k.verbosity != "silent")
8201 std::fprintf(stderr,
8202 "Warning: -a %s is the ecdf of the per-job response times the run "
8203 "observed, the same curve -a cdf reports; the reference's LDES "
8204 "getTranCdfRespT reads the same samples\n",
8205 analysis.c_str());
8206 // method='parallel' resolved to its replication count, after every
8207 // other knob has been validated against the caller's own Knobs.
8208 Knobs kldes = k;
8209 if (kldes.method == "parallel" && kldes.ldes_replications <= 1) kldes.ldes_replications = 8;
8210 if (is_avg_view(analysis)) return solve_avg_view<double>(file, kldes, "ldes", analysis);
8211 return solve_model_ldes(file, kldes, analysis);
8212 }
8213 // SolverAUTO resolves to a real engine BEFORE the knob checks below, so a
8214 // model the chooser sends to SSA accepts --samples and one it sends to CTMC
8215 // accepts --cutoff: the checks must see the solver that will actually run.
8216 if (s == "auto") {
8217 // `chooseSolverHeur` picks an engine from a GETTER, and the age laws are
8218 // not one of its getters: a model whose table it would send to MVA does
8219 // not thereby have an AoI answer. Refusing by name here beats letting
8220 // the chosen engine refuse an analysis it was never asked about.
8221 if (analysis == "aoi")
8223 "-a aoi is the AoI branch of the fluid 'mfq' method and no other engine reports "
8224 "it, so SolverAUTO does not choose for it: ask for it by name with -s fluid");
8225 const AutoPlan plan = choose_auto_plan_dispatch(arith, file, analysis, k.method);
8226 // `delegate.m` runs the chosen solver and, when it fails, every other
8227 // feasible candidate in slot order. Reproduced here by re-entering this
8228 // dispatch with a CONCRETE method name, so each attempt is validated against
8229 // the knobs of the solver that will actually run it. Only the first
8230 // attempt carries the method the ranking gated on ('exact'): a method
8231 // name is a solver's own vocabulary and does not travel to the next.
8232 if (plan.order.size() == 1) {
8233 // A forced method name (a method family, or the Environment envelope)
8234 // leaves ONE solver, and its own diagnostic is then the whole
8235 // story: the reference rethrows it rather than wrapping it.
8236 Knobs kk = k;
8237 kk.method = plan.method;
8238 std::printf("SolverAUTO selected %s%s\n", plan.order[0].c_str(), plan.note.c_str());
8239 return solve_model_dispatch(arith, plan.order[0], analysis, file, kk);
8240 }
8241 std::string first_error;
8242 for (std::size_t i = 0; i < plan.order.size(); ++i) {
8243 Knobs kk = k;
8244 kk.method = (i == 0) ? plan.method : std::string();
8245 if (i == 0)
8246 std::printf("SolverAUTO selected %s%s\n", plan.order[i].c_str(),
8247 plan.note.c_str());
8248 else
8249 std::printf("SolverAUTO retrying with %s\n", plan.order[i].c_str());
8250 try {
8251 return solve_model_dispatch(arith, plan.order[i], analysis, file, kk);
8252 } catch (const line::UnsupportedError& e) {
8253 if (first_error.empty()) first_error = plan.order[i] + ": " + e.what();
8254 std::printf("SolverAUTO: %s cannot serve this run (%s)\n", plan.order[i].c_str(),
8255 e.what());
8256 }
8257 }
8259 "SolverAUTO: every candidate refused this run. The chosen engine reported -- " +
8260 first_error);
8261 }
8262 // ---- knobs the CHOSEN solver does not have are refused, not dropped ----
8263 // Accepting an option and discarding it is the one place the port would
8264 // answer a question it was not asked: the caller believes a value the
8265 // solver never saw. Every refusal below names the option and the solver.
8266 const bool is_sim = (s == "ssa");
8267 // `-s ctmc -a sample` walks the chain with an exponential clock, so it has a
8268 // run length and a stream in the same sense a simulation does; every other
8269 // CTMC analysis is a solve and still refuses both.
8270 // The cftp methods draw iid stationary states, so they too have a run length
8271 // and a stream; unlike -a sample the draw is the ANSWER there, which is why
8272 // --samples is required rather than defaulted for them.
8273 const bool is_cftp =
8274 (s == "ctmc" && (k.method == "cftp" || k.method == "cftp.approx"));
8275 const bool draws_samples = is_sim || (s == "ctmc" && analysis == "sample") || is_cftp;
8276 if (k.samples && !draws_samples)
8277 throw line::UnsupportedError("--samples applies to the simulation solver (-s ssa), to "
8278 "-s ctmc -a sample and to -s ctmc --method cftp; '" + s +
8279 "' has no sample count");
8280 if (k.seed && !draws_samples)
8281 throw line::UnsupportedError("--seed applies to the simulation solver (-s ssa), to "
8282 "-s ctmc -a sample and to -s ctmc --method cftp; '" + s +
8283 "' draws no random numbers");
8284 // --mdd-tol / --mdd-maxiter drive the level iteration, which only the mdd
8285 // method runs. Accepting them elsewhere would let a caller believe a
8286 // tolerance was applied to a solve that has no iteration in it.
8287 if ((k.mdd_tol > 0.0 || k.mdd_maxiter > 0) && !(s == "ctmc" && k.method == "mdd"))
8289 "--mdd-tol and --mdd-maxiter set the coupled level iteration of -s ctmc --method mdd; "
8290 "'" + s + " / " + (k.method.empty() ? std::string("default") : k.method) +
8291 "' iterates no levels");
8292 // --tspan names a transient horizon, which only the CTMC transient analyses
8293 // have; accepting it elsewhere would let a caller believe a horizon was used.
8294 // The fluid solver integrates a forward equation too, and its horizon is what
8295 // `kp` reports its covariance AT, so --tspan reaches it as well; every other
8296 // fluid method restarts from its own end state until the moved mass stops
8297 // changing, and a horizon there caps that iteration rather than naming a time.
8298 if (k.t1 >= 0.0 &&
8299 !(s == "ctmc" &&
8300 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")) &&
8301 !(s == "fluid" || s == "fld") && !(s == "mam" && analysis == "tran"))
8303 "--tspan sets the horizon of a transient analysis and applies to -s ctmc -a tran, "
8304 "-a tranprob and -a tranreward, to -s mam -a tran and to -s fluid; '" + s + " / " +
8305 analysis + "' integrates no forward equation");
8306 // --node narrows an answer to one node's block of the state, which only the
8307 // per-node CTMC queries and the per-node MAM queue-length law have; every
8308 // other analysis reports the whole network.
8309 // `-s ctmc -a prob` takes it TOGETHER WITH --state and only then: the arm
8310 // reports every station's marginal, so a bare --node would narrow nothing,
8311 // while --state names a row of ONE node's own space and needs --node to say
8312 // whose.
8313 if (k.node && !(s == "ctmc" && (analysis == "tranprob" || analysis == "sample")) &&
8314 !(s == "ctmc" && analysis == "prob" && !k.state.empty()) &&
8315 !(s == "nc" && analysis == "prob" && !k.state.empty()) &&
8316 !(s == "ssa" && analysis == "sample") && !(s == "mam" && analysis == "prob") &&
8317 !((s == "mva" || s == "nc") && analysis == "marg"))
8319 "--node selects the stateful node a state query is labelled by and applies to -s ctmc "
8320 "-a tranprob and -a sample, to -s ctmc|nc -a prob beside --state, to -s ssa -a sample, "
8321 "to -s mam -a prob and to -s mva|nc -a marg; '" + s + " / " + analysis +
8322 "' reports the whole network");
8323 // --class and --marg-states are the remaining two arguments of getProbMarg,
8324 // and nothing else in the surface takes either: every other analysis reports
8325 // all classes, and no other law is evaluated at a caller-chosen job count.
8326 if ((k.jobclass || !k.marg_states.empty()) && !(s == "mva" && analysis == "marg"))
8328 "--class and --marg-states are the job class and the state list of getProbMarg and "
8329 "apply to -s mva -a marg; '" + s + " / " + analysis +
8330 "' reports every class over its own range");
8331 // --notation selects which document the ODE export writes, and only the
8332 // export writes one; every other analysis reports numbers, which have no
8333 // notation to choose.
8334 if (!k.notation.empty() && !((s == "fluid" || s == "fld") && analysis == "odes"))
8336 "--notation selects the form of the exported ODE document and applies to -s fluid -a "
8337 "odes; '" + s + " / " + analysis + "' exports no equations");
8338 // --symbolic and --equilibria select the computer-algebra backend and ask it
8339 // to solve f(x) = 0; only the Jacobian consults one.
8340 if ((!k.symbolic.empty() || k.equilibria) &&
8341 !((s == "fluid" || s == "fld") && analysis == "jacobian"))
8343 "--symbolic selects the computer-algebra backend and --equilibria asks it for the "
8344 "solutions of f(x) = 0; both apply to -s fluid -a jacobian, and '" + s + " / " +
8345 analysis + "' consults no backend");
8346 // ---- the five JAR knobs, each refused where it would be accepted and never
8347 // read. Same discipline as every knob above: a caller who passed one to an
8348 // arm that does not consult it would believe a setting had been applied.
8349 // `-s jmt -a prob` reads it too, and reads it DIFFERENTLY: an exact chain is
8350 // indexed by the ENCODED row of a node's own state space, while a JMT log
8351 // records per-class job counts and nothing else, so there the vector is
8352 // `getProbAggr(node, state_a)`'s per-class count. Both are "the state this
8353 // probability is about"; which encoding it is in follows the solver.
8354 if (!k.state.empty() &&
8355 !(analysis == "prob" && (s == "ctmc" || s == "auto" || s == "jmt" || s == "nc")))
8357 "--state names the state `getProb(node, state)` asks about and applies to -a prob "
8358 "under -s ctmc, -s nc and -s jmt, the arms whose answer is indexed by a state; '" + s +
8359 " / " + analysis + "' reports a mean or a law over all of them");
8360 if (k.events && analysis != "sample")
8362 "--events is the length of ONE sampled trajectory and applies to -a sample; use "
8363 "--samples for a solver's run length ('" + s + " / " + analysis + "')");
8364 if (!k.percentiles.empty() && !(s == "mam" && (analysis == "cdf" || analysis == "cdfpasst" ||
8365 analysis == "perct")))
8367 "--percentiles names the levels getPerctRespT is read at and applies to -s mam -a "
8368 "perct-respt (and to the percentiles printed beside -a cdf); '" + s + " / " +
8369 analysis + "' inverts no response-time law");
8370 if (!k.reward_name.empty() && analysis != "rewardvalue")
8372 "--reward-name selects which declared reward -a reward-value returns the value "
8373 "function of; -a reward returns every reward's steady-state expectation and needs no "
8374 "name ('" + s + " / " + analysis + "')");
8375 if (k.timestep > 0.0 &&
8376 !(s == "ctmc" &&
8377 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")))
8379 "--timestep is the fixed output grid of a transient CTMC solve, `options.timestep` of "
8380 "ctmc_transient.m, and applies to -s ctmc -a tran, -a tranprob and -a tranreward; the "
8381 "fluid "
8382 "and simulated transients report the points their own integrator or engine produced "
8383 "('" + s + " / " + analysis + "')");
8384 if ((!k.transient_method.empty() || k.fau_epsilon > 0.0 || k.fau_delta >= 0.0) &&
8385 !(s == "ctmc" &&
8386 (analysis == "tran" || analysis == "tranprob" || analysis == "tranreward")))
8388 "--transient-method (and --fau-epsilon / --fau-delta) selects how the CTMC forward "
8389 "equation is advanced, `options.config.transient_method` of "
8390 "solver_ctmc_transient_analyzer.m, and applies to -s ctmc -a tran, -a tranprob and "
8391 "-a tranreward; every other analysis solves no forward equation ('" +
8392 s + " / " + analysis + "')");
8393 // --cdf-algorithm selects how the sojourn law is inverted, which only the NC
8394 // response-time distribution does; the CTMC one is read off tagged chains
8395 // and has no such choice.
8396 if (!k.cdf_algorithm.empty() && !(s == "nc" && analysis == "cdf"))
8398 "--cdf-algorithm selects the sojourn-time inversion of the NC response-time "
8399 "distribution and applies to -s nc -a cdf; '" + s + " / " + analysis +
8400 "' inverts no generating function");
8401 // The passage flags name the two state sets of getCdfFirstPassT and of
8402 // getFirstPassTMoments, which are the only two arms that time a state-set
8403 // passage. --passage-method selects the inversion and so belongs to the
8404 // curve alone; the moments involve no inversion at all.
8405 const bool passage_arm =
8406 (s == "ctmc" && (analysis == "firstpasst" || analysis == "firstpasstmom"));
8407 if ((!k.passage_from.empty() || !k.passage_into.empty() || k.passage_orders > 0) &&
8408 !passage_arm)
8410 "--passage-from, --passage-into and --passage-orders name the state sets and the "
8411 "moment order of -s ctmc -a firstpasst / firstpasstmom; '" + s + " / " + analysis +
8412 "' times no state-set passage");
8413 if (!k.passage_method.empty() && !(s == "ctmc" && analysis == "firstpasst"))
8415 "--passage-method selects the transform inversion of -s ctmc -a firstpasst; '" + s +
8416 " / " + analysis + "' inverts none (the moments arm solves for them directly)");
8417 // --perm-engine selects the permanent estimator, which only the NC joint law
8418 // of the per-station totals uses; nothing else in the tree evaluates one.
8419 if (k.method_perm != "exact" && !(s == "nc" && analysis == "sysmarg"))
8421 "--perm-engine selects the permanent estimator of the NC joint total-queue-length "
8422 "law and applies to -s nc -a sysmarg; '" + s + " / " + analysis +
8423 "' evaluates no permanent");
8424 // The layered path's own knobs, refused here for the same reason every other
8425 // knob above is: a caller who passed one to a Network solve would believe a
8426 // setting was applied that no Network solver has. --sens-* is the exception:
8427 // getSensitivityTable is a @@NetworkSolver method, so it selects the branch
8428 // of `-s nc -a sens` as much as of the layered one.
8429 const bool nc_sens = s == "nc" && analysis == "sens";
8430 if (!nc_sens && (!k.sens_method.empty() || !k.sens_scheme.empty() || k.sens_step > 0.0))
8432 "--sens-method, --sens-scheme and --sens-step select the branch of a sensitivity "
8433 "table and apply to -i lqnx -a sens or to -s nc -a sens; '" + s + " / " + analysis +
8434 "' differentiates nothing");
8435 if (k.no_interlocking || k.repeat > 0 || !k.layer_solver.empty() ||
8436 !k.ln_transient.empty() || !k.ln_transient_channels.empty())
8438 "--no-interlocking, --repeat, --layer-solver and --ln-transient* are options "
8439 "of the layered solver and apply to -i lqnx; a Network model has no layers to "
8440 "interlock");
8441 if (s == "ba" && (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0))
8443 "--tol, --iter_tol and --iter_max do not apply to -s ba: a bound is a closed form, "
8444 "with nothing to converge");
8445 // Silent acceptance is the defect these guard against: a caller who passed
8446 // a QRF table to another solver would believe a parameterisation was
8447 // applied that nothing read.
8448 if (s != "ba" && (!k.qrf_params.empty() || !k.qrf_alpha.empty()))
8450 "--qrf-params and --qrf-alpha parameterise the QRF reduction bounds and apply to "
8451 "-s ba; '" + s + "' solves no reduction program");
8452 if (s != "ba" && k.level > 0)
8454 "--level is the hierarchy level of the SolverBA bound families and applies to -s ba; "
8455 "'" + s + "' has no bound hierarchy");
8456 if (is_sim && (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0))
8458 "--tol, --iter_tol and --iter_max do not apply to -s ssa: a sample path is not an "
8459 "iteration; use --samples to set its length");
8460 if (s == "mam" && k.iter_tol >= 0.0)
8462 "--iter_tol is not a SolverMAM option (MamOptions carries tol and iter_max); "
8463 "use --tol");
8464 if (s == "ag" && k.iter_tol >= 0.0)
8466 "--iter_tol is not a SolverAG option (AgOptions carries tol and iter_max, the "
8467 "tolerance and the sweep budget of the reversed-rate fixed point); use --tol");
8468 // --max-states BOUNDS AN OPEN AGENT'S QUEUE-LENGTH DIMENSION, and only the
8469 // RCAT agents have one: a closed class is bounded by its own population
8470 // instead, and 'inapinf' ignores the level entirely and solves the open
8471 // agents on the infinite state space. Accepting it elsewhere would be the
8472 // silent-acceptance defect these guards exist for -- a caller who passed it
8473 // to -s ctmc would believe a truncation applied that nothing truncated.
8474 if (k.max_states >= 0 && s != "ag")
8476 "--max-states truncates the queue-length dimension of a SolverAG agent and applies "
8477 "to -s ag; '" + s + "' truncates no agent (use --cutoff for a CTMC state space)");
8478 // The cutoff BOUNDS A STATE SPACE, and only SolverCTMC and the MAM
8479 // queue-length law have one -- the latter because an OPEN queue's level
8480 // process is unbounded and `getProb` has to stop somewhere. Accepting it
8481 // elsewhere would be the silent-acceptance defect: a caller who passed it to
8482 // -s mva would believe the answer was truncated when nothing truncated it.
8483 if (k.has_cutoff() && s != "ctmc" && !(s == "mam" && analysis == "prob") && s != "env")
8485 "--cutoff bounds the open population of a CTMC state space, and the level truncation "
8486 "of -s mam -a prob; '" + s + " / " + analysis + "' enumerates no states");
8487 // THE MATRIX SPELLING IS NARROWER THAN THE SCALAR ONE. Only the CTMC state
8488 // space is enumerated per station, so only it can honour a per-station
8489 // bound; the MAM level truncation and an environment's stage cutoff are one
8490 // number each. Refused rather than reduced to a maximum, because that
8491 // silently answers a LARGER chain than the caller asked for.
8492 if (!k.cutoff_mat.empty() && s != "ctmc")
8494 "--cutoff as a per-(station,class) matrix bounds an enumerated state space per "
8495 "station and applies to -s ctmc; '" + s + "' takes one number");
8496 // `--stage-solver` names the solver each STAGE of an environment is run
8497 // with, and nothing else has stages: a layer of an LQN takes
8498 // `--layer-solver`, which is a different set for a different reason (a
8499 // layer is solved in steady state, a stage transiently).
8500 if (!k.stage_solver.empty() && s != "env")
8502 "--stage-solver names the solver each stage of a random environment is run with and "
8503 "applies to -s env; '" + s + "' has no stages");
8504 if (!k.stage_solver.empty() && k.stage_solver != "fluid" && k.stage_solver != "ctmc" &&
8505 k.stage_solver != "mam")
8507 "--stage-solver '" + k.stage_solver +
8508 "' is not available: the environment coupling needs a TRANSIENT stage solve, and only "
8509 "the fluid analyzer, the enumerated CTMC and the flattened LD-QBD provide one in this "
8510 "port");
8511 // `mam` IS THE STATE-VECTOR COUPLING'S BACKEND ONLY. The mean-field one
8512 // carries marginal means and reads them off a transient mean the LD-QBD
8513 // reduction does not produce; accepting it there would run the CTMC
8514 // ensemble under the MAM name.
8515 if (k.stage_solver == "mam" && k.method != "statevec")
8517 "--stage-solver mam applies to -s env --method statevec: the LD-QBD backend flattens "
8518 "its blocks into a generator the state-vector coupling propagates a distribution "
8519 "across, and the mean-field coupling carries marginal MEANS instead");
8520 // The two FJ_codes knobs configure ONE analyzer, solver_mam_fj, which the
8521 // MAM dispatch reaches on a homogeneous fork-join model. Accepting them
8522 // anywhere else would let a caller believe an accuracy setting had been
8523 // honoured by a solver that never read it.
8524 if ((k.fj_accuracy > 0 || !k.fj_tmode.empty()) && s != "mam")
8526 "--fj-accuracy and --fj-tmode configure the FJ_codes fork-join approximation of "
8527 "solver_mam_fj.m and apply to -s mam; '" + s + "' does not run it");
8528 // --timescale gates the slotted branch of the MAM dispatch alone. SolverNC
8529 // has its own discrete product form and takes --slotted for it, so a
8530 // caller that names a time scale for any other solver is told rather than
8531 // silently answered on the continuous one.
8532 if (!k.timescale.empty() && s != "mam")
8534 "--timescale selects the time scale of the MAM discrete-time path and applies to "
8535 "-s mam; '" + s + "' does not read it (SolverNC takes --slotted)");
8536 // ---- `-a node`, ahead of the per-solver whitelists ---------------------
8537 // getAvgNodeTable is @@NetworkSolver's, not any one solver's: it is the
8538 // station table scattered to the node index space plus the two flow columns
8539 // recomputed from it, so every solver that produces an AvgResult can answer
8540 // it and none of them needs its own arm.
8541 // `-a node`, `-a sys`, `-a chain` and `-a nodechain` are the four
8542 // @@NetworkSolver tables that are VIEWS of one solved AvgResult -- scattered
8543 // to nodes, aggregated to chains, or reduced to the reference station -- so
8544 // they share the engine whitelist and the arithmetic ladder. Adding a
8545 // per-arm copy of either would let the four drift on which solvers and
8546 // which arithmetics they accept, for tables built from the same numbers.
8547 // `-s ssa` and `-s fluid` return their own solution types rather than an
8548 // AvgResult; `run_avg_engine` bridges them (`avg_result_from_sim`), so the
8549 // reference's rule holds here too -- a solver that reports an AvgTable
8550 // reports its four views. The two external wrappers obey the same rule and
8551 // are dispatched in their own arms above, which run before this one: they
8552 // validate knobs the ladder here knows nothing about, and LDES is handed the
8553 // model DOCUMENT rather than a parsed struct.
8554 if (analysis == "node" || analysis == "sys" || analysis == "chain" ||
8555 analysis == "nodechain" || analysis == "cache" || analysis == "item") {
8556 const bool is_sim_engine = (s == "ssa" || s == "fluid");
8557 // The two cache tables are read off the SAME solved result, so they
8558 // belong to the same group; SolverNC is the only engine here whose
8559 // branches fill `AvgResult::cache`, and the arms say so when it is
8560 // empty rather than being whitelisted to nc alone -- `-s auto` on a
8561 // cache model resolves to nc, and refusing the token would refuse the
8562 // model.
8563 //
8564 // BOTH SIMULATORS ARE ADMITTED TO `-a cache`, and only there.
8565 // `run_avg_engine` fills `r.cache` for `-s ssa` from
8566 // `cache_metrics_of_ssa` -- the realized hit, delayed-hit and miss
8567 // SHARES the sample path measured -- and for `-s fluid` from the
8568 // cacheqn decomposition's converged split, which is the same quantity
8569 // its refreshed struct is renormalized at. Both are how
8570 // `SSA(model).getAvgCacheTable()` and `Fluid(model).getAvgCacheTable()`
8571 // answer in the reference, which reads them off the node the analyzer
8572 // wrote. `-a item` stays refused for both: the per-item occupancy is a
8573 // recursion of the
8574 // NC/MVA cache branches and no simulator forms it, so admitting it
8575 // would report an empty table for a quantity that was never measured.
8576 const bool cache_table = (analysis == "cache" || analysis == "item");
8577 const bool sim_cache_ok = (analysis == "cache");
8578 // `-a cache` and `-a item` are the CACHE tables, which RCAT does not
8579 // form -- so `ag` joins the AvgResult views and not those two.
8580 const bool ag_view = (s == "ag" && !cache_table);
8581 if ((s != "mva" && s != "auto" && s != "nc" && s != "mam" && s != "ba" && s != "ctmc" &&
8582 !ag_view && !is_sim_engine) ||
8583 (cache_table && is_sim_engine && !sim_cache_ok))
8585 // `-a node`'s own wording is kept verbatim: it is the message a
8586 // caller has been reading since the arm existed, and the group
8587 // it now shares does not change what it says.
8588 (analysis == "node"
8589 ? std::string("-a node reports the per-node table of -s mva, nc, mam, ag, ba, "
8590 "ctmc, ssa, fluid, jmt, ldes and auto; '")
8591 : "-a " + analysis +
8592 " is a view of the station AvgResult and is reported by -s mva, nc, "
8593 "mam, " + (analysis == "item" ? "" : "ag, ") + "ba, ctmc" +
8594 (analysis == "item" ? "" : ", ssa, fluid, jmt, ldes") + " and auto; '") +
8595 s + "' does not return the station AvgResult it is built from");
8596 // The same refusal their own `-a avg` arms raise, for the same reason:
8597 // an SSA sample path is generated from exponential clocks and a fluid
8598 // trajectory is integrated by LSODA, so neither is carried by an exact
8599 // or an extended-precision backend. Refused BY NAME here rather than
8600 // narrowed silently in the ladder below.
8601 if (is_sim_engine && arith != "double")
8603 "-a " + analysis + " under -s " + s +
8604 " is read off a " + (s == "ssa" ? "sample path" : "fluid trajectory") +
8605 ", which is transcendental; rerun with --arith double (got '" + arith + "')");
8606 const std::string eng = (s == "auto") ? std::string("mva") : s;
8607#define LINE_CLI_TABLE_LADDER(FN) \
8608 do { \
8609 if (arith == "double") return FN<double>(file, k, eng); \
8610 if (arith == "exact") return FN<line::Rational>(file, k, eng); \
8611 if (arith == "real:16") return FN<line::Real<16> >(file, k, eng); \
8612 if (arith == "real" || arith == "real:32") return FN<line::Real<32> >(file, k, eng); \
8613 if (arith == "real:64") return FN<line::Real<64> >(file, k, eng); \
8614 if (arith == "real:128") return FN<line::Real<128> >(file, k, eng); \
8615 if (arith == "real:256") return FN<line::Real<256> >(file, k, eng); \
8616 } while (0)
8617 if (analysis == "node") LINE_CLI_TABLE_LADDER(solve_model_node);
8618 if (analysis == "sys") LINE_CLI_TABLE_LADDER(solve_model_sys);
8619 if (analysis == "chain") LINE_CLI_TABLE_LADDER(solve_model_chain);
8620 if (analysis == "nodechain") LINE_CLI_TABLE_LADDER(solve_model_nodechain);
8621 if (analysis == "cache") LINE_CLI_TABLE_LADDER(solve_model_cache);
8622 if (analysis == "item") LINE_CLI_TABLE_LADDER(solve_model_item);
8623#undef LINE_CLI_TABLE_LADDER
8624 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8625 }
8626 if (s == "ctmc") {
8627 // The CTMC surface the port reaches: the AvgTable, plus the @@SolverCTMC
8628 // methods that are not means -- the generator and the state space
8629 // (getInfGen, getStateSpace), the transient occupancy (getTranProbSysAggr),
8630 // a marked trajectory (sampleSys), the declared rewards (getAvgReward),
8631 // the response-time laws (getCdfRespT, getCdfSysRespT) and the parametric
8632 // sensitivity (getSensitivityRanking).
8633 if (analysis != "avg" && analysis != "prob" && analysis != "gen" && analysis != "states" &&
8634 analysis != "tran" && analysis != "tranprob" && analysis != "tranreward" &&
8635 analysis != "sample" && analysis != "reward" && analysis != "rewardvalue" &&
8636 analysis != "cdf" && analysis != "sens" && analysis != "firstpasst" &&
8637 analysis != "firstpasstmom")
8639 "the CTMC solver ports -a avg, node, sys, chain, nodechain, prob, gen, states, "
8640 "tran, tranprob, tranreward, sample, reward, reward-value, cdf, first-passt, "
8641 "first-passt-moments and sens (got '" + analysis + "')");
8642 // The generator-free methods answer MEANS and nothing else: mdd holds
8643 // the reachable set in a diagram and cftp never enumerates it at all, so
8644 // there is no state space to list, no filtration to split and no
8645 // trajectory to walk. Refused by name rather than served from the
8646 // enumerated chain, which would report an answer under a method that did
8647 // not produce it.
8648 if ((k.method == "mdd" || k.method == "cftp" || k.method == "cftp.approx") &&
8649 analysis != "avg")
8651 "the '" + k.method +
8652 "' method never builds the explicit generator, so it serves -a avg only (got '" +
8653 analysis + "'); use --method default for the state-space analyses");
8654 if (k.tol >= 0.0 || k.iter_tol >= 0.0 || k.iter_max >= 0)
8656 "--tol, --iter_tol and --iter_max do not apply to -s ctmc: the stationary vector "
8657 "is obtained by a direct solve of pi Q = 0, with nothing to converge. The mdd "
8658 "method's level iteration has --mdd-tol and --mdd-maxiter of its own");
8659 // Every step from the generator to the means is a field operation, so
8660 // there is no arithmetic to refuse: exact returns the exact rational
8661 // stationary law. The transient analyses refuse inside, by name.
8662 if (arith == "double") return solve_model_ctmc<double>(file, k, analysis);
8663 if (arith == "exact") return solve_model_ctmc<line::Rational>(file, k, analysis);
8664 if (arith == "real:16") return solve_model_ctmc<line::Real<16> >(file, k, analysis);
8665 if (arith == "real" || arith == "real:32")
8666 return solve_model_ctmc<line::Real<32> >(file, k, analysis);
8667 if (arith == "real:64") return solve_model_ctmc<line::Real<64> >(file, k, analysis);
8668 if (arith == "real:128") return solve_model_ctmc<line::Real<128> >(file, k, analysis);
8669 if (arith == "real:256") return solve_model_ctmc<line::Real<256> >(file, k, analysis);
8670 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8671 }
8672
8673 // ---- the solvers that carry their own arithmetic restriction -----------
8674 // Each refuses BY NAME rather than being narrowed silently, and the reason
8675 // is the solver's own, not a limitation of the CLI.
8676 if (s == "mam") {
8677 // The @@SolverMAM surface the port reaches: the AvgTable, the
8678 // queue-length law (getProb / getProbMarg), the response-time law
8679 // (getCdfRespT and its getSjrnT / sjrnT aliases, plus getPerctRespT),
8680 // the transient means (getTranAvg) and the M/G/1-type internals of the
8681 // queue (getMAMResult).
8682 if (analysis != "avg" && analysis != "prob" && analysis != "cdf" &&
8683 analysis != "cdfpasst" && analysis != "perct" && analysis != "tran" &&
8684 analysis != "internals")
8686 "the MAM solver ports -a avg, node, prob, cdf, cdf-passt, perct-respt, tran and "
8687 "internals (got '" + analysis + "')");
8688 if (arith != "double")
8690 "the MAM solver fits phase-type representations, whose fitter requires "
8691 "transcendental arithmetic; rerun with --arith double (got '" + arith + "')");
8692 if (analysis == "tran" && k.t1 < 0.0)
8694 "-s mam -a tran integrates the transient queue length over a horizon and there is "
8695 "no default for it; pass --tspan t0 t1");
8696 if (analysis == "prob") return solve_model_mam_prob<double>(file, k);
8697 if (analysis == "cdf") return solve_model_mam_cdf<double>(file, k, "cdf", "CdfRespT");
8698 // `getCdfPassT` IS `getCdfRespT` here, and that is the reference's
8699 // construction rather than an alias invented in the CLI: SolverMAM.java
8700 // computes both from the SAME `solver_mam_passage_time(sn, sn.proc,
8701 // options)` call. It is emitted under its own key so a caller that
8702 // asked the passage-time question is answered it, and the payload's
8703 // `type` records which of the two names produced the curve.
8704 if (analysis == "cdfpasst")
8705 return solve_model_mam_cdf<double>(file, k, "cdfpasst", "CdfPassT");
8706 if (analysis == "perct") return solve_model_mam_perct<double>(file, k);
8707 if (analysis == "tran") return solve_model_mam_tran<double>(file, k);
8708 if (analysis == "internals") return solve_model_mam_internals<double>(file, k);
8709 return solve_model_mam<double>(file, k);
8710 }
8711 if (s == "ssa") {
8712 // `-a cdf` is refused BY NAME rather than falling into the generic
8713 // message, because the refusal is the reference's own answer and not a
8714 // port gap: `@@SolverSSA/getCdfRespT.m` raises the same error, since SSA
8715 // samples state trajectories and not per-job sojourn times.
8716 if (analysis == "cdf") line::ssa::ssa_cdf_respt_refuse();
8717 if (analysis != "avg" && analysis != "prob" && analysis != "sample")
8718 throw line::UnsupportedError("the SSA solver ports -a avg, -a prob and -a sample (got '" +
8719 analysis + "')");
8720 if (arith != "double")
8722 "an SSA sample path is generated from exponential clocks, which are "
8723 "transcendental; rerun with --arith double (got '" + arith + "')");
8724 if (analysis == "prob") return solve_model_ssa_prob<double>(file, k);
8725 if (analysis == "sample") return solve_model_ssa_sample<double>(file, k);
8726 return solve_model_ssa<double>(file, k);
8727 }
8728 if (s == "nc") {
8729 if (analysis != "avg" && analysis != "prob" && analysis != "marg" &&
8730 analysis != "sysmarg" && analysis != "cdf" && analysis != "sens" &&
8731 analysis != "normconst" && analysis != "busyperiod")
8733 "the NC solver ports -a avg, -a node, -a prob, -a marg, -a sysmarg, -a cdf, "
8734 "-a sens, -a normconst and -a busyperiod (got '" + analysis + "')");
8735 if (analysis == "busyperiod") {
8736 if (arith == "double") return solve_model_nc_busyp<double>(file, k);
8737 if (arith == "exact") return solve_model_nc_busyp<line::Rational>(file, k);
8738 if (arith == "real:16") return solve_model_nc_busyp<line::Real<16> >(file, k);
8739 if (arith == "real" || arith == "real:32")
8740 return solve_model_nc_busyp<line::Real<32> >(file, k);
8741 if (arith == "real:64") return solve_model_nc_busyp<line::Real<64> >(file, k);
8742 if (arith == "real:128") return solve_model_nc_busyp<line::Real<128> >(file, k);
8743 if (arith == "real:256") return solve_model_nc_busyp<line::Real<256> >(file, k);
8744 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8745 }
8746 if (analysis == "sysmarg") {
8747 if (arith == "double") return solve_model_nc_sysmarg<double>(file, k);
8748 if (arith == "exact") return solve_model_nc_sysmarg<line::Rational>(file, k);
8749 if (arith == "real:16") return solve_model_nc_sysmarg<line::Real<16> >(file, k);
8750 if (arith == "real" || arith == "real:32")
8751 return solve_model_nc_sysmarg<line::Real<32> >(file, k);
8752 if (arith == "real:64") return solve_model_nc_sysmarg<line::Real<64> >(file, k);
8753 if (arith == "real:128") return solve_model_nc_sysmarg<line::Real<128> >(file, k);
8754 if (arith == "real:256") return solve_model_nc_sysmarg<line::Real<256> >(file, k);
8755 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8756 }
8757 if (analysis == "marg") {
8758 if (arith == "double") return solve_model_nc_marg<double>(file, k);
8759 if (arith == "exact") return solve_model_nc_marg<line::Rational>(file, k);
8760 if (arith == "real:16") return solve_model_nc_marg<line::Real<16> >(file, k);
8761 if (arith == "real" || arith == "real:32")
8762 return solve_model_nc_marg<line::Real<32> >(file, k);
8763 if (arith == "real:64") return solve_model_nc_marg<line::Real<64> >(file, k);
8764 if (arith == "real:128") return solve_model_nc_marg<line::Real<128> >(file, k);
8765 if (arith == "real:256") return solve_model_nc_marg<line::Real<256> >(file, k);
8766 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8767 }
8768 // Served under NC too so `-s auto -a normconst`, which chooseSolverHeur
8769 // sends to NC on a product-form model, lands on an arm that answers.
8770 if (analysis == "normconst") {
8771 if (arith == "double") return solve_model_normconst<double>(file, k, s);
8772 if (arith == "exact") return solve_model_normconst<line::Rational>(file, k, s);
8773 if (arith == "real:16") return solve_model_normconst<line::Real<16> >(file, k, s);
8774 if (arith == "real" || arith == "real:32")
8775 return solve_model_normconst<line::Real<32> >(file, k, s);
8776 if (arith == "real:64") return solve_model_normconst<line::Real<64> >(file, k, s);
8777 if (arith == "real:128") return solve_model_normconst<line::Real<128> >(file, k, s);
8778 if (arith == "real:256") return solve_model_normconst<line::Real<256> >(file, k, s);
8779 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8780 }
8781 if (analysis == "sens") {
8782 if (arith == "double") return solve_model_nc_sens<double>(file, k);
8783 if (arith == "exact") return solve_model_nc_sens<line::Rational>(file, k);
8784 if (arith == "real:16") return solve_model_nc_sens<line::Real<16> >(file, k);
8785 if (arith == "real" || arith == "real:32")
8786 return solve_model_nc_sens<line::Real<32> >(file, k);
8787 if (arith == "real:64") return solve_model_nc_sens<line::Real<64> >(file, k);
8788 if (arith == "real:128") return solve_model_nc_sens<line::Real<128> >(file, k);
8789 if (arith == "real:256") return solve_model_nc_sens<line::Real<256> >(file, k);
8790 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8791 }
8792 if (analysis == "cdf") {
8793 if (arith == "double") return solve_model_nc_cdf<double>(file, k);
8794 if (arith == "exact") return solve_model_nc_cdf<line::Rational>(file, k);
8795 if (arith == "real:16") return solve_model_nc_cdf<line::Real<16> >(file, k);
8796 if (arith == "real" || arith == "real:32")
8797 return solve_model_nc_cdf<line::Real<32> >(file, k);
8798 if (arith == "real:64") return solve_model_nc_cdf<line::Real<64> >(file, k);
8799 if (arith == "real:128") return solve_model_nc_cdf<line::Real<128> >(file, k);
8800 if (arith == "real:256") return solve_model_nc_cdf<line::Real<256> >(file, k);
8801 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8802 }
8803 if (analysis == "prob") {
8804 if (arith == "double") return solve_model_nc_prob<double>(file, k);
8805 if (arith == "exact") return solve_model_nc_prob<line::Rational>(file, k);
8806 if (arith == "real:16") return solve_model_nc_prob<line::Real<16> >(file, k);
8807 if (arith == "real" || arith == "real:32")
8808 return solve_model_nc_prob<line::Real<32> >(file, k);
8809 if (arith == "real:64") return solve_model_nc_prob<line::Real<64> >(file, k);
8810 if (arith == "real:128") return solve_model_nc_prob<line::Real<128> >(file, k);
8811 if (arith == "real:256") return solve_model_nc_prob<line::Real<256> >(file, k);
8812 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8813 }
8814 if (arith == "double") return solve_model_nc<double>(file, k);
8815 if (arith == "exact") return solve_model_nc<line::Rational>(file, k);
8816 if (arith == "real:16") return solve_model_nc<line::Real<16> >(file, k);
8817 if (arith == "real" || arith == "real:32") return solve_model_nc<line::Real<32> >(file, k);
8818 if (arith == "real:64") return solve_model_nc<line::Real<64> >(file, k);
8819 if (arith == "real:128") return solve_model_nc<line::Real<128> >(file, k);
8820 if (arith == "real:256") return solve_model_nc<line::Real<256> >(file, k);
8821 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8822 }
8823 if (s == "ag") {
8824 // The @@SolverAG surface the port reaches: the AvgTable and its four
8825 // views, plus -a cdf as the inherited base-class exponential fallback.
8826 // RCAT converges a fixed point over the synchronization rates and
8827 // reports mean measures; it forms no state probability and no
8828 // transient, so there is nothing else to expose.
8829 if (analysis != "avg" && analysis != "cdf")
8831 "the AG solver ports -a avg (with its views -a node, -a sys, -a chain and "
8832 "-a nodechain) and -a cdf, the inherited exponential fallback: RCAT converges a "
8833 "fixed point over the synchronization rates and reports mean measures, forming no "
8834 "state probability or transient (got '" + analysis + "')");
8835 if (analysis == "cdf") {
8836 if (arith == "double") return solve_model_ag_cdf<double>(file, k);
8837 if (arith == "exact") return solve_model_ag_cdf<line::Rational>(file, k);
8838 if (arith == "real:16") return solve_model_ag_cdf<line::Real<16> >(file, k);
8839 if (arith == "real" || arith == "real:32")
8840 return solve_model_ag_cdf<line::Real<32> >(file, k);
8841 if (arith == "real:64") return solve_model_ag_cdf<line::Real<64> >(file, k);
8842 if (arith == "real:128") return solve_model_ag_cdf<line::Real<128> >(file, k);
8843 if (arith == "real:256") return solve_model_ag_cdf<line::Real<256> >(file, k);
8844 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8845 }
8846 if (arith == "double") return solve_model_ag<double>(file, k);
8847 if (arith == "exact") return solve_model_ag<line::Rational>(file, k);
8848 if (arith == "real:16") return solve_model_ag<line::Real<16> >(file, k);
8849 if (arith == "real" || arith == "real:32") return solve_model_ag<line::Real<32> >(file, k);
8850 if (arith == "real:64") return solve_model_ag<line::Real<64> >(file, k);
8851 if (arith == "real:128") return solve_model_ag<line::Real<128> >(file, k);
8852 if (arith == "real:256") return solve_model_ag<line::Real<256> >(file, k);
8853 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8854 }
8855 if (s == "ba") {
8856 if (analysis != "avg" && analysis != "bounds" && analysis != "cdf")
8857 throw line::UnsupportedError("the BA solver ports -a avg, -a node, -a bounds and "
8858 "-a cdf, the inherited exponential fallback (got '" +
8859 analysis + "')");
8860 if (analysis == "cdf") {
8861 if (arith == "double") return solve_model_ba_cdf<double>(file, k);
8862 if (arith == "exact") return solve_model_ba_cdf<line::Rational>(file, k);
8863 if (arith == "real:16") return solve_model_ba_cdf<line::Real<16> >(file, k);
8864 if (arith == "real" || arith == "real:32")
8865 return solve_model_ba_cdf<line::Real<32> >(file, k);
8866 if (arith == "real:64") return solve_model_ba_cdf<line::Real<64> >(file, k);
8867 if (arith == "real:128") return solve_model_ba_cdf<line::Real<128> >(file, k);
8868 if (arith == "real:256") return solve_model_ba_cdf<line::Real<256> >(file, k);
8869 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8870 }
8871 if (analysis == "bounds") {
8872 if (arith == "double") return solve_model_ba_bounds<double>(file, k);
8873 if (arith == "exact") return solve_model_ba_bounds<line::Rational>(file, k);
8874 if (arith == "real:16") return solve_model_ba_bounds<line::Real<16> >(file, k);
8875 if (arith == "real" || arith == "real:32")
8876 return solve_model_ba_bounds<line::Real<32> >(file, k);
8877 if (arith == "real:64") return solve_model_ba_bounds<line::Real<64> >(file, k);
8878 if (arith == "real:128") return solve_model_ba_bounds<line::Real<128> >(file, k);
8879 if (arith == "real:256") return solve_model_ba_bounds<line::Real<256> >(file, k);
8880 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8881 }
8882 if (arith == "double") return solve_model_ba<double>(file, k);
8883 if (arith == "exact") return solve_model_ba<line::Rational>(file, k);
8884 if (arith == "real:16") return solve_model_ba<line::Real<16> >(file, k);
8885 if (arith == "real" || arith == "real:32") return solve_model_ba<line::Real<32> >(file, k);
8886 if (arith == "real:64") return solve_model_ba<line::Real<64> >(file, k);
8887 if (arith == "real:128") return solve_model_ba<line::Real<128> >(file, k);
8888 if (arith == "real:256") return solve_model_ba<line::Real<256> >(file, k);
8889 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8890 }
8891 if (s == "qns") {
8892 if (analysis != "avg" && analysis != "cdf")
8894 "SolverQNS reports -a avg and -a cdf, the inherited exponential fallback: "
8895 "qnsolver returns one chain-level table of means and computes no state "
8896 "probability and no transient (got '" + analysis + "')");
8897 // The numbers arrive as the decimal text qnsolver printed, so every
8898 // digit past double is one this port invented; the ladder is refused
8899 // rather than run at a width the answer does not have.
8900 if (arith != "double")
8902 "SolverQNS reads its results back as the decimal text an external binary printed, "
8903 "which is double at best; --arith " + arith +
8904 " would report a precision the tool never produced");
8905 if (analysis == "cdf") return solve_model_qns_cdf(file, k);
8906 return solve_model_qns<double>(file, k);
8907 }
8908 if (s == "fluid" || s == "fld") {
8909 if (analysis != "avg" && analysis != "odes" && analysis != "var" &&
8910 analysis != "tranvar" && analysis != "jacobian" && analysis != "tran" &&
8911 analysis != "prob" && analysis != "cdf" && analysis != "aoi" &&
8912 analysis != "statevec")
8914 "the fluid solver ports -a avg, -a tran, -a tranvar, -a prob, -a cdf, -a aoi, "
8915 "-a odes, -a statevec, -a var and -a jacobian (got '" + analysis + "')");
8916 // The drift is integrated by LSODA, whose coefficients assume double;
8917 // a higher-precision request is refused rather than quietly narrowed.
8918 if (arith != "double")
8920 "the fluid solver integrates its drift with LSODA, which is double precision by "
8921 "construction; rerun with --arith double (got '" + arith + "')");
8922 if (analysis == "odes") return solve_model_fluid_odes<double>(file, k);
8923 if (analysis == "statevec") return solve_model_fluid_statevec<double>(file, k);
8924 if (analysis == "jacobian") return solve_model_fluid_jacobian<double>(file, k);
8925 if (analysis == "var") return solve_model_fluid_var<double>(file, k);
8926 if (analysis == "tranvar") return solve_model_fluid_tranvar<double>(file, k);
8927 if (analysis == "tran") return solve_model_fluid_tran<double>(file, k);
8928 if (analysis == "prob") return solve_model_fluid_prob<double>(file, k);
8929 if (analysis == "cdf") return solve_model_fluid_cdf<double>(file, k);
8930 if (analysis == "aoi") return solve_model_fluid_aoi<double>(file, k);
8931 return solve_model_fluid<double>(file, k);
8932 }
8933 if (analysis == "prob") {
8934 if (arith == "double") return solve_model_prob<double>(file, k);
8935 if (arith == "exact") return solve_model_prob<line::Rational>(file, k);
8936 if (arith == "real:16") return solve_model_prob<line::Real<16> >(file, k);
8937 if (arith == "real" || arith == "real:32") return solve_model_prob<line::Real<32> >(file, k);
8938 if (arith == "real:64") return solve_model_prob<line::Real<64> >(file, k);
8939 if (arith == "real:128") return solve_model_prob<line::Real<128> >(file, k);
8940 if (arith == "real:256") return solve_model_prob<line::Real<256> >(file, k);
8941 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8942 }
8943 if (analysis == "marg") {
8944 if (arith == "double") return solve_model_marg<double>(file, k);
8945 if (arith == "exact") return solve_model_marg<line::Rational>(file, k);
8946 if (arith == "real:16") return solve_model_marg<line::Real<16> >(file, k);
8947 if (arith == "real" || arith == "real:32") return solve_model_marg<line::Real<32> >(file, k);
8948 if (arith == "real:64") return solve_model_marg<line::Real<64> >(file, k);
8949 if (arith == "real:128") return solve_model_marg<line::Real<128> >(file, k);
8950 if (arith == "real:256") return solve_model_marg<line::Real<256> >(file, k);
8951 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8952 }
8953 if (analysis == "normconst") {
8954 if (arith == "double") return solve_model_normconst<double>(file, k, s);
8955 if (arith == "exact") return solve_model_normconst<line::Rational>(file, k, s);
8956 if (arith == "real:16") return solve_model_normconst<line::Real<16> >(file, k, s);
8957 if (arith == "real" || arith == "real:32")
8958 return solve_model_normconst<line::Real<32> >(file, k, s);
8959 if (arith == "real:64") return solve_model_normconst<line::Real<64> >(file, k, s);
8960 if (arith == "real:128") return solve_model_normconst<line::Real<128> >(file, k, s);
8961 if (arith == "real:256") return solve_model_normconst<line::Real<256> >(file, k, s);
8962 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8963 }
8964 if (analysis == "cdf") {
8965 // The inherited base-class exponential fallback over the MVA means,
8966 // as @@NetworkSolver/getCdfRespT.m serves it for SolverMVA
8967 if (arith == "double") return solve_model_mva_cdf<double>(file, k);
8968 if (arith == "exact") return solve_model_mva_cdf<line::Rational>(file, k);
8969 if (arith == "real:16") return solve_model_mva_cdf<line::Real<16> >(file, k);
8970 if (arith == "real" || arith == "real:32")
8971 return solve_model_mva_cdf<line::Real<32> >(file, k);
8972 if (arith == "real:64") return solve_model_mva_cdf<line::Real<64> >(file, k);
8973 if (arith == "real:128") return solve_model_mva_cdf<line::Real<128> >(file, k);
8974 if (arith == "real:256") return solve_model_mva_cdf<line::Real<256> >(file, k);
8975 throw line::InputError("--arith '" + arith + "' is not a model-solve backend");
8976 }
8977 if (analysis != "avg")
8979 "the model-solving path ports -a avg, -a node, -a prob, -a marg, -a normconst and "
8980 "-a cdf (got '" +
8981 analysis + "')");
8982 // The mvaDispatch ladder's transcendental-only analyzers (open-queue closed
8983 // forms, DPS-exact, Marie, size-based) refuse by name under exact/real via
8984 // their if-constexpr guards, so the field-arithmetic branches (product-form
8985 // MVA, LD scaling) stay exact while a transcendental model is refused rather
8986 // than silently degraded.
8987 if (arith == "double") return solve_model_mva<double>(file, k);
8988 if (arith == "exact") return solve_model_mva<line::Rational>(file, k);
8989 if (arith == "real:16") return solve_model_mva<line::Real<16> >(file, k);
8990 if (arith == "real" || arith == "real:32") return solve_model_mva<line::Real<32> >(file, k);
8991 if (arith == "real:64") return solve_model_mva<line::Real<64> >(file, k);
8992 if (arith == "real:128") return solve_model_mva<line::Real<128> >(file, k);
8993 if (arith == "real:256") return solve_model_mva<line::Real<256> >(file, k);
8994 throw line::InputError(
8995 "--arith '" + arith +
8996 "' is not a model-solve backend; use double, exact or real:<16|32|64|128|256>");
8997}
8998
8999/**
9000 * The no-argument and `-h` message: the flags a first run actually needs.
9001 *
9002 * The full reference below is ~460 lines, which is not a thing a human reads at
9003 * a prompt; it stays one flag away under `--help-all` rather than being the
9004 * first thing the binary says.
9005 */
9006void print_brief_help() {
9007 std::printf(
9008 "LINE solver (C++), version %s\n"
9009 "\n"
9010 "Usage: line-cli -f <model> [-s <solver>] [-a <analysis>] [-o <format>]\n"
9011 " cat model.json | line-cli\n"
9012 "\n"
9013 "Common options:\n"
9014 " -f, --file <path> model file: .json (network), .lqnx (layered),\n"
9015 " .jsimg (JMT), .pnml (Petri net); stdin if omitted\n"
9016 " -s, --solver <name> auto (default), mva, nc, ctmc, mam, fluid, ssa,\n"
9017 " ldes, jmt, ag, ba, uq; ln for layered models, env\n"
9018 " for random environments\n"
9019 " -a, --analysis <type> avg (default), node, sys, chain, tran, prob, cdf,\n"
9020 " states, sample, normconst, bounds, ... (comma list)\n"
9021 " -o, --output <fmt> readable (default) | json\n"
9022 " --method <name> algorithm within the chosen solver\n"
9023 " --samples <n> simulation run length (ssa, ldes); default 10000\n"
9024 " --seed <n> random seed; default 23000\n"
9025 " -v, --verbosity <lvl> silent | standard | debug; debug turns on\n"
9026 " the solver console, a running progress log\n"
9027 " --find-solver [m] which solvers and methods can analyze this model,\n"
9028 " optionally only those answering measure <m>\n"
9029 " (avg, tran, cdf, prob, sample, ...); reports and exits\n"
9030 " --find-solver-all [m] the same, keeping the refused pairs and the\n"
9031 " reason each was refused\n"
9032 " -h, --help this message\n"
9033 " --help-all every option, solver by solver\n"
9034 " -V, --version version string\n"
9035 " --install environment check: which optional backends\n"
9036 " (Java/JMT, LQNS, qnsolver, SageMath) are reachable\n"
9037 "\n"
9038 "Examples:\n"
9039 " line-cli -f model.json solve, letting auto pick the solver\n"
9040 " line-cli -f model.json -s mva -a avg mean queue lengths, MVA\n"
9041 " line-cli -f model.lqnx -s ln solve a layered model\n"
9042 " line-cli -f model.json -s ssa --samples 1e6 -o json\n"
9043 " line-cli -f model.json --find-solver what can solve this model\n"
9044 " line-cli -f model.json --find-solver cdf ... and return a passage-time law\n"
9045 "\n"
9046 "Each solver has flags of its own (tolerances, horizons, engine choices):\n"
9047 "run `line-cli --help-all` for the full reference.\n",
9048 kVersion);
9049}
9050
9051void print_help() {
9052 std::printf(
9053 "LINE multiprecision solver (C++), version %s\n"
9054 "\n"
9055 "Usage: line-cli [OPTIONS]\n"
9056 " cat model.json | line-cli -i json [OPTIONS]\n"
9057 " line-cli model.lqnx [OPTIONS]\n"
9058 "\n"
9059 "Options (flag-compatible with jline.cli.LineCLI):\n"
9060 " -f, --file <path> model file; stdin when omitted (json only)\n"
9061 " -i, --input <fmt> input format: json | jsim | jsimg | jsimw |\n"
9062 " lqnx | xml | pnml. Without it a .lqnx or .xml\n"
9063 " path is read as a layered model, a\n"
9064 " .jsim/.jsimg/.jsimw path as a JMT simulation\n"
9065 " document, a .pnml path as a place/transition\n"
9066 " net (ISO/IEC 15909-2), and anything else as a\n"
9067 " Network model.json. The three jsim spellings\n"
9068 " name ONE format, as they do in JMT\n"
9069 " -o, --output <fmt> output format: readable | json (| layers, lqnx).\n"
9070 " json is honoured by EVERY analysis, not only\n"
9071 " -a avg: each answers under a key named after\n"
9072 " its -a, with the arithmetic and the resolved\n"
9073 " method beside it, and every index inside a\n"
9074 " payload is 0-based against the tables' 1-based\n"
9075 " columns (each payload states its indexBase)\n"
9076 " -s, --solver <name> Network: auto, mva, nc, ctmc, mam, ba, ssa, fluid, uq,\n"
9077 " ldes (the SSJ discrete-event engine, run as a\n"
9078 " subprocess on common/ldes or common/ldes.jar),\n"
9079 " jmt (the Java Modelling Tools engine),\n"
9080 " qns (the external qnsolver binary)\n"
9081 " layered: auto, ln, ln.mva, ln.comom, lqns,\n"
9082 " ldes (the native in-process LN simulator, a\n"
9083 " sample path of the layered model itself and\n"
9084 " not a decomposition into layers; it takes\n"
9085 " --samples and --seed, and NOT the --ldes-*\n"
9086 " family, which configures the subprocess\n"
9087 " engine that answers -s ldes on a Network)\n"
9088 " environment: env (an Environment model.json)\n"
9089 " -a, --analysis <type> analysis: avg, node, sys, chain, nodechain,\n"
9090 " stage, cache, item, prob, marg, cdf, cdf-passt,\n"
9091 " perct-respt, tran, tran-cdf-respt,\n"
9092 " tran-cdf-passt, tranprob, tranreward,\n"
9093 " reward-value, normconst, gen, states, sample,\n"
9094 " reward, sens, first-passt, odes, statevec,\n"
9095 " var, tranvar, busyperiod,\n"
9096 " jacobian, aoi, internals, bounds, posterior,\n"
9097 " interval. A COMMA LIST runs several in order\n"
9098 " (`-a avg,sys`), emitting one -o json envelope\n"
9099 " per analysis rather than one merged object.\n"
9100 " The JAR CLI's own spellings are accepted as\n"
9101 " aliases -- cdf-respt, prob-sys-aggr, tran-avg,\n"
9102 " generator, reward-steady, all -- and collapse\n"
9103 " onto the arm that already answers them whole:\n"
9104 " -a prob reports getProbSys, getProbSysAggr and\n"
9105 " the per-station pair together, -a sample walks\n"
9106 " all four samplers at once, and -a stage IS\n"
9107 " -a avg on a Network (one implicit stage).\n"
9108 " Which names a\n"
9109 " solver serves is stated by its own refusal;\n"
9110 " ssa serves avg, prob and sample (prob and\n"
9111 " sample run the SERIAL engine whatever -m\n"
9112 " said, since the NRM simulates counts rather\n"
9113 " than the state encoding)\n"
9114 " -v, --verbosity <lvl> silent | standard | debug; debug turns on\n"
9115 " the solver console, a running progress log\n"
9116 " of every solver run\n"
9117 " -d, --seed <n> random seed (SSA, ctmc --method cftp); default\n"
9118 " 23000. -d is the JAR CLI's spelling\n"
9119 " --warmupfrac <f> SSA: leading fraction of the path discarded\n"
9120 " before the means are taken, in [0,1)\n"
9121 " --pstar <p> fluid: exponent of the p-norm smoothing of the\n"
9122 " drift; without it the hard min() is integrated\n"
9123 " --busyperiod <n,..> -a busyperiod: the orders wanted; default 1\n"
9124 " --busyperiod-subnet <i,..>\n"
9125 " -a busyperiod: the 1-based stations forming the\n"
9126 " subnetwork. Required -- a busy period is defined\n"
9127 " for a NAMED set and no default can choose one\n"
9128 " --method <name> algorithm within the solver\n"
9129 " --samples <n> simulation run length (SSA); default 10000.\n"
9130 " Accepts 1e6 as well as 1000000. A simulation\n"
9131 " figure is only a measurement WITH this number,\n"
9132 " which is why the SSA banner reports it back.\n"
9133 " REQUIRED by ctmc --method cftp, where the draw\n"
9134 " is the answer rather than a run length.\n"
9135 " --mdd-tol <x> level-iteration tolerance of ctmc --method mdd;\n"
9136 " default 1e-12. NOT --tol: that iteration is an\n"
9137 " inner solve whose fixed point is checked\n"
9138 " against the population invariant at 1e-6, so a\n"
9139 " solver-sized tolerance stops short of it\n"
9140 " --mdd-maxiter <n> coupled sweeps before ctmc --method mdd is\n"
9141 " declared non-convergent; default 500\n"
9142 " --level <n> hierarchy level of the ba pbh/cbh/sib families\n"
9143 " and the iteration count of pbk/bjbk; default 2\n"
9144 " --qrf-params <j> JSON (inline or a path) with the QRF blocking\n"
9145 " tables of -s ba --method qrf.bas|qrf.rsrd:\n"
9146 " f, MR, BB, MM, MM1, ZZ and optionally F, the\n"
9147 " fields sn_to_qrf_params assembles. ZM is\n"
9148 " derived from ZZ. There is no default: assuming\n"
9149 " no blocking puts the bound ~31x farther from\n"
9150 " exact, so its absence is refused\n"
9151 " --qrf-alpha <j> JSON (nstations x N) load-dependent scaling of\n"
9152 " the ba qrf.mmi.ld, qrf.mmi.linear and qrf.rsrd\n"
9153 " arms; default all ones\n"
9154 " --tol <x> convergence tolerance (mva, nc, mam, ag, fluid)\n"
9155 " --iter_tol <x> outer-loop tolerance (mva, nc, fluid)\n"
9156 " --iter_max <n> iteration cap (mva, nc, mam, ag, fluid)\n"
9157 " --max-states <n> truncation level of an OPEN agent's queue-length\n"
9158 " dimension (ag), options.config.maxStates;\n"
9159 " default 100. A closed class is bounded by its\n"
9160 " own population instead, and --method inapinf\n"
9161 " ignores the level and solves the open agents on\n"
9162 " the infinite state space\n"
9163 " --fork-join <arm> which fork-join transform the mean-value fixed\n"
9164 " point takes (mva, nc): default|mmt|fjt is the\n"
9165 " MMT transform, ht|heidelberger-trivedi the\n"
9166 " Heidelberger-Trivedi one, which is CLOSED\n"
9167 " models only and a different answer to the same\n"
9168 " model rather than a faster route to one\n"
9169 " --cutoff <n|matrix> open jobs per class in the CTMC state space;\n"
9170 " a matrix is per (station,class), '1,1,0;3,3,0;0,0,3'\n"
9171 " (ctmc); without it the reference's\n"
9172 " ceil(6000^(1/(M*K))) is used and reported.\n"
9173 " Also the level truncation of mam -a prob,\n"
9174 " whose open queue has no bound of its own\n"
9175 " --fj-accuracy <n> FJ_codes truncation C of the queue-length\n"
9176 " difference between the two fork-join\n"
9177 " branches (mam, homogeneous fork-join);\n"
9178 " default 100, larger is more accurate\n"
9179 " --fj-tmode <mode> how that approximation solves for its T\n"
9180 " matrix: NARE (default) or Sylves\n"
9181 " --timescale <mode> auto (default), discrete or continuous: how\n"
9182 " -s mam reads the time scale. auto lets the\n"
9183 " distributions decide; discrete raises rather\n"
9184 " than solve a model that mixes lattice and\n"
9185 " non-lattice laws. The slot is --slotlength\n"
9186 " --tspan <t0>:<t1> transient horizon (ctmc -a tranprob,\n"
9187 " mam -a tran, fluid); a bare <t1> starts at 0.\n"
9188 " For the CTMC and MAM there is no default:\n"
9189 " pi(t) on an unstated horizon is not a\n"
9190 " quantity. For the fluid solver it bounds the\n"
9191 " integration, and is what -s fluid --method kp\n"
9192 " reports its covariance AT\n"
9193 " -n, --node <n> 1-based stateful node a state query is\n"
9194 " labelled by (ctmc -a tranprob, -a sample and\n"
9195 " ssa -a sample), the\n"
9196 " queue mam -a prob reports, or the station\n"
9197 " mva -a marg reports; without it the\n"
9198 " whole network is reported (the MAM queries\n"
9199 " take the model's only Queue)\n"
9200 " -c, --class <r> 1-based job class of mva -a marg; without it\n"
9201 " every class is reported\n"
9202 " NOTE ON THE INDEX BASE: -n and -c are 1-BASED\n"
9203 " here, as every station index this CLI takes\n"
9204 " is, and 0-BASED in jline.cli.LineCLI, which\n"
9205 " indexes as Java does. The short spellings are\n"
9206 " accepted so one command line parses in both,\n"
9207 " but the SAME number names a different node --\n"
9208 " the two bridges (cpp_dispatch, jar_dispatch)\n"
9209 " each convert for their own CLI\n"
9210 " --marg-states <ns> comma-separated job counts the mva -a marg\n"
9211 " curve is evaluated at, the reference's\n"
9212 " state_m; without it each law takes its own\n"
9213 " default range (0..N_r closed, mean + 5 sigma\n"
9214 " Poisson, the 1e-10 tail geometric)\n"
9215 " --notation <form> scalar (default) | matrix, the form the ODE\n"
9216 " export writes (fluid -a odes only)\n"
9217 " --symbolic <b> computer-algebra backend of fluid -a\n"
9218 " jacobian: auto (default, searches for a\n"
9219 " line-sage-rest service), a URL, an image\n"
9220 " name, or none to differentiate locally\n"
9221 " --equilibria also ask the backend for the solutions of\n"
9222 " f(x) = 0 (fluid -a jacobian). Needs a\n"
9223 " backend: solving is not differentiating\n"
9224 " --cdf-algorithm <a> exact (default, pfqn_stdf) | rd\n"
9225 " (pfqn_stdf_heur), how the sojourn law is\n"
9226 " inverted (nc -a cdf only)\n"
9227 " --perm-engine <e> exact (default, Ryser) | spm | bethe | heur |\n"
9228 " huberlaw | adapart, the permanent estimator\n"
9229 " of nc -a sysmarg. The five approximations\n"
9230 " refuse a demand matrix with a zero entry;\n"
9231 " spm is the saddle point, whose cost does not\n"
9232 " grow with the population\n"
9233 " --tran-points <n> points on the uniform transient grid the ENV\n"
9234 " mean-field coupling sums its stage exit\n"
9235 " metrics over (env only); default 1001\n"
9236 " --state <n,...> the ENCODED state row of the node named by\n"
9237 " --node that -a prob asks about, i.e.\n"
9238 " getProb(node, state)'s second argument; without\n"
9239 " it the query is about the model's default\n"
9240 " initial state\n"
9241 " --events <n> length of ONE sampled trajectory (-a sample);\n"
9242 " default 1000. NOT --samples, which is a\n"
9243 " solver's run length\n"
9244 " --timestep <dt> fixed output step of a transient CTMC solve\n"
9245 " --transient-method <m> ode (default) or fau, how the CTMC forward\n"
9246 " equation is advanced over the output grid\n"
9247 " --fau-epsilon <e> fau: probability mass the grid may discard\n"
9248 " --fau-delta <d> fau: occupancy below which a state is dropped\n"
9249 " (-a tranprob, -a tranreward), options.timestep\n"
9250 " of ctmc_transient.m; without it the grid is the\n"
9251 " integrator's own adaptive one. It changes WHERE\n"
9252 " the solution is reported, not how it is\n"
9253 " computed: the grid points are read off the same\n"
9254 " interpolant\n"
9255 " --percentiles <p,..> levels getPerctRespT is read at (-s mam),\n"
9256 " as fractions (0.9) or percents (90); default\n"
9257 " 0.50,0.90,0.95,0.99, the reference's pers_stored\n"
9258 " --reward-name <nm> which declared reward -a reward-value returns\n"
9259 " the value function of. REQUIRED there and never\n"
9260 " defaulted: two rewards have different value\n"
9261 " functions, and picking one would mislabel it\n"
9262 " -p, --port <n> run as a solve SERVER on this port, speaking\n"
9263 " LineWebSocketServer's protocol: one WebSocket\n"
9264 " text message per connection, its first line the\n"
9265 " comma-separated argument list and its remainder\n"
9266 " the model document; the CLI's output comes back\n"
9267 " as one text message. Plaintext, one connection\n"
9268 " at a time, and -f is refused beside it\n"
9269 " -m, --maxreq <n> quit after serving n requests; without it the\n"
9270 " server runs until interrupted\n"
9271 " -h, --help the short message: the flags a first run needs\n"
9272 " --help-all this message, every option solver by solver\n"
9273 " -V, --version version string\n"
9274 " --install environment check: report which optional backends\n"
9275 " (Java/JMT, LQNS, qnsolver, SageMath) are reachable\n"
9276 "\n"
9277 "Layered models (-i lqnx|xml) additionally take:\n"
9278 " --layer-solver <s> solver run in each layer: mva (default)|nc|\n"
9279 " fluid|ssa, the C++ spelling of the reference's\n"
9280 " factory argument: LN(model, @(m) MVA(m))\n"
9281 " against LN(model, @(m) Fluid(m)). They converge\n"
9282 " to DIFFERENT fixed points, not to the same one\n"
9283 " by different routes, because each layer's\n"
9284 " results feed the next outer iteration's demands.\n"
9285 " fluid is double only and refuses a layer with a\n"
9286 " fork; ssa is NOISY, so the outer loop switches\n"
9287 " to the Robbins-Monro / Polyak-Ruppert controller\n"
9288 " --method <name> the LN update: default | moment3 | mwba.upper |\n"
9289 " mwba.lower. moment3 fits an APH to each layer's\n"
9290 " response-time CDF and convolves along the entry,\n"
9291 " which is what makes -a cdf possible; mwba.*\n"
9292 " reports Majumdar-Woodside box BOUNDS on\n"
9293 " throughput and processor utilization and solves\n"
9294 " no layer at all (every other metric is NaN)\n"
9295 " --ln-transient <m> coupled (default) | decoupled, how -a tran\n"
9296 " couples the layers. decoupled freezes the\n"
9297 " inter-layer demands at the fixed point; coupled\n"
9298 " relaxes time-varying demands through the fluid\n"
9299 " rate schedule until the trajectories settle\n"
9300 " --ln-transient-channels <c> both (default) | thinkt | callservt,\n"
9301 " which inter-layer coupling the relaxation\n"
9302 " injects, for isolating one channel's share\n"
9303 " --sens-method <m> auto (default) | exact | fd, the branch each\n"
9304 " LAYER's sensitivity table takes (-a sens);\n"
9305 " the same three under -s nc -a sens\n"
9306 " --sens-scheme <s> forward (default) | central, the difference\n"
9307 " quotient of the fd branch\n"
9308 " --sens-step <h> relative rate perturbation of the fd branch,\n"
9309 " in (0,1); default 1e-4, or 1e-2 for ssa layers\n"
9310 " --no-interlocking disable the interlocking correction\n"
9311 " --repeat <k> re-solve k times, report the best wall clock\n"
9312 " -o layers dump every layer's stations, classes, rates\n"
9313 " and routing instead of the AvgTable\n"
9314 " -a takes avg (getAvgTable), tran (getTranAvg, needs --tspan and fluid\n"
9315 " layers), sens (getSensitivityTable) and cdf (getCdfRespT, moment3).\n"
9316 " --iter_max and --iter_tol set the outer LN loop; --tol does not apply,\n"
9317 " and neither does any Network solver token.\n"
9318 "\n"
9319 "The external LQNS binary (-s lqns) additionally takes:\n"
9320 " --method <name> default | lqns | srvn | exactmva |\n"
9321 " srvn.exactmva | sim | lqsim | lqnsdefault.\n"
9322 " sim and lqsim run lqsim, the SIMULATOR;\n"
9323 " lqnsdefault is lqns with no pragma at all,\n"
9324 " which is a different fixed point and not a\n"
9325 " synonym for default\n"
9326 " --multiserver <p> conway|rolia|zhou|suri|reiser|schmidt|default\n"
9327 " (= rolia), the -Pmultiserver= pragma. Not\n"
9328 " passed to lqsim, which has no MVA to configure\n"
9329 " --samples <n> lqsim run length (-A); default 10000\n"
9330 " --timeout <s> kill the child after s seconds; without it the\n"
9331 " wrapper waits\n"
9332 " --keep keep the working directory with model.lqnx and\n"
9333 " model.lqxo instead of removing it\n"
9334 " --verbose echo the command line and the binary's output\n"
9335 " --remote[-url <u>] solve on a host running lqns-rest instead of\n"
9336 " locally; -url implies --remote. LINE ships no\n"
9337 " LQNS binary, so this is the other way to reach\n"
9338 " one\n"
9339 " -a takes avg only: lqns computes no transient, no sensitivity and no\n"
9340 " response-time distribution. QLen is the element utilization, Util its\n"
9341 " processor utilization per server, RespT its phase-1 service time;\n"
9342 " ResidT and ArvR print NaN because lqns reports neither.\n"
9343 "\n"
9344 "The external qnsolver binary (-s qns) additionally takes:\n"
9345 " --method <name> default (= rolia) | conway | rolia | zhou |\n"
9346 " reiser. The multiserver approximation, passed\n"
9347 " as qnsolver -m and only when the model HAS a\n"
9348 " multiserver station. suri and schmidt are\n"
9349 " listed by the reference but reach the tool\n"
9350 " through its SolverLQNS branch, which this port\n"
9351 " does not carry, so they are refused by name\n"
9352 " --multiserver <p> the same choice under its config spelling;\n"
9353 " --method wins when it names one\n"
9354 " --timeout <s> kill the child after s seconds; without it the\n"
9355 " wrapper waits\n"
9356 " --keep keep the working directory with model.jmva and\n"
9357 " result.jmva instead of removing it\n"
9358 " -a takes avg only. The model is marshalled to the JMVA interchange\n"
9359 " format at CHAIN level and the chain results are de-aggregated back to\n"
9360 " classes, so only Queue, Delay and Source stations are expressible; any\n"
9361 " other station is refused rather than dropped. A closed model that is\n"
9362 " NOT product-form is refused too: the reference converts it with QN2LQN\n"
9363 " and delegates to SolverLQNS, and QN2LQN is not ported. --arith is\n"
9364 " double only, since the results arrive as the text an external binary\n"
9365 " printed.\n"
9366 "\n"
9367 "Discrete-event simulation (-s ldes) additionally takes:\n"
9368 " --ldes-tranfilter <f> warmup filter: mser5 (default), fixed, none\n"
9369 " --ldes-warmupfrac <x> fraction the fixed filter discards (0.2)\n"
9370 " --ldes-cimethod <m> CI estimator: obm (default), bm, spectral,\n"
9371 " none\n"
9372 " --ldes-cnvgon stop on relative precision instead of on\n"
9373 " the --samples budget\n"
9374 " --ldes-cnvgtol <x> that precision target (0.05); implies\n"
9375 " --ldes-cnvgon\n"
9376 " --slotted run the analytical solver on a discrete\n"
9377 " (slotted) time scale; SolverNC routes to the\n"
9378 " discrete-time product form and refuses a model\n"
9379 " outside it\n"
9380 " --slotlength <x> the slot in model time units; implies --slotted\n"
9381 " --ldes-slotted run on a discrete (slotted) time scale; a\n"
9382 " sample off the lattice is an error, never\n"
9383 " rounded\n"
9384 " --ldes-slotlength <x> the slot (1.0); implies --ldes-slotted\n"
9385 " --ldes-replications <n> independent runs, averaged. A single path\n"
9386 " is NOT E[N](t): -a tran over an ensemble\n"
9387 " mean needs this\n"
9388 " --ldes-numthreads <n> workers for those replications\n"
9389 " --ldes-maxtime <s> wall-clock budget; the engine stops early\n"
9390 " and reports stopping=max_time\n"
9391 " --ldes-initsol <v,..> warm-start placement, station-major\n"
9392 " [st0_cl0, st0_cl1, ...]. Add --ldes-tranfilter\n"
9393 " fixed --ldes-warmupfrac 0 to reproduce\n"
9394 " initFromSolver, which assumes the placement\n"
9395 " is already a steady state\n"
9396 " --ldes-rest-url <u> solve on an LDES REST server instead of a\n"
9397 " local binary; same wire format, same numbers\n"
9398 " The model.json is forwarded to the engine BYTE FOR BYTE, so a model\n"
9399 " this port cannot itself parse (a cache with retrieval, an SPN, a\n"
9400 " polling server) is simulated exactly as the MATLAB and Python clients\n"
9401 " simulate it. -a avg, tran, cdf (the empirical response-time law),\n"
9402 " sample and reward are served; -a prob is refused, because getProbSys\n"
9403 " weighs the trajectory against the model's current state and the C++\n"
9404 " NetworkStruct carries no such row. --arith is double only.\n"
9405 "\n"
9406 "Uncertainty quantification (-s uq) additionally takes:\n"
9407 " --uq-solver <s> the engine run at each design point: mva, nc,\n"
9408 " mam, ba, ctmc, fluid or ssa. REQUIRED: UQ\n"
9409 " computes nothing itself, and defaulting it\n"
9410 " would attribute the numbers to an engine the\n"
9411 " caller never chose\n"
9412 " A model whose service or arrival process is a Prior is a FAMILY of\n"
9413 " models. UQ discretizes each Prior, solves the tensor product of the\n"
9414 " alternatives, and reports the prior-weighted expectation. Under -s uq\n"
9415 " three flags describe the DESIGN and not the engine: --method is\n"
9416 " quadrature (default, and the alias of discrete) or montecarlo,\n"
9417 " --samples the nodes per continuous Prior (11), --seed the Monte Carlo\n"
9418 " stream. The stage solver therefore keeps its own sample count and its\n"
9419 " own seed; --tol, --iter_tol, --iter_max and --cutoff pass through to\n"
9420 " it, since UQ has no convergence of its own. -a posterior prints every\n"
9421 " design point, its weight and the means it substituted, which is what\n"
9422 " says whether the expectation averaged two nearby models or two very\n"
9423 " different ones. Every other solver REFUSES a model carrying a Prior\n"
9424 " rather than lowering it to its mixture moments.\n"
9425 " -a interval answers the OTHER epistemic question, in which a\n"
9426 " parameter is bounded but not distributed: it drops the weights and\n"
9427 " keeps the endpoints. On a single-class closed model of LI\n"
9428 " single-server queues and delays it is the EXACT hull of MVA over the\n"
9429 " demand box (2*(m+2) MVA calls, no design solved at all); otherwise it\n"
9430 " falls back to the range over the solved design points, which for a\n"
9431 " continuous Prior lies strictly inside the true range. The table says\n"
9432 " which, and the fallback warns on stderr: a range that is not an\n"
9433 " enclosure must not read like one.\n"
9434 "\n"
9435 "Random environments (-s env) read an Environment model.json, whose\n"
9436 " stages each hold a Network and whose transitions carry the stage\n"
9437 " holding times. The stages are solved TRANSIENTLY and coupled: each\n"
9438 " stage starts from the queue lengths the previous one left, and the\n"
9439 " reported means are the per-stage sojourn averages blended by the\n"
9440 " environment probabilities. --method selects the coupling: meanfield\n"
9441 " (default, the reference's, carries the marginal means across a\n"
9442 " switch) or statevec|blend (carries the whole joint distribution).\n"
9443 " meanfield solves each stage with the fluid transient and is double\n"
9444 " only; statevec uniformizes a CTMC and takes the whole --arith ladder.\n"
9445 " --method avg|dec asks instead for a closed-form limit, which carries\n"
9446 " nothing across a switch and iterates nothing: avg solves ONE model\n"
9447 " whose modulated rates are their probEnv-weighted averages (exact as\n"
9448 " the environment gets fast), dec solves each stage in steady state and\n"
9449 " blends by probEnv (exact as it gets slow). A model with an\n"
9450 " environment-declared node breakdown is read from the nodeFailures\n"
9451 " block, in the expanded or the one-stage macro form.\n"
9452 " --tspan bounds the stage horizon and --tran-points its grid; --iter_tol\n"
9453 " and --iter_max drive the fixed point. RespT and ArvR print as nan\n"
9454 " because ENV computes neither -- the reference returns them as NaN too,\n"
9455 " and ResidT carries QLen/Tput.\n"
9456 "\n"
9457 "Additions specific to this port:\n"
9458 " --arith <mode> double (default) | exact | real:<digits>\n"
9459 " --list-api list the API functions ported so far\n"
9460 " --api <name> invoke one API function directly\n"
9461 " --args <path> JSON arguments for --api; stdin when omitted\n"
9462 "\n"
9463 "Solvers and what they honour. Every model-solving solver reads -a avg,\n"
9464 "-f/-i and --method; nothing else is wired, so tolerances, iteration\n"
9465 "caps, seeds and sample counts keep their SolverOptions defaults rather\n"
9466 "than being invented here. mva and nc additionally read -a prob -- mva\n"
9467 "fits a binomial to its own means, nc returns the exact product-form\n"
9468 "probability, so the two disagree by construction. mva also reads -a\n"
9469 "marg, @SolverMVA's getProbMarg: P(n jobs of class r at station i) for\n"
9470 "every (station, class) pair, narrowed by --node / --class and evaluated\n"
9471 "at --marg-states. A closed class takes the Schmidt binomial fitted to\n"
9472 "Q(i,r); an open one takes the station's exact BCMP marginal (Poisson at\n"
9473 "an infinite server, multinomial-geometric at a queue). Both solvers read\n"
9474 "-a normconst, getProbNormConstAggr: nc reports the constant its solve\n"
9475 "already formed, mva RE-ENTERS its analyzer at method='exact', since only\n"
9476 "the exact recursion carries a G -- a model whose branch forms none, an\n"
9477 "open or mixed one above all, reports nan, as the reference does. nc also\n"
9478 "reads -a cdf,\n"
9479 "@SolverNC's getCdfRespT: the whole response-time law per (station,\n"
9480 "class) on one logarithmic grid, FCFS stations only, with\n"
9481 "--cdf-algorithm exact (pfqn_stdf, the default) or rd (pfqn_stdf_heur).\n"
9482 "nc reads -a sens as well, @NetworkSolver's getSensitivityTable: the\n"
9483 "derivative of each row's means with respect to its own service rate,\n"
9484 "selected with --sens-method / --sens-scheme / --sens-step. NC is one of\n"
9485 "the two engines whose exact branch differentiates the product-form\n"
9486 "recursion analytically, so auto takes it wherever the model is in its\n"
9487 "scope (single-server queues plus delays, not mixed) and falls back to\n"
9488 "finite differences elsewhere. NOT the -s ctmc -a sens analysis, which\n"
9489 "is getSensitivityRanking, a ranking of rate perturbations and not a\n"
9490 "table of derivatives.\n"
9491 "-a node is @NetworkSolver's getAvgNodeTable and is served by mva, nc,\n"
9492 "mam, ba and ctmc, the model solvers whose runner returns the station\n"
9493 "AvgResult it is built from. It is a DIFFERENT INDEX SPACE from -a avg,\n"
9494 "not a relabelling: the AvgTable has one row per STATION, so a\n"
9495 "ClassSwitch, Router, Fork, Join or Sink never appears in it, yet jobs\n"
9496 "flow through all of them. QLen, Util, RespT and ResidT are the station\n"
9497 "numbers scattered to their node indices and zero elsewhere -- a node\n"
9498 "that is not a station holds no jobs -- while ArvR and Tput are\n"
9499 "recomputed for every node by sn_get_node_arvr_from_tput and\n"
9500 "sn_get_node_tput_from_tput. The reference's finite-capacity-region\n"
9501 "pseudo-node rows are NOT emitted: this port's AvgResult carries no\n"
9502 "per-region queue length or utilization to fill them with.\n"
9503 "\n"
9504 "auto picks the engine\n"
9505 "with chooseSolverHeur and prints the name it picked; a branch selecting\n"
9506 "JMT or LDES refuses by name rather than substituting another. ctmc reads\n"
9507 "--cutoff and ports -a avg, prob, gen, states, tranprob, sample, reward,\n"
9508 "cdf, first-passt and sens, which are @SolverCTMC's\n"
9509 "getProbSys/getProbSysAggr and the per-station getProb/getProbAggr,\n"
9510 "getInfGen, getStateSpace, getTranProbSysAggr, sampleSys, getAvgReward,\n"
9511 "getCdfRespT, getCdfFirstPassT (state sets via --passage-from and\n"
9512 "--passage-into, as 1-based space rows '3,5' or state rows '0,2;1,1'),\n"
9513 "first-passt-moments (-a firstpasstmom, the same two sets plus\n"
9514 "--passage-orders: exact moments by one linear solve per order, so a\n"
9515 "variance or a skewness costs no truncated curve)\n"
9516 "and getSensitivityRanking. Its --method also takes mdd, which holds the\n"
9517 "reachable set in a decision diagram and solves K coupled level-CTMCs\n"
9518 "instead of the |S|-state generator, and cftp / cftp.approx, which draw\n"
9519 "iid states from the exact stationary law by coupling from the past; all\n"
9520 "three serve -a avg only, having no explicit chain to answer the rest\n"
9521 "from, and the cftp rows carry Monte Carlo error. mam ports -a avg,\n"
9522 "prob, cdf, tran and internals, which are @SolverMAM's getProb and\n"
9523 "getProbMarg (the joint (level, phase) law of the queue and its\n"
9524 "per-class marginals, truncated at --cutoff when the model is open),\n"
9525 "getCdfRespT with getPerctRespT beside it, getTranAvg over --tspan (the\n"
9526 "reference forces method ldqbd there, so --method does not select it),\n"
9527 "and getMAMResult, the M/G/1-type internals of a single queue. fluid\n"
9528 "additionally reads -a tran, -a prob, -a cdf, -a var and -a aoi, which\n"
9529 "are @SolverFLD's getTranAvg (the metrics along the trajectory, over\n"
9530 "--tspan or, without one, the horizon the reference's own adaptive loop\n"
9531 "converges at), getProbAggr (a law FITTED to the fluid means, so it does\n"
9532 "not agree with the mva or nc answer by construction), getCdfRespT (the\n"
9533 "response-time law per station and class, read off a marked-fluid\n"
9534 "integration started from the steady state), getMoments/getTranAvgVar\n"
9535 "and getAvgAoI with getCdfAoI beside it -- the last needing method mfq\n"
9536 "and the Source/Queue/Sink topology the age laws are defined for. It\n"
9537 "further reads -a odes, which is\n"
9538 "@SolverFLD/exportODEs, and --notation for the form it writes, and -a\n"
9539 "jacobian, which is @SolverFLD/getJacobian: d f_i / d x_j of the drift,\n"
9540 "differentiated locally over the structure of the system, with the\n"
9541 "equilibria beside it under --equilibria, which needs the\n"
9542 "line-sage-rest backend --symbolic names. Only the smooth methods have\n"
9543 "a Jacobian: min(n,S) has none where the regime switches, so the\n"
9544 "min-scaled drifts are refused by the factor that carries the kink.\n"
9545 "nc, ba and ctmc run\n"
9546 "under every --arith backend, ctmc's cftp method excepted: its sampler\n"
9547 "works in the log domain, so it refuses 'exact' by name rather than\n"
9548 "answering in a field it does not live in. mdd does run under 'exact'\n"
9549 "(its level solve drops Householder for a rational least squares there),\n"
9550 "but the LEVEL AGGREGATION is still an approximation away from product\n"
9551 "form: exact arithmetic pins the fixed point, not the model. mam is\n"
9552 "double only (its phase-type fitter\n"
9553 "needs transcendental arithmetic) and so is ssa (its sample path is\n"
9554 "generated from exponential clocks) and fluid (LSODA). ba reports a\n"
9555 "BOUND, not an estimate, and ssa a simulation carrying Monte Carlo\n"
9556 "error; neither is comparable with an exact solver except as such.\n"
9557 "\n"
9558 "Arithmetic: 'exact' computes in arbitrary-precision rationals and\n"
9559 "reports numerator and denominator alongside the double value; 'real'\n"
9560 "computes in fixed high-precision binary floating point, at 50, 100 or\n"
9561 "200 digits (a request in between is rounded up to the next tier).\n"
9562 "\n"
9563 "--api arguments are a JSON object keyed by the MATLAB parameter names,\n"
9564 "e.g. {\"L\": [[0.6,0.4]], \"N\": [2,1], \"Z\": [1,0.5]}: a 2-D array is a\n"
9565 "matrix (row-major), a 1-D array a row vector, a bare number a scalar.\n"
9566 "A JSON number is read as its shortest round-tripping decimal, so 0.6 is\n"
9567 "3/5 in exact arithmetic; pass a string such as \"1/3\" for anything else.\n",
9568 kVersion);
9569}
9570
9571/**
9572 * `--install`: the environment check, the C++ twin of MATLAB's `lineInstall`,
9573 * the JAR's `jline.cli.LineInstall` and Python's `line-install`.
9574 *
9575 * NOTHING IT LOOKS FOR IS REQUIRED. The C++ edition is header-only and its
9576 * native solvers stand alone, so every dependency probed here backs one
9577 * optional wrapper or backend. A miss is therefore a warning on stderr that
9578 * names the solvers it disables and how to install it, never an error: the
9579 * point of the command is to tell a fresh checkout which solvers it can
9580 * actually reach, not to refuse to run.
9581 *
9582 * @return true when nothing warned
9583 */
9584bool install_check() {
9585 bool has_warnings = false;
9586 // stdout is block-buffered when the check is piped or redirected while
9587 // stderr is not, so an unflushed progress line would surface AFTER the
9588 // warning it belongs to. Flush before every warning so the transcript reads
9589 // in the order the checks ran.
9590 const auto warn = [&](const std::string& text) {
9591 std::fflush(stdout);
9592 std::fprintf(stderr, "%s\n", text.c_str());
9593 std::fflush(stderr);
9594 has_warnings = true;
9595 };
9596
9597 std::printf("Checking LINE (C++)...\n");
9598 std::printf(" line-cli %s\n", kVersion);
9599
9600 std::printf("Checking Java runtime (JMT wrapper)...\n");
9601 const std::string java = line::jmt::detail::find_java();
9602 if (java.empty()) {
9603 warn("WARNING: no Java runtime was found in LINE_JAVA, JAVA_HOME or on PATH, so the "
9604 "JMT wrapper (-s jmt) cannot run. Install a JRE 8 or later.");
9605 } else {
9606 std::printf(" %s\n", java.c_str());
9607 }
9608
9609 std::printf("Checking JMT...\n");
9610 const std::string jmt_dir = line::jmt::detail::jmt_jar_path();
9611 if (jmt_dir.empty()) {
9612 warn("WARNING: JMT.jar was not found, this is required by the JMT wrapper. Download it "
9613 "from https://line-solver.sourceforge.net/latest/JMT.jar into common/, or point "
9614 "LINE_JMT_DIR at the folder holding it.");
9615 } else {
9616 std::printf(" %s/JMT.jar\n", jmt_dir.c_str());
9617 }
9618
9619 std::printf("Checking LQNS...\n");
9621 const std::string banner = line::lqns::lqns_version();
9622 if (banner.empty())
9623 warn("WARNING: lqns is not installed, this is required by the LQNS wrapper (-s lqns) "
9624 "for layered models. Download it at: https://github.com/layeredqueuing/dist");
9625 else
9626 warn("WARNING: the installed lqns is too old for LINE, which needs release 6 or "
9627 "later; it reports '" + banner + "'. Upgrade it from: "
9628 "https://github.com/layeredqueuing/dist");
9629 } else {
9630 std::printf(" %s\n", line::lqns::lqns_version().c_str());
9631 }
9632
9633 std::printf("Checking QNS (qnsolver)...\n");
9635 warn("WARNING: qnsolver is not installed, this is required by the QNS wrapper (-s qns). "
9636 "It ships with LQNS: https://github.com/layeredqueuing/dist");
9637
9638 std::printf("Checking symbolic backend (line-sage-rest)...\n");
9640 warn(std::string("WARNING: Docker is not available, so the SageMath symbolic backend "
9641 "cannot start. It is the only computer algebra system this edition "
9642 "reaches and is required by the symbolic methods of "
9643 "SolverCTMC/SolverFluid. Install Docker, then run: "
9644 "docker run -d -p 8080:8080 ") + line::sym::SYM_DOCKER_IMAGE);
9645 else if (line::sym::sym_find_image().empty())
9646 warn(std::string("WARNING: the line-sage-rest image is not present locally, this may be "
9647 "required by some LINE methods. Pull it with: "
9648 "docker run -d -p 8080:8080 ") + line::sym::SYM_DOCKER_IMAGE);
9649
9650 if (has_warnings)
9651 std::printf("Completed. LINE has warnings.\n");
9652 else
9653 std::printf("Success. LINE is ready to use.\n");
9654 return !has_warnings;
9655}
9656
9657void list_api() {
9658 const auto& reg = line::api_registry();
9659 std::printf("%-24s %-8s %-24s %s\n", "function", "domain", "arithmetic", "ported from");
9660 for (const auto& e : reg) {
9661 std::string modes;
9662 for (std::size_t k = 0; k < e.arith.size(); ++k) {
9663 if (k) modes += ",";
9664 modes += line::arith_name(e.arith[k]);
9665 }
9666 std::printf("%-24s %-8s %-24s %s\n", e.name.c_str(), e.domain.c_str(), modes.c_str(),
9667 e.reference.c_str());
9668 }
9669 std::printf("\n%zu of ~480 API functions ported.\n", reg.size());
9670}
9671
9672/**
9673 * Read the --api argument object: from the file named by --args, or from stdin
9674 * when --args is absent. A parse failure names the source and the position, so
9675 * a malformed file is a legible error rather than an empty argument set.
9676 */
9677line::reg::Json read_api_args(const std::string& path) {
9678 std::string text;
9679 std::string source;
9680 if (path.empty()) {
9681 source = "standard input";
9682 text.assign(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
9683 } else {
9684 source = "'" + path + "'";
9685 std::ifstream in(path.c_str());
9686 if (!in) throw line::InputError("cannot open the --args file " + source);
9687 text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
9688 }
9689 // No --args and nothing on stdin means the caller forgot the arguments.
9690 if (text.find_first_not_of(" \t\r\n") == std::string::npos)
9691 throw line::InputError("no --api arguments given: pass --args <path> or a JSON object on "
9692 "standard input (read from " +
9693 source + ")");
9694 try {
9695 return line::reg::Json::parse(text);
9696 } catch (const line::reg::Json::parse_error& e) {
9697 throw line::InputError("malformed --api arguments in " + source + ": " + e.what());
9698 }
9699}
9700
9701/**
9702 * The JAR CLI's `-a` spelling, mapped onto this port's token.
9703 *
9704 * `jline.cli.LineCLI` names its analyses with hyphenated, getter-shaped tokens
9705 * (`cdf-respt`, `prob-sys-aggr`, `tran-avg`) and this port names them by the
9706 * question (`cdf`, `prob`, `tran`), because one arm here answers what the JAR
9707 * splits across several: `-a prob` reports `getProbSys`, `getProbSysAggr` and
9708 * the per-station `getProb`/`getProbAggr` in ONE table, and `-a sample` walks
9709 * `sampleSys`, `sampleSysAggr` and the per-node pair in one trajectory. Both
9710 * spellings are therefore accepted and the JAR's collapse onto the arm that
9711 * already contains the answer -- a script written against the JAR CLI keeps
9712 * working, and nothing here is renamed to make that true.
9713 *
9714 * AN UNKNOWN METHOD NAME IS RETURNED UNCHANGED, not rejected here: the per-solver
9715 * whitelists downstream refuse by name and say which analyses that solver
9716 * serves, which is a better message than a table of every token in the CLI.
9717 */
9718std::string normalize_analysis(const std::string& a) {
9719 // Same question, different spelling.
9720 if (a == "cdf-respt" || a == "cdfrespt") return "cdf";
9721 if (a == "cdf-passt" || a == "cdfpasst") return "cdfpasst";
9722 if (a == "first-passt" || a == "cdf-firstpasst" || a == "cdffirstpasst") return "firstpasst";
9723 if (a == "first-passt-moments" || a == "firstpasst-moments" || a == "firstpasstmoments")
9724 return "firstpasstmom";
9725 if (a == "perct-respt" || a == "perctrespt") return "perct";
9726 if (a == "tran-avg" || a == "tranavg") return "tran";
9727 if (a == "tran-cdf-respt" || a == "trancdfrespt") return "trancdf";
9728 if (a == "tran-cdf-passt" || a == "trancdfpasst") return "trancdfpasst";
9729 if (a == "tran-prob" || a == "tranprob-sys-aggr") return "tranprob";
9730 if (a == "generator") return "gen";
9731 if (a == "state-space" || a == "statespace") return "states";
9732 if (a == "reward-steady" || a == "rewardsteady") return "reward";
9733 if (a == "reward-value" || a == "rewardvalue") return "rewardvalue";
9734 if (a == "node-chain" || a == "node-chain-table") return "nodechain";
9735 // The JAR's four probability getters and its four samplers, each answered
9736 // whole by one arm here. Collapsing them is not a loss: the arm emits every
9737 // one of the four, so a caller asking for the aggregate receives it beside
9738 // the joint rather than instead of it.
9739 if (a == "prob-aggr" || a == "prob-sys" || a == "prob-sys-aggr") return "prob";
9740 if (a == "prob-marg" || a == "probmarg") return "marg";
9741 if (a == "prob-sys-marg" || a == "probsysmarg" || a == "sys-marg") return "sysmarg";
9742 if (a == "sample-aggr" || a == "sample-sys" || a == "sample-sys-aggr") return "sample";
9743 // `getStageTable` IS `getAvgTable` on a Network model, in the reference and
9744 // in the JAR both: a network has one implicit stage, and only an
9745 // Environment has several. Mapped here rather than given an arm of its own,
9746 // because an arm would be a second name for one table and free to drift
9747 // from it.
9748 if (a == "stage") return "avg";
9749 return a;
9750}
9751
9752/** `-a` split on commas, each token normalized; never empty. */
9753std::vector<std::string> analysis_list(const std::string& spec) {
9754 std::vector<std::string> out;
9755 std::size_t at = 0;
9756 while (at <= spec.size()) {
9757 const std::size_t comma = spec.find(',', at);
9758 std::string tok =
9759 spec.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
9760 // A stray space around a comma is a typo, not a different analysis.
9761 while (!tok.empty() && std::isspace(static_cast<unsigned char>(tok.front())))
9762 tok.erase(tok.begin());
9763 while (!tok.empty() && std::isspace(static_cast<unsigned char>(tok.back())))
9764 tok.pop_back();
9765 if (tok.empty())
9766 throw line::InputError("-a takes a comma-separated list of analyses and one entry of '" +
9767 spec + "' is empty");
9768 // `all` is the JAR's composite of the station table and the system one,
9769 // expanded HERE rather than inside a solver arm so every downstream
9770 // whitelist sees the two analyses it already knows.
9771 if (normalize_analysis(tok) == "all") {
9772 out.push_back("avg");
9773 out.push_back("sys");
9774 } else {
9775 out.push_back(normalize_analysis(tok));
9776 }
9777 if (comma == std::string::npos) break;
9778 at = comma + 1;
9779 }
9780 if (out.empty()) throw line::InputError("-a takes at least one analysis");
9781 return out;
9782}
9783
9784struct Options {
9785 std::string file, input = "json", output = "readable", solver = "auto", analysis = "avg";
9786 std::string arith = "double", api, args;
9787 bool help = false, help_all = false, version = false, list = false;
9788 /**
9789 * `--find-solver [metric]`: report which solvers and methods can analyze the
9790 * model named by -f, and exit without solving it.
9791 *
9792 * `find_solver_all` is `--find-solver-all`, which keeps the refused pairs
9793 * and the reason each was refused. The report is arithmetic-independent --
9794 * it asks feature sets and shapes, not numbers -- so it always reads the
9795 * model at double and ignores --arith.
9796 */
9797 bool find_solver = false, find_solver_all = false;
9798 std::string find_solver_metric;
9799 /** `--install`: run the environment check and exit, solving nothing. */
9800 bool install = false;
9801 /**
9802 * Whether -i was actually passed.
9803 *
9804 * Without it a `.lqnx` path could not be recognised: `input` defaults to
9805 * json, and a defaulted json is indistinguishable from an explicit one, so
9806 * the extension sniff below would either never fire or would override a
9807 * caller who said `-i json` deliberately.
9808 */
9809 bool input_given = false;
9810 /**
9811 * `-p/--port` and `-m/--maxreq`: server mode.
9812 *
9813 * `port == 0` is "not given" and not "port 0": binding port 0 asks the
9814 * kernel for an ephemeral one, which a caller who typed no port did not
9815 * ask for. `maxreq == 0` is unbounded, matching the JAR's documented
9816 * "quit after this many requests" with no cap by default.
9817 */
9818 int port = 0;
9819 int maxreq = 0;
9820 Knobs knobs;
9821};
9822
9823/** Whether `file` ends in one of JMT's three simulation-document extensions. */
9824bool has_jsim_extension(const std::string& file) {
9825 const std::string::size_type dot = file.find_last_of('.');
9826 if (dot == std::string::npos) return false;
9827 std::string ext = file.substr(dot + 1);
9828 for (std::size_t i = 0; i < ext.size(); ++i)
9829 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
9830 return ext == "jsim" || ext == "jsimg" || ext == "jsimw";
9831}
9832
9833/** Whether `file` ends in the PNML extension. */
9834bool has_pnml_extension(const std::string& file) {
9835 const std::string::size_type dot = file.find_last_of('.');
9836 if (dot == std::string::npos) return false;
9837 std::string ext = file.substr(dot + 1);
9838 for (std::size_t i = 0; i < ext.size(); ++i)
9839 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
9840 return ext == "pnml";
9841}
9842
9843/** Whether `file` ends in one of the layered model's two extensions. */
9844bool has_lqn_extension(const std::string& file) {
9845 const std::string::size_type dot = file.find_last_of('.');
9846 if (dot == std::string::npos) return false;
9847 std::string ext = file.substr(dot + 1);
9848 for (std::size_t i = 0; i < ext.size(); ++i)
9849 ext[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(ext[i])));
9850 return ext == "lqnx" || ext == "xml";
9851}
9852
9853Options parse_args(int argc, char** argv) {
9854 Options o;
9855 for (int i = 1; i < argc; ++i) {
9856 std::string a = argv[i];
9857 auto next = [&](const char* what) -> std::string {
9858 if (i + 1 >= argc) throw line::InputError(std::string("missing value after ") + what);
9859 return argv[++i];
9860 };
9861 if (a == "-h" || a == "--help") o.help = true;
9862 else if (a == "--help-all" || a == "--help-full") o.help_all = true;
9863 else if (a == "-V" || a == "--version") o.version = true;
9864 else if (a == "--install") o.install = true;
9865 else if (a == "--list-api") o.list = true;
9866 else if (a == "--find-solver" || a == "--find-method" || a == "--help-model") {
9867 o.find_solver = true;
9868 // The metric is OPTIONAL, so it is taken only when the next token is
9869 // not itself a flag: `--find-solver -f m.json` must not swallow -f.
9870 if (i + 1 < argc && argv[i + 1][0] != '-') o.find_solver_metric = argv[++i];
9871 } else if (a == "--find-solver-all" || a == "--find-method-all") {
9872 o.find_solver = true;
9873 o.find_solver_all = true;
9874 if (i + 1 < argc && argv[i + 1][0] != '-') o.find_solver_metric = argv[++i];
9875 }
9876 else if (a == "-f" || a == "--file") o.file = next("-f");
9877 else if (a == "-i" || a == "--input") { o.input = next("-i"); o.input_given = true; }
9878 else if (a == "-o" || a == "--output") o.output = next("-o");
9879 else if (a == "-s" || a == "--solver") o.solver = next("-s");
9880 else if (a == "-a" || a == "--analysis") o.analysis = next("-a");
9881 else if (a == "--arith") o.arith = next("--arith");
9882 else if (a == "-p" || a == "--port") {
9883 const std::string v = next("-p");
9884 const long n = std::atol(v.c_str());
9885 if (n < 1 || n > 65535)
9886 throw line::InputError("-p takes a TCP port in 1..65535 (got '" + v + "')");
9887 o.port = static_cast<int>(n);
9888 } else if (a == "-m" || a == "--maxreq") {
9889 const std::string v = next("-m");
9890 const long n = std::atol(v.c_str());
9891 if (n < 1)
9892 throw line::InputError(
9893 "-m is the number of requests the server serves before quitting and must be "
9894 "positive; omit it to serve indefinitely (got '" + v + "')");
9895 o.maxreq = static_cast<int>(n);
9896 }
9897 else if (a == "--api") o.api = next("--api");
9898 else if (a == "--args") o.args = next("--args");
9899 else if (a == "--method") o.knobs.method = next("--method");
9900 else if (a == "--qrf-params") o.knobs.qrf_params = next("--qrf-params");
9901 else if (a == "--qrf-alpha") o.knobs.qrf_alpha = next("--qrf-alpha");
9902 else if (a == "--level") {
9903 const std::string v = next("--level");
9904 const int lv = std::atoi(v.c_str());
9905 if (lv < 1) throw line::InputError("--level must be a positive integer (got '" + v + "')");
9906 o.knobs.level = lv;
9907 }
9908 else if (a == "--samples") {
9909 const std::string v = next("--samples");
9910 const double d = std::atof(v.c_str()); // accepts 1e6 as well as 1000000
9911 if (!(d >= 1.0))
9912 throw line::InputError("--samples must be a positive count (got '" + v + "')");
9913 o.knobs.samples = static_cast<std::size_t>(d);
9914 } else if (a == "-d" || a == "--seed") {
9915 const std::string v = next("--seed");
9916 o.knobs.seed = std::strtoul(v.c_str(), nullptr, 10);
9917 if (o.knobs.seed == 0)
9918 throw line::InputError("--seed must be a positive integer (got '" + v + "')");
9919 } else if (a == "--warmupfrac") {
9920 const std::string v = next("--warmupfrac");
9921 const double f = std::atof(v.c_str());
9922 if (!(f >= 0.0 && f < 1.0))
9923 throw line::InputError(
9924 "--warmupfrac is the fraction of the path discarded before the means are "
9925 "taken and must lie in [0,1) (got '" + v + "')");
9926 o.knobs.warmupfrac = f;
9927 } else if (a == "--pstar") {
9928 const std::string v = next("--pstar");
9929 const double ps = std::atof(v.c_str());
9930 if (!(ps > 0.0))
9931 throw line::InputError(
9932 "--pstar is the exponent of the fluid p-norm smoothing and must be positive "
9933 "(got '" + v + "')");
9934 o.knobs.pstar = ps;
9935 } else if (a == "--busyperiod" || a == "--busyperiod-subnet") {
9936 // Same all-or-nothing parse as --marg-states: an entry dropped from
9937 // the list is a DIFFERENT report, not a shorter one.
9938 const bool orders = (a == "--busyperiod");
9939 const std::string v = next(orders ? "--busyperiod" : "--busyperiod-subnet");
9940 std::vector<std::size_t>& into = orders ? o.knobs.busy_orders : o.knobs.busy_subnet;
9941 std::size_t at = 0;
9942 while (at <= v.size()) {
9943 const std::size_t comma = v.find(',', at);
9944 const std::string tok =
9945 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
9946 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos ||
9947 std::atol(tok.c_str()) < 1)
9948 throw line::InputError(
9949 std::string(orders ? "--busyperiod takes a comma-separated list of "
9950 "positive orders"
9951 : "--busyperiod-subnet takes a comma-separated list of "
9952 "1-based station indexes") +
9953 " (got '" + v + "')");
9954 into.push_back(static_cast<std::size_t>(std::atol(tok.c_str())));
9955 if (comma == std::string::npos) break;
9956 at = comma + 1;
9957 }
9958 } else if (a == "--tol") o.knobs.tol = std::atof(next("--tol").c_str());
9959 else if (a == "--iter_tol") o.knobs.iter_tol = std::atof(next("--iter_tol").c_str());
9960 else if (a == "--iter_max") o.knobs.iter_max = std::atoi(next("--iter_max").c_str());
9961 else if (a == "--max-states") {
9962 const std::string v = next("--max-states");
9963 const long long n = std::atoll(v.c_str());
9964 if (n <= 0)
9965 throw line::InputError(
9966 "--max-states truncates an open agent's queue-length dimension and takes a "
9967 "positive state count (got '" + v + "')");
9968 o.knobs.max_states = n;
9969 }
9970 else if (a == "--multiserver") o.knobs.multiserver = next("--multiserver");
9971 else if (a == "--fork-join" || a == "--fork_join")
9972 o.knobs.fork_join = next("--fork-join");
9973 else if (a == "--tran-points" || a == "--tran_points") {
9974 const std::string v = next("--tran-points");
9975 const long n = std::atol(v.c_str());
9976 if (n < 2)
9977 throw line::InputError(
9978 "--tran-points is the number of points on the transient grid and needs at "
9979 "least two, a start and an end (got '" + v + "')");
9980 o.knobs.tran_points = static_cast<std::size_t>(n);
9981 }
9982 else if (a == "--mdd-tol" || a == "--mdd_tol") {
9983 const std::string v = next("--mdd-tol");
9984 const double d = std::atof(v.c_str());
9985 if (!(d > 0.0))
9986 throw line::InputError("--mdd-tol must be a positive tolerance (got '" + v + "')");
9987 o.knobs.mdd_tol = d;
9988 } else if (a == "--mdd-maxiter" || a == "--mdd_maxiter") {
9989 const std::string v = next("--mdd-maxiter");
9990 const long n = std::atol(v.c_str());
9991 if (n < 1)
9992 throw line::InputError(
9993 "--mdd-maxiter must be a positive sweep count (got '" + v + "')");
9994 o.knobs.mdd_maxiter = static_cast<int>(n);
9995 }
9996 else if (a == "--fj-accuracy") {
9997 const std::string v = next("--fj-accuracy");
9998 const long n = std::atol(v.c_str());
9999 if (n < 1)
10000 throw line::InputError(
10001 "--fj-accuracy is the FJ_codes truncation C of the queue-length difference "
10002 "between the two fork-join branches and must be at least 1 (got '" + v + "')");
10003 o.knobs.fj_accuracy = static_cast<int>(n);
10004 } else if (a == "--fj-tmode") {
10005 const std::string v = next("--fj-tmode");
10006 if (v != "NARE" && v != "Sylves")
10007 throw line::InputError(
10008 "--fj-tmode selects how computeT.m solves for the T matrix and is 'NARE' (the "
10009 "Riccati route, the default) or 'Sylves' (the fixed-point iteration); got '" +
10010 v + "'");
10011 o.knobs.fj_tmode = v;
10012 } else if (a == "--timescale") {
10013 const std::string v = next("--timescale");
10014 if (v != "auto" && v != "discrete" && v != "continuous")
10015 throw line::InputError(
10016 "--timescale decides whether the model is read on a slot lattice and is "
10017 "'auto' (the default), 'discrete' or 'continuous'; got '" + v + "'");
10018 o.knobs.timescale = v;
10019 }
10020 else if (a == "--force") {
10021 o.knobs.force = true;
10022 }
10023 else if (a == "--cutoff") {
10024 const std::string v = next("--cutoff");
10025 if (v.find(',') != std::string::npos || v.find(';') != std::string::npos) {
10026 o.knobs.cutoff_mat = parse_cutoff_matrix(v);
10027 if (o.knobs.cutoff_mat.empty())
10028 throw line::InputError(
10029 "--cutoff takes a number or a per-(station,class) matrix written "
10030 "'r1c1,r1c2;r2c1,r2c2' (got '" + v + "')");
10031 } else {
10032 const double d = std::atof(v.c_str());
10033 if (!(d >= 1.0))
10034 throw line::InputError(
10035 "--cutoff must be a positive job count per open class (got '" + v + "')");
10036 o.knobs.cutoff = d;
10037 }
10038 } else if (a == "--tspan" || a == "--timespan") {
10039 const std::string v = next("--tspan");
10040 // BOTH SEPARATORS. This port has always written the horizon
10041 // `t0:t1`, and `jline.cli.LineCLI --timespan` has always written it
10042 // `t0,t1`; accepting only one made a command line that names a
10043 // horizon unportable between the two CLIs even after the flag names
10044 // were reconciled.
10045 std::string::size_type sep = v.find(':');
10046 if (sep == std::string::npos) sep = v.find(',');
10047 // A bare value is the END of the horizon and starts at 0, which is
10048 // what a transient from the initial state means; `t0:t1` states both.
10049 const double lo = sep == std::string::npos ? 0.0 : std::atof(v.substr(0, sep).c_str());
10050 const double hi = std::atof(
10051 (sep == std::string::npos ? v : v.substr(sep + 1)).c_str());
10052 // An INFINITE horizon is refused here rather than integrated to:
10053 // pi(t) on [0, Inf) is the stationary vector, which -a avg reports.
10054 if (!(hi > lo) || !(lo >= 0.0) || !std::isfinite(hi))
10055 throw line::InputError(
10056 "--tspan must be a finite horizon 0 <= t0 < t1, given as <t1>, <t0>:<t1> or "
10057 "<t0>,<t1> (got '" + v + "')");
10058 o.knobs.t0 = lo;
10059 o.knobs.t1 = hi;
10060 } else if (a == "-n" || a == "--node") {
10061 const std::string v = next("--node");
10062 const long n = std::atol(v.c_str());
10063 if (n < 1)
10064 throw line::InputError("--node must be a positive 1-based node index (got '" + v +
10065 "')");
10066 o.knobs.node = static_cast<std::size_t>(n);
10067 } else if (a == "-c" || a == "--class") {
10068 const std::string v = next("--class");
10069 const long c = std::atol(v.c_str());
10070 if (c < 1)
10071 throw line::InputError("--class must be a positive 1-based class index (got '" + v +
10072 "')");
10073 o.knobs.jobclass = static_cast<std::size_t>(c);
10074 } else if (a == "--marg-states" || a == "--marg_states") {
10075 // Every entry must parse: a dropped one shortens the curve silently.
10076 const std::string v = next("--marg-states");
10077 std::size_t at = 0;
10078 while (at <= v.size()) {
10079 const std::size_t comma = v.find(',', at);
10080 const std::string tok =
10081 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
10082 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos)
10083 throw line::InputError(
10084 "--marg-states takes a comma-separated list of non-negative job counts "
10085 "(got '" + v + "')");
10086 o.knobs.marg_states.push_back(std::atol(tok.c_str()));
10087 if (comma == std::string::npos) break;
10088 at = comma + 1;
10089 }
10090 } else if (a == "--state") {
10091 // Same all-or-nothing discipline as --marg-states: a state vector
10092 // with one entry dropped is a DIFFERENT state, not a shorter one,
10093 // and the length is checked against the node's own space later.
10094 const std::string v = next("--state");
10095 std::size_t at = 0;
10096 while (at <= v.size()) {
10097 const std::size_t comma = v.find(',', at);
10098 const std::string tok =
10099 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
10100 if (tok.empty() || tok.find_first_not_of("0123456789") != std::string::npos)
10101 throw line::InputError(
10102 "--state is the state vector -a prob asks about and takes a "
10103 "comma-separated list of non-negative counts (got '" + v + "')");
10104 o.knobs.state.push_back(std::atol(tok.c_str()));
10105 if (comma == std::string::npos) break;
10106 at = comma + 1;
10107 }
10108 } else if (a == "--events") {
10109 const std::string v = next("--events");
10110 const double d = std::atof(v.c_str()); // accepts 5e3 as well as 5000
10111 if (!(d >= 1.0))
10112 throw line::InputError(
10113 "--events is the length of a sampled trajectory and must be a positive event "
10114 "count (got '" + v + "')");
10115 o.knobs.events = static_cast<std::size_t>(d);
10116 } else if (a == "--timestep") {
10117 const std::string v = next("--timestep");
10118 const double d = std::atof(v.c_str());
10119 if (!(d > 0.0) || !std::isfinite(d))
10120 throw line::InputError(
10121 "--timestep is the fixed output step of a transient analysis and must be a "
10122 "positive finite time (got '" + v + "')");
10123 o.knobs.timestep = d;
10124 } else if (a == "--percentiles") {
10125 const std::string v = next("--percentiles");
10126 std::size_t at = 0;
10127 while (at <= v.size()) {
10128 const std::size_t comma = v.find(',', at);
10129 const std::string tok =
10130 v.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
10131 if (tok.empty())
10132 throw line::InputError(
10133 "--percentiles takes a comma-separated list of levels (got '" + v + "')");
10134 double p = std::atof(tok.c_str());
10135 // A LEVEL ABOVE 1 IS A PERCENT, below it a probability. The JAR
10136 // documents `--percentiles 50,90,95,99` and MATLAB stores
10137 // `pers_stored` as fractions, so both spellings reach this port
10138 // and the magnitude is what tells them apart. 1 itself is read
10139 // as the fraction: P(T <= t) = 1 is a level, 1% is not one
10140 // anybody asks for beside 50, 90 and 99.
10141 if (p > 1.0) p /= 100.0;
10142 if (!(p > 0.0) || !(p < 1.0))
10143 throw line::InputError(
10144 "--percentiles levels lie strictly inside (0,1) as fractions or (0,100) "
10145 "as percents; the 100th percentile of an unbounded law is not finite "
10146 "(got '" + tok + "')");
10147 o.knobs.percentiles.push_back(p);
10148 if (comma == std::string::npos) break;
10149 at = comma + 1;
10150 }
10151 } else if (a == "--reward-name" || a == "--reward_name") {
10152 o.knobs.reward_name = next("--reward-name");
10153 } else if (a == "--notation") {
10154 const std::string v = next("--notation");
10155 // The exporter validates the name; it is not defaulted here, so an
10156 // unrecognised notation is refused rather than answered with scalar.
10157 o.knobs.notation = v;
10158 } else if (a == "--symbolic") {
10159 // `sym_resolve` validates the name: auto, none, a URL or an image.
10160 // It is not defaulted here, so `--symbolic none` stays local rather
10161 // than being read as "not given" and searching anyway.
10162 o.knobs.symbolic = next("--symbolic");
10163 } else if (a == "--equilibria") o.knobs.equilibria = true;
10164 else if (a == "--perm-engine") {
10165 // The analyzer validates the name; an unrecognised engine is
10166 // refused rather than answered with exact.
10167 o.knobs.method_perm = next("--perm-engine");
10168 } else if (a == "--transient-method") {
10169 // The analyzer validates the name, so an unrecognised one is
10170 // refused rather than answered with the default.
10171 o.knobs.transient_method = next("--transient-method");
10172 } else if (a == "--fau-epsilon") {
10173 const std::string v = next("--fau-epsilon");
10174 const double d = std::atof(v.c_str());
10175 if (!(d > 0.0) || !std::isfinite(d))
10176 throw line::InputError(
10177 "--fau-epsilon is the probability mass the transient grid may discard and "
10178 "must be a positive finite number (got '" + v + "')");
10179 o.knobs.fau_epsilon = d;
10180 } else if (a == "--fau-delta") {
10181 const std::string v = next("--fau-delta");
10182 const double d = std::atof(v.c_str());
10183 if (!(d >= 0.0) || !std::isfinite(d))
10184 throw line::InputError(
10185 "--fau-delta is the occupancy below which a state is dropped and must be a "
10186 "nonnegative finite number (got '" + v + "')");
10187 o.knobs.fau_delta = d;
10188 } else if (a == "--cdf-algorithm") {
10189 // The analyzer validates the name; it is not defaulted here, so an
10190 // unrecognised algorithm is refused rather than answered with exact.
10191 o.knobs.cdf_algorithm = next("--cdf-algorithm");
10192 } else if (a == "--passage-from") o.knobs.passage_from = next("--passage-from");
10193 else if (a == "--passage-into") o.knobs.passage_into = next("--passage-into");
10194 else if (a == "--passage-method") o.knobs.passage_method = next("--passage-method");
10195 else if (a == "--passage-orders")
10196 o.knobs.passage_orders = static_cast<std::size_t>(std::stoul(next("--passage-orders")));
10197 else if (a == "--no-interlocking") o.knobs.no_interlocking = true;
10198 else if (a == "--layer-solver") o.knobs.layer_solver = next("--layer-solver");
10199 else if (a == "--stage-solver") o.knobs.stage_solver = next("--stage-solver");
10200 else if (a == "--ln-transient") o.knobs.ln_transient = next("--ln-transient");
10201 else if (a == "--ln-transient-channels")
10202 o.knobs.ln_transient_channels = next("--ln-transient-channels");
10203 else if (a == "--sens-method") o.knobs.sens_method = next("--sens-method");
10204 else if (a == "--sens-scheme") o.knobs.sens_scheme = next("--sens-scheme");
10205 else if (a == "--sens-step") {
10206 const std::string v = next("--sens-step");
10207 const double h = std::atof(v.c_str());
10208 if (!(h > 0.0) || !(h < 1.0))
10209 throw line::InputError(
10210 "--sens-step is the RELATIVE rate perturbation and must lie in (0,1) (got '" +
10211 v + "')");
10212 o.knobs.sens_step = h;
10213 }
10214 else if (a == "--uq-solver") o.knobs.uq_solver = next("--uq-solver");
10215 else if (a == "--keep") o.knobs.keep = true;
10216 else if (a == "--verbose") o.knobs.verbose = true;
10217 else if (a == "--remote") o.knobs.remote = true;
10218 else if (a == "--remote-url") {
10219 // Implies --remote: a URL given and then ignored because the flag
10220 // was forgotten would solve LOCALLY and report nothing about it.
10221 o.knobs.remote_url = next("--remote-url");
10222 o.knobs.remote = true;
10223 }
10224 else if (a == "--timeout") {
10225 const std::string v = next("--timeout");
10226 const long s = std::atol(v.c_str());
10227 if (s < 1)
10228 throw line::InputError("--timeout is a deadline in seconds and must be positive "
10229 "(got '" + v + "')");
10230 o.knobs.timeout_seconds = static_cast<int>(s);
10231 }
10232 else if (a == "--repeat") {
10233 const std::string v = next("--repeat");
10234 const long n = std::atol(v.c_str());
10235 if (n < 1)
10236 throw line::InputError("--repeat must be a positive run count (got '" + v + "')");
10237 o.knobs.repeat = static_cast<int>(n);
10238 }
10239 // ---- the simulator's own knobs ------------------------------------
10240 else if (a == "--ldes-tranfilter") {
10241 const std::string v = next("--ldes-tranfilter");
10242 if (v != "mser5" && v != "fixed" && v != "none")
10243 throw line::InputError(
10244 "--ldes-tranfilter selects the warmup filter and is mser5, fixed or none (got '" +
10245 v + "')");
10246 o.knobs.ldes_tranfilter = v;
10247 }
10248 else if (a == "--ldes-warmupfrac") {
10249 const std::string v = next("--ldes-warmupfrac");
10250 const double d = std::atof(v.c_str());
10251 if (!(d >= 0.0 && d < 1.0))
10252 throw line::InputError(
10253 "--ldes-warmupfrac is the fraction of the run the fixed filter discards and "
10254 "lies in [0,1) (got '" + v + "')");
10255 o.knobs.ldes_warmupfrac = d;
10256 }
10257 else if (a == "--ldes-cimethod") {
10258 const std::string v = next("--ldes-cimethod");
10259 if (v != "obm" && v != "bm" && v != "spectral" && v != "none")
10260 throw line::InputError(
10261 "--ldes-cimethod selects the confidence-interval estimator and is obm, bm, "
10262 "spectral or none (got '" + v + "')");
10263 o.knobs.ldes_cimethod = v;
10264 }
10265 else if (a == "--ldes-cnvgon") o.knobs.ldes_cnvgon = true;
10266 else if (a == "--ldes-cnvgtol") {
10267 // Implies --ldes-cnvgon: a tolerance given and then ignored because
10268 // the switch was forgotten would run the full budget and say nothing.
10269 const std::string v = next("--ldes-cnvgtol");
10270 const double d = std::atof(v.c_str());
10271 if (!(d > 0.0 && d < 1.0))
10272 throw line::InputError(
10273 "--ldes-cnvgtol is a RELATIVE precision target and lies in (0,1) (got '" + v +
10274 "')");
10275 o.knobs.ldes_cnvgtol = d;
10276 o.knobs.ldes_cnvgon = true;
10277 }
10278 else if (a == "--ldes-slotted") o.knobs.ldes_slotted = true;
10279 else if (a == "--slotted") o.knobs.slotted = true;
10280 else if (a == "--slotlength") {
10281 // Implies --slotted, as --ldes-slotlength does for the simulator.
10282 const std::string v = next("--slotlength");
10283 const double d = std::atof(v.c_str());
10284 if (!(d > 0.0))
10285 throw line::InputError(
10286 "--slotlength is the slot of the discrete time scale and must be positive "
10287 "(got '" + v + "')");
10288 o.knobs.slotlength = d;
10289 o.knobs.slotted = true;
10290 }
10291 else if (a == "--ldes-slotlength") {
10292 // Implies --ldes-slotted, as --slotlength does on the engine's CLI.
10293 const std::string v = next("--ldes-slotlength");
10294 const double d = std::atof(v.c_str());
10295 if (!(d > 0.0))
10296 throw line::InputError(
10297 "--ldes-slotlength is the slot of the discrete time scale and must be positive "
10298 "(got '" + v + "')");
10299 o.knobs.ldes_slotlength = d;
10300 o.knobs.ldes_slotted = true;
10301 }
10302 else if (a == "--ldes-replications") {
10303 const std::string v = next("--ldes-replications");
10304 const long n = std::atol(v.c_str());
10305 if (n < 1)
10306 throw line::InputError(
10307 "--ldes-replications is a positive count of independent runs (got '" + v + "')");
10308 o.knobs.ldes_replications = static_cast<int>(n);
10309 }
10310 else if (a == "--ldes-numthreads") {
10311 const std::string v = next("--ldes-numthreads");
10312 const long n = std::atol(v.c_str());
10313 if (n < 1)
10314 throw line::InputError(
10315 "--ldes-numthreads is a positive worker count (got '" + v + "')");
10316 o.knobs.ldes_numthreads = static_cast<int>(n);
10317 }
10318 else if (a == "--ldes-maxtime") {
10319 const std::string v = next("--ldes-maxtime");
10320 const double d = std::atof(v.c_str());
10321 if (!(d > 0.0))
10322 throw line::InputError(
10323 "--ldes-maxtime is a wall-clock budget in seconds and must be positive (got '" +
10324 v + "')");
10325 o.knobs.ldes_maxtime = d;
10326 }
10327 else if (a == "--ldes-initsol") {
10328 // A STATION-MAJOR placement, [st0_cl0, st0_cl1, ...]; the engine
10329 // reads it as the initial state and skips its default placement.
10330 const std::string v = next("--ldes-initsol");
10331 std::size_t b = 0;
10332 while (b <= v.size()) {
10333 const std::size_t e = v.find(',', b);
10334 const std::string tok =
10335 v.substr(b, e == std::string::npos ? std::string::npos : e - b);
10336 if (tok.empty())
10337 throw line::InputError(
10338 "--ldes-initsol is a comma-separated placement with no empty entry (got '" +
10339 v + "')");
10340 o.knobs.ldes_initsol.push_back(std::atof(tok.c_str()));
10341 if (e == std::string::npos) break;
10342 b = e + 1;
10343 }
10344 }
10345 else if (a == "--ldes-rest-url") o.knobs.ldes_rest_url = next("--ldes-rest-url");
10346 else if (a == "-v" || a == "--verbosity") {
10347 const std::string v = next(a.c_str());
10348 // The JAR names two levels and this port's help documents three;
10349 // all five spellings are accepted, and an unknown one is refused
10350 // rather than read as `standard`, which would silence nothing while
10351 // reporting that it had.
10352 if (v != "silent" && v != "standard" && v != "normal" && v != "debug" &&
10353 v != "verbose")
10354 throw line::InputError(
10355 "-v takes silent, standard (the JAR spells it normal) or debug; got '" + v +
10356 "'");
10357 o.knobs.verbosity = (v == "normal") ? "standard" : v;
10358 }
10359 else if (!a.empty() && a[0] == '-')
10360 throw line::InputError("unknown option: " + a);
10361 else
10362 o.file = a;
10363 }
10364 return o;
10365}
10366
10367} // namespace
10368
10369/**
10370 * Everything one invocation does once the arguments are in hand.
10371 *
10372 * SPLIT OUT OF `main` FOR SERVER MODE, which runs it once per request with a
10373 * different `-f` and a captured stdout. Keeping one body means a request served
10374 * over the socket takes exactly the path the same command line takes at the
10375 * shell -- the failure this avoids is a server that answers slightly differently
10376 * from the CLI it is supposed to BE.
10377 */
10378/**
10379 * `--find-solver`: which solvers and methods can analyze the model named by -f.
10380 *
10381 * It reports rather than solves, so it stops before the solver ladder in
10382 * `solve_model_dispatch` and before every knob that describes a run. The answer
10383 * is arithmetic-independent -- `auto_find_solver` asks feature sets, shapes and
10384 * gates, never numbers -- so the model is read at double whatever --arith says,
10385 * and a caller who passed one is told rather than silently obeyed.
10386 *
10387 * The layered path is not covered: `auto_find_solver` narrows the flat Network
10388 * families, and a LayeredNetwork's are `ln` and `lqns`, which this port reaches
10389 * through `solve_lqn_dispatch` and not through an AUTO of its own.
10390 */
10391int find_solver_report(const Options& o) {
10392 if (o.file.empty())
10393 throw line::InputError("--find-solver reports on a model; name one with -f");
10394 if (!o.input_given && (has_lqn_extension(o.file) || line::io::is_layered_json(o.file)))
10396 "--find-solver reports on a flat Network model; a layered one is solved by -s ln "
10397 "and -s lqns, which this port reaches through the -i lqnx path");
10398 line::qn::Network<double> net = read_model<double>(o.file);
10399 const std::vector<line::autosolver::SolverCandidate> rows = line::autosolver::auto_find_solver(
10400 net.get_struct(), o.find_solver_metric, o.find_solver_all);
10401 std::printf("%s", line::autosolver::auto_find_solver_table(rows).c_str());
10402 return 0;
10403}
10404
10405int run_invocation(Options o) {
10406 // Validated on every path, not only --api: an unrecognised --arith is a
10407 // caller error whatever else the invocation asks for.
10408 line::reg::parse_arith(o.arith);
10409 if (!o.api.empty()) {
10410 if (o.output != "readable" && o.output != "json")
10411 throw line::InputError("unknown -o '" + o.output +
10412 "'; accepted forms are: readable, json");
10413 const line::reg::Json result =
10414 line::reg::api_invoke(o.api, o.arith, read_api_args(o.args));
10415 if (o.output == "json")
10416 std::printf("%s\n", result.dump(2).c_str());
10417 else
10418 std::printf("%s", line::reg::api_render_readable(result).c_str());
10419 return 0;
10420 }
10421 // A .lqnx/.xml path with no -i is taken as a layered model, so naming
10422 // the file is enough to solve it. An explicit -i always wins, so a
10423 // caller who says -i json about an oddly-named file still gets json.
10424 if (!o.input_given && has_lqn_extension(o.file)) o.input = "lqnx";
10425 // The same courtesy for a JMT document: naming the file is enough.
10426 if (!o.input_given && has_jsim_extension(o.file)) o.input = "jsimg";
10427 // And for a PNML document.
10428 if (!o.input_given && has_pnml_extension(o.file)) o.input = "pnml";
10429 // A model.json can carry EITHER model kind, and `linemodel_save` writes
10430 // `.json` for both, so the extension cannot decide it. The content can:
10431 // a `LayeredNetwork` type takes the layered path whatever `-i` says,
10432 // because handing it to the Network reader only produces "model type
10433 // 'LayeredNetwork' is not a Network" one frame further down.
10434 if (!o.file.empty() && o.input != "lqnx" && o.input != "xml" && o.input != "pnml" &&
10435 !has_pnml_extension(o.file) &&
10436 !has_jsim_extension(o.file) && o.input.compare(0, 4, "jsim") != 0 &&
10438 o.input = "lqnx";
10439 if (o.input == "lqnx" || o.input == "xml") {
10440 if (o.output != "readable" && o.output != "json" && o.output != "layers")
10441 throw line::InputError("unknown -o '" + o.output +
10442 "'; accepted forms on the layered path are: readable, "
10443 "json, layers");
10444 // ONE ENVELOPE PER ANALYSIS, in the order asked for. `-a avg,sens`
10445 // is the JAR's comma list, and the JAR merges the results into one
10446 // JSON object; here each arm prints as it computes, so the multi
10447 // form emits a sequence of the SAME envelopes a single `-a` emits
10448 // (JSON Lines). That keeps one parser for both spellings, where a
10449 // merged object would need a second one for the multi case alone.
10450 const std::vector<std::string> as = analysis_list(o.analysis);
10451 for (std::size_t i = 0; i + 1 < as.size(); ++i) {
10452 const int rc = solve_lqn_dispatch(o.arith, o.solver, as[i], o.output, o.file,
10453 o.knobs);
10454 if (rc != 0) return rc;
10455 }
10456 return solve_lqn_dispatch(o.arith, o.solver, as.back(), o.output, o.file, o.knobs);
10457 }
10458 // `-i jsim | jsimg | jsimw`: a JMT simulation document, read by
10459 // `read_jsim`. The three extensions name ONE format -- JMT writes the
10460 // same `<sim>` document under all three -- and are accepted separately
10461 // because the JAR CLI accepts them separately and a script naming the
10462 // wrong one is not describing a different model.
10463 if (o.input == "jsim" || o.input == "jsimg" || o.input == "jsimw")
10464 g_jsim_input = true;
10465 else if (o.input == "pnml")
10466 g_pnml_input = true;
10467 else if (o.input != "json")
10469 "the model-solving path reads -i json for a Network model, -i jsim|jsimg|jsimw "
10470 "for a JMT simulation document, -i pnml for a place/transition net and "
10471 "-i lqnx|xml for a layered one (got '" + o.input + "')");
10472 // `-o json` IS WIRED FOR EVERY ANALYSIS, not only `-a avg`. It was
10473 // avg-only, and the restriction was real rather than nominal: each other
10474 // arm printed its own readable shape and nothing else, so accepting
10475 // `json` for one would have handed a caller a table it asked to receive
10476 // as JSON -- the silent-acceptance failure the Knobs struct exists to
10477 // prevent, one layer up. The refusal is therefore removed only now that
10478 // every arm emits a payload (`emit_analysis`), and each answers under a
10479 // key named after its own `-a` so a caller can tell which question was
10480 // answered. On `-a avg` the solver BANNER still precedes the object on
10481 // stdout, as the JAR's does; it contains no brace, so the wrapper's
10482 // first-`{` scan is unaffected. The other arms print no banner: what it
10483 // carried -- the arithmetic, the resolved method, the state count and the
10484 // cutoff -- is inside the payload, where a host reads it as data instead
10485 // of scraping it from a line above.
10486 if (o.output != "readable" && o.output != "json")
10487 throw line::InputError("unknown -o '" + o.output +
10488 "'; accepted forms are: readable, json");
10489 g_json_output = (o.output == "json");
10490 // See the layered path above for why a comma list emits one envelope
10491 // per analysis rather than one merged object.
10492 const std::vector<std::string> as = analysis_list(o.analysis);
10493 for (std::size_t i = 0; i + 1 < as.size(); ++i) {
10494 const int rc = solve_model_dispatch(o.arith, o.solver, as[i], o.file, o.knobs);
10495 if (rc != 0) return rc;
10496 }
10497 return solve_model_dispatch(o.arith, o.solver, as.back(), o.file, o.knobs);
10498}
10499
10500/**
10501 * Run one invocation with stdout captured, and return what it printed.
10502 *
10503 * SERVER MODE'S ONE PIECE OF MACHINERY. Every arm of this CLI writes its answer
10504 * with `printf`, which is the right thing for a command-line tool and leaves a
10505 * server nothing to send. Redirecting fd 1 around the call means the arms need
10506 * no server-aware variant and cannot drift from the command-line behaviour;
10507 * stderr is deliberately NOT captured, so a warning still reaches the operator's
10508 * console rather than being folded into the client's answer.
10509 */
10510std::string run_invocation_captured(Options o, int& rc) {
10511 const char* tmpdir = std::getenv("TMPDIR");
10512 std::string path = std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + "/line-cli-out-XXXXXX";
10513 std::vector<char> buf(path.begin(), path.end());
10514 buf.push_back('\0');
10515 const int tfd = ::mkstemp(&buf[0]);
10516 if (tfd < 0) {
10517 rc = 3;
10518 return "line-cli: cannot create a capture file for the response\n";
10519 }
10520 path.assign(&buf[0]);
10521 std::fflush(stdout);
10522 const int saved = ::dup(1);
10523 ::dup2(tfd, 1);
10524 std::string err;
10525 try {
10526 rc = run_invocation(o);
10527 } catch (const line::Error& e) {
10528 rc = 2;
10529 err = std::string("line-cli: ") + e.what() + "\n";
10530 } catch (const std::exception& e) {
10531 rc = 3;
10532 err = std::string("line-cli: unexpected failure: ") + e.what() + "\n";
10533 }
10534 std::fflush(stdout);
10535 ::dup2(saved, 1);
10536 ::close(saved);
10537 ::close(tfd);
10538 std::ifstream in(path.c_str());
10539 std::string out((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
10540 in.close();
10541 std::remove(path.c_str());
10542 // THE ERROR IS THE ANSWER when the solve failed: a client that receives an
10543 // empty message cannot tell a refusal from a model with no rows.
10544 return err.empty() ? out : out + err;
10545}
10546
10547/**
10548 * `-p/--port`: serve solve requests over a WebSocket, as `LineWebSocketServer`
10549 * does.
10550 *
10551 * THE PROTOCOL IS THE JAR's, unchanged: one text message per connection, whose
10552 * FIRST LINE is the comma-separated argument list and whose remainder is the
10553 * model document. The JAR overwrites the first two arguments with `--file` and
10554 * the path it staged the document at, so the client's own first two tokens are
10555 * placeholders; the same substitution happens here, which is what lets an
10556 * existing client talk to this server without knowing which binary answered.
10557 */
10558int run_server(const Options& base) {
10559 line::ws::Server server(base.port);
10560 std::printf("--------------------------------------------------------------------\n");
10561 std::printf("LINE Solver - Command Line Interface (C++)\n");
10562 std::printf("Copyright (c) 2012-2026, QORE Lab, Imperial College London\n");
10563 std::printf("Version %s. All rights reserved.\n", kVersion);
10564 std::printf("--------------------------------------------------------------------\n");
10565 std::printf("Running in server mode on port %d.\n", base.port);
10566 if (base.maxreq)
10567 std::printf("Quitting after %d request(s).\n", base.maxreq);
10568 std::fflush(stdout);
10569
10570 int served = 0;
10571 while (base.maxreq == 0 || served < base.maxreq) {
10572 const bool ok = server.serve_one([&](const std::string& msg) -> std::string {
10573 const std::string::size_type nl = msg.find('\n');
10574 if (nl == std::string::npos)
10575 return "line-cli: the request's first line is the argument list and its "
10576 "remainder is the model document; this message has no newline\n";
10577 const std::string argline = msg.substr(0, nl);
10578 const std::string model = msg.substr(nl + 1);
10579
10580 const char* tmpdir = std::getenv("TMPDIR");
10581 std::string path =
10582 std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + "/line-cli-req-XXXXXX";
10583 std::vector<char> nb(path.begin(), path.end());
10584 nb.push_back('\0');
10585 const int mfd = ::mkstemp(&nb[0]);
10586 if (mfd < 0) return "line-cli: cannot stage the client model\n";
10587 ::close(mfd);
10588 path.assign(&nb[0]);
10589 {
10590 std::ofstream mf(path.c_str());
10591 mf << model;
10592 }
10593
10594 // The argument list, with the first two tokens replaced by the
10595 // staged path exactly as `LineWebSocketServer.onMessage` replaces
10596 // them. A list SHORTER than two is the client's error and is
10597 // reported rather than padded, since padding would solve the
10598 // default model instead of the one it sent.
10599 std::vector<std::string> toks;
10600 std::string::size_type at = 0;
10601 while (at <= argline.size()) {
10602 const std::string::size_type comma = argline.find(',', at);
10603 toks.push_back(argline.substr(
10604 at, comma == std::string::npos ? std::string::npos : comma - at));
10605 if (comma == std::string::npos) break;
10606 at = comma + 1;
10607 }
10608 std::string result;
10609 if (toks.size() < 2) {
10610 result = "line-cli: the argument list needs at least two tokens; the first two "
10611 "are replaced by --file and the staged model path\n";
10612 } else {
10613 toks[0] = "--file";
10614 toks[1] = path;
10615 std::vector<char*> argv;
10616 std::vector<std::string> store;
10617 store.push_back("line-cli");
10618 for (std::size_t i = 0; i < toks.size(); ++i) store.push_back(toks[i]);
10619 for (std::size_t i = 0; i < store.size(); ++i)
10620 argv.push_back(const_cast<char*>(store[i].c_str()));
10621 int rc = 0;
10622 try {
10623 Options ro = parse_args(static_cast<int>(argv.size()), &argv[0]);
10624 // The server's own flags never travel into a request: a
10625 // client that sent `-p` would otherwise make the server
10626 // recurse into a second listener on the same process.
10627 ro.port = 0;
10628 ro.maxreq = 0;
10629 result = run_invocation_captured(ro, rc);
10630 } catch (const line::Error& e) {
10631 result = std::string("line-cli: ") + e.what() + "\n";
10632 }
10633 }
10634 std::remove(path.c_str());
10635 return result;
10636 });
10637 // A dropped client is not a reason to stop serving, and it does not
10638 // count against --maxreq either: the JAR counts REQUESTS, and a peer
10639 // that vanished before sending one made none.
10640 if (ok) ++served;
10641 }
10642 return 0;
10643}
10644
10645int main(int argc, char** argv) {
10646 try {
10647 Options o = parse_args(argc, argv);
10648 // Before any arm runs: `read_model` raises the priority warning and has
10649 // no knobs of its own to read the level from.
10650 if (!o.knobs.verbosity.empty()) g_verbosity = o.knobs.verbosity;
10651 // Solver console: set for the whole process, so that the model compile
10652 // narrates too -- LineConsole::writes() falls back to the session level
10653 // when no run is open, and reading the model happens before any solver
10654 // exists. `-v debug` (and its `verbose` spelling) is the ONLY way in.
10656 g_verbosity == "silent" ? line::util::VerboseLevel::SILENT
10657 : (g_verbosity == "debug" || g_verbosity == "verbose")
10660 if (o.help_all) {
10661 print_help();
10662 return 0;
10663 }
10664 if (o.help || argc == 1) {
10665 print_brief_help();
10666 return 0;
10667 }
10668 if (o.version) {
10669 std::printf("line-cli %s\n", kVersion);
10670 return 0;
10671 }
10672 // A warning is not a failure: every dependency the check probes is
10673 // optional, so the command exits 0 whenever the check itself ran.
10674 if (o.install) {
10675 install_check();
10676 return 0;
10677 }
10678 if (o.list) {
10679 list_api();
10680 return 0;
10681 }
10682 if (o.find_solver) return find_solver_report(o);
10683 if (o.port) {
10684 // `-f` and `-p` together would be two model sources for one run;
10685 // the request carries the model in server mode, so a file named on
10686 // the command line could only be ignored.
10687 if (!o.file.empty())
10688 throw line::InputError(
10689 "-p runs the solver as a server, where each request carries its own model; "
10690 "-f names a model on the command line and the two cannot both be the source");
10691 return run_server(o);
10692 }
10693 return run_invocation(o);
10694 } catch (const line::Error& e) {
10695 std::fprintf(stderr, "line-cli: %s\n", e.what());
10696 return 2;
10697 } catch (const std::exception& e) {
10698 std::fprintf(stderr, "line-cli: unexpected failure: %s\n", e.what());
10699 return 3;
10700 }
10701}
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.
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
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:582
LnTranSolution get_tran_avg()
Port of @SolverLN/getTranAvg: the block-diagonal aggregate transient.
Definition solver_ln.h:563
std::size_t nlayers() const
Definition solver_ln.h:660
std::vector< LnCdf > get_cdf_respt()
Port of @SolverLN/getCdfRespT: the per-entry response-time distribution.
Definition solver_ln.h:524
LnSolution< T > get_ensemble_avg()
Port of getEnsembleAvg: run the iteration and aggregate onto LQN elements.
Definition solver_ln.h:500
const std::vector< qn::Layer< T > > & layers() const
Definition solver_ln.h:661
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::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 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
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,...
Read a JMT .jsim / .jsimg / .jsimw model into a qn::Network.
int main(int argc, char **argv)
Definition ldes_cli.cpp:406
The NATIVE LDES engine for LAYERED (LQN) models, the C++ twin of jline/solvers/ldes/handlers/Solver_s...
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.
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 ....
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...
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).
CtmcCftpSolution< T > solver_ctmc_cftp(const NetworkStruct< T > &sn, const CtmcOptions &opt, const CtmcCftpOptions &cftpopt)
Solve with the cftp / cftp.approx method.
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:909
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.
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.
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
std::vector< std::string > jmt_list_valid_methods()
Port of SolverJMT.listValidMethods.
Definition solver_jmt.h:838
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.
Definition solver_jmt.h:927
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
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:784
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)
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
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.
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,...
mva::AvgResult< T > solver_nc_run_analyzer(const qn::NetworkStruct< T > &L_in, const NcSolverOptions &opt_in)
Port of @@SolverNC/runAnalyzer.m for the lang='matlab' path: solve, then apply the metric filter @@Ne...
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.
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.
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.
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.
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
mva::AvgResult< T > solver_qns_run_analyzer(const qn::NetworkStruct< T > &L, const QnsOptions &opt)
Port of @@SolverQNS/runAnalyzer.m and solver_qns.m.
Definition solver_qns.h:291
bool is_available()
Port of SolverQNS.isAvailable: a native qnsolver binary on the PATH.
Definition solver_qns.h:154
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.
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:236
std::vector< std::string > chain_class_labels(const qn::NetworkStruct< T > &sn)
(ClassA ClassB), the JobClasses column: which classes a chain holds.
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)
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.
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.
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)
solver_ssa_analyzer.m: choose the method.
qn::NetworkStruct< T > sn_with_ssa_cache_split(const qn::NetworkStruct< T > &base, const std::vector< SsaCacheRatio > &cache)
The struct with the cache split the SIMULATION MEASURED, visits rebuilt.
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.
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
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 cftp and cftp.approx methods of SolverCTMC: stationary analysis of a closed single-class product-...
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 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.
Port of @@SolverQNS, the wrapper around qnsolver of the RADS/LQNS distribution.
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', 'inapplus', '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/cbh/sib and the iteration count k of pbk/bjbk.
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...
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 knobs of one perfect-sampling run.
std::size_t samples
Number of iid stationary draws; the reference has no default here.
unsigned long seed
Stream seed, so a row is reproducible within this port.
What one cftp solve produces beside the means.
std::vector< std::vector< int > > distinct_states
The distinct sampled states, aligned with paggr.
std::vector< long > horizon
(samples) per-draw coalescence horizon, or the mixing steps of M_A.
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,...
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.
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.
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:110
std::string method
The inter-stage coupling: meanfield is the reference's default.
Definition solver_env.h:122
std::string stage_solver
Which solver runs each FLAT stage: the fluid transient or the enumerated CTMC.
Definition solver_env.h:120
fluid::FluidOptions stage
Options handed to each stage solver.
Definition solver_env.h:126
double stage_cutoff
options.cutoff of a CTMC stage, read only when stage_solver is ctmc.
Definition solver_env.h:131
double timespan_end
options.timespan(2) of the inner solver: the transient horizon.
Definition solver_env.h:133
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:146
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.
The options of one JMT solve, SolverOptions('JMT') restricted to what is read.
Definition solver_jmt.h:86
std::string method
default | jsim | jmva | jmva.<alg>
Definition solver_jmt.h:87
bool keep
keep the scratch directory after the solve
Definition solver_jmt.h:90
double samples
samples per measure; raised to 5000 below
Definition solver_jmt.h:88
What jmt_prob_aggr reports: the system probability and the per-station ones.
Definition jmt_logs.h:747
std::vector< bool > station_seen
the same, per station
Definition jmt_logs.h:751
bool sys_seen
whether the joint state occurred at all
Definition jmt_logs.h:750
double sys
P(the whole network is in the declared state).
Definition jmt_logs.h:748
std::vector< double > station
P(station i holds its declared per-class counts).
Definition jmt_logs.h:749
The result of a JMT solve: the shared AvgResult plus what only JMT reports.
Definition solver_jmt.h:493
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:499
mva::AvgResult< T > avg
Definition solver_jmt.h:494
Matrix< T > DropRateNfcr
(nregions x nclasses) carried and lost rate
Definition solver_jmt.h:497
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
static constexpr double FineTol
Definition lang_types.h:668
static constexpr double CoarseTol
Definition lang_types.h:669
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:264
getSensitivityTable of the ensemble: the layer tables under a Layer column.
Definition solver_ln.h:414
std::vector< Row > rows
Definition solver_ln.h:419
std::string method
The summary label: the common branch, or "mixed" when they differ.
Definition solver_ln.h:423
The LQN-level answer, indexed by element 1..nidx.
Definition solver_ln.h:351
std::vector< T > WN
Definition solver_ln.h:352
std::vector< bool > defined_W
Definition solver_ln.h:353
std::vector< bool > defined_U
Definition solver_ln.h:353
std::vector< T > QN
Definition solver_ln.h:352
std::vector< bool > defined_R
Definition solver_ln.h:353
std::vector< T > RN
Definition solver_ln.h:352
std::vector< bool > defined_Q
Definition solver_ln.h:353
bool is_bound
True when the numbers are a BOUND (method = mwba.upper / mwba.lower) rather than the fixed point.
Definition solver_ln.h:362
std::vector< T > UN
Definition solver_ln.h:352
std::vector< bool > defined_T
Definition solver_ln.h:353
std::vector< T > TN
Definition solver_ln.h:352
The layered transient: one block per layer, plus how it was produced.
Definition solver_ln.h:405
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:399
std::vector< LqnElement > type
(nidx+1)
Definition lqn_struct.h:214
std::vector< bool > iscache
(tshift+ntasks+1)
Definition lqn_struct.h:266
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:265
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
The options SolverMAM reads.
Definition mam_types.h:29
int iter_max
SolverOptions('MAM') lowers this from the global 1000 to 100.
Definition mam_types.h:33
double timespan_start
options.timespan: the transient horizon.
Definition mam_types.h:118
std::string timescale
options.config.timescale: "auto", "discrete" or "continuous".
Definition mam_types.h:110
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:98
std::string method
Definition mam_types.h:30
std::size_t cutoff
options.cutoff: the level truncation getProb / getProbMarg use for an OPEN model, where the queue len...
Definition mam_types.h:70
std::string fj_tmode
options.config.fj_tmode: which route computeT.m takes to the T matrix, 'NARE' (Riccati,...
Definition mam_types.h:104
double slotlength
options.config.slotlength: the slot in model time units.
Definition mam_types.h:112
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
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:2140
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2141
SolverQNS.defaultOptions plus the two knobs the JMVA document carries.
Definition solver_qns.h:65
bool keep
options.keep: leave the scratch directory behind, to inspect what was sent.
Definition solver_qns.h:77
std::string multiserver
options.config.multiserver.
Definition solver_qns.h:72
int timeout
Seconds before a hung qnsolver is killed; not positive waits forever.
Definition solver_qns.h:75
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 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 | discrete | quadrature | montecarlo, MATLAB UQ.listValidMethods.
Definition solver_uq.h:92
std::size_t samples
Nodes per continuous Prior, or design points under montecarlo; the reference's options....
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: quadrature or montecarlo.
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.