from __future__ import annotations
from typing import Iterable, Sequence
import numpy as np
from graphcalc.quantum.states import QuantumState
__all__ = ["QuantumMeasurement"]
[docs]
class QuantumMeasurement:
"""
Finite quantum measurement represented by measurement operators.
The measurement is stored as a family ``(M_a)`` of operators on a fixed
finite-dimensional Hilbert space. The corresponding effects are
``E_a = M_a^dagger M_a``.
Conventions
-----------
- all measurement operators act on the same Hilbert space
- the measurement is complete when ``sum_a E_a = I``
- projective measurements are included as a special case
- post-measurement states are normalized conditional states
Parameters
----------
operators : iterable of array-like
Measurement operators.
dim : int | None, default=None
Hilbert-space dimension. If omitted, inferred from the operators.
validate : bool, default=True
Whether to validate the measurement as complete.
tol : float, default=1e-9
Numerical tolerance used in validation.
"""
def __init__(
self,
operators: Iterable[Sequence[Sequence[complex]] | np.ndarray],
*,
dim: int | None = None,
validate: bool = True,
tol: float = 1e-9,
) -> None:
self.tol = float(tol)
self._operators = [np.array(op, dtype=complex, copy=True) for op in operators]
if not self._operators:
raise ValueError("operators must be a nonempty iterable.")
first_shape = self._operators[0].shape
if len(first_shape) != 2 or first_shape[0] != first_shape[1]:
raise ValueError("Each measurement operator must be a square matrix.")
inferred_dim = first_shape[0]
self.dim = inferred_dim if dim is None else int(dim)
self._validate_parameters()
if validate:
self.validate()
def __repr__(self) -> str:
return (
f"QuantumMeasurement(num_outcomes={self.num_outcomes}, "
f"dim={self.dim}, tol={self.tol})"
)
@property
def operators(self) -> tuple[np.ndarray, ...]:
"""Return copies of the measurement operators."""
return tuple(op.copy() for op in self._operators)
@property
def num_outcomes(self) -> int:
"""Return the number of outcomes."""
return len(self._operators)
[docs]
@classmethod
def from_projectors(
cls,
projectors: Iterable[Sequence[Sequence[complex]] | np.ndarray],
*,
dim: int | None = None,
validate: bool = True,
tol: float = 1e-9,
) -> "QuantumMeasurement":
"""
Construct a projective measurement from projectors.
Notes
-----
For projective measurements, the measurement operators and effects
coincide.
"""
return cls(projectors, dim=dim, validate=validate, tol=tol)
[docs]
@classmethod
def computational_basis(
cls,
*,
dim: int = 2,
tol: float = 1e-9,
) -> "QuantumMeasurement":
"""
Return the computational-basis projective measurement in dimension ``dim``.
"""
if dim <= 0:
raise ValueError("dim must be positive.")
ops = []
for i in range(dim):
proj = np.zeros((dim, dim), dtype=complex)
proj[i, i] = 1.0
ops.append(proj)
return cls.from_projectors(ops, dim=dim, tol=tol)
def _validate_parameters(self) -> None:
if self.dim <= 0:
raise ValueError("dim must be positive.")
if self.tol < 0:
raise ValueError("tol must be nonnegative.")
for op in self._operators:
if len(op.shape) != 2 or op.shape != (self.dim, self.dim):
raise ValueError(
f"Each measurement operator must have shape ({self.dim}, {self.dim})."
)
[docs]
def effects(self) -> tuple[np.ndarray, ...]:
"""Return the POVM effects ``M_a^dagger M_a``."""
return tuple(op.conj().T @ op for op in self._operators)
[docs]
def validate(self) -> None:
"""
Validate that the measurement is complete.
Notes
-----
Completeness means that the effects satisfy ``sum_a E_a = I``.
"""
self._validate_parameters()
total = np.zeros((self.dim, self.dim), dtype=complex)
for eff in self.effects():
herm = 0.5 * (eff + eff.conj().T)
evals = np.linalg.eigvalsh(herm)
if np.any(evals < -self.tol):
raise ValueError("Each effect must be positive semidefinite.")
total += eff
ident = np.eye(self.dim, dtype=complex)
if not np.allclose(total, ident, atol=self.tol, rtol=0.0):
raise ValueError("Measurement effects must sum to the identity.")
[docs]
def outcome_probability(self, state: QuantumState, outcome: int) -> float:
"""
Return the probability of a specified outcome.
"""
if state.dimension != self.dim:
raise ValueError(
f"State dimension {state.dimension} does not match measurement dim={self.dim}."
)
if not 0 <= outcome < self.num_outcomes:
raise ValueError("Outcome index out of range.")
eff = self.effects()[outcome]
prob = np.trace(eff @ state.rho)
return float(prob.real)
[docs]
def outcome_probabilities(self, state: QuantumState) -> np.ndarray:
"""
Return the vector of outcome probabilities.
"""
probs = np.array(
[self.outcome_probability(state, i) for i in range(self.num_outcomes)],
dtype=float,
)
probs[np.abs(probs) < self.tol] = 0.0
return probs
[docs]
def post_measurement_state(
self,
state: QuantumState,
outcome: int,
) -> QuantumState:
"""
Return the normalized post-measurement state conditioned on an outcome.
Notes
-----
If the outcome probability is zero, this function raises ``ValueError``.
"""
if state.dimension != self.dim:
raise ValueError(
f"State dimension {state.dimension} does not match measurement dim={self.dim}."
)
if not 0 <= outcome < self.num_outcomes:
raise ValueError("Outcome index out of range.")
op = self._operators[outcome]
prob = self.outcome_probability(state, outcome)
if prob <= self.tol:
raise ValueError("Post-measurement state is undefined for zero-probability outcome.")
rho = op @ state.rho @ op.conj().T / prob
return QuantumState.from_density(rho, dims=(self.dim,), tol=state.tol)