io.+pybridge

class pybridge.Result

Native solves and their results back into MATLAB (averages, transient, state probabilities, LN and ENV ensembles). Part of the PYLINE facade.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

Method Summary
static alignLNVector(v, nElem, pyNames, mlNames)

ALIGNLNVECTOR One native LQN metric vector in MATLAB’s element order. With names on both sides the values are looked up BY NAME; with none (a numeric nElem was passed) it falls back to dropping the index-0 placeholder positionally.

static cacheSplitFromStruct(pysn, nodeIndex0)

CACHESPLITFROMSTRUCT sn.nodeparam[i].actualhitprob / actualmissprob / actualdelayedhitprob / actualresidt of the native struct, each [] when this solver wrote it nowhere.

static getAvg(solverName, model, options)

GETAVG Build the native model+solver, run getAvg(), marshal back. METHOD is the method the native solver reports in its result (a dict for SolverMVA, an object elsewhere), empty if none. Guarded import: line_to_pyline below calls assertPythonReady, but this line runs FIRST, so an unconfigured pyenv escaped as a raw MATLAB:Python:PyException instead of the classified message. That is not cosmetic – the parity harness reads the message to tell a missing environment (a named SKIP) from a wrong answer (a FAIL), so every [M2P] row on a host without the native package was scored as a failure of the solver under test.

static getEnsembleAvg(model, options, elemNames, layerSolverName)

GETENSEMBLEAVG Build the native LQN + SolverLN, run get_ensemble_avg(), marshal the per-element metric vectors back. Native get_ensemble_avg() returns (Q,U,R,T,A,W) as column vectors positionally aligned with the NATIVE LQN element index, and the native struct keeps a leading index-0 placeholder (empty name).

THE TWO ELEMENT ORDERS ARE NOT THE SAME ORDER. MATLAB indexes the activities in CREATION order, while the .lqnx interchange this bridge goes through writes them grouped per task, so parseXML rebuilds them in DOCUMENT order. On lcq_threehosts, which creates A2 before A1, MATLAB’s names run [… A2 A1 Ac Ac_hit Ac_miss] and the native ones [… A1 Ac Ac_hit Ac_miss A2]: a positional copy rotated the whole activity block by one and reported Ac_hit’s answer under Ac. Marshal by NAME, so the two orders never have to agree. elemNames is self.lqn.names; a numeric argument is still accepted and means “positional, nElem elements”.

static getEnvAvg(model, options, stageSolverName, stageOptions)

GETENVAVG Build a native Environment (via JSON) + SolverENV and marshal the environment-weighted (Q,U,T) metrics back.

STAGESOLVERNAME is the MATLAB class of the stage solvers the caller built (SolverENV holds one instance per stage) and STAGEOPTIONS is that solver’s own options struct. Both must be forwarded: the stage solver is what decides the cost and the accuracy of the whole ensemble, and substituting one here makes lang=’python’ answer a different question from lang=’matlab’. Hardcoding SolverCTMC once turned the MVA random-environment image of a two-class open FCFS model (mapEnvApprox, gallery mmap1_multiclass) into a truncated CTMC at the default cutoff, which exhausted the host. Dropping the stage options is just as destructive on the other side: a stage FLD built here without the caller’s timespan integrates an UNSTABLE down-stage to the default horizon instead of to 1e3, and renv_node_breakdown came back with QLen 475.78 against MATLAB’s 0.462.

static getLNSensitivityTable(model, options, varargin)

GETLNSENSITIVITYTABLE Build the native LQN + SolverLN and marshal get_sensitivity_table() back into the MATLAB layer-wise table. The native call runs the fixed-point loop itself when the layers have not been solved, so the layer derivatives are taken at the converged parameterization (see solver_ln.py getSensitivityTable).

The name-value options are those of @NetworkSolver/getSensitivityTable (‘method’, ‘step’, ‘scheme’); an empty step forwards as None, which selects the native default.

static getLNTranAvg(model, options, layerSolverName)

[QNT, UNT, TNT] = GETLNTRANAVG(MODEL, OPTIONS, LAYERSOLVERNAME) The layered transient from the native SolverLN.getTranAvg, in the answer form of MATLAB SolverLN.getTranAvg: a block-diagonal (M x K) cell over the layer networks, where layer e owns a disjoint block of rows (its stations) and columns (its classes). An in-block cell is the metricVal struct (handle, t, metric, isaggregate) @NetworkSolver/getTranAvg builds, or NaN where the layer has no series; an off-block cell is [], as the MATLAB block concatenation leaves it. options.timespan and options.config.ln_transient (and its _iter_max/_tol/_channels) cross through parseSolverOptions.

