Skip to content

Repertoires

iitx.repertoires

Repertoires: the distributions a mechanism specifies over a purview.

This is the shared kernel layer of the library (docs/design.md §6.1): everything both IIT versions compute is built from the repertoire algebra here, which in turn is nothing but mask-parameterized tensor algebra over the factored view of a system (:func:iitx.system.node_tpms).

Full-shape embedding. A repertoire over a purview Z is mathematically a distribution over the states of Z alone, but iitx always represents it as a full-shape tensor over the states of all units, with the units outside the purview carried as an exactly uniform, independent factor (docs/design.md §2, P3). Every repertoire therefore has the same shape regardless of its purview, which is what lets one compiled kernel vmap over tables of mechanism and purview masks. The embedding is lossless — the purview distribution is recovered by summing out the uniform axes — and it is transparent to the Hamming-metric EMD, which costs nothing for moving an identical uniform factor onto itself.

Semantics (the conventions of both theories, fixed by the papers and the oracle):

  • Cause (Bayes under a uniform interventional prior): each mechanism unit contributes the likelihood of its observed state as a function of the previous system state; each likelihood is uniformly marginalized over non-purview units before the product over mechanism units (the "virtual elements" / product-probability doctrine that severs common-input correlations); the product is then normalized.
  • Effect (forward perturbation): each purview unit contributes its next-value distribution given the mechanism clamped to its observed state and every other unit uniformly perturbed; the repertoire is the product over purview units.
  • The unconstrained repertoire is the same computation with an empty mechanism: uniform over the purview on the cause side, and the fully perturbed one-step distribution on the effect side (not uniform in general).

repertoire(node_tpms, state, mechanism, purview, direction)

Compute the repertoire a mechanism in a state specifies over a purview.

Implements Eqs. S2-S9 of Oizumi et al. (2014) Text S2 — equivalently the product probabilities of Eqs. 29-30 of Albantakis et al. (2023) — in the marginalize-then- multiply form both theories mandate. An empty mechanism yields the unconstrained repertoire.

Parameters:

Name Type Description Default
node_tpms tuple[Array, ...]

