tinydiffeq 0.3.0__tar.gz → 1.0.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.3.0 → tinydiffeq-1.0.0}/PKG-INFO +28 -16
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/README.md +24 -12
- tinydiffeq-1.0.0/benchmarks/README.md +38 -0
- tinydiffeq-1.0.0/benchmarks/__init__.py +1 -0
- tinydiffeq-1.0.0/benchmarks/compile_times.py +64 -0
- tinydiffeq-1.0.0/benchmarks/test_aux_dense_output.py +159 -0
- tinydiffeq-1.0.0/benchmarks/test_pytree_overhead.py +138 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/docs/api.md +2 -0
- tinydiffeq-1.0.0/docs/dae.md +184 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/docs/index.md +14 -9
- tinydiffeq-1.0.0/docs/llms.txt +13 -0
- tinydiffeq-1.0.0/docs/sdae.md +99 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/docs/sde.md +10 -4
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/docs/static_shapes.md +17 -2
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/mkdocs.yml +2 -1
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/pyproject.toml +4 -4
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/__init__.py +9 -5
- tinydiffeq-1.0.0/src/tinydiffeq/_tree.py +122 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/controllers.py +12 -12
- tinydiffeq-1.0.0/src/tinydiffeq/dae.py +872 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/interpolation.py +22 -12
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/ode.py +38 -18
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/save_at.py +1 -1
- tinydiffeq-1.0.0/src/tinydiffeq/sdae.py +248 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/sde.py +50 -13
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/solution.py +15 -12
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/solvers.py +52 -22
- tinydiffeq-1.0.0/tests/test_aux.py +319 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_dae.py +119 -1
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_float64_subprocess.py +159 -0
- tinydiffeq-1.0.0/tests/test_gpu.py +211 -0
- tinydiffeq-1.0.0/tests/test_pytree_states.py +269 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_save_at.py +1 -1
- tinydiffeq-1.0.0/tests/test_sdae.py +286 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/uv.lock +5 -5
- tinydiffeq-0.3.0/docs/dae.md +0 -120
- tinydiffeq-0.3.0/docs/llms.txt +0 -12
- tinydiffeq-0.3.0/src/tinydiffeq/dae.py +0 -453
- tinydiffeq-0.3.0/tests/test_gpu.py +0 -111
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/.githooks/pre-commit +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/.github/workflows/ci.yml +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/.github/workflows/docs.yml +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/.github/workflows/publish.yml +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/.gitignore +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/.python-version +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/LICENSE +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/docs/adaptive_ad.md +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/src/tinydiffeq/quadrature.py +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/conftest.py +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/kernels_reference.py +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_ad.py +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_adaptive.py +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_quadrature.py +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_recompile.py +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_sde.py +0 -0
- {tinydiffeq-0.3.0 → tinydiffeq-1.0.0}/tests/test_solvers_fixed.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinydiffeq
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Tiny differentiable ODE/SDE/DAE solvers for JAX with static shapes and composable AD
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Tiny differentiable ODE/SDE/DAE/SDAE solvers for JAX with static shapes and composable AD
|
|
5
5
|
Project-URL: Homepage, https://github.com/HighDimensionalEconLab/tinydiffeq
|
|
6
6
|
Project-URL: Repository, https://github.com/HighDimensionalEconLab/tinydiffeq
|
|
7
7
|
Project-URL: Documentation, https://highdimensionaleconlab.github.io/tinydiffeq/
|
|
@@ -10,7 +10,7 @@ Author-email: Jesse Perla <jesseperla@gmail.com>
|
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: autodiff,differential-equations,jax,ode,runge-kutta,sde
|
|
13
|
-
Classifier: Development Status ::
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Requires-Dist: jax>=0.7.0
|
|
21
|
-
Requires-Dist: nlls-gram>=1.
|
|
21
|
+
Requires-Dist: nlls-gram>=1.7.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
24
|
# tinydiffeq
|
|
@@ -30,9 +30,10 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
[](https://github.com/HighDimensionalEconLab/tinydiffeq/blob/main/LICENSE)
|
|
31
31
|
[](https://github.com/astral-sh/ruff)
|
|
32
32
|
|
|
33
|
-
Tiny differentiable ODE/SDE/DAE solvers for JAX: fixed-step Euler/RK4,
|
|
33
|
+
Tiny differentiable ODE/SDE/DAE/SDAE solvers for JAX: fixed-step Euler/RK4,
|
|
34
34
|
adaptive Tsit5 with integral or proportional-integral step-size control,
|
|
35
|
-
Euler–Maruyama for Itô SDEs, and nonstiff semi-explicit index-1
|
|
35
|
+
Euler–Maruyama for Itô SDEs, and nonstiff semi-explicit index-1 deterministic
|
|
36
|
+
and stochastic DAEs.
|
|
36
37
|
One bounded `lax.scan` of exactly `max_steps` iterations serves fixed and
|
|
37
38
|
adaptive stepping, so shapes are static, nothing recompiles as tolerances or
|
|
38
39
|
curvature change, and every solve is differentiable in **both** forward and
|
|
@@ -42,10 +43,10 @@ differentiates through a rollout. After a solve reaches its horizon, a
|
|
|
42
43
|
`lax.cond` skips solver and controller work during the padded scan tail.
|
|
43
44
|
|
|
44
45
|
This is a deliberately small, jvp/vjp-friendly subset of
|
|
45
|
-
[diffrax](https://docs.kidger.site/diffrax/). Use diffrax if you need
|
|
46
|
-
|
|
47
|
-
derivative-term PID control, events,
|
|
48
|
-
adjoints. The DAE algebraic solve uses `nlls-gram`.
|
|
46
|
+
[diffrax](https://docs.kidger.site/diffrax/). Use diffrax if you need stiff
|
|
47
|
+
or fully implicit solvers, higher-index DAEs, full
|
|
48
|
+
derivative-term PID control, events, continuous interpolation objects, or
|
|
49
|
+
checkpointed/backsolve adjoints. The DAE algebraic solve uses `nlls-gram`.
|
|
49
50
|
|
|
50
51
|
## Install
|
|
51
52
|
|
|
@@ -65,6 +66,11 @@ uv add tinydiffeq "jax[cuda13]"
|
|
|
65
66
|
The vector field may take `(x)`, `(x, t)`, `(x, t, args)`, or
|
|
66
67
|
`(x, t, args, p)` — always in that order. `args` is pass-through data (not an
|
|
67
68
|
AD target by convention); `p` holds differentiable parameters (any pytree).
|
|
69
|
+
The state may also be any JAX pytree. It must contain at least one leaf, and
|
|
70
|
+
every leaf must be a nonempty real floating array with the same dtype; vector
|
|
71
|
+
fields and `project` preserve that structure. Output keeps the structure and
|
|
72
|
+
adds the saved-time axis to each
|
|
73
|
+
leaf.
|
|
68
74
|
|
|
69
75
|
```python
|
|
70
76
|
import jax
|
|
@@ -121,25 +127,31 @@ def dae_f(y, z, t, args, p):
|
|
|
121
127
|
return p * z
|
|
122
128
|
|
|
123
129
|
|
|
124
|
-
def dae_g(y, z):
|
|
125
|
-
return z - y
|
|
130
|
+
def dae_g(y, z, t, args, p):
|
|
131
|
+
return z - y, {"flow": p * z}
|
|
126
132
|
|
|
127
133
|
|
|
128
134
|
dae_sol = solve_semi_explicit_dae(
|
|
129
135
|
dae_f, dae_g, Tsit5(), 0.0, 1.0,
|
|
130
136
|
jnp.asarray(1.0), jnp.asarray(0.5),
|
|
131
137
|
p=jnp.asarray(2.0), dt_0=0.1,
|
|
132
|
-
controller=IController(), max_steps=128,
|
|
138
|
+
controller=IController(), max_steps=128, has_aux=True,
|
|
133
139
|
)
|
|
134
|
-
print(dae_sol.ys, dae_sol.zs)
|
|
140
|
+
print(dae_sol.ys, dae_sol.zs, dae_sol.aux["flow"])
|
|
135
141
|
```
|
|
136
142
|
|
|
137
|
-
`z_0` is a guess and is made consistent automatically. Algebraic
|
|
138
|
-
|
|
143
|
+
`z_0` is a guess and is made consistent automatically. Algebraic equations
|
|
144
|
+
may return a floating aux pytree stored at every accepted node and
|
|
145
|
+
Hermite-interpolated with `z` on requested deterministic grids. Algebraic
|
|
146
|
+
roots use an implicitly differentiated square LM solve, so JVP, VJP, and
|
|
139
147
|
reverse-over-forward propagate with respect to `y`, `t`, and `p`. See the
|
|
140
148
|
[DAE documentation](https://highdimensionaleconlab.github.io/tinydiffeq/dae/)
|
|
141
149
|
for root controls, `SaveAt`, and scope limits.
|
|
142
150
|
|
|
151
|
+
Fixed-step semi-explicit Itô SDAEs use the corresponding
|
|
152
|
+
`solve_semi_explicit_sdae` interface with `EulerMaruyama`, a PRNG key, and
|
|
153
|
+
`n_steps`; see the [SDAE documentation](https://highdimensionaleconlab.github.io/tinydiffeq/sdae/).
|
|
154
|
+
|
|
143
155
|
## Gradients through the solve
|
|
144
156
|
|
|
145
157
|
```python
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
[](https://github.com/HighDimensionalEconLab/tinydiffeq/blob/main/LICENSE)
|
|
8
8
|
[](https://github.com/astral-sh/ruff)
|
|
9
9
|
|
|
10
|
-
Tiny differentiable ODE/SDE/DAE solvers for JAX: fixed-step Euler/RK4,
|
|
10
|
+
Tiny differentiable ODE/SDE/DAE/SDAE solvers for JAX: fixed-step Euler/RK4,
|
|
11
11
|
adaptive Tsit5 with integral or proportional-integral step-size control,
|
|
12
|
-
Euler–Maruyama for Itô SDEs, and nonstiff semi-explicit index-1
|
|
12
|
+
Euler–Maruyama for Itô SDEs, and nonstiff semi-explicit index-1 deterministic
|
|
13
|
+
and stochastic DAEs.
|
|
13
14
|
One bounded `lax.scan` of exactly `max_steps` iterations serves fixed and
|
|
14
15
|
adaptive stepping, so shapes are static, nothing recompiles as tolerances or
|
|
15
16
|
curvature change, and every solve is differentiable in **both** forward and
|
|
@@ -19,10 +20,10 @@ differentiates through a rollout. After a solve reaches its horizon, a
|
|
|
19
20
|
`lax.cond` skips solver and controller work during the padded scan tail.
|
|
20
21
|
|
|
21
22
|
This is a deliberately small, jvp/vjp-friendly subset of
|
|
22
|
-
[diffrax](https://docs.kidger.site/diffrax/). Use diffrax if you need
|
|
23
|
-
|
|
24
|
-
derivative-term PID control, events,
|
|
25
|
-
adjoints. The DAE algebraic solve uses `nlls-gram`.
|
|
23
|
+
[diffrax](https://docs.kidger.site/diffrax/). Use diffrax if you need stiff
|
|
24
|
+
or fully implicit solvers, higher-index DAEs, full
|
|
25
|
+
derivative-term PID control, events, continuous interpolation objects, or
|
|
26
|
+
checkpointed/backsolve adjoints. The DAE algebraic solve uses `nlls-gram`.
|
|
26
27
|
|
|
27
28
|
## Install
|
|
28
29
|
|
|
@@ -42,6 +43,11 @@ uv add tinydiffeq "jax[cuda13]"
|
|
|
42
43
|
The vector field may take `(x)`, `(x, t)`, `(x, t, args)`, or
|
|
43
44
|
`(x, t, args, p)` — always in that order. `args` is pass-through data (not an
|
|
44
45
|
AD target by convention); `p` holds differentiable parameters (any pytree).
|
|
46
|
+
The state may also be any JAX pytree. It must contain at least one leaf, and
|
|
47
|
+
every leaf must be a nonempty real floating array with the same dtype; vector
|
|
48
|
+
fields and `project` preserve that structure. Output keeps the structure and
|
|
49
|
+
adds the saved-time axis to each
|
|
50
|
+
leaf.
|
|
45
51
|
|
|
46
52
|
```python
|
|
47
53
|
import jax
|
|
@@ -98,25 +104,31 @@ def dae_f(y, z, t, args, p):
|
|
|
98
104
|
return p * z
|
|
99
105
|
|
|
100
106
|
|
|
101
|
-
def dae_g(y, z):
|
|
102
|
-
return z - y
|
|
107
|
+
def dae_g(y, z, t, args, p):
|
|
108
|
+
return z - y, {"flow": p * z}
|
|
103
109
|
|
|
104
110
|
|
|
105
111
|
dae_sol = solve_semi_explicit_dae(
|
|
106
112
|
dae_f, dae_g, Tsit5(), 0.0, 1.0,
|
|
107
113
|
jnp.asarray(1.0), jnp.asarray(0.5),
|
|
108
114
|
p=jnp.asarray(2.0), dt_0=0.1,
|
|
109
|
-
controller=IController(), max_steps=128,
|
|
115
|
+
controller=IController(), max_steps=128, has_aux=True,
|
|
110
116
|
)
|
|
111
|
-
print(dae_sol.ys, dae_sol.zs)
|
|
117
|
+
print(dae_sol.ys, dae_sol.zs, dae_sol.aux["flow"])
|
|
112
118
|
```
|
|
113
119
|
|
|
114
|
-
`z_0` is a guess and is made consistent automatically. Algebraic
|
|
115
|
-
|
|
120
|
+
`z_0` is a guess and is made consistent automatically. Algebraic equations
|
|
121
|
+
may return a floating aux pytree stored at every accepted node and
|
|
122
|
+
Hermite-interpolated with `z` on requested deterministic grids. Algebraic
|
|
123
|
+
roots use an implicitly differentiated square LM solve, so JVP, VJP, and
|
|
116
124
|
reverse-over-forward propagate with respect to `y`, `t`, and `p`. See the
|
|
117
125
|
[DAE documentation](https://highdimensionaleconlab.github.io/tinydiffeq/dae/)
|
|
118
126
|
for root controls, `SaveAt`, and scope limits.
|
|
119
127
|
|
|
128
|
+
Fixed-step semi-explicit Itô SDAEs use the corresponding
|
|
129
|
+
`solve_semi_explicit_sdae` interface with `EulerMaruyama`, a PRNG key, and
|
|
130
|
+
`n_steps`; see the [SDAE documentation](https://highdimensionaleconlab.github.io/tinydiffeq/sdae/).
|
|
131
|
+
|
|
120
132
|
## Gradients through the solve
|
|
121
133
|
|
|
122
134
|
```python
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
Run the opt-in CPU suite with:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
JAX_PLATFORMS=cpu uv run --group benchmark pytest benchmarks --benchmark-only
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It measures post-compilation primal, JVP, and VJP execution for RK4, Tsit5,
|
|
10
|
+
Euler-Maruyama, and the semi-explicit DAE/SDAE solvers. Each method is
|
|
11
|
+
exercised on a scalar array, a length-16 array, and an equal-sized two-leaf
|
|
12
|
+
pytree. The array cases are the performance-regression baseline; pytree
|
|
13
|
+
timings show the cost of executing separate leaves. Compilation is
|
|
14
|
+
deliberately performed outside the timed region.
|
|
15
|
+
|
|
16
|
+
Measure cold compilation separately (optionally selecting a subset) with:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
JAX_PLATFORMS=cpu uv run --group benchmark python -m benchmarks.compile_times
|
|
20
|
+
JAX_PLATFORMS=cpu uv run --group benchmark python -m benchmarks.compile_times \
|
|
21
|
+
--methods rk4 tsit5 --states vector16 --repeat 5
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The script clears JAX's compilation caches between `timeit` repetitions and
|
|
25
|
+
reports the median. Keep compilation and execution results separate.
|
|
26
|
+
|
|
27
|
+
`test_aux_dense_output.py` separately measures the intentionally changed DAE
|
|
28
|
+
grid-output path across accepted-step and query-grid counts, both with and
|
|
29
|
+
without aux, plus batched adaptive interpolation. The no-aux cases can run
|
|
30
|
+
against an older checkout for a direct cross-version comparison. Those
|
|
31
|
+
results are re-baselined rather than held to the unchanged endpoint/steps
|
|
32
|
+
array-path gate.
|
|
33
|
+
|
|
34
|
+
For changes to the state arithmetic, compare the JSON outputs before and
|
|
35
|
+
after the change using `--benchmark-json=PATH`. Treat an array-path slowdown
|
|
36
|
+
larger than `max(5%, 1 us)` as a regression. GPU timing is informative rather
|
|
37
|
+
than a release gate because available devices vary; GPU correctness belongs
|
|
38
|
+
in `tests/test_gpu.py`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Opt-in tinydiffeq benchmarks."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Cold JAX compilation timings for the pytree benchmark matrix."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import statistics
|
|
5
|
+
import timeit
|
|
6
|
+
from functools import partial
|
|
7
|
+
|
|
8
|
+
import jax
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
|
|
11
|
+
from benchmarks.test_pytree_overhead import (
|
|
12
|
+
make_solve,
|
|
13
|
+
make_state,
|
|
14
|
+
tree_map,
|
|
15
|
+
tree_sum,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def transformed_solve(method, state_kind, transform):
|
|
20
|
+
initial = make_state(state_kind)
|
|
21
|
+
solve = make_solve(method, initial)
|
|
22
|
+
tangent = tree_map(jnp.ones_like, initial)
|
|
23
|
+
if transform == "primal":
|
|
24
|
+
run = jax.jit(solve)
|
|
25
|
+
elif transform == "jvp":
|
|
26
|
+
run = jax.jit(lambda x: jax.jvp(solve, (x,), (tangent,)))
|
|
27
|
+
else:
|
|
28
|
+
run = jax.jit(lambda x: jax.grad(lambda value: tree_sum(solve(value)))(x))
|
|
29
|
+
return run, initial
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def compile_once(run, initial):
|
|
33
|
+
jax.clear_caches()
|
|
34
|
+
run.lower(initial).compile()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
parser = argparse.ArgumentParser()
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--methods", nargs="+", default=["rk4", "tsit5", "em", "dae", "sdae"]
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument("--states", nargs="+", default=["scalar", "vector16", "tree16"])
|
|
43
|
+
parser.add_argument("--transforms", nargs="+", default=["primal", "jvp", "vjp"])
|
|
44
|
+
parser.add_argument("--repeat", type=int, default=3)
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
|
|
47
|
+
print("method,state,transform,median_compile_seconds")
|
|
48
|
+
for method in args.methods:
|
|
49
|
+
for state_kind in args.states:
|
|
50
|
+
for transform in args.transforms:
|
|
51
|
+
run, initial = transformed_solve(method, state_kind, transform)
|
|
52
|
+
samples = timeit.repeat(
|
|
53
|
+
partial(compile_once, run, initial),
|
|
54
|
+
repeat=args.repeat,
|
|
55
|
+
number=1,
|
|
56
|
+
)
|
|
57
|
+
print(
|
|
58
|
+
f"{method},{state_kind},{transform},"
|
|
59
|
+
f"{statistics.median(samples):.6f}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Opt-in DAE aux/dense-output and batched interpolation benchmarks."""
|
|
2
|
+
|
|
3
|
+
import jax
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from tinydiffeq import (
|
|
8
|
+
RK4,
|
|
9
|
+
ConstantStepSize,
|
|
10
|
+
IController,
|
|
11
|
+
LMRootSolver,
|
|
12
|
+
SaveAt,
|
|
13
|
+
Tsit5,
|
|
14
|
+
solve_semi_explicit_dae,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make_solve(n_steps, n_queries):
|
|
19
|
+
grid = jnp.linspace(0.0, 1.0, n_queries)
|
|
20
|
+
|
|
21
|
+
def run(rate):
|
|
22
|
+
return solve_semi_explicit_dae(
|
|
23
|
+
lambda y, z, t, args, p: p * z,
|
|
24
|
+
lambda y, z, t, args, p: (
|
|
25
|
+
z - y**2 - 0.1 * t,
|
|
26
|
+
{"flow": p * z, "level": y + z},
|
|
27
|
+
),
|
|
28
|
+
RK4(),
|
|
29
|
+
0.0,
|
|
30
|
+
1.0,
|
|
31
|
+
jnp.asarray(0.8),
|
|
32
|
+
jnp.asarray(0.6),
|
|
33
|
+
p=rate,
|
|
34
|
+
dt_0=1.0 / n_steps,
|
|
35
|
+
controller=ConstantStepSize(),
|
|
36
|
+
root_solver=LMRootSolver(),
|
|
37
|
+
max_steps=n_steps,
|
|
38
|
+
save_at=SaveAt(ts=grid),
|
|
39
|
+
has_aux=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return run
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def make_adaptive_solve(n_queries):
|
|
46
|
+
grid = jnp.linspace(0.0, 1.0, n_queries)
|
|
47
|
+
|
|
48
|
+
def run(rate):
|
|
49
|
+
return solve_semi_explicit_dae(
|
|
50
|
+
lambda y, z, t, args, p: p * z,
|
|
51
|
+
lambda y, z, t, args, p: (
|
|
52
|
+
z - y**2 - 0.1 * t,
|
|
53
|
+
{"flow": p * z, "level": y + z},
|
|
54
|
+
),
|
|
55
|
+
Tsit5(),
|
|
56
|
+
0.0,
|
|
57
|
+
1.0,
|
|
58
|
+
jnp.asarray(0.8),
|
|
59
|
+
jnp.asarray(0.6),
|
|
60
|
+
p=rate,
|
|
61
|
+
dt_0=0.1,
|
|
62
|
+
controller=IController(),
|
|
63
|
+
root_solver=LMRootSolver(),
|
|
64
|
+
max_steps=128,
|
|
65
|
+
save_at=SaveAt(ts=grid),
|
|
66
|
+
has_aux=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return run
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def make_no_aux_solve(n_steps, n_queries):
|
|
73
|
+
grid = jnp.linspace(0.0, 1.0, n_queries)
|
|
74
|
+
|
|
75
|
+
def run(rate):
|
|
76
|
+
return solve_semi_explicit_dae(
|
|
77
|
+
lambda y, z, t, args, p: p * z,
|
|
78
|
+
lambda y, z, t, args, p: z - y**2 - 0.1 * t,
|
|
79
|
+
RK4(),
|
|
80
|
+
0.0,
|
|
81
|
+
1.0,
|
|
82
|
+
jnp.asarray(0.8),
|
|
83
|
+
jnp.asarray(0.6),
|
|
84
|
+
p=rate,
|
|
85
|
+
dt_0=1.0 / n_steps,
|
|
86
|
+
controller=ConstantStepSize(),
|
|
87
|
+
root_solver=LMRootSolver(),
|
|
88
|
+
max_steps=n_steps,
|
|
89
|
+
save_at=SaveAt(ts=grid),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return run
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.parametrize("n_steps", [8, 32, 128])
|
|
96
|
+
@pytest.mark.parametrize("n_queries", [8, 32, 128])
|
|
97
|
+
@pytest.mark.parametrize("transform", ["primal", "jvp", "vjp"])
|
|
98
|
+
def test_aux_dense_output_crossover(benchmark, n_steps, n_queries, transform):
|
|
99
|
+
solve = make_solve(n_steps, n_queries)
|
|
100
|
+
rate = jnp.asarray(0.2)
|
|
101
|
+
assert bool(solve(rate).ok)
|
|
102
|
+
|
|
103
|
+
def scalar_output(value):
|
|
104
|
+
sol = solve(value)
|
|
105
|
+
return jnp.sum(sol.ys + sol.zs + sol.aux["flow"] + sol.aux["level"])
|
|
106
|
+
|
|
107
|
+
if transform == "primal":
|
|
108
|
+
run = jax.jit(solve)
|
|
109
|
+
elif transform == "jvp":
|
|
110
|
+
run = jax.jit(
|
|
111
|
+
lambda value: jax.jvp(scalar_output, (value,), (jnp.ones_like(value),))
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
run = jax.jit(jax.grad(scalar_output))
|
|
115
|
+
compiled = run.lower(rate).compile()
|
|
116
|
+
jax.block_until_ready(compiled(rate))
|
|
117
|
+
benchmark.pedantic(
|
|
118
|
+
lambda: jax.block_until_ready(compiled(rate)), rounds=10, iterations=5
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize("n_steps", [8, 128])
|
|
123
|
+
@pytest.mark.parametrize("n_queries", [8, 128])
|
|
124
|
+
@pytest.mark.parametrize("transform", ["primal", "jvp", "vjp"])
|
|
125
|
+
def test_no_aux_grid_output_crossover(benchmark, n_steps, n_queries, transform):
|
|
126
|
+
"""Cross-version baseline for the intentionally changed DAE grid path."""
|
|
127
|
+
solve = make_no_aux_solve(n_steps, n_queries)
|
|
128
|
+
rate = jnp.asarray(0.2)
|
|
129
|
+
assert bool(solve(rate).ok)
|
|
130
|
+
|
|
131
|
+
def scalar_output(value):
|
|
132
|
+
sol = solve(value)
|
|
133
|
+
return jnp.sum(sol.ys + sol.zs)
|
|
134
|
+
|
|
135
|
+
if transform == "primal":
|
|
136
|
+
run = jax.jit(solve)
|
|
137
|
+
elif transform == "jvp":
|
|
138
|
+
run = jax.jit(
|
|
139
|
+
lambda value: jax.jvp(scalar_output, (value,), (jnp.ones_like(value),))
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
run = jax.jit(jax.grad(scalar_output))
|
|
143
|
+
compiled = run.lower(rate).compile()
|
|
144
|
+
jax.block_until_ready(compiled(rate))
|
|
145
|
+
benchmark.pedantic(
|
|
146
|
+
lambda: jax.block_until_ready(compiled(rate)), rounds=10, iterations=5
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_batched_adaptive_dense_output(benchmark):
|
|
151
|
+
solve = make_adaptive_solve(32)
|
|
152
|
+
rates = jnp.linspace(0.1, 1.0, 16)
|
|
153
|
+
assert bool(jnp.all(jax.vmap(solve)(rates).ok))
|
|
154
|
+
run = jax.jit(jax.vmap(lambda value: solve(value).aux["flow"]))
|
|
155
|
+
compiled = run.lower(rates).compile()
|
|
156
|
+
jax.block_until_ready(compiled(rates))
|
|
157
|
+
benchmark.pedantic(
|
|
158
|
+
lambda: jax.block_until_ready(compiled(rates)), rounds=10, iterations=5
|
|
159
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Opt-in steady-state array/pytree and AD benchmarks.
|
|
2
|
+
|
|
3
|
+
Run with ``uv run --group benchmark pytest benchmarks --benchmark-only``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import jax
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from tinydiffeq import (
|
|
11
|
+
RK4,
|
|
12
|
+
EulerMaruyama,
|
|
13
|
+
IController,
|
|
14
|
+
Tsit5,
|
|
15
|
+
solve_ode,
|
|
16
|
+
solve_sde,
|
|
17
|
+
solve_semi_explicit_dae,
|
|
18
|
+
solve_semi_explicit_sdae,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_state(kind):
|
|
23
|
+
if kind == "scalar":
|
|
24
|
+
return jnp.asarray(1.0)
|
|
25
|
+
if kind == "vector16":
|
|
26
|
+
return jnp.linspace(0.5, 1.5, 16)
|
|
27
|
+
return {"left": jnp.linspace(0.5, 1.0, 8), "right": jnp.linspace(1.0, 1.5, 8)}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def tree_map(fn, tree):
|
|
31
|
+
return jax.tree.map(fn, tree)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def tree_sum(tree):
|
|
35
|
+
return sum(jnp.sum(leaf) for leaf in jax.tree.leaves(tree))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def make_solve(method, initial):
|
|
39
|
+
if method == "rk4":
|
|
40
|
+
return lambda x: (
|
|
41
|
+
solve_ode(
|
|
42
|
+
lambda state: tree_map(lambda leaf: -0.2 * leaf, state),
|
|
43
|
+
RK4(),
|
|
44
|
+
0.0,
|
|
45
|
+
1.0,
|
|
46
|
+
x,
|
|
47
|
+
dt_0=1 / 64,
|
|
48
|
+
max_steps=64,
|
|
49
|
+
).xs
|
|
50
|
+
)
|
|
51
|
+
if method == "tsit5":
|
|
52
|
+
return lambda x: (
|
|
53
|
+
solve_ode(
|
|
54
|
+
lambda state: tree_map(lambda leaf: -0.2 * leaf, state),
|
|
55
|
+
Tsit5(),
|
|
56
|
+
0.0,
|
|
57
|
+
1.0,
|
|
58
|
+
x,
|
|
59
|
+
dt_0=0.05,
|
|
60
|
+
controller=IController(),
|
|
61
|
+
max_steps=64,
|
|
62
|
+
).xs
|
|
63
|
+
)
|
|
64
|
+
if method == "em":
|
|
65
|
+
return lambda x: (
|
|
66
|
+
solve_sde(
|
|
67
|
+
lambda state: tree_map(lambda leaf: -0.2 * leaf, state),
|
|
68
|
+
lambda state: tree_map(lambda leaf: 0.1 * jnp.ones_like(leaf), state),
|
|
69
|
+
EulerMaruyama(),
|
|
70
|
+
0.0,
|
|
71
|
+
1.0,
|
|
72
|
+
x,
|
|
73
|
+
key=jax.random.key(7),
|
|
74
|
+
n_steps=64,
|
|
75
|
+
).xs
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
z_0 = tree_map(lambda leaf: 0.9 * leaf, initial)
|
|
79
|
+
|
|
80
|
+
def constraint(y, z):
|
|
81
|
+
y_flat = jnp.concatenate([jnp.ravel(leaf) for leaf in jax.tree.leaves(y)])
|
|
82
|
+
z_flat = jnp.concatenate([jnp.ravel(leaf) for leaf in jax.tree.leaves(z)])
|
|
83
|
+
return z_flat - y_flat
|
|
84
|
+
|
|
85
|
+
if method == "sdae":
|
|
86
|
+
return lambda y: (
|
|
87
|
+
solve_semi_explicit_sdae(
|
|
88
|
+
lambda state, z: tree_map(lambda leaf: -0.2 * leaf, z),
|
|
89
|
+
lambda state, z: tree_map(
|
|
90
|
+
lambda leaf: 0.1 * jnp.ones_like(leaf), state
|
|
91
|
+
),
|
|
92
|
+
constraint,
|
|
93
|
+
EulerMaruyama(),
|
|
94
|
+
0.0,
|
|
95
|
+
1.0,
|
|
96
|
+
y,
|
|
97
|
+
z_0,
|
|
98
|
+
key=jax.random.key(7),
|
|
99
|
+
n_steps=64,
|
|
100
|
+
).ys
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return lambda y: (
|
|
104
|
+
solve_semi_explicit_dae(
|
|
105
|
+
lambda state, z: tree_map(lambda leaf: -0.2 * leaf, z),
|
|
106
|
+
constraint,
|
|
107
|
+
Tsit5(),
|
|
108
|
+
0.0,
|
|
109
|
+
1.0,
|
|
110
|
+
y,
|
|
111
|
+
z_0,
|
|
112
|
+
dt_0=0.1,
|
|
113
|
+
controller=IController(),
|
|
114
|
+
max_steps=32,
|
|
115
|
+
).ys
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.parametrize("method", ["rk4", "tsit5", "em", "dae", "sdae"])
|
|
120
|
+
@pytest.mark.parametrize("state_kind", ["scalar", "vector16", "tree16"])
|
|
121
|
+
@pytest.mark.parametrize("transform", ["primal", "jvp", "vjp"])
|
|
122
|
+
def test_steady_state(benchmark, method, state_kind, transform):
|
|
123
|
+
initial = make_state(state_kind)
|
|
124
|
+
solve = make_solve(method, initial)
|
|
125
|
+
tangent = tree_map(jnp.ones_like, initial)
|
|
126
|
+
|
|
127
|
+
if transform == "primal":
|
|
128
|
+
run = jax.jit(solve)
|
|
129
|
+
elif transform == "jvp":
|
|
130
|
+
run = jax.jit(lambda x: jax.jvp(solve, (x,), (tangent,)))
|
|
131
|
+
else:
|
|
132
|
+
run = jax.jit(lambda x: jax.grad(lambda value: tree_sum(solve(value)))(x))
|
|
133
|
+
|
|
134
|
+
compiled = run.lower(initial).compile()
|
|
135
|
+
jax.block_until_ready(compiled(initial))
|
|
136
|
+
benchmark.pedantic(
|
|
137
|
+
lambda: jax.block_until_ready(compiled(initial)), rounds=20, iterations=10
|
|
138
|
+
)
|