diff --git a/diffrax/_root_finder/_verychord.py b/diffrax/_root_finder/_verychord.py index f58fcb44..684bf9a7 100644 --- a/diffrax/_root_finder/_verychord.py +++ b/diffrax/_root_finder/_verychord.py @@ -157,7 +157,10 @@ def terminate( at_least_two = state.step >= 2 rate = state.diffsize / state.diffsize_prev factor = state.diffsize * rate / (1 - rate) - small = _small(state.diffsize) + # An increment well below the tolerances has converged, whatever its ratio to + # the previous one. (If the initial guess is already exact then the increments + # are just rounding errors, and their ratio is meaningless.) + small = _small(state.diffsize) | (state.diffsize < self.kappa) diverged = _diverged(rate) converged = _converged(factor, self.kappa) terminate = at_least_two & (small | diverged | converged) diff --git a/test/test_very_chord.py b/test/test_very_chord.py index 17424630..e710b616 100644 --- a/test/test_very_chord.py +++ b/test/test_very_chord.py @@ -84,3 +84,24 @@ def _assert_shape(a, b): ) assert sol.result == optx.RESULTS.successful assert tree_allclose(fn(sol.value, args), zero, rtol=tol, atol=tol) + + +def test_exact_prediction(): + # The vector field is almost constant, so the prediction of each implicit stage is + # exact up to rounding error. The chord increments are then just rounding errors, + # and their ratio should not be read as a divergence. See #782. + term = diffrax.ODETerm(lambda t, y, args: (t / 60 / (y / 0.01) + 3) * 0.01 / 60) + stepsize_controller = diffrax.PIDController(rtol=1e-10, atol=1e-12, dtmax=0.2) + sol = diffrax.diffeqsolve( + term, + diffrax.Kvaerno5(), + 0.0, + 10.0, + None, + jnp.array([1.0]), + stepsize_controller=stepsize_controller, + max_steps=2000, + throw=False, + ) + assert sol.result == diffrax.RESULTS.successful + assert sol.stats["num_rejected_steps"] == 0