tinydiffeq 0.1.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 (39) hide show
  1. tinydiffeq-0.1.0/.githooks/pre-commit +5 -0
  2. tinydiffeq-0.1.0/.github/workflows/ci.yml +29 -0
  3. tinydiffeq-0.1.0/.github/workflows/docs.yml +39 -0
  4. tinydiffeq-0.1.0/.github/workflows/publish.yml +36 -0
  5. tinydiffeq-0.1.0/.gitignore +23 -0
  6. tinydiffeq-0.1.0/.python-version +1 -0
  7. tinydiffeq-0.1.0/LICENSE +21 -0
  8. tinydiffeq-0.1.0/PKG-INFO +116 -0
  9. tinydiffeq-0.1.0/README.md +94 -0
  10. tinydiffeq-0.1.0/docs/adaptive_ad.md +88 -0
  11. tinydiffeq-0.1.0/docs/api.md +35 -0
  12. tinydiffeq-0.1.0/docs/index.md +125 -0
  13. tinydiffeq-0.1.0/docs/llms.txt +12 -0
  14. tinydiffeq-0.1.0/docs/migration.md +95 -0
  15. tinydiffeq-0.1.0/docs/sde.md +80 -0
  16. tinydiffeq-0.1.0/docs/static_shapes.md +96 -0
  17. tinydiffeq-0.1.0/mkdocs.yml +50 -0
  18. tinydiffeq-0.1.0/pyproject.toml +68 -0
  19. tinydiffeq-0.1.0/src/tinydiffeq/__init__.py +37 -0
  20. tinydiffeq-0.1.0/src/tinydiffeq/controllers.py +72 -0
  21. tinydiffeq-0.1.0/src/tinydiffeq/interpolation.py +47 -0
  22. tinydiffeq-0.1.0/src/tinydiffeq/ode.py +176 -0
  23. tinydiffeq-0.1.0/src/tinydiffeq/quadrature.py +31 -0
  24. tinydiffeq-0.1.0/src/tinydiffeq/saveat.py +39 -0
  25. tinydiffeq-0.1.0/src/tinydiffeq/sde.py +86 -0
  26. tinydiffeq-0.1.0/src/tinydiffeq/solution.py +28 -0
  27. tinydiffeq-0.1.0/src/tinydiffeq/solvers.py +129 -0
  28. tinydiffeq-0.1.0/tests/conftest.py +3 -0
  29. tinydiffeq-0.1.0/tests/kernels_reference.py +145 -0
  30. tinydiffeq-0.1.0/tests/test_ad.py +205 -0
  31. tinydiffeq-0.1.0/tests/test_adaptive.py +233 -0
  32. tinydiffeq-0.1.0/tests/test_float64_subprocess.py +158 -0
  33. tinydiffeq-0.1.0/tests/test_gpu.py +75 -0
  34. tinydiffeq-0.1.0/tests/test_quadrature.py +85 -0
  35. tinydiffeq-0.1.0/tests/test_recompile.py +73 -0
  36. tinydiffeq-0.1.0/tests/test_saveat.py +86 -0
  37. tinydiffeq-0.1.0/tests/test_sde.py +203 -0
  38. tinydiffeq-0.1.0/tests/test_solvers_fixed.py +144 -0
  39. tinydiffeq-0.1.0/uv.lock +1292 -0
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ set -e
3
+ cd "$(git rev-parse --show-toplevel)"
4
+ uv run ruff check .
5
+ uv run ruff format --check .
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ enable-cache: true
16
+ - run: uv sync --locked
17
+ - run: uv run ruff check .
18
+ - run: uv run ruff format --check .
19
+
20
+ test:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: astral-sh/setup-uv@v5
25
+ with:
26
+ enable-cache: true
27
+ python-version: "3.13"
28
+ - run: uv sync --locked
29
+ - run: uv run pytest -q
@@ -0,0 +1,39 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ pages: write
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: pages
15
+ cancel-in-progress: false
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: astral-sh/setup-uv@v5
23
+ with:
24
+ enable-cache: true
25
+ - run: uv sync --group docs
26
+ - run: uv run mkdocs build --strict
27
+ - uses: actions/upload-pages-artifact@v3
28
+ with:
29
+ path: site
30
+
31
+ deploy:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment:
35
+ name: github-pages
36
+ url: ${{ steps.deployment.outputs.page_url }}
37
+ steps:
38
+ - id: deployment
39
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,36 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv build
17
+ - run: uvx twine check dist/*
18
+ - uses: actions/upload-artifact@v4
19
+ with:
20
+ name: dist
21
+ path: dist/
22
+
23
+ publish:
24
+ needs: build
25
+ runs-on: ubuntu-latest
26
+ environment:
27
+ name: pypi
28
+ url: https://pypi.org/p/tinydiffeq
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,23 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+
7
+ # Build artifacts
8
+ build/
9
+ dist/
10
+
11
+ # Virtual environments
12
+ .venv/
13
+
14
+ # Docs build
15
+ site/
16
+
17
+ # Tooling caches
18
+ .pytest_cache/
19
+ .ruff_cache/
20
+ .mypy_cache/
21
+
22
+ # OS
23
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HighDimensionalEconLab
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,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: tinydiffeq
3
+ Version: 0.1.0
4
+ Summary: Tiny differentiable ODE/SDE solvers for JAX — bounded-scan adaptive stepping, static shapes, jvp/vjp-safe
5
+ Project-URL: Homepage, https://github.com/HighDimensionalEconLab/tinydiffeq
6
+ Project-URL: Repository, https://github.com/HighDimensionalEconLab/tinydiffeq
7
+ Project-URL: Documentation, https://highdimensionaleconlab.github.io/tinydiffeq/
8
+ Project-URL: Issues, https://github.com/HighDimensionalEconLab/tinydiffeq/issues
9
+ Author-email: Jesse Perla <jesseperla@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: autodiff,differential-equations,jax,ode,runge-kutta,sde
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: jax>=0.7.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # tinydiffeq
24
+
25
+ [![CI](https://github.com/HighDimensionalEconLab/tinydiffeq/actions/workflows/ci.yml/badge.svg)](https://github.com/HighDimensionalEconLab/tinydiffeq/actions/workflows/ci.yml)
26
+ [![Docs](https://github.com/HighDimensionalEconLab/tinydiffeq/actions/workflows/docs.yml/badge.svg)](https://highdimensionaleconlab.github.io/tinydiffeq/)
27
+ [![PyPI](https://img.shields.io/pypi/v/tinydiffeq.svg)](https://pypi.org/project/tinydiffeq/)
28
+ [![Python versions](https://img.shields.io/pypi/pyversions/tinydiffeq.svg)](https://pypi.org/project/tinydiffeq/)
29
+ [![License: MIT](https://img.shields.io/github/license/HighDimensionalEconLab/tinydiffeq)](https://github.com/HighDimensionalEconLab/tinydiffeq/blob/main/LICENSE)
30
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
31
+
32
+ Tiny differentiable ODE/SDE solvers for JAX: fixed-step Euler/RK4, adaptive
33
+ Tsit5 with an integral step-size controller, and Euler–Maruyama for Itô SDEs.
34
+ One bounded `lax.scan` of exactly `max_steps` iterations serves fixed and
35
+ adaptive stepping, so shapes are static, nothing recompiles as tolerances or
36
+ curvature change, and every solve is differentiable in **both** forward and
37
+ reverse mode — including reverse-over-forward, the pattern a
38
+ Levenberg–Marquardt optimizer with geodesic acceleration needs when it
39
+ differentiates through a rollout.
40
+
41
+ This is a deliberately small, jvp/vjp-friendly subset of
42
+ [diffrax](https://docs.kidger.site/diffrax/). Use diffrax if you need pytree
43
+ states, stiff/implicit solvers, PID control, events, dense output, or
44
+ checkpointed/backsolve adjoints. tinydiffeq's single runtime dependency is
45
+ `jax`.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ uv add tinydiffeq
51
+ ```
52
+
53
+ For GPU use, install the JAX accelerator build that matches your hardware,
54
+ for example:
55
+
56
+ ```bash
57
+ uv add tinydiffeq "jax[cuda13]"
58
+ ```
59
+
60
+ ## Minimal example
61
+
62
+ The vector field may take `(x)`, `(x, t)`, `(x, t, args)`, or
63
+ `(x, t, args, p)` — always in that order. `args` is pass-through data (not an
64
+ AD target by convention); `p` holds differentiable parameters (any pytree).
65
+
66
+ ```python
67
+ import jax
68
+ import jax.numpy as jnp
69
+ from tinydiffeq import solve_ode, Tsit5, IController, SaveAt
70
+
71
+ jax.config.update("jax_enable_x64", True) # your call — the library never sets it
72
+
73
+
74
+ def f(x, t, args, p):
75
+ return -p * x
76
+
77
+
78
+ sol = solve_ode(
79
+ f, Tsit5(), 0.0, 2.0, jnp.asarray(1.0),
80
+ p=jnp.asarray(1.3),
81
+ dt0=0.1,
82
+ controller=IController(rtol=1e-8, atol=1e-10),
83
+ max_steps=512,
84
+ saveat=SaveAt(ts=jnp.linspace(0.0, 2.0, 21)), # fixed output shape,
85
+ ) # however many steps adapt
86
+ print(sol.xs) # states on the grid
87
+ print(sol.ok) # reached t1 within the max_steps budget?
88
+ ```
89
+
90
+ ## Gradients through the solve
91
+
92
+ ```python
93
+ def endpoint(p):
94
+ return solve_ode(
95
+ f, Tsit5(), 0.0, 2.0, jnp.asarray(1.0), p=p,
96
+ dt0=0.1, controller=IController(rtol=1e-10, atol=1e-12),
97
+ max_steps=512,
98
+ ).xs
99
+
100
+ jax.grad(endpoint)(jnp.asarray(1.3)) # reverse mode
101
+ jax.jvp(endpoint, (jnp.asarray(1.3),), (jnp.asarray(1.0),)) # forward mode
102
+ jax.grad(lambda p: jax.jvp(endpoint, (p,), (jnp.asarray(1.0),))[1])(
103
+ jnp.asarray(1.3)
104
+ ) # reverse-over-forward
105
+ ```
106
+
107
+ The step-size controller is wrapped in `stop_gradient` (accept/reject is
108
+ non-differentiable either way, and the error-ratio power blows up at exactly
109
+ zero error); the states differentiate fully through the RK stages. See the
110
+ [docs](https://highdimensionaleconlab.github.io/tinydiffeq/) for the design
111
+ contracts: static shapes and `SaveAt`, AD through adaptive stepping, SDE key
112
+ semantics, and migration recipes from hand-rolled RK4/Tsit5 loops.
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,94 @@
1
+ # tinydiffeq
2
+
3
+ [![CI](https://github.com/HighDimensionalEconLab/tinydiffeq/actions/workflows/ci.yml/badge.svg)](https://github.com/HighDimensionalEconLab/tinydiffeq/actions/workflows/ci.yml)
4
+ [![Docs](https://github.com/HighDimensionalEconLab/tinydiffeq/actions/workflows/docs.yml/badge.svg)](https://highdimensionaleconlab.github.io/tinydiffeq/)
5
+ [![PyPI](https://img.shields.io/pypi/v/tinydiffeq.svg)](https://pypi.org/project/tinydiffeq/)
6
+ [![Python versions](https://img.shields.io/pypi/pyversions/tinydiffeq.svg)](https://pypi.org/project/tinydiffeq/)
7
+ [![License: MIT](https://img.shields.io/github/license/HighDimensionalEconLab/tinydiffeq)](https://github.com/HighDimensionalEconLab/tinydiffeq/blob/main/LICENSE)
8
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
+
10
+ Tiny differentiable ODE/SDE solvers for JAX: fixed-step Euler/RK4, adaptive
11
+ Tsit5 with an integral step-size controller, and Euler–Maruyama for Itô SDEs.
12
+ One bounded `lax.scan` of exactly `max_steps` iterations serves fixed and
13
+ adaptive stepping, so shapes are static, nothing recompiles as tolerances or
14
+ curvature change, and every solve is differentiable in **both** forward and
15
+ reverse mode — including reverse-over-forward, the pattern a
16
+ Levenberg–Marquardt optimizer with geodesic acceleration needs when it
17
+ differentiates through a rollout.
18
+
19
+ This is a deliberately small, jvp/vjp-friendly subset of
20
+ [diffrax](https://docs.kidger.site/diffrax/). Use diffrax if you need pytree
21
+ states, stiff/implicit solvers, PID control, events, dense output, or
22
+ checkpointed/backsolve adjoints. tinydiffeq's single runtime dependency is
23
+ `jax`.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ uv add tinydiffeq
29
+ ```
30
+
31
+ For GPU use, install the JAX accelerator build that matches your hardware,
32
+ for example:
33
+
34
+ ```bash
35
+ uv add tinydiffeq "jax[cuda13]"
36
+ ```
37
+
38
+ ## Minimal example
39
+
40
+ The vector field may take `(x)`, `(x, t)`, `(x, t, args)`, or
41
+ `(x, t, args, p)` — always in that order. `args` is pass-through data (not an
42
+ AD target by convention); `p` holds differentiable parameters (any pytree).
43
+
44
+ ```python
45
+ import jax
46
+ import jax.numpy as jnp
47
+ from tinydiffeq import solve_ode, Tsit5, IController, SaveAt
48
+
49
+ jax.config.update("jax_enable_x64", True) # your call — the library never sets it
50
+
51
+
52
+ def f(x, t, args, p):
53
+ return -p * x
54
+
55
+
56
+ sol = solve_ode(
57
+ f, Tsit5(), 0.0, 2.0, jnp.asarray(1.0),
58
+ p=jnp.asarray(1.3),
59
+ dt0=0.1,
60
+ controller=IController(rtol=1e-8, atol=1e-10),
61
+ max_steps=512,
62
+ saveat=SaveAt(ts=jnp.linspace(0.0, 2.0, 21)), # fixed output shape,
63
+ ) # however many steps adapt
64
+ print(sol.xs) # states on the grid
65
+ print(sol.ok) # reached t1 within the max_steps budget?
66
+ ```
67
+
68
+ ## Gradients through the solve
69
+
70
+ ```python
71
+ def endpoint(p):
72
+ return solve_ode(
73
+ f, Tsit5(), 0.0, 2.0, jnp.asarray(1.0), p=p,
74
+ dt0=0.1, controller=IController(rtol=1e-10, atol=1e-12),
75
+ max_steps=512,
76
+ ).xs
77
+
78
+ jax.grad(endpoint)(jnp.asarray(1.3)) # reverse mode
79
+ jax.jvp(endpoint, (jnp.asarray(1.3),), (jnp.asarray(1.0),)) # forward mode
80
+ jax.grad(lambda p: jax.jvp(endpoint, (p,), (jnp.asarray(1.0),))[1])(
81
+ jnp.asarray(1.3)
82
+ ) # reverse-over-forward
83
+ ```
84
+
85
+ The step-size controller is wrapped in `stop_gradient` (accept/reject is
86
+ non-differentiable either way, and the error-ratio power blows up at exactly
87
+ zero error); the states differentiate fully through the RK stages. See the
88
+ [docs](https://highdimensionaleconlab.github.io/tinydiffeq/) for the design
89
+ contracts: static shapes and `SaveAt`, AD through adaptive stepping, SDE key
90
+ semantics, and migration recipes from hand-rolled RK4/Tsit5 loops.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,88 @@
1
+ # Adaptive Stepping and AD
2
+
3
+ tinydiffeq's central claim is that one adaptive solve is safely
4
+ differentiable in forward mode, reverse mode, and reverse-over-forward. That
5
+ holds because of three deliberate choices about **what is not
6
+ differentiated**.
7
+
8
+ ## The controller is stop-gradiented
9
+
10
+ The step-size controller (`IController`) computes its scaled error norm and
11
+ next-step factor inside `stop_gradient`:
12
+
13
+ > accept/reject is a non-differentiable branch either way, the gradient of
14
+ > `E**(-1/order)` blows up at the exact-zero error of a flat-start policy,
15
+ > and the `d(dt)/dtheta` term only slides sample points along the visited
16
+ > trajectory — irrelevant to a residual that must vanish at every state.
17
+
18
+ The states themselves remain fully differentiable through the RK stages: the
19
+ gradient you get is the derivative of the numerical flow map *for the step
20
+ pattern actually taken*. This is the same convention diffrax uses.
21
+
22
+ The `E**(-1/5)` blow-up is not hypothetical: a policy initialized flat gives
23
+ an exactly-zero error estimate on the first step, and without the
24
+ `stop_gradient` (plus the `max(E, 1e-12)` floor) the backward pass is NaN
25
+ from iteration one. `tests/test_ad.py::test_grad_finite_on_flat_field` pins
26
+ this.
27
+
28
+ ## The horizon clip is the growth guard
29
+
30
+ Every attempt is clipped so it cannot step past `t1`, and — deliberately
31
+ unlike diffrax — the controller's next-step proposal is computed from the
32
+ **clipped** step:
33
+
34
+ > the horizon clip doubles as the guard on step growth: without it, a
35
+ > near-flat vector field lets steps quintuple into quarter-horizon leaps
36
+ > whose Gauss–Newton linearization stalls a trust-region optimizer
37
+ > differentiating through the rollout.
38
+
39
+ With `factormax = 5`, a flat field would otherwise reach `dt ≈ t1/4` within
40
+ a few accepted steps; residuals sampled from three or four giant steps make
41
+ the optimizer's linear model useless. Clipping first means the proposal can
42
+ never exceed `factormax × remaining horizon`.
43
+
44
+ One refinement over a bare `min(dt, remaining)`: when the remaining horizon
45
+ is within `max_steps × eps` of the desired step, the step is stretched to
46
+ land on `t1` exactly. Summing `n` rounded steps of `(t1 - t0)/n` can leave
47
+ `t` one accumulated ulp short of `t1`, and without the stretch that sliver
48
+ would cost an extra iteration a `max_steps = n` budget doesn't have.
49
+
50
+ ## Interpolation knots are non-differentiable
51
+
52
+ `SaveAt(ts=...)` brackets each query with `searchsorted` — integer indices,
53
+ no gradient. This is consistent with the stop-gradiented controller: the
54
+ term excluded is again "the knots slide along the trajectory as parameters
55
+ change". Values differentiate fully through the bracketing states and
56
+ derivatives (`xL`, `xR`, `fL`, `fR`).
57
+
58
+ Zero-width brackets (duplicate rows from rejections and the frozen tail) use
59
+ the **double-where trick**: the divisor is replaced by 1 *before* dividing,
60
+
61
+ ```python
62
+ width_safe = jnp.where(degenerate, 1.0, width)
63
+ s = jnp.clip((tau - t_left) / width_safe, 0.0, 1.0)
64
+ value = jnp.where(degenerate, x_left, hermite(s, ...))
65
+ ```
66
+
67
+ so neither the primal nor its jvp/vjp ever evaluates `0/0`. A single `where`
68
+ on the output is not enough — reverse mode differentiates both branches, and
69
+ `NaN * 0 = NaN`.
70
+
71
+ ## What this buys you
72
+
73
+ - `jax.grad`, `jax.jvp`, and `jax.grad(jax.jvp(...))` (the
74
+ Levenberg–Marquardt geodesic-acceleration pattern) all work through
75
+ adaptive solves and interpolated output, verified against closed forms in
76
+ `tests/test_ad.py`.
77
+ - `jax.vmap` over `x0` or `p` gives genuinely per-lane adaptivity: each lane
78
+ accepts/rejects independently through the masked scan, and batched results
79
+ equal the individual solves exactly.
80
+
81
+ ## What to watch
82
+
83
+ - Reductions over `SaveAt(steps=True)` rows are **discontinuous** in the
84
+ inputs: the number of duplicate rows changes when an accept flips to a
85
+ reject. Consume steps mode with residuals that vanish at every state (so
86
+ duplicates are harmless), or reduce to `xs[-1]`.
87
+ - Finite-difference checks of adaptive solves are noisy for the same reason;
88
+ compare AD against closed forms or use fixed-step solvers for FD tests.
@@ -0,0 +1,35 @@
1
+ # API Reference
2
+
3
+ ## Solve functions
4
+
5
+ ::: tinydiffeq.solve_ode
6
+
7
+ ::: tinydiffeq.solve_sde
8
+
9
+ ## Solvers
10
+
11
+ ::: tinydiffeq.Euler
12
+
13
+ ::: tinydiffeq.RK4
14
+
15
+ ::: tinydiffeq.Tsit5
16
+
17
+ ::: tinydiffeq.EulerMaruyama
18
+
19
+ ## Step-size controllers
20
+
21
+ ::: tinydiffeq.ConstantStepSize
22
+
23
+ ::: tinydiffeq.IController
24
+
25
+ ## Output selection and results
26
+
27
+ ::: tinydiffeq.SaveAt
28
+
29
+ ::: tinydiffeq.Solution
30
+
31
+ ## Utilities
32
+
33
+ ::: tinydiffeq.hermite_interpolate
34
+
35
+ ::: tinydiffeq.cumulative_trapezoid
@@ -0,0 +1,125 @@
1
+ # tinydiffeq
2
+
3
+ `tinydiffeq` is a deliberately tiny set of differentiable ODE/SDE integrators
4
+ for JAX: fixed-step Euler and RK4, adaptive Tsit5 with an integral step-size
5
+ controller, and fixed-step Euler–Maruyama for Itô SDEs. Everything runs
6
+ inside one bounded `lax.scan` with static shapes, and every solve is
7
+ differentiable in **both** forward and reverse mode — including
8
+ reverse-over-forward, the pattern a Levenberg–Marquardt optimizer with
9
+ geodesic acceleration needs when it differentiates through a rollout.
10
+
11
+ It is a jvp/vjp-friendly subset of
12
+ [diffrax](https://docs.kidger.site/diffrax/). **Use diffrax instead if you
13
+ need any of:**
14
+
15
+ - pytree states (tinydiffeq states are single arrays, scalar or vector)
16
+ - stiff or implicit solvers
17
+ - PID step-size control
18
+ - events, root-finding, or backward-time integration
19
+ - dense output / continuous interpolation objects
20
+ - checkpointed or backsolve adjoints for long horizons
21
+
22
+ tinydiffeq ships only the O(max_steps)-memory bounded-scan approach, because
23
+ that is the one that composes cleanly with `jax.jvp`, `jax.vjp`, `jax.vmap`,
24
+ and reverse-over-forward without custom adjoint machinery.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ uv add tinydiffeq
30
+ ```
31
+
32
+ For accelerator use, install the JAX build matching your hardware alongside
33
+ it, for example:
34
+
35
+ ```bash
36
+ uv add tinydiffeq "jax[cuda13]"
37
+ ```
38
+
39
+ ## Vector-field interface
40
+
41
+ The vector field may take one to four positional arguments — always in this
42
+ order:
43
+
44
+ ```python
45
+ f(x) # autonomous, closes over everything
46
+ f(x, t)
47
+ f(x, t, args)
48
+ f(x, t, args, p)
49
+ ```
50
+
51
+ - `x` is the array state (scalar or vector).
52
+ - `t` is time.
53
+ - `args` is pass-through data. By convention it is **not** an AD target —
54
+ nothing stops you differentiating with respect to it, but the library's
55
+ contracts and tests treat it as constants.
56
+ - `p` holds differentiable parameters — any pytree, e.g. neural-network
57
+ weights. jvp/vjp with respect to `p` and `x0` are first-class and tested.
58
+
59
+ The arity is inspected once and the function is wrapped into the canonical
60
+ four-argument form, so the compiled code is identical for all four. There is
61
+ no special autonomous code path: an unused `t` is dead-code-eliminated.
62
+ `drift` and `diffusion` in [`solve_sde`](sde.md) follow the same convention.
63
+
64
+ ## Minimal example
65
+
66
+ ```python
67
+ import jax
68
+ import jax.numpy as jnp
69
+ from tinydiffeq import solve_ode, Tsit5, IController, SaveAt
70
+
71
+ jax.config.update("jax_enable_x64", True) # your call, not the library's
72
+
73
+
74
+ def f(x, t, args, p):
75
+ return -p * x
76
+
77
+
78
+ sol = solve_ode(
79
+ f, Tsit5(), 0.0, 2.0, jnp.asarray(1.0),
80
+ p=jnp.asarray(1.3),
81
+ dt0=0.1,
82
+ controller=IController(rtol=1e-8, atol=1e-10),
83
+ max_steps=512,
84
+ saveat=SaveAt(ts=jnp.linspace(0.0, 2.0, 21)),
85
+ )
86
+ sol.xs # (21,) states on the grid, however many internal steps were taken
87
+ sol.ok # False if max_steps ran out before t1
88
+ ```
89
+
90
+ Gradients go straight through the solve:
91
+
92
+ ```python
93
+ def endpoint(p):
94
+ return solve_ode(
95
+ f, Tsit5(), 0.0, 2.0, jnp.asarray(1.0), p=p,
96
+ dt0=0.1, controller=IController(rtol=1e-10, atol=1e-12),
97
+ max_steps=512,
98
+ ).xs
99
+
100
+ jax.grad(endpoint)(jnp.asarray(1.3)) # reverse mode
101
+ jax.jvp(endpoint, (jnp.asarray(1.3),), (jnp.asarray(1.0),)) # forward mode
102
+ jax.grad(lambda p: jax.jvp(endpoint, (p,), (jnp.asarray(1.0),))[1])(
103
+ jnp.asarray(1.3)
104
+ ) # reverse-over-forward
105
+ ```
106
+
107
+ ## Design contracts at a glance
108
+
109
+ - **`dt0` is required.** There is no initial-step heuristic.
110
+ - **Forward time only**: `t1 > t0`.
111
+ - **Never poisons.** `sol.ok` reports whether `t1` was reached; callers that
112
+ want diverging residuals do `jnp.where(sol.ok, sol.xs, jnp.inf)`.
113
+ - **`project`** (an idempotent clamp, e.g. positivity) is applied at every
114
+ point where the vector field is evaluated and to every accepted state.
115
+ - **Never sets `jax_enable_x64`.** The time dtype follows
116
+ `jnp.result_type(x0, float)`; float32 problems stay float32 even under
117
+ x64.
118
+ - Solvers, controllers, `SaveAt`, and `Solution` are frozen dataclasses
119
+ registered as pytrees: numeric fields (tolerances, grids, `dt0`, `x0`) are
120
+ data leaves, so changing them never recompiles.
121
+
122
+ Read next: [Static Shapes](static_shapes.md) for the bounded-scan design and
123
+ `SaveAt`, [Adaptive Stepping and AD](adaptive_ad.md) for what is and is not
124
+ differentiated, [SDEs](sde.md), and
125
+ [Migration](migration.md) if you are replacing hand-rolled RK4/Tsit5 loops.
@@ -0,0 +1,12 @@
1
+ # tinydiffeq
2
+
3
+ > Tiny differentiable ODE/SDE solvers for JAX. Fixed-step (Euler, RK4) and adaptive (Tsit5 + integral controller) explicit Runge-Kutta inside one bounded lax.scan of exactly max_steps iterations: static shapes, one compilation across tolerances/curvature/initial conditions, and solves differentiable in both forward and reverse mode (including reverse-over-forward) with O(max_steps) memory. Array states only (scalar or vector). SaveAt selects endpoint, cubic-Hermite interpolation onto a fixed grid, or raw padded step rows. solve_sde is fixed-step Euler-Maruyama with presampled diagonal noise keyed on a PRNG key. A deliberately small subset of diffrax: use diffrax for pytree states, stiff/implicit solvers, PID control, events, dense output, and adjoint methods.
4
+
5
+ ## Docs
6
+
7
+ - [Home — positioning, vector-field signature convention f(x, t, args, p), minimal examples](https://highdimensionaleconlab.github.io/tinydiffeq/)
8
+ - [Static shapes — the bounded-scan design, SaveAt as the shape contract, duplicate rows vs inf padding, why nothing recompiles](https://highdimensionaleconlab.github.io/tinydiffeq/static_shapes/)
9
+ - [Adaptive stepping and AD — stop-gradiented controller rationale, horizon-clip growth guard, non-differentiable interpolation knots, double-where NaN safety](https://highdimensionaleconlab.github.io/tinydiffeq/adaptive_ad/)
10
+ - [SDEs — Euler-Maruyama orders, fixed-noise key semantics, shared-path strong-convergence testing, why SaveAt(ts) raises](https://highdimensionaleconlab.github.io/tinydiffeq/sde/)
11
+ - [Migration — exact recipes replacing hand-rolled rk4_grid/tsit5_free loops, poisoning via jnp.where(sol.ok, ...), documented behavior changes](https://highdimensionaleconlab.github.io/tinydiffeq/migration/)
12
+ - [API reference — solve_ode, solve_sde, solvers, controllers, SaveAt, Solution, hermite_interpolate, cumulative_trapezoid](https://highdimensionaleconlab.github.io/tinydiffeq/api/)