Source code for pyunicorn.timeseries.visibility_graph

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# This file is part of pyunicorn.
# Copyright (C) 2008--2019 Jonathan F. Donges and pyunicorn authors
# URL: <http://www.pik-potsdam.de/members/donges/software>
# License: BSD (3-clause)

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

# array object and fast numerics
import numpy as np

from ..core import InteractingNetworks

from ._ext.numerics import _visibility_relations_missingvalues, \
    _visibility_relations_no_missingvalues, _visibility_relations_horizontal, \
    _visibility, _retarded_local_clustering, _advanced_local_clustering

#
#  Class definitions
#


[docs]class VisibilityGraph(InteractingNetworks): """ Class VisibilityGraph for generating and analyzing visibility graphs of time series. Visibility graphs were initially applied for time series analysis by [Lacasa2008]_. """ # # Internal methods #
[docs] def __init__(self, time_series, timings=None, missing_values=False, horizontal=False, silence_level=0): """ Missing values are handled as infinite values, effectively separating the visibility graph into different disconnected components. .. note:: Missing values have to be marked by the Numpy NaN flag! :type time_series: 2D array (time, dimension) :arg time_series: The time series to be analyzed, can be scalar or multi-dimensional. :arg str timings: Timings of the observations in :attr:`time_series`. :arg bool missing_values: Toggle special treatment of missing values in :attr:`time_series`. :arg bool horizontal: Indicates whether a horizontal visibility relation is used. :arg number silence_level: Inverse level of verbosity of the object. """ # Set silence_level self.silence_level = silence_level """The inverse level of verbosity of the object.""" # Set missing_values flag self.missing_values = missing_values """Controls special treatment of missing values in :attr:`time_series`.""" # Store time series self.time_series = time_series.copy().astype("float32") """The time series from which the visibility graph is constructed.""" if timings is not None: timings = timings.copy().astype("float32") else: timings = np.arange(len(time_series), dtype="float32") # Store timings self.timings = timings """The timimgs of the time series data points.""" # Get missing value indices if self.missing_values: self.missing_value_indices = np.isnan(self.time_series) # Determine visibility relations if not horizontal: A = self.visibility_relations() else: A = self.visibility_relations_horizontal() # Initialize Network object InteractingNetworks.__init__(self, A, directed=False, silence_level=silence_level)
[docs] def __str__(self): """ Returns a string representation. """ return 'VisibilityGraph: time series shape %s.\n%s' % ( self.time_series.shape, InteractingNetworks.__str__(self))
# # Visibility methods #
[docs] def visibility_relations(self): """ TODO """ if self.silence_level <= 1: print("Calculating visibility relations...") # Prepare x = self.time_series t = self.timings N = len(self.time_series) A = np.zeros((N, N), dtype="int8") if self.missing_values: mv_indices = self.missing_value_indices _visibility_relations_missingvalues(x, t, N, A, mv_indices) else: _visibility_relations_no_missingvalues(x, t, N, A) return A
# FIXME: There is no option for missing values
[docs] def visibility_relations_horizontal(self): """ TODO """ if self.silence_level <= 1: print("Calculating horizontal visibility relations...") # Prepare x = self.time_series t = self.timings N = len(self.time_series) A = np.zeros((N, N), dtype="int8") _visibility_relations_horizontal(x, t, N, A) return A
# # Specific measures for visibility graphs #
[docs] def visibility(self, node1, node2): """ Returns the visibility between node 1 and 2 as boolean. :arg int node1: node index of node 1 :arg int node2: node index of node 2 :rtype: bool """ if node1 == node2: return False elif abs(node2-node1) == 1: return True else: time = self.timings val = self.time_series return _visibility(time, val, node1, node2)
[docs] def visibility_horizontal(self, node1, node2): """ Returns the horizontal visibility between node 1 and 2 as boolean. :arg int node1: node index of node 1 :arg int node2: node index of node 2 :rtype: bool """ if node1 == node2: return False else: val = self.time_series i, j = min(node1, node2), max(node1, node2) if np.sum(~(val[i+1:j] < min(val[i], val[j]))): return False else: return True
[docs] def visibility_single(self, node): """ Returns the visibility between all nodes of self.time_series and node as array of booleans. :arg int node: node index of the node :rtype: 1D array of bool """ time_series = self.time_series testfun = lambda j: self.visibility(node, j) return np.array(map(testfun, range(len(time_series[1]))))
[docs] def visibility_horizontal_single(self, node): """ Returns the horizontal visibility between all nodes of self.time_series and node as array of booleans. :arg int node: node index of the node :rtype: 1D array of bool """ time_series = self.time_series testfun = lambda j: self.visibility_horizontal(node, j) return np.array(map(testfun, range(len(time_series[1]))))
[docs] def retarded_degree(self): """Return number of neighbors in the past of a node.""" # Prepare retarded_degree = np.zeros(self.N) A = self.adjacency for i in range(self.N): retarded_degree[i] = A[i, :i].sum() return retarded_degree
[docs] def advanced_degree(self): """Return number of neighbors in the future of a node.""" # Prepare advanced_degree = np.zeros(self.N) A = self.adjacency for i in range(self.N): advanced_degree[i] = A[i, i:].sum() return advanced_degree
[docs] def retarded_local_clustering(self): """ Return probability that two neighbors of a node in its past are connected. """ # Prepare retarded_clustering = np.zeros(self.N) # Get full adjacency matrix A = self.adjacency # Get number of nodes N = self.N # Get left degree retarded_degree = self.retarded_degree() # Prepare normalization factor norm = retarded_degree * (retarded_degree - 1) / 2. _retarded_local_clustering(N, A, norm, retarded_clustering) return retarded_clustering
[docs] def advanced_local_clustering(self): """ Return probability that two neighbors of a node in its future are connected. """ # Prepare advanced_clustering = np.zeros(self.N) # Get full adjacency matrix A = self.adjacency # Get number of nodes N = self.N # Get right degree advanced_degree = self.advanced_degree() # Prepare normalization factor norm = advanced_degree * (advanced_degree - 1) / 2. _advanced_local_clustering(N, A, norm, advanced_clustering) return advanced_clustering
[docs] def retarded_closeness(self): """Return average path length to nodes in the past of a node.""" # Prepare retarded_closeness = np.zeros(self.N) path_lengths = self.path_lengths() for i in range(self.N): retarded_closeness[i] = path_lengths[i, :i].mean() ** (-1) return retarded_closeness
[docs] def advanced_closeness(self): """Return average path length to nodes in the future of a node.""" # Prepare advanced_closeness = np.zeros(self.N) path_lengths = self.path_lengths() for i in range(self.N): advanced_closeness[i] = path_lengths[i, i+1:].mean() ** (-1) return advanced_closeness
[docs] def retarded_betweenness(self): """ Return betweenness of a node with respect to all pairs of nodes in its past. """ # Prepare retarded_betweenness = np.zeros(self.N) for i in range(self.N): retarded_indices = np.arange(i) retarded_betweenness[i] = self.nsi_betweenness( sources=retarded_indices, targets=retarded_indices)[i] return retarded_betweenness
[docs] def advanced_betweenness(self): """ Return betweenness of a node with respect to all pairs of nodes in its future. """ # Prepare advanced_betweenness = np.zeros(self.N) for i in range(self.N): advanced_indices = np.arange(i+1, self.N) advanced_betweenness[i] = self.nsi_betweenness( sources=advanced_indices, targets=advanced_indices)[i] return advanced_betweenness
[docs] def trans_betweenness(self): """ Return betweenness of a node with respect to all pairs of nodes with one node the past and one node in the future, respectively. """ # Prepare trans_betweenness = np.zeros(self.N) for i in range(self.N): retarded_indices = np.arange(i) advanced_indices = np.arange(i+1, self.N) trans_betweenness[i] = self.nsi_betweenness( sources=retarded_indices, targets=advanced_indices)[i] return trans_betweenness
# # Measures corrected for boundary effects #
[docs] def boundary_corrected_degree(self): """Return a weighted degree corrected for trivial boundary effects.""" # Prepare N_past = np.arange(self.N) N_future = N_past[::-1] cdegree = (self.retarded_degree() * N_past + self.advanced_degree() * N_future) / float(self.N - 1) return cdegree
[docs] def boundary_corrected_closeness(self): """ Return a weighted closeness corrected for trivial boundary effects. """ # Prepare N_past = np.arange(self.N) N_future = N_past[::-1] ccloseness = (self.N - 1) * (self.retarded_closeness() / N_past + self.advanced_closeness() / N_future) return ccloseness