THE BLOCKS ARE PLACED BY NAME, NOT BY POSITION. The model crosses as .lqnx, which lists tasks grouped per processor, so the native layers are built in DOCUMENT order while MATLAB’s follow creation order (the same trap getEnsembleAvg marshals around). Each native layer is matched to the MATLAB layer of the same name, and its stations and classes to theirs, so a reordering moves a block rather than reporting one layer’s series under another’s handle. The handles are the MATLAB layer networks’ own station and class objects, which is what callers of the MATLAB answer index into.

static lnNameMap(mlNames, pyNames, what, where)

MAP = LNNAMEMAP(MLNAMES, PYNAMES, WHAT, WHERE) Position in PYNAMES of each MATLAB layer, station or class name. MATLAB tags the layer networks’ names with the element kind (‘P:P1’ for a processor layer or station, ‘T:T2’ a task, ‘R:T1’ a reference-task class) and the native ones carry the bare element name (‘P1’), so a name with no exact native match is matched with that tag removed. A processor and a task may share a bare name, so a bare name matching several native entries is resolved by occurrence: the k-th MATLAB entry with that bare name takes the k-th native one, both sides listing processors first.

static pySensStruct(pyobj)

PYSENSSTRUCT native pfqn_sens result -> the MATLAB pfqn_sens struct returned as the second output of getSensitivityTable. Empty for a layer that took the finite-difference branch, which carries no analytic Jacobian.

static restoreCacheResults(model, pynet)

RESTORECACHERESULTS Copy each Cache node’s solved hit/miss split back from the native model onto the LINE node.

A cache’s hit and miss probabilities are a SOLVER RESULT, not model state: the analyzer computes them and writes them onto the node, and getAvgNode then rebuilds the hit-class and miss-class node throughputs from them (sn_get_node_tput_from_tput). Solving in the native engine leaves the LINE node’s copy empty, so that reconstruction silently falls back to the nodevisits split, which is the 0.5/0.5 guess link() laid down before any cache was analysed: on cache_replc_fifo (5 items, capacity 2) the hit and miss throughputs came back 0.5/0.5 instead of 0.4/0.6. getHitRatio on the LINE node returned nothing for the same reason.

The struct is then refreshed HARD, because the visits that carry the hit and miss classes are derived from the split: without it a Router downstream of the cache still saw the 0.5/0.5 routing (cache_replc_routing Router ArvR 1/1 instead of 0.8/1.2, and the residence times scaled by the same ratio). Every MATLAB solver that writes a hit probability does the same, see SolverNC/runAnalyzer.m (refreshChains / refreshStruct(true)).

static stateProb(solverName, model, options, kind, node)

P = STATEPROB(SOLVERNAME, MODEL, OPTIONS, KIND, NODE) One of the four state-probability getters, answered natively. KIND is ‘prob’, ‘probAggr’, ‘probSys’ or ‘probSysAggr’; NODE is read by the first two and ignored by the other two.

THE STATE CROSSES WITH THE MODEL, NOT WITH THE CALL. getNetwork above sends every station’s per-class marginal through initFromMarginal, and that is exactly what these getters match against, so nothing extra has to be marshalled here. A state named in the CALL is a different question and the callers keep it native.

Not a sample-path getter: sampleAggr and sampleSysAggr also return the fired SYNCHRONIZATION at each step, and native python has no sn.sync to name one with. These four read only the state and the time it held, which the native engine measures directly.

static tranAvg(solverName, model, options)

[QNT, UNT, TNT] = TRANAVG(SOLVERNAME, MODEL, OPTIONS) The transient means from the native getTranAvg(), 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, exactly as in CPPLINE.tranAvg: getTranAvg returns a metricVal struct per cell and builds it in @NetworkSolver/getTranAvg from these tables, so the callers store what comes back and delegate there. One wrapping implementation, not a native one and a bridge one that can differ on the disabled case.

A DISABLED PAIR STAYS EMPTY. The native getter returns None for a (station, class) it has no series for, and filling it with zeros here would report an idle station where there is no measurement.

static tranGrid(pygrid)

G = TRANGRID(PYGRID) One [M][K] nested list of native TranResult(t, metric) objects as an (M x K) cell of [value, t] matrices.

static trimLNVector(v, nElem)

TRIMLNVECTOR Align a native LQN metric vector to nElem MATLAB elements: drop the leading index-0 placeholder when present.

class pybridge.Model

LINE models -> native py.line_solver models, element by element or over the JSON bridge. Part of the PYLINE facade.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

