Skip to content

Commit c63c36b

Browse files
authored
Merge pull request #515 from pynapple-org/dev
Bumping 0.10.1
2 parents 7db4807 + f7612b0 commit c63c36b

File tree

8 files changed

+314
-164
lines changed

8 files changed

+314
-164
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ pynapple is a light-weight python library for neurophysiological data analysis.
1919
2020
------------------------------------------------------------------------
2121

22+
Learning pynapple
23+
-----------------
24+
25+
Workshops are regularly organized by the [center for Computational Neuroscience ](https://www.simonsfoundation.org/flatiron/center-for-computational-neuroscience/) of the Flatiron institute
26+
to teach pynapple & [NeMos](https://nemos.readthedocs.io/en/latest/) to new users.
27+
28+
**The next workshop will take place in New York City on February 2 - 5, 2026. Register [here](https://www.simonsfoundation.org/event/flatiron-ccn-neural-data-analysis-workshop/).**
29+
30+
2231
New release :fire:
2332
------------------
2433

doc/releases.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Releases
22

3+
4+
### 0.10.1 (2025-10-30)
5+
6+
- Fixing smoothing for `nap.decode_bayes`.
7+
- Fixing `np.einsum`.
8+
39
### 0.10.0 (2025-10-27)
410

511
- Generalizing `nap.compute_tuning_curves`. It can take any time series object (Tsd, TsdFrame, TsGroup, TsdTensor) as input and

doc/user_guide/07_decoding.md

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ Input to both decoding functions always includes:
3232
- `tuning_curves`, computed using [`compute_tuning_curves`](pynapple.process.tuning_curves.compute_tuning_curves).
3333
- `data`, neural activity as a `TsGroup` (spikes) or `TsdFrame` (smoothed counts or calcium activity or any other time series).
3434
- `epochs`, to restrict decoding to certain intervals.
35-
- `smoothing`, type of smoothing to apply to `data`, defaults to `None`, indicating no smoothing, but can be `gaussian` or `uniform`.
36-
- `smoothing_window`, smoothing window to use if `smoothing` is provided.
35+
- `sliding_window_size`, uniform convolution window size (in number of bins) to smooth spike counts, only used if a `TsGroup` is passed (default is `None`, for no smoothing). This is equivalent to using a sliding window with overlapping bins.
3736
- `bin_size`, the size of the bins in which to count timestamps when data is a `TsGroup` object.
3837
- `time_units`, the units of `bin_size`, defaulting to seconds.
3938

@@ -84,7 +83,7 @@ First, we compute the tuning curves:
8483
tuning_curves_1d = nap.compute_tuning_curves(
8584
tsgroup,
8685
feature,
87-
bins=61,
86+
bins=60,
8887
range=(0, 2 * np.pi),
8988
feature_names=["Circular feature"]
9089
)
@@ -98,18 +97,17 @@ tuning_curves_1d.plot.line(x="Circular feature", add_legend=False)
9897
plt.show()
9998
```
10099

101-
We can then use `nap.decode_bayes` for Bayesian decoding.
102-
We will use the `smoothing` and `smoothing_window` arguments to additionally smooth the
103-
spike counts, this often helps with decoding:
100+
We can then use [`decode_bayes`](pynapple.process.decoding.decode_bayes) for Bayesian decoding.
101+
We will use the `sliding_window_size` argument to additionally smooth the
102+
spike counts with a uniform convolution window (i.e. use a sliding window), which often helps with decoding.
104103

105104
```{code-cell} ipython3
106105
decoded, proba_feature = nap.decode_bayes(
107106
tuning_curves=tuning_curves_1d,
108107
data=tsgroup,
109108
epochs=epochs,
110-
smoothing="gaussian",
111-
smoothing_window=0.1,
112-
bin_size=0.06,
109+
sliding_window_size=4,
110+
bin_size=0.02,
113111
)
114112
```
115113

@@ -198,16 +196,15 @@ tuning_curves_2d.plot(row="unit", col_wrap=6)
198196
plt.show()
199197
```
200198

201-
and then, `nap.decode_bayes` again performs bayesian decoding:
199+
and then, [`decode_bayes`](pynapple.process.decoding.decode_bayes) again performs bayesian decoding:
202200

203201
```{code-cell} ipython3
204202
decoded, proba_feature = nap.decode_bayes(
205203
tuning_curves=tuning_curves_2d,
206204
data=ts_group,
207205
epochs=epochs,
208-
smoothing="gaussian",
209-
smoothing_window=0.2,
210-
bin_size=0.1,
206+
sliding_window_size=2,
207+
bin_size=0.05,
211208
)
212209
```
213210

@@ -306,13 +303,14 @@ tuning_curves_1d.plot.line(x="Circular feature", add_legend=False)
306303
plt.show()
307304
```
308305

309-
We can then use `nap.decode_template` for template matching:
306+
We can then use [`decode_template`](pynapple.process.decoding.decode_template) for template matching:
310307

311308
```{code-cell} ipython3
312309
decoded, dist = nap.decode_template(
313310
tuning_curves=tuning_curves_1d,
314311
data=tsgroup,
315312
epochs=epochs,
313+
sliding_window_size=4,
316314
bin_size=0.05,
317315
metric="correlation"
318316
)
@@ -402,7 +400,7 @@ tuning_curves_2d.plot(row="unit", col_wrap=6)
402400
plt.show()
403401
```
404402

405-
and then, `nap.decode_template` again performs template matching:
403+
and then, [`decode_template`](pynapple.process.decoding.decode_template) again performs template matching:
406404

407405
```{code-cell} ipython3
408406
decoded, dist = nap.decode_template(

pynapple/core/metadata_class.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import itertools
44
import warnings
55
from collections import UserDict
6+
from functools import wraps
67
from numbers import Number
78
from typing import Union
89

@@ -28,6 +29,7 @@ def add_or_convert_metadata(func):
2829
Decorator for backwards compatibility of objects picked with older versions of pynapple.
2930
"""
3031

32+
@wraps(func)
3133
def _decorator(self, *args, **kwargs):
3234
if (
3335
(len(args) == 1)

pynapple/core/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ def modifies_time_axis(func, new_args, kwargs):
514514
try:
515515
sig = inspect.signature(func)
516516
except (TypeError, ValueError):
517-
return True # conservative
517+
return False
518518

519519
bound = sig.bind_partial(*new_args, **kwargs)
520520
bound.apply_defaults()
@@ -532,7 +532,7 @@ def modifies_time_axis(func, new_args, kwargs):
532532

533533
ndim = getattr(arr, "ndim", None)
534534
if ndim is None:
535-
return True # conservative
535+
return False
536536

537537
### 1) single-axis arguments ###
538538
axis = bound.arguments.get("axis", inspect._empty)

pynapple/process/decoding.py

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,12 @@ def wrapper(*args, **kwargs):
4545
units=kwargs["time_units"],
4646
),
4747
)[0]:
48-
raise ValueError(
49-
"passed bin_size too different from actual data bin size."
50-
)
48+
warnings.warn("passed bin_size is different from actual data bin size.")
5149
elif isinstance(data, nap.TsGroup):
5250
data = data.count(
5351
kwargs["bin_size"], kwargs["epochs"], time_units=kwargs["time_units"]
5452
)
55-
was_continuous = True
53+
was_continuous = False
5654
else:
5755
raise TypeError("Unknown format for data.")
5856

@@ -72,28 +70,22 @@ def wrapper(*args, **kwargs):
7270
)
7371

7472
# smooth
75-
smoothing = kwargs["smoothing"]
76-
smoothing_window = kwargs["smoothing_window"]
77-
if smoothing is not None:
78-
if smoothing not in ["gaussian", "uniform"]:
79-
raise ValueError("smoothing should be one of 'gaussian' or 'uniform'.")
80-
if not isinstance(smoothing_window, (int, float)):
81-
raise ValueError("smoothing_window should be a number.")
82-
if smoothing == "gaussian":
83-
data = data.smooth(
84-
smoothing_window,
85-
time_units=kwargs["time_units"],
86-
)
73+
sliding_window_size = kwargs["sliding_window_size"]
74+
if sliding_window_size is not None:
75+
if not isinstance(sliding_window_size, int):
76+
raise ValueError("sliding_window_size should be a integer.")
77+
if sliding_window_size < 1:
78+
raise ValueError("sliding_window_size should be >= 1.")
79+
data = data.convolve(
80+
np.ones(sliding_window_size),
81+
ep=kwargs["epochs"],
82+
)
83+
if was_continuous:
84+
data = data / sliding_window_size
8785
else:
88-
smoothing_window_bins = max(
89-
1, int(smoothing_window / kwargs["bin_size"])
90-
)
91-
data = data.convolve(
92-
np.ones(smoothing_window_bins),
93-
ep=kwargs["epochs"],
94-
)
95-
if was_continuous:
96-
data = data / smoothing_window_bins
86+
bin_size = sliding_window_size * kwargs["bin_size"]
87+
kwargs["bin_size"] = bin_size
88+
9789
kwargs["data"] = data
9890

9991
# Call the original function with validated inputs
@@ -112,7 +104,7 @@ def _format_decoding_outputs(dist, tuning_curves, data, epochs, greater_is_bette
112104
all_nan = np.isnan(dist).all(axis=1)
113105
idx[all_nan] = -1
114106

115-
# Format probability distribution
107+
# Format probability/distance distribution
116108
dist = dist.reshape(dist.shape[0], *tuning_curves.shape[1:])
117109
if dist.ndim > 2:
118110
dist = nap.TsdTensor(
@@ -167,8 +159,7 @@ def decode_bayes(
167159
data,
168160
epochs,
169161
bin_size,
170-
smoothing=None,
171-
smoothing_window=None,
162+
sliding_window_size=None,
172163
time_units="s",
173164
uniform_prior=True,
174165
):
@@ -224,10 +215,9 @@ def decode_bayes(
224215
The epochs on which decoding is computed
225216
bin_size : float
226217
Bin size. Default in seconds. Use ``time_units`` to change it.
227-
smoothing : str, optional
228-
Type of smoothing to apply to the binned spikes counts (``None`` [default], ``gaussian``, ``uniform``).
229-
smoothing_window : float, optional
230-
Size smoothing window. Default in seconds. Use ``time_units`` to change it.
218+
sliding_window_size : int, optional
219+
The size, in number of bins, for a uniform window to be convolved with the counts array for each neuron. Value should be >= 1.
220+
If None (default), no smoothing is applied.
231221
time_units : str, optional
232222
Time unit of the bin size (``s`` [default], ``ms``, ``us``).
233223
uniform_prior : bool, optional
@@ -382,29 +372,35 @@ def decode_bayes(
382372
98.5 1.0 1.0
383373
dtype: float64, shape: (98, 2)
384374
"""
385-
occupancy = (
375+
prior = (
386376
np.ones_like(tuning_curves[0]).flatten()
387377
if uniform_prior
388378
else tuning_curves.attrs["occupancy"].flatten()
389379
)
380+
prior = prior.astype(np.float64)
381+
prior /= prior.sum()
390382

391-
tc = tuning_curves.values.reshape(tuning_curves.sizes["unit"], -1).T
392-
ct = data.values
383+
rate_map = tuning_curves.values.reshape(tuning_curves.sizes["unit"], -1).T
384+
observed_counts = data.values
393385
bin_size_s = nap.TsIndex.format_timestamps(
394386
np.array([bin_size], dtype=np.float64), time_units
395387
)[0]
388+
observed_counts_expanded = np.tile(
389+
observed_counts[:, np.newaxis, :], (1, rate_map.shape[0], 1)
390+
)
396391

397-
p1 = np.exp(-bin_size_s * np.nansum(tc, 1))
398-
p2 = occupancy / occupancy.sum()
399-
400-
ct2 = np.tile(ct[:, np.newaxis, :], (1, tc.shape[0], 1))
392+
EPS = 1e-12
393+
log_likelihood = np.nansum(
394+
observed_counts_expanded * np.log(rate_map + EPS) - bin_size_s * rate_map,
395+
axis=-1,
396+
)
401397

402-
p3 = np.nanprod(tc**ct2, -1)
398+
log_posterior = log_likelihood + np.log(prior)
399+
posterior = np.exp(log_posterior - log_posterior.max(axis=1, keepdims=True))
400+
posterior /= posterior.sum(axis=1, keepdims=True)
403401

404-
p = p1 * p2 * p3
405-
p = p / p.sum(1)[:, np.newaxis]
406402
return _format_decoding_outputs(
407-
p, tuning_curves, data, epochs, greater_is_better=True
403+
posterior, tuning_curves, data, epochs, greater_is_better=True
408404
)
409405

410406

@@ -415,8 +411,7 @@ def decode_template(
415411
epochs,
416412
bin_size,
417413
metric="correlation",
418-
smoothing=None,
419-
smoothing_window=None,
414+
sliding_window_size=None,
420415
time_units="s",
421416
):
422417
"""
@@ -476,10 +471,9 @@ def decode_template(
476471
477472
If a callable, it must have the signature ``metric(u, v) -> float`` and
478473
return the distance between two 1D arrays.
479-
smoothing : str, optional
480-
Type of smoothing to apply to the binned spikes counts (``None`` [default], ``gaussian``, ``uniform``).
481-
smoothing_window : float, optional
482-
Size smoothing window. Default in seconds. Use ``time_units`` to change it.
474+
sliding_window_size : int, optional
475+
The size, in number of bins, for a uniform window to be convolved with the counts array for each neuron. Value should be >= 1.
476+
If None (default), no smoothing is applied.
483477
time_units : str, optional
484478
Time unit of the bin size (``s`` [default], ``ms``, ``us``).
485479

0 commit comments

Comments
 (0)