op-engine 0.3.0__tar.gz → 0.5.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.
Files changed (120) hide show
  1. {op_engine-0.3.0 → op_engine-0.5.0}/CHANGELOG.md +103 -0
  2. {op_engine-0.3.0 → op_engine-0.5.0}/PKG-INFO +36 -9
  3. {op_engine-0.3.0 → op_engine-0.5.0}/README.md +35 -8
  4. {op_engine-0.3.0 → op_engine-0.5.0}/docs/guides/backends.md +3 -0
  5. op_engine-0.5.0/docs/guides/bounded-thinning.md +112 -0
  6. op_engine-0.5.0/docs/guides/op-system-reactions.md +146 -0
  7. {op_engine-0.3.0 → op_engine-0.5.0}/docs/guides/solver-methods.md +106 -6
  8. op_engine-0.5.0/docs/guides/steady-states.md +183 -0
  9. op_engine-0.5.0/flepimop2-op_engine/README.md +745 -0
  10. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/benchmarks/README.md +4 -2
  11. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/benchmarks/prepared_execution.py +78 -19
  12. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/pyproject.toml +17 -9
  13. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/__init__.py +369 -31
  14. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/config.py +48 -1
  15. op_engine-0.5.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/reactions.py +119 -0
  16. op_engine-0.5.0/flepimop2-op_engine/tests/test_adaptive_tau_dependencies.py +145 -0
  17. op_engine-0.5.0/flepimop2-op_engine/tests/test_adaptive_tau_generated.py +173 -0
  18. op_engine-0.5.0/flepimop2-op_engine/tests/test_axis_less_reactions.py +210 -0
  19. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/test_config.py +123 -0
  20. op_engine-0.5.0/flepimop2-op_engine/tests/test_coord_shift.py +335 -0
  21. op_engine-0.5.0/flepimop2-op_engine/tests/test_core_reaction_network.py +107 -0
  22. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/test_engine.py +10 -2
  23. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/test_op_system_integration.py +497 -24
  24. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/test_reactions.py +33 -3
  25. op_engine-0.5.0/flepimop2-op_engine/tests/test_renewal.py +463 -0
  26. op_engine-0.5.0/flepimop2-op_engine/tests/test_routed_reactions.py +257 -0
  27. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/test_solver_matrix_benchmark.py +36 -1
  28. op_engine-0.5.0/flepimop2-op_engine/tests/test_steady_state_op_system.py +94 -0
  29. op_engine-0.5.0/flepimop2-op_engine/tests/test_stochastic_coverage.py +198 -0
  30. op_engine-0.5.0/flepimop2-op_engine/tests/test_thinning.py +515 -0
  31. {op_engine-0.3.0 → op_engine-0.5.0}/mkdocs.yml +6 -0
  32. {op_engine-0.3.0 → op_engine-0.5.0}/pyproject.toml +2 -1
  33. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/__init__.py +42 -0
  34. op_engine-0.5.0/src/op_engine/_forcing.py +53 -0
  35. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/adaptive_tau.py +160 -9
  36. op_engine-0.5.0/src/op_engine/reactions.py +965 -0
  37. op_engine-0.5.0/src/op_engine/steady_state.py +658 -0
  38. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/stochastic_solver.py +84 -26
  39. op_engine-0.5.0/src/op_engine/thinning_ssa.py +443 -0
  40. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_adaptive_tau.py +361 -0
  41. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_direct_ssa.py +262 -3
  42. op_engine-0.5.0/tests/op_engine/test_reactions.py +312 -0
  43. op_engine-0.5.0/tests/op_engine/test_steady_state.py +258 -0
  44. op_engine-0.5.0/tests/op_engine/test_tau_forcing.py +247 -0
  45. op_engine-0.5.0/tests/op_engine/test_thinning_ssa.py +509 -0
  46. op_engine-0.3.0/flepimop2-op_engine/README.md +0 -381
  47. op_engine-0.3.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/reactions.py +0 -622
  48. {op_engine-0.3.0 → op_engine-0.5.0}/.github/workflows/ci.yaml +0 -0
  49. {op_engine-0.3.0 → op_engine-0.5.0}/.github/workflows/gh-pages.yaml +0 -0
  50. {op_engine-0.3.0 → op_engine-0.5.0}/.github/workflows/release.yaml +0 -0
  51. {op_engine-0.3.0 → op_engine-0.5.0}/.gitignore +0 -0
  52. {op_engine-0.3.0 → op_engine-0.5.0}/LICENSE +0 -0
  53. {op_engine-0.3.0 → op_engine-0.5.0}/docs/api-reference/.gitignore +0 -0
  54. {op_engine-0.3.0 → op_engine-0.5.0}/docs/assets/chemostat-split.svg +0 -0
  55. {op_engine-0.3.0 → op_engine-0.5.0}/docs/assets/chemostat-summary.png +0 -0
  56. {op_engine-0.3.0 → op_engine-0.5.0}/docs/development/creating-a-release.md +0 -0
  57. {op_engine-0.3.0 → op_engine-0.5.0}/docs/development/fem-assembly-boundary.md +0 -0
  58. {op_engine-0.3.0 → op_engine-0.5.0}/docs/guides/biogeochemical-network.md +0 -0
  59. {op_engine-0.3.0 → op_engine-0.5.0}/docs/guides/chemostat-pde.md +0 -0
  60. {op_engine-0.3.0 → op_engine-0.5.0}/docs/guides/getting-started.md +0 -0
  61. {op_engine-0.3.0 → op_engine-0.5.0}/docs/guides/linear-multistep-methods.md +0 -0
  62. {op_engine-0.3.0 → op_engine-0.5.0}/docs/guides/nonlinear-solvers.md +0 -0
  63. {op_engine-0.3.0 → op_engine-0.5.0}/docs/index.md +0 -0
  64. {op_engine-0.3.0 → op_engine-0.5.0}/examples/__init__.py +0 -0
  65. {op_engine-0.3.0 → op_engine-0.5.0}/examples/biogeochemical_network.py +0 -0
  66. {op_engine-0.3.0 → op_engine-0.5.0}/examples/chemostat_pde.py +0 -0
  67. {op_engine-0.3.0 → op_engine-0.5.0}/examples/fem_diffusion.py +0 -0
  68. {op_engine-0.3.0 → op_engine-0.5.0}/examples/simple_sir.py +0 -0
  69. {op_engine-0.3.0 → op_engine-0.5.0}/examples/test_biogeochemical_network.py +0 -0
  70. {op_engine-0.3.0 → op_engine-0.5.0}/examples/test_chemostat_pde.py +0 -0
  71. {op_engine-0.3.0 → op_engine-0.5.0}/examples/test_fem_diffusion.py +0 -0
  72. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/LICENSE +0 -0
  73. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/benchmarks/__init__.py +0 -0
  74. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/benchmarks/adaptive_replay.py +0 -0
  75. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/benchmarks/solver_matrix.py +0 -0
  76. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/benchmarks/structured_blocks.py +0 -0
  77. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/explicit_operators.py +0 -0
  78. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/operators.py +0 -0
  79. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/py.typed +0 -0
  80. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/conftest.py +0 -0
  81. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/data/config.yaml +0 -0
  82. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/data/sir.py +0 -0
  83. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/test_integration_cli.py +0 -0
  84. {op_engine-0.3.0 → op_engine-0.5.0}/flepimop2-op_engine/tests/test_operators.py +0 -0
  85. {op_engine-0.3.0 → op_engine-0.5.0}/justfile +0 -0
  86. {op_engine-0.3.0 → op_engine-0.5.0}/overrides/main.html +0 -0
  87. {op_engine-0.3.0 → op_engine-0.5.0}/scripts/__init__.py +0 -0
  88. {op_engine-0.3.0 → op_engine-0.5.0}/scripts/api-reference.py +0 -0
  89. {op_engine-0.3.0 → op_engine-0.5.0}/scripts/release_validate.py +0 -0
  90. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/_additive_runge_kutta.py +0 -0
  91. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/_array.py +0 -0
  92. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/_multistep.py +0 -0
  93. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/_rosenbrock.py +0 -0
  94. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/_runge_kutta.py +0 -0
  95. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/_sdirk.py +0 -0
  96. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/_typing.py +0 -0
  97. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/core_solver.py +0 -0
  98. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/matrix_ops.py +0 -0
  99. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/model_core.py +0 -0
  100. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/nonlinear_solver.py +0 -0
  101. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/py.typed +0 -0
  102. {op_engine-0.3.0 → op_engine-0.5.0}/src/op_engine/spatial_grid.py +0 -0
  103. {op_engine-0.3.0 → op_engine-0.5.0}/tests/conftest.py +0 -0
  104. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test__version__.py +0 -0
  105. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_additive_runge_kutta.py +0 -0
  106. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_array_namespace.py +0 -0
  107. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_configuration.py +0 -0
  108. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_core_solver_accuracy.py +0 -0
  109. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_core_solver_ark.py +0 -0
  110. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_core_solver_plumbing.py +0 -0
  111. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_core_solver_sdirk.py +0 -0
  112. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_matrix_ops.py +0 -0
  113. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_model_core.py +0 -0
  114. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_multistep.py +0 -0
  115. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_nonlinear_solver.py +0 -0
  116. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_rosenbrock.py +0 -0
  117. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_runge_kutta.py +0 -0
  118. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_sdirk.py +0 -0
  119. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_spatial_grid.py +0 -0
  120. {op_engine-0.3.0 → op_engine-0.5.0}/tests/op_engine/test_stochastic_solver.py +0 -0
