-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: integrate.solve_ivp: allow event terminal
attribute to specify maximum number of events
#14771
Conversation
Probably SciPy
|
Thanks, I have updated the test file, happy to have your feedback about it. |
This implementation looks good to me. I'll defer to Tyler on the deprecation cycle. |
Hi @tylerjereddy, Considering @drhagen is supportive of the new implementation and that CI is green except for a failure on CircleCI that I cannot manage to reproduce locally and I believe is unrelated to the changes proposed here, do you think we can move towards merging this PR if I update the SciPy target version to 1.10? Thanks for your input. |
It looks like the |
Yes, please use |
…r to allow for integration termination to occur only after the event was triggered a given amount of times.
…the 'terminate_after' attribute.
…onal check within test_events to ensure multiple events can be captured before termination.
…d with deprecation; Update tests.
b3b80fd
to
c5e63ce
Compare
@patol what do you think about just adding a callback function, since that has also been requested, and that could take care of |
I see the idea. You would have access to the solution object after every integration step within the callback function and be able to interact with it as you wish. However, regarding the |
The idea was to give examples of how to use
For that, I thought I agree that, say, toggling |
@mdhaber Sorry, I never got to properly consider implementing a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a test that checks the deprecation warning is produced is terminal
is used (if you decide to go ahead)
scipy/integrate/_ivp/ivp.py
Outdated
terminal = event.terminal | ||
warnings.warn( | ||
"The 'terminal' event function attribute is deprecated and " | ||
"will be removed in SciPy release 1.11.0. Please use the new " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"will be removed in SciPy release 1.11.0. Please use the new " | |
"will be removed in SciPy release 1.13.0. Please use the new " |
I'm open to other opinions, but I've seen a lot of specialized feature requests in integrate and optimize that could be resolved if the relevant functions had a callback interface, so I think that a callback interface would be more efficient in terms of developer/reviewer time and API. |
I think I will be willing to look into implementing this Continuing on the |
Maybe. (But this would not be a reason to withhold such a capability from the function interface.)
Sometimes I would prefer that we have only one interface instead of two (e.g.
When I posted that, I thought that the
Why not? In |
I might be able to help with a callback feature (although I am also pretty swamped in the near term). I once hacked such an interface by overriding |
The main purpose of exposing the object-oriented interface solver interface (the step solvers) was to make it easy for users to implement whatever specialized features they wanted. |
Familiarity is probably a big part of it. In addition to what you wrote about Matlab's ODE solvers and
Even if one knows about the OO option, That said, I wouldn't object to a consensus that |
Would a callback allow something like this to work? A recent problem I have been working on is detecting steady state cyclic behavior in an ODE solution. One way to do this is to keep track of some solution property in different time windows, e.g. we can integrate over different time windows, and if those are constant within a tolerance, terminate. Right now the only solution seems to be solve over some interval, do these analyses, and if they are not satisfied then restart the integrator at the last position. Its not currently possible to accumulate events from an event function because it is called many times in the root finding code to locate the event. |
As much as it pains me to say this as a scipy maintainer, I would recommend checking out BifurcationKit.jl. It has implementations of the shooting, colocation and trapezoid methods for detecting periodic orbits/limit cycles in dynamical systems and is really quite nice to use. https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/periodicOrbit/ |
It looks like @mdhaber is suggesting consideration of an alternative approach that uses a callback design, and I don't see a completely unified agreement about things in the discussion just yet, so I'd be hesitant to merge this. I'll bump the milestone to |
Still unresolved, bumping milestone again ahead of |
@j-bowhay Since it doesn't look like |
👍 Sound much easier to review! |
scipy/integrate/_ivp/ivp.py
Outdated
@@ -1,5 +1,6 @@ | |||
import inspect | |||
import numpy as np | |||
import warnings |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import warnings |
terminal
attribute by max_events
in solve_ivp
terminal
attribute to specify maximum number of events
@j-bowhay would you be willing to take a look at this before 1.13 branches? I simplified |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Core logic lgtm, one query and one nonblocking suggestion
scipy/integrate/_ivp/ivp.py
Outdated
direction = np.empty(len(events)) | ||
for i, event in enumerate(events): | ||
terminal = int(event.terminal) if hasattr(event, 'terminal') else np.inf | ||
max_events[i] = terminal if terminal > 0 else np.inf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for not raising an error here for negative values? If I passed nonsense I think I'd rather know about it rather than silently receive some other behaviour
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I write new functions, I am pretty careful about this sort of thing. When I work on old functions - especially for minor work like this - I consider how it will affect the overall quality of the function's input validation and try to balance that against simplicity of the PR. Currently, for example:
- If
event.direction=None
, no events are detected. - If
event.terminal = 'no'
, then the function terminates after the first event.
No errors are raised and these behavior are surprising, so I didn't think treating negative values like 0 made the function's edges substantially rougher.
However, seeing now that the current behavior for event.terminal = -1
is also to cause the function to terminate after the first event, I agree that we should raise the error instead of changing that behavior. I'll add the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I'll also replace the ternary and hasattr
with getattr
.
Thanks @j-bowhay. Fixed that and added a test. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… maximum number of events (scipy#14771) * ENH: The terminal attribute of an event function can now be an integer to allow for integration termination to occur only after the event was triggered a given amount of times. * ENH: Deprecate the 'terminal' event function attribute and introduce the 'terminate_after' attribute. * ENH: Change attribute name terminate_after to max_events; TST: Additional check within test_events to ensure multiple events can be captured before termination. * MAINT: Rebase on top of current main branch; Update release associated with deprecation; Update tests. * MAINT: integrate.solve_ivp: simplify prepare_events * MAINT: integreate.solve_ivp: update documentation, add test * MAINT: integrate.solve_ivp: add input validation to attribute --------- Co-authored-by: Matt Haberland <mhaberla@calpoly.edu>
Reference issue
Closes gh-14176
Closes gh-14395
What does this implement/fix?
The
terminal
attribute of an event function as part ofsolve_ivp
is deprecated in favour of the newmax_events
attribute. Instead of asserting if integration should terminate on an event occurrence,max_events
allows specifying the number of occurrences of a given event after which integration must terminate. Through settingevent.max_events = 1
, the behaviour ofevent.terminal = True
is recovered. Additionally, usingevent.max_events = np.inf
or, more simply, not settingmax_events
will ensure that integration never terminates on the occurrence of a given event, similar to writingevent.terminal = False
. Finally, the enhancement lies in the fact thatevent.max_events
can be given any value in]1, np.inf[
and integration will only terminate if the number of occurrences of the specified event becomes greater or equal to the latter value.Additional information
Original concept by @drhagen in gh-14176.