55
66from collections .abc import Iterable
77from dataclasses import dataclass
8+ from dataclasses import replace
89from enum import Enum
910from enum import auto
1011from enum import unique
3132from pyphi .models .cuts import NullCut
3233from pyphi .models .cuts import SystemPartition
3334from pyphi .models .mechanism import RepertoireIrreducibilityAnalysis
35+ from pyphi .models .mechanism import StateSpecification
3436from pyphi .models .subsystem import CauseEffectStructure
3537from pyphi .models .subsystem import SystemStateSpecification
3638from 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
328377def 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 :
0 commit comments