Source code for neet.boolean.reca

"""
.. currentmodule:: neet.boolean

.. testsetup:: reca

    from neet.boolean import RewiredECA
"""
import numpy as np
from .network import BooleanNetwork


[docs]class RewiredECA(BooleanNetwork): """ RewiredECA represents elementary cellular automaton rule with a rewired topology. That is, RewiredECA is a variant of an :class:`neet.boolean.ECA` wherein the neighbors of a given cell can be specified by the user. This allows one to study, for example, the role of topology in the dynamics of a network. Every :class:`neet.boolean.ECA` can be represented as a RewiredECA with standard wiring, but all RewiredECA are *fixed sized* networks. For this reason, RewiredECA **does not** derive from :class:`neet.boolean.ECA`. .. inheritance-diagram:: RewiredECA :parts: 1 RewiredECA instances can be instantiated by providing an ECA rule ``code``, and either the number of nodes in the network (``size``) or a ``wiring`` matrix which specifies how the nodes are wired. Optionally, the user can specify boundary conditions as in :class:`neet.boolean.ECA`. As with all :class:`neet.Network` classes, the names of the nodes and network-wide metadata can be provided. In addition to all inherited methods, RewiredECA exposes the following properites .. autosummary:: :nosignatures: code boundary wiring .. rubric:: Examples If ``wiring`` is not provided, the network is wired as a standard :class:`neet.boolean.ECA`. .. doctest:: reca >>> reca = RewiredECA(30, size=5) >>> reca.code 30 >>> reca.size 5 >>> reca.wiring array([[-1, 0, 1, 2, 3], [ 0, 1, 2, 3, 4], [ 1, 2, 3, 4, 5]]) Wiring matrices are :math:`3 \times N` matrices where each column is a node of the network, and the rows represent the left-, middle- and right-input for the nodes. The number of nodes will be inferred from the width of the matrix. For example: .. doctest:: reca >>> reca = RewiredECA(30, wiring=[[0,1,2],[-1,0,0],[2,3,1]]) >>> reca.code 30 >>> reca.size 3 >>> reca.wiring array([[ 0, 1, 2], [-1, 0, 0], [ 2, 3, 1]]) Here the :math:`0`th node takes input from nodes :math:`0`, :math:`-1` and :math:`2` as left, middle and right input. Note that ``-1`` represents the left-boundary condition of the RewiredECA. If instance has periodic boundary conditions then ``-1`` is effectively ``N-1``. Similarly ``N`` is the right boundary condition. To see how the wiring affects the result: .. doctest:: reca >>> ca = RewiredECA(30, size=3) >>> ca.update([0, 1, 0]) [1, 1, 1] >>> ca = RewiredECA(30, wiring=[[0,1,3], [1,1,1], [2,1,2]]) >>> ca.update([0, 1, 0]) [1, 0, 1] :param code: the 8-bit Wolfram code for the rule :type code: int :param boundary: the boundary conditions for the CA :type boundary: tuple, None :param size: the number of cells in the lattice :type size: int or None :param wiring: a wiring matrix :type wiring: list, numpy.ndarray :param names: an iterable object of the names of the nodes in the network :type names: seq :param metadata: metadata dictionary for the network :type metadata: dict :raises ValueError: if both ``size`` and ``wiring`` are provided :raises ValueError: if neither ``size`` nor ``wiring`` are provided :raises ValueError: if ``size`` is less than :math:`1` (when provided) :raises ValueError: if ``wiring`` is not a :math:`3 \\times N` matrix (when provided) :raises ValueError: if any element of ``wiring`` is outside the range :math:`[-1, ``size``]` (when provided) """ def __init__(self, code, boundary=None, size=None, wiring=None, names=None, metadata=None): if size is not None and wiring is not None: raise ValueError("cannot provide size and wiring at the same time") elif size is not None: super(RewiredECA, self).__init__(size, names=names, metadata=metadata) self.code = code self.boundary = boundary self.__wiring = np.zeros((3, size), dtype=int) self.__wiring[0, :] = range(-1, size - 1) self.__wiring[1, :] = range(0, size) self.__wiring[2, :] = range(1, size + 1) elif wiring is not None: if not isinstance(wiring, (list, np.ndarray)): raise TypeError("wiring must be a list or an array") wiring_array = np.copy(wiring) shape = wiring_array.shape if wiring_array.ndim != 2: raise ValueError("wiring must be a matrix") elif shape[0] != 3: raise ValueError("wiring must have 3 rows") elif np.any(wiring_array < -1): raise ValueError("invalid input node in wiring") elif np.any(wiring_array > shape[1]): raise ValueError("invalid input node in wiring") super(RewiredECA, self).__init__(int(shape[1]), names=names, metadata=metadata) self.code = code self.boundary = boundary self.__wiring = wiring_array else: raise ValueError("either size or wiring must be provided") @property def code(self): """ The Wolfram code of the elementary cellular automaton .. rubric:: Examples .. doctest:: reca >>> reca = RewiredECA(30, size=55) >>> reca.code 30 >>> reca.code = 45 >>> reca.code 45 >>> reca.code = 256 Traceback (most recent call last): ... ValueError: invalid ECA code :type: int :raises ValueError: if code is not in :math:`\\{0,1,\\ldots,255\\}` """ return self.__code @code.setter def code(self, code): if not isinstance(code, int): raise TypeError("ECA code is not an int") if 255 < code or code < 0: raise ValueError("invalid ECA code") self.__code = code self.clear_landscape() @property def boundary(self): """ The boundary conditions of the elemenary cellular automaton .. rubric:: Examples .. doctest:: reca >>> reca = RewiredECA(30, size=5) >>> reca.boundary >>> reca.boundary = (0,1) >>> reca.boundary (0, 1) >>> reca.boundary = None >>> reca.boundary >>> reca.boundary = [0,1] Traceback (most recent call last): ... TypeError: ECA boundary are neither None nor a tuple :type: tuple, None :raises ValueError: if boundary is neither None nor a pair of binary states """ return self.__boundary @boundary.setter def boundary(self, boundary): if boundary and not isinstance(boundary, tuple): raise TypeError("ECA boundary are neither None nor a tuple") if boundary: if len(boundary) != 2: raise ValueError("invalid ECA boundary conditions") for x in boundary: if x != 0 and x != 1: raise ValueError("invalid ECA boundary value") self.__boundary = boundary self.clear_landscape() @property def wiring(self): """ The wiring matrix for the rule. .. rubric:: Examples .. doctest:: reca >>> reca = RewiredECA(30, size=4) >>> reca.wiring array([[-1, 0, 1, 2], [ 0, 1, 2, 3], [ 1, 2, 3, 4]]) >>> eca = RewiredECA(30, wiring=[[0,1],[1,1],[-1,-1]]) >>> eca.wiring array([[ 0, 1], [ 1, 1], [-1, -1]]) :type: numpy.ndarray """ return self.__wiring def _unsafe_update(self, lattice, index=None, pin=None, values=None): pin_states = pin is not None and pin != [] if self.boundary: left = self.boundary[0] right = self.boundary[1] else: left = lattice[-1] right = lattice[0] code = self.code wiring = self.wiring size = len(lattice) if index is None: if pin_states: pinned = np.asarray(lattice)[pin] temp = np.copy(lattice) for j in range(size): shift = 0 for i in range(3): k = wiring[i, j] if k == -1: shift = 2 * shift + left elif k == size: shift = 2 * shift + right else: shift = 2 * shift + lattice[k] temp[j] = 1 & (code >> (7 & shift)) lattice[:] = temp[:] if pin_states: for j, i in enumerate(pin): lattice[i] = pinned[j] else: if index < 0: index += len(lattice) shift = 0 for i in range(3): k = wiring[i, index] if k == -1: shift = 2 * shift + left elif k == size: shift = 2 * shift + right else: shift = 2 * shift + lattice[k] lattice[index] = 1 & (code >> (7 & shift)) if values is not None: for key in values: lattice[key] = values[key] return lattice def neighbors_in(self, index, *args, **kwargs): if not isinstance(index, int): raise TypeError("index must be a non-negative integer") if index < 0 or index > self.size - 1: return set() else: return set(self.wiring[:, index]) def neighbors_out(self, index, *args, **kwargs): if not isinstance(index, int): raise TypeError("index must be a non-negative integer") neighbors = set() for j in range(self.size): for i in range(3): if self.wiring[i, j] == index: neighbors.add(j) return neighbors def network_graph(self, *args, **kwargs): kwargs['code'] = self.code kwargs['boundary'] = self.boundary g = super(RewiredECA, self).network_graph(*args, **kwargs) if 'labels' not in kwargs: kwargs['labels'] = 'indices' if kwargs['labels'] == 'indices': g.add_edges_from(map(lambda n: (-1, n), self.neighbors_out(-1))) g.add_edges_from(map(lambda n: (5, n), self.neighbors_out(5))) elif kwargs['labels'] == 'names': names = self.names g.add_edges_from(map(lambda n: ('left', names[n]), self.neighbors_out(-1))) g.add_edges_from(map(lambda n: ('right', names[n]), self.neighbors_out(5))) return g
BooleanNetwork.register(RewiredECA)