Skip to content

Commit 211d7fe

Browse files
Remove tensorpac dependency (#239)
Re-implement ndPAC
1 parent 93b28b6 commit 211d7fe

7 files changed

Lines changed: 40 additions & 24 deletions

File tree

docs/conf.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,6 @@
585585
"scipy": ("https://docs.scipy.org/doc/scipy", None),
586586
"seaborn": ("https://seaborn.pydata.org", None),
587587
"sklearn": ("https://scikit-learn.org/stable", None),
588-
"tensorpac": ("https://etiennecmb.github.io/tensorpac", None),
589588
}
590589

591590
# The maximum number of days to cache remote inventories.

docs/index.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ The core dependencies of YASA are:
7070
* `lspopt <https://github.com/hbldh/lspopt>`_ >= 1.4
7171

7272
Some features require optional dependencies (`SleepECG <https://sleepecg.readthedocs.io/>`_,
73-
`TensorPAC <https://etiennecmb.github.io/tensorpac/>`_,
7473
`PyRiemann <https://pyriemann.readthedocs.io/>`_,
7574
`ipywidgets <https://ipywidgets.readthedocs.io/>`_).
7675

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@ dependencies = [
4545

4646
[project.optional-dependencies]
4747
heart = ["sleepecg>=0.5.0"]
48-
pac = ["tensorpac>=0.6.5"]
4948
art = ["pyriemann>=0.2.7"]
5049
plot = ["ipywidgets"]
51-
full = ["yasa[heart,pac,art,plot]"]
50+
full = ["yasa[heart,art,plot]"]
5251

5352
[dependency-groups]
5453
dev = [

src/yasa/detection.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
from scipy.stats import circmean
2222
from sklearn.ensemble import IsolationForest
2323

24-
from .io import is_pyriemann_installed, is_tensorpac_installed, set_log_level
24+
from .io import is_pyriemann_installed, set_log_level
2525
from .numba import _detrend, _rms
2626
from .others import (
2727
_index_to_events,
2828
_merge_close,
29+
_norm_direct_pac,
2930
_zerocrossings,
3031
get_centered_indices,
3132
moving_transform,
@@ -1576,9 +1577,8 @@ def sw_detect(
15761577
be calculated for each slow-waves using a 2-seconds epoch centered around the negative
15771578
peak of the slow-waves (i.e. 1 second on each side).
15781579
1579-
* ``p`` is a parameter passed to the :py:func:`tensorpac.methods.norm_direct_pac``
1580-
function. It represents the p-value to use for thresholding of unreliable coupling
1581-
values. Sub-threshold PAC values will be set to 0. To disable this behavior (no masking),
1580+
* ``p`` is the p-value used for thresholding of unreliable coupling values (ndPAC).
1581+
Sub-threshold PAC values will be set to 0. To disable this behavior (no masking),
15821582
use ``p=1`` or ``p=None``.
15831583
15841584
.. versionadded:: 0.6.0
@@ -1710,9 +1710,6 @@ def sw_detect(
17101710

17111711
# Extract the spindles-related sigma signal for coupling
17121712
if coupling:
1713-
is_tensorpac_installed()
1714-
import tensorpac.methods as tpm
1715-
17161713
# The width of the transition band is set to 1.5 Hz on each side,
17171714
# meaning that for freq_sp = (12, 15 Hz), the -6 dB points are located
17181715
# at 11.25 and 15.75 Hz. The frequency band for the amplitude signal
@@ -1925,9 +1922,7 @@ def sw_detect(
19251922
# 3) Normalized Direct PAC, with thresholding
19261923
# Unreliable values are set to 0
19271924
ndp = np.squeeze(
1928-
tpm.norm_direct_pac(
1929-
sw_pha_ev[None, ...], sp_amp_ev[None, ...], p=coupling_params["p"]
1930-
)
1925+
_norm_direct_pac(sw_pha_ev[None, ...], sp_amp_ev[None, ...], p=coupling_params["p"])
19311926
)
19321927
sw_params["ndPAC"] = np.ones(n_peaks) * np.nan
19331928
sw_params["ndPAC"][idx_valid] = ndp

src/yasa/io.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,6 @@ def set_log_level(verbose=None):
3535
raise ValueError("verbose must be in %s" % ", ".join(LOGGING_TYPES))
3636

3737

38-
def is_tensorpac_installed():
39-
"""Test if tensorpac is installed."""
40-
try:
41-
import tensorpac # noqa
42-
except ImportError: # pragma: no cover
43-
raise ImportError("tensorpac needs to be installed. Please use `pip install yasa[pac]`.")
44-
45-
4638
def is_pyriemann_installed():
4739
"""Test if pyRiemann is installed."""
4840
try:

src/yasa/others.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import numpy as np
88
from numpy.lib.stride_tricks import as_strided
99
from scipy.interpolate import interp1d
10+
from scipy.special import erfinv
1011

1112
from .numba import _corr, _covar, _rms, _slope_lstsq
1213

@@ -482,3 +483,36 @@ def rng(x):
482483
idx_ep_nomask = np.unique(idx_ep.nonzero()[0])
483484
idx_ep = np.ma.compress_rows(idx_ep)
484485
return idx_ep, idx_ep_nomask
486+
487+
488+
def _norm_direct_pac(pha, amp, p=0.05):
489+
"""Normalized direct PAC (ndPAC).
490+
491+
Re-implementation of tensorpac's ``norm_direct_pac`` (Ozkurt et al. 2012).
492+
493+
Parameters
494+
----------
495+
pha : array_like
496+
Phase array of shape (n_pha, ..., n_times).
497+
amp : array_like
498+
Amplitude array of shape (n_amp, ..., n_times).
499+
p : float | .05
500+
P-value threshold. Sub-threshold PAC values are set to 0.
501+
Use ``p=1`` or ``p=None`` to disable thresholding.
502+
503+
Returns
504+
-------
505+
pac : array_like
506+
Phase-amplitude coupling array of shape (n_amp, n_pha, ...).
507+
"""
508+
n_times = amp.shape[-1]
509+
amp = np.subtract(amp, np.mean(amp, axis=-1, keepdims=True))
510+
amp = np.divide(amp, np.std(amp, ddof=1, axis=-1, keepdims=True))
511+
pac = np.abs(np.einsum("i...j, k...j->ik...", amp, np.exp(1j * pha)))
512+
if p == 1.0 or p is None:
513+
return pac / n_times
514+
s = pac**2
515+
pac /= n_times
516+
xlim = n_times * erfinv(1 - p) ** 2
517+
pac[s <= 2 * xlim] = 0.0
518+
return pac

tests/test_io.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from yasa.io import (
99
is_pyriemann_installed,
1010
is_sleepecg_installed,
11-
is_tensorpac_installed,
1211
set_log_level,
1312
)
1413

@@ -38,6 +37,5 @@ def test_logger(self):
3837

3938
def test_dependence(self):
4039
"""Test dependancies."""
41-
is_tensorpac_installed()
4240
is_pyriemann_installed()
4341
is_sleepecg_installed()

0 commit comments

Comments
 (0)