"""System value type — ``(Substrate, state, node_subset, partition)``.
A System is the unit of analysis for IIT: a substrate evaluated in a specific
state over a specific subset of its nodes, optionally with a partition
applied. Immutable, hashable. The partition is a constructor argument
(default :class:`NullCut`, i.e. no edges severed), not a hidden mode.
"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass
from dataclasses import field
from functools import cached_property
from typing import TYPE_CHECKING
from typing import Any
import numpy as np
from pyphi import connectivity
from pyphi import utils
from pyphi import validate
from pyphi.conf.formalism import _VALID_BACKGROUND_CONDITIONING
from pyphi.display import HIGH
from pyphi.display import LOW
from pyphi.display import Description
from pyphi.display import Displayable
from pyphi.display import Row
from pyphi.display import Section
from pyphi.models.pandas import ToPandasMixin
from pyphi.models.partitions import DirectedBipartition
from pyphi.models.partitions import NullCut
from pyphi.models.partitions import concise_partition
from pyphi.serializable import Serializable
from pyphi.substrate import Substrate
from pyphi.substrate import _coerce_state_to_indices
from .core.tpm.factored import FactoredTPM
from .core.tpm.marginalization import CauseMarginals
from .core.tpm.marginalization import _effect_marginal_factored
from .core.tpm.marginalization import cause_conditioned as _condition_cause
from .core.tpm.marginalization import cause_marginal as _marginalize_cause
if TYPE_CHECKING:
from pyphi.types import NodeIndices
from pyphi.types import State
[docs]
@dataclass(frozen=True, eq=False, repr=False)
class System(Displayable, ToPandasMixin, Serializable):
"""A substrate evaluated in a specific state over a node subset, with partition.
The ``external_indices`` field specifies which substrate units are the
background when computing repertoires. When ``None`` (the default), it
resolves in ``__post_init__`` to ``substrate - node_indices``, the
background units W = U \\ S, which under the default convention are
causally marginalized conditional on the current state (Albantakis et al.
2023, Eqs. 3-4). An explicit override (used by
``TransitionSystem`` for actual-causation analysis) may overlap with
``node_indices``.
The ``background_conditioning`` field pins this System to one
cause-side background convention. ``None`` (the default) resolves
``config.formalism.iit.background_conditioning`` at compute time.
The ``background_state`` field supplies the state at which external
units are conditioned, when that differs from ``state``. ``None``
(the default) conditions them at ``state``. Used by
``TransitionSystem`` for actual causation, where the cause direction
evaluates mechanisms against the observed after-state while the
background is fixed at the observed before-state.
"""
substrate: Substrate
state: State
node_indices: NodeIndices = field(default=None) # type: ignore[assignment]
partition: DirectedBipartition = field(default=None) # type: ignore[assignment]
external_indices: tuple[int, ...] = field(default=None) # type: ignore[assignment]
background_conditioning: str | None = field(default=None)
background_state: tuple[int, ...] | None = field(default=None)
def __post_init__(self) -> None:
substrate = self.substrate
# Coerce label-states to integer indices before length/value validation.
coerced = _coerce_state_to_indices(tuple(self.state), substrate.state_space)
object.__setattr__(self, "state", coerced)
validate.state_length(self.state, substrate.size)
validate.node_states(self.state, substrate.factored_tpm.alphabet_sizes)
if self.node_indices is None:
object.__setattr__(self, "node_indices", tuple(range(substrate.size)))
else:
object.__setattr__(
self,
"node_indices",
substrate.node_labels.coerce_to_indices(self.node_indices),
)
if self.partition is None:
object.__setattr__(
self, "partition", NullCut(self.node_indices, substrate.node_labels)
)
else:
cut_idx = getattr(self.partition, "indices", None)
if cut_idx is not None and set(cut_idx) != set(self.node_indices):
raise ValueError(
f"{self.partition} nodes are not equal to "
f"system nodes {self.node_indices}"
)
# Resolve external_indices: None resolves to substrate-minus-system
# (the IIT 4.0 extended-background convention). An explicit override
# is validated for shape, then accepted as-is.
if self.external_indices is None:
all_indices = set(range(substrate.size))
object.__setattr__(
self,
"external_indices",
tuple(sorted(all_indices - set(self.node_indices))),
)
else:
ext = tuple(self.external_indices)
for i in ext:
if not (0 <= i < substrate.size):
raise ValueError(
f"external_indices contains out of range index {i}; "
f"must satisfy 0 <= i < {substrate.size}"
)
if list(ext) != sorted(ext):
raise ValueError(f"external_indices must be sorted; got {ext}")
if len(set(ext)) != len(ext):
raise ValueError(
f"external_indices must not contain duplicates; got {ext}"
)
object.__setattr__(self, "external_indices", ext)
if (
self.background_conditioning is not None
and self.background_conditioning not in _VALID_BACKGROUND_CONDITIONING
):
raise ValueError(
f"background_conditioning={self.background_conditioning!r} "
f"not in {sorted(_VALID_BACKGROUND_CONDITIONING)} (or None)"
)
if self.background_state is not None:
coerced_bg = _coerce_state_to_indices(
tuple(self.background_state), substrate.state_space
)
validate.state_length(coerced_bg, substrate.size)
validate.node_states(coerced_bg, substrate.factored_tpm.alphabet_sizes)
object.__setattr__(self, "background_state", coerced_bg)
from pyphi.conf import config as _config
if _config.infrastructure.validate_system_states:
validate.state_reachable(self)
def __lt__(self, other: object) -> bool:
if not isinstance(other, System):
return NotImplemented
return len(self.node_indices) < len(other.node_indices)
def __le__(self, other: object) -> bool:
if not isinstance(other, System):
return NotImplemented
return len(self.node_indices) <= len(other.node_indices)
def __gt__(self, other: object) -> bool:
if not isinstance(other, System):
return NotImplemented
return len(self.node_indices) > len(other.node_indices)
def __ge__(self, other: object) -> bool:
if not isinstance(other, System):
return NotImplemented
return len(self.node_indices) >= len(other.node_indices)
def __eq__(self, other: object) -> bool:
if not isinstance(other, System):
return NotImplemented
return (
self.substrate == other.substrate
and self.state == other.state
and self.node_indices == other.node_indices
and self.partition == other.partition
and self.external_indices == other.external_indices
and self.background_conditioning == other.background_conditioning
and self.background_state == other.background_state
)
def __hash__(self) -> int:
return hash(
(
self.substrate,
self.state,
self.node_indices,
self.partition,
self.external_indices,
self.background_conditioning,
self.background_state,
)
)
@cached_property
def _fingerprint(self) -> bytes:
"""blake2b-256 digest of the label-free system math identity.
Serializes exactly the components :meth:`__eq__` compares: the substrate
math fingerprint, the index-coerced state, the node and external indices,
the partition's mathematical content (``indices`` + ``removed_edges``),
and the pinned background convention. Used as the repertoire kernel
cache key so distinct-but-equivalent systems share entries.
"""
h = hashlib.blake2b(digest_size=32)
h.update(self.substrate._fingerprint)
h.update(repr(tuple(self.state)).encode())
h.update(repr(tuple(self.node_indices)).encode())
h.update(repr(tuple(self.external_indices)).encode())
h.update(repr(tuple(sorted(self.partition.indices))).encode())
h.update(repr(sorted(self.partition.removed_edges())).encode())
h.update(repr(self.background_conditioning).encode())
h.update(repr(self.background_state).encode())
return h.digest()
def __len__(self) -> int:
return len(self.node_indices)
def _unit_labels(self) -> tuple[str, ...]:
"""The system's per-unit display labels (node names), in node order."""
return tuple(
str(label) for label in self.node_labels.coerce_to_labels(self.node_indices)
)
def _to_pandas(self):
import pandas as pd
labels = self._unit_labels()
rows = [
{"node": idx, "label": label, "state": self.state[idx]}
for label, idx in zip(labels, self.node_indices, strict=True)
]
return pd.DataFrame(rows)
def _describe(self, verbosity: int) -> Description:
from pyphi.core.tpm import _display
labels = list(self._unit_labels())
label_str = ", ".join(labels)
compact = f"System({label_str})"
if verbosity == LOW:
return Description(title="System", compact=compact)
state_str = ", ".join(
f"{label}={self.state[idx]}"
for label, idx in zip(labels, self.node_indices, strict=True)
)
rows = [
Row("Units", label_str),
Row("State", state_str),
Row("Substrate", f"{self.substrate.size} units"),
]
if self.external_indices:
ext_labels = self.node_labels.coerce_to_labels(self.external_indices)
rows.append(
Row(
"Background",
", ".join(
f"{label}={self.state[idx]}"
for label, idx in zip(
ext_labels, self.external_indices, strict=True
)
),
)
)
if not isinstance(self.partition, NullCut):
rows.append(Row("Cut", concise_partition(self.partition)))
subset_cm = self.substrate.cm[np.ix_(self.node_indices, self.node_indices)]
sections = [
Section(rows=tuple(rows)),
Section(
label="Connectivity",
body=(_display.connectivity_grid(labels, subset_cm),),
),
]
# The conditioned cause/effect marginals are the heavy part — compute
# them only at HIGH verbosity.
if verbosity >= HIGH:
cause = self.proper_cause_marginal.grid_section()
effect = self.proper_effect_marginal.grid_section()
sections.append(Section(label="Cause TPM", body=cause.body, tone="cause"))
sections.append(Section(label="Effect TPM", body=effect.body, tone="effect"))
return Description(
title="System",
subtitle=f"{len(self.node_indices)} units · {label_str}",
sections=tuple(sections),
compact=compact,
)
[docs]
def apply_cut(self, partition: DirectedBipartition) -> System:
"""Return a new System with the given partition applied.
``substrate``, ``state``, and ``node_indices`` are unchanged. The
cause/effect marginals depend only on those inputs — the cut enters
downstream through the cut connectivity matrix when node TPMs
marginalize out severed inputs — so the marginals are materialized
on this instance and shared with the new one rather than re-derived.
"""
from dataclasses import replace
_ = self.cause_marginal
_ = self.effect_marginal
new = replace(self, partition=partition)
for name in (
"_typed_tpm",
"_cause_marginals",
"effect_marginal",
"proper_effect_marginal",
):
if name in self.__dict__:
new.__dict__[name] = self.__dict__[name]
return new
[docs]
@classmethod
def from_substrate(
cls,
substrate: Substrate,
state: Any,
nodes: Any | None = None,
partition: DirectedBipartition | None = None,
**kwargs: Any,
) -> System:
"""Construct a System from a substrate, state, and optional node subset."""
if nodes is None:
nodes = tuple(range(substrate.size))
return cls(
substrate=substrate,
state=tuple(state),
node_indices=tuple(nodes),
partition=partition, # type: ignore[arg-type]
**kwargs,
)
# ---- cached cheap derived properties ----
@cached_property
def node_labels(self) -> Any:
return self.substrate.node_labels
[docs]
def to_networkx(self, connectivity: str = "inferred") -> Any:
"""Return the substrate's :class:`networkx.DiGraph` with node attributes.
Each node carries ``state`` and ``in_system``. Edges default to the
TPM-inferred causal connectivity (``connectivity="declared"`` for the
declared ``cm``).
"""
from pyphi import graph
return graph.system_to_networkx(self, connectivity)
@cached_property
def proper_state(self) -> Any:
return utils.state_of(self.node_indices, self.state)
@cached_property
def _typed_tpm(self) -> Any:
"""The canonical FactoredTPM stored on the substrate."""
return self.substrate.factored_tpm
def _resolved_background_conditioning(self) -> str:
"""The cause-side background convention in effect for this System:
the instance pin when set, else the live config value."""
if self.background_conditioning is not None:
return self.background_conditioning
from pyphi.conf import config as _config
return _config.formalism.iit.background_conditioning
@cached_property
def _cause_marginals(self) -> dict[str, CauseMarginals]:
"""Per-convention cause factors, computed on demand."""
return {}
@property
def _background_reference_state(self) -> tuple[int, ...]:
"""The state at which external units are conditioned: the explicit
``background_state`` when set, else ``state``."""
return self.background_state if self.background_state is not None else self.state
@property
def cause_marginal(self) -> CauseMarginals:
"""Per-system-unit cause factors under the active background
convention: the marginalization of Albantakis et al. (2023), Eq. 4, or
the background conditioned at its observed state
(``CONDITION_CURRENT_STATE``).
"""
convention = self._resolved_background_conditioning()
if convention not in self._cause_marginals:
if convention == "CONDITION_CURRENT_STATE":
external_state = utils.state_of(
self.external_indices, self._background_reference_state
)
background = dict(
zip(self.external_indices, external_state, strict=True)
)
marginals = _condition_cause(
self._typed_tpm, self.node_indices, background
)
else:
marginals = _marginalize_cause(
self._typed_tpm,
self.state,
self.node_indices,
)
self._cause_marginals[convention] = marginals
return self._cause_marginals[convention]
[docs]
@cached_property
def effect_marginal(self) -> FactoredTPM:
"""Forward TPM conditioned on the external units at their observed state."""
external_state = utils.state_of(
self.external_indices, self._background_reference_state
)
background = dict(zip(self.external_indices, external_state, strict=False))
return _effect_marginal_factored(self._typed_tpm, background)
[docs]
@cached_property
def proper_effect_marginal(self) -> FactoredTPM:
"""Effect TPM restricted to system units.
Per system unit ``i`` in ``node_indices``, the returned FactoredTPM
carries the forward factor of :attr:`effect_marginal` — the external
units (``external_indices``) conditioned at the background reference
state — with all non-system input dims dropped, so the returned
shape is ``(*system_alphabet, k_i)`` per system output unit.
Substrate units neither in the system nor external are marginalized
uniformly (the noise-background convention). The effect-side dual of
:attr:`proper_cause_marginal`.
"""
factored = self.effect_marginal
return FactoredTPM(
factors=[
self._restrict_to_system_inputs(factored.factor(i))
for i in self.node_indices
],
node_labels=self._unit_labels(),
)
def _restrict_to_system_inputs(self, factor: np.ndarray) -> np.ndarray:
"""Drop a factor's non-system input dims.
Non-system axes that are size 1 — conditioned at the background
state, or marginalized under the weighting of Albantakis et al.
(2023), Eq. 4 — are squeezed. Non-system axes left free (units
neither in the system nor external, as under the noise-background
convention) are marginalized uniformly.
"""
system = set(self.node_indices)
drop = tuple(j for j in range(self._typed_tpm.n_nodes) if j not in system)
if not drop:
return factor
free = tuple(j for j in drop if factor.shape[j] != 1)
if free:
factor = factor.mean(axis=free, keepdims=True)
return np.squeeze(factor, axis=drop)
@property
def proper_cause_marginal(self) -> FactoredTPM:
"""Cause TPM restricted to system units.
Per system unit ``i`` in ``node_indices``, the returned FactoredTPM
carries the cause factor of :attr:`cause_marginal` — background
handled per the active convention (the marginalization of Albantakis
et al. (2023), Eq. 4, or the external units conditioned at the
background reference state) — with all non-system input dims dropped,
so the returned shape is ``(*system_alphabet, k_i)`` per system output
unit.
Substrate units neither in the system nor external are marginalized
uniformly (the noise-background convention).
"""
marginals = self.cause_marginal
return FactoredTPM(
factors=[
self._restrict_to_system_inputs(marginals.factor(i))
for i in self.node_indices
],
node_labels=self._unit_labels(),
)
@cached_property
def cm(self) -> Any:
return self.partition.apply_cut(self.substrate.cm)
@cached_property
def proper_cm(self) -> Any:
return connectivity.subadjacency(self.cm, self.node_indices)
@cached_property
def connectivity_matrix(self) -> Any:
return self.cm
@cached_property
def partition_indices(self) -> NodeIndices:
return self.node_indices
@cached_property
def partition_node_labels(self) -> Any:
from pyphi.labels import NodeLabels
if self.partition_indices == self.node_indices:
return self.node_labels
labels = self.node_labels.coerce_to_labels(self.partition_indices)
return NodeLabels(labels, self.partition_indices)
@cached_property
def is_partitioned(self) -> bool:
return not isinstance(self.partition, NullCut)
@cached_property
def size(self) -> int:
return len(self.node_indices)
@cached_property
def tpm_size(self) -> int:
return self.substrate.size
@cached_property
def _nodes_by_convention(self) -> dict[str, Any]:
return {}
@property
def nodes(self) -> Any:
from pyphi.node import generate_nodes
convention = self._resolved_background_conditioning()
if convention not in self._nodes_by_convention:
self._nodes_by_convention[convention] = generate_nodes(
self.cause_marginal,
self.effect_marginal,
self.cm,
self.state,
self.node_indices,
self.node_labels,
)
return self._nodes_by_convention[convention]
@cached_property
def partitioned_mechanisms(self) -> Any:
return list(self.partition.all_cut_mechanisms())
@cached_property
def _index2node_by_convention(self) -> dict[str, dict[int, Any]]:
return {}
@property
def _index2node(self) -> dict[int, Any]:
convention = self._resolved_background_conditioning()
if convention not in self._index2node_by_convention:
self._index2node_by_convention[convention] = {
node.index: node for node in self.nodes
}
return self._index2node_by_convention[convention]
@cached_property
def null_distinction(self) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.null_distinction(self)
[docs]
@cached_property
def null_concept(self) -> Any:
"""IIT 3.0 alias for :attr:`null_distinction`."""
return self.null_distinction
# ---- repertoire algebra proxies ----
def cause_repertoire(self, mechanism: Any, purview: Any, **kwargs: Any) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.cause_repertoire(self, mechanism, purview, **kwargs)
def effect_repertoire(self, mechanism: Any, purview: Any, **kwargs: Any) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.effect_repertoire(self, mechanism, purview, **kwargs)
def repertoire(
self, direction: Any, mechanism: Any, purview: Any, **kwargs: Any
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.repertoire(self, direction, mechanism, purview, **kwargs)
def unconstrained_cause_repertoire(self, purview: Any) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.unconstrained_cause_repertoire(self, purview)
def unconstrained_effect_repertoire(self, purview: Any) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.unconstrained_effect_repertoire(self, purview)
def unconstrained_repertoire(self, direction: Any, purview: Any) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.unconstrained_repertoire(self, direction, purview)
def partitioned_repertoire(
self,
direction: Any,
partition: Any,
*,
mechanism_measure: Any,
**kwargs: Any,
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.partitioned_repertoire(
self, direction, partition, mechanism_measure=mechanism_measure, **kwargs
)
def expand_repertoire(
self,
direction: Any,
repertoire_array: Any,
new_purview: Any | None = None,
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.expand_repertoire(
self, direction, repertoire_array, new_purview=new_purview
)
def expand_cause_repertoire(
self, repertoire_array: Any, *, new_purview: Any | None = None
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.expand_cause_repertoire(
self, repertoire_array, new_purview=new_purview
)
def expand_effect_repertoire(
self, repertoire_array: Any, *, new_purview: Any | None = None
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.expand_effect_repertoire(
self, repertoire_array, new_purview=new_purview
)
def forward_cause_repertoire(
self, mechanism: Any, purview: Any, purview_state: Any | None = None
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.forward_cause_repertoire(self, mechanism, purview, purview_state)
def forward_effect_repertoire(
self, mechanism: Any, purview: Any, **kwargs: Any
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.forward_effect_repertoire(self, mechanism, purview, **kwargs)
def forward_repertoire(
self,
direction: Any,
mechanism: Any,
purview: Any,
purview_state: Any | None = None,
**kwargs: Any,
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.forward_repertoire(
self, direction, mechanism, purview, purview_state, **kwargs
)
def unconstrained_forward_cause_repertoire(
self, mechanism: Any, purview: Any
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.unconstrained_forward_cause_repertoire(self, mechanism, purview)
def unconstrained_forward_effect_repertoire(
self, mechanism: Any, purview: Any
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.unconstrained_forward_effect_repertoire(self, mechanism, purview)
def unconstrained_forward_repertoire(
self, direction: Any, mechanism: Any, purview: Any
) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.unconstrained_forward_repertoire(self, direction, mechanism, purview)
def forward_cause_probability(
self,
mechanism: Any,
purview: Any,
purview_state: Any,
mechanism_state: Any | None = None,
) -> float:
from pyphi.core import repertoire_algebra as ra
return ra.forward_cause_probability(
self, mechanism, purview, purview_state, mechanism_state
)
def forward_effect_probability(
self, mechanism: Any, purview: Any, purview_state: Any
) -> float:
from pyphi.core import repertoire_algebra as ra
return ra.forward_effect_probability(self, mechanism, purview, purview_state)
def forward_probability(
self,
direction: Any,
mechanism: Any,
purview: Any,
purview_state: Any,
**kwargs: Any,
) -> float:
from pyphi.core import repertoire_algebra as ra
return ra.forward_probability(
self, direction, mechanism, purview, purview_state, **kwargs
)
# ---- info / phi proxies ----
def cause_info(self, mechanism: Any, purview: Any, **kwargs: Any) -> float:
from pyphi.conf import config as _config
from pyphi.core import repertoire_algebra as ra
from pyphi.measures.distribution import resolve_mechanism_measure
kwargs.setdefault(
"repertoire_distance",
resolve_mechanism_measure(
_config.formalism.iit.mechanism_phi_measure,
self.substrate.factored_tpm.alphabet_sizes,
),
)
return ra.cause_info(self, mechanism, purview, **kwargs)
def effect_info(self, mechanism: Any, purview: Any, **kwargs: Any) -> float:
from pyphi.conf import config as _config
from pyphi.core import repertoire_algebra as ra
from pyphi.measures.distribution import resolve_mechanism_measure
kwargs.setdefault(
"repertoire_distance",
resolve_mechanism_measure(
_config.formalism.iit.mechanism_phi_measure,
self.substrate.factored_tpm.alphabet_sizes,
),
)
return ra.effect_info(self, mechanism, purview, **kwargs)
def cause_effect_info(self, mechanism: Any, purview: Any, **kwargs: Any) -> float:
from pyphi.conf import config as _config
from pyphi.core import repertoire_algebra as ra
from pyphi.measures.distribution import resolve_mechanism_measure
kwargs.setdefault(
"repertoire_distance",
resolve_mechanism_measure(
_config.formalism.iit.mechanism_phi_measure,
self.substrate.factored_tpm.alphabet_sizes,
),
)
return ra.cause_effect_info(self, mechanism, purview, **kwargs)
def intrinsic_information(
self,
direction: Any,
mechanism: Any,
purview: Any,
*,
specification_measure: Any,
**kwargs: Any,
) -> Any:
from pyphi.core import repertoire_algebra as ra
spec = ra.intrinsic_information(
self,
direction,
mechanism,
purview,
specification_measure=specification_measure,
**kwargs,
)
# Stamp display labels here, at the labeled layer; the kernel that
# builds the specification stays label-free. Cover the tie family so
# tied-state cards are labeled too.
labels = self.node_labels
if labels is not None and hasattr(spec, "node_labels"):
for tied in {spec, *getattr(spec, "ties", ())}:
if tied.node_labels is None:
tied.node_labels = labels
return spec
# ---- formalism queries ----
#
# System-level entry points (``sia``, ``ces``, ``distinctions``) and the
# mechanism-level queries used to build them (``find_mip``, ``find_mice``,
# ``distinction``, ``all_distinctions``, ``evaluate_partition``, …) are
# exposed as thin convenience methods that dispatch via the active
# formalism. The same operations live as free functions in
# :mod:`pyphi.formalism` for callers who prefer that grammar.
[docs]
def sia(self, **kwargs: Any) -> Any:
"""Return the system irreducibility analysis of this system.
Resolves the system- and specification-level measures from config at
the public boundary and threads them to the active formalism
explicitly, so formalism methods are never called without explicit
measures in normal flow.
A ``system_state`` keyword, when given, must be the state
specification the analysis would itself compute (it is derived
deterministically from the system and configuration). Calls with
a caller-supplied ``system_state`` bypass the disk result cache:
nothing verifies the supplied state is the canonical one, so
sharing the cache entry would let a non-canonical state poison
(and be served by) the plain ``sia()`` result.
"""
from pyphi.cache.disk import maybe_disk_cached
def _compute() -> Any:
from pyphi.conf import config as _config
from pyphi.formalism import sia as _sia
from pyphi.measures.distribution import resolve_mechanism_measure
from pyphi.measures.distribution import resolve_system_measure
call_kwargs = dict(kwargs)
if _config.formalism.iit.version != "IIT_3_0":
call_kwargs.setdefault(
"system_measure",
resolve_system_measure(_config.formalism.iit.system_phi_measure),
)
call_kwargs.setdefault(
"specification_measure",
resolve_mechanism_measure(
_config.formalism.iit.specification_measure
),
)
return _sia(self, **call_kwargs)
return maybe_disk_cached(self, "sia", dict(kwargs), _compute)
[docs]
def ces(self, **kwargs: Any) -> Any:
"""Return the cause-effect structure of this system (Eq. 57).
The result is a :class:`CauseEffectStructure`: the distinctions plus
their relations.
Notes
-----
For results computed under an earlier version of IIT (see
:doc:`/howto/earlier-versions`): IIT 3.0 has no relations, so under it
the CES is exactly the set of distinctions, returned as a
:class:`~pyphi.models.distinctions.ResolvedDistinctions` with the
concepts as ``.concepts``.
"""
from pyphi.cache.disk import maybe_disk_cached
def _compute() -> Any:
from pyphi.conf import config as _config
call_kwargs = dict(kwargs)
formalism_name = _config.formalism.iit.version
if formalism_name == "IIT_3_0":
from pyphi.formalism.iit3 import (
_compute_distinctions as _ces, # pyright: ignore[reportPrivateUsage]
)
from pyphi.models.distinctions import ResolvedDistinctions
# IIT 3.0 has no tied specified states to resolve.
return ResolvedDistinctions(_ces(self, **call_kwargs))
from pyphi.formalism.iit4 import ces as _ces
from pyphi.measures.distribution import resolve_mechanism_measure
from pyphi.measures.distribution import resolve_system_measure
call_kwargs.setdefault(
"system_measure",
resolve_system_measure(_config.formalism.iit.system_phi_measure),
)
call_kwargs.setdefault(
"specification_measure",
resolve_mechanism_measure(_config.formalism.iit.specification_measure),
)
return _ces(self, **call_kwargs)
return maybe_disk_cached(self, "ces", kwargs, _compute)
[docs]
def distinctions(self, congruent: bool = False, **kwargs: Any) -> Any:
"""Return the :class:`Distinctions` of this system.
The set of irreducible cause-effect distinctions specified by
mechanisms in the system, without the relations that bind them.
Parameters
----------
congruent : bool
Filter to the distinctions congruent with the system's specified
state, as the cause-effect structure does, but without the
system-partition search. The result is a
:class:`~pyphi.models.distinctions.ResolvedDistinctions` equal to
``ces().distinctions`` when the specified state is untied, and
the unfiltered
:class:`~pyphi.models.distinctions.UnresolvedDistinctions` when it
ties (see
:func:`~pyphi.formalism.iit4.congruent_distinctions`).
Notes
-----
Congruence filtering can remove every distinction — a system whose
mechanisms specify states incongruent with the system's own has a
nonempty unfiltered set and an empty cause-effect structure. The
unfiltered count and Σφ_d are therefore upper bounds, not estimates.
For results computed under an earlier version of IIT (see
:doc:`/howto/earlier-versions`): IIT 3.0 has no tied specified states,
so its distinctions are congruent as computed.
"""
from pyphi.conf import config as _config
formalism_name = _config.formalism.iit.version
if formalism_name == "IIT_3_0":
from pyphi.formalism.iit3 import (
_compute_distinctions as _distinctions, # pyright: ignore[reportPrivateUsage]
)
from pyphi.models.distinctions import ResolvedDistinctions
distinctions = _distinctions(self, **kwargs)
return ResolvedDistinctions(distinctions) if congruent else distinctions
if congruent:
from pyphi.formalism.iit4 import congruent_distinctions
from pyphi.measures.distribution import resolve_mechanism_measure
kwargs.setdefault(
"specification_measure",
resolve_mechanism_measure(_config.formalism.iit.specification_measure),
)
return congruent_distinctions(self, **kwargs)
from pyphi.formalism import all_distinctions as _all_distinctions
return _all_distinctions(self, **kwargs)
[docs]
def find_mip(
self, direction: Any, mechanism: Any, purview: Any, **kwargs: Any
) -> Any:
"""Return the minimum partition for a mechanism over a purview.
Resolves mechanism- and specification-level measures from config at
the public boundary so the active formalism's MIP search is never
called without explicit measures in normal flow. Explicit
``mechanism_measure``/``specification_measure`` kwargs override.
"""
from pyphi.conf import config as _config
from pyphi.formalism import find_mip as _find_mip
from pyphi.measures.distribution import resolve_mechanism_measure
if _config.formalism.iit.version != "IIT_3_0":
alphabet_sizes = self.substrate.factored_tpm.alphabet_sizes
kwargs.setdefault(
"mechanism_measure",
resolve_mechanism_measure(
_config.formalism.iit.mechanism_phi_measure, alphabet_sizes
),
)
kwargs.setdefault(
"specification_measure",
resolve_mechanism_measure(_config.formalism.iit.specification_measure),
)
return _find_mip(self, direction, mechanism, purview, **kwargs)
def cause_mip(self, mechanism: Any, purview: Any, **kwargs: Any) -> Any:
from pyphi.formalism import cause_mip as _cause_mip
return _cause_mip(self, mechanism, purview, **kwargs)
def effect_mip(self, mechanism: Any, purview: Any, **kwargs: Any) -> Any:
from pyphi.formalism import effect_mip as _effect_mip
return _effect_mip(self, mechanism, purview, **kwargs)
def phi_cause_mip(self, mechanism: Any, purview: Any, **kwargs: Any) -> float:
from pyphi.formalism import phi_cause_mip as _phi_cause_mip
return _phi_cause_mip(self, mechanism, purview, **kwargs)
def phi_effect_mip(self, mechanism: Any, purview: Any, **kwargs: Any) -> float:
from pyphi.formalism import phi_effect_mip as _phi_effect_mip
return _phi_effect_mip(self, mechanism, purview, **kwargs)
def phi(self, mechanism: Any, purview: Any, **kwargs: Any) -> float:
from pyphi.formalism import phi as _phi
return _phi(self, mechanism, purview, **kwargs)
def find_mice(self, direction: Any, mechanism: Any, **kwargs: Any) -> Any:
from pyphi.formalism import find_mice as _find_mice
return _find_mice(self, direction, mechanism, **kwargs)
def mic(self, mechanism: Any, **kwargs: Any) -> Any:
from pyphi.formalism import mic as _mic
return _mic(self, mechanism, **kwargs)
def mie(self, mechanism: Any, **kwargs: Any) -> Any:
from pyphi.formalism import mie as _mie
return _mie(self, mechanism, **kwargs)
def phi_max(self, mechanism: Any) -> float:
from pyphi.formalism import phi_max as _phi_max
return _phi_max(self, mechanism)
def distinction(self, mechanism: Any) -> Any:
from pyphi.formalism import distinction as _distinction
return _distinction(self, mechanism)
def all_distinctions(self, **kwargs: Any) -> Any:
from pyphi.formalism import all_distinctions as _all_distinctions
return _all_distinctions(self, **kwargs)
def evaluate_partition(
self,
direction: Any,
mechanism: Any,
purview: Any,
partition: Any,
**kwargs: Any,
) -> Any:
from pyphi.formalism import evaluate_partition as _evaluate_partition
return _evaluate_partition(
self, direction, mechanism, purview, partition, **kwargs
)
def potential_purviews(self, direction: Any, mechanism: Any, **kwargs: Any) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.potential_purviews(self, direction, mechanism, **kwargs)
def indices2nodes(self, indices: Any) -> Any:
from pyphi.core import repertoire_algebra as ra
return ra.indices2nodes(self, indices)
# ---- cache surface + serialization ----
def cache_info(self) -> dict[str, Any]:
from pyphi.core import repertoire_algebra as ra
return ra.cache_info()
def clear_caches(self) -> None:
from pyphi.core import repertoire_algebra as ra
ra.clear_caches(self)