"""Convert between PyPhi domain objects and their msgspec schema Structs.
Two registries map a domain type to its encoder and a schema Struct type to its
decoder. Each serializable type adds one ``_register_<type>()`` populating both
registries, invoked on first use via ``_ensure_registered()``.
"""
import ast
import contextvars
import math
import threading
from collections.abc import Callable
from typing import Any
import msgspec
import numpy as np
from pyphi.direction import Direction
from . import arrays
from . import frames
from . import schema
_ENCODERS: dict[type, Callable[[Any], Any]] = {} # domain type -> encode
_DECODERS: dict[type, Callable[[Any], Any]] = {} # schema Struct -> decode
[docs]
def to_schema(obj: Any) -> Any:
_ensure_registered()
encode = _ENCODERS.get(type(obj))
if encode is None:
# A φ value stored as a native float serializes as-is (msgspec handles
# it), so it needs no schema Struct.
if type(obj) is float:
return obj
raise TypeError(f"No serializer registered for {type(obj).__name__}")
return encode(obj)
[docs]
def from_schema(struct: Any) -> Any:
_ensure_registered()
decode = _DECODERS.get(type(struct))
if decode is None:
# A native float decoded from a φ position round-trips unchanged.
if type(struct) is float:
return struct
raise TypeError(f"No deserializer registered for {type(struct).__name__}")
return decode(struct)
def _enc_optional(obj: Any) -> Any:
"""Encode a nested domain object that may be ``None``."""
return to_schema(obj) if obj is not None else None
# Document label frame. dumps()/loads() establish these contexts; encoders
# and decoders resolve per-object labels against them. Outside a document
# context (a direct to_schema/from_schema call), labels stay per-object.
_ENC_FRAME: contextvars.ContextVar[list | None] = contextvars.ContextVar(
"_ENC_FRAME", default=None
)
_DEC_FRAME: contextvars.ContextVar[Any] = contextvars.ContextVar(
"_DEC_FRAME", default=None
)
def _enc_labels(labels: Any) -> Any:
"""Encode a ``node_labels`` attribute against the document frame.
The first labeled object claims the frame and writes ``None`` into its
own struct; labels equal to the frame also write ``None``; labels that
differ are written per-object. Inside a document, ``None`` labels are
written as the explicit no-labels marker so decode does not attach the
frame to an object that genuinely carried no labels.
"""
holder = _ENC_FRAME.get()
if labels is None:
return None if holder is None else schema.NoNodeLabelsSchema()
encoded = to_schema(labels)
if holder is None:
return encoded
if holder[0] is None:
holder[0] = encoded
return None
if encoded == holder[0]:
return None
return encoded
def _dec_labels(stored: Any) -> Any:
"""Resolve labels: the object's own stored labels, else the frame.
The explicit no-labels marker resolves to ``None`` regardless of the
frame.
"""
if type(stored) is schema.NoNodeLabelsSchema:
return None
if stored is not None:
return from_schema(stored)
return _DEC_FRAME.get()
[docs]
def encode_document(obj: Any) -> tuple[Any, Any]:
"""Encode ``obj`` to a payload struct plus the claimed label frame."""
holder: list = [None]
token = _ENC_FRAME.set(holder)
try:
payload = to_schema(obj)
finally:
_ENC_FRAME.reset(token)
return payload, holder[0]
[docs]
def decode_document(payload: Any, frame: Any, node_labels: Any = None) -> Any:
"""Decode ``payload`` under a document label frame.
``frame`` is the document's stored ``NodeLabelsSchema`` (or ``None``);
``node_labels`` is a caller-supplied domain ``NodeLabels`` that
replaces it.
"""
resolved = node_labels
if resolved is None and frame is not None:
resolved = from_schema(frame)
token = _DEC_FRAME.set(resolved)
try:
return from_schema(payload)
finally:
_DEC_FRAME.reset(token)
def _dec_optional(struct: Any) -> Any:
"""Decode a nested schema struct that may be ``None``."""
return from_schema(struct) if struct is not None else None
def _register_direction() -> None:
_ENCODERS[Direction] = lambda d: schema.DirectionSchema(name=d.name)
_DECODERS[schema.DirectionSchema] = lambda s: Direction[s.name]
def _register_distance_result() -> None:
from pyphi.measures.distribution import DistanceResult
_ENCODERS[DistanceResult] = lambda r: schema.DistanceResultSchema(
value=float(r), aux=r._public_aux_data()
)
_DECODERS[schema.DistanceResultSchema] = lambda s: DistanceResult(s.value, **s.aux)
def _register_node_labels() -> None:
from pyphi.labels import NodeLabels
_ENCODERS[NodeLabels] = lambda n: schema.NodeLabelsSchema(
labels=tuple(n.labels), node_indices=tuple(n.node_indices)
)
_DECODERS[schema.NodeLabelsSchema] = lambda s: NodeLabels(s.labels, s.node_indices)
def _encode_state_spec(spec: Any, *, include_peers: bool) -> Any:
# Tie tri-state: None = ties never computed (or suppressed for a peer);
# () = computed with no peers (the tie family is just this spec);
# otherwise the peer tuple. The domain tie family always contains the
# spec itself, so the decoder prepends the instance.
peers: tuple | None = None
if include_peers and spec.ties:
peers = tuple(
_encode_state_spec(t, include_peers=False)
for t in spec.ties
if t is not spec
)
return schema.StateSpecificationSchema(
direction=schema.DirectionSchema(name=spec.direction.name),
purview=tuple(spec.purview),
state=tuple(spec.state),
intrinsic_information=to_schema(spec.intrinsic_information),
repertoire=arrays.array_to_bytes(np.asarray(spec.repertoire)),
unconstrained_repertoire=arrays.array_to_bytes(
np.asarray(spec.unconstrained_repertoire)
),
tie_peers=peers,
runner_up_state=_opt_tuple(spec.runner_up_state),
runner_up_intrinsic_information=_enc_optional(
spec.runner_up_intrinsic_information
),
node_labels=_enc_labels(spec.node_labels),
)
def _decode_state_spec(struct: Any) -> Any:
from pyphi.models.state_specification import StateSpecification
instance = StateSpecification(
direction=from_schema(struct.direction),
purview=tuple(struct.purview),
state=tuple(struct.state),
intrinsic_information=from_schema(struct.intrinsic_information),
repertoire=arrays.bytes_to_array(struct.repertoire),
unconstrained_repertoire=arrays.bytes_to_array(struct.unconstrained_repertoire),
runner_up_state=_opt_tuple(struct.runner_up_state),
runner_up_intrinsic_information=_dec_optional(
struct.runner_up_intrinsic_information
),
)
instance.node_labels = _dec_labels(struct.node_labels)
if struct.tie_peers is not None:
peers = tuple(_decode_state_spec(p) for p in struct.tie_peers)
tied = (instance, *peers)
instance.set_ties(tied)
for peer in peers:
peer.set_ties(tied)
return instance
def _register_state_specification() -> None:
from pyphi.models.state_specification import StateSpecification
_ENCODERS[StateSpecification] = lambda s: _encode_state_spec(s, include_peers=True)
_DECODERS[schema.StateSpecificationSchema] = _decode_state_spec
def _register_system_state_specification() -> None:
from pyphi.models.state_specification import SystemStateSpecification
_ENCODERS[SystemStateSpecification] = lambda s: (
schema.SystemStateSpecificationSchema(
cause=to_schema(s.cause),
effect=to_schema(s.effect),
)
)
_DECODERS[schema.SystemStateSpecificationSchema] = lambda s: (
SystemStateSpecification(
cause=from_schema(s.cause),
effect=from_schema(s.effect),
)
)
def _register_part() -> None:
from pyphi.models.partitions import Part
_ENCODERS[Part] = lambda p: schema.PartSchema(
mechanism=tuple(p.mechanism),
purview=tuple(p.purview),
node_labels=_enc_labels(p.node_labels),
)
_DECODERS[schema.PartSchema] = lambda s: Part(
tuple(s.mechanism), tuple(s.purview), node_labels=_dec_labels(s.node_labels)
)
def _register_null_cut() -> None:
from pyphi.models.partitions import NullCut
_ENCODERS[NullCut] = lambda c: schema.NullCutSchema(
indices=tuple(c.indices), node_labels=_enc_labels(c.node_labels)
)
_DECODERS[schema.NullCutSchema] = lambda s: NullCut(
tuple(s.indices), _dec_labels(s.node_labels)
)
def _register_directed_bipartition() -> None:
from pyphi.models.partitions import DirectedBipartition
_ENCODERS[DirectedBipartition] = lambda p: schema.DirectedBipartitionSchema(
direction=schema.DirectionSchema(name=p.direction.name),
from_nodes=tuple(p.from_nodes),
to_nodes=tuple(p.to_nodes),
node_labels=_enc_labels(p.node_labels),
)
_DECODERS[schema.DirectedBipartitionSchema] = lambda s: DirectedBipartition(
from_schema(s.direction),
tuple(s.from_nodes),
tuple(s.to_nodes),
_dec_labels(s.node_labels),
)
def _register_joint_partition() -> None:
from pyphi.models.partitions import JointPartition
_ENCODERS[JointPartition] = lambda p: schema.JointPartitionSchema(
parts=tuple(to_schema(part) for part in p.parts),
node_labels=_enc_labels(p.node_labels),
)
_DECODERS[schema.JointPartitionSchema] = lambda s: JointPartition(
*(from_schema(p) for p in s.parts),
node_labels=_dec_labels(s.node_labels),
)
def _register_joint_bipartition() -> None:
from pyphi.models.partitions import JointBipartition
_ENCODERS[JointBipartition] = lambda p: schema.JointBipartitionSchema(
part0=to_schema(p[0]),
part1=to_schema(p[1]),
node_labels=_enc_labels(p.node_labels),
)
_DECODERS[schema.JointBipartitionSchema] = lambda s: JointBipartition(
from_schema(s.part0),
from_schema(s.part1),
node_labels=_dec_labels(s.node_labels),
)
def _register_joint_tripartition() -> None:
from pyphi.models.partitions import JointTripartition
_ENCODERS[JointTripartition] = lambda p: schema.JointTripartitionSchema(
parts=tuple(to_schema(part) for part in p.parts),
node_labels=_enc_labels(p.node_labels),
)
_DECODERS[schema.JointTripartitionSchema] = lambda s: JointTripartition(
*(from_schema(p) for p in s.parts),
node_labels=_dec_labels(s.node_labels),
)
def _register_directed_joint_partition() -> None:
from pyphi.models.partitions import DirectedJointPartition
_ENCODERS[DirectedJointPartition] = lambda p: schema.DirectedJointPartitionSchema(
direction=schema.DirectionSchema(name=p.direction.name),
partition=to_schema(p.partition),
node_labels=_enc_labels(p.node_labels),
)
_DECODERS[schema.DirectedJointPartitionSchema] = lambda s: DirectedJointPartition(
from_schema(s.direction),
from_schema(s.partition),
_dec_labels(s.node_labels),
)
def _register_edge_cut() -> None:
from pyphi.models.partitions import EdgeCut
_ENCODERS[EdgeCut] = lambda c: schema.EdgeCutSchema(
node_indices=tuple(c.node_indices),
cut_matrix=arrays.array_to_bytes(np.asarray(c._cut_matrix)),
node_labels=_enc_labels(c.node_labels),
)
_DECODERS[schema.EdgeCutSchema] = lambda s: EdgeCut(
tuple(s.node_indices),
arrays.bytes_to_array(s.cut_matrix),
_dec_labels(s.node_labels),
)
def _register_complete_edge_cut() -> None:
from pyphi.models.partitions import TotalCut
_ENCODERS[TotalCut] = lambda c: schema.TotalCutSchema(
node_indices=tuple(c.node_indices),
node_labels=_enc_labels(c.node_labels),
)
_DECODERS[schema.TotalCutSchema] = lambda s: TotalCut(
tuple(s.node_indices), _dec_labels(s.node_labels)
)
def _register_directed_set_partition() -> None:
from pyphi.models.partitions import DirectedSetPartition
_ENCODERS[DirectedSetPartition] = lambda c: schema.DirectedSetPartitionSchema(
node_indices=tuple(c.node_indices),
cut_matrix=arrays.array_to_bytes(np.asarray(c._cut_matrix)),
set_partition=tuple(tuple(part) for part in c.set_partition),
node_labels=_enc_labels(c.node_labels),
)
_DECODERS[schema.DirectedSetPartitionSchema] = lambda s: DirectedSetPartition(
node_indices=tuple(s.node_indices),
cut_matrix=arrays.bytes_to_array(s.cut_matrix),
set_partition=[list(part) for part in s.set_partition],
node_labels=_dec_labels(s.node_labels),
)
def _enc_array(arr: Any) -> Any:
"""Encode an optional numpy array to ``.npy`` bytes (``None`` stays ``None``)."""
return arrays.array_to_bytes(np.asarray(arr)) if arr is not None else None
def _dec_array(data: Any) -> Any:
"""Decode optional ``.npy`` bytes to a numpy array (``None`` stays ``None``)."""
return arrays.bytes_to_array(data) if data is not None else None
def _opt_tuple(values: Any) -> Any:
return tuple(values) if values is not None else None
def _encode_ria(ria: Any, *, include_peers: bool) -> Any:
partition_peers = (
tuple(t for t in ria._partition_ties if t is not ria) if include_peers else ()
)
state_peers = (
tuple(t for t in ria._state_ties if t is not ria) if include_peers else ()
)
return schema.RIASchema(
phi=to_schema(ria.phi),
direction=schema.DirectionSchema(name=ria.direction.name),
mechanism=tuple(ria.mechanism),
mechanism_state=_opt_tuple(ria.mechanism_state),
purview=tuple(ria.purview),
purview_state=_opt_tuple(ria.purview_state),
partition=to_schema(ria.partition),
repertoire=_enc_array(ria.repertoire),
partitioned_repertoire=_enc_array(ria.partitioned_repertoire),
specified_state=_enc_optional(ria.specified_state),
node_labels=_enc_labels(ria.node_labels),
partition_tie_peers=tuple(
_encode_ria(p, include_peers=False) for p in partition_peers
),
state_tie_peers=tuple(_encode_ria(p, include_peers=False) for p in state_peers),
partition_margin=_enc_optional(ria.partition_margin),
signed_phi=_enc_optional(ria.signed_phi),
selectivity=ria.selectivity,
reasons=_enc_reasons(ria.reasons),
signed_normalized_phi=ria.signed_normalized_phi,
)
def _decode_ria(struct: Any) -> Any:
from pyphi.models.ria import RepertoireIrreducibilityAnalysis
instance = RepertoireIrreducibilityAnalysis(
phi=from_schema(struct.phi),
direction=from_schema(struct.direction),
mechanism=tuple(struct.mechanism),
purview=tuple(struct.purview),
partition=from_schema(struct.partition),
repertoire=_dec_array(struct.repertoire),
partitioned_repertoire=_dec_array(struct.partitioned_repertoire),
specified_state=_dec_optional(struct.specified_state),
mechanism_state=_opt_tuple(struct.mechanism_state),
purview_state=_opt_tuple(struct.purview_state),
node_labels=_dec_labels(struct.node_labels),
partition_margin=_dec_optional(struct.partition_margin),
signed_phi=_dec_optional(struct.signed_phi),
selectivity=struct.selectivity,
reasons=_dec_reasons(struct.reasons),
)
# The constructor recomputes normalized phi from the AMBIENT config's
# distinction_phi_normalization, which need not be the scheme the value
# was computed under. Restore the stored value instead; files written
# before the field existed (None) keep the recomputed fallback.
if struct.signed_normalized_phi is not None:
from pyphi import utils as _utils
instance._signed_normalized_phi = float(struct.signed_normalized_phi)
instance._normalized_phi = float(
_utils.positive_part(struct.signed_normalized_phi)
)
if struct.partition_tie_peers:
peers = tuple(_decode_ria(p) for p in struct.partition_tie_peers)
tied = (instance, *peers)
instance._partition_ties = tied
for peer in peers:
peer._partition_ties = tied
if struct.state_tie_peers:
peers = tuple(_decode_ria(p) for p in struct.state_tie_peers)
tied = (instance, *peers)
instance._state_ties = tied
for peer in peers:
peer._state_ties = tied
return instance
def _register_ria() -> None:
from pyphi.models.ria import RepertoireIrreducibilityAnalysis
_ENCODERS[RepertoireIrreducibilityAnalysis] = lambda r: _encode_ria(
r, include_peers=True
)
_DECODERS[schema.RIASchema] = _decode_ria
def _mice_struct_cls(mice: Any) -> Any:
from pyphi.models.mice import MaximallyIrreducibleCause
from pyphi.models.mice import MaximallyIrreducibleEffect
if isinstance(mice, MaximallyIrreducibleCause):
return schema.MICECauseSchema
if isinstance(mice, MaximallyIrreducibleEffect):
return schema.MICEEffectSchema
return schema.MICESchema
def _encode_mice(mice: Any, struct_cls: Any, *, include_peers: bool = True) -> Any:
# Purview ties are tri-state: None = never computed; () = computed with
# no ties; otherwise the tie tuple's members excluding this MICE, each
# encoded with its own tie field suppressed (a member's tie tuple
# contains it, so recursing into peers' ties would never terminate).
# A MICE need not be a member of its own tie tuple (state- and
# partition-tie MICE carry the winner's tuple), so membership is stored
# alongside the peers: the decoder prepends the instance only when it
# was a member.
peers: tuple | None = None
member = True
if mice._purview_ties is not None:
member = any(t is mice for t in mice._purview_ties)
peers = (
tuple(
_encode_mice(t, _mice_struct_cls(t), include_peers=False)
for t in mice._purview_ties
if t is not mice
)
if include_peers
else ()
)
return struct_cls(
ria=to_schema(mice.ria),
purview_margin=_enc_optional(mice.purview_margin),
purview_tie_peers=peers,
purview_tie_member=member,
)
def _decode_mice(cls: type, struct: Any) -> Any:
instance = cls(from_schema(struct.ria))
if struct.purview_tie_peers is None:
instance._purview_ties = None
else:
peers = tuple(from_schema(p) for p in struct.purview_tie_peers)
tied = (instance, *peers) if struct.purview_tie_member else peers
instance._purview_ties = tied
for peer in peers:
peer._purview_ties = tied
instance.purview_margin = _dec_optional(struct.purview_margin)
return instance
def _register_mice() -> None:
from pyphi.models.mice import MaximallyIrreducibleCause
from pyphi.models.mice import MaximallyIrreducibleCauseOrEffect
from pyphi.models.mice import MaximallyIrreducibleEffect
_ENCODERS[MaximallyIrreducibleCauseOrEffect] = lambda m: _encode_mice(
m, schema.MICESchema
)
_ENCODERS[MaximallyIrreducibleCause] = lambda m: _encode_mice(
m, schema.MICECauseSchema
)
_ENCODERS[MaximallyIrreducibleEffect] = lambda m: _encode_mice(
m, schema.MICEEffectSchema
)
_DECODERS[schema.MICESchema] = lambda s: _decode_mice(
MaximallyIrreducibleCauseOrEffect, s
)
_DECODERS[schema.MICECauseSchema] = lambda s: _decode_mice(
MaximallyIrreducibleCause, s
)
_DECODERS[schema.MICEEffectSchema] = lambda s: _decode_mice(
MaximallyIrreducibleEffect, s
)
def _register_distinction() -> None:
from pyphi.models.distinction import Distinction
_ENCODERS[Distinction] = lambda d: schema.DistinctionSchema(
mechanism=_opt_tuple(d.mechanism),
cause=to_schema(d.cause),
effect=to_schema(d.effect),
)
_DECODERS[schema.DistinctionSchema] = lambda s: Distinction(
mechanism=_opt_tuple(s.mechanism),
cause=from_schema(s.cause),
effect=from_schema(s.effect),
)
def _register_distinctions() -> None:
from pyphi.models.distinctions import Distinctions
from pyphi.models.distinctions import ResolvedDistinctions
from pyphi.models.distinctions import UnresolvedDistinctions
def encoder(struct_cls):
return lambda d: struct_cls(concepts=tuple(to_schema(c) for c in d.concepts))
def decoder(domain_cls):
return lambda s: domain_cls(tuple(from_schema(c) for c in s.concepts))
_ENCODERS[Distinctions] = encoder(schema.DistinctionsSchema)
_ENCODERS[UnresolvedDistinctions] = encoder(schema.UnresolvedDistinctionsSchema)
_ENCODERS[ResolvedDistinctions] = encoder(schema.ResolvedDistinctionsSchema)
_DECODERS[schema.DistinctionsSchema] = decoder(Distinctions)
_DECODERS[schema.UnresolvedDistinctionsSchema] = decoder(UnresolvedDistinctions)
_DECODERS[schema.ResolvedDistinctionsSchema] = decoder(ResolvedDistinctions)
def _register_provenance() -> None:
from pyphi.provenance import Provenance
_ENCODERS[Provenance] = lambda p: schema.ProvenanceSchema(
pyphi_version=p.pyphi_version,
git_sha=p.git_sha,
git_dirty=p.git_dirty,
timestamp=p.timestamp,
python_version=p.python_version,
numpy_version=p.numpy_version,
scipy_version=p.scipy_version,
platform=p.platform,
wall_time=p.wall_time,
seed=p.seed,
note=p.note,
estimator=p.estimator,
)
_DECODERS[schema.ProvenanceSchema] = lambda s: Provenance(
**msgspec.structs.asdict(s)
)
def _register_macro_unit() -> None:
from pyphi.macro.units import MacroUnit
def _enc(u: Any) -> Any:
return schema.MacroUnitSchema(
constituents=tuple(
_enc(c) if isinstance(c, MacroUnit) else int(c) for c in u.constituents
),
update_grain=u.update_grain,
mapping=tuple(u.mapping),
background_apportionment=tuple(u.background_apportionment),
)
def _dec(s: Any) -> Any:
return MacroUnit(
constituents=tuple(
_dec(c) if isinstance(c, schema.MacroUnitSchema) else int(c)
for c in s.constituents
),
update_grain=s.update_grain,
mapping=tuple(s.mapping),
background_apportionment=tuple(s.background_apportionment),
)
_ENCODERS[MacroUnit] = _enc
_DECODERS[schema.MacroUnitSchema] = _dec
def _encode_optional_units(units: Any) -> Any:
if units is None:
return None
return tuple(to_schema(u) for u in units)
def _decode_optional_units(units: Any) -> Any:
if units is None:
return None
return tuple(from_schema(u) for u in units)
def _register_excluded_candidate() -> None:
from pyphi.models.complex import ExcludedCandidate
_ENCODERS[ExcludedCandidate] = lambda e: schema.ExcludedCandidateSchema(
node_indices=tuple(e.node_indices),
phi=float(e.phi) if e.phi is not None else None,
units=_encode_optional_units(e.units),
ii_ceiling=float(e.ii_ceiling) if e.ii_ceiling is not None else None,
gated=e.gated,
)
_DECODERS[schema.ExcludedCandidateSchema] = lambda s: ExcludedCandidate(
s.node_indices,
s.phi,
units=_decode_optional_units(s.units),
ii_ceiling=s.ii_ceiling,
gated=s.gated,
)
def _encode_iit3_sia(sia: Any, *, include_peers: bool) -> Any:
peers = tuple(t for t in sia.ties if t is not sia) if include_peers else ()
return schema.IIT3SIASchema(
phi=_enc_optional(sia.phi),
distinctions=_enc_optional(sia.distinctions),
partitioned_distinctions=_enc_optional(sia.partitioned_distinctions),
partition=_enc_optional(sia.partition),
node_indices=_opt_tuple(sia.node_indices),
node_labels=_enc_labels(sia.node_labels),
current_state=_opt_tuple(sia.current_state),
tie_peers=tuple(_encode_iit3_sia(p, include_peers=False) for p in peers),
runner_up=_enc_runner_up(sia.runner_up),
reasons=_enc_reasons(sia.reasons),
config=_enc_config(sia.config),
provenance=_enc_optional(sia.provenance),
)
def _decode_iit3_sia(struct: Any) -> Any:
from pyphi.models.sia import IIT3SystemIrreducibilityAnalysis
instance = IIT3SystemIrreducibilityAnalysis(
phi=_dec_optional(struct.phi),
distinctions=_dec_optional(struct.distinctions),
partitioned_distinctions=_dec_optional(struct.partitioned_distinctions),
partition=_dec_optional(struct.partition),
node_indices=_opt_tuple(struct.node_indices),
node_labels=_dec_labels(struct.node_labels),
current_state=_opt_tuple(struct.current_state),
runner_up=_dec_runner_up(struct.runner_up),
reasons=_dec_reasons(struct.reasons),
config=_dec_config(struct.config),
provenance=_dec_optional(struct.provenance),
)
if struct.tie_peers:
peers = tuple(_decode_iit3_sia(p) for p in struct.tie_peers)
tied = [instance, *peers]
instance._ties = tied
for peer in peers:
peer._ties = tied
return instance
def _register_iit3_sia() -> None:
from pyphi.models.sia import IIT3SystemIrreducibilityAnalysis
_ENCODERS[IIT3SystemIrreducibilityAnalysis] = lambda s: _encode_iit3_sia(
s, include_peers=True
)
_DECODERS[schema.IIT3SIASchema] = _decode_iit3_sia
def _enc_intrinsic_diff(diff: Any) -> Any:
if diff is None:
return None
return tuple(
(schema.DirectionSchema(name=k.name), to_schema(v)) for k, v in diff.items()
)
def _dec_intrinsic_diff(pairs: Any) -> Any:
if pairs is None:
return None
return {from_schema(k): from_schema(v) for k, v in pairs}
def _enc_reasons(reasons: Any) -> Any:
# A reason is normally a NullResultReason enum, but some fixtures carry it
# as a bare name string; store the name either way.
if reasons is None:
return None
return tuple(r.name if hasattr(r, "name") else str(r) for r in reasons)
def _dec_reasons(names: Any) -> Any:
if names is None:
return None
from pyphi.models.explanation import NullResultReason
return [
NullResultReason[n] if n in NullResultReason.__members__ else n for n in names
]
def _enc_runner_up(runner_up: Any) -> Any:
if runner_up is None:
return None
return schema.RunnerUpSchema(
partition=to_schema(runner_up.partition),
phi=to_schema(runner_up.phi),
normalized_phi=(
None
if runner_up.normalized_phi is None
else to_schema(runner_up.normalized_phi)
),
)
def _dec_runner_up(struct: Any) -> Any:
if struct is None:
return None
from pyphi.models.explanation import RunnerUp
return RunnerUp(
partition=from_schema(struct.partition),
phi=from_schema(struct.phi),
normalized_phi=(
None if struct.normalized_phi is None else from_schema(struct.normalized_phi)
),
)
def _config_enc_hook(obj: Any) -> Any:
# The only non-builtin values in the config layers are the FrozenMap
# parallel-evaluation mappings; store them as plain dicts so they
# round-trip. Anything else is a lossy encode — fail loudly.
from collections.abc import Mapping as _Mapping
if isinstance(obj, _Mapping):
return dict(obj)
raise TypeError(
f"config field of type {type(obj).__name__} cannot be serialized losslessly"
)
def _enc_config(config: Any) -> Any:
if config is None:
return None
# ConfigSnapshot is a nested frozen-dataclass tree; encode to plain
# builtins (config-as-Struct is out of scope).
return msgspec.to_builtins(config, enc_hook=_config_enc_hook)
def _dec_config(data: Any) -> Any:
if data is None:
return None
from pyphi.conf.snapshot import ConfigSnapshot
# Payloads written by earlier 2.0 builds stored the FrozenMap
# parallel-evaluation mappings as their repr strings; recover them.
infra = data.get("infrastructure")
if infra:
for key, value in infra.items():
if (
isinstance(value, str)
and value.startswith("FrozenMap(")
and value.endswith(")")
):
infra[key] = ast.literal_eval(value[len("FrozenMap(") : -1])
return ConfigSnapshot.from_builtins(data)
def _iit4_sia_struct_cls(sia: Any) -> Any:
from pyphi.formalism.iit4 import NullSystemIrreducibilityAnalysis
if isinstance(sia, NullSystemIrreducibilityAnalysis):
return schema.NullIIT4SIASchema
return schema.IIT4SIASchema
def _encode_iit4_sia(sia: Any, *, include_peers: bool) -> Any:
peers = tuple(t for t in sia.ties if t is not sia) if include_peers else ()
struct_cls = _iit4_sia_struct_cls(sia)
return struct_cls(
phi=to_schema(sia.phi),
partition=to_schema(sia.partition),
normalized_phi=to_schema(sia.normalized_phi),
cause=_enc_optional(sia.cause),
effect=_enc_optional(sia.effect),
system_state=_enc_optional(sia.system_state),
current_state=_opt_tuple(sia.current_state),
node_indices=_opt_tuple(sia.node_indices),
node_labels=_enc_labels(sia.node_labels),
intrinsic_differentiation=_enc_intrinsic_diff(sia.intrinsic_differentiation),
reasons=_enc_reasons(sia.reasons),
signed_phi=_enc_optional(sia.signed_phi),
signed_normalized_phi=_enc_optional(sia.signed_normalized_phi),
config=_enc_config(sia.config),
provenance=_enc_optional(sia.provenance),
tie_peers=tuple(_encode_iit4_sia(p, include_peers=False) for p in peers),
partition_margin=_enc_optional(sia.partition_margin),
runner_up=_enc_runner_up(sia.runner_up),
)
def _decode_iit4_sia(struct: Any) -> Any:
from pyphi.formalism.iit4 import NullSystemIrreducibilityAnalysis
from pyphi.formalism.iit4 import SystemIrreducibilityAnalysis
kwargs = {
"phi": from_schema(struct.phi),
"partition": from_schema(struct.partition),
"normalized_phi": from_schema(struct.normalized_phi),
"cause": _dec_optional(struct.cause),
"effect": _dec_optional(struct.effect),
"system_state": _dec_optional(struct.system_state),
"current_state": _opt_tuple(struct.current_state),
"node_indices": _opt_tuple(struct.node_indices),
"node_labels": _dec_labels(struct.node_labels),
"intrinsic_differentiation": _dec_intrinsic_diff(
struct.intrinsic_differentiation
),
"reasons": _dec_reasons(struct.reasons),
"signed_phi": _dec_optional(struct.signed_phi),
"signed_normalized_phi": _dec_optional(struct.signed_normalized_phi),
"config": _dec_config(struct.config),
"provenance": _dec_optional(struct.provenance),
"partition_margin": _dec_optional(struct.partition_margin),
"runner_up": _dec_runner_up(struct.runner_up),
}
if type(struct) is schema.NullIIT4SIASchema:
instance = object.__new__(NullSystemIrreducibilityAnalysis)
SystemIrreducibilityAnalysis.__init__(instance, **kwargs)
else:
instance = SystemIrreducibilityAnalysis(**kwargs)
if struct.tie_peers:
peers = tuple(_decode_iit4_sia(p) for p in struct.tie_peers)
tied = [instance, *peers]
instance._ties = tied
for peer in peers:
peer._ties = tied
return instance
def _register_iit4_sia() -> None:
from pyphi.formalism.iit4 import NullSystemIrreducibilityAnalysis
from pyphi.formalism.iit4 import SystemIrreducibilityAnalysis
_ENCODERS[SystemIrreducibilityAnalysis] = lambda s: _encode_iit4_sia(
s, include_peers=True
)
_ENCODERS[NullSystemIrreducibilityAnalysis] = lambda s: _encode_iit4_sia(
s, include_peers=True
)
_DECODERS[schema.IIT4SIASchema] = _decode_iit4_sia
_DECODERS[schema.NullIIT4SIASchema] = _decode_iit4_sia
def _register_relation() -> None:
from pyphi.relations import Relation
_ENCODERS[Relation] = lambda r: schema.RelationSchema(
distinctions=tuple(to_schema(d) for d in r)
)
_DECODERS[schema.RelationSchema] = lambda s: Relation(
[from_schema(d) for d in s.distinctions]
)
def _register_relations() -> None:
from pyphi.relations import AnalyticalRelations
from pyphi.relations import ConcreteRelations
from pyphi.relations import NullRelations
_ENCODERS[ConcreteRelations] = lambda rs: schema.ConcreteRelationsSchema(
relations=tuple(to_schema(r) for r in rs)
)
_ENCODERS[NullRelations] = lambda _rs: schema.NullRelationsSchema()
_ENCODERS[AnalyticalRelations] = lambda rs: schema.AnalyticalRelationsSchema(
distinctions=to_schema(rs.distinctions)
)
_DECODERS[schema.ConcreteRelationsSchema] = lambda s: ConcreteRelations(
[from_schema(r) for r in s.relations]
)
_DECODERS[schema.NullRelationsSchema] = lambda _s: NullRelations()
_DECODERS[schema.AnalyticalRelationsSchema] = lambda s: AnalyticalRelations(
from_schema(s.distinctions)
)
def _relation_indices(relation: Any, table: list, by_id: dict) -> tuple[int, ...]:
indices = []
for distinction in relation:
index = by_id.get(id(distinction))
if index is None:
# Fallback to value equality if the relation's distinction is not
# the identity-shared instance from the CES table.
index = next((j for j, d in enumerate(table) if d == distinction), None)
if index is None:
raise ValueError(
"relation references a distinction absent from the CES table"
)
indices.append(index)
return tuple(sorted(indices))
def _encode_relations_ref(relations: Any, table: list, by_id: dict) -> Any:
from pyphi.relations import AnalyticalRelations
from pyphi.relations import ConcreteRelations
from pyphi.relations import NullRelations
if isinstance(relations, NullRelations):
return schema.NullRelationsRefSchema()
if isinstance(relations, AnalyticalRelations):
return schema.AnalyticalRelationsRefSchema()
if isinstance(relations, ConcreteRelations):
refs = tuple(
schema.RelationRefSchema(
distinction_indices=_relation_indices(rel, table, by_id)
)
for rel in relations
)
return schema.ConcreteRelationsRefSchema(relations=refs)
raise TypeError(f"Cannot normalize relations of type {type(relations).__name__}")
def _decode_relations_ref(
struct: Any, table: list, distinctions: Any | None = None
) -> Any:
from pyphi.models.distinctions import ResolvedDistinctions
from pyphi.relations import AnalyticalRelations
from pyphi.relations import ConcreteRelations
from pyphi.relations import NullRelations
from pyphi.relations import Relation
if type(struct) is schema.NullRelationsRefSchema:
return NullRelations()
if type(struct) is schema.AnalyticalRelationsRefSchema:
# Reuse the already-decoded distinctions object so the wrapper type
# and identity are shared with the structure's own distinctions.
if distinctions is not None:
return AnalyticalRelations(distinctions)
return AnalyticalRelations(ResolvedDistinctions(table))
relations = tuple(
Relation([table[i] for i in ref.distinction_indices]) for ref in struct.relations
)
return ConcreteRelations(relations)
def _encode_ces(ces: Any, struct_cls: Any) -> Any:
table = list(ces.distinctions)
by_id = {id(d): i for i, d in enumerate(table)}
return struct_cls(
sia=to_schema(ces.sia),
distinctions=to_schema(ces.distinctions),
relations=_encode_relations_ref(ces.relations, table, by_id),
config=_enc_config(ces.config),
provenance=_enc_optional(ces.provenance),
)
def _decode_ces(struct: Any, domain_cls: Any) -> Any:
distinctions = from_schema(struct.distinctions)
table = list(distinctions)
relations = _decode_relations_ref(struct.relations, table, distinctions)
return domain_cls(
sia=from_schema(struct.sia),
distinctions=distinctions,
relations=relations,
config=_dec_config(struct.config),
provenance=_dec_optional(struct.provenance),
)
def _register_ces() -> None:
from pyphi.formalism.iit4 import NullCauseEffectStructure
from pyphi.models.ces import CauseEffectStructure
_ENCODERS[CauseEffectStructure] = lambda c: _encode_ces(c, schema.CESSchema)
_ENCODERS[NullCauseEffectStructure] = lambda c: _encode_ces(c, schema.NullCESSchema)
_DECODERS[schema.CESSchema] = lambda s: _decode_ces(s, CauseEffectStructure)
_DECODERS[schema.NullCESSchema] = lambda s: _decode_ces(s, NullCauseEffectStructure)
def _encode_factor(f: Any) -> tuple[bytes, bool]:
"""Encode one conditional factor, storing only the on-probability slice
of a binary factor whose off slice is its exact float complement.
The trim is applied only after verifying ``factor[..., 0] == 1.0 −
factor[..., 1]`` elementwise, so reconstruction on decode is exact; any
factor failing the check (including every non-binary factor) is stored
in full.
"""
arr = np.asarray(f)
if arr.shape[-1] == 2 and np.array_equal(arr[..., 0], 1.0 - arr[..., 1]):
return arrays.array_to_bytes(np.ascontiguousarray(arr[..., 1])), True
return arrays.array_to_bytes(arr), False
def _decode_factor(data: bytes, trimmed: bool) -> np.ndarray:
arr = arrays.bytes_to_array(data)
if trimmed:
return np.stack([1.0 - arr, arr], axis=-1)
return arr
def _register_substrate() -> None:
from pyphi.core.tpm.factored import FactoredTPM
from pyphi.substrate import Substrate
def _encode_substrate(s: Substrate) -> schema.SubstrateSchema:
encoded = [_encode_factor(f) for f in s.factored_tpm.factors]
return schema.SubstrateSchema(
factors=tuple(data for data, _ in encoded),
state_space=tuple(tuple(labels) for labels in s.factored_tpm.state_space),
cm=arrays.array_to_bytes(np.asarray(s.cm)),
node_labels=_enc_labels(s.node_labels),
factors_trimmed=tuple(trimmed for _, trimmed in encoded),
)
_ENCODERS[Substrate] = _encode_substrate
def _decode_substrate(s: schema.SubstrateSchema) -> Substrate:
trimmed = s.factors_trimmed or (False,) * len(s.factors)
labels = _dec_labels(s.node_labels)
factored = FactoredTPM(
factors=tuple(
_decode_factor(f, t) for f, t in zip(s.factors, trimmed, strict=True)
),
state_space=s.state_space,
# Substrate construction stamps its labels onto the TPM so its
# repr shows node names; mirror that here (from_factored leaves
# the TPM's own labels untouched).
node_labels=None if labels is None else tuple(labels),
)
return Substrate.from_factored(
factored,
cm=arrays.bytes_to_array(s.cm),
node_labels=labels,
)
_DECODERS[schema.SubstrateSchema] = _decode_substrate
def _register_system() -> None:
from pyphi.system import System
_ENCODERS[System] = lambda s: schema.SystemSchema(
substrate=to_schema(s.substrate),
state=tuple(s.state),
node_indices=tuple(s.node_indices),
partition=to_schema(s.partition),
external_indices=tuple(s.external_indices),
background_conditioning=s.background_conditioning,
background_state=(
tuple(s.background_state) if s.background_state is not None else None
),
)
_DECODERS[schema.SystemSchema] = lambda s: System(
substrate=from_schema(s.substrate),
state=tuple(s.state),
node_indices=tuple(s.node_indices),
partition=from_schema(s.partition),
external_indices=tuple(s.external_indices),
background_conditioning=s.background_conditioning,
background_state=(
tuple(s.background_state) if s.background_state is not None else None
),
)
def _register_transition() -> None:
from pyphi.actual import Transition
_ENCODERS[Transition] = lambda t: schema.TransitionSchema(
substrate=to_schema(t.substrate),
before_state=tuple(t.before_state),
after_state=tuple(t.after_state),
cause_indices=tuple(t.cause_indices),
effect_indices=tuple(t.effect_indices),
partition=to_schema(t.partition),
noise_background=t.noise_background,
)
_DECODERS[schema.TransitionSchema] = lambda t: Transition(
substrate=from_schema(t.substrate),
before_state=tuple(t.before_state),
after_state=tuple(t.after_state),
cause_indices=tuple(t.cause_indices),
effect_indices=tuple(t.effect_indices),
partition=from_schema(t.partition),
noise_background=t.noise_background,
)
from pyphi.actual import TransitionSystem
_ENCODERS[TransitionSystem] = lambda t: schema.TransitionSystemSchema(
substrate=to_schema(t.substrate),
before_state=tuple(t.before_state),
after_state=tuple(t.after_state),
cause_indices=tuple(t.cause_indices),
effect_indices=tuple(t.effect_indices),
direction=schema.DirectionSchema(name=t.direction.name),
partition=to_schema(t.partition),
noise_background=t.noise_background,
)
_DECODERS[schema.TransitionSystemSchema] = lambda t: TransitionSystem(
substrate=from_schema(t.substrate),
before_state=tuple(t.before_state),
after_state=tuple(t.after_state),
cause_indices=tuple(t.cause_indices),
effect_indices=tuple(t.effect_indices),
direction=from_schema(t.direction),
partition=from_schema(t.partition),
noise_background=t.noise_background,
)
def _encode_ac_ria(ria: Any, *, include_peers: bool) -> Any:
peers: tuple = ()
if include_peers and ria._partition_ties is not None:
peers = tuple(t for t in ria._partition_ties if t is not ria)
return schema.AcRIASchema(
alpha=float(ria.alpha),
state=tuple(ria.state),
direction=schema.DirectionSchema(name=ria.direction.name),
mechanism=tuple(ria.mechanism),
# A reducible link's null RIA has no purview, partition, or
# probabilities.
purview=_opt_tuple(ria.purview),
partition=_enc_optional(ria.partition),
probability=None if ria.probability is None else float(ria.probability),
partitioned_probability=(
None
if ria.partitioned_probability is None
else float(ria.partitioned_probability)
),
partition_tie_peers=tuple(_encode_ac_ria(p, include_peers=False) for p in peers),
node_labels=_enc_labels(ria.node_labels),
reasons=_enc_reasons(ria.reasons),
)
def _decode_ac_ria(struct: Any) -> Any:
from pyphi.models.actual_causation import AcRepertoireIrreducibilityAnalysis
instance = AcRepertoireIrreducibilityAnalysis(
alpha=struct.alpha,
state=tuple(struct.state),
direction=from_schema(struct.direction),
mechanism=tuple(struct.mechanism),
purview=_opt_tuple(struct.purview),
partition=_dec_optional(struct.partition),
probability=struct.probability,
partitioned_probability=struct.partitioned_probability,
node_labels=_dec_labels(struct.node_labels),
reasons=_dec_reasons(struct.reasons),
)
if struct.partition_tie_peers:
peers = tuple(_decode_ac_ria(p) for p in struct.partition_tie_peers)
tied = (instance, *peers)
instance._partition_ties = tied
for peer in peers:
peer._partition_ties = tied
return instance
def _register_ac_ria() -> None:
from pyphi.models.actual_causation import AcRepertoireIrreducibilityAnalysis
_ENCODERS[AcRepertoireIrreducibilityAnalysis] = lambda r: _encode_ac_ria(
r, include_peers=True
)
_DECODERS[schema.AcRIASchema] = _decode_ac_ria
def _register_causal_link() -> None:
from pyphi.models.actual_causation import CausalLink
def encode(link):
peers = link._purview_ties or ()
extended = link._extended_purview
return schema.CausalLinkSchema(
ria=_encode_ac_ria(link.ria, include_peers=True),
extended_purview=(
None if extended is None else tuple(tuple(p) for p in extended)
),
purview_tie_peers=tuple(
_encode_ac_ria(p, include_peers=False) for p in peers
),
)
def decode(struct):
peers = tuple(_decode_ac_ria(p) for p in struct.purview_tie_peers)
extended = struct.extended_purview
return CausalLink(
ria=_decode_ac_ria(struct.ria),
extended_purview=(
None if extended is None else tuple(tuple(p) for p in extended)
),
purview_ties=peers if peers else None,
)
_ENCODERS[CausalLink] = encode
_DECODERS[schema.CausalLinkSchema] = decode
def _register_account() -> None:
from pyphi.models.actual_causation import Account
from pyphi.models.actual_causation import DirectedAccount
_ENCODERS[Account] = lambda a: schema.AccountSchema(
causal_links=tuple(to_schema(link) for link in a)
)
_ENCODERS[DirectedAccount] = lambda a: schema.DirectedAccountSchema(
causal_links=tuple(to_schema(link) for link in a)
)
_DECODERS[schema.AccountSchema] = lambda s: Account(
[from_schema(link) for link in s.causal_links]
)
_DECODERS[schema.DirectedAccountSchema] = lambda s: DirectedAccount(
[from_schema(link) for link in s.causal_links]
)
def _encode_ac_sia(s: Any, *, include_peers: bool) -> Any:
peers = tuple(t for t in s.ties if t is not s) if include_peers else ()
return schema.AcSIASchema(
alpha=None if s.alpha is None else float(s.alpha),
direction=_enc_optional_direction(s.direction),
account=_enc_optional(s.account),
partitioned_account=_enc_optional(s.partitioned_account),
partition=_enc_optional(s.partition),
before_state=_opt_tuple(s.before_state),
after_state=_opt_tuple(s.after_state),
size=s.size,
node_indices=_opt_tuple(s.node_indices),
cause_indices=_opt_tuple(s.cause_indices),
effect_indices=_opt_tuple(s.effect_indices),
node_labels=_enc_labels(s.node_labels),
reasons=_enc_reasons(s.reasons),
config=_enc_config(s.config),
provenance=_enc_optional(s.provenance),
tie_peers=tuple(_encode_ac_sia(p, include_peers=False) for p in peers),
)
def _decode_ac_sia(struct: Any) -> Any:
from pyphi.models.actual_causation import AcSystemIrreducibilityAnalysis
instance = AcSystemIrreducibilityAnalysis(
alpha=struct.alpha,
direction=_dec_optional(struct.direction),
account=_dec_optional(struct.account),
partitioned_account=_dec_optional(struct.partitioned_account),
partition=_dec_optional(struct.partition),
before_state=_opt_tuple(struct.before_state),
after_state=_opt_tuple(struct.after_state),
size=struct.size,
node_indices=_opt_tuple(struct.node_indices),
cause_indices=_opt_tuple(struct.cause_indices),
effect_indices=_opt_tuple(struct.effect_indices),
node_labels=_dec_labels(struct.node_labels),
reasons=_dec_reasons(struct.reasons),
config=_dec_config(struct.config),
provenance=_dec_optional(struct.provenance),
)
if struct.tie_peers:
peers = tuple(_decode_ac_sia(p) for p in struct.tie_peers)
tied = (instance, *peers)
instance._ties = tied
for peer in peers:
peer._ties = tied
return instance
def _register_ac_sia() -> None:
from pyphi.models.actual_causation import AcSystemIrreducibilityAnalysis
_ENCODERS[AcSystemIrreducibilityAnalysis] = lambda s: _encode_ac_sia(
s, include_peers=True
)
_DECODERS[schema.AcSIASchema] = _decode_ac_sia
def _enc_optional_direction(direction: Any) -> Any:
if direction is None:
return None
return schema.DirectionSchema(name=direction.name)
def _register_complex() -> None:
from pyphi.models.complex import Complex
_ENCODERS[Complex] = lambda c: schema.ComplexSchema(
sia=to_schema(c.sia),
substrate=to_schema(c.substrate),
is_maximal=bool(c.is_maximal),
excluded=tuple(to_schema(e) for e in c.excluded),
units=_encode_optional_units(c.units),
node_indices=tuple(c.node_indices),
)
_DECODERS[schema.ComplexSchema] = lambda s: Complex(
sia=from_schema(s.sia),
substrate=from_schema(s.substrate),
is_maximal=s.is_maximal,
excluded=tuple(from_schema(e) for e in s.excluded),
units=_decode_optional_units(s.units),
node_indices=s.node_indices,
)
def _register_macro_system() -> None:
from pyphi.core.tpm.factored import FactoredTPM
from pyphi.macro.system import MacroSystem
def _encode_macro_system(m: MacroSystem) -> schema.MacroSystemSchema:
cause = m.macro_cause_marginal
assert cause is not None
encoded = [_encode_factor(cause.factor(i)) for i in range(cause.n_nodes)]
return schema.MacroSystemSchema(
substrate=to_schema(m.substrate),
state=tuple(m.state),
node_indices=tuple(m.node_indices),
partition=to_schema(m.partition),
external_indices=tuple(m.external_indices),
units=tuple(to_schema(u) for u in m.units),
micro_substrate=to_schema(m.micro_substrate),
micro_history=tuple(tuple(s) for s in m.micro_history),
cause_factors=tuple(data for data, _ in encoded),
cause_factors_trimmed=tuple(trimmed for _, trimmed in encoded),
cause_state_space=tuple(tuple(labels) for labels in cause.state_space),
background_conditioning=m.background_conditioning,
background_state=(
tuple(m.background_state) if m.background_state is not None else None
),
)
_ENCODERS[MacroSystem] = _encode_macro_system
def _decode_macro_system(s: schema.MacroSystemSchema) -> MacroSystem:
cause = FactoredTPM(
factors=tuple(
_decode_factor(f, t)
for f, t in zip(s.cause_factors, s.cause_factors_trimmed, strict=True)
),
state_space=s.cause_state_space,
)
return MacroSystem(
substrate=from_schema(s.substrate),
state=tuple(s.state),
node_indices=tuple(s.node_indices),
partition=from_schema(s.partition),
external_indices=tuple(s.external_indices),
background_conditioning=s.background_conditioning,
background_state=(
tuple(s.background_state) if s.background_state is not None else None
),
units=tuple(from_schema(u) for u in s.units),
micro_substrate=from_schema(s.micro_substrate),
micro_history=tuple(tuple(h) for h in s.micro_history),
macro_cause_marginal=cause,
)
_DECODERS[schema.MacroSystemSchema] = _decode_macro_system
def _register_complexes_result() -> None:
from pyphi.macro.search import ComplexesResult
from pyphi.macro.search import EvaluationRecord
_ENCODERS[EvaluationRecord] = lambda r: schema.EvaluationRecordSchema(
system=to_schema(r.system),
phi=None if r.phi is None else float(r.phi),
ii_ceiling=None if r.ii_ceiling is None else float(r.ii_ceiling),
gated=bool(r.gated),
)
_DECODERS[schema.EvaluationRecordSchema] = lambda s: EvaluationRecord(
system=from_schema(s.system),
phi=s.phi,
ii_ceiling=s.ii_ceiling,
gated=s.gated,
)
_ENCODERS[ComplexesResult] = lambda r: schema.ComplexesResultSchema(
complexes=tuple(to_schema(c) for c in r.complexes),
records=tuple(to_schema(rec) for rec in r.records),
ties=tuple(tuple(to_schema(m) for m in clique) for clique in r.ties),
)
_DECODERS[schema.ComplexesResultSchema] = lambda s: ComplexesResult(
complexes=tuple(from_schema(c) for c in s.complexes),
records=tuple(from_schema(rec) for rec in s.records),
ties=tuple(tuple(from_schema(m) for m in clique) for clique in s.ties),
)
def _register_analysis() -> None:
from pyphi.analyze import Analysis
_ENCODERS[Analysis] = lambda a: schema.AnalysisSchema(
system=to_schema(a.system),
sia=to_schema(a.sia),
ces=to_schema(a.ces),
)
_DECODERS[schema.AnalysisSchema] = lambda s: Analysis(
system=from_schema(s.system),
sia=from_schema(s.sia),
ces=from_schema(s.ces),
)
def _register_coverage_report() -> None:
from pyphi.estimate import CoverageReport
_ENCODERS[CoverageReport] = lambda c: schema.CoverageReportSchema(
counts=arrays.array_to_bytes(np.asarray(c.counts)),
n_units=c.n_units,
)
_DECODERS[schema.CoverageReportSchema] = lambda s: CoverageReport(
counts=arrays.bytes_to_array(s.counts),
n_units=s.n_units,
)
def _register_substrate_posterior() -> None:
from pyphi.estimate import SubstratePosterior
_ENCODERS[SubstratePosterior] = lambda p: schema.SubstratePosteriorSchema(
alpha_on=arrays.array_to_bytes(np.asarray(p.alpha_on)),
alpha_off=arrays.array_to_bytes(np.asarray(p.alpha_off)),
regime=p.regime,
prior=float(p.prior),
coverage=to_schema(p.coverage),
node_labels=_opt_tuple(p.node_labels),
provenance=to_schema(p.provenance),
)
_DECODERS[schema.SubstratePosteriorSchema] = lambda s: SubstratePosterior(
alpha_on=arrays.bytes_to_array(s.alpha_on),
alpha_off=arrays.bytes_to_array(s.alpha_off),
regime=s.regime,
prior=s.prior,
coverage=from_schema(s.coverage),
node_labels=_opt_tuple(s.node_labels),
provenance=from_schema(s.provenance),
)
def _register_phi_posterior() -> None:
from pyphi.estimate import PhiPosterior
_ENCODERS[PhiPosterior] = lambda p: schema.PhiPosteriorSchema(
samples=arrays.array_to_bytes(np.asarray(p.samples)),
complex_samples=tuple(tuple(c) for c in p.complex_samples),
state=tuple(p.state),
subset=_opt_tuple(p.subset),
seed=p.seed,
regime=p.regime,
coverage=to_schema(p.coverage),
provenance=to_schema(p.provenance),
screen_margin=p.screen_margin,
screened=p.screened,
reference_margins=(
None if p.reference_margins is None else dict(p.reference_margins)
),
)
_DECODERS[schema.PhiPosteriorSchema] = lambda s: PhiPosterior(
samples=arrays.bytes_to_array(s.samples),
complex_samples=tuple(tuple(c) for c in s.complex_samples),
state=tuple(s.state),
subset=_opt_tuple(s.subset),
seed=s.seed,
regime=s.regime,
coverage=from_schema(s.coverage),
provenance=from_schema(s.provenance),
screen_margin=s.screen_margin,
screened=s.screened,
reference_margins=(
None if s.reference_margins is None else dict(s.reference_margins)
),
)
def _register_sweep_result() -> None:
from pyphi.sweep import SweepResult
_ENCODERS[SweepResult] = lambda r: schema.SweepResultSchema(
df=frames.dataframe_to_schema(r.df),
results=tuple(
obj if isinstance(obj, float) else to_schema(obj) for obj in r.results
),
skipped=tuple(
(label, formalism, tuple(subset), tuple(state))
for label, formalism, subset, state in r.skipped
),
)
def _decode_sweep_result(s: schema.SweepResultSchema) -> Any:
return SweepResult(
df=frames.schema_to_dataframe(s.df),
results=[
obj if isinstance(obj, float) else from_schema(obj) for obj in s.results
],
skipped=[
(label, formalism, tuple(subset), tuple(state))
for label, formalism, subset, state in s.skipped
],
)
_DECODERS[schema.SweepResultSchema] = _decode_sweep_result
def _register_campaign() -> None:
from pyphi.campaign import CampaignTask
from pyphi.campaign import CampaignTaskOutput
from pyphi.campaign import CellOutput
_ENCODERS[CampaignTask] = lambda t: schema.CampaignTaskSchema(
task_id=t.task_id,
kind=t.kind,
compute=t.compute,
compute_ref=t.compute_ref,
config_overrides=dict(t.config_overrides),
cells=tuple(
(label, formalism, tuple(subset), tuple(state))
for label, formalism, subset, state in t.cells
),
skip_uncomputable=t.skip_uncomputable,
)
def _decode_campaign_task(s: schema.CampaignTaskSchema) -> Any:
return CampaignTask(
task_id=s.task_id,
kind=s.kind,
compute=s.compute,
compute_ref=s.compute_ref,
config_overrides=dict(s.config_overrides),
cells=tuple(
(label, formalism, tuple(subset), tuple(state))
for label, formalism, subset, state in s.cells
),
skip_uncomputable=s.skip_uncomputable,
)
_DECODERS[schema.CampaignTaskSchema] = _decode_campaign_task
_ENCODERS[CellOutput] = lambda e: schema.CellOutputSchema(
status=e.status,
result=None if e.result is None else to_schema(e.result),
traceback=e.traceback,
aux=None if e.aux is None else dict(e.aux),
)
def _decode_cell_output(s: schema.CellOutputSchema) -> Any:
return CellOutput(
status=s.status,
result=None if s.result is None else from_schema(s.result),
traceback=s.traceback,
aux=None if s.aux is None else dict(s.aux),
)
_DECODERS[schema.CellOutputSchema] = _decode_cell_output
_ENCODERS[CampaignTaskOutput] = lambda o: schema.CampaignTaskOutputSchema(
task_id=o.task_id,
pyphi_version=o.pyphi_version,
entries=tuple(to_schema(e) for e in o.entries),
metrics=o.metrics,
)
def _decode_campaign_task_output(s: schema.CampaignTaskOutputSchema) -> Any:
return CampaignTaskOutput(
task_id=s.task_id,
pyphi_version=s.pyphi_version,
entries=tuple(from_schema(e) for e in s.entries),
metrics=s.metrics,
)
_DECODERS[schema.CampaignTaskOutputSchema] = _decode_campaign_task_output
from pyphi.campaign.scope import AxisScope
from pyphi.campaign.scope import CESScope
_ENCODERS[AxisScope] = lambda a: schema.AxisScopeSchema(
explicit=a.explicit,
min_order=a.min_order,
max_order=a.max_order,
containing=a.containing,
within=a.within,
)
def _decode_axis_scope(s: schema.AxisScopeSchema) -> Any:
return AxisScope(
explicit=None if s.explicit is None else tuple(tuple(e) for e in s.explicit),
min_order=s.min_order,
max_order=s.max_order,
containing=None if s.containing is None else tuple(s.containing),
within=None if s.within is None else tuple(s.within),
)
_DECODERS[schema.AxisScopeSchema] = _decode_axis_scope
_ENCODERS[CESScope] = lambda c: schema.CESScopeSchema(
mechanisms=to_schema(c.mechanisms),
cause_purviews=to_schema(c.cause_purviews),
effect_purviews=to_schema(c.effect_purviews),
max_purview_order_by_mechanism_order=c.max_purview_order_by_mechanism_order,
)
def _decode_ces_scope(s: schema.CESScopeSchema) -> Any:
return CESScope(
mechanisms=from_schema(s.mechanisms),
cause_purviews=from_schema(s.cause_purviews),
effect_purviews=from_schema(s.effect_purviews),
max_purview_order_by_mechanism_order=(
None
if s.max_purview_order_by_mechanism_order is None
else tuple((m, p) for m, p in s.max_purview_order_by_mechanism_order)
),
)
_DECODERS[schema.CESScopeSchema] = _decode_ces_scope
from pyphi.campaign import CESShardTask
from pyphi.campaign import SIAShardTask
from pyphi.campaign.shards import ShardSpec
_ENCODERS[ShardSpec] = lambda s: schema.ShardSpecSchema(
payload_kind=s.payload_kind,
mechanisms=tuple(tuple(m) for m in s.mechanisms),
mechanism=None if s.mechanism is None else tuple(s.mechanism),
direction=s.direction,
purviews=tuple(tuple(p) for p in s.purviews),
purview=None if s.purview is None else tuple(s.purview),
stride=s.stride,
units=s.units,
memory_bytes=s.memory_bytes,
)
def _decode_shard_spec(s: schema.ShardSpecSchema) -> Any:
return ShardSpec(
payload_kind=s.payload_kind,
mechanisms=tuple(tuple(m) for m in s.mechanisms),
mechanism=None if s.mechanism is None else tuple(s.mechanism),
direction=s.direction,
purviews=tuple(tuple(p) for p in s.purviews),
purview=None if s.purview is None else tuple(s.purview),
stride=None if s.stride is None else (s.stride[0], s.stride[1]),
units=s.units,
memory_bytes=s.memory_bytes,
)
_DECODERS[schema.ShardSpecSchema] = _decode_shard_spec
_ENCODERS[CESShardTask] = lambda t: schema.CESShardTaskSchema(
task_id=t.task_id,
kind=t.kind,
substrate_label=t.substrate_label,
state=tuple(t.state),
subset=None if t.subset is None else tuple(t.subset),
scope=to_schema(t.scope),
config_overrides=dict(t.config_overrides),
formalism=t.formalism,
spec=to_schema(t.spec),
ordering=t.ordering,
)
def _decode_ces_shard_task(s: schema.CESShardTaskSchema) -> Any:
return CESShardTask(
task_id=s.task_id,
kind=s.kind,
substrate_label=s.substrate_label,
state=tuple(s.state),
subset=None if s.subset is None else tuple(s.subset),
scope=from_schema(s.scope),
config_overrides=dict(s.config_overrides),
formalism=s.formalism,
spec=from_schema(s.spec),
ordering=s.ordering,
)
_DECODERS[schema.CESShardTaskSchema] = _decode_ces_shard_task
_ENCODERS[SIAShardTask] = lambda t: schema.SIAShardTaskSchema(
task_id=t.task_id,
kind=t.kind,
substrate_label=t.substrate_label,
state=tuple(t.state),
subset=None if t.subset is None else tuple(t.subset),
config_overrides=dict(t.config_overrides),
formalism=t.formalism,
stride=(t.stride[0], t.stride[1]),
)
def _decode_sia_shard_task(s: schema.SIAShardTaskSchema) -> Any:
return SIAShardTask(
task_id=s.task_id,
kind=s.kind,
substrate_label=s.substrate_label,
state=tuple(s.state),
subset=None if s.subset is None else tuple(s.subset),
config_overrides=dict(s.config_overrides),
formalism=s.formalism,
stride=(s.stride[0], s.stride[1]),
)
_DECODERS[schema.SIAShardTaskSchema] = _decode_sia_shard_task
def _register_optimization_result() -> None:
from pyphi.optimize import OptimizationResult
def _encode_optimization_result(r: Any) -> Any:
best_objective = float(r.best_objective)
return schema.OptimizationResultSchema(
best_params=arrays.array_to_bytes(np.asarray(r.best_params, dtype=float)),
best_objective=None if math.isnan(best_objective) else best_objective,
best_substrate=to_schema(r.best_substrate),
best_sia=_enc_optional(r.best_sia),
trajectory=frames.dataframe_to_schema(r.trajectory),
bounds=tuple((float(lo), float(hi)) for lo, hi in r.bounds),
seed=int(r.seed),
direction=r.direction,
objective_name=r.objective_name,
settings=dict(r.settings),
config_snapshot=dict(r.config_snapshot),
n_evaluations=int(r.n_evaluations),
n_unreachable=int(r.n_unreachable),
)
_ENCODERS[OptimizationResult] = _encode_optimization_result
def _decode_optimization_result(s: schema.OptimizationResultSchema) -> Any:
return OptimizationResult(
best_params=arrays.bytes_to_array(s.best_params),
best_objective=math.nan if s.best_objective is None else s.best_objective,
best_substrate=from_schema(s.best_substrate),
best_sia=_dec_optional(s.best_sia),
trajectory=frames.schema_to_dataframe(s.trajectory),
bounds=[(lo, hi) for lo, hi in s.bounds],
seed=s.seed,
direction=s.direction,
objective_name=s.objective_name,
settings=s.settings,
config_snapshot=s.config_snapshot,
n_evaluations=s.n_evaluations,
n_unreachable=s.n_unreachable,
)
_DECODERS[schema.OptimizationResultSchema] = _decode_optimization_result
_REGISTERED = False
_REGISTRATION_LOCK = threading.Lock()
def _ensure_registered() -> None:
"""Populate the encoder/decoder registries on first use.
Registration imports the domain modules; deferring it to the first
``to_schema``/``from_schema`` call keeps ``import pyphi.serialize`` free of
domain imports (and free of import cycles).
Registration is atomic to observers: the flag is set only after every
registration has run, under a lock (double-checked), so a concurrent
caller never sees a partially populated registry.
"""
global _REGISTERED # noqa: PLW0603
if _REGISTERED:
return
with _REGISTRATION_LOCK:
if _REGISTERED:
return
_do_register()
_REGISTERED = True
def _do_register() -> None:
_register_direction()
_register_distance_result()
_register_node_labels()
_register_state_specification()
_register_system_state_specification()
_register_part()
_register_null_cut()
_register_directed_bipartition()
_register_joint_partition()
_register_joint_bipartition()
_register_joint_tripartition()
_register_directed_joint_partition()
_register_edge_cut()
_register_complete_edge_cut()
_register_directed_set_partition()
_register_ria()
_register_mice()
_register_distinction()
_register_distinctions()
_register_provenance()
_register_macro_unit()
_register_excluded_candidate()
_register_iit3_sia()
_register_iit4_sia()
_register_relation()
_register_relations()
_register_ces()
_register_substrate()
_register_system()
_register_transition()
_register_ac_ria()
_register_causal_link()
_register_account()
_register_ac_sia()
_register_complex()
_register_macro_system()
_register_complexes_result()
_register_analysis()
_register_coverage_report()
_register_substrate_posterior()
_register_phi_posterior()
_register_sweep_result()
_register_campaign()
_register_optimization_result()