Design of callback for scipy.integrate.solve_ivp
(original) (raw)
January 13, 2025, 5:06pm 1
I have opened PR #21741 to add callbacks in scipy.integrate.solve_ivp
for use cases such as progress tracking and dynamic max step size control. This callback is not currently intended to directly control the state or RHS of the integration as currently solve_ivp
is not equipped to handle this.
The current discussion has aligned on passing the callback an instance of class IntermediateOdeResult
(current naming) inheriting from _RichResult
It would contain attributes that have information about the integration, e.g. time, state, nsteps, etc.
In review, there was a desire to open a discussion on when the callback should be called and what information should be supplied to the callback in those cases. I am trying to distill a few different pieces of the conversation down, and the list is meant as a starting point for discussion.
First, some definitions used in the options related to timing/type of callback:
- after-step callback: callback that is called after every (successful) integration step of the algorithm.
- upon-event callback: callback that is called when any event occurs
- upon-event-type callback: callback that is called only when a specific event occurs
More definitions:
t_current
: current integration time after stepy_current
current integration state after stept
: history of time for all steps including last stepy
: history of state for all steps including last stept_events_current
: times of events that occured at last stepy_events_current
: states of system at times in above linet_events
history of times of events for all steps including last stepy_events
history of states of system at times in above lineevent_id
index of a specific event that occured over last stepevent_indices
indices of all events that occured over last stepcontext
what triggered the callback
Options:
- Only use a single after-step callback with history: include
t
,y
,t_events
,y_events
,event_indices
. Thecurrent
values could also be passed for convenience. - Only use a single after-step callback without history: include
t_current
,y_current
,t_events_current
,y_events_current
,event_indices
. - Use a single callback, and call it after-step and upon-event without history.
- Using
context
to distinguish what triggered callback.
- Using
- Use a single callback, and call it after-step and upon-event-type without history.
- Similar to 3, but it would be called per step type.
context
could be the same asevent_id
in this case.
- Similar to 3, but it would be called per step type.
- Use different callbacks for after-step and upon-event without history.
- No need for
context
.
- No need for
- Use different callbacks for after-step and upon-event-type without history.
- Need
event_id
and could be same ascontext
. - 1 callback per event type could also be considered.
event_id
/context
is possibly unneeded, but could be useful.
- Need
I chose to include with history for option 1, but it could potentially be considered for any option 3-6, particularly for the after-step context.
For options 3-6 we could choose to limit the information available to the different types of callback calls, e.g. only after step information in after-step context and only the state at the time of event in the upon-event context. Or we could chose to provide similar types of information to all contexts.
My preference/proposal is to implement option 2. And if desired, option 1 and /or option 5 or 6 could be implemented later. Option 2 is the minimal viable product for all other options except Options 3-4. Implementing Options 3-4 after Option 2 would result in the changing the number of times the callback is called.
One problem of passing the history of solution, Option 1, in the intermediate result to the callback is that it is a different shape and type than the final result in solve_ivp
. The intermediate result y
is a list that contains the states of the solution at each step, i.e. the shape if converted to an ndarray
is (nsteps, n)
where n
is the number of state variables being integrated (n=len(y0)
). The return from solve_ivp
is an ndarray
that has shape (n, nsteps)
. This will make it confusing to users. It could be corrected by transposing the result after every step, however this might have performance/memory issues. Additionally, if users need the history, they can construct it themselves with the callback.
For Options 3-6, they are only for convenience as all functionality can be achieved in the after-step callback context if the event information is passed. In solve_ivp
the events are found after the step is completed, and the state of the solver will be the same when the callback is called in both after-step and upon-event(-type) contexts. Using Options 3-6 adds extra overhead to maintain and also describe in the documentation.
Using a single callback that is called after-step and again upon-event(-type), Options 3 and 4, adds the most complexity to the usage IMO. A single call back that is called multiple times with contexts adds more complexity to the user code if they want to do something different for after-step vs. upon-event compared to Options 5-6.
I like the idea of Options 5 and/or 6 as it reduces the complexity of the users code if they want to different things during after-step vs. upon-event(-type). However, they also increase the # of parameter in solve_ivp
and thus the complexity of documentation. Since these can be done after Option 2 (or 1), it is best to wait until feedback is gained from the community before implementing it.