@@ -5,6 +5,109 @@ versioning while the project remains pre-1.0.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.5.0] - 2026-10-06
9
+
10
+ ### Added
11
+
12
+ - `op_engine.from_compiled_rhs(compiled, params)` and
13
+ `op_engine.compile_reaction_network(reactions, template_shapes=...,
14
+ axis_sizes=..., params=...)` build the flat stochastic reaction network from
15
+ op_system reaction artifacts in the core package, with no flepimop2
16
+ dependency. `from_compiled_rhs` refuses a RHS with reaction gaps unless
17
+ `allow_gaps=True` or a `reaction_names` partition is given. The new guide
18
+ covers the `mean_drift` consistency check against the deterministic RHS
19
+ (#191).
20
+ - `op_engine.steady_state(rhs, y0, ...)` finds equilibria by pseudo-transient
21
+ continuation, without a long burn-in. `fixed` holds absorbing states
22
+ (cumulative counters) at their starting values; `invariants` enforces
23
+ conserved totals exactly through a bordered step, so `dt` can grow without
24
+ the total drifting; and convergence requires a small Newton correction as
25
+ well as a small residual, which catches slowly decaying modes.
26
+ `conserved_quantities`, `linear_invariants` (including rate-balanced totals
27
+ such as births `mu * N`), and `sink_states` find what to pass.
28
+ Diagnostics stay arrays, and `loop=jax.lax.fori_loop` runs the solve under
29
+ `jax.jit` and `jax.vmap`. See the new steady-states guide (#192).
30
+ - `AdaptiveTauLeapingSolver` accepts `dependency_incidence` and
31
+ `propensity_orders` for reactions that are not mass action, such as
32
+ frequency-dependent infection. Each species a reaction reads gets a
33
+ Cao-Gillespie-Petzold scaling of at least the reaction's elasticity order,
34
+ so its propensity's relative change stays within the leap tolerance; orders
35
+ above three are allowed. `CompiledReactionNetwork` builds both arrays from
36
+ the `dependencies`, `propensity_order`, and `dependencies_complete` fields
37
+ op_system 0.7.0 publishes for `reactants: auto`, counts such reactions as
38
+ complete, and the provider passes them to the solver, so adaptive
39
+ tau-leaping runs frequency-dependent op_system models (#193).
40
+
41
+ ### Changed
42
+
43
+ - The flepimop2 provider's `compile_reaction_network(system, params, ...)` is
44
+ now a thin adapter over the core function, with unchanged behavior and
45
+ error messages. `CompiledReactionNetwork` moved to `op_engine.reactions`
46
+ (still re-exported by the provider) (#191).
47
+ - The provider requires op_system 0.7.0. With it, a reaction whose rate reads
48
+ no state is complete without declarations, so constant-rate flows no longer
49
+ block adaptive tau-leaping, and the incomplete-reactants message suggests
50
+ `reactants: auto` / `catalysts: auto` (#191).
51
+
52
+ ## [0.4.0] - 2026-10-02
53
+
54
+ ### Added
55
+
56
+ - Prepared Flepimop2 execution plans now support explicit PyTree and JAX block
57
+ layouts for fixed steps and frozen adaptive replay, including structured typed
58
+ operators, while preserving dynamic-value cache contracts (#168).
59
+ - Direct SSA accepts explicit forcing breakpoints for exact piecewise-constant
60
+ rates, including dormant zero-rate intervals, while retaining pending events
61
+ across observation times. The provider exposes this for pure direct SSA (#171).
62
+ - Fixed and adaptive tau-leaping stop at declared forcing changes, so each
63
+ leap segment uses its own starting rate; forcing boundaries take precedence
64
+ over tied critical or exact-fallback events (#174).
65
+ - Pure stochastic runs consume the producer's declared `forcing_breakpoints`
66
+ (op_system `time_interpolation: previous`), combined with explicit engine
67
+ boundaries. Hybrid mode reports producer forcing as unsupported (#175).
68
+ - `stochastic_method: thinning-ssa` gives exact bounded thinning for smoothly
69
+ time-varying rates. Supply a total-rate bound as `thinning_rate_bound` or a
70
+ `RateBoundFunction` through `run(..., rate_bound=...)`; `ThinningSSASolver`
71
+ is available in the core package (#173, #176, #177).
72
+ - Stochastic execution consumes axis-wide `coord_shift` reactions from
73
+ op_system: each source bin of an `offsets` axis becomes a channel that moves
74
+ one unit by the published step, and off-axis destinations only deplete the
75
+ donor. Direct SSA reproduces Poisson bin occupancy and Erlang exit times for
76
+ pure aging chains (#180).
77
+ - Pure stochastic validation and execution reject models with dynamics that
78
+ have no reaction artifact: transitions listed in the producer's
79
+ `reaction_gaps` coverage records, and typed operators. Each uncovered
80
+ transition is named by origin, selectors, and reason. Hybrid mode and
81
+ producers without coverage records are unchanged (#182).
82
+ - Stochastic execution supports reactions between templates with different
83
+ axes, including axis-less states: scalar S→I→R models, axis-less donors
84
+ depositing into a pinned cell, and templated sources collapsing into an
85
+ axis-less state. Targets are indexed with op_system's `to_full_axes`,
86
+ falling back to `full_axes` for older producers (#184).
87
+ - Stochastic execution runs routing and target-only fan-out reactions: each
88
+ source cell and routed target coordinate (op_system's `routed_axes`) is one
89
+ channel, so routing generators move discrete units between bins (#186).
90
+ - Adaptive tau-leaping validation reports incomplete reactant metadata before
91
+ running, and both validation and run name each incomplete reaction and the
92
+ remedy: `reactants` on ordinary op_system transitions, or `catalysts` on
93
+ `chain:` and `coord_shift` entries (#188).
94
+
95
+ ### Changed
96
+
97
+ - The provider's optional `op_system` integration requires
98
+ `flepimop2-op-system>=0.6.0` and `op-system>=0.6.0`.
99
+ - Pure stochastic runs that previously dropped uncovered transitions or typed
100
+ operators silently now fail validation; use `mode: hybrid` or give each
101
+ transition a reaction artifact (#182).
102
+
103
+ ### Fixed
104
+
105
+ - `PreparedExecution` can be passed directly to JAX transformations, including
106
+ mixed NumPy/JAX samples whose effective namespaces and dtypes are canonicalized
107
+ by JAX during tracing (#168).
108
+ - Pinned source-only renewal births are accepted: donor-axis coverage is
109
+ validated only for reactions with a donor (#178, #179).
110
+
8
111
  ## [0.3.0] - 2026-09-27
9
112
 
10
113
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: op_engine
3
- Version: 0.3.0
3
+ Version: 0.5.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/
@@ -67,13 +67,17 @@ Operator-Partitioned Engine (OP Engine) is a lightweight multiphysics solver cor
67
67
  - `ModelCore`: state/time manager; configure axes, dtype, and optional history.
68
68
  - `CoreSolver`: portable explicit methods through Dormand--Prince 5(4), plus dense IMEX and linearly implicit methods; accepts `RunConfig` with `AdaptiveConfig`, `DtControllerConfig`, and `OperatorSpecs`.
69
69
  - `DirectSSASolver`: exact Gillespie direct-method trajectories with injected, backend-specific exponential and categorical sampling.
70
- - `AdaptiveTauLeapingSolver`: bounded adaptive tau-leaping with leap-condition control, exact critical events, and explicit post-leap rejection.
70
+ - `AdaptiveTauLeapingSolver`: bounded adaptive tau-leaping with leap-condition control, exact critical events, and explicit post-leap rejection. Non-mass-action propensities (frequency-dependent infection) are supported through per-reaction dependency incidence and elasticity orders.
71
71
  - `TauLeapingSolver`: fixed-step stochastic reaction-network integration with injected, backend-specific Poisson sampling.
72
+ - `from_compiled_rhs` / `compile_reaction_network`: build the flat stochastic network (stoichiometry, reactant orders, propensity) from an op_system spec's reaction artifacts, with no flepimop2 dependency.
73
+ - `steady_state`: equilibria by pseudo-transient continuation, with absorbing states held fixed, conserved totals enforced exactly, and a Newton-correction convergence test; runs under `jax.jit`/`vmap`.
72
74
  - `matrix_ops`: portable dense advection/diffusion, sparse Laplacian/Crank–Nicolson, implicit Euler/trapezoidal builders, predictor–corrector, implicit solve cache, Kronecker helpers, and grouped aggregations.
73
75
  - Extras: `OperatorSpecs`, `RunConfig`, `AdaptiveConfig`, `DtControllerConfig`, `Operator`, `GridGeometry`, `DiffusionConfig`.
74
76
 
75
77
  See the documentation guides for [solver selection](docs/guides/solver-methods.md),
76
78
  [the biogeochemical splitting tutorial](docs/guides/biogeochemical-network.md),
79
+ [stochastic simulation from an op_system spec](docs/guides/op-system-reactions.md),
80
+ [steady states](docs/guides/steady-states.md),
77
81
  and [backend boundaries](docs/guides/backends.md).
78
82
 
79
83
  ## Installation
@@ -88,6 +92,18 @@ With JAX as the array namespace:
88
92
  pip install "op_engine[jax]"
89
93
  ```
90
94
 
95
+ For the other packaged Array-API backends:
96
+
97
+ ```bash
98
+ pip install "op_engine[torch]"
99
+ pip install "op_engine[cupy]"
100
+ ```
101
+
102
+ NumPy is installed with the core package. Backend support is method-specific;
103
+ consult the [backend guide](docs/guides/backends.md) before assuming that JIT,
104
+ automatic differentiation, adaptivity, or stochastic sampling have identical
105
+ semantics across namespaces.
106
+
91
107
  With flepimop2 adapter:
92
108
 
93
109
  ```bash
@@ -139,7 +155,8 @@ adaptive controller is eager Python control flow. See the
139
155
 
140
156
  ```python
141
157
  import numpy as np
142
- from op_engine import CoreSolver, ModelCore, OperatorSpecs
158
+ from op_engine import CoreSolver, ModelCore
159
+ from op_engine.core_solver import OperatorSpecs, RunConfig
143
160
 
144
161
  n = 4
145
162
  times = np.linspace(0.0, 1.0, 11)
@@ -156,21 +173,31 @@ def rhs(t, y):
156
173
  return -0.1 * y
157
174
 
158
175
 
159
- solver = CoreSolver(core, operators=ops.default, operator_axis="state")
160
- solver.run(rhs, config=None) # defaults: method="heun" (explicit)
161
-
162
- # For IMEX methods set method and operators via RunConfig:
163
- # from op_engine.core_solver import RunConfig, AdaptiveConfig, DtControllerConfig
176
+ solver = CoreSolver(core, operator_axis="state")
177
+ solver.run(
178
+ rhs,
179
+ config=RunConfig(
180
+ method="imex-heun-tr",
181
+ operators=ops,
182
+ ),
183
+ )
164
184
  ```
165
185
 
186
+ This compact example uses identity left/right operators to show the API. For a
187
+ nontrivial implicit term, construct timestep-aware operator factories as in the
188
+ [solver guide](docs/guides/solver-methods.md#imex-methods-and-operator-factories).
189
+
166
190
  ## Public API
167
191
  - `ModelCore`: state tensor + time grid manager; supports extra axes and optional history.
168
192
  - `CoreSolver`: portable explicit and dense IMEX/implicit stepping selected by the state array namespace.
169
193
  - `DirectSSASolver`: exact event-by-event stochastic reaction trajectories with NumPy, JAX, or another Array-API namespace supplying random draws.
170
194
  - `AdaptiveTauLeapingSolver`: adaptive non-negative stochastic leaps using explicit reactant stoichiometry and injected Poisson plus exact-event sampling.
171
195
  - `TauLeapingSolver`: portable stoichiometric updates with NumPy, JAX, or another Array-API namespace supplying Poisson samples.
196
+ - `op_engine.reactions`: `from_compiled_rhs(compiled, params)` and `compile_reaction_network(reactions, template_shapes=..., axis_sizes=..., params=...)` return a `CompiledReactionNetwork` whose `stoichiometry`, `reactant_stoichiometry`, and `propensity` feed the stochastic solvers; `mean_drift` checks the network against the deterministic RHS.
172
197
  - 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`, `DtControllerConfig` for method/IMEX/adaptive control.
198
+ - Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`, and
199
+ `DtControllerConfig` from `op_engine.core_solver` for method, IMEX, and
200
+ adaptive control.
174
201
  - 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
202
 
176
203
  ## Development
@@ -15,13 +15,17 @@ Operator-Partitioned Engine (OP Engine) is a lightweight multiphysics solver cor
15
15
  - `ModelCore`: state/time manager; configure axes, dtype, and optional history.
16
16
  - `CoreSolver`: portable explicit methods through Dormand--Prince 5(4), plus dense IMEX and linearly implicit methods; accepts `RunConfig` with `AdaptiveConfig`, `DtControllerConfig`, and `OperatorSpecs`.
17
17
  - `DirectSSASolver`: exact Gillespie direct-method trajectories with injected, backend-specific exponential and categorical sampling.
18
- - `AdaptiveTauLeapingSolver`: bounded adaptive tau-leaping with leap-condition control, exact critical events, and explicit post-leap rejection.
18
+ - `AdaptiveTauLeapingSolver`: bounded adaptive tau-leaping with leap-condition control, exact critical events, and explicit post-leap rejection. Non-mass-action propensities (frequency-dependent infection) are supported through per-reaction dependency incidence and elasticity orders.
19
19
  - `TauLeapingSolver`: fixed-step stochastic reaction-network integration with injected, backend-specific Poisson sampling.
20
+ - `from_compiled_rhs` / `compile_reaction_network`: build the flat stochastic network (stoichiometry, reactant orders, propensity) from an op_system spec's reaction artifacts, with no flepimop2 dependency.
21
+ - `steady_state`: equilibria by pseudo-transient continuation, with absorbing states held fixed, conserved totals enforced exactly, and a Newton-correction convergence test; runs under `jax.jit`/`vmap`.
20
22
  - `matrix_ops`: portable dense advection/diffusion, sparse Laplacian/Crank–Nicolson, implicit Euler/trapezoidal builders, predictor–corrector, implicit solve cache, Kronecker helpers, and grouped aggregations.
21
23
  - Extras: `OperatorSpecs`, `RunConfig`, `AdaptiveConfig`, `DtControllerConfig`, `Operator`, `GridGeometry`, `DiffusionConfig`.
22
24
 
23
25
  See the documentation guides for [solver selection](docs/guides/solver-methods.md),
24
26
  [the biogeochemical splitting tutorial](docs/guides/biogeochemical-network.md),
27
+ [stochastic simulation from an op_system spec](docs/guides/op-system-reactions.md),
28
+ [steady states](docs/guides/steady-states.md),
25
29
  and [backend boundaries](docs/guides/backends.md).
26
30
 
27
31
  ## Installation
@@ -36,6 +40,18 @@ With JAX as the array namespace:
36
40
  pip install "op_engine[jax]"
37
41
  ```
38
42
 
43
+ For the other packaged Array-API backends:
44
+
45
+ ```bash
46
+ pip install "op_engine[torch]"
47
+ pip install "op_engine[cupy]"
48
+ ```
49
+
50
+ NumPy is installed with the core package. Backend support is method-specific;
51
+ consult the [backend guide](docs/guides/backends.md) before assuming that JIT,
52
+ automatic differentiation, adaptivity, or stochastic sampling have identical
53
+ semantics across namespaces.
54
+
39
55
  With flepimop2 adapter:
40
56
 
41
57
  ```bash
@@ -87,7 +103,8 @@ adaptive controller is eager Python control flow. See the
87
103
 
88
104
  ```python
89
105
  import numpy as np
90
- from op_engine import CoreSolver, ModelCore, OperatorSpecs
106
+ from op_engine import CoreSolver, ModelCore
107
+ from op_engine.core_solver import OperatorSpecs, RunConfig
91
108
 
92
109
  n = 4
93
110
  times = np.linspace(0.0, 1.0, 11)
@@ -104,21 +121,31 @@ def rhs(t, y):
104
121
  return -0.1 * y
105
122
 
106
123
 
107
- solver = CoreSolver(core, operators=ops.default, operator_axis="state")
108
- solver.run(rhs, config=None) # defaults: method="heun" (explicit)
109
-
110
- # For IMEX methods set method and operators via RunConfig:
111
- # from op_engine.core_solver import RunConfig, AdaptiveConfig, DtControllerConfig
124
+ solver = CoreSolver(core, operator_axis="state")
125
+ solver.run(
126
+ rhs,
127
+ config=RunConfig(
128
+ method="imex-heun-tr",
129
+ operators=ops,
130
+ ),
131
+ )
112
132
  ```
113
133
 
134
+ This compact example uses identity left/right operators to show the API. For a
135
+ nontrivial implicit term, construct timestep-aware operator factories as in the
136
+ [solver guide](docs/guides/solver-methods.md#imex-methods-and-operator-factories).
137
+
114
138
  ## Public API
115
139
  - `ModelCore`: state tensor + time grid manager; supports extra axes and optional history.
116
140
  - `CoreSolver`: portable explicit and dense IMEX/implicit stepping selected by the state array namespace.
117
141
  - `DirectSSASolver`: exact event-by-event stochastic reaction trajectories with NumPy, JAX, or another Array-API namespace supplying random draws.
118
142
  - `AdaptiveTauLeapingSolver`: adaptive non-negative stochastic leaps using explicit reactant stoichiometry and injected Poisson plus exact-event sampling.
119
143
  - `TauLeapingSolver`: portable stoichiometric updates with NumPy, JAX, or another Array-API namespace supplying Poisson samples.
144
+ - `op_engine.reactions`: `from_compiled_rhs(compiled, params)` and `compile_reaction_network(reactions, template_shapes=..., axis_sizes=..., params=...)` return a `CompiledReactionNetwork` whose `stoichiometry`, `reactant_stoichiometry`, and `propensity` feed the stochastic solvers; `mean_drift` checks the network against the deterministic RHS.
120
145
  - 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`, `DtControllerConfig` for method/IMEX/adaptive control.
146
+ - Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`, and
147
+ `DtControllerConfig` from `op_engine.core_solver` for method, IMEX, and
148
+ adaptive control.
122
149
  - 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
150
 
124
151
  ## 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.
@@ -0,0 +1,146 @@
1
+ # Stochastic Simulation From An op_system Spec
2
+
3
+ An [op_system](https://github.com/ACCIDDA/op_system) spec compiles to typed
4
+ reaction artifacts, one per named transition (`CompiledRhs.reactions`).
5
+ `op_engine.from_compiled_rhs` turns them into the flat network that
6
+ `DirectSSASolver`, `TauLeapingSolver`, and `AdaptiveTauLeapingSolver`
7
+ consume. No flepimop2 installation is needed.
8
+
9
+ ```shell
10
+ pip install op_engine op-system
11
+ ```
12
+
13
+ ## Build the network
14
+
15
+ ```python
16
+ import numpy as np
17
+ from op_system import compile_spec
18
+
19
+ from op_engine import from_compiled_rhs
20
+
21
+ spec = {
22
+ "kind": "transitions",
23
+ "axes": [{"name": "age", "coords": ["child", "adult"]}],
24
+ "state": ["S[age]", "I[age]", "R[age]"],
25
+ "transitions": [
26
+ {
27
+ "name": "infect",
28
+ "from": "S[age]",
29
+ "to": "I[age]",
30
+ "rate": "b * I[age]",
31
+ "reactants": "auto",
32
+ },
33
+ {"name": "recover", "from": "I[age]", "to": "R[age]", "rate": "g"},
34
+ ],
35
+ }
36
+ params = {"b": 0.002, "g": 0.1}
37
+
38
+ compiled = compile_spec(spec)
39
+ network = from_compiled_rhs(compiled, params)
40
+ ```
41
+
42
+ Each reaction becomes one channel per source cell: `network.stoichiometry` is
43
+ `(n_state, n_channels)`, and `network.channel_names` labels the columns
44
+ (`infect[age=0]`, ...). The flat state follows `compiled.template_shapes`:
45
+ each template's cells in C order, templates in declaration order.
46
+
47
+ ## Check the network against the RHS
48
+
49
+ The network's mean drift, `stoichiometry @ propensity`, must equal the
50
+ deterministic right-hand side. Check it once for each new model:
51
+
52
+ ```python
53
+ rng = np.random.default_rng(0)
54
+ y = rng.uniform(1.0, 100.0, network.n_state)
55
+ np.testing.assert_allclose(
56
+ network.mean_drift(0.0, y),
57
+ compiled.eval_fn(0.0, y, **params),
58
+ rtol=1e-12,
59
+ )
60
+ ```
61
+
62
+ A mismatch means some dynamics have no reaction. `from_compiled_rhs` already
63
+ refuses a RHS whose `reaction_gaps` lists transitions without a reaction
64
+ artifact (an unnamed transition, for example). Pass `allow_gaps=True` only
65
+ when you intend to simulate the reactions alone.
66
+
67
+ ## Simulate
68
+
69
+ ```python
70
+ from op_engine import DirectSSASolver, ModelCore, NumpySSASampler
71
+
72
+ times = np.linspace(0.0, 30.0, 7)
73
+ core = ModelCore(network.n_state, 1, times)
74
+ core.set_initial_state(np.asarray([[95.0], [90.0], [5.0], [10.0], [0.0], [0.0]]))
75
+ DirectSSASolver(core, network.stoichiometry).run(
76
+ network.propensity, NumpySSASampler(seed=1)
77
+ )
78
+ trajectory = core.state_array[:, :, 0] # (time, state)
79
+ ```
80
+
81
+ `TauLeapingSolver(core, network.stoichiometry)` runs the same network with a
82
+ `NumpyPoissonSampler`.
83
+
84
+ ## Adaptive tau-leaping
85
+
86
+ `AdaptiveTauLeapingSolver` chooses each leap so that no propensity changes
87
+ by more than `leap_tolerance` relative to itself. It therefore needs to know
88
+ which states each propensity depends on, and how strongly. The network must
89
+ describe every reaction: `network.reactants_complete` is false, and
90
+ `network.incomplete_reactions` names the culprits, when a reaction might read
91
+ a state op_system has not declared. In op_system 0.7.0 and later:
92
+
93
+ - a reaction whose rate reads no state (`recover` above) is complete as is;
94
+ - `reactants: auto` infers the molecular reactants of a rate that is a single
95
+ product of states (`infect` above: `S` consumed, `I` catalytic);
96
+ - for any other `reactants: auto` rate, such as a frequency-dependent force
97
+ of infection `beta * S * sum(I) / N`, op_system publishes the states the
98
+ propensity reads and a bound `E` on its elasticity
99
+ `sum_i |d log a / d log x_i|` (3 for that example). The network turns them
100
+ into `dependency_incidence` and `propensity_orders`;
101
+ - an explicit `reactants:` list is always authoritative.
102
+
103
+ Pass all four arrays; the dependency arrays are `None` when every reaction is
104
+ mass action:
105
+
106
+ ```python
107
+ from op_engine import AdaptiveTauLeapingSolver, NumpyPoissonSampler
108
+
109
+ assert network.reactants_complete
110
+ core = ModelCore(network.n_state, 1, times)
111
+ core.set_initial_state(np.asarray([[95.0], [90.0], [5.0], [10.0], [0.0], [0.0]]))
112
+ AdaptiveTauLeapingSolver(
113
+ core,
114
+ network.stoichiometry,
115
+ network.reactant_stoichiometry,
116
+ dependency_incidence=network.dependency_incidence,
117
+ propensity_orders=network.propensity_orders,
118
+ ).run(network.propensity, NumpyPoissonSampler(seed=2), NumpySSASampler(seed=3))
119
+ ```
120
+
121
+ For a reaction described by dependencies, every species it reads gets a
122
+ Cao-Gillespie-Petzold scaling of at least `E`, so its propensity's relative
123
+ change stays within the tolerance. Orders above three are allowed. Its
124
+ `reactant_stoichiometry` column lists only the consumed source, which is what
125
+ can be exhausted for the critical-reaction check.
126
+
127
+ ## Partitions and lower-level use
128
+
129
+ `from_compiled_rhs(compiled, params, reaction_names=[...])` compiles only the
130
+ named reactions, for a hybrid jump partition. `compile_reaction_network`
131
+ takes the artifacts and layout directly, for producers other than a
132
+ `CompiledRhs`:
133
+
134
+ ```python
135
+ from op_engine import compile_reaction_network
136
+
137
+ network = compile_reaction_network(
138
+ compiled.reactions,
139
+ template_shapes=compiled.template_shapes,
140
+ axis_sizes={"age": 2},
141
+ params=params,
142
+ )
143
+ ```
144
+
145
+ The flepimop2 provider (`flepimop2-op-engine`) builds its stochastic runs
146
+ through the same function.