Skip to content

Fix issue run at #1602

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

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 34 additions & 16 deletions brian2/core/clocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

__docformat__ = "restructuredtext en"

from abc import ABC, abstractmethod

import numpy as np

from brian2.core.names import Nameable
Expand Down Expand Up @@ -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
Expand All @@ -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
----------
Expand All @@ -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(
Expand All @@ -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 (
Expand All @@ -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).
Expand All @@ -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):
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel it is a bit confusing that the Quantity array gets converted into a list here. But maybe more importantly, I think we will need to create a times array with Brian's add_array mechanism to get things to work with C++ standalone mode.

Copy link
Contributor Author

@De-Cri De-Cri May 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created the times array, does it make sense to use a numpy array instead of a list there or maybe just sorting manually.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you create the Brian variable times below, it will already be stored internally as a numpy array. To not duplicate things, I'd not store self._times at all, e.g. you could do times = sorted(times) and times.append(np.inf*ms) before passing values=times to the add_array function.

Expand All @@ -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}")
Expand All @@ -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])
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)<epsilon*abs(t1), a standard test for equality of floating
point values. The value of `epsilon is 1e-14.
``abs(t1-t2)<epsilon*abs(t1)``, a standard test for equality of floating
point values. The value of ``epsilon`` is ``1e-14``.
"""

#: The relative difference for times (in terms of dt) so that they are
Expand Down
26 changes: 19 additions & 7 deletions brian2/tests/test_clocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from brian2 import *
from brian2.core.clocks import EventClock
from brian2.tests.test_network import NameLister
from brian2.units.fundamentalunits import DimensionMismatchError
from brian2.utils.logger import catch_logs


Expand Down Expand Up @@ -61,19 +62,31 @@ def test_set_interval_warning():

@pytest.mark.codegen_independent
def test_event_clock():
times = [0.0 * ms, 0.1 * ms, 0.2 * ms, 0.3 * ms]
times = [0.0 * ms, 0.3 * ms, 0.5 * ms, 0.6 * ms]
event_clock = EventClock(times)

assert_equal(event_clock.variables["t"].get_value(), 0.0)
assert_equal(event_clock[1], 0.1 * ms)
for i in range(4):
print(event_clock[i])
Comment on lines +68 to +69
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left-over print


assert_equal(event_clock.variables["t"].get_value(), 0.0 * ms)
assert_equal(event_clock[1], 0.3 * ms)

event_clock.advance()
assert_equal(event_clock.variables["timestep"].get_value(), 1)
assert_equal(event_clock.variables["t"].get_value(), 0.0001)
assert_equal(event_clock.variables["t"].get_value(), 0.0003)

event_clock.set_interval(0.1 * ms, 0.3 * ms)
event_clock.set_interval(0.3 * ms, 0.6 * ms)
assert_equal(event_clock.variables["timestep"].get_value(), 1)
assert_equal(event_clock.variables["t"].get_value(), 0.0001)
assert_equal(event_clock.variables["t"].get_value(), 0.0003)
event_clock.advance()
event_clock.advance()

with pytest.raises(StopIteration):
event_clock.advance()

invalid_times = [0.0 * volt, 0.5 * volt]
with pytest.raises(DimensionMismatchError) as excinfo:
EventClock(invalid_times)


@pytest.mark.codegen_independent
Expand All @@ -98,7 +111,6 @@ def test_combined_clocks_with_run_at():

# 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not mean to only remove the comment, but also to actually care about the exact output in the test below 😊

expected_x_count = 5
expected_y_count = 3

Expand Down
Loading