Source code for spacecore.space._vector
from __future__ import annotations
from math import prod
from typing import Any, Tuple, Callable
from ._base import Space
from ._checks import BackendCheck, DTypeCheck, ShapeCheck
from ..types import DenseArray
from ..backend import Context
[docs]
class VectorSpace(Space):
"""
Dense vector space R^{n1, ..., nK} or C^{n1, ..., nK}.
Elements:
- backend-native dense arrays;
- canonical shape is (n1, ..., nK).
Geometry:
- Euclidean / ℓ2 inner product
⟨x, y⟩ = vdot(x, y).
"""
def __init__(self, shape: Tuple[int, ...], ctx: Context | str | None = None) -> None:
super(VectorSpace, self).__init__(shape, ctx)
self._size = prod(self.shape)
self._is_flat_shape = self.shape == (self._size,)
def _local_checks(self):
return BackendCheck(), ShapeCheck(), DTypeCheck()
[docs]
def zeros(self) -> DenseArray:
return self.ops.zeros(self.shape, dtype=self.dtype)
[docs]
def add(self, x: Any, y: Any) -> DenseArray:
if self._enable_checks:
self._check_member(x)
self._check_member(y)
return x + y
[docs]
def scale(self, a: Any, x: Any) -> DenseArray:
if self._enable_checks:
self._check_member(x)
return a * x
[docs]
def inner(self, x: Any, y: Any) -> Any:
if self._enable_checks:
self._check_member(x)
self._check_member(y)
return self.ops.vdot(x, y)
[docs]
def eigh(self, x: Any, k: int = None) -> Any:
raise TypeError(
f"{type(self).__name__}.eigh is not defined for vector spaces."
)
[docs]
def flatten(self, X: DenseArray) -> DenseArray:
if self._enable_checks:
self._check_member(X)
return X if self._is_flat_shape else X.reshape((-1,))
[docs]
def unflatten(self, v: DenseArray) -> DenseArray:
V = self.ctx.assert_dense(v) if self._enable_checks else v
return V if self._is_flat_shape else V.reshape(self.shape)
def _convert(self, new_ctx: Context) -> VectorSpace:
return VectorSpace(self.shape, new_ctx)
def _apply_entrywise(self, x: DenseArray, f: Callable[[DenseArray], DenseArray]) -> DenseArray:
try:
y = f(x)
except Exception:
# optional fallback if backend has vectorize/map
y = self.ops.vectorize(f)(x)
if self._enable_checks:
if y.shape != x.shape:
raise ValueError("Function application changed shape.")
return y
[docs]
def apply(self, x: DenseArray, f: Callable[[DenseArray], DenseArray]) -> DenseArray:
r"""
Apply a scalar function to a vector-space element entrywise.
For a space element
$$
x \in \mathbb{K}^{n_1 \times \cdots \times n_k},
$$
this method returns the element
$$
y = f(x)
$$
obtained by applying ``f`` coordinatewise to the entries of ``x``.
Parameters
----------
x:
Element of this vector space. Must have shape ``self.shape`` and
dtype compatible with this space.
f:
Callable representing an entrywise transformation. It is expected
to act elementwise on backend arrays, or to be compatible with the
backend vectorization fallback.
Returns
-------
DenseArray
The transformed element, with the same shape as ``x``.
Raises
------
TypeError
If ``x`` is not a valid member of this space.
ValueError
If the result of the application does not preserve the shape of the
space element.
Notes
-----
This is the canonical functional calculus for ``VectorSpace``:
application is performed entrywise in the distinguished coordinate
representation.
"""
if self._enable_checks:
self._check_member(x)
y = self._apply_entrywise(x, f)
return y