Skip to content

Commit f92b981

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/develop' into develop
2 parents 3f66e29 + c61d95d commit f92b981

File tree

8 files changed

+479
-19
lines changed

8 files changed

+479
-19
lines changed

changelog.d/eq23-ii-cap.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement Eq. 23 ii(s) cap: system-level partition evaluation now uses GID only (Eqs. 19-20), and phi is capped by ii(s) = min(ii_c, ii_e) where ii_d = min(i_diff_d, i_spec_d). Mechanism-level partition evaluation also uses GID only when `REPERTOIRE_DISTANCE=INTRINSIC_INFORMATION` (the composite metric is a system-level concept).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix permutation symmetry breaking caused by arbitrary tie-breaking among specified states. When multiple states tie for maximum intrinsic information, `integration_value()` now evaluates all tied states and selects the minimum (the "cruelest cut"). The resolved state is back-propagated to `system_state` on the SIA so that downstream consumers (e.g., `phi_structure` congruence filtering) see the correct specified state.

pyphi/new_big_phi/__init__.py

Lines changed: 85 additions & 15 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
@@ -31,6 +32,7 @@
3132
from pyphi.models.cuts import NullCut
3233
from pyphi.models.cuts import SystemPartition
3334
from pyphi.models.mechanism import RepertoireIrreducibilityAnalysis
35+
from pyphi.models.mechanism import StateSpecification
3436
from pyphi.models.subsystem import CauseEffectStructure
3537
from pyphi.models.subsystem import SystemStateSpecification
3638
from pyphi.parallel import MapReduce
@@ -143,6 +145,31 @@ def ties(self):
143145
def set_ties(self, ties):
144146
self._ties = ties
145147

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+
146173
def __eq__(self, other):
147174
return cmp.general_eq(self, other, self._sia_attributes)
148175

@@ -288,16 +315,15 @@ def normalization_factor(partition: Cut | GeneralKCut) -> float:
288315
return 1.0
289316

290317

