Use with PyBNF

When bngsim is installed, PyBNF automatically uses it for BNGL model fitting instead of spawning run_network subprocesses:

pip install bngsim
cd tests/full_tests
pybnf -c T1-ssprop/polynomial.conf
# T1 benchmark: 3:16 (was 3+ hours without bngsim)

No configuration changes needed — PyBNF auto-detects bngsim at import time.

Scheduler-free cluster evaluation

A fitting run distributes thousands of independent evaluations across a cluster. BNGsim is the stateless single-evaluation kernel for that workload — the frontend (PyBNF) owns the scheduler: multistart, bootstrap, profile likelihood, Slurm/MPI fan-out. BNGsim adds no scheduler code; it exposes the raw output + sensitivity primitives (the (n_times, n_outputs, n_param) tensor), never a pre-baked loss — objective/noise/normalization composition stays in the frontend.

Statelessness / re-entrancy. Every evaluation runs against an independent model clone with no shared mutable state (the C++ engine is instance-based with no globals, no file I/O, no stdout). run_batch clones the model per row, so many threads — or many processes — evaluate concurrently against the one read-only compiled .so without interfering. For a fixed (model, θ, sensitivity set, solver options) the result is deterministic, and batch rows are returned in input order regardless of worker count.

Shared compiled artifact. The codegen cache is content-addressed by model hash and updated atomically (compile to a process-unique temp file, then os.replace into the cache), so concurrent jobs racing on the same model never observe a partial .so. Point BNGSIM_CODEGEN_CACHE_DIR at node-local scratch, or pre-warm it once on a login node and have worker jobs reuse the artifact:

# 1. Compile once on the login node into a shared/staged cache.
export BNGSIM_CODEGEN_CACHE_DIR=/scratch/$USER/bngsim_cache
python -c "import bngsim; bngsim.Simulator(bngsim.Model.from_net('model.net'), codegen=True)"

# 2. Every worker job inherits the env var and reuses the cached .so (no compile).
sbatch --export=ALL fit_job.slurm

Local batch over a parameter matrix. A sensitivity_params-configured Simulator yields the full per-row output-sensitivity tensor from run_batch, reusing the one shared artifact:

sim = bngsim.Simulator(model, method="ode", sensitivity_params=["kf", "kr"])
rows = sim.run_batch(
    t_span=(0, 100), n_points=101,
    params=[{"kf": kf, "kr": kr} for kf, kr in theta_matrix],
    num_processors=8,           # independent clones, one shared .so
)
for r in rows:                  # deterministic input order
    g = r.output_sensitivities(["observable:Atot"])   # (n_times, 1, n_param)

Checkpoint / restart. EvaluationSpec is a frozen, JSON-serializable record of one evaluation — model source (+ optional SHA-256 integrity guard), θ vector, time grid, sensitivity set, solver options, and output selectors. Ship it to a worker or write it to a checkpoint; evaluate() reconstructs the simulator and runs deterministically. Pair it with the compact Result.summary() for cheap indexing/logging without re-reading every full HDF5 payload:

spec = bngsim.EvaluationSpec(
    model_source="model.net", model_format="net",
    t_span=(0, 100), n_points=101,
    params={"kf": 0.5}, sensitivity_params=("kf",),
    outputs=("observable:Atot",),
)
blob = spec.to_json()                              # checkpoint / send to worker
result = bngsim.EvaluationSpec.from_json(blob).evaluate()
json.dump(result.summary(), open("eval_0001.json", "w"))   # compact index entry
result.save("eval_0001.h5")                        # full arrays (HDF5)

spec.with_params(theta_row) stamps a θ row onto a base spec, so a sweep serializes one spec plus a matrix rather than thousands of near-duplicates.