Skip to content

Commit

Permalink
Merge pull request #400 from pynapple-org/dev
Browse files Browse the repository at this point in the history
Bump v0.8.2
  • Loading branch information
gviejo authored Jan 22, 2025
2 parents 440a9f4 + 544eda7 commit 8363f9a
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 53 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ jobs:
- name: Sphinx build
run: |
sphinx-build doc _build
# - name: Deploy to GitHub Pages
# uses: peaceiris/actions-gh-pages@v3
# if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
# with:
# publish_branch: gh-pages
# github_token: ${{ secrets.GITHUB_TOKEN }}
# publish_dir: _build/
# force_orphan: true
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
with:
publish_branch: gh-pages
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: _build/
force_orphan: true
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pynapple is a light-weight python library for neurophysiological data analysis.
New release :fire:
------------------

### pynapple >= 0.8.1
### pynapple >= 0.8.2

The objects `IntervalSet`, `TsdFrame` and `TsGroup` inherits a new metadata class. It is now possible to add labels for
each interval of an `IntervalSet`, each column of a `TsdFrame` and each unit of a `TsGroup`.
Expand Down
4 changes: 2 additions & 2 deletions doc/external.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ As such, it can function as a foundational element for other analysis packages h

![image](https://raw.githubusercontent.com/flatironinstitute/nemos/main/docs/assets/glm_features_scheme.svg)

[NeMoS](https://nemos.readthedocs.io/en/stable/) is a statistical modeling framework optimized for systems neuroscience and powered by JAX. It streamlines the process of defining and selecting models, through a collection of easy-to-use methods for feature design.
[NeMoS](https://nemos.readthedocs.io/en/latest/index.html) is a statistical modeling framework optimized for systems neuroscience and powered by JAX. It streamlines the process of defining and selecting models, through a collection of easy-to-use methods for feature design.

The core of nemos includes GPU-accelerated, well-tested implementations of standard statistical models, currently focusing on the Generalized Linear Model (GLM).

Check out this [page](https://nemos.readthedocs.io/en/stable/generated/neural_modeling/) for many examples of neural modelling using nemos and pynapple.
Check out this [page](https://nemos.readthedocs.io/en/latest/tutorials/README.html) for many examples of neural modelling using nemos and pynapple.

```{eval-rst}
.. Note::
Expand Down
5 changes: 5 additions & 0 deletions doc/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ of the Flatiron institute.

## Releases

### 0.8.2 (2025-01-22)

- `compute_power_spectral_density` now computes the periodogram, where previously it was only computing the FFT
- `compute_fft` has been added that contains the old functionality of `compute_power_spectral_density`.

### 0.8.1 (2025-01-17)

- Bugfix : time support was not updated for `bin_average` and `interpolate` with new `_initialize_tsd_output` method
Expand Down
2 changes: 1 addition & 1 deletion pynapple/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.8.1"
__version__ = "0.8.2"
from .core import (
IntervalSet,
Ts,
Expand Down
5 changes: 1 addition & 4 deletions pynapple/process/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@
shift_timestamps,
shuffle_ts_intervals,
)
from .spectrum import (
compute_mean_power_spectral_density,
compute_power_spectral_density,
)
from .spectrum import compute_fft, compute_mean_fft, compute_power_spectral_density
from .tuning_curves import (
compute_1d_mutual_info,
compute_1d_tuning_curves,
Expand Down
78 changes: 72 additions & 6 deletions pynapple/process/spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,9 @@ def wrapper(*args, **kwargs):


@_validate_spectrum_inputs
def compute_power_spectral_density(
sig, fs=None, ep=None, full_range=False, norm=False, n=None
):
def compute_fft(sig, fs=None, ep=None, full_range=False, norm=False, n=None):
"""
Compute Power Spectral Density over a single epoch.
Compute Fast Fourier Transform over a single epoch.
Perform numpy fft on sig, returns output assuming a constant sampling rate for the signal.
Parameters
Expand All @@ -74,12 +72,14 @@ def compute_power_spectral_density(
-------
pandas.DataFrame
Time frequency representation of the input signal, indexes are frequencies, values
are powers.
are the FFT.
Notes
-----
This function computes fft on only a single epoch of data. This epoch be given with the ep
parameter otherwise will be sig.time_support, but it must only be a single epoch.
If `full_range` is False, the nyquist frequency is excluded in the output due to how computes the FFT frequencies in `numpy.fft.fftfreq`.
"""
if ep is None:
ep = sig.time_support
Expand All @@ -105,7 +105,73 @@ def compute_power_spectral_density(


@_validate_spectrum_inputs
def compute_mean_power_spectral_density(
def compute_power_spectral_density(sig, fs=None, ep=None, full_range=False, n=None):
"""
Compute Power Spectral Density over a single epoch.
Perform numpy fft on sig and obtain the periodogram, returns output assuming a constant sampling rate for the signal.
Parameters
----------
sig : pynapple.Tsd or pynapple.TsdFrame
Time series.
fs : float, optional
Sampling rate, in Hz. If None, will be calculated from the given signal
ep : None or pynapple.IntervalSet, optional
The epoch to calculate the fft on. Must be length 1.
full_range : bool, optional
If true, will return full fft frequency range, otherwise will return only positive values
n: int, optional
Length of the transformed axis of the output. If n is smaller than the length of the input,
the input is cropped. If it is larger, the input is padded with zeros. If n is not given,
the length of the input along the axis specified by axis is used.
Returns
-------
pandas.DataFrame
Power spectral density of the input signal, indexes are frequencies, values
are powers/frequency.
Notes
-----
This function computes fft on only a single epoch of data. This epoch be given with the ep
parameter otherwise will be sig.time_support, but it must only be a single epoch.
The power spectral density is calculated as the square of the absolute value of the FFT, scaled by the sampling rate and length of the signal.
See [this tutorial](https://www.mathworks.com/help/signal/ug/power-spectral-density-estimates-using-fft.html) for more information.
If `full_range` is False, the nyquist frequency is excluded in the output due to how computes the FFT frequencies in `numpy.fft.fftfreq`.
"""

if ep is None:
ep = sig.time_support
if len(ep) != 1:
raise ValueError("Given epoch (or signal time_support) must have length 1")
if fs is None:
fs = sig.rate
if n is None:
n = len(sig.restrict(ep))

fft = compute_fft(sig, fs=fs, ep=ep, n=n, full_range=full_range)

# transform to power spectral density, power/Hz
psd = (1 / (fs * n)) * np.abs(fft) ** 2

if full_range is False:
# frequencies not at 0 and not at the nyquist frequency occur twice
# subtract from the nyquist frequency to adjust for floating point error in np.fft.fftfreq
# nyquist freq may occur at negative end of frequencies if N is even
doubled_freqs = (
(fft.index != 0) # not 0
& (fft.index < (fs / 2 - 1e-6)) # less than positive nyquist freq
& (fft.index > (-fs / 2 + 1e-6)) # greater than negative nyquist freq
)
psd[doubled_freqs] *= 2

return psd


@_validate_spectrum_inputs
def compute_mean_fft(
sig,
interval_size,
fs=None,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pynapple"
version = "0.8.1"
version = "0.8.2"
description = "PYthon Neural Analysis Package Pour Laboratoires d’Excellence"
readme = "README.md"
authors = [{ name = "Guillaume Viejo", email = "[email protected]" }]
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
test_suite='tests',
tests_require=test_requirements,
url='https://github.com/pynapple-org/pynapple',
version='v0.8.1',
version='v0.8.2',
zip_safe=False,
long_description_content_type='text/markdown',
download_url='https://github.com/pynapple-org/pynapple/archive/refs/tags/v0.8.1.tar.gz'
download_url='https://github.com/pynapple-org/pynapple/archive/refs/tags/v0.8.2.tar.gz'
)
95 changes: 78 additions & 17 deletions tests/test_power_spectral_density.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,40 @@ def get_sorted_fft(data, fs):
return fft_freq[order], fft[order]


def test_compute_power_spectral_density():
def get_periodogram(data, fs, full_range=False):
return_onesided = not full_range
f, p = signal.periodogram(
data, fs, return_onesided=return_onesided, detrend=False, axis=0
)
if p.ndim == 1:
p = p[:, np.newaxis]
if full_range:
order = np.argsort(f)
f = f[order]
p = p[order]
else:
f = f[:-1]
p = p[:-1]
return f, p


def test_compute_fft():

t = np.linspace(0, 1, 1000)
sig = nap.Tsd(d=np.random.random(1000), t=t)
r = nap.compute_power_spectral_density(sig)
r = nap.compute_fft(sig)
assert isinstance(r, pd.DataFrame)
assert r.shape[0] == 500

a, b = get_sorted_fft(sig.values, sig.rate)
np.testing.assert_array_almost_equal(r.index.values, a[a >= 0])
np.testing.assert_array_almost_equal(r.values, b[a >= 0])

r = nap.compute_power_spectral_density(sig, norm=True)
r = nap.compute_fft(sig, norm=True)
np.testing.assert_array_almost_equal(r.values, b[a >= 0] / len(sig))

sig = nap.TsdFrame(d=np.random.random((1000, 4)), t=t)
r = nap.compute_power_spectral_density(sig)
r = nap.compute_fft(sig)
assert isinstance(r, pd.DataFrame)
assert r.shape == (500, 4)

Expand All @@ -47,14 +64,57 @@ def test_compute_power_spectral_density():
np.testing.assert_array_almost_equal(r.values, b[a >= 0])

sig = nap.TsdFrame(d=np.random.random((1000, 4)), t=t)
r = nap.compute_power_spectral_density(sig, full_range=True)
r = nap.compute_fft(sig, full_range=True)
assert isinstance(r, pd.DataFrame)
assert r.shape == (1000, 4)

a, b = get_sorted_fft(sig.values, sig.rate)
np.testing.assert_array_almost_equal(r.index.values, a)
np.testing.assert_array_almost_equal(r.values, b)

t = np.linspace(0, 1, 1000)
sig = nap.Tsd(d=np.random.random(1000), t=t)
r = nap.compute_fft(sig, ep=sig.time_support)
assert isinstance(r, pd.DataFrame)
assert r.shape[0] == 500

t = np.linspace(0, 1, 1000)
sig = nap.Tsd(d=np.random.random(1000), t=t)
r = nap.compute_fft(sig, fs=1000)
assert isinstance(r, pd.DataFrame)
assert r.shape[0] == 500


def test_compute_power_spectral_density():

t = np.linspace(0, 1, 1000)
sig = nap.Tsd(d=np.random.random(1000), t=t)
r = nap.compute_power_spectral_density(sig)
assert isinstance(r, pd.DataFrame)
assert r.shape[0] == 500

a, b = get_periodogram(sig.values, sig.rate)
np.testing.assert_array_almost_equal(r.index.values, a[a >= 0])
np.testing.assert_array_almost_equal(r.values, b[a >= 0])

sig = nap.TsdFrame(d=np.random.random((1000, 4)), t=t)
r = nap.compute_power_spectral_density(sig)
assert isinstance(r, pd.DataFrame)
assert r.shape == (500, 4)

a, b = get_periodogram(sig.values, sig.rate)
np.testing.assert_array_almost_equal(r.index.values, a[a >= 0])
np.testing.assert_array_almost_equal(r.values, b[a >= 0])

sig = nap.TsdFrame(d=np.random.random((1000, 4)), t=t)
r = nap.compute_power_spectral_density(sig, full_range=True)
assert isinstance(r, pd.DataFrame)
assert r.shape == (1000, 4)

a, b = get_periodogram(sig.values, sig.rate, full_range=True)
np.testing.assert_array_almost_equal(r.index.values, a)
np.testing.assert_array_almost_equal(r.values, b)

t = np.linspace(0, 1, 1000)
sig = nap.Tsd(d=np.random.random(1000), t=t)
r = nap.compute_power_spectral_density(sig, ep=sig.time_support)
Expand Down Expand Up @@ -135,9 +195,12 @@ def test_compute_power_spectral_density():
),
],
)
def test_compute_power_spectral_density_raise_errors(sig, kwargs, expectation):
def test_compute_fft_raise_errors(sig, kwargs, expectation):
with expectation:
nap.compute_power_spectral_density(sig, **kwargs)
nap.compute_fft(sig, **kwargs)
if "norm" not in kwargs:
with expectation:
nap.compute_power_spectral_density(sig, **kwargs)


############################################################
Expand Down Expand Up @@ -172,23 +235,23 @@ def get_signal_and_output(f=2, fs=1000, duration=100, interval_size=10, overlap=
return (sig, out, freq)


def test_compute_mean_power_spectral_density():
def test_compute_mean_fft():
sig, out, freq = get_signal_and_output()
psd = nap.compute_mean_power_spectral_density(sig, 10)
psd = nap.compute_mean_fft(sig, 10)
assert isinstance(psd, pd.DataFrame)
assert psd.shape[0] > 0 # Check that the psd DataFrame is not empty
np.testing.assert_array_almost_equal(psd.values.flatten(), out[freq >= 0])
np.testing.assert_array_almost_equal(psd.index.values, freq[freq >= 0])

# Full range
psd = nap.compute_mean_power_spectral_density(sig, 10, full_range=True)
psd = nap.compute_mean_fft(sig, 10, full_range=True)
assert isinstance(psd, pd.DataFrame)
assert psd.shape[0] > 0 # Check that the psd DataFrame is not empty
np.testing.assert_array_almost_equal(psd.values.flatten(), out)
np.testing.assert_array_almost_equal(psd.index.values, freq)

# Norm
psd = nap.compute_mean_power_spectral_density(sig, 10, norm=True)
psd = nap.compute_mean_fft(sig, 10, norm=True)
assert isinstance(psd, pd.DataFrame)
assert psd.shape[0] > 0 # Check that the psd DataFrame is not empty
np.testing.assert_array_almost_equal(
Expand All @@ -200,7 +263,7 @@ def test_compute_mean_power_spectral_density():
sig2 = nap.TsdFrame(
t=sig.t, d=np.repeat(sig.values[:, None], 2, 1), time_support=sig.time_support
)
psd = nap.compute_mean_power_spectral_density(sig2, 10, full_range=True)
psd = nap.compute_mean_fft(sig2, 10, full_range=True)
assert isinstance(psd, pd.DataFrame)
assert psd.shape[0] > 0 # Check that the psd DataFrame is not empty
np.testing.assert_array_almost_equal(psd.values, np.repeat(out[:, None], 2, 1))
Expand All @@ -210,7 +273,7 @@ def test_compute_mean_power_spectral_density():
sig2 = nap.TsdFrame(
t=sig.t, d=np.repeat(sig.values[:, None], 2, 1), time_support=sig.time_support
)
psd = nap.compute_mean_power_spectral_density(sig2, 10, full_range=True, fs=1000)
psd = nap.compute_mean_fft(sig2, 10, full_range=True, fs=1000)
assert isinstance(psd, pd.DataFrame)
assert psd.shape[0] > 0 # Check that the psd DataFrame is not empty
np.testing.assert_array_almost_equal(psd.values, np.repeat(out[:, None], 2, 1))
Expand Down Expand Up @@ -329,8 +392,6 @@ def test_compute_mean_power_spectral_density():
),
],
)
def test_compute_mean_power_spectral_density_raise_errors(
sig, interval_size, kwargs, expectation
):
def test_compute_mean_fft_raise_errors(sig, interval_size, kwargs, expectation):
with expectation:
nap.compute_mean_power_spectral_density(sig, interval_size, **kwargs)
nap.compute_mean_fft(sig, interval_size, **kwargs)
Loading

0 comments on commit 8363f9a

Please sign in to comment.