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
Substrate3 units · A, B, C
UnitsA, B, C
State spacebinary
Connectivity
ABC
A111
B111
C111
TPM
stateABC
(0,0,0)0.10.10.1
(1,0,0)0.10.90.1
(0,1,0)0.90.10.9
(1,1,0)0.90.90.1
(0,0,1)0.10.10.9
(1,0,1)0.90.10.9
(0,1,1)0.90.90.9
(1,1,1)0.10.90.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
Substrate4 units · A, B, C, D
UnitsA, B, C, D
State spacebinary
Connectivity
ABCD
A1111
B1111
C1111
D1111
TPM
stateABCD
(0,0,0,0)0.04742590.04742590.04742590.0474259
(1,0,0,0)0.141851 0.354344 0.08317270.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.08317270.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.08317270.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
Substrate3 units · A, B, C
UnitsA, B, C
State spacebinary
Connectivity
ABC
A·11
B1·1
C11·
TPM
stateABC
(0,0,0)0.00.00.0
(1,0,0)0.00.01.0
(0,1,0)1.00.01.0
(1,1,0)1.00.00.0
(0,0,1)1.00.00.0
(1,0,1)1.01.01.0
(0,1,1)1.00.01.0
(1,1,1)1.01.00.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()
Substrate3 units · A, B, C
UnitsA, B, C
State spacebinary
Connectivity
ABC
A111
B111
C111
TPM
stateABC
(0,0,0)0.05357140.08928570.0178571
(1,0,0)0.125 0.875 0.075
(0,1,0)0.934783 0.06521740.934783
(1,1,0)0.919355 0.887097 0.0806452
(0,0,1)0.166667 0.07142860.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#

  • substrate prints 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#