Source code for pyphi.conf.snapshot

"""Frozen snapshot of the three config layers, attached to result objects.

A :class:`ConfigSnapshot` mirrors the live ``pyphi.config`` shape but is
immutable: once a result object carries a snapshot, mutating the live
global doesn't change the snapshot's view of what produced the result.
"""

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass
from dataclasses import fields
from typing import Any

from pyphi.conf.formalism import ActualCausationConfig
from pyphi.conf.formalism import FormalismConfig
from pyphi.conf.formalism import IITConfig
from pyphi.conf.infrastructure import InfrastructureConfig
from pyphi.conf.numerics import NumericsConfig


[docs] @dataclass(frozen=True) class ConfigSnapshot: """Immutable snapshot of the three config layers at construction time. Result objects carry one of these so reproducibility is self-contained: rerunning a saved result is ``pyphi.config.override(**snap.as_overrides())``, which restores every field — including the formalism version and other fields whose bare names collide between the IIT and actual-causation sub-namespaces. """ formalism: FormalismConfig infrastructure: InfrastructureConfig numerics: NumericsConfig
[docs] def diff(self, other: ConfigSnapshot) -> dict[str, tuple[Any, Any]]: """Return ``{dotted-path: (self_value, other_value)}`` for fields that differ. Walks one level deeper into ``formalism`` so nested IIT and AC sub-namespace fields surface with their qualified paths (``formalism.iit.mechanism_phi_measure``, ...). """ result: dict[str, tuple[Any, Any]] = {} for layer_name in ("infrastructure", "numerics"): self_layer = getattr(self, layer_name) other_layer = getattr(other, layer_name) for f in fields(self_layer): self_val = getattr(self_layer, f.name) other_val = getattr(other_layer, f.name) if self_val != other_val: result[f"{layer_name}.{f.name}"] = (self_val, other_val) for sub_name in ("iit", "actual_causation"): self_sub = getattr(self.formalism, sub_name) other_sub = getattr(other.formalism, sub_name) for f in fields(self_sub): self_val = getattr(self_sub, f.name) other_val = getattr(other_sub, f.name) if self_val != other_val: result[f"formalism.{sub_name}.{f.name}"] = (self_val, other_val) return result
[docs] def as_overrides(self) -> dict[str, Any]: """Return a full-fidelity override mapping for this snapshot. Infrastructure and numerics fields appear under their flat names; every formalism field appears under its qualified dotted path (``iit.version``, ``actual_causation.mechanism_partition_scheme``, ...), so — unlike :meth:`as_kwargs` — fields whose bare names collide between the two formalism sub-namespaces are included. ``pyphi.config.override(**snap.as_overrides())`` reproduces the snapshotted configuration exactly. """ result: dict[str, Any] = {} for layer in (self.infrastructure, self.numerics): for f in fields(layer): result[f.name] = getattr(layer, f.name) for sub_name in ("iit", "actual_causation"): sub = getattr(self.formalism, sub_name) for f in fields(sub): result[f"{sub_name}.{f.name}"] = getattr(sub, f.name) return result
[docs] @classmethod def from_builtins(cls, data: Mapping[str, Any]) -> ConfigSnapshot: """Rehydrate a snapshot from its plain-builtins (dict) form. Inverse of ``msgspec.to_builtins`` on a snapshot, as used by the serialization layer. Unknown field names are ignored and missing fields take their defaults, so payloads written by other PyPhi versions still load. """ def build(layer_cls: type, layer_data: Mapping[str, Any]) -> Any: names = {f.name for f in fields(layer_cls)} return layer_cls(**{k: v for k, v in layer_data.items() if k in names}) formalism_data = data.get("formalism", {}) return cls( formalism=FormalismConfig( iit=build(IITConfig, formalism_data.get("iit", {})), actual_causation=build( ActualCausationConfig, formalism_data.get("actual_causation", {}) ), ), infrastructure=build(InfrastructureConfig, data.get("infrastructure", {})), numerics=build(NumericsConfig, data.get("numerics", {})), )
[docs] def as_kwargs(self) -> dict[str, Any]: """Return the snapshot's non-colliding fields as a flat kwargs dict. Field names that collide between the formalism's IIT and AC sub-namespaces (e.g. ``version``, ``mechanism_partition_scheme``) are excluded by :meth:`FormalismConfig.as_kwargs` — flat overrides on those names are ambiguous and :class:`pyphi.conf._global._GlobalConfig.__setattr__` rejects them. Because the colliding fields are excluded, the flat form does NOT reproduce a snapshot whose formalism differs from the ambient default: ``pyphi.config.override(**snap.as_kwargs())`` leaves ``version`` at the ambient value, and config validation rejects the resulting mix (e.g. an IIT 3.0 snapshot's measures under the default version). To reproduce a snapshot, use ``pyphi.config.override(**snap.as_overrides())``. """ result: dict[str, Any] = {} for layer in (self.infrastructure, self.numerics): for f in fields(layer): result[f.name] = getattr(layer, f.name) result.update(self.formalism.as_kwargs()) return result
__all__ = [ "ActualCausationConfig", "ConfigSnapshot", "FormalismConfig", "IITConfig", "InfrastructureConfig", "NumericsConfig", ]