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 omitting seed=): bngsim draws a fresh 31-bit seed from system entropy on each call. Two consecutive calls produce independent trajectories.

  • seed=N (any integer): bngsim passes N down to the backend verbatim. A fresh Simulator (or NfsimSession / RuleMonkeySession) initialized with the same seed=N reproduces 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.