77from scipy .stats import vonmises
88from scipy .signal import butter , filtfilt , hilbert
99
10- from meegsim ._check import check_numeric
10+ from meegsim ._check import check_numeric , check_option
1111from meegsim .snr import get_variance , amplitude_adjustment_factor
1212from meegsim .utils import normalize_variance
1313from meegsim .waveform import narrowband_oscillation , white_noise
1414
1515
16- def constant_phase_shift (waveform , sfreq , phase_lag , m = 1 , n = 1 , random_state = None ):
16+ def _get_envelope (waveform , envelope , sfreq , fmin = None , fmax = None , random_state = None ):
17+ check_option (
18+ "the amplitude envelope of the coupled waveform" , envelope , ["same" , "random" ]
19+ )
20+ if not np .iscomplexobj (waveform ):
21+ waveform = hilbert (waveform )
22+
23+ if envelope == "same" :
24+ return np .abs (waveform )
25+
26+ if fmin is None or fmax is None :
27+ raise ValueError (
28+ "Frequency limits are required for generating the envelope of the coupled waveform"
29+ )
30+ times = np .arange (waveform .size ) / sfreq
31+ random_waveform = narrowband_oscillation (
32+ 1 , times , fmin = fmin , fmax = fmax , random_state = random_state
33+ )
34+ random_waveform = hilbert (random_waveform )
35+
36+ # TODO: here we could also mix original and random envelope with different
37+ # values of SNR to achieve smooth control over the resulting envelope correlation
38+ return np .abs (random_waveform )
39+
40+
41+ def ppc_constant_phase_shift (
42+ waveform ,
43+ sfreq ,
44+ phase_lag ,
45+ fmin = None ,
46+ fmax = None ,
47+ envelope = "random" ,
48+ m = 1 ,
49+ n = 1 ,
50+ random_state = None ,
51+ ):
1752 """
1853 Generate a time series that is phase coupled to the input time series with
1954 a constant phase lag.
@@ -32,21 +67,32 @@ def constant_phase_shift(waveform, sfreq, phase_lag, m=1, n=1, random_state=None
3267 The input signal to be processed. It can be a real or complex time series.
3368
3469 sfreq : float
35- Sampling frequency of the signal, in Hz. This argument is not used in this
36- function but is accepted for consistency with other coupling methods.
70+ Sampling frequency of the signal, in Hz.
3771
3872 phase_lag : float
3973 Constant phase lag to apply to the waveform in radians.
4074
41- m : int, optional
75+ envelope : str, {"same", "random"}
76+ Controls the amplitude envelope of the coupled waveform to be either randomly
77+ generated (default) or to be the same as the envelope of the input waveform.
78+
79+ fmin : float, optional
80+ Lower cutoff frequency for the oscillation that gives rise to the random
81+ amplitude envelope (only if the ``envelope`` is set to ``"random"``).
82+
83+ fmax : float, optional
84+ Upper cutoff frequency for the oscillation that gives rise to the random
85+ amplitude envelope (only if the ``envelope`` is set to ``"random"``).
86+
87+ m : float, optional
4288 Multiplier for the base frequency of the output oscillation, default is 1.
4389
44- n : int , optional
90+ n : float , optional
4591 Multiplier for the base frequency of the input oscillation, default is 1.
4692
4793 random_state : None, optional
48- This parameter is accepted for consistency with other coupling functions
49- but not used since no randomness is involved .
94+ Random state can be fixed to provide reproducible results if the envelope
95+ is generated randomly. If not set, the results may differ between function calls .
5096
5197 Returns
5298 -------
@@ -56,16 +102,36 @@ def constant_phase_shift(waveform, sfreq, phase_lag, m=1, n=1, random_state=None
56102 if not np .iscomplexobj (waveform ):
57103 waveform = hilbert (waveform )
58104
59- waveform_amp = np . abs (waveform )
105+ waveform_amp = _get_envelope (waveform , envelope , sfreq , fmin , fmax , random_state )
60106 waveform_angle = np .angle (waveform )
61- waveform_coupled = waveform_amp * np .exp (
62- 1j * m / n * waveform_angle + 1j * phase_lag
107+ waveform_coupled = np .real (
108+ waveform_amp * np .exp (1j * m / n * waveform_angle + 1j * phase_lag )
109+ )
110+ if envelope == "same" :
111+ return normalize_variance (waveform_coupled )
112+
113+ # NOTE: if the envelope was modified, we filter the result again in the target
114+ # frequency range to suppress possible distortions due to merging amplitude
115+ # envelope and phase from different time series
116+ b , a = butter (
117+ N = 2 , Wn = np .array ([m / n * fmin , m / n * fmax ]) / sfreq * 2 , btype = "bandpass"
63118 )
64- return normalize_variance (np .real (waveform_coupled ))
119+ waveform_coupled = filtfilt (b , a , waveform_coupled )
120+
121+ return normalize_variance (waveform_coupled )
65122
66123
67124def ppc_von_mises (
68- waveform , sfreq , phase_lag , kappa , fmin , fmax , m = 1 , n = 1 , random_state = None
125+ waveform ,
126+ sfreq ,
127+ phase_lag ,
128+ kappa ,
129+ fmin ,
130+ fmax ,
131+ envelope = "random" ,
132+ m = 1 ,
133+ n = 1 ,
134+ random_state = None ,
69135):
70136 """
71137 Generate a time series that is phase coupled to the input time series with
@@ -102,10 +168,14 @@ def ppc_von_mises(
102168 fmax: float
103169 Upper cutoff frequency of the base frequency harmonic (in Hz).
104170
105- m : int, optional
171+ envelope : str, {"same", "random"}
172+ Controls the amplitude envelope of the coupled waveform to be either randomly
173+ generated (default) or to be the same as the envelope of the input waveform.
174+
175+ m : float, optional
106176 Multiplier for the base frequency of the output oscillation, default is 1.
107177
108- n : int , optional
178+ n : float , optional
109179 Multiplier for the base frequency of the input oscillation, default is 1.
110180
111181 random_state : None (default) or int
@@ -121,23 +191,26 @@ def ppc_von_mises(
121191 if not np .iscomplexobj (waveform ):
122192 waveform = hilbert (waveform )
123193
124- waveform_amp = np . abs (waveform )
194+ waveform_amp = _get_envelope (waveform , envelope , sfreq , fmin , fmax , random_state )
125195 waveform_angle = np .angle (waveform )
126- n_samples = len ( waveform )
196+ n_samples = waveform . size
127197
128198 ph_distr = vonmises .rvs (
129199 kappa , loc = phase_lag , size = n_samples , random_state = random_state
130200 )
131- tmp_waveform = np .real (
201+ waveform_coupled = np .real (
132202 waveform_amp * np .exp (1j * m / n * waveform_angle + 1j * ph_distr )
133203 )
204+
205+ # NOTE: we filter the result again in the target frequency range to suppress
206+ # possible distortions due to separate adjustment of the phase and amplitude
207+ # of the coupled time series
134208 b , a = butter (
135209 N = 2 , Wn = np .array ([m / n * fmin , m / n * fmax ]) / sfreq * 2 , btype = "bandpass"
136210 )
137- tmp_waveform = filtfilt (b , a , tmp_waveform )
138- waveform_coupled = waveform_amp * np .exp (1j * np .angle (hilbert (tmp_waveform )))
211+ waveform_coupled = filtfilt (b , a , waveform_coupled )
139212
140- return normalize_variance (np . real ( waveform_coupled ) )
213+ return normalize_variance (waveform_coupled )
141214
142215
143216def _shifted_copy_with_noise (
@@ -148,7 +221,9 @@ def _shifted_copy_with_noise(
148221 waveform and (2) mixing it with noise to achieve a desired level of signal-to-noise
149222 ratio, which determines the resulting phase-phase and amplitude-amplitude coupling.
150223 """
151- shifted_waveform = constant_phase_shift (waveform , sfreq , phase_lag )
224+ shifted_waveform = ppc_constant_phase_shift (
225+ waveform , sfreq , phase_lag , envelope = "same"
226+ )
152227 signal_var = get_variance (shifted_waveform , sfreq , fmin , fmax , filter = True )
153228
154229 # NOTE: to make coupling band-limited (substantial only in the band of interest),
0 commit comments