Source code for spacecore.space.concrete._tree_space

from __future__ import annotations

from dataclasses import dataclass
from itertools import accumulate, pairwise
from math import prod
from typing import Any, Callable, Sequence

import optree

from ..._checks import minimum_check_level, normalize_check_level
from ...contextual import resolve_context_priority
from ...contextual import Context
from ...backend import CheckLevel
from ...backend import PyTreeNode
from ...types import DenseArray
from ..base import (
    CoordinateSpace,
    Space,
)
from ..checks import SpaceCheck, _run_checks

from .._capabilities import (
    CapabilitySet, registry_key,
    _space_capabilities,
)

_TREE_REGISTRY: dict[CapabilitySet, type[TreeSpace]] = {}


def _validate_leaf_spaces(
    leaf_spaces: Any, owner: str = "TreeSpace"
) -> tuple[CoordinateSpace, ...]:
    """Validate ordered finite-coordinate leaf spaces."""
    # A non-empty sequence of coordinate spaces.
    if not isinstance(leaf_spaces, Sequence):
        raise TypeError(
            f"{owner} requires a sequence of CoordinateSpace leaves; "
            f"got {type(leaf_spaces).__name__}."
        )
    spaces = tuple(leaf_spaces)
    if not spaces:
        raise ValueError(f"{owner} requires at least one leaf space.")
    for index, space in enumerate(spaces):
        if not isinstance(space, CoordinateSpace):
            raise TypeError(
                f"{owner} requires every leaf to be a CoordinateSpace; "
                f"leaf {index} is {type(space).__name__}."
            )
    return spaces


def _tree_capabilities(spaces: Sequence[Space]) -> CapabilitySet:
    """Return capabilities shared by every tree leaf."""
    # A capability is kept only if every leaf has it. Leaf validity was checked by each leaf's __init__.
    return frozenset.intersection(*map(_space_capabilities, spaces))


def _format_path(path: tuple[Any, ...]) -> str:
    """Format an optree leaf path as a compact Python-style location."""
    # JSONPath-like string such as $.point[0], for error messages.
    result = "$"
    for entry in path:
        if isinstance(entry, str) and entry.isidentifier():
            result += f".{entry}"
        else:
            result += f"[{entry!r}]"
    return result


