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.
- tinydiffeq-0.1.0/.githooks/pre-commit +5 -0
- tinydiffeq-0.1.0/.github/workflows/ci.yml +29 -0
- tinydiffeq-0.1.0/.github/workflows/docs.yml +39 -0
- tinydiffeq-0.1.0/.github/workflows/publish.yml +36 -0
- tinydiffeq-0.1.0/.gitignore +23 -0
- tinydiffeq-0.1.0/.python-version +1 -0
- tinydiffeq-0.1.0/LICENSE +21 -0
- tinydiffeq-0.1.0/PKG-INFO +116 -0
- tinydiffeq-0.1.0/README.md +94 -0
- tinydiffeq-0.1.0/docs/adaptive_ad.md +88 -0
- tinydiffeq-0.1.0/docs/api.md +35 -0
- tinydiffeq-0.1.0/docs/index.md +125 -0
- tinydiffeq-0.1.0/docs/llms.txt +12 -0
- tinydiffeq-0.1.0/docs/migration.md +95 -0
- tinydiffeq-0.1.0/docs/sde.md +80 -0
- tinydiffeq-0.1.0/docs/static_shapes.md +96 -0
- tinydiffeq-0.1.0/mkdocs.yml +50 -0
- tinydiffeq-0.1.0/pyproject.toml +68 -0
- tinydiffeq-0.1.0/src/tinydiffeq/__init__.py +37 -0
- tinydiffeq-0.1.0/src/tinydiffeq/controllers.py +72 -0
- tinydiffeq-0.1.0/src/tinydiffeq/interpolation.py +47 -0
- tinydiffeq-0.1.0/src/tinydiffeq/ode.py +176 -0
- tinydiffeq-0.1.0/src/tinydiffeq/quadrature.py +31 -0
- tinydiffeq-0.1.0/src/tinydiffeq/saveat.py +39 -0
- tinydiffeq-0.1.0/src/tinydiffeq/sde.py +86 -0
- tinydiffeq-0.1.0/src/tinydiffeq/solution.py +28 -0
- tinydiffeq-0.1.0/src/tinydiffeq/solvers.py +129 -0
- tinydiffeq-0.1.0/tests/conftest.py +3 -0
- tinydiffeq-0.1.0/tests/kernels_reference.py +145 -0
- tinydiffeq-0.1.0/tests/test_ad.py +205 -0
- tinydiffeq-0.1.0/tests/test_adaptive.py +233 -0
- tinydiffeq-0.1.0/tests/test_float64_subprocess.py +158 -0
- tinydiffeq-0.1.0/tests/test_gpu.py +75 -0
- tinydiffeq-0.1.0/tests/test_quadrature.py +85 -0
- tinydiffeq-0.1.0/tests/test_recompile.py +73 -0
- tinydiffeq-0.1.0/tests/test_saveat.py +86 -0
- tinydiffeq-0.1.0/tests/test_sde.py +203 -0
- tinydiffeq-0.1.0/tests/test_solvers_fixed.py +144 -0
- tinydiffeq-0.1.0/uv.lock +1292 -0
|
@@ -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
|
tinydiffeq-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://github.com/HighDimensionalEconLab/tinydiffeq/actions/workflows/ci.yml)
|
|
26
|
+
[](https://highdimensionaleconlab.github.io/tinydiffeq/)
|
|
27
|
+
[](https://pypi.org/project/tinydiffeq/)
|
|
28
|
+
[](https://pypi.org/project/tinydiffeq/)
|
|
29
|
+
[](https://github.com/HighDimensionalEconLab/tinydiffeq/blob/main/LICENSE)
|
|
30
|
+
[](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
|
+
[](https://github.com/HighDimensionalEconLab/tinydiffeq/actions/workflows/ci.yml)
|
|
4
|
+
[](https://highdimensionaleconlab.github.io/tinydiffeq/)
|
|
5
|
+
[](https://pypi.org/project/tinydiffeq/)
|
|
6
|
+
[](https://pypi.org/project/tinydiffeq/)
|
|
7
|
+
[](https://github.com/HighDimensionalEconLab/tinydiffeq/blob/main/LICENSE)
|
|
8
|
+
[](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/)
|