"""Common classes and functions.
This module provides:
- `Plane`: Measurement planes for the MBQC.
- `Axis`: Measurement axis.
- `Sign`: Measurement sign.
- `MeasBasis`: Abstract class to represent a measurement basis.
- `PlannerMeasBasis`: Class to represent a planner measurement basis.
- `AxisMeasBasis`: Class to represent an axis measurement basis.
- `is_close_angle`: Check if an angle is close to a target angle.
- `is_clifford_angle`: Check if an angle is a Clifford angle.
- `determine_pauli_axis`: Function to determine Pauli axis for a measurement basis.
- `default_meas_basis`: Function to return the default measurement basis.
- `meas_basis`: Function to get the measurement basis vector.
"""
from __future__ import annotations
import abc
import cmath
import enum
import math
from abc import ABC
from enum import Enum
from typing import TYPE_CHECKING
import numpy as np
import typing_extensions
if TYPE_CHECKING:
from numpy.typing import NDArray
class Plane(Enum):
"""Measurement planes for MBQC.
We distinguish the axial measurements from the planar measurements.
"""
XY = enum.auto()
YZ = enum.auto()
XZ = enum.auto()
class Axis(Enum):
"""Measurement axis for Pauli measurement."""
X = enum.auto()
Y = enum.auto()
Z = enum.auto()
class Sign(Enum):
"""Measurement sign for Pauli measurement."""
PLUS = enum.auto()
MINUS = enum.auto()
[docs]
class MeasBasis(ABC):
"""Abstract class to represent a measurement basis."""
@property
@abc.abstractmethod
def plane(self) -> Plane:
"""Return the measurement plane."""
raise NotImplementedError
@property
@abc.abstractmethod
def angle(self) -> float:
"""Return the measurement angle."""
raise NotImplementedError
[docs]
@abc.abstractmethod
def flip(self) -> MeasBasis:
"""Flip the measurement basis."""
raise NotImplementedError
[docs]
@abc.abstractmethod
def conjugate(self) -> MeasBasis:
"""Return the angle-conjugated measurement basis."""
raise NotImplementedError
[docs]
@abc.abstractmethod
def vector(self) -> NDArray[np.complex128]:
"""Return the measurement basis vector."""
raise NotImplementedError
[docs]
class PlannerMeasBasis(MeasBasis):
"""Class to represent a planner measurement basis."""
[docs]
def __init__(self, plane: Plane, angle: float) -> None:
self.__plane = plane
self.__angle = angle
@property
@typing_extensions.override
def plane(self) -> Plane:
"""Return the measurement plane.
Returns
-------
`Plane`
measurement plane
"""
return self.__plane
@property
@typing_extensions.override
def angle(self) -> float:
"""Return the measurement angle.
Returns
-------
`float`
measurement angle
"""
return self.__angle
[docs]
@typing_extensions.override
def flip(self) -> PlannerMeasBasis:
"""Flip the measurement basis.
Returns
-------
`PlannerMeasBasis`
flipped PlannerMeasBasis
"""
return PlannerMeasBasis(self.plane, self.angle + math.pi)
[docs]
@typing_extensions.override
def conjugate(self) -> PlannerMeasBasis:
"""Return the angle-conjugated PlannerMeasBasis object.
This operation represents measurement-angle sign inversion. It is not
defined as complex conjugation of the basis vector.
Returns
-------
`PlannerMeasBasis`
angle-conjugated PlannerMeasBasis
Raises
------
TypeError
if the plane is not one of XY, YZ, XZ
"""
if not isinstance(self.plane, Plane):
msg = "The plane must be one of XY, YZ, XZ"
raise TypeError(msg)
return PlannerMeasBasis(self.plane, -self.angle)
[docs]
@typing_extensions.override
def vector(self) -> NDArray[np.complex128]:
r"""Return the measurement basis vector.
Returns
-------
`numpy.typing.NDArray`\[`numpy.complex128`\]
measurement basis vector
"""
return meas_basis(self.plane, self.angle)
[docs]
class AxisMeasBasis(MeasBasis):
"""Class to represent an axis measurement basis.
The aim is to pocess the accurate information of the axis measurement.
Attributes
----------
axis : `Axis`
measurement axis
sign : `Sign`
measurement sign
"""
[docs]
def __init__(self, axis: Axis, sign: Sign) -> None:
self.axis = axis
self.sign = sign
@property
@typing_extensions.override
def plane(self) -> Plane:
"""Return the measurement plane.
Returns
-------
`Plane`
measurement plane
Raises
------
TypeError
if the axis is not one of X, Y, Z
"""
if not isinstance(self.axis, Axis):
msg = "The axis must be one of X, Y, Z"
raise TypeError(msg)
if self.axis == Axis.X:
plane = Plane.XY
elif self.axis == Axis.Y:
plane = Plane.YZ
elif self.axis == Axis.Z:
plane = Plane.XZ
else:
typing_extensions.assert_never(self.axis)
return plane
@property
@typing_extensions.override
def angle(self) -> float:
"""Return the measurement angle.
Returns
-------
`float`
measurement angle
Raises
------
TypeError
if the axis is not one of X, Y,
"""
if not isinstance(self.axis, Axis):
msg = "The axis must be one of X, Y, Z"
raise TypeError(msg)
if self.axis == Axis.Y:
angle = math.pi / 2 if self.sign == Sign.PLUS else 3 * math.pi / 2
else:
angle = 0 if self.sign == Sign.PLUS else math.pi
return angle
[docs]
@typing_extensions.override
def flip(self) -> AxisMeasBasis:
"""Flip the measurement basis.
Returns
-------
`AxisMeasBasis`
flipped AxisMeasBasis
"""
return AxisMeasBasis(self.axis, Sign.MINUS if self.sign == Sign.PLUS else Sign.PLUS)
[docs]
@typing_extensions.override
def conjugate(self) -> AxisMeasBasis:
"""Return the conjugate of the AxisMeasBasis object.
Returns
-------
`AxisMeasBasis`
conjugate AxisMeasBasis
"""
if self.axis == Axis.Y:
return AxisMeasBasis(Axis.Y, Sign.MINUS if self.sign == Sign.PLUS else Sign.PLUS)
return AxisMeasBasis(self.axis, self.sign)
[docs]
@typing_extensions.override
def vector(self) -> NDArray[np.complex128]:
r"""Return the measurement basis vector.
Returns
-------
`numpy.typing.NDArray`\[`numpy.complex128`\]
measurement basis vector
"""
return meas_basis(self.plane, self.angle)
[docs]
def is_close_angle(angle: float, target: float, atol: float = 1e-9) -> bool:
"""Check if an angle is close to a target angle.
Parameters
----------
angle : `float`
angle to check
target : `float`
target angle
atol : `float`, optional
absolute tolerance, by default 1e-9
Returns
-------
`bool`
`True` if the angle is close to the target angle
"""
diff_angle = (angle - target) % (2 * math.pi)
if diff_angle > math.pi:
diff_angle = 2 * math.pi - diff_angle
return bool(np.isclose(diff_angle, 0, atol=atol))
[docs]
def is_clifford_angle(angle: float, atol: float = 1e-9) -> bool:
"""Check if an angle is a Clifford angle.
Parameters
----------
angle : `float`
angle to check
atol : `float`, optional
absolute tolerance, by default 1e-9
Returns
-------
`bool`
`True` if the angle is a Clifford angle
"""
angle_preprocessed = angle % (2 * math.pi)
return any(
is_close_angle(angle_preprocessed, target, atol=atol) for target in (0.0, math.pi / 2, math.pi, 3 * math.pi / 2)
)
[docs]
def determine_pauli_axis(meas_bases: MeasBasis) -> Axis | None:
"""Determine Pauli axis for a measurement basis if it's a Pauli measurement.
Parameters
----------
meas_bases : `MeasBasis`
Measurement basis to check
Returns
-------
`Axis` | None
Pauli axis if this is a Pauli measurement, None otherwise
"""
angle = meas_bases.angle
if not is_clifford_angle(angle):
return None
if is_close_angle(angle, 0) or is_close_angle(angle, math.pi):
# X measurement (XY plane, θ=0 or π) or Z measurement (XZ plane, θ=0 or π)
if meas_bases.plane == Plane.XY:
return Axis.X
if meas_bases.plane in {Plane.XZ, Plane.YZ}:
return Axis.Z
elif is_close_angle(angle, math.pi / 2) or is_close_angle(angle, 3 * math.pi / 2):
# Y measurement can occur in XY plane (θ=π/2 or 3π/2) or YZ plane (θ=π/2 or 3π/2)
if meas_bases.plane in {Plane.XY, Plane.YZ}:
return Axis.Y
if meas_bases.plane == Plane.XZ:
return Axis.X
return None
[docs]
def default_meas_basis() -> PlannerMeasBasis:
"""Return the default measurement basis.
The default measurement basis is the XY plane with angle 0.
Returns
-------
`PlannerMeasBasis`
default measurement basis
"""
return PlannerMeasBasis(Plane.XY, 0.0)
[docs]
def meas_basis(plane: Plane, angle: float) -> NDArray[np.complex128]:
r"""Return the measurement basis vector corresponding to the plane and angle.
Parameters
----------
plane : `Plane`
measurement plane
angle : `float`
measurement angle
Returns
-------
`numpy.typing.NDArray`\[`numpy.complex128`\]
measurement basis vector
Raises
------
TypeError
if the plane is not one of XY, YZ, XZ
"""
if not isinstance(plane, Plane):
msg = "The plane must be one of XY, YZ, XZ"
raise TypeError(msg)
if plane == Plane.XY:
basis = np.asarray([1, cmath.exp(1j * angle)]) / math.sqrt(2)
elif plane == Plane.YZ:
basis = np.asarray([math.cos(angle / 2), 1j * math.sin(angle / 2)])
elif plane == Plane.XZ:
basis = np.asarray([math.cos(angle / 2), math.sin(angle / 2)])
else:
typing_extensions.assert_never(plane)
return basis.astype(np.complex128)