|
| 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. |
0 commit comments