Source code for pyphi.formalism.base

# pyright: strict
"""Protocols and registry for phi formalisms.

A *formalism* is a strategy for computing integrated information. Each
formalism bundles a partition scheme, the distance measures it accepts, and
algorithms that combine them into mechanism-level RIAs, system-level SIAs,
and Φ-structures.

This module declares the abstract Protocol shape and the global registry.
Concrete implementations live in ``pyphi.formalism.iit3`` and
``pyphi.formalism.iit4``.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import Any
from typing import ClassVar
from typing import Literal
from typing import Protocol
from typing import runtime_checkable

from pyphi.registry import InstanceRegistry

if TYPE_CHECKING:
    pass

__all__ = [
    "ACTUAL_CAUSATION_FORMALISM_REGISTRY",
    "FORMALISM_REGISTRY",
    "ActualCausationFormalism",
    "ActualCausationFormalismRegistry",
    "ApproximateFormalism",
    "ErrorInfo",
    "ExactFormalism",
    "FormalismRegistry",
    "MeasureNotCompatibleError",
    "PhiFormalism",
    "check_background_conditioning_compatible",
    "check_measure_compatible",
    "check_mechanism_partition_scheme_compatible",
    "check_sia_tie_strategy_compatible",
    "check_specification_measure_compatible",
]


[docs] @runtime_checkable class PhiFormalism(Protocol): """The minimum shape every formalism satisfies. Concrete formalisms also declare: - ``name``: stable string identifier used in ``config.formalism.iit.version`` and registered in :data:`FORMALISM_REGISTRY`. - ``compatible_measures``: frozenset of measure names that this formalism accepts. - ``uses_system_phi_measure``: whether the formalism reads ``config.formalism.iit.system_phi_measure`` (IIT 3.0 derives system phi from the cause-effect-structure distance and never reads it). - ``partition_scheme``: name (string) of the partition scheme registered in ``pyphi.partition.partition_types`` to use by default. May be ``None`` for approximation methods that bypass partitions. - ``compatible_system_partition_schemes``: frozenset of system partition scheme names this formalism accepts, or ``None`` if it accepts any registered scheme. - ``compatible_mechanism_partition_schemes``: frozenset of mechanism partition scheme names this formalism accepts, or ``None`` if it accepts any registered scheme. - ``compatible_sia_tie_strategies``: frozenset of SIA tie-resolution strategy names this formalism's SIA result type supports, or ``None`` if it supports every registered strategy. - ``compatible_specification_measures``: frozenset of measure names this formalism accepts for ``config.formalism.iit.specification_measure``, or ``None`` for formalisms without a specified-state phase (IIT 3.0 never consults the field). - ``compatible_background_conditioning``: frozenset of cause-side background-conditioning conventions this formalism defines, or ``None`` if it accepts any registered convention. - ``applies_intrinsic_information_requirement``: whether the formalism is defined by the intrinsic-information requirement (Eq. 23 of Mayner et al. 2026) and therefore requires a system measure whose attribute of the same name is True. Signatures are permissive (``Any``) over the measure and partition arguments. """ name: ClassVar[str] compatible_measures: ClassVar[frozenset[str]] uses_system_phi_measure: ClassVar[bool] partition_scheme: ClassVar[str | None] compatible_system_partition_schemes: ClassVar[frozenset[str] | None] compatible_mechanism_partition_schemes: ClassVar[frozenset[str] | None] def evaluate_mechanism( self, system: Any, direction: Any, mechanism: Any, purview: Any, **kwargs: Any ) -> Any: ... def evaluate_mechanism_partition( self, system: Any, direction: Any, mechanism: Any, purview: Any, partition: Any, **kwargs: Any, ) -> Any: ... def evaluate_system(self, system: Any, **kwargs: Any) -> Any: ... def build_ces(self, system: Any, **kwargs: Any) -> Any: ...
[docs] class MeasureNotCompatibleError(Exception): """Raised when a configured measure isn't compatible with the active formalism. Each :class:`PhiFormalism` declares ``compatible_measures`` as a frozenset of measure names it accepts. Combinations outside that set (e.g., the IIT 4.0 formalism with the EMD distribution measure) compute a different mathematical object than the formalism's ``φ`` definition; rejecting them early prevents silently misleading results. """
[docs] def check_measure_compatible(formalism: PhiFormalism, measure: str) -> None: """Raise :class:`MeasureNotCompatibleError` if ``measure`` isn't accepted by ``formalism``. Called from each formalism's ``evaluate_*`` methods so the failure surface is at the dispatch boundary, not deep inside the math. """ if measure not in formalism.compatible_measures: raise MeasureNotCompatibleError( f"Measure {measure!r} is not compatible with " f"formalism {formalism.name!r}. Compatible measures for " f"this formalism: {sorted(formalism.compatible_measures)}. " "If you want a different measure, set " "config.formalism.iit.version to one whose compatible_measures " "set contains it." )
[docs] def check_sia_tie_strategy_compatible(formalism: PhiFormalism, strategy: Any) -> None: """Raise ``ConfigurationError`` if a SIA tie-resolution strategy uses a component the formalism's SIA result type does not support. Called from a formalism's ``evaluate_system`` so a config assembled by per-field assignment (which skips cross-field validation) still fails at the dispatch boundary with a clear diagnostic, not with an ``AttributeError`` deep inside tie resolution. Formalisms whose SIA type supports every registered strategy declare ``compatible_sia_tie_strategies = None`` and are not checked. """ compatible = getattr(formalism, "compatible_sia_tie_strategies", None) if compatible is None: return components = (strategy,) if isinstance(strategy, str) else tuple(strategy) for component in components: if component not in compatible: from pyphi.conf import ConfigurationError raise ConfigurationError( f"formalism.iit.sia_tie_resolution component {component!r} is " f"not compatible with formalism {formalism.name!r}. Compatible " f"SIA tie strategies for this formalism: {sorted(compatible)}. " f"Fix: set formalism.iit.sia_tie_resolution to use only those " f"(the shipped preset uses ['PHI', 'PARTITION_LEX']), or " f"change formalism.iit.version." )
def _validate_config_enabled() -> bool: """Whether reactive dispatch-boundary config checks are enabled. Mirrors the eager constraint gate (``infrastructure.validate_config``) so the two validation surfaces share one opt-out. """ from pyphi.conf import config return bool(config.infrastructure.validate_config)
[docs] def check_specification_measure_compatible( formalism: PhiFormalism, measure: str ) -> None: """Raise :class:`MeasureNotCompatibleError` if ``measure`` isn't accepted by ``formalism`` as a specification measure. Called from the formalism's measure-resolution sites so a config assembled by per-field assignment (which skips cross-field validation) still fails at the dispatch boundary rather than silently computing a different Φ. Formalisms without a specified-state phase declare ``compatible_specification_measures = None`` and are not checked. Respects the ``validate_config=False`` opt-out, so a deliberately unsupported combination can still be studied. """ compatible = getattr(formalism, "compatible_specification_measures", None) if compatible is None: return if not _validate_config_enabled(): return if measure not in compatible: raise MeasureNotCompatibleError( f"Specification measure {measure!r} is not compatible with " f"formalism {formalism.name!r}. Compatible specification " f"measures for this formalism: {sorted(compatible)}. Fix: set " f"config.formalism.iit.specification_measure to one of those, " f"or change config.formalism.iit.version." )
[docs] def check_background_conditioning_compatible( formalism: PhiFormalism, value: str ) -> None: """Raise ``ConfigurationError`` if the cause-side background-conditioning convention is one ``formalism`` does not define. Called from a formalism's ``evaluate_system`` so a config assembled by per-field assignment (which skips cross-field validation) still fails at the dispatch boundary rather than silently computing a different phi on proper-subset systems. Formalisms that accept any registered convention declare ``compatible_background_conditioning = None`` and are not checked. Respects the ``validate_config=False`` opt-out, so a deliberately unsupported combination can still be studied. """ compatible = getattr(formalism, "compatible_background_conditioning", None) if compatible is None: return if not _validate_config_enabled(): return if value not in compatible: from pyphi.conf import ConfigurationError raise ConfigurationError( f"background_conditioning {value!r} is not compatible with " f"formalism {formalism.name!r}. Compatible conventions for this " f"formalism: {sorted(compatible)}. Fix: set " f"formalism.iit.background_conditioning to one of those (the " f"shipped IIT 3.0 preset uses 'CONDITION_CURRENT_STATE'), or " f"change formalism.iit.version." )
[docs] def check_mechanism_partition_scheme_compatible( formalism: PhiFormalism, scheme: str ) -> None: """Raise ``ConfigurationError`` if a mechanism partition scheme is one ``formalism`` does not accept. Called from a formalism's evaluation methods so a config assembled by per-field assignment (which skips cross-field validation) still fails at the dispatch boundary rather than silently computing phi over a different partition family. Formalisms that accept any registered scheme declare ``compatible_mechanism_partition_schemes = None`` and are not checked. Respects the ``validate_config=False`` opt-out, so a deliberately unsupported combination can still be studied. """ compatible = getattr(formalism, "compatible_mechanism_partition_schemes", None) if compatible is None: return if not _validate_config_enabled(): return if scheme not in compatible: from pyphi.conf import ConfigurationError raise ConfigurationError( f"formalism.iit.mechanism_partition_scheme={scheme!r} is not " f"compatible with formalism {formalism.name!r}. Compatible " f"mechanism partition schemes for this formalism: " f"{sorted(compatible)}. Fix: set " f"formalism.iit.mechanism_partition_scheme to one of those, or " f"change formalism.iit.version." )
[docs] class ErrorInfo(Protocol): """Error characterization for an approximate formalism's output. Discriminates the three flavors of approximation: - ``upper_bound``: result is a guaranteed upper bound on the true value (e.g., Zaeemzadeh-style certified pruning). - ``approximation_error``: result approximates the true value with a bounded error. - ``different_quantity``: result computes a related but distinct quantity (e.g., φ* vs Φ). """ kind: Literal["upper_bound", "approximation_error", "different_quantity"] bound: float | None notes: str
[docs] @runtime_checkable class ExactFormalism(PhiFormalism, Protocol): """Formalism that computes exact values via exhaustive enumeration.""" exact: Literal[True]
[docs] @runtime_checkable class ApproximateFormalism(PhiFormalism, Protocol): """Formalism that computes approximate values with error characterization. Its ``exact`` attribute is False, and ``error_info`` characterizes the approximation. """ exact: Literal[False] def error_characterization(self, system: Any) -> ErrorInfo: ...
[docs] class FormalismRegistry(InstanceRegistry[PhiFormalism]): """Storage for phi formalisms. Validates registered objects against the :class:`PhiFormalism` Protocol so wrong-shape registrations fail at import. Concrete formalisms register themselves at the bottom of their module file: .. code-block:: python FORMALISM_REGISTRY.register("IIT_4_0_2023", IIT4_2023Formalism()) Lookup returns the registered formalism instance; the same string identifier ``config.formalism.iit.version`` holds is used as the key. """ desc = "phi formalisms"
[docs] def register(self, name: str, instance: object) -> PhiFormalism: """Register a formalism instance under ``name``, validating its shape. ``instance`` is typed ``object`` so the runtime Protocol check below is meaningful even for callers who do not run a type-checker; a wrong-shape object fails at import. """ if not isinstance(instance, PhiFormalism): raise TypeError( f"Cannot register {instance!r} as formalism {name!r}: " "object does not satisfy the PhiFormalism Protocol." ) return super().register(name, instance)
FORMALISM_REGISTRY: FormalismRegistry = FormalismRegistry() """Global registry of phi formalisms. Looked up by string name (the value held in ``config.formalism.iit.version``)."""
[docs] @runtime_checkable class ActualCausationFormalism(Protocol): """The minimum shape every actual-causation formalism satisfies. The AC analog of :class:`PhiFormalism`. AC operates on transitions (before/after state pairs) rather than systems-in-a-state, so its evaluation surface differs: ``evaluate_account`` / ``evaluate_system`` / ``evaluate_mechanism`` / ``evaluate_causal_link``. Concrete formalisms also declare: - ``name``: stable identifier held in ``config.formalism.actual_causation.version`` and registered in :data:`ACTUAL_CAUSATION_FORMALISM_REGISTRY`. - ``compatible_measures``: frozenset of alpha-measure names accepted. - ``config``: the :class:`FormalismConfig` snapshot operated against. Signatures are intentionally permissive (``Any``), matching :class:`PhiFormalism`. """ name: ClassVar[str] compatible_measures: ClassVar[frozenset[str]] def evaluate_account( self, transition: Any, direction: Any, **kwargs: Any ) -> Any: ... def evaluate_system(self, transition: Any, direction: Any, **kwargs: Any) -> Any: ... def evaluate_mechanism( self, transition: Any, direction: Any, mechanism: Any, purview: Any, **kwargs: Any, ) -> Any: ... def evaluate_causal_link( self, transition: Any, direction: Any, mechanism: Any, **kwargs: Any ) -> Any: ...
[docs] class ActualCausationFormalismRegistry(InstanceRegistry[ActualCausationFormalism]): """Storage for actual-causation formalisms. Validates registrations against :class:`ActualCausationFormalism` so wrong-shape registrations fail at import. Lookup returns the registered instance, keyed by the string held in ``config.formalism.actual_causation.version``. Parallel to :class:`FormalismRegistry` / :class:`ActualCausationMeasureRegistry`. """ desc = "actual-causation formalisms"
[docs] def register(self, name: str, instance: object) -> ActualCausationFormalism: if not isinstance(instance, ActualCausationFormalism): raise TypeError( f"Cannot register {instance!r} as AC formalism {name!r}: " "object does not satisfy the ActualCausationFormalism Protocol." ) return super().register(name, instance)
ACTUAL_CAUSATION_FORMALISM_REGISTRY: ActualCausationFormalismRegistry = ( ActualCausationFormalismRegistry() ) """Global registry of actual-causation formalisms. Looked up by the string held in ``config.formalism.actual_causation.version``."""