io.+cppbridge
- class cppbridge.Transport
line-cli resolution, the subprocess and its JSON transport, and the bridge’s refusals. Part of the CPPLINE facade.
Copyright (c) 2012-2026, Imperial College London All rights reserved.
- Method Summary
- static analysisViaCpp(solverName, model, options, analysis, flags)
PAYLOAD = ANALYSISVIACPP(SOLVERNAME, MODEL, OPTIONS, ANALYSIS, FLAGS) Run one non-average line-cli analysis and return ITS payload.
Every line-cli answer is keyed by its own -a, so reading the key back is also the check that the process answered the question that was put: a payload arriving under another key is a wrong answer, and taking whatever object came would turn that into wrong numbers instead of an error.
- static cppUnsupported(solverName, method, reason)
CPPUNSUPPORTED(SOLVERNAME, METHOD, REASON) Refuse a getter this bridge cannot serve, naming the getter and the reason. THE REASON IS THE POINT: what is unreachable is unreachable for a specific, per-getter cause, and collapsing those into one message would tell a caller to stop asking for something the port may be one arm away from answering.
- static extractJsonObject(text, binary)
OBJ = EXTRACTJSONOBJECT(TEXT, BINARY) Decode the first complete JSON object in TEXT, ignoring the solver banner line-cli prints ahead of it.
- static findLineCli()
BINARY = FINDLINECLI() Locate the line-cli executable. Search order, most explicit first: the LINE_CLI_BINARY environment variable, the checkout’s common/ directory, the in-tree cpp/build directories, then PATH. Errors rather than returning ‘’ so a missing binary cannot be mistaken for a solver refusal further down.
- static matchingBrace(s)
STOP = MATCHINGBRACE(S) Index of the ‘}’ closing the ‘{’ at S(1), scanning outside string literals so a brace inside a name cannot end the object early.
- static probeNetwork()
MODEL = PROBENETWORK() A minimal but valid closed network, used only to ask a SolverLN layer factory which solver class it builds. An adaptive factory inspects the layer it is given, so an empty Network would not survive the call.
- static rmdirQuiet(d)
RMDIRQUIET Best-effort recursive temp-directory removal.
- static runLineCli(binary, args)
RESULTS = RUNLINECLI(BINARY, ARGS) Run one line-cli invocation (ARGS a cellstr of argv entries after the binary) and return the jsondecode’d object. This is the seam an in-process binding would replace: nothing above it knows a process was involved. MATLAB’s own libstdc++ shadows the host one on LD_LIBRARY_PATH, so a binary built against this toolchain fails to load; see line_native_env.
- static shellQuote(tok)
Q = SHELLQUOTE(TOK) Quote one argv entry for the platform shell.
- static solveViaCpp(solverName, model, options, analysis, flags)
RESULTS = SOLVEVIACPP(SOLVERNAME, MODEL, OPTIONS, ANALYSIS, FLAGS) Serialize MODEL to model.json, run line-cli on it and return the parsed answer.
Only the knobs line-cli actually honours are forwarded, and only when the caller overrode the MATLAB default: an untouched option lets the port apply its own SolverOptions default rather than having this bridge’s default imposed on it. KNOBS ARE ALSO GATED BY METHOD NAME, because line-cli REFUSES an option the chosen solver does not have (–tol on -s ba, –samples outside ssa, –cutoff outside ctmc), so a solver whose default merely differs from the generic one would otherwise be refused for an option nobody set.
FLAGS is an optional cellstr of the analysis’s OWN argv entries (–tspan for a transient query, –node for a per-node one, –notation for the ODE export). They are arguments of the CALL and not settings of the solver, so the caller that knows the getter assembles them.
- static solverToken(solverName)
TOKEN = SOLVERTOKEN(SOLVERNAME) The line-cli -s method name for a LINE solver name. The C++ model-solving path’s token set is NOT the JAR’s: it has ‘ag’ and ‘ba’, which the JAR CLI lacks. It does carry ‘jmt’, which drives the same JMT.jar the MATLAB wrapper drives, and lacks the lqns, lqsim and qnsolver binaries SolverLQNS wraps.
THE TABLE IS THE AUTHORITY FOR WHAT lang=’cpp’ ACCEPTS, so a token missing here makes a CLI arm unreachable however complete the port is – ag was: line-cli -s ag served -a avg, its four views and -a cdf, and native Python’s _CPP_SOLVER_TOKENS already carried it, while this switch refused SolverAG outright and every ag_* [M2C] parity row read as a named gap in the C++ solver.
- static tryDelete(f)
TRYDELETE Best-effort temp-file removal.
- class cppbridge.Result
line-cli JSON payloads back into MATLAB matrices, events and cache results. Part of the CPPLINE facade.
Copyright (c) 2012-2026, Imperial College London All rights reserved.
- Method Summary
- static auxFiltFromPayload(gen, key, n)
F = AUXFILTFROMPAYLOAD(GEN, KEY, N) Rebuild an {nstations x nclasses} cell of sparse matrices from the wire’s per-(station,class) triplet blocks. Each block carries its own Station and Class fields, 0-based like every other index on this transport.
- static avgMatrices(results, sn)
[QN,UN,RN,TN,AN,WN] = AVGMATRICES(RESULTS, SN) Reduce a line-cli avg payload to station x class metric matrices.
ROWS ARE KEYED BY NAME, never by position: the CLI emits the stations in the order the model.json declares them, which is the NODE order, while the matrices are indexed by STATION. Reading them positionally would put a station’s numbers on another station’s row on any model with a non-station node before a station (a Source, a Router, a ClassSwitch).
- static cacheField(e, name)
V = CACHEFIELD(E, NAME) A per-class cache result as a row vector, [] when the solver computed none. The empty is the point: it CLEARS a stale value.
- static denseFromTriplets(payload, n)
M = DENSEFROMTRIPLETS(PAYLOAD, N) Rebuild an (n x n) sparse matrix from the wire’s From/To/Rate triplets, whose indices are 0-based.
- static jsonNumericList(v)
X = JSONNUMERICLIST(V) Normalize a jsondecode’d JSON array of numbers to a row vector, mapping a JSON null (which arrives as an empty cell entry) to 0. An INFINITY crosses as the string ‘Infinity’/’-Infinity’, since JSON has no infinite literal; read as a name and defaulted to 0 it turns a queue at its stability boundary into an idle one.
- static jsonNumericScalar(x)
V = JSONNUMERICSCALAR(X) One wire value as a double: the non-finite string spellings both CLIs emit, or str2double for anything else numeric-looking.
A NUMBER IS RETURNED AS ITSELF, and reaching char() with one was a silent zero: jsondecode gives a double for an ordinary JSON number, char(0.2231) is the character at CODE POINT 0, str2double of that empty string is NaN, and the NaN branch below floored it to 0. Every scalar read through here – ProbSys, ProbSysAggr, logNormConstAggr – therefore came back 0 whatever the engine said, and 0 is a legal probability, so nothing complained.
- static jsonScalar(s, field)
V = JSONSCALAR(S, FIELD) A numeric field of a jsondecode’d row, NaN when absent or null.
- static jsonStringList(v)
C = JSONSTRINGLIST(V) Normalize a jsondecode’d JSON array of strings to a cellstr. A one-element array decodes to a char row and an all-equal-length array may decode to a char matrix, so neither shape may be assumed.
- static probEntry(p, field, ist, solverName, method)
V = PROBENTRY(P, FIELD, IST, SOLVERNAME, METHOD) One station’s entry of a -a prob vector, or a named refusal when the solver’s arm does not report that view at all.
- static restoreCacheResults(results, model)
RESTORECACHERESULTS(RESULTS, MODEL) Write the C++ per-Cache results back onto the MATLAB Cache nodes.
A cache’s hit, miss and delayed-hit fractions and its retrieval latency are SOLVER RESULTS, not model state: getAvgCacheTable reads them off the node. Solving in the C++ engine used to leave the node untouched, so the table reported whatever the PREVIOUS solver had written – on retrieval_simple the MVA and NC tables were both the LDES table, identical to five digits, and nothing in those two rows was being measured at all.
ABSENT MUST CLEAR, NOT KEEP. Every field is written on every solve, [] included, mirroring the lang=’java’ path (SolverMVA/runAnalyzer.m:68-75). Writing only the fields the engine filled is what let one solver’s delayed-hit fraction survive into another solver’s row. The block rides INSIDE the “avg” payload, not beside it: line-cli merges extra into the analysis object, the way it already does for ListCost (line_cli.cpp:448). Reading it at envelope level finds nothing and silently restores no cache result at all.
- static rowsToMatrix(v)
M = ROWSTOMATRIX(V) A matrix sent as an array of row arrays. jsondecode already collapses a rectangular one into a numeric matrix, so this only has to reassemble the cell form and keep the empty case 0x0.
- static sampleEventIndices(v)
IDX = SAMPLEEVENTINDICES(V) The event column of -a sample as one-based synchronization indices, with NaN where the walk absorbed.
NOT jsonNumericList: that maps a JSON null to 0, and a 0 here becomes index 1 after the base shift – naming a synchronization that did not fire, at the one step where none did.
- static syncEvents(sn, eventIdx, t)
E = SYNCEVENTS(SN, EVENTIDX, T) The sample getters’ event cell: every active and passive arm of each fired synchronization, stamped with the epoch it fired at.
The synchronization DESCRIPTORS are model structure and are read from sn, as the native getters read them; what the C++ decided is WHICH one fired and WHEN, and that is what eventIdx and t carry.
- class cppbridge.Options
Solver options to line-cli flags. Part of the CPPLINE facade.
Copyright (c) 2012-2026, Imperial College London All rights reserved.
- Method Summary
- static configFlags(options, solverName, handled)
ARGS = CONFIGFLAGS(OPTIONS, SOLVERNAME, HANDLED) The line-cli flag of every options.config key whose value differs from the MATLAB default of SOLVERNAME, read off the schema cpp column (lineConfigSchema). HANDLED names keys the caller translates itself. A key with no flag never reaches here at a non-default value (lineConfigValidate refuses it), and a value its flag cannot state (a switch asked for the other way) is refused rather than dropped. Internal keys are LINE plumbing that line-cli recomputes.
- static jmtReplicationFlags(solverName, options)
FLAGS = JMTREPLICATIONFLAGS(SOLVERNAME, OPTIONS) –jmt-replications for a SolverJMT transient (-a tran, -a tranprob), from options.config.replications or its flat alias options.replications.
ONLY FOR METHOD ‘default’, which is the ensemble: an explicit ‘jsim’ is one JSIM run and line-cli refuses a count beside it rather than dropping it.
- static lnKnobs(options)
ARGS = LNKNOBS(OPTIONS) Translate the SolverLN options into layered-path CLI flags, refusing every setting the flag set cannot carry. A knob with no CLI counterpart is an error and not a silent drop: relax and relax_factor change the fixed point the MATLAB solver converges to. layering has no flag either, but folds into –method exactly.
- static num2arg(x)
S = NUM2ARG(X) Round-trip-exact decimal form of a scalar knob.
- static optionFlags(options, token, solverName)
ARGS = OPTIONFLAGS(OPTIONS, TOKEN, SOLVERNAME) The subset of SolverOptions line-cli honours for METHOD NAME, forwarded only where the caller overrode the MATLAB default.
- static overridden(options, field, defaultValue)
V = OVERRIDDEN(OPTIONS, FIELD, DEFAULTVALUE) The option value when the caller moved it off the MATLAB default, [] otherwise.
- static passageSetSpec(S)
SPEC = PASSAGESETSPEC(S) A state set as the –passage-from/–passage-into wire syntax: rows joined by ‘;’, entries by ‘,’. STATE ROWS travel rather than row indices, because the two enumerations need not order (or even purge) states identically – line-cli resolves rows by content against the space its own engine enumerated.
- static pnormMethod(options)
OPTIONS = PNORMMETHOD(OPTIONS) Restate MATLAB’s p-norm smoothing request in line-cli’s own terms.
THE TWO SIDES SELECT THE SMOOTHING DIFFERENTLY, and only the symbolic getters notice, because a min-scaled drift has no Jacobian and so must be smoothed before it can be differentiated at all. MATLAB turns the smoothing on whenever options.config.pstar is set, leaving the METHOD name alone (solver_fluid_symodes, use_pnorm); line-cli turns it on only under –method pnorm and has no flag carrying the exponent, so it always smooths at its own p = 20. Forwarding ‘matrix’ would therefore have line-cli refuse a model MATLAB answers for, under a message about a kink the caller had already smoothed away.
A DIFFERENT EXPONENT IS REFUSED RATHER THAN ROUNDED TO 20: p sets how sharply the smoothed min approaches the kink, so answering at another p is answering about another drift.
- static qrfParamsJson(qp)
TXT = QRFPARAMSJSON(QP) options.config.qrf_params as the –qrf-params document. ZM is not sent: the CLI derives it from ZZ, as every codebase now does.
- static rowCells(A)
C = ROWCELLS(A) A numeric matrix as a cell of its rows, so that jsonencode always emits an array of arrays. A bare one-row matrix encodes FLAT, which the CLI reads as a vector and not as a one-row table.
- static stateFlags(nodeIndex, state)
FLAGS = STATEFLAGS(NODEINDEX, STATE) getProb(node, state)’s second argument as argv: the node it belongs to and its ENCODED ROW, comma separated. Empty when no state was passed, which leaves the model’s declared one standing.
- class cppbridge.Analysis
One request per line-cli analysis (avg, prob, tran, cdf, sample, gen, reward, …) plus the Environment and LayeredNetwork paths. Part of the CPPLINE facade.
Copyright (c) 2012-2026, Imperial College London All rights reserved.
- Method Summary
- static aoiResults(solverName, model, options)
AOIRESULTS = AOIRESULTS(SOLVERNAME, MODEL, OPTIONS) The -a aoi payload in solver_mfq_aoi’s OWN struct shape, the one @SolverFLD/getAvgAoI and getCdfAoI already read out of self.result.solverSpecific.aoiResults.
Returning THAT struct rather than the finished answers is what keeps the port whole: both getters then run their existing bodies, so the summary table and the survival-function algebra have one implementation instead of a MATLAB one and a bridge one that can round differently. The (g,A,h) triples ride along for exactly this reason – getCdfAoI takes a caller’s own t_values, and a fixed grid could not answer that.
- static assertSingleState(solverName, method, model)
ASSERTSINGLESTATE(SOLVERNAME, METHOD, MODEL) Refuse a state-dependent analysis whose initial state the wire cannot carry.
THE SINGLE STATE NOW TRAVELS. linemodel_save emits the (stateSpace, statePrior) pair for every stateful node carrying a state, INCLUDING the one-row space with prior [1] that setState and initFromMarginal leave, and the C++ network_reader stores it; analyzer_detail::default_init_state takes that row in preference to the default marking, and api::sn_declared_marginal decodes it for the -a prob arms. So -a prob, the transient arms and a sampled trajectory all start where the caller put the jobs, and this used to refuse them all – unconditionally, because MATLAB has no marker telling a setState apart from an initDefault. That marker is no longer needed: the two are now sent alike, and where the state IS the default the C++ reconstructs the same row.
WHAT IS STILL REFUSED IS A GENUINE DISTRIBUTION over several rows, AND ONLY ON THE ARMS THAT SEED ONE STATE. default_init_state and sn_declared_marginal take row 0 only when the space has exactly ONE row and rebuild the default marking otherwise, so a prior over k > 1 states would be answered as one state under the name of a question about a mixture.
IT DOES NOT APPLY TO THE TRANSIENT ARMS. solver_ctmc_transient_analyzer seeds init_state_distribution, the product of the declared per-node priors over the enumerated space, and integrates ONCE from that mixture, which IS the reference’s weighted sum over the support. -a tran, -a tranprob and -a tranreward therefore take a mixture and are not gated here. See _kb/07-cross-language-parity.md.
- static avgReward(solverName, model, options)
[R, NAMES] = AVGREWARD(SOLVERNAME, MODEL, OPTIONS) The steady-state expectation of every declared reward, from -a reward, in @SolverCTMC/getAvgReward’s contract.
The serializability precondition is shared with getTranReward; see unbridgeableRewards for why a dropped reward must stop the -a reward route rather than shorten its answer, and rewardsLocally for the route it takes instead.
- static avgRewardLocally(solverName, model, options)
[R, NAMES] = AVGREWARDLOCALLY(SOLVERNAME, MODEL, OPTIONS) E[r] for rewards no wire format can carry, against the C++ law.
A FUNCTION HANDLE CANNOT CROSS THE WIRE, and there is nothing to serialize it into – but the reward is a function OF THE STATIONARY LAW, and that law is line-cli’s: pi and the aggregated state space arrive through CPPLINE.generator. Applying the caller’s own handle to them therefore still answers under lang=’cpp’: the engine that computed the chain, its stationary distribution and its enumeration is the C++ one, and this method only evaluates a map that no wire format can express. Refusing instead left rewardModel_aggregation, rewardModel_mm1k and rewardModel_multiclass – whose rewards are handles – unsolvable.
EVERY reward is evaluated here, not just the unbridgeable ones: -a reward answers for the serializable subset only, and splicing two vectors computed over the same law adds nothing but a pairing that can rotate.
- static cdfFirstPassT(solverName, model, options, Arows, Brows, Crows, tset)
[RD, OUT] = CDFFIRSTPASST(SOLVERNAME, MODEL, OPTIONS, AROWS, BROWS, CROWS, TSET) The state-set first passage law from -a firstpasst, in @SolverCTMC/getCdfFirstPassT’s own contract: RD an [n x 2] matrix of [F(t), t], OUT carrying tset, the density, reachprob and tabooprob. AROWS/BROWS/CROWS are STATE ROWS (already resolved by the caller against its own space); an empty AROWS selects the conditional stationary law, an empty CROWS names no taboo set and an empty TSET keeps the engine’s default horizon.
- static cdfRespT(solverName, model, options)
RD = CDFRESPT(SOLVERNAME, MODEL, OPTIONS) The per-station, per-class response-time CDF from -a cdf, in @SolverCTMC/getCdfRespT’s own contract: an (nstations x nclasses) cell whose entries are [F(t), t] two-column matrices.
This is the EXACT tagged-chain law, the same quantity the MATLAB getter builds, so the two are comparable value for value. A (station, class) pair the tagged chain never visits carries no curve on the wire and stays [] here, which is how the MATLAB getter also leaves it.
- static cdfSysRespT(solverName, model, options)
RD = CDFSYSRESPT(SOLVERNAME, MODEL, OPTIONS) The PER-CHAIN system response-time CDF, in @SolverCTMC’s getCdfSysRespT contract: a (1 x nchains) cell of [F(t), t]. The ROW shape is that getter’s, not a convention: it builds cell(1, nchains) and callers index it linearly.
It rides in the SAME -a cdf payload as the per-station curves, under ‘sysrespt’, because line-cli’s solve_ctmc_cdf computes both from one tagged-chain solve. Asking for it separately would solve the chain twice and report the second one.
- static exportODEs(solverName, model, options, notation)
DOC = EXPORTODES(SOLVERNAME, MODEL, OPTIONS, NOTATION) The LaTeX document of the fluid drift, from -a odes. NOTATION is ‘scalar’ or ‘matrix’ and REACHES the C++ exporter, so ‘matrix’ returns the matrix document and not the scalar one under another name.
- static firstPassTMoments(solverName, model, options, Arows, Brows, nmax, Crows)
[M, MALL, P] = FIRSTPASSTMOMENTS(SOLVERNAME, MODEL, OPTIONS, AROWS, BROWS, NMAX, CROWS) The state-set first passage MOMENTS from -a firstpasstmom, in @SolverCTMC/getFirstPassTMoments’s own contract: M the (1 x NMAX) moment vector, MALL the (nstates x NMAX) per-source matrix and P the per-source probability of entering B before the taboo set. AROWS/BROWS/CROWS are STATE ROWS, as in CDFFIRSTPASST.
No –passage-method here, and that is not an omission: the moments come from one linear solve per order and invert no transform, so there is no inversion for the flag to select. line-cli refuses it on this arm for the same reason.
- static generator(solverName, model, options)
G = GENERATOR(SOLVERNAME, MODEL, OPTIONS) The CTMC generator, state spaces and stationary law, as a struct with fields infGen, space, spaceAggr, pi and eventFilt.
TWO INVOCATIONS, ONE PER GETTER: -a gen is getGenerator and -a states is getStateSpace, and line-cli keeps them apart because they are different getters. The bridge asks for both and CHECKS THAT THEY AGREE on the state count before pairing Q with pi. An unchecked pairing is the one way this goes wrong in silence: a Q from one enumeration indexed by another’s states is not a chain, and every consumer reading a row of the space would read the wrong state rather than get an error.
- static getAvg(solverName, model, options)
[QN,UN,RN,TN,AN,WN,RUNTIME,METHOD] = GETAVG(SOLVERNAME, MODEL, OPTIONS) Run the C++ avg analysis and reduce it to (nstations x nclasses) matrices in the MATLAB struct’s own indexing. METHOD is the method line-cli reports having run, empty when it names none.
- static getEnsembleAvg(solver, options)
[QN,UN,RN,TN,AN,WN,RUNTIME] = GETENSEMBLEAVG(SOLVER, OPTIONS) Delegate a layered getEnsembleAvg() to line-cli, returning the per-LQN-element metric column vectors the MATLAB SolverLN uses.
ONE subprocess solves the whole ensemble: letting the MATLAB fixed point run and dispatching each layer separately would spawn one process per layer per iteration.
- static getEnvAvg(env, refModel, options)
[QN,UN,TN,RUNTIME] = GETENVAVG(ENV, REFMODEL, OPTIONS) Delegate a random-environment solve to line-cli’s -s env arm. ENV is the Environment, REFMODEL the stage model whose station and class indexing the returned matrices carry.
RN and AN are NOT returned: @SolverENV/getEnsembleAvg defines them as NaN because the environment analyzer computes no response time, and the CLI emits nan for the same reason.
- static jacobian(solverName, model, options, wantEquilibria)
[J, RHS, VARS, EQUILIBRIA] = JACOBIAN(SOLVERNAME, MODEL, OPTIONS, WANTEQUILIBRIA) The SYMBOLIC drift Jacobian from -a jacobian, in @SolverFLD/getJacobian’s own contract.
This is symbolic on both sides, which is why it can be bridged at all: line-cli’s arm builds the drift with fluid_symodes and differentiates it through the SAME symbolic backend the MATLAB getter reaches through SAGE, so the entries are expressions and not a numeric evaluation of them.
EQUILIBRIA are only computed when asked for – solving the system is the expensive part – so WANTEQUILIBRIA carries the CALLER’s nargout, not this function’s: called as [J,rhs,vars,eq] = …, nargout here is always 4 and would ask for them every time. hasEquilibria on the wire separates “asked and there are none” from “never asked”, so an empty list is never silently reported as “this system has none”.
- static lnLayerSolver(solver)
[LAYERSOLVER, PROBE] = LNLAYERSOLVER(SOLVER) Resolve –layer-solver from the layer solver the MATLAB SolverLN was built with, refusing anything the port has no layer engine for. The layer solver is what the fixed point is a fixed point of, so substituting MVA for a CTMC layer answers a different question. NC, SSA, MAM, CTMC, LDES and JMT layers ARE carried (solve_layer_nc, _ssa, _mam, _ctmc, _ldes, _jmt in solver_ln.h); refusing NC/SSA here was a stale claim that left lqn_twotasks and lqn_ofbiz with no table at all. PROBE is the factory’s solver on a probe network, whose options carry a simulated layer’s run length.
- static lqnxLossyTasks(solver)
OFFENDERS = LQNXLOSSYTASKS(SOLVER) Name the tasks whose think time an .lqnx serialization of this model would drop, i.e. the non-reference ones that carry one.
The lqnx schema accepts think time on a REFERENCE task only, and writeXML reports the loss and writes the file anyway (lqns would reject it otherwise). LINE nonetheless gives a non-reference task’s think time to its callers as a delay, so a model carrying one solves to DIFFERENT numbers through that transport: on gallery_lqn_basic, T3’s think time caps its throughput at multiplicity/think = 25/4 and drops it from 66.4 to 6.22. A non-empty return therefore does not refuse the solve – it routes it through linemodel_save, whose reader takes think time on any task.
- static mamCdfRespT(solverName, model, options)
RD = MAMCDFRESPT(SOLVERNAME, MODEL, OPTIONS) The MAM response-time CDF from -a cdf, in solver_mam_passage_time’s own contract: an (nstations x nclasses) cell whose QUEUE row holds [F(t), t] and whose Source row stays [].
The wire entries carry a class and NO station, and that is not an omission: solver_mam_passage_time covers exactly two stations, a Source and one queue, so the station is determined by the model rather than by the class. This resolves it the same way, and errors if the model is not of that shape rather than guessing a row.
- static mamProb(solverName, model, options, nodeIndex)
[JOINT, MARGINAL] = MAMPROB(SOLVERNAME, MODEL, OPTIONS, NODEINDEX) The queue-length law of ONE node under -s mam -a prob: JOINT is the (level x phase) table getProb returns and MARGINAL{r} is the per-class vector getProbMarg returns.
BOTH COME OFF ONE SOLVE, because they are two views of the same QBD law and the arm reports them together. The multi-queue refusal is the REFERENCE’S, raised there rather than here, so the two backends decline the same models for the same stated reason.
- static nodeSamplePath(solverName, model, options, node, numEvents, aggregate)
S = NODESAMPLEPATH(SOLVERNAME, MODEL, OPTIONS, NODE, NUMEVENTS, AGGREGATE) One node’s sample path in the contract of @@SolverCTMC/sample and sampleAggr: handle, t, state, event, isaggregate.
BOTH VIEWS COME OFF THE WIRE (nodeState, nodeAggr). The aggregate one is the port’s own marginal_of rather than a State.toMarginal applied here, so the reported counts are the engine’s reading of its own states.
- static normConstAggr(solverName, model, options)
LG = NORMCONSTAGGR(SOLVERNAME, MODEL, OPTIONS) getProbNormConstAggr: log G, from -a normconst.
- static probAggr(solverName, model, options, flags)
P = PROBAGGR(SOLVERNAME, MODEL, OPTIONS, FLAGS) The -a prob payload as a struct with the fields the four probability getters read: ProbSys, ProbSysAggr and the per-station Prob / ProbAggr vectors, in station order.
ALL FOUR COME OFF ONE SOLVE, which is why they share one helper: the CLI computes the stationary law once and reports every view of it, so a getter that ran its own process would pay for the chain again to read another column of the same answer.
-s mva emits no ProbSys / Prob: the binomial fit of Schmidt (1997) is an AGGREGATE law with no detailed counterpart, so those fields come back empty and the detailed getters refuse rather than quoting the aggregate. -s nc does emit both, from solver_nc_marg and solver_nc_joint, which carry the class-within-chain split the aggregate pair sums out. FLAGS is getProb’s second argument on the wire: –node and –state name one node’s encoded row, which the arms that accept it substitute for that node’s declared state before evaluating.
- static probMarg(solverName, model, options, ist, jobclass, state_m)
[PMARG, LOGPMARG] = PROBMARG(SOLVERNAME, MODEL, OPTIONS, IST, JOBCLASS, STATE_M) getProbMarg’s curve from -a marg, for the station IST.
THE TWO SOLVERS ANSWER DIFFERENT QUESTIONS UNDER ONE NAME, which is why the payload key differs and this reads both: SolverMVA’s getProbMarg is P(n jobs OF CLASS r), so its curves are keyed by (station, class) and it takes –class and –marg-states; SolverNC’s is P(n jobs IN TOTAL at the station), so its curves are keyed by station alone and neither flag applies. Reporting one under the other’s name would swap a per-class law for a total-occupancy one.
- static probSysMarg(solverName, model, options, nvec, engine)
[PN, LPN] = PROBSYSMARG(SOLVERNAME, MODEL, OPTIONS, NVEC, ENGINE) getProbSysMarg: the joint law of the per-station TOTALS, read at NVEC out of the whole lattice -a sysmarg reports.
LPN IS log(PN) HERE and not an independently accumulated log: the port’s solver_nc_jointmarg returns the probability alone, where the reference also carries the logarithm through. On a population whose probability underflows, this reports -Inf where the native getter still has a finite log.
- static rewardMatrixOver(model, spaceAggr)
[R, NAMES] = REWARDMATRIXOVER(MODEL, SPACEAGGR) The (nrewards x nstates) matrix of the declared rewards evaluated on the rows of SPACEAGGR, and their names in DECLARATION order.
The reward map is a function of the AGGREGATE state row and of nothing else, so the same evaluation serves whichever engine enumerated the space. This is solver_ctmc_reward’s inner loop, kept identical down to the two calling conventions it accepts, so a handle answers the same number under lang=’cpp’ as it does natively – the only thing that differs is which engine built the space and the law it is weighted against.
- static rewardWireOrder(model, names)
[ORDER, OK] = REWARDWIREORDER(MODEL, NAMES) The permutation taking line-cli’s answer back into DECLARATION order.
THE WIRE SORTS THE REWARDS; THE MODEL DOES NOT. linemodel_save emits them in NAME order so the three writers byte-match, so line-cli answers in that order while the native getter answers in DECLARATION order. A caller pairing the values with its own declaration list then reads every reward under the wrong name – on rewardModel_templates a three-way rotation, not a numeric error.
- static samplePath(solverName, model, options, numEvents, nodeIndex)
S = SAMPLEPATH(SOLVERNAME, MODEL, OPTIONS, NUMEVENTS, NODEINDEX) The -a sample payload, decoded into the pieces the four sample getters read: the epoch of each step, the system state in both views, the node’s own two views, and the synchronization that fired.
ONE WALK ANSWERS ALL FOUR, as with probAggr. A trajectory is not a mean: two walks of the same chain are two different answers, so a getter that ran its own process would report a trace that never lines up with the one its sibling reported.
THE STATE SPACE COMES BACK WITH IT (space, NodeWidths), and the visited rows are indices into THAT enumeration. Resolving them against a state space built here instead would pair two orderings that have no reason to agree, and the mismatch would read as a trajectory rather than as an error.
- static solveLqnViaCpp(solver, options)
RESULTS = SOLVELQNVIACPP(SOLVER, OPTIONS) Serialize the SolverLN’s LayeredNetwork, run line-cli on it and return the parsed object. The wire format is .lqnx unless that would drop a non-reference task’s think time, in which case the model.json interchange carries it instead – see lqnxLossyTasks.
- static sysSamplePath(solverName, model, options, numEvents, aggregate)
S = SYSSAMPLEPATH(SOLVERNAME, MODEL, OPTIONS, NUMEVENTS, AGGREGATE) The system sample path in the contract of @@SolverCTMC/sampleSys and sampleSysAggr: state is a CELL PER STATEFUL NODE.
The blocks are cut out of the wire’s own space with its own NodeWidths – the local-space widths, which is the padding rule sampleSys uses and sample.m does not.
THE AGGREGATE IS TAKEN HERE, with State.toMarginal, and not read off the payload’s sysAggr: that matrix is (nstations x nclasses) in station order with Sources dropped, while this getter’s contract is one entry per STATEFUL node. The two index different things, so quoting one under the other’s name would misattribute whole columns. What the engine decided – which state, at what epoch – is what P carries; only the aggregation is done here, by the same function the native getter calls.
- static tranAvg(solverName, model, options)
[QNT, UNT, TNT] = TRANAVG(SOLVERNAME, MODEL, OPTIONS) The transient means from -a tran, in the contract of self.result.Tran.Avg: (nstations x nclasses) cells whose entries are [value, t] two-column matrices.
THAT IS THE RESULT TABLE, NOT THE GETTER’S ANSWER. getTranAvg returns a metricVal STRUCT per cell (handle, t, metric, isaggregate) and NaN where a handle is disabled, and it builds those in @NetworkSolver/getTranAvg from exactly these tables. The callers therefore store what comes back and delegate there, so the wrapping has one implementation rather than a native one and a bridge one that can disagree on the disabled case.
THE HORIZON IS THE CALLER’S, not this bridge’s. line-cli refuses -a tran without –tspan rather than inventing one, so the timespan is read from options here and a model asked for a transient without one is refused on this side, where the option can be named.
- static tranAvgVar(solverName, model, options)
[T, QVART, SIGMAT] = TRANAVGVAR(SOLVERNAME, MODEL, OPTIONS) The transient second moments from -a tranvar, in @SolverFLD/getTranAvgVar’s contract: T the abscissae, QVART an (nstations x nclasses) cell of variance series, and SIGMAT the (dim x dim x numel(t)) covariance trajectory.
THE HORIZON IS ALREADY RESOLVED by the caller, which applies the same slowest-rate rule the native path uses for an unbounded timespan; this only transmits it. line-cli enforces the kp precondition on its own side, so both agree that the other fluid methods integrate the mean alone.
- static tranProb(solverName, model, options, nodeIndex, aggregate)
[PI_T, SS] = TRANPROB(SOLVERNAME, MODEL, OPTIONS, NODEINDEX, AGGREGATE) The four getTranProb* answers from -a tranprob: PI_T is [t, pi(t)] and SS is the labelling of the columns of pi(t).
PI_T IS THE SAME OBJECT IN ALL FOUR. Every one of the reference getters takes the transient analyzer’s DETAILED law and differs only in which labelling it returns beside it, so the aggregate ones read labelsAggr and NOT pitAggr – that is the law over aggregate states, a different quantity that would sum to one just as convincingly under the wrong name.
SCOPE IS THE –node FLAG: without it the labels span the system, with it they are that node’s block, which is the slice the node getters cut out of SS themselves.
- static tranReward(solverName, model, options, rewardName)
[RT, T, NAMES] = TRANREWARD(SOLVERNAME, MODEL, OPTIONS, REWARDNAME) The transient expectation of every declared reward, from -a tranreward, in @SolverCTMC/getTranReward’s contract: RT a (nrewards x 1) cell of time series, T the shared abscissae, NAMES the matching (nrewards x 1) cell. A named reward returns that one alone, as the native getter does.
The serializability precondition and the wire-order correction are the SAME as getAvgReward’s, and are shared rather than restated: a bare function handle still has no serializable form here, and the wire still sorts by name where the model declares in its own order.
- static tranRewardLocally(solverName, model, options, tspan)
[RT, T, NAMES] = TRANREWARDLOCALLY(SOLVERNAME, MODEL, OPTIONS, TSPAN) E[r(X(t))] for rewards no wire format can carry, against the C++ transient law.
The twin of avgRewardLocally, over pi(t) instead of pi: -a tranprob integrates the forward equation ONCE from the model’s own initial distribution and returns the trajectory beside the AGGREGATED labels its columns are indexed by, which is exactly the pair the reward map needs.
- static unbridgeableRewards(model)
UNBRIDGEABLE = UNBRIDGEABLEREWARDS(MODEL) The declared rewards that no wire format can carry, by name.
A REWARD THE WRITER DROPS MUST STOP THE -a reward ROUTE, NOT SHORTEN ITS ANSWER. linemodel_save omits a reward whose descriptor is absent or Custom – a bare function handle has no serializable form – and only warns. Taking that route anyway hands line-cli the SURVIVING rewards, whose shorter vector the caller then pairs with its own full declaration list; where every reward is a handle it hands over none and line-cli exits 2. This list is what sends the solve down rewardsLocally instead, and is shared by the steady-state and the transient arm so the two cannot disagree about which model is bridgeable.