Source code for pyphi.matching.system

"""PerceptualSystem: a system embedded in an environment via a sensory interface."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from pyphi import utils

from .triggered_tpm import TriggeredTPM
from .triggered_tpm import _validate_binary_substrate
from .triggered_tpm import _validate_sorted_indices
from .triggered_tpm import build_triggered_tpm

if TYPE_CHECKING:
    from pyphi.substrate import Substrate


[docs] @dataclass(frozen=True) class PerceptualSystem: """A system S within a substrate U, coupled to its environment E = U∖S through a sensory interface ∂S ⊆ E. Produces the fixed-lag triggered TPM and the triggered response state for each stimulus (a state of the sensory interface ∂S). Only binary substrates are currently supported. Attributes ---------- substrate : Substrate The full substrate U. system_indices : tuple of int Indices of the units composing the system S. sensory_indices : tuple of int Indices of the sensory interface ∂S. Must be non-empty and disjoint from ``system_indices``; the environment E is the remaining units. """ substrate: Substrate system_indices: tuple[int, ...] sensory_indices: tuple[int, ...] def __post_init__(self): node_indices = set(self.substrate.node_indices) system = set(self.system_indices) sensory = set(self.sensory_indices) if not system <= node_indices: raise ValueError(f"system_indices {self.system_indices} not in substrate") if not sensory <= node_indices: raise ValueError(f"sensory_indices {self.sensory_indices} not in substrate") if not system or not sensory: raise ValueError("system_indices and sensory_indices must be non-empty") if system & sensory: raise ValueError( "system_indices and sensory_indices must be disjoint; " f"got overlap {sorted(system & sensory)}" ) _validate_binary_substrate(self.substrate) _validate_sorted_indices("system_indices", self.system_indices) _validate_sorted_indices("sensory_indices", self.sensory_indices) @property def environment_indices(self) -> tuple[int, ...]: system = set(self.system_indices) return tuple(i for i in self.substrate.node_indices if i not in system) @property def node_labels(self): return self.substrate.node_labels @staticmethod def _validate_tau(tau, tau_clamp): if not isinstance(tau, int) or not isinstance(tau_clamp, int): raise ValueError("tau and tau_clamp must be integers") if tau < 1: raise ValueError(f"tau must be >= 1; got {tau}") if not 0 <= tau_clamp <= tau: raise ValueError(f"require 0 <= tau_clamp <= tau; got {tau_clamp}, {tau}")
[docs] def triggered_tpm(self, *, tau, tau_clamp) -> TriggeredTPM: """The fixed-lag response distribution Pr(Sₜ | ∂S_{t−τ} = x). Parameters ---------- tau : int The lag τ >= 1 at which the stimulus's effect is evaluated. tau_clamp : int The number of initial steps for which the sensory interface is held at the stimulus before being marginalized. Must satisfy ``0 <= tau_clamp <= tau``. Returns ------- TriggeredTPM One system-state distribution per stimulus. """ self._validate_tau(tau, tau_clamp) return build_triggered_tpm( self.substrate, self.sensory_indices, self.system_indices, tau=tau, tau_clamp=tau_clamp, )
[docs] def triggered_states(self, *, tau, tau_clamp) -> dict: """Mapping ``{stimulus: response_state}`` over all stimuli. The response state is the most-probable (argmax) system state for each stimulus, the triggered state the Φ-structure computation consumes. """ ttpm = self.triggered_tpm(tau=tau, tau_clamp=tau_clamp) return { x: ttpm.argmax_state(x) for x in utils.all_states(len(self.sensory_indices)) }
[docs] def triggered_state(self, stimulus, *, tau, tau_clamp) -> tuple[int, ...]: """The response state (argmax system state) for a single stimulus.""" ttpm = self.triggered_tpm(tau=tau, tau_clamp=tau_clamp) return ttpm.argmax_state(tuple(stimulus))