-
Notifications
You must be signed in to change notification settings - Fork 224
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
Multiple heartbeat listeners called at automatic intervals #650
base: main
Are you sure you want to change the base?
Multiple heartbeat listeners called at automatic intervals #650
Conversation
- Code largely copied from ODE implementation - TODO: python interface - Decreases performance penalty from using using python heartbeat functions by building in calls to reb_output_check
- keep the function pointers in sim to avoid GC
All of the function names here probably need some adjusting. |
Thanks. This is interesting and your implementation is very nicely done! When I run into a similar issue, I just do something like: while sim.t < end:
sim.integrate(sim.t + Delta_t)
output() or while sim.t < end:
sim.steps(N_steps)
output() Your solution with callback functions is definitely more elegant. However, I think one downside is that it requires the user to understand more API calls, how python decorators work, etc. For example, I'd need to read the documentation to find out if the heartbeat gets called at the precise intervals that I'm giving it or wether it could be before or after, depending on where the timesteps fall. Similarly, what is the expected behaviour if the interval is shorter than a timestep? Also, it's not clear to me your implementation works for negative timesteps (integrating back in time). Finally, it's not clear to me what the expected behaviour is when using In short, there are many complication and there is a question of whether the increased complexity outweighs any potential benefits. Let me think about it for a while! |
I didn't realize negative timestepping was officially supported! I was just using -1.0f as a magic number meaning "don't I agree there are plenty of complications arising from exact intervals, interval less than timestep, etc. However, these are already the current practice of void heartbeat(struct reb_simulation* r) {
if (reb_output_check(r, <interval>)) {
do_something();
}
} The new changes exactly copies that behavior and let's you do it from Python performantly (whether that's ideal is another question). The old I think heartbeat callbacks have an advantage over the manual integration loop when you have different things you want to do at different intervals, which comes up a lot I agree that the current interface with the CFUNCTYPE wrapping decorator is not the best. I will modify |
I've changed |
It probably doesn't make much sense, but I was trying to replicate the existing example behavior (i.e. rebound/examples/dragforce/problem.c Line 61 in 2d609ba
I think what we should actually do is have a separate output checking function that looks at |
Wow. That was fast. Please slow down a little! I understand that it doesn't break any existing code or functionality, but I'm still not sure whether the increased complexity is worth it! |
@@ -1186,7 +1191,10 @@ def additional_forces(self): | |||
raise AttributeError("You can only set C function pointers from python.") | |||
@additional_forces.setter | |||
def additional_forces(self, func): | |||
self._afp = AFF(func) | |||
if isinstance(func, _CFuncPtr): |
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.
There used to be an issue with retaining a reference to the callback function type. I don't remember all the details. I don't know if this change will resurface the issue.
Well, going over it again, I like this new version much better! A few questions:
If the above is all resolved, I'm happy to merge it in. Let's not add it to the documentation / examples. I like to keep those as simple as possible and I think the very basic syntax I mentioned earlier will be sufficient for most users. |
Sorry! The semester just started for me so I have nothing better to do...
Yeah, I will remove it.
The reference issue should be ok since I'm still keeping a reference in def fn(sim: Simulation):
do_something_with(sim); rather than def fn(sim_pointer: POINTER(Simulation)):
do_something_with(sim.contents); as before. I think requiring users to use |
I see. I agree, not having the I believe part of the issue with the reference was that we needed to keep the reference to the function |
In other words, what does this output: import rebound
sim = rebound.Simulation()
sim.integrator = "leapfrog"
sim.add()
sim.dt = 1
def hb(simp):
simp.contents.dt = 2
sim.heartbeat = hb
sim.integrate(3,exact_finish_time=0)
print(sim.dt) |
The dereferencing with
Hmmm... I'd be interested if that was the case.
2.0 |
Great. That is a much nice syntax to have (which we need to update in the documentation and examples). I agree that it looks like ctypes is keeping a reference. For some reason we decided 7 years ago that we need to move AFF (and other variables) outside the class definition (see 4ced0c2). Let's see if @dtamayo has a better memory. |
Oof I really don't remember the details. But I could swear I remember this causing a problem, and trying to keep track of references to make a minimal working example that showed the issue. I was going to put it into that 2015 PyCon talk but I just looked at it and I think it was too technical and didn't make it into the talk. Sorry! |
Well, it seems to work and I think it should work. So maybe we were just a bit naive and didn't really understand where the problem was coming from back then. I'd be willing to take the risk and use the nice syntax without the pointers that @alchzh has suggested. |
We've dropped the ball on this one. I'm happy to think about it a bit more - just wary of breaking things if I don't fully understand the memory management stuff in python... |
I can rewrite this PR to not change the pointer access semantics and just implement the heartbeat stuff- I can't remember exactly from a year ago but it should work with the older style anyway... |
Hm - I'd actually be more keen to merge in the pointer access semantics than the multiple heartbeat changes! |
Current, rebound only accepts a single function pointer as a heartbeat callback. The current interface makes simulations using Python heartbeats very slow since they incur significant overhead on every timestep.
Most heartbeat functions are called either on every timestep, after a certain interval (using
reb_output_check
), or after a certain multiple of the simulation dt. In the latter two, I think Python heartbeat callbacks should be accepted.The solution is allowing multiple heartbeat listeners functions to be added and calling them automatically inside a
reb_output_check
on a specified interval. This avoids incurring Python call overhead on every timestep, and also creates what I think to be a nicer modular interface for heartbeats.TODO: create examples and docs