"""Phase-locking value (PLV) computation.
Implements the PLV metric for quantifying phase coupling between
two signals (e.g., gastric EGG phase and brain BOLD phase).
References
----------
Lachaux, J. P., Rodriguez, E., Martinerie, J., & Varela, F. J. (1999).
Measuring phase synchrony in brain signals. *Human Brain Mapping*, 8(4),
194-208.
Cohen, M. X. (2014). *Analyzing Neural Time Series Data*. MIT Press.
"""
import numpy as np
[docs]
def phase_locking_value(phase_a, phase_b, mask=None):
"""Compute the phase-locking value between two phase time series.
PLV measures the consistency of the phase difference between two
signals across time. A PLV of 1 indicates perfect phase locking,
while 0 indicates no consistent phase relationship.
Parameters
----------
phase_a : array_like, shape (n_timepoints,) or (n_timepoints, n_signals)
Phase time series in radians. When 2D, PLV is computed between
each column and ``phase_b``.
phase_b : array_like, shape (n_timepoints,)
Reference phase time series in radians. Broadcast against
columns of ``phase_a`` when ``phase_a`` is 2D.
mask : array_like of bool, shape (n_timepoints,), optional
Boolean mask where ``True`` = include, ``False`` = exclude.
Only the included timepoints contribute to the PLV.
Returns
-------
plv : float or np.ndarray
Phase-locking value(s) in [0, 1]. Scalar if both inputs are 1D,
otherwise array of shape ``(n_signals,)``.
See Also
--------
phase_locking_value_complex : Returns full complex PLV (magnitude + lag).
Examples
--------
>>> import numpy as np
>>> t = np.arange(0, 100, 0.1)
>>> phase_a = 2 * np.pi * 0.05 * t # constant-frequency phase
>>> phase_b = phase_a + 0.3 # constant offset = perfect locking
>>> plv = phase_locking_value(phase_a, phase_b)
>>> round(plv, 2)
1.0
"""
return np.abs(phase_locking_value_complex(phase_a, phase_b, mask=mask))
[docs]
def phase_locking_value_complex(phase_a, phase_b, mask=None):
"""Compute the complex phase-locking value.
Returns the complex mean of the phase difference, from which both
the PLV magnitude and the preferred phase lag can be extracted.
Parameters
----------
phase_a : array_like, shape (n_timepoints,) or (n_timepoints, n_signals)
Phase time series in radians.
phase_b : array_like, shape (n_timepoints,)
Reference phase time series in radians.
mask : array_like of bool, shape (n_timepoints,), optional
Boolean mask where ``True`` = include, ``False`` = exclude.
Only the included timepoints contribute to the complex mean.
Returns
-------
cplv : complex or np.ndarray
Complex PLV. ``abs(cplv)`` gives the PLV magnitude,
``np.angle(cplv)`` gives the preferred phase lag.
Examples
--------
>>> import numpy as np
>>> phase_a = np.zeros(100)
>>> phase_b = np.full(100, 0.5)
>>> cplv = phase_locking_value_complex(phase_a, phase_b)
>>> round(abs(cplv), 2)
1.0
>>> round(np.angle(cplv), 2) # phase lag ~ -0.5
-0.5
"""
phase_a = np.asarray(phase_a, dtype=float)
phase_b = np.asarray(phase_b, dtype=float)
if (phase_a.ndim == 1 and phase_b.ndim == 1) or phase_a.ndim == 2:
if phase_a.shape[0] != phase_b.shape[0]:
raise ValueError(
f"phase_a and phase_b must have the same number of timepoints, "
f"got {phase_a.shape[0]} and {phase_b.shape[0]}"
)
else:
raise ValueError(f"phase_a must be 1D or 2D, got {phase_a.ndim}D")
# Apply mask before computing phase difference
if mask is not None:
mask = np.asarray(mask, dtype=bool)
phase_a = phase_a[mask]
phase_b = phase_b[mask]
# Broadcast phase_b to match 2D phase_a
if phase_a.ndim == 2:
phase_b = phase_b[:, np.newaxis]
phase_diff = phase_a - phase_b
cplv = np.mean(np.exp(1j * phase_diff), axis=0)
# Return scalar for 1D-1D case
if cplv.ndim == 0 or (cplv.ndim == 1 and cplv.shape[0] == 1 and phase_a.ndim == 1):
return complex(cplv)
return cplv
__all__ = ["phase_locking_value", "phase_locking_value_complex"]