op-engine 0.2.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.
Files changed (118) hide show
  1. {op_engine-0.2.0 → op_engine-0.4.0}/.gitignore +2 -1
  2. op_engine-0.4.0/CHANGELOG.md +199 -0
  3. {op_engine-0.2.0 → op_engine-0.4.0}/PKG-INFO +36 -10
  4. {op_engine-0.2.0 → op_engine-0.4.0}/README.md +32 -9
  5. op_engine-0.4.0/docs/assets/chemostat-split.svg +41 -0
  6. op_engine-0.4.0/docs/assets/chemostat-summary.png +0 -0
  7. op_engine-0.4.0/docs/development/fem-assembly-boundary.md +102 -0
  8. {op_engine-0.2.0 → op_engine-0.4.0}/docs/guides/backends.md +21 -9
  9. op_engine-0.4.0/docs/guides/bounded-thinning.md +112 -0
  10. op_engine-0.4.0/docs/guides/chemostat-pde.md +97 -0
  11. {op_engine-0.2.0 → op_engine-0.4.0}/docs/guides/getting-started.md +12 -6
  12. {op_engine-0.2.0 → op_engine-0.4.0}/docs/guides/solver-methods.md +171 -13
  13. {op_engine-0.2.0 → op_engine-0.4.0}/docs/index.md +3 -0
  14. op_engine-0.4.0/examples/chemostat_pde.py +614 -0
  15. op_engine-0.4.0/examples/fem_diffusion.py +189 -0
  16. op_engine-0.4.0/examples/test_chemostat_pde.py +107 -0
  17. op_engine-0.4.0/examples/test_fem_diffusion.py +79 -0
  18. op_engine-0.4.0/flepimop2-op_engine/README.md +745 -0
  19. op_engine-0.4.0/flepimop2-op_engine/benchmarks/README.md +95 -0
  20. op_engine-0.4.0/flepimop2-op_engine/benchmarks/__init__.py +17 -0
  21. op_engine-0.4.0/flepimop2-op_engine/benchmarks/adaptive_replay.py +319 -0
  22. op_engine-0.4.0/flepimop2-op_engine/benchmarks/prepared_execution.py +370 -0
  23. op_engine-0.4.0/flepimop2-op_engine/benchmarks/solver_matrix.py +944 -0
  24. op_engine-0.4.0/flepimop2-op_engine/benchmarks/structured_blocks.py +266 -0
  25. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/pyproject.toml +15 -11
  26. op_engine-0.4.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/__init__.py +3904 -0
  27. op_engine-0.4.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/config.py +404 -0
  28. op_engine-0.4.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/explicit_operators.py +300 -0
  29. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/operators.py +314 -13
  30. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/reactions.py +351 -53
  31. op_engine-0.4.0/flepimop2-op_engine/tests/test_adaptive_tau_generated.py +155 -0
  32. op_engine-0.4.0/flepimop2-op_engine/tests/test_axis_less_reactions.py +210 -0
  33. op_engine-0.4.0/flepimop2-op_engine/tests/test_config.py +608 -0
  34. op_engine-0.4.0/flepimop2-op_engine/tests/test_coord_shift.py +335 -0
  35. op_engine-0.4.0/flepimop2-op_engine/tests/test_engine.py +1318 -0
  36. op_engine-0.4.0/flepimop2-op_engine/tests/test_op_system_integration.py +2159 -0
  37. op_engine-0.4.0/flepimop2-op_engine/tests/test_operators.py +696 -0
  38. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_reactions.py +50 -0
  39. op_engine-0.4.0/flepimop2-op_engine/tests/test_renewal.py +463 -0
  40. op_engine-0.4.0/flepimop2-op_engine/tests/test_routed_reactions.py +257 -0
  41. op_engine-0.4.0/flepimop2-op_engine/tests/test_solver_matrix_benchmark.py +134 -0
  42. op_engine-0.4.0/flepimop2-op_engine/tests/test_stochastic_coverage.py +198 -0
  43. op_engine-0.4.0/flepimop2-op_engine/tests/test_thinning.py +515 -0
  44. {op_engine-0.2.0 → op_engine-0.4.0}/justfile +1 -0
  45. {op_engine-0.2.0 → op_engine-0.4.0}/mkdocs.yml +5 -0
  46. {op_engine-0.2.0 → op_engine-0.4.0}/pyproject.toml +5 -1
  47. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/__init__.py +24 -1
  48. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/_additive_runge_kutta.py +3 -2
  49. op_engine-0.4.0/src/op_engine/_array.py +33 -0
  50. op_engine-0.4.0/src/op_engine/_forcing.py +53 -0
  51. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/_multistep.py +2 -14
  52. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/_runge_kutta.py +66 -0
  53. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/_sdirk.py +1 -1
  54. op_engine-0.4.0/src/op_engine/_typing.py +36 -0
  55. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/adaptive_tau.py +30 -5
  56. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/core_solver.py +492 -136
  57. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/matrix_ops.py +179 -99
  58. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/model_core.py +3 -17
  59. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/nonlinear_solver.py +2 -14
  60. op_engine-0.4.0/src/op_engine/spatial_grid.py +252 -0
  61. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/stochastic_solver.py +86 -41
  62. op_engine-0.4.0/src/op_engine/thinning_ssa.py +443 -0
  63. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_adaptive_tau.py +207 -0
  64. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_array_namespace.py +244 -2
  65. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_configuration.py +22 -0
  66. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_core_solver_accuracy.py +80 -0
  67. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_core_solver_plumbing.py +88 -2
  68. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_direct_ssa.py +262 -3
  69. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_matrix_ops.py +153 -0
  70. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_runge_kutta.py +41 -0
  71. op_engine-0.4.0/tests/op_engine/test_spatial_grid.py +140 -0
  72. op_engine-0.4.0/tests/op_engine/test_tau_forcing.py +247 -0
  73. op_engine-0.4.0/tests/op_engine/test_thinning_ssa.py +509 -0
  74. op_engine-0.2.0/CHANGELOG.md +0 -51
  75. op_engine-0.2.0/flepimop2-op_engine/README.md +0 -120
  76. op_engine-0.2.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/__init__.py +0 -868
  77. op_engine-0.2.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/config.py +0 -193
  78. op_engine-0.2.0/flepimop2-op_engine/tests/test_config.py +0 -250
  79. op_engine-0.2.0/flepimop2-op_engine/tests/test_engine.py +0 -363
  80. op_engine-0.2.0/flepimop2-op_engine/tests/test_op_system_integration.py +0 -947
  81. op_engine-0.2.0/flepimop2-op_engine/tests/test_operators.py +0 -259
  82. op_engine-0.2.0/src/op_engine/_typing.py +0 -38
  83. {op_engine-0.2.0 → op_engine-0.4.0}/.github/workflows/ci.yaml +0 -0
  84. {op_engine-0.2.0 → op_engine-0.4.0}/.github/workflows/gh-pages.yaml +0 -0
  85. {op_engine-0.2.0 → op_engine-0.4.0}/.github/workflows/release.yaml +0 -0
  86. {op_engine-0.2.0 → op_engine-0.4.0}/LICENSE +0 -0
  87. {op_engine-0.2.0 → op_engine-0.4.0}/docs/api-reference/.gitignore +0 -0
  88. {op_engine-0.2.0 → op_engine-0.4.0}/docs/development/creating-a-release.md +0 -0
  89. {op_engine-0.2.0 → op_engine-0.4.0}/docs/guides/biogeochemical-network.md +0 -0
  90. {op_engine-0.2.0 → op_engine-0.4.0}/docs/guides/linear-multistep-methods.md +0 -0
  91. {op_engine-0.2.0 → op_engine-0.4.0}/docs/guides/nonlinear-solvers.md +0 -0
  92. {op_engine-0.2.0 → op_engine-0.4.0}/examples/__init__.py +0 -0
  93. {op_engine-0.2.0 → op_engine-0.4.0}/examples/biogeochemical_network.py +0 -0
  94. {op_engine-0.2.0 → op_engine-0.4.0}/examples/simple_sir.py +0 -0
  95. {op_engine-0.2.0 → op_engine-0.4.0}/examples/test_biogeochemical_network.py +0 -0
  96. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/LICENSE +0 -0
  97. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/py.typed +0 -0
  98. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/conftest.py +0 -0
  99. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/data/config.yaml +0 -0
  100. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/data/sir.py +0 -0
  101. {op_engine-0.2.0 → op_engine-0.4.0}/flepimop2-op_engine/tests/test_integration_cli.py +0 -0
  102. {op_engine-0.2.0 → op_engine-0.4.0}/overrides/main.html +0 -0
  103. {op_engine-0.2.0 → op_engine-0.4.0}/scripts/__init__.py +0 -0
  104. {op_engine-0.2.0 → op_engine-0.4.0}/scripts/api-reference.py +0 -0
  105. {op_engine-0.2.0 → op_engine-0.4.0}/scripts/release_validate.py +0 -0
  106. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/_rosenbrock.py +0 -0
  107. {op_engine-0.2.0 → op_engine-0.4.0}/src/op_engine/py.typed +0 -0
  108. {op_engine-0.2.0 → op_engine-0.4.0}/tests/conftest.py +0 -0
  109. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test__version__.py +0 -0
  110. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_additive_runge_kutta.py +0 -0
  111. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_core_solver_ark.py +0 -0
  112. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_core_solver_sdirk.py +0 -0
  113. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_model_core.py +0 -0
  114. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_multistep.py +0 -0
  115. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_nonlinear_solver.py +0 -0
  116. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_rosenbrock.py +0 -0
  117. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_sdirk.py +0 -0
  118. {op_engine-0.2.0 → op_engine-0.4.0}/tests/op_engine/test_stochastic_solver.py +0 -0
