Skip to content

Commit 3f23822

Browse files
wmaynerclaude
andcommitted
Merge ac-partition-scheme: AC reads its own mechanism-partition scheme
Fixes a Wave-1 correctness bug from the 2026-07-13 whole-library review: actual-causation partition enumeration silently read the IIT mechanism_partition_scheme field, deflating AC alpha of first-order occurrences under an IIT 3.0 pin via paper-forbidden partitions. AC now routes through its own field (default JOINT_PARTITION_ALL, the 2019-paper family). Reconciles the tests that codified the leak, regenerates the AC perf pin, and logs a benign tie-resolver quirk surfaced by the fix as a ROADMAP follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UB4jbV4bQ8Y7Eywt1TNgJe
2 parents 99ded45 + ddccae3 commit 3f23822

11 files changed

Lines changed: 648 additions & 40 deletions

File tree

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ New ideas surfaced by the 2026-06-13 audit (the roadmap "started as an engineeri
333333

334334
*Correctness & rigor:* **(N1)** a comprehensive **paper-reproduction acceptance suite** as a CI gate — every worked example (IIT 4.0 Figs 1/2/4/6/7, Marshall 2024, AC 2019 Fig 11, Gómez p53) with pinned values, *including a network that exercises the 2026 ii-cap with non-zero φ*. **(N2)** a standing **`parallel ≡ sequential` Hypothesis invariant** in CI (the loky bug shows results can silently diverge/crash). **(N3)** recurring **mutation testing** as a scheduled gate so the golden+property net is proven to bite.
335335

336+
**Follow-up — tie-resolver returns None on cut-equivalent partitions** *(surfaced 2026-07-15 by the AC mechanism-partition-scheme fix; open)*. `resolve_ac_partition_tie` (and the shared `cascade` Determinism level, keyed on `partition.lex_key()`) cannot break a tie between two partitions that sever the same connections but split the intact side differently — `JOINT_PARTITION_ALL` emits such cut-equivalent full-cut partitions, they share a `lex_key`, and `argmin` over equal keys leaves the tie unresolved, so `_find_mip` returns `None`. Benign for actual causation today (the full cut is maximal-α and never the minimal-α MIP, so the redundant pair never ties at the minimum; every end-to-end AC test passes), but the resolver should still resolve deterministically. Fix options: a final index-order fallback in `cascade` when `lex_key`s collide, or deduplicating cut-equivalent partitions in `all_joint_partitions`. Both touch shared machinery used by IIT, so each needs its own regression coverage. The AC fix left the contrived unit test (`test_ac_system_ties.py::TestFindMipPartitionTies`) targeting distinct-cut partitions to avoid the collision.
337+
336338
*Performance / algorithmic:* **(N4)** a **disk-backed result cache** keyed on the P9.5 math-fingerprint, so notebook re-runs and paper reproductions skip recomputation. **(N5)** elevate the "**Rust/PyO3 kernel**" aside to a concrete, P17-gated item for the partition-enumeration + repertoire inner loops — the one lever that touches the O(2ⁿ) floor caching can't. **(N6 — landed 2026-07-11)** a **lazy / top-K relations mode** (relations are the n≥6 bottleneck that OxiDD + analytical folds only partly address). The 2026-07-07 relations exploration (`docs/superpowers/specs/2026-07-07-relations-without-materialization-design.md`) settled the design — the relation set is a deterministic view of a linear-size summary; exact/top-K/sampling query tiers verified at Fig-6D scale — and its query surface is landed on ``pyphi/relations.py`` (closed-form moments/degrees/histogram/binding/faces, lazy top-K ``strongest()``, seeded sampling, bounded ``materialize()``), discharging this item together with N24.
337339

338340
*API ergonomics / usability:* **(N7 — landed)** one high-level **`pyphi.analyze(substrate, state, formalism=…)`** entry point. **(N8)** a full **provenance stamp** on every result (pyphi version, git sha, seed, wall-time) extending the P10 config snapshot. **(N9 — landed)** the unified labeled-export (`to_pandas`/xarray) story — already a P14d follow-on, elevated because it interlocks with the freeze. **(N11 — landed)** **Bayesian-network / dynamic-BN interop** — render a substrate as a **2-timeslice DBN** (nodes_t → nodes_{t+1}) where the TPM becomes CPDs, enabling pgmpy / d-separation / Markov-blanket workflows. The static-BN view is **unfaithful** — substrates are cyclic by construction (feedback/self-loops); the 2-TBN unrolling is the correct acyclic target. Builds on B20's topology bridge.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Actual-causation partition enumeration now reads its own
2+
`actual_causation.mechanism_partition_scheme` config field instead of silently
3+
inheriting the IIT `mechanism_partition_scheme`. Under an IIT 3.0 pin the AC
4+
alpha of first-order occurrences was deflated by the paper-forbidden bipartition
5+
family; AC now defaults to the 2019-paper `JOINT_PARTITION_ALL` family
6+
regardless of the IIT setting.

docs/superpowers/plans/2026-07-15-ac-mechanism-partition-scheme-threading.md

Lines changed: 356 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Actual Causation reads its own mechanism-partition scheme
2+
3+
**Status:** design. Fixes a confirmed correctness bug from the 2026-07-13
4+
whole-library review (Wave 1).
5+
6+
## 0. Summary
7+
8+
Actual-causation partition enumeration silently reads the *IIT* config field
9+
`config.formalism.iit.mechanism_partition_scheme`, so AC α values change with an
10+
unrelated IIT setting, while the dedicated
11+
`ActualCausationConfig.mechanism_partition_scheme` field (default
12+
`JOINT_PARTITION_ALL`) is never read. Under any IIT 3.0 pin (which sets the IIT
13+
field to `JOINT_BIPARTITION`), AC enumerates partitions the 2019 paper forbids
14+
for first-order occurrences, deflating α to 0 where the paper defines α = ρ.
15+
16+
The fix routes AC through its own field, mirroring how AC already threads its
17+
other three schemes (`alpha_measure`, `partitioned_repertoire_scheme`,
18+
`background_scheme`, `alpha_aggregation`). The field remains freely
19+
configurable — users may investigate partition variants deliberately — but the
20+
choice is now an explicit AC setting rather than a value silently inherited
21+
from an unrelated formalism.
22+
23+
## 1. The bug
24+
25+
`pyphi/formalism/actual_causation/compute.py``_find_mip` and `_get_partitions`
26+
call `pyphi.partition.mechanism_partitions(mechanism, purview, node_labels)`,
27+
which dispatches on `config.formalism.iit.mechanism_partition_scheme`
28+
(`partition.py:473`) and takes no scheme argument. So:
29+
30+
- AC partition enumeration is governed by the **IIT** field.
31+
- `config.formalism.actual_causation.mechanism_partition_scheme` (defined at
32+
`formalism.py:148`, default `JOINT_PARTITION_ALL`) has **no reader** — it is
33+
dead.
34+
- The `iit3` preset sets `iit.mechanism_partition_scheme="JOINT_BIPARTITION"`,
35+
so every AC computation pinned with `IIT_3_CONFIG` silently uses the IIT
36+
bipartition family.
37+
38+
**Why this is wrong (paper-grounded).** Albantakis et al. (2019), *What Caused
39+
What?*, Eq. 7 defines a partition ψ of an occurrence into `m` parts as a split
40+
of the mechanism with the constrained purview distributed across the parts
41+
(any part's purview may be empty). Fig 3B (p. 9) and p. 13 add the m=1 rule:
42+
for a first-order occurrence the *only* permitted partition is the full cut
43+
(occurrence completely severed from its purview), giving α = ρ. The paper's
44+
family is therefore *all* partitions of the occurrence with the m=1 non-full-cut
45+
cases excluded — which is precisely what `JOINT_PARTITION_ALL`
46+
(`all_joint_partitions`) implements, via its `partition.py:663` guard that skips
47+
partitions leaving the whole mechanism in one part with a non-empty purview.
48+
`JOINT_BIPARTITION` (m=2) and `WEDGE_TRIPARTITION` (m=3) are IIT-3.0 constructs
49+
that restrict to a fixed part count; the 2019 paper names neither. Under
50+
`JOINT_BIPARTITION`, first-order occurrences over a multi-unit purview get
51+
purview-splitting m=1 partitions with the occurrence intact — the "Not
52+
permitted" case — and minimizing over them drives α to 0.
53+
54+
**Verified.** On the 4-node OR/AND `actual_causation_substrate`, cause link
55+
`_find_mip(t, CAUSE, (0,), (0,1))`: α = 0.415037 under `JOINT_PARTITION_ALL`
56+
(= ρ, paper-correct) vs α = 0.0 under `JOINT_BIPARTITION`.
57+
58+
## 2. The fix
59+
60+
### 2.1 Thread AC's own field
61+
62+
`_resolve_ac_measures` (`formalism.py`) already resolves the AC alpha measure and
63+
the other three AC schemes into callables read from
64+
`config.formalism.actual_causation`. Extend it to also resolve
65+
`mechanism_partition_scheme`:
66+
67+
```python
68+
mp_name = (
69+
mechanism_partition_scheme_name
70+
if mechanism_partition_scheme_name is not None
71+
else ac.mechanism_partition_scheme
72+
)
73+
...
74+
"mechanism_partitions": partition_types[mp_name],
75+
```
76+
77+
`partition_types[mp_name]` is the generator callable with signature
78+
`(mechanism, purview, node_labels) -> Iterable[partition]` — the same signature
79+
the global `mechanism_partitions()` invokes. An unregistered name raises
80+
`KeyError` from the registry (the only validation needed; no allowlist).
81+
82+
Thread the resolved callable through the AC compute entry points
83+
(`_account`, `_directed_account`, `_sia`, `_find_mip`, `_find_causal_link`) as a
84+
`mechanism_partitions` keyword argument, exactly as `partitioned_repertoire_scheme`
85+
is threaded today. `_find_mip` and `_get_partitions` call the passed callable
86+
instead of the global `mechanism_partitions()`.
87+
88+
### 2.2 IIT paths untouched
89+
90+
`pyphi.partition.mechanism_partitions()` keeps reading the IIT field and keeps
91+
its current signature; IIT distinction/MICE computation is unchanged. Only AC
92+
stops routing through it.
93+
94+
### 2.3 Configurable, informed — no restriction
95+
96+
The AC field accepts any registered mechanism-partition scheme; selecting a
97+
non-default scheme is a deliberate variant investigation. The
98+
`ActualCausationConfig.mechanism_partition_scheme` field docstring documents the
99+
semantics so the choice is informed: `JOINT_PARTITION_ALL` is the 2019-paper
100+
family (Eq. 7 + Fig 3B); other schemes deviate — notably `JOINT_BIPARTITION`
101+
admits m=1 partitions the paper forbids for first-order occurrences, so it
102+
produces α below the published values. No config constraint or allowlist is
103+
added.
104+
105+
## 3. Tests and goldens
106+
107+
Fixing the leak changes AC results that were computed under the leaked scheme,
108+
and flips test expectations that codified the bug. All AC tests live under
109+
`test/test_actual.py` (the `TestActualCausationIIT30` class pins `IIT_3_CONFIG`);
110+
AC goldens live in `test/golden/zoo.py` / `test/golden/compute.py`.
111+
112+
### 3.1 New guarantees (regression tests)
113+
114+
- **The IIT field no longer affects AC; the AC field does.** Set
115+
`iit.mechanism_partition_scheme` and
116+
`actual_causation.mechanism_partition_scheme` to different values and assert AC
117+
α tracks the AC field, not the IIT field.
118+
- **First-order occurrence is paper-correct by default.** The §1 repro: under the
119+
AC default `JOINT_PARTITION_ALL`, `_find_mip(t, CAUSE, (0,), (0,1)).alpha` =
120+
ρ = 0.415037, not 0.
121+
122+
### 3.2 Expectation changes (bug was codified)
123+
124+
- `test_sia_cause_direction` asserts `alpha == 0.0` under the inherited
125+
`JOINT_BIPARTITION`. After the fix, AC uses its own default
126+
`JOINT_PARTITION_ALL`, so the cause α is the paper value. Re-assert the
127+
recomputed value. A separate test may set the *AC* field to `JOINT_BIPARTITION`
128+
explicitly to retain coverage of that regime as a deliberate variant.
129+
- `test_prevention` overrides the *IIT* field to `WEDGE_TRIPARTITION`. Repoint the
130+
override to the *AC* field; `WEDGE_TRIPARTITION` remains a legitimate
131+
intentional variant. Re-verify the asserted values under the corrected routing.
132+
- The remaining `TestActualCausationIIT30` tests and AC goldens recompute under
133+
`JOINT_PARTITION_ALL`. Regenerate affected goldens deliberately, each documented
134+
as previously computed under the leaked IIT scheme.
135+
136+
### 3.3 Verification
137+
138+
`uv run pytest test/test_actual.py` and the golden regression suite green;
139+
`uv run pytest` (no path argument) green for the doctest sweep, since the AC
140+
config field docstring changes.
141+
142+
## 4. Scope boundaries
143+
144+
- **AC only.** IIT and IIT 3.0 mechanism partitioning are untouched.
145+
- **No new partition schemes.** The fix routes existing schemes correctly; it does
146+
not add or modify any `partition_types` entry.
147+
- **No config-constraint machinery.** The field stays a free choice; the registry
148+
lookup is the only validation.
149+
- **Not in scope:** the other Wave 1 remainders (macro TPM bugs, factored-backend
150+
select crash) are separate fixes.

pyphi/conf/CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ option-by-option reference, loaded only when working with this directory.
5656

5757
- **`mechanism_partition_scheme`**: Default `"JOINT_PARTITION_ALL"`
5858
- **`system_partition_scheme`**: Default `"DIRECTED_SET_PARTITION"`
59+
- **`config.formalism.actual_causation.mechanism_partition_scheme`**: AC
60+
partition family (default `"JOINT_PARTITION_ALL"`, the Albantakis et al. 2019
61+
Eq. 7 / Fig. 3B family). Read independently of the IIT field above;
62+
`"JOINT_BIPARTITION"` is a deliberate variant that deviates from the published
63+
α on first-order occurrences.
5964

6065
## Debugging & Output (`config.infrastructure`)
6166

pyphi/conf/formalism.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ class ActualCausationConfig:
162162

163163
version: str = "AC_2019"
164164
alpha_measure: str = "PMI"
165+
# The partition family for actual-causation MIP search. JOINT_PARTITION_ALL
166+
# is the Albantakis et al. (2019) family (Eq. 7 + Fig. 3B: all partitions of
167+
# the occurrence, excluding the m=1 non-full-cut cases forbidden for
168+
# first-order occurrences). Other registered schemes are deliberate
169+
# variants — notably JOINT_BIPARTITION admits those m=1 partitions and so
170+
# yields alpha below the published values on first-order occurrences.
165171
mechanism_partition_scheme: str = "JOINT_PARTITION_ALL"
166172
partitioned_repertoire_scheme: str = "PRODUCT"
167173
background_scheme: str = "UNIFORM"

pyphi/formalism/actual_causation/compute.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from pyphi.models import _null_ac_ria
3939
from pyphi.models import _null_ac_sia
4040
from pyphi.parallel import map_reduce
41-
from pyphi.partition import mechanism_partitions
41+
from pyphi.partition import partition_types
4242
from pyphi.registry import Registry
4343

4444
if TYPE_CHECKING:
@@ -198,6 +198,21 @@ def account_distance(A1, A2):
198198
return sum([action.alpha for action in A1]) - sum([action.alpha for action in A2])
199199

200200

201+
def _ac_mechanism_partitions(mechanism, purview, node_labels=None):
202+
"""Yield mechanism partitions under the actual-causation partition scheme.
203+
204+
Resolves ``config.formalism.actual_causation.mechanism_partition_scheme``
205+
through the partition-scheme registry at call time, so actual-causation
206+
partitioning is governed by the AC formalism and never by the IIT
207+
``mechanism_partition_scheme`` field. The default ``JOINT_PARTITION_ALL`` is
208+
the partition family of Albantakis et al. (2019), Eq. 7 and Fig. 3B: all
209+
partitions of the occurrence, excluding the m=1 non-full-cut cases the paper
210+
forbids for first-order occurrences.
211+
"""
212+
scheme = config.formalism.actual_causation.mechanism_partition_scheme
213+
return partition_types[scheme](mechanism, purview, node_labels)
214+
215+
201216
def _find_mip(
202217
transition: Transition,
203218
direction,
@@ -254,7 +269,9 @@ def _find_mip(
254269

255270
probability = transition.probability(direction, mechanism, purview)
256271
candidates: list[AcRepertoireIrreducibilityAnalysis] = []
257-
for partition in mechanism_partitions(mechanism, purview, transition.node_labels):
272+
for partition in _ac_mechanism_partitions(
273+
mechanism, purview, transition.node_labels
274+
):
258275
partitioned_probability = transition.partitioned_probability(
259276
direction,
260277
partition,
@@ -557,7 +574,7 @@ def _get_partitions(transition, direction):
557574
else:
558575
mechanism = transition.mechanism_indices(direction)
559576
purview = transition.purview_indices(direction)
560-
for inner_partition in mechanism_partitions(
577+
for inner_partition in _ac_mechanism_partitions(
561578
mechanism, purview, transition.node_labels
562579
):
563580
yield DirectedJointPartition(

test/data/perf/call_counts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"actual_causation::account": {
33
"conf/:override": 2,
44
"relations.py:relations": 0,
5-
"repertoire_algebra.py:cause_repertoire": 41,
6-
"repertoire_algebra.py:effect_repertoire": 43,
5+
"repertoire_algebra.py:cause_repertoire": 54,
6+
"repertoire_algebra.py:effect_repertoire": 63,
77
"system.py:find_mip": 0
88
},
99
"basic_iit3_emd::sia": {

test/formalism/test_ac_system_ties.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ def _find_mip_with_alphas(make_alphas):
146146
purview = (0, 1)
147147
n = len(
148148
list(
149-
compute.mechanism_partitions(mechanism, purview, transition.node_labels)
149+
compute._ac_mechanism_partitions(
150+
mechanism, purview, transition.node_labels
151+
)
150152
)
151153
)
152154
alphas = make_alphas(n)
@@ -170,8 +172,13 @@ def test_unique_minimum_yields_no_partition_ties(self):
170172
assert ria.partition_ties is None
171173

172174
def test_tied_minimum_yields_exact_alpha_cluster(self):
175+
# The tied minimum is placed on the last two partitions, which are
176+
# distinct cuts. The JOINT_PARTITION_ALL family leads with cut-equivalent
177+
# full-cut partitions (mechanism intact vs. internally split, same cut
178+
# matrix, same lex_key); a minimum landing on that redundant pair has no
179+
# unique lex-canonical winner (see the resolver follow-up in ROADMAP).
173180
ria, alphas = _find_mip_with_alphas(
174-
lambda n: [0.2, 0.2] + [0.9 + 0.1 * i for i in range(n - 2)]
181+
lambda n: [0.9 + 0.1 * i for i in range(n - 2)] + [0.2, 0.2]
175182
)
176183
assert len(alphas) > 2 # cluster must be a strict subset of candidates
177184
assert ria.partition_ties is not None

test/integration/test_cross_formalism_invariants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@
9090
from pyphi.conf.formalism import IITConfig
9191
from pyphi.direction import Direction
9292
from pyphi.formalism import iit3
93-
from pyphi.formalism.actual_causation.compute import mechanism_partitions
9493
from pyphi.formalism.actual_causation.compute import probability_distance
9594
from pyphi.measures.distribution import generalized_intrinsic_difference
95+
from pyphi.partition import mechanism_partitions
9696
from test.hypothesis_utils import binary_state
9797
from test.hypothesis_utils import small_substrate
9898

0 commit comments

Comments
 (0)