Quickstart¶
Load a model and simulate¶
import bngsim
# Load a BioNetGen .net file
model = bngsim.Model.from_net("model.net")
# Create an ODE simulator
sim = bngsim.Simulator(model, method="ode")
# Run simulation
result = sim.run(t_span=(0, 100), n_points=101)
# Access results as NumPy arrays
print(result.time.shape) # (101,)
print(result.species.shape) # (101, n_species)
print(result.observables.shape) # (101, n_observables)
Parameter sweeps¶
# Update parameters in-memory (no file I/O)
model.set_param("kf", 0.5)
model.set_param("kr", 0.1)
result2 = sim.run(t_span=(0, 100), n_points=101)
# Reset species to initial conditions
model.reset()
# Set multiple parameters at once
model.set_params({"kf": 1.0, "kr": 0.2, "kcat": 5.0})
Stochastic simulation (SSA)¶
ssa = bngsim.Simulator(model, method="ssa")
result = ssa.run(t_span=(0, 100), n_points=101, seed=42)
Stochastic simulation (PSA)¶
PSA (partial-scaling approximation) accelerates large-population runs by
leaping reaction firings whose minimum reactant population exceeds
poplevel = N_c, while preserving Gillespie statistics. The dispatch and
the validate_for_ssa gate are shared with SSA, so the same call shape
works for .net, SBML, and Antimony models:
import bngsim
# .net
net_model = bngsim.Model.from_net("psa/tcr_signaling.net")
psa = bngsim.Simulator(net_model, method="psa", poplevel=100.0)
result = psa.run(t_span=(0, 300), n_points=301, seed=42)
# SBML / Antimony — same dispatch
sbml_model = bngsim.Model.from_sbml("ssa_sbml/tcr_signaling.xml")
psa = bngsim.Simulator(sbml_model, method="psa", poplevel=100.0)
result = psa.run(t_span=(0, 300), n_points=301, seed=42)
Choose N_c per Lin, Feng, Hlavacek (2019);
accuracy degrades when N_c is too small relative to per-species populations.
SBML/Antimony models with kineticLaw shapes that SSA cannot handle (e.g.
reversible_non_mass_action, non_integer_stoichiometry) raise
SsaValidationError at construct time.
Seed semantics for stochastic methods¶
The seed= keyword on every stochastic entry point follows one contract:
seed=None(the default, or omittingseed=): bngsim draws a fresh 31-bit seed from system entropy on each call. Two consecutive calls produce independent trajectories.seed=N(any integer): bngsim passesNdown to the backend verbatim. A freshSimulator(orNfsimSession/RuleMonkeySession) initialized with the sameseed=Nreproduces the same trajectory.
Result.seed exposes the integer that was actually used, so
caller-generated and bngsim-drawn seeds are equally inspectable:
import bngsim
sim = bngsim.Simulator(model, method="ssa")
# Default — fresh seed each call:
r1 = sim.run(t_span=(0, 100), n_points=101)
r2 = sim.run(t_span=(0, 100), n_points=101)
assert r1.seed != r2.seed # seeds differ
# Explicit, reproducible:
sim_a = bngsim.Simulator(model.clone(), method="ssa")
sim_b = bngsim.Simulator(model.clone(), method="ssa")
ra = sim_a.run(t_span=(0, 100), n_points=101, seed=42)
rb = sim_b.run(t_span=(0, 100), n_points=101, seed=42)
assert ra.seed == rb.seed == 42 # exact reproduction
The same contract applies to Simulator.run_batch,
NfsimSession.initialize, and RuleMonkeySession.initialize. For
run_batch, the resolved value is the base seed; per-sim seeds are
base_seed + i and stamped onto each Result.seed.
Result.seed is None for ODE results and round-trips through HDF5
Result.save() / Result.load(). For sessions, the seed used by
initialize() is exposed as session.seed and stamped onto every
Result returned by simulate().
Reproducibility unit. The reproducibility contract is:
same starting model state + same
seed=N→ same trajectory
The Simulator object itself is not the unit. The C++ SSA/PSA backends
construct a fresh std::mt19937_64 from the passed seed on every
.run() call, so passing the same explicit seed= always seeds the
RNG identically. What persists across .run() calls on the same
Simulator is the model state (species concentrations) — that’s
what makes simulate(...); simulate({continue=>1, ...}) multi-segment
protocols work. So:
sim = bngsim.Simulator(model, method="ssa")
# First run: starts from model's initial state, seed=42.
r1 = sim.run(t_span=(0, 100), n_points=101, seed=42)
# Second run on the same sim with seed=42 *continues from r1's end
# state*, then advances with the seed=42 RNG stream. r2 is correctly a
# different trajectory because it starts from a different state — not
# because of any RNG issue.
r2 = sim.run(t_span=(0, 100), n_points=101, seed=42)
assert r2.species[0].tolist() == r1.species[-1].tolist() # state continues
# To re-run the same trajectory: reset the model first.
model.reset()
r3 = sim.run(t_span=(0, 100), n_points=101, seed=42)
assert (r3.species == r1.species).all() # exactly reproduces r1
Two fresh Simulator instances with the same seed also reproduce —
they just both start from the model’s initial state, which is the
same as calling model.reset() between runs.