"""
Rewired Elementary Cellular Automata
====================================
The :class:`neet.automata.reca.RewiredECA` implements a variant of an ECA
wherein the neighbors of a give 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 ``ECA`` can be represented as a ``RewiredECA`` with standard
wiring, but all ``RewiredECA`` are *fixed sized* networks.
.. rubric:: Examples
.. doctest:: automata
>>> 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]
"""
import numpy as np
from neet.statespace import StateSpace
from . import eca
[docs]class RewiredECA(eca.ECA):
"""
RewiredECA is a class to represent elementary cellular automata rules with
arbitrarily defined topology. Since the topology must be provided,
RewiredECA are naturally fixed-sized.
"""
[docs] def __init__(self, code, boundary=None, size=None, wiring=None):
"""
Construct a rewired elementary cellular automaton rule.
.. rubric:: Examples
.. doctest:: automata
>>> reca = RewiredECA(30, size=3)
>>> reca.code
30
>>> reca.size
3
>>> reca.wiring
array([[-1, 0, 1],
[ 0, 1, 2],
[ 1, 2, 3]])
.. doctest:: automata
>>> 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]])
:param code: the 8-bit Wolfram code for the rule
:type code: int
:param boundary: the boundary conditions for the CA
:type boundary: tuple or None
:param size: the number of cells in the lattice
:type size: int or None
:param wiring: a wiring matrix
:raises ValueError: if ``size is None and wiring is None``
:raises ValueError: if ``size is not None and wiring is not None``
:raises TypeError: if ``size is not None and not
isinstance(size, int)``
:raises ValueError: if ``size is not None and size <= 0``
:raises TypeError: if ``not isinstance(wiring, list) and not
isinstance(wiring, numpy.ndarray)``
:raises ValueError: if ``wiring`` is not :math:`3 \times N`
:raises ValueError: if ``any(wiring < -1) or any(wiring > N)``
"""
super(RewiredECA, self).__init__(code, boundary=boundary)
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:
if not isinstance(size, int):
raise TypeError("size must be an int")
elif size <= 0:
raise ValueError("size must be positive, nonzero")
else:
self.__size = size
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")
self.__size = int(shape[1])
self.__wiring = wiring_array
else:
raise ValueError("either size or wiring must be provided")
@property
def wiring(self):
"""
The wiring matrix for the rule.
.. rubric:: Examples
.. doctest:: automata
>>> reca = RewiredECA(30, size=3)
>>> reca.wiring
array([[-1, 0, 1],
[ 0, 1, 2],
[ 1, 2, 3]])
>>> 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
@property
def size(self):
"""
The number of cells in the CA lattice.
.. rubric:: Examples
.. doctest:: automata
>>> eca = RewiredECA(30, size=3)
>>> eca.size
3
>>> eca = RewiredECA(30, wiring=[[-1,0], [0,1], [1,0]])
>>> eca.size
2
:type: int
"""
return self.__size
[docs] def state_space(self):
"""
Return a :class:`neet.statespace.StateSpace` object for the
cellular automaton lattice.
.. rubric:: Examples
.. doctest:: automata
>>> eca = RewiredECA(30, size=3)
>>> eca.state_space()
<neet.statespace.StateSpace object at 0x...>
>>> space = eca.state_space()
>>> list(space)
[[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]]
:returns: :class:`neet.statespace.StateSpace`
"""
return StateSpace(self.__size, base=2)
def _unsafe_update(self, lattice, index=None, pin=None, values=None):
"""
Update the state of the ``lattice``, in place, without
checking the validity of the arguments.
:param lattice: the one-dimensional sequence of states
:param index: the index to update (optional)
:param pin: a sequence of indices to fix (optional)
:param values: a dict of index/value pairs to set (optional)
:returns: the updated lattice
"""
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
[docs] def update(self, lattice, index=None, pin=None, values=None):
"""
Update the state of the ``lattice`` in place.
.. rubric:: Examples
.. doctest:: automata
>>> reca = RewiredECA(30, size=5)
>>> reca.update([1,0,0,0,0])
[1, 1, 0, 0, 1]
>>> reca.wiring[:,:] = [[-1, 2, 1, 2, -1], [0, 1, 2, 3, 4], [0, 2, 3, 4, 5]]
>>> reca.update([0,0,1,0,0])
[0, 0, 1, 1, 0]
>>> reca.update([1,1,1,1,1])
[0, 0, 0, 0, 0]
>>> reca.update([1,1,1,1,1], index=2)
[1, 1, 0, 1, 1]
>>> reca.update([1,1,1,1,1], pin=[1, 3])
[0, 1, 0, 1, 0]
>>> reca.update([1,1,1,1,1], values={0: 1, -1: 1})
[1, 0, 0, 0, 1]
:param lattice: the one-dimensional sequence of states
:param index: the index to update (optional)
:param pin: a sequence of indices to fix (optional)
:param values: a dict of index/value pairs to set (optional)
:returns: the updated lattice
"""
size = len(lattice)
if lattice not in self.state_space():
msg = "the provided state is not in the RewiredECA's state space"
raise ValueError(msg)
if index is not None:
if index < -size:
raise IndexError("lattice index out of range")
elif pin is not None and pin != []:
msg = "cannot provide both the index and pin arguments"
raise ValueError(msg)
elif values is not None and values != {}:
msg = "cannot provide both the index and values arguments"
raise ValueError(msg)
elif pin is not None and values is not None:
for key in values.keys():
if key in pin:
raise ValueError("cannot set a value for a pinned state")
if values is not None:
for val in values.values():
if val != 0 and val != 1:
raise ValueError("invalid state in values argument")
return self._unsafe_update(lattice, index=index, pin=pin, values=values)