# Source code for neurokit2.complexity.entropy_spectral

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

from ..signal.signal_psd import signal_psd
from .entropy_shannon import entropy_shannon

[docs]
def entropy_spectral(signal, bins=None, show=False, **kwargs):
"""**Spectral Entropy (SpEn)**

Spectral entropy (SE or SpEn) treats the signal's normalized power spectrum density (PSD) in the
frequency domain as a probability distribution, and calculates the Shannon entropy of it.

.. math:: H(x, sf) =  -\\sum P(f) \\log_2[P(f)]

A signal with a single frequency component (i.e., pure sinusoid) produces the smallest entropy.
On the other hand, a signal with all frequency components of equal power value (white
noise) produces the greatest entropy.

Parameters
----------
signal : Union[list, np.array, pd.Series]
The signal (i.e., a time series) in the form of a vector of values.
bins : int
If an integer is passed, will cut the PSD into a number of bins of frequency.
show : bool
Display the power spectrum.
**kwargs : optional
Keyword arguments to be passed to signal_psd().

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

--------
entropy_shannon, entropy_wiener, .signal_psd

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

import neurokit2 as nk

# Simulate a Signal with Laplace Noise
signal = nk.signal_simulate(duration=2, sampling_rate=200, frequency=[5, 6, 10], noise=0.1)

# Compute Spectral Entropy
@savefig p_entropy_spectral1.png scale=100%
SpEn, info = nk.entropy_spectral(signal, show=True)
@suppress
plt.close()

.. ipython:: python

SpEn

Bin the frequency spectrum.

.. ipython:: python

@savefig p_entropy_spectral2.png scale=100%
SpEn, info = nk.entropy_spectral(signal, bins=10, show=True)
@suppress
plt.close()

References
----------
* Crepeau, J. C., & Isaacson, L. K. (1991). Spectral Entropy Measurements of Coherent
Structures in an Evolving Shear Layer. Journal of Non-Equilibrium Thermodynamics, 16(2).
doi:10.1515/jnet.1991.16.2.137

"""
# 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."
)

# Power-spectrum density (PSD) (actual sampling rate does not matter)
psd = signal_psd(signal, sampling_rate=1000, **kwargs)

# Cut into bins
if isinstance(bins, int):
psd = psd.groupby(pd.cut(psd["Frequency"], bins=bins), observed=False).agg(
"sum"
)
idx = psd.index.values.astype(str)
else:
idx = psd["Frequency"].values

# Area under normalized spectrum should sum to 1 (np.sum(psd["Power"]))
psd["Power"] = psd["Power"] / psd["Power"].sum()

if show is True:
plt.bar(idx, psd["Power"])
if not np.issubdtype(idx.dtype, np.floating):
plt.xticks(rotation=90)
plt.title("Normalized Power Spectrum")
plt.xlabel("Frequency (Hz)")
plt.ylabel("Normalized Power")

# Compute Shannon entropy
se, _ = entropy_shannon(freq=psd["Power"].values)

# Normalize
se /= np.log2(len(psd))  # between 0 and 1

return se, {"PSD": psd}