-
Notifications
You must be signed in to change notification settings - Fork 221
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
run_once or run_at method #1288
Comments
Hello, I am interested in working on this 😃 . I saw brian2 when I was working on my dissertation, where I was making my own rather simple simulator. I am trying to get into computational neuroscience masters, and I think contributing to this project would be a great way to learn.
and this produces the following result let me know if it's fine to work on this, and if I am going in the right direction with my solution 😁 |
Hi @TheSquake . Many thanks for your interest in working on this, it would be much appreciated 😄 Also thanks for your proof of concept. Your approach would work (at least in the context of the default "runtime mode" – for standalone mode, no Python code will be executed during the run). I think it is not the optimal approach, though: the problem is that it keeps a clock that makes the object execute the times = [0.5*ms]
for timestep in range(10):
t = timestep * 0.1*ms
if t in times:
print(f"Executing at t={t!s}") (Prints "Executing at t=0.5 ms") I think a more promising and not that complicated approach would be to introduce a new type of clock, an |
Here's a simplified version of what a network does during a run to support multiple clocks and multiple runs. This code does not actually simulate anything, it just demonstrates the decisions of how to advance time and how to decide which objects should be simulated next: class ClockNetwork:
def __init__(self, clocks):
self._clocks = clocks
self.t = 0 * second
def _nextclocks(self):
clocks_times_dt = [(c, c.t, c.dt) for c in self._clocks]
minclock, min_time, minclock_dt = min(clocks_times_dt, key=lambda k: k[1])
curclocks = {
clock
for clock, time, dt in clocks_times_dt
if (
time == min_time
or abs(time - min_time) / min(minclock_dt, dt) < Clock.epsilon_dt
)
}
return minclock, curclocks
def run(self, runtime):
print(f"Starting a simulation run at t={self.t!s} for {runtime!s}")
for clock in self._clocks:
clock.set_interval(self.t, self.t + runtime)
clock, curclocks = self._nextclocks()
running = clock.timestep < clock._i_end
while running:
# Run objects for current clock
print(
f"t={clock.t[:]!s}, running objects for: {', '.join(sorted(c.name for c in curclocks))}"
)
# Advance clocks
for c in curclocks:
# Since these attributes are read-only for normal users, we have to
# update them via the variables object directly
c.variables["timestep"].set_value(c.timestep + 1)
c.variables["t"].set_value(c.timestep * c.dt)
clock, curclocks = self._nextclocks()
running = clock.timestep < clock._i_end
print("End of simulation")
self.t = clock.t[:] For a single clock, all this is unnecessarily complex, but if you have some objects that should be executed every 0.1ms, and some that should be executed every 0.2ms, it will do the expected thing:
The situation above could also be handled with something that is closer to your approach, i.e. by determining the faster clock and advancing it, and checking each timestep whether the other clock should be considered as well. Brian's approach is more general, though, it also works if the >>> net = ClockNetwork(
[Clock(dt=0.1 * ms, name="fast"), Clock(dt=0.11 * ms, name="less_fast")]
)
>>> net.run(.5 * ms)
Starting a simulation run at t=0. s for 0.5 ms
t=0. s, running objects for: fast, less_fast
t=100. us, running objects for: fast
t=110. us, running objects for: less_fast
t=200. us, running objects for: fast
t=220. us, running objects for: less_fast
t=300. us, running objects for: fast
t=0.33 ms, running objects for: less_fast
t=0.4 ms, running objects for: fast
t=0.44 ms, running objects for: less_fast
End of simulation Now my idea of an Does that make sense to you and would you be interested in working on that? I can give you more details on the specific changes we'd need to support all that (and, obviously, don't hesitate to ask if anything is unclear). Oh, and don't worry about the standalone mode, I can take care of that part later. |
Hey @mstimberg Thank you for this comment, yes I think making this a new clock would be much better. I think I understand the idea and while making my first solution I was wondering if this should be a new clock. Of course I am still interested in doing this 😀 I have started working on that today, will comment again once I get version of that working. I guess my only question is whether that clock should be a subclass of the regular Clock and then what should be the |
I think the cleanest solution would be to have a new PS: There is also |
yep seems good, I was wondering what about making |
Sorry for the very late reply – end-of-year illness and end-of-year holidays got into the way… I see your point, it would probably make sense to unify things in this way. It's probably slightly less efficient (not even sure), but advancing the clock is negligible compared to everything that happens within the time step. Actually, now that I am thinking about it, we should probably simply wrap this into some kind of on-the-fly evaluation (creating a huge array for long simulations is probably a bit of a waste). Something along these lines: class ClockArray:
def __init__(self, clock):
self.clock = clock
def __getitem__(self, item):
return item * self.clock.dt The standard clock would then use |
We have a
run_regularly
method andnetwork_operation
, but people often want to have something run just once and it's a bit of a fiddle at the moment. How about we have aGroup.run_once
or to set something to happen just a few timesGroup.run_at
. We could also similarly have a version ofnetwork_operation
that only runs once. There are workarounds now, but it seems to be a pretty common use case.The text was updated successfully, but these errors were encountered: