fast-numerical-integration 0.2.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 (53) hide show
  1. fast_numerical_integration-0.2.0/LICENSE +21 -0
  2. fast_numerical_integration-0.2.0/MANIFEST.in +4 -0
  3. fast_numerical_integration-0.2.0/PKG-INFO +380 -0
  4. fast_numerical_integration-0.2.0/README.md +348 -0
  5. fast_numerical_integration-0.2.0/pyproject.toml +88 -0
  6. fast_numerical_integration-0.2.0/setup.cfg +4 -0
  7. fast_numerical_integration-0.2.0/setup.py +82 -0
  8. fast_numerical_integration-0.2.0/src/fast_numerical_integration/__init__.py +0 -0
  9. fast_numerical_integration-0.2.0/src/fast_numerical_integration/batch.pyx +141 -0
  10. fast_numerical_integration-0.2.0/src/fast_numerical_integration/benchmark.py +224 -0
  11. fast_numerical_integration-0.2.0/src/fast_numerical_integration/integrators.pxd +211 -0
  12. fast_numerical_integration-0.2.0/src/fast_numerical_integration/integrators.pyx +2053 -0
  13. fast_numerical_integration-0.2.0/src/fast_numerical_integration/main.py +63 -0
  14. fast_numerical_integration-0.2.0/src/fast_numerical_integration/nbody.pyx +110 -0
  15. fast_numerical_integration-0.2.0/src/fast_numerical_integration/problems/__init__.py +0 -0
  16. fast_numerical_integration-0.2.0/src/fast_numerical_integration/problems/catalog.py +786 -0
  17. fast_numerical_integration-0.2.0/src/fast_numerical_integration/problems/events.py +170 -0
  18. fast_numerical_integration-0.2.0/src/fast_numerical_integration/problems/native.pyx +548 -0
  19. fast_numerical_integration-0.2.0/src/fast_numerical_integration/suite.py +594 -0
  20. fast_numerical_integration-0.2.0/src/fast_numerical_integration/tableaus.py +253 -0
  21. fast_numerical_integration-0.2.0/src/fast_numerical_integration/testing.py +109 -0
  22. fast_numerical_integration-0.2.0/src/fast_numerical_integration/three_body.py +155 -0
  23. fast_numerical_integration-0.2.0/src/fast_numerical_integration/visualize.py +64 -0
  24. fast_numerical_integration-0.2.0/src/fast_numerical_integration.egg-info/PKG-INFO +380 -0
  25. fast_numerical_integration-0.2.0/src/fast_numerical_integration.egg-info/SOURCES.txt +51 -0
  26. fast_numerical_integration-0.2.0/src/fast_numerical_integration.egg-info/dependency_links.txt +1 -0
  27. fast_numerical_integration-0.2.0/src/fast_numerical_integration.egg-info/entry_points.txt +4 -0
  28. fast_numerical_integration-0.2.0/src/fast_numerical_integration.egg-info/requires.txt +5 -0
  29. fast_numerical_integration-0.2.0/src/fast_numerical_integration.egg-info/top_level.txt +1 -0
  30. fast_numerical_integration-0.2.0/tests/accuracy/test_convergence_order.py +77 -0
  31. fast_numerical_integration-0.2.0/tests/accuracy/test_detest.py +85 -0
  32. fast_numerical_integration-0.2.0/tests/accuracy/test_hairer.py +60 -0
  33. fast_numerical_integration-0.2.0/tests/accuracy/test_references.py +62 -0
  34. fast_numerical_integration-0.2.0/tests/baseline/test_benchmark.py +54 -0
  35. fast_numerical_integration-0.2.0/tests/baseline/test_suite.py +13 -0
  36. fast_numerical_integration-0.2.0/tests/baseline/test_three_body.py +67 -0
  37. fast_numerical_integration-0.2.0/tests/c_api/test_integrate.py +223 -0
  38. fast_numerical_integration-0.2.0/tests/c_api/test_params.py +105 -0
  39. fast_numerical_integration-0.2.0/tests/conftest.py +31 -0
  40. fast_numerical_integration-0.2.0/tests/events/test_edge_cases.py +247 -0
  41. fast_numerical_integration-0.2.0/tests/events/test_physical_events.py +162 -0
  42. fast_numerical_integration-0.2.0/tests/long_time/test_invariants.py +56 -0
  43. fast_numerical_integration-0.2.0/tests/long_time/test_kepler.py +86 -0
  44. fast_numerical_integration-0.2.0/tests/long_time/test_periodic_orbits.py +53 -0
  45. fast_numerical_integration-0.2.0/tests/long_time/test_solar_system.py +80 -0
  46. fast_numerical_integration-0.2.0/tests/parity/test_features.py +384 -0
  47. fast_numerical_integration-0.2.0/tests/parity/test_problem_parity.py +115 -0
  48. fast_numerical_integration-0.2.0/tests/parity/test_scipy_port.py +830 -0
  49. fast_numerical_integration-0.2.0/tests/parity/test_stepping.py +172 -0
  50. fast_numerical_integration-0.2.0/tests/stability/test_blowup.py +65 -0
  51. fast_numerical_integration-0.2.0/tests/stability/test_chaos.py +53 -0
  52. fast_numerical_integration-0.2.0/tests/stability/test_linear_stability.py +97 -0
  53. fast_numerical_integration-0.2.0/tests/stability/test_stiff.py +123 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ian Mackey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ # Generated C is rebuilt from the .pyx sources at build time; never ship stale copies.
