You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The adaptive timestep solvers currently use a P controler to adjust the timestep given an estimate for the current truncation error. In the literature and in practice, different kinds of controlers have been explored for that such as I, PI and PID controlers.
It would make sense to implement a new class TruncationErrorControler to modularize this and enable different controler types.
Hi @milanofthe, I'd like to take this on. Before implementing, here's the design I'm proposing — happy to adjust based on your input.
Current state: the P-controller logic is currently duplicated 4 times (ExplicitRungeKutta.error_controller, DiagonallyImplicitRungeKutta.error_controller in _rungekutta.py, and two variants in gear.py), each computing scale = beta / error_norm**(1/(min(m,n)+1)), clipped to [SOL_SCALE_MIN, SOL_SCALE_MAX].
Proposed design:
New module solvers/_stepcontrol.py with a small class hierarchy: a base StepController (or should it be TruncationErrorController — note the issue title has a typo, "Controler", happy to go either way) exposing update(error_norm, order) -> scale, plus PController (wraps existing behavior exactly, becomes the default so results don't change for anyone), PIController, and possibly PIDController following Söderlind's digital-filter formulation ("Digital Filters in Adaptive Time-Stepping", 2003).
Composition, not inheritance: each solver (ExplicitRungeKutta, DiagonallyImplicitRungeKutta, GEAR, BDF) holds a self._step_controller instance instead of reimplementing the scale formula. The controller owns whatever state it needs (e.g. previous error norm(s) for PI/PID) and gets reset alongside revert().
Backward compatibility: default controller stays PController with the current beta=0.9, so no existing test or simulation output changes unless a user opts into a different controller.
Open question for you: should the controller type be user-configurable (e.g. RKF45(..., controller='PI')), or is this purely an internal refactor for now with PI/PID added as a later step?
Let me know if this direction makes sense before I start implementing.
Activity
miguelpi314 commentedon Jul 4, 2026
Hi @milanofthe, I'd like to take this on. Before implementing, here's the design I'm proposing — happy to adjust based on your input.
Current state: the P-controller logic is currently duplicated 4 times (
ExplicitRungeKutta.error_controller,DiagonallyImplicitRungeKutta.error_controllerin_rungekutta.py, and two variants ingear.py), each computingscale = beta / error_norm**(1/(min(m,n)+1)), clipped to[SOL_SCALE_MIN, SOL_SCALE_MAX].Proposed design:
solvers/_stepcontrol.pywith a small class hierarchy: a baseStepController(or should it beTruncationErrorController— note the issue title has a typo, "Controler", happy to go either way) exposingupdate(error_norm, order) -> scale, plusPController(wraps existing behavior exactly, becomes the default so results don't change for anyone),PIController, and possiblyPIDControllerfollowing Söderlind's digital-filter formulation ("Digital Filters in Adaptive Time-Stepping", 2003).ExplicitRungeKutta,DiagonallyImplicitRungeKutta,GEAR,BDF) holds aself._step_controllerinstance instead of reimplementing the scale formula. The controller owns whatever state it needs (e.g. previous error norm(s) for PI/PID) and gets reset alongsiderevert().PControllerwith the currentbeta=0.9, so no existing test or simulation output changes unless a user opts into a different controller.RKF45(..., controller='PI')), or is this purely an internal refactor for now with PI/PID added as a later step?Let me know if this direction makes sense before I start implementing.