Source code for pyphi.analysis

"""One high-level entry point for IIT analysis.

``analyze`` takes a substrate and a state, builds the candidate system, runs
the analysis, and returns an :class:`Analysis` — a small bundle exposing the
system irreducibility analysis, the cause-effect structure, and their scalar
values. A ``compute`` argument selects a cheaper or custom result instead of
the bundle; a ``grains`` argument runs the bounded intrinsic-unit search over
the whole substrate instead, returning its complexes.
"""

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import replace
from typing import Any

import pandas as pd

from pyphi import numerics
from pyphi.conf import presets
from pyphi.conf.formalism import IITConfig
from pyphi.display import FULL
from pyphi.display import Description
from pyphi.display import Displayable
from pyphi.display import Row
from pyphi.display import Section
from pyphi.display import system_phi_label
from pyphi.display.numbers import format_value
from pyphi.models.ces import CauseEffectStructure
from pyphi.relations import NullRelations
from pyphi.serializable import Serializable
from pyphi.system import System


[docs] @dataclass(frozen=True, repr=False) class Analysis(Displayable, Serializable): """A single system's analysis: its SIA, its CES, and their scalar values. The cause-effect structure embeds the system irreducibility analysis, and ``sia``, ``ces``, and ``phi`` are always populated. Notes ----- ``phi`` and ``big_phi`` are different quantities. ``phi`` is φₛ, the system integrated information, which decides whether the system exists as one whole; ``big_phi`` is Φ, the structure integrated information, the sum of φ over the Φ-structure's distinctions and relations. A system can have φₛ = 0 — it is reducible — while its distinctions still sum to a nonzero Φ. For results computed under an earlier version of IIT (see :doc:`/howto/earlier-versions`): under IIT 3.0 the CES has no relations, the system-level quantity ``phi`` is that version's Φ, and ``big_phi`` is not defined. """ system: System sia: Any ces: CauseEffectStructure @property def phi(self) -> float: """float: φₛ, the system integrated information.""" return float(self.sia.phi) @property def formalism(self) -> str: """str: The version of IIT that produced this analysis: ``"IIT_4_0_2026"`` (the default), ``"IIT_4_0_2023"``, or ``"IIT_3_0"``. Read from the configuration snapshot the system irreducibility analysis carries.""" return self.sia.config.formalism.iit.version @property def _phi_label(self) -> str: return system_phi_label(self.ces.config) @property def big_phi(self) -> float: """float: Φ, the structure integrated information — the sum of φ over the Φ-structure's distinctions and relations. Raises ------ AttributeError For a result computed under IIT 3.0, which has no relations and so no structure integrated information. That version's Φ is :attr:`phi`. """ return float(self.ces.big_phi) def _describe(self, verbosity: int) -> Description: # Reuse the cause-effect structure's flat rich card, which folds in # the embedded SIA at FULL verbosity. desc = self.ces._describe(verbosity) sections = list(desc.sections) # A result computed under an earlier version of IIT closes its summary # with that version. The version is always recorded on ``.formalism`` # and in serialized results. if self.formalism != IITConfig.version: first = sections[0] sections[0] = replace( first, rows=(*first.rows, Row("Formalism", self.formalism)) ) if verbosity < FULL and getattr(self.sia, "partition", None) is not None: # Below FULL the structure card does not embed the SIA; show the # system-level facts a reader needs to interpret φₛ. sections.append(Section(label="System", rows=self._system_rows())) phi_label = self._phi_label return Description( title="Analysis", sections=tuple(sections), compact=f"Analysis({phi_label}={format_value(self.phi)})", ) def _system_rows(self) -> tuple[Row, ...]: from pyphi.models.partitions import concise_partition rows = [Row("Units", self.sia._system_label())] if self.sia.current_state is not None: rows.append(Row("Current state", self.sia.current_state)) state = getattr(self.sia, "system_state", None) if state is not None: if state.cause is not None: rows.append(Row("Specified cause state", state.cause.state)) if state.effect is not None: rows.append(Row("Specified effect state", state.effect.state)) rows.append(Row("MIP", concise_partition(self.sia.partition))) # A null result records why φₛ is zero without evaluating partitions. rows.extend( Row(f"Why {self._phi_label} = 0", reason.description) for reason in self.sia.reasons or () ) # ii(s) enters φₛ only where the intrinsic-information requirement # applies (Mayner et al. 2026, Eq. 23); IIT 4.0 as published in 2023 # does not use it. ii = getattr(self.sia, "intrinsic_information", None) if ii is not None and self.sia._applies_requirement(): rows.append(Row("ii(s)", ii)) findings = {f.kind: f for f in self.sia.explain().findings} binding = findings.get("requirement_binding") if binding is not None: direction = dict(binding.detail)["direction"] rows.append(Row("Requirement binds", f"{binding.value} ({direction})")) # An irreducible system whose φₛ is zero only because it has no # intrinsic information: say so, since nothing else on the card does. sides = dict(getattr(findings.get("binding_direction"), "detail", ())) if ( binding is not None and binding.value == "differentiation" and numerics.is_zero(ii) and sides and numerics.is_positive(min(sides.values())) ): rows.append( Row(f"Why {self._phi_label} = 0", "no repertoire of alternatives") ) return tuple(rows)
[docs] def to_pandas(self) -> pd.DataFrame: """A one-row DataFrame that identifies the analysis and gives its values. The identifiers are ``formalism`` (the version name), ``subset`` (the system's sorted unit indices) and ``state`` (the substrate's state), as in a sweep's rows, and ``current_state`` (the system's state), as in the SIA's own row. The values are φₛ (``phi``), ``normalized_phi``, Φ (``big_phi``), ``n_distinctions``, ``sum_phi_d`` and ``sum_phi_r``, the same columns as a sweep's rows for ``compute="ces"`` or ``compute=None``. """ record = { "formalism": self.formalism, "subset": tuple(int(i) for i in self.system.node_indices), "state": tuple(int(x) for x in self.system.state), "current_state": tuple(int(x) for x in self.system.proper_state), **_structure_record(self.sia, self.ces), } return pd.DataFrame([record])
def _structure_record(sia: Any, ces: CauseEffectStructure) -> dict[str, Any]: """The scalar summary of one system's analysis, keyed by column name. IIT 3.0 defines no relations and no structure integrated information, so there ``sum_phi_r`` and ``big_phi`` are NaN rather than zero. """ relations = ces.relations iit3 = system_phi_label(ces.config) == "Φ" return { "phi": float(sia.phi), "normalized_phi": float(getattr(sia, "normalized_phi", float("nan"))), "big_phi": float("nan") if iit3 else float(ces.big_phi), "n_distinctions": len(ces.distinctions), "sum_phi_d": float(ces.distinctions.sum_phi()), "sum_phi_r": ( float("nan") if isinstance(relations, NullRelations) else float(relations.sum_phi()) ), } _COMPUTE_NAMES = ("sia", "ces", "distinctions") def _check_compute(compute: Any) -> None: """Raise ``ValueError`` unless ``compute`` is in the shared vocabulary: ``None``, a name in ``_COMPUTE_NAMES``, or a callable.""" if compute is None or callable(compute) or compute in _COMPUTE_NAMES: return raise ValueError( f"unknown compute: {compute!r}; expected 'sia', 'ces', 'distinctions', " "a callable, or None for the full Analysis bundle" ) def _run_compute(system: System, compute: Any) -> Any: """Compute what ``compute`` selects for ``system``; see :func:`analyze`.""" _check_compute(compute) if callable(compute): return compute(system) if compute == "sia": return system.sia() if compute == "ces": return system.ces() if compute == "distinctions": return system.distinctions(congruent=True) ces = system.ces() return Analysis(system=system, sia=ces.sia, ces=ces)
[docs] def analyze( substrate: Any, state: tuple[int, ...], *, subset: Any = None, formalism: str | None = None, compute: Any = None, grains: Any = None, parallel: bool | None = None, progress: bool | None = None, ) -> Analysis | Any: """Analyze one candidate system over ``substrate`` in ``state``, or run a grain search over the whole substrate. Parameters ---------- substrate The substrate to analyze. state : tuple[int, ...] The state of the substrate's nodes. When the bounds' maximum micro grain (``max_update_grain ** max_depth``) exceeds 1, a sequence of that many micro states (oldest first) instead — see :func:`pyphi.macro.complexes`. subset : optional Node indices of the candidate system; ``None`` uses the whole substrate. Incompatible with ``grains``. formalism : str or None, optional Leave unset to compute IIT under the active configuration. Pass an earlier version's name (``"IIT_4_0_2023"`` or ``"IIT_3_0"``) only to reproduce published results; it applies to this call only. The name a preset is exported under (``"iit3"``) and the preset itself (``pyphi.iit3``) select the same version; a modified copy of a preset raises ``ValueError``, since this argument only selects published versions (apply modified settings with ``pyphi.config.override`` and leave ``formalism`` unset). ``"IIT_4_0_2026"`` names the default; passing it applies that version's complete preset, which differs from leaving ``formalism`` unset only if the active configuration has been customized. See :doc:`/howto/earlier-versions`. compute : optional ``None`` returns an :class:`Analysis` bundle; ``"sia"``, ``"ces"``, or ``"distinctions"`` returns the raw result object; a callable returns ``compute(system)``. Incompatible with ``grains``. ``"distinctions"`` skips the system-partition search, which is the whole cost of a cause-effect structure over a sparse substrate; see :meth:`~pyphi.system.System.distinctions`. grains : optional ``None`` or ``False`` analyzes the single candidate system. ``True`` runs the bounded intrinsic-unit search with default :class:`~pyphi.macro.SearchBounds`; a :class:`~pyphi.macro.SearchBounds` instance runs it with those bounds. The search returns its :class:`~pyphi.macro.ComplexesResult`. parallel : bool or None, optional ``None`` follows the configuration. ``True`` runs this call in parallel: a single system at the system-partition, purview and distinction levels, a grain search over its candidate systems. ``False`` runs it sequentially. Finer control is through the per-level options in ``config.infrastructure``; see :doc:`/howto/parallel`. progress : bool or None, optional Show progress bars; ``None`` follows ``config.infrastructure.progress_bars``. Returns ------- Analysis The full analysis bundle; the raw result object when ``compute`` selects one; or the grain search's :class:`~pyphi.macro.ComplexesResult` when ``grains`` is set. Raises ------ ValueError If ``formalism`` is not a known version name; if ``compute`` is not ``"sia"``, ``"ces"``, ``"distinctions"``, a callable, or ``None``; if ``grains`` is not a bool, a :class:`~pyphi.macro.SearchBounds`, or ``None``; or if ``grains`` is combined with ``subset`` or ``compute``. """ from pyphi.conf import config from pyphi.conf._helpers import parallel_overrides formalism = presets.canonical(formalism) _check_compute(compute) bounds: Any = None if grains is not None and grains is not False: from pyphi.macro.search import SearchBounds if subset is not None: raise ValueError( "grains cannot be combined with subset: the grain search " "assembles candidate systems over the whole universe" ) if compute is not None: raise ValueError( "grains cannot be combined with compute: the grain search " "returns its ComplexesResult" ) if grains is True: bounds = SearchBounds() elif isinstance(grains, SearchBounds): bounds = grains else: raise ValueError( f"grains must be True, False, or a SearchBounds instance; got {grains!r}" ) from pyphi.conf._helpers import check_progress_flag progress = check_progress_flag(progress) progress_override = {} if progress is None else {"progress_bars": progress} with presets.use(formalism), config.override(**progress_override): if bounds is not None: from pyphi.macro.search import complexes as grain_complexes return grain_complexes( substrate, state, bounds=bounds, parallel=parallel, progress=progress ) with config.override(**parallel_overrides(parallel)): return _run_compute(System(substrate, state, subset=subset), compute)