When you already have a path, Model.load picks the right factory for you:
model = bngsim.Model.load("model.ant") # -> Model.from_antimony
model = bngsim.Model.load("model.xml") # -> Model.from_sbml (.sbml too)
model = bngsim.Model.load("model.net") # -> Model.from_net
model = bngsim.Model.load("model.bngl") # -> Model.from_bngl (needs BNG2.pl)Matching is case-insensitive, and defer_jacobian is forwarded to the selected
factory. An unrecognized suffix raises ModelError listing the loadable ones.
The format-specific factories below remain the explicit route when the suffix
does not match the contents (for example an SBML document saved as .txt).
BNGsim simulates reaction networks; BNGL describes rules. Turning one into
the other is network generation — BNG2.pl's job — so Model.from_bngl runs it
rather than parsing BNGL itself:
model = bngsim.Model.from_bngl("egfr.bngl")That needs BNG2.pl on the machine. Either install the extra, which brings PyBioNetGen (BNG2.pl is bundled with it):
pip install 'bngsim[bngl]'...or point at a BioNetGen you already have. The resolution order is
bng2_pl= argument → $BNG2_PL → $BNGPATH → BNG2.pl on PATH → installed
PyBioNetGen, so an environment variable always overrides an installed package:
export BNGPATH=/path/to/BioNetGen-2.9.3BNG2.pl is a Perl script, so perl must also be on PATH — macOS and most
Linux distributions ship one; stock Windows does not, and BNGL loading is simply
unavailable there. bngsim.HAS_BNGL is the probe for the whole arrangement
(both halves, checked at the moment you ask), and when it is False,
bngsim.capabilities()["missing"]["bngl"] names every location that was
searched:
if not bngsim.HAS_BNGL:
print(bngsim.capabilities()["missing"]["bngl"])A .bngl in the wild ends in simulate({...}) or parameter_scan({...}).
Running the file as written would run the author's entire experiment just to
obtain a network. So the actions stop at the source's first generate_network,
which runs with the source's own max_iter / max_agg / max_stoich, since
those are what make an unbounded rule set finite. A file with no
generate_network before its first run stops at that run, the state the run
starts from.
- Before that point: actions are applied, as BNG2.pl applies them. A
setParameterorsetConcentrationthere seeds the network. Only file writes,setOutputDirandquitare left out. - At and after it: everything is dropped, as the author's experiment.
If that experiment sets a parameter or a concentration before its first run
(simulate, parameter_scan or bifurcate), the model you get is not the one
the author first simulated. from_bngl warns with a ConversionWarning naming
those actions. It names only the actions that change the state the run starts
from: a setConcentration to the amount the species already holds is not one,
and neither is a value set and then set back, or a saveConcentrations()
followed by resetConcentrations(). Pass protocol=True to recover the actions
instead of losing them:
model, spec = bngsim.Model.from_bngl("egfr.bngl", protocol=True)
[e.t_span for e in spec.experiments] # the simulate(...) calls, as a ProtocolSpecSee Interchange for what a ProtocolSpec carries.
bngsim.run_bngl loads a file and runs its actions in order, as BNG2.pl runs
the file. bngsim.run_protocol(model, spec) runs a spec you already hold on a
copy of the model:
out = bngsim.run_bngl("egfr_path.bngl") # equilibrate without EGF, then add it
out[1].result.observables["Sos_act"] # the 120 s stimulation, as BNG2.pl runs it
out.model # the model in the state the file leaves itThere is one entry per simulate, parameter_scan or bifurcate. You can
index the result by position, or by a run's suffix. A scan's points are in
scan_results, one Result per value. A bifurcation's sweep back down is in
backward_results. The steps follow BNG2.pl:
- State between runs. Each run starts from the state the last one left.
continue=>1also starts at the time it left; any other run starts at itst_start, 0 by default. - Values. A species whose amount is a parameter or an expression follows it
until a run makes it a number.
setParameter,setConcentrationandaddConcentrationchange the live model. - Saved states.
saveConcentrations/resetConcentrationsandsaveParameters/resetParameterskeep one saved state per label. - Scans.
parameter_scanresets to its starting state before each point whenreset_concis on (the default), and again at the end. The scanned parameter keeps its last value.bifurcatenever resets. - Tolerances. ODE runs use
run_network's tolerances, 1e-8 absolute and relative, unless the action or the call gives others.seed=seeds the stochastic runs.
What bngsim cannot run as written is refused before anything runs:
- a network-free (NFsim) step, which needs a live NFsim session (issue #1482);
- PLA, which bngsim does not have;
- a run argument such as
stop_iformax_sim_steps; - an action that changes results but has no step to run, such as
setVolume; - an argument the protocol holds only approximately.
allow_lossy=True runs the last two as nearly as the protocol holds them, with
a warning. Two things BNG2.pl refuses run here instead: a run that names no
method runs as ODE, and a steady_state=>1 run that does not reach a steady
state is warned about while the protocol goes on, where BNG2.pl stops it.
Network generation is the expensive step, so the emitted .net is kept under
~/.cache/bngsim/networks (override with $BNGSIM_BNGL_CACHE_DIR) and reused
when the same BNGL is loaded again. The key is a digest of the flattened model
text and the BNG2.pl that produced it, so the cache cannot go stale: editing
the model — or upgrading BioNetGen — regenerates, while editing only the actions
block correctly reuses.
Keeping the network is not only a speed choice. Model.from_net records the
path, and codegen prefers that file because a BNG2.pl network carries derived
rate-constant parameters whose chain rules the in-memory path does not
reconstruct. To keep the .net as an artifact of your own, pass net_out=:
model = bngsim.Model.from_bngl("egfr.bngl", net_out="egfr.net")cache=False regenerates into a per-process directory instead.
Compartmental (cBNGL) models load: BNG2.pl bakes each compartment's volume into
the generated rate constants, exactly as for a hand-generated .net. For that
reason Model.load refuses compartment_sizes= on .bngl as it does on
.net — the volume has to change in the BNGL source, before generation.
set_param refuses to change a volume for the same reason, and a sensitivity
column for one is refused (issue #711): the parameter is still in the model, and
no rate reads it. Model.frozen_params lists them.
A model whose network is unbounded hits timeout= (600 s by default) and gets a
ModelError saying so; a model BNG2.pl rejects gets one carrying the tail of
BNG2.pl's own output, which is the only thing that can localize a BNGL syntax
error.
BNGsim can load models from Antimony (.ant) and SBML (.xml) files in addition
to BNG .net files. This uses libantimony for parsing and libsbml for correct
SBML semantics (compartments, boundary species, initial assignments, piecewise
functions, function definitions).
# Load from Antimony file
model = bngsim.Model.from_antimony("model.ant")
# Load from Antimony string
model = bngsim.Model.from_antimony_string("""
S1 = 100; S2 = 0;
k1 = 0.1; k2 = 0.05;
J1: S1 -> S2; k1 * S1;
J2: S2 -> S1; k2 * S2;
""")
# Load from SBML file
model = bngsim.Model.from_sbml("model.xml")
# Load from SBML XML string
model = bngsim.Model.from_sbml_string(sbml_xml_text)
# All model types support the same simulation API
sim = bngsim.Simulator(model, method="ode")
result = sim.run(t_span=(0, 100), n_points=101)Antimony loading requires bngsim[antimony]. Direct SBML loading requires
python-libsbml>=5.20 (installed automatically with the base package).
SBML Level 3 packages. A document that declares a package required="true"
— SBML's way of saying the package changes the mathematical meaning of the model
— is refused by name unless bngsim accounts for that package. comp does load
(the composition is flattened first); so does distrib, whose random draws the
math translator refuses individually. multi, qual, spatial and the rest are
refused, because reading only the core <listOfSpecies> / <listOfReactions> of
such a document builds a different model — a multi document's core species are
rule templates, and a qual document's core layer is typically empty. A
presentation-only package (layout, render, fbc) declares required="false"
and loads untouched.
parse_net_file returns the components of a .net file as a plain Python
dict, which you can inspect, modify and build, or hand to another engine. It is
the C++ loader's own reading of the file, the one Model.from_net builds, with
the values the built model holds, so the two cannot disagree about what a line
means, and a file Model.from_net refuses is refused here too. It needs
bngsim's compiled extension.
import bngsim
parsed = bngsim.parse_net_file("model.net")
print(parsed["parameters"]) # [(name, value, expr, is_expr), ...]
print(parsed["species"]) # [(name, init_conc, is_fixed), ...]
print(parsed["species_ic_params"]) # [(sp_idx, param_name), ...] — ICs written
# as a parameter name rather than a number
print(parsed["observables"]) # [(name, [(sp_idx, factor), ...]), ...]
print(parsed["functions"]) # [(name, expression), ...]
print(parsed["reactions"]) # [{"reactants": [...], "products": [...],
# "type": "elementary"|"functional"|"mm",
# "rate_law": "k1", "legacy_constants": [],
# "stat_factor": 1.0}, ...]
print(parsed["net_file_dir"]) # the file's own directory — what a relative
# tfun('...') path resolves againstA reaction's "type" says how to read its "rate_law":
"elementary": a parameter name. BNG2.pl writes a numeric prefix on the rate constant,0.5*k1, for a symmetry factor or, in a compartmental model, a unit conversion folded into it (1.6605503e-12*kf); the dict carries that as"rate_law": "k1"and"stat_factor": 0.5, so the rate constant isstat_factortimes the parameter."functional": a name from thefunctionsblock. The function's value is a rate constant too: the rate isstat_factortimes the function times the reactant amounts, as for an elementary reaction (0.5*f1comes back as"rate_law": "f1","stat_factor": 0.5)."mm":"<kcat>,<Km>"for aMM kcat Kmrate column, with"legacy_constants": [kcat, Km]. BNG2.pl writes a symmetry factor ahead of the type,2*MM kcat Km, and it comes back as"stat_factor": 2.0, which multiplies the whole rate (likewise2*Sat k Kand2*Hill V K h, whose factor stays the rewritten reaction's"stat_factor").
Values are evaluated: a parameter's is the number Model.from_net puts in its
slot, and a species whose initial concentration names a parameter carries that
parameter's value. An initial concentration written as an expression comes back
as a synthetic _InitialConc<N> parameter, as BNG2.pl writes one, named by the
species.
The deprecated Sat and Hill rate-law tokens come back rewritten, as
Model.from_net rewrites them, with the same warning: the reaction is
"functional", driven by an explicit function, and a single-species observable
is added for each reactant the function reads. A factor ahead of the type
(2*Sat k K, 2*Hill V K h) stays the reaction's "stat_factor".
A reaction line is <index> <reactants> <products> <rate law>, and only a
Sat, Hill or MM rate law takes more tokens: its constants.
parse_net_file and Model.from_net refuse a line with anything else after
the rate law, as run_network does, rather than load it without that part.
bngsim reads no per-reaction modifiers, such as a delay (issue #505). A stat
factor ahead of the rate law's type (2*MM kcat Km) is read off the first
token before the type is matched, as run_network reads it.
Use with BNGsim (fastest path — C++ CVODE/SSA):
model = bngsim.build_model_from_parsed(parsed)
sim = bngsim.Simulator(model, method="ode")
result = sim.run(t_span=(0, 100), n_points=101)build_model_from_parsed makes the ModelBuilder calls the loader makes, so an
unmodified dict builds the model Model.from_net loads, table functions and
rewritten Sat/Hill rate laws included; a test pins that over every .net
file in the repository. A modified dict builds the modified model. Three 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 whose
is_expressionis true 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 its"type"says.
Use with scipy:
import numpy as np
from scipy.integrate import solve_ivp
parsed = bngsim.parse_net_file("model.net")
y0 = np.array([ic for _, ic, _ in parsed["species"]])
pvals = {n: v for n, v, _, _ in parsed["parameters"]}
fixed = [i for i, (_, _, is_fixed) in enumerate(parsed["species"]) if is_fixed]
# Build your own RHS from the parsed data (mass-action reactions only)
def rhs(t, y):
dydt = np.zeros(len(y))
for rxn in parsed["reactions"]:
if rxn["type"] != "elementary":
raise NotImplementedError(rxn["type"])
rate = rxn["stat_factor"] * pvals[rxn["rate_law"]]
for ri in rxn["reactants"]:
rate *= y[ri]
for ri in rxn["reactants"]:
dydt[ri] -= rate
for pi in rxn["products"]:
dydt[pi] += rate
dydt[fixed] = 0.0 # a `$`-clamped species holds its value
return dydt
sol = solve_ivp(rhs, (0, 100), y0, method='LSODA')Use with gillespy2 (Python SSA):
import gillespy2
parsed = bngsim.parse_net_file("model.net")
m = gillespy2.Model(name="my_model")
for name, val, _, _ in parsed["parameters"]:
m.add_parameter(gillespy2.Parameter(name=name, expression=str(val)))
for name, ic, _ in parsed["species"]:
m.add_species(gillespy2.Species(name=name, initial_value=int(ic)))
# ... add reactions from parsed["reactions"], each at stat_factor * rate_law