2
+ global-exclude *.c *.so *.pyd
3
+ recursive-include tests *.py
4
+ include LICENSE README.md
@@ -0,0 +1,380 @@
1
+ Metadata-Version: 2.4
2
+ Name: fast-numerical-integration
3
+ Version: 0.2.0
4
+ Summary: Fast Cython RK23/RK45/DOP853 integrators that reproduce scipy.integrate.solve_ivp step for step, with a GIL-free C API
5
+ Author: Ian Mackey
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/idm23/fast-numerical-integration
8
+ Project-URL: Repository, https://github.com/idm23/fast-numerical-integration
9
+ Project-URL: Issues, https://github.com/idm23/fast-numerical-integration/issues
10
+ Keywords: ode,numerical-integration,runge-kutta,dop853,cython,solve_ivp,scipy
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Cython
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
23
+ Classifier: Topic :: Scientific/Engineering :: Physics
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy>=2.0
28
+ Requires-Dist: scipy>=1.13
29
+ Provides-Extra: plot
30
+ Requires-Dist: matplotlib>=3.8; extra == "plot"
31
+ Dynamic: license-file
32
+
33
+ # fast-numerical-integration
34
+
35
+ A bridge for **fast numerical integration** of differential equations in Python.
36
+
37
+ The core idea: author a system of ODEs in a computer algebra system (SymPy or
38
+ similar), **code-print that system to C**, compile it and **bridge it into Python
39
+ with Cython**, then integrate it with hand-written numerical integrators
40
+ (DOP853, Runge-Kutta families, ...). The goal is to be **considerably faster than
41
+ `scipy.integrate.solve_ivp`** while producing reusable, composable tooling that
42
+ other projects can drop in.
43
+
44
+ ## Why
45
+
46
+ `solve_ivp` is flexible and battle-tested, but every right-hand-side evaluation
47
+ crosses back into Python, which dominates the cost for stiff or long-horizon
48
+ problems. By generating a compiled C RHS and driving it from a Cython integration
49
+ loop, we keep the hot path entirely in native code — no per-step Python overhead —
50
+ while still authoring the physics symbolically.
51
+
52
+ ## Architecture (target)
53
+
54
+ ```
55
+ SymPy / CAS Code printing Cython bridge Integrators
56
+ ┌────────────┐ -> ┌──────────────┐ -> ┌───────────────┐ -> ┌──────────────┐
57
+ │ dy/dt = │ │ generated │ │ compiled .so │ │ DOP853 / RK │
58
+ │ f(t, y) │ │ C kernel │ │ + Python API │ │ step loop │
59
+ └────────────┘ └──────────────┘ └───────────────┘ └──────────────┘
60
+ ```
61
+
62
+ Each stage is meant to be reusable on its own: the code printer, the Cython
63
+ build glue, and the integrators are independent tools.
64
+
65
+ ## Current status
66
+
67
+ **Milestone 1 — SciPy baseline (done).** A `main` entry point solves the classic
68
+ [figure-eight three-body choreography](https://en.wikipedia.org/wiki/Three-body_problem#Special-case_solutions)
69
+ with `scipy.integrate.solve_ivp` (DOP853) and saves a static trajectory plot.
70
+ This gives us a **correct reference solution** and a **timing target** for the
71
+ custom engine that follows.
72
+
73
+ **Milestone 3 — Cython integrators (done).** `RK23`, `RK45`, and `DOP853` are
74
+ reimplemented in Cython as a step-for-step copy of SciPy's `RungeKutta`
75
+ solvers. They use the same tableaus, initial-step heuristic, error norms, and
76
+ step-size controller, so they accept the same steps and make the same number of
77
+ RHS evaluations as `solve_ivp`. State-sized work is done in typed-memoryview loops
78
+ over buffers that cnumpy allocates once per solve. The step loop makes no NumPy
79
+ calls and creates no temporary arrays. Right-hand sides are `cdef` subclasses of
80
+ `OdeSystem`, and the integrator calls them through the vtable. A Python callable
81
+ also works through an adapter, which is slower.
82
+
83
+ **Milestone 3b — the rest of `solve_ivp` (done).** `t_eval`, `dense_output`
84
+ (an `OdeSolution`), `events` (`terminal` as a bool or a count, plus
85
+ `direction`), `vectorized`, `args`, per-component `rtol`/`atol`, `first_step`,
86
+ `max_step`, and SciPy's validation and warnings are all supported. Output
87
+ points, interpolants, and event times and states match SciPy to round-off,
88
+ with identical RHS evaluation counts. That includes DOP853's 3 extra
89
+ dense-output stages, which are evaluated only when an interpolant is needed.
90
+ Event roots are found with a C port of SciPy's `brentq`. Events can be native
91
+ `EventFunction` subclasses, such as the built-in `ComponentCrossing`, so a fully
92
+ compiled problem never calls into Python. The stepper classes mirror
93
+ `scipy.integrate.OdeSolver` (`step()`, `dense_output()`, `t_old`, `step_size`).
94
+ Not supported: complex-valued `y0`.
95
+
96
+ Package layout (`src/` layout):
97
+
98
+ ```
99
+ src/fast_numerical_integration/
100
+ integrators.pyx/.pxd # GIL-free C core (fni_*), OdeSystem/EventFunction, steppers, solve_ivp, integrate
101
+ batch.pyx # integrate_batch: many segments in one nogil loop (C API example)
102
+ nbody.pyx # NBodyGravity: native N-body RHS (stand-in for generated C)
103
+ tableaus.py # Butcher tableaus, bit-identical to SciPy's
104
+ three_body.py # figure-eight ICs, three_body_rhs, ThreeBodySystem(.native_rhs())
105
+ visualize.py # plot_trajectories(solution, out_path) -> PNG
106
+ main.py # main(): solve with solve_ivp, print stats, write outputs/figure_eight.png
107
+ benchmark.py # timing harness: scipy vs cython (Python RHS) vs cython (native RHS)
108
+ suite.py # fni-suite: benchmark + figures for every catalog problem and event scenario
109
+ testing.py # run SciPy with our summation order (bit-identical comparisons)
110
+ problems/
111
+ catalog.py # 41 canonical test problems: RHS, exact/published references, invariants
112
+ native.pyx # native (Cython) RHS and event classes for every catalog problem
113
+ events.py # event scenarios with exact event times (apsides, pendulum, bouncing ball)
114
+ setup.py # Cython extension build (metadata stays in pyproject.toml)
115
+ ```
116
+
117
+ **Milestone 3c — canonical test suite (done).** A catalog of 41 standard
118
+ problems, each with a NumPy RHS and an equivalent native one:
119
+
120
+ - **DETEST** A1–E5, with the published reference end values (Hull et al. 1972;
121
+ Enright & Pryce 1987).
122
+ - **Hairer–Nørsett–Wanner:** Arenstorf, Pleiades, Brusselator.
123
+ - **Long-time:** Kepler over 100–1000 orbits, the outer solar system over
124
+ 2×10⁵ days, the figure-eight choreography over 50 periods.
125
+ - **Stability:** Prothero–Robinson, Van der Pol for μ = 1…1000, Robertson,
126
+ Lorenz, finite-time blow-up.
127
+ - **Event scenarios:** Kepler apsides, a large-amplitude pendulum, a bouncing
128
+ ball with restarts.
129
+
130
+ The tests are grouped by the oracle they check against:
131
+
132
+ | directory | checks |
133
+ |---|---|
134
+ | `tests/parity/` | SciPy's own `solve_ivp` tests, ported; RHS-evaluation parity on every catalog problem; **bit-identical** trajectories against SciPy patched to our summation order |
135
+ | `tests/accuracy/` | DETEST global error vs tolerance; observed order 3/5/8 with fixed steps; dense-output order; references cross-checked against Radau and LSODA |
136
+ | `tests/long_time/` | Kepler: linear invariant drift and quadratic phase-error growth; two independent solar-system formulations agree; linear invariants exact to round-off |
137
+ | `tests/stability/` | step size rides the stability boundary computed from each tableau (2.51 / 3.31 / 6.39); stiffness scaling; blow-up; Lorenz error growth at the Lyapunov rate |
138
+ | `tests/events/` | exact event times on physical problems; edge cases (t0, t_end, simultaneous/terminal ordering, root pairs and grazing touches missed exactly as in SciPy) |
139
+ | `tests/baseline/` | SciPy baseline, benchmark harness, suite smoke test |
140
+
141
+ Findings worth knowing:
142
+
143
+ - RK23 and RK45 match SciPy's RHS-evaluation count on every problem. DOP853
144
+ can differ by a few percent: its error estimate is a heavily cancelling
145
+ sum, so near round-off level summation order flips accept/reject decisions.
146
+ SciPy shows the same sensitivity to a 1-ulp change in `y0`. With SciPy
147
+ patched to our summation order (`fast_numerical_integration.testing`),
148
+ every trajectory is bit-identical.
149
+ - RK45 at rtol=1e-3 goes unstable on Robertson (|y| ~ 1e7, then fails). This
150
+ is a known Dormand–Prince stiffness pathology, and SciPy does the same.
151
+
152
+ ### Using the Cython engine
153
+
154
+ `integrators.solve_ivp` takes the same arguments as `scipy.integrate.solve_ivp`
155
+ (default method `RK45`):
156
+
157
+ ```python
158
+ import numpy as np
159
+ from fast_numerical_integration.integrators import ComponentCrossing, solve_ivp
160
+ from fast_numerical_integration.three_body import figure_eight_system
161
+
162
+ system = figure_eight_system()
163
+ sol = solve_ivp(
164
+ system.native_rhs(), (0.0, system.period), system.y0, method="DOP853",
165
+ t_eval=np.linspace(0.0, system.period, 2000), dense_output=True,
166
+ events=[ComponentCrossing(1, direction=1)], # y1 crosses 0 upward
167
+ rtol=1e-10, atol=1e-12,
168
+ )
169
+ sol.t, sol.y # solution at t_eval
170
+ sol.sol(1.234) # dense interpolant anywhere in the interval
171
+ sol.t_events, sol.y_events
172
+ ```
173
+
174
+ Plain Python RHS and event callables also work, with SciPy's `terminal` and
175
+ `direction` attributes. They are slower because every call goes back through
176
+ Python.
177
+
178
+ ### Native systems, runtime parameters and the C API
179
+
180
+ A native RHS is a GIL-free C function plus a context pointer. To write one,
181
+ subclass `OdeSystem` (or `EventFunction` for events) in a `.pyx` file. In
182
+ `__init__`, register the C function with `c_fn` / `c_ctx` and declare the
183
+ runtime parameters:
184
+
185
+ ```cython
186
+ from fast_numerical_integration.integrators cimport OdeSystem
187
+
188
+ cdef int _vdp(double t, const double* y, double* dydt, Py_ssize_t n, void* ctx) noexcept nogil:
189
+ cdef const double* p = <const double*> ctx # p[0] = mu
190
+ dydt[0] = y[1]
191
+ dydt[1] = p[0] * (1.0 - y[0] * y[0]) * y[1] - y[0]
192
+ return 0
193
+
194
+ cdef class VanDerPol(OdeSystem):
195
+ def __init__(self, double mu=1.0):
196
+ self.n_states = 2
197
+ self._init_params([mu], ("mu",))
198
+ self.c_fn = _vdp
199
+ self.c_ctx = self._params_ptr()
200
+ ```
201
+
202
+ Parameters change between solves without recompiling, which is what
203
+ continuation needs:
204
+
205
+ ```python
206
+ vdp = VanDerPol()
207
+ for mu in (1.0, 10.0, 100.0):
208
+ vdp.params = [mu]
209
+ sol = solve_ivp(vdp, (0, 20), [2.0, 0.0], method="DOP853")
210
+ ```
211
+
212
+ (Overriding `cdef eval` still works, but such systems need the GIL.)
213
+
214
+ All three entry points run on one C core, so they take bit-identical steps:
215
+
216
+ | entry point | what it does |
217
+ |---|---|
218
+ | `solve_ivp(...)` | full SciPy-compatible API: trajectory, `t_eval`, dense output, events |
219
+ | `integrate(...)` | endpoint + event locations only (shooting). Releases the GIL when the system and events are native, so Python threads run in parallel |
220
+ | `fni_integrate(...)` (C) | `cimport`able from other Cython modules and callable inside `with nogil`. `batch.integrate_batch` is the reference example: it propagates many segments at ~1 µs per segment |
221
+
222
+ ```cython
223
+ from fast_numerical_integration.integrators cimport (
224
+ fni_integrate, fni_options, fni_stats, FNI_DOP853, FNI_OK)
225
+
226
+ with nogil:
227
+ rc = fni_integrate(FNI_DOP853, rhs_fn, ctx, n, t0, y0, t1, &opt,
228
+ events, n_events, y_out, rec_event, rec_t, rec_y, max_records, &stats)
229
+ ```
230
+
231
+ The lower-level `fni_solver_init` / `fni_solver_step` / `fni_solver_dense_eval` /
232
+ `fni_events_check` functions expose single steps for custom drivers. All
233
+ solver state lives in a caller-owned struct, so concurrent integrations are
234
+ safe if the RHS kernels are reentrant. Every catalog kernel in
235
+ `problems/native.pyx` is written that way.
236
+
237
+ ## Installation
238
+
239
+ ```bash
240
+ pip install fast-numerical-integration # or: uv add fast-numerical-integration
241
+ pip install "fast-numerical-integration[plot]" # + matplotlib, for fni / fni-suite / visualize
242
+ ```
243
+
244
+ Pre-built wheels cover Linux x86_64 and macOS on Apple Silicon for CPython
245
+ 3.11–3.14, and every wheel passes the full test suite before it's
246
+ published. Other platforms install from the source distribution, which needs
247
+ a C compiler. All extensions are built with floating-point contraction off,
248
+ so results are identical across platforms.
249
+
250
+ To `cimport` the C API from your own Cython package, add
251
+ `fast-numerical-integration` and `cython` to your `[build-system] requires`.
252
+ `examples/downstream/` is a minimal working package; the release workflow
253
+ builds it against every release.
254
+
255
+ ## Quickstart (development)
256
+
257
+ Requires [`uv`](https://docs.astral.sh/uv/).
258
+
259
+ ```bash
260
+ uv sync # create the venv, install deps, compile Cython
261
+ uv run fni # or: uv run python -m fast_numerical_integration.main
262
+ ```
263
+
264
+ This writes `outputs/figure_eight.png` and prints solver stats, e.g.:
265
+
266
+ ```
267
+ System: figure-eight
268
+ Method: DOP853 (scipy solve_ivp)
269
+ Steps taken: 2000 sampled, 1454 RHS evals
270
+ Wall time: 22.30 ms
271
+ Energy: E0=-1.287142e+00 ET=-1.287142e+00 |drift|=2.61e-10
272
+ Saved plot -> outputs/figure_eight.png
273
+ ```
274
+
275
+ The tiny energy drift over one period confirms the dynamics and initial
276
+ conditions are correct.
277
+
278
+ The Cython extensions build when the package installs. `uv` tracks the `.pyx` and
279
+ `.pxd` files as cache keys, so `uv sync` or `uv run` rebuilds them after an
280
+ edit. Building needs a C compiler.
281
+
282
+ ### Benchmark
283
+
284
+ ```bash
285
+ uv run fni-bench # or: uv run python -m fast_numerical_integration.benchmark
286
+ ```
287
+
288
+ Times `RK23`, `RK45`, and `DOP853` on the figure-eight system (one period,
289
+ rtol=1e-10, atol=1e-12) with three engines:
290
+
291
+ - `scipy`: `scipy.integrate.solve_ivp`.
292
+ - `cython+pyrhs`: the Cython integrators calling the NumPy RHS through Python.
293
+ The RHS cost matches SciPy's, so this measures integrator overhead alone.
294
+ - `cython`: the Cython integrators with the native `NBodyGravity` RHS.
295
+
296
+ Representative run:
297
+
298
+ ```
299
+ solver best (ms) mean±sd (ms) nfev eval/ms E drift period err
300
+ ---------------------------------------------------------------------------------------------
301
+ scipy RK23 626.758 636.353±10.984 43064 68.7 7.66e-10 6.93e-08
302
+ cython+pyrhs RK23 432.419 435.549±2.256 43064 99.6 7.67e-10 6.93e-08
303
+ cython RK23 3.747 3.966±0.299 43064 11494.1 7.67e-10 6.93e-08
304
+ scipy RK45 39.150 39.270±0.093 2936 75.0 2.98e-10 7.78e-08
305
+ cython+pyrhs RK45 29.834 31.064±1.231 2936 98.4 2.98e-10 7.78e-08
306
+ cython RK45 0.201 0.207±0.005 2936 14633.5 2.98e-10 7.78e-08
307
+ scipy DOP853 15.282 15.353±0.060 1202 78.7 2.61e-10 7.29e-08
308
+ cython+pyrhs DOP853 12.178 12.234±0.037 1202 98.7 2.61e-10 7.29e-08
309
+ cython DOP853 0.086 0.091±0.003 1202 13906.7 2.61e-10 7.29e-08
310
+ ```
311
+
312
+ The Cython engine makes exactly the same number of RHS evaluations as SciPy and
313
+ reaches the same accuracy. With a native RHS, DOP853 runs about **175x faster**
314
+ than `solve_ivp`. With the Python RHS the gain is only ~1.3x, so most of SciPy's
315
+ time is spent calling the Python RHS. That is why the remaining milestones
316
+ generate compiled kernels. Numbers vary by machine.
317
+
318
+ ### Benchmark and visualize every problem
319
+
320
+ ```bash
321
+ uv run fni-suite # all problems + event scenarios, ~2 minutes
322
+ uv run fni-suite --problems D5 pleiades --quick
323
+ ```
324
+
325
+ This writes one figure per problem to `outputs/suite/<group>/<name>.png`. Each
326
+ figure shows the solution, error along the trajectory (or conserved-quantity
327
+ drift, or step size), a DOP853 work-precision curve and wall-time bars for
328
+ SciPy and Cython. It also writes one figure per event scenario and
329
+ `summary_speedup.png`, plus `summary.md` / `summary.csv` tables. Cython's
330
+ end-to-end speedup with DOP853 ranges from about 6× (C4, a dense 51×51 linear
331
+ system, where the work is the same matrix-vector product as NumPy's) to about
332
+ 500× (outer solar system), with identical step counts and accuracy.
333
+
334
+ Run the tests:
335
+
336
+ ```bash
337
+ uv run pytest # ~1.5 minutes
338
+ uv run pytest --runslow # include long integrations
339
+ uv run pytest tests/events # one oracle group
340
+ ```
341
+
342
+ CI (`.github/workflows/tests.yml`) runs the full suite, including
343
+ `--runslow`, on Python 3.11, 3.12 and 3.13 for every pull request and push
344
+ to `main`. The `all-tests` check aggregates the matrix and is the status to
345
+ require before merging.
346
+
347
+ ## Roadmap
348
+
349
+ - [x] **Milestone 1** — SciPy `solve_ivp` figure-eight baseline + visualization.
350
+ - [x] **Milestone 2** — Benchmark harness timing `solve_ivp` across `RK23`,
351
+ `RK45`, and `DOP853` on the same system (baseline numbers to beat).
352
+ - [x] **Milestone 3** — Cython RK23 / RK45 / DOP853 with adaptive steps, matching
353
+ SciPy step for step; native `OdeSystem` RHS interface.
354
+ - [x] **Milestone 3b** — Dense output, `t_eval`, events, and the rest of
355
+ SciPy's `solve_ivp` options for these methods.
356
+ - [x] **Milestone 3c** — Canonical test problems (DETEST, Hairer, long-time,
357
+ stability, events), split by oracle, with a benchmark/figure suite.
358
+ - [x] **Milestone 3d** — GIL-free C core with a `cimport`able C API
359
+ (`fni_integrate`, single-step functions), runtime `params` on native
360
+ systems and events, `integrate()` endpoint API, `integrate_batch`.
361
+ - [ ] **Milestone 4** — SymPy → C code printing for the RHS.
362
+ - [ ] **Milestone 5** — Build glue that compiles the generated C into an
363
+ `OdeSystem` subclass; benchmark against the baseline.
364
+
365
+ ## Releasing
366
+
367
+ Releases are published from GitHub Actions (`.github/workflows/release.yml`)
368
+ with PyPI trusted publishing, so no tokens are stored.
369
+
370
+ 1. Bump `version` in `pyproject.toml` and merge the change to `main`.
371
+ 2. Optional rehearsal: run the *Release* workflow manually with
372
+ `target: testpypi`, then `pip install -i https://test.pypi.org/simple/
373
+ --extra-index-url https://pypi.org/simple/ fast-numerical-integration`.
374
+ 3. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z`. The workflow
375
+ checks that the tag matches the version, builds and tests every wheel, and
376
+ uploads to PyPI.
377
+
378
+ ## License
379
+
380
+ MIT. See [LICENSE](LICENSE).