Source code for spacecore.functional._base

from __future__ import annotations

from functools import wraps
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar

from .._batching import _leading_batch_size, _warn_vmap_fallback_once

from .._checks import checked_method, mark_checked
from ..mapping import Mapping
from ..contextual import Context, resolve_context_priority
from .._checks import CheckLevel
from ..space import Space, RealField

if TYPE_CHECKING:
    from ..linop import LinOp
    from ._linear import LinearFunctional


Domain = TypeVar("Domain", bound=Space)


_Method = TypeVar("_Method", bound=Callable[..., Any])


def _checked_pair(method: _Method) -> _Method:
    """Check a ``(value, second)`` evaluator: input in the domain, value in the codomain.

    ``second`` is checked against the domain only for ``value_and_grad``; a derivative is
    a functional, not an element.
    """
    pair_with_grad = method.__name__ == "value_and_grad"

    @wraps(method)
    def outputs_checked(self: Any, *args: Any, **kwargs: Any) -> Any:
        value, second = method(self, *args, **kwargs)
        if self.check_level != "none":
            self.codomain._check_member(value)
            if pair_with_grad:
                self.domain._check_member(second)
        return value, second

    checked = checked_method(in_space="domain")(outputs_checked)
    mark_checked(checked, _checked_pair, f"_{method.__name__}_core")
    return checked


