from __future__ import annotations
from typing import Any, Tuple, Callable, cast
from ..checks import HermitianCheck, SquareMatrixCheck
from ..base import EuclideanJordanAlgebraSpace, StarSpace
from ._dense_coordinate import DenseCoordinateSpace
from ..._checks import checked_method
from ...types import DenseArray
from ...backend import Context
[docs]
class HermitianSpace(DenseCoordinateSpace, StarSpace, EuclideanJordanAlgebraSpace):
r"""
Represent dense Hermitian matrices with Frobenius geometry.
Elements are backend-native dense arrays with shape ``(n, n)``.
Membership enforces Hermitian structure up to tolerances.
The inner product is Frobenius / Hilbert-Schmidt:
``<X, Y> = vdot(vec(X), vec(Y))``, where ``vdot`` conjugates the
first argument according to backend rules.
``HermitianSpace`` currently uses Euclidean/Frobenius geometry in flattened
coordinates and does not expose custom geometry injection. Metric-aware
Hermitian geometries should be introduced as a separate class or explicit
extension.
Parameters
----------
n : int
Matrix dimension.
atol : float, optional
Absolute tolerance for Hermitian membership checks.
rtol : float, optional
Relative tolerance for Hermitian membership checks.
enforce_herm : bool, optional
Whether membership checks enforce Hermitian structure.
ctx : Context, str, or None, optional
Backend context specification.
Attributes
----------
n : int
Matrix dimension.
"""
def __init__(
self,
n: int,
atol: float = 0.0,
rtol: float = 0.0,
enforce_herm: bool = True,
ctx: Context | str | None = None,
):
if n <= 0:
raise ValueError("n must be positive.")
shape = (n, n)
super(HermitianSpace, self).__init__(shape, ctx)
self.atol = atol
self.rtol = rtol
self.enforce_herm = enforce_herm
# Equality is inherited from DenseCoordinateSpace (backend gate + field +
# shape (= n) + fixed Frobenius geometry). The membership tolerances
# ``atol``/``rtol``/``enforce_herm`` are deliberately NOT part of identity:
# they are validation policy (like ``check_level``), not the mathematical
# space of n x n Hermitian matrices they describe.
def _space_descriptor(self) -> str:
"""Return ``Herm(n)``; the real/complex field shows in the dtype tag."""
return f"Herm({self.n})"
@property
def n(self) -> int:
"""Matrix dimension of this Hermitian space."""
return self.shape[0]
def _local_checks(self):
"""Return membership checks local to Hermitian spaces."""
return (
SquareMatrixCheck(),
HermitianCheck(
atol=self.atol,
rtol=self.rtol,
enforce=self.enforce_herm,
),
)
[docs]
def is_hermitian(self, x: DenseArray) -> bool:
"""Return whether ``x`` satisfies this space's Hermitian check."""
return HermitianCheck(
atol=self.atol,
rtol=self.rtol,
enforce=self.enforce_herm,
).is_valid(self, x)
[docs]
def symmetrize(self, x: DenseArray) -> DenseArray:
r"""Project ``x`` onto the Hermitian subspace as :math:`(X + X^*) / 2`."""
x_adj = self.ops.conj(self.ops.swapaxes(x, -1, -2))
return (x + x_adj) * 0.5
[docs]
@checked_method(in_space="self")
def star(self, x: DenseArray) -> DenseArray:
"""Return the canonical star operation for Hermitian elements: identity."""
return x
[docs]
@checked_method(in_space="self", arg_positions=(0, 1))
def jordan(self, x: DenseArray, y: DenseArray) -> DenseArray:
"""Return the Hermitian Jordan product ``(xy + yx) / 2``."""
xy = self.ops.matmul(x, y)
yx = self.ops.matmul(y, x)
return self.symmetrize((xy + yx) * 0.5)
[docs]
def spectrum(self, x: DenseArray) -> DenseArray:
"""Return the Hermitian eigenvalue spectrum of ``x``."""
self._check_unbatched_member(x)
return self.ops.eigh(x)[0]
[docs]
def spectral_decompose(self, x: DenseArray) -> Tuple[DenseArray, DenseArray]:
"""Return the Hermitian eigendecomposition ``(evals, evecs)``."""
self._check_unbatched_member(x)
return self.ops.eigh(x)
[docs]
def from_spectrum(self, eigvals: DenseArray, frame: DenseArray) -> DenseArray:
"""Reconstruct a Hermitian element from eigenvalues and eigenvectors."""
return self.eig_to_dense(eigvals, frame)
[docs]
def trace(self, x: DenseArray) -> DenseArray:
"""Return the (real) trace as the diagonal sum, avoiding an eigendecomposition.
``'...ii->...'`` sums the trailing-two-axes diagonal and preserves leading
batch axes; ``ops.trace``/``ops.diagonal`` default to the *first* two axes
and are not batch-safe for ``(..., n, n)`` input.
"""
self._check_unbatched_member(x)
return self.ops.real(self.ops.einsum("...ii->...", x))
[docs]
def unit(self) -> DenseArray:
"""Return the Jordan identity: the ``n x n`` identity in the space dtype."""
return self.ops.eye(self.n, dtype=self.dtype)
[docs]
def unflatten(self, v: DenseArray) -> DenseArray:
"""Reshape dense coordinates and symmetrize the result."""
vv = self._coerce_dense(v)
X = vv.reshape(self.shape)
return self.symmetrize(X)
[docs]
@checked_method(in_space="self")
def psd_proj(self, x: DenseArray) -> DenseArray:
"""Project a Hermitian element onto the positive semidefinite cone."""
evals, evecs = self.spectral_decompose(x)
evals = self.ops.maximum(evals, cast(Any, 0.0))
return self.eig_to_dense(evals, evecs)
[docs]
def eig_to_dense(self, evals: DenseArray, evecs: DenseArray) -> DenseArray:
"""Reconstruct a Hermitian matrix from eigenvalues and eigenvectors.
The ``U diag(evals) U^*`` reconstruction is mathematically Hermitian but
accumulates floating-point skew at the level of a few ULP, which a
zero-tolerance Hermitian space would otherwise reject. Symmetrizing
projects onto the Hermitian part (a no-op up to that skew) so the
reconstruction is a valid member of this space.
"""
self.ctx.assert_dense(evals)
self.ctx.assert_dense(evecs)
X = self.ops.einsum("...ij,...j,...kj->...ik", evecs, evals, self.ops.conj(evecs))
X = self.symmetrize(X)
self._check_unbatched_member(X)
return X
def _convert(self, new_ctx: Context) -> HermitianSpace:
"""Convert this Hermitian space to ``new_ctx``."""
return HermitianSpace(self.n, self.atol, self.rtol, self.enforce_herm, new_ctx)
def _apply_entrywise(self, x: DenseArray, f: Callable[[DenseArray], DenseArray]) -> DenseArray:
"""Apply ``f`` entrywise and verify that shape is preserved."""
try:
y = f(x)
except Exception:
y = self.ops.vectorize(f)(x)
if self._checks_at_least("cheap") and y.shape != x.shape:
raise ValueError("Function application changed shape.")
return y
[docs]
@checked_method(in_space="self")
def spectral_apply(self, x: DenseArray, f: Callable[[DenseArray], DenseArray]) -> DenseArray:
r"""
Apply a scalar function to a Hermitian matrix via spectral calculus.
For a Hermitian matrix
$$
X \in \mathbb{H}^n,
$$
with eigendecomposition
$$
X = U \operatorname{diag}(\lambda) U^*,
$$
this method returns
$$
f(X) = U \operatorname{diag}(f(\lambda)) U^*,
$$
where ``f`` is applied entrywise to the eigenvalue vector
$$
\lambda \in \mathbb{R}^n.
$$
Parameters
----------
x:
Hermitian matrix in this space. Must have shape ``(n, n)`` and
satisfy the Hermitian membership conditions of the space.
f:
Callable applied to the eigenvalues of ``x``. It should accept a
dense backend array of eigenvalues and return an array of the same
shape.
Returns
-------
DenseArray
The Hermitian matrix obtained by spectral application of ``f`` to
``x``.
Raises
------
TypeError
If ``x`` is not a valid Hermitian element of this space.
Notes
-----
This is not an entrywise matrix transformation. The function is applied
to the spectrum of ``x``, not to its matrix entries.
In particular, if
$$
X = U \operatorname{diag}(\lambda) U^*,
$$
then the eigenvectors are preserved and only the eigenvalues are
transformed.
"""
evals, evecs = self.spectral_decompose(x)
fevals = self._apply_entrywise(evals, f)
return self.eig_to_dense(fevals, evecs)