The implementation targets time-homogeneous continuous-time Markov chains (CTMCs) represented as reaction networks. The state is a vector of nonnegative integer counts,
and the model contains
- a stoichiometric increment
$\boldsymbol{\nu}_j \in \mathbb{Z}^D$ ; - a propensity
$a_j(\mathbf{x}) \ge 0$ .
Conditioned on the current state
The total propensity is
The public propensity interface is propensity(state). This restriction makes
the time-homogeneity assumption explicit: propensities may depend on the
current state and fixed model parameters, but not continuously on absolute
time between jumps. Nonhomogeneous processes require an integrated-hazard or
thinning method and are outside the scope of this implementation.
While the state remains fixed,
so that
Conditioned on an event occurring, its channel is categorical:
Equivalently, the joint density of waiting time and channel is
The implementation samples
The strict inequality matches NumPy's half-open uniform interval and ensures
that a zero-propensity channel is not selected when
For initial state
t <- t_start
x <- copy(initial_state)
record(t, x)
while t < t_end:
if accepted_events == max_events:
terminate with max_events
a <- evaluate_propensities(x)
require finite(a) and a >= 0
a0 <- sum(a)
if a0 == 0:
terminate with absorbing_state
tau <- Exponential(rate=a0)
t_next <- t + tau
if t_next > t_end:
terminate with time_horizon
mu <- categorical_channel(weights=a)
x_next <- x + nu[mu]
validate(x_next)
t <- t_next
x <- x_next
record(t, x, mu)
An event with t_next == t_end is accepted. An event with
t_next > t_end is not selected or applied.
| Reason | Condition | Final boundary row |
|---|---|---|
time_horizon |
The next event lies beyond t_end, or an accepted event reaches it |
Present at t_end unless the last event is already there |
absorbing_state |
The absorbing state is extended to t_end
|
|
max_events |
The accepted-event limit is reached before t_end
|
Not added, because the uncomputed continuation is unknown |
reaction_indices contains only accepted reactions. A synthetic row at
t_end records the state at the boundary but does not increment
SimulationResult.event_count.
The engine enforces the following conditions:
- the state is a one-dimensional
int64vector of nonnegative counts; - each stoichiometric increment has the same dimension as the state;
- species names and reaction names are nonempty and unique;
- every propensity is scalar, finite, and nonnegative;
- the total propensity is finite;
- accepted state updates remain nonnegative and do not overflow
int64; - event times are strictly increasing;
- optional model-specific invariants hold initially and after every event.
A zero total propensity is a valid absorbing state, not an exception. A
negative or nonfinite propensity is a model error. If FloatingPointError
rather than entering a non-progressing loop.
For a trajectory with
SimulationResult.times and SimulationResult.states represent a
right-continuous path with left limits. For recorded event times
the state on a sampling grid is defined by previous-value interpolation:
At an event time, this convention returns the post-reaction state. Between events, it returns the most recently accepted state. Linear interpolation is not appropriate for integer-valued jump processes, and nearest-neighbor sampling can introduce look-ahead bias by exposing a state before its event.
Ensemble statistics are computed only after all trajectories have been mapped to a common grid with this convention. The grid changes the representation of the paths, not the simulated event process.
The example model uses the state
with two reaction channels.
Both reactions conserve
When
simulate constructs a run-local NumPy generator from the supplied seed; it
does not use global numpy.random state. run_ensemble expands a master seed
with numpy.random.SeedSequence and assigns one child seed to each run. The
ordering of the returned trajectories is deterministic for fixed inputs and a
fixed master seed.
Reproducible analysis requires retaining
- model parameters and initial state;
t_start,t_end, andmax_events;- the run seed or ensemble master seed;
- the package, Python, and NumPy versions.
The result arrays do not encode arbitrary parameters captured by propensity
callbacks. Consequently, summarize_ensemble verifies species and reaction
catalogs but cannot detect different parameterizations with identical names.
Only trajectories generated from the same configured model should be combined
in one summary.
This implementation provides the exact Direct Method for finite, time-homogeneous reaction networks. It does not implement tau-leaping, time-dependent hazards, delayed reactions, spatial processes, or deterministic integration. Those methods require different mathematical assumptions and separate numerical kernels.
The benchmark in benchmarks/benchmark_core.py reports median wall time and
event throughput without imposing a hardware-dependent pass/fail threshold.
It is intended for regression measurements under a controlled software and
hardware configuration.
- D. T. Gillespie, “A general method for numerically simulating the stochastic time evolution of coupled chemical reactions”, Journal of Computational Physics, 1976.
- D. T. Gillespie, “Exact stochastic simulation of coupled chemical reactions”, The Journal of Physical Chemistry, 1977.