291-
def integration_value(
318+
def _integration_value_for_state(
292319
direction: Direction,
293320
subsystem: Subsystem,
321+
cut_subsystem: Subsystem,
294322
partition: Cut,
295-
system_state: SystemStateSpecification,
296-
repertoire_distance: str | None = None,
323+
specified: StateSpecification,
324+
repertoire_distance: str,
297325
) -> RepertoireIrreducibilityAnalysis:
298-
repertoire_distance = fallback(repertoire_distance, config.REPERTOIRE_DISTANCE)
299-
cut_subsystem = subsystem.apply_cut(partition)
300-
# TODO(4.0) deal with proliferation of special cases for GID
326+
"""Compute the integration value for a single specified state."""
301327
mechanism = purview = subsystem.node_indices
302328
if repertoire_distance in [
303329
"GENERALIZED_INTRINSIC_DIFFERENCE",
@@ -307,22 +333,45 @@ def integration_value(
307333
direction,
308334
mechanism,
309335
purview,
310-
system_state[direction].state,
311-
).squeeze()[system_state[direction].state]
336+
specified.state,
337+
).squeeze()[specified.state]
312338
else:
313339
partitioned_repertoire = cut_subsystem.repertoire(
314340
direction, subsystem.node_indices, subsystem.node_indices
315341
)
316-
ria = subsystem.evaluate_partition(
342+
return subsystem.evaluate_partition(
317343
direction,
318344
subsystem.node_indices,
319345
subsystem.node_indices,
320346
partition, # pyright: ignore[reportArgumentType] - Cut passed to Bipartition param in IIT 4.0
321347
partitioned_repertoire=partitioned_repertoire,
322348
repertoire_distance=repertoire_distance,
323-
state=system_state[direction],
349+
state=specified,
324350
)
325-
return ria
351+
352+
353+
def integration_value(
354+
direction: Direction,
355+
subsystem: Subsystem,
356+
partition: Cut,
357+
system_state: SystemStateSpecification,
358+
repertoire_distance: str | None = None,
359+
) -> RepertoireIrreducibilityAnalysis:
360+
repertoire_distance = fallback(repertoire_distance, config.REPERTOIRE_DISTANCE)
361+
cut_subsystem = subsystem.apply_cut(partition)
362+
specified = system_state[direction]
363+
tied_specs = specified.ties if specified.ties else (specified,)
364+
# When there are tied specified states, evaluate all of them and take the
365+
# minimum integration (the "cruelest cut"): among equally-specified states,
366+
# the partition should be evaluated against the one it hurts most.
367+
best_ria = None
368+
for spec in tied_specs:
369+
ria = _integration_value_for_state(
370+
direction, subsystem, cut_subsystem, partition, spec, repertoire_distance
371+
)
372+
if best_ria is None or ria.phi < best_ria.phi:
373+
best_ria = ria
374+
return best_ria
326375

327376

328377
def intrinsic_differentiation_value(
@@ -365,19 +414,26 @@ def evaluate_partition(
365414
directions = Direction.both()
366415
directions = tuple(directions)
367416
validate.directions(directions)
417+
418+
# Eqs. 19-20: system-level partition integration uses GID only.
419+
# The ii(s) cap (Eq. 23) is applied separately below.
420+
effective_distance = fallback(repertoire_distance, config.REPERTOIRE_DISTANCE)
421+
partition_distance = (
422+
"GENERALIZED_INTRINSIC_DIFFERENCE"
423+
if effective_distance == "INTRINSIC_INFORMATION"
424+
else effective_distance
425+
)
426+
368427
integration = {
369428
direction: integration_value(
370429
direction,
371430
subsystem,
372431
partition,
373432
system_state,
374-
repertoire_distance=repertoire_distance,
433+
repertoire_distance=partition_distance,
375434
)
376435
for direction in directions
377436
}
378-
phi = min(integration[direction].phi for direction in directions)
379-
norm = normalization_factor(partition)
380-
normalized_phi = phi * norm
381437

382438
intrinsic_differentiation = {
383439
direction: intrinsic_differentiation_value(
@@ -388,6 +444,19 @@ def evaluate_partition(
388444
for direction in directions
389445
}
390446

447+
phi = min(integration[direction].phi for direction in directions)
448+
449+
# Eq. 23: φ_s(s) = min{φ_c(s), φ_e(s), ii(s)}
450+
# where ii(s) = min_d{min(i_diff_d, i_spec_d)}
451+
if effective_distance == "INTRINSIC_INFORMATION":
452+
for direction in directions:
453+
i_spec = float(system_state[direction].intrinsic_information)
454+
i_diff = float(intrinsic_differentiation[direction])
455+
phi = min(phi, i_spec, i_diff)
456+
457+
norm = normalization_factor(partition)
458+
normalized_phi = phi * norm
459+
391460
result = SystemIrreducibilityAnalysis(
392461
phi=phi,
393462
normalized_phi=normalized_phi,
@@ -548,6 +617,7 @@ def is_disconnecting_partition(partition):
548617
elif candidate_key == mip_key:
549618
ties.append(candidate_mip_sia)
550619
for tied_mip in ties:
620+
tied_mip.resolve_system_state()
551621
tied_mip.set_ties(ties)
552622

553623
if config.CLEAR_SUBSYSTEM_CACHES_AFTER_COMPUTING_SIA:

pyphi/subsystem.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -811,15 +811,16 @@ def evaluate_partition(
811811
partitioned repertoires, and the partitioned repertoire.
812812
"""
813813
repertoire_distance = fallback(repertoire_distance, config.REPERTOIRE_DISTANCE)
814+
# Mechanism-level partition evaluation uses GID only.
815+
# INTRINSIC_INFORMATION is a system-level composite (Eq. 23).
816+
if repertoire_distance == "INTRINSIC_INFORMATION":
817+
repertoire_distance = "GENERALIZED_INTRINSIC_DIFFERENCE"
814818
# TODO(4.0) refactor
815819
# TODO(4.0) consolidate logic with system level partitions
816820
if repertoire is None:
817821
repertoire = self.repertoire(direction, mechanism, purview)
818822
# TODO(4.0) use same partitioned_repertoire func
819-
if repertoire_distance in [
820-
"GENERALIZED_INTRINSIC_DIFFERENCE",
821-
"INTRINSIC_INFORMATION",
822-
]:
823+
if repertoire_distance == "GENERALIZED_INTRINSIC_DIFFERENCE":
823824
func = metrics.distribution.measures[repertoire_distance]
824825
assert not isinstance(repertoire, (int, float)), (
825826
"GID requires full repertoire"

test/example_networks.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pyphi
44
from pyphi import utils
5+
from pyphi.labels import NodeLabels
56
from pyphi.macro import Blackbox
67
from pyphi.macro import MacroSubsystem
78
from pyphi.network import Network
@@ -754,3 +755,45 @@ def propagation_delay():
754755
return MacroSubsystem(
755756
network, cs, network.node_indices, time_scale=time_scale, blackbox=blackbox
756757
)
758+
759+
760+
# Permutation-equivalent pair for symmetry tests
761+
# ================================================
762+
763+
764+
def and_xor_network():
765+
"""AND-XOR 2-node network. Node 0: AND(0,1), Node 1: XOR(0,1).
766+
767+
Both nodes receive input from both nodes (all-ones CM).
768+
Deterministic transitions:
769+
(0,0) -> (0,0), (1,0) -> (0,1), (0,1) -> (0,1), (1,1) -> (1,0)
770+
"""
771+
# fmt: off
772+
tpm = np.array([
773+
[0, 0], # (0,0) -> (0,0)
774+
[0, 1], # (1,0) -> (0,1)
775+
[0, 1], # (0,1) -> (0,1)
776+
[1, 0], # (1,1) -> (1,0)
777+
])
778+
# fmt: on
779+
cm = np.ones((2, 2))
780+
return Network(tpm, cm=cm, node_labels=NodeLabels(("AND", "XOR"), tuple(range(2))))
781+
782+
783+
def xor_and_network():
784+
"""XOR-AND 2-node network (AND-XOR with nodes 0 and 1 permuted).
785+
786+
Both nodes receive input from both nodes (all-ones CM).
787+
Deterministic transitions:
788+
(0,0) -> (0,0), (1,0) -> (1,0), (0,1) -> (1,0), (1,1) -> (0,1)
789+
"""
790+
# fmt: off
791+
tpm = np.array([
792+
[0, 0], # (0,0) -> (0,0)
793+
[1, 0], # (1,0) -> (1,0)
794+
[1, 0], # (0,1) -> (1,0)
795+
[0, 1], # (1,1) -> (0,1)
796+
])
797+
# fmt: on
798+
cm = np.ones((2, 2))
799+
return Network(tpm, cm=cm, node_labels=NodeLabels(("XOR", "AND"), tuple(range(2))))

0 commit comments

Comments
 (0)