The factored view of the (already background-conditioned) system: one array of shape (*shape, q_i) per unit, p(u_i' = v | u).

required
state Array

Current state of the system, shape (n,).

required
mechanism Array

Mask of the mechanism units, shape (n,).

required
purview Array

Mask of the purview units, shape (n,).

required
direction Direction

CAUSE for the distribution over previous states, EFFECT for the distribution over next states. Static.

required

Returns:

Type Description
Array

Full-shape tensor of shape shape: the purview distribution with an exactly

Array

uniform factor on non-purview axes. Sums to 1, except for a cause repertoire whose

Array

mechanism state is unreachable within the purview, which is identically zero (the

Array

convention under which its φ is zero, matching PyPhi).

Source code in src/iitx/repertoires.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def repertoire(
	node_tpms: tuple[jax.Array, ...],
	state: jax.Array,
	mechanism: jax.Array,
	purview: jax.Array,
	direction: Direction,
) -> jax.Array:
	"""Compute the repertoire a mechanism in a state specifies over a purview.

	Implements Eqs. S2-S9 of Oizumi et al. (2014) Text S2 — equivalently the product
	probabilities of Eqs. 29-30 of Albantakis et al. (2023) — in the marginalize-then-
	multiply form both theories mandate. An empty ``mechanism`` yields the unconstrained
	repertoire.

	Args:
		node_tpms: The factored view of the (already background-conditioned) system: one
			array of shape ``(*shape, q_i)`` per unit, ``p(u_i' = v | u)``.
		state: Current state of the system, shape ``(n,)``.
		mechanism: Mask of the mechanism units, shape ``(n,)``.
		purview: Mask of the purview units, shape ``(n,)``.
		direction: ``CAUSE`` for the distribution over previous states, ``EFFECT`` for the
			distribution over next states. Static.

	Returns:
		Full-shape tensor of shape ``shape``: the purview distribution with an exactly
		uniform factor on non-purview axes. Sums to 1, except for a cause repertoire whose
		mechanism state is unreachable within the purview, which is identically zero (the
		convention under which its φ is zero, matching PyPhi).

	"""
	shape = tuple(node_tpm.shape[-1] for node_tpm in node_tpms)
	if direction is Direction.CAUSE:
		joint = jnp.ones(shape, dtype=node_tpms[0].dtype)
		for i, node_tpm in enumerate(node_tpms):
			# Likelihood of unit i's observed state as a function of the previous state,
			# uniformly marginalized over non-purview axes *before* the product (virtual
			# elements); units outside the mechanism contribute the identity.
			likelihood = jnp.take_along_axis(
				node_tpm, state[i].reshape((1,) * len(shape) + (1,)), axis=-1
			).squeeze(-1)
			joint = joint * jnp.where(mechanism[i], _smear(likelihood, purview), 1.0)
		return _normalize(joint * _uniform(shape, purview, joint.dtype))

	joint = jnp.ones(shape, dtype=node_tpms[0].dtype)
	for j, node_tpm in enumerate(node_tpms):
		# Next-value distribution of purview unit j with the mechanism clamped and every
		# other unit uniformly perturbed; non-purview units contribute uniform.
		conditioned = node_tpm
		for i in range(len(shape)):
			weights = jnp.where(
				mechanism[i],
				jnp.arange(shape[i]) == state[i],
				jnp.full(shape[i], 1.0 / shape[i]),
			).astype(node_tpm.dtype)
			# The leading axis is always the lowest not-yet-contracted previous-state axis.
			conditioned = jnp.tensordot(conditioned, weights, axes=([0], [0]))
		factor = jnp.where(purview[j], conditioned, jnp.full(shape[j], 1.0 / shape[j]))
		joint = joint * factor.reshape((1,) * j + (-1,) + (1,) * (len(shape) - 1 - j))
	return joint

purview_distribution(full, purview)

Recover the bare purview distribution from a full-shape repertoire.

Sums out the uniform non-purview axes (keeping dimensions, so the shape is stable under vmap): the result is the distribution over purview states, broadcast along singleton-like constant axes.

Parameters:

Name Type Description Default
full Array

Full-shape repertoire, shape shape.

required
purview Array

Mask of the purview units, shape (n,).

required

Returns:

Type Description
Array

Tensor of shape shape, constant along non-purview axes, whose values are the

Array

purview-state probabilities.

Source code in src/iitx/repertoires.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def purview_distribution(full: jax.Array, purview: jax.Array) -> jax.Array:
	"""Recover the bare purview distribution from a full-shape repertoire.

	Sums out the uniform non-purview axes (keeping dimensions, so the shape is stable
	under ``vmap``): the result is the distribution over purview states, broadcast along
	singleton-like constant axes.

	Args:
		full: Full-shape repertoire, shape ``shape``.
		purview: Mask of the purview units, shape ``(n,)``.

	Returns:
		Tensor of shape ``shape``, constant along non-purview axes, whose values are the
		purview-state probabilities.

	"""
	out = full
	for axis in range(full.ndim):
		out = jnp.where(purview[axis], out, out.sum(axis=axis, keepdims=True))
	return out

condition(node_tpms, state, candidate)

Clamp non-candidate previous-state axes of the factors at the current state.

This is the frozen-background conditioning shared by both theory versions on the effect side (and by IIT 3.0 on the cause side): units outside the candidate system are fixed at their current state. The clamped factors are constant along background axes, so downstream uniform marginalization leaves them untouched.

Parameters:

Name Type Description Default
node_tpms tuple[Array, ...]

Per-unit conditionals of the whole system.

required
state Array

Current state of the whole system, shape (n,).

required
candidate Array

Mask of the candidate system's units, shape (n,).

required

Returns:

Type Description
Array

Factors constant along background axes, equal to their value at the current

...

background state.

Source code in src/iitx/repertoires.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def condition(
	node_tpms: tuple[jax.Array, ...],
	state: jax.Array,
	candidate: jax.Array,
) -> tuple[jax.Array, ...]:
	"""Clamp non-candidate previous-state axes of the factors at the current state.

	This is the frozen-background conditioning shared by both theory versions on the
	effect side (and by IIT 3.0 on the cause side): units outside the candidate system
	are fixed at their current state. The clamped factors are constant along background
	axes, so downstream uniform marginalization leaves them untouched.

	Args:
		node_tpms: Per-unit conditionals of the whole system.
		state: Current state of the whole system, shape ``(n,)``.
		candidate: Mask of the candidate system's units, shape ``(n,)``.

	Returns:
		Factors constant along background axes, equal to their value at the current
		background state.

	"""
	clamped = []
	for factor in node_tpms:
		out = factor
		for axis in range(len(node_tpms)):
			selected = jnp.take(out, state[axis], axis=axis)
			out = jnp.where(candidate[axis], out, jnp.expand_dims(selected, axis))
		clamped.append(out)
	return tuple(clamped)

sever(node_tpms, cut)

Noise the connections a cut severs.

Severed inputs are uniformly marginalized out of the receiving unit's conditional, so each unit perceives its severed sources as independent noise — the cut semantics of both theory versions (IIT 3.0 unidirectional cuts and IIT 4.0 directional partitions differ only in which cut matrices are enumerated).

Parameters:

Name Type Description Default
node_tpms tuple[Array, ...]

Per-unit conditionals.

required
cut Array

Cut matrix, shape (n, n); entry (i, j) severs the connection from unit i to unit j.

required

Returns:

Type Description
tuple[Array, ...]

The partitioned factors.

Source code in src/iitx/repertoires.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def sever(node_tpms: tuple[jax.Array, ...], cut: jax.Array) -> tuple[jax.Array, ...]:
	"""Noise the connections a cut severs.

	Severed inputs are uniformly marginalized out of the receiving unit's conditional,
	so each unit perceives its severed sources as independent noise — the cut semantics
	of both theory versions (IIT 3.0 unidirectional cuts and IIT 4.0 directional
	partitions differ only in which cut matrices are enumerated).

	Args:
		node_tpms: Per-unit conditionals.
		cut: Cut matrix, shape ``(n, n)``; entry ``(i, j)`` severs the connection from
			unit ``i`` to unit ``j``.

	Returns:
		The partitioned factors.

	"""
	severed = []
	for j, factor in enumerate(node_tpms):
		out = factor
		for axis in range(len(node_tpms)):
			out = jnp.where(cut[axis, j], out.mean(axis=axis, keepdims=True), out)
		severed.append(out)
	return tuple(severed)