Skip to content

Commit c9268f8

Browse files
wmaynerclaude
andcommitted
Back-propagate MIP-resolved specified states to system_state on SIA
After integration_value() resolves tied specified states by selecting the one most vulnerable to the partition ("cruelest cut"), that resolution was recorded on the MIP's cause/effect RIAs (via specified_state) but not on sia.system_state, which still held the arbitrary initial choice. This matters because system_state is used downstream by phi_structure() for congruence filtering. Add resolve_system_state() method to SystemIrreducibilityAnalysis that back-propagates the winning specified states from the MIP's RIAs into system_state. Called in sia() after the MIP is found, for all tied MIPs. Ties are preserved on the updated system_state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 876629b commit c9268f8

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

pyphi/new_big_phi/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from collections.abc import Iterable
77
from dataclasses import dataclass
8+
from dataclasses import replace
89
from enum import Enum
910
from enum import auto
1011
from enum import unique
@@ -144,6 +145,31 @@ def ties(self):
144145
def set_ties(self, ties):
145146
self._ties = ties
146147

148+
def resolve_system_state(self) -> None:
149+
"""Update system_state to reflect the specified states resolved by the MIP.
150+
151+
When the system has tied specified states, the MIP resolves the tie by
152+
selecting the state most vulnerable to the winning partition. This
153+
back-propagates that resolution into system_state so that downstream
154+
consumers (e.g., congruence filtering in phi_structure) see the correct
155+
specified states.
156+
"""
157+
if self.system_state is None:
158+
return
159+
new_cause = self.system_state.cause
160+
new_effect = self.system_state.effect
161+
if self.cause is not None and self.cause.specified_state is not None:
162+
new_cause = self.cause.specified_state
163+
if self.effect is not None and self.effect.specified_state is not None:
164+
new_effect = self.effect.specified_state
165+
if (
166+
new_cause is not self.system_state.cause
167+
or new_effect is not self.system_state.effect
168+
):
169+
self.system_state = replace(
170+
self.system_state, cause=new_cause, effect=new_effect
171+
)
172+
147173
def __eq__(self, other):
148174
return cmp.general_eq(self, other, self._sia_attributes)
149175

@@ -571,6 +597,7 @@ def is_disconnecting_partition(partition):
571597
elif candidate_key == mip_key:
572598
ties.append(candidate_mip_sia)
573599
for tied_mip in ties:
600+
tied_mip.resolve_system_state()
574601
tied_mip.set_ties(ties)
575602

576603
if config.CLEAR_SUBSYSTEM_CACHES_AFTER_COMPUTING_SIA:

test/test_invariants.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,39 @@ def test_sia_phi_e_symmetric(self):
386386
f"phi_e differs for permuted systems: "
387387
f"AND-XOR(0,1)={phi_e_ax}, XOR-AND(1,0)={phi_e_xa}"
388388
)
389+
390+
def test_system_state_reflects_mip_resolution(self):
391+
"""system_state should reflect the specified state chosen by the MIP.
392+
393+
When tied specified states are resolved by the MIP, the winning state
394+
(most vulnerable to the partition) should be back-propagated to
395+
system_state, so downstream consumers see the correct state.
396+
"""
397+
sub = Subsystem(example_networks.and_xor_network(), (0, 1))
398+
sia = new_big_phi.sia(sub)
399+
if sia.cause and sia.cause.specified_state:
400+
assert sia.system_state.cause.state == sia.cause.specified_state.state
401+
if sia.effect and sia.effect.specified_state:
402+
assert sia.system_state.effect.state == sia.effect.specified_state.state
403+
404+
def test_system_state_preserves_ties_after_resolution(self):
405+
"""system_state should still record all tied states after resolution."""
406+
sub = Subsystem(example_networks.and_xor_network(), (0, 1))
407+
sia = new_big_phi.sia(sub)
408+
# The cause direction had 2 tied states
409+
assert len(sia.system_state.cause.ties) == 2
410+
tied_states = {t.state for t in sia.system_state.cause.ties}
411+
assert tied_states == {(0, 1), (1, 0)}
412+
413+
def test_system_state_symmetric(self):
414+
"""system_state.cause.state should be permutation-equivalent."""
415+
sub_ax = Subsystem(example_networks.and_xor_network(), (0, 1))
416+
sub_xa = Subsystem(example_networks.xor_and_network(), (1, 0))
417+
sia_ax = new_big_phi.sia(sub_ax)
418+
sia_xa = new_big_phi.sia(sub_xa)
419+
ax_state = sia_ax.system_state.cause.state
420+
xa_state = sia_xa.system_state.cause.state
421+
assert ax_state == tuple(reversed(xa_state)), (
422+
f"system_state.cause.state not permutation-equivalent: "
423+
f"AND-XOR={ax_state}, XOR-AND={xa_state}"
424+
)

test/test_subsystem.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,10 @@ def test_no_ties_when_unique_max(self):
222222
spec = sub.intrinsic_information(Direction.EFFECT, (0, 1), (0, 1))
223223
# Effect direction should have a unique max (no tie)
224224
assert len(spec.ties) == 1
225+
226+
def test_null_sia_resolve_system_state_safe(self):
227+
"""resolve_system_state should be a no-op for NullSIA."""
228+
from pyphi.new_big_phi import NullSystemIrreducibilityAnalysis
229+
230+
null_sia = NullSystemIrreducibilityAnalysis()
231+
null_sia.resolve_system_state() # Should not raise

0 commit comments

Comments
 (0)