Method Summary
static from_line_class(line_class, pynet, L, pynodes)

FROM_LINE_CLASS LINE job class -> py.line_solver class. THE SIGNAL BRANCHES MUST COME FIRST. OpenSignal derives from OpenClass and ClosedSignal from ClosedClass, so testing the bases first matches a G-network signal and marshals it as an ORDINARY class: the negative customers stop removing jobs and the bridge silently solves a different model (on ag_gnetwork the signal class picked up a queue length, a utilization and a throughput where MATLAB has zero). A silent downgrade is worse than a refusal, and nothing downstream can detect it.

static from_line_distribution(line_dist, L)

FROM_LINE_DISTRIBUTION LINE distribution -> py.line_solver process.

static from_line_layered_network(model)

FROM_LINE_LAYERED_NETWORK LINE LayeredNetwork -> native LQN. Bridged through the canonical LQN XML interchange format: the element-by-element LQN graph (processors/tasks/entries/activities/ calls/precedences) is large and error-prone to marshal over py.*, whereas writeXML/parseXML is a faithful, well-established round-trip.

FROM_LINE_LINKS Reconstruct routing on the Python model.

static from_line_network(model)

FROM_LINE_NETWORK Build a native Python Network from a LINE Network.

static from_line_node(line_node, pynet, L, forkNode)

FROM_LINE_NODE LINE node -> py.line_solver node.

static from_line_via_json(model)

FROM_LINE_VIA_JSON Bridge a model through the canonical line-model JSON schema: MATLAB linemodel_save -> temp .json -> native load_model. Used for models whose element-by-element construction is impractical over py.* (Cache, SPN Place/Transition, Environment).

static line_to_pyline(model)

LINE_TO_PYLINE Top-level entry: LINE model -> py.line_solver model.

static needsJsonBridge(model, line_nodes)

TF = NEEDSJSONBRIDGE(MODEL, LINE_NODES) True when a station carries a feature the element-by-element py.* construction below does not marshal.

SILENTLY DROPPING ONE IS THE WORST OUTCOME: from_line_node carries the discipline, the server count, the capacity and the load-dependent scaling and NOTHING ELSE, so a setup time, a server breakdown, a switchover, a balking rule or a finite capacity region crossed as a model that simply did not have it – lqn_setup came back with processor Util 0.75 against 0.436 because the setup layer lost its setup. linemodel_save serializes every one of these, so the whole model goes through the JSON bridge instead, exactly as a Cache, an SPN or an OI/PAS station already does.

static redeclare_derived_routing(pynodes, pyclasses, line_nodes, derivedRouting, L)

link() declares every entry it applies as PROB, so the pairs whose routing the native side derives from the connection matrix are re-declared afterwards, as the native JSON reader does after link(P).

static restoreNonRefThinkTimes(model, pynet)

RESTORENONREFTHINKTIMES Reapply the one piece of LINE state that the .lqnx interchange cannot carry. lqns rejects think-time on a non-reference task, so writeXML omits it there; LINE nonetheless gives such a think time to the task’s callers as a delay, and without this the bridged model would be a DIFFERENT model (the layer of the called task loses its delay, which moves that layer’s throughput and every quantity derived from it).

static set_derived_routing(rm, pyclasses, pynodes, sn, connections, m2p, i, k)

Routing entries for a (node,class) pair whose strategy the native side DERIVES from its connection matrix rather than from declared probabilities: RAND, JSQ and SQ.

link() builds that connection matrix out of the entries it is given and nothing else, so a pair contributing none leaves the node with NO outgoing arc and every solver reads the model as degenerate – an empty result, or the whole closed population parked at one station. The probabilities are MATLAB’s own resolution (sn.rtnodes), which already excludes the Sink for a closed class and routes nothing out of a Source; the native side re-derives the same uniform split once the strategy is re-declared after link().

static set_drop_rules(line_node, pynode, pyclasses, L)

SET_DROP_RULES Transfer Station.dropRule, the per-class rule that says what a full buffer does to an arriving job. It is user state with no derivation, so a rule that fails to cross is invisible: on cqn_bas_blocking the native SolverMVA then sees a finite capacity with no blocking policy attached and REFUSES the model, because its finite-capacity gate exempts Blocking-After-Service alone.

static set_service(line_node, pynode, line_classes, pyclasses, L)

SET_SERVICE Transfer arrival/service processes.

static set_signal_removal(line_class, pyclass, L)

SET_SIGNAL_REMOVAL Transfer how many jobs a signal removes, and which. Empty removalDistribution means “exactly one”, which is the native default, so it is left alone rather than encoded.

