Skip to content

Commit f28f8fd

Browse files
authored
Merge pull request #65 from AllenNeuralDynamics/kh-dev_reward-rate
adding a util function to calculate decaying event(reward)-rate
2 parents aa20883 + 32922f3 commit f28f8fd

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Compute an event-rate timeseries by convolving discrete events"""
2+
3+
import numpy as np
4+
from scipy.signal import fftconvolve
5+
6+
7+
def compute_event_rate(
8+
event_times,
9+
t_start=None,
10+
t_end=None,
11+
dt=1 / 20, # matching photometry frame rate
12+
tau=25.0, # to be optimised
13+
kernel="exp", # exp/hyperbolic
14+
hyper_p=1.0, # only for hyperbolic
15+
normalize_kernel=True,
16+
):
17+
"""
18+
Compute an event-rate timeseries by convolving discrete events
19+
(e.g. rewards) with a decay kernel.
20+
21+
Parameters
22+
----------
23+
event_times : (N,) array-like
24+
1-D array of event (e.g. reward) timestamps [s].
25+
t_start, t_end : float, optional
26+
Output time range [s]. Defaults: t_start=0, t_end=max(event_times).
27+
dt : float, default 0.001
28+
Time step of the output signal [s].
29+
If too small, computation gets too slow, now matching photometry frame rate
30+
tau : float, default 1.0
31+
Time constant of the kernel [s].
32+
A parameter to be optimised using behavior relationship etc.
33+
kernel : {"exp", "hyperbolic"}, default "exp"
34+
* "exp": k(t) = exp(-t / tau) (classic EWMA)
35+
* "hyperbolic": k(t) = (1 + t / tau)^{-hyper_p} (hyperbolic discounting)
36+
hyper_p : float, default 1.0
37+
Power *p* of the hyperbolic kernel. Ignored if kernel="exp".
38+
normalize_kernel : bool, default True
39+
If True, scale the kernel so its integral equals 1.
40+
41+
Returns
42+
-------
43+
t : (T,) ndarray
44+
Time axis [s].
45+
e_rate : (T,) ndarray
46+
event rate (events · s⁻¹).
47+
48+
Notes
49+
-----
50+
* Kernel is truncated at 5 × tau to keep computation fast.
51+
* fftconvolve gives O(N log N) performance.
52+
"""
53+
54+
event_times = np.asarray(event_times, dtype=float)
55+
if event_times.ndim != 1:
56+
raise ValueError("event_times must be a 1-D array of timestamps.")
57+
58+
# ------------------------------------------------------------------
59+
# Time axis: start at 0 so the series is zero until the first reward
60+
# ------------------------------------------------------------------
61+
if t_start is None:
62+
t_start = 0.0
63+
if t_end is None:
64+
t_end = event_times.max()
65+
t = np.arange(t_start, t_end + dt, dt)
66+
67+
# ------------------------------------------------------------------
68+
# Binary event series aligned to the timeline
69+
# ------------------------------------------------------------------
70+
event_series = np.zeros_like(t)
71+
idx = np.searchsorted(t, event_times - 1e-12)
72+
idx = idx[(idx >= 0) & (idx < len(t))]
73+
event_series[idx] = 1.0
74+
75+
# ------------------------------------------------------------------
76+
# Build the decay kernel
77+
# ------------------------------------------------------------------
78+
t_kernel = np.arange(0, 5 * tau, dt)
79+
80+
if kernel == "exp":
81+
k = np.exp(-t_kernel / tau)
82+
83+
elif kernel == "hyperbolic":
84+
if hyper_p <= 0:
85+
raise ValueError("hyper_p must be > 0 for a valid hyperbolic kernel.")
86+
k = (1.0 + t_kernel / tau) ** (-hyper_p)
87+
88+
else:
89+
raise ValueError("kernel must be 'exp' or 'hyperbolic'.")
90+
91+
if normalize_kernel:
92+
k /= k.sum() * dt # area = 1
93+
94+
# ------------------------------------------------------------------
95+
# Convolution (truncate to match timeline length)
96+
# ------------------------------------------------------------------
97+
e_rate = fftconvolve(event_series, k, mode="full")[: len(t)]
98+
99+
return t, e_rate

0 commit comments

Comments
 (0)