-
Notifications
You must be signed in to change notification settings - Fork 246
Feat: Introduce abstract RateMonitor class for unified rate analysis #1657
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
base: master
Are you sure you want to change the base?
Conversation
@mstimberg as initially anticipated, adding rate analysis directly to That said, I might be overthinking it—perhaps we can restrict it for spikes only :) |
@mstimberg bump on this :) |
Hi @Legend101Zz. Sorry, I did not yet have time to look into the code in detail, but regarding the
|
@mstimberg hope you are doing well :) So I add a very peculiar bug in the code I wrote and it a while to catch that , phew ... for a very brief context here for the binned fn for eventmonitor if (bin_size / self.clock.dt) % 1 > 1e-6:
raise ValueError("bin_size has to be a multiple of dt.")
# Get the total duration and number of bins
duration = self.clock.t
num_bins = int(duration / bin_size) I had a slip and had ![]() ![]() ![]() Next I'll add the tests and also check why are the current tests failing ... :) |
also @mstimberg can you pleaase guide me here , why do we get this error
|
smoothed2 = r_mon.smooth_rate(window="flat", width=5.4 * defaultclock.dt) | ||
assert_array_equal(smoothed, smoothed2) | ||
|
||
# The window size should round up to 7. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needed to fix this test
Regarding test failures: at least partly this is explained by a bug in pyparsing 3.2.4 (see pyparsing/pyparsing#618) – consider merging in |
@mstimberg what is strange I just did this change : ![]() and everything works now 😵💫 maybe it can be because the master branch was merged here ... or something related to not having assert statements inside classes , expect for tests ... But all testsuites pass now 🥳 |
The PR tests actually run on the merged version, i.e. the workaround from |
Ahh understood thanks , that makes more sense , also can you please test and let me know if this was what you expected from the newly added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks very good to me and appears to work correctly! I've left a few comments, mostly I think there are a few places where we can greatly simplify the code by being less generic and using numpy
functions. The final missing piece would be the user documentation, but let's wait until we are sure about everything else before tackling this.
|
||
|
||
class PopulationRateMonitor(Group, CodeRunner): | ||
class RateMonitor(CodeRunner, Group, Clock, ABC): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit confused: why does RateMonitor
has to inherit from Clock
? (It has a clock, but it isn't a clock)
Returns | ||
------- | ||
rate : `Quantity` | ||
The smoothed firing rate(s) in Hz. For EventMonitor subclasses, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this docstring is slightly incorrect: the smoothing function does not use any binning, the resulting array will always have the size of the original number of time steps.
np.convolve(self.rate_, window * 1.0 / sum(window), mode="same"), | ||
dim=hertz.dim, | ||
) | ||
num_bins = int(self.clock.t / bin_size) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
num_bins = int(self.clock.t / bin_size) | |
num_bins = int(round(self.clock.t / bin_size)) |
This is safer for numerical edge cases.
np.arange(num_bins) * bin_size + bin_size / 2.0 | ||
) # as we want Bin centers (not edges) | ||
|
||
t_indices = (self.t / self.clock.dt).astype(int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
t_indices = (self.t / self.clock.dt).astype(int) | |
t_indices = np.round(self.t / self.clock.dt).astype(int) |
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, let's discuss this during our meeting, the issue is slightly more complicated than this.
) # as we want Bin centers (not edges) | ||
|
||
t_indices = (self.t / self.clock.dt).astype(int) | ||
bin_indices = (t_indices * self.clock.dt / bin_size).astype(int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bin_indices = (t_indices * self.clock.dt / bin_size).astype(int) | |
bin_indices = np.round(t_indices * self.clock.dt / bin_size).astype(int) |
binned_values[neuron_idx, bin_idx] += 1 | ||
|
||
# Convert counts to rates (Hz) | ||
binned_values = binned_values / float(bin_size) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something I didn't think about earlier: if we don't return spike counts but rates here (which probably makes sense given that we are inheriting from RateMonitor, and that this is the behaviour in the PopulationRateMonitor.binned
method), then we should probably call this method binned_rate
– this makes it nicely parallel with smooth_rate
.
binned_values = np.zeros((num_neurons, num_bins)) | ||
if self.record: | ||
# Get the event times and indices | ||
event_times = self.t[:] | ||
event_indices = ( | ||
self.i[:] - neuron_offset | ||
) # Adjust for subgroups as stated above | ||
|
||
bin_indices = (event_times / bin_size).astype(int) | ||
# Now this is the main core code , here we count the events in each bin that happened for each neuron | ||
# Like after this we should have something like : | ||
# Example : | ||
# binned_values = [ | ||
# [2.0, 0.0, 1.0, 0.0, 0.0], # Neuron 0: 2 in bin 0, 1 in bin 2 | ||
# [0.0, 1.0, 0.0, 1.0, 0.0], # Neuron 1: 1 in bin 1, 1 in bin 3 | ||
# [0.0, 2.0, 0.0, 0.0, 1.0] # Neuron 2: 2 in bin 1, 1 in bin 4 | ||
# ] | ||
for event_idx, neuron_idx in enumerate(event_indices): | ||
if 0 <= neuron_idx < num_neurons: # sanity check | ||
bin_idx = bin_indices[event_idx] | ||
if bin_idx < num_bins: # To handle edge case at the end | ||
binned_values[neuron_idx, bin_idx] += 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do not care about half-filled bins in the end of the array (and I think we should not), then I think all of this can be replaced by a call to numpy's bincount
function (https://numpy.org/doc/stable/reference/generated/numpy.bincount.html) 😊
@pytest.mark.codegen_independent | ||
def test_population_rate_monitor_binning(): | ||
"""Test binning functionality for PopulationRateMonitor.""" | ||
# Create a group with regular spiking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The group below has random instead of regular spiking. It would probably be better to use regular spiking, though (e.g. SpikeGeneratorGroup
), in that way we could have a precise test instead of only assuring that the values are in a reasonable range.
times = np.arange(40, 61) * ms # Burst from 40-60ms | ||
indices = np.zeros(21, dtype=int) | ||
|
||
G = SpikeGeneratorGroup(1, indices, times) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be important here to test with more than one neuron in the group.
num_neurons, num_bins = binned_array.shape | ||
smoothed = np.zeros((num_neurons, num_bins)) | ||
for i in range(num_neurons): | ||
smoothed[i, :] = np.convolve(binned_array[i, :], window, mode="same") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We previously depended on scipy
which has a function convolve2d
, but without it this is probably the best approach.
Closes #1368
Based on the great discussions and instructions in the issue (thanks, they were really helpful!), I've started the implementation for a unified rate-handling mechanism.
So far, I've created the abstract
RateMonitor
base class and refactoredPopulationRateMonitor
to inherit from it. I've also added the newbinned()
method toPopulationRateMonitor
to allow for re-binning of the recorded population rate.So I guess ,
EventMonitor
is a bit more complex since it handles generic events, not just spikes. I'll need to look more closely at how to pass the correct variable information to the template for that implementation.Did:
RateMonitor
class.PopulationRateMonitor
to inherit fromRateMonitor
.binned()
method forPopulationRateMonitor
.ToDo:
binned()
method forEventMonitor
to calculate binned rates from event times.