Skip to content

Transforms

iitx.transforms

Macro-level transforms: coarse-graining and black-boxing.

Both transforms are pure functions from a :class:~iitx.system.System to a :class:~iitx.system.System — the resulting macro system feeds the unchanged Φ pipeline, with no special macro code path (docs/design.md §10). This is what forces the core's generality: coarse-graining produces multi-valued macro units even from binary micro-units, and macro TPMs need not factorize (conditional independence stays a checked property, verified when a measure derives the factored view).

  • Coarse-graining (Hoel et al. 2013; Hoel et al. 2016) aggregates: a partition of the micro-units into macro-units, with a per-macro-unit state grouping, and the macro TPM as the fiber average D⁻¹ Gᵀ Tᵗ G of the (τ-step) micro TPM under maximum- entropy perturbation within each fiber.
  • Black-boxing (Marshall et al. 2018) projects: boxes of micro-units with designated output units; the macro state is the output units' state at the end of a τ-step window; hidden initial states are uniformly noised and boxes cannot read each other's hidden units. Following the oracle, cross-box connections beyond the first micro-step are severed entirely, and the black-box TPM is the conditionally independent factorization (per-unit marginals) of the window dynamics — the Markovian projection Marshall et al. leave implicit and PyPhi makes by construction.

Both transforms are jittable, differentiable in the TPM entries, and batchable over same-shaped candidate mappings; the mappings themselves (partitions, groupings, outputs, τ) are static structure.

coarse_grain(system, partition, groupings, steps=1)

Coarse-grain a system into macro-units.