static to_py_drop_rule(dropId, L)

TO_PY_DROP_RULE MATLAB DropStrategy id -> py DropStrategy member. Resolved through the member NAME. The two enums happen to agree numerically today, but DropStrategy.toText returns prose (‘BAS blocking’) that no python member is keyed by, so an ordinal is the only other option and an ordinal is exactly what silently picks the wrong member the day one side renumbers.

static to_py_sched(schedId, L)

TO_PY_SCHED SchedStrategy id -> py.line_solver.SchedStrategy enum.

static to_py_signal_type(signalType, L)

TO_PY_SIGNAL_TYPE MATLAB SignalType constant -> py SignalType. Bridged through the LOWERCASE TEXT both enums already use on the JSON wire, never through the ordinal: MATLAB numbers REPLY 0 and NEGATIVE 1, while the python member values are the strings, so an ordinal handed over directly would silently pick another signal.

static tryDelete(f)

TRYDELETE Best-effort temp-file removal.

class pybridge.Matrix

MATLAB matrices <-> numpy arrays and Python sequences. Part of the PYLINE facade.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

Method Summary
static asRow(v)

ASROW A per-class result as a row vector, [] when absent. setResult* stores what it is given, so an empty here is what CLEARS a stale value from an earlier solve on the same model.

static from_line_matrix(matrix)

FROM_LINE_MATRIX MATLAB double matrix -> numpy ndarray.

static from_pyline_matrix(pyarr)

FROM_PYLINE_MATRIX numpy ndarray / scalar -> MATLAB double.

static pyNameList(pyval)

PYNAMELIST A native LQN struct name vector -> column cellstr. The native struct stores it as a numpy array of str, whose first entry is the empty index-0 placeholder.

static pyNumericColumn(T, name)

PYNUMERICCOLUMN pandas column of float -> column double vector.

static pySeries(x)

V = PYSERIES(X) One native 1-D series as a MATLAB row vector, WITHOUT crossing the buffer protocol.

double(numpy_array) binds MATLAB’s own libmwbuffer, and inside the embedded interpreter that import can fail outright – “ImportError: PyCapsule_Import could not import module libmwbuffer”, raised where a transient should have been. It is also dtype-sensitive: this interpreter warns that its numpy has “broken support” for longdouble, and a series carrying one converts for t and dies for metric in the same call. Going through tolist() hands MATLAB native Python floats and no buffer at all, so neither failure is reachable. A caller that already has a plain list (the native getters return either) is served by the same path.

static pyStringColumn(T, name)

PYSTRINGCOLUMN pandas column of str -> column cell of char.

class pybridge.Solvers

Native solver construction, options marshalling, the interpreter checks and the bridge’s refusals. Part of the PYLINE facade.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

Method Summary
static Solver(name, pynet, options, L)

SOLVER Dispatch to the native Python solver constructor by name.

static SolverLN(pynet, options, L, layerSolverName)

SOLVERLN Native SolverLN over an LQN model.

LAYERSOLVERNAME is the MATLAB class of the LAYER solvers the caller built (SolverLN holds one instance per layer). Without it the native side falls back to its own default layer solver, so LN(model, @(l)NC(l,…)) under lang=’python’ silently answered with AMVA layers: on lqn_twotasks that is E1 RespT 390.1 against the exact 390. The native constructor takes the factory as its second positional argument, the same shape as MATLAB’s.

static acceptedKwargs(solverName)

ACCEPTEDKWARGS Native SolverXOptions constructor parameter names.

READ OFF THE NATIVE DATACLASS, not transcribed. A hand-kept list drifts the moment a native option is added or renamed, and the failure is silent: the field is dropped and the default answers under the caller’s name. dataclasses.fields is the same source the constructor itself uses, so the two cannot disagree.

static assertPythonReady()

ASSERTPYTHONREADY Verify the native line_solver is importable and pin the embedded interpreter’s BLAS/OpenMP thread pools to a single thread. Running numpy/scipy in-process (py.*) alongside MATLAB’s own worker threads otherwise causes BLAS oversubscription that spins or deadlocks the session on heavy linear-algebra algorithms (e.g. CTMC).

static bindSupportedInterpreter()

BINDSUPPORTEDINTERPRETER Ensure pyenv names a CPython that this MATLAB release can actually load, and bind one that it can when it does not.

