@@ -388,3 +388,205 @@ def test_sia_sequential_equals_parallel_phi(self, subsystem_fixture, request):
388388 f" Parallel: { par_result .phi } \n "
389389 f" Diff: { abs (seq_result .phi - par_result .phi )} "
390390 )
391+
392+
393+ # ============================================================================
394+ # Eq. 23: ii(s) cap and GID-only partition evaluation
395+ # ============================================================================
396+
397+
398+ class TestEq23IntrinsicInformationCap :
399+ """Test that sia() implements Eq. 23 from Mayner, Marshall, & Tononi 2025.
400+
401+ φ_s(s) = min{φ_c(s), φ_e(s), ii(s)}
402+
403+ where ii(s) = min{ii_c(s), ii_e(s)} and ii_d = min{i_diff_d, i_spec_d}.
404+
405+ Partition evaluation uses GID only (Eqs. 19-20); i_diff and i_spec are
406+ applied as the ii(s) cap separately.
407+ """
408+
409+ II_CONFIG = dict (
410+ REPERTOIRE_DISTANCE = "INTRINSIC_INFORMATION" ,
411+ REPERTOIRE_DISTANCE_SPECIFICATION = "INTRINSIC_SPECIFICATION" ,
412+ REPERTOIRE_DISTANCE_DIFFERENTIATION = "INTRINSIC_DIFFERENTIATION" ,
413+ )
414+
415+ @staticmethod
416+ def _noisy_copy_subsystem (p , state ):
417+ """Create a 2-node noisy COPY system.
418+
419+ Each node copies the other with probability p (LOLI state ordering).
420+ """
421+ import numpy as np
422+ from pyphi import Network , Subsystem
423+
424+ tpm = np .array ([
425+ [1 - p , 1 - p ], # (0,0)
426+ [1 - p , p ], # (1,0)
427+ [p , 1 - p ], # (0,1)
428+ [p , p ], # (1,1)
429+ ])
430+ cm = np .array ([[0 , 1 ], [1 , 0 ]])
431+ network = Network (tpm , cm = cm , node_labels = ["A" , "B" ])
432+ return Subsystem (network , state )
433+
434+ def test_phi_capped_by_ii (self ):
435+ """phi is capped by ii(s) = min(i_diff, i_spec) per direction.
436+
437+ With p=0.8, state (1,1): GID(MIP) ≈ 0.868 but i_diff ≈ 0.644,
438+ so ii(s) ≈ 0.644 caps phi below GID(MIP).
439+ """
440+ from pyphi .direction import Direction
441+ from pyphi .new_big_phi import sia , system_intrinsic_information
442+
443+ subsystem = self ._noisy_copy_subsystem (0.8 , (1 , 1 ))
444+ with config .override (** self .II_CONFIG ):
445+ sys_state = system_intrinsic_information (subsystem )
446+ result = sia (subsystem )
447+
448+ # Compute ii(s) from components
449+ ii_cause = min (
450+ float (sys_state .cause .intrinsic_information ),
451+ float (result .intrinsic_differentiation [Direction .CAUSE ]),
452+ )
453+ ii_effect = min (
454+ float (sys_state .effect .intrinsic_information ),
455+ float (result .intrinsic_differentiation [Direction .EFFECT ]),
456+ )
457+ ii_s = min (ii_cause , ii_effect )
458+
459+ # phi must equal ii(s), not GID(MIP)
460+ assert float (result .phi ) == pytest .approx (ii_s , abs = 1e-9 )
461+ # GID(MIP) is larger than ii(s) — cap is binding
462+ gid_mip = min (float (result .cause .phi ), float (result .effect .phi ))
463+ assert gid_mip > ii_s + 1e-6
464+
465+ def test_partition_evaluation_uses_gid_only (self ):
466+ """Per-direction phi values at MIP are GID, not min(GID, i_diff).
467+
468+ With p=0.8: GID ≈ 0.868, i_diff ≈ 0.644. The cause/effect phi
469+ values on the SIA should be the GID values (un-folded), not the
470+ old min(GID, i_diff).
471+ """
472+ from pyphi .direction import Direction
473+ from pyphi .new_big_phi import sia
474+
475+ subsystem = self ._noisy_copy_subsystem (0.8 , (1 , 1 ))
476+ with config .override (** self .II_CONFIG ):
477+ result = sia (subsystem )
478+
479+ i_diff = float (result .intrinsic_differentiation [Direction .CAUSE ])
480+ cause_phi = float (result .cause .phi )
481+ effect_phi = float (result .effect .phi )
482+
483+ # cause/effect phi should be GID, which is LARGER than i_diff
484+ assert cause_phi > i_diff + 1e-6
485+ assert effect_phi > i_diff + 1e-6
486+
487+ def test_gid_distance_unaffected (self , s ):
488+ """GID-based computation is unchanged by the Eq. 23 logic.
489+
490+ The ii(s) cap and GID-only partition override only activate when
491+ REPERTOIRE_DISTANCE=INTRINSIC_INFORMATION.
492+ """
493+ from pyphi .new_big_phi import sia
494+
495+ # Default config uses GENERALIZED_INTRINSIC_DIFFERENCE
496+ result = sia (s )
497+ assert float (result .phi ) == pytest .approx (
498+ EXPECTED_PHI_VALUES ["s" ], abs = 1e-9
499+ )
500+
501+
502+ # ============================================================================
503+ # Paper examples: Mayner, Marshall, & Tononi 2025 (arXiv:2510.03881)
504+ # ============================================================================
505+
506+
507+ class TestPaperExamples :
508+ """Regression tests for the paper's worked examples.
509+
510+ These verify that PyPhi reproduces the analytical results from
511+ Mayner, Marshall, & Tononi 2025, "Intrinsic cause-effect power:
512+ the tradeoff between differentiation and specification."
513+ """
514+
515+ II_CONFIG = dict (
516+ REPERTOIRE_DISTANCE = "INTRINSIC_INFORMATION" ,
517+ REPERTOIRE_DISTANCE_SPECIFICATION = "INTRINSIC_SPECIFICATION" ,
518+ REPERTOIRE_DISTANCE_DIFFERENTIATION = "INTRINSIC_DIFFERENTIATION" ,
519+ )
520+
521+ @staticmethod
522+ def _monad_subsystem (p ):
523+ """Single-node system that stays in current state with probability p."""
524+ import numpy as np
525+ from pyphi import Network , Subsystem
526+
527+ tpm = np .array ([[1 - p ], [p ]])
528+ cm = np .array ([[1 ]])
529+ network = Network (tpm , cm = cm )
530+ return Subsystem (network , state = (1 ,))
531+
532+ def test_monad_intrinsic_information (self ):
533+ """Example 1, Eq. 27: ii(s) = min{p*log(2p), -log(p)}.
534+
535+ At p=0.744 (near the optimal): i_diff ≈ i_spec ≈ 0.427.
536+ The paper reports φ_s = 0.427 (Figure 2C).
537+ """
538+ import numpy as np
539+ from pyphi .new_big_phi import system_intrinsic_information
540+
541+ p = 0.744
542+ subsystem = self ._monad_subsystem (p )
543+ i_diff_expected = - np .log2 (p )
544+ i_spec_expected = p * np .log2 (2 * p )
545+ ii_expected = min (i_diff_expected , i_spec_expected )
546+
547+ with config .override (** self .II_CONFIG ):
548+ sys_state = system_intrinsic_information (subsystem )
549+ # system_intrinsic_information uses INTRINSIC_SPECIFICATION,
550+ # so it returns i_spec
551+ i_spec_pyphi = float (sys_state .effect .intrinsic_information )
552+ assert i_spec_pyphi == pytest .approx (i_spec_expected , abs = 1e-6 )
553+ # Verify the analytical ii value matches the paper
554+ assert ii_expected == pytest .approx (0.427 , abs = 0.001 )
555+
556+ @pytest .mark .parametrize (
557+ "p,i_diff_expected,i_spec_expected" ,
558+ [
559+ (0.744 , 0.426625 , 0.426591 ), # crossover point (Figure 2C)
560+ (0.9 , 0.152003 , 0.763197 ), # high determinism
561+ (0.6 , 0.736966 , 0.157821 ), # high noise
562+ ],
563+ )
564+ def test_monad_i_diff_i_spec_tradeoff (self , p , i_diff_expected , i_spec_expected ):
565+ """Verify i_diff and i_spec values across the tradeoff curve (Figure 2C).
566+
567+ i_diff = -log2(p), i_spec = p*log2(2p) for a monad in its ON state.
568+ """
569+ import numpy as np
570+ from pyphi import direction , metrics
571+ from pyphi .new_big_phi import system_intrinsic_information
572+
573+ subsystem = self ._monad_subsystem (p )
574+
575+ with config .override (** self .II_CONFIG ):
576+ sys_state = system_intrinsic_information (subsystem )
577+ i_spec = float (sys_state .effect .intrinsic_information )
578+ assert i_spec == pytest .approx (i_spec_expected , abs = 1e-5 )
579+
580+ # Compute i_diff from forward repertoire
581+ fr = subsystem .forward_repertoire (
582+ direction .Direction .EFFECT ,
583+ subsystem .node_indices ,
584+ subsystem .node_indices ,
585+ None ,
586+ )
587+ i_diff = float (
588+ metrics .distribution .intrinsic_differentiation (
589+ fr , fr , state = subsystem .proper_state
590+ )
591+ )
592+ assert i_diff == pytest .approx (i_diff_expected , abs = 1e-5 )
0 commit comments