# This file is part of pyunicorn.
# Copyright (C) 2008--2024 Jonathan F. Donges and pyunicorn authors
# URL: <https://www.pik-potsdam.de/members/donges/software-2/software>
# License: BSD (3-clause)
#
# Please acknowledge and cite the use of this software and its authors
# when results are used in publications or published elsewhere.
#
# You can use the following reference:
# J.F. Donges, J. Heitzig, B. Beronov, M. Wiedermann, J. Runge, Q.-Y. Feng,
# L. Tupikina, V. Stolbova, R.V. Donner, N. Marwan, H.A. Dijkstra,
# and J. Kurths, "Unified functional network and nonlinear time series analysis
# for complex systems science: The pyunicorn package"
"""
Provides classes for the analysis of dynamical systems and time series based
on recurrence plots, including measures of recurrence quantification
analysis (RQA) and recurrence network analysis.
"""
from typing import Tuple
from collections.abc import Hashable
import numpy as np
from ..core.cache import Cached
from .recurrence_plot import RecurrencePlot
from ..core._ext.types import to_cy, DFIELD
from ._ext.numerics import _manhattan_distance_matrix_crp, \
_euclidean_distance_matrix_crp, _supremum_distance_matrix_crp
[docs]
class CrossRecurrencePlot(RecurrencePlot):
"""
Class CrossRecurrencePlot for generating and quantitatively analyzing
:index:`cross recurrence plots <single: cross recurrence plot>`.
The CrossRecurrencePlot class supports the construction of cross recurrence
plots from two multi-dimensional time series, optionally using embedding.
Currently, manhattan, euclidean and supremum norms are provided for
measuring distances in phase space.
Methods for calculating commonly used measures of :index:`recurrence
quantification analysis <pair: RQA; cross recurrence plot>` (RQA) are
provided, e.g., determinism, maximum diagonal line length and laminarity.
The definitions of these measures together with a review of the theory and
applications of cross recurrence plots are given in [Marwan2007]_.
**Examples:**
- Create an instance of CrossRecurrencePlot from time series x and y with
a fixed recurrence threshold and without embedding::
CrossRecurrencePlot(x, y, threshold=0.1)
- Create an instance of CrossRecurrencePlot at a fixed recurrence rate and
using time delay embedding::
CrossRecurrencePlot(x, y, dim=3, tau=2,
recurrence_rate=0.05).recurrence_rate()
"""
#
# Internal methods
#
[docs]
def __init__(self, x, y, metric="supremum", normalize=False,
sparse_rqa=False, silence_level=0, **kwds):
"""
Initialize an instance of CrossRecurrencePlot.
.. note::
For a cross recurrence plot, time series x and y generally do
**not** need to have the same length!
Either recurrence threshold ``threshold`` or recurrence rate
``recurrence_rate`` have to be given as keyword arguments.
Embedding is only supported for scalar time series. If embedding
dimension ``dim`` and delay ``tau`` are **both** given as keyword
arguments, embedding is applied. Multidimensional time series are
processed as is by default. The same delay embedding is applied to both
time series x and y.
:type x: 2D array (time, dimension)
:arg x: One of the time series to be analyzed, can be scalar or
multi-dimensional.
:type y: 2D array (time, dimension)
:arg y: One of the time series to be analyzed, can be scalar or
multi-dimensional.
:arg str metric: The metric for measuring distances in phase space
("manhattan", "euclidean", "supremum").
:arg bool normalize: Decide whether to normalize both time series to
zero mean and unit standard deviation.
:arg number silence_level: Inverse level of verbosity of the object.
:arg number threshold: The recurrence threshold keyword for generating
the cross recurrence plot using a fixed threshold.
:arg number recurrence_rate: The recurrence rate keyword for generating
the cross recurrence plot using a fixed recurrence rate.
:arg number dim: The embedding dimension.
:arg number tau: The embedding delay.
"""
threshold = kwds.get("threshold")
recurrence_rate = kwds.get("recurrence_rate")
RecurrencePlot.__init__(
self, np.empty((2, 0)), metric=metric, normalize=normalize,
sparse_rqa=sparse_rqa, silence_level=silence_level,
skip_recurrence=True)
self.CR = None
"""The cross recurrence matrix."""
self.N = 0
"""The length of the embedded time series x."""
self.M = 0
"""The length of the embedded time series y."""
# Store time series
self.x = x.copy()
"""The time series x."""
self.y = y.copy()
"""The time series y."""
# Reshape time series
self.x.shape = (self.x.shape[0], -1)
self.y.shape = (self.y.shape[0], -1)
# Normalize time series
if normalize:
self.normalize_time_series(self.x)
self.normalize_time_series(self.y)
# Get embedding dimension and delay from **kwds
dim = kwds.get("dim")
tau = kwds.get("tau")
self._mut_embedding: int = 0
if (dim is not None) and (tau is not None):
# Embed the time series
self.x_embedded = self.embed_time_series(self.x, dim, tau)
self.y_embedded = self.embed_time_series(self.y, dim, tau)
else:
self.x_embedded = self.x
self.y_embedded = self.y
# construct recurrence plot accordingly to threshold / recurrence rate
if threshold is not None:
# Calculate the recurrence matrix R using the radius of
# neighborhood threshold
CrossRecurrencePlot.set_fixed_threshold(self, threshold)
elif recurrence_rate is not None:
# Calculate the recurrence matrix R using a fixed recurrence rate
CrossRecurrencePlot.\
set_fixed_recurrence_rate(self, recurrence_rate)
else:
raise NameError("Please give either threshold or recurrence_rate \
to construct the cross recurrence plot!")
[docs]
def __cache_state__(self) -> Tuple[Hashable, ...]:
return (self._mut_embedding,)
[docs]
def __str__(self):
"""
Returns a string representation.
"""
return ("CrossRecurrencePlot: "
f"time series shapes {self.x.shape}, {self.y.shape}.\n"
f"Embedding dimension {self.dim if self.dim else 0}\n"
f"Threshold {self.threshold}, {self.metric} metric")
@property
def x_embedded(self) -> np.ndarray:
"""
The embedded time series x.
"""
return self._x_embedded
@x_embedded.setter
def x_embedded(self, embedding: np.ndarray):
self._x_embedded = to_cy(embedding, DFIELD)
self._mut_embedding += 1
@property
def y_embedded(self) -> np.ndarray:
"""
The embedded time series y.
"""
return self._y_embedded
@y_embedded.setter
def y_embedded(self, embedding: np.ndarray):
self._y_embedded = to_cy(embedding, DFIELD)
self._mut_embedding += 1
#
# Service methods
#
[docs]
def recurrence_matrix(self):
"""
Return the current cross recurrence matrix :math:`CR`.
:rtype: 2D square Numpy array
:return: the current cross recurrence matrix :math:`CR`.
"""
return self.CR
[docs]
def distance_matrix(self, metric):
"""
Return phase space cross distance matrix :math:`D` according to the
chosen metric.
:arg str metric: The metric for measuring distances in phase space
("manhattan", "euclidean", "supremum").
:rtype: 2D square array
:return: the phase space cross distance matrix :math:`D`
"""
assert metric in self._known_metrics, f"unknown metric: {metric}"
return getattr(self, f"{metric}_distance_matrix")()
#
# Calculate recurrence plot
#
[docs]
@Cached.method(name="the manhattan distance matrix")
def manhattan_distance_matrix(self):
"""
Return the manhattan distance matrix from two (embedded) time series.
:type x_embedded: 2D Numpy array (time, embedding dimension)
:arg x_embedded: The phase space trajectory x.
:type y_embedded: 2D Numpy array (time, embedding dimension)
:arg y_embedded: The phase space trajectory y.
:rtype: 2D rectangular Numpy array
:return: the manhattan distance matrix.
"""
ntime_x = self.x_embedded.shape[0]
ntime_y = self.y_embedded.shape[0]
dim = self.x_embedded.shape[1]
return _manhattan_distance_matrix_crp(ntime_x, ntime_y, dim,
self.x_embedded, self.y_embedded)
[docs]
@Cached.method(name="the euclidean distance matrix")
def euclidean_distance_matrix(self):
"""
Return the euclidean distance matrix from two (embedded) time series.
:rtype: 2D rectangular Numpy array
:return: the euclidean distance matrix.
"""
ntime_x = self.x_embedded.shape[0]
ntime_y = self.y_embedded.shape[0]
dim = self.x_embedded.shape[1]
return _euclidean_distance_matrix_crp(ntime_x, ntime_y, dim,
self.x_embedded, self.y_embedded)
[docs]
@Cached.method(name="the supremum distance matrix")
def supremum_distance_matrix(self):
"""
Return the supremum distance matrix from two (embedded) time series.
:rtype: 2D rectangular Numpy array
:return: the supremum distance matrix.
"""
ntime_x = self.x_embedded.shape[0]
ntime_y = self.y_embedded.shape[0]
dim = self.x_embedded.shape[1]
return _supremum_distance_matrix_crp(ntime_x, ntime_y, dim,
self.x_embedded, self.y_embedded)
[docs]
def set_fixed_threshold(self, threshold):
"""
Set the cross recurrence plot to a fixed threshold.
Modifies / sets the class variables :attr:`CR`, :attr:`N` and :attr:`M`
accordingly.
:arg number threshold: The recurrence threshold.
"""
if self.silence_level <= 1:
print("Calculating cross recurrence plot at fixed threshold...")
distance = self.distance_matrix(self.metric)
(N, M) = distance.shape
recurrence = np.zeros((N, M), dtype="int8")
recurrence[distance < threshold] = 1
self.CR = recurrence
self.N = N
self.M = M
[docs]
def set_fixed_recurrence_rate(self, recurrence_rate):
"""
Set the cross recurrence plot to a fixed recurrence rate.
Modifies / sets the class variables :attr:`CR`, :attr:`N` and :attr:`M`
accordingly.
:arg number recurrence_rate: The recurrence rate.
"""
if self.silence_level <= 1:
print("Calculating cross recurrence plot at "
"fixed recurrence rate...")
distance = self.distance_matrix(self.metric)
(N, M) = distance.shape
threshold = self.threshold_from_recurrence_rate(distance,
recurrence_rate)
recurrence = np.zeros((N, M), dtype="int8")
recurrence[distance < threshold] = 1
self.CR = recurrence
self.N = N
self.M = M
#
# Extended RQA measures
#
[docs]
def recurrence_rate(self):
"""
Return cross recurrence rate.
Alias to :meth:`cross_recurrence_rate`, since
:meth:`RecurrencePlot.recurrence_rate` would give incorrect results
here.
:rtype: number (float)
:return: the cross recurrence rate.
"""
return self.cross_recurrence_rate()
[docs]
def cross_recurrence_rate(self):
"""
Return cross recurrence rate.
:rtype: number (float)
:return: the cross recurrence rate.
"""
return float(self.CR.sum()) / (self.N * self.M)
[docs]
def balance(self):
"""
Return balance of the cross recurrence plot.
Might be useful for detecting the direction of coupling between systems
using cross recurrence analysis.
"""
# Get cross recurrence matrix
CR = self.recurrence_matrix()
# Get sum of upper triangle of cross recurrence matrix, excluding the
# main diagonal
upper_triangle_sum = np.triu(CR, k=1).sum()
# Get sum of lower triangle of cross recurrence matrix, excluding the
# main diagonal
lower_triangle_sum = np.tril(CR, k=-1).sum()
# Return balance
return (upper_triangle_sum - lower_triangle_sum) / \
float(upper_triangle_sum + lower_triangle_sum)
[docs]
def diagline_dist(self):
"""Not implemented yet"""
raise NotImplementedError(
"Line distributions are not yet "
"available for cross-recurrence plots")
[docs]
def vertline_dist(self):
"""Not implemented yet"""
raise NotImplementedError(
"Line distributions are not yet "
"available for cross-recurrence plots")
[docs]
def white_vertline_dist(self):
"""Not implemented yet"""
raise NotImplementedError(
"Line distributions are not yet "
"available for cross-recurrence plots")