"""
.. currentmodule:: neet
.. testsetup:: statespace
from neet import StateSpace, UniformSpace
The :mod:`neet` module provides the following classes from which all Neet
network classes inherit:
.. autosummary::
:nosignatures:
StateSpace
UniformSpace
.. inheritance-diagram:: neet.StateSpace neet.UniformSpace
:parts: 1
This endows networks with methods for iterating over the states of the network,
determining if a state exists in the network, and the ability to encode and
decode states as integer values. In other words, these classes provide an
interface for accessing the *unstructured* set of states of the network, with
no dynamical information.
"""
from .python import long
[docs]class StateSpace(object):
"""
StateSpace represents a (potentially in-homogeneous) discrete state space.
It implements iteration, inclusion testing and methods for encoding and
decoding states as integers sutable for array indexing:
.. autosummary::
:nosignatures:
size
shape
volume
__iter__
__contains__
_unsafe_encode
encode
decode
StateSpace instances are created from a ``shape`` array of integer
representing the number of discrete states for each dimension of the state
space.
.. rubric:: Examples
.. doctest:: statespace
>>> StateSpace([2]) # 1-D state space
<neet.statespace.StateSpace object at 0x...>
>>> StateSpace([2,2]) # 2-D uniform state space
<neet.statespace.StateSpace object at 0x...>
>>> StateSpace([2,3,5]) # 3-D inhomogeneous space
<neet.statespace.StateSpace object at 0x...>
From the network perspective, each dimension of the state space corresponds
to a node of the network. The number of discrete states of that node is the
base of the corresponding dimension.
The algorithms implemented by this class are intended to be as generic as
possible. This comes at the cost of performance in some cases. This can be
dealt with by deriving and overloading the appropriate methods, in
particular :meth:`_unsafe_encode`. In fact, the following methods are
recommended for overloading:
* :meth:`__iter__`
* :meth:`__contains__`
* :meth:`_unsafe_encode`
* :meth:`decode`
The :meth:`encode` method uses :meth:`__contains__` and
:meth:`_unsafe_encode` internally and rarely needs to be overloaded.
:param shape: the base of each dimension of the state space
:type shape: list
:see: :class:`UniformSpace`
"""
def __init__(self, shape):
if isinstance(shape, list):
if len(shape) == 0:
raise ValueError("shape cannot be empty")
else:
self._volume = 1
for base in shape:
if not isinstance(base, int):
raise TypeError("shape must be a list of ints")
elif base < 1:
raise ValueError("shape may only contain positive elements")
self._volume *= base
self._size = len(shape)
self._shape = shape[:]
else:
raise TypeError("shape must be a list")
@property
def size(self):
"""
Get the size of the state space. That is the number of dimensions.
.. rubric:: Examples
.. doctest:: statespace
>>> StateSpace([2]).size
1
>>> StateSpace([2,3,4]).size
3
:returns: the number of dimensions of the state space
"""
return self._size
@property
def shape(self):
"""
Get the shape of the state space. That is the base of each dimension.
.. rubric:: Examples
.. doctest:: statespace
>>> StateSpace([2]).shape
[2]
>>> StateSpace([2,3,4]).shape
[2, 3, 4]
:returns: the shape of the state space
"""
return self._shape
@property
def volume(self):
"""
Get the volume of the state space. That is the number of states in the space.
.. rubric:: Examples
.. doctest:: statespace
>>> StateSpace([2]).volume
2
>>> StateSpace([2,3,4]).volume
24
:returns: the number of states in the space
"""
return self._volume
[docs] def __iter__(self):
"""
Iterate over the states of the state space.
.. rubric:: Examples
.. doctest:: statespace
>>> list(StateSpace([2]))
[[0], [1]]
>>> list(StateSpace([2,2]))
[[0, 0], [1, 0], [0, 1], [1, 1]]
>>> list(StateSpace([3,2]))
[[0, 0], [1, 0], [2, 0], [0, 1], [1, 1], [2, 1]]
"""
size, shape = self.size, self.shape
state = [0] * size
yield state[:]
i = 0
while i != size:
base = shape[i]
if state[i] + 1 < base:
state[i] += 1
for j in range(i):
state[j] = 0
i = 0
yield state[:]
else:
i += 1
[docs] def __contains__(self, states):
"""
Determine if a state is in the state space.
.. rubric:: Examples
.. doctest:: statespace
>>> space = StateSpace([2])
>>> [0] in space
True
>>> 0 in space
False
.. doctest:: statespace
>>> space = StateSpace([3,2])
>>> [2,0] in space
True
>>> [0,2] in space
False
>>> [2,0,0] in space
False
"""
try:
if len(states) != self.size:
return False
for state, base in zip(states, self.shape):
if state < 0 or state >= base:
return False
return True
except TypeError:
return False
[docs] def _unsafe_encode(self, state):
"""
Unsafely encode a state as an integer value.
.. rubric:: Examples
.. doctest:: statespace
>>> space = StateSpace([2,3])
>>> space._unsafe_encode([1,1])
3
The resulting numeric encodings must be consistent with the ordering of
the states produced by :meth:`__iter__`. This allows necessary for
memory-efficient implementations of many algorithms.
.. doctest:: statespace
>>> space = StateSpace([2,3])
>>> list(space)
[[0, 0], [1, 0], [0, 1], [1, 1], [0, 2], [1, 2]]
>>> list(map(space._unsafe_encode, space))
[0, 1, 2, 3, 4, 5]
.. Note::
This method is **not** safe. It does not ensure that ``state`` is
in fact in the space; if that's not the case then there are not
guaruntees on the output. As such it should only be used in
situations where the state is already known to be in the space,
e.g. it is a state that was generated by :meth:`__iter__`. This is
designed to allow algorithms to utilize state encoding without
incurring the cost of consistency checking.
:param state: the state as a list of coordinates
:type state: int
:returns: the state encoded as an integer
:see: :meth:`encode`, :meth:`decode`
"""
encoded, place = long(0), long(1)
for (x, b) in zip(state, self.shape):
encoded += place * long(x)
place *= b
return encoded
[docs] def encode(self, state):
"""
Encode a state as an integer.
.. rubric:: Examples
.. doctest:: statespace
>>> space = StateSpace([2,3])
>>> space.encode([1,1])
3
The resulting numeric encodings are consistent with the ordering of the
states produced by :meth:`__iter__`.
.. doctest:: statespace
>>> space = StateSpace([2,3])
>>> list(space)
[[0, 0], [1, 0], [0, 1], [1, 1], [0, 2], [1, 2]]
>>> list(map(space.encode, space))
[0, 1, 2, 3, 4, 5]
This method is the inverse of the :meth:`decode` method:
.. doctest:: statespace
>>> space = StateSpace([3,2])
>>> space.decode(space.encode([1,1]))
[1, 1]
>>> space.encode(space.decode(3))
3
:param state: the state as a list of coordinates
:type state: int
:returns: the state encoded as an integer
:see: :meth:`encode`, :meth:`decode`
"""
if state not in self:
raise ValueError("state is not in state space")
return self._unsafe_encode(state)
[docs] def decode(self, encoded):
"""
Decode an integer-encoded state into a coordinate list.
.. rubric:: Examples
.. doctest:: statespace
>>> space = StateSpace([2,3])
>>> space.decode(3)
[1, 1]
The resulting decoded states are consistent with the ordering of the
states produced by :meth:`__iter__`.
.. doctest:: statespace
>>> space = StateSpace([2,3])
>>> list(space)
[[0, 0], [1, 0], [0, 1], [1, 1], [0, 2], [1, 2]]
>>> list(map(space.decode, range(0,6)))
[[0, 0], [1, 0], [0, 1], [1, 1], [0, 2], [1, 2]]
This method is the inverse of the :meth:`encode` method:
.. doctest:: statespace
>>> space = StateSpace([3,2])
>>> space.decode(space.encode([1,1]))
[1, 1]
>>> space.encode(space.decode(3))
3
:param encoded: an integer-encoded state
:type encoded: int
:returns: the coordinate list of the decoded state
:see: :meth:`encode`, :meth:`decode`
"""
size = self.size
state = [0] * size
for (i, base) in enumerate(self.shape):
state[i] = encoded % base
encoded = int(encoded / base)
return state