TWO THINGS WENT WRONG HERE AND EITHER ALONE IS ENOUGH TO LOSE THE DIAGNOSIS. First, with nothing configured MATLAB auto-discovers python3 from PATH, and that is routinely NEWER than the release supports – every host of this cluster answers python3 -V with 3.14 while R2026a loads at most 3.13, so the bridge had no usable interpreter at all. Second, the guard meant to say so was DEAD CODE: pyenv returns Executable as a STRING SCALAR, and isempty(“”) is false for a 1x1 string, so isempty(pe.Executable) never fired however unconfigured the environment was. Hence the clear message was never reached and the first py.* call died with MATLAB’s own “Python commands require a supported version of CPython” – an error naming neither the interpreter nor the bridge, which reached the [M2P] parity rows as “solver missing from output”. Emptiness is therefore tested on char(), never on the string property.

So the interpreter is asserted the way sys.path is asserted in assertPythonReady: probe it, and when it cannot serve, bind one that can. LINE_PYTHON pins an executable and is tried first. pyenv PERSISTS its choice across MATLAB sessions, so a switch is announced rather than made silently.

static candidateInterpreters()

CANDIDATEINTERPRETERS Executables to try, newest first, when the pyenv default cannot serve. No MATLAB-release-to-CPython table is kept: a version this release refuses simply fails its probe in bindSupportedInterpreter and the next candidate is tried, so the list stays correct as both sides move.

static canonicalSolverName(solverName)

CANONICALSOLVERNAME Short alias class -> canonical Solver* name. class(FLD(model)) is ‘FLD’, not ‘SolverFluid’: the short classes SUBCLASS the canonical ones, so anything keyed on class() must canonicalize first or a perfectly valid solver reads as unported.

static interpreterUsable()

INTERPRETERUSABLE True when py.* commands actually execute. Loading is the only reliable test: pyenv reports the CONFIGURED executable, not whether MATLAB can bind it.

THE PROBE MUST BE A PLAIN FUNCTION CALL, and py.sys.version_info is not one. sys.version_info is a structseq TYPE, so MATLAB is free to read py.sys.version_info as a CONSTRUCTOR and call it, which raises TypeError: cannot create ‘sys.version_info’ instances against a perfectly healthy interpreter. Which reading MATLAB takes is not stable across a session: measured 2026-09-08, every probe below answered before dispatch_closed ran and only this one failed after it, with py.str, py.list and importlib.import_module all still fine.

The cost of getting it wrong is the whole rest of the session: bindSupportedInterpreter reports “cannot run py.* commands”, and MATLAB cannot swap a loaded interpreter, so every later row sees the same refusal. That is what skipped all 174 [M2P] parity rows of run 20260908_104240 from dispatch_closed onwards, against an interpreter that was answering the whole time.

static lnLayerSolverName(solver)

NAME = LNLAYERSOLVERNAME(SOLVER) Class of the layer solver a MATLAB SolverLN was built with, or ‘’ when it kept the default. Under lang=’python’ no layer is constructed, so the factory is probed on a throwaway network rather than read off self.solvers – the same resolution CPPLINE.lnLayerSolver performs for –layer-solver. The layer solver is what the fixed point is a fixed point OF, so leaving it unnamed hands the native side a different question to answer. CANONICALIZE: class(MVA(model)) is ‘MVA’, not ‘SolverMVA’, because the short solver classes SUBCLASS the canonical ones. Both spellings happen to resolve today (line_solver exports the short aliases too), so this is fragility rather than a live fault – but every other consumer of a solver name on this bridge goes through canonicalSolverName, and one that does not is the shape that made PYLINE.Solver refuse a valid FLD as “not supported yet”.

static loggerFlag(v)

TF = LOGGERFLAG(V) One Logger column flag as a scalar logical. MATLAB stores it as the char ‘true’/’false’ and the native node wants a bool; a logical() cast on the char is BOTH the wrong type and the wrong value, since every character is nonzero.

static nativeOptionsClass(canon)

NATIVEOPTIONSCLASS Canonical solver name -> native options class.

static parseSolverOptions(options, solverName)

PARSESOLVEROPTIONS MATLAB options struct -> native solver kwargs. The native SolverXOptions constructors have per-solver parameter lists, so build a candidate native-name->value map and forward only the keys the target options class accepts.

static pyUnsupported(solverName, method, reason)

PYUNSUPPORTED(SOLVERNAME, METHOD, REASON) Refuse a getter this bridge cannot serve, naming the getter and the reason, exactly as CPPLINE.cppUnsupported does for lang=’cpp’. A getter that is not bridged must say so: the delegation in runAnalyzerPreamble sets the AVERAGE results only, so a transient getter that fell through found no self.result.Tran and died on a dot-index rather than on a statement about the port.

static verboseFlag(options)

VERBOSEFLAG options.verbose (VerboseLevel or logical) -> logical.