diffrax-bdf 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.
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.3
2
+ Name: diffrax-bdf
3
+ Version: 0.1.0
4
+ Summary: Diffrax-compatible backward differentiation formula ODE solver
5
+ Author-email: groves.teddy@gmail.com
6
+ License: MIT
7
+ Requires-Dist: diffrax>=0.7.2
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # diffrax-bdf
12
+
13
+ A [backward differentiation formula](https://en.wikipedia.org/wiki/Backward_differentiation_formula)
14
+ solver for [diffrax](https://github.com/patrick-kidger/diffrax), aimed at stiff
15
+ steady-state problems — in particular the kinetic reaction networks in
16
+ [enzax](https://github.com/dtu-qmcm/enzax).
17
+
18
+ diffrax has no BDF ([#8](https://github.com/patrick-kidger/diffrax/issues/8)). This
19
+ package supplies one as an ordinary `AbstractSolver` subclass, with no patching of
20
+ diffrax itself.
21
+
22
+ ```python
23
+ import diffrax
24
+ import jax.numpy as jnp
25
+ from diffrax_bdf import BDF, BDFController
26
+
27
+ sol = diffrax.diffeqsolve(
28
+ diffrax.ODETerm(vector_field),
29
+ BDF(), # variable order 1-5, as scipy and CVODE do
30
+ t0=0.0,
31
+ t1=jnp.inf,
32
+ dt0=1e-6,
33
+ y0=guess,
34
+ args=parameters,
35
+ stepsize_controller=BDFController(rtol=1e-8, atol=1e-10, dtmax=1e6),
36
+ event=diffrax.Event(diffrax.steady_state_event()),
37
+ adjoint=diffrax.ImplicitAdjoint(),
38
+ saveat=diffrax.SaveAt(t1=True),
39
+ )
40
+ ```
41
+
42
+ Pin the order with `BDF(bdf_order=3)` to use any adaptive step size controller,
43
+ including diffrax's own `PIDController`.
44
+
45
+ ## Status
46
+
47
+ Variable order 1-5, adaptive step size, reuse of the Jacobian and its
48
+ factorisation across steps, and semi-explicit index-1 DAEs.
49
+
50
+ ## Method
51
+
52
+ The formulation follows `scipy.integrate.BDF` and MATLAB's `ode15s`: the history is
53
+ a backward-difference array rescaled on each change of step size, with the Shampine
54
+ & Reichelt NDF correction available via `use_ndf` (`False` gives the classical BDF
55
+ coefficients, the family SUNDIALS CVODE implements). The order is chosen each step
56
+ by scipy's heuristic, comparing the error estimate at the current order against what
57
+ it would have been one order lower and one order higher.
58
+
59
+ The linear-algebra reuse policy is CVODE's, because that is where CVODE's advantage
60
+ over scipy actually lives. `Kvaerno5` already computes one Jacobian per step and
61
+ reuses it across its five stages, so "one Newton solve per step instead of five
62
+ stage solves" buys only a few linear solves. The real saving is keeping `J` and the
63
+ factorisation of `I - c * J` across *steps*, gated on two separate counters:
64
+
65
+ - the factorisation is rebuilt when `c` has moved by more than `max_gamma_change`
66
+ (0.3), or after `lu_reuse_steps` (20) accepted steps;
67
+ - the Jacobian itself is rebuilt only after `jac_reuse_steps` (51) accepted steps,
68
+ so a refactorisation reassembles `I - c * J` from the stored `J` without touching
69
+ the vector field.
70
+
71
+ `BDFController` adds CVODE's step-size deadband on top of `diffrax.PIDController`:
72
+ an accepted step keeps its step size unless the controller wants to grow it by at
73
+ least 1.5x, and never shrinks it. Holding `h` still holds `c` still, which is what
74
+ lets the factorisation survive.
75
+
76
+ It also carries the order. diffrax asks a solver for its error order once, before
77
+ the solve (`solver.error_order(terms)` sees neither state nor time), so a
78
+ variable-order solver cannot report a per-step order through that route. The only
79
+ other channel to the controller is `y_error`, which the integration loop treats
80
+ opaquely, so the order travels inside it.
81
+
82
+ Everything is written for static shapes -- the order is a *traced* integer used to
83
+ mask fixed-size buffers -- because diffrax carries the solver state through a
84
+ `lax.while_loop`.
85
+
86
+ ## Order selection on a steady-state solve
87
+
88
+ Integrating the `n=3` problem below to steady state, the order climbs through the
89
+ transient and then falls back as the residual collapses and the step grows:
90
+
91
+ ```
92
+ order: 1 2 2 3 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 2 1 1 1 1 1 1 1
93
+ step: 1.2e-07 ------------------------------------------------> 7.9e+05
94
+ ```
95
+
96
+ The tail is the useful part: BDF1 with an enormous step *is* Newton's method on
97
+ `f(y) = 0`, so a correct variable-order BDF turns into a steady-state solver by
98
+ itself as it converges.
99
+
100
+ ## Differential algebraic equations
101
+
102
+ `BDF` also solves semi-explicit index-1 DAEs, where some components of `y` obey differential equations and the rest obey algebraic constraints:
103
+
104
+ ```python
105
+ term = SemiExplicitDAETerm(vector_field, algebraic_mask=(False, True))
106
+ ```
107
+
108
+ Here `vector_field` returns `(f, g)` for `y = (z, u)`, meaning `dz/dt = f` and `0 = g`. The corrector solves the constraints in the same chord iteration as the differential equations, with iteration matrix `M − cJ` where `M` is 1 on differential components and 0 on algebraic ones, and reuses its factorisation across steps as for an ODE.
109
+
110
+ - By default `init` corrects an inconsistent initial value by a Newton solve on the constraints. diffrax keeps its own copy of `y0`, so `SaveAt(t0=True)` shows the value you passed, not the corrected one.
111
+ - `suppress_algebraic_error=True` drops the algebraic components from the error estimate, which suits problems where only `z` is of interest. On a rapid-equilibrium steady state it saved 29% of steps.
112
+ - The steady-state event and `ImplicitAdjoint` work unchanged: the steady state is the root of `(f, g)`.
113
+
114
+ ## Caveats
115
+
116
+ - **Enable `jax_enable_x64`.** High-order backward differences suffer heavy
117
+ cancellation; above order 2 they are noise in float32.
118
+ - **Set `dtmax` when integrating to `t1=inf`.** As the residual collapses the
119
+ controller's growth factor saturates and the step grows without bound until it
120
+ overflows the time variable.
121
+ - Variable order requires `BDFController`; a fixed `bdf_order` works with any
122
+ adaptive controller.
123
+ - The Jacobian is formed densely, so this suits small to medium systems.
124
+ - Requires a term whose control is a scalar, i.e. `ODETerm` or `SemiExplicitDAETerm`.
125
+
126
+ ## Performance
127
+
128
+ Steady-state solves to `rtol=1e-8`, CPU, float64, medians of interleaved rounds.
129
+ Compare within a block, not across them -- absolute timings drift between runs.
130
+
131
+ **Whether BDF beats `Kvaerno5` depends entirely on the problem.** Where `Kvaerno5`'s
132
+ step control struggles, BDF wins by a wide margin; where it does not, `Kvaerno5`
133
+ wins on step count.
134
+
135
+ | problem | solver | steps | rejected | median |
136
+ |---|---|---|---|---|
137
+ | fast equilibrium, n=3 | `Kvaerno5` | 7784 | 4563 | 192.4 ms |
138
+ | fast equilibrium, n=3 | `BDF(bdf_order=5)` | 276 | 14 | 3.5 ms |
139
+ | fast equilibrium, n=3 | `BDF()` variable | 231 | 6 | **3.3 ms** |
140
+ | stiff random network, n=18 | `Kvaerno5` | 212 | 14 | **23.6 ms** |
141
+ | stiff random network, n=18 | `BDF(bdf_order=5)` | 593 | 11 | 28.8 ms |
142
+ | stiff random network, n=18 | `BDF()` variable | 589 | 12 | 31.7 ms |
143
+ | stiff random network, n=60 | `Kvaerno5` | 193 | 7 | **73.6 ms** |
144
+ | stiff random network, n=60 | `BDF(bdf_order=5)` | 586 | 6 | **145.2 ms** |
145
+ | stiff random network, n=60 | `BDF()` variable | 580 | 7 | 153.7 ms |
146
+
147
+ The first problem is the interesting one: `Kvaerno5` rejects 59% of its steps, where
148
+ BDF rejects 2-5%.
149
+
150
+ Variable order helps where the order genuinely needs to move -- 231 steps against
151
+ 276 for the best fixed order on the steady-state problem. On the random networks,
152
+ where order 5 is right throughout, it matches fixed order 5 on step count and pays
153
+ about 8% per step for the heuristic. Reuse is worth a further 16-18%.
154
+
155
+ ## On enzax's models
156
+
157
+ The motivating case. Measured against
158
+ [enzax](https://github.com/dtu-qmcm/enzax)'s own steady-state configuration
159
+ (`t1=inf`, `steady_state_event`, `ImplicitAdjoint`, `rtol=atol=1e-9`), each solver
160
+ with the step size controller that suits it. Medians of interleaved rounds.
161
+
162
+ | model | n | regime | `Kvaerno5` (enzax today) | `BDF()` | speedup |
163
+ |---|---|---|---|---|---|
164
+ | methionine | 5 | bad guess | 555 ms (649 steps, 145 rejected) | 158 ms (423, 52) | **3.5x** |
165
+ | methionine | 5 | warm guess, 2% parameter jitter | 30.7 ms (38 steps) | 23.3 ms (88) | 1.3x |
166
+ | methionine | 5 | `jacrev` through the solve | 27.9 ms | 22.4 ms | 1.25x |
167
+ | conserved moiety | 5 | bad guess | 191 ms (225, 30) | 53.7 ms (293, 4) | **3.6x** |
168
+ | conserved moiety | 5 | warm guess, 2% jitter | 58.3 ms (68) | 31.4 ms (165) | 1.9x |
169
+ | conserved moiety | 5 | `jacrev` | 53.0 ms | 27.6 ms | 1.9x |
170
+ | glycolysis | 18 | bad guess | 2302 ms (1007, 247) | 271 ms (821, 70) | **8.5x** |
171
+ | glycolysis | 18 | guess perturbed 20% | 491 ms (212, 38) | 108 ms (277, 18) | **4.6x** |
172
+ | glycolysis | 18 | `jacrev` | 427 ms | 104 ms | **4.1x** |
173
+
174
+ "bad guess" is enzax's own `BAD_GUESS = full(shape, 0.01)`. Steady states agree with
175
+ `Kvaerno5` to 2e-7 relative or better and gradients to 1e-5 or better.
176
+
177
+ Note the step counts: BDF often takes *more* steps and is still several times
178
+ faster, because a step is one Newton solve against a reused factorisation rather
179
+ than five stage solves. It also rejects far fewer steps -- 70 against 247 on
180
+ glycolysis from a bad guess. Compile time is 30-45% lower throughout, which matters
181
+ when every leapfrog step of a NUTS chain pays it.
182
+
183
+ The gap widens with model size, which is the encouraging part: 1.3x on the 5-state
184
+ methionine model against 4-8x on the 18-state glycolysis one.
185
+
186
+ ### Tuning
187
+
188
+ enzax's `PIDController(pcoeff=0.1, icoeff=0.3)` is tuned for `Kvaerno5`, which
189
+ rejects heavily; it damps step size growth to `0.4 / error_order` where the default
190
+ pure-I controller uses `1 / error_order`. BDF rejects very few steps and does not
191
+ need the damping, so it should be left on `BDFController`'s defaults. Using enzax's
192
+ tuning costs BDF roughly a factor of two -- 174 steps and 45.6 ms against 88 steps
193
+ and 26.9 ms on methionine.
194
+
195
+ ## Testing
196
+
197
+ `uv run pytest`. scipy's BDF is pure Python and is used as a white-box oracle: the
198
+ coefficient tables, the difference-array rescaling, and a single pinned step are all
199
+ asserted against it directly. The reuse policy, the deadband and the order-selection
200
+ heuristic are tested directly as well, since without them the solver has no
201
+ advantage over `Kvaerno5` at all.
@@ -0,0 +1,191 @@
1
+ # diffrax-bdf
2
+
3
+ A [backward differentiation formula](https://en.wikipedia.org/wiki/Backward_differentiation_formula)
4
+ solver for [diffrax](https://github.com/patrick-kidger/diffrax), aimed at stiff
5
+ steady-state problems — in particular the kinetic reaction networks in
6
+ [enzax](https://github.com/dtu-qmcm/enzax).
7
+
8
+ diffrax has no BDF ([#8](https://github.com/patrick-kidger/diffrax/issues/8)). This
9
+ package supplies one as an ordinary `AbstractSolver` subclass, with no patching of
10
+ diffrax itself.
11
+
12
+ ```python
13
+ import diffrax
14
+ import jax.numpy as jnp
15
+ from diffrax_bdf import BDF, BDFController
16
+
17
+ sol = diffrax.diffeqsolve(
18
+ diffrax.ODETerm(vector_field),
19
+ BDF(), # variable order 1-5, as scipy and CVODE do
20
+ t0=0.0,
21
+ t1=jnp.inf,
22
+ dt0=1e-6,
23
+ y0=guess,
24
+ args=parameters,
25
+ stepsize_controller=BDFController(rtol=1e-8, atol=1e-10, dtmax=1e6),
26
+ event=diffrax.Event(diffrax.steady_state_event()),
27
+ adjoint=diffrax.ImplicitAdjoint(),
28
+ saveat=diffrax.SaveAt(t1=True),
29
+ )
30
+ ```
31
+
32
+ Pin the order with `BDF(bdf_order=3)` to use any adaptive step size controller,
33
+ including diffrax's own `PIDController`.
34
+
35
+ ## Status
36
+
37
+ Variable order 1-5, adaptive step size, reuse of the Jacobian and its
38
+ factorisation across steps, and semi-explicit index-1 DAEs.
39
+
40
+ ## Method
41
+
42
+ The formulation follows `scipy.integrate.BDF` and MATLAB's `ode15s`: the history is
43
+ a backward-difference array rescaled on each change of step size, with the Shampine
44
+ & Reichelt NDF correction available via `use_ndf` (`False` gives the classical BDF
45
+ coefficients, the family SUNDIALS CVODE implements). The order is chosen each step
46
+ by scipy's heuristic, comparing the error estimate at the current order against what
47
+ it would have been one order lower and one order higher.
48
+
49
+ The linear-algebra reuse policy is CVODE's, because that is where CVODE's advantage
50
+ over scipy actually lives. `Kvaerno5` already computes one Jacobian per step and
51
+ reuses it across its five stages, so "one Newton solve per step instead of five
52
+ stage solves" buys only a few linear solves. The real saving is keeping `J` and the
53
+ factorisation of `I - c * J` across *steps*, gated on two separate counters:
54
+
55
+ - the factorisation is rebuilt when `c` has moved by more than `max_gamma_change`
56
+ (0.3), or after `lu_reuse_steps` (20) accepted steps;
57
+ - the Jacobian itself is rebuilt only after `jac_reuse_steps` (51) accepted steps,
58
+ so a refactorisation reassembles `I - c * J` from the stored `J` without touching
59
+ the vector field.
60
+
61
+ `BDFController` adds CVODE's step-size deadband on top of `diffrax.PIDController`:
62
+ an accepted step keeps its step size unless the controller wants to grow it by at
63
+ least 1.5x, and never shrinks it. Holding `h` still holds `c` still, which is what
64
+ lets the factorisation survive.
65
+
66
+ It also carries the order. diffrax asks a solver for its error order once, before
67
+ the solve (`solver.error_order(terms)` sees neither state nor time), so a
68
+ variable-order solver cannot report a per-step order through that route. The only
69
+ other channel to the controller is `y_error`, which the integration loop treats
70
+ opaquely, so the order travels inside it.
71
+
72
+ Everything is written for static shapes -- the order is a *traced* integer used to
73
+ mask fixed-size buffers -- because diffrax carries the solver state through a
74
+ `lax.while_loop`.
75
+
76
+ ## Order selection on a steady-state solve
77
+
78
+ Integrating the `n=3` problem below to steady state, the order climbs through the
79
+ transient and then falls back as the residual collapses and the step grows:
80
+
81
+ ```
82
+ order: 1 2 2 3 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 2 1 1 1 1 1 1 1
83
+ step: 1.2e-07 ------------------------------------------------> 7.9e+05
84
+ ```
85
+
86
+ The tail is the useful part: BDF1 with an enormous step *is* Newton's method on
87
+ `f(y) = 0`, so a correct variable-order BDF turns into a steady-state solver by
88
+ itself as it converges.
89
+
90
+ ## Differential algebraic equations
91
+
92
+ `BDF` also solves semi-explicit index-1 DAEs, where some components of `y` obey differential equations and the rest obey algebraic constraints:
93
+
94
+ ```python
95
+ term = SemiExplicitDAETerm(vector_field, algebraic_mask=(False, True))
96
+ ```
97
+
98
+ Here `vector_field` returns `(f, g)` for `y = (z, u)`, meaning `dz/dt = f` and `0 = g`. The corrector solves the constraints in the same chord iteration as the differential equations, with iteration matrix `M − cJ` where `M` is 1 on differential components and 0 on algebraic ones, and reuses its factorisation across steps as for an ODE.
99
+
100
+ - By default `init` corrects an inconsistent initial value by a Newton solve on the constraints. diffrax keeps its own copy of `y0`, so `SaveAt(t0=True)` shows the value you passed, not the corrected one.
101
+ - `suppress_algebraic_error=True` drops the algebraic components from the error estimate, which suits problems where only `z` is of interest. On a rapid-equilibrium steady state it saved 29% of steps.
102
+ - The steady-state event and `ImplicitAdjoint` work unchanged: the steady state is the root of `(f, g)`.
103
+
104
+ ## Caveats
105
+
106
+ - **Enable `jax_enable_x64`.** High-order backward differences suffer heavy
107
+ cancellation; above order 2 they are noise in float32.
108
+ - **Set `dtmax` when integrating to `t1=inf`.** As the residual collapses the
109
+ controller's growth factor saturates and the step grows without bound until it
110
+ overflows the time variable.
111
+ - Variable order requires `BDFController`; a fixed `bdf_order` works with any
112
+ adaptive controller.
113
+ - The Jacobian is formed densely, so this suits small to medium systems.
114
+ - Requires a term whose control is a scalar, i.e. `ODETerm` or `SemiExplicitDAETerm`.
115
+
116
+ ## Performance
117
+
118
+ Steady-state solves to `rtol=1e-8`, CPU, float64, medians of interleaved rounds.
119
+ Compare within a block, not across them -- absolute timings drift between runs.
120
+
121
+ **Whether BDF beats `Kvaerno5` depends entirely on the problem.** Where `Kvaerno5`'s
122
+ step control struggles, BDF wins by a wide margin; where it does not, `Kvaerno5`
123
+ wins on step count.
124
+
125
+ | problem | solver | steps | rejected | median |
126
+ |---|---|---|---|---|
127
+ | fast equilibrium, n=3 | `Kvaerno5` | 7784 | 4563 | 192.4 ms |
128
+ | fast equilibrium, n=3 | `BDF(bdf_order=5)` | 276 | 14 | 3.5 ms |
129
+ | fast equilibrium, n=3 | `BDF()` variable | 231 | 6 | **3.3 ms** |
130
+ | stiff random network, n=18 | `Kvaerno5` | 212 | 14 | **23.6 ms** |
131
+ | stiff random network, n=18 | `BDF(bdf_order=5)` | 593 | 11 | 28.8 ms |
132
+ | stiff random network, n=18 | `BDF()` variable | 589 | 12 | 31.7 ms |
133
+ | stiff random network, n=60 | `Kvaerno5` | 193 | 7 | **73.6 ms** |
134
+ | stiff random network, n=60 | `BDF(bdf_order=5)` | 586 | 6 | **145.2 ms** |
135
+ | stiff random network, n=60 | `BDF()` variable | 580 | 7 | 153.7 ms |
136
+
137
+ The first problem is the interesting one: `Kvaerno5` rejects 59% of its steps, where
138
+ BDF rejects 2-5%.
139
+
140
+ Variable order helps where the order genuinely needs to move -- 231 steps against
141
+ 276 for the best fixed order on the steady-state problem. On the random networks,
142
+ where order 5 is right throughout, it matches fixed order 5 on step count and pays
143
+ about 8% per step for the heuristic. Reuse is worth a further 16-18%.
144
+
145
+ ## On enzax's models
146
+
147
+ The motivating case. Measured against
148
+ [enzax](https://github.com/dtu-qmcm/enzax)'s own steady-state configuration
149
+ (`t1=inf`, `steady_state_event`, `ImplicitAdjoint`, `rtol=atol=1e-9`), each solver
150
+ with the step size controller that suits it. Medians of interleaved rounds.
151
+
152
+ | model | n | regime | `Kvaerno5` (enzax today) | `BDF()` | speedup |
153
+ |---|---|---|---|---|---|
154
+ | methionine | 5 | bad guess | 555 ms (649 steps, 145 rejected) | 158 ms (423, 52) | **3.5x** |
155
+ | methionine | 5 | warm guess, 2% parameter jitter | 30.7 ms (38 steps) | 23.3 ms (88) | 1.3x |
156
+ | methionine | 5 | `jacrev` through the solve | 27.9 ms | 22.4 ms | 1.25x |
157
+ | conserved moiety | 5 | bad guess | 191 ms (225, 30) | 53.7 ms (293, 4) | **3.6x** |
158
+ | conserved moiety | 5 | warm guess, 2% jitter | 58.3 ms (68) | 31.4 ms (165) | 1.9x |
159
+ | conserved moiety | 5 | `jacrev` | 53.0 ms | 27.6 ms | 1.9x |
160
+ | glycolysis | 18 | bad guess | 2302 ms (1007, 247) | 271 ms (821, 70) | **8.5x** |
161
+ | glycolysis | 18 | guess perturbed 20% | 491 ms (212, 38) | 108 ms (277, 18) | **4.6x** |
162
+ | glycolysis | 18 | `jacrev` | 427 ms | 104 ms | **4.1x** |
163
+
164
+ "bad guess" is enzax's own `BAD_GUESS = full(shape, 0.01)`. Steady states agree with
165
+ `Kvaerno5` to 2e-7 relative or better and gradients to 1e-5 or better.
166
+
167
+ Note the step counts: BDF often takes *more* steps and is still several times
168
+ faster, because a step is one Newton solve against a reused factorisation rather
169
+ than five stage solves. It also rejects far fewer steps -- 70 against 247 on
170
+ glycolysis from a bad guess. Compile time is 30-45% lower throughout, which matters
171
+ when every leapfrog step of a NUTS chain pays it.
172
+
173
+ The gap widens with model size, which is the encouraging part: 1.3x on the 5-state
174
+ methionine model against 4-8x on the 18-state glycolysis one.
175
+
176
+ ### Tuning
177
+
178
+ enzax's `PIDController(pcoeff=0.1, icoeff=0.3)` is tuned for `Kvaerno5`, which
179
+ rejects heavily; it damps step size growth to `0.4 / error_order` where the default
180
+ pure-I controller uses `1 / error_order`. BDF rejects very few steps and does not
181
+ need the damping, so it should be left on `BDFController`'s defaults. Using enzax's
182
+ tuning costs BDF roughly a factor of two -- 174 steps and 45.6 ms against 88 steps
183
+ and 26.9 ms on methionine.
184
+
185
+ ## Testing
186
+
187
+ `uv run pytest`. scipy's BDF is pure Python and is used as a white-box oracle: the
188
+ coefficient tables, the difference-array rescaling, and a single pinned step are all
189
+ asserted against it directly. The reuse policy, the deadband and the order-selection
190
+ heuristic are tested directly as well, since without them the solver has no
191
+ advantage over `Kvaerno5` at all.
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "diffrax-bdf"
3
+ version = "0.1.0"
4
+ description = "Diffrax-compatible backward differentiation formula ODE solver"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = ["diffrax>=0.7.2"]
8
+
9
+ [[project.authors]]
10
+ email = "groves.teddy@gmail.com"
11
+
12
+ [project.license]
13
+ text = "MIT"
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.12.7,<0.13.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [dependency-groups]
20
+ dev = ["pytest>=9.1.1"]
21
+
22
+ [tool.pytest.ini_options]
23
+ pythonpath = ["tests"]
24
+ testpaths = ["tests"]
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "diffrax-bdf"
3
+ version = "0.1.0"
4
+ description = "Diffrax-compatible backward differentiation formula ODE solver"
5
+ readme = "README.md"
6
+ authors = [
7
+ { email = "groves.teddy@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "diffrax>=0.7.2",
12
+ ]
13
+ license = {text = "MIT"}
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.12.7,<0.13.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=9.1.1",
22
+ ]
23
+
24
+ [tool.pytest.ini_options]
25
+ pythonpath = ["tests"]
26
+ testpaths = ["tests"]
@@ -0,0 +1,7 @@
1
+ from ._bdf import BDF as BDF, ErrorWithOrder as ErrorWithOrder
2
+ from ._controller import BDFController as BDFController
3
+ from ._coeffs import MAX_ORDER as MAX_ORDER
4
+ from ._term import SemiExplicitDAETerm as SemiExplicitDAETerm
5
+
6
+
7
+ __version__ = "0.1.0"