# State Spaces¶

Network derives from StateSpace which endows it with structural information about the state space of the network, and provides a number of vital methods.

## Attributes¶

First and foremost, StateSpace provides (readonly) attributes for assessing gross properties of the state space, namely StateSpace.size, StateSpace.shape and StateSpace.volume.

>>> s_pombe.size  # number of dimension (nodes)
9
>>> s_pombe.shape  # the number of states by dimension (states per node)
[2, 2, 2, 2, 2, 2, 2, 2, 2]
>>> s_pombe.volume  # total number of states of the network
512


## States in the Space¶

As a StateSpace, you can determining whether or not an array represents a valid state of the network. This is accomplished using the in keyword.

>>> 0 in s_pombe
False
>>> [0]*9 in s_pombe
True
>>> numpy.zeros(9, dtype=int) in s_pombe
True
>>> [2, 0, 0, 0, 0, 0, 0, 0, 0] in s_pombe  # the nodes are binary
False


Of course, after asking whether a state is valid, the next thing you might want to do is iterate over the states.

>>> for state in s_pombe:
...     print(state)
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[1, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 1, 0, 0, 0, 0, 0, 0, 0]
[1, 1, 0, 0, 0, 0, 0, 0, 0]
...
[0, 1, 1, 1, 1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1, 1, 1, 1]


Since the networks are iterable, you can treat them like any other kind of sequence.

>>> list(s_pombe)
[[0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0], ...]
>>> list(map(lambda s: s[0], s_pombe))
[0, 1, 0, 1, ...]
>>> list(filter(lambda s: s[0] ^ s[1] == 1, s_pombe))
[[1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0, 0], ...]


## State Encoding and Decoding¶

For particularly large networks, storing a list of states it’s states can use a lot of memory. What’s more, it is often useful to be able to index an array or key a dictionary based by a state of the network, e.g. when efficiently computing the attractors of the network. A simple solution to this problem is to encode the state as an integer. StateSpace provides this functionality via the StateSpace.encode() and StateSpace.decode() methods.

Encoding States

>>> s_pombe.encode([0, 1, 0, 1, 0, 1, 0, 1, 0])
170
>>> s_pombe.encode(numpy.ones(9)) == s_pombe.volume - 1
True
>>> s_pombe.encode('apples')
Traceback (most recent call last):
...
ValueError: state is not in state space


Decoding States

>>> s_pombe.decode(170)
[0, 1, 0, 1, 0, 1, 0, 1, 0]
>>> s_pombe.decode(511)
[1, 1, 1, 1, 1, 1, 1, 1, 1]
>>> s_pombe.decode(512)
[0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> s_pombe.decode(-1)
[1, 1, 1, 1, 1, 1, 1, 1, 1]


Notice that decoding states does not raise an error when the state encoding is invalid. Instead, the codes wrap around so that any integer can be decoded. This was a decision made more for the sake of performance than anything. Just be mindful of it.

By and large, the StateSpace.encode() and StateSpace.decode() methods are inverses:

>>> s_pombe.encode(s_pombe.decode(170))
170
>>> s_pombe.decode(s_pombe.encode([0, 0, 1, 0, 0, 1, 0, 0, 1]))
[0, 0, 1, 0, 0, 1, 0, 0, 1]


## Encoding Scheme¶

There are a number of ways of encoding a sequence of integers as an integer. We’ve chosen the one we did so that the encoded value of the state is consistent with the order the states are produced upon iteration.

>>> states = list(s_pombe)
>>> states[5] == s_pombe.decode(5)
True
>>> numpy.all([i == s_pombe.encode(s) for i, s in enumerate(s_pombe)])
True
>>> numpy.all([s_pombe.decode(i) == s for i, s in enumerate(s_pombe)])
True


This makes implementing the algorithms associated with landscape dynamics and sensitivity analyses much simpler and as light on memory as possible.