Source code for pyphi.measures.protocols

"""Protocol types for measure callables.

Four Protocol classes capture the shape diversity in pyphi's measure
machinery. Each registered measure satisfies exactly one of these
Protocols; the registries are typed against the corresponding Protocol.

- ``DistributionMeasure``: (p, q) -> float | DistanceResult.
  Distribution-to-distribution distance. Symmetric or asymmetric (see
  ``asymmetric`` attribute). Most implementations return a plain float,
  but several (ID, AID, L1, EMD, etc.) return DistanceResult.
- ``StateAwareMeasure``: (p, state) -> float | DistanceResult. Pointwise
  probability at a specified state. INTRINSIC_DIFFERENTIATION returns
  DistanceResult.
- ``CompositeMeasure``: (forward, partitioned, selectivity, \\*, state)
  -> DistanceResult. Multi-input measure returning rich metadata; used
  by GID / INTRINSIC_INFORMATION / INTRINSIC_SPECIFICATION at the
  system level.
- ``StatefulDistributionMeasure``: (p, q, state) -> float | DistanceResult.
  Two-distribution measure evaluated at a specified state. Both
  distributions contribute to the result. IIT_4.0_SMALL_PHI variants return
  DistanceResult.

Plain functions don't carry the class-level ``name``/``asymmetric``
attributes the runtime-checkable Protocols declare, so ``isinstance``
returns False for them. The ``satisfies_*`` helpers below capture the
structural intent by inspecting parameter names with
``inspect.signature``; the typed registries use these at registration
time.
"""

from __future__ import annotations

import functools
import inspect
from typing import TYPE_CHECKING
from typing import Protocol
from typing import runtime_checkable

from numpy.typing import ArrayLike

if TYPE_CHECKING:
    from collections.abc import Callable

    from pyphi.measures.distribution import DistanceResult


[docs] @runtime_checkable class DistributionMeasure(Protocol): """Distribution-to-distribution distance.""" name: str asymmetric: bool def __call__(self, p: ArrayLike, q: ArrayLike) -> float | DistanceResult: ...
[docs] @runtime_checkable class StateAwareMeasure(Protocol): """Pointwise probability at a specified state.""" name: str def __call__(self, p: ArrayLike, state: object) -> float | DistanceResult: ...
[docs] @runtime_checkable class CompositeMeasure(Protocol): """Multi-input measure returning DistanceResult metadata. ``applies_intrinsic_information_requirement`` is True only for ``INTRINSIC_INFORMATION``; it says the measure applies the intrinsic-information requirement (Mayner et al. 2026, Eq. 23), ``φ_s = min(φ_c, φ_e, ii(s))``. ``partition_measure`` names the measure used to score partitions when this composite is the system measure. ``None`` means "use self"; ``INTRINSIC_INFORMATION`` sets it to GID so that partition integration is computed with GID and the requirement is applied on top. """ name: str applies_intrinsic_information_requirement: bool partition_measure: CompositeMeasure | None def __call__( self, forward: ArrayLike, partitioned: ArrayLike, selectivity: ArrayLike | None = None, *, state: object | None = None, ) -> DistanceResult: ...
[docs] @runtime_checkable class StatefulDistributionMeasure(Protocol): """Two-distribution measure evaluated at a specified state. Both distributions contribute to the result; the state selects a single element from the resulting pointwise array. """ name: str def __call__( self, p: ArrayLike, q: ArrayLike, state: object, ) -> float | DistanceResult: ...
# --------------------------------------------------------------------------- # Structural classification helpers # --------------------------------------------------------------------------- def _required_params(func: Callable[..., object]) -> list[str]: """Return the names of required positional parameters (no default).""" sig = inspect.signature(func) return [ p.name for p in sig.parameters.values() if p.default is inspect.Parameter.empty and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) ] def _all_params(func: Callable[..., object]) -> list[str]: """Return all parameter names (required and optional).""" return list(inspect.signature(func).parameters.keys()) # These classifiers introspect ``func``'s signature, which is immutable, and # are called per partition on the hot path with stable registered-measure # singletons; memoizing by function identity collapses the repeated # ``inspect.signature`` work to one call per measure.
[docs] @functools.cache def satisfies_distribution_measure(func: Callable[..., object]) -> bool: """Return True if ``func`` has the (p, q) shape of a DistributionMeasure.""" return _required_params(func) == ["p", "q"]
[docs] @functools.cache def satisfies_state_aware_measure(func: Callable[..., object]) -> bool: """Return True if ``func`` has the (p, state) shape of a StateAwareMeasure.""" return _required_params(func) == ["p", "state"]
[docs] @functools.cache def satisfies_composite_measure(func: Callable[..., object]) -> bool: """Return True if ``func`` has the (forward, partitioned, selectivity, ...) shape of a CompositeMeasure. Matches on parameter-name substrings to permit the canonical PyPhi spellings (``forward_repertoire``, ``partitioned_forward_repertoire``, ``selectivity_repertoire``). """ params = _all_params(func) if len(params) < 3: return False return ( "forward" in params[0] and "partitioned" in params[1] and "selectivity" in params[2] )
[docs] @functools.cache def satisfies_stateful_distribution_measure(func: Callable[..., object]) -> bool: """Return True if ``func`` has the (p, q, state) shape of a StatefulDistributionMeasure.""" return _required_params(func) == ["p", "q", "state"]