Source code for neet.landscape

"""
.. currentmodule:: neet

.. testsetup:: landscape

    from neet.boolean import ECA
    from neet.boolean.examples import s_pombe
    import numpy as np

The :mod:`neet` module provides the :class:`LandscapeMixin` class from which
the :class:`neet.Network` class inherits. This endows all networks with the
various methods for computing the various landscape-related properties of the
networks, such as :attr:`LandscapeMixin.attractors`. These properties are often
associated with the *state space* of the network; however, we have opted to
provide them via a separate mixin because the :class:`neet.StateSpace` class
represents an *unstructured* set of states, with no dynamical information

A key feature of the :class:`LandscapeMixin` is that it is lazy and caches
results as they are computed. For example, the attractors of the landscape are
computed the first the user requests the :attr:`LandscapeMixin.attractors`
property, but the result is cached in the :attr:`LandscapeMixin.landscape_data`
attribute. Subsequent calls simply return the cached data. What's more, many of
the properties of the landscape can be determined using almost the exact same
algorithm, so whenever one is requested, they are all simultaneously computed.
See :class:`LandscapeMixin.expound` for a list of such properties.
"""
import networkx as nx
import numpy as np
import pyinform as pi


[docs]class LandscapeData(object): """ The LandscapeData class stores the various landscape properties computed in the :class:`LandscapeMixin`. This is used rather an individual properties within :class:`LandscapeMixin` to make it simple for users to extract all of the landscape properties before modifying a network and observing the effects of that change on the landscape. The following properties are stored in LandscapeData: .. autosummary:: :nosignatures: LandscapeMixin.transitions LandscapeMixin.attractors LandscapeMixin.attractor_lengths LandscapeMixin.basins LandscapeMixin.basin_sizes LandscapeMixin.basin_entropy LandscapeMixin.heights LandscapeMixin.recurrence_times LandscapeMixin.in_degrees .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.attractors array([array([76]), array([4]), array([8]), array([12]), array([144, 110, 384]), array([68]), array([72]), array([132]), array([136]), array([140]), array([196]), array([200]), array([204])], dtype=object) >>> default_landscape = s_pombe.landscape_data >>> s_pombe.landscape(pin=[0,1]).attractors array([array([0]), array([1]), array([386, 402, 178, 162]), array([387, 403, 179, 163]), array([4]), array([8]), array([12]), array([76]), array([65]), array([64]), array([68]), array([72]), array([132]), array([136]), array([140]), array([192]), array([193]), array([196]), array([200]), array([204])], dtype=object) >>> default_landscape.attractors array([array([76]), array([4]), array([8]), array([12]), array([144, 110, 384]), array([68]), array([72]), array([132]), array([136]), array([140]), array([196]), array([200]), array([204])], dtype=object) >>> s_pombe.clear_landscape() """ transitions = None attractors = None attractor_lengths = None basins = None basin_sizes = None basin_entropy = None heights = None recurrence_times = None in_degrees = None
[docs]class LandscapeMixin: """ The LandscapeMixin class represents the structure and topology of the "landscape" of state transitions. That is, it is the state space together with information about state transitions and the topology of the state transition graph. The LandscapeMixin class exposes the following methods: .. autosummary:: :nosignatures: landscape clear_landscape landscape_data transitions attractors attractor_lengths basins basin_sizes basin_entropy heights recurrence_times in_degrees trajectory timeseries landscape_graph draw_landscape_graph expound """ # Whether or not the landscape data has been populated __landscaped = False # The landscape data cache __landscape_data = LandscapeData()
[docs] def landscape(self, index=None, pin=None, values=None): """ Setup the landscape. Prepares the landscape for computation of the various properties, specifying which nodes will be updated (``index``), pinned (``pin``) or set to a particular state (``values``). In particular, it computes the state transitions of the network and prepares private variables for a subsequent call to :meth:`expound`, :meth:`landscape_graph`, etc... This function is implicitly called with no arguments by the various landscape accessors if it has not already been called. This is intended as a convenience since most of the time the user would do this anyway. This function implicitly calls :attr:`clear_landscape`, so make sure to create a reference to :attr:`landscape_data` if landscape information has previously been compute and you wish to keep it around. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.landscape_data.transitions >>> s_pombe.landscape() <neet.boolean.wtnetwork.WTNetwork object at 0x...> >>> len(s_pombe.landscape_data.transitions) 512 .. rubric:: Pinning States .. doctest:: landscape # Prevents all states from transitioning >>> s_pombe.landscape(pin = range(s_pombe.size)) <neet.boolean.wtnetwork.WTNetwork object at 0x...> >>> np.array_equal(s_pombe.landscape_data.transitions, range(s_pombe.volume)) True >>> s_pombe.clear_landscape() .. rubric:: Overriding Node States .. doctest:: landscape # Forces all states to transition to 0 >>> s_pombe.landscape(values={i: 0 for i in range(s_pombe.size)}) <neet.boolean.wtnetwork.WTNetwork object at 0x...> >>> np.all(s_pombe.landscape_data.transitions == 0) True >>> s_pombe.clear_landscape() :param index: the index to update (or None) :param pin: the indices to pin during update (or None) :param values: a dictionary of index-value pairs to set after update :return: ``self`` """ self.__index = index self.__pin = pin self.__values = values self.__expounded = False update = self._unsafe_update encode = self._unsafe_encode transitions = np.empty(self.volume, dtype=np.int) for i, state in enumerate(self): transitions[i] = encode(update(state, index=self.__index, pin=self.__pin, values=self.__values)) self.clear_landscape() self.__landscape_data.transitions = transitions self.__landscaped = True return self
[docs] def clear_landscape(self): """ Clear the landscape's data and graph from memory. """ self.__landscaped = False self.__landscape_graph = None self.__landscape_data = LandscapeData()
@property def landscape_data(self): """ Get the :class:`LandscapeData` object. The :class:`LandscapeData` object contains any cached attractor landscape information generated by a call to :meth:`expound`. """ return self.__landscape_data @property def transitions(self): """ Get the state transitions as an array. Each element of the array is the next (encoded) state of the system starting from the initial state equal to the index. For example, if :: >>> net.transitions array([ 0, 3, 1, 2 ]) then state ``0`` will transition to ``0``, ``1`` to ``3``, etc... Be aware that if :meth:`landscape` has not been called, this method will call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.transitions array([ 2, 2, 130, 130, 4, 0, 128, 128, 8, 0, 128, 128, 12, 0, 128, 128, 256, 256, 384, 384, 260, 256, 384, 384, 264, 256, ... 208, 208, 336, 336, 464, 464, 340, 336, 464, 464, 344, 336, 464, 464, 348, 336, 464, 464]) .. rubric:: Pinned States A preceding call to :meth:`landscape` can, for example, pin specific nodes to their current state, thus affecting the state transitions. .. doctest:: landscape >>> s_pombe.landscape(pin = [0]).transitions array([ 2, 3, 130, 131, 4, 1, 128, 129, 8, 1, 128, 129, 12, 1, 128, 129, 256, 257, 384, 385, 260, 257, 384, 385, 264, 257, ... 208, 209, 336, 337, 464, 465, 340, 337, 464, 465, 344, 337, 464, 465, 348, 337, 464, 465]) >>> s_pombe.clear_landscape() :return: a :class:`numpy.ndarray` of state transitions """ if not self.__landscaped: self.landscape() return self.__landscape_data.transitions @property def attractors(self): """ Get the attractors of the landscape as an array. Each element of the array is an attractor cycle, each of which is an array of states in the cycle. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.attractors array([array([76]), array([4]), array([8]), array([12]), array([144, 110, 384]), array([68]), array([72]), array([132]), array([136]), array([140]), array([196]), array([200]), array([204])], dtype=object) .. rubric:: Update Only a Single Node A preceding call to :meth:`landscape` can, for example, specify which nodes will be updated in the process of computing the attractors. For example, we can allow only the first node of the state to be updated. .. doctest:: landscape >>> s_pombe.landscape(index=0).attractors array([[ 0], [ 2], [ 4], ... [506], [508], [510]]) >>> s_pombe.clear_landscape() :return: a :class:`numpy.ndarray` of attractor cycles, each of which is an array of encoded states """ if not self.__landscaped: self.landscape() if not self.__expounded: self.expound() return self.__landscape_data.attractors @property def attractor_lengths(self): """ Get the length of the attractors as an array. The array is indexed by the basin number. The order of the attractor lengths is the same as in :attr:`attractors`. For example, :: >>> net.attractors array([ array([0,1]), array([1]) ] >>> net.attractor_lengths array([2, 1]) If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.attractor_lengths array([1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1]) .. rubric:: Pinned States A preceding call to :meth:`landscape` can pin specific nodes to their current state, thus affecting the attractor lengths. .. doctest:: landscape >>> s_pombe.landscape(pin = [0]).attractor_lengths array([1, 6, 1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1]) >>> s_pombe.clear_landscape() :return: a :class:`numpy.ndarray` of the lengths of the attractors """ if not self.__landscaped: self.landscape() if not self.__expounded: self.expound() return self.__landscape_data.attractor_lengths @property def basins(self): """ Get the basins of the states as an array. Each index of the array is an encoded state and the corresponding value is the attractor basin in which it resides. The attractor basins are integers which can be used to index the :attr:`attractors` array, providing the attractor cycle for the base. For example, if :: >>> net.basins array([ 0, 1, 2, 1 ]) >>> net.attractors array([ array([0]), array([1]), array([2]) ]) then the states ``1`` and ``3`` are both in the attractor basin which attracts to the fixed-point ``1``. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.basins array([ 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 4, 4, 0, 0, 4, 4, 0, 0, 4, 4, 0, 0, 4, 4, 4, 4, ... 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) .. rubric:: Resetting Node States A preceding call to :meth:`landscape` can, for example, specify that specific nodes are reset to a particular value after the updating the. For example, we can force the first and second nodes to ``0``, thus affecting the basins. .. doctest:: landscape >>> s_pombe.landscape(values={0: 0, 1: 0}).basins array([ 0, 0, 1, 1, 2, 0, 1, 1, 3, 0, 1, 1, 4, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ... 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) >>> s_pombe.clear_landscape() :return: a :class:`numpy.ndarray` of each state's attractor basin """ if not self.__landscaped: self.landscape() if not self.__expounded: self.expound() return self.__landscape_data.basins @property def basin_sizes(self): """ Get the sizes of the attractor basins as an array. The array is indexed by the basin number. The order of the basin sizes is the same as in :attr:`attractors`. For example, if :: >>> net.attractors array([ array([0,1]), array([3,6]) ] >>> net.basin_sizes array([ 5, 3 ]) then the attractor ``[0, 1]`` has a basin size of :math:`5` with the remaining states in the other attractor's basin. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.basin_sizes array([378, 2, 2, 2, 104, 6, 6, 2, 2, 2, 2, 2, 2]) .. rubric:: Pinning States A preceding call to :meth:`landscape` can specify that some of the nodes are not updated, say the first two. .. doctest:: landscape >>> s_pombe.landscape(pin=[0,1]).basin_sizes array([ 1, 4, 128, 128, 1, 1, 1, 114, 120, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1]) >>> s_pombe.clear_landscape() :return: a :class:`numpy.ndarray` of each attractor's basin size """ if not self.__landscaped: self.landscape() if not self.__expounded: self.expound() return self.__landscape_data.basin_sizes @property def basin_entropy(self): """ Compute the basin entropy of the landscape [Krawitz2007]_. That is the Shannon entropy (in bits) of the distribution of basin sizes. For example, :: >>> net.basin_sizes array([6, 2]) >>> net.basin_entropy 0.8112781244591328 which is :math:`-\\frac{6}{8}\\log_2{\\frac{6}{8}) - \\frac{2}{8}\\log_2{\\frac{2}{8})`. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.basin_entropy 1.2218888... .. rubric:: Pinning States A preceding call to :meth:`landscape` can specify that some of the nodes are not updated, say the first two. .. doctest:: landscape >>> s_pombe.landscape(pin=[0,1]).basin_entropy 2.328561849437885 >>> s_pombe.clear_landscape() :return: basin entropy in bits """ if not self.__landscaped: self.landscape() if not self.__expounded: self.expound() return self.__landscape_data.basin_entropy @property def heights(self): """ Get the heights of each state in the landscape. That is the fewest number of time steps from that state to a state in it's attractor cycle, as an array. Each index of the array is an encoded state, and the corresponding value is the height. For example, if :: >>> net.heights array([ 3, 0, 1, ... ]) then it will take :math:`3` time steps for the state ``0`` to reach an attractor state while state ``1`` **is** an attractor state`. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.heights array([7, 7, 6, 6, 0, 8, 6, 6, 0, 8, 6, 6, 0, 8, 6, 6, 8, 8, 1, 1, 2, 8, 1, 1, 2, 8, 1, 1, 2, 8, 1, 1, 2, 2, 2, 2, 9, 9, 1, 1, 9, 9, 1, 1, ... 3, 9, 9, 9, 3, 9, 9, 9, 3, 9, 9, 9, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]) .. rubric:: Resetting Node States A preceding call to :meth:`landscape` can specify that specific nodes are reset to a particular value after the updating the. For example, we can force the first and second nodes to ``0``, thus affecting the basins. .. doctest:: landscape >>> s_pombe.landscape(values={0: 0, 1: 0}).heights array([0, 1, 6, 6, 0, 1, 6, 6, 0, 1, 6, 6, 0, 1, 6, 6, 2, 2, 5, 5, 2, 2, 5, 5, 2, 2, 5, 5, 2, 2, 5, 5, 3, 3, 6, 6, 3, 3, 6, 6, 3, 3, 6, 6, ... 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]) >>> s_pombe.clear_landscape() :return: a :class:`numpy.ndarray`, each value of which is the height of the indexing state """ if not self.__landscaped: self.landscape() if not self.__expounded: self.expound() return self.__landscape_data.heights @property def recurrence_times(self): """ Get the recurrence time of each state in the landscape. That is the number of time steps from that state after which *some* state is repeated, as an array. Each index of the array is an encoded state, and the corresponding value is the recurrence time of that state. For example, if :: >>> net.recurrent_times array([ 3, 10, 0, ... ]) then a state will be seen at least twice if the ``0`` state is updated more than :math:`3` times. The ``2`` state is a fixed-point attractor state as updating even once will repeat a state. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.recurrence_times array([7, 7, 6, 6, 0, 8, 6, 6, 0, 8, 6, 6, 0, 8, 6, 6, 8, 8, 3, 3, 2, 8, 3, 3, 2, 8, 3, 3, 2, 8, 3, 3, 4, 4, 4, 4, 9, 9, 3, 3, 9, 9, 3, 3, ... 3, 9, 9, 9, 3, 9, 9, 9, 3, 9, 9, 9, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]) .. rubric:: Resetting Node States A preceding call to :meth:`landscape` can specify that specific nodes are reset to a particular value after the updating the. For example, we can force the first and second nodes to ``0``, thus affecting the basins. .. doctest:: landscape >>> s_pombe.landscape(pin=[0,1]).recurrence_times array([0, 0, 5, 5, 0, 1, 5, 5, 0, 1, 5, 5, 0, 1, 5, 5, 2, 2, 4, 4, 2, 2, 4, 4, 2, 2, 4, 4, 2, 2, 4, 4, 3, 3, 5, 5, 3, 3, 5, 5, 3, 3, 5, 5, ... 3, 3, 5, 5, 3, 3, 5, 5, 3, 3, 5, 5, 3, 3, 8, 8, 3, 3, 8, 8, 3, 3, 8, 8, 3, 3, 8, 8]) >>> s_pombe.clear_landscape() :return: a :class:`numpy.ndarray` of recurrence times, one for each state """ if not self.__landscaped: self.landscape() if not self.__expounded: self.expound() return self.__landscape_data.recurrence_times @property def in_degrees(self): """ Get the in-degree of each state in the landscape. That is the number of states which transition to that state in a single time step, as a array. Each index of the array is an encoded state, and the corresponding value is the number of preceding states. For example, if :: >>> net.in_degrees array([ 5, 2, 0, 0, ... ] then :math:`5` states transition to the ``0`` state in a single time step, while states ``2`` and ``3`` are in the `Garden of Eden <https://wikipedia.org/wiki/Garden_of_Eden_(cellular_automaton)>`_. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.in_degrees array([ 6, 0, 4, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 12, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) .. rubric:: Pinning States A preceding call to :meth:`landscape` can specify that some of the nodes are not updated, say nodes ``7`` and ``8``. .. doctest:: landscape >>> s_pombe.landscape(pin=[7,8]).in_degrees array([36, 0, 6, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 42, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) >>> s_pombe.clear_landscape() :return: a :class:`numpy.ndarray` of the in-degree of each state """ if not self.__landscaped: self.landscape() if not self.__expounded: self.expound() return self.__landscape_data.in_degrees
[docs] def landscape_graph(self, **kwargs): """ Construct a :class:`networkx.DiGraph` of the state transitions. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.landscape_graph() <networkx.classes.digraph.DiGraph object at 0x...> :param kwargs: kwargs to pass to :class:`networkx.DiGraph` :return: a :class:`networkx.DiGraph` representing the state transition graph of the landscape """ if not self.__landscaped: self.landscape() if self.__landscape_graph is None: self.__landscape_graph = nx.DiGraph( list(enumerate(self.__landscape_data.transitions)), **kwargs) elif (len(kwargs) != 0): self.__landscape_graph.graph.update(kwargs) return self.__landscape_graph
[docs] def draw_landscape_graph(self, graphkwargs={}, pygraphkwargs={}): """ Draw the state transition graph. This method requires the optional dependency `pygraphviz <https://pygraphviz.github.io>`_, which can be installed via ``pip``. Be aware that ``pygraphviz`` requires native binaries of `Graphviz <https://graphviz.org>`_ which **cannot** be installed via pip. If :meth:`landscape` has not been called, this method will implicitly call it. .. rubric:: Basic Usage :: >>> s_pombe.draw_landscape_graph() :param graphkwargs: kwargs to pass to `landscape_graph` :param pygraphkwargs: kwargs to pass to `view_pygraphviz` """ from .draw import view_pygraphviz default_args = {'prog': 'dot'} graph = self.landscape_graph(**graphkwargs) view_pygraphviz(graph, **dict(default_args, **pygraphkwargs))
[docs] def trajectory(self, init, timesteps=None, encode=None): """ Compute the trajectory from a given state. This method computes a trajectory from ``init`` to the last before the trajectory begins to repeat. If ``timesteps`` is provided, then the trajectory will have a length of ``timesteps + 1`` regardless of repeated states. The ``encode`` argument forces the states in the trajectory to be either encoded or not. When ``encode is None``, whether or not the states of the trajectory are encoded is determined by whether or not the initial state (``init``) is provided in encoded form. Note that when ``timesteps is None``, the length of the resulting trajectory should be one greater than the recurrence time of the state. If :meth:`landscape` has not been called, this method will implicitly call it. Otherwise, it respects any settings provided by such a call. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.trajectory([1,0,0,1,0,1,1,0,1]) [[1, 0, 0, 1, 0, 1, 1, 0, 1], ... [0, 0, 1, 1, 0, 0, 1, 0, 0]] >>> s_pombe.trajectory([1,0,0,1,0,1,1,0,1], encode=True) [361, 80, 320, 78, 128, 162, 178, 400, 332, 76] >>> s_pombe.trajectory(361) [361, 80, 320, 78, 128, 162, 178, 400, 332, 76] >>> s_pombe.trajectory(361, encode=False) [[1, 0, 0, 1, 0, 1, 1, 0, 1], ... [0, 0, 1, 1, 0, 0, 1, 0, 0]] >>> s_pombe.trajectory(361, timesteps=5) [361, 80, 320, 78, 128, 162] >>> s_pombe.trajectory(361, timesteps=10) [361, 80, 320, 78, 128, 162, 178, 400, 332, 76, 76] :param init: the initial state :type init: int or seq :param timesteps: the number of time steps to include in the trajectory :type timesteps: int or None :param encode: whether to encode the states in the trajectory :type encode: bool or None :return: a list whose elements are subsequent states of the trajectory :raises ValueError: if ``init`` an empty array :raises ValueError: if ``timesteps`` is less than :math:`1` """ if not self.__landscaped: self.landscape() decoded = isinstance(init, list) or isinstance(init, np.ndarray) if decoded: if init == []: raise ValueError("initial state cannot be empty") elif encode is None: encode = False init = self.encode(init) elif encode is None: encode = True trans = self.__landscape_data.transitions if timesteps is not None: if timesteps < 1: raise ValueError("number of steps must be positive, non-zero") path = [init] * (timesteps + 1) for i in range(1, len(path)): path[i] = trans[path[i - 1]] else: path = [init] state = trans[init] while state not in path: path.append(state) state = trans[state] if not encode: decode = self.decode path = [decode(state) for state in path] return path
[docs] def timeseries(self, timesteps): """ Compute a time series from all states. This method computes a 3-dimensional array elements are the states of each node in the network. The dimensions of the array are indexed by, in order, the node, the initial state and the time step. If :meth:`landscape` has not been called, this method will implicitly call it. Otherwise, it respects any settings provided by such a call. .. rubric:: Basic Usage .. doctest:: landscape >>> s_pombe.timeseries(5) array([[[0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], ..., [1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0]], <BLANKLINE> [[0, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 0], [1, 1, 1, 1, 0, 0], ..., [0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0]], <BLANKLINE> ... <BLANKLINE> [[0, 0, 1, 1, 1, 1], [0, 0, 1, 1, 1, 1], [0, 1, 1, 1, 1, 0], ..., [1, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0]], <BLANKLINE> [[0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 1], ..., [1, 1, 1, 0, 0, 0], [1, 1, 1, 0, 0, 0], [1, 1, 1, 0, 0, 0]]]) :param timesteps: the number of timesteps to evolve the system :type timesteps: int :return: a 3-D array of node states :raises ValueError: if ``timesteps`` is less than :math:`1` """ if not self.__landscaped: self.landscape() if timesteps < 1: raise ValueError("number of steps must be positive, non-zero") trans = self.__landscape_data.transitions decode = self.decode decoded_trans = [decode(state) for state in trans] shape = (self.size, self.volume, timesteps + 1) series = np.empty(shape, dtype=np.int) for index, init in enumerate(self): k = index series[:, index, 0] = init[:] for time in range(1, timesteps + 1): series[:, index, time] = decoded_trans[k][:] k = trans[k] return series
[docs] def expound(self): """ Compute all cached data. This function performs the bulk of the calculations that the LandscapeMixin is concerned with. Most of the properties in this class are computed by this function whenever *any one* of them is requested and the results are cached. The advantage of this is that it saves computation time; why traverse the state space for every property call when you can do it all at once? The downside is that the cached results may use a good bit more memory. This is a trade-off that we are willing to make for now. The properties that are computed by this function include: .. autosummary:: :nosignatures: attractors attractor_lengths basins basin_sizes basin_entropy heights recurrence_times in_degrees """ if not self.__landscaped: self.landscape() # Get the state transitions trans = self.__landscape_data.transitions # Create an array to store whether a given state has visited visited = np.zeros(self.volume, dtype=np.bool) # Create an array to store which attractor basin each state is in basins = np.full(self.volume, -1, dtype=np.int) # Create an array to store the in-degree of each state in_degrees = np.zeros(self.volume, dtype=np.int) # Create an array to store the height of each state heights = np.zeros(self.volume, dtype=np.int) # Create an array to store the recurrence time of each state recurrence_times = np.zeros(self.volume, dtype=np.int) # Create a counter to keep track of how many basins have been visited basin_number = 0 # Create a list of basin sizes basin_sizes = [] # Create a list of attractor cycles attractors = [] # Create a list of attractor lengths attractor_lengths = [] # Start at state 0 initial_state = 0 # While the initial state is a state of the system while initial_state < len(trans): # Create a stack to store the state so far visited state_stack = [] # Create a array to store the states in the attractor cycle cycle = [] # Create a flag to signify whether the current state is part of # the cycle in_cycle = False # Set the current state to the initial state state = initial_state # Store the next state and terminus variables to the next state terminus = next_state = trans[state] # Set the visited flag of the current state visited[state] = True # Increment in-degree in_degrees[next_state] += 1 # While the next state hasn't been visited while not visited[next_state]: # Push the current state onto the stack state_stack.append(state) # Set the current state to the next state state = next_state # Update the terminus and next_state variables terminus = next_state = trans[state] # Update the visited flag for the current state visited[state] = True # Increment in-degree in_degrees[next_state] += 1 # If the next state hasn't been assigned a basin yet if basins[next_state] == -1: # Set the current basin to the basin number basin = basin_number # Increment the basin number basin_number += 1 # Add a new basin size basin_sizes.append(0) # Add a new attractor length attractor_lengths.append(1) # Add the current state to the attractor cycle cycle.append(state) # Set the current state's recurrence time recurrence_times[state] = 0 # We're still in the cycle until the current state is equal to # the terminus in_cycle = (terminus != state) else: # Set the current basin to the basin of next_state basin = basins[next_state] # Set the state's height to one greater than the next state's heights[state] = heights[next_state] + 1 # Set the state's recurrence time to one greater than the next # state's recurrence_times[state] = recurrence_times[next_state] + 1 # Set the basin of the current state basins[state] = basin # Increment the basin size basin_sizes[basin] += 1 # While we still have states on the stack while len(state_stack) != 0: # Save the current state as the next state next_state = state # Pop the current state off of the top of the stack state = state_stack.pop() # Set the basin of the current state basins[state] = basin # Increment the basin_size basin_sizes[basin] += 1 # If we're still in the cycle if in_cycle: # Add the current state to the attractor cycle cycle.append(state) # Increment the current attractor length attractor_lengths[basin] += 1 # We're still in the cycle until the current state is # equal to the terminus in_cycle = (terminus != state) # Set the cycle state's recurrence times if not in_cycle: for cycle_state in cycle: rec_time = attractor_lengths[basin] - 1 recurrence_times[cycle_state] = rec_time else: # Set the state's height to one create than the next # state's heights[state] = heights[next_state] + 1 # Set the state's recurrence time to one greater than the # next state's recurrence_times[state] = recurrence_times[next_state] + 1 # Find the next unvisited initial state while initial_state < len(visited) and visited[initial_state]: initial_state += 1 # If the cycle isn't empty, append it to the attractors list if len(cycle) != 0: attractors.append(np.asarray(cycle, dtype=np.int)) data = self.__landscape_data data.basins = basins data.basin_sizes = np.asarray(basin_sizes) data.attractors = np.asarray(attractors) data.attractor_lengths = np.asarray(attractor_lengths) data.in_degrees = in_degrees data.heights = heights data.recurrence_times = np.asarray(recurrence_times) dist = pi.Dist(self.__landscape_data.basin_sizes) data.basin_entropy = pi.shannon.entropy(dist, b=2) self.__expounded = True return self