#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# This file is part of pyunicorn.
# Copyright (C) 2008--2015 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 .. import Network
from .recurrence_plot import RecurrencePlot


#
#  Class definitions
#

class RecurrenceNetwork(RecurrencePlot, Network):

    """
    Class RecurrenceNetwork for generating and quantitatively analyzing
    recurrence networks.

    More information on the theory and applications of recurrence networks can
    be found in [Marwan2009]_, [Donner2010b]_.

    Examples:

     - Create an instance of RecurrenceNetwork with a fixed recurrence
       threshold and without embedding::

           RecurrenceNetwork(time_series, threshold=0.1)

     - Create an instance of RecurrenceNetwork at a fixed (global) recurrence
       rate and using time delay embedding::

           RecurrenceNetwork(time_series, dim=3, tau=2,
                             recurrence_rate=0.05).recurrence_rate()
    """

    #
    #  Internal methods
    #

    def __init__(self, time_series, metric="supremum", normalize=False,
                 missing_values=False, silence_level=0, **kwds):
        """
        Initialize an instance of RecurrenceNetwork.

        Creates an embedding of the given time series, calculates a recurrence
        plot from the embedding and then creates a Network object from the
        recurrence plot, interpreting the recurrence matrix as the adjacency
        matrix of a complex network.

        Either recurrence threshold ``threshold``/``threshold_std``, recurrence
        rate ``recurrence_rate`` or local recurrence rate
        ``local_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.

        :type time_series: 2D array (time, dimension)
        :arg time_series: 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 the time series to
            zero mean and unit standard deviation.
        :arg bool missing_values: Toggle special treatment of missing values in
            :attr:`.RecurrencePlot.time_series`.
        :arg number silence_level: Inverse level of verbosity of the object.
        :arg number threshold: The recurrence threshold keyword for generating
            the recurrence network using a fixed threshold.
        :arg number threshold_std: The recurrence threshold keyword for
            generating the recurrence plot using a fixed threshold in units of
            the time series' STD.
        :arg number recurrence_rate: The recurrence rate keyword for generating
            the recurrence network using a fixed recurrence rate.
        :arg number local_recurrence_rate: The local recurrence rate keyword
            for generating the recurrence plot using a fixed local recurrence
            rate (same number of recurrences for each state vector).
        :arg number adaptive_neighborhood_size: The adaptive neighborhood size
            parameter for generating recurrence plots based on the algorithm in
            [Xu2008]_.
        :arg number dim: The embedding dimension.
        :arg number tau: The embedding delay.
        :type node_weights: 1D array (time)
        :arg node_weights: The sequence of weights associated with each
            node for calculating n.s.i. network measures.
        """
        #  Initialize the underlying RecurrencePlot object
        RecurrencePlot.__init__(self, time_series=time_series, metric=metric,
                                normalize=normalize,
                                missing_values=missing_values,
                                silence_level=silence_level, **kwds)

        #  Set diagonal of R to zero to avoid self-loops in the recurrence
        #  network
        A = self.R.copy()
        A.flat[::self.N+1] = 0

        #  Get keywords
        local_recurrence_rate = kwds.get("local_recurrence_rate")
        node_weights = kwds.get("node_weights")

        #  Assign to each embedded state vector the weight of the earliest
        #  sample contained in it (too simple!)
        if node_weights is not None:
            node_weights = node_weights[:self.N]

        #  Remove state vectors containing missing values
        if missing_values:
            A = np.delete(A, np.where(self.missing_value_indices), axis=0)
            A = np.delete(A, np.where(self.missing_value_indices), axis=1)

        #  Create a Network object interpreting the recurrence matrix as the
        #  graph adjacency matrix.
        if local_recurrence_rate is not None:
            #  A recurrence network with fixed local recurrence rate (Eckmann
            #  definition of a recurrence plot) is directed by definition.
            Network.__init__(self, A, directed=True, node_weights=node_weights,
                             silence_level=silence_level)
        else:
            #  Create a Network object interpreting the recurrence matrix as
            #  the graph adjacency matrix. Recurrence networks are undirected
            #  by definition.
            Network.__init__(self, A, directed=False,
                             node_weights=node_weights,
                             silence_level=silence_level)

    def clear_cache(self):
        """
        Clean up memory by deleting information that can be recalculated from
        basic data.

        Extends the clean up methods of the parent classes.
        """
        #  Call clean up of RecurrencePlot
        RecurrencePlot.clear_cache(self)
        #  Call clean up of Network
        Network.clear_cache(self)

    #
    #  Methods to handle recurrence networks
    #
    def set_fixed_threshold(self, threshold):
        """
        Create a recurrence network at a fixed threshold.

        :arg number threshold: The threshold.
        """
        #  Set fixed threshold on recurrence plot level
        RecurrencePlot.set_fixed_threshold(self, threshold)

        #  Set diagonal of R to zero to avoid self-loops in the recurrence
        #  network
        A = self.R.copy()
        A.flat[::self.N+1] = 0

        #  Create a Network object interpreting the recurrence matrix as the
        #  graph adjacency matrix. Recurrence networks are undirected by
        #  definition.
        Network.__init__(self, A, directed=False,
                         silence_level=self.silence_level)

    def set_fixed_threshold_std(self, threshold_std):
        """
        Set the recurrence network to a fixed threshold in units of the
        standard deviation of the time series.

        :arg number threshold_std: The recurrence threshold in units of the
            standard deviation of the time series.
        """
        #  Set fixed threshold in units of STD on recurrence plot level
        RecurrencePlot.set_fixed_threshold_std(self, threshold_std)

        #  Set diagonal of R to zero to avoid self-loops in the recurrence
        #  network
        A = self.R.copy()
        A.flat[::self.N+1] = 0

        #  Create a Network object interpreting the recurrence matrix as the
        #  graph adjacency matrix. Recurrence networks are undirected by
        #  definition.
        Network.__init__(self, A, directed=False,
                         silence_level=self.silence_level)

    def set_fixed_recurrence_rate(self, recurrence_rate):
        """
        Create a recurrence network at a fixed link density (recurrence rate).

        :arg number recurrence_rate: The link density / recurrence rate.
        """
        #  Set fixed recurrence rate on recurrence plot level
        RecurrencePlot.set_fixed_recurrence_rate(self, recurrence_rate)

        #  Set diagonal of R to zero to avoid self-loops in the recurrence
        #  network
        A = self.R.copy()
        A.flat[::self.N+1] = 0

        #  Create a Network object interpreting the recurrence matrix as the
        #  graph adjacency matrix. Recurrence networks are undirected by
        #  definition.
        Network.__init__(self, A, directed=False,
                         silence_level=self.silence_level)

    def set_fixed_local_recurrence_rate(self, local_recurrence_rate):
        """
        Create a recurrence network at a fixed local recurrence rate.

        This leads to a directed recurrence network with identical out-degree
        :math:`int(N * local_recurrence_rate)`, but variable in-degree. The
        associated recurrence plot coincides with the original Eckmann
        definition.

        :arg number local_recurrence_rate: The local recurrence rate.
        """
        #  Set fixed local recurrence rate on recurrence plot level
        RecurrencePlot.\
            set_fixed_local_recurrence_rate(self, local_recurrence_rate)

        #  Create a Network object interpreting the recurrence matrix as the
        #  graph adjacency matrix. Set diagonal of R to zero to avoid
        #  self-loops in the recurrence network
        A = self.R.copy()
        A.flat[::self.N+1] = 0

        #  A recurrence network with fixed local recurrence rate (Eckmann
        #  definition of a recurrence plot) is directed by definition.
        Network.__init__(self, A, directed=True,
                         silence_level=self.silence_level)

    def set_adaptive_neighborhood_size(self, adaptive_neighborhood_size,
                                       order=None):
        """
        Create a recurrence network using the :index:`adaptive neighborhood
        size <single: adaptive neighborhood size; recurrence network>`
        algorithm used in [Xu2008]_.

        The exact algorithm was deduced from private correspondence with the
        authors.  It leads to an undirected network with mean degree
        :math:`<k> = 2 * m`, where m is the adaptive_neighborhood_size.
        The degree :math:`k_v` of single nodes may vary, but :math:`k_v >= m`
        holds!

        :arg number adaptive_neighborhood_size: The number of adaptive nearest
            neighbors (recurrences) assigned to each state vector.
        :type order: 1D array (int32)
        :arg order: The indices of state vectors in the order desired for
            processing by the algorithm.
        """
        #  Set adaptive neighborhood size on recurrence plot level
        RecurrencePlot.set_adaptive_neighborhood_size(
            self, adaptive_neighborhood_size, order)

        #  Create a Network object interpreting the recurrence matrix as the
        #  graph adjacency matrix. Set diagonal of R to zero to avoid
        #  self-loops in the recurrence network
        A = self.R.copy()
        A.flat[::self.N+1] = 0

        #  A recurrence network with fixed local recurrence rate (Eckmann
        #  definition of a recurrence plot) is directed by definition.
        Network.__init__(self, A, directed=False,
                         silence_level=self.silence_level)
