amduat-api/notes/canonical.py

114 lines
3 KiB
Python
Raw Normal View History

# canonical.py
from __future__ import annotations
import numpy as np
from typing import Tuple
# ---------------------------------------------------------------------
# Canonicalization configuration
# ---------------------------------------------------------------------
# Numerical tolerance for zero detection
EPSILON: float = 1e-12
# ---------------------------------------------------------------------
# Canonicalization helpers
# ---------------------------------------------------------------------
def _normalize(values: np.ndarray) -> np.ndarray:
"""
Normalize a complex amplitude vector.
"""
norm = np.linalg.norm(values)
if norm == 0:
raise ValueError("Cannot canonicalize zero-norm state")
return values / norm
def _remove_global_phase(values: np.ndarray) -> np.ndarray:
"""
Remove global phase by forcing the first non-zero amplitude
to be real and non-negative.
"""
for v in values:
if abs(v) > EPSILON:
phase = np.angle(v)
values = values * np.exp(-1j * phase)
if values.real[0] < 0:
values *= -1
break
return values
# ---------------------------------------------------------------------
# Public canonicalization API
# ---------------------------------------------------------------------
def canonicalize_sparse(
indices: np.ndarray,
values: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray]:
"""
Canonicalize a sparse amplitude representation.
Guarantees:
- Deterministic normalization
- Global phase removed
- Output arrays are copies (caller mutation-safe)
- Index ordering preserved (caller responsibility)
Parameters
----------
indices:
Integer basis indices (shape: [k])
values:
Complex amplitudes (shape: [k])
Returns
-------
(indices, values):
Canonicalized sparse representation
"""
if indices.ndim != 1 or values.ndim != 1:
raise ValueError("indices and values must be 1-D arrays")
if len(indices) != len(values):
raise ValueError("indices and values must have the same length")
# Copy defensively
ci = np.array(indices, dtype=np.int64, copy=True)
cv = np.array(values, dtype=np.complex128, copy=True)
# Normalize
cv = _normalize(cv)
# Canonical global phase
cv = _remove_global_phase(cv)
return ci, cv
# ---------------------------------------------------------------------
# Optional utilities (explicit, not implicit)
# ---------------------------------------------------------------------
def canonicalize_dense(
amplitudes: np.ndarray,
) -> np.ndarray:
"""
Canonicalize a dense amplitude vector.
Provided for completeness and testing;
sparse canonicalization is preferred for infrastructure.
"""
if amplitudes.ndim != 1:
raise ValueError("amplitudes must be a 1-D array")
values = np.array(amplitudes, dtype=np.complex128, copy=True)
values = _normalize(values)
values = _remove_global_phase(values)
return values