# 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.