# Source code for neurokit2.complexity.entropy_phase

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from .entropy_shannon import entropy_shannon

[docs]
def entropy_phase(signal, delay=1, k=4, show=False, **kwargs):
"""**Phase Entropy (PhasEn)**

Phase entropy (PhasEn or PhEn) has been developed by quantifying the distribution of the signal
in accross *k* parts (of a two-dimensional phase space referred to as a second order difference
plot (SODP). It build on the concept of :func:Grid Entropy <entropy_grid>, that uses
:func:Poincaré plot <.hrv_nonlinear> as its basis.

.. figure:: ../img/rohila2019.png
:alt: Figure from Rohila et al. (2019).
:target: https://doi.org/10.1088/1361-6579/ab499e

Parameters
----------
signal : Union[list, np.array, pd.Series]
The signal (i.e., a time series) in the form of a vector of values.
delay : int
Time delay (often denoted *Tau* :math:\\tau, sometimes referred to as *lag*) in samples.
See :func:complexity_delay to estimate the optimal value for this parameter.
k : int
The number of sections that the SODP is divided into. It is a coarse graining parameter
that defines how fine the grid is. It is recommended to use even-numbered (preferably
multiples of 4) partitions for sake of symmetry.
show : bool
Plot the Second Order Difference Plot (SODP).
**kwargs : optional
Other keyword arguments, such as the logarithmic base to use for
:func:entropy_shannon.

Returns
-------
phasen : float
Phase Entropy
info : dict
A dictionary containing additional information regarding the parameters used.

--------
entropy_shannon

Examples
----------
.. ipython:: python

import neurokit2 as nk

# Simulate a Signal
signal = nk.signal_simulate(duration=2, sampling_rate=200, frequency=[5, 6], noise=0.5)

# Compute Phase Entropy
@savefig p_entropy_phase1.png scale=100%
phasen, info = nk.entropy_phase(signal, k=4, show=True)
@suppress
plt.close()

.. ipython:: python

phasen

.. ipython:: python

@savefig p_entropy_phase2.png scale=100%
phasen, info = nk.entropy_phase(signal, k=8, show=True)
@suppress
plt.close()

References
----------
* Rohila, A., & Sharma, A. (2019). Phase entropy: A new complexity measure for heart rate
variability. Physiological Measurement, 40(10), 105006.

"""
# Sanity checks
if isinstance(signal, (np.ndarray, pd.DataFrame)) and signal.ndim > 1:
raise ValueError(
"Multidimensional inputs (e.g., matrices or multichannel data) are not supported yet."
)

info = {"k": k, "Delay": delay}

# 1. Compute SODP axes
y = signal[2 * delay :] - signal[delay:-delay]
x = signal[delay:-delay] - signal[: -2 * delay]

# 2. Compute the slope (angle theta) of each scatter point from the origin
with np.errstate(divide="ignore", invalid="ignore"):
theta = np.arctan(y / x)
theta[np.logical_and((y < 0), (x < 0))] += np.pi
theta[np.logical_and((y < 0), (x > 0))] += 2 * np.pi
theta[np.logical_and((y > 0), (x < 0))] += np.pi

# 3. The entire plot is divided into k sections having an angle span of 2pi*k radians each
angles = np.linspace(0, 2 * np.pi, k + 1)

# 4. The cumulative slope of each sector is obtained by adding the slope of each scatter point # within that sector
# 5. The probability distribution of the slopes in each sector is computed
freq = [
np.sum(theta[np.logical_and((theta > angles[i]), (theta < angles[i + 1]))])
for i in range(k)
]
freq = np.array(freq) / np.sum(freq)

# 6. the Shannon entropy computed from the distribution p(i)
phasen, _ = entropy_shannon(freq=freq, **kwargs)

# Normalize
phasen = phasen / np.log(k)

if show is True:
Tx = np.zeros((k, len(theta)))
for i in range(k):
Temp = np.logical_and((theta > angles[i]), (theta < angles[i + 1]))
Tx[i, Temp] = 1

limx = np.ceil(np.max(np.abs([y, x])))
Tx = Tx.astype(bool)
Ys = np.sin(angles) * limx * np.sqrt(2)
Xs = np.cos(angles) * limx * np.sqrt(2)
resampled_cmap = plt.get_cmap("jet").resampled(k)
colors = resampled_cmap(np.linspace(0, 1, k))

plt.figure()
for i in range(k):
plt.plot(x[Tx[i, :]], y[Tx[i, :]], ".", color=tuple(colors[i, :]))
plt.plot(
np.vstack((np.zeros(k + 1), Xs)), np.vstack((np.zeros(k + 1), Ys)), color="red"
)
plt.axis([-limx, limx, -limx, limx])
plt.gca().set_aspect("equal", "box")
plt.xlabel(r"$X(n + \tau) - X(n)$"),
plt.ylabel(r"$X(n + 2 \tau) - X(n + \tau)$")
plt.xticks([-limx, 0, limx])
plt.yticks([-limx, 0, limx])
plt.title("Second Order Difference Plot (SODP)")

return phasen, info