@@ -174,6 +174,8 @@ __marimo__/
174
174
  *.png
175
175
  *.pdf
176
176
  *.svg
177
+ !docs/assets/*.png
178
+ !docs/assets/*.svg
177
179
 
178
180
  # Numpy saved arrays
179
181
  *.npy
@@ -184,4 +186,3 @@ __marimo__/
184
186
 
185
187
  # Optional example output directory convention
186
188
  examples/output/
187
-
@@ -0,0 +1,199 @@
1
+ # Changelog
2
+
3
+ Notable user-facing changes are recorded here. Versions follow semantic
4
+ versioning while the project remains pre-1.0.
5
+
6
+ ## [Unreleased]
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
+
67
+ ## [0.3.0] - 2026-09-27
68
+
69
+ ### Added
70
+
71
+ - A trait-structured chemostat PDE example combines a curvature-adapted
72
+ non-uniform finite-volume grid, no-flux implicit diffusion, nonlinear
73
+ resource coupling, structured trait moments, an RK45 reference, and a
74
+ documented three-panel diagnostic (#47).
75
+ - The Flepimop provider compiles typed `jump_integral` descriptors into
76
+ conservative Array-API operators for implicit/IMEX and structured explicit
77
+ execution. It preserves dynamic rates and kernel matrices under JAX,
78
+ applies declared direction and continuous target quadrature, and reuses
79
+ selector/multi-axis lifting with real op_system integration coverage (#94).
80
+ - A finite-element boundary design note and tested 1D P1 diffusion proof of
81
+ concept show how externally assembled mass/stiffness systems use the existing
82
+ stage-operator contract, without adding mesh or element abstractions to core
83
+ (#107).
84
+ - Curvature-weighted 1D spatial-grid generators accept vectorized profile
85
+ callables or sampled data, with optional curvature smoothing and a feasible
86
+ minimum-spacing constraint. They return explicit NumPy geometry for use by
87
+ any runtime array backend (#46).
88
+ - Dense Array-API and sparse SciPy diffusion builders now accept monotone
89
+ non-uniform cell-center grids with conservative no-flux boundaries. The
90
+ flepimop2 provider uses the same geometry for flat, PyTree, and block
91
+ explicit or IMEX execution while retaining JAX differentiation (#45).
92
+ - Explicit adaptive JAX discovery now keeps attempted-step acceptance and
93
+ controller state in a bounded device loop, including conservative shared-mesh
94
+ discovery for block layouts. Compact frozen-mesh replay reports array-valued
95
+ local-error freshness diagnostics and can reject materially stale schedules
96
+ at an outer orchestration boundary without compromising JIT or gradients;
97
+ the solver matrix now benchmarks discovery in each row's actual backend
98
+ (#148).
99
+ - Fixed flat-state JAX DOPRI5 now computes only its high-order solution, keeps
100
+ FSAL reuse, and carries a requested-output buffer instead of materializing
101
+ every hidden internal state. Optional `fixed_checkpoint: step` and `chunk`
102
+ policies control reverse-mode rematerialization; RK4 retains its smaller
103
+ compile-sensitive scan. The solver matrix records the resulting forward and
104
+ gradient IR, compile, memory, runtime, and accuracy changes (#147).
105
+ - The flepimop2 provider can prepare and cache stable flat explicit execution
106
+ plans for fixed stepping and frozen adaptive replay. Prepared callables keep
107
+ array values dynamic for NumPy or caller-owned JAX transforms while reusing
108
+ system binding, packing contracts, step grids, and execution structure;
109
+ explicit cache invalidation and matched staging benchmarks are included (#146).
110
+ - A machine-readable solver benchmark now separates provider construction,
111
+ adaptive discovery, JAX trace/lower/compile, first and warm execution, and
112
+ value-and-gradient phases while reporting analytic accuracy, work counts,
113
+ memory, revision, and environment metadata. SciPy and optional Diffrax
114
+ references use explicit solver, controller, and adjoint settings (#145).
115
+ - Numerical namespace discovery now uses `array-api-compat`, including native
116
+ PyTorch tensors that do not implement `__array_namespace__`. A public
117
+ `array_namespace` helper and optional Torch contract test preserve JAX
118
+ JIT/grad behavior while extending eager fixed-step autograd support (#144).
119
+ - Explicit fixed-step runs can bound their internal integration step with
120
+ `fixed_max_step` independently of requested output times, including compact
121
+ differentiable JAX provider trajectories (#130).
122
+ - The flepimop2 provider exposes SDIRK2 with explicit full-RHS Jacobian and
123
+ nonlinear-solver options, plus adaptive initial-step and safety limits. A
124
+ method-surface parity test now requires explicit provider decisions for new
125
+ core methods, and frozen schedules tolerate representational rounding when
126
+ replayed on a lower-precision backend (#131).
127
+ - Compact JAX fixed-step provider trajectories use a single `jax.lax.scan`
128
+ while the core retains backend-neutral functional step kernels (#123).
129
+ - The flepimop2 provider exposes eager adaptive-schedule discovery and
130
+ validated frozen-mesh replay for conditional JAX JIT and differentiation
131
+ workflows (#127).
132
+
133
+ ### Fixed
134
+
135
+ - Sparse implicit-solver cache entries retain and verify their exact operator
136
+ objects, preventing stale factorizations when Python recycles an object ID
137
+ for a different same-shape matrix (#164).
138
+ - IMEX Heun--trapezoidal now assembles its endpoint predictor and corrector as
139
+ additive solves, so nonlinear and non-commuting explicit/implicit splits
140
+ retain second-order convergence across NumPy and Array-API execution (#162).
141
+ - The public `Array` protocol now matches `array-api-compat` namespace
142
+ discovery and accepts native tensors such as `torch.Tensor` without requiring
143
+ a direct `__array_namespace__` method (#155).
144
+ - `flepimop2-op-engine` now types public trajectories with Flepimop2's
145
+ backend-neutral `Array` protocol and returns the native NumPy or JAX result
146
+ without a NumPy-only cast (#154).
147
+ - Explicit flepimop2 provider methods now add typed op_system axis-kernel,
148
+ advection, and diffusion drift at every Runge--Kutta stage. Flat, PyTree,
149
+ and block layouts share the same namespace-preserving operator path instead
150
+ of silently omitting operators outside IMEX execution (#140).
151
+ - Typed advection and transport descriptors now honor op_system's explicit
152
+ `increasing`/`decreasing` direction contract in both IMEX and explicit
153
+ provider paths while preserving JAX differentiation through dynamic
154
+ coefficients (#142).
155
+
156
+ ## [0.2.0] - 2026-09-26
157
+
158
+ ### Added
159
+
160
+ - Array-API-native state, explicit Runge--Kutta, dense implicit, and IMEX
161
+ paths preserve the namespace selected by the initial state. Portable
162
+ fixed-step methods support JAX tracing and differentiation without adding a
163
+ backend-specific solver to the core method surface (#75, #76, #85, #87,
164
+ #89, #90).
165
+ - Reusable explicit Runge--Kutta tableaus through Dormand--Prince 5(4),
166
+ adaptive step control with differentiable accepted-schedule replay, and
167
+ validated public run configuration (#95, #100, #108).
168
+ - A portable nonlinear-solver contract plus corrected ROS2, reusable
169
+ multistep BDF2, SDIRK2 diagnostics, and higher-order paired IMEX
170
+ Runge--Kutta integration (#113, #114, #115, #117, #118, #119).
171
+ - Backend-neutral fixed tau-leaping, exact direct SSA, and bounded adaptive
172
+ tau-leaping with injected random samplers (#102, #120, #121).
173
+ - Portable advection and typed diffusion/advection operator compilation for
174
+ flepimop2, including dynamic Array-API parameters (#81, #91, #92, #93).
175
+ - The flepimop2 provider consumes typed op_system reaction artifacts and can
176
+ execute deterministic, stochastic, or hybrid reaction networks without
177
+ parsing raw model configuration (#122).
178
+
179
+ ### Changed
180
+
181
+ - Provider development and integration coverage now targets the op_system
182
+ 0.4 typed operator/reaction contract.
183
+ - `flepimop2-op-engine` declares its actual `flepimop2>=0.3.0` compatibility
184
+ floor. Clean-wheel release validation installs the lowest published direct
185
+ dependencies instead of installing flepimop2 from its development branch.
186
+ - The documented flepimop2 installation uses the separately distributed
187
+ `flepimop2-op-engine` package. Its `op-system` extra installs the matching
188
+ system provider integration.
189
+
190
+ ### Fixed
191
+
192
+ - Explicit Euler once again performs one Euler update per requested output
193
+ interval (#79).
194
+ - Solver-method documentation now matches the implemented biogeochemical
195
+ splitting order and backend boundaries (#98).
196
+
197
+ ## [0.1.1] - 2026-04-27
198
+
199
+ - Initial PyPI release of `op-engine` and `flepimop2-op-engine`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: op_engine
3
- Version: 0.2.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/
@@ -39,12 +39,15 @@ Classifier: Programming Language :: Python :: 3.14
39
39
  Classifier: Topic :: Scientific/Engineering
40
40
  Classifier: Topic :: Scientific/Engineering :: Mathematics
41
41
  Requires-Python: <3.15,>=3.11
42
+ Requires-Dist: array-api-compat>=1.12
42
43
  Requires-Dist: numpy>=2.0
43
44
  Requires-Dist: scipy>=1.11
44
45
  Provides-Extra: cupy
45
46
  Requires-Dist: cupy>=13; extra == 'cupy'
46
47
  Provides-Extra: jax
47
48
  Requires-Dist: jax>=0.4.38; extra == 'jax'
49
+ Provides-Extra: torch
50
+ Requires-Dist: torch>=2.6; extra == 'torch'
48
51
  Description-Content-Type: text/markdown
49
52
 
50
53
  # op_engine
@@ -85,6 +88,18 @@ With JAX as the array namespace:
85
88
  pip install "op_engine[jax]"
86
89
  ```
87
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
+
88
103
  With flepimop2 adapter:
89
104
 
90
105
  ```bash
@@ -124,9 +139,10 @@ solution = core.state_array # shape (n_timesteps, state, subgroup)
124
139
  ### Array namespaces
125
140
 
126
141
  `ModelCore` infers its numerical namespace from the initial state through
127
- `initial_state.__array_namespace__()`. The state, stored history, solver stages,
142
+ `array_api_compat.array_namespace()`. The state, stored history, solver stages,
128
143
  and dense solver operations stay in that namespace. There is no `xp=` or
129
- backend option. Fixed-step methods can be differentiated by JAX; the built-in
144
+ backend option. This supports standard Array-API objects and native arrays such
145
+ as `torch.Tensor`. Fixed-step methods can be differentiated by JAX; the built-in
130
146
  adaptive controller is eager Python control flow. See the
131
147
  [backend guide](docs/guides/backends.md) for the precise contract and the
132
148
  [solver guide](docs/guides/solver-methods.md) for method-specific examples.
@@ -135,7 +151,8 @@ adaptive controller is eager Python control flow. See the
135
151
 
136
152
  ```python
137
153
  import numpy as np
138
- from op_engine import CoreSolver, ModelCore, OperatorSpecs
154
+ from op_engine import CoreSolver, ModelCore
155
+ from op_engine.core_solver import OperatorSpecs, RunConfig
139
156
 
140
157
  n = 4
141
158
  times = np.linspace(0.0, 1.0, 11)
@@ -152,13 +169,20 @@ def rhs(t, y):
152
169
  return -0.1 * y
153
170
 
154
171
 
155
- solver = CoreSolver(core, operators=ops.default, operator_axis="state")
156
- solver.run(rhs, config=None) # defaults: method="heun" (explicit)
157
-
158
- # For IMEX methods set method and operators via RunConfig:
159
- # from op_engine.core_solver import RunConfig, AdaptiveConfig, DtControllerConfig
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
+ )
160
180
  ```
161
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
+
162
186
  ## Public API
163
187
  - `ModelCore`: state tensor + time grid manager; supports extra axes and optional history.
164
188
  - `CoreSolver`: portable explicit and dense IMEX/implicit stepping selected by the state array namespace.
@@ -166,7 +190,9 @@ solver.run(rhs, config=None) # defaults: method="heun" (explicit)
166
190
  - `AdaptiveTauLeapingSolver`: adaptive non-negative stochastic leaps using explicit reactant stoichiometry and injected Poisson plus exact-event sampling.
167
191
  - `TauLeapingSolver`: portable stoichiometric updates with NumPy, JAX, or another Array-API namespace supplying Poisson samples.
168
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.
169
- - Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`, `DtControllerConfig` for method/IMEX/adaptive control.
193
+ - Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`, and
194
+ `DtControllerConfig` from `op_engine.core_solver` for method, IMEX, and
195
+ adaptive control.
170
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`.
171
197
 
172
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
@@ -75,9 +87,10 @@ solution = core.state_array # shape (n_timesteps, state, subgroup)
75
87
  ### Array namespaces
76
88
 
77
89
  `ModelCore` infers its numerical namespace from the initial state through
78
- `initial_state.__array_namespace__()`. The state, stored history, solver stages,
90
+ `array_api_compat.array_namespace()`. The state, stored history, solver stages,
79
91
  and dense solver operations stay in that namespace. There is no `xp=` or
80
- backend option. Fixed-step methods can be differentiated by JAX; the built-in
92
+ backend option. This supports standard Array-API objects and native arrays such
93
+ as `torch.Tensor`. Fixed-step methods can be differentiated by JAX; the built-in
81
94
  adaptive controller is eager Python control flow. See the
82
95
  [backend guide](docs/guides/backends.md) for the precise contract and the
83
96
  [solver guide](docs/guides/solver-methods.md) for method-specific examples.
@@ -86,7 +99,8 @@ adaptive controller is eager Python control flow. See the
86
99
 
87
100
  ```python
88
101
  import numpy as np
89
- from op_engine import CoreSolver, ModelCore, OperatorSpecs
102
+ from op_engine import CoreSolver, ModelCore
103
+ from op_engine.core_solver import OperatorSpecs, RunConfig
90
104
 
91
105
  n = 4
92
106
  times = np.linspace(0.0, 1.0, 11)
@@ -103,13 +117,20 @@ def rhs(t, y):
103
117
  return -0.1 * y
104
118
 
105
119
 
106
- solver = CoreSolver(core, operators=ops.default, operator_axis="state")
107
- solver.run(rhs, config=None) # defaults: method="heun" (explicit)
108
-
109
- # For IMEX methods set method and operators via RunConfig:
110
- # from op_engine.core_solver import RunConfig, AdaptiveConfig, DtControllerConfig
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
+ )
111
128
  ```
112
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
+
113
134
  ## Public API
114
135
  - `ModelCore`: state tensor + time grid manager; supports extra axes and optional history.
115
136
  - `CoreSolver`: portable explicit and dense IMEX/implicit stepping selected by the state array namespace.
@@ -117,7 +138,9 @@ solver.run(rhs, config=None) # defaults: method="heun" (explicit)
117
138
  - `AdaptiveTauLeapingSolver`: adaptive non-negative stochastic leaps using explicit reactant stoichiometry and injected Poisson plus exact-event sampling.
118
139
  - `TauLeapingSolver`: portable stoichiometric updates with NumPy, JAX, or another Array-API namespace supplying Poisson samples.
119
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.
120
- - Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`, `DtControllerConfig` for method/IMEX/adaptive control.
141
+ - Configuration helpers: `RunConfig`, `OperatorSpecs`, `AdaptiveConfig`, and
142
+ `DtControllerConfig` from `op_engine.core_solver` for method, IMEX, and
143
+ adaptive control.
121
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`.
122
145
 
123
146
  ## Development
@@ -0,0 +1,41 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="900" height="280" viewBox="0 0 900 280" role="img" aria-labelledby="title desc">
2
+ <title id="title">Chemostat PDE operator split</title>
3
+ <desc id="desc">Population density and resource enter explicit reaction and implicit diffusion partitions, which are combined by the IMEX solver into observables.</desc>
4
+ <defs>
5
+ <marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
6
+ <path d="M0,0 L0,6 L9,3 z" fill="#4051b5"/>
7
+ </marker>
8
+ <style>
9
+ .box { fill: #f7f8fc; stroke: #4051b5; stroke-width: 2; rx: 10; }
10
+ .accent { fill: #eef7f3; stroke: #16866b; stroke-width: 2; rx: 10; }
11
+ .text { font: 16px sans-serif; fill: #202124; text-anchor: middle; }
12
+ .small { font: 13px sans-serif; fill: #4b4d52; text-anchor: middle; }
13
+ .arrow { stroke: #4051b5; stroke-width: 2.5; fill: none; marker-end: url(#arrow); }
14
+ </style>
15
+ </defs>
16
+ <rect class="box" x="25" y="75" width="190" height="120"/>
17
+ <text class="text" x="120" y="110">State</text>
18
+ <text class="small" x="120" y="140">trait density n(theta)</text>
19
+ <text class="small" x="120" y="165">resource S</text>
20
+
21
+ <path class="arrow" d="M215 115 H295"/>
22
+ <path class="arrow" d="M215 165 H295"/>
23
+
24
+ <rect class="accent" x="300" y="25" width="245" height="95"/>
25
+ <text class="text" x="422" y="58">Explicit partition</text>
26
+ <text class="small" x="422" y="83">growth + dilution</text>
27
+ <text class="small" x="422" y="103">resource coupling</text>
28
+
29
+ <rect class="accent" x="300" y="155" width="245" height="95"/>
30
+ <text class="text" x="422" y="188">Implicit partition</text>
31
+ <text class="small" x="422" y="213">non-uniform no-flux</text>
32
+ <text class="small" x="422" y="233">trait diffusion</text>
33
+
34
+ <path class="arrow" d="M545 73 C600 73 600 112 640 125"/>
35
+ <path class="arrow" d="M545 203 C600 203 600 164 640 151"/>
36
+
37
+ <rect class="box" x="645" y="75" width="225" height="120"/>
38
+ <text class="text" x="757" y="110">IMEX trajectory</text>
39
+ <text class="small" x="757" y="140">biomass + resource</text>
40
+ <text class="small" x="757" y="165">mean + central moments</text>
41
+ </svg>
@@ -0,0 +1,102 @@
1
+ # Finite-element assembly boundary
2
+
3
+ Finite elements are a spatial discretization and assembly system, not a new
4
+ `CoreSolver` time-method name. The current decision is **not to add a mesh,
5
+ element, or quadrature framework to `op_engine` core and not to create an
6
+ `op_engine-fem` provider yet**. Externally assembled linear finite-element
7
+ systems can already use the stage-operator interface.
8
+
9
+ ## Supported semi-discrete form
10
+
11
+ Consider the linear system
12
+
13
+ \[
14
+ M \dot y = K y + g(t, y).
15
+ \]
16
+
17
+ An operator factory can map its implicit linear part into the existing solve
18
+
19
+ \[
20
+ L y_{n+1} = R x.
21
+ \]
22
+
23
+ For example, the factory for a trapezoidal stage of size \(h\) returns
24
+
25
+ \[
26
+ L = M - \tfrac{h}{2}K,
27
+ \qquad
28
+ R = M + \tfrac{h}{2}K.
29
+ \]
30
+
31
+ An implicit-Euler stage instead returns \(L=M-hK\) and \(R=M\). This supports
32
+ fixed or variable stage sizes and permits the existing IMEX Euler,
33
+ Heun--trapezoidal, and TR-BDF2 control flow to advance an externally assembled
34
+ mass/stiffness pair.
35
+
36
+ The explicit callable still has derivative units. A finite-element load vector
37
+ `g` must therefore be supplied as `solve(M, g)`; the current API does not inject
38
+ an unscaled weak-form load directly. Likewise, `imex-ark3` currently consumes
39
+ only the left implicit solve at each additive stage, so a non-identity mass
40
+ matrix must be resolved to an operator such as `solve(M, K)` before using that
41
+ method. Changing either contract should be motivated and designed separately,
42
+ not hidden in an FEM adapter.
43
+
44
+ ## Ownership
45
+
46
+ The assembly package or application owns:
47
+
48
+ - mesh topology and geometry;
49
+ - basis functions, quadrature, and element integration;
50
+ - degree-of-freedom maps and constraint elimination;
51
+ - essential and natural boundary-condition assembly;
52
+ - mass, stiffness, load, and nonlinear residual assembly;
53
+ - any matrix reassembly or preconditioner lifecycle.
54
+
55
+ `op_engine` owns time integration, stage context, operator-axis application,
56
+ accepted-step control, and the `L @ y_next = R @ x` solve boundary. This keeps
57
+ optional SciPy, PETSc, FEniCSx, and JAX FEM ecosystems from becoming core
58
+ dependencies.
59
+
60
+ ## Sparse operators and differentiation
61
+
62
+ SciPy CSR pairs use the registered sparse adapter. A provider should reuse
63
+ operator objects when their values do not change so the factorization cache is
64
+ effective. Other array backends can supply dense native arrays and use the
65
+ Array-API linear solve. A JAX-native dense assembly remains differentiable
66
+ through matrix construction and solve; eager NumPy/SciPy assembly and
67
+ factorization do not. An external PETSc or FEniCSx provider would own any
68
+ custom differentiation rule for its solver.
69
+
70
+ Nonlinear forms such as \(M(y,t)\dot y=r(t,y)\), differential-algebraic
71
+ constraints, and external Krylov/preconditioner callbacks are outside the
72
+ current stage-operator contract. They require a concrete use case before a new
73
+ portable solver interface is added.
74
+
75
+ ## Proof of concept
76
+
77
+ [`examples/fem_diffusion.py`](https://github.com/ACCIDDA/op_engine/blob/main/examples/fem_diffusion.py)
78
+ assembles a consistent P1 mass matrix and diffusion stiffness matrix on a 1D
79
+ mesh, eliminates homogeneous Dirichlet boundary degrees of freedom, and runs
80
+ the result with fixed-step `imex-heun-tr`. Assembly uses SciPy only because the
81
+ example is demonstrating the external sparse boundary; it is not a required
82
+ FEM representation.
83
+
84
+ The example intentionally uses fixed steps. For pure implicit diffusion,
85
+ Heun--trapezoidal's embedded low/high pair differs only in its explicit term
86
+ and therefore cannot estimate the implicit truncation error. Adaptive FEM runs
87
+ should currently use a method with a suitable estimator, such as TR-BDF2 step
88
+ doubling, or supply and validate a fixed/replayed mesh at a higher layer.
89
+
90
+ ## When to reconsider a provider
91
+
92
+ Reconsider a separate provider, rather than extending core, when at least one
93
+ real unstructured-domain model identifies all of the following:
94
+
95
+ 1. a specific external assembly and linear-solver ecosystem;
96
+ 2. required boundary-condition and constraint semantics;
97
+ 3. sparse/device and differentiation expectations;
98
+ 4. nonlinear and reassembly requirements;
99
+ 5. a benchmark showing that the existing `(L, R)` boundary is insufficient.
100
+
101
+ Until then, the proof of concept is the supported design pattern and a general
102
+ FEM abstraction would add dependencies and policy without a validated user.
@@ -1,6 +1,9 @@
1
1
  # Backend and solve-strategy boundaries
2
2
 
3
- `op_engine` selects the numerical namespace from the state array. Explicit,
3
+ `op_engine` selects the numerical namespace from the state array with
4
+ `array_api_compat.array_namespace()`. This supports arrays with the standard
5
+ `__array_namespace__` protocol and native arrays such as `torch.Tensor`
6
+ through compatibility namespaces. Explicit,
4
7
  dense linearly implicit, and IMEX methods use that namespace's Array-API
5
8
  operations, including `linalg.solve`. The numerical method does not change when
6
9
  the array namespace changes.
@@ -9,6 +12,10 @@ Namespace portability and automatic differentiation are related but distinct.
9
12
  The Array API does not define `grad`, `jit`, or traced control flow. A namespace
10
13
  such as JAX can nevertheless differentiate the ordinary `op_engine` methods
11
14
  because their fixed-step numerical operations remain in that namespace.
15
+ The optional `torch` dependency group qualifies representative eager
16
+ fixed-step kernels with PyTorch autograd. Compiler integration and complete
17
+ method qualification remain provider/backend capabilities rather than
18
+ consequences of namespace selection.
12
19
 
13
20
  ## Portable fixed-step methods
14
21
 
@@ -47,15 +54,17 @@ parameters move, and always refresh it after a material model or tolerance
47
54
  change. A schedule is validated against the output grid, while matching the
48
55
  method and other configuration is the caller's responsibility.
49
56
 
50
- Schedule replay statically unrolls the accepted steps when JAX traces it. For
51
- very long meshes, a provider may instead implement bounded, masked compiled
52
- control flow around the same portable kernels.
57
+ Direct `CoreSolver` schedule replay statically unrolls accepted steps when JAX
58
+ traces it. The optional flepimop2 provider consumes the same functional kernels
59
+ but defaults explicit JAX replay to one compact `lax.scan`. Its forced compact
60
+ mode can also checkpoint/rematerialize each step for long reverse-mode solves;
61
+ non-JAX and nonlinear-diagnostic paths retain portable core replay.
53
62
 
54
- Projects that require a compiled adaptive controller, checkpointed adjoints, or
55
- other solver-specific capabilities can provide those at an external plugin or
56
- provider boundary. A specialized integration may return a complete trajectory
57
- and adopt it through `ModelCore.apply_trajectory`; it does not need to add a
58
- backend-specific method to `CoreSolver`.
63
+ Projects that require a compiled adaptive controller or other solver-specific
64
+ capabilities can provide those at an external plugin or provider boundary. A
65
+ specialized integration may return a complete trajectory and adopt it through
66
+ `ModelCore.apply_trajectory`; it does not need to add a backend-specific method
67
+ to `CoreSolver`.
59
68
 
60
69
  This keeps optional packages such as Diffrax out of the core dependency and
61
70
  method surfaces. The portable fixed-step methods and their differentiation
@@ -73,6 +82,9 @@ semantics.
73
82
  `NumpySSASampler` and `NumpyPoissonSampler` are provided as conveniences.
74
83
  JAX users can construct fresh keys from the stable direct-SSA `draw_index` or
75
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.
76
88
 
77
89
  The current safety checks and stochastic event loops are eager, so these
78
90
  stochastic solvers are not JIT-compatible paths. Adaptive selection also reads