"""
Pauli gates ± {1,j} × {I, X, Y, Z}
"""
from __future__ import annotations
import enum
import numpy as np
import pydantic
import graphix.clifford
class IXYZ(enum.Enum):
I = -1
X = 0
Y = 1
Z = 2
class ComplexUnit:
"""
Complex unit: 1, -1, j, -j.
Complex units can be multiplied with other complex units,
with Python constants 1, -1, 1j, -1j, and can be negated.
"""
def __init__(self, sign: bool, im: bool):
self.__sign = sign
self.__im = im
@property
def sign(self):
return self.__sign
@property
def im(self):
return self.__im
@property
def complex(self) -> complex:
"""
Return the unit as complex number
"""
result: complex = 1
if self.__sign:
result *= -1
if self.__im:
result *= 1j
return result
def __repr__(self):
if self.__im:
result = "1j"
else:
result = "1"
if self.__sign:
result = "-" + result
return result
def prefix(self, s: str) -> str:
"""
Prefix the given string by the complex unit as coefficient,
1 leaving the string unchanged.
"""
if self.__im:
result = "1j*" + s
else:
result = s
if self.__sign:
result = "-" + result
return result
def __mul__(self, other):
if isinstance(other, ComplexUnit):
im = self.__im != other.__im
sign = (self.__sign != other.__sign) != (self.__im and other.__im)
return COMPLEX_UNITS[sign][im]
return NotImplemented
def __rmul__(self, other):
if other == 1:
return self
elif other == -1:
return COMPLEX_UNITS[not self.__sign][self.__im]
elif other == 1j:
return COMPLEX_UNITS[self.__sign != self.__im][not self.__im]
elif other == -1j:
return COMPLEX_UNITS[self.__sign == self.__im][not self.__im]
def __neg__(self):
return COMPLEX_UNITS[not self.__sign][self.__im]
COMPLEX_UNITS = [[ComplexUnit(sign, im) for im in (False, True)] for sign in (False, True)]
UNIT = COMPLEX_UNITS[False][False]
UNITS = [UNIT, -UNIT, 1j * UNIT, -1j * UNIT]
class Axis(enum.Enum):
X = 0
Y = 1
Z = 2
[docs]
class Plane(enum.Enum):
XY = 0
YZ = 1
XZ = 2
@property
def axes(self) -> list[Axis]:
# match self:
# case Plane.XY:
# return [Axis.X, Axis.Y]
# case Plane.YZ:
# return [Axis.Y, Axis.Z]
# case Plane.XZ:
# return [Axis.X, Axis.Z]
if self == Plane.XY:
return [Axis.X, Axis.Y]
elif self == Plane.YZ:
return [Axis.Y, Axis.Z]
elif self == Plane.XZ:
return [Axis.X, Axis.Z]
@property
def cos(self) -> Axis:
# match self:
# case Plane.XY:
# return Axis.X
# case Plane.YZ:
# return Axis.Z # former convention was Y
# case Plane.XZ:
# return Axis.Z # former convention was X
if self == Plane.XY:
return Axis.X
elif self == Plane.YZ:
return Axis.Z # former convention was Y
elif self == Plane.XZ:
return Axis.Z # former convention was X
@property
def sin(self) -> Axis:
# match self:
# case Plane.XY:
# return Axis.Y
# case Plane.YZ:
# return Axis.Y # former convention was Z
# case Plane.XZ:
# return Axis.X # former convention was Z
if self == Plane.XY:
return Axis.Y
elif self == Plane.YZ:
return Axis.Y # former convention was Z
elif self == Plane.XZ:
return Axis.X # former convention was Z
def polar(self, angle: float) -> tuple[float, float, float]:
result = [0, 0, 0]
result[self.cos.value] = np.cos(angle)
result[self.sin.value] = np.sin(angle)
return tuple(result)
@staticmethod
def from_axes(a: Axis, b: Axis) -> Plane:
if b.value < a.value:
a, b = b, a
# match a, b:
# case Axis.X, Axis.Y:
# return Plane.XY
# case Axis.Y, Axis.Z:
# return Plane.YZ
# case Axis.X, Axis.Z:
# return Plane.XZ
if a == Axis.X and b == Axis.Y:
return Plane.XY
elif a == Axis.Y and b == Axis.Z:
return Plane.YZ
elif a == Axis.X and b == Axis.Z:
return Plane.XZ
assert a == b
raise ValueError(f"Cannot make a plane giving the same axis {a} twice.")
[docs]
class Pauli:
"""
Pauli gate: u * {I, X, Y, Z} where u is a complex unit
Pauli gates can be multiplied with other Pauli gates (with @),
with complex units and unit constants (with *),
and can be negated.
"""
[docs]
def __init__(self, symbol: IXYZ, unit: ComplexUnit):
self.__symbol = symbol
self.__unit = unit
@staticmethod
def from_axis(axis: Axis) -> Pauli:
return Pauli(IXYZ[axis.name], UNIT)
@property
def axis(self) -> Axis:
if self.__symbol == IXYZ.I:
raise ValueError("I is not an axis.")
return Axis[self.__symbol.name]
@property
def symbol(self):
return self.__symbol
@property
def unit(self):
return self.__unit
@property
def matrix(self) -> np.ndarray:
"""
Return the matrix of the Pauli gate.
"""
return self.__unit.complex * graphix.clifford.CLIFFORD[self.__symbol.value + 1]
def __repr__(self):
return self.__unit.prefix(self.__symbol.name)
def __matmul__(self, other):
if isinstance(other, Pauli):
if self.__symbol == IXYZ.I:
symbol = other.__symbol
unit = 1
elif other.__symbol == IXYZ.I:
symbol = self.__symbol
unit = 1
elif self.__symbol == other.__symbol:
symbol = IXYZ.I
unit = 1
elif (self.__symbol.value + 1) % 3 == other.__symbol.value:
symbol = IXYZ((self.__symbol.value + 2) % 3)
unit = 1j
else:
symbol = IXYZ((self.__symbol.value + 1) % 3)
unit = -1j
return get(symbol, unit * self.__unit * other.__unit)
return NotImplemented
def __rmul__(self, other):
return get(self.__symbol, other * self.__unit)
def __neg__(self):
return get(self.__symbol, -self.__unit)
TABLE = [
[[Pauli(symbol, COMPLEX_UNITS[sign][im]) for im in (False, True)] for sign in (False, True)]
for symbol in (IXYZ.I, IXYZ.X, IXYZ.Y, IXYZ.Z)
]
LIST = [pauli for sign_im_list in TABLE for im_list in sign_im_list for pauli in im_list]
def get(symbol: IXYZ, unit: ComplexUnit) -> Pauli:
"""Return the Pauli gate with given symbol and unit."""
return TABLE[symbol.value + 1][unit.sign][unit.im]
I = get(IXYZ.I, UNIT)
X = get(IXYZ.X, UNIT)
Y = get(IXYZ.Y, UNIT)
Z = get(IXYZ.Z, UNIT)
def parse(name: str) -> Pauli:
"""
Return the Pauli gate with the given name (limited to "I", "X", "Y" and "Z").
"""
return get(IXYZ[name], UNIT)
[docs]
class MeasureUpdate(pydantic.BaseModel):
new_plane: Plane
coeff: int
add_term: float
@staticmethod
def compute(plane: Plane, s: bool, t: bool, clifford: graphix.clifford.Clifford) -> MeasureUpdate:
gates = list(map(Pauli.from_axis, plane.axes))
if s:
clifford = graphix.clifford.X @ clifford
if t:
clifford = graphix.clifford.Z @ clifford
gates = list(map(clifford.measure, gates))
new_plane = Plane.from_axes(*(gate.axis for gate in gates))
cos_pauli = clifford.measure(Pauli.from_axis(plane.cos))
sin_pauli = clifford.measure(Pauli.from_axis(plane.sin))
exchange = cos_pauli.axis != new_plane.cos
if exchange == (cos_pauli.unit.sign == sin_pauli.unit.sign):
coeff = -1
else:
coeff = 1
add_term = 0
if cos_pauli.unit.sign:
add_term += np.pi
if exchange:
add_term = np.pi / 2 - add_term
return MeasureUpdate(new_plane=new_plane, coeff=coeff, add_term=add_term)