Skip to content

Commit cb2e817

Browse files
authored
[ENH] Add (poly)coherence functions (nbara#76)
1 parent e220ac7 commit cb2e817

29 files changed

+1236
-88
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,19 @@ and was adapted to python by [Giuseppe Ferraro](mailto:giuseppe.ferraro@isae-sup
154154
2022 Sep 27;22(19):7314. https://doi.org/10.3390/s22197314.
155155
```
156156

157-
### 6. Real-Time Phase Estimation
157+
### 6. Phase Estimation
158158

159-
This code is based on the Matlab implementation from [Michael Rosenblum](http://www.stat.physik.uni-potsdam.de/~mros), and its corresponding paper [1].
159+
The oscillator code is based on the Matlab implementation from [Michael
160+
Rosenblum](http://www.stat.physik.uni-potsdam.de/~mros), and its corresponding
161+
paper [1]. The Endpoint Corrected Hilbert Transform (ECHT) method was adapted
162+
from [2].
160163

161164
```sql
162-
[1] Rosenblum, M., Pikovsky, A., Kühn, A.A. et al. Real-time estimation of phase and amplitude with application to neural data. Sci Rep 11, 18037 (2021). https://doi.org/10.1038/s41598-021-97560-5
165+
[1] Rosenblum, M., Pikovsky, A., Kühn, A.A. et al. Real-time estimation of phase
166+
and amplitude with application to neural data. Sci Rep 11, 18037 (2021).
167+
https://doi.org/10.1038/s41598-021-97560-5
168+
[2] Schreglmann, S. R., Wang, D., Peach, R. L., Li, J., Zhang, X., Latorre, A.,
169+
... & Grossman, N. (2021). Non-invasive suppression of essential tremor via
170+
phase-locked disruption of its temporal coherence. Nature communications, 12(1), 363.
163171

164172
```

doc/conf.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# -- Project information -----------------------------------------------------
2727

2828
project = "MEEGkit"
29-
copyright = "2023, Nicolas Barascud"
29+
copyright = "2024, Nicolas Barascud"
3030
author = "Nicolas Barascud"
3131
release = meegkit.__version__
3232
version = meegkit.__version__
@@ -63,7 +63,7 @@
6363
"show-inheritance": True,
6464
"exclude-members": "__weakref__"
6565
}
66-
numpydoc_show_class_members = True
66+
numpydoc_show_class_members = False
6767

6868
# The suffix(es) of source filenames.
6969
# You can specify multiple suffix as a list of string:
@@ -129,3 +129,5 @@
129129
"ignore_pattern": "config.py",
130130
"run_stale_examples": False,
131131
}
132+
133+
suppress_warnings = ["config.cache"]

doc/index.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ Here is a list of the methods and techniques available in ``meegkit``:
4646
~meegkit.tspca
4747
~meegkit.utils
4848

49-
5049
Examples gallery
5150
----------------
5251

doc/modules/meegkit.phase.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
meegkit.phase
2+
=============
3+
4+
.. automodule:: meegkit.phase
5+
6+
.. rubric:: Classes
7+
8+
.. autosummary::
9+
10+
NonResOscillator
11+
ResOscillator
12+
Device
13+
ECHT
14+
15+
.. rubric:: Functions
16+
17+
.. autosummary::
18+
19+
locking_based_phase
20+
rk
21+
init_coefs
22+
one_step_oscillator
23+
one_step_integrator

doc/modules/meegkit.utils.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ meegkit.utils
66
.. autosummary::
77

88
auditory
9+
buffer
10+
coherence
911
covariances
1012
denoise
1113
matrix
1214
sig
1315
stats
16+
trca
1417

1518
|
1619
@@ -23,6 +26,28 @@ Auditory
2326
.. autosummary::
2427

2528

29+
|
30+
31+
----
32+
33+
Buffer
34+
------
35+
.. automodule:: meegkit.utils.buffer
36+
37+
.. autosummary::
38+
39+
40+
|
41+
42+
----
43+
44+
Coherence
45+
---------
46+
.. automodule:: meegkit.utils.coherence
47+
48+
.. autosummary::
49+
50+
2651
|
2752
2853
----

doc/sg_execution_times.rst

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
2+
:orphan:
3+
4+
.. _sphx_glr_sg_execution_times:
5+
6+
7+
Computation times
8+
=================
9+
**00:29.509** total execution time for 13 files **from all galleries**:
10+
11+
.. container::
12+
13+
.. raw:: html
14+
15+
<style scoped>
16+
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet" />
17+
<link href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css" rel="stylesheet" />
18+
</style>
19+
<script src="https://code.jquery.com/jquery-3.7.0.js"></script>
20+
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
21+
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
22+
<script type="text/javascript" class="init">
23+
$(document).ready( function () {
24+
$('table.sg-datatable').DataTable({order: [[1, 'desc']]});
25+
} );
26+
</script>
27+
28+
.. list-table::
29+
:header-rows: 1
30+
:class: table table-striped sg-datatable
31+
32+
* - Example
33+
- Time
34+
- Mem (MB)
35+
* - :ref:`sphx_glr_auto_examples_example_trca.py` (``../examples/example_trca.py``)
36+
- 00:11.533
37+
- 0.0
38+
* - :ref:`sphx_glr_auto_examples_example_dss_line.py` (``../examples/example_dss_line.py``)
39+
- 00:07.115
40+
- 0.0
41+
* - :ref:`sphx_glr_auto_examples_example_phase_estimation.py` (``../examples/example_phase_estimation.py``)
42+
- 00:07.064
43+
- 0.0
44+
* - :ref:`sphx_glr_auto_examples_example_mcca.py` (``../examples/example_mcca.py``)
45+
- 00:01.182
46+
- 0.0
47+
* - :ref:`sphx_glr_auto_examples_example_mcca_2.py` (``../examples/example_mcca_2.py``)
48+
- 00:00.535
49+
- 0.0
50+
* - :ref:`sphx_glr_auto_examples_example_asr.py` (``../examples/example_asr.py``)
51+
- 00:00.512
52+
- 0.0
53+
* - :ref:`sphx_glr_auto_examples_example_ress.py` (``../examples/example_ress.py``)
54+
- 00:00.459
55+
- 0.0
56+
* - :ref:`sphx_glr_auto_examples_example_detrend.py` (``../examples/example_detrend.py``)
57+
- 00:00.259
58+
- 0.0
59+
* - :ref:`sphx_glr_auto_examples_example_star_dss.py` (``../examples/example_star_dss.py``)
60+
- 00:00.246
61+
- 0.0
62+
* - :ref:`sphx_glr_auto_examples_example_star.py` (``../examples/example_star.py``)
63+
- 00:00.197
64+
- 0.0
65+
* - :ref:`sphx_glr_auto_examples_example_dss.py` (``../examples/example_dss.py``)
66+
- 00:00.146
67+
- 0.0
68+
* - :ref:`sphx_glr_auto_examples_example_echt.py` (``../examples/example_echt.py``)
69+
- 00:00.132
70+
- 0.0
71+
* - :ref:`sphx_glr_auto_examples_example_dering.py` (``../examples/example_dering.py``)
72+
- 00:00.129
73+
- 0.0

examples/example_echt.ipynb

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"\n# Endpoint-corrected Hilbert transform (ECHT) phase estimation\n\nThis example shows how to causally estimate the phase of a signal using\n\nUses `meegkit.phase.ECHT()`.\n\n## References\n.. [1] Schreglmann, S. R., Wang, D., Peach, R. L., Li, J., Zhang, X., Latorre,\n A., ... & Grossman, N. (2021). Non-invasive suppression of essential tremor\n via phase-locked disruption of its temporal coherence. Nature\n communications, 12(1), 363.\n"
8+
]
9+
},
10+
{
11+
"cell_type": "code",
12+
"execution_count": null,
13+
"metadata": {
14+
"collapsed": false
15+
},
16+
"outputs": [],
17+
"source": [
18+
"import matplotlib.pyplot as plt\nimport numpy as np\nfrom scipy.signal import hilbert\n\nfrom meegkit.phase import ECHT\n\nrng = np.random.default_rng(38872)\n\nplt.rcParams[\"axes.grid\"] = True\nplt.rcParams[\"grid.linestyle\"] = \":\""
19+
]
20+
},
21+
{
22+
"cell_type": "markdown",
23+
"metadata": {},
24+
"source": [
25+
"## Build data\nFirst, we generate a multi-component signal with amplitude and phase\nmodulations, as described in the paper [1]_.\n\n"
26+
]
27+
},
28+
{
29+
"cell_type": "code",
30+
"execution_count": null,
31+
"metadata": {
32+
"collapsed": false
33+
},
34+
"outputs": [],
35+
"source": [
36+
"f0 = 2\n\nN = 500\nsfreq = 100\ntime = np.linspace(0, N / sfreq, N)\nX = np.cos(2 * np.pi * f0 * time - np.pi / 4)\nphase_true = np.angle(hilbert(X))\nX += rng.normal(0, 0.5, N) # Add noise"
37+
]
38+
},
39+
{
40+
"cell_type": "markdown",
41+
"metadata": {},
42+
"source": [
43+
"### Compute phase and amplitude\nWe compute the Hilbert phase, as well as the phase obtained with the ECHT\nfilter.\n\n"
44+
]
45+
},
46+
{
47+
"cell_type": "code",
48+
"execution_count": null,
49+
"metadata": {
50+
"collapsed": false
51+
},
52+
"outputs": [],
53+
"source": [
54+
"phase_hilbert = np.angle(hilbert(X)) # Hilbert phase\n\n# Compute ECHT-filtered signal\nfilt_BW = f0 / 2\nl_freq = f0 - filt_BW / 2\nh_freq = f0 + filt_BW / 2\necht = ECHT(l_freq, h_freq, sfreq)\n\nXf = echt.fit_transform(X)\nphase_echt = np.angle(Xf)"
55+
]
56+
},
57+
{
58+
"cell_type": "markdown",
59+
"metadata": {},
60+
"source": [
61+
"### Visualize signal\nPlot the results\n\n"
62+
]
63+
},
64+
{
65+
"cell_type": "code",
66+
"execution_count": null,
67+
"metadata": {
68+
"collapsed": false
69+
},
70+
"outputs": [],
71+
"source": [
72+
"fig, ax = plt.subplots(3, 1, figsize=(8, 6))\nax[0].plot(time, X)\nax[0].set_xlabel(\"Time (s)\")\nax[0].set_title(\"Test signal\")\nax[0].set_ylabel(\"Amplitude\")\nax[1].psd(X, Fs=sfreq, NFFT=2048*4, noverlap=sfreq)\nax[1].set_ylabel(\"PSD (dB/Hz)\")\nax[1].set_title(\"Test signal's Fourier spectrum\")\nax[2].plot(time, phase_true, label=\"True phase\", ls=\":\")\nax[2].plot(time, phase_echt, label=\"ECHT phase\", lw=.5, alpha=.8)\nax[2].plot(time, phase_hilbert, label=\"Hilbert phase\", lw=.5, alpha=.8)\nax[2].set_title(\"Phase\")\nax[2].set_ylabel(\"Amplitude\")\nax[2].set_xlabel(\"Time (s)\")\nax[2].legend(loc=\"upper right\", fontsize=\"small\")\nplt.tight_layout()\nplt.show()"
73+
]
74+
}
75+
],
76+
"metadata": {
77+
"kernelspec": {
78+
"display_name": "Python 3",
79+
"language": "python",
80+
"name": "python3"
81+
},
82+
"language_info": {
83+
"codemirror_mode": {
84+
"name": "ipython",
85+
"version": 3
86+
},
87+
"file_extension": ".py",
88+
"mimetype": "text/x-python",
89+
"name": "python",
90+
"nbconvert_exporter": "python",
91+
"pygments_lexer": "ipython3",
92+
"version": "3.12.2"
93+
}
94+
},
95+
"nbformat": 4,
96+
"nbformat_minor": 0
97+
}

examples/example_echt.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Endpoint-corrected Hilbert transform (ECHT) phase estimation
3+
============================================================
4+
5+
This example shows how to causally estimate the phase of a signal using the
6+
Endpoint-corrected Hilbert transform (ECHT) [1]_.
7+
8+
Uses `meegkit.phase.ECHT()`.
9+
10+
References
11+
----------
12+
.. [1] Schreglmann, S. R., Wang, D., Peach, R. L., Li, J., Zhang, X., Latorre,
13+
A., ... & Grossman, N. (2021). Non-invasive suppression of essential tremor
14+
via phase-locked disruption of its temporal coherence. Nature
15+
communications, 12(1), 363.
16+
17+
"""
18+
import matplotlib.pyplot as plt
19+
import numpy as np
20+
from scipy.signal import hilbert
21+
22+
from meegkit.phase import ECHT
23+
24+
rng = np.random.default_rng(38872)
25+
26+
plt.rcParams["axes.grid"] = True
27+
plt.rcParams["grid.linestyle"] = ":"
28+
29+
###############################################################################
30+
# Build data
31+
# -----------------------------------------------------------------------------
32+
# First, we generate a multi-component signal with amplitude and phase
33+
# modulations, as described in the paper [1]_.
34+
f0 = 2
35+
36+
N = 500
37+
sfreq = 100
38+
time = np.linspace(0, N / sfreq, N)
39+
X = np.cos(2 * np.pi * f0 * time - np.pi / 4)
40+
phase_true = np.angle(hilbert(X))
41+
X += rng.normal(0, 0.5, N) # Add noise
42+
43+
###############################################################################
44+
# Compute phase and amplitude
45+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
46+
# We compute the Hilbert phase, as well as the phase obtained with the ECHT
47+
# filter.
48+
phase_hilbert = np.angle(hilbert(X)) # Hilbert phase
49+
50+
# Compute ECHT-filtered signal
51+
filt_BW = f0 / 2
52+
l_freq = f0 - filt_BW / 2
53+
h_freq = f0 + filt_BW / 2
54+
echt = ECHT(l_freq, h_freq, sfreq)
55+
56+
Xf = echt.fit_transform(X)
57+
phase_echt = np.angle(Xf)
58+
59+
###############################################################################
60+
# Visualize signal
61+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
62+
# Here we plot the original signal, its Fourier spectrum, and the phase obtained
63+
# with the Hilbert transform and the ECHT filter. The ECHT filter provides a
64+
# much smoother phase estimate than the Hilbert transform
65+
fig, ax = plt.subplots(3, 1, figsize=(8, 6))
66+
ax[0].plot(time, X)
67+
ax[0].set_xlabel("Time (s)")
68+
ax[0].set_title("Test signal")
69+
ax[0].set_ylabel("Amplitude")
70+
ax[1].psd(X, Fs=sfreq, NFFT=2048*4, noverlap=sfreq)
71+
ax[1].set_ylabel("PSD (dB/Hz)")
72+
ax[1].set_title("Test signal's Fourier spectrum")
73+
ax[2].plot(time, phase_true, label="True phase", ls=":")
74+
ax[2].plot(time, phase_echt, label="ECHT phase", lw=.5, alpha=.8)
75+
ax[2].plot(time, phase_hilbert, label="Hilbert phase", lw=.5, alpha=.8)
76+
ax[2].set_title("Phase")
77+
ax[2].set_ylabel("Amplitude")
78+
ax[2].set_xlabel("Time (s)")
79+
ax[2].legend(loc="upper right", fontsize="small")
80+
plt.tight_layout()
81+
plt.show()

0 commit comments

Comments
 (0)