@@ -364,6 +364,64 @@ def test_jump_at_t1_with_large_t1_in_float32():
364364 assert sol .ts == jnp .array ([t1 ])
365365
366366
367+ # https://github.com/patrick-kidger/diffrax/issues/756
368+ # In float32 with a large t1, the 100-ULP clip window is wide enough to contain
369+ # the PID controller's desired step. If tprev sits just outside the clip window
370+ # but the controller's proposed tnext is inside it, the rejected step must not
371+ # be overridden with a fixed midpoint between tprev and t1: tprev does not move
372+ # on rejection, so a fixed midpoint would be retried forever.
373+ def test_clip_to_end_does_not_override_rejected_step ():
374+ from diffrax ._integrate import _clip_to_end
375+
376+ t1 = jnp .array (600.0 , dtype = jnp .float32 )
377+ t1_clip_floor = t1
378+ for _ in range (100 ):
379+ t1_clip_floor = eqxi .prevbefore (t1_clip_floor )
380+ # tnext lies inside the clip window; the controller has just rejected the
381+ # previous step, so keep_step is False. Expect the controller's tnext to
382+ # pass through unchanged (so the next iteration sees a strictly shrunken
383+ # proposal rather than the same midpoint).
384+ tnext = jnp .array (599.996 , dtype = jnp .float32 )
385+ assert tnext > t1_clip_floor
386+ out = _clip_to_end (tnext , t1 , t1_clip_floor , jnp .array (False ))
387+ assert out == tnext
388+ # On an accepted step we still snap to t1.
389+ out = _clip_to_end (tnext , t1 , t1_clip_floor , jnp .array (True ))
390+ assert out == t1
391+ # And outside the clip window we never touch tnext.
392+ tnext_far = jnp .array (599.0 , dtype = jnp .float32 )
393+ for keep in (jnp .array (True ), jnp .array (False )):
394+ assert _clip_to_end (tnext_far , t1 , t1_clip_floor , keep ) == tnext_far
395+
396+
397+ # End-to-end version of the above: a harmonic oscillator in float32 with t1=600
398+ # and a tolerance that drives the PID controller's desired step into the 100-ULP
399+ # clip window. Before the #756 fix this hung at max_steps with tprev pinned to
400+ # the clip floor; after the fix it completes in ~30 steps.
401+ def test_no_infinite_reject_loop_at_t1_in_float32 ():
402+ t1 = jnp .float32 (600.0 )
403+ omega = jnp .float32 (1000.0 )
404+
405+ def vf (t , y , args ):
406+ y0 , y1 = y
407+ return jnp .stack ([y1 , - omega * omega * y0 ])
408+
409+ sol = diffrax .diffeqsolve (
410+ diffrax .ODETerm (vf ),
411+ diffrax .Tsit5 (),
412+ t0 = jnp .float32 (599.99 ),
413+ t1 = t1 ,
414+ dt0 = None ,
415+ y0 = jnp .array ([1.0 , 0.0 ], dtype = jnp .float32 ),
416+ stepsize_controller = diffrax .PIDController (rtol = 1e-4 , atol = 1e-4 , safety = 0.9 ),
417+ saveat = diffrax .SaveAt (t1 = True ),
418+ max_steps = 1000 ,
419+ )
420+ assert sol .result == diffrax .RESULTS .successful
421+ assert sol .ts is not None
422+ assert sol .ts [- 1 ] == t1
423+
424+
367425# https://github.com/patrick-kidger/diffrax/issues/713
368426def test_t0_at_jump_time ():
369427 jump_time = 0.98
0 commit comments