# -*- coding: utf-8 -*-
"""
biosppy.features.phase_space
----------------------------
This module provides methods to extract phase-space features.
:copyright: (c) 2015-2023 by Instituto de Telecomunicacoes
:license: BSD 3-clause, see LICENSE for more details.
"""
# Imports
# 3rd party
import numpy as np
from scipy.signal import resample
from scipy.spatial.distance import pdist, squareform
# local
from .. import utils
[docs]def phase_space(signal=None):
"""Compute phase-space features describing the signal.
Parameters
----------
signal : array
Input signal.
Returns
-------
feats : ReturnTuple object
Phase-space features of the signal.
Notes
-----
Check biosppy.features.phase_space.recurrence_plot_features for the list of
available features.
"""
# check inputs
if signal is None:
raise TypeError("Please specify an input signal.")
# ensure numpy
signal = np.array(signal)
# initialize output
feats = utils.ReturnTuple((), ())
# compute recurrence plot
rp = compute_recurrence_plot(signal)["rec_matrix"]
# compute recurrence plot features
rp_feats = recurrence_plot_features(rp)
feats = feats.join(rp_feats)
return feats
[docs]def compute_recurrence_plot(signal=None, out_dim=224):
"""Compute recurrence plot (distance matrix).
Parameters
----------
signal : array
Input signal.
out_dim : int, optional
Output dimension of the recurrence plot.
Returns
-------
rec_matrix : array
Recurrence plot matrix.
"""
# check inputs
if signal is None:
raise TypeError("Please specify an input signal.")
# ensure numpy
signal = np.array(signal)
# resample to out_dim points
sig_down = resample(signal, out_dim)
d = pdist(sig_down[:,None])
rec = squareform(d)
args = (rec,)
names = ('rec_matrix',)
return utils.ReturnTuple(args, names)
[docs]def recurrence_plot_features(rec_matrix=None):
"""Compute recurrence plot features.
The following features are based on the GitHub repository by bmfreis:
https://github.com/bmfreis/recurrence_python
Parameters
----------
rec_matrix : array
Recurrence plot matrix.
Returns
-------
rec_rate : float
Recurrence rate: the percentage of recurrence points.
rec_determ : float
Recurrence determinism: the percentage of recurrence points which form diagonal lines.
rec_lamin : float
Recurrence laminarity: the percentage of recurrence points which form vertical lines.
rec_determ_rec_rate_ratio : float
Determinism/recurrence rate ratio.
rec_lamin_determ_ratio : float
Laminarity/determinism ratio.
rec_avg_diag_line_len : float
Average length of the diagonal lines.
rec_avg_vert_line_len : float
Average length of the vertical lines.
rec_avg_white_vert_line_len : float
Average length of the white vertical lines.
rec_plot_trapping_tm : float
Trapping time.
rec_plot_lgst_diag_line_len : float
Length of the longest diagonal line.
rec_plot_lgst_vert_line_len : float
Length of the longest vertical line.
rec_plot_lgst_white_vert_line_len : float
Length of the longest white vertical line.
rec_plot_entropy_diag_line : float
Entropy of the probability distribution of the diagonal line lengths.
rec_plot_entropy_vert_line : float.
Entropy of the probability distribution of the vert line lengths.
rec_plot_entropy_white_vert_line : float
Entropy of the probability distribution of the white vert line lengths.
"""
# check inputs
if rec_matrix is None:
raise TypeError("Please specify a recurrence plot matrix.")
# ensure numpy
rec_matrix = np.array(rec_matrix)
# initialize output
feats = utils.ReturnTuple((), ())
# variables
MIN = 2
# compute features
threshold = 0.5
len_rec_matrix = len(rec_matrix)
for l in range(len_rec_matrix):
for c in range(len_rec_matrix):
rec_matrix[l, c] = 1 if rec_matrix[l, c] < threshold else 0
len_rec_matrix = len(rec_matrix)
diag_freq = np.zeros(len_rec_matrix + 1, dtype=int)
# upper diagonal
for k in range(1, len_rec_matrix - 1, 1):
d = np.diag(rec_matrix, k=k)
d_l = 0
for _, i in enumerate(d):
if i: # has a dot
d_l += 1
# if its end of line, finishes counting and adds to hist
if _ == (len(d) - 1):
diag_freq[d_l] += 1
else: # doesn't have a dot
if d_l != 0:
diag_freq[d_l] += 1
# if it's not end of the line and d_l != 0, line ended
d_l = 0
diag_freq[d_l] += 1
# lower diagonal
for k in range(-1, -(len_rec_matrix - 1), -1):
d = np.diag(rec_matrix, k=k)
d_l = 0
for _, i in enumerate(d):
if i: # has a dot
d_l += 1
# if its end of line, finishes counting and adds to hist
if _ == (len(d) - 1):
diag_freq[d_l] += 1
else: # doesn't have a dot
if d_l != 0:
diag_freq[d_l] += 1
# if it's not end of the line and d_l != 0, line ended
d_l = 0
diag_freq[d_l] += 1
# vertical lines
vert_freq = np.zeros(len_rec_matrix + 1, dtype=int)
for k in range(len_rec_matrix):
d = rec_matrix[:, k]
d_l = 0
for _, i in enumerate(d):
if i: # has a dot
d_l += 1
# if its end of line, finishes counting and adds to hist
if _ == (len(d) - 1):
vert_freq[d_l] += 1
else: # doesn't have a dot
if d_l != 0:
vert_freq[d_l] += 1
# if it's not end of the line and d_l != 0, line ended
d_l = 0
vert_freq[d_l] += 1
# white vertical lines
white_vert_freq = np.zeros(len_rec_matrix + 1, dtype=int)
for k in range(len_rec_matrix):
d = rec_matrix[:, k]
d_l = 0
for _, i in enumerate(d):
if i == 0: # has a dot
d_l += 1
# if its end of line, finishes counting and adds to hist
if _ == (len(d) - 1):
white_vert_freq[d_l] += 1
else: # doesn't have a dot
if d_l != 0:
white_vert_freq[d_l] += 1
# if it's not end of the line and d_l != 0, line ended
d_l = 0
white_vert_freq[d_l] += 1
# extract features
# recurrence rate
rec_rate = 0
for l in range(len_rec_matrix):
rec_rate += np.sum(rec_matrix[l])
rec_rate /= (len_rec_matrix ** 2)
feats = feats.append(rec_rate, 'rec_rate')
# determinism
_sum = np.sum([i * diag_freq[i] for i in range(1, len(diag_freq), 1)])
if _sum > 0:
determ = np.sum([i * diag_freq[i] for i in range(MIN, len(diag_freq), 1)]) / _sum
else:
determ = None
feats = feats.append(determ, 'rec_determ')
# laminarity
_sum = np.sum([i * vert_freq[i] for i in range(len(vert_freq))])
if _sum > 0:
lamin = np.sum([i * vert_freq[i] for i in range(MIN, len(vert_freq), 1)]) / _sum
else:
lamin = None
feats = feats.append(lamin, 'rec_lamin')
# determinism/recurrence rate ratio
_sum = np.sum([i * diag_freq[i] for i in range(1, len(diag_freq), 1)])
if _sum > 0:
det_rr_ratio = len_rec_matrix ** 2 * (np.sum([i * diag_freq[i] for i in range(MIN, len(diag_freq), 1)]) / _sum ** 2)
else:
det_rr_ratio = None
feats = feats.append(det_rr_ratio, 'rec_determ_rec_rate_ratio')
# laminarity/determinism ratio
if determ is not None and determ > 0:
lamin_determ_ratio = lamin / determ
else:
lamin_determ_ratio = None
feats = feats.append(lamin_determ_ratio, 'rec_lamin_determ_ratio')
# average diagonal line length
_sum = np.sum(diag_freq)
if _sum > 0:
avg_diag_line_len = np.sum([i * diag_freq[i] for i in range(MIN, len(diag_freq), 1)]) / _sum
else:
avg_diag_line_len = None
feats = feats.append(avg_diag_line_len, 'rec_avg_diag_line_len')
# average vertical line length
_sum = np.sum(vert_freq)
if _sum > 0:
avg_vert_line_len = np.sum([i * vert_freq[i] for i in range(MIN, len(vert_freq), 1)]) / _sum
else:
avg_vert_line_len = None
feats = feats.append(avg_vert_line_len, 'rec_avg_vert_line_len')
# average white vertical line length
_sum = np.sum(white_vert_freq)
if _sum > 0:
avg_white_vert_line_len = np.sum([i * white_vert_freq[i] for i in range(MIN, len(white_vert_freq), 1)]) / _sum
else:
avg_white_vert_line_len = None
feats = feats.append(avg_white_vert_line_len, 'rec_avg_white_vert_line_len')
# trapping time
_sum = np.sum(vert_freq)
if _sum > 0:
rec_plot_trapping_tm = np.sum([i * vert_freq[i] for i in range(MIN, len(vert_freq), 1)]) / _sum
else:
rec_plot_trapping_tm = None
feats = feats.append(rec_plot_trapping_tm, 'rec_plot_trapping_tm')
# longest diagonal line length
i_ll = np.sign(diag_freq)
rec_plot_lgst_diag_line_len = np.where(i_ll == 1)[0][-1]
feats = feats.append(rec_plot_lgst_diag_line_len, 'rec_plot_lgst_diag_line_len')
# longest vertical line length
i_ll = np.sign(vert_freq)
rec_plot_lgst_vert_line_len = np.where(i_ll == 1)[0][-1]
feats = feats.append(rec_plot_lgst_vert_line_len, 'rec_plot_lgst_vert_line_len')
# longest white vertical line length
i_ll = np.sign(white_vert_freq)
rec_plot_lgst_white_vert_line_len = np.where(i_ll == 1)[0][-1]
feats = feats.append(rec_plot_lgst_white_vert_line_len, 'rec_plot_lgst_white_vert_line_len')
# entropy of diagonal lines
rec_plot_entropy_diag_line = - np.sum(
[diag_freq[i] * np.log(diag_freq[i]) if diag_freq[i] > 0 else 0 for i in range(MIN, len(diag_freq), 1)])
feats = feats.append(rec_plot_entropy_diag_line, 'rec_plot_entropy_diag_line')
# entropy of vertical lines
rec_plot_entropy_vert_line = - np.sum(
[vert_freq[i] * np.log(vert_freq[i]) if vert_freq[i] > 0 else 0 for i in range(MIN, len(vert_freq), 1)])
feats = feats.append(rec_plot_entropy_vert_line, 'rec_plot_entropy_vert_line')
# entropy of white vertical lines
rec_plot_entropy_white_vert_line = - np.sum(
[white_vert_freq[i] * np.log(white_vert_freq[i]) if white_vert_freq[i] > 0 else 0 for i in
range(MIN, len(white_vert_freq), 1)])
feats = feats.append(rec_plot_entropy_white_vert_line, 'rec_plot_entropy_white_vert_line')
return feats