Source code for pyphi.matching.differentiation
"""Differentiation across the structures triggered by a set of stimuli."""
from __future__ import annotations
from dataclasses import dataclass
from functools import cached_property
from itertools import combinations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .perception import Perception
def _component_perceptions(perception):
"""Yield (component, perception) for each component of one structure."""
for distinction in perception.ces.distinctions:
yield distinction, perception.distinction_perception(distinction)
for relation in perception._relations:
yield relation, perception.relation_perception(relation)
[docs]
@dataclass(frozen=True)
class Differentiation:
"""The union of components across the structures triggered by a sequence.
A view over ``Perception`` objects realizing the differentiation structure
C_D (Eq. 15): the distinctions and relations of the structures are pooled
and deduplicated by value equality, and each unique component carries the
maximum perception it attains in any of the structures. Duplicate
structures collapse in the union, so sequence order and repeats do not
affect the result.
Attributes
----------
perceptions : tuple of Perception
The perceptual structures triggered by the stimuli in the sequence.
"""
perceptions: tuple[Perception, ...]
[docs]
@cached_property
def projection(self) -> dict:
"""Mapping ``{component: maximum perception across structures}``.
Each unique distinction or relation maps to the largest perception
value it attains in any triggered structure containing it.
"""
projection = {}
for perception in self.perceptions:
for component, value in _component_perceptions(perception):
existing = projection.get(component)
if existing is None or value > existing:
projection[component] = value
return projection
[docs]
@cached_property
def differentiation(self) -> float:
"""Differentiation D (Eq. 16): summed φ of the unique components."""
return float(sum(float(component.phi) for component in self.projection))
[docs]
@cached_property
def perceptual_differentiation(self) -> float:
"""Perceptual differentiation D_p (Eq. 19): summed maximum perception.
The sum over unique components of the maximum perception value each
attains across the triggered structures.
"""
return float(sum(self.projection.values()))
[docs]
@cached_property
def analytical_differentiation(self) -> float:
"""Differentiation D (Eq. 16) in closed form, without concrete relations.
Equal to :attr:`differentiation` wherever that is computable, but reads
only each structure's ``distinctions`` (never ``relations``), so it is
the cheap path when the structures carry ``AnalyticalRelations``: it never
enumerates a relation, where the concrete properties would first materialize
the relation sets. D splits into the distinction-union term Σφ_d plus
the relation-union term, the latter computed by inclusion-exclusion over
the unique structures:
.. math::
\\sum_r \\varphi_r = \\sum_{\\emptyset \\neq T} (-1)^{|T|+1}\\,
\\mathrm{AnalyticalRelations}\\!\\left(\\bigcap_{k \\in T} D_k\\right)
.\\mathrm{sum\\_phi}()
Cost is ``2**K - 1`` analytical relation-sum calls for ``K`` unique
structures, which is practical only for small ``K`` (a small sensory
interface), the regime where enumerating concrete relations is instead
the bottleneck.
Returns
-------
float
The differentiation D, ``0.0`` when there are no perceptions.
"""
if not self.perceptions:
return 0.0
from pyphi.models.distinctions import ResolvedDistinctions
from pyphi.relations import AnalyticalRelations
# Distinction union term: Σφ_d over the identity-deduplicated union.
union_distinctions: dict = {}
for perception in self.perceptions:
for distinction in perception.ces.distinctions:
union_distinctions.setdefault(distinction, distinction)
distinction_sum = sum(float(d.phi) for d in union_distinctions)
# Relation union term: inclusion-exclusion over the unique structures
# (deduplicated by distinction set — duplicates do not change the union).
structures = list({frozenset(p.ces.distinctions) for p in self.perceptions})
relation_sum = 0.0
for size in range(1, len(structures) + 1):
sign = 1.0 if size % 2 == 1 else -1.0
for subset in combinations(structures, size):
common = frozenset.intersection(*subset)
if common:
relations = AnalyticalRelations(ResolvedDistinctions(common))
relation_sum += sign * float(relations.sum_phi())
return float(distinction_sum + relation_sum)