From 738d92e30e669f141ee991ea8652157bb29af72d Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 21 Mar 2025 18:16:21 +0100 Subject: [PATCH 01/23] Run_at-issue --- brian2/core/clocks.py | 95 +++++++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 21 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 40cdd8d11..709b4b28b 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -62,7 +62,65 @@ def check_dt(new_dt, old_dt, target_t): ) -class Clock(VariableOwner): +class EventClock(VariableOwner): + def __init__(self, times, name="eventclock*"): + Nameable.__init__(self, name=name) + self.variables = Variables(self) + self.times = times + self.variables.add_array( + "timestep", size=1, dtype=np.int64, read_only=True, scalar=True + ) + self.variables.add_array( + "t", + dimensions=second.dim, + size=1, + dtype=np.float64, + read_only=True, + scalar=True, + ) + self.variables["timestep"].set_value(0) + self.variables["t"].set_value(self.times[0]) + + self.variables.add_constant("N", value=1) + + self._enable_group_attributes() + + self._i_end = None + logger.diagnostic(f"Created clock {self.name}") + + def advance(self): + """ + Advance the clock to the next timestep. + """ + current_timestep = self.variables["timestep"].get_value() + next_timestep = current_timestep + 1 + if self._i_end is not None and next_timestep > self._i_end: + raise StopIteration("Clock has reached the end of its available times.") + else: + self.variables["timestep"].set_value(next_timestep) + self.variables["t"].set_value(self.times[next_timestep]) + + @check_units(start=second, end=second) + def set_interval(self, start, end): + """ + Set the start and end time of the simulation. + """ + # For an EventClock with explicit times, find nearest indices + if not isinstance(self.times, ClockArray): + # Find closest time indices + start_idx = np.searchsorted(self.times, float(start)) + end_idx = np.searchsorted(self.times, float(end)) + + self.variables["timestep"].set_value(start_idx) + self.variables["t"].set_value(self.times[start_idx]) + self._i_end = end_idx - 1 # -1 since we want to include this step + else: + # For regular clocks, delegate to the specific implementation + # This will be handled by child classes that implement ClockArray + pass + + +class RegularClock(EventClock): """ An object that holds the simulation time and the time step. @@ -82,23 +140,13 @@ class Clock(VariableOwner): point values. The value of ``epsilon`` is ``1e-14``. """ - def __init__(self, dt, name="clock*"): + def __init__(self, dt, name="regularclock*"): # We need a name right away because some devices (e.g. cpp_standalone) # need a name for the object when creating the variables - Nameable.__init__(self, name=name) - self._old_dt = None - self.variables = Variables(self) - self.variables.add_array( - "timestep", size=1, dtype=np.int64, read_only=True, scalar=True - ) - self.variables.add_array( - "t", - dimensions=second.dim, - size=1, - dtype=np.float64, - read_only=True, - scalar=True, - ) + self._dt = float(dt) + self._old_dt = None # Initialize _old_dt to None + times = ClockArray(self) + super().__init__(times, name=name) self.variables.add_array( "dt", dimensions=second.dim, @@ -109,10 +157,6 @@ def __init__(self, dt, name="clock*"): constant=True, scalar=True, ) - self.variables.add_constant("N", value=1) - self._enable_group_attributes() - self.dt = dt - logger.diagnostic(f"Created clock {self.name} with dt={self.dt}") @check_units(t=second) def _set_t_update_dt(self, target_t=0 * second): @@ -129,7 +173,10 @@ def _set_t_update_dt(self, target_t=0 * second): # update them via the variables object directly self.variables["timestep"].set_value(new_timestep) self.variables["t"].set_value(new_timestep * new_dt) - logger.diagnostic(f"Setting Clock {self.name} to t={self.t}, dt={self.dt}") + # Use self.variables["t"].get_value() instead of self.t for logging + t_value = self.variables["t"].get_value() + dt_value = self.variables["dt"].get_value() + logger.diagnostic(f"Setting Clock {self.name} to t={t_value}, dt={dt_value}") def _calc_timestep(self, target_t): """ @@ -211,6 +258,12 @@ def set_interval(self, start, end): epsilon_dt = 1e-4 +class Clock(RegularClock): # Fixed the typo here + def __init__(self, dt, name="clock*"): + super().__init__(dt, name) + + + class DefaultClockProxy: """ Method proxy to access the defaultclock of the currently active device From f4c7eb03827168577ec0eb7f755c5077d0fe548d Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 21 Mar 2025 18:41:22 +0100 Subject: [PATCH 02/23] Run_at-issue --- brian2/core/clocks.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 709b4b28b..b261d08da 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -61,7 +61,13 @@ def check_dt(new_dt, old_dt, target_t): f"time {t} is not a multiple of {new}." ) - +class ClockArray: + def __init__(self, clock): + self.clock = clock + + def __getitem__(self, timestep): + return self.clock.dt * timestep + class EventClock(VariableOwner): def __init__(self, times, name="eventclock*"): Nameable.__init__(self, name=name) From a42c5f8a42417ef0d56069490c09b1585a466a3a Mon Sep 17 00:00:00 2001 From: Samuel Date: Sat, 22 Mar 2025 17:38:45 +0100 Subject: [PATCH 03/23] work in progress --- brian2/core/clocks.py | 51 +++++++++++++++++++++++++++++++----------- brian2/core/network.py | 18 ++++++--------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index b261d08da..0e24d2e04 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -98,7 +98,7 @@ def advance(self): """ Advance the clock to the next timestep. """ - current_timestep = self.variables["timestep"].get_value() + current_timestep = self.variables["timestep"].get_value().item() next_timestep = current_timestep + 1 if self._i_end is not None and next_timestep > self._i_end: raise StopIteration("Clock has reached the end of its available times.") @@ -111,19 +111,45 @@ def set_interval(self, start, end): """ Set the start and end time of the simulation. """ - # For an EventClock with explicit times, find nearest indices + if not isinstance(self.times, ClockArray): - # Find closest time indices + start_idx = np.searchsorted(self.times, float(start)) end_idx = np.searchsorted(self.times, float(end)) self.variables["timestep"].set_value(start_idx) self.variables["t"].set_value(self.times[start_idx]) - self._i_end = end_idx - 1 # -1 since we want to include this step + self._i_end = end_idx - 1 else: - # For regular clocks, delegate to the specific implementation - # This will be handled by child classes that implement ClockArray + pass + + def __lt__(self, other): + return self.variables["t"].get_value().item() < other.variables["t"].get_value().item() + + def __eq__(self, other): + t1 = self.variables["t"].get_value().item() + t2 = other.variables["t"].get_value().item() + + if hasattr(self, 'dt'): + dt = self.variables["dt"].get_value().item() + return abs(t1 - t2) / dt < self.epsilon_dt + elif hasattr(other, 'dt'): + dt = other.variables["dt"].get_value().item() + return abs(t1 - t2) / dt < self.epsilon_dt + else: + # Both are pure EventClocks without dt + epsilon = 1e-10 + return abs(t1 - t2) < epsilon + + def __le__(self, other): + return self.__lt__(other) or self.__eq__(other) + + def __gt__(self, other): + return not self.__le__(other) + + def __ge__(self, other): + return not self.__lt__(other) class RegularClock(EventClock): @@ -150,7 +176,7 @@ def __init__(self, dt, name="regularclock*"): # We need a name right away because some devices (e.g. cpp_standalone) # need a name for the object when creating the variables self._dt = float(dt) - self._old_dt = None # Initialize _old_dt to None + self._old_dt = None times = ClockArray(self) super().__init__(times, name=name) self.variables.add_array( @@ -179,9 +205,9 @@ def _set_t_update_dt(self, target_t=0 * second): # update them via the variables object directly self.variables["timestep"].set_value(new_timestep) self.variables["t"].set_value(new_timestep * new_dt) - # Use self.variables["t"].get_value() instead of self.t for logging - t_value = self.variables["t"].get_value() - dt_value = self.variables["dt"].get_value() + # Use self.variables["t"].get_value().item() and self.variables["dt"].get_value().item() for logging + t_value = self.variables["t"].get_value().item() + dt_value = self.variables["dt"].get_value().item() logger.diagnostic(f"Setting Clock {self.name} to t={t_value}, dt={dt_value}") def _calc_timestep(self, target_t): @@ -264,12 +290,11 @@ def set_interval(self, start, end): epsilon_dt = 1e-4 -class Clock(RegularClock): # Fixed the typo here +class Clock(RegularClock): def __init__(self, dt, name="clock*"): super().__init__(dt, name) - class DefaultClockProxy: """ Method proxy to access the defaultclock of the currently active device @@ -289,4 +314,4 @@ def __setattr__(self, key, value): #: The standard clock, used for objects that do not specify any clock or dt -defaultclock = DefaultClockProxy() +defaultclock = DefaultClockProxy() \ No newline at end of file diff --git a/brian2/core/network.py b/brian2/core/network.py index 4f2a44424..16967687b 100644 --- a/brian2/core/network.py +++ b/brian2/core/network.py @@ -1035,21 +1035,17 @@ def after_run(self): obj.after_run() def _nextclocks(self): - clocks_times_dt = [ - (c, self._clock_variables[c][1][0], self._clock_variables[c][2][0]) - for c in self._clocks - ] - minclock, min_time, minclock_dt = min(clocks_times_dt, key=lambda k: k[1]) + + minclock = min(self._clocks) + 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 - ) + for clock in self._clocks + if clock == minclock } + return minclock, curclocks - + @device_override("network_run") @check_units(duration=second, report_period=second) def run( From f8975bee7b290fcb965b37aaacddead794708f1f Mon Sep 17 00:00:00 2001 From: Samuel Date: Sat, 29 Mar 2025 18:31:48 +0100 Subject: [PATCH 04/23] new changes made --- brian2/core/clocks.py | 11 ++++++++--- brian2/core/network.py | 21 +++++++++------------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 0e24d2e04..957b6191f 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -13,7 +13,7 @@ from brian2.units.fundamentalunits import Quantity, check_units from brian2.utils.logger import get_logger -__all__ = ["Clock", "defaultclock"] +__all__ = ["Clock", "defaultclock","EventClock","RegularClock"] logger = get_logger(__name__) @@ -61,6 +61,7 @@ def check_dt(new_dt, old_dt, target_t): f"time {t} is not a multiple of {new}." ) + class ClockArray: def __init__(self, clock): self.clock = clock @@ -72,7 +73,11 @@ class EventClock(VariableOwner): def __init__(self, times, name="eventclock*"): Nameable.__init__(self, name=name) self.variables = Variables(self) - self.times = times + self.times = sorted(times) + + if len(self.times)!=len(set(self.times)): + raise ValueError("The times provided to EventClock must not contain duplicates") + self.variables.add_array( "timestep", size=1, dtype=np.int64, read_only=True, scalar=True ) @@ -127,7 +132,7 @@ def set_interval(self, start, end): def __lt__(self, other): return self.variables["t"].get_value().item() < other.variables["t"].get_value().item() - def __eq__(self, other): + def same_time(self, other): t1 = self.variables["t"].get_value().item() t2 = other.variables["t"].get_value().item() diff --git a/brian2/core/network.py b/brian2/core/network.py index 16967687b..b662acdeb 100644 --- a/brian2/core/network.py +++ b/brian2/core/network.py @@ -1036,12 +1036,12 @@ def after_run(self): def _nextclocks(self): - minclock = min(self._clocks) + minclock = min(self._clocks, key = lambda c: c.variables["t"].get_value().item()) curclocks = { clock for clock in self._clocks - if clock == minclock + if clock.same_time(minclock) } return minclock, curclocks @@ -1123,7 +1123,6 @@ def run( c: ( c.variables["timestep"].get_value(), c.variables["t"].get_value(), - c.variables["dt"].get_value(), ) for c in self._clocks } @@ -1170,11 +1169,11 @@ def run( profiling_info = defaultdict(float) if single_clock: - timestep, t, dt = ( + timestep, t = ( clock.variables["timestep"].get_value(), clock.variables["t"].get_value(), - clock.variables["dt"].get_value(), ) + else: # Find the first clock to be updated (see note below) clock, curclocks = self._nextclocks() @@ -1185,8 +1184,8 @@ def run( active_objects = [obj for obj in all_objects if obj.active] while running and not self._stopped and not Network._globally_stopped: - if not single_clock: - timestep, t, dt = self._clock_variables[clock] + if not single_clock : + timestep, t = self._clock_variables[clock] # update the network time to this clock's time self.t_ = t[0] if report is not None: @@ -1211,8 +1210,8 @@ def run( for obj in active_objects: obj.run() - timestep[0] += 1 - t[0] = timestep[0] * dt[0] + clock.advance() + else: if profile: for obj in active_objects: @@ -1226,9 +1225,7 @@ def run( obj.run() for c in curclocks: - timestep, t, dt = self._clock_variables[c] - timestep[0] += 1 - t[0] = timestep[0] * dt[0] + c.advance() # find the next clocks to be updated. The < operator for Clock # determines that the first clock to be updated should be the one # with the smallest t value, unless there are several with the From 1b5a0cf46fcf46a6429a562da6106d030e6f3e15 Mon Sep 17 00:00:00 2001 From: Samuel Date: Sun, 30 Mar 2025 12:48:34 +0200 Subject: [PATCH 05/23] new changes made --- brian2/core/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brian2/core/network.py b/brian2/core/network.py index b662acdeb..b148ac946 100644 --- a/brian2/core/network.py +++ b/brian2/core/network.py @@ -1177,7 +1177,7 @@ def run( else: # Find the first clock to be updated (see note below) clock, curclocks = self._nextclocks() - timestep, _, _ = self._clock_variables[clock] + timestep, t = self._clock_variables[clock] running = timestep[0] < clock._i_end @@ -1231,7 +1231,7 @@ def run( # with the smallest t value, unless there are several with the # same t value in which case we update all of them clock, curclocks = self._nextclocks() - timestep, _, _ = self._clock_variables[clock] + timestep, t = self._clock_variables[clock] if ( device._maximum_run_time is not None From 9019e10d84c702174358cbdb79ede3d6013d12dd Mon Sep 17 00:00:00 2001 From: Samu Date: Sat, 5 Apr 2025 01:21:56 +0200 Subject: [PATCH 06/23] fix in ClockArray --- brian2/core/clocks.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 957b6191f..79d753d22 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -67,16 +67,19 @@ def __init__(self, clock): self.clock = clock def __getitem__(self, timestep): - return self.clock.dt * timestep + return self.clock._dt * timestep class EventClock(VariableOwner): def __init__(self, times, name="eventclock*"): Nameable.__init__(self, name=name) self.variables = Variables(self) - self.times = sorted(times) + if isinstance(times, ClockArray): + self.times = times # Don't sort, don't check for duplicates + else: + self.times = sorted(times) + if len(self.times) != len(set(self.times)): + raise ValueError("The times provided to EventClock must not contain duplicates") - if len(self.times)!=len(set(self.times)): - raise ValueError("The times provided to EventClock must not contain duplicates") self.variables.add_array( "timestep", size=1, dtype=np.int64, read_only=True, scalar=True From a586a113010c4da8026bc130f4649102eb58cce2 Mon Sep 17 00:00:00 2001 From: Samu Date: Mon, 7 Apr 2025 20:19:51 +0200 Subject: [PATCH 07/23] final implementation --- brian2/core/clocks.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 79d753d22..f6e215912 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -13,7 +13,7 @@ from brian2.units.fundamentalunits import Quantity, check_units from brian2.utils.logger import get_logger -__all__ = ["Clock", "defaultclock","EventClock","RegularClock"] +__all__ = ["Clock", "defaultclock","EventClock"] logger = get_logger(__name__) @@ -160,7 +160,7 @@ def __ge__(self, other): return not self.__lt__(other) -class RegularClock(EventClock): +class Clock(EventClock): """ An object that holds the simulation time and the time step. @@ -180,7 +180,7 @@ class RegularClock(EventClock): point values. The value of ``epsilon`` is ``1e-14``. """ - def __init__(self, dt, name="regularclock*"): + def __init__(self, dt, name="clock*"): # We need a name right away because some devices (e.g. cpp_standalone) # need a name for the object when creating the variables self._dt = float(dt) @@ -251,6 +251,7 @@ def _get_dt_(self): def _set_dt_(self, dt_): self._old_dt = self._get_dt_() self.variables["dt"].set_value(dt_) + self._dt=dt_ @check_units(dt=second) def _set_dt(self, dt): @@ -298,11 +299,6 @@ def set_interval(self, start, end): epsilon_dt = 1e-4 -class Clock(RegularClock): - def __init__(self, dt, name="clock*"): - super().__init__(dt, name) - - class DefaultClockProxy: """ Method proxy to access the defaultclock of the currently active device From 629b0fa1308a4aca0e55d3ad287f76a2599af9d0 Mon Sep 17 00:00:00 2001 From: Samu Date: Mon, 7 Apr 2025 21:18:51 +0200 Subject: [PATCH 08/23] fixing format code --- brian2/core/clocks.py | 39 ++++++++++++++++++++++----------------- brian2/core/network.py | 18 +++++++----------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index f6e215912..237aede91 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -13,7 +13,7 @@ from brian2.units.fundamentalunits import Quantity, check_units from brian2.utils.logger import get_logger -__all__ = ["Clock", "defaultclock","EventClock"] +__all__ = ["Clock", "defaultclock", "EventClock"] logger = get_logger(__name__) @@ -65,10 +65,11 @@ def check_dt(new_dt, old_dt, target_t): class ClockArray: def __init__(self, clock): self.clock = clock - + def __getitem__(self, timestep): return self.clock._dt * timestep - + + class EventClock(VariableOwner): def __init__(self, times, name="eventclock*"): Nameable.__init__(self, name=name) @@ -78,9 +79,10 @@ def __init__(self, times, name="eventclock*"): else: self.times = sorted(times) if len(self.times) != len(set(self.times)): - raise ValueError("The times provided to EventClock must not contain duplicates") + raise ValueError( + "The times provided to EventClock must not contain duplicates" + ) - self.variables.add_array( "timestep", size=1, dtype=np.int64, read_only=True, scalar=True ) @@ -98,7 +100,7 @@ def __init__(self, times, name="eventclock*"): self.variables.add_constant("N", value=1) self._enable_group_attributes() - + self._i_end = None logger.diagnostic(f"Created clock {self.name}") @@ -113,7 +115,7 @@ def advance(self): else: self.variables["timestep"].set_value(next_timestep) self.variables["t"].set_value(self.times[next_timestep]) - + @check_units(start=second, end=second) def set_interval(self, start, end): """ @@ -124,30 +126,33 @@ def set_interval(self, start, end): start_idx = np.searchsorted(self.times, float(start)) end_idx = np.searchsorted(self.times, float(end)) - + self.variables["timestep"].set_value(start_idx) self.variables["t"].set_value(self.times[start_idx]) - self._i_end = end_idx - 1 + self._i_end = end_idx - 1 else: pass - + def __lt__(self, other): - return self.variables["t"].get_value().item() < other.variables["t"].get_value().item() + return ( + self.variables["t"].get_value().item() + < other.variables["t"].get_value().item() + ) def same_time(self, other): t1 = self.variables["t"].get_value().item() t2 = other.variables["t"].get_value().item() - if hasattr(self, 'dt'): + if hasattr(self, "dt"): dt = self.variables["dt"].get_value().item() return abs(t1 - t2) / dt < self.epsilon_dt - elif hasattr(other, 'dt'): + elif hasattr(other, "dt"): dt = other.variables["dt"].get_value().item() return abs(t1 - t2) / dt < self.epsilon_dt else: # Both are pure EventClocks without dt - epsilon = 1e-10 + epsilon = 1e-10 return abs(t1 - t2) < epsilon def __le__(self, other): @@ -184,7 +189,7 @@ def __init__(self, dt, name="clock*"): # We need a name right away because some devices (e.g. cpp_standalone) # need a name for the object when creating the variables self._dt = float(dt) - self._old_dt = None + self._old_dt = None times = ClockArray(self) super().__init__(times, name=name) self.variables.add_array( @@ -251,7 +256,7 @@ def _get_dt_(self): def _set_dt_(self, dt_): self._old_dt = self._get_dt_() self.variables["dt"].set_value(dt_) - self._dt=dt_ + self._dt = dt_ @check_units(dt=second) def _set_dt(self, dt): @@ -318,4 +323,4 @@ def __setattr__(self, key, value): #: The standard clock, used for objects that do not specify any clock or dt -defaultclock = DefaultClockProxy() \ No newline at end of file +defaultclock = DefaultClockProxy() diff --git a/brian2/core/network.py b/brian2/core/network.py index b148ac946..9e2132b7b 100644 --- a/brian2/core/network.py +++ b/brian2/core/network.py @@ -17,7 +17,7 @@ from collections.abc import Mapping, Sequence from brian2.core.base import BrianObject, BrianObjectException -from brian2.core.clocks import Clock, defaultclock +from brian2.core.clocks import defaultclock from brian2.core.names import Nameable from brian2.core.namespace import get_local_namespace from brian2.core.preferences import BrianPreference, prefs @@ -1036,16 +1036,12 @@ def after_run(self): def _nextclocks(self): - minclock = min(self._clocks, key = lambda c: c.variables["t"].get_value().item()) - - curclocks = { - clock - for clock in self._clocks - if clock.same_time(minclock) - } - + minclock = min(self._clocks, key=lambda c: c.variables["t"].get_value().item()) + + curclocks = {clock for clock in self._clocks if clock.same_time(minclock)} + return minclock, curclocks - + @device_override("network_run") @check_units(duration=second, report_period=second) def run( @@ -1184,7 +1180,7 @@ def run( active_objects = [obj for obj in all_objects if obj.active] while running and not self._stopped and not Network._globally_stopped: - if not single_clock : + if not single_clock: timestep, t = self._clock_variables[clock] # update the network time to this clock's time self.t_ = t[0] From 46f34ad62c204d3ea7a4749395e941b28dd905e2 Mon Sep 17 00:00:00 2001 From: Samu Date: Fri, 11 Apr 2025 13:31:13 +0200 Subject: [PATCH 09/23] network/clock check --- brian2/core/clocks.py | 72 +++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 237aede91..068e1aafd 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -36,7 +36,7 @@ def check_dt(new_dt, old_dt, target_t): ------ ValueError If using the new dt value would lead to a difference in the target - time of more than `Clock.epsilon_dt` times ``new_dt`` (by default, + time of more than Clock.epsilon_dt times `new_dt (by default, 0.01% of the new dt). Examples @@ -66,23 +66,16 @@ class ClockArray: def __init__(self, clock): self.clock = clock - def __getitem__(self, timestep): - return self.clock._dt * timestep + def __getitem__( + self, timestep + ): # here is also possible to add a check since the only time we need the initial_dt is when setupping t the first call, to avoid any checks in the constructor of eventclock. + return self.clock.dt * timestep class EventClock(VariableOwner): def __init__(self, times, name="eventclock*"): Nameable.__init__(self, name=name) self.variables = Variables(self) - if isinstance(times, ClockArray): - self.times = times # Don't sort, don't check for duplicates - else: - self.times = sorted(times) - if len(self.times) != len(set(self.times)): - raise ValueError( - "The times provided to EventClock must not contain duplicates" - ) - self.variables.add_array( "timestep", size=1, dtype=np.int64, read_only=True, scalar=True ) @@ -95,6 +88,24 @@ def __init__(self, times, name="eventclock*"): scalar=True, ) self.variables["timestep"].set_value(0) + if isinstance(times, ClockArray): + self.times = times # Don't sort, don't check for duplicates + self.variables.add_array( + "dt", + dimensions=second.dim, + size=1, + values=times.clock.initial_dt, + dtype=np.float64, + read_only=True, + constant=True, + scalar=True, + ) + else: + self.times = sorted(times) + if len(self.times) != len(set(self.times)): + raise ValueError( + "The times provided to EventClock must not contain duplicates" + ) self.variables["t"].set_value(self.times[0]) self.variables.add_constant("N", value=1) @@ -105,16 +116,14 @@ def __init__(self, times, name="eventclock*"): logger.diagnostic(f"Created clock {self.name}") def advance(self): - """ - Advance the clock to the next timestep. - """ - current_timestep = self.variables["timestep"].get_value().item() - next_timestep = current_timestep + 1 - if self._i_end is not None and next_timestep > self._i_end: + # Cache the variable object for timestep + # Directly compute the next timestep in one step. + new_ts = self.variables["timestep"].get_value().item() + 1 + if self._i_end is not None and new_ts > self._i_end: raise StopIteration("Clock has reached the end of its available times.") - else: - self.variables["timestep"].set_value(next_timestep) - self.variables["t"].set_value(self.times[next_timestep]) + # Update the timestep and directly set the new time based on the times lookup. + self.variables["timestep"].set_value(new_ts) + self.variables["t"].set_value(self.times[new_ts]) @check_units(start=second, end=second) def set_interval(self, start, end): @@ -178,30 +187,20 @@ class Clock(EventClock): Notes ----- - Clocks are run in the same `Network.run` iteration if `~Clock.t` is the + Clocks are run in the same Network.run iteration if ~Clock.t is the same. The condition for two clocks to be considered as having the same time is - ``abs(t1-t2) Date: Sat, 12 Apr 2025 14:51:39 +0200 Subject: [PATCH 10/23] network/clock check --- brian2/core/clocks.py | 415 ++++++++++++++++++++++++++++-------------- 1 file changed, 281 insertions(+), 134 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 068e1aafd..28e1aa550 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -13,7 +13,7 @@ from brian2.units.fundamentalunits import Quantity, check_units from brian2.utils.logger import get_logger -__all__ = ["Clock", "defaultclock", "EventClock"] +__all__ = ["BaseClock", "Clock", "defaultclock", "EventClock"] logger = get_logger(__name__) @@ -62,18 +62,19 @@ def check_dt(new_dt, old_dt, target_t): ) -class ClockArray: - def __init__(self, clock): - self.clock = clock - def __getitem__( - self, timestep - ): # here is also possible to add a check since the only time we need the initial_dt is when setupping t the first call, to avoid any checks in the constructor of eventclock. - return self.clock.dt * timestep - - -class EventClock(VariableOwner): - def __init__(self, times, name="eventclock*"): +class BaseClock(VariableOwner): + """ + Base class for all clocks in the simulator. + + Parameters + ---------- + name : str, optional + An explicit name, if not specified gives an automatically generated name + """ + epsilon = 1e-10 + + def __init__(self, name): Nameable.__init__(self, name=name) self.variables = Variables(self) self.variables.add_array( @@ -88,93 +89,177 @@ def __init__(self, times, name="eventclock*"): scalar=True, ) self.variables["timestep"].set_value(0) - if isinstance(times, ClockArray): - self.times = times # Don't sort, don't check for duplicates - self.variables.add_array( - "dt", - dimensions=second.dim, - size=1, - values=times.clock.initial_dt, - dtype=np.float64, - read_only=True, - constant=True, - scalar=True, - ) - else: - self.times = sorted(times) - if len(self.times) != len(set(self.times)): - raise ValueError( - "The times provided to EventClock must not contain duplicates" - ) - self.variables["t"].set_value(self.times[0]) - + self.variables.add_constant("N", value=1) - + self._enable_group_attributes() - + self._i_end = None logger.diagnostic(f"Created clock {self.name}") - + def advance(self): - # Cache the variable object for timestep - # Directly compute the next timestep in one step. - new_ts = self.variables["timestep"].get_value().item() + 1 - if self._i_end is not None and new_ts > self._i_end: - raise StopIteration("Clock has reached the end of its available times.") - # Update the timestep and directly set the new time based on the times lookup. - self.variables["timestep"].set_value(new_ts) - self.variables["t"].set_value(self.times[new_ts]) - + """ + Advance the clock to the next time step. + Must be implemented by subclasses. + """ + raise NotImplementedError("This method must be implemented by subclasses") + @check_units(start=second, end=second) def set_interval(self, start, end): """ Set the start and end time of the simulation. + Must be implemented by subclasses. """ - - if not isinstance(self.times, ClockArray): - - start_idx = np.searchsorted(self.times, float(start)) - end_idx = np.searchsorted(self.times, float(end)) - - self.variables["timestep"].set_value(start_idx) - self.variables["t"].set_value(self.times[start_idx]) - self._i_end = end_idx - 1 - else: - - pass - + raise NotImplementedError("This method must be implemented by subclasses") + def __lt__(self, other): return ( self.variables["t"].get_value().item() < other.variables["t"].get_value().item() ) - + + def __gt__(self, other): + return ( + self.variables["t"].get_value().item() + > other.variables["t"].get_value().item() + ) + + def __le__(self, other): + return self.__lt__(other) or self.same_time(other) + + def __ge__(self, other): + return self.__gt__(other) or self.same_time(other) + def same_time(self, other): + """ + Check if two clocks are at the same time (within epsilon). + + Parameters + ---------- + other : BaseClock + The other clock to compare with + + Returns + ------- + bool + True if both clocks are at the same time + """ t1 = self.variables["t"].get_value().item() t2 = other.variables["t"].get_value().item() + + return abs(t1 - t2) < self.epsilon - if hasattr(self, "dt"): - dt = self.variables["dt"].get_value().item() - return abs(t1 - t2) / dt < self.epsilon_dt - elif hasattr(other, "dt"): - dt = other.variables["dt"].get_value().item() - return abs(t1 - t2) / dt < self.epsilon_dt + + +class EventClock(BaseClock): + """ + A clock that advances through a predefined sequence of times. + + Parameters + ---------- + times : array-like + The sequence of times for the clock to advance through + name : str, optional + An explicit name, if not specified gives an automatically generated name + """ + + def __init__(self, times, name="eventclock*"): + super().__init__(name=name) + + self.times = sorted(times) + if len(self.times) != len(set(self.times)): + raise ValueError( + "The times provided to EventClock must not contain duplicates" + ) + + self.variables["t"].set_value(self.times[0]) + + logger.diagnostic(f"Created event clock {self.name}") + + def advance(self): + """ + Advance to the next time in the sequence. + """ + new_ts = self.variables["timestep"].get_value().item() + 1 + if self._i_end is not None and new_ts > self._i_end: + raise StopIteration("Clock has reached the end of its available times.") + + self.variables["timestep"].set_value(new_ts) + self.variables["t"].set_value(self.times[new_ts]) + + @check_units(start=second, end=second) + def set_interval(self, start, end): + """ + Set the start and end time of the simulation. + + Parameters + ---------- + start : second + The start time of the simulation + end : second + The end time of the simulation + """ + start = float(start) + end = float(end) + + start_idx = np.searchsorted(self.times, start) + end_idx = np.searchsorted(self.times, end) + + self.variables["timestep"].set_value(start_idx) + self.variables["t"].set_value(self.times[start_idx]) + + self._i_end = end_idx - 1 + + def __getitem__(self, timestep): + """ + Get the time at a specific timestep. + + Parameters + ---------- + timestep : int + The timestep to get the time for + + Returns + ------- + float + The time at the specified timestep + """ + return self.times[timestep] + def same_time(self, other): + """ + Check if two clocks are at the same time. + + For comparisons with Clock objects, uses the Clock's dt and epsilon_dt. + For comparisons with other EventClocks or BaseClock objects, uses the base + epsilon value. + + Parameters + ---------- + other : BaseClock + The other clock to compare with + + Returns + ------- + bool + True if both clocks are at the same time + """ + t1 = self.variables["t"].get_value().item() + t2 = other.variables["t"].get_value().item() + + if isinstance(other, Clock): + return abs(t1 - t2) / other.dt_ < Clock.epsilon_dt else: - # Both are pure EventClocks without dt - epsilon = 1e-10 - return abs(t1 - t2) < epsilon + return abs(t1 - t2) < self.epsilon + def __le__(self, other): - return self.__lt__(other) or self.__eq__(other) - - def __gt__(self, other): - return not self.__le__(other) - + return self.__lt__(other) or self.same_time(other) + def __ge__(self, other): - return not self.__lt__(other) + return self.__gt__(other) or self.same_time(other) -class Clock(EventClock): +class Clock(BaseClock): """ An object that holds the simulation time and the time step. @@ -195,71 +280,52 @@ class Clock(EventClock): """ def __init__(self, dt, name="clock*"): - # We need a name right away because some devices (e.g. cpp_standalone) - # need a name for the object when creating the variables - self.initial_dt = dt + super().__init__(name=name) + self._old_dt = None - times = ClockArray(self) - super().__init__(times, name=name) - - @check_units(t=second) - def _set_t_update_dt(self, target_t=0 * second): - new_dt = self.dt_ - old_dt = self._old_dt - target_t = float(target_t) - if old_dt is not None and new_dt != old_dt: - self._old_dt = None - # Only allow a new dt which allows to correctly set the new time step - check_dt(new_dt, old_dt, target_t) - - new_timestep = self._calc_timestep(target_t) - # Since these attributes are read-only for normal users, we have to - # update them via the variables object directly - self.variables["timestep"].set_value(new_timestep) - self.variables["t"].set_value(self.times[new_timestep]) - # Use self.variables["t"].get_value().item() and self.variables["dt"].get_value().item() for logging - t_value = self.variables["t"].get_value().item() - dt_value = self.variables["dt"].get_value().item() - logger.diagnostic(f"Setting Clock {self.name} to t={t_value}, dt={dt_value}") - - def _calc_timestep(self, target_t): - """ - Calculate the integer time step for the target time. If it cannot be - exactly represented (up to 0.01% of dt), round up. - - Parameters - ---------- - target_t : float - The target time in seconds - - Returns - ------- - timestep : int - The target time in integers (based on dt) - """ - new_i = np.int64(np.round(target_t / self.dt_)) - new_t = new_i * self.dt_ - if new_t == target_t or np.abs(new_t - target_t) / self.dt_ <= Clock.epsilon_dt: - new_timestep = new_i - else: - new_timestep = np.int64(np.ceil(target_t / self.dt_)) - return new_timestep - + + self.variables.add_array( + "dt", + dimensions=second.dim, + size=1, + values=float(dt), + dtype=np.float64, + read_only=True, + constant=True, + scalar=True, + ) + + self.dt = dt + + logger.diagnostic(f"Created clock {self.name} with dt={self.dt}") + def __repr__(self): return f"Clock(dt={self.dt!r}, name={self.name!r})" + def advance(self): + """ + Advance to the next time step. + """ + new_ts = self.variables["timestep"].get_value().item() + 1 + if self._i_end is not None and new_ts > self._i_end: + raise StopIteration("Clock has reached the end of its available times.") + + self.variables["timestep"].set_value(new_ts) + new_t = new_ts * self.dt_ + self.variables["t"].set_value(new_t) + def _get_dt_(self): return self.variables["dt"].get_value().item() - + @check_units(dt_=1) def _set_dt_(self, dt_): self._old_dt = self._get_dt_() self.variables["dt"].set_value(dt_) - + @check_units(dt=second) def _set_dt(self, dt): self._set_dt_(float(dt)) - + dt = property( fget=lambda self: Quantity(self.dt_, dim=second.dim), fset=_set_dt, @@ -270,21 +336,76 @@ def _set_dt(self, dt): fset=_set_dt_, doc="""The time step of the simulation as a float (in seconds)""", ) + + def _calc_timestep(self, target_t): + """ + Calculate the integer time step for the target time. If it cannot be + exactly represented (up to epsilon_dt of dt), round up. + + Parameters + ---------- + target_t : float + The target time in seconds + + Returns + ------- + timestep : int + The target time in integers (based on dt) + """ + new_i = np.int64(np.round(target_t / self.dt_)) + new_t = new_i * self.dt_ + if new_t == target_t or np.abs(new_t - target_t) / self.dt_ <= self.epsilon_dt: + new_timestep = new_i + else: + new_timestep = np.int64(np.ceil(target_t / self.dt_)) + return new_timestep + + @check_units(target_t=second) + def _set_t_update_dt(self, target_t=0 * second): + """ + Set the time to a specific value, checking if dt has changed. + + Parameters + ---------- + target_t : second + The target time to set + """ + new_dt = self.dt_ + old_dt = self._old_dt + target_t = float(target_t) + + if old_dt is not None and new_dt != old_dt: + self._old_dt = None + check_dt(new_dt, old_dt, target_t) + + new_timestep = self._calc_timestep(target_t) + + self.variables["timestep"].set_value(new_timestep) + self.variables["t"].set_value(new_timestep * self.dt_) + set_t=self.variables["t"].get_value().item() + logger.diagnostic(f"Setting Clock {self.name} to t={set_t}, dt={new_dt}") + @check_units(start=second, end=second) def set_interval(self, start, end): """ - set_interval(self, start, end) - Set the start and end time of the simulation. - + Sets the start and end value of the clock precisely if - possible (using epsilon) or rounding up if not. This assures that + possible (using epsilon_dt) or rounding up if not. This assures that multiple calls to `Network.run` will not re-run the same time step. + + Parameters + ---------- + start : second + The start time of the simulation + end : second + The end time of the simulation """ self._set_t_update_dt(target_t=start) end = float(end) self._i_end = self._calc_timestep(end) + if self._i_end > 2**40: logger.warn( "The end time of the simulation has been set to " @@ -297,9 +418,35 @@ def set_interval(self, start, end): "many_timesteps", ) - #: The relative difference for times (in terms of dt) so that they are - #: considered identical. - epsilon_dt = 1e-4 + def same_time(self, other): + """ + Check if two clocks are at the same time (within epsilon_dt * dt). + + Parameters + ---------- + other : BaseClock + The other clock to compare with + + Returns + ------- + bool + True if both clocks are at the same time + """ + t1 = self.variables["t"].get_value().item() + t2 = other.variables["t"].get_value().item() + + if isinstance(other, Clock): + dt = min(self.dt_, other.dt_) + return abs(t1 - t2) / dt < self.epsilon_dt + else: + + return abs(t1 - t2) / self.dt_ < self.epsilon_dt + + def __le__(self, other): + return self.__lt__(other) or self.same_time(other) + + def __ge__(self, other): + return self.__gt__(other) or self.same_time(other) class DefaultClockProxy: @@ -321,4 +468,4 @@ def __setattr__(self, key, value): #: The standard clock, used for objects that do not specify any clock or dt -defaultclock = DefaultClockProxy() +defaultclock = DefaultClockProxy() \ No newline at end of file From 9a20d0c6840724ec1fec2ea0bf23fdca2fb8bf9b Mon Sep 17 00:00:00 2001 From: Samu Date: Sat, 12 Apr 2025 15:26:00 +0200 Subject: [PATCH 11/23] network/clock check --- brian2/core/clocks.py | 132 +++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 28e1aa550..4bbc37adb 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -62,18 +62,18 @@ def check_dt(new_dt, old_dt, target_t): ) - class BaseClock(VariableOwner): """ Base class for all clocks in the simulator. - + Parameters ---------- name : str, optional An explicit name, if not specified gives an automatically generated name """ + epsilon = 1e-10 - + def __init__(self, name): Nameable.__init__(self, name=name) self.variables = Variables(self) @@ -89,21 +89,21 @@ def __init__(self, name): scalar=True, ) self.variables["timestep"].set_value(0) - + self.variables.add_constant("N", value=1) - + self._enable_group_attributes() - + self._i_end = None logger.diagnostic(f"Created clock {self.name}") - + def advance(self): """ Advance the clock to the next time step. Must be implemented by subclasses. """ raise NotImplementedError("This method must be implemented by subclasses") - + @check_units(start=second, end=second) def set_interval(self, start, end): """ @@ -111,34 +111,34 @@ def set_interval(self, start, end): Must be implemented by subclasses. """ raise NotImplementedError("This method must be implemented by subclasses") - + def __lt__(self, other): return ( self.variables["t"].get_value().item() < other.variables["t"].get_value().item() ) - + def __gt__(self, other): return ( self.variables["t"].get_value().item() > other.variables["t"].get_value().item() ) - + def __le__(self, other): return self.__lt__(other) or self.same_time(other) - + def __ge__(self, other): return self.__gt__(other) or self.same_time(other) - + def same_time(self, other): """ Check if two clocks are at the same time (within epsilon). - + Parameters ---------- other : BaseClock The other clock to compare with - + Returns ------- bool @@ -146,15 +146,14 @@ def same_time(self, other): """ t1 = self.variables["t"].get_value().item() t2 = other.variables["t"].get_value().item() - + return abs(t1 - t2) < self.epsilon - class EventClock(BaseClock): """ A clock that advances through a predefined sequence of times. - + Parameters ---------- times : array-like @@ -162,20 +161,20 @@ class EventClock(BaseClock): name : str, optional An explicit name, if not specified gives an automatically generated name """ - + def __init__(self, times, name="eventclock*"): super().__init__(name=name) - + self.times = sorted(times) if len(self.times) != len(set(self.times)): raise ValueError( "The times provided to EventClock must not contain duplicates" ) - + self.variables["t"].set_value(self.times[0]) - + logger.diagnostic(f"Created event clock {self.name}") - + def advance(self): """ Advance to the next time in the sequence. @@ -183,15 +182,15 @@ def advance(self): new_ts = self.variables["timestep"].get_value().item() + 1 if self._i_end is not None and new_ts > self._i_end: raise StopIteration("Clock has reached the end of its available times.") - + self.variables["timestep"].set_value(new_ts) self.variables["t"].set_value(self.times[new_ts]) - + @check_units(start=second, end=second) def set_interval(self, start, end): """ Set the start and end time of the simulation. - + Parameters ---------- start : second @@ -201,43 +200,44 @@ def set_interval(self, start, end): """ start = float(start) end = float(end) - + start_idx = np.searchsorted(self.times, start) end_idx = np.searchsorted(self.times, end) - + self.variables["timestep"].set_value(start_idx) self.variables["t"].set_value(self.times[start_idx]) - + self._i_end = end_idx - 1 - + def __getitem__(self, timestep): """ Get the time at a specific timestep. - + Parameters ---------- timestep : int The timestep to get the time for - + Returns ------- float The time at the specified timestep """ return self.times[timestep] + def same_time(self, other): """ Check if two clocks are at the same time. - + For comparisons with Clock objects, uses the Clock's dt and epsilon_dt. For comparisons with other EventClocks or BaseClock objects, uses the base epsilon value. - + Parameters ---------- other : BaseClock The other clock to compare with - + Returns ------- bool @@ -245,16 +245,16 @@ def same_time(self, other): """ t1 = self.variables["t"].get_value().item() t2 = other.variables["t"].get_value().item() - + if isinstance(other, Clock): return abs(t1 - t2) / other.dt_ < Clock.epsilon_dt else: return abs(t1 - t2) < self.epsilon - + def __le__(self, other): return self.__lt__(other) or self.same_time(other) - + def __ge__(self, other): return self.__gt__(other) or self.same_time(other) @@ -281,9 +281,9 @@ class Clock(BaseClock): def __init__(self, dt, name="clock*"): super().__init__(name=name) - + self._old_dt = None - + self.variables.add_array( "dt", dimensions=second.dim, @@ -294,11 +294,11 @@ def __init__(self, dt, name="clock*"): constant=True, scalar=True, ) - + self.dt = dt - + logger.diagnostic(f"Created clock {self.name} with dt={self.dt}") - + def __repr__(self): return f"Clock(dt={self.dt!r}, name={self.name!r})" @@ -309,23 +309,23 @@ def advance(self): new_ts = self.variables["timestep"].get_value().item() + 1 if self._i_end is not None and new_ts > self._i_end: raise StopIteration("Clock has reached the end of its available times.") - + self.variables["timestep"].set_value(new_ts) new_t = new_ts * self.dt_ self.variables["t"].set_value(new_t) - + def _get_dt_(self): return self.variables["dt"].get_value().item() - + @check_units(dt_=1) def _set_dt_(self, dt_): self._old_dt = self._get_dt_() self.variables["dt"].set_value(dt_) - + @check_units(dt=second) def _set_dt(self, dt): self._set_dt_(float(dt)) - + dt = property( fget=lambda self: Quantity(self.dt_, dim=second.dim), fset=_set_dt, @@ -336,17 +336,17 @@ def _set_dt(self, dt): fset=_set_dt_, doc="""The time step of the simulation as a float (in seconds)""", ) - + def _calc_timestep(self, target_t): """ Calculate the integer time step for the target time. If it cannot be exactly represented (up to epsilon_dt of dt), round up. - + Parameters ---------- target_t : float The target time in seconds - + Returns ------- timestep : int @@ -359,12 +359,12 @@ def _calc_timestep(self, target_t): else: new_timestep = np.int64(np.ceil(target_t / self.dt_)) return new_timestep - + @check_units(target_t=second) def _set_t_update_dt(self, target_t=0 * second): """ Set the time to a specific value, checking if dt has changed. - + Parameters ---------- target_t : second @@ -373,28 +373,28 @@ def _set_t_update_dt(self, target_t=0 * second): new_dt = self.dt_ old_dt = self._old_dt target_t = float(target_t) - + if old_dt is not None and new_dt != old_dt: self._old_dt = None check_dt(new_dt, old_dt, target_t) - + new_timestep = self._calc_timestep(target_t) - + self.variables["timestep"].set_value(new_timestep) self.variables["t"].set_value(new_timestep * self.dt_) - set_t=self.variables["t"].get_value().item() + set_t = self.variables["t"].get_value().item() logger.diagnostic(f"Setting Clock {self.name} to t={set_t}, dt={new_dt}") - + @check_units(start=second, end=second) def set_interval(self, start, end): """ Set the start and end time of the simulation. - + Sets the start and end value of the clock precisely if possible (using epsilon_dt) or rounding up if not. This assures that multiple calls to `Network.run` will not re-run the same time step. - + Parameters ---------- start : second @@ -421,12 +421,12 @@ def set_interval(self, start, end): def same_time(self, other): """ Check if two clocks are at the same time (within epsilon_dt * dt). - + Parameters ---------- other : BaseClock The other clock to compare with - + Returns ------- bool @@ -434,17 +434,17 @@ def same_time(self, other): """ t1 = self.variables["t"].get_value().item() t2 = other.variables["t"].get_value().item() - + if isinstance(other, Clock): dt = min(self.dt_, other.dt_) return abs(t1 - t2) / dt < self.epsilon_dt else: return abs(t1 - t2) / self.dt_ < self.epsilon_dt - + def __le__(self, other): return self.__lt__(other) or self.same_time(other) - + def __ge__(self, other): return self.__gt__(other) or self.same_time(other) @@ -468,4 +468,4 @@ def __setattr__(self, key, value): #: The standard clock, used for objects that do not specify any clock or dt -defaultclock = DefaultClockProxy() \ No newline at end of file +defaultclock = DefaultClockProxy() From a842da89d53e90c7a3017492a3892d453fdea64d Mon Sep 17 00:00:00 2001 From: Samu Date: Sat, 12 Apr 2025 17:41:06 +0200 Subject: [PATCH 12/23] network/clock check --- brian2/core/clocks.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 4bbc37adb..47c96b935 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -354,7 +354,7 @@ def _calc_timestep(self, target_t): """ new_i = np.int64(np.round(target_t / self.dt_)) new_t = new_i * self.dt_ - if new_t == target_t or np.abs(new_t - target_t) / self.dt_ <= self.epsilon_dt: + if new_t == target_t or np.abs(new_t - target_t) / self.dt_ <= Clock.epsilon_dt: new_timestep = new_i else: new_timestep = np.int64(np.ceil(target_t / self.dt_)) @@ -437,10 +437,10 @@ def same_time(self, other): if isinstance(other, Clock): dt = min(self.dt_, other.dt_) - return abs(t1 - t2) / dt < self.epsilon_dt + return abs(t1 - t2) / dt < Clock.epsilon_dt else: - return abs(t1 - t2) / self.dt_ < self.epsilon_dt + return abs(t1 - t2) / self.dt_ < Clock.epsilon_dt def __le__(self, other): return self.__lt__(other) or self.same_time(other) @@ -448,6 +448,8 @@ def __le__(self, other): def __ge__(self, other): return self.__gt__(other) or self.same_time(other) + epsilon_dt = 1e-4 + class DefaultClockProxy: """ From 37fbafee57173e18f8763291c8b8a7e2cabf8424 Mon Sep 17 00:00:00 2001 From: Samu Date: Sun, 13 Apr 2025 12:41:40 +0200 Subject: [PATCH 13/23] network/clock check --- brian2/core/clocks.py | 18 ++++++++++-------- brian2/tests/test_clocks.py | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 47c96b935..6f932f6c8 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -72,7 +72,7 @@ class BaseClock(VariableOwner): An explicit name, if not specified gives an automatically generated name """ - epsilon = 1e-10 + epsilon = 1e-14 def __init__(self, name): Nameable.__init__(self, name=name) @@ -247,9 +247,9 @@ def same_time(self, other): t2 = other.variables["t"].get_value().item() if isinstance(other, Clock): - return abs(t1 - t2) / other.dt_ < Clock.epsilon_dt + return abs(t1 - t2) / other.dt_ < other.epsilon_dt else: - + # Both are pure EventClocks without dt. return abs(t1 - t2) < self.epsilon def __le__(self, other): @@ -279,6 +279,10 @@ class Clock(BaseClock): point values. The value of `epsilon is 1e-14. """ + #: The relative difference for times (in terms of dt) so that they are + #: considered identical. + epsilon_dt = 1e-4 + def __init__(self, dt, name="clock*"): super().__init__(name=name) @@ -436,11 +440,11 @@ def same_time(self, other): t2 = other.variables["t"].get_value().item() if isinstance(other, Clock): + # Both are pure Clocks with dt so we take the min. dt = min(self.dt_, other.dt_) - return abs(t1 - t2) / dt < Clock.epsilon_dt + return abs(t1 - t2) / dt < self.epsilon_dt else: - - return abs(t1 - t2) / self.dt_ < Clock.epsilon_dt + return abs(t1 - t2) / self.dt_ < self.epsilon_dt def __le__(self, other): return self.__lt__(other) or self.same_time(other) @@ -448,8 +452,6 @@ def __le__(self, other): def __ge__(self, other): return self.__gt__(other) or self.same_time(other) - epsilon_dt = 1e-4 - class DefaultClockProxy: """ diff --git a/brian2/tests/test_clocks.py b/brian2/tests/test_clocks.py index ef9249dd5..93a9e0f5e 100644 --- a/brian2/tests/test_clocks.py +++ b/brian2/tests/test_clocks.py @@ -57,6 +57,29 @@ def test_set_interval_warning(): assert logs[0][1].endswith("many_timesteps") +@pytest.mark.codegen_independent +def test_event_clock(): + times = [0.0, 0.1, 0.2, 0.3] + event_clock = EventClock(times) + + assert_equal(event_clock.variables["t"].get_value(), 0.0) + assert_equal(event_clock[1], 0.1) + + event_clock.advance() + assert_equal(event_clock.variables["timestep"].get_value(), 1) + assert_equal(event_clock.variables["t"].get_value(), 0.1) + + event_clock.set_interval(0.1 * second, 0.3 * second) + assert_equal(event_clock.variables["timestep"].get_value(), 1) + assert_equal(event_clock.variables["t"].get_value(), 0.1) + + # Simulate reaching the end + event_clock.advance() + event_clock.advance() + with pytest.raises(StopIteration): + event_clock.advance() + + if __name__ == "__main__": test_clock_attributes() restore_initial_state() From 8bc0dfe1b12ba209d0bcdce73de5e0a4e3d63d60 Mon Sep 17 00:00:00 2001 From: Samuele De Cristofaro <164782142+Samuele-DeCristofaro@users.noreply.github.com> Date: Sun, 13 Apr 2025 13:01:55 +0200 Subject: [PATCH 14/23] Update test_clocks.py --- brian2/tests/test_clocks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brian2/tests/test_clocks.py b/brian2/tests/test_clocks.py index 93a9e0f5e..a54c03f99 100644 --- a/brian2/tests/test_clocks.py +++ b/brian2/tests/test_clocks.py @@ -87,3 +87,4 @@ def test_event_clock(): restore_initial_state() test_defaultclock() test_set_interval_warning() + test_event_clock() From dd43b8c0e6310a08e17e8e6941fa3d3c66925049 Mon Sep 17 00:00:00 2001 From: Samu Date: Fri, 18 Apr 2025 02:47:37 +0200 Subject: [PATCH 15/23] network/clock check --- brian2/core/clocks.py | 27 ++++++----- brian2/core/network.py | 2 +- brian2/groups/group.py | 93 ++++++++++++++++++++++++++++++++++++- brian2/tests/test_clocks.py | 55 ++++++++++++++++++++-- 4 files changed, 156 insertions(+), 21 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 6f932f6c8..61c7994cd 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -164,14 +164,13 @@ class EventClock(BaseClock): def __init__(self, times, name="eventclock*"): super().__init__(name=name) - - self.times = sorted(times) - if len(self.times) != len(set(self.times)): + self._times = sorted(times) + """if len(self._times) != len(set(self._times)): raise ValueError( "The times provided to EventClock must not contain duplicates" - ) + )""" - self.variables["t"].set_value(self.times[0]) + self.variables["t"].set_value(self._times[0]) logger.diagnostic(f"Created event clock {self.name}") @@ -179,12 +178,12 @@ def advance(self): """ Advance to the next time in the sequence. """ - new_ts = self.variables["timestep"].get_value().item() + 1 - if self._i_end is not None and new_ts > self._i_end: - raise StopIteration("Clock has reached the end of its available times.") - + new_ts = self.variables["timestep"].get_value().item() + if self._i_end is not None and new_ts + 1 > self._i_end: + return + new_ts += 1 self.variables["timestep"].set_value(new_ts) - self.variables["t"].set_value(self.times[new_ts]) + self.variables["t"].set_value(self._times[new_ts]) @check_units(start=second, end=second) def set_interval(self, start, end): @@ -201,11 +200,11 @@ def set_interval(self, start, end): start = float(start) end = float(end) - start_idx = np.searchsorted(self.times, start) - end_idx = np.searchsorted(self.times, end) + start_idx = np.searchsorted(self._times, start) + end_idx = np.searchsorted(self._times, end) self.variables["timestep"].set_value(start_idx) - self.variables["t"].set_value(self.times[start_idx]) + self.variables["t"].set_value(self._times[start_idx]) self._i_end = end_idx - 1 @@ -223,7 +222,7 @@ def __getitem__(self, timestep): float The time at the specified timestep """ - return self.times[timestep] + return self._times[timestep] def same_time(self, other): """ diff --git a/brian2/core/network.py b/brian2/core/network.py index 1d108888a..8f290b1d8 100644 --- a/brian2/core/network.py +++ b/brian2/core/network.py @@ -1019,7 +1019,7 @@ def before_run(self, run_namespace): "creating a new one." ) - clocknames = ", ".join(f"{obj.name} (dt={obj.dt})" for obj in self._clocks) + clocknames = ", ".join(f"{obj.name}" for obj in self._clocks) logger.debug( f"Network '{self.name}' uses {len(self._clocks)} clocks: {clocknames}", "before_run", diff --git a/brian2/groups/group.py b/brian2/groups/group.py index 4e13b1a9e..d44dd743c 100644 --- a/brian2/groups/group.py +++ b/brian2/groups/group.py @@ -1185,6 +1185,51 @@ def run_regularly( # contained objects would no be enough, since the CodeObject needs a # reference to the group providing the context for the operation, i.e. # the subgroup instead of the parent group. See github issue #922 + + return self.run_custom_clock(code, clock, when, order, name, codeobj_class) + + def run_custom_clock( + self, + code, + clock=None, + when="start", + order=0, + name=None, + codeobj_class=None, + ): + """ + This method is used by `run_regularly` and `run_at` to register operations + that are executed at times determined by a user-supplied or internally generated `Clock`. + The resulting `CodeRunner` is automatically added to the group. + + Parameters + ---------- + code : str + The abstract code to run. + clock : `Clock`, optional + The update clock to use for this operation. If neither a clock nor + the `dt` argument is specified, defaults to the clock of the group. + when : str, optional + When to run within a time step, defaults to the ``'start'`` slot. + See :ref:`scheduling` for possible values. + name : str, optional + A unique name, if non is given the name of the group appended with + 'run_custom_clock', 'run_custom_clock_1', etc. will be used. If a + name is given explicitly, it will be used as given (i.e. the group + name will not be prepended automatically). + codeobj_class : class, optional + The `CodeObject` class to run code with. If not specified, defaults + to the `group`'s ``codeobj_class`` attribute. + + Returns + ------- + obj : `CodeRunner` + A reference to the object that will be run. + """ + if name is None: + names = [o.name for o in self.contained_objects] + name = find_name(f"{self.name}_run_custom*", names) + source_group = getattr(self, "source", None) if source_group is not None: if self not in source_group.contained_objects: @@ -1195,7 +1240,6 @@ def run_regularly( "stateupdate", code=code, name=name, - dt=dt, clock=clock, when=when, order=order, @@ -1204,6 +1248,53 @@ def run_regularly( self.contained_objects.append(runner) return runner + def run_at( + self, + code, + times, + when="start", + order=0, + name=None, + codeobj_class=None, + ): + """ + Run abstract code in the group's namespace. The created `CodeRunner` + object will be automatically added to the group, it therefore does not + need to be added to the network manually. However, a reference to the + object will be returned, which can be used to later remove it from the + group or to set it to inactive. + + Parameters + ---------- + code : str + The abstract code to run. + times : array-like + The specific simulation times at which to execute the code. + when : str, optional + When to run within a time step, defaults to the ``'start'`` slot. + See :ref:`scheduling` for possible values. + name : str, optional + A unique name, if non is given the name of the group appended with + 'run_at', 'run_at_1', etc. will be used. If a + name is given explicitly, it will be used as given (i.e. the group + name will not be prepended automatically). + codeobj_class : class, optional + The `CodeObject` class to run code with. If not specified, defaults + to the `group`'s ``codeobj_class`` attribute. + + Returns + ------- + obj : `CodeRunner` + A reference to the object that will be run. + """ + if name is None: + names = [o.name for o in self.contained_objects] + name = find_name(f"{self.name}_run_at*", names) + from brian2.core.clocks import EventClock + + clock = EventClock(times) + return self.run_custom_clock(code, clock, when, order, name, codeobj_class) + def _check_for_invalid_states(self): """ Checks if any state variables updated by differential equations have diff --git a/brian2/tests/test_clocks.py b/brian2/tests/test_clocks.py index a54c03f99..bb0e9647f 100644 --- a/brian2/tests/test_clocks.py +++ b/brian2/tests/test_clocks.py @@ -1,7 +1,9 @@ +import numpy as np import pytest from numpy.testing import assert_array_equal, assert_equal from brian2 import * +from brian2.core.clocks import EventClock from brian2.utils.logger import catch_logs @@ -73,11 +75,53 @@ def test_event_clock(): assert_equal(event_clock.variables["timestep"].get_value(), 1) assert_equal(event_clock.variables["t"].get_value(), 0.1) - # Simulate reaching the end - event_clock.advance() - event_clock.advance() - with pytest.raises(StopIteration): - event_clock.advance() + +@pytest.mark.codegen_independent +def test_combined_clocks_with_run_at(): + # Create a simple NeuronGroup + G = NeuronGroup(1, "v : 1") + G.v = 0 + + # Regular clock monitoring + regular_mon = StateMonitor(G, "v", record=0, dt=1 * ms) + + # Define specific times for events + event_times = [0.5 * ms, 2.5 * ms, 4.5 * ms] + + # Create an array to store event times + event_values = [] + + # Function to record values at specific times + @network_operation(when="start") + def record_event_values(): + # Store current time if it's one of our event times + t = defaultclock.t + for event_time in event_times: + if abs(float(t - event_time)) < 1e-10: + event_values.append(float(t)) + + # Use run_at to modify v at specific times + G.run_at("v += 1", times=event_times) + + # Run simulation + run(5 * ms) + + # Verify regular monitoring worked + assert_array_equal(regular_mon.t, np.arange(0, 5.1, 1) * ms) + + # Check events occurred at correct times + assert_equal(len(event_values), len(event_times)) + assert_allclose(event_values, [float(t) for t in event_times]) + + # Check the v values reflect the events + expected_v = np.zeros_like(regular_mon.v[0]) + for t in event_times: + time_idx = np.searchsorted(regular_mon.t, t) + # All times at or after an event should have v increased + if time_idx < len(expected_v): + expected_v[time_idx:] += 1 + + assert_array_equal(regular_mon.v[0], expected_v) if __name__ == "__main__": @@ -88,3 +132,4 @@ def test_event_clock(): test_defaultclock() test_set_interval_warning() test_event_clock() + test_combined_clocks_with_run_at() From 7c37d404a1d1e3930d73393694c278ebbf7d8c4a Mon Sep 17 00:00:00 2001 From: Samu Date: Fri, 18 Apr 2025 03:00:38 +0200 Subject: [PATCH 16/23] network/clock check --- brian2/core/clocks.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 61c7994cd..039e133e9 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -165,10 +165,18 @@ class EventClock(BaseClock): def __init__(self, times, name="eventclock*"): super().__init__(name=name) self._times = sorted(times) - """if len(self._times) != len(set(self._times)): + seen = set() + duplicates = [] + for time in self._times: + if float(time) in seen: + duplicates.append(time) + else: + seen.add(float(time)) + if duplicates: raise ValueError( - "The times provided to EventClock must not contain duplicates" - )""" + "The times provided to EventClock must not contain duplicates. " + f"Duplicates found: {duplicates}" + ) self.variables["t"].set_value(self._times[0]) From fd4b3522d5fe09fbc8590795369aac34b0bc417d Mon Sep 17 00:00:00 2001 From: Samu Date: Fri, 25 Apr 2025 13:54:54 +0200 Subject: [PATCH 17/23] network/clock check --- brian2/core/clocks.py | 9 ++++ brian2/groups/group.py | 6 +-- brian2/tests/test_clocks.py | 93 +++++++++++++++++-------------------- 3 files changed, 55 insertions(+), 53 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 039e133e9..453fb5436 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -164,6 +164,15 @@ class EventClock(BaseClock): def __init__(self, times, name="eventclock*"): super().__init__(name=name) + times = Quantity(times) + from brian2.units.fundamentalunits import fail_for_dimension_mismatch + + fail_for_dimension_mismatch( + times, + second.dim, + error_message="'times' must have dimensions of time, got %(dim)s", + dim=times, + ) self._times = sorted(times) seen = set() duplicates = [] diff --git a/brian2/groups/group.py b/brian2/groups/group.py index d44dd743c..e3abbe981 100644 --- a/brian2/groups/group.py +++ b/brian2/groups/group.py @@ -1186,9 +1186,9 @@ def run_regularly( # reference to the group providing the context for the operation, i.e. # the subgroup instead of the parent group. See github issue #922 - return self.run_custom_clock(code, clock, when, order, name, codeobj_class) + return self.run_on_clock(code, clock, when, order, name, codeobj_class) - def run_custom_clock( + def run_on_clock( self, code, clock=None, @@ -1293,7 +1293,7 @@ def run_at( from brian2.core.clocks import EventClock clock = EventClock(times) - return self.run_custom_clock(code, clock, when, order, name, codeobj_class) + return self.run_on_clock(code, clock, when, order, name, codeobj_class) def _check_for_invalid_states(self): """ diff --git a/brian2/tests/test_clocks.py b/brian2/tests/test_clocks.py index bb0e9647f..fc2cff353 100644 --- a/brian2/tests/test_clocks.py +++ b/brian2/tests/test_clocks.py @@ -1,9 +1,9 @@ -import numpy as np import pytest from numpy.testing import assert_array_equal, assert_equal from brian2 import * from brian2.core.clocks import EventClock +from brian2.tests.test_network import NameLister from brian2.utils.logger import catch_logs @@ -61,67 +61,60 @@ def test_set_interval_warning(): @pytest.mark.codegen_independent def test_event_clock(): - times = [0.0, 0.1, 0.2, 0.3] + times = [0.0 * ms, 0.1 * ms, 0.2 * ms, 0.3 * ms] event_clock = EventClock(times) assert_equal(event_clock.variables["t"].get_value(), 0.0) - assert_equal(event_clock[1], 0.1) + assert_equal(event_clock[1], 0.1 * ms) event_clock.advance() assert_equal(event_clock.variables["timestep"].get_value(), 1) - assert_equal(event_clock.variables["t"].get_value(), 0.1) + assert_equal(event_clock.variables["t"].get_value(), 0.0001) - event_clock.set_interval(0.1 * second, 0.3 * second) + event_clock.set_interval(0.1 * ms, 0.3 * ms) assert_equal(event_clock.variables["timestep"].get_value(), 1) - assert_equal(event_clock.variables["t"].get_value(), 0.1) + assert_equal(event_clock.variables["t"].get_value(), 0.0001) @pytest.mark.codegen_independent def test_combined_clocks_with_run_at(): - # Create a simple NeuronGroup - G = NeuronGroup(1, "v : 1") - G.v = 0 - - # Regular clock monitoring - regular_mon = StateMonitor(G, "v", record=0, dt=1 * ms) - - # Define specific times for events - event_times = [0.5 * ms, 2.5 * ms, 4.5 * ms] - - # Create an array to store event times - event_values = [] - - # Function to record values at specific times - @network_operation(when="start") - def record_event_values(): - # Store current time if it's one of our event times - t = defaultclock.t - for event_time in event_times: - if abs(float(t - event_time)) < 1e-10: - event_values.append(float(t)) - - # Use run_at to modify v at specific times - G.run_at("v += 1", times=event_times) - - # Run simulation - run(5 * ms) - - # Verify regular monitoring worked - assert_array_equal(regular_mon.t, np.arange(0, 5.1, 1) * ms) - - # Check events occurred at correct times - assert_equal(len(event_values), len(event_times)) - assert_allclose(event_values, [float(t) for t in event_times]) - - # Check the v values reflect the events - expected_v = np.zeros_like(regular_mon.v[0]) - for t in event_times: - time_idx = np.searchsorted(regular_mon.t, t) - # All times at or after an event should have v increased - if time_idx < len(expected_v): - expected_v[time_idx:] += 1 - - assert_array_equal(regular_mon.v[0], expected_v) + defaultclock.dt = 1 * ms + + # Reset updates + NameLister.updates[:] = [] + + # Regular NameLister at 1ms interval + regular_lister = NameLister(name="x", dt=1 * ms, order=0) + + # Event NameLister at specific times + event_times = [0.5 * ms, 2.5 * ms, 4 * ms] + event_lister = NameLister(name="y", clock=EventClock(times=event_times), order=1) + + # Create and run the network + net = Network(regular_lister, event_lister) + net.run(5 * ms) + + # Get update string + updates = "".join(NameLister.updates) + + # Expected output: "x" at 0,1,2,3,4ms = 5 times + # "y" at 0.5, 2.5, 4.0ms = 3 times + # We don't care about exact timing here, just the sequence + expected_x_count = 5 + expected_y_count = 3 + + x_count = updates.count("x") + y_count = updates.count("y") + + assert ( + x_count == expected_x_count + ), f"Expected {expected_x_count} x's, got {x_count}" + assert ( + y_count == expected_y_count + ), f"Expected {expected_y_count} y's, got {y_count}" + + # Optional: check full string if needed + print(updates) if __name__ == "__main__": From bd16629a48ed172785ecca000a3c46e7b035a5af Mon Sep 17 00:00:00 2001 From: Samu Date: Sat, 26 Apr 2025 01:18:08 +0200 Subject: [PATCH 18/23] network/clock check --- brian2/core/clocks.py | 3 ++- brian2/tests/test_clocks.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 453fb5436..717361710 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -11,6 +11,7 @@ from brian2.groups.group import VariableOwner from brian2.units.allunits import second from brian2.units.fundamentalunits import Quantity, check_units +from brian2.units.stdunits import ms from brian2.utils.logger import get_logger __all__ = ["BaseClock", "Clock", "defaultclock", "EventClock"] @@ -186,7 +187,7 @@ def __init__(self, times, name="eventclock*"): "The times provided to EventClock must not contain duplicates. " f"Duplicates found: {duplicates}" ) - + self._times.append(np.inf * ms) self.variables["t"].set_value(self._times[0]) logger.diagnostic(f"Created event clock {self.name}") diff --git a/brian2/tests/test_clocks.py b/brian2/tests/test_clocks.py index fc2cff353..02a40bfd0 100644 --- a/brian2/tests/test_clocks.py +++ b/brian2/tests/test_clocks.py @@ -78,7 +78,6 @@ def test_event_clock(): @pytest.mark.codegen_independent def test_combined_clocks_with_run_at(): - defaultclock.dt = 1 * ms # Reset updates NameLister.updates[:] = [] From 2402c23c6aa6272dbf7c9424af1197749e36a35e Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Mon, 28 Apr 2025 11:42:36 +0200 Subject: [PATCH 19/23] Fix end of simulation for `EventClock` Add test that tests for run continuation --- brian2/core/clocks.py | 2 +- brian2/tests/test_network.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index 717361710..dd1352c58 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -224,7 +224,7 @@ def set_interval(self, start, end): self.variables["timestep"].set_value(start_idx) self.variables["t"].set_value(self._times[start_idx]) - self._i_end = end_idx - 1 + self._i_end = end_idx def __getitem__(self, timestep): """ diff --git a/brian2/tests/test_network.py b/brian2/tests/test_network.py index 9b623fd36..5b9385ff3 100644 --- a/brian2/tests/test_network.py +++ b/brian2/tests/test_network.py @@ -1676,12 +1676,14 @@ def test_small_runs(): # One long run and multiple small runs should give the same results group_1 = NeuronGroup(10, "dv/dt = -v / (10*ms) : 1") group_1.v = "(i + 1) / N" + group_1.run_at("v += 0.1", times=[100 * ms, 300 * ms]) mon_1 = StateMonitor(group_1, "v", record=True) net_1 = Network(group_1, mon_1) net_1.run(1 * second) group_2 = NeuronGroup(10, "dv/dt = -v / (10*ms) : 1") group_2.v = "(i + 1) / N" + group_2.run_at("v += 0.1", times=[100 * ms, 300 * ms]) mon_2 = StateMonitor(group_2, "v", record=True) net_2 = Network(group_2, mon_2) runtime = 1 * ms From 2085b7fd081709932c9762130303ae15131a0015 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Mon, 28 Apr 2025 11:44:22 +0200 Subject: [PATCH 20/23] Fix `run_regularly` to not ignore dt --- brian2/groups/group.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/brian2/groups/group.py b/brian2/groups/group.py index e3abbe981..3389d30db 100644 --- a/brian2/groups/group.py +++ b/brian2/groups/group.py @@ -1169,13 +1169,16 @@ def run_regularly( obj : `CodeRunner` A reference to the object that will be run. """ + from brian2.core.clocks import Clock # Avoid circular import + if name is None: names = [o.name for o in self.contained_objects] name = find_name(f"{self.name}_run_regularly*", names) if dt is None and clock is None: clock = self._clock - + elif clock is None: + clock = Clock(dt=dt) # Subgroups are normally not included in their parent's # contained_objects list, since there's no need to include them in the # network (they don't perform any computation on their own). However, From 49b5f7135d32c1fe1358046054f60e2cc582eb8e Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Mon, 28 Apr 2025 11:45:16 +0200 Subject: [PATCH 21/23] Move comment to correct place --- brian2/groups/group.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/brian2/groups/group.py b/brian2/groups/group.py index 3389d30db..c364f4a88 100644 --- a/brian2/groups/group.py +++ b/brian2/groups/group.py @@ -1179,15 +1179,6 @@ def run_regularly( clock = self._clock elif clock is None: clock = Clock(dt=dt) - # Subgroups are normally not included in their parent's - # contained_objects list, since there's no need to include them in the - # network (they don't perform any computation on their own). However, - # if a subgroup declares a `run_regularly` operation, then we want to - # include this operation automatically, i.e. with the parent group - # (adding just the run_regularly operation to the parent group's - # contained objects would no be enough, since the CodeObject needs a - # reference to the group providing the context for the operation, i.e. - # the subgroup instead of the parent group. See github issue #922 return self.run_on_clock(code, clock, when, order, name, codeobj_class) @@ -1233,6 +1224,15 @@ def run_on_clock( names = [o.name for o in self.contained_objects] name = find_name(f"{self.name}_run_custom*", names) + # Subgroups are normally not included in their parent's + # contained_objects list, since there's no need to include them in the + # network (they don't perform any computation on their own). However, + # if a subgroup declares a `run_regularly` operation, then we want to + # include this operation automatically, i.e. with the parent group + # (adding just the run_regularly operation to the parent group's + # contained objects would no be enough, since the CodeObject needs a + # reference to the group providing the context for the operation, i.e. + # the subgroup instead of the parent group. See github issue #922 source_group = getattr(self, "source", None) if source_group is not None: if self not in source_group.contained_objects: From c30592b4d17fded7c8c33fa524a38334e9326346 Mon Sep 17 00:00:00 2001 From: Samu Date: Sun, 4 May 2025 19:44:37 +0200 Subject: [PATCH 22/23] network/clock check --- brian2/core/clocks.py | 50 +++++++++++++++++++++++++------------ brian2/tests/test_clocks.py | 26 +++++++++++++------ 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index dd1352c58..a6b7a7664 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -4,6 +4,8 @@ __docformat__ = "restructuredtext en" +from abc import ABC, abstractmethod + import numpy as np from brian2.core.names import Nameable @@ -37,7 +39,7 @@ def check_dt(new_dt, old_dt, target_t): ------ ValueError If using the new dt value would lead to a difference in the target - time of more than Clock.epsilon_dt times `new_dt (by default, + time of more than `Clock.epsilon_dt` times ``new_dt`` (by default, 0.01% of the new dt). Examples @@ -63,9 +65,12 @@ def check_dt(new_dt, old_dt, target_t): ) -class BaseClock(VariableOwner): +class BaseClock(VariableOwner, ABC): """ - Base class for all clocks in the simulator. + Abstract base class for all clocks in the simulator. + + This class should never be instantiated directly, use one of the subclasses + like Clock or EventClock instead. Parameters ---------- @@ -76,6 +81,8 @@ class BaseClock(VariableOwner): epsilon = 1e-14 def __init__(self, name): + # We need a name right away because some devices (e.g. cpp_standalone) + # need a name for the object when creating the variables Nameable.__init__(self, name=name) self.variables = Variables(self) self.variables.add_array( @@ -98,20 +105,22 @@ def __init__(self, name): self._i_end = None logger.diagnostic(f"Created clock {self.name}") + @abstractmethod def advance(self): """ Advance the clock to the next time step. Must be implemented by subclasses. """ - raise NotImplementedError("This method must be implemented by subclasses") + pass + @abstractmethod @check_units(start=second, end=second) def set_interval(self, start, end): """ Set the start and end time of the simulation. Must be implemented by subclasses. """ - raise NotImplementedError("This method must be implemented by subclasses") + pass def __lt__(self, other): return ( @@ -131,6 +140,7 @@ def __le__(self, other): def __ge__(self, other): return self.__gt__(other) or self.same_time(other) + @abstractmethod def same_time(self, other): """ Check if two clocks are at the same time (within epsilon). @@ -145,10 +155,7 @@ def same_time(self, other): bool True if both clocks are at the same time """ - t1 = self.variables["t"].get_value().item() - t2 = other.variables["t"].get_value().item() - - return abs(t1 - t2) < self.epsilon + pass class EventClock(BaseClock): @@ -171,7 +178,7 @@ def __init__(self, times, name="eventclock*"): fail_for_dimension_mismatch( times, second.dim, - error_message="'times' must have dimensions of time, got %(dim)s", + error_message="'times' must have dimensions of time", dim=times, ) self._times = sorted(times) @@ -187,7 +194,16 @@ def __init__(self, times, name="eventclock*"): "The times provided to EventClock must not contain duplicates. " f"Duplicates found: {duplicates}" ) + self._times.append(np.inf * ms) + self.variables.add_array( + "times", + dimensions=second.dim, + size=len(self._times), + values=self._times, + dtype=np.float64, + read_only=True, + ) self.variables["t"].set_value(self._times[0]) logger.diagnostic(f"Created event clock {self.name}") @@ -198,7 +214,9 @@ def advance(self): """ new_ts = self.variables["timestep"].get_value().item() if self._i_end is not None and new_ts + 1 > self._i_end: - return + raise StopIteration( + "EventClock has reached the end of its available times." + ) new_ts += 1 self.variables["timestep"].set_value(new_ts) self.variables["t"].set_value(self._times[new_ts]) @@ -246,8 +264,8 @@ def same_time(self, other): """ Check if two clocks are at the same time. - For comparisons with Clock objects, uses the Clock's dt and epsilon_dt. - For comparisons with other EventClocks or BaseClock objects, uses the base + For comparisons with `Clock` objects, uses the Clock's dt and epsilon_dt. + For comparisons with other `EventClock` or `BaseClock` objects, uses the base epsilon value. Parameters @@ -289,11 +307,11 @@ class Clock(BaseClock): Notes ----- - Clocks are run in the same Network.run iteration if ~Clock.t is the + Clocks are run in the same `Network.run` iteration if `~Clock.t` is the same. The condition for two clocks to be considered as having the same time is - `abs(t1-t2) Date: Mon, 22 Sep 2025 15:52:13 +0200 Subject: [PATCH 23/23] Minor adjustments requested --- brian2/core/clocks.py | 17 ++++++----------- brian2/tests/test_clocks.py | 7 ++----- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/brian2/core/clocks.py b/brian2/core/clocks.py index a6b7a7664..7ab0d7978 100644 --- a/brian2/core/clocks.py +++ b/brian2/core/clocks.py @@ -181,20 +181,15 @@ def __init__(self, times, name="eventclock*"): error_message="'times' must have dimensions of time", dim=times, ) - self._times = sorted(times) - seen = set() - duplicates = [] - for time in self._times: - if float(time) in seen: - duplicates.append(time) - else: - seen.add(float(time)) - if duplicates: + + times_array = np.asarray(times, dtype=float) + unique_times = np.unique(times_array) + if len(unique_times) != len(times_array): raise ValueError( - "The times provided to EventClock must not contain duplicates. " - f"Duplicates found: {duplicates}" + "The times provided to EventClock must not contain duplicates." ) + self._times = sorted(times) self._times.append(np.inf * ms) self.variables.add_array( "times", diff --git a/brian2/tests/test_clocks.py b/brian2/tests/test_clocks.py index 189742c7c..b2c0675a7 100644 --- a/brian2/tests/test_clocks.py +++ b/brian2/tests/test_clocks.py @@ -65,9 +65,6 @@ def test_event_clock(): times = [0.0 * ms, 0.3 * ms, 0.5 * ms, 0.6 * ms] event_clock = EventClock(times) - for i in range(4): - print(event_clock[i]) - assert_equal(event_clock.variables["t"].get_value(), 0.0 * ms) assert_equal(event_clock[1], 0.3 * ms) @@ -124,8 +121,8 @@ def test_combined_clocks_with_run_at(): y_count == expected_y_count ), f"Expected {expected_y_count} y's, got {y_count}" - # Optional: check full string if needed - print(updates) + expected_output = "xyxxyxxy" + assert updates == expected_output, f"Expected {expected_output}, got {updates}" if __name__ == "__main__":