API reference¶
bngsim.Model¶
Factory methods:
Model.load(path)— Load any supported file, dispatching on its suffix (.ant,.xml/.sbml,.net,.bngl); raisesModelErrorfor anything elseModel.from_net(path)— Load from BNG.netfileModel.from_bngl(path, *, protocol=False, bng2_pl=None, net_out=None, timeout=600, cache=True)— Load from BNGL.bnglby runningBNG2.pl generate_network(requiresbngsim[bngl], or$BNGPATH/$BNG2_PL, plusperl). The file’s experiment is not run: its actions before the firstgenerate_network(or, without one, its first run) are applied, except file writes, andgenerate_networkcarries the source’s ownmax_iter/max_agg/max_stoich.protocol=Truereturns(model, ProtocolSpec)so the actions are recovered rather than discarded, andbngsim.run_protocolruns them. Generated networks are cached under~/.cache/bngsim/networks($BNGSIM_BNGL_CACHE_DIR), keyed on the flattened model text and the BNG2.pl that produced itModel.from_antimony(path)— Load from Antimony.antfile (requiresantimony,python-libsbml)Model.from_antimony_string(text)— Load from Antimony stringModel.from_sbml(path)— Load from SBML.xmlfile (requirespython-libsbml)Model.from_sbml_string(text)— Load from SBML XML string
Properties:
n_species,n_reactions,n_observables,n_parameters,n_functions,n_eventsspecies_names,param_names,observable_names,function_namesanalytical_jacobian_status—"complete","pending"(functional rate laws not derived yet), or"declined: <reason>"when the model runs on the finite-difference Jacobian: the rate law and what stopped its derivation, the derivation budget, the C++ attach gate’s verdict, or the environment switch. The gate’s verdict names the entry it refused, with the probe state and both values —∂f[B()]/∂[A()] is non-finite (inf) at probe 0 (the seed state) while the RHS is finite, orthe finite-difference self-check found a trustworthy mismatch at probe 0 (the seed state) in ∂f[LX()]/∂[LX()]: analytical 0, finite-difference -10(issue #534). The same reason is logged at INFO on thebngsimlogger when it happens (issue #506)analytical_jacobian_decline— the same decline as a dict a census can group by:Nonewhile the status is"complete"or"pending", otherwisekind(a stable category frombngsim._jacobian.DECLINE_KINDS, such asbudget,unresolved_symbolor the C++ gate’sfd_mismatch) andreason(the status text afterdeclined:). The gate’s kinds also carry the fields their verdict sets (ofrxn_idx,per_observable,target_idx,row,col,probe,analytical,finite_difference,n_nonfiniteanddetail; a field a kind does not set is absent) and the speciesrowandcolname asrow_species/col_species. Group bykind, not by the text, whose wording changes (issue #538)primary_param_names— the model’s independent knobs, and the list to hand an optimizer or sampler, lessfrozen_paramswhere a model has any.param_namesminus two classes, one per flag:param_is_expression(a derivedConstantExpression, recomputed from the primaries underneath it) andparam_is_internal(a slot bngsim synthesized — an SBML_V0_<comp>, or a function’s backing slot — whichset_paramrefuses to write). A constant written as arithmetic (gamma 1/7) is not in either class and is an ordinary knob (issue #227)frozen_params— the parameters that a number the model was built with was read from. For SBML (compartment sizes,speciesReferenceids and a kinetic law’s own parameters among them): a compartment size set by aninitialAssignmentor a constant assignment rule, the initial value of a species converted by the size of a compartment that is a state, a stoichiometry, aconversionFactor, an initial value that could not be kept symbolic, each followed down through what it reads. For a compartmental BNGL model: the volumes BNG2.pl wrote into the rate constants (each parameter aunit_conversion=comment names, and what a derived parameter or a function among them reads).set_paramrefuses to change one and a sensitivity column for one is refused (issues #695, #696, #711). Empty for most models
Methods:
set_param(name, value)— Set a parameter valueget_param(name)— Get a parameter valueset_params(dict)— Set multiple parameters (atomic)reset()— Reset species to initial concentrations (=restore_concentrations()with no label)clone()— Deep copy for parallel workers, and the only way to get an independentModel. All of a model’s state lives behind one handle into the compiled extension, socopy.copy(model)would share it — a write through the “copy” would change the original, which would then simulate differently with nothing raised. Bothcopy.copyandcopy.deepcopytherefore refuse and name this method (issue #643). Same for aSimulator: build a second one overmodel.clone(). AResultdoes still shallow-copy, having no setter to write throughsave_concentrations(label=None)— Snapshot current concentrations. No label rebases the default slot (reset()returns here); alabelstores a named snapshot (BNGsaveConcentrations("name")), and multiple named states coexist.restore_concentrations(label=None)— Restore a snapshot; no label restores the default slot, alabelrestores that named state (BNGresetConcentrations("name"))has_saved_concentrations(label=None)— Whether a named snapshot (or, with no label, any) existssaved_concentration_labels— Sorted names of saved named snapshotsset_concentration(name, value)— Set a single species concentration. A fixed species can be named with its$or without it ("$X(a)"or"X(a)","@c::$X()"or"@c::X()"), as BNG2.pl’ssetConcentrationtakes either: the model’sspecies_namesdrop the$the.netwritesget_concentration(name)— Get a single species concentration, named the same waypure_sink_species()→list[str]— write-only accumulator species: a product of ≥1 reaction, a reactant of none, read by no other species’ derivative, and not$-fixed. These put a structural floor under||f(y)||_2/n(issue #74)is_pure_sink()→ndarray (n_species,) of bool— the array form, somask=~model.is_pure_sink()declare_ic_sensitivity({species: {param: d_ic_d_param}})— Declare∂x_k(0)/∂θfor an initial condition your own code assigns (issue #111). A declared species’ row is taken as fully specified: parameters not named get0.0, so it replaces the model’s parameter-graph row rather than adding to iteffective_ic_sensitivity(params=None)→dict[str, dict[str, float]]— the reader paired with the writer above:{species: {param: ∂x(0)/∂θ}}, the initial-condition seeding a run from this state would use, after the #113 retirement and the #111 overlay. Answers from model structure alone — no integration, no simulation — so a fitting frontend can build its gradient routing once at setup. Keys are the idssensitivity_params=accepts (a #147-lowered compound<initialAssignment>reports the original symbols, never the synthetic_ic_<species>carrier). A present entry valued0.0means seeded, coefficient zero at this state; an absent entry means no seeding path — the two are different answers. Needed becauseoutput_sensitivities(axis="parameter")is a total derivative that already contains this matrix times theicaxis (GH #155)add_table_function(name, *, file, times, values, index, method)— Give a table function the model declares new data, in place, over the same index and keeping its method unlessmethodis given; every engine reads it from then on. A plain function’s name raisesModelError; a new name adds a table nothing reads, with a warning (issue #772)n_table_functions,table_function_names— TFUN introspection
Evaluators (issue #523; see Model evaluators). Each takes a state y of length n_species in species_names order, reads the live parameters, and leaves the stored concentrations alone:
rhs(y, t=0.0)→ndarray (n_species,)—dy/dtat(t, y): the interpreted RHS the CVODE callback evaluates, with its observable/function refresh run atyjacobian(y, t=0.0, *, sparse=False)→JacobianMatrixorscipy.sparse.csc_array—∂f/∂y,J[i, j] = ∂f_i/∂x_j; closed form when it covers every reaction, else the steady-state solver’s difference quotient, and.sourcesays which ("analytical"/"finite-difference"). A difference quotient whose step decides an entry (it moves by more than 1e-4 of itself, 1e-10 of its column’s largest at least, when the column is taken again at the species’ own step or at half the first) raisesSimulationError(issue #1002).sparse=Trueuses the model’s structural patternpropensities(y, t=0.0)→ndarray (n_reactions,)— every reaction’s SSA propensity in the SSA volume convention (falling factorial for a repeated reactant, times the compartment volume), after the SSA loop’s own observable refresh atystoichiometry_matrix(*, sparse=False)→ndarray (n_species, n_reactions)orcsc_array— net coefficients, 0-based; a$-fixed species has a zero row. The matrixconservation_lawswas row-reduced from, soL @ S == 0(except in an SBML model with a reaction between compartments of different size, whose laws carry the sizes:L @ rhs(y) == 0)
bngsim.JacobianMatrix¶
The dense return of Model.jacobian: a float64 ndarray subclass with one extra attribute, source — "analytical" or "finite-difference". Behaves as an ordinary array otherwise.
bngsim.Simulator¶
Constructor:
Simulator(model, method="ode", *, jacobian, codegen, net_path, sensitivity_params, poplevel, xml_path, gml)method:"ode","ssa","psa","nf","nf_reject","nfsim","nf_exact","rulemonkey","rm"(and aliases)ODE-specific kwargs:
jacobian("auto","analytical","fd","jax"),codegen(bool orNone),sensitivity_params(list[str])codegen=Truecompiles the model as built, whatever it was loaded from;None(the default) compiles at or aboveBNGSIM_CODEGEN_THRESHOLDspecies (256).jacobian="jax"differentiates a JAX RHS built from the model; a model it does not implement (a compartment size that is a parameter, as in every SBML model, amount-valued species, rate rules,rateOf, table functions) is refused with the construct named.net_pathis deprecated and ignored.PSA:
poplevel(float, required). Accepts.net, SBML, and Antimony models (same dispatch as SSA, sharing thevalidate_for_ssagate). NFsim/RuleMonkey:xml_path(str, required),gml(int).
Simulation:
run(t_span, n_points, *, seed, rtol, atol, max_steps, timeout, steady_state, steady_state_tol)→Resultrun_batch(t_span, n_points, *, params, seed, num_processors, squeeze, timeout, steady_state, steady_state_tol)→list[Result]orResult— clones + resets each point to the.netseedparameter_scan(parameter, par_scan_vals=None, *, par_min, par_max, n_scan_pts, log_scale, t_span, n_points, reset_conc=True, reset_to=None, on_point=None, seed, squeeze, …)→list[Result]orResult— BNGparameter_scan. Withreset_conc=True, each point resets to the state at scan invocation (or to the named snapshotreset_to), not the seed — so a pre-equilibrate → intervene → scan protocol carries its post-intervention state.on_point(model, value)applies coupledsetConcentrationoverrides that track the scanned value.bifurcate(parameter, par_scan_vals=None, *, …)→list[Result]orResult— continuation scan (reset_conc=0): each point continues from the previous point’s end-statecompute_all_sensitivities(t_span, n_points, *, params, chunk_size, n_workers, rtol, atol, max_steps)→Resultwith full sensitivity tensor
timeout is a wall-clock budget in seconds (or None to disable, the default).
When the budget is exceeded, run / run_batch raise
bngsim.SimulationTimeout (a typed BngsimError) carrying timeout
(the configured limit) and elapsed (actual wall-clock time at the trip).
Supported on every backend (ODE, SSA, PSA, NFsim, RuleMonkey). NFsim and
RuleMonkey poll the budget at a coarser granularity than ODE/SSA/PSA
(between stepTo() output points for NFsim; every ~1024 SSA events for
RuleMonkey, via its upstream cancellation hook), so the surfaced elapsed
time may overshoot the budget by one such interval.
Interactive:
run_until(t, *, n_points, seed)→Resultintervene(params)— Change parameters mid-simulationsave_concentrations(label=None)/restore_concentrations(label=None)— Snapshot / restore the model’s concentrations by name (delegates toModel); pair withparameter_scan(reset_to=label)for a preincubate → save → scan protocolsnapshot()→dict— Save staterestore(snapshot)— Restore stateget_state()→ndarray/set_state(state, *, time=None)— Bulk species vector;time=also setscurrent_time, so a step can be redone from a saved state and time withoutrestore’s backend rebuild (rolling a step back)set_time(t)— Setcurrent_time(finite, elseValueError); the nextrun_untilintegrates from there
Stop conditions:
add_stop_condition(condition, *, label)—strexpression orcallable.runraisesStopConditionMetwith the result cut at the first sample a condition holds at;run_batch,run_replicatesandparameter_scanreturn each row that meets one cut there, withResult.stop_conditionset, and refusesqueeze=Truewhile one is registered (issue #1369). An expression reads the observables, a species by its name with(and)written as_, andt/time; a name it cannot read,^or a tuple is refused withValueErrorwhere it is added (issue #1509)clear_stop_conditions()
Steady-state (method ∈ "integration" (default), "newton", "kinsol" alias):
steady_state(*, tol, max_time, method, rtol, atol, max_steps, sensitivity_params, mask)→SteadyStateResultsteady_state_batch(params=None, *, states, tol, max_time, method, rtol, atol, max_steps, n_workers, mask, newton_burst)→list[SteadyStateResult]. Withoutstates, every entry starts from the model’s initial conditions (Model.reset), asrun_batch’s points do, and the batch warns where the current state differs from them (issue #1521).statesstarts each entry from a state of its own, paired row by row withparams;newton_burst=Falsewithmethod="newton"solves from that state with no integration burst and reports an unstable root instead of rejecting it (issue #524)steady_states(*, n_starts, seed, upper, decades, dedupe_rtol, tol, n_workers, mask)→SteadyStates: every steady state multistart Newton finds from starts sampled on the conservation manifold, each with its stability (issue #524). See the user guide’s steady-state pageBoth raise
SimulationErrorfor a model with an event, or one that reads the time (issue #710): the solvers read the right-hand side att = 0and fire no event.run(steady_state=True)integrates such a model to the end of its span and does not stop early.mask(issue #74): which species enter the convergence norm — a boolean array of lengthn_species, or the species names to keep. DefaultNonetests every species (BNG2.pl parity). It also restricts the KINSOL unknown set and thedY_ss/dpsystem to the same subspace; excluded species get a NaNdY_ss/dprow. SeeModel.is_pure_sink().
Configuration:
set_tolerances(rtol, atol)— ODE solver tolerancesset_max_steps(max_steps)— Max internal solver stepsauto_atol(*, rtol=None, floor=None)→ndarray— derive a per-species absolute tolerance from the model’s current state,rtol * max(|y_i|, floor)(issue #196). For a tolerance that is a constant of the model rather than of the point being integrated, usebngsim.derive_atolbelow.
Every atol above takes a float, bngsim.ExactAtol(value) (exact,
CVodeSStolerances), a sequence of
n_species values ordered like model.species_names (per-species,
CVodeSVtolerances), or "auto" (derive one — see auto_atol). run,
run_batch, parameter_scan and compute_all_sensitivities read a float as a
per-species ceiling, min(atol, rtol * max(|x_i(0)|, floor)) (issue #1420; the
solver guide
defines the floor), except where sensitivities run on CVODES’ difference quotient,
which keeps the scalar; steady_state holds a float you pass exactly. A wrong-length
vector raises ValueError rather than broadcasting. Multi-point calls resolve it
once, so every point of a batch or scan shares one tolerance. See the
solver guide.
Properties:
method,model,current_timehas_analytic_sens_rhs(bool) andsens_rhs_decline_reason(str | None, issue #438) — whether a forward-sensitivity run on this Simulator uses bngsim’s analytic∂f/∂por CVODES’ internal difference quotient, and why not when it does not. A property of the (build, model) pair rather than of the build, so nocapabilities()key can answer it. Ask a Simulator built for a sensitivity run: a plain one carries no analytic∂f/∂pbecause nobody asked for one, which is not a decline. See the PyBNF guide.
bngsim.NfsimSession¶
Stateful NFsim session API for multi-action workflows where parameters or live particle counts are changed between simulation segments.
Constructor:
NfsimSession(xml_path, *, molecule_limit)— Create an NFsim session from a BNGL-generated NFsim XML file
Session control:
initialize(seed)— Initialize the live NFsim systemsimulate(t_start, t_end, *, n_points, timeout)→Result—timeoutis a wall-clock budget in seconds (Nonedisables, the default). Checked betweenstepTo()output points; on overrun raisesbngsim.SimulationTimeoutand the session must be destroyed before reuse.destroy()— Release the live NFsim session
Parameters:
set_param(name, value)— Set a parameter before initializationget_parameter(name)— Evaluate a parameter in the live session
Live counts:
get_molecule_count(mol_type)— Count all live molecules of a typeadd_molecules(mol_type, count)— Add default unbound moleculesget_species_count(pattern)— Count an exact single-molecule BNGL speciesadd_species(pattern, count)— Add exact single-molecule species instancesremove_species(pattern, count)— Remove exact single-molecule species instancesset_species_count(pattern, count)— Set the exact species count by adding or removing instances
Species count mutation currently supports exact, unbound, single-molecule BNGL
patterns such as "X(p~0,y)", "L(r)", and "TNF()". Patterns must list every
component and specify every stateful component state. Multi-molecule complex
patterns fail with a clear SimulationError.
Saved states (in-process):
save_concentrations(label=None)— Snapshot the live molecular state (counts, component states, bonds). An optionallabelnames it (BNGsaveConcentrations("name")).restore_concentrations(label=None)— Rewind to the snapshot; alabelmust match the currently-held one.has_saved_concentrations(label=None)— Whether a (named) snapshot is held.The NFsim backend keeps a single in-session snapshot, so each save overwrites the previous one; the label lets a mismatched restore fail loudly. The network-based
Modelsupports true multi-slot named states.
bngsim.RuleMonkeySession¶
Stateful RuleMonkey session API for exact network-free workflows where parameters are configured before initialization and live particle counts can be changed between simulation segments.
Constructor:
RuleMonkeySession(xml_path, *, molecule_limit, block_same_complex_binding)— Create a RuleMonkey session from a BNGL-generated NFsim XML file
Session control:
initialize(seed, t_start=None)— Initialize the live RuleMonkey system with the seed state att_starton the model’s clock (current_timereads it). LeftNone, the clock starts at 0, and for a model that reads the clock the firstsimulatemoves the seed state to its own start if nothing has moved the session first (issue #1022)simulate(t_start, t_end, *, n_points, timeout)→Result—timeoutis a wall-clock budget in seconds (Nonedisables, the default). Polled by upstream every ~1024 SSA events; on overrun raisesbngsim.SimulationTimeoutand the session must be destroyed before reuse. A segment that starts atcurrent_timeruns on the session clock, which is model time. One that starts elsewhere continues fromcurrent_timeand is labelled with the times asked for; for a model that reads the clock (time()or a table function indexed by time) that raisesSimulationError.solver_stats["n_steps"]counts the events sinceinitialize, as anNfsimSession’s does.step_to(time, *, timeout)— Advance totimeon the model’s clock without recording output; honors the sametimeoutsemantics assimulate(...).destroy()— Release the live RuleMonkey session
Every time a session takes or reports (current_time, the clock read through
evaluate, a save_state snapshot) is model time. A model that reads the
clock in its functions has its events placed on time elapsed since the
session’s start, which keeps a late start’s waiting times (issue #1063).
Parameters and counts:
set_param(name, value)— Set a parameter before initializationclear_param_overrides()— Clear parameter overrides before initializationget_parameter(name)— Evaluate a parameter from XML plus overridesget_molecule_count(mol_type)— Count all live molecules of a typeadd_molecules(mol_type, count)— Add default unbound moleculesget_observable_names()— Return observable names from the XMLget_observable_values()— Return current observable values
bngsim.Result¶
Properties:
time—ndarray (n_times,)species—ndarray (n_times, n_species)with named accessstate—ndarray (n_times, n_state): the full integrator state, unprojected and unremapped, inModel.species_namesorder — the rowsModel.rhs/Model.jacobiantake. Wider thanspecieson an SBML model whose event assigns a parameter or compartment; the stored rather than the reported value where a species column is remapped after the solve. Solve results only (issue #508)state_names—list[str]column names ofstate(= the producing model’sspecies_names)observables—ndarray (n_times, n_obs)with named access (e.g.result.observables["A_tot"])expressions—ndarray (n_times, n_expr)with named accesssensitivities—ndarray (n_times, n_species, n_params)forward sensitivity tensorhas_sensitivities—boolsensitivity_params—list[str]parameter names for sensitivityic_sensitivity_seed—dict[str, dict[str, float]] | None— the per-run record of the same matrixModel.effective_ic_sensitivity()reports: what CVODES was actually seeded with for this run. Use theModelaccessor to build routing at setup; use this to see what a particular run used, which is the only correct answer for a batch/scan over a nonlinear derived IC (Rtot = R0*scale), where the coefficients move per point.{}= nothing seeds;None= not recorded (no parameter sensitivities, non-ODE, an older HDF5 file, a squeezed batch whose rows disagreed, or acarry_sensitivities=Truephase, where the engine seeds from the prior phase’sdx/dθand discards these rows — GH #210/#81)dataframe—pandas.DataFrame(requires pandas)xr— AMICI-style xarray accessor;result.xr.species,.observables,.expressions,.sensitivities,.sensitivities_icreturn labeledxarray.DataArrays withtime/state/observable/expression/parameter/ic_statecoords (requires xarray)solver_stats—dictwith solver diagnosticsstop_condition—str | None: the label (else the expression) of the stop condition the result was cut at,Nonewhere none was met (issue #1369)custom_attrs—dictfor user metadatan_times,n_species,n_observables,n_expressionsspecies_names,observable_names,expression_names
Methods:
resolve_outputs(selectors)→list[dict]— resolve typed output selectors (species:/observable:/expression:, aliasesstate:→species:,function:→expression:, withexpression:foo()→expression:foo) to per-output metadata{"selector","kind","name","index","column_label"}. Bare names resolve only if unique across all three kinds; ambiguous/unresolved selectors raise with the candidates listed.outputs(selectors)→ndarray (n_times, n_outputs)— value columns for the named selectors (one column per selector, in order)fisher_information(sigma)→ndarray (n_params, n_params)— Fisher Information Matrix from sensitivity datagradient(loss_fn)→ndarray (n_params,)— parameter gradient ∇_p L from sensitivity tensor and loss functionto_gdat(path)— Export observables as BNG.gdatto_cdat(path)— Export species as BNG.cdatto_csv(path, *, kind="observables"|"species", sep=",", include_time=True, header=True)— Plain delimited-text export (CSV / TSV); no#prefix; SBML/RoadRunner-friendlyto_xarray()→xarray.Datasetbundling species/observables/expressions/sensitivities/sensitivities_ic with sharedtimecoord (requires xarray);custom_attrs+seedpropagate tods.attrssave(path)— Save to HDF5 (requires h5py)Result.load(path)— Load from HDF5 (class method)Result.squeeze(results)— Stack list into 3D batch result; refuses a list holding a row that met a stop condition, or a row whose time points, species/observable/expression names, sensitivity parameters or IC species, reaction labels or present blocks differ from the first row’s (issue #1513)
bngsim.SteadyStateResult¶
Returned by Simulator.steady_state().
Properties:
concentrations—ndarray (n_species,)species values at steady statespecies_names—list[str]residual,method_used,converged,n_steps,n_rhs_evals— solve diagnosticsn_residual_species,excluded_species— how many species entered||f||_2/n, and the 0-based indicesmask=excluded (issue #74)unconverged_pure_sinks—list[str]; on a failed solve, the write-only accumulators that were in the test and are carrying flux — i.e. the residual has a structural floor, not a slow tail. Empty otherwisesensitivity—ndarray (n_species, n_params)speciesdY_ss/dp(orNoneif nosensitivity_params)sensitivity_params—list[str]observable_names,expression_names—list[str]labelling the output-sensitivity rows (populated on a sensitivity run;expression_namesfilters the auto-generated_rateLawNfunctions, matchingResult)sensitivities_observables—ndarray (n_observables, n_params)exactd(observable)/dpsensitivities_expressions—ndarray (n_expressions, n_params)d(function)/dptotal derivative (state chain + explicit∂func/∂p)rhs_backend,sens_jacobian_source,sens_dfdp_source,sens_output_source— which numerical path each piece took.sens_output_source(issues #75, #1002) is"codegen"/"partial"/"none"for thed(function)/dpblock: the compiledbngsim_codegen_output_senschain rule answered every function, some of them, or none. A function it does not answer is a NaN row, refused by name byoutput_sensitivities; it is not differencedsens_jacobian_rcond—min|U_jj|/max|U_jj|from the LU of the (reduced) Jacobian that was inverted. It depends on the units of the species and decides nothing; the measures below dosens_root_determinant_ratio,sens_root_pivot_share,sens_root_condition,sens_root_column_shift,sens_root_state_shift,sens_root_hold_shift,sens_root_growth_rate,sens_root_relaxation— whether the steady state is an isolated root of the systemdY_ss/dpwas solved on, one the system rests at, and the returned state on it (issue #995): what the determinant keeps of itself when the state is corrected by one Newton step, the least a pivot is of the terms it was computed from, the componentwise condition number (the Perron root of|A⁻¹|·|A|), the largest move of a column solved again a Newton step on, against the larger of itself and1e-3/|p|, and of a species under that step (where a column moves by more than 0.01 of itself, or the state does, the state is stepped on, up to ten times, andsens_root_newton_stepscounts the steps:concentrations,residualand the sensitivities are then those of the stepped state), the same where a run ends that is taken on formax_timefrom a millionth beside the returned state, the largest real part among the eigenvalues (up to 512 unknowns; above that, among those of the species the state has at a zero), and the most of a column that a run ofmax_timewould leave unestablished.steady_stateraisesSimulationErroroutside 0.6 to 1.67, below 1e-13, above 1e13 (1e-6 and 1e6 wheresens_jacobian_sourceis"finite-difference"), above 0.01 (either, after the steps), above 0.01, where the real part timesmax_timeis above 0.01 (sens_root_spectral_radiusis the largest eigenvalue in size), and above 0.01; and where the run that is taken on fails or uses itsmax_stepssteps short ofmax_time.sens_root_hold_drift,sens_root_hold_stepsandsens_root_hold_timedescribe that run, andsens_species_scaleis what an entry for each species is small against: its own concentration, or for a species at a zero where it has been and what it is coupled to. Thesens_root_*_speciesandsens_root_*_paramattributes name the species and the parameter each is read at (Nonewhere none)sens_mask_held_species,sens_mask_reader_species— a speciesmask=left out that the solve held, and a kept species whose rate reads it;steady_stateraises where there is oneroot_stability,n_unstable_roots_rejected— the issue #78 linear-stability verdict on a Newton root ("stable"/"undetermined"/"unstable",""for integration), and how many candidate roots the certificate discardedeigenvalues—ndarray (n_unknowns,) complex128; the spectrum the verdict was read off (issue #523): the Jacobian restricted to the species the polish solved for, sorted by descending real part, a conjugate pair adjacent with+imagfirst. Empty when no spectrum was computed — an integration result, or a root the certificate declined on (more than 512 unknowns)
Methods:
resolve_outputs(selectors)→list[dict]— same selector grammar asResult.resolve_outputsoutput_sensitivities(selectors, *, axis="parameter")→ndarray (n_selectors, n_params)— steady-state∂(named output)/∂pforspecies:/observable:/expression:selectors, mirroringResult.output_sensitivities(no time axis).axis="ic"raises: the initial-condition axis is not computed (a parameter that sets an initial amount carries its share of a conserved total in its own column, issue #704)__getitem__(name)→float— steady-state concentration by species nameto_dict()→dict[str, float]
Running a BNGL protocol (issue #1366)¶
bngsim.run_bngl(path, *, seed=None, rtol=None, atol=None, allow_lossy=False, bng2_pl=None, net_out=None, timeout=600, cache=True)→ProtocolResult— load a.bnglwithModel.from_bngl, parse its actions, and run them, as BNG2.pl runs the filebngsim.run_protocol(model, protocol, *, seed=None, rtol=None, atol=None, allow_lossy=False)→ProtocolResult— run aProtocolSpec’s steps on a clone ofmodel, from the spec’sgeneration_step. ODE, SSA and PSA runs; NFsim and PLA runs,stop_if/max_sim_steps, and an action the protocol cannot hold are refused before anything runs (ConversionError)ProtocolResult— a sequence ofProtocolRun, one per experiment, indexed by position or bysuffix;.labels,.model(the model as the protocol left it),.protocolProtocolRun—.experiment,.kind("simulate","scan"or"bifurcate"),.label,.method;.resultfor asimulate;.scan_values/.scan_resultsfor a scan or a bifurcation’s upward sweep, and.backward_values/.backward_resultsfor its sweep back down
Exceptions¶
All inherit from bngsim.BngsimError (which inherits RuntimeError):
ModelError—.netparse failures, invalid model stateSimulationError— Solver failures (convergence, NaN)SparseJacobianUnavailableError—force_sparse_linear_solver=Trueon a model that cannot supply a sparse Jacobian for KLU: its analytical Jacobian is unavailable (or suppressed byjacobian="fd") and its sparsity pattern has no structural nonzero to colour. A refusal by name, not a solver failure: drop the flag to use the dense solver. ASimulationError, as it was raised before it had a type, fromrun()andrun_batch()alike.
SimulationTimeout— Wall-clock budget exceeded;.timeoutand.elapsedattributesParameterError— Unknown parameter name, invalid valueSensitivityUnsupportedError— Forward sensitivity declined for this model: an event whose crossing time moves in a waydt*/dpcannot be computed for, or a rate law that does not differentiate to closed form. A declared capability gap, not a failure — catch it to fall back on a derivative-free optimizer. Also inheritsValueError(what both sites raised before the class existed), soexcept ValueErrorstill catches it. A missing compiler/JIT is not this exception; that stays a plainRuntimeError.SensitivityRefusedDuringRunError— the solver’s refusal partway through a sensitivity run, of a construct whose derivative it cannot produce, by a condition no choice of requested parameters changes: an event that fires as its trigger leaves its threshold with no first-order motion, a compartment that jumps twice at one instant, a delay arun_batchrow made positive. Also aSimulationError, as those were raised before they had a type. A refusal whose condition can turn on a requested column (a crossing or fire it moves) is its subclass below; the one exception is #1279’s refusal of a held equality the state leaves, which had this type first and turns on a column where a restart lands on the stop.SensitivityRefusedForParametersError— a refusal partway through a run whose condition can turn on the parameters requested: a crossing or a fire one moves, two times one moves apart, an edge one moves the run onto. The message names it and, where it can, the parameters to drop.compute_all_sensitivitiesretries a chunk refused this way one column at a time and raises this type, naming the columns refused alone and the ones that ran, when some are refused alone too. Before it had a type it was a plainSimulationError, as a solver failure is; it still is one.
CodegenDeclinedError—codegen=Trueon a model codegen declines, such as one whose functions read each other: a statement about the model, which runs on the interpreted RHS withcodegen=Noneorcodegen=False. A codegen build failure (a compiler error, a timeout) says nothing about the model and stays a plainRuntimeError. The deprecatedbngsim.prepare_codegenraises it for a decline too.CodegenToolchainError— the C toolchain cannot build a codegen library for any model: a compile failed and the same command fails on a trivial C file too, or the compiler is not there. The message names the compiler, the linker and, on macOS, the SDK, and suggests an installed SDK that links when one does. While the compiler and its environment are unchanged the verdict holds, re-probed every 5 minutes, andcodegen=Trueand forward sensitivities raise it without compiling, and past the cache lookup without generating the model’s source, so a model codegen would have declined gets this error too; a library already in the cache is still used. UnderBNGSIM_CODEGEN_STRICT=1an automatic build raises it too (issue #1544).StopConditionMet— Stop condition triggered;.resulthas partial data
Warnings (UserWarning subclasses; filter or escalate them with warnings):
RuleMonkeyWarning—method="rm"or aRuleMonkeySessionloaded a model for which RuleMonkey reports a warning-level diagnostic: how it reads a rule or rate law, such as a rule whose products can never be what it writes, which it never fires as BioNetGen generates no reaction for it (issue #1022)
.net files as a dict¶
bngsim.parse_net_file(path)→dict— The components of a.netfile as a plain Python dict with keysparameters,species,species_ic_params,observables,functions,reactionsandnet_file_dir. It is the C++ loader’s own reading of the file, the oneModel.from_netbuilds, with the values the built model holds, so it needs the compiled extension, raisesValueErrorwhereModel.from_netrefuses the file (FileNotFoundErrororIsADirectoryErrorfor a path that is not a file), and reports the numbersModel.from_netputs in the same slots. DeprecatedSat/Hillrate laws come back rewritten as explicit functions, with the loader’s warning.bngsim.build_model_from_parsed(parsed)→Model— Build a BNGsimModelfrom the dict returned byparse_net_file(), or one of the same shape. It makes theModelBuildercalls the loader makes, so an unmodified dict builds the modelModel.from_netloads.
Absolute tolerance (issues #196, #212, #213, #1420)¶
bngsim.AUTO→"auto"— the token everyatol=accepts. Also the capability probe for the whole per-species feature:hasattr(bngsim, "AUTO"). The version string is not one — the checkout that first carried #196 still declared0.12.2.bngsim.derive_atol(state, rtol, *, floor=None)→ndarray—rtol * max(|y_i|, floor)over a state you supply, ordered likemodel.species_names.floordefaults to the smallest strictly positive entry ofstate, but not above1.0(also the floor if every entry is zero).bngsim.normalize_atol_vector(atol, n_species, species_names=None, *, where="atol")→list[float]— the length/position/finiteness checkrun()applies, callable at the point a vector is built rather than at the first run.bngsim.TRACKING→"tracking"— the token for the over-time mode of issue #213; sugar forTrackingAtol().bngsim.TrackingAtol(decades=12.0, ceiling="auto")— anatol=value that re-evaluates the vector against the state being integrated (CVodeWFtolerances):atol_i(y) = clamp(rtol*|y_i|, ceiling_i * 10**-decades, ceiling_i). A vector fixes the tolerance per species; this fixes it per instant, which is what a species that starts at order one and decays to something tiny needs. Atdecades=0it is the ceiling vector exactly, and it is never looser than that vector.hasattr(bngsim, "TrackingAtol")is the probe. Costs steps and can make a stiff model unintegrable — see the solver guide for the corpus numbers.bngsim.ExactAtol(value)— anatol=value that holds every species tovalueexactly (CVodeSStolerances). A floatatol, the default1e-8or one you pass, is read as a ceiling,min(atol, rtol * max(|x_i|, floor))per species (issue #1420); this is the opt-out, and how a scalar was read before. Accepted byset_tolerancesandsteady_statetoo, and carried by anEvaluationSpecas{"exact": value};bngsim.convert.read_sedml(..., exact_atol=True)reads a SED-ML file’s absolute tolerance this way. See the solver guide.
derive_atol and Simulator.auto_atol run the same rule; they differ only in
which state they read. auto_atol (and atol="auto") reads the model’s live
state, which is right for a one-off run and wrong for a fit that moves initial
conditions — there the tolerance would become a function of the fit point and
put a step in the objective. derive_atol reads whatever state you hand it, so
the vector can be derived once from the nominal state and held constant. See the
solver guide.
Codegen artifact cache (issue #205)¶
The compiled-artifact cache is content-addressed and nothing prunes it
automatically, so these are the supported way to see how large it has grown and
to bound it. Each mirrors one bngsim-cache verb and takes dry_run=True; all four
accept a cache_dir positional to operate on a directory other than the configured
one. See the codegen guide.
bngsim.codegen_cache_info(cache_dir=None)→CacheInfo— path, entry count,total_bytes,by_kind,by_key,live/orphaned(artifacts under this install’s codegen key and under any other, issue #363),other_arch(this install’s source built for another architecture, which is neither, issue #1486),built_span/used_span, andatime_is_live(whether this filesystem records access times, i.e. whether prune’s LRU order is real recency or build order). Reads directory metadata only — the key comes off the artifact’s name..to_dict()is the JSON form.bngsim.clean_codegen_cache(cache_dir=None, *, min_age=3600.0, dry_run=False)→CacheSweep— remove leaked compile partials only (bngsim_shard_*directories, stray.c, temp libraries). No compiled artifact is touched, so no cache hit is lost.bngsim.prune_codegen_cache(cache_dir=None, *, orphaned=False, keep_keys=(), older_than=None, max_size=None, min_age=3600.0, dry_run=False)→CacheSweep— bound the cache by orphan status (orphaned=Truedrops every artifact built under another codegen key, the targeted sweep after an emitter edit, but leavesother_archto the install that can load it), age (older_than="30d") and/or total size (max_size="2G"), evicting least-recently-used artifacts. At least one bound is required; the partial sweep runs first, so the size cap covers the whole directory.keep_keysspares other installs sharing the directory (Nonein it spares the pre-#363 names) and is only accepted withorphaned=True.bngsim.clear_codegen_cache(cache_dir=None, *, min_age=0.0, dry_run=False)→CacheSweep— remove every artifact and partial bngsim owns.
Beside an artifact whose analytic ∂f/∂p was declined, bngsim writes a small
decline note (rhs_<key>_<hash>.sens.json, issue #438) so the reason survives a
warm cache. Beside an artifact built with the output-sensitivity code it writes a
second note (rhs_<key>_<hash>.outputs.json, issue #1004): which functions the
artifact holds a derivative for, and why not for the others. A process that is
handed the artifact from the cache reads it, so output_sensitivities([...])
raises the reason the build recorded, and the map is not worked out again. (The
note answers for a model whose tables are the builder’s: one given a table
function after it ran keeps its artifact and is analysed for itself, beside
what the note says the build has no derivative for.) Both
are bngsim’s own files: counted under one kind, removed by clear, and removed by
prune exactly when the artifact they describe is removed.
Entries bngsim did not write are classified foreign, reported, and never
removed — by clear as much as by clean — because BNGSIM_CODEGEN_CACHE_DIR is a
user-supplied path. Nothing modified within min_age is removed either, so a sweep
cannot delete the scratch files of a compile that is still running.
bngsim.cache carries the rest: the CacheEntry / CacheInfo / CacheSweep types,
the KIND_* taxonomy, classify() and artifact_key() (the kind and the codegen key
an entry’s name identifies it by), and the parse_duration / parse_size value
parsers the CLI uses.
Utility functions¶
bngsim.reserved_names()→dictwith"constants"and"functions"listsbngsim.configure_logging(level)— Enable log output
Generated API (autodoc)¶
The curated summaries above cover the common surface. The full, always-in-sync reference below is generated directly from the package docstrings.
bngsim — Embeddable simulation engine for BioNetGen reaction networks.
Usage:
import bngsim
model = bngsim.Model.from_net("model.net")
sim = bngsim.Simulator(model, method="ode")
model.set_param("kf", 0.5)
result = sim.run(t_span=(0, 1000), n_points=1001)
result.time # (1001,) ndarray
result.observables # (1001, n_obs) ndarray
result.species # (1001, n_species) ndarray
See the package README for installation, usage, and API overview.
- class bngsim.Model(_core, _guarded=None)[source]¶
Bases:
objectA BioNetGen reaction network model.
A Model holds species, reactions, observables, parameters, and functions. It can be loaded from
.netfiles and, via the factory methods below, from Antimony and SBML inputs.Models are not thread-safe. For parallel workers, use
clone()to create independent copies.- Parameters:
Examples
>>> model = bngsim.Model.from_net("model.net") >>> model.n_species 5 >>> model.set_param("kf", 0.5) >>> model.get_param("kf") 0.5 >>> model.set_params({"kf": 1.0, "kr": 0.1})
- classmethod load(path, *, defer_jacobian=None, compartment_sizes=None)[source]¶
Load a model from a file, dispatching on its suffix.
A single entry point over the format-specific factories, so callers who already know the path do not have to know the format:
Suffix
Factory
.ant.xml.sbml.net.bnglMatching is case-insensitive.
.bnglneeds BNG2.pl on the machine (pip install 'bngsim[bngl]') because bngsim has no BNGL parser — seefrom_bngl(), and note that extra is not the parity_checks/paritygroup, which pins an exact PyBioNetGen commit for engine-routing provenance rather than for BNG2.pl.- Parameters:
defer_jacobian (
bool|None) – GH #145 escape hatch, forwarded to the selected factory (seefrom_sbml()). Default lazy;defer_jacobian=Falsederives the analytical Functional Jacobian eagerly at load.compartment_sizes (
dict[str,float] |None) – Compartment id → volume, forwarded to the SBML-family factories (issue #164 — seefrom_sbml()). Rejected for.netand.bngl, whose volumes BNG2.pl folds into the network’s rate constants during network generation.
- Returns:
The loaded model.
- Return type:
- Raises:
ImportError – If the format’s optional dependency is not installed (
antimonyfor.ant—pip install 'bngsim[antimony]').FileNotFoundError – If the file does not exist.
ModelError – If the suffix is not a loadable format, the file cannot be parsed,
compartment_sizesis given for a.net/.bnglmodel, or BNG2.pl is unavailable for a.bnglone.
- classmethod from_antimony(path, *, compartment_sizes=None)[source]¶
Load a model from an Antimony
.antfile.Antimony is a human-readable model description language. Internally converts to SBML via libantimony, then loads via libsbml for correct SBML semantics. A text with more than one module is loaded at its main module, as libantimony names it (issue #1508); see the user guide’s “Antimony and SBML model loading”.
Requires:
pip install antimony python-libsbml- Parameters:
- Returns:
The loaded model.
- Return type:
- Raises:
ImportError – If
antimonyorlibsbmlis not installed.FileNotFoundError – If the file does not exist.
ModelError – If the file cannot be parsed.
- classmethod from_antimony_string(text, *, compartment_sizes=None)[source]¶
Load a model from an Antimony string.
A text with more than one module is loaded at its main module, as
from_antimony()loads a file (issue #1508).
- classmethod from_sbml(path, *, defer_jacobian=None, compartment_sizes=None)[source]¶
Load a model from an SBML
.xmlfile.- Parameters:
defer_jacobian (
bool|None) – GH #145 escape hatch. The analytical Functional Jacobian (GH #76) is derived lazily at the first ODE-solve setup by default (None); passdefer_jacobian=Falseto derive it eagerly at load instead (the pre-#145 behavior, for A/B and safety).BNGSIM_EAGER_JACOBIAN=1forces eager for every load path.compartment_sizes (
dict[str,float] |None) –Compartment id → volume, applied to the parsed document before bngsim interprets it (issue #164). This is how a compartment volume is changed:
set_param()refuses a compartment-size write, because the size is folded at load into constants the write cannot reach — per-species volume factors, amount-declared initial conditions, mass-action rate constants, SSA propensity volumes, and the emitted RHS/sensitivity sources. Overriding here moves the size before any of those are derived, so the result is the model you would get by editing thesize=attribute in the file. Scan or fit a volume by looping over loads:>>> for v in [1.0, 2.0, 4.0]: ... m = Model.from_sbml("pbpk.xml", compartment_sizes={"Liver": v})
Ids are SBML ids as the document carries them (before bngsim’s identifier mangling, after any
compflattening). AninitialAssignmenton an overridden compartment is dropped, since it would otherwise take precedence. A compartment whose size an assignment rule computes is refused — the rule, not the attribute, is its volume.
- Returns:
The loaded model.
- Return type:
- Raises:
ImportError – If
python-libsbmlis not installed.FileNotFoundError – If the file does not exist.
ModelError – If the file cannot be parsed, or
compartment_sizesnames an unknown compartment, a non-positive size, or an assignment-rule compartment.
- classmethod from_sbml_string(text, *, defer_jacobian=None, compartment_sizes=None)[source]¶
Load a model from an SBML XML string.
- Parameters:
text (
str) – SBML XML text.defer_jacobian (
bool|None) – GH #145 escape hatch (seefrom_sbml()). Default lazy; passdefer_jacobian=False(or setBNGSIM_EAGER_JACOBIAN=1) to derive the analytical Functional Jacobian eagerly at load.compartment_sizes (
dict[str,float] |None) – Compartment id → volume, applied before interpretation — the supported way to change a volume, sinceset_param()refuses a compartment-size write (issue #164). Seefrom_sbml().
- Returns:
The loaded model.
- Return type:
- classmethod from_net(path, *, defer_jacobian=None)[source]¶
Load a model from a BNG
.netfile.- Parameters:
- Returns:
The loaded model.
- Return type:
- Raises:
ModelError – If the file cannot be parsed, or holds none of the parameters, species, functions, reactions and groups blocks, as a 0-byte file, one of comments only, or one whose lines end in a bare carriage return does (issue #1511); or if a line the loader reads is not UTF-8 (issue #1653).
FileNotFoundError – If the file does not exist.
- classmethod from_bngl(path, *, protocol=False, bng2_pl=None, net_out=None, timeout=600, cache=True, defer_jacobian=None)[source]¶
Load a rule-based BNGL (
.bngl) model, via BNG2.pl (GH #162).- Overloads:
path (
str|Path), *, protocol (Literal[False]) =..., bng2_pl (str|Path|None) =..., net_out (str|Path|None) =..., timeout (int) =..., cache (bool) =..., defer_jacobian (bool|None) =...→Modelpath (
str|Path), *, protocol (Literal[True]), bng2_pl (str|Path|None) =..., net_out (str|Path|None) =..., timeout (int) =..., cache (bool) =..., defer_jacobian (bool|None) =...→tuple[Model,ProtocolSpec]
- Parameters:
- Return type:
bngsim simulates reaction networks, and BNGL describes rules, so this is not a parser: the model block is handed to
BNG2.pl generate_network, and the.netit emits is loaded throughfrom_net(). That makes BNG2.pl a requirement —pip install 'bngsim[bngl]'supplies it (PyBioNetGen bundles it), or point$BNGPATH/$BNG2_PLat a BioNetGen you already have. It is a Perl script, soperlmust be onPATHtoo (macOS and most Linux distributions ship one; stock Windows does not).The file’s experiment is not executed. A
.bngltypically ends insimulate({...})orparameter_scan({...}), and running the file as written would run the author’s entire experiment just to obtain a network. Its actions before the firstgenerate_networkare applied, except file writes, and a relative path one reads (readFile) resolves against the model’s directory.generate_networkitself runs with the source’s ownmax_iter/max_agg/max_stoich, since those are what make an unbounded rule set finite. Everything after it is dropped. A file with nogenerate_networkbefore its first run stops at that run, the state the run starts from. Passprotocol=Trueto get the actions back as aProtocolSpecrather than lose them, andbngsim.run_protocol()to run them as BNG2.pl does (bngsim.run_bngl()loads and runs in one call).A
ConversionWarningnames the dropped actions that set a parameter or a concentration the firstsimulate,parameter_scanorbifurcatestarts from, where the loaded model holds another value (issue #1366). Withprotocol=Truethere is no warning: the spec returns the actions themselves.- Parameters:
protocol (
bool) – Return(model, ProtocolSpec)instead of just the model, the spec being the parsed actions block (bngsim.convert.parse_bngl_protocol()in non-strict mode, so a fidelity-affecting action is recorded inspec.lossyand warned about rather than raising).bng2_pl (
str|Path|None) – An explicit BioNetGen folder orBNG2.plpath, outranking$BNG2_PL,$BNGPATH,PATHand an installed PyBioNetGen.net_out (
str|Path|None) – Write the generated network here and load it from there, instead of from the cache. The way to keep the.netas an artifact.timeout (
int) – Seconds to allowgenerate_network(default 600). A rule set whose network is unbounded hits this.cache (
bool) – Reuse a previously generated network for unchanged BNGL (default). The cache is content-addressed under~/.cache/bngsim/networks(override with$BNGSIM_BNGL_CACHE_DIR), keyed on the flattened model text and the BioNetGen that produced it (BNG2.pl and thePerl2library it loads), so it cannot go stale. A model that reads another file beforegenerate_network(readFile,readModel,readNetwork,readSBML,readNFspecies) is not cached: its network depends on that file, which the key cannot see (issue #1494).Falseregenerates into a per-process directory.defer_jacobian (
bool|None) – GH #145 escape hatch, forwarded tofrom_net().
- Returns:
The loaded model, and the actions block when
protocol=True.- Return type:
- Raises:
FileNotFoundError – If the file does not exist.
ModelError – If BNG2.pl (or
perl) cannot be found — with every mechanism that was consulted named — or ifgenerate_networkfails or times out, with the tail of BNG2.pl’s own output attached.
Notes
Compartmental BNGL loads: BNG2.pl bakes each compartment’s volume into the generated rate constants, exactly as for a hand-generated
.net. For the same reason there is nocompartment_sizes=here — the volume has to change in the BNGL source, before generation.Examples
>>> model = bngsim.Model.from_bngl("egfr.bngl") >>> model, spec = bngsim.Model.from_bngl( ... "egfr.bngl", protocol=True) >>> [e.t_end for e in spec.experiments] [100.0]
- time_discontinuity_conditions()[source]¶
The branch conditions this model switches on simulation time with.
What
Simulator.run()resolves to crossing times and stops the integration step on, so anif(time() >= 100, ...)window cannot be stepped over (issue #305 for the mechanism, issue #440 for why a BNGL model needs it).Two sources, in that order. The SBML loader walks the document at load time and registers each condition it finds as a CVODE root, and hands the registered set over verbatim — reading it back here rather than re-deriving one is what keeps the stops a subset of the roots. A
.net/BNGL model is built entirely in C++, so its loader has no seam to register a root at and there is nothing to read back; the conditions are recovered from the built model’s own function bodies instead, and only when something asks.The recovery is cached, keyed by what it reads from parameter values (issue #726): which species pass the unit-rate clock probes at the model’s current values, and the size of the live compartment each such amount-valued species is read through. A BNGL counter fed at
k_tis a clock atk_t = 1and not otherwise, so its thresholds are time conditions at one value and not at another; an SBML substance-only counter fed at its compartment’s size is one only while that size is 1. Cached once, a model first run atk_t = 0and then at 1 got no stop at a clock-gated window, and one first run at 1 and then at 0.5 got neither a stop nor a root. Every recovery is kept by its key and shared with clones, so a batch whose rows sit at another verdict than the parent’s recovers that verdict once.A recovered entry is not always a condition. Since issue #869 it can also be a bare step call of a clock, such as
floor((time()-t0)/P), with no comparison in it, and since issue #904 a call that bends (max(0, 1 - abs(time()-t0)/w)). Such a call jumps or turns the rate law itself, and each jump and turn is a stop.The recovery runs for a model that registered conditions too, and what it finds beside them is added. The loader registers comparisons, so a
floorpulse or amaxpulse standing beside apiecewisein an SBML model had no entry at all, and was stepped over where the same pulse alone was not.
- state_switch_root_conditions()[source]¶
The rate-law conditions over model state a plain run roots on (issue #897).
A rate law switched by a comparison that reads the state, such as
if((time() >= 4*S) && (time() < 4*S + w), k, 0), has crossings at times the state decides, so no stop can be placed at them before the run.Simulator.run()registers each one as a CVODE root instead, which stops the step at the crossing rather than letting it span a narrow window and miss it. A step or bend call over state, such asfloor(T/100)ormax(0, 1 - abs(T-50)/w), is rooted where it jumps or bends, since it spells the same pulse with no comparison (issue #1068).The crossings
time_discontinuity_conditions()already covers are left out, and so is every condition in a function that no rate law reads. Past those, the answer depends on the model’s structure alone, so it is cached for the covered set it was derived beside, and a clone inherits it. The covered set moves with a clock’s rate (issue #726): a threshold on a counter is covered while the counter is a clock and has to be rooted once it is not.
- table_function_breaks()[source]¶
Where the model’s table functions break (issue #709): for each table with any, its index kind, the observable it is indexed by, the breakpoints at which a run restarts, and those at which it only ends a step.
Simulator.run()stops the integration on each one a table over time, or over a unit-rate counter, reaches in the run. Which breakpoints count isbngsim._switch_sensitivity.table_function_breaks()’s rule: a table of many samples of a smooth signal has none.Read from the tables alone, so it is kept until a table is added or given new data (
add_table_function()drops it), and a clone inherits it. A model with no table pays one attribute read.- Return type:
- prepare_analytical_jacobian()[source]¶
Derive and attach the analytical Functional Jacobian (GH #76), at most once.
Idempotent (GH #145): the SymPy derivation runs only on the first call; later calls are no-ops guarded by the model’s once-only sentinel. Returns whether the model now carries a complete analytical Jacobian (
Falseif it fell back to finite differences, or was already FD / all-Elementary with the closed-form C++ Jacobian).This is the lazy-derivation entry point. The Jacobian is consumed only by ODE solves (CVODE’s dense Jacobian, the steady-state Newton solver, and codegen’s analytical-Jacobian emitter), so it is deferred off the model- load path (
from_sbml/from_netno longer derive) and triggered at ODE-solve setup. Call it directly to warm a parent template beforeclone()fan-out: a warmed parent passes the derived terms to clones (which re-compile the derivative ExprTk strings with no SymPy), so parallel fitting derives once, not once per worker.- Return type:
- property analytical_jacobian_status: str¶
Whether the model carries a complete analytical Jacobian, in words.
"complete"when every reaction’s derivative is closed-form — a mass-action model from the start, a model with functional rate laws onceprepare_analytical_jacobian()attached them;"pending"while a model with functional rate laws has not derived them yet (an ODE solve, or that call, derives them); and"declined: <reason>"when it runs on the finite-difference Jacobian, with the reason the derivation gave — the rate law and what stopped it, the derivation budget, the C++ attach gate’s verdict (the entry it refused, with the probe state and the analytical and finite-difference values; issue #534), or the environment switch. The same reason goes to thebngsimlogger at INFO when it happens; this is the readable form a harness can record instead of only the booleanbngsim._bngsim_core.NetworkModel.analytical_jacobian_complete(issue #506).
- property analytical_jacobian_decline: dict | None¶
Why the model runs on the finite-difference Jacobian, as a stable category a census can group by (issue #538).
Nonewhileanalytical_jacobian_statusis"complete"or"pending". Otherwise a dict withkind, one ofbngsim._jacobian.DECLINE_KINDS, andreason, the status’s text after"declined: ". For the C++ attach gate’s kinds (fd_mismatch,nonfinite_entry, …) it also carries the fields that kind’s verdict sets — ofrxn_idx,per_observable,target_idx,row,col,probe,analytical,finite_difference,n_nonfiniteanddetail— and the speciesrowandcolname asrow_species/col_species. A field the kind does not set is absent, not a placeholder.The kind is what to group by: the reason embeds the rate law, names and values, and its wording changes. A clone carries it, as it carries the status.
- property last_libsbml_parse_sec: float¶
Wall seconds the SBML loader spent in the libSBML parse phase (
readSBML*+ document-level error check).0.0for a non-SBML model (e.g.Model.from_net). SeeSimulator.last_libsbml_parse_sec.
- property last_interpret_sec: float¶
Wall seconds spent interpreting the parsed libSBML document into the internal
_coremodel (excludes libSBML parse, Jacobian derivation, and codegen).0.0for a non-SBML model. SeeSimulator.last_interpret_sec.
- property last_jacobian_sec: float¶
Wall seconds spent symbolically deriving this model’s analytical Functional Jacobian (GH #76).
0.0until the derivation runs (it is lazy since GH #145, and never runs on the SSA/PSA/NFsim paths). SeeSimulator.last_jacobian_sec.
- clone()[source]¶
Deep copy the model for parallel workers.
Each clone is fully independent — it has its own parameter values, species concentrations, and expression evaluator state. (What a model and its clones do hold together is a memo of analyses of their functions, which are the same for all of them: read-only once made, and kept under a lock.)
This is also the answer to “give me my own copy” in general: all of a
Model’s state lives behind one handle into the compiled extension, so neithercopy.copynorcopy.deepcopycan produce an independent model, and both refuse rather than hand back an alias (issue #643).- Returns:
An independent deep copy.
- Return type:
- validate_for_ssa()[source]¶
Return SSA-compatibility issues detected by the SBML loader.
- Returns:
One entry per detected construct; empty for SSA-clean models and for models loaded outside the SBML path (
Model.from_net, builder).- Return type:
See also
bngsim.validate_for_ssamodule-level function with the same body.
- set_param(name, value, *, force_override=False)[source]¶
Set a parameter value by name.
- Parameters:
name (
str) – Parameter name (e.g. “kf”, “Km”).value (
float) – New value.force_override (
bool) – Pin a derived parameter tovalueregardless of whether its expression currently produces that value, and permanently for this model object — neitherreset(),clone(), nor a later write lifts it. The default (False) overrides only while the value differs from the expression’s, which is what makes an ordinary write round-trip (issue #188, see Notes). Use this only when the caller’s contract is “this parameter is an independent input”; the one such caller in bngsim is the functionbngsim.jax.differentiable_solve()withflat=True. Has no effect on a parameter that has no defining expression.
- Raises:
ParameterError – If the parameter name is not found, or if
nameis one offrozen_paramsandvaluediffers from the value it holds.ValueError – If
nameis an SBML compartment size this model cannot resolve to a live volume, or a synthesized slot, andvaluediffers from the value it holds. See Notes.
- Return type:
Notes
A compartment’s value is folded at load into constants — per-species volume factors, amount-declared initial conditions, mass-action rate constants, SSA propensity volumes, the emitted RHS, and every
<initialAssignment>that reads it. Issue #170 put the volume back into each fold it can reach, so most sizes are now ordinary writable parameters; the residue is refused by name rather than half-honoured, because honoring only some folds leaves the model internally inconsistent rather than moving the volume. They are listed inunwritable_compartment_size_params; load at the size you want instead (from_sbml()acceptscompartment_sizes={...}). Writing the value it already holds is allowed, so round-tripping a full parameter vector still works.A synthesized slot —
param_is_internal— is refused on the same unchanged-write rule. One of those is a function’s name: the engine keeps the function’s evaluated value in a parameter slot and rewrites it from the function’s own expression before every derivative evaluation, so a write here is discarded at the next one. It used to be accepted —get_param()echoed the new value back and the trajectory did not move (issue #227). Write the parameters the function’s expression reads instead.Writing a parameter also re-derives every expression-valued parameter that reads it (BNG
setParametersemantics), and — issue #79 — every species initial condition that names one of them.A() Stotin a.netspecies block, or an SBMLinitialAssignmentthat is a bare<ci>, declares that species’ initial condition to be the parameter, so a dose scan over a total amount moves it:>>> model.set_param("Stot", 1e6) >>> model.get_state()[0] 1000000.0
The declared initial condition always follows, so
reset()rebuilds from current parameter values rather than from a snapshot taken at load. The live concentration follows only while the species is still sitting on that initial condition: a species aSimulator.run()has advanced, or thatset_concentration()has assigned, keeps its value and picks the new initial condition up at the nextreset().Two cases where the write deliberately does not reach an initial condition: after
save_concentrations()with no label (the baseline is now a captured state, which the declared initial condition no longer describes — dose such a protocol withset_concentration()), and for an SBMLinitialAssignmenttoo complex to be a single parameter reference (2*init_X + offset), which is evaluated once at load.Writing a derived parameter — one of the symbols in
param_is_expression,dind = d__FREEor a loader-synthesized_rateLaw{N}— overrides its expression rather than moving it:dstops trackingd__FREE, which is BNG’ssetParametersemantics. The override is keyed on the value and so is reversible (issue #188): writing back the value the expression produces re-attaches it, and a write of the value the parameter already holds is not an override at all, soset_params(dict(zip(param_names, vec)))round-trips a full parameter vector unchanged. Read the current state offparam_is_expression, which goesFalsefor the duration of an override whileparam_expressionskeeps the defining expression.An override is a structural change and is meant to be: a derived parameter pinned to a literal no longer carries the chain rule from the primaries underneath it, so those primaries lose that reaction’s term from their sensitivity columns, and the generated RHS changes to match. That is the correct derivative of the model you asked for — but it is why an accidental override used to be so hard to see, and why the round-trip above is the one to rely on.
- property frozen_params: list[str]¶
Parameters that were folded to a number when the model was built (issues #313, #695, #696, #711).
An SBML document may write a compartment’s size, a stoichiometry or a conversion factor over its parameters, or set an initial value, of a parameter or of a species, by an initialAssignment that bngsim cannot keep symbolic. Each is evaluated once, at load, and the model holds the number. Every parameter that number was read from is listed: the ones the expression names, and the ones that gave each symbol it names its value at load (through another assignment, a rule, a reaction’s kinetic law, a species’ initial value or its compartment’s size). So is what sized a compartment when a species’ declared value was converted with it: a species declared in the unit it is not held in, in a compartment that a rate rule or an event resizes and an initialAssignment starts, or that an assignment rule sizes (issue #742; every name such a rule reads).
A write to one does not move what it was folded into, so
set_param()refuses to change one, and a forward-sensitivity column for one is refused, where both used to be silent: the write took the value and left the number, and the column lacked everything that goes through it (an exact 0 where nothing else reads the parameter). Edit the document and load it again to move one.A compartmental BNGL model has them too (issue #711). BNG2.pl writes each reaction’s volume factor into its rate constant as a number,
0.1*kbfor1/VeatVe = 10, and leaves the expression in a comment,unit_conversion=1/Ve. Every parameter such a comment names is listed, with what a derived parameter or a function among them reads: the network holds the number, and it is the BNGL source that has to change.Empty for most models.
- property compartment_size_params: list[str]¶
Names of the parameters that are SBML compartment sizes.
set_param()writes these (issue #170): the storage convention a volume decides — the amount↔concentration conversion, an amount-declared initial condition, the mass-action scalar’sΠ V^n, the SSA propensity volume — is re-derived from the parameter rather than left at the size the model happened to load at. What they are still not is differentiable: forward sensitivity refuses ad/dVcolumn (issue #170 stage 3), which is why this list is worth having.A handful cannot be written even so; see
unwritable_compartment_size_params.Empty for
.netmodels, and for any compartment the SBML loader promoted to a species (rate-rule or event-resized): that one is live state, written withset_concentration(), not a parameter.Examples
>>> model = bngsim.Model.from_sbml("pbpk.xml") >>> gradient_free = [p for p in model.param_names ... if p not in set(model.compartment_size_params)]
- property unwritable_compartment_size_params: list[str]¶
Compartment sizes
set_param()still refuses to change (#170).The residue of issue #164’s blanket refusal: a compartment whose size an assignment rule recomputes every step (a write would not survive the next evaluation); one whose storage divide a single mass-action scalar shares across two compartments that merely happen to have the same load-time size (that scalar stops being exact the moment they differ); and one an initialAssignment folds into a quantity no parameter holds — a species amount, a reaction rate — since SBML evaluates every such expression once, at load, against the sizes. All three are decided at load and named in the error.
A subset of
compartment_size_params, and usually empty — reload withModel.from_sbml(..., compartment_sizes={...})to move one.
- get_param(name)[source]¶
Get a parameter value by name.
- Parameters:
name (
str) – Parameter name.- Returns:
Current value.
- Return type:
- Raises:
ParameterError – If the parameter name is not found.
- set_params(params)[source]¶
Set multiple parameters from a dict.
- Parameters:
- Raises:
ParameterError – If any parameter name is not found, or any value cannot be converted to float. Atomic: either all succeed or none do.
ValueError – If any entry is an SBML compartment size being changed (issue #164 — see
set_param()). Checked in the validation phase with the names and the values, so the atomicity above holds: a dict with one compartment write in it applies none of its entries.
- Return type:
Examples
>>> model.set_params({"kf": 0.5, "kr": 0.1})
Notes
Each write goes through the same path as
set_param(), so a parameter that names a species initial condition re-resolves it here too (issue #79).
- reset()[source]¶
Reset all species to their initial concentrations.
Parameter values are not reset — only species concentrations.
Equivalent to
restore_concentrations()with no label: it returns to the seed initial conditions, or — after an unlabeledsave_concentrations()— to that saved snapshot. Named snapshots (save_concentrations(label=...)) are unaffected.The seed initial conditions are the ones the current parameter values imply, not the ones the network was generated with: a species whose initial condition names a parameter (
A() Stot) returns to that parameter’s value as of now (issue #79).- Return type:
- save_concentrations(label=None)[source]¶
Snapshot the current species concentrations for later restore.
Implements BNG
saveConcentrations()/saveConcentrations("name").- Parameters:
label (
str|None) – Name for the snapshot. When omitted (orNone), this preserves the historical single-slot behavior: the current concentrations become the new baseline initial state, so a subsequentreset()(orrestore_concentrations()with no label) returns here rather than to the original.netseed. When alabelis given, the snapshot is stored under that name in a separate multi-slot store and does not disturb the default slot; restore it later withrestore_concentrations(label). Multiple named states coexist, so a multi-phase protocol (e.g.saveConcentrations("t=0")…saveConcentrations("start_competition")) round-trips faithfully.- Return type:
Notes
A named snapshot captures only the species concentrations (the bulk state vector, ordered like
species_names); parameters and the current time are not part of it, matching BNGresetConcentrations.A snapshot taken of a pre-equilibrated state also captures that state’s forward-sensitivity matrix
dx/dθwhen one is pending, sorestore_concentrations()puts the θ-derivative back with the concentrations (issue #81). Without it, a restored equilibrated state would be re-seeded as a fresh start (∂x(0)/∂θ = 0) — wrong, not approximate, since the restored initial condition is a function of θ. The unlabeled form hands the derivative to the new IC baseline instead, soreset()restores it too.Saving does not change whether a run advanced the state. A state a run advanced without tracking
dx/dθis stillx(θ)with no derivative to carry, so a sensitivity run from it raises, from the live state, from the baseline afterreset(), and from a restored snapshot alike. Run the equilibration with thesensitivity_paramsand passcarry_sensitivities=Trueto measure from it. It used to start fromdx(0)/dθ = 0, with no error (issue #1528).
- restore_concentrations(label=None)[source]¶
Restore species concentrations from a saved snapshot.
Implements BNG
resetConcentrations()/resetConcentrations("name").A snapshot saved with a forward-sensitivity seed (a pre-equilibrated state — see
save_concentrations()) restores thatdx/dθalong with the concentrations, so a followingrun(carry_sensitivities=True)measures from the right seed (issue #81). A snapshot of a state a run advanced restores its carried-over mark too, seed or not, so a sensitivity run from it raises without one (issue #1528). Restoring a snapshot never clears a mark the model already carries, asset_state()does not. Like the state a run left, a restored snapshot of an advanced state is not re-dosed by a laterset_param()of an initial-condition parameter; dose it withset_concentration().- Parameters:
label (
str|None) – Name of the snapshot to restore. When omitted (orNone), restores the default slot — identical toreset()(the seed initial conditions, or the last unlabeledsave_concentrations()). When alabelis given, restores the named snapshot saved bysave_concentrations(label).- Raises:
ModelError – If
labelis given but no snapshot was saved under that name.- Return type:
- has_saved_concentrations(label=None)[source]¶
Whether a named concentration snapshot is available to restore.
- property saved_concentration_labels: list[str]¶
Sorted names of the currently saved named concentration snapshots.
Does not include the default (unlabeled) slot, which is restored via
reset()/restore_concentrations()with no label.
- set_concentration(name, value)[source]¶
Set a single species concentration by name.
- Parameters:
- Raises:
ModelError – If the species name is not found.
- Return type:
Notes
Implements BNG
setConcentration("name", value)action.The value is the start of the next
Simulator.run(), of every replicate ofSimulator.run_replicates(), and ofSimulator.parameter_scan(), whose default reset target is the model’s state at the call.reset()does not keep it, and neither doSimulator.run_batch()(issue #1362),Simulator.steady_state_batch()withoutstates=(issue #1521) orbngsim.jax.differentiable_solve, which start every row from the initial conditions and warn that they do. Callsave_concentrations()after it to make it the statereset()returns to.For forward sensitivity, this is a literal initial condition:
∂x_k(0)/∂θ = 0. It therefore supersedes whatever the species’ declared initial condition depended on — if the.netset it from a parameter (R() R0), that parameter no longer reaches this species’ IC and its seed row is dropped (issue #113). When the value you assign does depend on a differentiated parameter, say so withdeclare_ic_sensitivity(). On a state a run advanced, the other species are stillx(θ): a sensitivity run from it raises, and saving it does not change that (issue #1528).
- get_concentration(name)[source]¶
Get a single species concentration by name.
- Parameters:
name (
str) – Species name, a fixed species’$given or left out, as forset_concentration().- Returns:
Current concentration. For an assignment-rule species after a run, the value the run reports for its last row, in the model’s storage convention (amount over the compartment’s static size), before a variable-volume
Resultrescales it to the live size (issue #1510).Simulator.steady_state()writes no species, so it leaves a rule species as the last run did.- Return type:
- Raises:
ModelError – If the species name is not found.
- get_state()[source]¶
Bulk-copy the full live species-concentration vector (GH #102).
Returns a fresh
float64array of lengthn_species, ordered likespecies_names. This is the low-overhead per-step state-exchange primitive for driving bngsim as a reaction kernel from an external orchestrator (e.g. a hybrid SSA/ODE splitting loop): one Python call marshals the entire state, so per-step exchange cost stays negligible next to the ODE solve even at ~100K species.See also
set_statethe inverse bulk assignment.
species_namesthe ordering of the returned vector.
- Return type:
- set_state(state)[source]¶
Bulk-assign the full live species-concentration vector (GH #102).
- Parameters:
state (
ndarray) – 1-D array of lengthn_species, ordered likespecies_names. Copied into the model’s live concentrations; observables and other derived state are recomputed on the next RHS or observable evaluation.- Raises:
ValueError – If
stateis not 1-D or its length differs fromn_species.- Return type:
- declare_ic_sensitivity(sens)[source]¶
Declare
∂x_k(0)/∂θfor initial conditions a per-point hook assigns.Call this from a
Simulator.parameter_scan()on_pointhook when the value it assigns is computed from a parameter being differentiated — a ligand dose converted with a fitted volume, say. The scan then uses the declared row verbatim instead of measuring it through the hook, which is both exact and cheaper (a declared species is not probed).- Parameters:
sens (
Mapping[str,Mapping[str,float]]) –{species_name: {param_name: d_ic_d_param}}. A declared species’ row is taken as fully specified: parameters not named get∂x_k(0)/∂θ = 0. Declaring{}for a species therefore pins its whole row to zero (an explicitly θ-independent literal). Repeated calls merge per species; a species declared twice keeps the last row.- Raises:
ModelError – If a species or parameter name is unknown.
- Return type:
Notes
Also honoured by a plain
Simulator.run()— an initial condition assigned by hand is no longer described by the.netexpression the parameter-graph seeding (issue #43) differentiates, so for a hand-assigned θ-dependent IC this declaration is the only way the engine can know∂x_k(0)/∂θ.A declaration lasts until the initial condition it describes changes: assigning the same species again (
set_concentration()), or a wholesalereset()/set_state(), drops it. The scan primitive additionally clears declarations before eachon_pointcall, so a hook declares afresh per point and points stay independent.Nothing needs declaring for the ordinary dose: a literal
set_concentrationinside anon_pointhook has∂x_k(0)/∂θ = 0, which the scan measures on its own.Examples
>>> def on_point(model, dose_nM): # dose in molecules, fitted volume ... v = dose_nM * 1e-9 * NA * model.get_param("Vecf") ... model.set_concentration("L(r)", v) ... model.declare_ic_sensitivity({"L(r)": {"Vecf": v / model.get_param("Vecf")}})
- effective_ic_sensitivity(params=None)[source]¶
∂x(0)/∂θ— the initial-condition seeding a run from this state would use.The reader paired with
declare_ic_sensitivity(), and the single defining site for the seeding the forward-sensitivity solver is initialized with. Answers from model structure alone — the parameter graph, the live initial conditions and any declarations — with no integration and no simulation, so a frontend can build its gradient routing once at setup (issue #155).Why a consumer needs it:
Result.output_sensitivities()withaxis="parameter"is a total derivative,d_param[θ] = (right-hand-side path) + Σ_k (∂x_k(0)/∂θ)·d_ic[x_k]
so the seeding is already inside the
parameteraxis. A caller that routes a fitted parameter to several columns and sums them must add anic-axis term only for the part of∂x(0)/∂θreported here as absent — anything present is carried already, and adding it again counts it twice.This is the effective matrix, not the model file’s: a species retired because an assignment moved it off the initial condition its expression describes (issue #113) is gone, and a species the caller declared (issue #111) carries the declared row in place of the parameter-graph one. It is therefore state-dependent by design — call it on the configured model, before the run whose gradients it describes.
- Parameters:
params (
Iterable[str] |None) – Restrict to these parameter names. Default: every parameter. Pass the same list assensitivity_params=to see exactly the rows that run’sparametercolumns will carry.- Returns:
{species_name: {param_name: ∂x_k(0)/∂θ}}, keyed by the namesspecies_names/param_namesreport — the same idssensitivity_params=accepts. A compound<initialAssignment>lowered by issue #147 reports the original symbols it was written over, never the synthetic_ic_<species>carrier.A present entry whose value is
0.0means seeded, and the coefficient is zero at this state — a chain-rule factor that vanishes here but need not at another point. An absent entry means no seeding path at all, and is the one that says “this parameter’s initial-condition term is yours to supply”. The two are different answers; do not collapse them.- Return type:
Examples
>>> model.effective_ic_sensitivity(["R0", "kf"]) {'R(r)': {'R0': 1.0}}
- property function_names: list[str]¶
Names of the model’s functions, in declaration order.
A function is not a parameter, even though each one also names a parameter slot — where the engine stores the value it last evaluated to, rewritten before every derivative evaluation.
param_is_internalflags those slots andprimary_param_namesomits them (issue #227).
- property conservation_laws: dict¶
Conservation laws detected from the stoichiometry matrix.
Found for every input format (
.net, Antimony, SBML, programmaticModelBuilder). The returned dict has keys:n_laws: number of independent conservation lawsn_species: number of species the laws are expressed overdependent/independent: 0-based species index listsconstants: conservation constants evaluated from the initial conditionscoefficients:n_lawsxn_speciescoefficient matrix
Each law is over the stored state,
coefficients[k] @ yconstant along a run, socoefficients @ rhs(y)is zero at anyy. What a reaction between compartments of different size conserves is an amount, so a species’ coefficient there carries its compartment’s size:Ain a compartment of size 1 exchanged withBin one of size 2 givesA + 2*B(issue #758). Such a model’s laws follow a size written withset_param(). A species whose share of such a reaction is divided by a size that moves, a concentration in a compartment that a rate rule or an event resizes, is in no law.Consumed internally by the reduced-space Newton steady-state solver, which needs the independent subspace to sidestep the rank-deficient Jacobian these laws imply.
- stoichiometry_matrix(*, sparse=False)[source]¶
The net stoichiometry matrix
S, shape(n_species, n_reactions).S[i, r]is the net change in speciesiwhen reactionrfires: products positive, reactants negative, soA + A -> Bputs-2in A’s row. 0-based, inspecies_namesand reaction-index order. For a mass-action.netmodelrhs(y) == S @ v(y)for the vector of ODE reaction rates, and every conservation law satisfiesL @ S == 0— this is the matrixconservation_lawswas row-reduced from. (An SBML model with a reaction between compartments of different size is the exception: its laws are found for the amounts, and satisfyL @ rhs(y) == 0and notL @ S == 0.)A
$-prefixed boundary species has an all-zero row: the RHS zeroes its derivative and an SSA firing never updates it, so the matrix that describes what the engines do has no entry there, whatever the reaction line says.- Parameters:
sparse (
bool) –False(default) returns a dense float64ndarray.Truereturns ascipy.sparse.csc_arrayof the same shape, which is the form to ask for on a large network — the dense matrix of a 10,000-species, 100,000-reaction model is 8 GB. Needs scipy.
Notes
The 1-based C++ entry list is converted once per model and cached; each call assembles a fresh matrix from it, so the caller may modify what it gets back.
- rhs(y, t=0.0)[source]¶
dy/dtat stateyand timet, under the live parameters.The same interpreted RHS the CVODE callback evaluates, with the same observable-and-function refresh it runs — observable totals and function-bound rate parameters are recomputed at
yfor a model that has functions and skipped for a pure mass-action one (therhs_evaluates_observablesgate) — against the model’s currentget_param()values. A$-fixed species reports a zero derivative, as it does in a solve.The model’s stored concentrations (
get_state()) are not read and not written; only the caller’syenters. What does change is the model’s cached observable and function values, exactly as one integrator step would change them.- Parameters:
y (array_like, shape
(n_species,)) – The state to evaluate at, inspecies_namesorder.t (
float) – The timerateOf,time-dependent functions and table functions read.0.0by default.
- Returns:
f(t, y), float64.- Return type:
- Raises:
ValueError – If
yis not 1-D or its length is notn_species.ModelError – If rates of change are written in terms of each other’s
rateOfin a ring (an algebraic loop, which has no right-hand side).
See also
jacobian∂f/∂yat the same point.propensitiesthe SSA-convention reaction rates at
y.
- jacobian(y, t=0.0, *, sparse=False)[source]¶
∂f/∂yat stateyand timet— the ODE Jacobian.J[i, j] = ∂f_i/∂x_j, both indices inspecies_namesorder. Built from the model’s closed-form terms whenever they cover every reaction, and from a one-sided difference quotient ofrhs()otherwise; the result says which. That is the rule the ODE integrator and the steady-state solver apply underjacobian="auto", and the difference quotient is stepped by the steady-state solver’s own rule, so a caller gets the matrix a solve would have factored at that point rather than a third approximation. A model whose analytical Jacobian is partial (a rate law the differentiator declined) is never handed the partial matrix: it is differenced, and reported as such.The first call on a model with functional rate laws derives the closed-form terms (
prepare_analytical_jacobian()), which an ODE solve would do at its own setup; later calls reuse them.Where there is no closed form, the matrix is exact all the same, by dual numbers through the interpreter (issue #1330): each rate law and function as the interpreter evaluates it, differentiated branch by branch, with no step. Only a model the dual evaluation cannot follow (a ring of functions that read each other,
rateOf) is differenced.- Parameters:
y (array_like, shape
(n_species,)) – The state to linearize at.t (
float) – The time, for time-dependent rate laws.0.0by default.sparse (
bool) –False(default) returns aJacobianMatrix, a dense(n_species, n_species)float64ndarraywith asourceattribute.Truereturns ascipy.sparse.csc_arrayover the model’s structural sparsity pattern — the same pattern from either source, so a consumer can allocate for it once — with the samesourceattribute set on it. Needs scipy.
- Returns:
.sourceis"analytical","automatic-differentiation"or"finite-difference", the spellingsbngsim.SteadyStateResult.sens_jacobian_sourceuses for the same three cases.- Return type:
- Raises:
ValueError – If
yis not 1-D or its length is notn_species.ModelError – If rates of change are written in terms of each other’s
rateOfin a ring (an algebraic loop, which has no right-hand side).SimulationError – If the matrix is exact by dual numbers and
ysits where a rate law changes branch (if(X > c, …)at X = c,abs(X)ormax(X, 0)at X = 0, a step offloor()) such that an entry is not the same from the two sides (issue #1330): the Jacobian has no value there. Or if the matrix is a difference quotient whose step decides an entry (issue #1002): the entry moves by more than 1e-4 of itself (1e-10 of its column’s largest, at least) when its column is taken again at the species’ own step or at half the first.
- propensities(y, t=0.0)[source]¶
Every reaction’s SSA propensity at state
yand timet.One entry per reaction, in reaction-index order: the amount/time propensity a stochastic step samples from, in the SSA volume convention rather than the ODE one. Two things differ from the ODE rate of the same reaction. A repeated reactant takes the falling factorial over its molecule count
nwhen bngsim reads the reaction as mass action —A + A -> Bfires atk·n(n−1)/2, where the ODE rate isk·n²/2, withn = x·Vfor a species stored as a concentration in a compartment of sizeV(issue #692). An SBML law it does not read as mass action is evaluated as written, still with the falling factorial when the law is a product of its species; one that writes its own combinatorics (k*X*(X-1)/2) is left alone — and a reaction in a compartment of volumeVis multiplied byV(Reaction::ssa_volume_factor, or the live value of the compartment-size parameter it names), converting the ODE’s storage-units rate to a per-event rate. For a.netmodel with volume 1 and no repeated reactant the two conventions agree.Observable totals and function-bound parameters are refreshed at
yfirst, as the SSA loop refreshes them before its propensity pass, under the same gaterhs()takes. The stored concentrations are not touched. Values are read fromyas given — pass molecule counts for a count-valued answer.- Parameters:
y (array_like, shape
(n_species,)) – The state to evaluate at, inspecies_namesorder.t (
float) – The time, for time-dependent rate laws.0.0by default.
- Returns:
The propensity vector
a(y), float64.- Return type:
- Raises:
ValueError – If
yis not 1-D or its length is notn_species.
See also
stoichiometry_matrixthe state change each of these reactions makes.
- property param_is_expression: list[bool]¶
Per-parameter
is_expressionflag, parallel toparam_names.Truefor derivedConstantExpressionparameters such as the_rateLaw{N}symbols BNG2.pl emits when a BNGL rate law is a compound expression (e.g.chi*kon). These are not independent knobs — their values are computed from primary parameters and are re-evaluated automatically byset_param().Derived means the value expression references another of the model’s symbols, which is the condition that makes the chain rule necessary and the line BNG2.pl itself draws between
# ConstantExpressionand# Constant. A constant written as arithmetic —gamma 1/7,pi 2*asin(1),c6 ln(2)/120— names nothing, so it differentiates as a leaf exactly like a literal and reportsFalsehere (issue #227). It used to reportTrue, on the narrower reading that the value text does not parse as a float, and the recovery rate ofSIR.netwas consequently missing fromprimary_param_names.This is live state, not a property of the declaration: writing a derived parameter a value its expression does not currently produce overrides the expression and flips its entry to
Falseuntil the override is lifted (issue #188 — seeset_param()).param_expressionskeeps the defining expression throughout, so an entry that isFalsehere while its expression there names another of the model’s symbols is exactly an overridden derived parameter, as distinct from a genuine primary, whose text is a number (2e7,1/7) or empty. That pairing is also the whole of what the generated C source depends on: an override moves the chain rule, and nothing else about a parameter write does.
- property param_expressions: list[str]¶
Per-parameter defining text, parallel to
param_names.For a derived parameter (
param_is_expression), the expression it is re-evaluated from, such as1/S0. It stays on record while aset_param()override pins the parameter to a literal, which is how an overridden derived parameter is told from a genuine primary.Anything else has a number or nothing here: a primary read from a
.netfile keeps its value as the file wrote it (2e7,1/7), and most SBML primaries and every synthesized slot (param_is_internal) have"". This is the declaration, which a write does not change, so read a parameter’s value withget_param().
- property param_is_internal: list[bool]¶
Per-parameter synthesized-slot flag, parallel to
param_names.Truefor a slot bngsim created for its own bookkeeping rather than one the model declared. Two kinds, and neither is a knob — each is left out ofprimary_param_namesand refused a value-changingset_param():_V0_<comp>(issue #170) — an SBML compartment’s size as it was at load, which the rate constants in that compartment are normalised against. Moving it rescales those rates while the volume stays put.a function’s backing slot (issue #227) — where the engine stores what the function last evaluated to. Every function that does not already name a declared parameter has one, so every model with a
functionsblock carries these.
Distinct from
param_is_expression: a derived parameter is recomputed from primaries, these are not primaries in the first place. The two flags are disjoint in practice, andprimary_param_namesisparam_namesminus both.
- property primary_param_names: list[str]¶
List of parameter names that are the model’s independent knobs.
These are the genuine knobs of the model — primary rate constants, initial-condition parameters, etc. Use this when you want to expose the model to an external optimizer or sampler that should treat each parameter as an independent variable; varying a primary via
set_param()automatically propagates to derived parameters. Leave outfrozen_paramswhere a model has any: those are listed here, and a write that changes one is refused.Two kinds are left out, one per flag. A derived
ConstantExpression(param_is_expression, e.g._rateLaw{N}) is recomputed from its primaries, so it is not independent of them. And a synthesized slot (param_is_internal) is not a parameter at all:_V0_<comp>, an SBML compartment’s size as it was at load, which the rate constants in that compartment are normalised against — moving it would rescale those rates without moving the volume, so set the compartment size itself, an ordinary writable parameter since issue #170 — or the slot holding a function’s evaluated value, which the engine overwrites before every derivative evaluation (issue #227).Both exclusions are about the same failure: a coordinate an optimizer cannot move. Issue #227 is where this list stopped being either. It listed every function name — 327 of 327 function-carrying
.netmodels leaked them — sojax.gradspent a coordinate on one and got exactly0.0, forever. And it dropped a constant written as arithmetic (gamma 1/7), which is a working knob by every other measure, so a fit over this list held the recovery rate ofSIR.netfixed with no warning.
- pure_sink_species()[source]¶
Names of the write-only accumulator species in this network.
A pure sink is a species this network only ever writes to — the
degraded/produced/secretedpool a BNGL model carries to count cumulative flux. Its derivative is a non-zero constant for as long as its producing reactions fire, so||f(y)||_2 / n_specieshas a floor abovetolandSimulator.steady_state()reports failure however long it integrates — even when every other species has settled. These are the species to hand (negated) tosteady_state(mask=...).Detection is purely structural — nothing is measured, no annotation is needed. A species qualifies when all of the following hold:
it is a product of at least one reaction,
it is a reactant of none,
no species’ derivative depends on it, and
it is not a
$-prefixed (fixed / boundary-condition) species.
Clause 3 is not implied by clauses 1-2 and is what makes excluding the species provably harmless to the rest of the system: an Elementary rate law reads only its reactants, but a Functional one reads observables, so a product-only species named in an observable that a rate law consumes still feeds back into the dynamics. Clause 4 drops the boundary conditions, whose derivative is zeroed anyway, so they never hold the residual up.
What this does not claim is that the returned species are the only obstacle to convergence, or that a species not returned is settled. It answers exactly one question: which species can be dropped from the convergence test without changing the problem the remaining ones solve.
Examples
>>> model.pure_sink_species() ['Ad()'] >>> ss = sim.steady_state(mask=~model.is_pure_sink()) >>> ss.converged True
- is_pure_sink()[source]¶
Boolean mask of the pure-sink species, shape
(n_species,).The array form of
pure_sink_species(), so the convergence-test mask is a negation:ss = sim.steady_state(mask=~model.is_pure_sink())
- Returns:
Truewhere the species is a write-only accumulator.- Return type:
ndarray of bool
- add_table_function(name, *, file=None, times=None, values=None, index='time', method=None)[source]¶
Give a table function of the model new data, or add a table function.
Under the name of a table function the model has (
table_function_names: atfunthe.netdeclares, or atfunwritten inside a function, registered as<function>__tfun<k>), the table takes the new data in place, and every engine reads it from then on: the interpreter, the compiled code, the SSA and the steady-state solvers, and aSimulatormade before the call. The new data has to be read over the same index as the table’s, since that is compiled into the model’s code. It keeps the table’s interpolation unlessmethodnames another. A clone keeps the data it was cloned with.Under a new name the table is added, but nothing in the model reads it: a loaded model’s expressions are fixed, and none of them can name a table the model’s source did not declare. A
UserWarningsays so, and the table changes no run.- Parameters:
name (
str) – Table function name (e.g.,"cumNcases").file (
str|Path|None) – Path to a.tfunfile. Mutually exclusive withtimes/values.times (
list[float] |None) – X (index) values. Must be used withvalues.values (
list[float] |None) – Y (function) values. Must be used withtimes.index (
str) – Index variable name. Default"time". Can also be the name of a parameter, an observable or a function, for a table not read on the clock. New data for a table has to name the table’s own index.method (
str|None) – Interpolation method,"linear"or"step".None(the default) keeps the table’s own for new data, as a.tfunfile carries none, and is"linear"for a new table.
- Raises:
ModelError – If the file cannot be read or data is invalid; if
nameis the name of a function of the model that is not a table, whose body a table cannot replace; or if the model’s table of that name is read over another index (issue #772). The model is left as it was.ValueError – If arguments are inconsistent (e.g., both
fileandtimes).
- Return type:
Examples
>>> model.add_table_function("cumNcases", file="case_data.tfun") >>> model.add_table_function("response", file="dose.tfun", index="drug_conc") >>> model.add_table_function("drive", times=[0, 1, 2], values=[0, 5, 10])
- class bngsim.Simulator(model, method='ode', *, xml_path='', poplevel=None, gml=None, connectivity=None, nfsim_v1143_compat=False, block_same_complex_binding=True, traversal_limit=None, jacobian='auto', force_dense_linear_solver=False, force_sparse_linear_solver=False, codegen=None, net_path='', sensitivity_params=None, sensitivity_ic=None, sensitivity_method='staggered', strict_ssa=True, reaction_stats=False)[source]¶
Bases:
objectUnified simulation interface for ODE, SSA, PSA, and network-free methods.
- Parameters:
model (
Model) – The model to simulate.method (
str) –Simulation method:
Deterministic / network-based:
"ode"— CVODE adaptive BDF integrator (deterministic)"ssa"— Variant of Gillespie’s direct method (exact stochastic)"psa"— Partial Scaling Algorithm (approximate stochastic). Lin, Feng, Hlavacek, J. Chem. Phys. 150, 244101 (2019). Requirespoplevelkeyword argument.
Network-free (canonical tokens):
"nf"— Network-free simulation (default policy; currently routes tonf_reject)."nf_reject"— Rejection/null-event algorithm (NFsim-style). Requiresxml_pathkeyword argument."nf_exact"— Exact non-local network-free algorithm (RuleMonkey). Requiresxml_pathkeyword argument."nf_fixed"— Legacy compatibility token. Recognized but unavailable in this build.
Legacy aliases (accepted for compatibility):
"nfsim"→"nf_reject""rulemonkey"/"rm"→"nf_exact""dynstoc"/"ds"→ unavailable compatibility alias
xml_path (
str)poplevel (
float|None) – Critical population size N_c for PSA. Required whenmethod="psa". Must be > 1. Larger values are more conservative (less acceleration, less approximation error). Typical values: 100–1000.connectivity (
bool|None) – Only used formethod="nf"/"nf_reject"/"nfsim". Controls NFsim’s reaction-connectivity optimization at XML initialization.Falseuses the conservative full membership update path;Trueenables the inferred dependency-graph path. If omitted, the underlying NFsim wrapper default is used (currentlyFalse).nfsim_v1143_compat (
bool) – Only used formethod="nf"/"nf_reject"/"nfsim". When true, preserve NFsim v1.14.3’s extra selector draw for same-seed trajectory compatibility with the standalone CLI.block_same_complex_binding (
bool) – Used formethod="nf"/"nf_reject"/"nfsim"andmethod="rm"/"rulemonkey". WhenTrue(the default), a bimolecular rule cannot bind two molecules of one complex: BNGL’s semantics, as BNG2.pl’s network applies them.Falseallows it. With the NFsim methods, a rule whose products are written apart (A.B -> A + B) and that deletes a bond or a molecule does not fire where the molecules it acts on would stay in one complex across products, whatever this setting (seeNfsimSessionfor the cases not yet tested), so withFalsea gelling model can run far slower: it fills with ring bonds such a rule may never break, and each attempt on one is refused. Withmethod="rm",Falsealso stops RuleMonkey making that test, so on a model with rings such a rule can break a bond inside a ring, which gives wrong counts with no error. RuleMonkey 3.13.0 deprecatesFalsewithmethod="rm": it prints a warning to stderr the first time in a process, and RuleMonkey 4.0 removes the mode. Complex bookkeeping forSpecies-typed observables is enabled whenever the model declares one, whatever this setting.traversal_limit (
int|None) – Only used formethod="nf"/"nf_reject"/"nfsim". NFsim-utl N: universal traversal limit.None(default) lets NFsim auto-compute a suggested limit from the XML.jacobian (
str)For
method="ode": when true, use a compiled C RHS, generated from the model bngsim built – whatever it was loaded from (.net, BNGL, SBML, Antimony or the builder).None(default) compiles automatically at or aboveBNGSIM_CODEGEN_THRESHOLDspecies (256), where native code beats the interpreter.Falseruns the interpreter: it never compiles, and it ignores an artifact that an earlier Simulator orModel.clone()left on the model.For
method="ssa": unlessFalse, a small pure mass-action model with no events gets a compiled propensity vector and the recompute-all loop, reported asssa_diagnostics["propensity_backend"].Falseruns the interpreted propensities, on every engine the Simulator builds,run_replicatesandrun_batchincluded (issue #1213). Other methods ignore it.net_path (
str) – Deprecated and ignored (issue #803). Every backend reads the model it is given – codegen since step 3,jacobian="jax"since step 4 – so passing it warns and changes nothing.sensitivity_params (
list[str] |None) –Parameter names to integrate forward sensitivities for, alongside the state ODEs. The result then carries a
(n_times, n_species, n_params)sensitivitiestensor whose(t, i, k)entry is∂x_i(t) / ∂p_kevaluated at the baseline parameter values. Only valid formethod="ode".Each name must be a coordinate some write can move. A slot a model function owns — a
functionsblock entry, or an SBML<parameter>an<assignmentRule>defines — is not one: the engine rewrites it from the function’s own expression before every derivative evaluation, so its column can only be identically zero. Naming one raisesValueErrorrather than returning that zero (issue #329); differentiate the parameters the function’s expression reads instead.Model.primary_param_namesomits them.sensitivity_ic (
list[str] |None) – Species names to integrate forward initial-condition sensitivities for. The result carries a(n_times, n_species, n_ic)sensitivities_ictensor whose(t, i, k)entry is∂x_i(t) / ∂x_k(0). Useful when fitting IC parameters via chain rule from a Python-side reparameterization (e.g.,model.set_concentration("Epo", 10**theta)) without a corresponding model parameter to differentiate against. Requires the codegen sensitivity RHS path; codegen is auto-enabled for any sensitivity workflow. Only valid formethod="ode".reaction_stats (
bool) –method="ssa"only. Record, at every output time, each reaction’s cumulative firing count and integrated propensity — the two accumulators a likelihood-ratio (Girsanov) parameter gradient of an ensemble is built from — onResult.reaction_firing_countsandResult.reaction_propensity_integrals(bngsim.girsanovturns them into scores and gradients). DefaultFalse; enabling it leaves every trajectory byte-identical. Refused for PSA, whose scaled channels are not the exact process, and for ODE and network-free runs, which have no reaction channels (GH #616).strict_ssa (
bool) –Only used for
method="ssa"/"psa". DefaultTrue.SBML loader records SSA-compatibility issues at load time (e.g.
reversible_non_mass_action,assignment_rule_on_reactant). WhenTruethe Simulator raisesSsaValidationErroron any error-severity issue — this is the safe default that prevents fitting workflows from silently consuming wrong dynamics on broken-under-SSA constructs.Pass
Falseto downgrade most error-severity issues to warnings (logged vialogging) and let the Simulator construct anyway. This mirrors roadrunner’s warn-and-run behavior undergillespieintegration. Useful when comparing bngsim against roadrunner on the same model, or when the user understands that the dynamics under SSA will be approximate for these constructs.Two issue codes remain non-overridable even with
strict_ssa=False:non_integer_stoichiometry(SSA requires ±1 fire deltas) andfast_reaction(no fast-equilibrium constraint solver).sensitivity_method (
str) –CVODES corrector strategy for the coupled state + sensitivity system. Both modes integrate state and all sensitivity ODEs as one extended ODE in a single CVODES pass; they differ only in how each integration step’s nonlinear solve is structured:
"staggered"(default,CV_STAGGERED): advance the state first, then — with the new state in hand — advance the sensitivity ODEs as a separate solve. Two smaller nonlinear solves per step instead of one big one. Often more robust on stiff or large systems; this is CVODES’ / BNGsim’s default."simultaneous"(CV_SIMULTANEOUS): solve state and all sensitivity variables together as one coupled nonlinear system at every step. Often a touch faster per step on small / well-conditioned problems; the per-step solve is larger so it can struggle on stiff or large systems. This is AMICI’s default, so this is the value to use when you want apples-to-apples timing against AMICI.
CVODES has a third mode (
CV_STAGGERED1, one parameter at a time) that BNGsim does not currently expose.force_dense_linear_solver (
bool) –Only used for
method="ode". DefaultFalse. Force CVODE’s dense direct linear solver even for large, low-density models that would otherwise auto-select sparse KLU. This is orthogonal tojacobian(which selects how the Jacobian is computed) — it overrides only the linear-solver kind. Intended for benchmarking the dense path against KLU on the same model; it has no effect in a build compiled without KLU (already always dense).Since issue #128 this reaches
steady_state()andsteady_state_batch()too — specifically their CVODE march, which routes by this same rule and reports the outcome asSteadyStateResult.linear_solver. The KINSOL polish and thedY_ss/dpsolve factor a reduced matrix, whose sparsity pattern is a different object from the model’s, and stay dense either way.force_sparse_linear_solver (
bool) –Only used for
method="ode". DefaultFalse. The mirror image offorce_dense_linear_solver: force sparse KLU even on a model the auto rule would send to the dense solver for being too small (n_species < 50), too dense (Jacobian density>= 10%), or without a structural nonzero in its Jacobian (issue #1296). Those three gates and the LU-fill test below are bypassed — KLU still needs a sparsity pattern and a non-JAX Jacobian — so it is likewise a no-op in a build without KLU. Passing both force flags raisesValueError.It also keeps KLU where the LU-fill test would leave it. A model of 256 to 5,000 species with a sparse Jacobian but an LU factor estimated to fill 10% or more of
n²— typical of rule-derived networks, where a few hub species couple to most complexes — is factored with the BLAS dense solver by default, on a build that has one (bngsim.HAS_LAPACK_DENSE) and with the analytical Jacobian in use.force_sparse_linear_solver=Trueis the way back to KLU for a model that rule misjudges;model._core.jacobian_sparsity["lu_fill_estimate"]reports the estimate.Intended for measuring the auto-selection rule against its own alternative: forced-dense shows what KLU buys on large sparse networks, forced-sparse shows KLU’s setup and indexing overhead on the small dense ones. A model that is both too dense to have been graph-colored and without a usable analytical Jacobian has no way to fill a sparse matrix at all;
run()raises there rather than quietly reverting to dense.Applies to
steady_state()as well (issue #128) — with one difference: a model whose Jacobian has no structural nonzero is left on the dense solver there even when forced, whererun()takes KLU with the analytical Jacobian and raises withjacobian="fd". f(y) ≡ 0 makes such a model a steady state that solves immediately, as it did before #128.
Examples
>>> model = bngsim.Model.from_net("model.net") >>> sim = bngsim.Simulator(model, method="ode") >>> result = sim.run(t_span=(0, 100), n_points=101) >>> result.time.shape (101,)
>>> ssa = bngsim.Simulator(model, method="ssa") >>> result = ssa.run(t_span=(0, 100), n_points=101, seed=42)
>>> psa = bngsim.Simulator(model, method="psa", poplevel=300) >>> result = psa.run(t_span=(0, 100), n_points=101, seed=42)
>>> # Network-free (all equivalent): >>> nf1 = bngsim.Simulator(model, method="nf", xml_path="m.xml") >>> nf2 = bngsim.Simulator(model, method="nf_reject", xml_path="m.xml") >>> nf3 = bngsim.Simulator(model, method="nfsim", xml_path="m.xml") >>> rm = bngsim.Simulator(model, method="rm", xml_path="m.xml")
- METHODS = {'ds', 'dynstoc', 'nf', 'nf_exact', 'nf_fixed', 'nf_reject', 'nfsim', 'ode', 'psa', 'rm', 'rulemonkey', 'ssa'}¶
- auto_atol(*, rtol=None, floor=None)[source]¶
Derive a per-species absolute tolerance from this model’s state.
atol[i] = rtol * max(|y_i|, floor), whereyis the model’s current species vector — the state the nextrun()would start from. Every species is therefore resolved tortolof its own magnitude, which is the statement a scalaratolcannot make about a model whose species span decades.This is what
atol="auto"runs internally. It is public so a caller can see the numbers before committing to them, and adjust: the return value is an ordinary array, and passing a modified copy back asatol=is the supported way to override any single species.If you need the tolerance to be a constant of the model rather than of the point, do not use this — call
bngsim.derive_atol()with the state you chose. Reading the live state is right for a one-off run and wrong for a parameter fit that moves initial conditions: there the vector would be re-derived at every evaluation, and the objective would step wherever the derivation crossed a rounding boundary. Derive once from the nominal state, then pass that same array asatol=for the whole fit.- Parameters:
rtol (
float|None) – Relative tolerance to scale by. Defaults to this Simulator’s (set_tolerances, else1e-8).floor (
float|None) – Magnitude substituted for a species whose current value is smaller — the case where there is no magnitude to scale. Defaults to the smallest strictly positive species value in the model, but not above1.0(seebngsim.derive_atol()).
- Returns:
n_speciesabsolute tolerances, ordered likeModel.species_names.- Return type:
See also
bngsim.derive_atolthe same rule against a state you supply.
Notes
Derived from initial values, this cannot see a species that starts at order one and decays to something tiny — a within-species, over-time mismatch that needs an error-weight function, not a vector. What it removes is the cross-species compromise. The vector returned here is exactly what
bngsim.TrackingAtoltakes as its ceiling and re-evaluates against the trajectory (CVodeWFtolerances, issue #213).Examples
>>> atol = sim.auto_atol() >>> atol[sim.model.species_names.index("IRp")] *= 10 # loosen one >>> result = sim.run(t_span=(0, 100), atol=atol)
- run(t_span=(0.0, 100.0), n_points=101, *, sample_times=None, seed=None, rtol=None, atol=None, max_steps=None, max_step=None, timeout=None, steady_state=False, steady_state_tol=None, carry_sensitivities=False)[source]¶
Run a simulation.
- Parameters:
t_span (
tuple[float,float]) –(t_start, t_end)time interval.n_points (
int) – Number of output time points (including t_start).sample_times (
list[float] |None) –Explicit output time points. When provided, overrides
t_spanandn_points. Must contain at least 2 values, all finite. Values are sorted automatically. A time may repeat, as replicate measurements do ([0, 0, 0, 5, 5, 5]): each copy is a row of its own, and the copies are equal.The override covers the interval, not just the output grid: the run starts at the first sample time and ends at the last, so
sample_times=[5.0, 10.0]integrates over[5, 10]from the model’s initial conditions and never sees[0, 5]. Pass the start time as the first entry whenever the pre-history matters.seed (
int|None) – Random seed for stochastic methods. When omitted (orNone), bngsim draws a fresh seed from system entropy so consecutiverun()calls produce independent trajectories. Pass an explicit integer for reproducibility, from 0 to 2**64 - 1, or to 2**32 - 1 under NFsim, whose generators read 32 bits (issue #1515); a seed outside its range raisesValueError. The actual seed used is exposed viaResult.seed. Formethod="ode"it orders only simultaneous equal-priority events (GH #242).rtol (
float|None) – Relative tolerance for ODE solver. Default1e-8.atol (
Union[float,Sequence[float],NDArray[double],Literal['auto'],Literal['tracking'],TrackingAtol,ExactAtol,None]) –Absolute tolerance for the ODE solver. A float, or the default
1e-8where none is given, is a ceiling: each species takesmin(atol, rtol * max(|x_i|, floor))from the state the run starts from (arun_until()step keeps the one its trajectory started with), whereflooris the smallest value of a species the solver integrates, but not under 1e-6 of the largest (issue #1420). A species below the floor keeps its own value whereverrtolof it is more than double precision resolves beside the largest (issue #1439). That is the number itself wherever the number is a tolerance for the species, and in a model of counts, whose species start at 0 or at least 1, it is the scalar on the scalar’s path. In a model in molar units it is what keeps a species at 1e-9 from running under the noise floor. Where the default still cannot scale a species, as in a model whose species all start at 0,bngsim.AtolScaleWarningnames it.A
bngsim.ExactAtolholds every species to its value exactly, the scalar tolerance CVODE has always taken (CVodeSStolerances), as a float was read before issue #1420. An array ofn_speciesvalues, ordered likeModel.species_names, is a per-species tolerance (CVodeSVtolerances, issue #196) — entryiis the absolute tolerance for speciesi. A wrong-length array raisesValueError; it is never broadcast or truncated, because the consequence of guessing is speciesiheld to the number written for speciesj."auto"derives the vector from the model’s own current state asrtol * max(|y_i|, floor)— seeauto_atol(), which returns the same array so it can be inspected or adjusted first. Note the current in that sentence: if you need a tolerance that is a constant of the model rather than of the point being integrated — a fit that moves initial conditions is the case — derive it once withbngsim.derive_atol()from the state you chose and pass that array here instead of"auto".A per-species tolerance is what a model spanning decades of species magnitude needs: one scalar cannot be a tolerance for a species at 1e-09 and one at 1e+01 at the same time, and picking either end makes the model unintegrable or leaves the small species under the noise floor.
"tracking"(orbngsim.TrackingAtol, which is the same thing with the depth and ceiling spelled out) is the third mode, issue #213. It hands CVODE an error-weight function (CVodeWFtolerances) that re-evaluates the vector above against the state being integrated, so a species that starts at order one and decays to something tiny keeps a tolerance that means something for it. A vector says which species; this says when. It costs steps — roughly 4x on a pure decay resolved twelve decades down — and it is the difference between an answer and noise for the species it is about.Passing a float leaves the run on exactly the code path it was on before, bit for bit. Neither of the other two is the same thing: a constant vector is the same tolerance reaching CVODE through
CVodeSVtolerances, and a depth-0TrackingAtolis the same tolerance again throughCVodeWFtolerances. All three compute the same error weights by differently contractable expressions, so they agree to about one ulp rather than exactly.max_steps (
int|None) – Max internal solver steps per output point. Default10000.max_step (
float|None) – ODE-only. Upper bound on a single internal integrator step (time units).None(default) leaves the step unconstrained, except that a model loaded from SBML with a periodicfloor()/modulo dosing schedule auto-applies a bound that keeps the integrator from stepping over a narrow dose pulse (GH #88). Pass an explicit value to override that (or to bound any model);<= 0disables the bound.timeout (
float|None) – Wall-clock budget in seconds. When set (and positive), the simulator raisesbngsim.SimulationTimeoutif elapsed wall-clock time exceeds this limit.Noneor<= 0disables the budget. Supported on every backend (ODE/SSA/PSA/NFsim/RuleMonkey); RuleMonkey polls every ~1024 SSA events via its upstream cancellation hook. Partial results are not attached to the timeout exception.steady_state (
bool) – ODE-only. WhenTrue, the integrator checks||f(t,y)||_2 / n_speciesafter recording each output point and stops once it falls belowsteady_state_tol. The returnedResultis truncated to only the rows actually integrated (BNG2.plsimulate({steady_state=>1})parity, i.e.run_network -c). DefaultFalse.Result.solver_stats["steady_state_reached"]reports whether the criterion fired beforet_end. A model that reads the time, or that has an event, is integrated to the end of its span whatever this says, and is never marked steady (issue #710): the criterion is of the right-hand side at one instant, which for such a model says nothing of the next.steady_state_tol (
float|None) – Tolerance for thesteady_statecheck above.Noneor<= 0falls back toatol(matching BNG2.pl, which reuses the integrator atol as the steady-state cutoff) — the scalar atol, also when a per-species vector is in force, since||f(t,y)||_2 / n_speciesis one norm over every species and has no per-species reading. Say what “steady” means explicitly when running with a vector.carry_sensitivities (
bool) – ODE-only, pre-equilibration (GH #210, ADR-0052). WhenTrueand this run continues a carried-over species state from a priorrun()on the same persistentSimulator(a two-phase equilibrate-then-measure protocol with no reset between phases), the forward-sensitivity initial conditionsyS(0)are seeded from the prior phase’s final steady-state sensitivitydx_ss/dθinstead of a fresh start. This makesoutput_sensitivities()correct across the pre-equilibration boundary: the measurement phase’s IC isx_ss(θ), so∂x(0)/∂θis the equilibration sensitivity, not zero. Requires the equilibration phase to have been run on the sameSimulatorwith the samesensitivity_params(and no reset). Requesting sensitivities on a carried-over state without this flag raises (no silent wrong derivatives); a fresh single run is unaffected. DefaultFalse.
- Returns:
Simulation results with time, species, observables.
- Return type:
- Raises:
SimulationError – If the solver fails.
SimulationTimeout – If
timeoutis set and the wall-clock budget is exceeded.StopConditionMet – If a stop condition triggers (partial result attached).
ValueError – If t_span, n_points or sample_times are invalid (a time that is not finite,
t_end <= t_start,n_points < 2, fewer than two sample times), or if output sensitivities were requested (sensitivity_params/sensitivity_ic, including thecarry_sensitivitiespath) on a model that contains events. Events reinitialise the CVODE state discontinuously without a matching forward-sensitivity reinitialisation, so derivatives go silently stale at and after an event; bngsim refuses rather than return wrong numbers (GH #205). Discontinuity triggers (forcing pulses / piecewise-time dosing) do not jump state and are unaffected.
- run_batch(t_span=(0.0, 100.0), n_points=101, *, params=None, seed=None, rtol=None, atol=None, max_steps=None, max_step=None, num_processors=None, squeeze=False, timeout=None, steady_state=False, steady_state_tol=None)[source]¶
Run a batch of simulations over parameter sets.
For each parameter set: 1. Clone the model (independent copy) 2. Apply parameters via
set_params3. Reset species to initial conditions 4. Run the simulation (GIL released during each run) 5. Collect the resultStep 3 starts each point from the initial conditions (
Model.reset()), not from the model’s current state, and warns where the two differ (issue #1362).- Parameters:
t_span (
tuple[float,float]) –(t_start, t_end)time interval for each simulation.n_points (
int) – Number of output time points per simulation.params (
Sequence[dict[str,float]] |None) – Parameter sets. Each dict maps parameter names to values.seed (
int|None) – Base random seed for stochastic methods. Simulation 0 usesbase_seeditself and simulation i > 0 a seed drawn from numpy’sSeedSequence(base_seed), so batches from nearby bases share nothing (issue #1365); the seeds of one batch are distinct. When omitted (orNone),base_seedis drawn fresh from system entropy on each call so consecutive batches produce independent trajectories. The actual per-sim seed is exposed viaResult.seedon each result; simulation 0’s is the base, soseed=results[0].seedreruns the whole batch.atol (
Union[float,Sequence[float],NDArray[double],Literal['auto'],Literal['tracking'],TrackingAtol,ExactAtol,None]) – Absolute tolerance for the ODE solver — scalar, or one value per species ordered likeModel.species_names(issue #196). Seerun()for the full contract. A vector,"auto"or a tracking tolerance is resolved once for the whole batch: every point integrates at the same tolerance, so the results are comparable across the sweep."auto", and a tracking ceiling it gives, take each species’ smallest scale over the rows’ starts, so no row is held above a species’ size where it starts. A scalar, the default or the caller’s, is a ceiling thatrun()reads from the state it starts from. Each row reads it from the model’s initial conditions (where a row that writes none starts) and from its own start, and each species takes the tighter of the two: a row that starts below the model is therun()from there, and no row is held looser than the model’s start holds it (issue #1660). The model’s live state is no row’s start, and after a run, read from there, it held every row to where that run ended.max_steps (
int|None) – Maximum internal solver steps per output point.num_processors (
int|None) – Number of threads for parallel execution. DefaultNone(sequential). The GIL is released during each simulation, so threads parallelize effectively.squeeze (
bool) – IfTrue, return a single Result with 3D arrays(n_sims, n_times, n_cols)instead of a list. Refused while a stop condition is registered (add_stop_condition()): a row one cuts is shorter than the rest, and a stack does not say which stopped (issue #1369).steady_state (
bool) – ODE-only. WhenTrue, every simulation in the batch stops early once||f(t,y)||_2 / n_speciesfalls belowsteady_state_toland itsResultis truncated to the rows actually integrated (BNG2.plsimulate({steady_state=>1})/run_network -cparity, applied per parameter point). DefaultFalse. Because each point truncates independently, the per-Result row counts may differ; usesqueeze=False(the default) when mixing steady-state early-stop with heterogeneous equilibration times. A model that reads the time, or that has an event, is integrated to the end of its span whatever this says, and is never marked steady (issue #710): the criterion is of the right-hand side at one instant, which for such a model says nothing of the next.steady_state_tol (
float|None) – Tolerance for thesteady_statecheck above.Noneor<= 0falls back toatol(matching BNG2.pl).
- Returns:
One Result per parameter set (list), or a single squeezed Result with 3D arrays if
squeeze=True. Each row is checked against the registered stop conditions asrun()checks its result: a row that meets one comes back cut at the first sample it holds at, withResult.stop_conditionnaming it, and nothing is raised (issue #1369).- Return type:
- Raises:
SimulationError – If any simulation fails.
ValueError – If params is empty or t_span/n_points are invalid, or
squeeze=Truewhile a stop condition is registered.
Examples
>>> param_sets = [{"k1": v} for v in [0.1, 1.0, 10.0]] >>> results = sim.run_batch( ... t_span=(0, 100), n_points=101, ... params=param_sets, num_processors=4, ... ) >>> len(results) 3
>>> batch = sim.run_batch( ... t_span=(0, 100), n_points=101, ... params=param_sets, squeeze=True, ... ) >>> batch.species.shape (3, 101, n_species)
- run_replicates(n_replicates, t_span=(0.0, 100.0), n_points=101, *, seed=None, timeout=None, num_processors=None, squeeze=False)[source]¶
Run
n_replicatesstochastic replicates of the same model.Unlike
run_batch()— a parameter scan that clones the model per point — replicates share identical parameters and differ only in RNG seed, so this reuses a single simulator and callsreset()between replicates instead of cloning + reconstructing one each time. Reusing the simulator also reuses its cached SSA dependency graph (built once), so on low-activity models the per-replicate cost collapses to the actual trajectory work rather than the fixed clone + graph-rebuild overhead.Every replicate starts from the model’s state at this call, as
run()does: aModel.set_concentration()made first is the start of each (issue #1362), and after arun()so is that run’s end state, as it is for a secondrun(). CallModel.reset()first to start each from the initial conditions. The model is left in the state it was in at the call.Replicate 0 uses
seed_baseitself and replicate i > 0 a seed drawn from numpy’sSeedSequence(seed_base)(seed_baseresolved once fromseed, or from system entropy whenseed is None; each value is exposed via the correspondingResult.seed, andrun()from it repeats that replicate), matching the seed schedulerun_batch()uses across parameter points. Replicate 0’s seed is the base, soseed=reps[0].seedreruns the whole ensemble. It wasseed_base + i, so an ensemble from seed 1 was the one from seed 0 shifted by one replicate, 49 of 50 trajectories the same (issue #1365); the derived seeds of nearby bases are unrelated, and the seeds of one ensemble are distinct.Sequential execution (
num_processorsNoneor1) reuses this simulator directly. Parallel execution clones the model once per worker thread (not per replicate) for thread-safety, each thread reusing its clone across the replicates it handles.SSA/PSA only — ODE has no replicate concept; use
run_batch()for ODE parameter scans.- Parameters:
n_replicates (
int) – Number of replicate trajectories (>= 1).t_span (
tuple[float,float]) – As inrun()/run_batch();squeeze=Trueis refused while a stop condition is registered.n_points (
int) – As inrun()/run_batch();squeeze=Trueis refused while a stop condition is registered.seed (
int|None) – As inrun()/run_batch();squeeze=Trueis refused while a stop condition is registered.timeout (
float|None) – As inrun()/run_batch();squeeze=Trueis refused while a stop condition is registered.num_processors (
int|None) – As inrun()/run_batch();squeeze=Trueis refused while a stop condition is registered.squeeze (
bool) – As inrun()/run_batch();squeeze=Trueis refused while a stop condition is registered.
- Returns:
One
Resultper replicate, or a single squeezed Result with 3D arrays(n_replicates, n_times, n_cols)whensqueeze=True. A replicate that meets a registered stop condition comes back cut there, as arun_batch()row does (issue #1369).- Return type:
Examples
>>> ssa = bngsim.Simulator(model, method="ssa") >>> reps = ssa.run_replicates(30, t_span=(0, 100), n_points=101, seed=0) >>> len(reps) 30
- parameter_scan(parameter, par_scan_vals=None, *, par_min=None, par_max=None, n_scan_pts=None, log_scale=False, t_span=(0.0, 100.0), n_points=101, reset_conc=True, reset_to=None, on_point=None, seed=None, rtol=None, atol=None, max_steps=None, max_step=None, timeout=None, steady_state=False, steady_state_tol=None, squeeze=False)[source]¶
Sweep one parameter, running a simulation per value (BNG
parameter_scan).This is the native scan primitive whose
reset_concsemantics match BNG2.pl (issue #11). Unlike a hand-rolled loop that re-derives every point’s species from the.netseed initializers, each point here resets to the state at scan invocation (or to a named snapshot) — so a pre-equilibrate → intervene → scan protocol carries its post-intervention state into the sweep faithfully, instead of discarding it.For each scanned value the model is: reset to the snapshot (when
reset_conc), assigned the scannedparameter, passed through the optionalon_pointhook (for coupledsetConcentrationoverrides that track the scanned parameter), then integrated overt_span.Supported on the stateful model-backed backends (ODE / SSA / PSA) only; the XML network-free backends have no in-process scan path.
- Parameters:
parameter (
str) – Name of the parameter to scan. Must exist in the model.par_scan_vals (
Sequence[float] |None) – Explicit values to scan. Mutually exclusive with thepar_min/par_max/n_scan_ptstriple.par_min (
float|None) – Endpoints of a generated scan range (inclusive).par_max (
float|None) – Endpoints of a generated scan range (inclusive).n_scan_pts (
int|None) – Number of points in the generated range (>= 1).log_scale (
bool) – When generating a range, space points geometrically (requires positive endpoints) rather than linearly. DefaultFalse.t_span (
tuple[float,float]) –(t_start, t_end)for each per-point simulation.n_points (
int) – Output time points per simulation (includingt_start).reset_conc (
bool) – BNGreset_conc. WhenTrue(default), every point resets to the snapshot before applying the scanned parameter — points are independent. WhenFalse, points are not reset between values; each continues from the previous point’s end-state (a continuation scan — seebifurcate()).reset_to (
str|None) – Name of a saved concentration snapshot (Model.save_concentrations(label=...)) to reset each point to. WhenNone(default), the reset target is the model’s live state captured at the moment this method is called. Only consulted whenreset_conc=True.on_point (
Callable[[Model,float],None] |None) –on_point(model, value)invoked after the reset + scanned-parameter assignment and before integration, for each point. Use it to apply coupledsetConcentrationoverrides whose value tracks the scanned parameter (e.g. a ligand species whose count isvalue * NA * Vecf) — the model-specific bookkeeping the primitive cannot infer on its own.seed (
int|None) – Base seed for stochastic methods; point 0 usesseed_baseitself and point i > 0 a seed drawn from numpy’sSeedSequence(seed_base), asrun_batch()does (issue #1365; drawn fresh from entropy whenNone, and point 0’sResult.seedreruns the whole scan). Ignored for ODE.rtol (
float|None) – Per-simulation solver options, forwarded torun().atolmay be a per-species vector or"auto"(issue #196); that is resolved once, so every point of the scan is held to the same tolerance rather than to one derived from its own initial conditions. Withreset_conc=True,"auto"(and a tracking ceiling it gives) takes each species’ smallest scale over the points’ starts, the reset with the scanned value written; a point whoseon_pointhook writes a species lower holds that species tighter still. A continuation scan’s later points start where the last ended, and its"auto"is read from its first point’s start, the state at the call with the first value written. A scalar, the default or the caller’s, is a ceiling thatrun()reads from the state it starts from; each point reads it from the invocation state and from its own start, and each species takes the tighter of the two (issue #1660).atol (
Union[float,Sequence[float],NDArray[double],Literal['auto'],Literal['tracking'],TrackingAtol,ExactAtol,None]) – Per-simulation solver options, forwarded torun().atolmay be a per-species vector or"auto"(issue #196); that is resolved once, so every point of the scan is held to the same tolerance rather than to one derived from its own initial conditions. Withreset_conc=True,"auto"(and a tracking ceiling it gives) takes each species’ smallest scale over the points’ starts, the reset with the scanned value written; a point whoseon_pointhook writes a species lower holds that species tighter still. A continuation scan’s later points start where the last ended, and its"auto"is read from its first point’s start, the state at the call with the first value written. A scalar, the default or the caller’s, is a ceiling thatrun()reads from the state it starts from; each point reads it from the invocation state and from its own start, and each species takes the tighter of the two (issue #1660).max_steps (
int|None) – Per-simulation solver options, forwarded torun().atolmay be a per-species vector or"auto"(issue #196); that is resolved once, so every point of the scan is held to the same tolerance rather than to one derived from its own initial conditions. Withreset_conc=True,"auto"(and a tracking ceiling it gives) takes each species’ smallest scale over the points’ starts, the reset with the scanned value written; a point whoseon_pointhook writes a species lower holds that species tighter still. A continuation scan’s later points start where the last ended, and its"auto"is read from its first point’s start, the state at the call with the first value written. A scalar, the default or the caller’s, is a ceiling thatrun()reads from the state it starts from; each point reads it from the invocation state and from its own start, and each species takes the tighter of the two (issue #1660).max_step (
float|None) – Per-simulation solver options, forwarded torun().atolmay be a per-species vector or"auto"(issue #196); that is resolved once, so every point of the scan is held to the same tolerance rather than to one derived from its own initial conditions. Withreset_conc=True,"auto"(and a tracking ceiling it gives) takes each species’ smallest scale over the points’ starts, the reset with the scanned value written; a point whoseon_pointhook writes a species lower holds that species tighter still. A continuation scan’s later points start where the last ended, and its"auto"is read from its first point’s start, the state at the call with the first value written. A scalar, the default or the caller’s, is a ceiling thatrun()reads from the state it starts from; each point reads it from the invocation state and from its own start, and each species takes the tighter of the two (issue #1660).timeout (
float|None) – Per-simulation solver options, forwarded torun().atolmay be a per-species vector or"auto"(issue #196); that is resolved once, so every point of the scan is held to the same tolerance rather than to one derived from its own initial conditions. Withreset_conc=True,"auto"(and a tracking ceiling it gives) takes each species’ smallest scale over the points’ starts, the reset with the scanned value written; a point whoseon_pointhook writes a species lower holds that species tighter still. A continuation scan’s later points start where the last ended, and its"auto"is read from its first point’s start, the state at the call with the first value written. A scalar, the default or the caller’s, is a ceiling thatrun()reads from the state it starts from; each point reads it from the invocation state and from its own start, and each species takes the tighter of the two (issue #1660).steady_state (
bool) – Per-simulation solver options, forwarded torun().atolmay be a per-species vector or"auto"(issue #196); that is resolved once, so every point of the scan is held to the same tolerance rather than to one derived from its own initial conditions. Withreset_conc=True,"auto"(and a tracking ceiling it gives) takes each species’ smallest scale over the points’ starts, the reset with the scanned value written; a point whoseon_pointhook writes a species lower holds that species tighter still. A continuation scan’s later points start where the last ended, and its"auto"is read from its first point’s start, the state at the call with the first value written. A scalar, the default or the caller’s, is a ceiling thatrun()reads from the state it starts from; each point reads it from the invocation state and from its own start, and each species takes the tighter of the two (issue #1660).steady_state_tol (
float|None) – Per-simulation solver options, forwarded torun().atolmay be a per-species vector or"auto"(issue #196); that is resolved once, so every point of the scan is held to the same tolerance rather than to one derived from its own initial conditions. Withreset_conc=True,"auto"(and a tracking ceiling it gives) takes each species’ smallest scale over the points’ starts, the reset with the scanned value written; a point whoseon_pointhook writes a species lower holds that species tighter still. A continuation scan’s later points start where the last ended, and its"auto"is read from its first point’s start, the state at the call with the first value written. A scalar, the default or the caller’s, is a ceiling thatrun()reads from the state it starts from; each point reads it from the invocation state and from its own start, and each species takes the tighter of the two (issue #1660).squeeze (
bool) – WhenTrue, stack the per-point results into a singleResultwith 3-D arrays (likerun_batch()); otherwise return a list of per-point results. Refused while a stop condition is registered, asrun_batch()refuses it.
- Returns:
One
Resultper scanned value (in order), each carryingcustom_attrs["scan_parameter"]andcustom_attrs["scan_value"]. A squeezedResultwhensqueeze=True. A point that meets a registered stop condition comes back cut there, withResult.stop_conditionnaming it, rather than raisingStopConditionMetout of the scan (issue #1369).- Return type:
Notes
The persistent model + backend simulator are left as they were before the call: the scanned parameter, the reset-target concentrations and the carried sensitivity state are restored afterward, so a
Simulatorcan be scanned repeatedly (and the returned trajectories, not the live model, are the product).Output sensitivities across the scan boundary (issue #81). On a
Simulatorbuilt withsensitivity_params, each point’s forward-sensitivity seed∂x(0)/∂θis the carrieddx/dθof the state it starts from — the pre-equilibration’s steady-state sensitivity — because that state, not the model’s seed initial conditions, is the point’s initial condition. So a scan is run with sensitivities only when that state carries a matchingdx/dθ:reset_conc=True— every point restores the reset target’s state and itsdx/dθ, then integrates withcarry_sensitivities=True. The target must carry one: the live state at invocation does after an equilibration run on this sameSimulator, and areset_tosnapshot does when it was saved while thatdx/dθwas pending.reset_conc=False(bifurcate()) — each point continues the previous point’s state and itsdx/dθ, so the whole continuation is one differentiable protocol.
An
on_pointhook assigns the initial condition its point starts from, so for the species it writes the seed is its derivative, not the carried one (issue #111). Row by row, the most specific thing available wins: a row the hook installed wholesale, then one declared withModel.declare_ic_sensitivity(), then one measured through the hook (bngsim calls the hook at perturbed inputs, so a literal dose measures0, a dose computed from a differentiated parameter measures its true derivative, and an increment of the carried pool measures the carried row plus the dose’s), and otherwise the carried row unchanged. Measuring calls the hook several extra times per point, so it must be a deterministic function of(model, value); declaring a row skips its measurement.The scanned parameter must not be a
sensitivity_paramsentry (each point overwrites it, which cannot be composed with the derivative carried into the point),sensitivity_icis not supported across the boundary, anon_pointhook may not move a differentiated parameter, and a hook whose assigned initial condition is not differentiable in a parameter (a dose rounded to whole molecules) must declare that row rather than have a difference quotient of the jump reported as a derivative. Every one of those is a raise, never a silently-reseeded gradient. For a sweep whose points start from the model’s own seed initial conditions, userun_batch().
- bifurcate(parameter, par_scan_vals=None, *, par_min=None, par_max=None, n_scan_pts=None, log_scale=False, t_span=(0.0, 100.0), n_points=101, seed=None, rtol=None, atol=None, max_steps=None, max_step=None, timeout=None, steady_state=False, steady_state_tol=None, squeeze=False)[source]¶
Continuation scan of one parameter (BNG
bifurcate,reset_conc=0).A
parameter_scan()sibling that does not reset concentrations between points: each point continues from the previous point’s end-state, so the sweep traces a branch of steady states as the parameter is stepped. Sweeppar_scan_valsup then down (two calls) to expose hysteresis. The first point starts from the model’s live state at invocation.Accepts the same arguments as
parameter_scan()exceptreset_conc(pinned toFalse),reset_to, andon_point(which pertain to the per-point reset that continuation omits). Seeparameter_scan()for the shared parameters.
- compute_all_sensitivities(t_span=(0.0, 100.0), n_points=101, *, params=None, chunk_size=2, n_workers=None, rtol=None, atol=None, max_steps=None)[source]¶
Compute full sensitivity tensor via parallel chunked CVODES jobs.
Splits
Npparameters into⌈Np/chunk_size⌉independent CVODES forward-sensitivity jobs, runs them in parallel viamodel.clone()+ThreadPoolExecutor(GIL released during C++ CVODE integration), and stitches the partial sensitivity arrays into a complete(n_times, n_species, n_params)tensor.Benchmarks showed that 2-parameter sensitivity chunks add only ~1.2× overhead for large models (593–1281 species). With
⌈Np/2⌉parallel jobs, the full sensitivity tensor costs ~1.2× wall-clock of a plain ODE solve — making gradients nearly free with cores.- Parameters:
t_span (
tuple[float,float]) –(t_start, t_end)time interval.n_points (
int) – Number of output time points (including t_start).params (
list[str] |None) – Parameter names to compute sensitivities for. Default (None):Model.primary_param_names— the model’s independent knobs, which is a subset ofModel.param_names. A derived (expression-backed) parameter is left out because its column is not a coordinate of its own: the primaries underneath it already carry its effect by the chain rule, so the two columns are not independent andResult.gradient()would count the same physical effect twice (issue #203). A synthesized_V0_<comp>is left out because it is bngsim’s record of a compartment’s load-time size rather than a knob at all (issue #170). Both are dropped with a warning naming them, exactly as an unwritable compartment size is. Naming either inparamskeeps its column — an explicit ask is a statement that you want that derivative on its own terms, which is howbngsim.jax.differentiable_solve(..., flat=True)obtains one.chunk_size (
int) – Number of sensitivity parameters per CVODES job. Default 2, which benchmarking found to work well for large models because 2-parameter chunks add only ~1.2× overhead. Chiefly a throughput knob, but not only one: a chunk’s columns share one CVODES error test, so the grouping can decide whether a marginal column’s step is accepted (issue #243). A chunk that fails as a group is therefore retried column by column before anything is reported — see_recover_failed_chunk().n_workers (
int|None) – Number of parallel threads. Default:min(⌈Np/chunk_size⌉, os.cpu_count()). Set to 1 for serial execution (debugging/profiling).atol (
Union[float,Sequence[float],NDArray[double],Literal['auto'],Literal['tracking'],TrackingAtol,ExactAtol,None]) – Absolute tolerance for the ODE solver — scalar, or one value per species ordered likeModel.species_names(issue #196). Seerun(). Resolved once and shared by every chunk, so the columns being stitched back together were error-controlled identically. Each column’s own tolerance is then built from the per-species value of the state it differentiates.max_steps (
int|None) – Max internal solver steps per output point.
- Returns:
Simulation result with full
sensitivitiestensor of shape(n_times, n_species, n_params). Thesensitivity_paramsattribute lists all parameter names in the order they appear in the tensor, and is the authority on the column set — underparams=Noneit is narrower thanModel.param_names, as described above. Species trajectories are from the first chunk’s ODE solve (all chunks produce identical trajectories since they share the same model and parameters).- Return type:
- Raises:
ValueError – If method is not ‘ode’, or
t_span/n_pointsare onesrun()refuses (a time that is not finite,t_end <= t_start,n_points < 2; issue #1523), or params contains unknown names, or the analytical sensitivity RHS every chunk needs cannot be built —codegen=False/BNGSIM_NO_CODEGEN, no codegen backend, or rate laws that do not differentiate to closed form. That is the same hard requirementSimulator(..., sensitivity_params=...)carries (GH #214): the interpreted finite-difference sensitivity path is not reliable at tight tolerances, so it is refused rather than used. Also ifparamsnames a slot a model function owns (issue #329) — underparams=Nonethose are dropped with the warning below, and naming one gets the same answer as a hard refusal rather than the identically-zero column it would otherwise produce.SimulationError – If a chunk simulation fails and the column-by-column retry shows at least one of its columns cannot be integrated on its own either (issue #243). A chunk that fails only as a group is recovered from those single-column solves, with a warning, rather than raising.
Examples
>>> model = bngsim.Model.from_net("model.net") >>> sim = bngsim.Simulator(model, method="ode") >>> result = sim.compute_all_sensitivities( ... t_span=(0, 100), n_points=101, ... chunk_size=2, n_workers=8, ... ) >>> result.sensitivities.shape # (101, n_species, n_params) (101, 149, 40) >>> fim = result.fisher_information(sigma=0.1) >>> grad = result.gradient( ... lambda species, time: np.sum((species - data)**2) ... )
Notes
Architecture: Each chunk clones the model (deep copy, thread-safe), creates a fresh
CvodeSimulator, runs CVODES withchunk_sizesensitivity parameters, and returns its partial(n_times, n_species, chunk_size)sensitivity tensor. The main thread stitches these along axis 2 (the parameter axis).Why ThreadPoolExecutor works: The GIL is released during C++ CVODE integration (
py::call_guard<py::gil_scoped_release>), so threads achieve true parallelism for the compute-intensive portion. Python overhead is negligible (model clone + setup).Optimal chunk_size: Benchmarks show that
chunk_size=2minimizes per-chunk overhead while keeping thread count manageable.chunk_size=1works but doubles the number of threads needed.
- steady_state(*, tol=1e-09, max_time=1000000.0, method='integration', rtol=None, atol=None, max_steps=None, sensitivity_params=None, mask=None, criterion='scaled')[source]¶
Find the steady state of the ODE system f(y) = 0.
A model with an event, or one that reads the time (
time()or a table function indexed by time, in a rate law, a rule or a reported function), is refused withSimulationError(issue #710): the solvers read the right-hand side att = 0and fire no event. Integrate such a model withrun().Solver methods:
"integration"(default): CVODE BDF integrated untilcriterionis met (by default"scaled";"residual"is BNG2.pl’s||f(y)||_2 / n_species < tol,run_network -c)."newton": two-tier integrate-first solver. The same CVODE burst carries the state into the physical root’s basin, then KINSOL polishes. The polish is accepted only once it is seed-stable (agrees across two successively tighter bursts) and carries no eigenvalue in the right half-plane (issue #78, seeSteadyStateResult.root_stability); otherwise integration continues. Seeding Newton at the raw initial condition instead can converge to a spurious root off(y)=0the trajectory never reaches, or walk a species negative intoNaN(GH #27) — hence the burst."kinsol": accepted alias for"newton".
Both tiers factor the model’s closed-form Jacobian when it has one and
jacobian=asks for it (issue #127):CVodeSetJacFnon the march,KINSetJacFnon the polish, the latter projected onto the polish’s own unknown set.jacobian="fd"pins the difference quotient instead — one RHS evaluation per unknown per Jacobian setup, which is what both tiers paid unconditionally before #127.ss.solver_jacobian_sourcereports which matrix was factored. The same option still selects how the stability certificate anddY_ss/dpbuild theirs.Because
"newton"integrates first and only then polishes, it is"integration"plus extra work: across six published dose-response models it cost 1.4-3.9x more wall clock (GH #28), which is why"integration"is the default. Two things still argue for"newton":A much tighter root. The polish lands near a residual of ~1e-13 where integration stops the moment it crosses
tol(~1e-9) — worth having when the steady state feeds a stiff downstream solve.A tight ``max_time`` budget. Newton reaches
tolfrom a looser burst than integration needs on its own, so whenmax_timeis cut well below the default it can converge where integration runs out of time. At the defaultmax_time=1e6this does not happen on any model in the benchmark corpus, but atmax_time=1e3several models flip.
- Parameters:
tol (
float) – Convergence tolerance on||f(y)||_2 / n_species. Default 1e-9. Both criteria ask it.criterion (
str) –What
convergedasks of a state (issue #1001)."scaled"(default): the parity test,||f(y)||_2 / n_species < tol, and the run test. A run taken on from the state formax_time(at most 1e6) moves no species by more thanrtol·|y_i| + atol_i: what tells a state the system rests at from one undertolthat a run leaves (a model in small units, or a slow one). An unstable root that a run from it does not leave passes (the cubic’s 0.3); on the Newton pathSteadyStateResult.root_stabilitysays so.atol_iis the march’s own, never more thanrtoltimes the largest species the solve starts with or reaches. The Newton stepδ = −J⁻¹ffrom the state to the root, within the same tolerances, decides where the solve stops (up to 512 unknowns), which takes the issue’s cubic at a scale of 1e-9 on from 0.894 to its root at 1, and the state it stops at is polished by KINSOL; the step decides nothing aboutconverged.masksays which species are measured, not which move: the step moves every species but a$-fixed one and a masked one that nothing reads or that reads nothing (a counter), which is held; the run moves every species but a$-fixed one. Where neitheratolnorset_tolerances()gives one, each species is marched at the smaller of whatatol="auto"gives it and the scalar 1e-8: the scalar is no tolerance for a model in small units, nor"auto"for a species that starts large and settles small."residual":||f(y)||_2 / n_species < tol, BNG2.pl’s test alone, kept for comparisons withrun_network. The norm is in the model’s units of concentration per unit time, so a model in molar units, or a slow one, passes it far from its steady state: the cubicX' = -k·x·(x - 0.3)·(x - 1)at a scale of 1e-9 came backconverged=Trueat x = 0.894, 1.003 and 0.131 after one step, where every run ends at 1.
Under
"scaled"the run test and the step are reported asSteadyStateResult.continued_run_distanceandSteadyStateResult.newton_distance.max_time (
float) – Max integration time for the integration path, and how long the run ofcriterion="scaled"’s run test is taken on for, up to 1e6. Default 1e6.method (
str) –"integration"(default),"newton", or"kinsol"(alias for"newton").rtol (float, optional) – Relative tolerance for the CVODE march, and under
criterion="scaled"for the run test. Default1e-8.atol (
Union[float,Sequence[float],NDArray[double],Literal['auto'],Literal['tracking'],TrackingAtol,ExactAtol,None]) – Absolute tolerance for the CVODE march — scalar, or one value per species ordered likeModel.species_names(issue #196). Seerun()for the contract. This is the accuracy the march is held to on the way to the root, and undercriterion="scaled"the run test’s;tolabove is the separate test for having arrived, a single norm over all species.max_steps (int, optional) – Max CVODE internal steps per output point.
sensitivity_params (list[str], optional) – Parameter names for dY_ss/dp sensitivity. Requires code generation, exactly as
run()andcompute_all_sensitivities()do — see the sensitivity note below.mask (array-like of bool, or sequence of str, optional) – Which species the convergence test covers (issue #74). A boolean array of length
n_species(True= keep), or the species names to keep.None(default) tests every species, which is the BNG2.pl parity criterion. See the accumulator note below.
- Return type:
Notes
Write-only accumulators (issue #74). A species some reaction produces and none consumes — a
degraded/produced/secretedpool counting cumulative flux, a common BNGL idiom — has a constant non-zero derivative forever.||f(y)||_2 / n_speciesthen has a floor abovetoland this method reportsconverged=Falsehowever far it integrates, even when every other species is settled to 1e-10.maskrestricts the test to the subspace that does have a steady state;Model.is_pure_sink()finds the accumulators structurally, so the recipe needs no hand-listed species:ss = sim.steady_state(method="newton", mask=~model.is_pure_sink())
Everything still integrates — the excluded species’ equations stay in the RHS and their trajectories come back in
ss.concentrations. What the mask restricts is the residual norm (overn_included, sotolkeeps its meaning), the KINSOL polish’s unknown set, and thedY_ss/dplinear system; those last two have to follow, because an accumulator contributes a structurally zero Jacobian column and makes both systems singular at every seed. Excluded species come back with a NaNdY_ss/dprow: no steady value, no steady-state gradient.When a solve fails,
ss.unconverged_pure_sinksnames any accumulator that was in the test and is carrying flux — that is a structural floor, not a slow tail, and nomax_timewill move it. Detection is not a convergence verdict, though:A -> Bwith nothing feedingAhas a textbook pure sink and converges anyway, because the flux dies out. The time-course early stop,run(steady_state=True), keeps the unrestricted criterion and takes no mask.Codegen (issue #63). The solve runs whatever RHS this Simulator’s
codegen_backendreports — the compiled.so, the MIR JIT, or the ExprTk interpreter. Until #63 the steady-state path read no codegen option at all, so a Simulator built withcodegen=Truestill solved interpreted;ss.rhs_backendnow reports which backend actually ran.Sensitivity requires codegen.
dY_ss/dp = -J⁻¹·(∂f/∂p)used to build both factors from finite differences. It now prefers the model’s analytical Jacobian and the analytical∂f/∂pthe codegen sensitivity RHS emits, and — likerun()andcompute_all_sensitivities()since GH #214 — refuses rather than silently degrading when codegen is unavailable.ss.sens_jacobian_source/ss.sens_dfdp_sourcereport which path each factor took. Since GH #67 a Functional model whose rate laws are smooth algebra has an analytical∂f/∂phere too — it is the same emittedbngsim_codegen_sens_rhs, read atyS = 0. What still differences that factor, with a warning, is Michaelis-Menten and the Functional laws carrying a condition or a non-smooth builtin.The expression block
d(func)/dpis compiled code too (issue #75): it is evaluated bybngsim_codegen_output_sens, the GH #198 chain rule a CVODES forward-sensitivityrun()already uses, fed the solveddY_ss/dpcolumns — so a steady-state gradient and a long-run one come from the same evaluator.Where there is no closed form, the derivatives are exact all the same (issue #1330): the Jacobian,
∂f/∂pand a function’s row are taken by dual numbers through the interpreter, each law and function as it is evaluated, differentiated branch by branch with no step. That coversjacobian="fd", a model whose closed form declines a law, a rate law the sensitivity emitter declines (Michaelis-Menten, a condition, a non-smooth builtin), and a function the compiled chain rule declines (a table function,if(),abs(),max()). At a steady state that sits exactly where a law changes branch, the answer is taken from each side, and refused where the two differ. Only a model the dual evaluation cannot follow (a ring of functions that read each other,rateOf) falls back: its Jacobian and∂f/∂pto difference quotients checked at more than one step (issue #1002), and its function rows toNaN, whichss.output_sensitivitiesrefuses by name, with the reason.ss.sens_jacobian_source,ss.sens_dfdp_sourceandss.sens_output_sourcesay which path each took. The observable block is an exact linear projection through the group factors and never differences anything.
- steady_state_batch(params=None, *, states=None, tol=1e-09, max_time=1000000.0, method='integration', rtol=None, atol=None, max_steps=None, n_workers=None, mask=None, newton_burst=True, criterion='scaled', certify=True)[source]¶
Compute steady states for multiple parameter sets, starting states, or both.
A model with an event, or one that reads the time, is refused, as
steady_state()refuses it (issue #710).- Parameters:
params (sequence of dict[str, float], optional) – Parameter sets. Omitted with
states: every entry runs at this model’s parameters. Withoutstates, each entry starts from the model’s initial conditions (Model.reset()), asrun_batch()’s points do, not from its current state, and the batch warns where the two differ (issue #1521): passstates=[model.get_state()] * len(params), or callModel.save_concentrations()first, to start from it. Either start drops a dose an entry writes to an initial-condition parameter, which only the reset applies; for a dose scan after a run, callModel.reset()first.states (array-like, optional) – Starting states, shape
(n_entries, n_species)ordered likeModel.species_names(issue #524). Entryiresets its clone and then starts fromstates[i]; withparams, the two have the same length and pair up row by row. A conservation law’s total is the one the starting state holds.tol (
float)max_time (
float)newton_burst (
bool) – Withmethod="newton",Falsesolves from each starting state directly, with no integration burst first, and reports the root’s stability (root_stability,eigenvalues) instead of rejecting an unstable one: whatsteady_states()enumerates roots with. A$-fixed species is then left out of the unknowns, whose Newton system it would make singular. DefaultTrue, the two-tier solver ofsteady_state().certify (
Union[bool,Literal['start']]) – Withnewton_burst=False,Falseskips the stability certificate, a dense eigendecomposition of the reduced Jacobian, and a converged result then hasroot_stability == ""and noeigenvalues."start"takes no Newton step: each starting state is the result, converged where it passestol, and certified there.steady_states()solves its starts withFalseand certifies each distinct root once with"start"(issue #1191). DefaultTrue. Ignored otherwise.method (
str) –"integration"(default),"newton", or"kinsol"(alias for"newton"). Seesteady_state().criterion (
str) –"scaled"(default) or"residual", as insteady_state()(issue #1001).rtol (optional) – Solver options for each march, as in
steady_state().atolmay be a per-species vector or"auto"(issue #196), resolved once and applied identically to every entry. The"auto"criterion="scaled"takes when noatolis given is each entry’s own, from its starting state.atol (
Union[float,Sequence[float],NDArray[double],Literal['auto'],Literal['tracking'],TrackingAtol,ExactAtol,None]) – Solver options for each march, as insteady_state().atolmay be a per-species vector or"auto"(issue #196), resolved once and applied identically to every entry. The"auto"criterion="scaled"takes when noatolis given is each entry’s own, from its starting state.max_steps (optional) – Solver options for each march, as in
steady_state().atolmay be a per-species vector or"auto"(issue #196), resolved once and applied identically to every entry. The"auto"criterion="scaled"takes when noatolis given is each entry’s own, from its starting state.n_workers (int, optional) – Number of parallel threads.
mask (array-like of bool, or sequence of str, optional) – Which species the convergence test covers, applied identically to every entry (issue #74). See
steady_state(). The species set is structural, so it does not move with the parameter set — which is what makes one mask correct for a whole dose scan.
- Return type:
Notes
Every entry runs this Simulator’s
codegen_backend, the same assteady_state()(issue #63). Each entry resolves the artifact for itself — onedlopenof the shared path, or one JIT compile of the shared source, per solve — so a dose-response sweep pays that per entry rather than once for the batch.r.rhs_backendreports what ran.
- steady_states(*, n_starts=200, seed=0, upper=None, decades=4.0, dedupe_rtol=1e-06, tol=1e-09, n_workers=None, mask=None)[source]¶
Enumerate the steady states of this parameter set by multistart Newton (issue #524).
n_startsstarting states are sampled on the model’s conservation manifold, KINSOL is run from each with no integration burst (steady_state_batch(states=..., method="newton", newton_burst=False)), and the roots it reaches are clustered. Each distinct root comes with the stability certificate’s verdict, so the saddles a run of the dynamics never rests on are reported too. A start that KINSOL stops short of its root, undertolbut at a Newton distance above 1, is solved once more from where it stopped before the roots are grouped (issue #1518).The root one ordinary
steady_state()solve reaches (with the samemask) is solved as one more start, with each held species set as the draws hold it, so the call returns that root too wherever Newton holds it attol(issue #1189). It is no draw: it adds nothing tomultiplicityorbasins, and a root only it found has multiplicity 0. The seed is then put on the conservation laws at the model’s totals, as every draw is. Two cases return less. Where Newton cannot hold the root attol, as at the large rates below, it is missed. And where the solve had moved a held species, it solved another problem: a masked sink at the end of a chain, as the pure-sink mask below makes it, moves its law’s total, and a held species the rates read moves the rates. Newton from the seed then reaches a root of the problem the draws solve, or none, and it need not be the solve’s.This is a search, not a proof: a root with a small basin, or a saddle KINSOL never lands on, can be missed, and nothing here claims the list is complete. Completeness for polynomial mass-action systems is what homotopy continuation offers (#502). Nor is a continuum of steady states a list of roots: where one root’s spectrum holds a zero eigenvalue (SIR’s line of disease-free states), each start lands somewhere on it and comes back as a root of its own. A write-only accumulator makes Newton’s system singular at every start; exclude it with
mask=~model.is_pure_sink().Sampling: each species a start moves is drawn log-uniformly from
decadesdecades below a bound up to it. For a species in no conservation law the bound isupper, which defaults to ten times the largest concentration of the model’s state, law total, and such species of the steady state an ordinarysteady_state()solve reaches (one solve, for the scale), or 10 when all are 0. The species in a law are drawn the same way below the largest total and then moved onto the laws together, at the totals the model’s state holds: to the point that satisfies them nearest the draw in relative entropy, which keeps every species positive however many a law holds. Members of a law therefore start within aboutdecadesdecades of each other, and a root with one further below its law’s total is reached only if Newton walks there. A$-fixed species, and onemaskleaves out, keeps its value in every start and is no unknown of the solve, though a law’s dependent species is rebuilt from the law.tolis absolute, as insteady_state(). A model at so small a scale that most starts are already under it is refused. Each solve rebuilds, from each conservation law, the member that carries the most of the law’s total where the solve is, so a large total’s round-off lands in a species at its own scale rather than in a small one (issue #1190). Where a root’s rates are themselves large, their own rounding can still exceedtol, and the root is then reported as a failed start:A <-> Bat rates 1 and 2 and a total of1e8ends at a residual of5e-9when Newton starts at the root itself. Below concentrations of about1e-9, KINSOL’s step limits are absolute too, and most starts fail even with atolscaled to match.- Parameters:
n_starts (
int) – How many starting states to solve from.seed (
int) – Seed for the sampling, so a call is reproducible.upper (float or array-like, optional) – The sampling bound for a species in no conservation law: a scalar or one value per species. Unused when every species a start moves is in a law.
decades (
float) – How many decades below its bound a species is drawn from. Wider reaches roots near zero and spends more starts where Newton diverges: on the Gardner toggle, 6 decades lost 95 of 200 starts and 2 found one of its three roots; 4 found all three and lost 52.dedupe_rtol (
float) – Two roots are the same when every species agrees to withindedupe_rtolof the larger magnitude, or to within a floor for a species at or near 0, where a root at 0 is reached at 0 or a hair beside it (-5.9e-24 on #1001’s cubic at a nanomolar scale):1e-9of the largest value the species takes at any converged start, or double precision’s resolution beside the largest magnitude the solves saw, whichever is larger. A species in a conservation law agrees, too, within1e-9of its smallest law’s scale,Σ|L·y|: the solve places it no closer than that. The residualtolis no distance in concentration and plays no part: at1e-9it merged the 0.2 nM and 0.5 nM roots of a nanomolar switch (issue #1518).tol (
float) – As insteady_state_batch().n_workers – As in
steady_state_batch().mask – As in
steady_state_batch().
- Return type:
- add_stop_condition(condition, *, label='')[source]¶
Add a stop condition checked after each simulation.
- Parameters:
str: A Python expression evaluated at each sample. It reads the observables by name, a species by its name with
(and)written as_(A()reads asA__), and the time astortime; nothing else is bound, Python builtins included. It is met at the first sample where it is true, or, for a number, > 0. Arithmetic numpy warns of (0/0, x/0, an overflow) is read at the value numpy gives, so a NaN is not met, and one RuntimeWarning names it (issue #1556). Example:"A_tot < 10"callable: A function
f(result) -> boolcalled after the simulation. If it returns a true value (in Python’s sense), the stop condition is triggered.
label (
str) – Human-readable label for the condition.
- Raises:
ValueError – Here, for an expression that reads a name it cannot (a misspelled observable, a parameter, a model function, a builtin such as
abs), that uses^(bitwise XOR in Python; write**for a power), or that is a tuple. From the run, for an expression that raises at a sample or gives something other than a truth value or a real number. Each used to be accepted: a name it could not read made it a condition never met, and a tuple one met at the first sample (issue #1509). An exception a callable raises propagates.- Return type:
Notes
A result that meets a condition is cut at the first sample it holds at, and
Result.stop_conditionnames the condition (its label, else its expression).run()raisesStopConditionMetwith that result.run_batch(),run_replicates()andparameter_scan()(and sobifurcate()) check every row the same way and return the cut rows in their list, beside the rest, raising nothing; they refusesqueeze=Truewhile a condition is registered (issue #1369). Conditions are checked in the order they were added, and the first one met cuts the row where it first holds, even where a condition added later held earlier. A callable is met at the last sample, so the row it cuts keeps every sample. A cut result carries the reported blocks only, as the onerun()raises with always has:Result.staterefuses on it.Examples
>>> sim.add_stop_condition("A_tot < 10", label="low_A") >>> sim.add_stop_condition( ... lambda r: r.species[-1, 0] < 5, ... label="very_low_A", ... )
- run_until(t, *, n_points=None, seed=None, rtol=None, atol=None, max_steps=None)[source]¶
Run simulation from current time to t.
This enables interactive simulation: run to a time point, inspect or modify the model, then continue. Supported for stateful model-backed solvers (ODE / SSA / PSA) only.
- Parameters:
t (
float) – Target time. Must be finite and > current time.n_points (
int|None) – Number of output points. Default: max(2, int(dt)+1).seed (
int|None) – Random seed for stochastic methods.None(default) draws a fresh seed; pass an integer for reproducibility. SeeSimulator.runfor the full contract.rtol (
float|None) – Solver options (ODE only).atoltakes the same scalar, per-species vector,"auto"or"tracking"asrun()(issues #196, #213); an"auto"vector — including a tracking ceiling — is derived from the state this leg starts from, which for an interactive protocol is where the previous leg ended.atol (
Union[float,Sequence[float],NDArray[double],Literal['auto'],Literal['tracking'],TrackingAtol,ExactAtol,None]) – Solver options (ODE only).atoltakes the same scalar, per-species vector,"auto"or"tracking"asrun()(issues #196, #213); an"auto"vector — including a tracking ceiling — is derived from the state this leg starts from, which for an interactive protocol is where the previous leg ended.max_steps (
int|None) – Solver options (ODE only).atoltakes the same scalar, per-species vector,"auto"or"tracking"asrun()(issues #196, #213); an"auto"vector — including a tracking ceiling — is derived from the state this leg starts from, which for an interactive protocol is where the previous leg ended.
- Returns:
Simulation result for the [current_time, t] interval.
- Return type:
Examples
>>> sim.run_until(t=50) # simulate to t=50 >>> sim.intervene({"k1": 0.0}) # knock out a reaction >>> result = sim.run_until(t=100) # continue to t=100
Notes
A stop condition does not stop this leg early. Conditions are evaluated against the completed result, so the backend integrates the whole
[current_time, t]interval and leaves the model att; theStopConditionMetcarries the trajectory truncated at the trigger point, andcurrent_timereadstrather than the trigger time (issue #553). To continue from the trigger point instead, take asnapshot()first andrestore()it — the partial result cannot be used to rewind the model, because on a model with a promoted state entry it carries only the reported species block.
- intervene(params)[source]¶
Apply a perturbation (parameter change) mid-simulation.
Use between
run_until()calls to modify the model during an interactive simulation session.Examples
>>> sim.run_until(t=50) >>> sim.intervene({"k1": 0.0}) # knock out reaction >>> sim.run_until(t=100) # continue
- save_concentrations(label=None)[source]¶
Snapshot the model’s current concentrations (BNG
saveConcentrations).Thin delegator to
Model.save_concentrations()on the underlying model. With nolabelthis overwrites the default slot (a laterModel.reset()returns here); with alabelit stores a named snapshot thatparameter_scan()can reset each point to viareset_to=label. Use it to capture a post-intervention state betweenrun_untilphases and a following scan (issue #11).
- restore_concentrations(label=None)[source]¶
Restore the model’s concentrations from a snapshot (BNG
resetConcentrations).Thin delegator to
Model.restore_concentrations(). With nolabelthis restores the default slot (identical toModel.reset()); with alabelit restores that named snapshot. The backend simulator is rebuilt so a subsequentrun()/run_until()seeds from the restored state.
- snapshot()[source]¶
Capture the current simulation state.
Returns a dict that can be passed to
restore()to return to this point.- Returns:
Opaque snapshot of model + simulator state.
- Return type:
Examples
>>> sim.run_until(t=50) >>> snap = sim.snapshot() >>> sim.run_until(t=100) >>> sim.restore(snap) # back to t=50
- restore(snapshot=None)[source]¶
Restore simulation state from a snapshot.
A snapshot of a state a run advanced restores the model’s carried-over mark (issue #1528): a sensitivity run from it needs a carried seed, and a later
set_paramof an initial-condition parameter leaves the restored species as they are, as after a run; dose them withModel.set_concentration.- Parameters:
snapshot (
dict|None) – A snapshot fromsnapshot(). IfNone, restores the most recent snapshot from the internal stack.- Return type:
Examples
>>> snap = sim.snapshot() >>> sim.run_until(t=100) >>> sim.restore(snap) # back to snapshot point >>> sim.restore() # same (uses internal stack)
- get_state()[source]¶
Bulk-copy the live species-concentration vector (GH #102).
Thin delegator to
Model.get_state()on the underlying model. After a statefulrun_until/runthe model holds the final state (the ODE/SSA backends write it back), so this returns the post-step state. It is the per-stepgethalf of driving bngsim as a reaction kernel from an external orchestrator; pair withset_state().- Return type:
- set_state(state, *, time=None)[source]¶
Bulk-assign the live species-concentration vector (GH #102).
Thin delegator to
Model.set_state(). The C++ simulator reads the model’s current concentrations as the initial condition at the start of the nextrun_until/run, so a bulkset_statebetween steps is the per-stepsethalf of the kernel exchange (e.g. injecting the SSA-subset coupling species before advancing the ODE subset).time, when given, also setscurrent_time(seeset_time()), so the state and the clock it belongs to move together — what a predictor-corrector loop needs to redo a step. Both are validated before either is assigned, so a rejected call changes neither.
- set_time(t)[source]¶
Set
current_time, the time the stored state is at.The next
run_until()integrates from here, and everything that reads the clock sees it: time-indexed table functions,time()in rate laws and functions. Paired withset_state()(orset_state(state, time=t)) this rolls a stepped simulation back to a saved point — a predictor-corrector coupling loop advances a step, corrects the coupling input, and redoes the step from the saved state and time. Unlikerestore(), it does not rebuild the backend, so the ODE solver’s warm state (and its linear-solver setup) is kept.- Parameters:
t (
float) – The new time; must be finite.- Raises:
ValueError – If
tis not finite.- Return type:
- set_tolerances(rtol=1e-08, atol=None)[source]¶
Set ODE solver tolerances for every subsequent call.
- Parameters:
rtol (
float) – Relative tolerance.atol (
Union[float,Sequence[float],NDArray[double],Literal['auto'],Literal['tracking'],TrackingAtol,ExactAtol,None]) –Absolute tolerance; omitted, each run takes its own per species, never looser than
1e-8(seerun()), and asteady_state(criterion="scaled")march its own (seesteady_state()). A float is read by every run as a ceiling, per species, the same way (seerun()); abngsim.ExactAtolis held exactly (CVodeSStolerances). A sequence ofn_speciesvalues, ordered likeModel.species_names, is a per-species tolerance (CVodeSVtolerances, issue #196);"auto"derives one now from the model’s current state (seeauto_atol()) and pins it, so a later state change does not silently re-derive it. Abngsim.TrackingAtol(or"tracking") installs the error-weight function of issue #213, which re-evaluates that vector against the state being integrated; its ceiling is pinned here for the same reason"auto"is.Each of the three clears the others rather than sitting behind them — the alternative is a call that appears to set the tolerance and changes nothing.
- Raises:
ValueError – If a per-species vector is not
n_specieslong, or holds an entry that is not finite and>= 0(strictly> 0when it is a tracking ceiling).- Return type:
- property requested_method: str¶
Original method token as provided by the user.
Useful for logging and reproducibility metadata. For example, if the user passed
method="nf", this returns"nf"whilemethodreturns the backend dispatch key ("nfsim"fornf/nf_reject).
- property codegen_backend: str¶
The RHS codegen backend this Simulator hands the ODE engine.
Returns one of:
"mir"— in-process MIR micro-JIT (GH #78): the generated C source is JIT-compiled inside C++ (reached only whenBNGSIM_CODEGEN_JIT=miron a MIR-enabled build prepared codegen for this model)."cc"— native C compiled to a.sobyccanddlopen’d (auto-selected at/aboveBNGSIM_CODEGEN_THRESHOLDspecies — 256 by default — or whencodegen=Truewas passed explicitly)."exprtk"— the ExprTk bytecode interpreter, no native code (the default below the codegen threshold).
This is the backend that actually runs, not a request: it reflects exactly what
run()passes the engine — a non-empty JIT source selects MIR, else a non-empty.sopath selects cc, else ExprTk (mirroring theopts.codegen_*dispatch). Only meaningful formethod="ode"; other backends never codegen and report"exprtk".
- property jacobian_strategy: str¶
"analytical","fd", or"jax".This is the post-resolution strategy, not the requested
jacobian=mode. Withjacobian="auto"(the default) the engine uses the analytical Jacobian when the model has one — every Elementary mass-action law, plus Functional laws whose derivatives were symbolically derived within the build-time budget (GH #76/#95) — and finite differences otherwise. So this reports"analytical"only when the analytical Jacobian is genuinely complete and not overridden:jacobian="fd"→ always"fd".BNGSIM_ANALYTICAL_FUNCTIONAL_JAC=0(a Functional model loaded with this set never attaches its analytical terms) →"fd".a derivation that blew the budget and fell back →
"fd".jacobian="auto"whose analytical attempt failed to integrate and fell back to FD at run time (GH #176) →"fd".jacobian="jax"→"jax".
Mirrors the engine’s callback selection exactly (cvode_simulator.cpp): analytical iff the requested mode is auto/analytical and
analytical_jacobian_complete. Only meaningful formethod="ode".- Type:
The Jacobian strategy the ODE engine actually uses
- property last_codegen_sec: float¶
Wall seconds spent generating/compiling this model’s codegen RHS.
≈0.0for an ExprTk model (no codegen runs) or a codegen cache hit; thecccompile time on a cold"cc"model; the source-generation time on a"mir"model. Recorded once at setup by thebngsim._codegen.prepare_*entry points (whether codegen ran at model load or in this Simulator), so a singlerun()exposes the codegen cost directly — no run-twice-and-subtract needed. Purely a setup-time figure; the per-step integration hot path is never instrumented.
- property last_libsbml_parse_sec: float¶
Wall seconds the SBML loader spent in the libSBML parse phase (
readSBML*+ document-level error check) — the shared C++ core both engines use. Recorded once at load; setup-time only, never the hot path.0.0for a model not loaded from SBML (e.g. a.netmodel).
- property last_interpret_sec: float¶
Wall seconds spent interpreting the parsed libSBML document into the internal
_coremodel (bngsim’s Python interpretation layer, including thebuilder.build()core construction; excludes libSBML parse, the analytical-Jacobian derivation, and codegen, which are timed separately). Recorded once at load; setup-time only.
- property last_jacobian_sec: float¶
Wall seconds spent symbolically deriving this model’s analytical Functional Jacobian (GH #76,
sympysp.diff), with SymPy already imported — the one-time SymPy import is process-warmup, measured separately, not here.≈0.0for an all-Elementary model, an FD fallback, an over-budget derivation (GH #95), orBNGSIM_ANALYTICAL_FUNCTIONAL_JAC=0. A bngsim-only per-model cost — RoadRunner uses a difference-quotient Jacobian and has no analog. Setup-time only; the per-step integration hot path is never instrumented. Recorded wherever the derivation runs (at load today; at first ODE-solve setup after the GH #145 lazy deferral), so this accessor is stable across that change.
- property codegen_cache_hit: bool | None¶
Whether this model’s compiled
.sowas reused from the on-disk codegen cache (~/.cache/bngsim/codegen/).True— the.sowas found in the cache and loaded without recompiling (thecccompile was skipped).False— no cached.somatched, so it was compiled fresh.None— no.sowas involved at all: an ExprTk model (no codegen) or a MIR model (in-process JIT, which has no on-disk.socache).
This is the definitive cache signal recorded by the codegen pipeline at the
get_cached_so/ memo branch — not inferred fromlast_codegen_sec(a model-based cache hit still spends time on source generation, so a small wall time does not imply a cache hit). Only meaningful formethod="ode"with the"cc"backend.
- property has_analytic_sens_rhs: bool¶
Whether a forward-sensitivity run on this Simulator uses bngsim’s analytic
∂f/∂prather than CVODES’ internal difference quotient (issue #438).This is a property of the (build, model) pair, not of the build.
CVodeSensInit1takes ONE sensitivity-RHS callback for every column, so a single rate law bngsim cannot differentiate declines the analytic∂f/∂pfor the whole model — there is no per-reaction fallback to mix in — and the difference quotient that replaces it costs one extra RHS evaluation per column per step. An N-parameter fit therefore pays roughly N times the sensitivity cost, which on a fit measured in hours is what ends runs: onSmith_BMCSystBiol2013all 25 columns fell back and every gradient start timed out. Two models on one bngsim get different answers here, and the same model can flip on a derivation-budget timeout (GH #90), so nobngsim.capabilities()key can answer it — the question is per run.Read off the artifact this run installs: it either exports
bngsim_codegen_sens_rhs, the exact symbol the C++ resolves withtry_symbolto choose the analytic RHS, or it does not. That is the ground truth whatever the codegen cache did, and it is the same answer_apply_switch_time_sens()and_raise_if_uncompensated_crossing_sensitivities()already act on.Falseon a Simulator built for a plain solve is not a decline. Since issues #209/#217 the sensitivity RHS is emitted only when a sensitivity is actually requested, so an artifact built forSimulator(model)carries no analytic∂f/∂psimply because nobody asked for one; ask a Simulator built withsensitivity_params=(or aftercompute_all_sensitivities(), which rebuilds), and readsens_rhs_decline_reasonfor the half that says why.A run with no codegen artifact at all —
codegen=False, an interpreted ExprTk RHS — reportsFalsetoo, and correctly: the analytic∂f/∂pis a compiled symbol, so a run that compiles nothing has none and CVODES’ difference quotient carries every column there as well.
- property sens_rhs_decline_reason: str | None¶
Why this model is on CVODES’ difference quotient, or
None(issue #438).Nonemeans one of two things, andhas_analytic_sens_rhsis what separates them: with the analytic∂f/∂pin use there is nothing to report, and without it there is nothing recorded — a build older than issue #438, a cache directory the note could not be written into, or a plain run that never asked for a sensitivity RHS at all. Absence is never evidence that a model is on the analytic path.The reason is what makes the verdict actionable. “uses unsupported construct: abs()” tells a modeller precisely what to re-encode, while “no analytic RHS, expect roughly 25x the sensitivity cost” tells them only to wait or give up.
It also carries the one distinction that is about correctness rather than cost. Where the decline is an
UncompensatedCrossingReasonthe difference quotient does not answer the same question more slowly: it integrates the variational equation smoothly through a branch crossing whose time moves, dropping the saltation jump(f⁻−f⁺)·dt*/dθ, so every column is wrong at and after that crossing (GH #146). The returned string is an instance of that class exactly when it applies, so anisinstancecheck tells “wrong” from “slow”. bngsim itself refuses such a run since GH #414 (_raise_if_uncompensated_crossing_sensitivities()), so on this build the distinction is mostly of interest to a consumer reporting on artifacts built elsewhere.Resolved from the artifact first and the model second. A decline is derived once, while source is being generated, and since issue #174 the codegen cache key is structural — so a warm cache resolves the
.sowith no source generated and no decline derived. The reason is written into a small note beside the artifact for that reason, and a run that hits the cache reads it back; a MIR JIT build, which compiles no.so, has the reason its own codegen recorded on the model instead.
- property current_time: float¶
The time the model’s stored state is at.
Every solve advances it to where that solve left off,
runandrun_untilalike, because the backend writes the end-of-span concentrations back into the model and this names the state the model then holds (issue #553). It is the point a followingrun_untilintegrates from, so it moves even when the solve ends by raising — notably theStopConditionMetraised after the interval was integrated, which truncates the reported result without rewinding the model.parameter_scanrewinds the model when it finishes and puts this back with it.
- class bngsim.ReactionKernel(model, *, method='ode', **simulator_kwargs)[source]¶
Bases:
objectDrive a bngsim model per step as a pluggable reaction kernel (GH #102).
- Parameters:
model (
Model) – The model to drive. Its live species concentrations are the kernel’s state vector (ordered likestate_names).method (
str) – Simulation method passed tobngsim.Simulator. Default"ode". Stepping (advance()) requires a stateful backend (ode/ssa/psa);nfsim/rulemonkeymay be wrapped for introspection but cannot be advanced.**simulator_kwargs – Forwarded verbatim to
bngsim.Simulator(e.g.codegen,jacobian,poplevel,xml_path,sensitivity_params).
See also
ReactionKernel.from_simulatorwrap an already-configured Simulator.
bngsim.Model.get_statethe bulk state primitive the kernel exchanges.
Notes
Not thread-safe — it owns mutable model + simulator state. For parallel workers, build one kernel per
bngsim.Model.clone().- classmethod from_simulator(simulator)[source]¶
Wrap an already-constructed
bngsim.Simulator.Use this when the caller has configured solver options the kernel does not surface directly (custom tolerances via
set_tolerances, a prepared codegen.so, sensitivity parameters, …). The kernel adopts the simulator as-is, including its current interactive time.- Parameters:
simulator (
Simulator) – The simulator to drive.- Return type:
- get_state()[source]¶
Bulk-copy the live species-concentration vector (GH #102).
Returns a fresh
float64array of lengthn_species, ordered likestate_names. After anadvance()this is the post-step state. OneO(n_species)Python call — thegethalf of the per-step kernel exchange.
- set_state(state, *, time=None)[source]¶
Bulk-assign the live species-concentration vector (GH #102).
- Parameters:
state (
NDArray[double]) – 1-D array of lengthn_species, ordered likestate_names. Copied into the model’s live concentrations; the nextadvance()reads them as its initial condition. Thesethalf of the per-step kernel exchange.Also set
timeto this value (Simulator.set_time). With a state saved byget_state()and itstime, this redoes a step — the rollback a predictor-corrector coupling loop needs, and the only way to get it right when the model reads the clock (a time-indexed table function,time()in a rate law):>>> saved, t0 = kernel.get_state(), kernel.time >>> kernel.advance(dt) # predictor >>> kernel.set_state(corrected, time=t0) # roll back, correct >>> kernel.advance(dt) # corrector
- Raises:
ValueError – If
statehas the wrong length ortimeis not finite; the call then changes neither the state nor the clock.- Return type:
- advance(dt, *, n_points=2, seed=None, **run_kwargs)[source]¶
Advance the simulation by
dtand return the post-step state.Integrates (or steps, for stochastic backends) from the current
timetotime + dt, then returnsget_state(). The model is left holding the post-step state, so the typical per-step loop isset_state(...) → advance(dt) → get_state()(or just use the returned array).- Parameters:
dt (
float) – Coupling step. Must be > 0.n_points (
int) – Output points recorded over the step. Default 2 (endpoints only), which is all an orchestrator needs and keeps recording cheap; the full sub-step trajectory is available vialast_result.seed (
int|None) – Random seed for stochastic backends (ssa/psa). Ignored byode.Nonedraws a fresh seed each step.**run_kwargs – Forwarded to
Simulator.run_until(e.g.rtol,atol,max_steps).
- Returns:
The post-step state vector (a copy; ordered like
state_names).- Return type:
- Raises:
ValueError – If
dt <= 0.NotImplementedError – If the backend is network-free (
nfsim/rulemonkey), which has no stateful per-step API.
- reset()[source]¶
Reset to the model’s initial concentrations and
time = 0.Restores species to their initial values (
bngsim.Model.reset()) and rewinds the interactive clock, so the kernel can be re-driven from scratch. Clears any cached step result.- Return type:
- observables()[source]¶
Observable values at the current simulation state.
Returns a
float64array of lengthn_observables, ordered likeobservable_names. Right after anadvance()these are the post-step observables (read straight from the step result, no recomputation). Otherwise — before the first advance, after aset_state()(with or withouttime=), or after an advance that raised having moved the state (a stop condition) — they are computed once, side-effect-free, from the current state via a throwaway model clone.last_resultis left as it was either way.
- property state_names: list[str]¶
Species names, in the order of the
get_state()vector.
- property species_names: list[str]¶
Alias for
state_names.
- property observable_names: list[str]¶
Observable names, in the order of
observables().
- property simulator: Simulator¶
The underlying
bngsim.Simulatordriving the steps.
- property last_result: Result | None¶
The
bngsim.Resultfrom the most recentadvance().Nonebefore the first advance. Carries the full sub-step trajectory of the last step (pern_points), beyond the endpoint stateget_state()returns. It keeps describing that step after aset_state(), when it no longer describes the live state.
- class bngsim.UnitConverter(volume_factors, *, names=None)[source]¶
Bases:
objectBulk count ↔ concentration ↔ amount conversion over the state vector.
Wraps the per-species storage→amount factor vector
volume_factor(V_c;Species.volume_factor, baked at load time) and converts a wholeget_state/set_statestorage vector to and from the units an external orchestrator works in. See the module docstring for the unit algebra; the short version isamount = storage * V_c(volume-invariant) andconcentration = amount / V(needs a volume).Build one with
from_model()(orfrom_kernel()); it is immutable and cheap to keep alongside a kernel.- Parameters:
volume_factors (
ArrayLike) – Per-speciesV_c, ordered like the model’s state vector. Must be finite and > 0.names (
Sequence[str] |None) – Species names parallel tovolume_factors(for error messages andfor_species()). Not required for the array conversions.
- classmethod from_model(model)[source]¶
Gather the per-species
volume_factorvector from a model/kernel.- Parameters:
model (
Model|ReactionKernel)- Return type:
- classmethod from_kernel(model)¶
Alias — a kernel exposes
.model, sofrom_model()already accepts it; kept for symmetry with the rest of the API.- Parameters:
model (
Model|ReactionKernel)- Return type:
- property volume_factors: NDArray[float64]¶
Per-species
V_c(a copy).
- to_counts(storage, *, policy='nearest', rng=None)[source]¶
Storage vector → integer molecule counts (rounded amounts).
- to_concentrations(storage, *, volume=None)[source]¶
Storage vector → concentrations (
storage * V_c / volume).volume=Nonereturns the at-load concentration (==storage). Pass the current compartment volume to report concentrations against a framework-grown compartment whose bakedV_cis now stale.
- from_concentrations(concentrations, *, volume=None)[source]¶
Concentrations → storage vector (
concentration * volume / V_c).Inverse of
to_concentrations(); pass the samevolumeyou would read the concentration against.
- class bngsim.CouplingMap(all_names, shared_names)[source]¶
Bases:
objectName ↔ index addressing of the shared coupling subset within a state vector.
Two coupled subsets share species by name but order their state vectors independently. A
CouplingMappins the index of each shared name in one subset’s vector, so the orchestrator gathers / scatters only the coupling subset and exchanges it in a single canonical order across subsets.- Parameters:
- classmethod from_model(model, shared_names)[source]¶
Build from a model/kernel’s
species_names.- Parameters:
model (
Model|ReactionKernel)
- Return type:
- classmethod from_kernel(model, shared_names)¶
Build from a model/kernel’s
species_names.- Parameters:
model (
Model|ReactionKernel)
- Return type:
- property indices: NDArray[int64]¶
Indices of the shared species into the full state vector (a copy).
Number of shared (coupling) species.
- scatter(state, values, *, copy=True)[source]¶
Write
values(exchange order) into the shared slots ofstate.Returns the updated full vector. With
copy=True(default)stateis not mutated;copy=Falsewrites in place and returns the same array.
- read(source)[source]¶
Pull the shared subset straight from a live model/kernel’s state.
- Parameters:
source (
Model|ReactionKernel)- Return type:
- class bngsim.DiscreteExchange(n_species, *, policy='nearest', dither=True, nonneg=True, rng=None)[source]¶
Bases:
objectExplicit, inspectable rounding at the SSA/NFsim hand-off with leak accounting.
The SSA engine already rounds whatever continuous storage it is handed at the next
advance(round_initial_population_to_storageinsrc/ssa_simulator.cpp), but that entry rounding is implicit and silent — repeatedly injecting continuous amounts and reading integer counts back sheds the fractional remainder every step, which leaks mass over a long hybrid run.DiscreteExchangemakes the boundary explicit and conserving: it carries the per-species fractional residual forward (error feedback / dithering), so the discrete counts track the continuous amounts with bounded, accounted error rather than a silent downward drift.- Parameters:
n_species (
int) – Length of the amount vectors handed across the boundary.policy (
Literal['nearest','floor','ceil','stochastic']) – Per-step rounding rule (RoundingPolicy).dither (
bool) – Carry the fractional residual into the next step (defaultTrue). With dithering the cumulative leak stays bounded by the carry; without it, each step rounds independently and leak can accumulate — setFalseonly when you explicitly want memoryless rounding.nonneg (
bool) – Clamp counts at 0 (defaultTrue) so the SSA boundary never receives a negative population. When dithering, a clamp keeps the unrepresentable deficit in the carry, so conservation accounting is undisturbed.
Examples
>>> dx = DiscreteExchange(3, policy="nearest") >>> counts = dx.discretize([0.4, 0.4, 0.4]) # rounds down this step >>> later = dx.discretize([0.4, 0.4, 0.4]) # carry has built up to 0.8 → rounds up >>> dx.leak # net mass injected vs the continuous input
- discretize(amounts)[source]¶
Round
amountsto integer counts, carrying the residual forward.Returns integer-valued
float64counts of lengthn_species. Updatescarry(dithering),last_residual(this step’scounts - amounts) andleak(cumulative net mass discretization has added or removed since construction /reset()).
- property carry: NDArray[float64]¶
Current per-species fractional residual buffer (a copy).
- property last_residual: NDArray[float64]¶
counts - amountsfrom the most recentdiscretize()(a copy).
- bngsim.round_to_counts(amounts, policy='nearest', *, rng=None)[source]¶
Discretize continuous amounts to integer molecule counts (stateless).
- Parameters:
amounts (
ArrayLike) – Continuous molecule amounts (the volume-invariant exchange currency; useUnitConverter.to_amounts()to get here from storage).policy (
Literal['nearest','floor','ceil','stochastic']) – SeeRoundingPolicy."nearest"rounds half away from zero, matching the SSA engine’s entry rounding so an explicit hand-off and the implicit one agree."stochastic"rounds up with probability equal to the fractional part (unbiased), and needsrng.rng (
Generator|None) – Required forpolicy="stochastic"; ignored otherwise.
- Returns:
Integer-valued
float64counts (same dtype as the state vector, so it feeds straight back throughUnitConverter.from_counts()).- Return type:
- bngsim.round_half_up(value)[source]¶
Round a fractional molecule count to the nearest integer, ties away from zero.
bngsim’s repo-wide stochastic initial-count policy (GH #51):
floor(x + 0.5)forx >= 0andceil(x - 0.5)forx < 0. The two branches differ only at negative half-integers, which never arise for molecule populations. The rule is idempotent on integers (round_half_up(n) == n), so warm-restart paths that re-round already-integer state are no-ops.Examples
>>> [round_half_up(x) for x in (0.389, 0.10001, 5.7, 155.6747, 466.98)] [0, 0, 6, 156, 467] >>> [round_half_up(x) for x in (0.5, 1.5, 2.5)] # ties away from zero, not bankers' [1, 2, 3]
- Parameters:
value (
float) – Continuous molecule count, already in molecule-number space. Any volume / concentration factors must be applied before calling.- Returns:
Nearest integer count.
- Return type:
- Raises:
ValueError – If value is not finite.
- class bngsim.ConservationLedger(weights=None, *, atol=1e-09, rtol=1e-09, name='total')[source]¶
Bases:
objectTrack a conserved moiety across exchange-boundary round-trips (GH #102).
Records the moiety total at a baseline state and on every subsequent state, so an operator-split loop can assert the shared moiety is preserved across
get_state → orchestrator → set_stateexchanges and the discrete rounding.- Parameters:
weights (
ArrayLike|None) – Moiety weight vector (seemoiety_total()). Default: total count.atol (
float) – Absolute / relative tolerance forcheck()andassert_conserved(). The drift bound isatol + rtol * |baseline|.rtol (
float) – Absolute / relative tolerance forcheck()andassert_conserved(). The drift bound isatol + rtol * |baseline|.name (
str) – Label used in error messages.
- record(state)[source]¶
Record the moiety total of
state; set the baseline on first call.Returns the moiety total.
- assert_conserved(state)[source]¶
Record
state; raiseConservationErrorif drift exceeds tol.Returns the signed drift from baseline.
- exception bngsim.ConservationError[source]¶
Bases:
BngsimErrorA conserved moiety drifted beyond tolerance across the exchange boundary.
- bngsim.moiety_total(state, weights=None)[source]¶
Total of a conserved moiety:
weights · state(orstate.sum()).A conserved moiety of a reaction network is a left-null-space vector
wof the stoichiometry matrix;w · nis invariant under the reactions. The defaultweights=Noneis the all-ones vector — the total molecule count of a closed transfer network, the moiety an operator split must not leak.
- class bngsim.Divider(*, method='binomial', rng=None)[source]¶
Bases:
objectPartition molecule counts across daughter cells at division (GH #102).
Cell division is a pure count-space operation with no analogue elsewhere in bngsim: the molecules of each species are dealt out among the daughters, exactly conserving the parent total (
sum(daughters) == parentfor every partitioned species). It composes on top ofUnitConverter— convert storage → counts, divide, then convert each daughter’s counts back to storage viaUnitConverter.from_counts()and inject withset_state. Volume is the orchestrator’s to halve (seeset_compartment_volume()).- Parameters:
method (
Literal['binomial','multinomial','deterministic']) –"binomial"/"multinomial"(synonyms) deal each molecule to a uniformly-random daughter — the physically faithful stochastic split, exactly conserving."deterministic"splits as evenly as possible (floor share + largest-remainder distribution of the leftover), also exactly conserving; reproducible without an RNG.rng (
Generator|None) – Required for the stochastic methods.
- divide(counts, n_daughters=2, *, partition_mask=None)[source]¶
Partition integer
countsinton_daughtersdaughter vectors.- Parameters:
counts (
ArrayLike) – Non-negative, integer-valued molecule counts (e.g. fromUnitConverter.to_counts()).n_daughters (
int) – Number of daughters (default 2).partition_mask (
ArrayLike|None) –Truewhere a species is a partitioned molecule pool (split and conserved);Falsewhere it is shared environment (copied identically to every daughter, not split). Default: partition all.
- Returns:
n_daughtersinteger-valuedfloat64count vectors. For every partitioned species the daughters sum exactly to the parent count.- Return type:
- bngsim.make_subset_model(model, *, keep_reactions=None, fixed_species=None, compute_conservation_laws=False)[source]¶
Build an operator-split subset as its own model (GH #102 Stage 1, #6).
bngsim runs a whole network with one method — there is no native reaction-subset integration. For a static operator split the continuous (ODE) subset is supplied as its own model: the same species namespace, only the subset’s reactions, and the other operator’s species marked
fixed=Trueso the integrator holds them at the boundary values the orchestrator writes each step viaset_state. Thefixedmechanism already clamps a species in both the ODE (zeroed derivative) and SSA (skipped fire) backends — this helper just constructs the partitioned model; no engine change is involved.The full species set is kept (so the two subsets share one addressing space for
CouplingMap); only the reaction set is subset and the boundary species fixed. A species touched by no kept reaction is already inert, sofixed_speciesis needed only for boundary species whose value a kept rate law reads but should not evolve.- Parameters:
model (
Model|ReactionKernel) – The full network to partition.keep_reactions (
Sequence[int] |None) – 0-based indices of the reactions this subset integrates.Nonekeeps all reactions (a pure re-fix with no reaction split).fixed_species (
Sequence[str] |None) – Species names to additionally markfixed(the other operator’s species). Species already fixed in the source stay fixed.compute_conservation_laws (
bool) – Forwarded to the builder (defaultFalse— the dense O(n³) detector is unused by stepping and costly at scale; see GH #102 MVP).
- Returns:
A freshly built subset model at the source’s initial concentrations (taken from a reset clone, so it is immune to a prior simulation having left the source mid-trajectory). Inject a custom starting state with
set_stateon the subset afterwards.- Return type:
- Raises:
NotImplementedError – If the source uses features this reconstruction cannot reproduce faithfully (events, table functions, discontinuity triggers,
amount_valuedspecies, orper_species_volume_scalingreactions). The supported class — mass action, functional, and Michaelis–Menten rate laws, parameters, observables, functions — covers the issue’s ~100K first-order target.
- bngsim.get_compartment_volume(model, name)[source]¶
Read a compartment volume (a model parameter) by name.
There is no
Compartmentobject in bngsim — a compartment volume is a plain parameter, so this isModel.get_param(), named for the coupling use it serves: feeding the current volume intoUnitConverter.to_concentrations()/UnitConverter.from_concentrations()as the live-volume override.- Parameters:
model (
Model|ReactionKernel)name (
str)
- Return type:
- bngsim.set_compartment_volume(model, name, volume)[source]¶
Couple a framework’s volume growth into bngsim’s compartment volume.
Sets the compartment-volume parameter. On the ODE subset this flows through bngsim’s variable-volume machinery natively (the integrator dilutes by the live compartment symbol; GH #74/#85), so growing or halving (at division) a compartment is just this call. On the SSA subset the baked per-reaction
ssa_volume_factordoes not track the new value — the exchange layer compensates by reading concentrations against the live volume (theUnitConverteroverride), but the stochastic propensities using the live volume are Stage 2.- Parameters:
model (
Model|ReactionKernel)name (
str)volume (
float)
- Return type:
- class bngsim.Result(core=None, *, custom_attrs=None, _time=None, _species=None, _observables=None, _expressions=None, _species_names=None, _observable_names=None, _expression_names=None, _solver_stats=None, _species_volume_factors=None, _seed=None, _ic_sensitivity_seed=None, _sensitivities=None, _sensitivity_params=None, _sensitivities_ic=None, _sensitivity_ic_species=None, _observable_sensitivities=None, _expression_sensitivities=None, _observable_sensitivities_ic=None, _expression_sensitivities_ic=None, _reaction_firing_counts=None, _reaction_propensity_integrals=None, _reaction_labels=None, _ssa_diagnostics=None)[source]¶
Bases:
objectSimulation result container.
- Variables:
time (ndarray, shape (n_times,)) – Time points.
species (ndarray, shape (n_times, n_species)) – Species concentrations at each time point.
observables (ndarray, shape (n_times, n_observables)) – Observable values at each time point.
solver_stats (dict) – Solver diagnostics (n_steps, n_rhs_evals, etc.).
- Parameters:
core (ResultCore | None)
_time (NDArray | None)
_species (NDArray | None)
_observables (NDArray | None)
_expressions (NDArray | None)
_species_volume_factors (list[float] | list[list[float]] | None)
_seed (int | None)
_sensitivities (NDArray | None)
_sensitivities_ic (NDArray | None)
_observable_sensitivities (NDArray | None)
_expression_sensitivities (NDArray | None)
_observable_sensitivities_ic (NDArray | None)
_expression_sensitivities_ic (NDArray | None)
_reaction_firing_counts (NDArray | None)
_reaction_propensity_integrals (NDArray | None)
Examples
>>> result = sim.run(t_span=(0, 100), n_points=101) >>> result.time.shape (101,) >>> result.observables.shape (101, 5) >>> result.observables["Atot"] # named access array([100., 99.5, ...])
Notes
A
Resultholds a handle into bngsim’s compiled extension, so it cannot be pickled or deep-copied;save()andResult.load(), or one of the conversions (as_roadrunner(),to_xarray()), carry it across a process boundary instead.copy.copy(result)does work, and the copy shares that handle and the result arrays with the original. That is left working, where the same sharing makescopy.copyrefuse on aModelor aSimulator(issue #643), because aResulthas no setter and no mutating method: no call you can make on the copy writes into the original. Writing into its arrays in place does —copy.copy(result).species[0] = ...changesresult— but so doesresult.species[0] = ..., because every access returns the same live view rather than a fresh array. Take.copy()of a block you mean to modify, whichever name you reach it through.- Parameters:
- property seed: int | None¶
The integer RNG seed actually used for this simulation.
Nonefor deterministic (ODE) results and for results loaded from HDF5 files predating seed exposure. For stochastic methods (SSA, PSA, NFsim, RuleMonkey), this is the integer that was passed down to the backend — equal to theseed=keyword when the caller supplied one, or the freshly drawn integer when the caller passedseed=None(or omitted it). A seed is from 0 to 2**64 - 1, and under NFsim from 0 to 2**32 - 1, the width its generators read, so the seed passed is the seed used (issue #1515).For squeezed batch results, this is the single seed if every underlying sim used the same one, otherwise
None; the per-sim seeds remain accessible on the individualResultobjects fromSimulator.run_batch(..., squeeze=False).
- property stop_condition: str | None¶
The stop condition this run met, by its label, or
None.A condition registered with
Simulator.add_stop_condition()cuts the result at the first sample it holds at:Simulator.run()raisesStopConditionMetwith that result, andSimulator.run_batch()andSimulator.run_replicates()return it in their list, with this set, beside the rows that ran to the end (issue #1369). A condition registered without a label reads as its expression, or a callable as itsrepr().save()andto_xarray()carry it.A cut result holds the reported blocks only, so
staterefuses on it, as it does on the resultSimulator.run()raises with.
- property ic_sensitivity_seed: dict[str, dict[str, float]] | None¶
∂x(0)/∂θ— the initial-condition seeding this run’s gradients carry.The per-run record of the matrix
Model.effective_ic_sensitivity()reports. Prefer that one to build gradient routing: it answers at setup, from model structure alone, with no simulation. Reach for this to see what a particular run used — the only correct answer for a batch or scan over a nonlinear derived initial condition (Rtot = R0*scale), whose coefficients move from point to point, and the one that reportsNonefor a carried-seed phase (below).{species_name: {param_name: ∂x_k(0)/∂θ}}, the forward-sensitivity seed CVODES was actually initialized with for this simulation: the parameter-graph derivative of each species’ initial condition (issues #43/#147), after the issue #113 retirement of a species moved off its declared initial condition and the issue #111Model.declare_ic_sensitivity()overlay.Read this to compose your own initial-condition chain rule without double-counting.
output_sensitivities()withaxis="parameter"is a total derivative:d_param[θ] = (right-hand-side path) + Σ_k (∂x_k(0)/∂θ)·d_ic[x_k]
so for any
θappearing here, the seeding term is already insided_param[θ]and adding anic-axis term for it counts it twice. Aθabsent from this mapping has an RHS-onlyd_paramcolumn, and an initial-condition term your own code supplies is the missing piece (or hand it toModel.declare_ic_sensitivity()and let theparameteraxis carry it instead).Only parameters in
sensitivity_paramsappear — a row the engine computed for a parameter you did not request has no column to describe. Coefficients are evaluated at this run’s parameter values, so a batch or scan over a nonlinear derived initial condition (Rtot = R0*scale) reports a different matrix per point.- Returns:
{}means recorded, and nothing seeds — everyd_paramcolumn is right-hand-side only.Nonemeans not recorded: no parameter sensitivities were requested; the result came from a non-ODE method or an HDF5 file predating this field; a squeezed batch whose rows disagreed (the per-rowResultobjects fromrun_batch(..., squeeze=False)still carry theirs); or the run usedcarry_sensitivities=True, where the seed is the prior phase’sdx/dθand these rows are discarded by the engine (GH #210/#81 — and no double-count can arise there, because anicaxis across a carry boundary is refused outright). The two are deliberately distinct — an empty mapping is a positive statement.- Return type:
dict or None
Examples
>>> sim = Simulator(model, method="ode", sensitivity_params=["R0", "kf"]) >>> r = sim.run(t_span=(0, 10), n_points=11) >>> r.ic_sensitivity_seed {'R(r)': {'R0': 1.0}}
- property time: NDArray[float64]¶
Time points, shape
(n_times,).
- property species: NDArray[float64]¶
Species concentrations, shape
(n_times, n_species).
- property state: NDArray[float64]¶
The full integrator state trajectory, shape
(n_times, n_state).Every core state entry, exactly as the integrator held it and in
bngsim.Model.species_namesorder, so a row is a statebngsim.Model.rhs()andbngsim.Model.jacobian()accept as is (issue #508). It differs fromspecieson two kinds of model. An SBML model whose event assigns a parameter or a compartment promotes the target to a state entry thatspeciesandspecies_namesomit (GH #71), so this has more columns. A model whose reported species column is remapped after the solve — an assignment-rule target reported at the rule’s live value, or a species in an event-resized or rate-ruled compartment reported atamount / V_live(t)(GH #85, #131) — reports the remapped value inspeciesand the stored one here. Elsewhere the two are identical.Only a result returned by a solve carries it: a result loaded from disk or stacked from a batch holds the reported species block alone.
- Raises:
ValueError – If this result was not produced directly by a solve.
- property state_names: list[str]¶
the producing model’s
bngsim.Model.species_names, promoted entries included.- Type:
Names of the
statecolumns
- property observables: _ObservableAccessor¶
Observable values with named access.
Can be used as: -
result.observables→ full array(n_times, n_obs)-result.observables["name"]→ single column(n_times,)
- property expressions: _ObservableAccessor¶
Expression (function) values with named access.
- property has_simulation_data: bool¶
True iff the simulator produced at least one numeric column.
Returns
Falsefor runs that complete without any species, observable, or expression columns — e.g. a BNGL file with asimulateaction but an empty model body. Used by batch harnesses to distinguish “ran and produced data” from “ran and produced nothing meaningful” without an exception path.
- property expression_names: list[str]¶
Expression (function) names (bare, in-memory column keys).
Auto-generated
_rateLawNrate-law intermediates are filtered out; recover them viaraw_expression_names.
- property gdat_expression_names: list[str]¶
Function-column labels for
.gdat/.scanheaders.bngsim emits bare function headers (no
()suffix, issue #58), so this is identical toexpression_names. It is retained for consumers that assemble a BNG-native file header (e.g. a consumer-built.scan) and want an intent-revealing name.
- property raw_expression_names: list[str]¶
Unfiltered function names, including internal
_rateLawN.On a freshly-simulated result these include the auto-generated
_rateLawNrate-law intermediates thatexpression_namesfilters out (the #58 recoverability fix). On a result loaded from HDF5 or assembled from raw arrays, only the filtered (bare) set was persisted, so this returns the same columns asexpression_names.
- property raw_expressions: _ObservableAccessor¶
Unfiltered function values, including internal
_rateLawN.Columns correspond to
raw_expression_names. See that property for the freshly-simulated vs. loaded distinction.
- resolve_outputs(selectors)[source]¶
Resolve typed output selectors to structured metadata.
A selector names one output column of this result. Fitting frontends compare against named outputs (species, observables, global functions) rather than raw column indices; this is the unified, typed lookup that maps a selector string onto the column it refers to. No sensitivity or value computation happens here — it is pure name resolution.
- Parameters:
selectors (
str|Iterable[str]) –One selector or a sequence of them. A single string is treated as a one-element list (so
resolve_outputs("observable:Atot")andresolve_outputs(["observable:Atot"])are equivalent).Accepted forms:
"species:<name>"— a species column."observable:<name>"— an observable column."expression:<name>"— a global-function / expression column. A trailing()is stripped ("expression:foo()"→"expression:foo"), matching the BNG.gdat/.scanheader convention while in-memory column keys stay bare (issue #58).Aliases:
"state:"→"species:","function:"→"expression:".A bare name (no prefix) resolves only if it is unique across species, observables, and expressions. A bare
"foo()"that does not match any column verbatim is retried as the expressionfoo(the function-call convention).
- Returns:
One dict per input selector, in input order, each with keys:
"selector"— the canonical typed selector ("<kind>:<name>"), suitable as a stable downstream key."kind"—"species","observable"or"expression"."name"— the bare in-memory column name (the key used by the named accessors, e.g.result.observables[name])."index"— column index within that kind’s array."column_label"— the label as written to a.gdat/.cdatheader. bngsim emits bare headers (issue #58), so this is currently identical to"name".
- Return type:
- Raises:
ValueError – If a selector uses an unknown kind prefix, is empty, is unresolved, or is a bare name that matches more than one column. Ambiguity errors list every matching typed selector; unresolved errors list the available candidates.
TypeError – If a selector is not a string.
Examples
>>> result.resolve_outputs("observable:Atot") [{'selector': 'observable:Atot', 'kind': 'observable', 'name': 'Atot', 'index': 0, 'column_label': 'Atot'}] >>> [m["selector"] for m in result.resolve_outputs( ... ["species:A()", "function:scaled()", "Btot"])] ['species:A()', 'expression:scaled', 'observable:Btot']
- outputs(selectors)[source]¶
Return the value columns named by selectors.
Thin value accessor on top of
resolve_outputs(): resolves each selector and stacks the named columns into a single array.- Parameters:
selectors (
str|Iterable[str]) – Selectors accepted byresolve_outputs().- Returns:
Shape
(n_times, n_outputs)for a single-simulation result, with one column per selector in input order. For a stacked batch result (3-D arrays) the shape is(n_sims, n_times, n_outputs). An empty selector list yields a(n_times, 0)array.- Return type:
- Raises:
ValueError, TypeError – Propagated from
resolve_outputs().
- output_sensitivities(selectors, *, axis='parameter')[source]¶
Return
d(named output)/dθfor each selector, stacked.Selector-addressed companion to
outputs(): resolves each selector and stacks the matching sensitivity slice — the chain-rule derivative of that output column with respect to the sensitivity parameters (default) or the differentiated initial conditions.Observable sensitivities are computed at simulation time from the CVODES species sensitivities via the linear chain rule
d obs_j/dθ = Σ_i c_ji·dx_i/dθ(GH #197);expression:selectors carry the codegen output-sensitivity chain rule for global functions (GH #198).species:selectors read the species sensitivities directly — except for an SBML AssignmentRule-target species, whose reported value is the rule’s live value (its state slot is frozen), so its output sensitivity follows the assignment expression: the sensitivity of the rule’s observable (linear-on-species rule) or function (everything else), GH #205. Since GH #221 thesensitivitiestensor carries that same redirected row, so this method and the tensor agree for those species; the redirect is kept here because it raises by name where the tensor can only holdNaN(an expression whose output sensitivity a construct declines). A rule target in a compartment whose size changes is read from the tensor, whose row has the size’s share in it (issue #787), and is refused by name where that row isNaN.The two axes are not independent (GH #155)
axis="parameter"is the total derivativedy(t)/dθ: it carries every path by whichθreaches the trajectory, the right-hand side and the initial-condition seeding∂x(0)/∂θ(issues #43/#147).axis="ic"is∂y(t)/∂x_k(0)with the initial value held as an independent variable. So for a parameter that seeds an initial condition the two overlap:d_param[θ] = (right-hand-side path) + Σ_k (∂x_k(0)/∂θ)·d_ic[x_k]
Do not sum the axes. A consumer that routes a fitted parameter to several columns and adds them must add an
icterm only for the part of∂x(0)/∂θthis engine does not already carry — whichic_sensitivity_seedreports exactly, per run. The common case that makes the overlap easy to miss: a parameter appearing only in the initial condition, with coefficient 1, gives aparametercolumn bit-identical to the seeded species’iccolumn (same seed vector, same variational equation), so summing reports it at exactly 2×.On
SteadyStateResultthe question does not arise: thatparameteraxis is the implicit-function derivative of the algebraic system, there is no seeding term in it, andaxis="ic"raises.- Parameters:
selectors (
str|Iterable[str]) – Selectors accepted byresolve_outputs().axis (
str) – Which sensitivity axis to return."parameter"(default) givesd output/dpoversensitivity_params— the total derivative, seeding included, see above;"ic"givesd output/dY(0)oversensitivity_ic_species.
- Returns:
Shape
(n_times, n_outputs, n_axis)for a single-simulation result ((n_sims, n_times, n_outputs, n_axis)for a stacked batch), one slice per selector in input order.n_axisis the number of sensitivity parameters (or IC species). An empty selector list yields a(n_times, 0, n_axis)array.- Return type:
- Raises:
ValueError – If
axisis invalid; if the requested sensitivity axis was not computed for this result; or if a selector names a kind whose sensitivities are unavailable (e.g. anexpression:selector before GH #198).TypeError – Propagated from
resolve_outputs().
Examples
>>> sim = Simulator(model, method="ode", sensitivity_params=["k1"]) >>> result = sim.run(t_span=(0, 10), n_points=11) >>> result.output_sensitivities("observable:Atot").shape (11, 1, 1)
- fisher_information(sigma=1.0, *, outputs=None, axis='parameter')[source]¶
Compute the Fisher Information Matrix from sensitivity data.
The FIM quantifies how much information the simulated output trajectories carry about each parameter. It is a pure function of the model + output-sensitivity tensor — no measurement data, no residuals, no objective — so it measures the model’s practical identifiability (sloppiness), independent of any fit. It is computed as:
\[\text{FIM} = \sum_t \left(\frac{\partial Y}{\partial \theta}\right)^T \Sigma^{-1} \left(\frac{\partial Y}{\partial \theta}\right)\]where \(\partial Y / \partial \theta\) is the
(n_outputs, n_params)sensitivity matrix at time t, and \(\Sigma\) is the diagonal output-noise covariance.By default the FIM is built over species (back-compatible with the original species-only method). Pass outputs to build it over named outputs — any mix of
species:/observable:/expression:selectors (GH #202) — using the output-sensitivity tensor (GH #197/#198). For the richer eigenvalue / identifiability readout, seeidentifiability().Unlike the gradient methods, the FIM weights every output it is built over, so there is no unweighted row to drop: a model with a refused assignment-rule row (
ar_sensitivity_refused, GH #221) returns aNaNmatrix by default. Name the outputs you want in outputs to build it over the rows that are known.- Parameters:
sigma (
float|NDArray[double]) –Output-noise standard deviation used only to scale the FIM (it is not measurement data). Defaults to
1.0(unscaled).scalar — homogeneous: every output shares the same σ.
1-D array — per-output σ. Length must match the number of outputs:
n_specieswhen outputs isNone, otherwise the number of selectors.
outputs (
str|Iterable[str] |None) – Output selectors (seeresolve_outputs()) to build the FIM over.None(default) uses every species — the original species-only behaviour.axis (
str) – Sensitivity axis:"parameter"(default) builds the FIM oversensitivity_params;"ic"oversensitivity_ic_species.
- Returns:
Symmetric positive-semidefinite FIM over
nparameters (or IC species, foraxis="ic").- Return type:
- Raises:
ValueError – If the requested sensitivity axis was not computed (run with
sensitivity_params/sensitivity_ic); if sigma’s shape is wrong; if axis is invalid; or if this is a batch result (the FIM is defined per single simulation — iterate replicates).
Notes
The FIM is the Cramér–Rao lower bound on the inverse of the parameter covariance: \(\text{Cov}(\hat\theta) \geq \text{FIM}^{-1}\). Large diagonal entries indicate identifiable parameters; small entries indicate practical non-identifiability.
np.linalg.cond(fim)(orIdentifiabilityReport.condition_number) gauges the overall identifiability of the parameter set.Examples
>>> sim = Simulator(model, method="ode", ... sensitivity_params=["k1", "k2"]) >>> result = sim.run(t_span=(0, 100), n_points=101) >>> fim = result.fisher_information(sigma=0.1) # species >>> fim.shape (2, 2) >>> result.fisher_information(outputs=["observable:Atot"]).shape (2, 2)
- identifiability(sigma=1.0, *, outputs=None, axis='parameter', rtol=None)[source]¶
Model-identifiability readout from the Fisher Information Matrix.
Builds the FIM (see
fisher_information()) and returns its eigen-decomposition with practical-identifiability flags: which parameter combinations are well-constrained by the model and which are “sloppy” (near-zero eigenvalues / practically non-identifiable). This is sensitivity analysis of the model — no data, no residuals, no objective.- Parameters:
sigma (
float|NDArray[double]) – Output-noise σ; seefisher_information(). Defaults to1.0.outputs (
str|Iterable[str] |None) – Output selectors to build the FIM over;None(default) uses every species.axis (
str) – Sensitivity axis; seefisher_information().rtol (
float|None) – Relative eigenvalue cutoff for flagging non-identifiable / sloppy directions: a direction is flagged when its eigenvalue is belowrtol * lambda_max. Defaults ton * eps(NumPy’s numerical-rank tolerance), which flags only numerically singular directions; pass a larger value (e.g.1e-6) to surface sloppy-but-nonzero directions.
- Returns:
Eigenvalues/eigenvectors, numerical rank, condition number, per-direction identifiability flags, and the Cramér–Rao bound
FIM⁻¹(all-NaN + a warning when the FIM is rank-deficient).- Return type:
Examples
>>> report = result.identifiability(sigma=0.1) >>> report.rank, report.condition_number (2, 3.4) >>> report.non_identifiable_directions []
- gradient(loss_fn)[source]¶
Compute parameter gradient from sensitivity data and a loss function.
Given forward sensitivities \(\partial Y / \partial p\) and a user-supplied loss function that returns the per-species loss gradient \(\partial L / \partial Y\) at each time point, computes the parameter gradient:
\[\nabla_p L = \sum_t \left(\frac{\partial Y}{\partial p}\right)^T \frac{\partial L}{\partial Y}(t)\]This enables gradient-based optimization via
scipy.optimize.minimize(method='L-BFGS-B').- Parameters:
loss_fn (
Callable[[NDArray[double],NDArray[double]],NDArray[double]]) –A function with signature
loss_fn(species, time) -> dL_dYwhere:species—species, the(n_times, n_species)array of species valuestime—(n_times,)array of time pointsreturns
dL_dY—(n_times, n_species)array of \(\partial L / \partial Y_{i,t}\).
dL_dYis contracted againstsensitivities, whose species axis is the same onespeciesis indexed on (GH #202), so adL_dYshaped like thespeciesargument always fits.Common example (sum-of-squares vs data):
def loss_fn(species, time): return 2 * (species - data) # dL/dY for SSE
- Returns:
Parameter gradient \(\nabla_p L\), one entry per column of
sensitivities, ordered likesensitivity_params. That list — notModel.param_names— is what the returned vector is aligned with, and it is what an optimizer’s parameter vector must be built from (issue #203; see Notes).- Return type:
- Raises:
ValueError – If sensitivity data is not available.
TypeError – If
loss_fnis not callable.
Examples
>>> # Sum-of-squares loss vs observed data >>> data = np.random.randn(101, 2) # observed data >>> result = sim.compute_all_sensitivities( ... t_span=(0, 10), n_points=101 ... ) >>> grad = result.gradient( ... lambda species, time: 2 * (species - data) ... ) >>> grad.shape (n_params,)
>>> # Use with scipy L-BFGS-B. The fitted vector is the *default* >>> # sensitivity column set, so `grad` lines up with `p_vec`. >>> from scipy.optimize import minimize >>> names = model.primary_param_names >>> def objective(p_vec): ... model.set_params(dict(zip(names, p_vec))) ... model.reset() ... result = sim.compute_all_sensitivities(...) ... assert result.sensitivity_params == names ... loss = np.sum((result.species - data)**2) ... grad = result.gradient( ... lambda sp, t: 2 * (sp - data) ... ) ... return loss, grad >>> x0 = [model.get_param(n) for n in names] >>> minimize(objective, x0, method='L-BFGS-B', jac=True)
Notes
The columns must be independent coordinates, and under the ``compute_all_sensitivities()`` default they are (issue #203). This method contracts the whole parameter axis into one
(n_params,)vector, so it is only a gradient if no two columns describe the same physical effect. A derived parameter’s column does describe an effect the primaries underneath it already carry —_rateLaw1 = chi*konreaches the trajectory only throughchiandkon, whose own columns are total derivatives through it — so summing all three counts that effect twice.compute_all_sensitivities(params=None)drops the derived columns for this reason and warns; asensitivity_params=list you wrote yourself is your own to keep independent, and if it names both a derived parameter and a primary underneath it, do not hand this vector to an optimizer over both. (Same shape as issue #155 one level down: theparameteraxis of an initial-condition sensitivity is a total derivative and must not be summed with theicaxis.)The gradient computation is O(n_times × n_species × n_params) — a single matrix multiply per time point. With parallel
compute_all_sensitivities(), the total cost of computing both the loss value and its gradient is ~1.2× a plain ODE solve (with sufficient cores).
- sse_gradient(data, *, target='species', species_indices=None, observable_indices=None)[source]¶
Compute SSE loss and parameter gradient in one call.
Sum-of-squared-errors: \(L = \sum_{t,i} (Y_{t,i} - D_{t,i})^2\).
This is the most common objective in parameter estimation. The analytical derivative is \(\partial L / \partial Y = 2(Y - D)\), so no user-supplied
loss_fnis needed.- Parameters:
data (
NDArray[double]) – Observed data, of the shape of whattargetnames (or of the columnsspecies_indices/observable_indicesselect).target (
str) – What the data is compared with:species, throughsensitivities, orobservables, throughsensitivities_observables(issue #1363). Named, not read from the data’s shape, which is the same for both when a model has as many observables as species.species_indices (
list[int] |None) – If given, only these species columns are used for the loss (target="species"). Sensitivities for all parameters are still included.observable_indices (
list[int] |None) – The same, over observables (target="observables").
- Returns:
(loss, gradient)where loss is the scalar SSE and gradient is shape(n_params,).- Return type:
- Raises:
ValueError – If sensitivity data is missing,
targetis neither, or shapes don’t match.
Examples
>>> result = sim.compute_all_sensitivities(...) >>> loss, grad = result.sse_gradient(observed_data) >>> loss, grad = result.sse_gradient(observed_obs, target="observables") >>> # Use with scipy L-BFGS-B: >>> minimize(objective, x0, method='L-BFGS-B', jac=True)
- chi2_gradient(data, sigma, *, target='species', species_indices=None, observable_indices=None)[source]¶
Compute chi-squared loss and parameter gradient.
\(\chi^2 = \sum_{t,i} \left(\frac{Y_{t,i} - D_{t,i}}{\sigma_i}\right)^2\)
Equivalent to weighted SSE with weights \(1/\sigma_i^2\). The derivative is \(\partial L / \partial Y_{t,i} = 2(Y - D)/\sigma_i^2\).
- Parameters:
data (
NDArray[double]) – Observed data, of the shape of whattargetnames.sigma (
float|NDArray[double]) –Measurement noise standard deviation.
scalar — same σ for all columns.
1-D array, shape ``(n_columns,)`` — per-column σ.
2-D array, shape ``(n_times, n_columns)`` — per-point σ.
target (
str) – What the data is compared with, as insse_gradient().species_indices (
list[int] |None) – Subset of species columns to include.observable_indices (
list[int] |None) – Subset of observable columns to include.
- Returns:
(chi2, gradient)where chi2 is the scalar value and gradient is shape(n_params,).- Return type:
Examples
>>> loss, grad = result.chi2_gradient(data, sigma=0.1) >>> loss, grad = result.chi2_gradient(data, sigma=per_species_sigma)
- neg_log_likelihood_gradient(data, sigma, *, target='species', species_indices=None, observable_indices=None)[source]¶
Compute negative Gaussian log-likelihood and gradient.
\(-\ln \mathcal{L} = \frac{1}{2} \sum_{t,i} \left[\left(\frac{Y_{t,i} - D_{t,i}}{\sigma_i}\right)^2 + \ln(2\pi\sigma_i^2)\right]\)
The constant term \(\ln(2\pi\sigma^2)\) doesn’t affect the gradient but is included in the loss value for correct log-likelihood.
- Parameters:
data (
NDArray[double]) – Observed data, of the shape of whattargetnames.sigma (
float|NDArray[double]) – Measurement noise standard deviation (scalar, per-column, or per-point).target (
str) – What the data is compared with, as insse_gradient().species_indices (
list[int] |None) – Subset of species columns.observable_indices (
list[int] |None) – Subset of observable columns.
- Returns:
(nll, gradient)— negative log-likelihood and gradient.- Return type:
Examples
>>> nll, grad = result.neg_log_likelihood_gradient(data, sigma=0.1)
- property sensitivities: NDArray[float64]¶
Forward sensitivity data dY/dp.
Shape
(n_times, n_species, n_params)when sensitivities are available, or empty(0, 0, 0)otherwise.The species axis is the same axis as
species/species_names: row i isd species_names[i]/dp, so adL/dYbuilt fromspeciescontracts against this tensor row-for-row (gradient(),sse_gradient(), …). On a model where an SBML parameter or compartment is an event-assignment target, bngsim promotes that symbol to full integrator state but does not report it as a trajectory column (GH #71); its row is likewise absent here, and its derivative isoutput_sensitivities("observable:<name>")— the loader gives every promoted symbol a same-named observable (GH #202).Row i is the derivative of the value in column *i*, which for an SBML AssignmentRule-target species is the rule’s live value, not the frozen state slot the integrator carries (GH #221): those rows are the chain rule through the assignment, the same quantity
output_sensitivities("species:<name>")returns. Where that chain rule is unavailable the row isNaN— never a structural zero, which no consumer could tell from a measured one — and the run warns naming the species (ar_sensitivity_refused).Example
>>> sim = Simulator(model, method="ode", ... sensitivity_params=["k"]) >>> result = sim.run(t_span=(0, 10), n_points=11) >>> result.sensitivities.shape (11, 2, 1) >>> result.sensitivities[-1, 0, 0] # dA/dk at t=10
- property sensitivities_ic: NDArray[float64]¶
Forward IC sensitivity data dY/dY(0).
Shape
(n_times, n_species, n_ic_species)when IC sensitivities are available, or empty(0, 0, 0)otherwise. Columnkis∂Y(t)/∂Y_k(0)where the ordering ofkmatchessensitivity_ic_species. The row axis isspecies_names, exactly as insensitivities(GH #202).
- property reaction_firing_counts: NDArray[float64]¶
Cumulative firings of each reaction at each output time,
N_r(t).Shape
(n_times, n_reactions)for a single run and(n_sims, n_times, n_reactions)for a stacked batch, or empty(0, 0)unless the run was made withSimulator(model, method="ssa", reaction_stats=True). Columnris labelled byreaction_labels. Integer-valued, held as float64 so it subtracts directly fromreaction_propensity_integrals:N_r(t) − ∫₀ᵗ a_r dsis the compensated counting process of reactionr, whose mean over replicates is zero, and whichbngsim.girsanovturns into likelihood-ratio scores for the rate constants.
- property reaction_propensity_integrals: NDArray[float64]¶
Integrated propensity of each reaction at each output time,
∫₀ᵗ |a_r| ds.Same shape and labelling as
reaction_firing_counts. The integrand is the propensity magnitude the sampler used to draw the reaction — a reaction whose rate law goes negative fires in reverse at|a_r|(GH #110) — so the block describes the process as sampled.
- property reaction_labels: list[str]¶
Labels for the reaction axis of the per-reaction statistics.
"R<index> (A + B -> C)": the.netfile’s 1-based reaction index, then the reaction written in species names,0standing for no reactant or no product. Empty unless the statistics were recorded.
- property has_reaction_stats: bool¶
Whether per-reaction firing counts and propensity integrals were recorded.
- property sensitivities_species: NDArray[float64]¶
Alias for
sensitivities(species forward sensitivities).Provided so the four output-sensitivity blocks have a parallel, self-describing name on the same object:
sensitivities_species,sensitivities_observables,sensitivities_expressions(+ the_icvariants). Identical array tosensitivities.Every row is the derivative of the value the matching
speciescolumn reports. For an SBML AssignmentRule-target species that is the rule’s live value: GH #205 routedoutput_sensitivities("species:…")through the rule, and GH #221 put the same row in this tensor, because the raw integratedySof the frozen slot it used to hold is identically zero — not a low-level escape hatch, just a silent zero thatgradient()read as a flat objective.
- property ar_sensitivity_refused: frozenset[str]¶
assignment-rule targets, and amounts in a compartment whose size’s derivative is not to be had.
An AR-target species’ sensitivity row is the chain rule through its assignment (GH #221). Where that is unavailable the row is
NaNrather than left at the structural zero no consumer could distinguish from a measurement, and the run warns naming these species. Two causes: codegen declined the rule’s own output sensitivity (GH #198 refuses anif()conditional or a table function rather than guess — the common one), or the target is an amount in a compartment whose size changes and codegen declined the output sensitivity of the size’s own rule (issue #787).Also listed: an amount-valued species (
hasOnlySubstanceUnits) that is no rule’s target, in a compartment an assignment rule resizes, where the rule’s own output sensitivity is not available. Its row is the quotient rule through the compartment’s size (issue #742), and without that derivative it isNaN.output_sensitivities()returns that row, where for an assignment-rule target it raises.Membership means some entry of the row is
NaN, not necessarily all of them.gradient()and the built-in objective gradients drop these rows whereverdL/dYis exactly zero, so a fit that never scores these species still gets a number; an entry the loss does weight staysNaN.output_sensitivities()raises for an assignment-rule target among them, with the specific reason.Empty on every model that resolves: 590 of the 639 AR rows reachable across the 215 rr_parity corpus models that run under sensitivities.
- Type:
Species whose sensitivity row carries
NaN
- property sensitivities_observables: NDArray[float64]¶
Observable parameter sensitivities
d observable / dp.Shape
(n_times, n_observables, n_params)when computed, or empty(0, 0, 0)otherwise. The row axis matchesobservable_names(the loader’s internal network-rewrite observables are dropped here as well, GH #202) and the parameter axis matchessensitivity_params.
- property has_sensitivities_observables: bool¶
Whether observable parameter sensitivities are available.
- property sensitivities_expressions: NDArray[float64]¶
Expression (function) parameter sensitivities
d expression / dp.Shape
(n_times, n_expressions, n_params)when computed, or empty(0, 0, 0)otherwise. The parameter axis matchessensitivity_params.
- property has_sensitivities_expressions: bool¶
Whether expression parameter sensitivities are available.
- property sensitivities_observables_ic: NDArray[float64]¶
Observable IC sensitivities
d observable / dY(0).Shape
(n_times, n_observables, n_ic_species)when computed, or empty(0, 0, 0)otherwise. The row axis matchesobservable_names(GH #202) and the IC axis matchessensitivity_ic_species.
- property sensitivities_expressions_ic: NDArray[float64]¶
Expression (function) IC sensitivities
d expression / dY(0).Shape
(n_times, n_expressions, n_ic_species)when computed, or empty(0, 0, 0)otherwise. The IC axis matchessensitivity_ic_species.
- property solver_stats: dict[str, int]¶
Solver diagnostics dict.
Keys:
n_steps,n_rhs_evals,n_jac_evals,n_jac_rhs_evals,n_jac_formations,n_err_test_fails,n_nonlin_iters,n_nonlin_conv_fails,n_sens_err_test_fails,n_sens_nonlin_conv_fails.n_rhs_evalsis CVODE’s count of the integration’s own evaluations.n_jac_rhs_evalscounts those spent forming Jacobians by difference quotients (issue #1333); it is 0 where every Jacobian is closed-form.n_jac_evalscounts the linear solver’s setups, which can reuse a Jacobian;n_jac_formationscounts the Jacobians the solver asked for, one that a failed right-hand-side call cut short included.
- property ssa_diagnostics: dict[str, Any]¶
SSA boundary diagnostics dict (GH #110).
The exact SSA evaluates rate laws literally: it does not floor species at zero, and a reaction whose rate law evaluates negative fires in reverse (propensity
|rate|) so the SSA mean tracks the ODE. Both events are surfaced here instead of being silent.Keys:
n_negative_crossings(int): times a species count crossed from>= 0to< 0(no floor applied).first_negative_species(str): name of the first species to go negative;""if none.n_reverse_fires(int): reactions fired in reverse because their rate law was negative.first_reverse_reaction(str): label of the first reaction reversed;""if none.n_rounded_populations(int): molecule counts that were not whole numbers when the run (orrun_untilleg) started and were rounded to one. Event-assigned parameters and compartments and rate-rule targets hold continuous values and are never rounded (issue #718).first_rounded_species(str): name of the first rounded species;""if none.
All zero/empty on every non-SSA backend.
- property psa_diagnostics: dict[str, Any]¶
PSA partial-scaling diagnostics dict (GH #15).
A cheap audit layer for a partial-scaling run: per-reaction, time- integrated scaling statistics plus a measured event-rate speedup. These are measured accept/reject signals for a fixed PSA run, not a certification of accuracy. Inactive (
active=False, all zero/empty) on exact SSA and every non-SSA backend.For reaction
r, the integer leap multiplier ism_r(N) = max(1, floor(N'_min / N_c))(N'_min= smallest participating population,N_c=poplevel),a_rits unscaled propensity, andTthe simulated horizon.Keys:
active(bool): whether this run used partial scaling.reaction_index(list[int]): 1-based reaction indices, parallel to every per-reaction list below (matches.netreaction numbering).time(float):T, the simulated horizon (integral denominator).mbar(list[float]): dwell-time-averaged multiplierm̄_r = (1/T)∫ m_r dt. A non-scaling reaction reports1.0.qexc(list[float]): propensity-weighted excessq̄_r = (1/T)∫ (m_r−1)·a_r dt— the correct per-reaction statistic for attributing variance inflation (E[m·a] ≠ E[m]·a(E[N])), and the source term for an affine/monomolecular covariance correction.mbar_integral/qexc_integral(list[float]): the raw∫ m_r dtand∫ (m_r−1)·a_r dt(divide bytimefor the averages; kept so a transient correction can use the un-normalized integral).exact_event_integral(float):∫ Σ_r a_r dt(would-be exact event count).scaled_event_integral(float):∫ Σ_r a_r/m_r dt(actual scaled event count).speedup(float): measured per-path event-rate ratioŝ = exact/scaled ≥ 1, computable from the scaled run alone. It is not a wall-clock speedup and ignores correction-estimator variance.peak_population(float): running max over the populations (counts) participating in scaling-eligible reactions.activation_crossed(bool): whetherpeak_population ≥ 2·poplevel. Scaling can only engage past2·poplevel; if this isFalseno channel ever scaled and the run was pathwise identical to exact SSA.
To turn
speedupinto a cost/precision go/no-go, pair it with the observable’s variance-inflation ratioρ(from a calibration/anchor run) viabngsim.psa_cost_decision().On a stacked batch (
squeeze(),squeeze=True) the per-run statistics are not combined into one:activeis whether any run used partial scaling,peak_populationthe largest andactivation_crossedwhether any run crossed, and the per-reaction lists, the integrals andspeedupare left empty (NaN). Each run’s own report is inpsa_diagnostics_per_run.
- property psa_diagnostics_per_run: list[dict[str, Any]]¶
Each run’s
psa_diagnostics, in the order the runs were stacked.One report for a single run; one per row for a stacked batch (
squeeze()), each exactly as that run reported it.
- property dataframe¶
Return a pandas DataFrame with time + observables.
Requires pandas (
pip install bngsim[pandas]).- Returns:
Columns:
time, then one per observable.- Return type:
- Raises:
ImportError – If pandas is not installed.
- property xr: _XarrayAccessor¶
Per-field xarray
DataArrayaccessor (AMICI-style).Each attribute access (
result.xr.species,result.xr.observables, etc.) builds a freshxarray.DataArraywith labeled coords. Mirrors AMICI’srdata.xr.x/.y/.sxergonomics so downstream code accustomed to that ecosystem can slice by name:>>> result.xr.species.sel(state="A").values >>> result.xr.observables.sel(observable="A_tot") >>> result.xr.sensitivities.sel(parameter="k1", state="A")
Available fields (empty when the corresponding block has zero width):
species— dims(time, state)observables— dims(time, observable)expressions— dims(time, expression)sensitivities— dims(time, state, parameter)(only when sensitivities were computed)sensitivities_ic— dims(time, state, ic_state)(only when IC sensitivities were computed)
Requires the optional xarray dependency (
pip install xarray). For a one-shotxarray.Datasetof every field with a sharedtimecoord, seeto_xarray().- Raises:
ImportError – If xarray is not installed (raised on first attribute access).
AttributeError – If the requested field is not a known xarray-exposable block, or if it has zero width.
- to_xarray()[source]¶
Bundle every field into a single
xarray.Dataset.One-call complement to
xr. The returned Dataset has a sharedtimecoord and exposesspecies,observables,expressions, and (when present)sensitivities/sensitivities_icas data variables, each with the same labeled coords as the per-field DataArrays onxr.custom_attrsis mirrored ontods.attrs; the stochasticseed(when present) is stored asds.attrs["seed"], and thestop_conditionthe result was cut at asds.attrs["stop_condition"].- Returns:
A Dataset with all populated trajectory fields as data vars.
- Return type:
xarray.Dataset
- Raises:
ImportError – If xarray is not installed.
RuntimeError – If the result holds 3-D batch arrays. Iterate per-replicate and call
to_xarrayon eachResult.
Examples
>>> ds = result.to_xarray() >>> ds["species"].sel(species="A").plot() >>> ds.to_netcdf("result.nc") # archive via xarray's writer
- as_roadrunner(selections=None)[source]¶
Return a
bngsim.NamedArraymimickingrr.simulate.Drop-in shape replacement for libRoadRunner’s
simulate(...)return value: a 2-D array whose columns are labeled per selections. Used to swap RR for BNGsim in PyBNF stochastic- fitting workflows without changing call sites.- Parameters:
selections (
list[str] |None) –Column selectors to include, in order. Each entry is one of:
"time"— the time column."[X]"— the concentration of speciesX(BNGsim’s stored species value, which equalsamount/V_cuniformly per Phase 2)."X"— the amount of speciesX(concentration ×V_c); equals the stored value when the species’s compartment volume is 1 (the default and the .net case).
When None, defaults to RoadRunner’s
["time"] + [f"[{s}]" for s in species_names].- Returns:
Shape
(n_times, len(selections)); columns ordered to match selections. The result subclassesnumpy.ndarray, so all numpy operations work. Indexing carries the column names over —arr[:, 1:]is labeled with the columns the slice kept — and any other transform (arithmetic, a transpose, a reduction) returns an array with no names at all, so a lookup by name on it raises rather than answering with a column that has moved (issue #561). The names also survive a pickle round trip, so an array collected from a worker process arrives labeled (issue #629).- Return type:
- Raises:
ValueError – If a selector is unrecognized or names a species not in the model, or if it asks for an amount (
"X") from a result loaded from a file that does not record compartment sizes (one written before bngsim saved them, issue #787).RuntimeError – If the result is a stacked batch (3-D species, from
squeeze()orrun_batch(..., squeeze=True)); call this on each row’s Result fromrun_batch(..., squeeze=False).
- Warns:
UserWarning – When a
"[X]"column is asked of a result loaded from such a file: it is the stored value, which is not the concentration where a compartment’s size changes during a stochastic run, or for an amount-valued species an event resizes.
Examples
Replace
rr.simulate(0, 100, 101)(RoadRunner) with the BNGsim equivalent:sim = bngsim.Simulator(model, method="ssa") arr = sim.run(t_span=(0, 100), n_points=101).as_roadrunner() # arr has columns ["time", "[X]", "[Y]", ...] x_trace = arr["[X]"]
Custom selections:
arr = result.as_roadrunner( selections=["time", "[X]", "X", "[Y]"] )
- summary()[source]¶
Return a compact, JSON-serializable description of this result.
The HPC scheduler-free contract (GH #203) pairs a serializable
EvaluationSpec(what was run) with a lightweight summary (what came back), so a cluster driver can index/log/checkpoint thousands of evaluations without re-reading every full HDF5 payload (usesave()/load()for the complete arrays). The summary carries shapes, output names, sensitivity availability, solver diagnostics, and the seed — every value a built-in JSON-encodable type.- Returns:
Keys:
version(bngsim version),is_batch,n_sims(Noneunless batch),n_times,t_start/t_end(Nonefor an empty time grid),shapes(per-array shape lists),species_names/observable_names/expression_names, thehas_sensitivities*flags,sensitivity_params,sensitivity_ic_species,solver_stats, andseed.- Return type:
Examples
>>> import json >>> s = result.summary() >>> json.dumps(s) # always succeeds '...'
- save(path)[source]¶
Save result to HDF5 file.
- Parameters:
- Raises:
ImportError – If h5py is not installed.
- Return type:
- classmethod load(path)[source]¶
Load result from HDF5 file.
- Parameters:
- Returns:
Loaded result.
- Return type:
- Raises:
ImportError – If h5py is not installed.
FileNotFoundError – If the file does not exist.
Notes
A file written before bngsim saved compartment sizes (issue #787) loads, but does not say what the sizes were.
as_roadrunner()on it refuses a bare-id amount selector and warns on a"[X]"one; the arrays themselves are as they were saved.
- to_gdat(path, *, print_functions=False, print_rate_laws=False)[source]¶
Export observables in BNG
.gdatformat.Function-column headers are always bare (no
()suffix) and identical across every simulation method (issue #58).- Parameters:
print_functions (
bool) – When True, append the user-named function columns after the observables (auto-generated_rateLawNrate-law columns are still omitted). Default False keeps the observables-only output, matching BNG2.pl’s behavior whenprint_functions=>1is not set.print_rate_laws (
bool) – When True, additionally append the auto-generated_rateLawNrate-law columns (bare names). These are internal rate-law intermediates, omitted by default. On a result loaded from HDF5 or assembled from raw arrays they are unavailable (only the filtered set is persisted), so this flag is a no-op there. Default False.
- Return type:
- to_cdat(path)[source]¶
Export species in BNG
.cdatformat.The columns are
species, the values this Result reports.- Parameters:
- Raises:
ValueError – For a squeezed
run_batchorrun_replicatesResult, which holds one trajectory per row: write each row’s.cdatfrom the unsqueezed results (squeeze=False).RuntimeError – If the file cannot be opened for writing.
- Return type:
- to_csv(path, *, kind='observables', sep=',', include_time=True, header=True)[source]¶
Export the trajectory as a plain delimited text file.
Unlike
to_gdat()/to_cdat()(BNG-native,#-prefixed, fixed-width space-padded), this writer produces a format SBML/ RoadRunner/Tellurium users expect: a header row of column names followed by data rows, with a user-chosen separator (,for CSV by default;"\t"for TSV; any single character).The first column is
time(unlessinclude_time=False); the remaining columns are the observable or species columns named exactly as they appear on the in-memoryResult. No#comment prefix is emitted, so the file can be loaded directly bypandas.read_csvornumpy.loadtxt.- Parameters:
path (
str|Path) – Output file path. The caller chooses the path; nothing is written to the current working directory implicitly.kind (
str) – Which trajectory block to write."observables"(default) matches what.gdatwould contain;"species"matches.cdat.Result.expressionsis not currently exported viato_csv— useResult.save()(HDF5) for a lossless capture.sep (
str) – Column separator. Default",". Use"\t"for TSV. Must be a single character.include_time (
bool) – Whether to prepend atimecolumn. DefaultTrue.header (
bool) – Whether to write the header row of column names. DefaultTrue.
- Raises:
ValueError – If
kindis not"observables"or"species", ifsepis not a single character, or if the trajectory is not a 2-D single-sim array (call this per-replicate on batch results).OSError – If the file cannot be opened for writing.
- Return type:
Notes
Backend coverage: works on every method (ODE / SSA / PSA / NFsim / RuleMonkey) and on results loaded from HDF5. What is not exported by this writer:
expressions,sensitivities,solver_stats,custom_attrs,seed. Use HDF5 (Result.save()) when any of those need to round-trip.Examples
Plain CSV of observables, ready for
pandas.read_csv:sim = bngsim.Simulator(model, method="ode") result = sim.run(t_span=(0, 100), n_points=101) result.to_csv("out.csv")
Tab-separated species amounts:
result.to_csv("out.tsv", kind="species", sep="\t")
Headerless data block (e.g. to append to an existing file):
result.to_csv("data.csv", header=False)
- static squeeze(results)[source]¶
Stack a list of single-sim Results into one batch Result.
The returned Result has 3D arrays: -
species.shape == (n_sims, n_times, n_species)-observables.shape == (n_sims, n_times, n_observables)Any populated sensitivity block (species, observable, expression, and their IC variants — GH #196) is carried through with the sim axis prepended, e.g.
sensitivities.shape == (n_sims, n_times, n_rows, n_cols); a block that is empty on the inputs stays empty. Each row keeps its own compartment sizes and per-sample volume factors.- Parameters:
results (
list[Result]) – Results from individual simulations. All must have the same time points, the same species, observable and expression names in the same order, the same sensitivity parameters and IC species, the same reaction labels, and the same blocks present: the stack has one time axis and one set of column names, which every row is read by (issue #1513).- Returns:
A batch result with 3D arrays.
- Return type:
- Raises:
ValueError – If
resultsis empty, any of them met a stop condition (stop_condition, issue #1369), or a row’s time points, names or blocks differ from the first row’s (issue #1513).
- bngsim.run_bngl(path, *, seed=None, rtol=None, atol=None, allow_lossy=False, bng2_pl=None, net_out=None, timeout=600, cache=True)[source]¶
Load a
.bnglfile and run its actions, as BNG2.pl runs the file.The network comes from
Model.from_bngl()(bng2_pl,net_out,timeoutandcacheare its arguments), the actions fromparse_bngl_protocol(), and they run throughrun_protocol()(seed,rtol,atol,allow_lossy).Examples
>>> out = bngsim.run_bngl("egfr_path.bngl") >>> [run.experiment.t_span for run in out] [(0.0, 100000.0), (0.0, 120.0)] >>> out[1].result.observables["Sos_act"][-1]
- bngsim.run_protocol(model, protocol, *, seed=None, rtol=None, atol=None, allow_lossy=False)[source]¶
Run a BNGL protocol’s steps on model, in order, as BNG2.pl runs them.
- Parameters:
model (
Model) – The protocol’s network, asModel.from_bngl()loads it from the same file: the state at the file’sgenerate_network, which already holds the actions before it. It is not changed; the protocol runs on a clone.protocol (
ProtocolSpec) – The steps, fromModel.from_bngl(path, protocol=True)orparse_bngl_protocol(). A spec with no generation point (generation_stepNone: SED-ML, SBML events) runs from its first step.seed (
int|None) – Seeds the stochastic runs that do not name their ownseed: each draws its seed from this one, in order.Nonedraws fresh seeds.rtol (
float|None) – Override every ODE run’s tolerances. By default a run uses its ownrtol/atol, orrun_network’s 1e-8;atolis held exactly.atol (
float|None) – Override every ODE run’s tolerances. By default a run uses its ownrtol/atol, orrun_network’s 1e-8;atolis held exactly.allow_lossy (
bool) – Run a protocol that has an action bngsim cannot apply, or a run argument it holds only approximately, as nearly as it can, with aConversionWarning, instead of refusing it.
- Returns:
One
ProtocolRunper experiment, and the model as the protocol left it.- Return type:
- Raises:
ConversionError – Before anything runs, if a step cannot be run here: a network-free or PLA run, a run argument such as
stop_if, an action that changes results but carries no step (unlessallow_lossy), or an argument the protocol holds only approximately (unlessallow_lossy). While running, if a step names a parameter or species the model does not have, or resets to a state nothing saved, which BNG2.pl refuses too.
- class bngsim.ProtocolResult(runs, model, protocol)[source]¶
Bases:
Sequence[ProtocolRun]What
run_protocol()ran: oneProtocolRunper experiment.Index it by position, or by a run’s
suffix.modelis the model in the state the protocol left it;protocolis the spec that ran.- Parameters:
runs (
Sequence[ProtocolRun])model (
Model)protocol (
ProtocolSpec)
- property runs: tuple[ProtocolRun, ...]¶
- class bngsim.ProtocolRun(experiment, result=None, scan_values=(), scan_results=(), backward_values=(), backward_results=())[source]¶
Bases:
objectOne experiment of a protocol, as it ran.
A
simulatehas its time course inresult. Aparameter_scanhas oneResultper value inscan_results, in the order they ran, each carryingcustom_attrs["scan_parameter"]andcustom_attrs["scan_value"]asSimulator.parameter_scan()’s do. Abifurcatehas its upward sweep there and the sweep back down inbackward_results.- Parameters:
- experiment: Experiment¶
- class bngsim.IdentifiabilityReport(fim, parameters, eigenvalues, eigenvectors, rank, condition_number, identifiable, non_identifiable_directions, cramer_rao_bound, threshold)[source]¶
Bases:
objectModel-identifiability readout of a Fisher Information Matrix (GH #202).
A pure function of the model + output-sensitivity tensor (no data, no residuals, no objective): the eigen-decomposition of the FIM and the practical-identifiability flags derived from it. This is sloppiness / practical-identifiability analysis of the model, independent of any fit.
Produced by
Result.identifiability().- Variables:
fim (ndarray, shape
(n, n)) – The (symmetric, positive-semidefinite) Fisher Information Matrix the readout was computed from, overnparameters (or IC species).parameters (list of str) – Names labelling the FIM axes — the
Result.sensitivity_params(axis="parameter") orResult.sensitivity_ic_species(axis="ic") in matrix order.eigenvalues (ndarray, shape
(n,)) – FIM eigenvalues in ascending order. Tiny negative values from round-off are clipped to0.eigenvalues[0]is the smallest — the sloppiest (least-constrained) direction.eigenvectors (ndarray, shape
(n, n)) – Orthonormal eigenvectors as columns, aligned witheigenvalues:eigenvectors[:, i]is the parameter combination foreigenvalues[i]. A near-zero eigenvalue marks its eigenvector as a practically non-identifiable parameter combination.rank (int) – Numerical rank — the number of eigenvalues above
threshold.condition_number (float) – \(\lambda_\max / \lambda_\min\);
infwhen rank-deficient. A large value signals an ill-conditioned (sloppy) parameter set.identifiable (ndarray of bool, shape
(n,)) – Per-eigen-direction flag (aligned witheigenvalues):Truewhere the eigenvalue exceedsthreshold,Falsefor a practically non-identifiable / sloppy direction.non_identifiable_directions (list of int) – Indices into
eigenvalues/eigenvectorscolumns that are flagged non-identifiable (theFalseentries ofidentifiable), smallest eigenvalue first.cramer_rao_bound (ndarray, shape
(n, n)) – \(\text{FIM}^{-1}\) — the Cramér–Rao lower bound on parameter variance, an identifiability aid only (a property of the FIM). This is not a data/noise-weighted fit covariance. All-NaNwhen the FIM is rank-deficient (the inverse is undefined; a warning is emitted).threshold (float) – Eigenvalue cutoff (
rtol * lambda_max) below which a direction is flagged non-identifiable.
- Parameters:
- fim: NDArray[float64]¶
- eigenvalues: NDArray[float64]¶
- eigenvectors: NDArray[float64]¶
- cramer_rao_bound: NDArray[float64]¶
- class bngsim.EvaluationSpec(model_source, model_format='net', method='ode', t_span=(0.0, 100.0), n_points=101, params=dict(), sensitivity_params=(), sensitivity_ic=(), sensitivity_method='staggered', outputs=(), rtol=None, atol=None, max_steps=None, model_sha256=None, output_start=None, seed=None)[source]¶
Bases:
objectA serializable, stateless description of one bngsim evaluation.
- Parameters:
model_source (
str) – For a path format (net/sbml/antimony), the model file path. For a*_stringformat, the inline model text.model_format (
str) – One of"net","sbml","antimony","sbml_string","antimony_string".method (
str) – Simulation method. Default"ode"(the only method with sensitivities).t_span (
tuple[float,float]) –(t_start, t_end)integration interval.n_points (
int) – Number of output time points (includingt_start, oroutput_startwhen one is set).output_start (
float|None) – Where the output starts, when that is aftert_start: the run integrates fromt_startand returnsn_pointseven times over(output_start, t_end). This is a SED-ML uniform time course whoseoutputStartTimeis past itsinitialTime(issue #1493).None(the default) outputs fromt_start. It takessensitivity_paramsacross the unreported part ascarry_sensitivitiesdoes, and refusessensitivity_ic, which does not cross it.params (
Mapping[str,float]) – Parameter overrides applied to the model before running (the θ vector).sensitivity_params (
Sequence[str]) – Parameter names to compute forward output sensitivities for. Empty disables parameter sensitivities.sensitivity_ic (
Sequence[str]) – Species names whose initial conditions to differentiate against.sensitivity_method (
str) – CVODES sensitivity method ("staggered"/"simultaneous").outputs (
Sequence[str]) – Output selectors (species:/observable:/expression:…) that the caller intends to read off the result. Recorded for provenance; not applied during integration.rtol (
float|None) – Relative solver tolerance.Noneuses the Simulator default.atol (
float|Sequence[float] |str|ExactAtol|None) – Absolute solver tolerance.Noneuses the Simulator default. A sequence is the per-species tolerance of issue #196, ordered likeModel.species_namesand normalized to a tuple so the spec stays hashable and byte-stable;"auto"asks the worker to derive one from the model it materializes. Both survive the JSON round trip, so a decade-spanning model can be distributed at a tolerance that means something for every species."tracking"is the over-time mode of issue #213 at its default depth — a token, so it round-trips too. Abngsim.TrackingAtolwith a non-default depth or an explicit ceiling does not: it is not JSON, and a spec is a wire format. A float is read as a ceiling, asbngsim.Simulator.run()reads one (issue #1420); abngsim.ExactAtolholds every species to it exactly, and goes out as{"exact": value}. Distribute the token, or the ceiling as a plain sequence.max_steps (
int|None) – Max internal solver steps per output point.Noneuses the default.model_sha256 (
str|None) – Hex SHA-256 of the model source (file bytes for a path format, the UTF-8 text for a*_stringformat). When set,build_model()verifies the live source matches and raises on mismatch — a cluster integrity guard against a stale/edited model file on a shared filesystem.seed (
int|None) – The run’s random seed, from 0 to 2**64 - 1, passed tobngsim.Simulator.run()(issue #1517). A stochastic spec with a seed gives one trajectory on every node; without one, eachevaluate()draws a fresh seed and reports it onResult.seed, soreplace(spec, seed=result.seed)reruns that trajectory. An ODE spec reads it only to order simultaneous equal-priority events (GH #242). Withoutput_start, the output leg runs atseed, whichResult.seedreports, and the leg up to the output start at the second seed drawn from it, asbngsim.Simulator.run_batch()draws its row seeds (issue #1365), so the two legs do not replay one stream. An unseeded stochastic spec draws that seed before the first leg, so itsResult.seedreruns both. Emitted byto_dict()only when set, so a spec without one keeps its bytes. SED-ML carries it asKISAO:0000488.
- to_dict()[source]¶
Return a deterministic, JSON-encodable dict of this spec.
paramsis emitted sorted by name and sequences as lists, so the dict (andto_json()) is byte-stable for a fixed spec — usable directly as a content key for caching/deduplication.output_startandseedare emitted only when set, so a spec without them keeps the bytes it always had. A stochastic spec without a seed is one key for many trajectories: setseedfor a key that names one run.
- classmethod from_dict(data)[source]¶
Reconstruct a spec from a
to_dict()mapping (extra keys rejected).- Parameters:
- Return type:
- classmethod from_json(text)[source]¶
Deserialize from a
to_json()string.- Parameters:
text (
str)- Return type:
- with_params(params, *, merge=False)[source]¶
Return a copy with
paramsreplaced (or merged whenmerge=True).The ergonomic path for a parameter sweep / multistart: serialize one base spec, then stamp each θ row through
with_paramson the worker. Returns a new frozen instance; the original is untouched.- Parameters:
- Return type:
- compute_source_sha256()[source]¶
SHA-256 of the model source (file bytes for a path, UTF-8 text inline).
- Return type:
- build_model()[source]¶
Load the model from
model_sourcepermodel_format.When
model_sha256is set, the live source is hashed and compared first; a mismatch raisesValueError(cluster integrity guard).- Return type:
- evaluate()[source]¶
Materialize and run this evaluation, returning the
Result.Deterministic for fixed inputs (model, θ, sensitivity set, solver options, and
seedfor a stochastic method). Read named outputs viaResult.outputs()and gradients viaResult.output_sensitivities()withoutputsas selectors.- Return type:
- class bngsim.SteadyStateResult(core)[source]¶
Bases:
objectResult of a steady-state computation.
- Variables:
concentrations (ndarray) – Species concentrations at steady state, shape
(n_species,).residual (float) –
max|f(y)|at convergence.newton_distance (float) – The Newton step from the state returned to the root, in each species’ own tolerance, which under
criterion="scaled"decided where the solve stopped (issue #1001); at most 1 is within it. NaN undercriterion="residual", which does not take the step, and above 512 unknowns;infwhere the Jacobian is singular at a state that is not a root, or a differenced one straddles a jump.continued_run_distance (float) – The run test of
criterion="scaled": the largest move of a measured species, over its tolerance, across the run taken on from the state returned; at most 1 passes (issue #1001).infwhere the run could not be taken to its time; NaN where none was taken (under"residual", where the parity test already refused, and for a burst-free Newton solve).criterion (str) – The test
convergedwas decided by,"scaled"or"residual".method_used (str) –
"integration"or"newton".converged (bool) – Whether steady state was found.
n_steps (int) – Number of solver steps.
n_rhs_evals (int) – Number of RHS evaluations.
n_state_switch_roots (int) – The crossings of a rate-law condition over model state (or of an SBML model’s discontinuity trigger) the march stopped at, so a window the approach passes through is integrated over (issue #904). It restarts on each one up to 1000; the 1001st, where it stops rooting them, is counted too.
state_switch_roots_dropped (bool) – Whether the march stopped rooting those conditions after 1000 crossings, as a state that slides along its threshold makes, and marched over the switch from there as it did before #904. If that march ends further from a steady state than where it stopped rooting, the state there is returned.
n_residual_species (int) – How many species entered
||f||_2 / n—n_speciesunlesssteady_state(mask=...)restricted it (issue #74). Assert on this rather than inferring from a residual that moved.excluded_species (list[int]) – 0-based indices the mask excluded, ascending; empty without a mask. Their concentrations are returned but their settling was never tested, and their
sensitivityrows are NaN.unconverged_pure_sinks (list[str]) – On a failed solve, the write-only accumulator species that were in the convergence test and are carrying flux at the returned state — the structural reason the residual has a floor (issue #74). Empty otherwise, including on every converged solve. See
Model.pure_sink_species().root_stability (str) – Whether the system can rest on the returned root (issue #78), from the eigenvalues of the Jacobian restricted to the species the Newton polish solved for:
"stable"(every eigenvalue in the closed left half-plane),"undetermined"(the certificate declined — more than 512 unknowns, or a Jacobian whose spectrum it could not compute), or"unstable".""when the result came from integration, which needs no certificate: a trajectory cannot come to rest on an unstable equilibrium.""too from a burst-free solve told to skip it (steady_state_batch(newton_burst=False, certify=False), issue #1191)."unstable"is returned only when the initial condition itself was that root; a polish that lands on one is discarded instead.n_unstable_roots_rejected (int) – How many candidate Newton roots this solve discarded as unstable (issue #78). Non-zero means the polish landed on a saddle the trajectory was merely passing near — on a bistable model the seed-stability guard cannot catch that, because near a separatrix the trajectory slows down and two successively tighter bursts hand Newton the same seed. Explains a
method_usedof"integration"from amethod="newton"solve.eigenvalues (ndarray, complex128) – The spectrum
root_stabilitywas read off (issue #523): the eigenvalues of the Jacobian restricted to the species the Newton polish solved for, sorted by descending real part, a conjugate pair adjacent with its+imaginarymember first. One per unknown —n_speciesless one per conservation law less the masked species — so the full system’s spectrum is this plus one zero per conservation law. Filled whenever the certificate computed a spectrum, whatever it concluded; empty (shape(0,)) when it did not: an integration result, or a root it declined on before the eigensolver — more than 512 unknowns, a solver failure — which is every"undetermined"except an all-zero spectrum, reported as-is. Compare againstnumpy.linalg.eigvals(model.jacobian(ss.concentrations))to see the conservation-law zeros the restriction removes.sensitivity (ndarray or None) – Species
dY_ss/dpmatrix, shape(n_species, n_params).Noneif no sensitivity was requested.rhs_backend (str) – Which RHS actually evaluated
f(y):"exprtk"(interpreted),"codegen-so"(compiled +dlopen’d), or"codegen-jit"(in-process MIR). Issue #63 — before it, a steady-state solve ignored the Simulator’s codegen artifact, so this was always effectively"exprtk"no matter whatSimulator.codegen_backendreported.solver_jacobian_source (str) – Which Newton matrix the solver tiers factored (issue #127):
"codegen"(the compiled analytical Jacobian),"analytical"(the interpreted fill), or"finite-difference"(CVODE’s and KINSOL’s own difference quotients, one RHS evaluation per unknown per Jacobian setup). Reported on every solve, and it covers both tiers — the CVODE march and the KINSOL polish take the same gate, so they cannot disagree. Before #127 neither installed a callback and this was always the difference quotient, on models whose closed form was already loaded.solver_jacobian_retried (bool) – Whether the closed-form Jacobian had to be called off: CVODE gave up on the march with it installed, and the solve was retried on difference quotients (issue #127, the steady-state half of GH #176). The usual cause is a rate law that is discontinuous in a state variable, whose exact derivative omits the jump. The returned answer is the retry’s, so
solver_jacobian_sourcereads"finite-difference"alongside this. Onlyjacobian="auto"retries;"analytical"surfaces the failure.linear_solver (str) – Which direct linear solver the CVODE march factored its Newton matrix with (issue #128):
"klu"(sparse CSC),"dense"(the built-in dense LU) or"lapack-dense"(the GH #84 BLAS factor). Chosen by the same size/density/force-flag ruleSimulator.run()uses, so the two agree on a model. Before #128 the steady-state paths had no sparse route and this was always dense — 1.9x to 3.1x of wall clock on the 1000+ species corpus models, for the same answer. The KINSOL polish and thedY_ss/dpsolve factor a reduced matrix and are always dense; this field does not describe them.sens_dfdp_source (sens_jacobian_source,) – How each factor of
dY_ss/dp = -J⁻¹·(∂f/∂p)was built.sens_jacobian_sourceis"codegen"(compiled analytical Jacobian),"analytical"(interpreted analytical),"automatic-differentiation"(exact, by dual numbers through the interpreter, where there is no closed form orjacobian="fd"asked for none; issue #1330), or"finite-difference"(a model the dual evaluation cannot follow: a ring of functions that read each other,rateOf);sens_dfdp_sourceis"codegen","automatic-differentiation"or"finite-difference". Both are""when no sensitivity was requested. Issue #63 — both factors were unconditionally finite-differenced before it, and the result gave no way to tell.sens_output_source (str) – What answered the expression block
d(func)/dp(issues #75, #1002, #1330):"codegen"(the compiledbngsim_codegen_output_senschain rule, the same evaluator a CVODES forward-sensitivityrun()uses, for every function),"automatic-differentiation"(dual numbers through the interpreter, for every function: those the chain rule declines, or every one of a model with none),"codegen+automatic-differentiation"(every function, by the two),"partial"(some), or"none"(not one). A row that has no answer isNaN, andoutput_sensitivities()refuses it by name, with the reason: a function’s partials are not differenced at a steady state (issue #1002).""when no sensitivity was requested or the model has no global functions. The observable block is an exact linear projection either way and is not covered by this field.sens_jacobian_rcond (float) –
min|U_jj| / max|U_jj|from the LU of the (reduced) Jacobian that was inverted — how close to singular the sensitivity system was.dY_ss/dpexists only when that Jacobian has full rank; a steady state that is a continuum rather than an isolated point makes it rank-deficient. Well-posed corpus models measure 1e-4 to 1e-1; rank-deficient ones 1e-12 to 1e-9.0.0when no sensitivity was requested. It depends on the units of the species and is not what decides whether the columns are returned: thesens_root_*measures are (issue #995), and a result that carries sensitivities has passed them.sens_root_determinant_ratio (float) – The determinant of the system
dY_ss/dpis solved on at the state one Newton step on, over the one at the returned state (issue #995). 1, to the accuracy of the solve, at an isolated root; next to nothing where the Jacobian is singular at the steady state, a continuum of steady states; 1/2 at a double root; negative or large where the returned state is far from its root or a rate law is discontinuous between the two.steady_stateraises outside 0.6 to 1.67.1.0when no sensitivity was requested.sens_root_pivot_share (float) – The least a pivot of that system’s factorization is of the terms it was computed from: 1 for a pivot nothing cancelled into, 1e-16 for one that is what rounding left of a zero, where the Jacobian is singular whatever the state. Read at the state the solver stopped at, as the condition number is.
steady_stateraises below 1e-13.1.0when no sensitivity was requested.sens_root_condition (float) – The componentwise condition number of that system, the Perron root of
|A⁻¹|·|A|: how many times a relative error in each entry of the Jacobian is magnified in the columns. The same in any units.steady_stateraises above 1e13.1.0when no sensitivity was requested.sens_root_column_shift (float) – The largest move of a column of
dY_ss/dpwhen it is solved again a Newton step on, as a fraction of the column’s largest entry at the two states and of no less than1e-3/|p|, each entry over its species’ scale (sens_species_scale): how far the columns are from those of the root. Where a column moves by more than 0.01 of itself the solve steps the state on, up to ten times, and this is the move at the last step;steady_stateraises where it is still above 0.01.0.0when no sensitivity was requested.sens_root_state_shift (float)
sens_root_state_species (str or None) – The largest move of a species under that Newton step, over its scale, and the species. A state that moves by more than 0.01 is stepped on as one whose columns move is, and
steady_stateraises where it still does after ten steps.sens_root_newton_steps (int) – The Newton steps the returned state and columns are from the state the solver stopped at: 0 where neither it nor its columns moved, and the result is the solver’s own. Otherwise
concentrations,residualand the sensitivities are those of the stepped state, which is nearer the root:tolbounds the residual and not the distance to it, and a model whose concentrations are small passes it a long way off.steady_stateraises where a state that was stepped to is more than 1% in any species from where the run that is taken on ends.sens_root_hold_drift (sens_root_hold_shift,) – A run is taken on for
max_timefrom the state the solver stopped at, with every concentration moved by up to a millionth of itself (a species that is at nothing is not moved).sens_root_hold_shiftis the largest move of a column when it is solved again where that run ends, measured as the column shift is, andsteady_stateraises above 0.01, or where the run could not be made (not a number): the state is one a run is passing, or a root the system leaves.sens_root_hold_driftis the largest move of a species over the run, against what its entries are taken over there: the larger of its two values, or for a species at a zero what stands beside it.0.0when no sensitivity was requested.sens_root_hold_steps (int)
sens_root_hold_time (float) – The steps that run took and the time it reached. The run has
max_stepssteps, andsteady_stateraises where they are used up short ofmax_time: the run was not seen to stay.sens_root_spectral_radius (sens_root_growth_rate,) – The largest real part among the eigenvalues of that system at the returned state, and the largest eigenvalue in size, for systems of up to 512 unknowns (above that, those of the species the state has at a zero). A species that is at nothing is among them, with what it would do if there were any, also where nothing makes it.
steady_stateraises where the first, timesmax_time, is above 0.01: a state beside this one is more than 1% further off after a run of that length. Not a number, and0.0, where the eigenvalues were not taken and when no sensitivity was requested.sens_root_relaxation (float) – The most of a column of
dY_ss/dpthat a run ofmax_timewould leave unestablished, as a fraction of the column’s largest entry: the boundA⁻¹·column / max_timewhere that is under 1e-3, and what eight implicit steps ofmax_time/8leave of the column where it is not.steady_stateraises above 0.01.0.0when no sensitivity was requested.sens_species_scale (numpy.ndarray) – What an entry for each species is small against: its own concentration, the larger of the returned one and the corrected one. A species at a zero has none. It is one the corrected state has at nothing and that nothing left there makes (its rate is zero with every such species set to zero, and it does not grow from next to nothing), and it is taken over where it has been, its starting value among them, and the largest such among the species the Jacobian couples it to, no more than a conserved total it belongs to allows. A species that is small and has a steady value, 1e-12 beside another at 1, keeps its own. Empty when no sensitivity was requested.
sens_mask_reader_species (sens_mask_held_species,) – A species
mask=left out that is held where the solve left it, and a kept species whose rate reads it.steady_stateraises where there is one: the columns are not those of the steady state then.sens_root_pivot_species (sens_root_determinant_species,)
sens_root_hold_species (sens_root_condition_species,) – The species each of those points at: for the determinant, the one whose pivot moved furthest.
sens_root_hold_param (sens_root_column_param,)
sens_root_relaxation_param (str or None) – The parameter whose column each of those is read at.
sens_fd_own_step_species (list of str) – Where
sens_jacobian_sourceis"finite-difference": the species far below the state’s scale whose columns of the Jacobian were taken again at their own step, for the columns to be solved a second time with (issue #1002). Empty where there is none, or the Jacobian is in closed form.sens_fd_half_step_species (list of str) – The same for the columns taken again whole at half the floored step, for a third solve: those of the species far below the state’s scale, and of species at 0. A secant across the step moves with it and a slope does not, also in a row that a much larger species is in, which the own step cannot read.
sens_fd_disagree_param (sens_fd_disagree_species,)
sens_fd_half_step (sens_fd_floored, sens_fd_own_step,) – The entry of
dY_ss/dpfurthest from the floored answer, by more than 1e-4 of itself (1e-10 of its column’s largest, at least), and its three values: the one with every species stepped against the state’s scale, which is whatsensitivityholds, the one with those species at their own step, and the one with them at half the floored step (each the floored one where that matrix has no column taken).steady_stateraises where there is one. None where they agree.sens_tie_param (sens_tie_species,)
sens_tie_below (sens_tie_value, sens_tie_above,) – Where the Jacobian or
∂f/∂pwas taken by dual numbers ("automatic-differentiation") and the steady state sits where a rate law changes branch —if(X > c, ...)at X = c,abs(X)ormax(X, 0)at X = 0, a step offloor()— so that the derivative of the branch it is on is one of two one-sided ones: the entry ofdY_ss/dpfurthest apart when the columns are solved with the matrices from the two sides, by more than 1e-8 of itself (1e-10 of its column’s largest, at least), its value insensitivityand its values from above and below (issue #1330).steady_stateraises where there is one: the steady state has no derivative there. None where the side does not matter.sens_tie_jump (bool) – Whether that entry was refused because a rate law’s value jumps along its species there and the parameter moves the species, so that the steady state crosses the jump (the two sides may agree then).
sens_held_species (str or None) – Where the state is held on a jump in a rate law’s value within the solver’s tolerance of it, with no root of its branch beside it: a Newton step to that branch’s root changed some law’s branch and left a larger residual than it started from. The species whose residual the step left largest (issue #1330);
steady_stateraises where there is one. None where the step changed no branch or took the residual down.sens_held_after (sens_held_before,) – The largest residual before and after that step, each over its species’ terms (at least 1e-9 of the largest species’).
Notes
When
sensitivity_paramsis passed toSimulator.steady_state(), the result also carries the observable- and expression-level steady-state forward sensitivities (GH #12): read them by name withoutput_sensitivities(), mirroringbngsim.Result.output_sensitivities()on a CVODE run. These are the chain-rule projection of the speciesdY_ss/dponto the model’s observables and global functions, so a gradient consumer gets∂(observable)/∂θdirectly without re-deriving the output Jacobian.Examples
>>> ss = sim.steady_state() >>> ss.converged True >>> ss["A"] # species A at steady state 50.0 >>> ss.concentrations array([50., 25., ...]) >>> ss = sim.steady_state(sensitivity_params=["k_deg"]) >>> ss.output_sensitivities(["observable:Stot"]) # (n_sel, n_params) array([[-1.25]])
- residual¶
- newton_distance¶
- continued_run_distance¶
- criterion¶
- method_used¶
- converged¶
- n_steps¶
- n_rhs_evals¶
- n_state_switch_roots¶
- state_switch_roots_dropped¶
- n_residual_species¶
- excluded_species¶
- unconverged_pure_sinks¶
- root_stability¶
- n_unstable_roots_rejected¶
- eigenvalues¶
- rhs_backend¶
- solver_jacobian_source¶
- solver_jacobian_retried¶
- linear_solver¶
- sens_jacobian_source¶
- sens_dfdp_source¶
- sens_output_source¶
- sens_jacobian_rcond¶
- sens_root_determinant_ratio¶
- sens_root_condition¶
- sens_root_column_shift¶
- sens_root_relaxation¶
- sens_root_hold_drift¶
- sens_root_hold_shift¶
- sens_root_hold_steps¶
- sens_species_scale¶
- sens_root_hold_time¶
- sens_root_newton_steps¶
- sens_root_state_shift¶
- sens_root_growth_rate¶
- sens_root_spectral_radius¶
- sens_root_pivot_species¶
- sens_root_hold_species¶
- sens_root_state_species¶
- sens_mask_held_species¶
- sens_mask_reader_species¶
- sens_root_determinant_species¶
- sens_root_condition_species¶
- sens_root_hold_param¶
- sens_root_column_param¶
- sens_root_relaxation_param¶
- sens_fd_own_step_species¶
- sens_fd_half_step_species¶
- sens_fd_disagree_species¶
- sens_fd_disagree_param¶
- sens_fd_floored¶
- sens_fd_own_step¶
- sens_fd_half_step¶
- sens_tie_species¶
- sens_tie_param¶
- sens_tie_value¶
- sens_tie_above¶
- sens_tie_below¶
- sens_tie_jump¶
- sens_held_species¶
- sens_held_before¶
- sens_held_after¶
- property concentrations¶
Steady-state species concentrations.
- property sensitivity¶
Sensitivity matrix dY_ss/dp, shape (n_species, n_params).
None if no sensitivity was requested.
- property observable_names: list[str]¶
Observable names labelling the observable output-sensitivity rows.
Populated on a sensitivity run (
sensitivity_params=[...]); empty otherwise. Provided for parity withbngsim.Result.observable_names.
- property expression_names: list[str]¶
Expression (global-function) names for the expression rows.
Bare, user-facing names (the auto-generated
_rateLawNintermediates are filtered out, matchingbngsim.Result.expression_names). Populated on a sensitivity run; empty otherwise.
- property sensitivities_observables: ndarray¶
Observable steady-state sensitivities
d(observable)/dp.Shape
(n_observables, n_params)on a sensitivity run, aligned withobservable_names(rows) andsensitivity_params(columns); empty(0, 0)otherwise. This is the exact linear projection of the speciessensitivitythrough each observable’s group factors — including the amount-valued volume factor for an SBMLhasOnlySubstanceUnits="true"species, whose observable denotes an amount rather than the stored concentration (issue #119). So for such a species this block is not simplysensitivityre-weighted by the group coefficients; it is the derivative of the observable’s value.
- property sensitivities_expressions: ndarray¶
Expression (global-function) steady-state sensitivities
d(func)/dp.Shape
(n_expressions, n_params)on a sensitivity run, aligned withexpression_names(rows) andsensitivity_params(columns); empty(0, 0)otherwise. Carries the function’s full total derivative — the state-chain term(∂func/∂x)·dY_ss/dpplus the function’s explicit parameter dependence∂func/∂p.
- resolve_outputs(selectors)[source]¶
Resolve typed output selectors to structured metadata.
Same selector grammar as
bngsim.Result.resolve_outputs()(species:/observable:/expression:withstate:/function:aliases,()handling, and bare-name uniqueness). Observable and expression names resolve only on a sensitivity run.
- output_sensitivities(selectors, *, axis='parameter')[source]¶
Return steady-state
d(named output)/dpfor each selector, stacked.The steady-state analogue of
bngsim.Result.output_sensitivities(): resolves each selector and stacks the matching steady-state sensitivity row, so a gradient consumer reads∂(observable)/∂θ/∂(expression)/∂θdirectly instead of re-deriving the output Jacobian.species:selectors read the speciesdY_ss/dprows;observable:selectors the exact linear group projection;expression:selectors the compiled chain rule’s total derivative of the global function (state chain + explicit parameter dependence), and refuse by name a function it has none for (issue #1002).- Parameters:
selectors (
str|Iterable[str]) – Selectors accepted byresolve_outputs().axis (
str) – Only"parameter"(the default) is computed here. A stable steady state keeps nothing of its initial conditions but the conserved totals, so the"ic"axis is zero in a model with no conservation law and the projection onto the totals in one with any; it is not computed, and requesting it raisesValueError. A parameter that sets an initial amount has its share of the totals in its own"parameter"column (issue #704).
- Returns:
Shape
(n_selectors, n_params), one row per selector in input order (no time axis — a steady state is a single point). An empty selector list yields a(0, n_params)array.- Return type:
- Raises:
ValueError – If
axisis not"parameter"; if no parameter sensitivities were computed (run withsensitivity_params=[...]); or if a selector names a kind whose sensitivities are unavailable.TypeError – Propagated from
resolve_outputs().
Examples
>>> ss = sim.steady_state(sensitivity_params=["k_deg"]) >>> ss.output_sensitivities(["observable:Stot", "expression:foo"]).shape (2, 1)
- class bngsim.SteadyStates[source]¶
Bases:
objectThe distinct steady states
Simulator.steady_states()found (issue #524).One row per distinct root, in the order first reached.
- Variables:
states (ndarray, shape
(n_roots, n_species)) – Each root’s concentrations, ordered likespecies_names: the start that reached it with the smallest residual, the extra start fromSimulator.steady_state()’s root among them.stability (list[str]) –
"stable","unstable"or"undetermined"per root, from the stability certificate.eigenvalues (list[ndarray]) – The reduced Jacobian’s spectrum at each root (empty where the certificate did not compute it).
multiplicity (ndarray of int) – How many starts reached each root: a rough measure of its basin. The root of
Simulator.steady_state(), solved as one more start, is no draw and counts for nothing, so a root only it reached reads 0 (issue #1189).basins (ndarray of int, shape
(n_starts,)) – Root index per start, -1 where the solve did not converge.residuals (ndarray) –
||f||_2 / nat each root.n_failed (int) – Starts whose solve did not converge.
- ROUNDOFF: float = 1e-09¶
What a solve leaves in a species it places at a root, as a share of the scale that species is computed against: its smallest law’s for a law member, and for every species the largest value it takes at any converged start (a root at 0 is reached at 0 or a hair beside it: -5.9e-24 on #1001’s cubic at a nanomolar scale).
- classmethod cluster(results, species_names, *, dedupe_rtol, atol=None, laws=None, scale=0.0)[source]¶
Group converged
resultsinto distinct roots.Two results are one root when every species agrees to
dedupe_rtolof the larger magnitude, or to an absolute floor in concentration.atolis that floor, a scalar or one value per species.Nonederives it per species as the larger of two terms:ROUNDOFFof the largest value the species takes in any converged result, the round-off a solve leaves in it at a root where it is zero; or double precision’s resolution (ε) beside the largest magnitude the solves saw, the larger ofscaleand the largest value any result holds.scaleis the largest magnitude of the starts, 0 to read it from the results alone. The second term is for a species at 0 at every root, where the first is round-off itself: where every species is at 0 at every root, the results hold nothing else, and withoutscalea model whose only root is 0 came back as 108 roots. The first is the species’ own scale, not the model’s: a switch in a pool of 1e-3 beside a pool of 1e8 (#1265) is told apart at its own 1e-3. And it is a distance in concentration, not the solve’s residual tolerance:Simulator.steady_states()once passed itstol, and at1e-9in molar units it grouped the 0.2 nM and 0.5 nM roots of a nanomolar switch as one (issue #1518).laws, shape(n_laws, n_species): a species in a conservation law is told apart only toROUNDOFFof the smallest of its laws’ scales,Σ_j |L_kj·y_j|, the round-off a solve leaves in it. egfr_path’s EGF, 5e-8 in a law over 1.8e5, came back from 40 starts at 34 values, and each counted as a root of its own. The floor is notdedupe_rtol’s: at1e-6of a pool of1e8it merged the toggle’s saddle into an attractor. It still matters beside the floor above: a member far below its law’s total, as EGF is, carries the law’s round-off rather than its own, and a coefficient above 1 multiplies it.A result joins a root by agreeing with the first result that reached it. The state reported is the member with the smallest residual, and comparing with that instead made the grouping depend on the order of the residuals.
- class bngsim.NfsimSession(xml_path, *, molecule_limit=None, connectivity=None, v1143_compat=False, block_same_complex_binding=True, traversal_limit=None)[source]¶
Bases:
objectStateful NFsim simulation session with per-step control.
Use this class when you need fine-grained control over an NFsim session: setting parameters before initialization, running multiple simulation segments, and mutating molecule or exact species counts between segments.
For simple one-shot NFsim simulations,
bngsim.Simulator(model, method="nf", xml_path=...)is easier.- Parameters:
xml_path (
str|Path) – Path to a BNG XML file (generated by BNG2.plwriteXML).molecule_limit (
int|None) – Global molecule limit (NFsim-gmlflag).connectivity (
bool|None) – Enable NFsim’s reaction-connectivity optimization.v1143_compat (
bool) – Enable NFsim v1.14.3 selector compatibility mode for same-seed trajectory reproducibility with the standalone CLI.block_same_complex_binding (
bool) – WhenTrue(the default), a bimolecular rule cannot bind two molecules of one complex: BNGL’s semantics, as BNG2.pl’s network applies them. NFsim blocks same-complex binding under-cb(or-bscb, the same flag), which BNG2.pl’ssimulate_nfpasses by default (complex=>1).Falseallows it, so a binding rule can close a ring inside one complex. Either way, a rule whose products are written apart (A.B -> A + B) and that deletes a bond or a molecule does not fire where the molecules it acts on would stay in one complex across products, as BNG2.pl’s network requires. NFsim v1.14.3 (BioNetGen 2.9.3) never makes that test, and current NFsim makes it only under-cb, for one-reactant rules. Not tested yet: a product held apart only by a molecule the rule deletes, or tied to the rest of its pattern only by.; and a product written joined (A.B -> A.B) that breaking the bond splits. WithFalse, a bimolecular rule that deletes nothing (A + B -> A + Bchanging states) may match two molecules of one complex and fire. WithFalsea gelling model can run far slower: it fills with ring bonds such a rule may never break, and each attempt on one is refused. Complex bookkeeping forSpecies-typed observables is enabled whenever the model declares one, whatever this setting.traversal_limit (
int|None) – NFsim-utl N: universal traversal limit.None(default) lets NFsim auto-compute a suggested limit from the XML.
- Raises:
RuntimeError – If NFsim support is not available in this bngsim build.
Examples
Basic one-shot simulation:
with bngsim.NfsimSession("model.xml") as nf: nf.initialize(seed=42) result = nf.simulate(0, 100, n_points=101)
Multi-step protocol:
with bngsim.NfsimSession("model.xml", molecule_limit=100000) as nf: nf.set_param("kf", 0.5) nf.initialize(seed=42) r1 = nf.simulate(0, 50, n_points=51) nf.add_species("L(r)", 500) r2 = nf.simulate(50, 100, n_points=51)
Parameter scan (new session per point):
for kf_val in [0.1, 1.0, 10.0]: with bngsim.NfsimSession("model.xml") as nf: nf.set_param("kf", kf_val) nf.initialize(seed=42) result = nf.simulate(0, 100, n_points=101)
- set_molecule_limit(limit)[source]¶
Set the global molecule limit.
Can be called before or after
initialize()to change the NFsim-gmlcap.
- set_param(name, value)[source]¶
Set a parameter value and propagate to dependents.
May be called before or after
initialize(). Pre-init writes are stored and applied on session start; post-init writes update the live NFsim System in place. In both cases bngsim re-evaluates every parameter whose BNG XMLexpr=transitively depends onname, so derived values likeLT = LT_conc_M*NA*V_simand_rateLaw*follow automatically.Pre-init writes are baked into the XML that NFsim parses, so any
<Species concentration="X">seed-species expression that referencesname(directly or through a derived parameter) is evaluated against the override-resolved namespace when the agent population is created. This matches BNG2.pl’s behavior whensetParameteris invoked betweengenerate_networkand amethod=>"nf"simulate.Post-init writes do not alter the live agent population — they only update reaction rates from the new parameter namespace. To change live counts after
initialize(), useadd_species()/remove_species().NfsimSession refuses a compartment model (GH #1067). Once that refusal is lifted, note that neither kind of write reaches a compartment’s size: BioNetGen writes the size into the XML as a number (issue #1061).
- Parameters:
- Raises:
ParameterError – If the parameter name is not recognized or if expression propagation fails for some dependent parameter.
- Return type:
- clear_param_overrides()[source]¶
Clear all parameter overrides set via
set_param.When called on a live session, parameters revert to their XML-time values and dependent expressions are re-evaluated so the System matches the original BNG XML namespace.
- Return type:
- evaluate(expr, overrides=None)[source]¶
Evaluate a BNG expression in the current parameter namespace.
Uses the same ExprTk evaluator that powers
set_parampropagation, so expressions can reference any BNG parameter, BNG built-in (_pi,_NA, …), or BNG-style construct (if(cond, then, else),&&,||,mratio).- Parameters:
- Returns:
Numeric value of the expression.
- Return type:
- Raises:
ParameterError – If the expression fails to compile.
- initialize(seed=None)[source]¶
Initialize the NFsim session.
Parses the XML, seeds the RNG, and prepares the simulation system. Must be called before
simulate().- Parameters:
seed (
int|None) – Random number generator seed. When omitted (orNone), bngsim draws a fresh seed from system entropy so consecutive session lifetimes produce independent trajectories. Pass an explicit integer for reproducibility, from 0 to 2**32 - 1: NFsim seeds its generators from 32 bits (issue #1515). The actual seed used is exposed via theseedproperty and stamped onto everyResultreturned bysimulate().- Raises:
ValueError – If
seedis negative or past 2**32 - 1. A live session is left as it was.SimulationError – If initialization fails.
- Return type:
- destroy()[source]¶
Destroy the session and release C++ resources.
Safe to call multiple times. Called automatically by
__exit__when using the context manager.- Return type:
- simulate(t_start=None, t_end=None, n_points=None, *, timeout=None, relative_time=False, sample_times=None)[source]¶
Run a simulation segment and return a Result.
Session state is preserved across calls, enabling multi-step protocols (simulate, mutate, simulate again).
- Parameters:
t_start (
float|None) – Segment start time. Required unlesssample_timesis given.t_end (
float|None) – Segment end time. Required unlesssample_timesis given.n_points (
int|None) – Number of output time points (including endpoints). Required unlesssample_timesis given.sample_times (
list[float] |None) – Explicit output instants (GH #184). When provided, overridest_start/t_end/n_points: the returnedResult.timearray equals these times exactly (sorted ascending, treated as absolute), instead of a uniform grid. The live NFsim clock advances from the current session time by the span of the list, so this works mid-protocol (aftersetConcentration/ a prior segment), continuing the same trajectory. Must contain at least two finite points. Mutually exclusive withrelative_time(sample_times are absolute).timeout (
float|None) – Wall-clock budget in seconds.Noneor0disables the budget. Positive values arm a check between eachstepTo()output point; on overrun this method raisesbngsim.SimulationTimeoutand the session must be destroyed (or the context manager exited) before reuse — the live NFsim System has advanced an indeterminate distance into the segment and the session clock is not updated.relative_time (
bool) – WhenTrue, the returnedResult.timearray is offset so it begins at0.0and ends att_end - t_start— the convention used by BNG2.pl’ssimulate({method=>"nf", ...})action, which prints “NFsim timepoints are reported as time elapsed sincet_start=$t_start” at runtime. The internal NFsim clock andsession_logical_timeare unaffected, so multi-segment threading (simulate, mutate, simulate again) keeps the same absolute physical time it would have under the default. Recommended for hosts that thread NF protocols across multiple bngsimsimulate()calls and want their stitched time axis to match BNG2.pl’s. DefaultFalsepreserves the absolute-time labelling used by every other bngsim backend.
- Returns:
Simulation result for this segment.
- Return type:
- Raises:
SimulationError – If the session is not initialized or simulation fails.
SimulationTimeout – If
timeoutis set and the wall-clock budget is exceeded.
- step_to(time)[source]¶
Advance the simulation to time without recording output.
Useful for fast-forwarding before a protocol action.
- Parameters:
time (
float) – Target time to advance to.- Raises:
SimulationError – If the session is not initialized.
- Return type:
- add_molecules(mol_type, count)[source]¶
Add molecules of a given type in their default (unbound) state.
- Parameters:
- Raises:
ValueError – If count is not positive.
- Return type:
- get_species_count(pattern)[source]¶
Get the current count of an exact BNGL species pattern.
The current implementation supports exact single-molecule species patterns such as
"X(p~0,y)"or"TNF()". Mutation-oriented species patterns must list every component and specify every stateful component state so that the live-session operation is unambiguous. Multi-molecule complex patterns fail clearly.Symmetric components (e.g.
L(r~u~c~g, r~u~c~g)declares two sites namedr) are matched by sorted multiset of state/bond constraints across class members, so heterogeneous patterns likeL(r~u,r~c)andL(r~c,r~u)resolve to the same species count.
- remove_species(pattern, count)[source]¶
Remove exact BNGL species instances from the live NFsim session.
- set_species_count(pattern, count)[source]¶
Set the live count for an exact BNGL species pattern.
This is implemented as a diff against
get_species_count(pattern)and may add or remove exact species instances. A fractional count is rounded to the nearest integer (round-half-up, GH #51).
- get_parameter(name)[source]¶
Evaluate and return a parameter value from the live session.
Can evaluate expressions, not just named parameters.
- save_species(path)[source]¶
Write the live System’s molecular species to a BNG
.speciesfile.Mirrors the artifact BNG2.pl emits when running
simulate({method=>"nf", get_final_state=>1, ...}). The file is a plain-text listing of species patterns and counts, formatted exactly as NFsim’s standalone CLI writes it. A host like PyBioNetGen that drives multi-segment NF protocols can read this file to thread state across segments without needing in-process snapshot support (see issue #52 for the in-process path).- Parameters:
path (
str|Path) – Output file path. Overwritten if it exists.- Raises:
SimulationError – If the session is not initialized or the file cannot be written.
- Return type:
- save_concentrations(label=None)[source]¶
Snapshot the live session’s full molecular state for later restore.
Captures every molecule’s count, component states, and bonds into an in-memory snapshot held by the session. A later
restore_concentrations()call rewinds the session to exactly this state. Mirrors the BNGsaveConcentrations()/saveConcentrations("name")action.Unlike
save_species()(which writes a.speciesfile for out-of-process state threading), this keeps the state in process — useful for equilibrate → snapshot → perturb → restore workflows without touching disk.- Parameters:
Name for the snapshot, so a multi-phase protocol can refer to it by name in
restore_concentrations()(BNGsaveConcentrations("name")). Mirrorsbngsim.Model.save_concentrations(). When omitted (orNone), the snapshot goes to the default (unlabeled) slot.Each distinct
labelowns its own snapshot, so multiple named NFsim states coexist and round-trip faithfully (issue #11): a latersave_concentrations("other")does not disturb a snapshot saved under a different name, andrestore_concentrations(label)rewinds to exactly the state saved under that name. Saving to the samelabelagain overwrites just that slot.- Raises:
SimulationError – If the session is not initialized.
- Return type:
- restore_concentrations(label=None)[source]¶
Restore the molecular state captured by
save_concentrations().Rewinds the live session to the snapshotted state (counts, component states, and bonds), discarding any simulation progress since the snapshot. Mirrors the BNG
resetConcentrations()/resetConcentrations("name")action.- Parameters:
label (
str|None) – Name of the snapshot to restore. When omitted (orNone), restores the default (unlabeled) slot saved bysave_concentrations()with no label. When given, restores the state saved under that exact name bysave_concentrations(label); named and default slots are independent (issue #11).- Raises:
SimulationError – If the session is not initialized, or no snapshot has been saved under the requested
label(or the default slot whenlabelis omitted).- Return type:
- has_saved_concentrations(label=None)[source]¶
Whether a
save_concentrations()snapshot is available to restore.
- property saved_concentration_labels: list[str]¶
Sorted names of the currently saved named concentration snapshots.
Does not include the default (unlabeled) slot, which is saved by
save_concentrations()with no label and restored viarestore_concentrations()with no label. Mirrorsbngsim.Model.saved_concentration_labels. Empty beforeinitialize().
- class bngsim.RuleMonkeySession(xml_path, *, molecule_limit=None, block_same_complex_binding=True)[source]¶
Bases:
objectStateful RuleMonkey simulation session with per-step control.
RuleMonkey reads BNG XML and implements the
nf_exactnetwork-free backend. Use this class for multi-action workflows that need to initialize, simulate, add molecules, and simulate again.The session clock is the model’s clock: a rate law that reads
time(), or a table function indexed by time, reads absolute model time, asmethod="ode"andmethod="ssa"do (issue #1022). Seeinitialize()for where it starts andsimulate()for a segment that starts elsewhere.Events are placed on time elapsed since the session’s start wherever the model allows, which keeps their waiting times from rounding away at a large start (issue #1063): RuleMonkey reads a copy of the XML whose clock is
time()plus the start, asSimulator.rundoes, and every time this class takes or reports is model time. A model with a table function indexed by time runs on model time itself (seebngsim/_rulemonkey_clock.py).block_same_complex_bindingis as forSimulator. RuleMonkey 3.13.0 deprecatesFalse: it prints a warning to stderr the first time in a process, and RuleMonkey 4.0 removes the mode.- initialize(seed=None, t_start=None)[source]¶
Initialize the RuleMonkey session.
- Parameters:
seed (
int|None) – RNG seed.None(default) draws a fresh seed; pass an integer from 0 to 2**64 - 1 for reproducibility (a seed outside that range raisesValueError). The actual seed used is exposed via theseedproperty and stamped onto everyResultreturned bysimulate().t_start (
float|None) – For a model that reads the clock (time(), or a table function indexed by time), where the seed state sits on the model’s clock:current_timereads it, and a rate law that readstime()starts from its value there.None(default) starts the clock at 0, and lets the firstsimulate()move the seed state to its own start if nothing has moved the session first (step_to(), or a change to its molecules), so thatinitialize(seed)thensimulate(5, 7, ...)runs the model from its initial state at t = 5, asSimulator.run(t_span=(5, 7))does (issue #1022). A model that reads no clock cannot tell, and its session always starts at 0, which keeps event times small; a segment fromt_startis labelled with its own times.
- Return type:
- simulate(t_start=None, t_end=None, n_points=None, *, timeout=None, sample_times=None)[source]¶
Run a simulation segment and return a Result.
A segment that starts at
current_timeruns on the session clock, the model’s. One that starts elsewhere continues fromcurrent_timeby the same span and is labelled with the times asked for, which a model that does not read the clock cannot tell apart. For a model that reads the clock (time(), or a table function indexed by time) the labels would not be the times it read, so such a segment raisesbngsim.SimulationError, unlessinitialize()left the start unset and nothing has moved the session since: the seed state then moves to the segment’s start.The result’s
solver_stats["n_steps"]counts the events sinceinitialize(), not this segment’s alone, as anbngsim.NfsimSession’s does.- Parameters:
t_start (
float|None) – Segment endpoints and number of output samples. Required unlesssample_timesis given.t_end (
float|None) – Segment endpoints and number of output samples. Required unlesssample_timesis given.n_points (
int|None) – Segment endpoints and number of output samples. Required unlesssample_timesis given.sample_times (
list[float] |None) – Explicit output instants (GH #184). When provided, overridest_start/t_end/n_points: the returnedResult.timearray equals these times exactly (sorted ascending, treated as absolute), instead of a uniform grid. The live session advances from its current time by the span of the list, so this works mid-protocol (after a prior segment orsetConcentration), continuing the same trajectory. Must contain at least two finite points.timeout (
float|None) – Wall-clock budget in seconds.Noneor0disables the budget. Positive values arm a check polled by upstream every ~1024 SSA events; on overrun this method raisesbngsim.SimulationTimeoutand the session should be destroyed (or the context manager exited) before reuse — the live RuleMonkey session sits at the last completed event and subsequent state is undefined for bngsim purposes.
- Return type:
- step_to(time, *, timeout=None)[source]¶
Advance the simulation to time without recording output.
- Parameters:
time (
float) – Target time to advance to.timeout (
float|None) – Wall-clock budget; seesimulate()for semantics.
- Return type:
- add_molecules(mol_type, count)[source]¶
Add molecules of a given type in their default unbound state.
A fractional count is rounded to the nearest integer (round-half-up, GH #51), matching bngsim SSA and the cold-start seed.
- get_species_count(pattern)[source]¶
Get the current count of an exact BNGL species pattern.
Peer of
bngsim.NfsimSession.get_species_count(). pattern is parsed and canonicalized by RuleMonkey’s runtime species-pattern parser, so it need not be byte-identical to a label RuleMonkey emits. Scope is exact, fully-specified, connected species: every molecule lists every component, every stateful component carries a concrete~state, and bonds are numeric labels. Wildcards (!+,!?) or omitted components raise.
- add_species(pattern, count)[source]¶
Add exact BNGL species instances to the live RuleMonkey session.
- remove_species(pattern, count)[source]¶
Remove exact BNGL species instances from the live RuleMonkey session.
- set_species_count(pattern, count)[source]¶
Set the live count for an exact BNGL species pattern.
Drives the live count of pattern to exactly count, adding or removing the difference. A fractional count is rounded to the nearest integer (round-half-up, GH #51). Mirrors
bngsim.NfsimSession.set_species_count(); PyBioNetGen’s_apply_nfsim_concentration_changesprobes this method viagetattrto propagatesetConcentrationtormruns.
- evaluate(expr, overrides=None)[source]¶
Evaluate a BNG expression against the live session’s current state.
Resolvable symbols: every declared parameter, the clock
t/time(), every observable, and every global function — all settled against the current pool, exactly as a rate law would see them between events. overrides supplies extraname → valuebindings layered on top for a single evaluation; an override name shadows a model symbol on a clash.Note
Unlike
bngsim.NfsimSession.evaluate()(which only requires a live simulator), the RuleMonkey engine resolves expressions against the active session’s pool, so this method requires an initialized session — callinitialize()first.
- save_species(path)[source]¶
Write the live session’s molecular species to a BNG
.speciesfile.Exact peer of
bngsim.NfsimSession.save_species(). Enumerates the live complex pool, deduplicates by graph isomorphism, and writes a BNG-format.speciesfile (#comment header followed by one<pattern> <count>line per species) readable by BNG2.pl’sreadNFspecies. This is the artifact PyBioNetGen’ssimulate_nfhook reads to threadget_final_statecontinuation acrosssaveConcentrations/resetConcentrationssegments — binding it makes multi-segmentmethod=>"rm"runs reproduce the subprocess trajectory with no PyBioNetGen change.- Parameters:
path (
str|Path) – Output file path. Overwritten if it exists.- Raises:
SimulationError – If the session is not initialized or the file cannot be written.
- Return type:
- save_state(path)[source]¶
Save the active session state to a binary snapshot file.
Serializes the full molecular pool plus the RNG state, keyed by a fingerprint of the molecule-type schema. A later
load_state()(on a session built from the same XML) resumes the trajectory exactly. This is RuleMonkey’s in-process state-continuation path; NFsim has no equivalent (here RuleMonkey is ahead).For threading state into a BNG2.pl-driven host like PyBioNetGen, prefer
save_species()— that goes through BNG’s text.species+readNFspeciesmechanism, which is what the backend hook actually calls.Caveats¶
The schema fingerprint covers molecule-type schema only (molecule types, components, allowed states) — not ReactionRule or Observable patterns. Loading into a simulator built from a different XML that keeps the molecule schema but changes rules / observables succeeds silently and continues with the new rules.
The RNG state is serialized via the C++ stdlib’s
mt19937_64stream operator, which is not specified to be byte-identical across stdlib implementations. Save/load is reliable only between binaries built against the same toolchain.
- load_state(path)[source]¶
Load session state from a snapshot written by
save_state().Creates a new live session from the snapshot, replacing any existing one. The snapshot’s schema fingerprint must match this session’s XML (see
save_state()caveats). On success the session is initialized andcurrent_timereflects the snapshot’s logical time — feed it tosimulate(t_start, ...)to resume sampling.The RNG seed is not recoverable from a snapshot, so
seedreads backNoneafterload_state()even though the underlying RNG stream is restored.- Parameters:
path (
str|Path) – Snapshot path written bysave_state().- Raises:
SimulationError – If the snapshot cannot be read or its schema fingerprint does not match this session’s model.
- Return type:
- class bngsim.NamedArray(data, colnames)[source]¶
Bases:
ndarrayA 2-D ndarray with named columns (RoadRunner-compatible).
Constructed via
bngsim.Result.as_roadrunner(). End users rarely instantiate it directly; subclass primarily exists so that callers can identify the array shape (arr.colnames) and look up columns by name (arr["[X]"],arr[:, "[X]"]).- Variables:
One name per column, in column order — the invariant
__new__()checks, and one that holds for the lifetime of every array of this class (issue #561). Names are positional, so they are carried exactly where this class can see the whole operation and knows what became of the columns: construction; indexing, which relabels whatever survives the key (arr[:, 1:],arr[:, ["time", "[X]"]],arr[:, ::-1], a row slice); and a pickle round trip, so an array that crosses a process boundary arrives labeled (issue #629).Any other array numpy hands back — a product, a transpose, a reduction,
arr.copy(),copy.deepcopy(),np.copy(arr, subok=True),astype,view— hascolnames == []and raisesKeyErroron a lookup by name, rather than answering with whichever column now sits at the name’s old position. A copy is in that list although its columns are plainly the parent’s:.copy()is one of numpy’s own primitives (np.sort(a, axis=1)is a copy followed by an in-place sort), so a copy that carried labels would hand them to operations that permute the columns underneath them. The comment on the method that is not overridden has the measurement.libroadrunner keeps the labels through pickle as this class does, and drops them on every derived array, copies and slices included. Where this class keeps more of them, it is additive — code written against RoadRunner cannot depend on a label being absent — and it keeps one only where the column is established to be the same column.
- Parameters:
- Return type:
Examples
>>> result = sim.run(t_span=(0, 10), n_points=11) >>> arr = result.as_roadrunner() >>> arr.colnames ['time', '[X]', '[Y]'] >>> arr["[X]"] # 1-D column view array([ ... ]) >>> arr[:, "[X]"] # equivalent array([ ... ]) >>> arr[:, 1:].colnames # the columns the slice kept ['[X]', '[Y]']
- Parameters:
- Return type:
- class bngsim.JacobianMatrix(data, source)[source]¶
Bases:
ndarrayA dense
(n_species, n_species)Jacobian that knows how it was built.Returned by
bngsim.Model.jacobian()withsparse=False. An ordinary float64numpy.ndarrayin every respect —J @ v,numpy.linalg.eigvals(J), slicing — with one extra attribute, below.J[i, j]is∂f_i/∂x_j— rowiis the species whose derivative is being differentiated, columnjthe species it is differentiated with respect to, both inbngsim.Model.species_namesorder.- Variables:
source (str) –
"analytical"when every reaction’s derivative came from the model’s closed form;"automatic-differentiation"when it has none and the matrix is exact by dual numbers through the interpreter (issue #1330);"finite-difference"when the matrix is the one-sided difference quotient ofbngsim.Model.rhs(), for a model with neither. A model whose analytical Jacobian is incomplete never hands back the partial matrix under the first label.- Parameters:
- Return type:
- exception bngsim.BngsimError[source]¶
Bases:
RuntimeErrorBase exception for all bngsim errors.
- exception bngsim.CodegenDeclinedError[source]¶
Bases:
BngsimErrorcodegen=Trueon a model codegen declines, such as one whose functions read each other (issue #621): a statement about the model, which runs on the interpreted RHS withcodegen=Noneorcodegen=False(issue #1313). The deprecatedbngsim.prepare_codegen()raises it for a decline too.Distinct from a codegen build failure (a compiler error, a timeout), which says nothing about the model and stays a plain
RuntimeError. ARuntimeError(throughBngsimError), as it was raised before it had a type, so an existingexcept RuntimeErrorstill catches it.Unrelated to
bngsim._codegen.CodegenDeclined, the emitter’s private signal to decline, which the prepare calls turn into aNonereturn; this is what a caller who asked for compiled code is told.
- exception bngsim.CodegenToolchainError[source]¶
Bases:
BngsimErrorThe C toolchain cannot build a codegen library for any model (issue #1544).
Raised when a codegen compile has failed and the same command fails on a trivial source too, or when the compiler is not there at all: the failure is the environment’s (a linker that cannot read the SDK’s
libSystem.tbd, a missing compiler), not the model’s. The message names the compiler, the linker and, on macOS, the SDK, and suggests an installed SDK that links when one does.The verdict holds while the compiler and its environment stay as they are (it is probed again every 5 minutes), so a later build is refused without compiling, and before its source is generated (a model codegen would have declined is refused with this too); a library already in the cache is still used.
codegen=True, forward sensitivities and, underBNGSIM_CODEGEN_STRICT, automatic codegen raise it, each in its own words. ARuntimeError(throughBngsimError), as a build failure was raised before it had a type.
- exception bngsim.ConversionError[source]¶
Bases:
BngsimErrorA format conversion (e.g. SBML→.net) cannot be completed faithfully.
Raised by
bngsim.convert.sbml_to_net()when the source model uses a construct that the target format cannot represent without changing the model’s meaning — for example anhasOnlySubstanceUnitsspecies in a compartment whose volume is not 1, a cross-compartment reaction that needs per-species volume scaling, a rate-rule ODE, a Michaelis–Menten rate-law type, or a table (interpolation) function. The message names the offending construct and the model so the cause is actionable.Pass
strict=False(API) /--allow-lossy(CLI) to downgrade these to aConversionWarningand emit a best-effort.netanyway.
- exception bngsim.ConversionWarning[source]¶
Bases:
UserWarningA format conversion dropped or approximated part of the source model.
Emitted when the network channel is preserved but something outside it was left behind — most commonly SBML
<event>elements, which belong to the simulation-protocol channel (a SED-ML sidecar), not the.netnetwork. Also used whenstrict=Falsedowngrades an otherwise-fatalConversionError. Filter or escalate it like anyUserWarning:import warnings, bngsim warnings.simplefilter("error", bngsim.ConversionWarning) # promote warnings.simplefilter("ignore", bngsim.ConversionWarning) # silence
- exception bngsim.ModelError[source]¶
Bases:
BngsimErrorError loading or manipulating a model.
Raised when: - A .net file cannot be parsed - A model is in an invalid state for simulation - A reserved name conflict is detected
- exception bngsim.ParameterError[source]¶
Bases:
BngsimErrorError with parameter access.
Raised when: - A parameter name is not found in the model - A parameter value is invalid (NaN, Inf, wrong type)
- exception bngsim.SimulationError[source]¶
Bases:
BngsimErrorError during simulation.
Raised when: - CVODE fails to converge - NaN/Inf detected in species concentrations - Maximum number of steps exceeded
- exception bngsim.SimulationTimeout(message, *, timeout=0.0, elapsed=0.0, partial_result=None)[source]¶
Bases:
BngsimErrorRaised when a simulation exceeds its wall-clock budget.
Distinct from
SimulationErrorso callers (e.g. PyBNF’swall_time_sim) can classify wall-clock terminations separately from solver/convergence failures. Inherits fromBngsimError(and thereforeRuntimeError).- Variables:
timeout (float) – The configured wall-clock budget, in seconds.
elapsed (float) – Actual elapsed wall-clock time at the point the timeout fired, in seconds.
elapsed >= timeoutalways holds.partial_result (Result | None) – Reserved for future use. Currently always
None— bngsim does not yet salvage a partial Result from a timed-out run.
- Parameters:
- Return type:
None
Bases:
SimulationErrorforce_sparse_linear_solver=Trueon a model that cannot supply a sparse Jacobian for KLU to factorize: its analytical Jacobian is unavailable (or suppressed byjacobian="fd") and its sparsity pattern has no structural nonzero to colour for finite differences (issue #1313).A refusal by name, not a solver failure: drop the flag to use the dense solver. A
SimulationError, as it was raised before it had a type, so an existingexcept SimulationErrorstill catches it.
- exception bngsim.SsaBoundaryWarning[source]¶
Bases:
UserWarningAn SSA run hit a literal-rate-law boundary condition (GH #110).
Emitted once per run when the exact SSA either drove a species count negative (no non-negativity floor is applied — non-negativity is the rate law’s responsibility, matching the CVODE path) or fired a reaction in reverse because its rate law evaluated negative. Both keep the SSA mean consistent with the ODE; the warning surfaces what would otherwise be a silent boundary behavior. Filter or escalate it like any
UserWarning:import warnings, bngsim warnings.simplefilter("error", bngsim.SsaBoundaryWarning) # promote warnings.simplefilter("ignore", bngsim.SsaBoundaryWarning) # silence
The structured counts are always available on
result.ssa_diagnosticsregardless of warning filters.
- exception bngsim.SsaRoundingWarning[source]¶
Bases:
UserWarningAn SSA run rounded a fractional molecule count to a whole one (issue #718).
The exact SSA fires whole molecules, so a species whose count is not a whole number when a run starts is rounded to the nearest one, as
run_networkdoes. This is emitted for every run and everyrun_untilleg that rounds, not only when the Simulator is built: a count set between legs, or left fractional by an event assignment, is rounded at the next leg. A parameter or compartment that an event assigns and a rate-rule target hold continuous values and are never rounded.result.ssa_diagnosticscarries the count (n_rounded_populations) regardless of filters:import warnings, bngsim warnings.simplefilter("error", bngsim.SsaRoundingWarning) # promote
- exception bngsim.DenseSolverFallbackWarning[source]¶
Bases:
UserWarningA large ODE model is running on the dense solver for lack of KLU (GH #209).
Emitted once per process when
Simulator.run()integrates a large model (n_speciespast the warn threshold) on the dense linear solver only because this bngsim install was built without SuiteSparse/KLU — not because the user asked forforce_dense_linear_solverorjacobian="jax"(both legitimately dense). Dense LU factorizes the full N×N Jacobian at O(N³); for a sparse genome-scale network that is the difference between minutes and hours. The fix is to rebuild bngsim with KLU (seebngsim.capabilities()and GH #209). Filter or escalate it like anyUserWarning:import warnings, bngsim warnings.simplefilter("ignore", bngsim.DenseSolverFallbackWarning) # silence warnings.simplefilter("error", bngsim.DenseSolverFallbackWarning) # promote
- exception bngsim.AtolScaleWarning[source]¶
Bases:
UserWarningAn ODE run’s default absolute tolerance is above species that matter (issue #1361).
BNG2.pl’s scalar
atolof1e-8was chosen for species held as molecule counts. A model in molar units (an SBML model of species at 1e-12 to 1e-6 M, say) holds species below it, and CVODE then controls none of their error: the binding example of the issue returned a negative concentration, and BIOMD0000000002, whose ligand is above it and whose complexes are below it, errors of 1e-2 against its reference. The default now takes each species’ scale from the state the run starts from (issue #1420, seeSimulator.run()), which fixes both. What it cannot scale is a species that starts at 0, or below every scale the start gives, in a model where no species starts small: one whose species all start at 0 runs at the scalar.Emitted by
Simulator.run()(and sorun_until),run_batch,parameter_scanandcompute_all_sensitivitieswhen the caller gave noatoland a species the solver integrates stays below its tolerance all run while reaching at least 1e-6 of the run’s largest such species. A$-fixed species or an assignment-rule target is not integrated and is never named.atol="tracking"(a tolerance that follows each species’ magnitude, issue #213) is the remedy it names. Filter or escalate it like anyUserWarning:import warnings, bngsim warnings.simplefilter("ignore", bngsim.AtolScaleWarning) # silence warnings.simplefilter("error", bngsim.AtolScaleWarning) # promote
- exception bngsim.RuleMonkeyWarning[source]¶
Bases:
UserWarningRuleMonkey runs the model but reports how it reads part of it (issue #1022).
Emitted when
method="rm"("nf_exact") or abngsim.RuleMonkeySessionloads a BNG XML for which RuleMonkey reports a warning-level diagnostic: a rule whose products can never be what it writes (BioNetGen generates no reaction for it, and RuleMonkey never fires it), a Michaelis-Menten law over a pattern that matches several species (BNG2 expands it per species pair, RuleMonkey sums the counts), an observable read at local and global scope in one function, a rule priority,TotalRate, and the like. Each message names the rule or construct and says what RuleMonkey does with it. Errors are refused instead. Filter or escalate it like anyUserWarning:import warnings, bngsim warnings.simplefilter("error", bngsim.RuleMonkeyWarning) # promote warnings.simplefilter("ignore", bngsim.RuleMonkeyWarning) # silence
- exception bngsim.SensitivityRefusedDuringRunError[source]¶
Bases:
SensitivityUnsupportedError,SimulationErrorA forward-sensitivity run the solver refused partway, by declaration.
The solver reached a construct whose derivative it cannot produce. Raised as this type itself, by a condition no choice of requested parameters changes: an event that fires as its trigger leaves its threshold with no first-order motion (issue #934), a compartment that jumps twice at one instant (issue #1181), a delay a
run_batchrow made positive (issue #144), and the like. The message names it and what to change. ASensitivityUnsupportedError, as the refusals raised before a run are, so a fitting driver or a parity suite tells it from a solver failure by its type (issue #1279). And aSimulationError, which all but one of these were raised as before they had a type, so an existingexcept SimulationErrorstill catches it.A refusal whose condition turns on a requested column (a crossing or a fire the column moves, two times it parts) is its subclass
SensitivityRefusedForParametersError, becauseSimulator.compute_all_sensitivities()retries a failed chunk of those one column at a time and a column alone can run, where it raises this type as it is. The one exception is the refusal of a held equality the state leaves (issue #1279), which had this type first: where a restart lands on the stop, a column that moves it is refused and one that does not runs.
- exception bngsim.SensitivityRefusedForParametersError[source]¶
Bases:
SensitivityRefusedDuringRunErrorA forward-sensitivity run the solver refused partway, for the parameters requested.
The condition can turn on which sensitivities were asked for: a crossing or a fire a requested parameter moves, two times it moves apart, or an edge it moves the run onto, where the result has a kink or a jump and no derivative (issues #49, #150, #760, #767, #945, #1025 and others). The message names it and, where it can, the parameters to drop. A smaller set may run, so
Simulator.compute_all_sensitivities()retries a chunk refused this way one column at a time (GH #243) and raises this type, naming the parameters refused alone and the ones that ran, when some are refused alone too.A
SensitivityRefusedDuringRunError, so aSensitivityUnsupportedErrorand aSimulationError: a fitting driver or a parity suite tells it from a solver failure by its type, and anexcept SimulationErrorthat caught these before they had one still does (issue #1435).
- exception bngsim.SensitivityUnsupportedError[source]¶
Bases:
BngsimError,ValueErrorForward sensitivity is unavailable for this model, by declaration.
A clean refusal, not a failure. bngsim inspected the model, found a construct whose derivative it cannot produce correctly, and declined rather than return silently-wrong numbers. Two constructs raise it today:
an event whose crossing time moves with a requested parameter and whose
dt*/dpbngsim cannot compute, so every post-jump sensitivity would be stale (GH #205, remaining subclasses tracked in issue #144); anda rate law codegen could not differentiate to closed form — a non-smooth
min/max/abs/floor,rateOf()inside a rate law, or an unparseable expression (GH #214). bngsim refuses rather than fall back to finite differences.
The solver’s refusals partway through a run are its subclass
SensitivityRefusedDuringRunError, which is aSimulationErrortoo.This is the forward-sensitivity peer of
SsaValidationError: both say “this model exercises a feature this regime does not support”, and both are distinguishable by type, so a caller can separate a declared non-run from an actionable bug without pattern-matching on message text. A fitting driver can catch it to fall back on a derivative-free optimizer; the parity suites bucket it asUNSUPPORTED(non-scoring) rather thanEXCEPTION.Also inherits
ValueError, which both sites raised before this class existed, so existingexcept ValueErrorhandlers keep working.Environment failures are deliberately NOT this exception: “no C compiler and no JIT available” is a fixable local problem, not a property of the model, and stays a plain
RuntimeError.
- exception bngsim.SsaValidationError(issues, *, override_attempted=False)[source]¶
Bases:
BngsimErrorAn SBML model contains constructs that cannot be simulated under SSA.
Raised by
bngsim.Simulatorwhenmethod="ssa"is requested on a model whose loader-captured SSA issues include any withseverity="error". The full issue list (errors and warnings) is available asissues.The error message advertises
strict_ssa=Falseas a workaround when at least one of the raised codes is overridable.override_attemptedflips the message to call out that the user did passstrict_ssa=Falsebut the listed codes are non-overridable.- NON_OVERRIDABLE_CODES = frozenset({'fast_reaction', 'non_integer_stoichiometry'})¶
- exception bngsim.StopConditionMet(message, *, result=None, condition='')[source]¶
Bases:
BngsimErrorA stop condition was triggered during simulation.
The partial result up to the trigger point is attached as
self.result.Conditions are checked against a completed result, so this does not interrupt the solve: the backend integrates the whole requested interval and leaves the model at its end, and
self.resultis that trajectory truncated at the first row where the condition held.Simulator’scurrent_timetherefore reads the end of the interval, not the trigger time — the two differ on purpose, and the clock is the one that names the state the model holds (issue #553). Take asnapshot()beforehand to continue from the trigger point instead.- Variables:
- Parameters:
- Return type:
None
- exception bngsim.UnderSpecifiedModelError[source]¶
Bases:
ModelErrorThe model reads a symbol it never gives a value.
Raised at load when a
<parameter>has novalueattribute and no<initialAssignment>, yet is referenced by a kineticLaw / rule / event / initialAssignment expression, or when a kineticLaw’s local parameter has novalueand the law reads it (issue #580). SBML leaves such a value undefined, so there is nothing to integrate; bngsim refuses rather than default it to0.0, which would silently zero out every expression that consumes it and return a wrong-but-plausible trajectory. RoadRunner refuses the global case too (it runs a Level 2 local one at 0).That failure mode is not hypothetical. MODEL1006230032 under
BNGSIM_ALLOW_UNSET_PARAMS=1integrates to all five species identically zero for the whole run, and every cross-engine check — state, sensitivity, both engines — agrees, because AMICI defaults the same symbol the same way. A model computing nothing, with every verdict green.Typed (issue #323) so a caller can tell “this model is under-specified” from the other things
ModelErrorcovers — a.netparse failure, an invalid model state. Parity suites bucket itUNSUPPORTED: a clean, documented refusal, not an actionable bngsim bug. Set the parameter’s value (a global one can take an<initialAssignment>instead), or opt into the legacy lenient behavior withBNGSIM_ALLOW_UNSET_PARAMS=1(which downgrades this to a warning).
- class bngsim.SsaIssue(severity, code, message, location=None)[source]¶
Bases:
objectA single SSA-compatibility finding from an SBML model.
- Variables:
severity (
Literal['error','warning']) –"error"(Simulator init will raise) or"warning"(logged).code (
str) – Stable machine-readable identifier for the issue category.message (
str) – Human-readable description, ideally pointing the user at a fix.location (
str|None) – Optional locator string (e.g."reaction:R1","compartment:cell","species:S").
- Parameters:
- bngsim.validate_for_ssa(model)[source]¶
Return SSA-compatibility issues recorded for model.
The list is populated by the SBML loader at load time, with an additional model-agnostic warning when the current species state contains non-integer molecule populations. Fractional populations are rounded by the C++ SSA setup path before simulation.
PyBNF and other consumers can call this before constructing a
Simulatorto pre-flight a model — the same list will be re-checked bySimulator(model, method="ssa").
- bngsim.reserved_names()[source]¶
Return dict of reserved constant and function names.
Example
>>> import bngsim >>> names = bngsim.reserved_names() >>> "_pi" in names["constants"] True >>> "time" in names["functions"] True
- bngsim.configure_logging(level=20, *, handler=None, fmt='%(asctime)s [bngsim] %(levelname)s %(message)s')[source]¶
Configure the
bngsimlogger.By default, bngsim is silent (no handler attached). Call this function to enable log output.
- Parameters:
- Returns:
The configured
bngsimlogger.- Return type:
Examples
>>> import bngsim, logging >>> bngsim.configure_logging(logging.DEBUG) >>> # Now all bngsim operations produce log output
- bngsim.normalize_method(requested)[source]¶
Normalize a user-requested method token to its canonical form.
- Parameters:
requested (
str) – The method string as provided by the user.- Returns:
(canonical, dispatch)where canonical is the normalized algorithm name (e.g."nf_reject") and dispatch is the internal backend key used for simulator creation (e.g."nfsim").- Return type:
- Raises:
ValueError – If the method token is not recognized at all, or if it maps to an unavailable backend.
- bngsim.derive_atol(state, rtol, *, floor=None)[source]¶
Build a per-species absolute tolerance from a state vector.
The rule is
atol[i] = rtol * max(|y[i]|, floor): every species is resolved tortolof its own magnitude, which is what a scalar cannot say and what every model spanning decades needs said.This derives from the state you hand it, which is the difference that matters between this function and
bngsim.Simulator.auto_atol()(andatol="auto", which is the same thing): those read the model’s live state, the one the nextrun()would start from. Reach for this one when the tolerance has to be a constant of the model rather than of the point being integrated — a parameter fit that moves initial conditions is the case. There, derive once from the nominal state, hold the vector for the whole fit, and pass it asatol=;"auto"would re-derive at every evaluation and put a step in the objective wherever the derivation crossed a rounding boundary, which is invisible in the usual way (the objective still looks right and only the search behaves oddly).floorexists for exactly one case — a species whose value here is zero has no magnitude to scale, andatol[i] = 0would put it under pure relative error control, which CVODE will not integrate the moment that species is genuinely zero. The default takes the smallest strictly positive entry ofstate, but not above 1: a species with no scale of its own is treated as living at the smallest scale the model actually exhibits, or at one unit (one molecule, in a model of counts) where every species that has a scale is larger. That is the tight choice, deliberately — it can cost step size on a zero-initialized species that later grows, but it never quietly drops one below the noise floor, and it is the direction a caller can measure and then override. Without the cap, a model whose every start is large held its species at 0 to that scale: MODEL2009230001’s, beside one at 1.1e10, to an absolute 110, and a species it grew from 0 came out 100% off.- Parameters:
state (
Sequence[float] |NDArray[double]) – Species values to scale from, ordered likebngsim.Model.species_names. Normally the model’s initial concentrations.rtol (
float) – Relative tolerance the run will use. Must be > 0.floor (
float|None) – Magnitude to substitute for a species whose|y[i]|is below it. Must be > 0. Defaults to the smallest strictly positive|y[i]|, but not above1.0, which is also the floor when every entry is zero.
- Returns:
float64array oflen(state)absolute tolerances.- Return type:
See also
bngsim.Simulator.auto_atolthe same rule against the model’s live state.
bngsim.normalize_atol_vectorvalidate a vector you built yourself.
Notes
A tolerance derived from initial values still cannot see a species that starts at order one and decays to something tiny; that is a within-species, over-time mismatch, and what this removes is the cross-species compromise.
bngsim.TrackingAtol(issue #213) is the other half — it takes the vector built here as a ceiling and re-evaluates it against the trajectory, so a decaying species keeps a tolerance that means something for it.Examples
Derive once from the nominal state, then hold it across a fit that moves initial conditions:
nominal = model.get_state() # before any fitting atol = bngsim.derive_atol(nominal, rtol=1e-8) # a constant of the model for theta in search: model.set_params(theta) result = sim.run(t_span=(0, 100), rtol=1e-8, atol=atol)
- bngsim.normalize_atol_vector(atol, n_species, species_names=None, *, where='atol')[source]¶
Validate a caller-supplied per-species
atoland return it as a list.Every
atol=entry point runs a vector through this before handing it to the solver, so calling it yourself buys nothing except when the error arrives. That is the point of it being public (issue #212): a caller that assembles its own vector — from a nominal state, from a per-species clamp, from a table — can take the contract check once at setup rather than at the firstrun(), which on a fit is a long way from where the vector was built.- Parameters:
atol (
Sequence[float] |NDArray[double]) – The candidate vector.n_species (
int) – Length the model requires (bngsim.Model.n_species).species_names (
Sequence[str] |None) – Used only to make the length error concrete about which ordering the vector was supposed to be in (bngsim.Model.species_names).where (
str) – Names the caller in the error message. Defaults to"atol"; pass your own label to point the message at your call site.
- Returns:
The same values as plain Python floats.
- Return type:
- Raises:
ValueError – If the vector is not 1-D, is the wrong length, or holds an entry that is not finite and >= 0.
See also
bngsim.derive_atolbuild the vector this validates.
- class bngsim.TrackingAtol(decades=12.0, ceiling='auto')[source]¶
Bases:
objectAn absolute tolerance that follows the trajectory (issue #213).
A per-species vector (
derive_atol(),atol="auto") removes the cross-species compromise: a model whose species span decades no longer has to pick one number that is a tolerance for the largest and the smallest at once. It does not touch the within-species, over-time version of the same problem. Whatever number speciesiis given, it keeps for the whole run — so a species that starts at order one and decays to something tiny outgrows its own tolerance partway through and stops being error-controlled from there on.CVODE’s construct for that is
CVodeWFtolerances: instead of a fixed vector, the integrator is handed a callback that computes the error weights at the state actually being integrated. This is that mode, and the rule it installs is:atol_i(y) = clamp(rtol * |y_i|, ceiling_i * 10**-decades, ceiling_i)
Read it as: hold every species to
rtolof the magnitude it currently has, never looser than the vector already asked for, and stop tightening once it has fallendecadesdecades below where it started.Two consequences worth knowing before reaching for it:
It is not free. Resolving a decay over twelve decades is real work. On
tests/data/deep_decay.netthe step count goes 162 → 605. What it buys is an answer instead of noise: that species’ value at the end of the run is 3.8e-10 under the scalar tolerance and 9.36e-14 under tracking, against an analytical 9.357623e-14.It cannot conjure digits that are not there. A species whose value is formed as a difference of large fluxes carries absolute roundoff of order
eps * flux, and asking for relative accuracy below that will collapse the step size rather than sharpen the answer. That is whatdecadesbounds, and why it is finite.
- Parameters:
decades (
float) –How far below its ceiling a species keeps being resolved relatively.
0reduces the rule to the ceiling vector exactly — the mode is a strict extension ofderive_atol(), not a different rule that happens to agree somewhere. The default, 12, is where the measured accuracy stops improving. Ontests/data/deep_decay.net, worst relative error in the decaying species against its analytical value:decades
rel. err.
0
1.8e+04
3
3.6e+01
6
9.5e-02
8
2.0e-03
12
2.6e-06
16
5.0e-06
20
9.4e-06
Past 12 it gets slightly worse, which is the tell that the limit there is roundoff rather than the tolerance — and 12 also stays clear of the
eps-relative floor where a coupled model’s own roundoff starts failing error tests.ceiling (
Union[float,Sequence[float],NDArray[double],Literal['auto']]) – The tolerance being tracked below — the same per-species vector the non-tracking path takes."auto"(the default) derives it exactly asatol="auto"does: from the model’s live state for a run, from the rows’ starts in a batch or a scan (issue #1660); a float broadcasts to every species; a sequence is used as given. Every entry must be strictly positive: a zero ceiling scales to a zero floor, and a species then at exactly zero would have an infinite error weight.
See also
bngsim.derive_atolthe ceiling this tracks below, built from a state.
bngsim.Simulator.auto_atolthe same, against the model’s live state.
Examples
>>> result = sim.run(t_span=(0, 30), atol=bngsim.TrackingAtol()) >>> result = sim.run(t_span=(0, 30), atol="tracking") # same thing >>> result = sim.run(t_span=(0, 30), atol=bngsim.TrackingAtol(decades=6))
- class bngsim.ExactAtol(value)[source]¶
Bases:
objectAn absolute tolerance held exactly, the same for every species (issue #1420).
A scalar
atol, the default1e-8or one you pass, is read as a ceiling: each species takes the smaller of it andrtolof its own scale in the state the run starts from (seebngsim.Simulator.run()). That is what keeps a species at 1e-9 M from running under the noise floor of a tolerance written for molecule counts, and a SED-ML file’satolis read the same way.ExactAtol(value)opts out: every species is held tovalueitself, which is how bngsim read a scalar before.Reach for it to reproduce a run made under the old reading, or to compare against a tool that applies one absolute tolerance to every species. A sequence of
n_speciesvalues is also used exactly as given;ExactAtol(a)is the same as[a] * model.n_specieson the scalar’s own code path.bngsim.convert.read_sedmltakesexact_atol=Trueto read a SED-ML file’s absolute tolerance this way.- Parameters:
value (
float) – The absolute tolerance. Must be finite and >= 0.
Examples
>>> result = sim.run(t_span=(0, 100), rtol=1e-6, atol=bngsim.ExactAtol(1e-9)) >>> sim.set_tolerances(rtol=1e-8, atol=bngsim.ExactAtol(1e-8)) # the old default
- bngsim.psa_cost_decision(speedup, rho, *, kind='mean', kappa_scaled=3.0, kappa_true=3.0)[source]¶
Decide whether PSA is a net win at fixed precision for one observable.
- Parameters:
speedup (
float) – The measured per-path event-rate ratioŝ(> 0), e.g.result.psa_diagnostics["speedup"].rho (
float) – The variance-inflation ratioρ = v_scaled / v_true(> 0) for the observable of interest. Required — obtain it from a calibration/anchor run or an excess model; it cannot be read from the scaled run alone.kind (
Literal['mean','variance']) –"mean"for sample-mean estimation (thresholdρ) or"variance"for sample-variance estimation (Gaussian thresholdρ²).kappa_scaled (
float) – Kurtosesκ_s = μ4_s/v_s²andκ_0 = μ4_0/v_0²of the scaled and true laws, used only forkind="variance". Both default to3(Gaussian), recovering theρ²rule. Each must be> 1.kappa_true (
float) – Kurtosesκ_s = μ4_s/v_s²andκ_0 = μ4_0/v_0²of the scaled and true laws, used only forkind="variance". Both default to3(Gaussian), recovering theρ²rule. Each must be> 1.
- Return type:
- Returns:
dict with keys
kind(str),speedup(float),rho(float)
- ``threshold`` (float) (the speedup
ŝmust exceed for a net win.)- ``cost_ratio`` (float) (total-cost ratio
n_s c_s / (n_0 c_0)at matched) – precision;< 1means scaling is cheaper.- ``net_win`` (bool) (
cost_ratio < 1.)- ``message`` (str) (a human-readable go/no-go summary.)
- bngsim.capabilities()[source]¶
Return a structured capability report for this bngsim install.
- Returns:
A dict with four keys:
"version"— the bngsim package version string."features"—dict[str, bool]mapping each feature/backend name to its availability flag in this install."missing"—dict[str, str]mapping each unavailable feature to a human-readable explanation that distinguishes a missing compiled backend (rebuild flag) from a missing optional Python dependency (pip install ...)."build"—{"commit": str | None, "stale": bool}, which build this is (GH #431). See below.
"features"always contains the same keys regardless of build:nfsim,rulemonkey,klu,lapack_dense,mir,libsbml,antimony,vivarium,bngl,sbml_import,sbml_ssa,sbml_psa,antimony_import,codegen,output_sensitivities,effective_ic_sensitivity,event_sensitivities,cross_compartment_sensitivities,per_species_atol,tracking_atol."missing"is empty when every feature is available.bnglreports whetherbngsim.Model.from_bngl()(andModel.load("x.bngl")) can run here — GH #162. Unlike its neighbours this one is a runtime probe, not an import check: BNGL loading shells out to BNG2.pl, so it needs both a locatable BioNetGen (thebnglextra, or$BNGPATH/$BNG2_PL, orBNG2.plonPATH) and aperlto run it. Computing it is whycapabilities()can be slow on its first call in a process wherebionetgenis installed.output_sensitivitiesreports whether this install can emit the(n_times, n_outputs, n_param)output-sensitivity tensor viaResult.output_sensitivities()(species/observable/expression derivatives w.r.t. parameters and ICs). Likecodegenit is alwaysTrue— it is the capability handshake fitting frontends (e.g. PyBNF) gate their gradient path on (GH #207).effective_ic_sensitivityreports whetherModel.effective_ic_sensitivity()exists — the∂x(0)/∂θmatrix a frontend reads at setup to compose its own initial-condition chain rule without double-counting the seeding theparameteraxis already carries (GH #155). A build without it cannot say what the seed matrix holds, and every answer a consumer could guess is silently wrong, so gate the gradient path on this rather than on a version string.klureports whether the SuiteSparse/KLU sparse linear solver was compiled in. WhenFalsethe ODE backend has only the dense solver, so large/sparse models factorize the full N×N Jacobian at O(N³) — use this to detect a dense-only install before a slow genome-scale run (GH #209).lapack_densereports whether a BLAS/LAPACK backend was linked for the optional optimized dense factor (GH #84). Unlikekluthis one changes speed and nothing else — the built-in dense LU is used either way unlessBNGSIM_LAPACK_DENSE=1is set, and both produce the same trajectory — so it answers “will that env var do anything here?”.Falsein the manylinux and Windows wheels,Truein the macOS ones (Accelerate) and in a source build on a host with LAPACK.The last four are behaviour keys (GH #431), and they answer a different kind of question from their neighbours: not “was this backend compiled in?” but “does this build compute the thing correctly?”. They exist because a version string cannot answer that — bngsim bumps
__version__at the start of a release cycle, so a from-source build made before a fix declares the same number as the release that carries it — and neither can ahasattrprobe, because these fixes change what a build computes rather than what it exposes. A build without one of them returns a finite number with a term missing rather than refusing, so guessing is expensive in a way it is not elsewhere here. See the module-level probes for the full story of each:event_sensitivities— forward sensitivities survive a discrete event, carrying the state-reading assignment’s∂h/∂x·s⁻and the sensitivity history across a root that fires nothing (issues #144, #146).cross_compartment_sensitivities— a reaction whose species live in compartments of different size keeps the analytic∂f/∂pinstead of putting every column of the model on CVODES’ difference quotients (issue #160).per_species_atol—Simulator.run(atol=...)accepts a vector (issue #196).tracking_atol—Simulator.run(atol=TrackingAtol(...))is honoured (issue #213).
"build"identifies the build itself. Two installs can report the sameversionand be different builds, andbuild["commit"]— the commit CMake baked into the compiled extension, orNonewhen it was built outside a git checkout — is the only thing here that tells them apart.build["stale"]isTruewhen that extension is older than the C++ source next to it, which happens only in a source checkout (auto-rebuild is off by design, issue #23) and which every version-, metadata- and feature-based check passes straight through, because nothing in the Python layer moved. bngsim also warns about it at import, but a consumer package’s import happens before that package configures its own logging, so reading it here at a moment of your choosing is the supported way to surface it.Feature names are stable across releases; new features may be added but existing names will not be renamed or removed.
- Return type:
Examples
>>> import bngsim >>> caps = bngsim.capabilities() >>> set(caps) == {"version", "features", "missing", "build"} True >>> set(caps["build"]) == {"commit", "stale"} True >>> caps["features"]["nfsim"] == bngsim.HAS_NFSIM True >>> caps["features"]["sbml_ssa"] == bngsim.HAS_LIBSBML True >>> caps["features"]["klu"] == bngsim.HAS_KLU True >>> caps["features"]["lapack_dense"] == bngsim.HAS_LAPACK_DENSE True
- bngsim.prepare_codegen(net_path, model=None, emit_jac=True)[source]¶
Compile a
.netmodel’s codegen library and return its path.Deprecated since version Codegen: compiles the model bngsim built, whatever it was loaded from (issue #803). Pass
codegen=Truetobngsim.Simulator, or callprepare_model_codegen()on the loaded model.Kept so existing callers keep working, as a thin wrapper:
model(loaded fromnet_pathwithbngsim.Model.from_net()when not given) goes throughprepare_model_codegen(). The.netfile is no longer re-read by a codegen parser of its own – the second reading that disagreed with the loader (#784).emit_jacderives the analytical Jacobian of a model loaded here, asSimulatordoes, so the compiled one is appended; the solver uses it only when itsjacobianasks for an analytical Jacobian. A model the caller passes is compiled as it stands and left as it was: its Jacobian is appended only if the caller already derived it, as the old path did.- Raises:
ValueError – When
net_pathis no.netfile the loader reads (an SBML file, which it refuses by name since issue #1498 and used to load as a 0-species model), or when the model loaded has no species or no reactions (GH #101), as the old path did.CodegenDeclinedError – When codegen declines the model. A
RuntimeError, as the old path raised.RuntimeError – When the build fails, where the old path raised too.
- Parameters:
- Return type:
- bngsim.codegen_cache_info(cache_dir=None)[source]¶
Census the codegen artifact cache: what is in it, by kind and by codegen key.
Reads only directory metadata (one
scandir, plus a walk of any shard directory), so it is cheap even on the 14,000-entry cache issue #205 reports and never opens an artifact — the key comes off the name (issue #363).Examples
>>> import bngsim >>> info = bngsim.codegen_cache_info() >>> info.total_bytes >= 0 True >>> set(info.by_kind) == set(bngsim.cache.KINDS) True >>> len(info.live) + len(info.orphaned) + len(info.other_arch) == sum( ... 1 for e in info.entries if e.is_artifact ... ) True
- bngsim.clean_codegen_cache(cache_dir=None, *, min_age=3600.0, dry_run=False)[source]¶
Remove leaked compile partials — and only those.
Safe by construction: the selection is exactly
PARTIAL_KINDS, so no compiled artifact is touched and no cache hit is lost. What it collects is the debris of compiles that died between writing their scratch files and thefinallythat removes them:bngsim_shard_*directories full of.o, therhs_<key>_<hash>.<pid>_<n>.cbeside them, and (on Windows) the.lib/.expsidecarscompile_rhsnever unlinks.- Parameters:
cache_dir (
str|PathLike[str] |None) – Which cache to sweep. Default: the live one (seecodegen_cache_dir()).min_age (
float|str) – Seconds, or a duration string like"90m". Nothing used or written more recently than this is removed. SeeDEFAULT_MIN_AGE.dry_run (
bool) – Select and report, delete nothing.
- Return type:
CacheSweep
- bngsim.prune_codegen_cache(cache_dir=None, *, orphaned=False, keep_keys=(), older_than=None, max_size=None, min_age=3600.0, dry_run=False)[source]¶
Bound the cache by orphan status, age, size, or any combination of them.
At least one of
orphaned/older_than/max_sizeis required — a prune with no bound is either a no-op or aclear_codegen_cache(), and guessing which is not this function’s business.orphaned=Trueis the targeted one, and usually the one wanted after an emitter edit: it evicts every artifact whose name carries a codegen key other than this install’s (CacheInfo.orphaned), which is exactly the set no run can hit again. An age bound cannot express that — it keeps orphans that happen to be recent and throws away live artifacts that merely have not been loaded lately.Order of operations, and the one surprise worth stating: prune subsumes clean. The partial sweep runs first, then artifacts are evicted. That is what makes
max_sizemean what a user means by it — the cap is on the whole directory, so debris counting toward it has to be collectable before artifacts are thrown away to make room for it.max_sizemay be unreachable: foreign entries are never removed, and neither are artifacts inside themin_agefloor. The sweep then evicts everything it legitimately can and stops. Comparesweep.total_bytes_afteragainst the cap to detect it (the CLI does, and says so).- Parameters:
orphaned (
bool) – Evict every artifact whose codegen key is not this install’s, including the pre-#363 names that carry no key at all. Bounded bykeep_keys.keep_keys (
Sequence[str|None]) –Additional codegen keys to treat as live, for a cache directory shared by several bngsim installs — a venv per project is ordinary, and each has its own key, so what is orphaned from one venv’s point of view is live from another’s.
Nonein this sequence spares the pre-#363 names (the artifacts of an install too old to write a key), which is the same situation one version further back. Only meaningful withorphaned=True; passing it without raises, rather than reading as a protection the age and size bounds do not in fact honor.An explicit list rather than a registry of live keys maintained beside the cache: such a registry would have to be written by every install that ever compiles — including ones that never run this CLI — and a stale entry either protects a dead corpus forever or, in the direction that costs something, goes missing and authorizes deleting a live one. The flag is a decision the person sweeping makes with
codegen_cache_info()’s per-key table in front of them.older_than (
float|str|None) – Seconds, or a duration string like"30d". Artifacts not used within this window are evicted. “Used” ismax(atime, mtime)— see_stamp().max_size (
int|str|None) – Bytes, or a size string like"2G"(powers of 1024). After the orphan and age passes, artifacts are evicted least-recently-used until the directory fits.min_age (
float|str) – The floor below which nothing is removed, whatever the other bounds say. It applies to the LRU pass too, which is where it earns its keep: on a cache whose entries have all been loaded recently and which is over the cap, LRU order still has to nominate one of them — and the one it nominates may be the artifact a sibling process is about todlopen. It applies to the orphan pass as well: an orphan minutes old is one another process just compiled under its own key and is about todlopen.dry_run (
bool) – Select and report, delete nothing.
- Return type:
CacheSweep
- bngsim.clear_codegen_cache(cache_dir=None, *, min_age=0.0, dry_run=False)[source]¶
Remove every artifact and partial bngsim owns. Foreign entries are left alone.
min_agedefaults to0here, unlike the other verbs: “clear” is an explicit instruction to empty the cache, and a floor that quietly left the newest entries behind would make it a lie. Pass one to reinstate it —clear_codegen_cache(min_age=DEFAULT_MIN_AGE)is the belt-and-braces form.The one thing a zero floor does not switch off is the POSIX liveness check: a partial whose compile is still running stays, because emptying a cache is a reason to reclaim disk and not a reason to break a build that is in progress. It is reported in
sweep.heldrather than silently skipped.The cache directory itself is not removed:
compile_rhsrecreates it on demand, but a caller that pointedBNGSIM_CODEGEN_CACHE_DIRat a directory somebody else provisioned (mode, ACLs, a mount point) should get it back intact.Examples
>>> import bngsim >>> sweep = bngsim.clear_codegen_cache(dry_run=True) # what would go >>> sweep.dry_run True
- bngsim.parse_net_file(path)[source]¶
Read a BNG .net file into a structured dictionary.
The reading is the C++ loader’s (
Model.from_net’s), so it needs bngsim’s compiled extension: the records it hands ModelBuilder, with the values the model it builds from them holds. The build runs here too, so a fileModel.from_netrefuses is refused here, and theSat/Hillrewrite and the lifting of expression-valued initial concentrations happen as they do there.- Parameters:
- Returns:
Structured contents of the
.netfile, with keys:parameters : list of (name, value, expression, is_expression) species : list of (name, init_conc, is_fixed) species_ic_params : list of (species_idx0, param_name) for each species whose IC column names a parameter observables : list of (name, entries), entries = [(sp_idx0, factor), ...] functions : list of (name, expression) reactions : list of dict with keys reactants, products (0-based species indices), type, rate_law, legacy_constants, stat_factor net_file_dir : the file's own directory, absolute, which is what a relative ``tfun('...')`` path resolves against unit_conversions : list of (reaction, expression), one for each ``unit_conversion=`` comment BNG2.pl left on a reaction line of a compartmental model. The reaction's rate holds that factor as a number, so the parameters the expression reads are refused for writing in the model built from this dictionary (``Model.frozen_params``, issue #711)Values are evaluated: a parameter’s is the number
Model.from_netputs in its slot, and a species whose initial concentration names a parameter carries that parameter’s value. An expression-valued initial concentration is a synthetic_InitialConc<N>parameter, as BNG2.pl writes one, and the species names it.A reaction’s
typeis one of:"elementary"rate_lawis a parameter name;stat_factorcarries a<number>*prefix on it."functional"rate_lawnames a function in thefunctionsblock. A deprecatedSatorHillrate law comes back as one of these, rewritten into an explicit function (and a single-species observable for each reactant it reads), with the loader’sUserWarning."mm"Michaelis-Menten (
MM kcat Km);rate_lawis"<kcat>,<Km>", the formModelBuildertakes.
stat_factoris the<number>*prefix on the rate column, whatever the type: BNG2.pl writes it ahead of a typed rate law too (2*MM kcat Km,2*Sat k K,2*Hill V K h, for a substrate with equivalent sites), and it comes back on the"mm"or rewritten"functional"reaction, multiplying the whole rate (issue #1230).legacy_constantsholds an"mm"reaction’s[kcat, Km]and is empty otherwise: aSatorHillrate law, whose operands it used to carry, now comes back rewritten (issue #803).- Return type:
- Raises:
FileNotFoundError, IsADirectoryError – If
pathis not a file.ValueError – If
Model.from_netwould refuse the file. The reading builds the model, so that includes atfun('...')data fileModel.from_netwould not find beside the.net: put it there to read the file, then setnet_file_dirin the dict to build against another location.ImportError – If bngsim’s compiled extension is not available.
- bngsim.build_model_from_parsed(parsed)[source]¶
Build a NetworkModel from parsed .net data via ModelBuilder.
It makes the calls
NetFileLoader::loadmakes, in the same order, so the dictionaryparse_net_filereturns builds the modelModel.from_netloads, and a modified dictionary builds the modified model. Four things to know when modifying one:a species listed in
species_ic_paramstakes its initial concentration from that parameter, so change the parameter, or drop the entry, rather than the number inspecies;a parameter with
is_expressiontrue is evaluated from its expression, so change the expression, or setis_expressionfalse with a value;a reaction’s rate law is read by what it names: a function in
functionsmakes it functional, anything else elementary ("mm"aside), whatever itstypesays;the parameters a
unit_conversionsentry reads are refused for writing in the model built (issue #711): the rates hold that factor as a number. Drop an entry only with the number it stands for taken out of the rate. An entry names parameters by the names inparameters: rename one in both, or the entry names nothing and the renamed parameter is written with no rate following it. A dictionary with nounit_conversionskey builds a model with nothing refused.
- Parameters:
parsed (
dict[str,Any]) – Output ofparse_net_file(), or a dictionary of the same shape.- Returns:
The constructed model.
- Return type:
- Raises:
ValueError – If a reaction’s
rate_lawis empty, or itstypeis"legacy"(aSat/Hilltoken, whichparse_net_fileno longer returns — write the rate law as a function), or aspecies_ic_paramsentry names a parameter the dictionary does not declare or a species it does not have.RuntimeError – If
ModelBuilderrefuses the model, as it refuses the same records from the loader: an elementaryrate_lawthat names neither a parameter nor a function, for instance, or a parameter expression that reads an observable or a function (issue #844).
- bngsim.sbml_to_net(sbml_path, out_path=None, *, validate='L2', strict=True, sedml=None, t_span=(0.0, 100.0), n_points=101, rhs_tol=1e-06)[source]¶
Convert an SBML model to a BioNetGen
.netnetwork.- Parameters:
out_path (
str|Path|None) – Where to write the.net. If None, the text is returned in the report but no file is written.validate (
str|None) –"L2"(default) runs the lightweight structural check plus two trajectory-free faithfulness probes at the initial and a nonlinear-probe state (no integration): a direct ODE-RHS identity self-check (max_rhs_delta) that reloads the.netand confirms it reproduces the source right-hand side, and an assignment-rule report check (max_ar_report_delta) that compares what the two models report for each AssignmentRule-target species emittedfixed(its live value is a Simulator report transform the reloaded network rebuilds from the.net’s structure, #515 — invisible to the RHS probe because itsdy/dtis zero in both models; GH #18). Together they close the GH #223 / GH #18 silent-loss hole: a network whose constructs were emitted as constants the.netcannot drive (assignment/rate-rule forcing the writer can’t carry) is flagged (strict=Trueraises;strict=Falsewarns and recordsConversionReport.rhs_faithfulFalse)."L1"runs only the structural-equivalence check (counts + topology)."full"runs the complete L0–L4 conversion-validation ladder (GH #217) and gates on the hard levels L0–L3 — “convert and prove faithful”; the L4 symbolic check is recorded but never blocks.Noneskips validation. A"full"gate’s verdict is attached asConversionReport.validationand decidesConversionReport.ok.strict (
bool) – Raisebngsim.ConversionErroron constructs the.nettext format cannot carry faithfully.Falsedowngrades to abngsim.ConversionWarningand emits a best-effort network.sedml (
str|Path|None) – The SED-ML simulation protocol that accompanies the SBML (the COMBINE sidecar). When given, its time course is parsed and the"full"gate’s L3 numerical comparison runs over the model’s own horizon — the exact mirror ofnet_to_sbml()’sbngl=(avoids the blanket-grid stiff-hang and exercises the trajectory the modeller actually ran). The parsedProtocolSpecis attached to the report.t_span (
tuple[float,float]) – Fallback time grid for the"full"gate’s L3 comparison when nosedmlprotocol horizon is available (ignored for othervalidatevalues).n_points (
int) – Fallback time grid for the"full"gate’s L3 comparison when nosedmlprotocol horizon is available (ignored for othervalidatevalues).rhs_tol (
float) – Scale-relative tolerance for the"full"gate’s L2 ODE-RHS identity.
- Return type:
ConversionReport