op-engine 0.2.0__tar.gz → 0.3.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.2.0 → op_engine-0.3.0}/.gitignore +2 -1
- op_engine-0.3.0/CHANGELOG.md +140 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/PKG-INFO +7 -3
- {op_engine-0.2.0 → op_engine-0.3.0}/README.md +3 -2
- op_engine-0.3.0/docs/assets/chemostat-split.svg +41 -0
- op_engine-0.3.0/docs/assets/chemostat-summary.png +0 -0
- op_engine-0.3.0/docs/development/fem-assembly-boundary.md +102 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/guides/backends.md +18 -9
- op_engine-0.3.0/docs/guides/chemostat-pde.md +97 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/guides/getting-started.md +12 -6
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/guides/solver-methods.md +65 -7
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/index.md +3 -0
- op_engine-0.3.0/examples/chemostat_pde.py +614 -0
- op_engine-0.3.0/examples/fem_diffusion.py +189 -0
- op_engine-0.3.0/examples/test_chemostat_pde.py +107 -0
- op_engine-0.3.0/examples/test_fem_diffusion.py +79 -0
- op_engine-0.3.0/flepimop2-op_engine/README.md +381 -0
- op_engine-0.3.0/flepimop2-op_engine/benchmarks/README.md +93 -0
- op_engine-0.3.0/flepimop2-op_engine/benchmarks/__init__.py +17 -0
- op_engine-0.3.0/flepimop2-op_engine/benchmarks/adaptive_replay.py +319 -0
- op_engine-0.3.0/flepimop2-op_engine/benchmarks/prepared_execution.py +311 -0
- op_engine-0.3.0/flepimop2-op_engine/benchmarks/solver_matrix.py +944 -0
- op_engine-0.3.0/flepimop2-op_engine/benchmarks/structured_blocks.py +266 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/pyproject.toml +15 -11
- op_engine-0.3.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/__init__.py +3605 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/config.py +168 -4
- op_engine-0.3.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/explicit_operators.py +300 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/operators.py +314 -13
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/reactions.py +167 -20
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/tests/test_config.py +238 -3
- op_engine-0.3.0/flepimop2-op_engine/tests/test_engine.py +1310 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/tests/test_op_system_integration.py +755 -11
- op_engine-0.3.0/flepimop2-op_engine/tests/test_operators.py +696 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/tests/test_reactions.py +50 -0
- op_engine-0.3.0/flepimop2-op_engine/tests/test_solver_matrix_benchmark.py +99 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/justfile +1 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/mkdocs.yml +3 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/pyproject.toml +5 -1
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/__init__.py +8 -1
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/_additive_runge_kutta.py +3 -2
- op_engine-0.3.0/src/op_engine/_array.py +33 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/_multistep.py +2 -14
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/_runge_kutta.py +66 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/_sdirk.py +1 -1
- op_engine-0.3.0/src/op_engine/_typing.py +36 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/adaptive_tau.py +1 -1
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/core_solver.py +492 -136
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/matrix_ops.py +179 -99
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/model_core.py +3 -17
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/nonlinear_solver.py +2 -14
- op_engine-0.3.0/src/op_engine/spatial_grid.py +252 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/stochastic_solver.py +2 -15
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_array_namespace.py +244 -2
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_configuration.py +22 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_core_solver_accuracy.py +80 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_core_solver_plumbing.py +88 -2
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_matrix_ops.py +153 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_runge_kutta.py +41 -0
- op_engine-0.3.0/tests/op_engine/test_spatial_grid.py +140 -0
- op_engine-0.2.0/CHANGELOG.md +0 -51
- op_engine-0.2.0/flepimop2-op_engine/README.md +0 -120
- op_engine-0.2.0/flepimop2-op_engine/src/flepimop2/engine/op_engine/__init__.py +0 -868
- op_engine-0.2.0/flepimop2-op_engine/tests/test_engine.py +0 -363
- op_engine-0.2.0/flepimop2-op_engine/tests/test_operators.py +0 -259
- op_engine-0.2.0/src/op_engine/_typing.py +0 -38
- {op_engine-0.2.0 → op_engine-0.3.0}/.github/workflows/ci.yaml +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/.github/workflows/gh-pages.yaml +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/.github/workflows/release.yaml +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/LICENSE +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/api-reference/.gitignore +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/development/creating-a-release.md +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/guides/biogeochemical-network.md +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/guides/linear-multistep-methods.md +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/docs/guides/nonlinear-solvers.md +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/examples/__init__.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/examples/biogeochemical_network.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/examples/simple_sir.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/examples/test_biogeochemical_network.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/LICENSE +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/src/flepimop2/engine/op_engine/py.typed +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/tests/conftest.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/tests/data/config.yaml +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/tests/data/sir.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/flepimop2-op_engine/tests/test_integration_cli.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/overrides/main.html +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/scripts/__init__.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/scripts/api-reference.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/scripts/release_validate.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/_rosenbrock.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/src/op_engine/py.typed +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/conftest.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test__version__.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_adaptive_tau.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_additive_runge_kutta.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_core_solver_ark.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_core_solver_sdirk.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_direct_ssa.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_model_core.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_multistep.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_nonlinear_solver.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_rosenbrock.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.0}/tests/op_engine/test_sdirk.py +0 -0
- {op_engine-0.2.0 → op_engine-0.3.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,140 @@
|
|
|
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.3.0] - 2026-09-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- A trait-structured chemostat PDE example combines a curvature-adapted
|
|
13
|
+
non-uniform finite-volume grid, no-flux implicit diffusion, nonlinear
|
|
14
|
+
resource coupling, structured trait moments, an RK45 reference, and a
|
|
15
|
+
documented three-panel diagnostic (#47).
|
|
16
|
+
- The Flepimop provider compiles typed `jump_integral` descriptors into
|
|
17
|
+
conservative Array-API operators for implicit/IMEX and structured explicit
|
|
18
|
+
execution. It preserves dynamic rates and kernel matrices under JAX,
|
|
19
|
+
applies declared direction and continuous target quadrature, and reuses
|
|
20
|
+
selector/multi-axis lifting with real op_system integration coverage (#94).
|
|
21
|
+
- A finite-element boundary design note and tested 1D P1 diffusion proof of
|
|
22
|
+
concept show how externally assembled mass/stiffness systems use the existing
|
|
23
|
+
stage-operator contract, without adding mesh or element abstractions to core
|
|
24
|
+
(#107).
|
|
25
|
+
- Curvature-weighted 1D spatial-grid generators accept vectorized profile
|
|
26
|
+
callables or sampled data, with optional curvature smoothing and a feasible
|
|
27
|
+
minimum-spacing constraint. They return explicit NumPy geometry for use by
|
|
28
|
+
any runtime array backend (#46).
|
|
29
|
+
- Dense Array-API and sparse SciPy diffusion builders now accept monotone
|
|
30
|
+
non-uniform cell-center grids with conservative no-flux boundaries. The
|
|
31
|
+
flepimop2 provider uses the same geometry for flat, PyTree, and block
|
|
32
|
+
explicit or IMEX execution while retaining JAX differentiation (#45).
|
|
33
|
+
- Explicit adaptive JAX discovery now keeps attempted-step acceptance and
|
|
34
|
+
controller state in a bounded device loop, including conservative shared-mesh
|
|
35
|
+
discovery for block layouts. Compact frozen-mesh replay reports array-valued
|
|
36
|
+
local-error freshness diagnostics and can reject materially stale schedules
|
|
37
|
+
at an outer orchestration boundary without compromising JIT or gradients;
|
|
38
|
+
the solver matrix now benchmarks discovery in each row's actual backend
|
|
39
|
+
(#148).
|
|
40
|
+
- Fixed flat-state JAX DOPRI5 now computes only its high-order solution, keeps
|
|
41
|
+
FSAL reuse, and carries a requested-output buffer instead of materializing
|
|
42
|
+
every hidden internal state. Optional `fixed_checkpoint: step` and `chunk`
|
|
43
|
+
policies control reverse-mode rematerialization; RK4 retains its smaller
|
|
44
|
+
compile-sensitive scan. The solver matrix records the resulting forward and
|
|
45
|
+
gradient IR, compile, memory, runtime, and accuracy changes (#147).
|
|
46
|
+
- The flepimop2 provider can prepare and cache stable flat explicit execution
|
|
47
|
+
plans for fixed stepping and frozen adaptive replay. Prepared callables keep
|
|
48
|
+
array values dynamic for NumPy or caller-owned JAX transforms while reusing
|
|
49
|
+
system binding, packing contracts, step grids, and execution structure;
|
|
50
|
+
explicit cache invalidation and matched staging benchmarks are included (#146).
|
|
51
|
+
- A machine-readable solver benchmark now separates provider construction,
|
|
52
|
+
adaptive discovery, JAX trace/lower/compile, first and warm execution, and
|
|
53
|
+
value-and-gradient phases while reporting analytic accuracy, work counts,
|
|
54
|
+
memory, revision, and environment metadata. SciPy and optional Diffrax
|
|
55
|
+
references use explicit solver, controller, and adjoint settings (#145).
|
|
56
|
+
- Numerical namespace discovery now uses `array-api-compat`, including native
|
|
57
|
+
PyTorch tensors that do not implement `__array_namespace__`. A public
|
|
58
|
+
`array_namespace` helper and optional Torch contract test preserve JAX
|
|
59
|
+
JIT/grad behavior while extending eager fixed-step autograd support (#144).
|
|
60
|
+
- Explicit fixed-step runs can bound their internal integration step with
|
|
61
|
+
`fixed_max_step` independently of requested output times, including compact
|
|
62
|
+
differentiable JAX provider trajectories (#130).
|
|
63
|
+
- The flepimop2 provider exposes SDIRK2 with explicit full-RHS Jacobian and
|
|
64
|
+
nonlinear-solver options, plus adaptive initial-step and safety limits. A
|
|
65
|
+
method-surface parity test now requires explicit provider decisions for new
|
|
66
|
+
core methods, and frozen schedules tolerate representational rounding when
|
|
67
|
+
replayed on a lower-precision backend (#131).
|
|
68
|
+
- Compact JAX fixed-step provider trajectories use a single `jax.lax.scan`
|
|
69
|
+
while the core retains backend-neutral functional step kernels (#123).
|
|
70
|
+
- The flepimop2 provider exposes eager adaptive-schedule discovery and
|
|
71
|
+
validated frozen-mesh replay for conditional JAX JIT and differentiation
|
|
72
|
+
workflows (#127).
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- Sparse implicit-solver cache entries retain and verify their exact operator
|
|
77
|
+
objects, preventing stale factorizations when Python recycles an object ID
|
|
78
|
+
for a different same-shape matrix (#164).
|
|
79
|
+
- IMEX Heun--trapezoidal now assembles its endpoint predictor and corrector as
|
|
80
|
+
additive solves, so nonlinear and non-commuting explicit/implicit splits
|
|
81
|
+
retain second-order convergence across NumPy and Array-API execution (#162).
|
|
82
|
+
- The public `Array` protocol now matches `array-api-compat` namespace
|
|
83
|
+
discovery and accepts native tensors such as `torch.Tensor` without requiring
|
|
84
|
+
a direct `__array_namespace__` method (#155).
|
|
85
|
+
- `flepimop2-op-engine` now types public trajectories with Flepimop2's
|
|
86
|
+
backend-neutral `Array` protocol and returns the native NumPy or JAX result
|
|
87
|
+
without a NumPy-only cast (#154).
|
|
88
|
+
- Explicit flepimop2 provider methods now add typed op_system axis-kernel,
|
|
89
|
+
advection, and diffusion drift at every Runge--Kutta stage. Flat, PyTree,
|
|
90
|
+
and block layouts share the same namespace-preserving operator path instead
|
|
91
|
+
of silently omitting operators outside IMEX execution (#140).
|
|
92
|
+
- Typed advection and transport descriptors now honor op_system's explicit
|
|
93
|
+
`increasing`/`decreasing` direction contract in both IMEX and explicit
|
|
94
|
+
provider paths while preserving JAX differentiation through dynamic
|
|
95
|
+
coefficients (#142).
|
|
96
|
+
|
|
97
|
+
## [0.2.0] - 2026-09-26
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
|
|
101
|
+
- Array-API-native state, explicit Runge--Kutta, dense implicit, and IMEX
|
|
102
|
+
paths preserve the namespace selected by the initial state. Portable
|
|
103
|
+
fixed-step methods support JAX tracing and differentiation without adding a
|
|
104
|
+
backend-specific solver to the core method surface (#75, #76, #85, #87,
|
|
105
|
+
#89, #90).
|
|
106
|
+
- Reusable explicit Runge--Kutta tableaus through Dormand--Prince 5(4),
|
|
107
|
+
adaptive step control with differentiable accepted-schedule replay, and
|
|
108
|
+
validated public run configuration (#95, #100, #108).
|
|
109
|
+
- A portable nonlinear-solver contract plus corrected ROS2, reusable
|
|
110
|
+
multistep BDF2, SDIRK2 diagnostics, and higher-order paired IMEX
|
|
111
|
+
Runge--Kutta integration (#113, #114, #115, #117, #118, #119).
|
|
112
|
+
- Backend-neutral fixed tau-leaping, exact direct SSA, and bounded adaptive
|
|
113
|
+
tau-leaping with injected random samplers (#102, #120, #121).
|
|
114
|
+
- Portable advection and typed diffusion/advection operator compilation for
|
|
115
|
+
flepimop2, including dynamic Array-API parameters (#81, #91, #92, #93).
|
|
116
|
+
- The flepimop2 provider consumes typed op_system reaction artifacts and can
|
|
117
|
+
execute deterministic, stochastic, or hybrid reaction networks without
|
|
118
|
+
parsing raw model configuration (#122).
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
|
|
122
|
+
- Provider development and integration coverage now targets the op_system
|
|
123
|
+
0.4 typed operator/reaction contract.
|
|
124
|
+
- `flepimop2-op-engine` declares its actual `flepimop2>=0.3.0` compatibility
|
|
125
|
+
floor. Clean-wheel release validation installs the lowest published direct
|
|
126
|
+
dependencies instead of installing flepimop2 from its development branch.
|
|
127
|
+
- The documented flepimop2 installation uses the separately distributed
|
|
128
|
+
`flepimop2-op-engine` package. Its `op-system` extra installs the matching
|
|
129
|
+
system provider integration.
|
|
130
|
+
|
|
131
|
+
### Fixed
|
|
132
|
+
|
|
133
|
+
- Explicit Euler once again performs one Euler update per requested output
|
|
134
|
+
interval (#79).
|
|
135
|
+
- Solver-method documentation now matches the implemented biogeochemical
|
|
136
|
+
splitting order and backend boundaries (#98).
|
|
137
|
+
|
|
138
|
+
## [0.1.1] - 2026-04-27
|
|
139
|
+
|
|
140
|
+
- 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.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -124,9 +127,10 @@ solution = core.state_array # shape (n_timesteps, state, subgroup)
|
|
|
124
127
|
### Array namespaces
|
|
125
128
|
|
|
126
129
|
`ModelCore` infers its numerical namespace from the initial state through
|
|
127
|
-
`
|
|
130
|
+
`array_api_compat.array_namespace()`. The state, stored history, solver stages,
|
|
128
131
|
and dense solver operations stay in that namespace. There is no `xp=` or
|
|
129
|
-
backend option.
|
|
132
|
+
backend option. This supports standard Array-API objects and native arrays such
|
|
133
|
+
as `torch.Tensor`. Fixed-step methods can be differentiated by JAX; the built-in
|
|
130
134
|
adaptive controller is eager Python control flow. See the
|
|
131
135
|
[backend guide](docs/guides/backends.md) for the precise contract and the
|
|
132
136
|
[solver guide](docs/guides/solver-methods.md) for method-specific examples.
|
|
@@ -75,9 +75,10 @@ solution = core.state_array # shape (n_timesteps, state, subgroup)
|
|
|
75
75
|
### Array namespaces
|
|
76
76
|
|
|
77
77
|
`ModelCore` infers its numerical namespace from the initial state through
|
|
78
|
-
`
|
|
78
|
+
`array_api_compat.array_namespace()`. The state, stored history, solver stages,
|
|
79
79
|
and dense solver operations stay in that namespace. There is no `xp=` or
|
|
80
|
-
backend option.
|
|
80
|
+
backend option. This supports standard Array-API objects and native arrays such
|
|
81
|
+
as `torch.Tensor`. Fixed-step methods can be differentiated by JAX; the built-in
|
|
81
82
|
adaptive controller is eager Python control flow. See the
|
|
82
83
|
[backend guide](docs/guides/backends.md) for the precise contract and the
|
|
83
84
|
[solver guide](docs/guides/solver-methods.md) for method-specific examples.
|
|
@@ -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>
|
|
Binary file
|
|
@@ -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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Trait-structured chemostat PDE
|
|
2
|
+
|
|
3
|
+
The
|
|
4
|
+
[`chemostat_pde.py`](https://github.com/ACCIDDA/op_engine/blob/main/examples/chemostat_pde.py)
|
|
5
|
+
example couples a population density over a continuous trait coordinate to one
|
|
6
|
+
well-mixed resource. It demonstrates non-uniform finite-volume geometry,
|
|
7
|
+
implicit diffusion, nonlinear explicit coupling, and structured observables in
|
|
8
|
+
one reproducible comparison.
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Model and split
|
|
13
|
+
|
|
14
|
+
For population density `n(theta, t)` and resource `S(t)`, the example solves
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
dn/dt = n g(theta, S) - D n + d d2n/dtheta2
|
|
18
|
+
dS/dt = D (S_in - S) - integral[n g(theta, S) w(theta) dtheta].
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Growth combines Monod resource limitation with a Gaussian trait optimum. The
|
|
22
|
+
positive uptake weight varies exponentially with trait and is scaled by the
|
|
23
|
+
yield coefficient.
|
|
24
|
+
|
|
25
|
+
The split supplied to `CoreSolver` is:
|
|
26
|
+
|
|
27
|
+
| Partition | Terms |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| implicit linear | no-flux trait diffusion |
|
|
30
|
+
| explicit nonlinear | growth, dilution, resource inflow, and consumption |
|
|
31
|
+
|
|
32
|
+
`imex-heun-tr` treats the two partitions to second order. The implicit stage is
|
|
33
|
+
assembled additively as `L y = R y_n + explicit_increment`; the right
|
|
34
|
+
trapezoidal operator does not multiply the explicit increment.
|
|
35
|
+
|
|
36
|
+
The example does not clip stage states or project the result onto the positive
|
|
37
|
+
orthant. Positivity in the canonical run is an observed numerical diagnostic,
|
|
38
|
+
not an extra model operation.
|
|
39
|
+
|
|
40
|
+
## Non-uniform finite volumes
|
|
41
|
+
|
|
42
|
+
`build_trait_grid()` calls `generate_adaptive_grid()` on the curvature of the
|
|
43
|
+
initial Gaussian trait profile. More centers are placed where the initial
|
|
44
|
+
profile bends rapidly, while the domain endpoints remain fixed.
|
|
45
|
+
|
|
46
|
+
For adjacent center spacings `h_i`, the integration weights are
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
q_0 = h_0
|
|
50
|
+
q_i = (h_(i-1) + h_i) / 2
|
|
51
|
+
q_(N-1) = h_(N-2).
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
These are the same control-volume widths used by the non-uniform diffusion
|
|
55
|
+
operator. With reflecting boundaries, `q^T A = 0`, so diffusion alone
|
|
56
|
+
conserves total biomass.
|
|
57
|
+
|
|
58
|
+
## Structured observables
|
|
59
|
+
|
|
60
|
+
At every output time the example records resource and five summaries of the
|
|
61
|
+
trait distribution:
|
|
62
|
+
|
|
63
|
+
| Observable | Discrete definition |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| biomass | `I = sum_i q_i n_i` |
|
|
66
|
+
| mean trait | `rho = sum_i q_i n_i theta_i / I` |
|
|
67
|
+
| variance | `Sigma = sum_i q_i n_i (theta_i-rho)^2 / I` |
|
|
68
|
+
| skewness | third central moment divided by `Sigma^(3/2)` |
|
|
69
|
+
| kurtosis | fourth central moment divided by `Sigma^2` |
|
|
70
|
+
|
|
71
|
+
Kurtosis is the raw standardized fourth moment, not excess kurtosis.
|
|
72
|
+
|
|
73
|
+
## Run and interpret the comparison
|
|
74
|
+
|
|
75
|
+
From a development checkout:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv sync --dev
|
|
79
|
+
uv run python examples/chemostat_pde.py
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The command writes
|
|
83
|
+
`examples/output/chemostat_pde/chemostat_summary.png` and reports maximum state
|
|
84
|
+
and biomass differences. The committed reference figure below uses the default
|
|
85
|
+
41-cell grid and 0.02-day time step.
|
|
86
|
+
|
|
87
|
+

|
|
88
|
+
|
|
89
|
+
The panels compare initial and final trait densities, resource and total
|
|
90
|
+
biomass, and the evolving mean trait with one standard deviation. Dashed lines
|
|
91
|
+
come from SciPy RK45 applied to the same semi-discrete system with tight
|
|
92
|
+
tolerances. This comparison isolates time integration: RK45 uses the identical
|
|
93
|
+
grid, quadrature, reaction functions, diffusion matrix, and initial state.
|
|
94
|
+
|
|
95
|
+
The RK45 trajectory is a high-accuracy reference for this finite-dimensional
|
|
96
|
+
discretization, not an exact solution of the continuous PDE. Grid convergence
|
|
97
|
+
and time-step convergence remain separate checks.
|
|
@@ -21,22 +21,28 @@ CoreSolver(core).run(decay)
|
|
|
21
21
|
solution = core.state_array
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Array-backend solves
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
`array-api-compat` discovers the numerical namespace from NumPy, JAX, CuPy,
|
|
27
|
+
PyTorch, and other supported native arrays. Passing an array to
|
|
27
28
|
`set_initial_state` selects the namespace for state storage, history, solver
|
|
28
|
-
stages, and adaptive error control. No backend argument is needed. An RHS
|
|
29
|
-
derive operations from its state when it needs namespace functions:
|
|
29
|
+
stages, and adaptive error control. No backend argument is needed. An RHS
|
|
30
|
+
should derive operations from its state when it needs namespace functions:
|
|
30
31
|
|
|
31
32
|
```python
|
|
33
|
+
from op_engine import array_namespace
|
|
34
|
+
|
|
35
|
+
|
|
32
36
|
def portable_decay(_time, state):
|
|
33
|
-
xp = state
|
|
37
|
+
xp = array_namespace(state)
|
|
34
38
|
return xp.multiply(state, -0.25)
|
|
35
39
|
```
|
|
36
40
|
|
|
37
41
|
Dense implicit and IMEX operators are converted into the state's namespace and
|
|
38
42
|
solved with its Array-API `linalg.solve` implementation. This provides the
|
|
39
|
-
portable correctness path for NumPy, JAX, and other
|
|
43
|
+
portable correctness path for NumPy, JAX, PyTorch, and other supported
|
|
44
|
+
backends. Array portability alone does not promise backend compilation,
|
|
45
|
+
automatic differentiation, sparse support, or random-number semantics.
|
|
40
46
|
|
|
41
47
|
Sparse acceleration is an optional second tier. SciPy sparse operators retain
|
|
42
48
|
cached factorization for NumPy state. Installing `op-engine[cupy]` registers the
|
|
@@ -236,8 +236,22 @@ CoreSolver(core).run(rhs, config=config)
|
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
With `adaptive=False`, every adjacent pair of output times defines one solver
|
|
239
|
-
step.
|
|
240
|
-
|
|
239
|
+
step by default. Set `fixed_max_step` to bound explicit Euler, Heun, RK4, or
|
|
240
|
+
Dormand--Prince integration steps independently of the output grid:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
config = RunConfig(method="rk4", fixed_max_step=0.25)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Each output interval then uses as many full `fixed_max_step` steps as fit plus
|
|
247
|
+
a final remainder step that lands exactly on the next output time. Only the
|
|
248
|
+
requested output states are stored. Output grids aligned with the fixed-step
|
|
249
|
+
mesh can therefore be made more or less frequent without changing states at
|
|
250
|
+
shared times. `fixed_max_step` is incompatible with `adaptive=True`; adaptive
|
|
251
|
+
methods use their controller configuration instead.
|
|
252
|
+
|
|
253
|
+
With `adaptive=True`, the controller may take multiple internal steps but still
|
|
254
|
+
stores only the requested output times.
|
|
241
255
|
|
|
242
256
|
Heun, RK4, and Dormand--Prince share one validated explicit Runge--Kutta
|
|
243
257
|
tableau kernel. `rk4` takes four RHS stages per fixed step. Under adaptivity it
|
|
@@ -310,6 +324,46 @@ config = RunConfig(
|
|
|
310
324
|
CoreSolver(core, operator_axis="state").run(reaction, config=config)
|
|
311
325
|
```
|
|
312
326
|
|
|
327
|
+
For a non-uniform axis, pass strictly increasing cell-center coordinates and
|
|
328
|
+
set `dx=None`:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
centers = np.asarray([0.0, 0.08, 0.21, 0.5, 1.0])
|
|
332
|
+
operator = build_diffusion_matrix(
|
|
333
|
+
centers.size,
|
|
334
|
+
None,
|
|
335
|
+
diffusivity,
|
|
336
|
+
grid=centers,
|
|
337
|
+
bc="neumann",
|
|
338
|
+
)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The no-flux operator is conservative and self-adjoint under the inferred
|
|
342
|
+
cell-volume weights. It is generally not symmetric in the unweighted Euclidean
|
|
343
|
+
inner product. Periodic non-uniform grids need an explicit wrap spacing, which
|
|
344
|
+
cannot be inferred from center coordinates alone, and are rejected.
|
|
345
|
+
|
|
346
|
+
Use `generate_adaptive_grid` to create a curvature-weighted point grid from a
|
|
347
|
+
vectorized profile callable:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
from op_engine import generate_adaptive_grid
|
|
351
|
+
|
|
352
|
+
centers = generate_adaptive_grid(
|
|
353
|
+
lambda x: np.exp(-10.0 * x * x),
|
|
354
|
+
(-1.0, 1.0),
|
|
355
|
+
65,
|
|
356
|
+
smoothing_window=5,
|
|
357
|
+
minimum_spacing=1e-3,
|
|
358
|
+
)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The generator returns exactly the requested number of NumPy coordinates,
|
|
362
|
+
including both interval endpoints. It is eager preprocessing rather than part
|
|
363
|
+
of the differentiable solve; the resulting static geometry can drive NumPy,
|
|
364
|
+
JAX, Torch, or another supported Array-API backend. The sampled-data companion
|
|
365
|
+
is `generate_adaptive_grid_from_data`.
|
|
366
|
+
|
|
313
367
|
For `imex-trbdf2`, provide both stage schemes:
|
|
314
368
|
|
|
315
369
|
```python
|
|
@@ -348,10 +402,11 @@ across Array-API namespaces.
|
|
|
348
402
|
|
|
349
403
|
`imex-ars443` and `ars443` are aliases for `imex-ark3`.
|
|
350
404
|
|
|
351
|
-
The flepimop2 provider can compile typed `op_system` `axis_kernel`,
|
|
352
|
-
and `diffusion` descriptors into these same
|
|
353
|
-
axis-
|
|
354
|
-
|
|
405
|
+
The flepimop2 provider can compile typed `op_system` `axis_kernel`,
|
|
406
|
+
`jump_integral`, `advection`, and `diffusion` descriptors into these same
|
|
407
|
+
factories. Descriptor parsing, axis-type/coordinate resolution, and layout
|
|
408
|
+
lifting belong to the provider; portable jump assembly follows op_system's
|
|
409
|
+
public reference semantics, while stepping methods remain in `op_engine`.
|
|
355
410
|
|
|
356
411
|
## Linearly implicit methods
|
|
357
412
|
|
|
@@ -360,8 +415,11 @@ well as the Rosenbrock method, take a Jacobian callable. The callable returns an
|
|
|
360
415
|
operator acting along `operator_axis`:
|
|
361
416
|
|
|
362
417
|
```python
|
|
418
|
+
from op_engine import array_namespace
|
|
419
|
+
|
|
420
|
+
|
|
363
421
|
def jacobian(_time, state):
|
|
364
|
-
xp = state
|
|
422
|
+
xp = array_namespace(state)
|
|
365
423
|
return xp.multiply(xp.eye(state.shape[0], dtype=state.dtype), -0.5)
|
|
366
424
|
|
|
367
425
|
|
|
@@ -7,6 +7,9 @@ Start with [Getting Started](guides/getting-started.md), then use
|
|
|
7
7
|
explicit, IMEX, or linearly implicit method. The
|
|
8
8
|
[biogeochemical network tutorial](guides/biogeochemical-network.md) shows
|
|
9
9
|
stage-dependent operator splitting in a complete example.
|
|
10
|
+
The [trait-structured chemostat tutorial](guides/chemostat-pde.md) combines a
|
|
11
|
+
non-uniform finite-volume grid, implicit diffusion, nonlinear resource
|
|
12
|
+
coupling, and structured trait observables.
|
|
10
13
|
|
|
11
14
|
`op_engine` numerical methods use the array namespace supplied by model state.
|
|
12
15
|
The [backend guide](guides/backends.md) explains portability, JAX
|