The macro transition probability is the uniform ("maximum-entropy") average over each fiber of micro-states mapping to a macro-state: T_M(m, m') = mean over s in fiber(m) of sum over s' in fiber(m') of T^steps(s, s').

Parameters:

Name Type Description Default
system System

The micro system.

required
partition tuple[tuple[int, ...], ...]

Disjoint blocks of micro-units, jointly covering the system; block j becomes macro-unit j. Static.

required
groupings tuple[tuple[int, ...], ...]

For each block, the macro value of each of its joint micro-states (indexed little-endian within the block, units in ascending order). Macro values must cover range(max + 1). For the grouping to respect IIT's macro-unit doctrine it should not distinguish micro-states that differ only by which unit carries which value; the transform itself is general. Static.

required
steps int

Temporal grain: the number of micro-steps per macro-step.

1

Returns:

Type Description
System

The macro system (state-by-state; conditional independence is not implied — it

System

is checked downstream when a measure derives the factored view).

Source code in src/iitx/transforms.py
39
40
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
def coarse_grain(
	system: System,
	partition: tuple[tuple[int, ...], ...],
	groupings: tuple[tuple[int, ...], ...],
	steps: int = 1,
) -> System:
	"""Coarse-grain a system into macro-units.

	The macro transition probability is the uniform ("maximum-entropy") average over
	each fiber of micro-states mapping to a macro-state:
	``T_M(m, m') = mean over s in fiber(m) of sum over s' in fiber(m') of T^steps(s, s')``.

	Args:
		system: The micro system.
		partition: Disjoint blocks of micro-units, jointly covering the system; block
			``j`` becomes macro-unit ``j``. Static.
		groupings: For each block, the macro value of each of its joint micro-states
			(indexed little-endian within the block, units in ascending order). Macro
			values must cover ``range(max + 1)``. For the grouping to respect IIT's
			macro-unit doctrine it should not distinguish micro-states that differ only
			by which unit carries which value; the transform itself is general. Static.
		steps: Temporal grain: the number of micro-steps per macro-step.

	Returns:
		The macro system (state-by-state; conditional independence is not implied — it
		is checked downstream when a measure derives the factored view).

	"""
	blocks = _validated_partition(system, partition)
	mapping, macro_shape = _grouping_map(system.shape, blocks, groupings)

	num_macro = math.prod(macro_shape)
	members = jnp.asarray(mapping[:, None] == np.arange(num_macro)[None, :], dtype=system.tpm.dtype)
	stepped = jnp.linalg.matrix_power(system.tpm, steps)
	# D^-1 G^T T^t G: average rows within each source fiber, sum columns per target fiber.
	fiber_sizes = members.sum(axis=0)
	macro_tpm = (members.T @ stepped @ members) / fiber_sizes[:, None]
	return System(tpm=macro_tpm, shape=macro_shape)

coarse_grain_state(system, partition, groupings, state)

Map a micro state to its macro state under a coarse-graining.

Parameters:

Name Type Description Default
system System

The micro system.

required
partition tuple[tuple[int, ...], ...]

The unit partition of the coarse-graining.

required
groupings tuple[tuple[int, ...], ...]

The state groupings of the coarse-graining.

required
state Array

Micro state, shape (n,).

required

Returns:

Type Description
Array

Macro state, shape (k,) for k macro-units.

Source code in src/iitx/transforms.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def coarse_grain_state(
	system: System,
	partition: tuple[tuple[int, ...], ...],
	groupings: tuple[tuple[int, ...], ...],
	state: jax.Array,
) -> jax.Array:
	"""Map a micro state to its macro state under a coarse-graining.

	Args:
		system: The micro system.
		partition: The unit partition of the coarse-graining.
		groupings: The state groupings of the coarse-graining.
		state: Micro state, shape ``(n,)``.

	Returns:
		Macro state, shape ``(k,)`` for ``k`` macro-units.

	"""
	values = []
	for block, grouping in zip(partition, groupings, strict=True):
		units = sorted(block)
		weights = radix_weights(tuple(system.shape[u] for u in units))
		index = sum(state[u] * int(w) for u, w in zip(units, weights, strict=True))
		values.append(jnp.asarray(grouping)[index])
	return jnp.stack(values)

black_box(system, partition, outputs, steps=1)

Black-box a system: hide within-box micro-units behind designated outputs.

The macro state is the joint state of the output units at the end of a steps- long micro window. Within the window, boxes may read other boxes' outputs only at the first micro-step (later steps sever all cross-box connections); hidden units' initial states are uniformly noised; and the resulting window dynamics is factorized per unit — all following the oracle's semantics for the conventions the papers leave open (docs/notes/macro.md §6).

Parameters:

Name Type Description Default
system System

The micro system. Must be conditionally independent (the window is composed from its factored view).

required
partition tuple[tuple[int, ...], ...]

Disjoint boxes of micro-units, jointly covering the system. Static.

required
outputs tuple[tuple[int, ...], ...]

For each box, its designated output units (a nonempty subset of the box). Hidden units are the rest. Static.

required
steps int

Temporal grain: the number of micro-steps per macro-step.

1

Returns:

Type Description
System

The macro system over the output units (in ascending micro-unit order), with

System

their micro alphabets.

Source code in src/iitx/transforms.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def black_box(
	system: System,
	partition: tuple[tuple[int, ...], ...],
	outputs: tuple[tuple[int, ...], ...],
	steps: int = 1,
) -> System:
	"""Black-box a system: hide within-box micro-units behind designated outputs.

	The macro state is the joint state of the output units at the end of a ``steps``-
	long micro window. Within the window, boxes may read other boxes' outputs only at
	the first micro-step (later steps sever all cross-box connections); hidden units'
	initial states are uniformly noised; and the resulting window dynamics is factorized
	per unit — all following the oracle's semantics for the conventions the papers leave
	open (``docs/notes/macro.md`` §6).

	Args:
		system: The micro system. Must be conditionally independent (the window is
			composed from its factored view).
		partition: Disjoint boxes of micro-units, jointly covering the system. Static.
		outputs: For each box, its designated output units (a nonempty subset of the
			box). Hidden units are the rest. Static.
		steps: Temporal grain: the number of micro-steps per macro-step.

	Returns:
		The macro system over the output units (in ascending micro-unit order), with
		their micro alphabets.

	"""
	blocks = _validated_partition(system, partition)
	box_of = {}
	for box_index, block in enumerate(blocks):
		for unit in block:
			box_of[unit] = box_index
	output_units = sorted(
		unit
		for box_index, box in enumerate(outputs)
		for unit in _validated_outputs(blocks, box_index, box)
	)
	hidden_units = [u for u in range(system.n) if u not in output_units]

	factors = node_tpms(system)

	def noised(sever_outputs: bool) -> System:
		severed = []
		for j, factor in enumerate(factors):
			out = factor
			for i in range(system.n):
				cross_box = box_of[i] != box_of[j]
				hidden = i not in output_units
				if cross_box and (hidden or sever_outputs):
					out = out.mean(axis=i, keepdims=True)
			severed.append(jnp.broadcast_to(out, factor.shape))
		return System.from_node_tpms(tuple(severed))

	window = noised(sever_outputs=False).tpm
	if steps > 1:
		window = window @ jnp.linalg.matrix_power(noised(sever_outputs=True).tpm, steps - 1)

	# Factorize the window dynamics per unit (the oracle's Markovian projection), noise
	# the hidden initial states, and keep the output units.
	window_factors = node_tpms(System(tpm=window, shape=system.shape), check_independence=False)
	macro_factors = []
	for j in output_units:
		factor = window_factors[j]
		for i in hidden_units:
			factor = factor.mean(axis=i, keepdims=True)
		# Constant along hidden axes now; drop them (in descending order).
		for i in sorted(hidden_units, reverse=True):
			factor = jnp.take(factor, 0, axis=i)
		macro_factors.append(factor)
	return System.from_node_tpms(tuple(macro_factors))

black_box_state(outputs, state)

Map a micro state to its black-box macro state (the outputs' projection).

Parameters:

Name Type Description Default
outputs tuple[tuple[int, ...], ...]

The per-box output units of the black-boxing.

required
state Array

Micro state, shape (n,).

required

Returns:

Type Description
Array

Macro state: the state of the output units in ascending micro-unit order.

Source code in src/iitx/transforms.py
179
180
181
182
183
184
185
186
187
188
189
190
191
def black_box_state(outputs: tuple[tuple[int, ...], ...], state: jax.Array) -> jax.Array:
	"""Map a micro state to its black-box macro state (the outputs' projection).

	Args:
		outputs: The per-box output units of the black-boxing.
		state: Micro state, shape ``(n,)``.

	Returns:
		Macro state: the state of the output units in ascending micro-unit order.

	"""
	units = sorted(unit for box in outputs for unit in box)
	return jnp.asarray(state)[jnp.asarray(units)]