[docs] @dataclass(frozen=True, eq=False) class TreeElement(PyTreeNode): r""" Leaves bound to a :class:`TreeSpace`; plain trees of the right structure work too. Leaves are not coerced; the leaf spaces check them. Parameters ---------- space : TreeSpace The space fixing leaf order and structure. leaves : sequence Leaf values in ``space.leaf_paths`` order. Raises ------ TypeError If ``space`` is not a TreeSpace. ValueError If the leaf count differs from ``space.arity``. Examples -------- >>> import numpy as np >>> import spacecore as sc >>> ctx = sc.Context(sc.NumpyOps(), dtype=np.float64) >>> X = sc.DenseCoordinateSpace((1,), ctx) >>> T = sc.TreeSpace((0, 0), (X, X), ctx=ctx) >>> element = sc.TreeElement(T, (ctx.asarray([1.0]), ctx.asarray([2.0]))) >>> element.value (array([1.]), array([2.])) """ space: "TreeSpace" leaves: tuple[Any, ...] def __init__(self, space: "TreeSpace", leaves: Sequence[Any]) -> None: # Leaves must match the space's arity; the fields are frozen. if not isinstance(space, TreeSpace): raise TypeError(f"TreeElement space must be a TreeSpace, got {type(space).__name__}.") leaves_tuple = tuple(leaves) if len(leaves_tuple) != space.arity: raise ValueError( f"TreeElement expected {space.arity} leaves, got {len(leaves_tuple)}." ) object.__setattr__(self, "space", space) object.__setattr__(self, "leaves", leaves_tuple) @property def value(self) -> Any: """Reconstruct the Python tree value represented by this element.""" return self.space.unflatten_tree(self.leaves) def __repr__(self) -> str: """Summarize leaves rather than dumping their full array contents.""" from ..._repr import summarize_value return f"TreeElement({self.space._short_repr()}, leaves={summarize_value(self.leaves)})"
[docs] def tree_flatten(self): """Expose element leaves as JAX pytree children.""" return self.leaves, self.space
[docs] @classmethod def tree_unflatten(cls, space: "TreeSpace", children: Sequence[Any]) -> "TreeElement": """Rebuild an element from JAX pytree children.""" return cls(space, tuple(children))
class _TreeStructureCheck(SpaceCheck): """Validate TreeElement ownership and leaf arity.""" name = "tree_structure" minimum_level = "cheap" def _failure(self, space: "TreeSpace", x: Any) -> str | None: try: space.flatten_tree(x) except Exception as exc: return str(exc) return None def is_valid(self, space: "TreeSpace", x: Any) -> bool: return self._failure(space, x) is None def error_message(self, space: "TreeSpace", x: Any) -> str: failure = self._failure(space, x) return "Invalid TreeSpace structure." if failure is None else failure class _TreeLeafCheck(SpaceCheck): """Validate each TreeElement leaf against its corresponding space.""" name = "tree_leaves" minimum_level = "cheap" def is_valid(self, space: "TreeSpace", x: Any) -> bool: return self.validate(space, x, allow_leading=False) def _failure(self, space: "TreeSpace", x: Any, allow_leading: bool) -> str | None: # Every leaf must pass its own leaf space's checks; report the first failure by its path. try: leaves = space._components(x) except Exception as exc: return str(exc) for path, leaf_space, leaf in zip(space.leaf_paths, space.leaf_spaces, leaves): try: _run_checks(leaf_space, leaf, allow_leading=allow_leading) except Exception as exc: return f"Invalid leaf at {_format_path(path)} for {type(leaf_space).__name__}: {exc}" return None def validate(self, space: "TreeSpace", x: Any, *, allow_leading: bool) -> bool: return self._failure(space, x, allow_leading) is None def error_message(self, space: "TreeSpace", x: Any) -> str: return self.validation_message(space, x, allow_leading=False) def validation_message(self, space: "TreeSpace", x: Any, *, allow_leading: bool) -> str: failure = self._failure(space, x, allow_leading) return "Invalid TreeSpace leaf." if failure is None else failure
[docs] class TreeSpace(PyTreeNode, CoordinateSpace): r""" Finite direct product :math:`\prod_\ell X_\ell` whose elements are Python trees. Any tuple, list, dict, named-tuple or optree-registered structure works. Vector operations, conversion, checks and batching act leaf by leaf, and a capability (inner product, star, Jordan) is offered only when every leaf has it. Parameters ---------- treedef : optree.PyTreeSpec or tree Structure, or an example tree; its leaf order pairs with ``leaf_spaces``. leaf_spaces : sequence of CoordinateSpace Leaf spaces, in leaf order. ctx : Context, str, or None, optional Backend context; resolved from ``leaf_spaces`` when omitted. check_level : {"none", "cheap", "standard", "strict"}, optional Validation level; the ambient default when omitted. Raises ------ TypeError If ``leaf_spaces`` are not coordinate spaces. ValueError If there are no leaves or the leaf count does not match the tree. Examples -------- >>> import numpy as np >>> import spacecore as sc >>> ctx = sc.Context(sc.NumpyOps(), dtype=np.float64) >>> X = sc.DenseCoordinateSpace((2,), ctx) >>> S = sc.DenseCoordinateSpace((1,), ctx) >>> T = sc.TreeSpace({"point": 0, "weight": 0}, (X, S), ctx=ctx) >>> x = {"point": ctx.asarray([1.0, 2.0]), "weight": ctx.asarray([3.0])} >>> T.flatten(x) array([1., 2., 3.]) >>> T.scale(2.0, x)["point"] array([2., 4.]) """ _fields = ("treedef", "leaf_spaces", "ctx") def __new__( cls, treedef: optree.PyTreeSpec | Any, leaf_spaces: Sequence[CoordinateSpace], *, ctx: Context | str | None = None, check_level: CheckLevel | str | None = None, ): # Pick the subclass matching the capabilities all leaves share. if cls is TreeSpace: spaces = _validate_leaf_spaces(leaf_spaces) resolved_ctx = resolve_context_priority(ctx, *spaces) converted = tuple(space.convert(resolved_ctx) for space in spaces) cls = _TREE_REGISTRY.get(registry_key(_tree_capabilities(converted)), TreeSpace) return super(TreeSpace, cls).__new__(cls) def __init__( self, treedef: optree.PyTreeSpec | Any, leaf_spaces: Sequence[CoordinateSpace], *, ctx: Context | str | None = None, check_level: CheckLevel | str | None = None, ) -> None: # The structure must have exactly one leaf per leaf space. spaces = _validate_leaf_spaces(leaf_spaces, type(self).__name__) resolved_ctx = resolve_context_priority(ctx, *spaces) treespec = treedef if isinstance(treedef, optree.PyTreeSpec) else optree.tree_structure(treedef) if treespec.num_leaves != len(spaces): raise ValueError( "TreeSpace leaf-count mismatch: " f"treedef has {treespec.num_leaves} leaves but {len(spaces)} leaf spaces were given." ) # Convert leaves to one context; record flat sizes and slices into the coordinate vector. uniform_spaces = tuple(space.convert(resolved_ctx) for space in spaces) self._treedef = treespec self._leaf_paths = tuple(optree.treespec_paths(treespec)) self.leaf_spaces = uniform_spaces offsets = (0, *accumulate(prod(map(int, s.shape)) for s in uniform_spaces)) self._slices = tuple(slice(a, b) for a, b in pairwise(offsets)) # Coordinate shape is the total size; check level: explicit, else the least strict leaf. super(TreeSpace, self).__init__((offsets[-1],), resolved_ctx) if check_level is not None: self._check_level = normalize_check_level(check_level) else: leaf_levels = tuple(space.check_level for space in uniform_spaces) self._check_level = minimum_check_level(leaf_levels)
[docs] @classmethod def from_leaf_spaces( cls, leaf_spaces: Sequence[CoordinateSpace], ctx: Context | str | None = None, *, check_level: CheckLevel | str | None = None, ) -> "TreeSpace": """Build a tuple-structured ``TreeSpace`` from ordered leaf spaces.""" spaces = _validate_leaf_spaces(leaf_spaces) return cls(tuple(range(len(spaces))), spaces, ctx=ctx, check_level=check_level)
[docs] @classmethod def from_template( cls, template: Any, leaf_spaces: Sequence[CoordinateSpace], *, ctx: Context | str | None = None, check_level: CheckLevel | str | None = None, ) -> "TreeSpace": """Build a tree space from an example Python tree value.""" return cls(template, leaf_spaces, ctx=ctx, check_level=check_level)
def _eq_algebra(self, other: Any) -> bool: # Tier 2: tree structure (treedef) + ordered leaf spaces. return ( super()._eq_algebra(other) and self.treedef == other.treedef and self.leaf_spaces == other.leaf_spaces ) @property def treedef(self) -> optree.PyTreeSpec: """Return the immutable optree structure definition.""" return self._treedef @property def leaf_paths(self) -> tuple[tuple[Any, ...], ...]: """Return paths in the same deterministic order as ``leaf_spaces``.""" return self._leaf_paths @property def arity(self) -> int: """Return the number of ordered leaves.""" return len(self.leaf_spaces) def _repr_class_name(self) -> str: """Present the public ``TreeSpace`` label, not the private dispatch subclass.""" return "TreeSpace" def _space_descriptor(self) -> str: """Return ``Tree(<leaf descriptors>)``, abbreviating wide trees.""" from ..._repr import describe_space leaves = self.leaf_spaces shown = [describe_space(leaf) for leaf in leaves[:4]] if len(leaves) > 4: shown.append(f"…(+{len(leaves) - 4})") return f"Tree({', '.join(shown)})" def _local_checks(self): return _TreeStructureCheck("tree_structure"), _TreeLeafCheck("tree_leaves")
[docs] def flatten_tree(self, value: Any) -> tuple[Any, ...]: """Flatten a matching Python tree value into ordered leaves.""" # A bound TreeElement already holds ordered leaves; a plain tree must match the structure. if isinstance(value, TreeElement): if value.space != self: raise TypeError("TreeElement is bound to a different TreeSpace.") return value.leaves leaves, treespec = optree.tree_flatten(value) if treespec != self.treedef: raise TypeError( f"TreeSpace structure mismatch: expected {self.treedef}, got {treespec}." ) return tuple(leaves)
[docs] def unflatten_tree(self, leaves: Sequence[Any]) -> Any: """Rebuild the configured Python tree from ordered leaves.""" leaves_tuple = tuple(leaves) if len(leaves_tuple) != self.arity: raise ValueError(f"TreeSpace expected {self.arity} leaves, got {len(leaves_tuple)}.") return optree.tree_unflatten(self.treedef, leaves_tuple)
[docs] def element(self, value: Any) -> TreeElement: """Bind a matching Python tree value to this space.""" return TreeElement(self, self.flatten_tree(value))
def _components(self, x: TreeElement) -> tuple[Any, ...]: return self.flatten_tree(x) def _from_components(self, parts: tuple[Any, ...]) -> Any: return self.unflatten_tree(parts)
[docs] def zero(self) -> Any: """Return the additive identity.""" return self.zeros()
[docs] def zeros(self) -> Any: """Return the leafwise additive identity.""" return self._from_components(tuple(space.zeros() for space in self.leaf_spaces))
[docs] def ones(self) -> Any: """Return a leafwise all-ones element when leaf spaces support it.""" # Each leaf's own ones if it has them, else an array of ones. parts = [] for space in self.leaf_spaces: ones = getattr(space, "ones", None) parts.append(ones() if callable(ones) else self.ops.ones(space.shape, dtype=self.dtype)) return self._from_components(tuple(parts))
def _leafwise(self, fn: Callable[..., Any], *xs: Any) -> tuple[Any, ...]: """Return ``fn(leaf_space, leaf_of_x, leaf_of_y, ...)`` for each leaf, in leaf order.""" return tuple(map(fn, self.leaf_spaces, *map(self._components, xs))) def _add_core(self, x: Any, y: Any) -> Any: """Return the componentwise sum.""" return self._from_components(self._leafwise(lambda s, xi, yi: s._add_core(xi, yi), x, y)) def _add_batch_core(self, x: Any, y: Any) -> Any: """Return the componentwise leading-axis batch sum.""" return self._from_components(self._leafwise(lambda s, xi, yi: s._add_batch_core(xi, yi), x, y)) def _scale_core(self, a: Any, x: Any) -> Any: """Return the componentwise scalar product.""" return self._from_components(self._leafwise(lambda s, leaf: s._scale_core(a, leaf), x)) def _scale_batch_core(self, a: Any, x: Any) -> Any: """Return the componentwise batched scalar product.""" return self._from_components(self._leafwise(lambda s, leaf: s._scale_batch_core(a, leaf), x))
[docs] def stacked(self, count: int) -> "TreeSpace": """Return a tree whose leaves are stacked leaf spaces.""" return TreeSpace( self.treedef, tuple(space.stacked(count) for space in self.leaf_spaces), ctx=self.ctx, )
def _flatten_core(self, x: Any) -> DenseArray: """Concatenate leaf coordinate vectors into one dense vector.""" # Concatenate the leaves' flat coordinates in leaf order. parts = [] for space, leaf in zip(self.leaf_spaces, self._components(x)): coordinates = space._flatten_core(leaf) if self._checks_at_least("cheap"): coordinates = self.ctx.assert_dense(coordinates) parts.append(coordinates) if len(parts) == 1: return parts[0] return self.ops.concatenate(parts, axis=0) def _unflatten_core(self, v: DenseArray) -> Any: """Split dense coordinates into a tree element.""" # Slice the flat vector back into leaves and rebuild the tree. if self._checks_at_least("cheap"): v = self.ctx.assert_dense(v) v = v.reshape((-1,)) # self.shape is (size,), so a matching v is unchanged leaves = tuple( space._unflatten_core(v[leaf_slice]) for space, leaf_slice in zip(self.leaf_spaces, self._slices) ) return self._from_components(leaves) def _flatten_batch_core(self, xs: Any) -> DenseArray: """Concatenate batched leaf coordinates into shape ``(N, size)``.""" parts = self._leafwise(lambda s, leaf: s._flatten_batch_core(leaf), xs) if len(parts) == 1: return parts[0] return self.ops.concatenate(parts, axis=1) def _unflatten_batch_core(self, vs: DenseArray) -> Any: """Split batched dense coordinates into batched leaves.""" if self._checks_at_least("cheap"): vs = self.ctx.assert_dense(vs) return self._from_components( tuple( space._unflatten_batch_core(vs[:, leaf_slice]) for space, leaf_slice in zip(self.leaf_spaces, self._slices) ) )
[docs] def check(self, x: Any) -> None: """Validate an element according to this space's check level.""" self.check_member(x)
[docs] def convert_element( self, x: Any, new_ctx: Context | str | None = None ) -> Any: """Convert each leaf and preserve the configured tree structure.""" # Move each leaf through its flat coordinates onto the target context. target = self.convert(new_ctx) converted = [] for source_space, target_space, leaf in zip( self.leaf_spaces, target.leaf_spaces, self._components(x) ): coordinates = source_space._flatten_core(leaf) converted.append(target_space._unflatten_core(target.ctx.asarray(coordinates))) return target._from_components(tuple(converted))
# Imported last: the capability subclasses derive from TreeSpace and fill _TREE_REGISTRY. from ._tree_capabilities import TreeSpectralDecomposition # noqa: E402 __all__ = [ "TreeElement", "TreeSpace", "TreeSpectralDecomposition", "_space_capabilities", "_tree_capabilities", ]