Build a substrate#
A Substrate is a set of units and the probability
of each unit’s next state given the current state of all of them. This page
shows the four ways to make one: from a transition probability matrix you
already have, from a weight matrix with logistic units, from a function per
unit, and from recorded transitions. It ends with how to check the result
before analyzing it.
import numpy as np
import pyphi
pyphi.config.progress_bars = False
From a transition probability matrix#
The usual input is state-by-node form: one row per current state, one
column per unit, each entry the probability that the unit is ON at the next
step. Rows are ordered so that the first unit changes fastest (PyPhi’s
little-endian convention): for three units the rows are the states
(0,0,0), (1,0,0), (0,1,0), (1,1,0), (0,0,1), (1,0,1), (0,1,1), (1,1,1).
tpm = np.array([
[0.1, 0.1, 0.1], # current state (0, 0, 0)
[0.1, 0.9, 0.1], # (1, 0, 0)
[0.9, 0.1, 0.9], # (0, 1, 0)
[0.9, 0.9, 0.1], # (1, 1, 0)
[0.1, 0.1, 0.9], # (0, 0, 1)
[0.9, 0.1, 0.9], # (1, 0, 1)
[0.9, 0.9, 0.9], # (0, 1, 1)
[0.1, 0.9, 0.1], # (1, 1, 1)
])
substrate = pyphi.Substrate(tpm, node_labels=("A", "B", "C"))
substrate
| Units | A, B, C |
|---|---|
| State space | binary |
| A | B | C | |
|---|---|---|---|
| A | 1 | 1 | 1 |
| B | 1 | 1 | 1 |
| C | 1 | 1 | 1 |
| state | A | B | C |
|---|---|---|---|
| (0,0,0) | 0.1 | 0.1 | 0.1 |
| (1,0,0) | 0.1 | 0.9 | 0.1 |
| (0,1,0) | 0.9 | 0.1 | 0.9 |
| (1,1,0) | 0.9 | 0.9 | 0.1 |
| (0,0,1) | 0.1 | 0.1 | 0.9 |
| (1,0,1) | 0.9 | 0.1 | 0.9 |
| (0,1,1) | 0.9 | 0.9 | 0.9 |
| (1,1,1) | 0.1 | 0.9 | 0.1 |
A state-by-state matrix (one row and one column per state) is accepted too and converted; the TPM conventions page describes every accepted form and the row order in full.
Pass a connectivity matrix (cm=; cm[i, j] = 1 when unit i is an input
to unit j) only when you know the wiring. Without one PyPhi assumes every
unit may influence every other, which is always correct and only slower. A connectivity matrix that omits a real input changes the result.
Check one transition you know before trusting anything computed from the matrix. The card above prints the matrix with its row states, and the matrix’s table view can be indexed by state:
substrate.tpm.to_pandas().loc[(1, 0, 0)] # the row for current state (1, 0, 0)
A 0.1
B 0.9
C 0.1
Name: (1, 0, 0), dtype: float64
From a weight matrix and logistic units#
The networks of the IIT 4.0 papers are logistic (sigmoid) units of their
weighted inputs. Give build_substrate() a
weight matrix, where w[i, j] is the weight from unit i to unit j, and
the unit function by name. determinism is the slope k of the logistic
function in Albantakis et al. (2023, Eq. 60) and Marshall et al. (2023,
Eq. 2); inputs enter as ±1.
from pyphi.substrate_generator import build_substrate
w = np.array([
[0.2, 0.4, 0.1, 0.3],
[0.3, 0.2, 0.4, 0.1],
[0.1, 0.3, 0.2, 0.4],
[0.4, 0.1, 0.3, 0.2],
])
logistic = build_substrate("sigmoid", w, determinism=3.0, node_labels=("A", "B", "C", "D"))
logistic
| Units | A, B, C, D |
|---|---|
| State space | binary |
| A | B | C | D | |
|---|---|---|---|---|
| A | 1 | 1 | 1 | 1 |
| B | 1 | 1 | 1 | 1 |
| C | 1 | 1 | 1 | 1 |
| D | 1 | 1 | 1 | 1 |
| state | A | B | C | D |
|---|---|---|---|---|
| (0,0,0,0) | 0.0474259 | 0.0474259 | 0.0474259 | 0.0474259 |
| (1,0,0,0) | 0.141851 | 0.354344 | 0.0831727 | 0.231475 |
| (0,1,0,0) | 0.231475 | 0.141851 | 0.354344 | 0.0831727 |
| (1,1,0,0) | 0.5 | 0.645656 | 0.5 | 0.354344 |
| (0,0,1,0) | 0.0831727 | 0.231475 | 0.141851 | 0.354344 |
| (1,0,1,0) | 0.231475 | 0.768525 | 0.231475 | 0.768525 |
| (0,1,1,0) | 0.354344 | 0.5 | 0.645656 | 0.5 |
| (1,1,1,0) | 0.645656 | 0.916827 | 0.768525 | 0.858149 |
| (0,0,0,1) | 0.354344 | 0.0831727 | 0.231475 | 0.141851 |
| (1,0,0,1) | 0.645656 | 0.5 | 0.354344 | 0.5 |
| (0,1,0,1) | 0.768525 | 0.231475 | 0.768525 | 0.231475 |
| (1,1,0,1) | 0.916827 | 0.768525 | 0.858149 | 0.645656 |
| (0,0,1,1) | 0.5 | 0.354344 | 0.5 | 0.645656 |
| (1,0,1,1) | 0.768525 | 0.858149 | 0.645656 | 0.916827 |
| (0,1,1,1) | 0.858149 | 0.645656 | 0.916827 | 0.768525 |
| (1,1,1,1) | 0.952574 | 0.952574 | 0.952574 | 0.952574 |
The connectivity matrix is read from the nonzero weights. This is the
construction behind every iit4_2023_* and marshall_2023_* example in
pyphi.examples; open one of them to see the weights of a published
network.
From a function per unit#
Logic gates and other named mechanisms go through
create_substrate(), one specification per
unit: the mechanism’s name, its inputs, and any parameters.
from pyphi.substrate_generator import create_substrate
gates = create_substrate(
[
{"mechanism": "or", "inputs": (1, 2)},
{"mechanism": "and", "inputs": (0, 2)},
{"mechanism": "xor", "inputs": (0, 1)},
],
labels=("A", "B", "C"),
)
gates
| Units | A, B, C |
|---|---|
| State space | binary |
| A | B | C | |
|---|---|---|---|
| A | · | 1 | 1 |
| B | 1 | · | 1 |
| C | 1 | 1 | · |
| state | A | B | C |
|---|---|---|---|
| (0,0,0) | 0.0 | 0.0 | 0.0 |
| (1,0,0) | 0.0 | 0.0 | 1.0 |
| (0,1,0) | 1.0 | 0.0 | 1.0 |
| (1,1,0) | 1.0 | 0.0 | 0.0 |
| (0,0,1) | 1.0 | 0.0 | 0.0 |
| (1,0,1) | 1.0 | 1.0 | 1.0 |
| (0,1,1) | 1.0 | 0.0 | 1.0 |
| (1,1,1) | 1.0 | 1.0 | 0.0 |
The mechanism names are the keys of pyphi.substrate_generator.MECHANISMS.
A unit you write yourself is a function f(element, weights, state, **params)
returning the probability that element is ON at the next step; pass it (or
a list mixing functions and names, one per unit) to
build_substrate() with a weight matrix.
From recorded transitions#
pyphi.estimate_substrate() fits a posterior over transition
probabilities to observed (current, next) state pairs. IIT’s analysis is
defined on an interventional matrix, what each unit does when the system
is put into each state, so say which regime produced the data:
"perturbational" when states were set, "observational" when they were
only recorded.
rng = np.random.default_rng(0)
current = rng.integers(0, 2, size=(200, 3))
# The row of ``tpm`` for each sampled state: little-endian, so the first
# unit is the least significant bit.
rows = np.ravel_multi_index(current.T[::-1], (2, 2, 2))
next_state = (rng.random((200, 3)) < tpm[rows]).astype(int)
posterior = pyphi.estimate_substrate(
(current, next_state), regime="perturbational", node_labels=("A", "B", "C")
)
posterior.mean_substrate()
| Units | A, B, C |
|---|---|
| State space | binary |
| A | B | C | |
|---|---|---|---|
| A | 1 | 1 | 1 |
| B | 1 | 1 | 1 |
| C | 1 | 1 | 1 |
| state | A | B | C |
|---|---|---|---|
| (0,0,0) | 0.0535714 | 0.0892857 | 0.0178571 |
| (1,0,0) | 0.125 | 0.875 | 0.075 |
| (0,1,0) | 0.934783 | 0.0652174 | 0.934783 |
| (1,1,0) | 0.919355 | 0.887097 | 0.0806452 |
| (0,0,1) | 0.166667 | 0.0714286 | 0.880952 |
| (1,0,1) | 0.9 | 0.18 | 0.86 |
| (0,1,1) | 0.886364 | 0.886364 | 0.931818 |
| (1,1,1) | 0.118421 | 0.934211 | 0.0657895 |
The posterior’s mean substrate is a reference point, not an estimate of φ: analyzing it mixes what is unknown about the matrix with the substrate’s own indeterminism. Sample substrates from the posterior to carry that uncertainty through an analysis; see the what’s-new tour.
Units with more than two states#
The state-by-node and state-by-state inputs are for binary units. A unit
with more states is given in factored form: marginals=, one array per
unit of shape (*alphabet_sizes, k_i) holding the unit’s next-state
distribution for every current state, together with alphabet= (one size
for every unit) or state_space= (a tuple of state labels per unit). The
TPM conventions page has a worked example.
Check the substrate before analyzing it#
substrateprints the units, the connectivity, and the matrix with its row states; a wrong row order shows up here as a transition you did not intend.PyPhi rejects a matrix whose units are not conditionally independent given the previous state (a hidden common cause); see Conditional independence.
A state the substrate cannot reach is refused at analysis time (FAQ).
Before a large run, count the work: Estimate the cost before you run.
Where to go next#
Getting started analyzes a substrate end to end.
Read a result explains what comes back.
Substrate and system gives the theory behind the matrix.