114 lines
3 KiB
Python
114 lines
3 KiB
Python
|
|
# 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
|
||
|
|
|