[docs] class Functional(Mapping, Generic[Domain]): r""" A real-valued map ``F : X -> R``; subclasses implement ``value`` and optionally ``grad``. ``grad`` is the Riesz gradient, an element of ``X``. Batched evaluation falls back to ``vmap``. Parameters ---------- dom : Space Domain ``X``. ctx : Context, str, or None, optional Backend context; resolved from ``dom`` when omitted. check_level : {"none", "cheap", "standard", "strict"}, optional Validation level; the ambient default when omitted. """ def __init__( self, dom: Domain, ctx: Context | str | None = None, check_level: CheckLevel | None = None, ) -> None: # Domain storage may be complex; functional values are real scalars. resolved = resolve_context_priority(ctx, dom) dom = dom.convert(resolved) (self.dom,) = self._bind_context(resolved, dom, check_level=check_level) # Output checks follow the functional's policy. self.cod = RealField(self.ctx, check_level=self.check_level) def _init_map(self, domain: Any, codomain: Any, ctx: Any, check_level: Any) -> None: """Bind a node of this family (the hook the shared lazy nodes in ``mapping`` call).""" Functional.__init__(self, domain, ctx, check_level=check_level) @property def codomain(self) -> RealField: """The independently bound scalar field of this functional's values.""" return self.cod @property def domain(self) -> Domain: """Domain space of this scalar-valued map.""" return self.dom # Public evaluators: check, then call the core. Subclasses implement the cores # (``_value_core``, ``_grad_core``, ...); composites call their children's cores.
[docs] @checked_method(in_space="domain", out_space="codomain") def value(self, x: Any, *args: Any, **kwargs: Any) -> Any: """Evaluate this functional at an element of ``self.domain``. Subclasses may accept extra positional/keyword arguments — auxiliary parameters such as data, temperature, or a penalty weight — that are not part of the domain. Overrides that take only ``x`` remain valid. """ return self._value_core(x, *args, **kwargs)
[docs] @checked_method(in_space="domain", out_space="domain") def grad(self, x: Any, *args: Any, **kwargs: Any) -> Any: """Riesz gradient at an element of ``self.domain``. Implement ``_grad_core``, or ``_derivative_core`` with an available Riesz representer; otherwise raises ``NotImplementedError``. Any auxiliary ``*args``/``**kwargs`` mirror those accepted by :meth:`value`. """ return self._grad_core(x, *args, **kwargs)
[docs] @_checked_pair def value_and_grad(self, x: Any, *args: Any, **kwargs: Any) -> tuple[Any, Any]: """Return ``(value, gradient)`` at ``x``. Implement ``_value_and_grad_core`` for a single-pass evaluator (e.g. ``jax.value_and_grad``); it must return the same pair as the separate cores, with the gradient in the same geometry as :meth:`grad`. """ return self._value_and_grad_core(x, *args, **kwargs)
[docs] @checked_method(in_space="domain", out_space="domain", core=None) def prox(self, x: Any, step: Any) -> Any: r"""Minimize ``f(y) + ||y-x||_X^2 / (2 step)`` for positive ``step``. This optional operation uses a closed-form rule registered with ``register("prox", FunctionalType, GeometryType, rule)``. The rule takes ``(functional, geometry, x, step)`` and returns a native domain element. An unsupported functional/geometry pair raises ``CapabilityError``; sums and linear pullbacks do not acquire a prox automatically. Prox rules assert a proper, lower-semicontinuous convex real objective (also on complex spaces, using the real inner product). At check level ``"none"``, traced steps must be finite and positive; their values are the caller's responsibility. Concrete invalid Python steps still raise. """ from ._prox import apply_prox return apply_prox(self, x, step)
[docs] def conjugate_prox(self, x: Any, step: Any) -> Any: r"""Return the conjugate prox by Hilbert-space Moreau decomposition. The conjugate is ``f*_X(p) = sup_z Re <p,z>_X - f(z)``: dual variables are Riesz representers in the same space, not raw coordinate covectors. Thus ``prox_(step f*_X)(x) = x - step prox_(f/step)(x/step)``. Requires a valid convex prox and a finite, strictly positive step. """ from ._prox import validate_step validate_step(step, self, "conjugate_prox") X = self.domain return X.axpy(-step, self.prox(X.scale(1 / step, x), 1 / step), x)
[docs] @checked_method(in_space="domain") def differential(self, x: Any, *args: Any, **kwargs: Any) -> Any: r"""Return the covector :math:`df(x)` with :math:`Df(x)[h] = \langle df(x), h\rangle`. The covector is a coordinate array paired with ``domain.pair`` and involves no metric. The default reads it off the Riesz gradient, ``domain.riesz(grad(x))``; functionals that compute coordinate partials implement ``_differential_core``. """ return self._differential_core(x, *args, **kwargs)
[docs] @checked_method(in_space="domain", core="_derivative_core") def derivative_at(self, x: Any, *args: Any, **kwargs: Any) -> "LinearFunctional": """Return the differential at ``x``, acting on domain directions. The default wraps the Riesz gradient, ``Re <grad, h>``. Implement ``_derivative_core`` for a native directional evaluator. At nonsmooth points a supplied subgradient is not a Frechet derivative. """ return self._derivative_core(x, *args, **kwargs)
def _derivative_from_gradient(self, gradient: Any) -> "LinearFunctional": from ._linear import InnerProductFunctional return InnerProductFunctional( gradient, self.domain, self.ctx, check_level=self.check_level, )
[docs] @_checked_pair def value_and_derivative(self, x: Any, *args: Any, **kwargs: Any) -> tuple[Any, "LinearFunctional"]: """Return value and differential, preserving fused gradient evaluators. Derivative-first subclasses may implement ``_value_and_derivative_core`` to share work. No representer is reconstructed from a callable differential. """ return self._value_and_derivative_core(x, *args, **kwargs)
[docs] @checked_method(in_space="domain", out_space="domain", in_batched=True, out_batched=True) def vgrad(self, xs: Any) -> Any: """Gradient over a leading batch axis; implement ``_vgrad_core`` to support it.""" return self._vgrad_core(xs)
# Default cores. An undecorated public override becomes the class's core (see # install_public_checks), so these defaults only derive one core from another. def _has_native_derivative(self) -> bool: return type(self)._derivative_core is not Functional._derivative_core def _value_core(self, x: Any, *args: Any, **kwargs: Any) -> Any: """Check-free value core.""" raise NotImplementedError(f"{type(self).__name__} defines neither value nor _value_core.") def _grad_core(self, x: Any, *args: Any, **kwargs: Any) -> Any: """Check-free gradient core; the base reads a native derivative's representer.""" if self._has_native_derivative(): return self._derivative_core(x, *args, **kwargs).representer raise NotImplementedError(f"{type(self).__name__} does not implement grad().") def _value_and_grad_core(self, x: Any, *args: Any, **kwargs: Any) -> tuple[Any, Any]: """Check-free ``(value, grad)``; the base evaluates the two cores separately.""" return self._value_core(x, *args, **kwargs), self._grad_core(x, *args, **kwargs) def _derivative_core(self, x: Any, *args: Any, **kwargs: Any) -> "LinearFunctional": """Check-free derivative; the base wraps the gradient core.""" return self._derivative_from_gradient(self._grad_core(x, *args, **kwargs)) def _value_and_derivative_core(self, x: Any, *args: Any, **kwargs: Any) -> tuple[Any, "LinearFunctional"]: """Check-free ``(value, derivative)``, using a native derivative when one exists.""" if self._has_native_derivative(): return self._value_core(x, *args, **kwargs), self._derivative_core(x, *args, **kwargs) value, gradient = self._value_and_grad_core(x, *args, **kwargs) return value, self._derivative_from_gradient(gradient) def _differential_core(self, x: Any, *args: Any, **kwargs: Any) -> Any: """Check-free covector; the base applies the domain's Riesz map to the gradient core.""" return self.domain._riesz_core(self._grad_core(x, *args, **kwargs)) def _vvalue_core(self, xs: Any) -> Any: """Check-free batched-value core; the base vmaps ``value``.""" return self._vmap_vvalue(xs) def _vmap_vvalue(self, xs: Any) -> Any: # Generic batching: vmap the single-element value over the leading axis. _warn_vmap_fallback_once(self, "vvalue", _leading_batch_size(self.domain, xs)) return self.ops.vmap(self.value, in_axes=0, out_axes=0)(xs) def _vgrad_core(self, xs: Any) -> Any: """Check-free batched-gradient core.""" raise NotImplementedError(f"{type(self).__name__} does not implement vgrad().") def __call__(self, x: Any, *args: Any, **kwargs: Any) -> Any: """Evaluate this functional at ``x``.""" return self.value(x, *args, **kwargs)
[docs] def compose(self, A: "LinOp") -> "Functional": """ Return the pull-back ``self o A``. Parameters ---------- A : LinOp Linear operator whose codomain matches this functional's domain. Returns ------- Functional Functional on ``A.domain`` evaluating ``self.value(A.apply(x))``. """ from ._composed import make_functional_composed return make_functional_composed(self, A)
[docs] @checked_method(in_space="domain", out_space="codomain", in_batched=True, out_batched=True, out_batched_scalar=True) def vvalue(self, xs: Any) -> Any: """Evaluate over a leading batch axis. Input must have shape ``(N,) + domain.shape``; use ``moveaxis`` for other layouts.""" return self._vvalue_core(xs)
[docs] def assert_domain(self, x: Any) -> None: """Raise if ``x`` is not in the domain.""" self.dom.check_member(x)
# tree_flatten / tree_unflatten are inherited from PyTreeNode, which owns the # flatten contract for every SpaceCore container and auto-registers concrete # subclasses with each backend's tree protocol. Every concrete Functional is a # container, so the capability belongs on this base rather than being # re-declared (and separately registered) per functional. Functional._family = Functional