op-engine 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {op_engine-0.3.0 → op_engine-0.4.0}/CHANGELOG.md +59 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/PKG-INFO +30 -8
- {op_engine-0.3.0 → op_engine-0.4.0}/README.md +29 -7
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/guides/backends.md +3 -0
- op_engine-0.4.0/docs/guides/bounded-thinning.md +112 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/guides/solver-methods.md +106 -6
- op_engine-0.4.0/flepimop2-op_engine/README.md +745 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/benchmarks/README.md +4 -2
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/benchmarks/prepared_execution.py +78 -19
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/pyproject.toml +8 -8
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/__init__.py +330 -31
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/config.py +48 -1
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/reactions.py +185 -34
- op_engine-0.4.0/flepimop2-op_engine/tests/test_adaptive_tau_generated.py +155 -0
- op_engine-0.4.0/flepimop2-op_engine/tests/test_axis_less_reactions.py +210 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_config.py +123 -0
- op_engine-0.4.0/flepimop2-op_engine/tests/test_coord_shift.py +335 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_engine.py +10 -2
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_op_system_integration.py +488 -20
- op_engine-0.4.0/flepimop2-op_engine/tests/test_renewal.py +463 -0
- op_engine-0.4.0/flepimop2-op_engine/tests/test_routed_reactions.py +257 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_solver_matrix_benchmark.py +36 -1
- op_engine-0.4.0/flepimop2-op_engine/tests/test_stochastic_coverage.py +198 -0
- op_engine-0.4.0/flepimop2-op_engine/tests/test_thinning.py +515 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/mkdocs.yml +2 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/pyproject.toml +1 -1
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/__init__.py +16 -0
- op_engine-0.4.0/src/op_engine/_forcing.py +53 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/adaptive_tau.py +29 -4
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/stochastic_solver.py +84 -26
- op_engine-0.4.0/src/op_engine/thinning_ssa.py +443 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_adaptive_tau.py +207 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_direct_ssa.py +262 -3
- op_engine-0.4.0/tests/op_engine/test_tau_forcing.py +247 -0
- op_engine-0.4.0/tests/op_engine/test_thinning_ssa.py +509 -0
- op_engine-0.3.0/flepimop2-op_engine/README.md +0 -381
- {op_engine-0.3.0 → op_engine-0.4.0}/.github/workflows/ci.yaml +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/.github/workflows/gh-pages.yaml +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/.github/workflows/release.yaml +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/.gitignore +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/LICENSE +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/api-reference/.gitignore +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/assets/chemostat-split.svg +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/assets/chemostat-summary.png +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/development/creating-a-release.md +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/development/fem-assembly-boundary.md +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/guides/biogeochemical-network.md +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/guides/chemostat-pde.md +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/guides/getting-started.md +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/guides/linear-multistep-methods.md +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/guides/nonlinear-solvers.md +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/docs/index.md +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/examples/__init__.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/examples/biogeochemical_network.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/examples/chemostat_pde.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/examples/fem_diffusion.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/examples/simple_sir.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/examples/test_biogeochemical_network.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/examples/test_chemostat_pde.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/examples/test_fem_diffusion.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/LICENSE +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/benchmarks/__init__.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/benchmarks/adaptive_replay.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/benchmarks/solver_matrix.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/benchmarks/structured_blocks.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/explicit_operators.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/operators.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/py.typed +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/conftest.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/data/config.yaml +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/data/sir.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_integration_cli.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_operators.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_reactions.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/justfile +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/overrides/main.html +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/scripts/__init__.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/scripts/api-reference.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/scripts/release_validate.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/_additive_runge_kutta.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/_array.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/_multistep.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/_rosenbrock.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/_runge_kutta.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/_sdirk.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/_typing.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/core_solver.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/matrix_ops.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/model_core.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/nonlinear_solver.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/py.typed +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/src/op_engine/spatial_grid.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/conftest.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test__version__.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_additive_runge_kutta.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_array_namespace.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_configuration.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_core_solver_accuracy.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_core_solver_ark.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_core_solver_plumbing.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_core_solver_sdirk.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_matrix_ops.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_model_core.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_multistep.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_nonlinear_solver.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_rosenbrock.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_runge_kutta.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_sdirk.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_spatial_grid.py +0 -0
- {op_engine-0.3.0 → op_engine-0.4.0}/tests/op_engine/test_stochastic_solver.py +0 -0
|
@@ -5,6 +5,65 @@ versioning while the project remains pre-1.0.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-10-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Prepared Flepimop2 execution plans now support explicit PyTree and JAX block
|
|
13
|
+
layouts for fixed steps and frozen adaptive replay, including structured typed
|
|
14
|
+
operators, while preserving dynamic-value cache contracts (#168).
|
|
15
|
+
- Direct SSA accepts explicit forcing breakpoints for exact piecewise-constant
|
|
16
|
+
rates, including dormant zero-rate intervals, while retaining pending events
|
|
17
|
+
across observation times. The provider exposes this for pure direct SSA (#171).
|
|
18
|
+
- Fixed and adaptive tau-leaping stop at declared forcing changes, so each
|
|
19
|
+
leap segment uses its own starting rate; forcing boundaries take precedence
|
|
20
|
+
over tied critical or exact-fallback events (#174).
|
|
21
|
+
- Pure stochastic runs consume the producer's declared `forcing_breakpoints`
|
|
22
|
+
(op_system `time_interpolation: previous`), combined with explicit engine
|
|
23
|
+
boundaries. Hybrid mode reports producer forcing as unsupported (#175).
|
|
24
|
+
- `stochastic_method: thinning-ssa` gives exact bounded thinning for smoothly
|
|
25
|
+
time-varying rates. Supply a total-rate bound as `thinning_rate_bound` or a
|
|
26
|
+
`RateBoundFunction` through `run(..., rate_bound=...)`; `ThinningSSASolver`
|
|
27
|
+
is available in the core package (#173, #176, #177).
|
|
28
|
+
- Stochastic execution consumes axis-wide `coord_shift` reactions from
|
|
29
|
+
op_system: each source bin of an `offsets` axis becomes a channel that moves
|
|
30
|
+
one unit by the published step, and off-axis destinations only deplete the
|
|
31
|
+
donor. Direct SSA reproduces Poisson bin occupancy and Erlang exit times for
|
|
32
|
+
pure aging chains (#180).
|
|
33
|
+
- Pure stochastic validation and execution reject models with dynamics that
|
|
34
|
+
have no reaction artifact: transitions listed in the producer's
|
|
35
|
+
`reaction_gaps` coverage records, and typed operators. Each uncovered
|
|
36
|
+
transition is named by origin, selectors, and reason. Hybrid mode and
|
|
37
|
+
producers without coverage records are unchanged (#182).
|
|
38
|
+
- Stochastic execution supports reactions between templates with different
|
|
39
|
+
axes, including axis-less states: scalar S→I→R models, axis-less donors
|
|
40
|
+
depositing into a pinned cell, and templated sources collapsing into an
|
|
41
|
+
axis-less state. Targets are indexed with op_system's `to_full_axes`,
|
|
42
|
+
falling back to `full_axes` for older producers (#184).
|
|
43
|
+
- Stochastic execution runs routing and target-only fan-out reactions: each
|
|
44
|
+
source cell and routed target coordinate (op_system's `routed_axes`) is one
|
|
45
|
+
channel, so routing generators move discrete units between bins (#186).
|
|
46
|
+
- Adaptive tau-leaping validation reports incomplete reactant metadata before
|
|
47
|
+
running, and both validation and run name each incomplete reaction and the
|
|
48
|
+
remedy: `reactants` on ordinary op_system transitions, or `catalysts` on
|
|
49
|
+
`chain:` and `coord_shift` entries (#188).
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- The provider's optional `op_system` integration requires
|
|
54
|
+
`flepimop2-op-system>=0.6.0` and `op-system>=0.6.0`.
|
|
55
|
+
- Pure stochastic runs that previously dropped uncovered transitions or typed
|
|
56
|
+
operators silently now fail validation; use `mode: hybrid` or give each
|
|
57
|
+
transition a reaction artifact (#182).
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- `PreparedExecution` can be passed directly to JAX transformations, including
|
|
62
|
+
mixed NumPy/JAX samples whose effective namespaces and dtypes are canonicalized
|
|
63
|
+
by JAX during tracing (#168).
|
|
64
|
+
- Pinned source-only renewal births are accepted: donor-axis coverage is
|
|
65
|
+
validated only for reactions with a donor (#178, #179).
|
|
66
|
+
|
|
8
67
|
## [0.3.0] - 2026-09-27
|
|
9
68
|
|
|
10
69
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: op_engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Multiphysics integration engine for ODEs and PDEs using implicit-explicit operator splitting
|
|
5
5
|
Project-URL: Repository, https://github.com/ACCIDDA/op_engine
|
|
6
6
|
Project-URL: Documentation, https://accidda.github.io/op_engine/
|
|
@@ -88,6 +88,18 @@ With JAX as the array namespace:
|
|
|
88
88
|
pip install "op_engine[jax]"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
For the other packaged Array-API backends:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install "op_engine[torch]"
|
|
95
|
+
pip install "op_engine[cupy]"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
NumPy is installed with the core package. Backend support is method-specific;
|
|
99
|
+
consult the [backend guide](docs/guides/backends.md) before assuming that JIT,
|
|
100
|
+
automatic differentiation, adaptivity, or stochastic sampling have identical
|
|
101
|
+
semantics across namespaces.
|
|
102
|
+
|
|
91
103
|
With flepimop2 adapter:
|
|
92
104
|
|
|
93
105
|
```bash
|
|
@@ -139,7 +151,8 @@ adaptive controller is eager Python control flow. See the
|
|
|
139
151
|
|
|
140
152
|
```python
|
|
141
153
|
import numpy as np
|
|
142
|
-
from op_engine import CoreSolver, ModelCore
|
|
154
|
+
from op_engine import CoreSolver, ModelCore
|
|
155
|
+
from op_engine.core_solver import OperatorSpecs, RunConfig
|
|
143
156
|
|
|
144
157
|
n = 4
|
|
145
158
|
times = np.linspace(0.0, 1.0, 11)
|
|
@@ -156,13 +169,20 @@ def rhs(t, y):
|
|
|
156
169
|
return -0.1 * y
|
|
157
170
|
|
|
158
171
|
|
|
159
|
-
solver = CoreSolver(core,
|
|
160
|
-
solver.run(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
solver = CoreSolver(core, operator_axis="state")
|
|
173
|
+
solver.run(
|
|
174
|
+
rhs,
|
|
175
|
+
config=RunConfig(
|
|
176
|
+
method="imex-heun-tr",
|
|
177
|
+
operators=ops,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
164
180
|
```
|
|
165
181
|
|
|
182
|
+
This compact example uses identity left/right operators to show the API. For a
|
|
183
|
+
nontrivial implicit term, construct timestep-aware operator factories as in the
|
|
184
|
+
[solver guide](docs/guides/solver-methods.md#imex-methods-and-operator-factories).
|
|
185
|
+
|
|
166
186
|
## Public API
|
|
167
187
|
- `ModelCore`: state tensor + time grid manager; supports extra axes and optional history.
|
|
168
188
|
- `CoreSolver`: portable explicit and dense IMEX/implicit stepping selected by the state array namespace.
|
|
@@ -170,7 +190,9 @@ solver.run(rhs, config=None) # defaults: method="heun" (explicit)
|
|
|
170
190
|
- `AdaptiveTauLeapingSolver`: adaptive non-negative stochastic leaps using explicit reactant stoichiometry and injected Poisson plus exact-event sampling.
|
|
171
191
|
- `TauLeapingSolver`: portable stoichiometric updates with NumPy, JAX, or another Array-API namespace supplying Poisson samples.
|
|
172
192
|
- Operator utilities (`matrix_ops`): portable upwind advection, Laplacian, Crank–Nicolson/implicit Euler/trapezoidal operators, predictor-corrector builders, implicit solve cache, Kronecker helpers, and grouped aggregation utilities.
|
|
173
|
-
- Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`,
|
|
193
|
+
- Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`, and
|
|
194
|
+
`DtControllerConfig` from `op_engine.core_solver` for method, IMEX, and
|
|
195
|
+
adaptive control.
|
|
174
196
|
- Adapters: optional flepimop2 integration (extra dependency) via entrypoints in the adapter package. The adapter merges any `mixing_kernels` already computed by op_system (no automatic generation) and consumes config-supplied IMEX operator specs (dict or `OperatorSpecs`), forwarding the chosen `operator_axis` to `CoreSolver`.
|
|
175
197
|
|
|
176
198
|
## Development
|
|
@@ -36,6 +36,18 @@ With JAX as the array namespace:
|
|
|
36
36
|
pip install "op_engine[jax]"
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
For the other packaged Array-API backends:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install "op_engine[torch]"
|
|
43
|
+
pip install "op_engine[cupy]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
NumPy is installed with the core package. Backend support is method-specific;
|
|
47
|
+
consult the [backend guide](docs/guides/backends.md) before assuming that JIT,
|
|
48
|
+
automatic differentiation, adaptivity, or stochastic sampling have identical
|
|
49
|
+
semantics across namespaces.
|
|
50
|
+
|
|
39
51
|
With flepimop2 adapter:
|
|
40
52
|
|
|
41
53
|
```bash
|
|
@@ -87,7 +99,8 @@ adaptive controller is eager Python control flow. See the
|
|
|
87
99
|
|
|
88
100
|
```python
|
|
89
101
|
import numpy as np
|
|
90
|
-
from op_engine import CoreSolver, ModelCore
|
|
102
|
+
from op_engine import CoreSolver, ModelCore
|
|
103
|
+
from op_engine.core_solver import OperatorSpecs, RunConfig
|
|
91
104
|
|
|
92
105
|
n = 4
|
|
93
106
|
times = np.linspace(0.0, 1.0, 11)
|
|
@@ -104,13 +117,20 @@ def rhs(t, y):
|
|
|
104
117
|
return -0.1 * y
|
|
105
118
|
|
|
106
119
|
|
|
107
|
-
solver = CoreSolver(core,
|
|
108
|
-
solver.run(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
solver = CoreSolver(core, operator_axis="state")
|
|
121
|
+
solver.run(
|
|
122
|
+
rhs,
|
|
123
|
+
config=RunConfig(
|
|
124
|
+
method="imex-heun-tr",
|
|
125
|
+
operators=ops,
|
|
126
|
+
),
|
|
127
|
+
)
|
|
112
128
|
```
|
|
113
129
|
|
|
130
|
+
This compact example uses identity left/right operators to show the API. For a
|
|
131
|
+
nontrivial implicit term, construct timestep-aware operator factories as in the
|
|
132
|
+
[solver guide](docs/guides/solver-methods.md#imex-methods-and-operator-factories).
|
|
133
|
+
|
|
114
134
|
## Public API
|
|
115
135
|
- `ModelCore`: state tensor + time grid manager; supports extra axes and optional history.
|
|
116
136
|
- `CoreSolver`: portable explicit and dense IMEX/implicit stepping selected by the state array namespace.
|
|
@@ -118,7 +138,9 @@ solver.run(rhs, config=None) # defaults: method="heun" (explicit)
|
|
|
118
138
|
- `AdaptiveTauLeapingSolver`: adaptive non-negative stochastic leaps using explicit reactant stoichiometry and injected Poisson plus exact-event sampling.
|
|
119
139
|
- `TauLeapingSolver`: portable stoichiometric updates with NumPy, JAX, or another Array-API namespace supplying Poisson samples.
|
|
120
140
|
- Operator utilities (`matrix_ops`): portable upwind advection, Laplacian, Crank–Nicolson/implicit Euler/trapezoidal operators, predictor-corrector builders, implicit solve cache, Kronecker helpers, and grouped aggregation utilities.
|
|
121
|
-
- Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`,
|
|
141
|
+
- Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`, and
|
|
142
|
+
`DtControllerConfig` from `op_engine.core_solver` for method, IMEX, and
|
|
143
|
+
adaptive control.
|
|
122
144
|
- Adapters: optional flepimop2 integration (extra dependency) via entrypoints in the adapter package. The adapter merges any `mixing_kernels` already computed by op_system (no automatic generation) and consumes config-supplied IMEX operator specs (dict or `OperatorSpecs`), forwarding the chosen `operator_axis` to `CoreSolver`.
|
|
123
145
|
|
|
124
146
|
## Development
|
|
@@ -82,6 +82,9 @@ semantics.
|
|
|
82
82
|
`NumpySSASampler` and `NumpyPoissonSampler` are provided as conveniences.
|
|
83
83
|
JAX users can construct fresh keys from the stable direct-SSA `draw_index` or
|
|
84
84
|
tau-leaping `step_index`.
|
|
85
|
+
Direct SSA's optional forcing schedule is static Python configuration. It
|
|
86
|
+
preserves the state namespace and uses fresh draw indices when a forcing
|
|
87
|
+
boundary invalidates a pending event; observation times retain that event.
|
|
85
88
|
|
|
86
89
|
The current safety checks and stochastic event loops are eager, so these
|
|
87
90
|
stochastic solvers are not JIT-compatible paths. Adaptive selection also reads
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Bounded thinning SSA
|
|
2
|
+
|
|
3
|
+
`ThinningSSASolver` provides an exact waiting-time method for smoothly varying
|
|
4
|
+
propensities with user-certified bounds. This design addresses
|
|
5
|
+
[#173](https://github.com/ACCIDDA/op_engine/issues/173), including the flepimop2
|
|
6
|
+
provider's explicit `thinning-ssa` method.
|
|
7
|
+
|
|
8
|
+
## Bound contract
|
|
9
|
+
|
|
10
|
+
`ThinningSSASolver.run(propensity_func, thinning_sampler, *, rate_bound, config=None)`
|
|
11
|
+
uses the same stoichiometry, reaction axis, and batched propensity contract as
|
|
12
|
+
direct SSA. It permits arbitrary time dependence between events when a valid
|
|
13
|
+
upper bound is supplied.
|
|
14
|
+
|
|
15
|
+
`rate_bound` is either a finite non-negative real constant, or a
|
|
16
|
+
`RateBoundFunction(t, state, limit)` returning `TotalRateBound(rate, valid_until)`.
|
|
17
|
+
The bound covers the sum of **all** reaction and batch-cell propensities with
|
|
18
|
+
the supplied state held fixed, over `[t, valid_until)`. The callback chooses a
|
|
19
|
+
finite endpoint satisfying `t < valid_until <= limit`. `limit` is the earlier
|
|
20
|
+
of the next declared forcing change and the solve's final time; observations
|
|
21
|
+
do not shorten it. A constant bound applies to every state reached during the
|
|
22
|
+
solve, until the next forcing change or final time.
|
|
23
|
+
|
|
24
|
+
After an accepted event, the state changes and the callback is queried again.
|
|
25
|
+
A rejected candidate leaves the state and current bound unchanged. At expiry,
|
|
26
|
+
the solver discards a tied or later candidate, advances without firing, and
|
|
27
|
+
requests a new bound. The same rule applies at forcing changes, with
|
|
28
|
+
right-continuous propensities. No candidate fires exactly at an interval's
|
|
29
|
+
right endpoint, including the solve's final endpoint.
|
|
30
|
+
|
|
31
|
+
A zero bound declares the entire interval inactive. The solver checks rates
|
|
32
|
+
at the interval's start, advances to its endpoint without sampling, and can
|
|
33
|
+
resume with a positive bound. Zero instantaneous propensity with a positive
|
|
34
|
+
bound still produces candidates, allowing smooth rates to become active.
|
|
35
|
+
The caller must prove the bound over the whole interval. Checks at interval
|
|
36
|
+
starts and candidate times detect encountered violations but cannot certify
|
|
37
|
+
unsampled times. A violation fails visibly rather than clipping acceptance.
|
|
38
|
+
Bounds and cumulative rates use the state's floating dtype. Bounds that
|
|
39
|
+
overflow or become zero on conversion fail; tight bounds should allow for
|
|
40
|
+
floating-point rounding when summing many channels. Callback inputs must be
|
|
41
|
+
treated as read-only, and callbacks must not depend on observation storage.
|
|
42
|
+
|
|
43
|
+
## Sampling contract
|
|
44
|
+
|
|
45
|
+
`ThinningSampler(bound_rate, draw_index)` returns
|
|
46
|
+
`ThinningSample(waiting_time, uniform)`, both scalar arrays in the state array
|
|
47
|
+
namespace. The wait is finite and strictly positive, sampled exponentially at
|
|
48
|
+
`bound_rate`. The independent uniform lies in `[0, 1)`. Draw indices start at
|
|
49
|
+
zero and increase globally, including rejected and expired candidates.
|
|
50
|
+
`NumpyThinningSampler(seed)` supplies the NumPy implementation; other backends
|
|
51
|
+
inject their own sampler.
|
|
52
|
+
|
|
53
|
+
At a candidate time, the solver evaluates the actual channel rates. If
|
|
54
|
+
`uniform * bound_rate` is at least the actual total rate, it rejects the
|
|
55
|
+
candidate. Otherwise it selects the flattened channel/batch event whose
|
|
56
|
+
cumulative propensity contains that threshold. This uses one uniform for
|
|
57
|
+
acceptance and channel selection: each channel has probability
|
|
58
|
+
`channel_rate / bound_rate`, and the remaining probability rejects.
|
|
59
|
+
|
|
60
|
+
Candidates beyond an observation are retained with their sampled uniform;
|
|
61
|
+
their propensities are evaluated only when the solver reaches them. Inserting
|
|
62
|
+
observations with the same solve endpoints therefore preserves the seeded path
|
|
63
|
+
and callback/sampler history. `ThinningSSAConfig.max_candidates` bounds all
|
|
64
|
+
candidate draws during one run, including rejection and expiry. Invalid
|
|
65
|
+
sampler values, non-advancing clocks, invalid bounds, and impossible population
|
|
66
|
+
updates fail explicitly.
|
|
67
|
+
|
|
68
|
+
The construction follows
|
|
69
|
+
[Lewis and Shedler's thinning method](https://doi.org/10.1002/nav.3800260304).
|
|
70
|
+
The bound and expiry contract extends it to state-dependent reaction networks;
|
|
71
|
+
exactness remains conditional on the caller's bound and random sampling laws.
|
|
72
|
+
Existing direct SSA and tau-leaping behavior and samplers are unchanged.
|
|
73
|
+
|
|
74
|
+
## Example
|
|
75
|
+
|
|
76
|
+
For an independent birth channel in each batch cell with rate `2 * time`, the
|
|
77
|
+
integrated intensity over `[0, 1]` is one per cell. The total upper bound must
|
|
78
|
+
include every batch cell:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import numpy as np
|
|
82
|
+
|
|
83
|
+
from op_engine import ModelCore, NumpyThinningSampler, ThinningSSASolver
|
|
84
|
+
|
|
85
|
+
core = ModelCore(1, 100, np.linspace(0, 1, 11))
|
|
86
|
+
core.set_initial_state(np.zeros((1, 100)))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def smooth_birth(time, state):
|
|
90
|
+
return np.full(state.shape, 2 * time, dtype=state.dtype)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
ThinningSSASolver(core, np.asarray([[1]])).run(
|
|
94
|
+
smooth_birth, NumpyThinningSampler(seed=173), rate_bound=200,
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A callback can instead return `TotalRateBound(2 * limit * state.shape[1], limit)`
|
|
99
|
+
for this example. For consuming reactions, a bound that depends on the current
|
|
100
|
+
state can be refreshed after each accepted event. A loose bound remains valid
|
|
101
|
+
but generates more rejected candidates.
|
|
102
|
+
|
|
103
|
+
The flepimop2 provider selects this method through
|
|
104
|
+
`mode: stochastic` and `stochastic_method: thinning-ssa`. Supply a constant
|
|
105
|
+
`thinning_rate_bound` in configuration, or leave it unset and pass a scalar or
|
|
106
|
+
`RateBoundFunction` through `run(..., rate_bound=...)`. Both interfaces cannot
|
|
107
|
+
be supplied together. `thinning_max_candidates` sets the per-run candidate
|
|
108
|
+
guard. NumPy has seeded sampling through `random_seed`; other backends inject
|
|
109
|
+
`thinning_sampler=`. Hybrid thinning is unsupported. See the
|
|
110
|
+
[provider examples](https://github.com/ACCIDDA/op_engine/tree/main/flepimop2-op_engine#bounded-thinning-ssa)
|
|
111
|
+
for configuration and callback usage. Existing direct SSA and tau-leaping
|
|
112
|
+
defaults remain unchanged.
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
`ModelCore` owns output times and state storage. Deterministic systems use
|
|
4
4
|
`CoreSolver` and a method selected by `RunConfig`. Stochastic reaction networks
|
|
5
|
-
use `DirectSSASolver`
|
|
6
|
-
|
|
5
|
+
use `DirectSSASolver`, `ThinningSSASolver`, `TauLeapingSolver`, or
|
|
6
|
+
`AdaptiveTauLeapingSolver`, because
|
|
7
|
+
propensities and stoichiometry have different semantics from an ODE right-hand side. In every
|
|
7
8
|
case the state array selects the Array-API namespace; choosing JAX instead of
|
|
8
9
|
NumPy does not select a different numerical method.
|
|
9
10
|
|
|
@@ -48,7 +49,8 @@ reaction axis.
|
|
|
48
49
|
exponential waiting time and one categorical reaction/batch event, applies
|
|
49
50
|
exactly one stoichiometric update, and repeats. A draw beyond an output boundary
|
|
50
51
|
is retained rather than discarded, so adding observation times does not alter
|
|
51
|
-
the simulated path. Zero total propensity is absorbing
|
|
52
|
+
the simulated path. Zero total propensity is absorbing when no declared future
|
|
53
|
+
forcing boundary remains.
|
|
52
54
|
|
|
53
55
|
Use direct SSA as the reference method for low-copy-number networks, rare-event
|
|
54
56
|
questions, or checking a tau-leaping approximation. Its cost is proportional to
|
|
@@ -78,13 +80,111 @@ DirectSSASolver(core, stoichiometry).run(
|
|
|
78
80
|
)
|
|
79
81
|
```
|
|
80
82
|
|
|
81
|
-
The direct method assumes propensities remain constant
|
|
82
|
-
|
|
83
|
-
samples from the superposed event process; the result is equivalent to
|
|
83
|
+
The direct method assumes propensities remain constant in time while the state
|
|
84
|
+
is unchanged, except at explicitly declared forcing boundaries. Across batch
|
|
85
|
+
cells it samples from the superposed event process; the result is equivalent to
|
|
84
86
|
independent direct-method trajectories for independent batch cells.
|
|
85
87
|
|
|
88
|
+
#### Piecewise-constant forcing
|
|
89
|
+
|
|
90
|
+
Declare every forcing change through `DirectSSAConfig.forcing_breakpoints`:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from op_engine import DirectSSAConfig, array_namespace
|
|
94
|
+
|
|
95
|
+
core = ModelCore(n_states=1, n_subgroups=1, time_grid=np.linspace(0.0, 3.0, 13))
|
|
96
|
+
core.set_initial_state(np.zeros((1, 1)))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def forced_birth_rate(time, state):
|
|
100
|
+
xp = array_namespace(state)
|
|
101
|
+
rate = 0.0 if time < 1.0 else (3.0 if time < 2.0 else 1.0)
|
|
102
|
+
return xp.full(state.shape, rate, dtype=state.dtype)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
DirectSSASolver(core, np.asarray([[1]])).run(
|
|
106
|
+
forced_birth_rate,
|
|
107
|
+
NumpySSASampler(seed=2026),
|
|
108
|
+
config=DirectSSAConfig(forcing_breakpoints=(1.0, 2.0)),
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
If an event would reach or cross a forcing boundary, the solver advances to
|
|
113
|
+
that boundary without firing, reevaluates the rates, and redraws. Exponential
|
|
114
|
+
memorylessness makes this exact for piecewise-constant forcing. Zero-rate
|
|
115
|
+
intervals advance to the next forcing boundary without drawing randomness.
|
|
116
|
+
|
|
117
|
+
Forcing is right-continuous: the callback must return the new rates at a
|
|
118
|
+
breakpoint. In an exact event/breakpoint tie, the forcing boundary wins and the
|
|
119
|
+
old event is discarded. Observation times remain independent of the forcing
|
|
120
|
+
schedule, even when an observation coincides with a breakpoint.
|
|
121
|
+
|
|
122
|
+
Breakpoints must be finite real scalars in strictly increasing order. They are
|
|
123
|
+
snapshotted as an immutable tuple; a global schedule may include times outside
|
|
124
|
+
the solve interval. Boundaries at the initial time are already reflected in
|
|
125
|
+
the first evaluation, and the final boundary causes no extra random draw.
|
|
126
|
+
An omitted or empty schedule preserves ordinary time-homogeneous execution.
|
|
127
|
+
|
|
128
|
+
The schedule does not freeze or interpolate callback values. All time
|
|
129
|
+
dependence between boundaries must be constant for this exactness claim;
|
|
130
|
+
smoothly varying rates require a different waiting-time method. In particular,
|
|
131
|
+
`op_system` defaults to linear interpolation of time-indexed parameters, so
|
|
132
|
+
listing their grid points as breakpoints does not make direct SSA exact for
|
|
133
|
+
those interpolated rates. Producers containing
|
|
134
|
+
[op_system PR #241](https://github.com/ACCIDDA/op_system/pull/241) can select
|
|
135
|
+
`time_interpolation: previous` to hold table values until the next coordinate.
|
|
136
|
+
The flepimop2 engine provider consumes their published `forcing_breakpoints`
|
|
137
|
+
automatically in pure stochastic mode and combines them with any extra
|
|
138
|
+
boundaries in the engine configuration. This also supports fixed and adaptive
|
|
139
|
+
tau-leaping. Any additional direct time dependence must still satisfy the
|
|
140
|
+
chosen solver's exactness conditions.
|
|
141
|
+
|
|
142
|
+
### Exact SSA for smooth forcing
|
|
143
|
+
|
|
144
|
+
`ThinningSSASolver` accepts time-dependent propensities and a user-certified
|
|
145
|
+
total-rate bound. It samples candidates at the bound rate, then evaluates
|
|
146
|
+
channel rates at the candidate time to reject or apply one event. See the
|
|
147
|
+
[bounded thinning guide](bounded-thinning.md) for constant and callback bounds,
|
|
148
|
+
sampling injection, interval expiry, and a runnable smooth-birth example.
|
|
149
|
+
|
|
86
150
|
### Bounded adaptive tau-leaping
|
|
87
151
|
|
|
152
|
+
Both tau-leaping configurations accept the same `forcing_breakpoints` schedule
|
|
153
|
+
as direct SSA. Each leap is capped at the next observation or forcing time,
|
|
154
|
+
as well as the method's usual step-size limit. The next leap evaluates the
|
|
155
|
+
right-continuous rates at that boundary. Boundaries count toward the fixed or
|
|
156
|
+
adaptive method's `max_steps` guard for the entire output interval; they do
|
|
157
|
+
not reset it.
|
|
158
|
+
|
|
159
|
+
For example, the piecewise birth callback above can also use fixed tau-leaping:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from op_engine import NumpyPoissonSampler, TauLeapingConfig, TauLeapingSolver
|
|
163
|
+
|
|
164
|
+
core = ModelCore(n_states=1, n_subgroups=1, time_grid=np.linspace(0.0, 3.0, 13))
|
|
165
|
+
core.set_initial_state(np.zeros((1, 1)))
|
|
166
|
+
|
|
167
|
+
TauLeapingSolver(core, np.asarray([[1]])).run(
|
|
168
|
+
forced_birth_rate,
|
|
169
|
+
NumpyPoissonSampler(seed=2026),
|
|
170
|
+
config=TauLeapingConfig(max_step=0.1, forcing_breakpoints=(1.0, 2.0)),
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Adaptive tau-leaping uses these caps for both accepted leaps and rejected
|
|
175
|
+
proposals. A critical event tied with a forcing boundary is discarded, while
|
|
176
|
+
noncritical Poisson counts cover the segment ending there. During exact
|
|
177
|
+
fallback, a pending event survives observation times but is discarded at or
|
|
178
|
+
beyond the next forcing change. Zero total propensity waits for that change
|
|
179
|
+
without drawing randomness; without a future boundary it remains absorbing.
|
|
180
|
+
|
|
181
|
+
Boundary caps keep a leap from using the old rate across a known change. The
|
|
182
|
+
usual tau-leaping approximation still freezes state-dependent propensities
|
|
183
|
+
within each leap. Smooth time variation between boundaries also remains an
|
|
184
|
+
approximation for leaps and is unsupported by adaptive exact fallback. For
|
|
185
|
+
that fallback, use rates constant in time between declared forcing changes
|
|
186
|
+
while the state is unchanged.
|
|
187
|
+
|
|
88
188
|
`AdaptiveTauLeapingSolver` implements the species-based pre-leap selector of
|
|
89
189
|
[Cao, Gillespie, and Petzold](https://doi.org/10.1063/1.2159468) together with
|
|
90
190
|
their [non-negative modified Poisson
|