From 77f7fb11fe346f9e14f3c68a242815ff6a951d57 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:02:26 +0900 Subject: [PATCH 01/61] :technologist: Annotate --- graphix/_db.py | 22 +++++++++++----------- graphix/states.py | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/graphix/_db.py b/graphix/_db.py index 648f2325..cebdbfe9 100644 --- a/graphix/_db.py +++ b/graphix/_db.py @@ -242,14 +242,14 @@ class _CliffordMeasure(NamedTuple): class WellKnownMatrix: """Collection of well-known matrices.""" - I: ClassVar = _C0 - X: ClassVar = _C1 - Y: ClassVar = _C2 - Z: ClassVar = _C3 - S: ClassVar = _C4 - SDG: ClassVar = _C5 - H: ClassVar = _C6 - CZ: ClassVar = _lock( + I: ClassVar[npt.NDArray[np.complex128]] = _C0 + X: ClassVar[npt.NDArray[np.complex128]] = _C1 + Y: ClassVar[npt.NDArray[np.complex128]] = _C2 + Z: ClassVar[npt.NDArray[np.complex128]] = _C3 + S: ClassVar[npt.NDArray[np.complex128]] = _C4 + SDG: ClassVar[npt.NDArray[np.complex128]] = _C5 + H: ClassVar[npt.NDArray[np.complex128]] = _C6 + CZ: ClassVar[npt.NDArray[np.complex128]] = _lock( np.asarray( [ [1, 0, 0, 0], @@ -259,7 +259,7 @@ class WellKnownMatrix: ], ) ) - CNOT: ClassVar = _lock( + CNOT: ClassVar[npt.NDArray[np.complex128]] = _lock( np.asarray( [ [1, 0, 0, 0], @@ -269,7 +269,7 @@ class WellKnownMatrix: ], ) ) - SWAP: ClassVar = _lock( + SWAP: ClassVar[npt.NDArray[np.complex128]] = _lock( np.asarray( [ [1, 0, 0, 0], @@ -279,7 +279,7 @@ class WellKnownMatrix: ], ) ) - CCX: ClassVar = _lock( + CCX: ClassVar[npt.NDArray[np.complex128]] = _lock( np.asarray( [ [1, 0, 0, 0, 0, 0, 0, 0], diff --git a/graphix/states.py b/graphix/states.py index 7d2480cc..d242e20c 100644 --- a/graphix/states.py +++ b/graphix/states.py @@ -80,12 +80,12 @@ def get_statevector(self) -> npt.NDArray[np.complex128]: class BasicStates: """Basic states.""" - ZERO: ClassVar = PlanarState(Plane.XZ, 0) - ONE: ClassVar = PlanarState(Plane.XZ, np.pi) - PLUS: ClassVar = PlanarState(Plane.XY, 0) - MINUS: ClassVar = PlanarState(Plane.XY, np.pi) - PLUS_I: ClassVar = PlanarState(Plane.XY, np.pi / 2) - MINUS_I: ClassVar = PlanarState(Plane.XY, -np.pi / 2) + ZERO: ClassVar[PlanarState] = PlanarState(Plane.XZ, 0) + ONE: ClassVar[PlanarState] = PlanarState(Plane.XZ, np.pi) + PLUS: ClassVar[PlanarState] = PlanarState(Plane.XY, 0) + MINUS: ClassVar[PlanarState] = PlanarState(Plane.XY, np.pi) + PLUS_I: ClassVar[PlanarState] = PlanarState(Plane.XY, np.pi / 2) + MINUS_I: ClassVar[PlanarState] = PlanarState(Plane.XY, -np.pi / 2) # remove that in the end # need in TN backend - VEC: ClassVar = [PLUS, MINUS, ZERO, ONE, PLUS_I, MINUS_I] + VEC: ClassVar[list[PlanarState]] = [PLUS, MINUS, ZERO, ONE, PLUS_I, MINUS_I] From 08c089221a03a602562648acf3fc46ed970386fe Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:02:49 +0900 Subject: [PATCH 02/61] :truck: Define is_integer --- graphix/gflow.py | 14 +++++++------- graphix/type_utils.py | 7 ++++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/graphix/gflow.py b/graphix/gflow.py index 56baf1a4..81ba4794 100644 --- a/graphix/gflow.py +++ b/graphix/gflow.py @@ -20,7 +20,7 @@ import numpy as np import sympy as sp -from graphix import pauli +from graphix import pauli, type_utils from graphix.command import CommandKind from graphix.linalg import MatGF2 from graphix.pauli import Plane @@ -1372,18 +1372,18 @@ def get_pauli_nodes( l_x, l_y, l_z = set(), set(), set() for node, plane in meas_planes.items(): if plane == Plane.XY: - if pauli.is_int(meas_angles[node]): # measurement angle is integer + if type_utils.is_integer(meas_angles[node]): # measurement angle is integer l_x |= {node} - elif pauli.is_int(2 * meas_angles[node]): # measurement angle is half integer + elif type_utils.is_integer(2 * meas_angles[node]): # measurement angle is half integer l_y |= {node} elif plane == Plane.XZ: - if pauli.is_int(meas_angles[node]): + if type_utils.is_integer(meas_angles[node]): l_z |= {node} - elif pauli.is_int(2 * meas_angles[node]): + elif type_utils.is_integer(2 * meas_angles[node]): l_x |= {node} elif plane == Plane.YZ: - if pauli.is_int(meas_angles[node]): + if type_utils.is_integer(meas_angles[node]): l_y |= {node} - elif pauli.is_int(2 * meas_angles[node]): + elif type_utils.is_integer(2 * meas_angles[node]): l_z |= {node} return l_x, l_y, l_z diff --git a/graphix/type_utils.py b/graphix/type_utils.py index e1fbc0cc..aae1cf3d 100644 --- a/graphix/type_utils.py +++ b/graphix/type_utils.py @@ -4,7 +4,7 @@ import sys import typing -from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, Literal, SupportsInt, TypeVar if TYPE_CHECKING: from collections.abc import Iterable @@ -41,3 +41,8 @@ def check_kind(cls: type, scope: dict[str, Any]) -> None: if typing.get_origin(ann) is not Literal: msg = "Tag attribute must be a literal." raise TypeError(msg) + + +def is_integer(value: SupportsInt) -> bool: + """Return `True` if `value` is an integer, `False` otherwise.""" + return value == int(value) From 2d03e9ad222b4bd626e1cd9d41eeb1c8baa36ec2 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:06:14 +0900 Subject: [PATCH 03/61] :construction: Refactor pauli.py --- graphix/_db.py | 54 +++--- graphix/clifford.py | 13 +- graphix/command.py | 2 +- graphix/pattern.py | 2 +- graphix/pauli.py | 448 ++++++++++++++++++++++---------------------- 5 files changed, 270 insertions(+), 249 deletions(-) diff --git a/graphix/_db.py b/graphix/_db.py index cebdbfe9..c3079963 100644 --- a/graphix/_db.py +++ b/graphix/_db.py @@ -151,34 +151,40 @@ class _CliffordMeasure(NamedTuple): sign: Literal[-1, +1] +class _CliffordMeasureTuple(NamedTuple): + x: _CliffordMeasure + y: _CliffordMeasure + z: _CliffordMeasure + + # Conjugation of Pauli gates P with Clifford gate C, # i.e. C @ P @ C^dagger result in Pauli group, i.e. {\pm} \times {X, Y, Z}. # CLIFFORD_MEASURE contains the effect of Clifford conjugation of Pauli gates. CLIFFORD_MEASURE = ( - (_CliffordMeasure("X", +1), _CliffordMeasure("Y", +1), _CliffordMeasure("Z", +1)), - (_CliffordMeasure("X", +1), _CliffordMeasure("Y", -1), _CliffordMeasure("Z", -1)), - (_CliffordMeasure("X", -1), _CliffordMeasure("Y", +1), _CliffordMeasure("Z", -1)), - (_CliffordMeasure("X", -1), _CliffordMeasure("Y", -1), _CliffordMeasure("Z", +1)), - (_CliffordMeasure("Y", -1), _CliffordMeasure("X", +1), _CliffordMeasure("Z", +1)), - (_CliffordMeasure("Y", +1), _CliffordMeasure("X", -1), _CliffordMeasure("Z", +1)), - (_CliffordMeasure("Z", +1), _CliffordMeasure("Y", -1), _CliffordMeasure("X", +1)), - (_CliffordMeasure("X", +1), _CliffordMeasure("Z", -1), _CliffordMeasure("Y", +1)), - (_CliffordMeasure("Z", +1), _CliffordMeasure("Y", +1), _CliffordMeasure("X", -1)), - (_CliffordMeasure("Y", -1), _CliffordMeasure("X", -1), _CliffordMeasure("Z", -1)), - (_CliffordMeasure("Y", +1), _CliffordMeasure("X", +1), _CliffordMeasure("Z", -1)), - (_CliffordMeasure("Z", -1), _CliffordMeasure("Y", -1), _CliffordMeasure("X", -1)), - (_CliffordMeasure("Z", -1), _CliffordMeasure("Y", +1), _CliffordMeasure("X", +1)), - (_CliffordMeasure("X", -1), _CliffordMeasure("Z", -1), _CliffordMeasure("Y", -1)), - (_CliffordMeasure("X", -1), _CliffordMeasure("Z", +1), _CliffordMeasure("Y", +1)), - (_CliffordMeasure("X", +1), _CliffordMeasure("Z", +1), _CliffordMeasure("Y", -1)), - (_CliffordMeasure("Z", +1), _CliffordMeasure("X", +1), _CliffordMeasure("Y", +1)), - (_CliffordMeasure("Z", -1), _CliffordMeasure("X", +1), _CliffordMeasure("Y", -1)), - (_CliffordMeasure("Z", -1), _CliffordMeasure("X", -1), _CliffordMeasure("Y", +1)), - (_CliffordMeasure("Z", +1), _CliffordMeasure("X", -1), _CliffordMeasure("Y", -1)), - (_CliffordMeasure("Y", +1), _CliffordMeasure("Z", +1), _CliffordMeasure("X", +1)), - (_CliffordMeasure("Y", -1), _CliffordMeasure("Z", -1), _CliffordMeasure("X", +1)), - (_CliffordMeasure("Y", +1), _CliffordMeasure("Z", -1), _CliffordMeasure("X", -1)), - (_CliffordMeasure("Y", -1), _CliffordMeasure("Z", +1), _CliffordMeasure("X", -1)), + _CliffordMeasureTuple(_CliffordMeasure("X", +1), _CliffordMeasure("Y", +1), _CliffordMeasure("Z", +1)), + _CliffordMeasureTuple(_CliffordMeasure("X", +1), _CliffordMeasure("Y", -1), _CliffordMeasure("Z", -1)), + _CliffordMeasureTuple(_CliffordMeasure("X", -1), _CliffordMeasure("Y", +1), _CliffordMeasure("Z", -1)), + _CliffordMeasureTuple(_CliffordMeasure("X", -1), _CliffordMeasure("Y", -1), _CliffordMeasure("Z", +1)), + _CliffordMeasureTuple(_CliffordMeasure("Y", -1), _CliffordMeasure("X", +1), _CliffordMeasure("Z", +1)), + _CliffordMeasureTuple(_CliffordMeasure("Y", +1), _CliffordMeasure("X", -1), _CliffordMeasure("Z", +1)), + _CliffordMeasureTuple(_CliffordMeasure("Z", +1), _CliffordMeasure("Y", -1), _CliffordMeasure("X", +1)), + _CliffordMeasureTuple(_CliffordMeasure("X", +1), _CliffordMeasure("Z", -1), _CliffordMeasure("Y", +1)), + _CliffordMeasureTuple(_CliffordMeasure("Z", +1), _CliffordMeasure("Y", +1), _CliffordMeasure("X", -1)), + _CliffordMeasureTuple(_CliffordMeasure("Y", -1), _CliffordMeasure("X", -1), _CliffordMeasure("Z", -1)), + _CliffordMeasureTuple(_CliffordMeasure("Y", +1), _CliffordMeasure("X", +1), _CliffordMeasure("Z", -1)), + _CliffordMeasureTuple(_CliffordMeasure("Z", -1), _CliffordMeasure("Y", -1), _CliffordMeasure("X", -1)), + _CliffordMeasureTuple(_CliffordMeasure("Z", -1), _CliffordMeasure("Y", +1), _CliffordMeasure("X", +1)), + _CliffordMeasureTuple(_CliffordMeasure("X", -1), _CliffordMeasure("Z", -1), _CliffordMeasure("Y", -1)), + _CliffordMeasureTuple(_CliffordMeasure("X", -1), _CliffordMeasure("Z", +1), _CliffordMeasure("Y", +1)), + _CliffordMeasureTuple(_CliffordMeasure("X", +1), _CliffordMeasure("Z", +1), _CliffordMeasure("Y", -1)), + _CliffordMeasureTuple(_CliffordMeasure("Z", +1), _CliffordMeasure("X", +1), _CliffordMeasure("Y", +1)), + _CliffordMeasureTuple(_CliffordMeasure("Z", -1), _CliffordMeasure("X", +1), _CliffordMeasure("Y", -1)), + _CliffordMeasureTuple(_CliffordMeasure("Z", -1), _CliffordMeasure("X", -1), _CliffordMeasure("Y", +1)), + _CliffordMeasureTuple(_CliffordMeasure("Z", +1), _CliffordMeasure("X", -1), _CliffordMeasure("Y", -1)), + _CliffordMeasureTuple(_CliffordMeasure("Y", +1), _CliffordMeasure("Z", +1), _CliffordMeasure("X", +1)), + _CliffordMeasureTuple(_CliffordMeasure("Y", -1), _CliffordMeasure("Z", -1), _CliffordMeasure("X", +1)), + _CliffordMeasureTuple(_CliffordMeasure("Y", +1), _CliffordMeasure("Z", -1), _CliffordMeasure("X", -1)), + _CliffordMeasureTuple(_CliffordMeasure("Y", -1), _CliffordMeasure("Z", +1), _CliffordMeasure("X", -1)), ) # Decomposition of Clifford gates with H, S and Z. diff --git a/graphix/clifford.py b/graphix/clifford.py index 5cc01932..55a4f891 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -7,6 +7,8 @@ from enum import Enum from typing import TYPE_CHECKING +import typing_extensions + from graphix._db import ( CLIFFORD, CLIFFORD_CONJ, @@ -111,8 +113,15 @@ def measure(self, pauli: Pauli) -> Pauli: if pauli.symbol == IXYZ.I: return copy.deepcopy(pauli) table = CLIFFORD_MEASURE[self.value] - symbol, sign = table[pauli.symbol.value] - return pauli.unit * Pauli(IXYZ[symbol], ComplexUnit(Sign(sign), False)) + if pauli.symbol == IXYZ.X: + symbol, sign = table.x + elif pauli.symbol == IXYZ.Y: + symbol, sign = table.y + elif pauli.symbol == IXYZ.Z: + symbol, sign = table.z + else: + typing_extensions.assert_never(pauli.symbol) + return pauli.unit * Pauli(IXYZ[symbol], ComplexUnit.from_properties(sign=Sign(sign))) def commute_domains(self, domains: Domains) -> Domains: """ diff --git a/graphix/command.py b/graphix/command.py index 6bd30f5b..02567ef1 100644 --- a/graphix/command.py +++ b/graphix/command.py @@ -164,7 +164,7 @@ def compute(plane: Plane, s: bool, t: bool, clifford_gate: Clifford) -> MeasureU else: coeff = 1 add_term: float = 0 - if cos_pauli.unit.sign == Sign.Minus: + if cos_pauli.unit.sign == Sign.MINUS: add_term += np.pi if exchange: add_term = np.pi / 2 - add_term diff --git a/graphix/pattern.py b/graphix/pattern.py index 45cced75..45957ae9 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -2130,7 +2130,7 @@ def measure_pauli(pattern, leave_input, copy=False, use_rustworkx=False): measure = graph_state.measure_z else: typing_extensions.assert_never(basis.axis) - if basis.sign == Sign.Plus: + if basis.sign == Sign.PLUS: results[pattern_cmd.node] = measure(pattern_cmd.node, choice=0) else: results[pattern_cmd.node] = 1 - measure(pattern_cmd.node, choice=1) diff --git a/graphix/pauli.py b/graphix/pauli.py index 8e179c84..35075499 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -2,41 +2,46 @@ from __future__ import annotations +import dataclasses import enum -import sys +import functools +import math import typing -from numbers import Number -from typing import TYPE_CHECKING +from enum import Enum +from typing import TYPE_CHECKING, ClassVar, NamedTuple -import numpy as np -import numpy.typing as npt import typing_extensions -from graphix._db import CLIFFORD -from graphix.ops import Ops +from graphix import type_utils +from graphix._db import WellKnownMatrix if TYPE_CHECKING: + from collections.abc import Iterator + + import numpy as np + import numpy.typing as npt + from graphix.states import PlanarState -class IXYZ(enum.Enum): +class IXYZ(Enum): """I, X, Y or Z.""" - I = -1 - X = 0 - Y = 1 - Z = 2 + I = enum.auto() + X = enum.auto() + Y = enum.auto() + Z = enum.auto() -class Sign(enum.Enum): +class Sign(Enum): """Sign, plus or minus.""" - Plus = 1 - Minus = -1 + PLUS = 1 + MINUS = -1 def __str__(self) -> str: """Return `+` or `-`.""" - if self == Sign.Plus: + if self == Sign.PLUS: return "+" return "-" @@ -44,37 +49,63 @@ def __str__(self) -> str: def plus_if(b: bool) -> Sign: """Return `+` if `b` is `True`, `-` otherwise.""" if b: - return Sign.Plus - return Sign.Minus + return Sign.PLUS + return Sign.MINUS @staticmethod def minus_if(b: bool) -> Sign: """Return `-` if `b` is `True`, `+` otherwise.""" if b: - return Sign.Minus - return Sign.Plus + return Sign.MINUS + return Sign.PLUS def __neg__(self) -> Sign: """Swap the sign.""" - return Sign.minus_if(self == Sign.Plus) + return Sign.minus_if(self == Sign.PLUS) + + @typing.overload + def __mul__(self, other: Sign) -> Sign: ... + + @typing.overload + def __mul__(self, other: int) -> int: ... - def __mul__(self, other: SignOrNumber) -> SignOrNumber: + @typing.overload + def __mul__(self, other: float) -> float: ... + + @typing.overload + def __mul__(self, other: complex) -> complex: ... + + def __mul__(self, other: Sign | complex) -> Sign | int | float | complex: """Multiply the sign with another sign or a number.""" if isinstance(other, Sign): return Sign.plus_if(self == other) - if isinstance(other, Number): - return self.value * other + if isinstance(other, int): + return int(self) * other + if isinstance(other, float): + return float(self) * other + if isinstance(other, complex): + return complex(self) * other return NotImplemented - def __rmul__(self, other) -> Number: + @typing.overload + def __rmul__(self, other: int) -> int: ... + + @typing.overload + def __rmul__(self, other: float) -> float: ... + + @typing.overload + def __rmul__(self, other: complex) -> complex: ... + + def __rmul__(self, other: complex) -> int | float | complex: """Multiply the sign with a number.""" - if isinstance(other, Number): - return self.value * other + if isinstance(other, int | float | complex): + return self.__mul__(other) return NotImplemented def __int__(self) -> int: """Return `1` for `+` and `-1` for `-`.""" - return self.value + # mypy does not infer the return type correctly + return self.value # type: ignore[no-any-return] def __float__(self) -> float: """Return `1.0` for `+` and `-1.0` for `-`.""" @@ -85,13 +116,7 @@ def __complex__(self) -> complex: return complex(self.value) -if sys.version_info >= (3, 10): - SignOrNumber = typing.TypeVar("SignOrNumber", bound=Sign | Number) -else: - SignOrNumber = typing.TypeVar("SignOrNumber", bound=typing.Union[Sign, Number]) - - -class ComplexUnit: +class ComplexUnit(Enum): """ Complex unit: 1, -1, j, -j. @@ -99,125 +124,110 @@ class ComplexUnit: with Python constants 1, -1, 1j, -1j, and can be negated. """ - def __init__(self, sign: Sign, is_imag: bool): - self.__sign = sign - self.__is_imag = is_imag + # HACK: Related to arg(z) + PLUS = 0 + PLUS_J = 1 + MINUS = 2 + MINUS_J = 3 + + @staticmethod + def try_from_complex(value: complex) -> ComplexUnit | None: + """Return the ComplexUnit instance if the value is compatible, None otherwise.""" + if value == 1: + return ComplexUnit.PLUS + if value == -1: + return ComplexUnit.MINUS + if value == 1j: + return ComplexUnit.PLUS_J + if value == -1j: + return ComplexUnit.MINUS_J + return None + + @staticmethod + def from_properties(*, sign: Sign = Sign.PLUS, is_imag: bool = False) -> ComplexUnit: + """Construct ComplexUnit from its properties.""" + osign = 0 if sign == Sign.PLUS else 2 + oimag = 1 if is_imag else 0 + return ComplexUnit(osign + oimag) @property def sign(self) -> Sign: """Return the sign.""" - return self.__sign + return Sign.plus_if(self.value < 2) @property def is_imag(self) -> bool: """Return `True` if `j` or `-j`.""" - return self.__is_imag + return bool(self.value % 2) def __complex__(self) -> complex: """Return the unit as complex number.""" - result: complex = complex(self.__sign) - if self.__is_imag: - result *= 1j - return result + ret: complex = 1j**self.value + return ret def __repr__(self) -> str: """Return a string representation of the unit.""" - if self.__is_imag: - result = "1j" - else: - result = "1" - if self.__sign == Sign.Minus: + result = "1j" if self.is_imag else "1" + if self.sign == Sign.MINUS: 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.__is_imag: - result = "1j*" + s - else: - result = s - if self.__sign == Sign.Minus: - result = "-" + result - return result - - def __mul__(self, other: ComplexUnit) -> ComplexUnit: + def __mul__(self, other: ComplexUnit | complex) -> ComplexUnit: """Multiply the complex unit with another complex unit.""" if isinstance(other, ComplexUnit): - is_imag = self.__is_imag != other.__is_imag - sign = self.__sign * other.__sign * Sign.minus_if(self.__is_imag and other.__is_imag) - return COMPLEX_UNITS[sign == Sign.Minus][is_imag] + return ComplexUnit((self.value + other.value) % 4) + if other_ := ComplexUnit.try_from_complex(other): + return self.__mul__(other_) return NotImplemented - def __rmul__(self, other): + def __rmul__(self, other: complex) -> ComplexUnit: """Multiply the complex unit with a number.""" - if other == 1: - return self - elif other == -1: - return COMPLEX_UNITS[self.__sign == Sign.Plus][self.__is_imag] - elif other == 1j: - return COMPLEX_UNITS[self.__sign == Sign.plus_if(self.__is_imag)][not self.__is_imag] - elif other == -1j: - return COMPLEX_UNITS[self.__sign == Sign.minus_if(self.__is_imag)][not self.__is_imag] - - def __neg__(self): - """Return the opposite of the complex unit.""" - return COMPLEX_UNITS[self.__sign == Sign.Plus][self.__is_imag] - - -COMPLEX_UNITS = tuple( - tuple(ComplexUnit(sign, is_imag) for is_imag in (False, True)) for sign in (Sign.Plus, Sign.Minus) -) - - -UNIT = COMPLEX_UNITS[False][False] - + if isinstance(other, complex): + return self.__mul__(other) + return NotImplemented -UNITS = (UNIT, -UNIT, 1j * UNIT, -1j * UNIT) + def __neg__(self) -> ComplexUnit: + """Return the opposite of the complex unit.""" + return ComplexUnit((self.value + 2) % 4) -class Axis(enum.Enum): +class Axis(Enum): """Axis: `X`, `Y` or `Z`.""" - X = 0 - Y = 1 - Z = 2 + X = enum.auto() + Y = enum.auto() + Z = enum.auto() @property - def op(self) -> npt.NDArray: + def op(self) -> npt.NDArray[np.complex128]: """Return the single qubit operator associated to the axis.""" if self == Axis.X: - return Ops.X + return WellKnownMatrix.X if self == Axis.Y: - return Ops.Y + return WellKnownMatrix.Y if self == Axis.Z: - return Ops.Z - + return WellKnownMatrix.Z typing_extensions.assert_never(self) -class Plane(enum.Enum): +class Plane(Enum): + # TODO: Refactor using match """Plane: `XY`, `YZ` or `XZ`.""" - XY = 0 - YZ = 1 - XZ = 2 + XY = enum.auto() + YZ = enum.auto() + XZ = enum.auto() @property - def axes(self) -> list[Axis]: + def axes(self) -> tuple[Axis, Axis]: """Return the pair of axes that carry the plane.""" - # 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] + return (Axis.X, Axis.Y) + if self == Plane.YZ: + return (Axis.Y, Axis.Z) + if self == Plane.XZ: + return (Axis.X, Axis.Z) + typing_extensions.assert_never(self) @property def orth(self) -> Axis: @@ -233,66 +243,76 @@ def orth(self) -> Axis: @property def cos(self) -> Axis: """Return the axis of the plane that conventionally carries the cos.""" - # 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: + if self == Plane.YZ: return Axis.Z # former convention was Y - elif self == Plane.XZ: + if self == Plane.XZ: return Axis.Z # former convention was X + typing_extensions.assert_never(self) @property def sin(self) -> Axis: """Return the axis of the plane that conventionally carries the sin.""" - # 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: + if self == Plane.YZ: return Axis.Y # former convention was Z - elif self == Plane.XZ: + if self == Plane.XZ: return Axis.X # former convention was Z + typing_extensions.assert_never(self) def polar(self, angle: float) -> tuple[float, float, float]: """Return the Cartesian coordinates of the point of module 1 at the given angle, following the conventional orientation for cos and sin.""" - result = [0, 0, 0] - result[self.cos.value] = np.cos(angle) - result[self.sin.value] = np.sin(angle) - return tuple(result) + pp = (self.cos, self.sin) + if pp == (Axis.X, Axis.Y): + return (math.cos(angle), math.sin(angle), 0) + if pp == (Axis.Z, Axis.Y): + return (0, math.sin(angle), math.cos(angle)) + if pp == (Axis.Z, Axis.X): + return (math.sin(angle), 0, math.cos(angle)) + raise RuntimeError("Unreachable.") @staticmethod def from_axes(a: Axis, b: Axis) -> Plane: """Return the plane carried by the given axes.""" - 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: + ab = {a, b} + if ab == {Axis.X, Axis.Y}: return Plane.XY - elif a == Axis.Y and b == Axis.Z: + if ab == {Axis.Y, Axis.Z}: return Plane.YZ - elif a == Axis.X and b == Axis.Z: + if ab == {Axis.X, Axis.Z}: return Plane.XZ assert a == b raise ValueError(f"Cannot make a plane giving the same axis {a} twice.") +@functools.lru_cache(maxsize=None) +def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: + """Return the product of two Paulis.""" + if lhs == IXYZ.I: + return Pauli(rhs) + if rhs == IXYZ.I: + return Pauli(lhs) + if lhs == rhs: + return Pauli() + lr = (lhs, rhs) + if lr == (IXYZ.X, IXYZ.Y): + return Pauli(IXYZ.Z, ComplexUnit.PLUS_J) + if lr == (IXYZ.Y, IXYZ.X): + return Pauli(IXYZ.Z, ComplexUnit.MINUS_J) + if lr == (IXYZ.Y, IXYZ.Z): + return Pauli(IXYZ.X, ComplexUnit.PLUS_J) + if lr == (IXYZ.Z, IXYZ.Y): + return Pauli(IXYZ.X, ComplexUnit.MINUS_J) + if lr == (IXYZ.Z, IXYZ.X): + return Pauli(IXYZ.Y, ComplexUnit.PLUS_J) + if lr == (IXYZ.X, IXYZ.Z): + return Pauli(IXYZ.Y, ComplexUnit.MINUS_J) + raise RuntimeError("Unreachable.") + + +@dataclasses.dataclass(frozen=True) class Pauli: """Pauli gate: `u * {I, X, Y, Z}` where u is a complex unit. @@ -301,14 +321,17 @@ class Pauli: and can be negated. """ - def __init__(self, symbol: IXYZ, unit: ComplexUnit): - self.__symbol = symbol - self.__unit = unit + symbol: IXYZ = IXYZ.I + unit: ComplexUnit = ComplexUnit.PLUS + I: ClassVar[Pauli] + X: ClassVar[Pauli] + Y: ClassVar[Pauli] + Z: ClassVar[Pauli] @staticmethod def from_axis(axis: Axis) -> Pauli: """Return the Pauli associated to the given axis.""" - return Pauli(IXYZ[axis.name], UNIT) + return Pauli(IXYZ[axis.name]) @property def axis(self) -> Axis: @@ -316,29 +339,32 @@ def axis(self) -> Axis: Fails if the Pauli is identity. """ - if self.__symbol == IXYZ.I: + if self.symbol == IXYZ.I: raise ValueError("I is not an axis.") - return Axis[self.__symbol.name] - - @property - def symbol(self) -> IXYZ: - """Return the symbol (without the complex unit).""" - return self.__symbol + return Axis[self.symbol.name] @property - def unit(self) -> ComplexUnit: - """Return the complex unit.""" - return self.__unit - - @property - def matrix(self) -> npt.NDArray: + def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix of the Pauli gate.""" - return complex(self.__unit) * CLIFFORD[self.__symbol.value + 1] + co = complex(self.unit) + if self.symbol == IXYZ.I: + return co * WellKnownMatrix.I + if self.symbol == IXYZ.X: + return co * WellKnownMatrix.X + if self.symbol == IXYZ.Y: + return co * WellKnownMatrix.Y + if self.symbol == IXYZ.Z: + return co * WellKnownMatrix.Z + typing_extensions.assert_never(self.symbol) - def get_eigenstate(self, eigenvalue=0) -> PlanarState: + def get_eigenstate(self, eigenvalue: int | Sign = 0) -> PlanarState: """Return the eigenstate of the Pauli.""" from graphix.states import BasicStates + if isinstance(eigenvalue, Sign): + # Normalize the eigenvalue + eigenvalue = 0 if eigenvalue == Sign.PLUS else 1 + if self.symbol == IXYZ.X: return BasicStates.PLUS if eigenvalue == 0 else BasicStates.MINUS if self.symbol == IXYZ.Y: @@ -350,78 +376,61 @@ def get_eigenstate(self, eigenvalue=0) -> PlanarState: return BasicStates.PLUS typing_extensions.assert_never(self.symbol) - def __repr__(self) -> str: - """Return a fully qualified string representation of the Pauli.""" - return self.__unit.prefix(f"graphix.pauli.{self.__symbol.name}") - def __str__(self) -> str: """Return a string representation of the Pauli (without module prefix).""" - return self.__unit.prefix(self.__symbol.name) + if self.unit == ComplexUnit.PLUS: + return str(self.symbol) + if self.unit == ComplexUnit.MINUS: + return f"-{self.symbol}" + if self.unit == ComplexUnit.PLUS_J: + return f"1j * {self.symbol}" + if self.unit == ComplexUnit.MINUS_J: + return f"-1j * {self.symbol}" + typing_extensions.assert_never(self.unit) def __matmul__(self, other: Pauli) -> Pauli: """Return the product of two Paulis.""" 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 _matmul_impl(self.symbol, other.symbol) * (self.unit * other.unit) + return NotImplemented + + def __mul__(self, other: ComplexUnit) -> Pauli: + """Return the product of two Paulis.""" + if isinstance(other, ComplexUnit): + return dataclasses.replace(self, unit=self.unit * other) return NotImplemented def __rmul__(self, other: ComplexUnit) -> Pauli: """Return the product of two Paulis.""" if isinstance(other, ComplexUnit): - return get(self.__symbol, other * self.__unit) + return self.__mul__(other) return NotImplemented def __neg__(self) -> Pauli: """Return the opposite.""" - return get(self.__symbol, -self.__unit) - - -TABLE = tuple( - tuple(tuple(Pauli(symbol, COMPLEX_UNITS[sign][is_imag]) for is_imag in (False, True)) for sign in (False, True)) - for symbol in (IXYZ.I, IXYZ.X, IXYZ.Y, IXYZ.Z) -) - - -LIST = tuple(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 == Sign.Minus][unit.is_imag] - - -# TODO: Include in Pauli namespace -I = get(IXYZ.I, UNIT) -X = get(IXYZ.X, UNIT) -Y = get(IXYZ.Y, UNIT) -Z = get(IXYZ.Z, UNIT) + return dataclasses.replace(self, unit=-self.unit) + @staticmethod + def iterate(include_unit: bool = True) -> Iterator[Pauli]: + """Iterate over all Pauli gates. -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) + Parameters + ---------- + include_unit (bool, optional): Include the unit in the iteration. Defaults to True. + """ + us = iter(ComplexUnit) if include_unit else (ComplexUnit.PLUS,) + for unit in us: + for symbol in IXYZ: + yield Pauli(symbol, unit) -def is_int(value: Number) -> bool: - """Return `True` if `value` is an integer, `False` otherwise.""" - return value == int(value) +Pauli.I = Pauli(IXYZ.I) +Pauli.X = Pauli(IXYZ.X) +Pauli.Y = Pauli(IXYZ.Y) +Pauli.Z = Pauli(IXYZ.Z) -class PauliMeasurement(typing.NamedTuple): +class PauliMeasurement(NamedTuple): """Pauli measurement.""" axis: Axis @@ -431,12 +440,9 @@ class PauliMeasurement(typing.NamedTuple): def try_from(plane: Plane, angle: float) -> PauliMeasurement | None: """Return the Pauli measurement description if a given measure is Pauli.""" angle_double = 2 * angle - if not is_int(angle_double): + if not type_utils.is_integer(angle_double): return None angle_double_mod_4 = int(angle_double) % 4 - if angle_double_mod_4 % 2 == 0: - axis = plane.cos - else: - axis = plane.sin + axis = plane.cos if angle_double_mod_4 % 2 == 0 else plane.sin sign = Sign.minus_if(angle_double_mod_4 >= 2) return PauliMeasurement(axis, sign) From 0a142e0d4795eab0a52db6103b7f67cce67ee8e2 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:22:54 +0900 Subject: [PATCH 04/61] :white_check_mark: Update tests --- tests/test_clifford.py | 8 ++++---- tests/test_pauli.py | 15 ++++----------- tests/test_statevec_backend.py | 25 +++++++++++++++---------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/tests/test_clifford.py b/tests/test_clifford.py index de57d0ff..2a0589ae 100644 --- a/tests/test_clifford.py +++ b/tests/test_clifford.py @@ -54,10 +54,10 @@ def test_repr(self, c: Clifford) -> None: Pauli(sym, u) for sym in IXYZ for u in ( - ComplexUnit(Sign.Plus, False), - ComplexUnit(Sign.Minus, False), - ComplexUnit(Sign.Plus, True), - ComplexUnit(Sign.Minus, True), + ComplexUnit.from_properties(sign=Sign.PLUS, is_imag=False), + ComplexUnit.from_properties(sign=Sign.MINUS, is_imag=False), + ComplexUnit.from_properties(sign=Sign.PLUS, is_imag=True), + ComplexUnit.from_properties(sign=Sign.MINUS, is_imag=True), ) ), ), diff --git a/tests/test_pauli.py b/tests/test_pauli.py index 860b0583..65f2cb2a 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -5,29 +5,22 @@ import numpy as np import pytest -from graphix import pauli from graphix.clifford import Clifford from graphix.command import MeasureUpdate -from graphix.pauli import UNITS, ComplexUnit, Pauli, Plane +from graphix.pauli import ComplexUnit, Pauli, Plane class TestPauli: @pytest.mark.parametrize( ("u", "p"), - itertools.product( - UNITS, - pauli.LIST, - ), + itertools.product(ComplexUnit, Pauli.iterate()), ) def test_unit_mul(self, u: ComplexUnit, p: Pauli) -> None: assert np.allclose((u * p).matrix, complex(u) * p.matrix) @pytest.mark.parametrize( ("a", "b"), - itertools.product( - pauli.LIST, - pauli.LIST, - ), + itertools.product(Pauli.iterate(), Pauli.iterate()), ) def test_matmul(self, a: Pauli, b: Pauli) -> None: assert np.allclose((a @ b).matrix, a.matrix @ b.matrix) @@ -35,7 +28,7 @@ def test_matmul(self, a: Pauli, b: Pauli) -> None: @pytest.mark.parametrize( ("plane", "s", "t", "clifford", "angle", "choice"), itertools.product( - Plane, + iter(Plane), (False, True), (False, True), Clifford, diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index 4aa2ae24..a79cc63c 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -6,9 +6,8 @@ import numpy as np import pytest -from graphix import pauli from graphix.clifford import Clifford -from graphix.pauli import Plane +from graphix.pauli import Pauli, Plane from graphix.sim.base_backend import MeasurementDescription from graphix.sim.statevec import Statevec, StatevectorBackend from graphix.states import BasicStates, PlanarState @@ -82,6 +81,7 @@ def test_init_success(self, hadamardpattern, fx_rng: Generator) -> None: # random planar state rand_angle = fx_rng.random() * 2 * np.pi rand_plane = fx_rng.choice(np.array([i for i in Plane])) + rand_plane = fx_rng.choice(np.array([i for i in Plane])) state = PlanarState(rand_plane, rand_angle) backend = StatevectorBackend() backend.add_nodes(hadamardpattern.input_nodes, data=state) @@ -95,6 +95,7 @@ def test_init_success(self, hadamardpattern, fx_rng: Generator) -> None: def test_init_fail(self, hadamardpattern, fx_rng: Generator) -> None: rand_angle = fx_rng.random(2) * 2 * np.pi rand_plane = fx_rng.choice(np.array([i for i in Plane]), 2) + rand_plane = fx_rng.choice(np.array([i for i in Plane]), 2) state = PlanarState(rand_plane[0], rand_angle[0]) state2 = PlanarState(rand_plane[1], rand_angle[1]) @@ -119,14 +120,15 @@ def test_deterministic_measure_one(self, fx_rng: Generator): coins = [fx_rng.choice([0, 1]), fx_rng.choice([0, 1])] expected_result = sum(coins) % 2 states = [ - pauli.X.get_eigenstate(eigenvalue=coins[0]), - pauli.Z.get_eigenstate(eigenvalue=coins[1]), + Pauli.X.get_eigenstate(eigenvalue=coins[0]), + Pauli.Z.get_eigenstate(eigenvalue=coins[1]), ] nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) backend.entangle_nodes(edge=(nodes[0], nodes[1])) measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) + measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) node_to_measure = backend.node_index[0] result = backend.measure(node=node_to_measure, measurement_description=measurement_description) assert result == expected_result @@ -137,13 +139,14 @@ def test_deterministic_measure(self): # plus state (default) backend = StatevectorBackend() n_neighbors = 10 - states = [pauli.X.get_eigenstate()] + [pauli.Z.get_eigenstate() for i in range(n_neighbors)] + states = [Pauli.X.get_eigenstate()] + [Pauli.Z.get_eigenstate() for i in range(n_neighbors)] nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) for i in range(1, n_neighbors + 1): backend.entangle_nodes(edge=(nodes[0], i)) measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) + measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) node_to_measure = backend.node_index[0] result = backend.measure(node=node_to_measure, measurement_description=measurement_description) assert result == 0 @@ -157,9 +160,9 @@ def test_deterministic_measure_many(self): n_traps = 5 n_neighbors = 5 n_whatever = 5 - traps = [pauli.X.get_eigenstate() for _ in range(n_traps)] - dummies = [pauli.Z.get_eigenstate() for _ in range(n_neighbors)] - others = [pauli.I.get_eigenstate() for _ in range(n_whatever)] + traps = [Pauli.X.get_eigenstate() for _ in range(n_traps)] + dummies = [Pauli.Z.get_eigenstate() for _ in range(n_neighbors)] + others = [Pauli.I.get_eigenstate() for _ in range(n_whatever)] states = traps + dummies + others nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) @@ -172,6 +175,7 @@ def test_deterministic_measure_many(self): # Same measurement for all traps measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) + measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) for trap in nodes[:n_traps]: node_to_measure = trap @@ -191,8 +195,8 @@ def test_deterministic_measure_with_coin(self, fx_rng: Generator): n_neighbors = 10 coins = [fx_rng.choice([0, 1])] + [fx_rng.choice([0, 1]) for _ in range(n_neighbors)] expected_result = sum(coins) % 2 - states = [pauli.X.get_eigenstate(eigenvalue=coins[0])] + [ - pauli.Z.get_eigenstate(eigenvalue=coins[i + 1]) for i in range(n_neighbors) + states = [Pauli.X.get_eigenstate(eigenvalue=coins[0])] + [ + Pauli.Z.get_eigenstate(eigenvalue=coins[i + 1]) for i in range(n_neighbors) ] nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) @@ -200,6 +204,7 @@ def test_deterministic_measure_with_coin(self, fx_rng: Generator): for i in range(1, n_neighbors + 1): backend.entangle_nodes(edge=(nodes[0], i)) measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) + measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) node_to_measure = backend.node_index[0] result = backend.measure(node=node_to_measure, measurement_description=measurement_description) assert result == expected_result From 408e33032700acf339a1ecb478c8a3d8f7ec2b5e Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:23:50 +0900 Subject: [PATCH 05/61] :wrench: Check pauli.py --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 397e99c7..26544760 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,6 +108,7 @@ files = [ "graphix/instruction.py", "graphix/linalg_validations.py", "graphix/ops.py", + "graphix/pauli.py", "graphix/pyzx.py", "graphix/rng.py", "graphix/states.py", @@ -135,6 +136,7 @@ include = [ "graphix/instruction.py", "graphix/linalg_validations.py", "graphix/ops.py", + "graphix/pauli.py", "graphix/pyzx.py", "graphix/rng.py", "graphix/states.py", From e768295451061a297c51b7d8b1aafc2ececebf4e Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:58:02 +0900 Subject: [PATCH 06/61] :truck: Create measurements.py --- graphix/clifford.py | 12 -------- graphix/measurements.py | 64 +++++++++++++++++++++++++++++++++++++++++ graphix/opengraph.py | 28 ------------------ graphix/pauli.py | 18 ------------ 4 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 graphix/measurements.py diff --git a/graphix/clifford.py b/graphix/clifford.py index 55a4f891..96de1c17 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -25,18 +25,6 @@ import numpy.typing as npt -@dataclasses.dataclass -class Domains: - """ - Represent `X^sZ^t` where s and t are XOR of results from given sets of indices. - - This representation is used in `Clifford.commute_domains`. - """ - - s_domain: set[int] - t_domain: set[int] - - class Clifford(Enum): """Clifford gate.""" diff --git a/graphix/measurements.py b/graphix/measurements.py new file mode 100644 index 00000000..2be19ded --- /dev/null +++ b/graphix/measurements.py @@ -0,0 +1,64 @@ +"""MBQC measurements.""" + +from __future__ import annotations + +import dataclasses +import math + +from graphix import type_utils +from graphix.pauli import Axis, Plane, Sign + + +@dataclasses.dataclass +class Domains: + """Represent `X^sZ^t` where s and t are XOR of results from given sets of indices.""" + + s_domain: set[int] + t_domain: set[int] + + +@dataclasses.dataclass(frozen=True) +class Measurement: + """An MBQC measurement. + + :param angle: the angle of the measurement. Should be between [0, 2) + :param plane: the measurement plane + """ + + angle: float + plane: Plane + + def isclose(self, other: Measurement, rel_tol: float = 1e-09, abs_tol: float = 0.0) -> bool: + """Compare if two measurements have the same plane and their angles are close. + + Example + ------- + >>> from graphix.opengraph import Measurement + >>> from graphix.pauli import Plane + >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.XY)) + True + >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.YZ)) + False + >>> Measurement(0.1, Plane.XY).isclose(Measurement(0.0, Plane.XY)) + False + """ + return math.isclose(self.angle, other.angle, rel_tol=rel_tol, abs_tol=abs_tol) and self.plane == other.plane + + +@dataclasses.dataclass(frozen=True) +class PauliMeasurement: + """Pauli measurement.""" + + axis: Axis + sign: Sign + + @staticmethod + def try_from(plane: Plane, angle: float) -> PauliMeasurement | None: + """Return the Pauli measurement description if a given measure is Pauli.""" + angle_double = 2 * angle + if not type_utils.is_integer(angle_double): + return None + angle_double_mod_4 = int(angle_double) % 4 + axis = plane.cos if angle_double_mod_4 % 2 == 0 else plane.sin + sign = Sign.minus_if(angle_double_mod_4 >= 2) + return PauliMeasurement(axis, sign) diff --git a/graphix/opengraph.py b/graphix/opengraph.py index 82de7ff9..8a9af3be 100644 --- a/graphix/opengraph.py +++ b/graphix/opengraph.py @@ -15,34 +15,6 @@ from graphix.pauli import Plane -@dataclass(frozen=True) -class Measurement: - """An MBQC measurement. - - :param angle: the angle of the measurement. Should be between [0, 2) - :param plane: the measurement plane - """ - - angle: float - plane: Plane - - def isclose(self, other: Measurement, rel_tol: float = 1e-09, abs_tol: float = 0.0) -> bool: - """Compare if two measurements have the same plane and their angles are close. - - Example - ------- - >>> from graphix.opengraph import Measurement - >>> from graphix.pauli import Plane - >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.XY)) - True - >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.YZ)) - False - >>> Measurement(0.1, Plane.XY).isclose(Measurement(0.0, Plane.XY)) - False - """ - return math.isclose(self.angle, other.angle, rel_tol=rel_tol, abs_tol=abs_tol) and self.plane == other.plane - - @dataclass(frozen=True) class OpenGraph: """Open graph contains the graph, measurement, and input and output nodes. diff --git a/graphix/pauli.py b/graphix/pauli.py index 35075499..327b3814 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -428,21 +428,3 @@ def iterate(include_unit: bool = True) -> Iterator[Pauli]: Pauli.X = Pauli(IXYZ.X) Pauli.Y = Pauli(IXYZ.Y) Pauli.Z = Pauli(IXYZ.Z) - - -class PauliMeasurement(NamedTuple): - """Pauli measurement.""" - - axis: Axis - sign: Sign - - @staticmethod - def try_from(plane: Plane, angle: float) -> PauliMeasurement | None: - """Return the Pauli measurement description if a given measure is Pauli.""" - angle_double = 2 * angle - if not type_utils.is_integer(angle_double): - return None - angle_double_mod_4 = int(angle_double) % 4 - axis = plane.cos if angle_double_mod_4 % 2 == 0 else plane.sin - sign = Sign.minus_if(angle_double_mod_4 >= 2) - return PauliMeasurement(axis, sign) From 87a8b31f37f618fd53497d430556ddf2d4ea2870 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:08:49 +0900 Subject: [PATCH 07/61] :truck: Update symbols --- graphix/clifford.py | 2 +- graphix/command.py | 12 ++++++------ graphix/opengraph.py | 3 +-- graphix/pattern.py | 5 +++-- graphix/pyzx.py | 3 ++- graphix/sim/base_backend.py | 20 ++++---------------- graphix/sim/tensornet.py | 11 ++++++----- graphix/simulator.py | 9 +++++---- tests/test_opengraph.py | 3 ++- tests/test_pattern.py | 3 ++- tests/test_statevec_backend.py | 22 +++++++++------------- 11 files changed, 41 insertions(+), 52 deletions(-) diff --git a/graphix/clifford.py b/graphix/clifford.py index 96de1c17..b4f8596d 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -3,7 +3,6 @@ from __future__ import annotations import copy -import dataclasses from enum import Enum from typing import TYPE_CHECKING @@ -18,6 +17,7 @@ CLIFFORD_MUL, CLIFFORD_TO_QASM3, ) +from graphix.measurements import Domains from graphix.pauli import IXYZ, ComplexUnit, Pauli, Sign if TYPE_CHECKING: diff --git a/graphix/command.py b/graphix/command.py index 02567ef1..59b262f0 100644 --- a/graphix/command.py +++ b/graphix/command.py @@ -11,7 +11,8 @@ import numpy as np from graphix import type_utils -from graphix.clifford import Clifford, Domains +from graphix.clifford import Clifford +from graphix.measurements import Domains from graphix.pauli import Pauli, Plane, Sign from graphix.states import BasicStates, State @@ -147,9 +148,11 @@ class MeasureUpdate: add_term: float @staticmethod - def compute(plane: Plane, s: bool, t: bool, clifford_gate: Clifford) -> MeasureUpdate: + def compute(plane: Plane, s: int, t: int, clifford_gate: Clifford) -> MeasureUpdate: """Compute the update for a given plane, signals and vertex operator.""" gates = list(map(Pauli.from_axis, plane.axes)) + s %= 2 + t %= 2 if s: clifford_gate = Clifford.X @ clifford_gate if t: @@ -159,10 +162,7 @@ def compute(plane: Plane, s: bool, t: bool, clifford_gate: Clifford) -> MeasureU cos_pauli = clifford_gate.measure(Pauli.from_axis(plane.cos)) sin_pauli = clifford_gate.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 + coeff = -1 if exchange == (cos_pauli.unit.sign == sin_pauli.unit.sign) else 1 add_term: float = 0 if cos_pauli.unit.sign == Sign.MINUS: add_term += np.pi diff --git a/graphix/opengraph.py b/graphix/opengraph.py index 8a9af3be..05eba499 100644 --- a/graphix/opengraph.py +++ b/graphix/opengraph.py @@ -2,17 +2,16 @@ from __future__ import annotations -import math from dataclasses import dataclass from typing import TYPE_CHECKING import networkx as nx from graphix.generator import generate_from_graph +from graphix.measurements import Measurement if TYPE_CHECKING: from graphix.pattern import Pattern - from graphix.pauli import Plane @dataclass(frozen=True) diff --git a/graphix/pattern.py b/graphix/pattern.py index 45957ae9..2ec19b5b 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -15,12 +15,13 @@ import typing_extensions from graphix import command -from graphix.clifford import Clifford, Domains +from graphix.clifford import Clifford from graphix.command import Command, CommandKind from graphix.device_interface import PatternRunner from graphix.gflow import find_flow, find_gflow, get_layers from graphix.graphsim.graphstate import GraphState -from graphix.pauli import Axis, PauliMeasurement, Plane, Sign +from graphix.measurements import Domains, PauliMeasurement +from graphix.pauli import Axis, Plane, Sign from graphix.simulator import PatternSimulator from graphix.states import BasicStates from graphix.visualization import GraphVisualizer diff --git a/graphix/pyzx.py b/graphix/pyzx.py index 281640ea..a3dfa6f3 100644 --- a/graphix/pyzx.py +++ b/graphix/pyzx.py @@ -15,7 +15,8 @@ from pyzx.graph import Graph from pyzx.utils import EdgeType, FractionLike, VertexType -from graphix.opengraph import Measurement, OpenGraph +from graphix.measurements import Measurement +from graphix.opengraph import OpenGraph from graphix.pauli import Plane if TYPE_CHECKING: diff --git a/graphix/sim/base_backend.py b/graphix/sim/base_backend.py index 544c8f3c..9f974515 100644 --- a/graphix/sim/base_backend.py +++ b/graphix/sim/base_backend.py @@ -2,13 +2,13 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING import numpy as np from graphix.clifford import Clifford from graphix.command import CommandKind +from graphix.measurements import Measurement from graphix.ops import Ops from graphix.rng import ensure_rng from graphix.states import BasicStates @@ -18,16 +18,6 @@ from numpy.random import Generator - from graphix.pauli import Plane - - -@dataclass -class MeasurementDescription: - """An MBQC measurement.""" - - plane: Plane - angle: float - class NodeIndex: """A class for managing the mapping between node numbers and qubit indices in the internal state of the backend. @@ -195,18 +185,16 @@ def entangle_nodes(self, edge: tuple[int, int]) -> None: control = self.node_index.index(edge[1]) self.state.entangle((target, control)) - def measure(self, node: int, measurement_description: MeasurementDescription) -> bool: + def measure(self, node: int, meas: Measurement) -> bool: """Perform measurement of a node and trace out the qubit. Parameters ---------- node: int - measurement_description: MeasurementDescription + meas: Measurement """ loc = self.node_index.index(node) - result = perform_measure( - loc, measurement_description.plane, measurement_description.angle, self.state, self.__rng, self.__pr_calc - ) + result = perform_measure(loc, meas.plane, meas.angle, self.state, self.__rng, self.__pr_calc) self.node_index.remove(node) self.state.remove_qubit(loc) return result diff --git a/graphix/sim/tensornet.py b/graphix/sim/tensornet.py index 90283a1b..80ca2aa6 100644 --- a/graphix/sim/tensornet.py +++ b/graphix/sim/tensornet.py @@ -11,9 +11,10 @@ from quimb.tensor import Tensor, TensorNetwork from graphix import command +from graphix.measurements import Measurement from graphix.ops import Ops from graphix.rng import ensure_rng -from graphix.sim.base_backend import Backend, MeasurementDescription, State +from graphix.sim.base_backend import Backend, State from graphix.states import BasicStates, PlanarState if TYPE_CHECKING: @@ -143,7 +144,7 @@ def entangle_nodes(self, edge) -> None: elif self.graph_prep == "opt": pass - def measure(self, node: int, measurement_description: MeasurementDescription) -> tuple[Backend, int]: + def measure(self, node: int, meas: Measurement) -> tuple[Backend, int]: """Perform measurement of the node. In the context of tensornetwork, performing measurement equals to @@ -153,7 +154,7 @@ def measure(self, node: int, measurement_description: MeasurementDescription) -> ---------- node : int index of the node to measure - measurement_description : MeasurementDescription + meas : Measurement measure plane and angle """ if node in self._isolated_nodes: @@ -168,9 +169,9 @@ def measure(self, node: int, measurement_description: MeasurementDescription) -> result = self.__rng.choice([0, 1]) self.results[node] = result buffer = 2**0.5 - vec = PlanarState(measurement_description.plane, measurement_description.angle).get_statevector() + vec = PlanarState(meas.plane, meas.angle).get_statevector() if result: - vec = measurement_description.plane.orth.op @ vec + vec = meas.plane.orth.op @ vec proj_vec = vec * buffer self.state.measure_single(node, basis=proj_vec) return result diff --git a/graphix/simulator.py b/graphix/simulator.py index 412028c5..941c676f 100644 --- a/graphix/simulator.py +++ b/graphix/simulator.py @@ -14,7 +14,8 @@ from graphix.clifford import Clifford from graphix.command import BaseM, CommandKind, M, MeasureUpdate -from graphix.sim.base_backend import Backend, MeasurementDescription +from graphix.measurements import Measurement +from graphix.sim.base_backend import Backend from graphix.sim.density_matrix import DensityMatrixBackend from graphix.sim.statevec import StatevectorBackend from graphix.sim.tensornet import TensorNetworkBackend @@ -41,7 +42,7 @@ def measure(self, backend: Backend, cmd, noise_model=None) -> None: self.set_measure_result(cmd.node, result) @abc.abstractmethod - def get_measurement_description(self, cmd: BaseM) -> MeasurementDescription: + def get_measurement_description(self, cmd: BaseM) -> Measurement: """Return the description of the measurement performed by a given measure command (possibly blind).""" ... @@ -64,7 +65,7 @@ def __init__(self, results=None): results = dict() self.results = results - def get_measurement_description(self, cmd: BaseM) -> MeasurementDescription: + def get_measurement_description(self, cmd: BaseM) -> Measurement: """Return the description of the measurement performed by a given measure command (cannot be blind in the case of DefaultMeasureMethod).""" assert isinstance(cmd, M) angle = cmd.angle * np.pi @@ -73,7 +74,7 @@ def get_measurement_description(self, cmd: BaseM) -> MeasurementDescription: t_signal = sum(self.results[j] for j in cmd.t_domain) measure_update = MeasureUpdate.compute(cmd.plane, s_signal % 2 == 1, t_signal % 2 == 1, Clifford.I) angle = angle * measure_update.coeff + measure_update.add_term - return MeasurementDescription(measure_update.new_plane, angle) + return Measurement(angle, measure_update.new_plane) def get_measure_result(self, node: int) -> bool: """Return the result of a previous measurement.""" diff --git a/tests/test_opengraph.py b/tests/test_opengraph.py index 78d6ab74..a0e63c58 100644 --- a/tests/test_opengraph.py +++ b/tests/test_opengraph.py @@ -2,7 +2,8 @@ import networkx as nx -from graphix.opengraph import Measurement, OpenGraph +from graphix.measurements import Measurement +from graphix.opengraph import OpenGraph from graphix.pauli import Plane diff --git a/tests/test_pattern.py b/tests/test_pattern.py index 6679abcb..d8c5fdc2 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -12,8 +12,9 @@ from graphix.clifford import Clifford from graphix.command import C, CommandKind, E, M, N, X, Z +from graphix.measurements import PauliMeasurement from graphix.pattern import CommandNode, Pattern, shift_outcomes -from graphix.pauli import PauliMeasurement, Plane +from graphix.pauli import Plane from graphix.random_objects import rand_circuit, rand_gate from graphix.sim.density_matrix import DensityMatrix from graphix.simulator import PatternSimulator diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index a79cc63c..59582523 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -7,8 +7,8 @@ import pytest from graphix.clifford import Clifford +from graphix.measurements import Measurement from graphix.pauli import Pauli, Plane -from graphix.sim.base_backend import MeasurementDescription from graphix.sim.statevec import Statevec, StatevectorBackend from graphix.states import BasicStates, PlanarState from tests.test_graphsim import meas_op @@ -127,10 +127,9 @@ def test_deterministic_measure_one(self, fx_rng: Generator): backend.add_nodes(nodes=nodes, data=states) backend.entangle_nodes(edge=(nodes[0], nodes[1])) - measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) - measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) + meas = Measurement(0, Plane.XY) node_to_measure = backend.node_index[0] - result = backend.measure(node=node_to_measure, measurement_description=measurement_description) + result = backend.measure(node=node_to_measure, meas=meas) assert result == expected_result def test_deterministic_measure(self): @@ -145,10 +144,9 @@ def test_deterministic_measure(self): for i in range(1, n_neighbors + 1): backend.entangle_nodes(edge=(nodes[0], i)) - measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) - measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) + meas = Measurement(0, Plane.XY) node_to_measure = backend.node_index[0] - result = backend.measure(node=node_to_measure, measurement_description=measurement_description) + result = backend.measure(node=node_to_measure, meas=meas) assert result == 0 assert list(backend.node_index) == list(range(1, n_neighbors + 1)) @@ -174,12 +172,11 @@ def test_deterministic_measure_many(self): backend.entangle_nodes(edge=(other, dummy)) # Same measurement for all traps - measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) - measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) + meas = Measurement(0, Plane.XY) for trap in nodes[:n_traps]: node_to_measure = trap - result = backend.measure(node=node_to_measure, measurement_description=measurement_description) + result = backend.measure(node=node_to_measure, meas=meas) assert result == 0 assert list(backend.node_index) == list(range(n_traps, n_neighbors + n_traps + n_whatever)) @@ -203,9 +200,8 @@ def test_deterministic_measure_with_coin(self, fx_rng: Generator): for i in range(1, n_neighbors + 1): backend.entangle_nodes(edge=(nodes[0], i)) - measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) - measurement_description = MeasurementDescription(plane=Plane.XY, angle=0) + meas = Measurement(0, Plane.XY) node_to_measure = backend.node_index[0] - result = backend.measure(node=node_to_measure, measurement_description=measurement_description) + result = backend.measure(node=node_to_measure, meas=meas) assert result == expected_result assert list(backend.node_index) == list(range(1, n_neighbors + 1)) From ab36a0c4dc03ef6b4213f253dcf4cb1b0056192c Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:16:11 +0900 Subject: [PATCH 08/61] :rotating_light: Fix linter warnings --- graphix/pauli.py | 3 +-- graphix/sim/base_backend.py | 3 ++- graphix/sim/tensornet.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index 327b3814..a970fa39 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -8,11 +8,10 @@ import math import typing from enum import Enum -from typing import TYPE_CHECKING, ClassVar, NamedTuple +from typing import TYPE_CHECKING, ClassVar import typing_extensions -from graphix import type_utils from graphix._db import WellKnownMatrix if TYPE_CHECKING: diff --git a/graphix/sim/base_backend.py b/graphix/sim/base_backend.py index 9f974515..b3c8de9b 100644 --- a/graphix/sim/base_backend.py +++ b/graphix/sim/base_backend.py @@ -8,7 +8,6 @@ from graphix.clifford import Clifford from graphix.command import CommandKind -from graphix.measurements import Measurement from graphix.ops import Ops from graphix.rng import ensure_rng from graphix.states import BasicStates @@ -18,6 +17,8 @@ from numpy.random import Generator + from graphix.measurements import Measurement + class NodeIndex: """A class for managing the mapping between node numbers and qubit indices in the internal state of the backend. diff --git a/graphix/sim/tensornet.py b/graphix/sim/tensornet.py index 80ca2aa6..81071f0c 100644 --- a/graphix/sim/tensornet.py +++ b/graphix/sim/tensornet.py @@ -11,7 +11,6 @@ from quimb.tensor import Tensor, TensorNetwork from graphix import command -from graphix.measurements import Measurement from graphix.ops import Ops from graphix.rng import ensure_rng from graphix.sim.base_backend import Backend, State @@ -21,6 +20,7 @@ from numpy.random import Generator from graphix.clifford import Clifford + from graphix.measurements import Measurement from graphix.simulator import MeasureMethod From 53303927ae36ea68bc9fa21f6b9a4bdf71a8fd28 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:29:51 +0900 Subject: [PATCH 09/61] :truck: Create test_command.py --- tests/test_command.py | 51 +++++++++++++++++++++++++++++++++++++++++++ tests/test_pauli.py | 44 +------------------------------------ 2 files changed, 52 insertions(+), 43 deletions(-) create mode 100644 tests/test_command.py diff --git a/tests/test_command.py b/tests/test_command.py new file mode 100644 index 00000000..91228d34 --- /dev/null +++ b/tests/test_command.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import itertools +import math + +import numpy as np +import pytest + +from graphix.clifford import Clifford +from graphix.command import MeasureUpdate +from graphix.pauli import Plane + + +@pytest.mark.parametrize( + ("plane", "s", "t", "clifford", "angle", "choice"), + itertools.product( + Plane, + (False, True), + (False, True), + Clifford, + (0, math.pi), + (False, True), + ), +) +def test_measure_update( + plane: Plane, + s: bool, + t: bool, + clifford: Clifford, + angle: float, + choice: bool, +) -> None: + measure_update = MeasureUpdate.compute(plane, s, t, clifford) + new_angle = angle * measure_update.coeff + measure_update.add_term + vec = measure_update.new_plane.polar(new_angle) + op_mat = np.eye(2, dtype=np.complex128) / 2 + for i in range(3): + op_mat += (-1) ** (choice) * vec[i] * Clifford(i + 1).matrix / 2 + + if s: + clifford = Clifford.X @ clifford + if t: + clifford = Clifford.Z @ clifford + vec = plane.polar(angle) + op_mat_ref = np.eye(2, dtype=np.complex128) / 2 + for i in range(3): + op_mat_ref += (-1) ** (choice) * vec[i] * Clifford(i + 1).matrix / 2 + clifford_mat = clifford.matrix + op_mat_ref = clifford_mat.conj().T @ op_mat_ref @ clifford_mat + + assert np.allclose(op_mat, op_mat_ref) or np.allclose(op_mat, -op_mat_ref) diff --git a/tests/test_pauli.py b/tests/test_pauli.py index 65f2cb2a..28ea4cbb 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -5,9 +5,7 @@ import numpy as np import pytest -from graphix.clifford import Clifford -from graphix.command import MeasureUpdate -from graphix.pauli import ComplexUnit, Pauli, Plane +from graphix.pauli import ComplexUnit, Pauli class TestPauli: @@ -24,43 +22,3 @@ def test_unit_mul(self, u: ComplexUnit, p: Pauli) -> None: ) def test_matmul(self, a: Pauli, b: Pauli) -> None: assert np.allclose((a @ b).matrix, a.matrix @ b.matrix) - - @pytest.mark.parametrize( - ("plane", "s", "t", "clifford", "angle", "choice"), - itertools.product( - iter(Plane), - (False, True), - (False, True), - Clifford, - (0, np.pi), - (False, True), - ), - ) - def test_measure_update( - self, - plane: Plane, - s: bool, - t: bool, - clifford: Clifford, - angle: float, - choice: bool, - ) -> None: - measure_update = MeasureUpdate.compute(plane, s, t, clifford) - new_angle = angle * measure_update.coeff + measure_update.add_term - vec = measure_update.new_plane.polar(new_angle) - op_mat = np.eye(2, dtype=np.complex128) / 2 - for i in range(3): - op_mat += (-1) ** (choice) * vec[i] * Clifford(i + 1).matrix / 2 - - if s: - clifford = Clifford.X @ clifford - if t: - clifford = Clifford.Z @ clifford - vec = plane.polar(angle) - op_mat_ref = np.eye(2, dtype=np.complex128) / 2 - for i in range(3): - op_mat_ref += (-1) ** (choice) * vec[i] * Clifford(i + 1).matrix / 2 - clifford_mat = clifford.matrix - op_mat_ref = clifford_mat.conj().T @ op_mat_ref @ clifford_mat - - assert np.allclose(op_mat, op_mat_ref) or np.allclose(op_mat, -op_mat_ref) From 1607ab14ae84b3fafd7254e72de0db4a82f703e2 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:36:10 +0900 Subject: [PATCH 10/61] :wrench: Check test_command.py --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 26544760..19d14cb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ files = [ "noxfile.py", "tests/conftest.py", "tests/test_clifford.py", + "tests/test_command.py", "tests/test_db.py", "tests/test_kraus.py", "tests/test_pauli.py", @@ -145,6 +146,7 @@ include = [ "noxfile.py", "tests/conftest.py", "tests/test_clifford.py", + "tests/test_command.py", "tests/test_db.py", "tests/test_kraus.py", "tests/test_pauli.py", From 978f7823467187030c977d158a7ca0afb1055938 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:31:26 +0900 Subject: [PATCH 11/61] :wrench: Change glob pattern --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19d14cb7..19c4cebd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,6 @@ filterwarnings = ["ignore:Couldn't import `kahypar`"] [tool.mypy] # Keep in sync with pyright files = [ - "**/__init__.py", "graphix/channels.py", "graphix/clifford.py", "graphix/command.py", @@ -113,6 +112,7 @@ files = [ "graphix/rng.py", "graphix/states.py", "graphix/type_utils.py", + "graphix/**/__init__.py", "graphix/_db.py", "noxfile.py", "tests/conftest.py", @@ -130,7 +130,6 @@ strict = true [tool.pyright] # Keep in sync with mypy include = [ - "**/__init__.py", "graphix/channels.py", "graphix/clifford.py", "graphix/command.py", @@ -142,6 +141,7 @@ include = [ "graphix/rng.py", "graphix/states.py", "graphix/type_utils.py", + "graphix/**/__init__.py", "graphix/_db.py", "noxfile.py", "tests/conftest.py", From 781e01353584da19e7c11b9bcfa6c269e6d1c2af Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:33:43 +0900 Subject: [PATCH 12/61] :rotating_light: Resolve linter warnings --- graphix/gflow.py | 2 +- graphix/sim/base_backend.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/graphix/gflow.py b/graphix/gflow.py index 81ba4794..adb8099e 100644 --- a/graphix/gflow.py +++ b/graphix/gflow.py @@ -20,7 +20,7 @@ import numpy as np import sympy as sp -from graphix import pauli, type_utils +from graphix import type_utils from graphix.command import CommandKind from graphix.linalg import MatGF2 from graphix.pauli import Plane diff --git a/graphix/sim/base_backend.py b/graphix/sim/base_backend.py index b3c8de9b..9737f083 100644 --- a/graphix/sim/base_backend.py +++ b/graphix/sim/base_backend.py @@ -18,6 +18,7 @@ from numpy.random import Generator from graphix.measurements import Measurement + from graphix.pauli import Plane class NodeIndex: From be6faeeb4db1f15127b05ee9d5d848053b098ca6 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:53:06 +0900 Subject: [PATCH 13/61] :truck: Move to fundamentals.py to resolve circular imports --- examples/visualization.py | 2 +- graphix/clifford.py | 3 +- graphix/command.py | 3 +- graphix/fundamentals.py | 280 +++++++++++++++++++++++++++++++++ graphix/generator.py | 2 +- graphix/gflow.py | 2 +- graphix/instruction.py | 2 +- graphix/measurements.py | 4 +- graphix/pattern.py | 2 +- graphix/pauli.py | 268 +------------------------------ graphix/pyzx.py | 2 +- graphix/sim/base_backend.py | 2 +- graphix/states.py | 2 +- graphix/transpiler.py | 2 +- graphix/visualization.py | 2 +- tests/test_clifford.py | 3 +- tests/test_command.py | 2 +- tests/test_density_matrix.py | 2 +- tests/test_generator.py | 2 +- tests/test_gflow.py | 2 +- tests/test_graphsim.py | 2 +- tests/test_opengraph.py | 2 +- tests/test_pattern.py | 2 +- tests/test_pauli.py | 3 +- tests/test_statevec.py | 2 +- tests/test_statevec_backend.py | 3 +- tests/test_transpiler.py | 2 +- 27 files changed, 312 insertions(+), 293 deletions(-) create mode 100644 graphix/fundamentals.py diff --git a/examples/visualization.py b/examples/visualization.py index 0d2c057a..71bab0c6 100644 --- a/examples/visualization.py +++ b/examples/visualization.py @@ -22,7 +22,7 @@ import numpy as np from graphix import Circuit -from graphix.pauli import Plane +from graphix.fundamentals import Plane circuit = Circuit(3) circuit.cnot(0, 1) diff --git a/graphix/clifford.py b/graphix/clifford.py index b4f8596d..3bfb5bbb 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -17,8 +17,9 @@ CLIFFORD_MUL, CLIFFORD_TO_QASM3, ) +from graphix.fundamentals import IXYZ, ComplexUnit, Sign from graphix.measurements import Domains -from graphix.pauli import IXYZ, ComplexUnit, Pauli, Sign +from graphix.pauli import Pauli if TYPE_CHECKING: import numpy as np diff --git a/graphix/command.py b/graphix/command.py index 59b262f0..400baa5a 100644 --- a/graphix/command.py +++ b/graphix/command.py @@ -12,8 +12,9 @@ from graphix import type_utils from graphix.clifford import Clifford +from graphix.fundamentals import Plane, Sign from graphix.measurements import Domains -from graphix.pauli import Pauli, Plane, Sign +from graphix.pauli import Pauli from graphix.states import BasicStates, State Node = int diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py new file mode 100644 index 00000000..a8a5143b --- /dev/null +++ b/graphix/fundamentals.py @@ -0,0 +1,280 @@ +"""Fundamental components related to quantum mechanics.""" + +from __future__ import annotations + +import enum +import math +import typing +from enum import Enum +from typing import TYPE_CHECKING + +import typing_extensions + +from graphix._db import WellKnownMatrix + +if TYPE_CHECKING: + import numpy as np + import numpy.typing as npt + + +class IXYZ(Enum): + """I, X, Y or Z.""" + + I = enum.auto() + X = enum.auto() + Y = enum.auto() + Z = enum.auto() + + +class Sign(Enum): + """Sign, plus or minus.""" + + PLUS = 1 + MINUS = -1 + + def __str__(self) -> str: + """Return `+` or `-`.""" + if self == Sign.PLUS: + return "+" + return "-" + + @staticmethod + def plus_if(b: bool) -> Sign: + """Return `+` if `b` is `True`, `-` otherwise.""" + if b: + return Sign.PLUS + return Sign.MINUS + + @staticmethod + def minus_if(b: bool) -> Sign: + """Return `-` if `b` is `True`, `+` otherwise.""" + if b: + return Sign.MINUS + return Sign.PLUS + + def __neg__(self) -> Sign: + """Swap the sign.""" + return Sign.minus_if(self == Sign.PLUS) + + @typing.overload + def __mul__(self, other: Sign) -> Sign: ... + + @typing.overload + def __mul__(self, other: int) -> int: ... + + @typing.overload + def __mul__(self, other: float) -> float: ... + + @typing.overload + def __mul__(self, other: complex) -> complex: ... + + def __mul__(self, other: Sign | complex) -> Sign | int | float | complex: + """Multiply the sign with another sign or a number.""" + if isinstance(other, Sign): + return Sign.plus_if(self == other) + if isinstance(other, int): + return int(self) * other + if isinstance(other, float): + return float(self) * other + if isinstance(other, complex): + return complex(self) * other + return NotImplemented + + @typing.overload + def __rmul__(self, other: int) -> int: ... + + @typing.overload + def __rmul__(self, other: float) -> float: ... + + @typing.overload + def __rmul__(self, other: complex) -> complex: ... + + def __rmul__(self, other: complex) -> int | float | complex: + """Multiply the sign with a number.""" + if isinstance(other, int | float | complex): + return self.__mul__(other) + return NotImplemented + + def __int__(self) -> int: + """Return `1` for `+` and `-1` for `-`.""" + # mypy does not infer the return type correctly + return self.value # type: ignore[no-any-return] + + def __float__(self) -> float: + """Return `1.0` for `+` and `-1.0` for `-`.""" + return float(self.value) + + def __complex__(self) -> complex: + """Return `1.0 + 0j` for `+` and `-1.0 + 0j` for `-`.""" + return complex(self.value) + + +class ComplexUnit(Enum): + """ + 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. + """ + + # HACK: Related to arg(z) + PLUS = 0 + PLUS_J = 1 + MINUS = 2 + MINUS_J = 3 + + @staticmethod + def try_from_complex(value: complex) -> ComplexUnit | None: + """Return the ComplexUnit instance if the value is compatible, None otherwise.""" + if value == 1: + return ComplexUnit.PLUS + if value == -1: + return ComplexUnit.MINUS + if value == 1j: + return ComplexUnit.PLUS_J + if value == -1j: + return ComplexUnit.MINUS_J + return None + + @staticmethod + def from_properties(*, sign: Sign = Sign.PLUS, is_imag: bool = False) -> ComplexUnit: + """Construct ComplexUnit from its properties.""" + osign = 0 if sign == Sign.PLUS else 2 + oimag = 1 if is_imag else 0 + return ComplexUnit(osign + oimag) + + @property + def sign(self) -> Sign: + """Return the sign.""" + return Sign.plus_if(self.value < 2) + + @property + def is_imag(self) -> bool: + """Return `True` if `j` or `-j`.""" + return bool(self.value % 2) + + def __complex__(self) -> complex: + """Return the unit as complex number.""" + ret: complex = 1j**self.value + return ret + + def __repr__(self) -> str: + """Return a string representation of the unit.""" + result = "1j" if self.is_imag else "1" + if self.sign == Sign.MINUS: + result = "-" + result + return result + + def __mul__(self, other: ComplexUnit | complex) -> ComplexUnit: + """Multiply the complex unit with another complex unit.""" + if isinstance(other, ComplexUnit): + return ComplexUnit((self.value + other.value) % 4) + if other_ := ComplexUnit.try_from_complex(other): + return self.__mul__(other_) + return NotImplemented + + def __rmul__(self, other: complex) -> ComplexUnit: + """Multiply the complex unit with a number.""" + if isinstance(other, complex): + return self.__mul__(other) + return NotImplemented + + def __neg__(self) -> ComplexUnit: + """Return the opposite of the complex unit.""" + return ComplexUnit((self.value + 2) % 4) + + +class Axis(Enum): + """Axis: `X`, `Y` or `Z`.""" + + X = enum.auto() + Y = enum.auto() + Z = enum.auto() + + @property + def op(self) -> npt.NDArray[np.complex128]: + """Return the single qubit operator associated to the axis.""" + if self == Axis.X: + return WellKnownMatrix.X + if self == Axis.Y: + return WellKnownMatrix.Y + if self == Axis.Z: + return WellKnownMatrix.Z + typing_extensions.assert_never(self) + + +class Plane(Enum): + # TODO: Refactor using match + """Plane: `XY`, `YZ` or `XZ`.""" + + XY = enum.auto() + YZ = enum.auto() + XZ = enum.auto() + + @property + def axes(self) -> tuple[Axis, Axis]: + """Return the pair of axes that carry the plane.""" + if self == Plane.XY: + return (Axis.X, Axis.Y) + if self == Plane.YZ: + return (Axis.Y, Axis.Z) + if self == Plane.XZ: + return (Axis.X, Axis.Z) + typing_extensions.assert_never(self) + + @property + def orth(self) -> Axis: + """Return the axis orthogonal to the plane.""" + if self == Plane.XY: + return Axis.Z + if self == Plane.YZ: + return Axis.X + if self == Plane.XZ: + return Axis.Y + typing_extensions.assert_never(self) + + @property + def cos(self) -> Axis: + """Return the axis of the plane that conventionally carries the cos.""" + if self == Plane.XY: + return Axis.X + if self == Plane.YZ: + return Axis.Z # former convention was Y + if self == Plane.XZ: + return Axis.Z # former convention was X + typing_extensions.assert_never(self) + + @property + def sin(self) -> Axis: + """Return the axis of the plane that conventionally carries the sin.""" + if self == Plane.XY: + return Axis.Y + if self == Plane.YZ: + return Axis.Y # former convention was Z + if self == Plane.XZ: + return Axis.X # former convention was Z + typing_extensions.assert_never(self) + + def polar(self, angle: float) -> tuple[float, float, float]: + """Return the Cartesian coordinates of the point of module 1 at the given angle, following the conventional orientation for cos and sin.""" + pp = (self.cos, self.sin) + if pp == (Axis.X, Axis.Y): + return (math.cos(angle), math.sin(angle), 0) + if pp == (Axis.Z, Axis.Y): + return (0, math.sin(angle), math.cos(angle)) + if pp == (Axis.Z, Axis.X): + return (math.sin(angle), 0, math.cos(angle)) + raise RuntimeError("Unreachable.") + + @staticmethod + def from_axes(a: Axis, b: Axis) -> Plane: + """Return the plane carried by the given axes.""" + ab = {a, b} + if ab == {Axis.X, Axis.Y}: + return Plane.XY + if ab == {Axis.Y, Axis.Z}: + return Plane.YZ + if ab == {Axis.X, Axis.Z}: + return Plane.XZ + assert a == b + raise ValueError(f"Cannot make a plane giving the same axis {a} twice.") diff --git a/graphix/generator.py b/graphix/generator.py index 17faf9ff..cb5a62c6 100644 --- a/graphix/generator.py +++ b/graphix/generator.py @@ -3,9 +3,9 @@ from __future__ import annotations from graphix.command import E, M, N, X, Z +from graphix.fundamentals import Plane from graphix.gflow import find_flow, find_gflow, find_odd_neighbor, get_layers from graphix.pattern import Pattern -from graphix.pauli import Plane def generate_from_graph(graph, angles, inputs, outputs, meas_planes=None): diff --git a/graphix/gflow.py b/graphix/gflow.py index adb8099e..a1c2a729 100644 --- a/graphix/gflow.py +++ b/graphix/gflow.py @@ -22,8 +22,8 @@ from graphix import type_utils from graphix.command import CommandKind +from graphix.fundamentals import Plane from graphix.linalg import MatGF2 -from graphix.pauli import Plane if TYPE_CHECKING: from graphix.pattern import Pattern diff --git a/graphix/instruction.py b/graphix/instruction.py index 2759f92c..983569c0 100644 --- a/graphix/instruction.py +++ b/graphix/instruction.py @@ -9,7 +9,7 @@ from typing import ClassVar, Literal, Union from graphix import type_utils -from graphix.pauli import Plane +from graphix.fundamentals import Plane class InstructionKind(Enum): diff --git a/graphix/measurements.py b/graphix/measurements.py index 2be19ded..bdbc796e 100644 --- a/graphix/measurements.py +++ b/graphix/measurements.py @@ -6,7 +6,7 @@ import math from graphix import type_utils -from graphix.pauli import Axis, Plane, Sign +from graphix.fundamentals import Axis, Plane, Sign @dataclasses.dataclass @@ -34,7 +34,7 @@ def isclose(self, other: Measurement, rel_tol: float = 1e-09, abs_tol: float = 0 Example ------- >>> from graphix.opengraph import Measurement - >>> from graphix.pauli import Plane + >>> from graphix.fundamentals import Plane >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.XY)) True >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.YZ)) diff --git a/graphix/pattern.py b/graphix/pattern.py index 2ec19b5b..652e16e6 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -18,10 +18,10 @@ from graphix.clifford import Clifford from graphix.command import Command, CommandKind from graphix.device_interface import PatternRunner +from graphix.fundamentals import Axis, Plane, Sign from graphix.gflow import find_flow, find_gflow, get_layers from graphix.graphsim.graphstate import GraphState from graphix.measurements import Domains, PauliMeasurement -from graphix.pauli import Axis, Plane, Sign from graphix.simulator import PatternSimulator from graphix.states import BasicStates from graphix.visualization import GraphVisualizer diff --git a/graphix/pauli.py b/graphix/pauli.py index a970fa39..8997f29e 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -3,16 +3,13 @@ from __future__ import annotations import dataclasses -import enum import functools -import math -import typing -from enum import Enum from typing import TYPE_CHECKING, ClassVar import typing_extensions from graphix._db import WellKnownMatrix +from graphix.fundamentals import IXYZ, Axis, ComplexUnit, Sign if TYPE_CHECKING: from collections.abc import Iterator @@ -23,269 +20,6 @@ from graphix.states import PlanarState -class IXYZ(Enum): - """I, X, Y or Z.""" - - I = enum.auto() - X = enum.auto() - Y = enum.auto() - Z = enum.auto() - - -class Sign(Enum): - """Sign, plus or minus.""" - - PLUS = 1 - MINUS = -1 - - def __str__(self) -> str: - """Return `+` or `-`.""" - if self == Sign.PLUS: - return "+" - return "-" - - @staticmethod - def plus_if(b: bool) -> Sign: - """Return `+` if `b` is `True`, `-` otherwise.""" - if b: - return Sign.PLUS - return Sign.MINUS - - @staticmethod - def minus_if(b: bool) -> Sign: - """Return `-` if `b` is `True`, `+` otherwise.""" - if b: - return Sign.MINUS - return Sign.PLUS - - def __neg__(self) -> Sign: - """Swap the sign.""" - return Sign.minus_if(self == Sign.PLUS) - - @typing.overload - def __mul__(self, other: Sign) -> Sign: ... - - @typing.overload - def __mul__(self, other: int) -> int: ... - - @typing.overload - def __mul__(self, other: float) -> float: ... - - @typing.overload - def __mul__(self, other: complex) -> complex: ... - - def __mul__(self, other: Sign | complex) -> Sign | int | float | complex: - """Multiply the sign with another sign or a number.""" - if isinstance(other, Sign): - return Sign.plus_if(self == other) - if isinstance(other, int): - return int(self) * other - if isinstance(other, float): - return float(self) * other - if isinstance(other, complex): - return complex(self) * other - return NotImplemented - - @typing.overload - def __rmul__(self, other: int) -> int: ... - - @typing.overload - def __rmul__(self, other: float) -> float: ... - - @typing.overload - def __rmul__(self, other: complex) -> complex: ... - - def __rmul__(self, other: complex) -> int | float | complex: - """Multiply the sign with a number.""" - if isinstance(other, int | float | complex): - return self.__mul__(other) - return NotImplemented - - def __int__(self) -> int: - """Return `1` for `+` and `-1` for `-`.""" - # mypy does not infer the return type correctly - return self.value # type: ignore[no-any-return] - - def __float__(self) -> float: - """Return `1.0` for `+` and `-1.0` for `-`.""" - return float(self.value) - - def __complex__(self) -> complex: - """Return `1.0 + 0j` for `+` and `-1.0 + 0j` for `-`.""" - return complex(self.value) - - -class ComplexUnit(Enum): - """ - 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. - """ - - # HACK: Related to arg(z) - PLUS = 0 - PLUS_J = 1 - MINUS = 2 - MINUS_J = 3 - - @staticmethod - def try_from_complex(value: complex) -> ComplexUnit | None: - """Return the ComplexUnit instance if the value is compatible, None otherwise.""" - if value == 1: - return ComplexUnit.PLUS - if value == -1: - return ComplexUnit.MINUS - if value == 1j: - return ComplexUnit.PLUS_J - if value == -1j: - return ComplexUnit.MINUS_J - return None - - @staticmethod - def from_properties(*, sign: Sign = Sign.PLUS, is_imag: bool = False) -> ComplexUnit: - """Construct ComplexUnit from its properties.""" - osign = 0 if sign == Sign.PLUS else 2 - oimag = 1 if is_imag else 0 - return ComplexUnit(osign + oimag) - - @property - def sign(self) -> Sign: - """Return the sign.""" - return Sign.plus_if(self.value < 2) - - @property - def is_imag(self) -> bool: - """Return `True` if `j` or `-j`.""" - return bool(self.value % 2) - - def __complex__(self) -> complex: - """Return the unit as complex number.""" - ret: complex = 1j**self.value - return ret - - def __repr__(self) -> str: - """Return a string representation of the unit.""" - result = "1j" if self.is_imag else "1" - if self.sign == Sign.MINUS: - result = "-" + result - return result - - def __mul__(self, other: ComplexUnit | complex) -> ComplexUnit: - """Multiply the complex unit with another complex unit.""" - if isinstance(other, ComplexUnit): - return ComplexUnit((self.value + other.value) % 4) - if other_ := ComplexUnit.try_from_complex(other): - return self.__mul__(other_) - return NotImplemented - - def __rmul__(self, other: complex) -> ComplexUnit: - """Multiply the complex unit with a number.""" - if isinstance(other, complex): - return self.__mul__(other) - return NotImplemented - - def __neg__(self) -> ComplexUnit: - """Return the opposite of the complex unit.""" - return ComplexUnit((self.value + 2) % 4) - - -class Axis(Enum): - """Axis: `X`, `Y` or `Z`.""" - - X = enum.auto() - Y = enum.auto() - Z = enum.auto() - - @property - def op(self) -> npt.NDArray[np.complex128]: - """Return the single qubit operator associated to the axis.""" - if self == Axis.X: - return WellKnownMatrix.X - if self == Axis.Y: - return WellKnownMatrix.Y - if self == Axis.Z: - return WellKnownMatrix.Z - typing_extensions.assert_never(self) - - -class Plane(Enum): - # TODO: Refactor using match - """Plane: `XY`, `YZ` or `XZ`.""" - - XY = enum.auto() - YZ = enum.auto() - XZ = enum.auto() - - @property - def axes(self) -> tuple[Axis, Axis]: - """Return the pair of axes that carry the plane.""" - if self == Plane.XY: - return (Axis.X, Axis.Y) - if self == Plane.YZ: - return (Axis.Y, Axis.Z) - if self == Plane.XZ: - return (Axis.X, Axis.Z) - typing_extensions.assert_never(self) - - @property - def orth(self) -> Axis: - """Return the axis orthogonal to the plane.""" - if self == Plane.XY: - return Axis.Z - if self == Plane.YZ: - return Axis.X - if self == Plane.XZ: - return Axis.Y - typing_extensions.assert_never(self) - - @property - def cos(self) -> Axis: - """Return the axis of the plane that conventionally carries the cos.""" - if self == Plane.XY: - return Axis.X - if self == Plane.YZ: - return Axis.Z # former convention was Y - if self == Plane.XZ: - return Axis.Z # former convention was X - typing_extensions.assert_never(self) - - @property - def sin(self) -> Axis: - """Return the axis of the plane that conventionally carries the sin.""" - if self == Plane.XY: - return Axis.Y - if self == Plane.YZ: - return Axis.Y # former convention was Z - if self == Plane.XZ: - return Axis.X # former convention was Z - typing_extensions.assert_never(self) - - def polar(self, angle: float) -> tuple[float, float, float]: - """Return the Cartesian coordinates of the point of module 1 at the given angle, following the conventional orientation for cos and sin.""" - pp = (self.cos, self.sin) - if pp == (Axis.X, Axis.Y): - return (math.cos(angle), math.sin(angle), 0) - if pp == (Axis.Z, Axis.Y): - return (0, math.sin(angle), math.cos(angle)) - if pp == (Axis.Z, Axis.X): - return (math.sin(angle), 0, math.cos(angle)) - raise RuntimeError("Unreachable.") - - @staticmethod - def from_axes(a: Axis, b: Axis) -> Plane: - """Return the plane carried by the given axes.""" - ab = {a, b} - if ab == {Axis.X, Axis.Y}: - return Plane.XY - if ab == {Axis.Y, Axis.Z}: - return Plane.YZ - if ab == {Axis.X, Axis.Z}: - return Plane.XZ - assert a == b - raise ValueError(f"Cannot make a plane giving the same axis {a} twice.") - - @functools.lru_cache(maxsize=None) def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: """Return the product of two Paulis.""" diff --git a/graphix/pyzx.py b/graphix/pyzx.py index a3dfa6f3..2925780f 100644 --- a/graphix/pyzx.py +++ b/graphix/pyzx.py @@ -15,9 +15,9 @@ from pyzx.graph import Graph from pyzx.utils import EdgeType, FractionLike, VertexType +from graphix.fundamentals import Plane from graphix.measurements import Measurement from graphix.opengraph import OpenGraph -from graphix.pauli import Plane if TYPE_CHECKING: from pyzx.graph.base import BaseGraph diff --git a/graphix/sim/base_backend.py b/graphix/sim/base_backend.py index 9737f083..5d11a961 100644 --- a/graphix/sim/base_backend.py +++ b/graphix/sim/base_backend.py @@ -17,8 +17,8 @@ from numpy.random import Generator + from graphix.fundamentals import Plane from graphix.measurements import Measurement - from graphix.pauli import Plane class NodeIndex: diff --git a/graphix/states.py b/graphix/states.py index d242e20c..af639a74 100644 --- a/graphix/states.py +++ b/graphix/states.py @@ -11,7 +11,7 @@ import pydantic.dataclasses import typing_extensions -from graphix.pauli import Plane +from graphix.fundamentals import Plane # generic class State for all States diff --git a/graphix/transpiler.py b/graphix/transpiler.py index 5abfd8ce..e4d1e2d9 100644 --- a/graphix/transpiler.py +++ b/graphix/transpiler.py @@ -16,9 +16,9 @@ from graphix import command, instruction from graphix.clifford import Clifford from graphix.command import CommandKind, E, M, N, X, Z +from graphix.fundamentals import Plane from graphix.ops import Ops from graphix.pattern import Pattern -from graphix.pauli import Plane from graphix.sim import base_backend from graphix.sim.statevec import Data, Statevec diff --git a/graphix/visualization.py b/graphix/visualization.py index 79874396..5bbf3b33 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -11,7 +11,7 @@ from matplotlib import pyplot as plt from graphix import gflow -from graphix.pauli import Plane +from graphix.fundamentals import Plane if TYPE_CHECKING: # MEMO: Potential circular import diff --git a/tests/test_clifford.py b/tests/test_clifford.py index 2a0589ae..b8cf6962 100644 --- a/tests/test_clifford.py +++ b/tests/test_clifford.py @@ -9,7 +9,8 @@ import pytest from graphix.clifford import Clifford -from graphix.pauli import IXYZ, ComplexUnit, Pauli, Sign +from graphix.fundamentals import IXYZ, ComplexUnit, Sign +from graphix.pauli import Pauli _QASM3_DB: Final = { "id": Clifford.I, diff --git a/tests/test_command.py b/tests/test_command.py index 91228d34..f3993d5e 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -8,7 +8,7 @@ from graphix.clifford import Clifford from graphix.command import MeasureUpdate -from graphix.pauli import Plane +from graphix.fundamentals import Plane @pytest.mark.parametrize( diff --git a/tests/test_density_matrix.py b/tests/test_density_matrix.py index b240a07f..7bf2d9a0 100644 --- a/tests/test_density_matrix.py +++ b/tests/test_density_matrix.py @@ -11,8 +11,8 @@ import graphix.random_objects as randobj from graphix.channels import KrausChannel, dephasing_channel, depolarising_channel +from graphix.fundamentals import Plane from graphix.ops import Ops -from graphix.pauli import Plane from graphix.sim.density_matrix import DensityMatrix, DensityMatrixBackend from graphix.sim.statevec import CNOT_TENSOR, CZ_TENSOR, SWAP_TENSOR, Statevec, StatevectorBackend from graphix.simulator import DefaultMeasureMethod diff --git a/tests/test_generator.py b/tests/test_generator.py index 1f5229cd..f03077cf 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -6,8 +6,8 @@ import numpy as np import pytest +from graphix.fundamentals import Plane from graphix.generator import generate_from_graph -from graphix.pauli import Plane from graphix.random_objects import rand_gate if TYPE_CHECKING: diff --git a/tests/test_gflow.py b/tests/test_gflow.py index acef5b4a..e9ae212b 100644 --- a/tests/test_gflow.py +++ b/tests/test_gflow.py @@ -9,6 +9,7 @@ from numpy.random import PCG64, Generator from graphix import command +from graphix.fundamentals import Plane from graphix.gflow import ( find_flow, find_gflow, @@ -19,7 +20,6 @@ verify_pauliflow, ) from graphix.pattern import Pattern -from graphix.pauli import Plane from graphix.random_objects import rand_circuit if TYPE_CHECKING: diff --git a/tests/test_graphsim.py b/tests/test_graphsim.py index da451506..4abd8a3f 100644 --- a/tests/test_graphsim.py +++ b/tests/test_graphsim.py @@ -10,10 +10,10 @@ from networkx.utils import graphs_equal from graphix.clifford import Clifford +from graphix.fundamentals import Plane from graphix.graphsim.graphstate import GraphState from graphix.graphsim.utils import convert_rustworkx_to_networkx, is_graphs_equal from graphix.ops import Ops -from graphix.pauli import Plane from graphix.sim.statevec import Statevec with contextlib.suppress(ModuleNotFoundError): diff --git a/tests/test_opengraph.py b/tests/test_opengraph.py index a0e63c58..6e68e82c 100644 --- a/tests/test_opengraph.py +++ b/tests/test_opengraph.py @@ -2,9 +2,9 @@ import networkx as nx +from graphix.fundamentals import Plane from graphix.measurements import Measurement from graphix.opengraph import OpenGraph -from graphix.pauli import Plane # Tests whether an open graph can be converted to and from a pattern and be diff --git a/tests/test_pattern.py b/tests/test_pattern.py index d8c5fdc2..8ab600db 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -12,9 +12,9 @@ from graphix.clifford import Clifford from graphix.command import C, CommandKind, E, M, N, X, Z +from graphix.fundamentals import Plane from graphix.measurements import PauliMeasurement from graphix.pattern import CommandNode, Pattern, shift_outcomes -from graphix.pauli import Plane from graphix.random_objects import rand_circuit, rand_gate from graphix.sim.density_matrix import DensityMatrix from graphix.simulator import PatternSimulator diff --git a/tests/test_pauli.py b/tests/test_pauli.py index 28ea4cbb..76de612d 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -5,7 +5,8 @@ import numpy as np import pytest -from graphix.pauli import ComplexUnit, Pauli +from graphix.fundamentals import ComplexUnit +from graphix.pauli import Pauli class TestPauli: diff --git a/tests/test_statevec.py b/tests/test_statevec.py index 16dbaa78..855ad0f0 100644 --- a/tests/test_statevec.py +++ b/tests/test_statevec.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from graphix.pauli import Plane +from graphix.fundamentals import Plane from graphix.sim.statevec import Statevec from graphix.states import BasicStates, PlanarState diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index 59582523..6ad93c89 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -7,8 +7,9 @@ import pytest from graphix.clifford import Clifford +from graphix.fundamentals import Plane from graphix.measurements import Measurement -from graphix.pauli import Pauli, Plane +from graphix.pauli import Pauli from graphix.sim.statevec import Statevec, StatevectorBackend from graphix.states import BasicStates, PlanarState from tests.test_graphsim import meas_op diff --git a/tests/test_transpiler.py b/tests/test_transpiler.py index 1534c61a..047242f6 100644 --- a/tests/test_transpiler.py +++ b/tests/test_transpiler.py @@ -4,7 +4,7 @@ import pytest from numpy.random import PCG64, Generator -from graphix.pauli import Plane +from graphix.fundamentals import Plane from graphix.random_objects import rand_circuit, rand_gate from graphix.transpiler import Circuit From 39cb128302b0eebab9c7a01ef78d0feb5b501884 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:58:49 +0900 Subject: [PATCH 14/61] :technologist: Resolve test name conflict --- tests/test_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_db.py b/tests/test_db.py index 3c496dde..f6202c83 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -16,7 +16,7 @@ ) -class TestClifford: +class TestCliffordDB: @staticmethod def classify_pauli(arr: npt.NDArray[np.complex128]) -> _CliffordMeasure: """Compare the gate arr with Pauli gates and return the tuple of (Pauli string, sign). From 22fd4a4f80e5f01915fbd32d63a8bb60e62a9ba0 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:41:55 +0900 Subject: [PATCH 15/61] :children_crossing: Enhance generality --- graphix/fundamentals.py | 27 +++++++++++++++++++++------ graphix/pauli.py | 14 ++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index a8a5143b..969c8743 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -4,9 +4,10 @@ import enum import math +import sys import typing from enum import Enum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, SupportsComplex, SupportsFloat, SupportsIndex import typing_extensions @@ -17,6 +18,14 @@ import numpy.typing as npt +if sys.version_info >= (3, 9): + SupportsComplexCtor = SupportsComplex | SupportsFloat | SupportsIndex | complex +else: + from typing import Union + + SupportsComplexCtor = Union[SupportsComplex, SupportsFloat, SupportsIndex, complex] + + class IXYZ(Enum): """I, X, Y or Z.""" @@ -124,8 +133,14 @@ class ComplexUnit(Enum): MINUS_J = 3 @staticmethod - def try_from_complex(value: complex) -> ComplexUnit | None: + def try_from(value: ComplexUnit | SupportsComplexCtor) -> ComplexUnit | None: """Return the ComplexUnit instance if the value is compatible, None otherwise.""" + if isinstance(value, ComplexUnit): + return value + try: + value = complex(value) + except Exception: + return None if value == 1: return ComplexUnit.PLUS if value == -1: @@ -165,17 +180,17 @@ def __repr__(self) -> str: result = "-" + result return result - def __mul__(self, other: ComplexUnit | complex) -> ComplexUnit: + def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> ComplexUnit: """Multiply the complex unit with another complex unit.""" if isinstance(other, ComplexUnit): return ComplexUnit((self.value + other.value) % 4) - if other_ := ComplexUnit.try_from_complex(other): + if other_ := ComplexUnit.try_from(other): return self.__mul__(other_) return NotImplemented - def __rmul__(self, other: complex) -> ComplexUnit: + def __rmul__(self, other: SupportsComplexCtor) -> ComplexUnit: """Multiply the complex unit with a number.""" - if isinstance(other, complex): + if isinstance(other, SupportsComplexCtor): return self.__mul__(other) return NotImplemented diff --git a/graphix/pauli.py b/graphix/pauli.py index 8997f29e..5ca6f327 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -9,7 +9,7 @@ import typing_extensions from graphix._db import WellKnownMatrix -from graphix.fundamentals import IXYZ, Axis, ComplexUnit, Sign +from graphix.fundamentals import IXYZ, Axis, ComplexUnit, Sign, SupportsComplexCtor if TYPE_CHECKING: from collections.abc import Iterator @@ -127,17 +127,15 @@ def __matmul__(self, other: Pauli) -> Pauli: return _matmul_impl(self.symbol, other.symbol) * (self.unit * other.unit) return NotImplemented - def __mul__(self, other: ComplexUnit) -> Pauli: + def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> Pauli: """Return the product of two Paulis.""" - if isinstance(other, ComplexUnit): - return dataclasses.replace(self, unit=self.unit * other) + if u := ComplexUnit.try_from(other): + return dataclasses.replace(self, unit=self.unit * u) return NotImplemented - def __rmul__(self, other: ComplexUnit) -> Pauli: + def __rmul__(self, other: ComplexUnit | SupportsComplexCtor) -> Pauli: """Return the product of two Paulis.""" - if isinstance(other, ComplexUnit): - return self.__mul__(other) - return NotImplemented + return self.__mul__(other) def __neg__(self) -> Pauli: """Return the opposite.""" From ab5b05aab08f1dd40fe6747ecbe1cb931b090b73 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:48:08 +0900 Subject: [PATCH 16/61] :recycle: Change iteration order --- graphix/pauli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index 5ca6f327..4aa15154 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -149,9 +149,9 @@ def iterate(include_unit: bool = True) -> Iterator[Pauli]: ---------- include_unit (bool, optional): Include the unit in the iteration. Defaults to True. """ - us = iter(ComplexUnit) if include_unit else (ComplexUnit.PLUS,) - for unit in us: - for symbol in IXYZ: + us = tuple(ComplexUnit) if include_unit else (ComplexUnit.PLUS,) + for symbol in IXYZ: + for unit in us: yield Pauli(symbol, unit) From 2561dd6ca4baa9850715b92b612c62f817eef201 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:02:55 +0900 Subject: [PATCH 17/61] :white_check_mark: Add tests --- tests/test_pauli.py | 54 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_pauli.py b/tests/test_pauli.py index 76de612d..85c35ca1 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -5,11 +5,23 @@ import numpy as np import pytest -from graphix.fundamentals import ComplexUnit +from graphix.fundamentals import Axis, ComplexUnit from graphix.pauli import Pauli class TestPauli: + def test_from_axis(self) -> None: + assert Pauli.from_axis(Axis.X) == Pauli.X + assert Pauli.from_axis(Axis.Y) == Pauli.Y + assert Pauli.from_axis(Axis.Z) == Pauli.Z + + def test_axis(self) -> None: + with pytest.raises(ValueError): + _ = Pauli.I.axis + assert Pauli.X.axis == Axis.X + assert Pauli.Y.axis == Axis.Y + assert Pauli.Z.axis == Axis.Z + @pytest.mark.parametrize( ("u", "p"), itertools.product(ComplexUnit, Pauli.iterate()), @@ -23,3 +35,43 @@ def test_unit_mul(self, u: ComplexUnit, p: Pauli) -> None: ) def test_matmul(self, a: Pauli, b: Pauli) -> None: assert np.allclose((a @ b).matrix, a.matrix @ b.matrix) + + def test_str(self) -> None: + assert str(Pauli.I) == "IXYZ.I" + assert str(1 * Pauli.I) == "IXYZ.I" + assert str(1j * Pauli.I) == "1j * IXYZ.I" + assert str(-1 * Pauli.I) == "-IXYZ.I" + assert str(-1j * Pauli.I) == "-1j * IXYZ.I" + + @pytest.mark.parametrize("p", Pauli.iterate()) + def test_neg(self, p: Pauli) -> None: + pneg = -p + assert pneg == -p + + def test_iterate_false(self) -> None: + cmp = list(Pauli.iterate(include_unit=False)) + assert len(cmp) == 4 + assert cmp[0] == Pauli.I + assert cmp[1] == Pauli.X + assert cmp[2] == Pauli.Y + assert cmp[3] == Pauli.Z + + def test_iterate_true(self) -> None: + cmp = list(Pauli.iterate(include_unit=True)) + assert len(cmp) == 16 + assert cmp[0] == Pauli.I + assert cmp[1] == 1j * Pauli.I + assert cmp[2] == -1 * Pauli.I + assert cmp[3] == -1j * Pauli.I + assert cmp[4] == Pauli.X + assert cmp[5] == 1j * Pauli.X + assert cmp[6] == -1 * Pauli.X + assert cmp[7] == -1j * Pauli.X + assert cmp[8] == Pauli.Y + assert cmp[9] == 1j * Pauli.Y + assert cmp[10] == -1 * Pauli.Y + assert cmp[11] == -1j * Pauli.Y + assert cmp[12] == Pauli.Z + assert cmp[13] == 1j * Pauli.Z + assert cmp[14] == -1 * Pauli.Z + assert cmp[15] == -1j * Pauli.Z From e439ffe4db9f6b4bb98b4e98393ace7357b016f0 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:46:25 +0900 Subject: [PATCH 18/61] :construction: Update fundamentals.py --- graphix/fundamentals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index 969c8743..92277912 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -173,7 +173,7 @@ def __complex__(self) -> complex: ret: complex = 1j**self.value return ret - def __repr__(self) -> str: + def __str__(self) -> str: """Return a string representation of the unit.""" result = "1j" if self.is_imag else "1" if self.sign == Sign.MINUS: @@ -181,7 +181,7 @@ def __repr__(self) -> str: return result def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> ComplexUnit: - """Multiply the complex unit with another complex unit.""" + """Multiply the complex unit with a number.""" if isinstance(other, ComplexUnit): return ComplexUnit((self.value + other.value) % 4) if other_ := ComplexUnit.try_from(other): From cfe4395ee817b032259a9f5e4f03c5945f26aa9c Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:47:37 +0900 Subject: [PATCH 19/61] :white_check_mark: Test fundamentals.py --- tests/test_fundamentals.py | 154 +++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 tests/test_fundamentals.py diff --git a/tests/test_fundamentals.py b/tests/test_fundamentals.py new file mode 100644 index 00000000..4408e800 --- /dev/null +++ b/tests/test_fundamentals.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import itertools +import math + +import pytest + +from graphix.fundamentals import Axis, ComplexUnit, Plane, Sign + + +class TestSign: + def test_str(self) -> None: + assert str(Sign.PLUS) == "+" + assert str(Sign.MINUS) == "-" + + def test_plus_if(self) -> None: + assert Sign.plus_if(True) == Sign.PLUS + assert Sign.plus_if(False) == Sign.MINUS + + def test_minus_if(self) -> None: + assert Sign.minus_if(True) == Sign.MINUS + assert Sign.minus_if(False) == Sign.PLUS + + def test_neg(self) -> None: + assert -Sign.PLUS == Sign.MINUS + assert -Sign.MINUS == Sign.PLUS + + def test_mul_sign(self) -> None: + assert Sign.PLUS * Sign.PLUS == Sign.PLUS + assert Sign.PLUS * Sign.MINUS == Sign.MINUS + assert Sign.MINUS * Sign.PLUS == Sign.MINUS + assert Sign.MINUS * Sign.MINUS == Sign.PLUS + + def test_mul_int(self) -> None: + left = Sign.PLUS * 1 + assert isinstance(left, int) + assert left == int(Sign.PLUS) + right = 1 * Sign.PLUS + assert isinstance(right, int) + assert right == int(Sign.PLUS) + + left = Sign.MINUS * 1 + assert isinstance(left, int) + assert left == int(Sign.MINUS) + right = 1 * Sign.MINUS + assert isinstance(right, int) + assert right == int(Sign.MINUS) + + def test_mul_float(self) -> None: + left = Sign.PLUS * 1.0 + assert isinstance(left, float) + assert left == float(Sign.PLUS) + right = 1.0 * Sign.PLUS + assert isinstance(right, float) + assert right == float(Sign.PLUS) + + left = Sign.MINUS * 1.0 + assert isinstance(left, float) + assert left == float(Sign.MINUS) + right = 1.0 * Sign.MINUS + assert isinstance(right, float) + assert right == float(Sign.MINUS) + + def test_mul_complex(self) -> None: + left = Sign.PLUS * complex(1) + assert isinstance(left, complex) + assert left == complex(Sign.PLUS) + right = complex(1) * Sign.PLUS + assert isinstance(right, complex) + assert right == complex(Sign.PLUS) + + left = Sign.MINUS * complex(1) + assert isinstance(left, complex) + assert left == complex(Sign.MINUS) + right = complex(1) * Sign.MINUS + assert isinstance(right, complex) + assert right == complex(Sign.MINUS) + + def test_int(self) -> None: + # Necessary to justify `type: ignore` + assert isinstance(int(Sign.PLUS), int) + assert isinstance(int(Sign.MINUS), int) + + +class TestComplexUnit: + def test_try_from(self) -> None: + assert ComplexUnit.try_from(ComplexUnit.PLUS) == ComplexUnit.PLUS + assert ComplexUnit.try_from(1) == ComplexUnit.PLUS + assert ComplexUnit.try_from(1.0) == ComplexUnit.PLUS + assert ComplexUnit.try_from(1.0 + 0.0j) == ComplexUnit.PLUS + + def test_from_properties(self) -> None: + assert ComplexUnit.from_properties() == ComplexUnit.PLUS + assert ComplexUnit.from_properties(is_imag=True) == ComplexUnit.PLUS_J + assert ComplexUnit.from_properties(sign=Sign.MINUS) == ComplexUnit.MINUS + assert ComplexUnit.from_properties(sign=Sign.MINUS, is_imag=True) == ComplexUnit.MINUS_J + + @pytest.mark.parametrize(("sign", "is_imag"), itertools.product([Sign.PLUS, Sign.MINUS], [True, False])) + def test_properties(self, sign: Sign, is_imag: bool) -> None: + assert ComplexUnit.from_properties(sign=sign, is_imag=is_imag).sign == sign + assert ComplexUnit.from_properties(sign=sign, is_imag=is_imag).is_imag == is_imag + + def test_complex(self) -> None: + assert complex(ComplexUnit.PLUS) == 1 + assert complex(ComplexUnit.PLUS_J) == 1j + assert complex(ComplexUnit.MINUS) == -1 + assert complex(ComplexUnit.MINUS_J) == -1j + + def test_str(self) -> None: + assert str(ComplexUnit.PLUS) == "1" + assert str(ComplexUnit.PLUS_J) == "1j" + assert str(ComplexUnit.MINUS) == "-1" + assert str(ComplexUnit.MINUS_J) == "-1j" + + @pytest.mark.parametrize(("lhs", "rhs"), itertools.product(ComplexUnit, ComplexUnit)) + def test_mul_self(self, lhs: ComplexUnit, rhs: ComplexUnit) -> None: + assert complex(lhs * rhs) == complex(lhs) * complex(rhs) + + def test_mul_number(self) -> None: + assert ComplexUnit.PLUS * 1 == ComplexUnit.PLUS + assert 1 * ComplexUnit.PLUS == ComplexUnit.PLUS + assert ComplexUnit.PLUS * 1.0 == ComplexUnit.PLUS + assert 1.0 * ComplexUnit.PLUS == ComplexUnit.PLUS + assert ComplexUnit.PLUS * complex(1) == ComplexUnit.PLUS + assert complex(1) * ComplexUnit.PLUS == ComplexUnit.PLUS + + def test_neg(self) -> None: + assert -ComplexUnit.PLUS == ComplexUnit.MINUS + assert -ComplexUnit.PLUS_J == ComplexUnit.MINUS_J + assert -ComplexUnit.MINUS == ComplexUnit.PLUS + assert -ComplexUnit.MINUS_J == ComplexUnit.PLUS_J + + +_PLANE_INDEX = {Axis.X: 0, Axis.Y: 1, Axis.Z: 2} + + +class TestPlane: + @pytest.mark.parametrize("p", Plane) + def test_polar_consistency(self, p: Plane) -> None: + icos = _PLANE_INDEX[p.cos] + isin = _PLANE_INDEX[p.sin] + irest = 3 - icos - isin + po = p.polar(1) + assert po[icos] == pytest.approx(math.cos(1)) + assert po[isin] == pytest.approx(math.sin(1)) + assert po[irest] == 0 + + def test_from_axes(self) -> None: + assert Plane.from_axes(Axis.X, Axis.Y) == Plane.XY + assert Plane.from_axes(Axis.Y, Axis.Z) == Plane.YZ + assert Plane.from_axes(Axis.X, Axis.Z) == Plane.XZ + assert Plane.from_axes(Axis.Y, Axis.X) == Plane.XY + assert Plane.from_axes(Axis.Z, Axis.Y) == Plane.YZ + assert Plane.from_axes(Axis.Z, Axis.X) == Plane.XZ From 3627d4b4d8d826d95b6a1b000f9f2dc87a35ddc0 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:06:44 +0900 Subject: [PATCH 20/61] :wrench: Check new files --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 19c4cebd..51a7c68b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,8 +104,10 @@ files = [ "graphix/channels.py", "graphix/clifford.py", "graphix/command.py", + "graphix/fundamentals.py", "graphix/instruction.py", "graphix/linalg_validations.py", + "graphix/measurements.py", "graphix/ops.py", "graphix/pauli.py", "graphix/pyzx.py", @@ -119,6 +121,7 @@ files = [ "tests/test_clifford.py", "tests/test_command.py", "tests/test_db.py", + "tests/test_fundamentals.py", "tests/test_kraus.py", "tests/test_pauli.py", "tests/test_pyzx.py", @@ -133,8 +136,10 @@ include = [ "graphix/channels.py", "graphix/clifford.py", "graphix/command.py", + "graphix/fundamentals.py", "graphix/instruction.py", "graphix/linalg_validations.py", + "graphix/measurements.py", "graphix/ops.py", "graphix/pauli.py", "graphix/pyzx.py", @@ -148,6 +153,7 @@ include = [ "tests/test_clifford.py", "tests/test_command.py", "tests/test_db.py", + "tests/test_fundamentals.py", "tests/test_kraus.py", "tests/test_pauli.py", "tests/test_pyzx.py", From 55e958eaa5396441a76f7fd5c13025b75c220abe Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:56:53 +0900 Subject: [PATCH 21/61] :children_crossing: Use .matrix for consistency --- graphix/clifford.py | 3 ++- graphix/fundamentals.py | 38 ++++++++++++++++++++++++++------------ graphix/sim/tensornet.py | 2 +- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/graphix/clifford.py b/graphix/clifford.py index 3bfb5bbb..83a79024 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy +import functools from enum import Enum from typing import TYPE_CHECKING @@ -63,7 +64,7 @@ class Clifford(Enum): _22 = 22 _23 = 23 - @property + @functools.cached_property def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix of the Clifford gate.""" return CLIFFORD[self.value] diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index 92277912..f3296c45 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -3,6 +3,7 @@ from __future__ import annotations import enum +import functools import math import sys import typing @@ -26,15 +27,6 @@ SupportsComplexCtor = Union[SupportsComplex, SupportsFloat, SupportsIndex, complex] -class IXYZ(Enum): - """I, X, Y or Z.""" - - I = enum.auto() - X = enum.auto() - Y = enum.auto() - Z = enum.auto() - - class Sign(Enum): """Sign, plus or minus.""" @@ -199,6 +191,28 @@ def __neg__(self) -> ComplexUnit: return ComplexUnit((self.value + 2) % 4) +class IXYZ(Enum): + """I, X, Y or Z.""" + + I = enum.auto() + X = enum.auto() + Y = enum.auto() + Z = enum.auto() + + @functools.cached_property + def matrix(self) -> npt.NDArray[np.complex128]: + """Return the matrix representation.""" + if self == IXYZ.I: + return WellKnownMatrix.I + if self == IXYZ.X: + return WellKnownMatrix.X + if self == IXYZ.Y: + return WellKnownMatrix.Y + if self == IXYZ.Z: + return WellKnownMatrix.Z + typing_extensions.assert_never(self) + + class Axis(Enum): """Axis: `X`, `Y` or `Z`.""" @@ -206,9 +220,9 @@ class Axis(Enum): Y = enum.auto() Z = enum.auto() - @property - def op(self) -> npt.NDArray[np.complex128]: - """Return the single qubit operator associated to the axis.""" + @functools.cached_property + def matrix(self) -> npt.NDArray[np.complex128]: + """Return the matrix representation.""" if self == Axis.X: return WellKnownMatrix.X if self == Axis.Y: diff --git a/graphix/sim/tensornet.py b/graphix/sim/tensornet.py index 81071f0c..3bc94fc3 100644 --- a/graphix/sim/tensornet.py +++ b/graphix/sim/tensornet.py @@ -171,7 +171,7 @@ def measure(self, node: int, meas: Measurement) -> tuple[Backend, int]: buffer = 2**0.5 vec = PlanarState(meas.plane, meas.angle).get_statevector() if result: - vec = meas.plane.orth.op @ vec + vec = meas.plane.orth.matrix @ vec proj_vec = vec * buffer self.state.measure_single(node, basis=proj_vec) return result From 99d987fd853539f908480ecc5869d43c4c24cdb7 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 19 Oct 2024 02:17:31 +0900 Subject: [PATCH 22/61] :recycle: Remove get_ --- graphix/pauli.py | 2 +- tests/test_statevec_backend.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index 4aa15154..db6f4870 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -90,7 +90,7 @@ def matrix(self) -> npt.NDArray[np.complex128]: return co * WellKnownMatrix.Z typing_extensions.assert_never(self.symbol) - def get_eigenstate(self, eigenvalue: int | Sign = 0) -> PlanarState: + def eigenstate(self, eigenvalue: int | Sign = 0) -> PlanarState: """Return the eigenstate of the Pauli.""" from graphix.states import BasicStates diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index 6ad93c89..adbb89e3 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -121,8 +121,8 @@ def test_deterministic_measure_one(self, fx_rng: Generator): coins = [fx_rng.choice([0, 1]), fx_rng.choice([0, 1])] expected_result = sum(coins) % 2 states = [ - Pauli.X.get_eigenstate(eigenvalue=coins[0]), - Pauli.Z.get_eigenstate(eigenvalue=coins[1]), + Pauli.X.eigenstate(eigenvalue=coins[0]), + Pauli.Z.eigenstate(eigenvalue=coins[1]), ] nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) @@ -139,7 +139,7 @@ def test_deterministic_measure(self): # plus state (default) backend = StatevectorBackend() n_neighbors = 10 - states = [Pauli.X.get_eigenstate()] + [Pauli.Z.get_eigenstate() for i in range(n_neighbors)] + states = [Pauli.X.eigenstate()] + [Pauli.Z.eigenstate() for i in range(n_neighbors)] nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) @@ -159,9 +159,9 @@ def test_deterministic_measure_many(self): n_traps = 5 n_neighbors = 5 n_whatever = 5 - traps = [Pauli.X.get_eigenstate() for _ in range(n_traps)] - dummies = [Pauli.Z.get_eigenstate() for _ in range(n_neighbors)] - others = [Pauli.I.get_eigenstate() for _ in range(n_whatever)] + traps = [Pauli.X.eigenstate() for _ in range(n_traps)] + dummies = [Pauli.Z.eigenstate() for _ in range(n_neighbors)] + others = [Pauli.I.eigenstate() for _ in range(n_whatever)] states = traps + dummies + others nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) @@ -193,8 +193,8 @@ def test_deterministic_measure_with_coin(self, fx_rng: Generator): n_neighbors = 10 coins = [fx_rng.choice([0, 1])] + [fx_rng.choice([0, 1]) for _ in range(n_neighbors)] expected_result = sum(coins) % 2 - states = [Pauli.X.get_eigenstate(eigenvalue=coins[0])] + [ - Pauli.Z.get_eigenstate(eigenvalue=coins[i + 1]) for i in range(n_neighbors) + states = [Pauli.X.eigenstate(eigenvalue=coins[0])] + [ + Pauli.Z.eigenstate(eigenvalue=coins[i + 1]) for i in range(n_neighbors) ] nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) From e85b828bb0b5f066994edab994e15c034c3cb4c6 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:07:23 +0900 Subject: [PATCH 23/61] :truck: Change filename --- graphix/command.py | 4 ++-- graphix/gflow.py | 14 +++++++------- graphix/instruction.py | 4 ++-- graphix/measurements.py | 4 ++-- graphix/sim/statevec.py | 6 +++--- graphix/{type_utils.py => utils.py} | 0 6 files changed, 16 insertions(+), 16 deletions(-) rename graphix/{type_utils.py => utils.py} (100%) diff --git a/graphix/command.py b/graphix/command.py index 400baa5a..a4045071 100644 --- a/graphix/command.py +++ b/graphix/command.py @@ -10,7 +10,7 @@ import numpy as np -from graphix import type_utils +from graphix import utils from graphix.clifford import Clifford from graphix.fundamentals import Plane, Sign from graphix.measurements import Domains @@ -38,7 +38,7 @@ class _KindChecker: def __init_subclass__(cls) -> None: super().__init_subclass__() - type_utils.check_kind(cls, {"CommandKind": CommandKind, "Clifford": Clifford}) + utils.check_kind(cls, {"CommandKind": CommandKind, "Clifford": Clifford}) @dataclasses.dataclass diff --git a/graphix/gflow.py b/graphix/gflow.py index a1c2a729..2f17b39a 100644 --- a/graphix/gflow.py +++ b/graphix/gflow.py @@ -20,7 +20,7 @@ import numpy as np import sympy as sp -from graphix import type_utils +from graphix import utils from graphix.command import CommandKind from graphix.fundamentals import Plane from graphix.linalg import MatGF2 @@ -1372,18 +1372,18 @@ def get_pauli_nodes( l_x, l_y, l_z = set(), set(), set() for node, plane in meas_planes.items(): if plane == Plane.XY: - if type_utils.is_integer(meas_angles[node]): # measurement angle is integer + if utils.is_integer(meas_angles[node]): # measurement angle is integer l_x |= {node} - elif type_utils.is_integer(2 * meas_angles[node]): # measurement angle is half integer + elif utils.is_integer(2 * meas_angles[node]): # measurement angle is half integer l_y |= {node} elif plane == Plane.XZ: - if type_utils.is_integer(meas_angles[node]): + if utils.is_integer(meas_angles[node]): l_z |= {node} - elif type_utils.is_integer(2 * meas_angles[node]): + elif utils.is_integer(2 * meas_angles[node]): l_x |= {node} elif plane == Plane.YZ: - if type_utils.is_integer(meas_angles[node]): + if utils.is_integer(meas_angles[node]): l_y |= {node} - elif type_utils.is_integer(2 * meas_angles[node]): + elif utils.is_integer(2 * meas_angles[node]): l_z |= {node} return l_x, l_y, l_z diff --git a/graphix/instruction.py b/graphix/instruction.py index 983569c0..42fa6044 100644 --- a/graphix/instruction.py +++ b/graphix/instruction.py @@ -8,7 +8,7 @@ from enum import Enum from typing import ClassVar, Literal, Union -from graphix import type_utils +from graphix import utils from graphix.fundamentals import Plane @@ -39,7 +39,7 @@ class _KindChecker: def __init_subclass__(cls) -> None: super().__init_subclass__() - type_utils.check_kind(cls, {"InstructionKind": InstructionKind, "Plane": Plane}) + utils.check_kind(cls, {"InstructionKind": InstructionKind, "Plane": Plane}) @dataclasses.dataclass diff --git a/graphix/measurements.py b/graphix/measurements.py index bdbc796e..40c68179 100644 --- a/graphix/measurements.py +++ b/graphix/measurements.py @@ -5,7 +5,7 @@ import dataclasses import math -from graphix import type_utils +from graphix import utils from graphix.fundamentals import Axis, Plane, Sign @@ -56,7 +56,7 @@ class PauliMeasurement: def try_from(plane: Plane, angle: float) -> PauliMeasurement | None: """Return the Pauli measurement description if a given measure is Pauli.""" angle_double = 2 * angle - if not type_utils.is_integer(angle_double): + if not utils.is_integer(angle_double): return None angle_double_mod_4 = int(angle_double) % 4 axis = plane.cos if angle_double_mod_4 % 2 == 0 else plane.sin diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 6548fa29..bfc5ec6a 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -11,7 +11,7 @@ import numpy as np import numpy.typing as npt -from graphix import states, type_utils +from graphix import states, utils from graphix.sim.base_backend import Backend, State from graphix.states import BasicStates @@ -111,7 +111,7 @@ def __init__( else: if isinstance(input_list[0], states.State): - type_utils.check_list_elements(input_list, states.State) + utils.check_list_elements(input_list, states.State) if nqubit is None: nqubit = len(input_list) elif nqubit != len(input_list): @@ -121,7 +121,7 @@ def __init__( # reshape self.psi = tmp_psi.reshape((2,) * nqubit) elif isinstance(input_list[0], numbers.Number): - type_utils.check_list_elements(input_list, numbers.Number) + utils.check_list_elements(input_list, numbers.Number) if nqubit is None: length = len(input_list) if length & (length - 1): diff --git a/graphix/type_utils.py b/graphix/utils.py similarity index 100% rename from graphix/type_utils.py rename to graphix/utils.py From a6443e4cfe4fb57e2f850a03f02dcf51bc0d6066 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:08:05 +0900 Subject: [PATCH 24/61] :memo: Update module docstring --- graphix/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix/utils.py b/graphix/utils.py index aae1cf3d..ee102ac6 100644 --- a/graphix/utils.py +++ b/graphix/utils.py @@ -1,4 +1,4 @@ -"""Type utilities.""" +"""Utilities.""" from __future__ import annotations From 05221710cf965da8c351e076e3a1e9bde4c94377 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:25:09 +0900 Subject: [PATCH 25/61] :sparkles: Update utils.py --- graphix/utils.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/graphix/utils.py b/graphix/utils.py index ee102ac6..5356bcaf 100644 --- a/graphix/utils.py +++ b/graphix/utils.py @@ -6,6 +6,9 @@ import typing from typing import TYPE_CHECKING, Any, ClassVar, Literal, SupportsInt, TypeVar +import numpy as np +import numpy.typing as npt + if TYPE_CHECKING: from collections.abc import Iterable @@ -46,3 +49,26 @@ def check_kind(cls: type, scope: dict[str, Any]) -> None: def is_integer(value: SupportsInt) -> bool: """Return `True` if `value` is an integer, `False` otherwise.""" return value == int(value) + + +G = TypeVar("G", bound=np.generic) + + +@typing.overload +def lock(data: npt.NDArray[Any]) -> npt.NDArray[np.complex128]: ... + + +@typing.overload +def lock(data: npt.NDArray[Any], dtype: type[G]) -> npt.NDArray[G]: ... + + +def lock(data: npt.NDArray[Any], dtype: type = np.complex128) -> npt.NDArray[Any]: + """Create a true immutable view. + + data must not have aliasing references, otherwise users can still turn on writeable flag of m. + """ + m: npt.NDArray[Any] = data.astype(dtype) + m.flags.writeable = False + v = m.view() + assert not v.flags.writeable + return v From 0f9f85b214730bca29755a79fa3ea2364dfa56d8 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:42:30 +0900 Subject: [PATCH 26/61] :recycle: Remove WellKnownMatrix --- graphix/_db.py | 123 +++++++++------------------------------- graphix/fundamentals.py | 16 +++--- graphix/ops.py | 57 ++++++++++++++++++- graphix/pauli.py | 10 ++-- 4 files changed, 95 insertions(+), 111 deletions(-) diff --git a/graphix/_db.py b/graphix/_db.py index c3079963..d14654d3 100644 --- a/graphix/_db.py +++ b/graphix/_db.py @@ -2,51 +2,38 @@ from __future__ import annotations -from typing import ClassVar, Literal, NamedTuple, TypeVar +from typing import Literal, NamedTuple import numpy as np -import numpy.typing as npt - -_T = TypeVar("_T", bound=np.generic) - - -def _lock(data: npt.NDArray[_T]) -> npt.NDArray[np.complex128]: - """Create a true immutable view. - - data must not have aliasing references, otherwise users can still turn on writeable flag of m. - """ - m = data.astype(np.complex128) - m.flags.writeable = False - v = m.view() - assert not v.flags.writeable - return v +from graphix import utils +from graphix.ops import Ops # 24 Unique 1-qubit Clifford gates -_C0 = _lock(np.asarray([[1, 0], [0, 1]])) # identity -_C1 = _lock(np.asarray([[0, 1], [1, 0]])) # X -_C2 = _lock(np.asarray([[0, -1j], [1j, 0]])) # Y -_C3 = _lock(np.asarray([[1, 0], [0, -1]])) # Z -_C4 = _lock(np.asarray([[1, 0], [0, 1j]])) # S = \sqrt{Z} -_C5 = _lock(np.asarray([[1, 0], [0, -1j]])) # S dagger -_C6 = _lock(np.asarray([[1, 1], [1, -1]]) / np.sqrt(2)) # Hadamard -_C7 = _lock(np.asarray([[1, -1j], [-1j, 1]]) / np.sqrt(2)) # \sqrt{iX} -_C8 = _lock(np.asarray([[1, -1], [1, 1]]) / np.sqrt(2)) # \sqrt{iY} -_C9 = _lock(np.asarray([[0, 1 - 1j], [-1 - 1j, 0]]) / np.sqrt(2)) # sqrt{I} -_C10 = _lock(np.asarray([[0, -1 - 1j], [1 - 1j, 0]]) / np.sqrt(2)) # sqrt{-I} -_C11 = _lock(np.asarray([[1, -1], [-1, -1]]) / np.sqrt(2)) # sqrt{I} -_C12 = _lock(np.asarray([[-1, -1], [1, -1]]) / np.sqrt(2)) # sqrt{-iY} -_C13 = _lock(np.asarray([[1j, -1], [1, -1j]]) / np.sqrt(2)) # sqrt{-I} -_C14 = _lock(np.asarray([[1j, 1], [-1, -1j]]) / np.sqrt(2)) # sqrt{-I} -_C15 = _lock(np.asarray([[-1, -1j], [-1j, -1]]) / np.sqrt(2)) # sqrt{-iX} -_C16 = _lock(np.asarray([[-1 + 1j, 1 + 1j], [-1 + 1j, -1 - 1j]]) / 2) # I^(1/3) -_C17 = _lock(np.asarray([[-1 + 1j, -1 - 1j], [1 - 1j, -1 - 1j]]) / 2) # I^(1/3) -_C18 = _lock(np.asarray([[1 + 1j, 1 - 1j], [-1 - 1j, 1 - 1j]]) / 2) # I^(1/3) -_C19 = _lock(np.asarray([[-1 - 1j, 1 - 1j], [-1 - 1j, -1 + 1j]]) / 2) # I^(1/3) -_C20 = _lock(np.asarray([[-1 - 1j, -1 - 1j], [1 - 1j, -1 + 1j]]) / 2) # I^(1/3) -_C21 = _lock(np.asarray([[-1 + 1j, -1 + 1j], [1 + 1j, -1 - 1j]]) / 2) # I^(1/3) -_C22 = _lock(np.asarray([[1 + 1j, -1 - 1j], [1 - 1j, 1 - 1j]]) / 2) # I^(1/3) -_C23 = _lock(np.asarray([[-1 + 1j, 1 - 1j], [-1 - 1j, -1 - 1j]]) / 2) # I^(1/3) +_C0 = Ops.I # identity +_C1 = Ops.X # X +_C2 = Ops.Y # Y +_C3 = Ops.Z # Z +_C4 = Ops.S # S = \sqrt{Z} +_C5 = Ops.SDG # S dagger +_C6 = Ops.H # Hadamard +_C7 = utils.lock(np.asarray([[1, -1j], [-1j, 1]]) / np.sqrt(2)) # \sqrt{iX} +_C8 = utils.lock(np.asarray([[1, -1], [1, 1]]) / np.sqrt(2)) # \sqrt{iY} +_C9 = utils.lock(np.asarray([[0, 1 - 1j], [-1 - 1j, 0]]) / np.sqrt(2)) # sqrt{I} +_C10 = utils.lock(np.asarray([[0, -1 - 1j], [1 - 1j, 0]]) / np.sqrt(2)) # sqrt{-I} +_C11 = utils.lock(np.asarray([[1, -1], [-1, -1]]) / np.sqrt(2)) # sqrt{I} +_C12 = utils.lock(np.asarray([[-1, -1], [1, -1]]) / np.sqrt(2)) # sqrt{-iY} +_C13 = utils.lock(np.asarray([[1j, -1], [1, -1j]]) / np.sqrt(2)) # sqrt{-I} +_C14 = utils.lock(np.asarray([[1j, 1], [-1, -1j]]) / np.sqrt(2)) # sqrt{-I} +_C15 = utils.lock(np.asarray([[-1, -1j], [-1j, -1]]) / np.sqrt(2)) # sqrt{-iX} +_C16 = utils.lock(np.asarray([[-1 + 1j, 1 + 1j], [-1 + 1j, -1 - 1j]]) / 2) # I^(1/3) +_C17 = utils.lock(np.asarray([[-1 + 1j, -1 - 1j], [1 - 1j, -1 - 1j]]) / 2) # I^(1/3) +_C18 = utils.lock(np.asarray([[1 + 1j, 1 - 1j], [-1 - 1j, 1 - 1j]]) / 2) # I^(1/3) +_C19 = utils.lock(np.asarray([[-1 - 1j, 1 - 1j], [-1 - 1j, -1 + 1j]]) / 2) # I^(1/3) +_C20 = utils.lock(np.asarray([[-1 - 1j, -1 - 1j], [1 - 1j, -1 + 1j]]) / 2) # I^(1/3) +_C21 = utils.lock(np.asarray([[-1 + 1j, -1 + 1j], [1 + 1j, -1 - 1j]]) / 2) # I^(1/3) +_C22 = utils.lock(np.asarray([[1 + 1j, -1 - 1j], [1 - 1j, 1 - 1j]]) / 2) # I^(1/3) +_C23 = utils.lock(np.asarray([[-1 + 1j, 1 - 1j], [-1 - 1j, -1 - 1j]]) / 2) # I^(1/3) # list of unique 1-qubit Clifford gates @@ -243,59 +230,3 @@ class _CliffordMeasureTuple(NamedTuple): ("h", "x", "sdg"), ("h", "x", "s"), ) - - -class WellKnownMatrix: - """Collection of well-known matrices.""" - - I: ClassVar[npt.NDArray[np.complex128]] = _C0 - X: ClassVar[npt.NDArray[np.complex128]] = _C1 - Y: ClassVar[npt.NDArray[np.complex128]] = _C2 - Z: ClassVar[npt.NDArray[np.complex128]] = _C3 - S: ClassVar[npt.NDArray[np.complex128]] = _C4 - SDG: ClassVar[npt.NDArray[np.complex128]] = _C5 - H: ClassVar[npt.NDArray[np.complex128]] = _C6 - CZ: ClassVar[npt.NDArray[np.complex128]] = _lock( - np.asarray( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, -1], - ], - ) - ) - CNOT: ClassVar[npt.NDArray[np.complex128]] = _lock( - np.asarray( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 0, 1], - [0, 0, 1, 0], - ], - ) - ) - SWAP: ClassVar[npt.NDArray[np.complex128]] = _lock( - np.asarray( - [ - [1, 0, 0, 0], - [0, 0, 1, 0], - [0, 1, 0, 0], - [0, 0, 0, 1], - ], - ) - ) - CCX: ClassVar[npt.NDArray[np.complex128]] = _lock( - np.asarray( - [ - [1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 1, 0], - ], - ) - ) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index f3296c45..b7d24c97 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -12,7 +12,7 @@ import typing_extensions -from graphix._db import WellKnownMatrix +from graphix.ops import Ops if TYPE_CHECKING: import numpy as np @@ -203,13 +203,13 @@ class IXYZ(Enum): def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix representation.""" if self == IXYZ.I: - return WellKnownMatrix.I + return Ops.I if self == IXYZ.X: - return WellKnownMatrix.X + return Ops.X if self == IXYZ.Y: - return WellKnownMatrix.Y + return Ops.Y if self == IXYZ.Z: - return WellKnownMatrix.Z + return Ops.Z typing_extensions.assert_never(self) @@ -224,11 +224,11 @@ class Axis(Enum): def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix representation.""" if self == Axis.X: - return WellKnownMatrix.X + return Ops.X if self == Axis.Y: - return WellKnownMatrix.Y + return Ops.Y if self == Axis.Z: - return WellKnownMatrix.Z + return Ops.Z typing_extensions.assert_never(self) diff --git a/graphix/ops.py b/graphix/ops.py index 538c65dc..ae9b93e7 100644 --- a/graphix/ops.py +++ b/graphix/ops.py @@ -4,16 +4,69 @@ from functools import reduce from itertools import product +from typing import ClassVar import numpy as np import numpy.typing as npt -from graphix._db import WellKnownMatrix +from graphix import utils -class Ops(WellKnownMatrix): +class Ops: """Basic single- and two-qubits operators.""" + I: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[1, 0], [0, 1]])) + X: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[0, 1], [1, 0]])) + Y: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[0, -1j], [1j, 0]])) + Z: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[1, 0], [0, -1]])) + S: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[1, 0], [0, 1j]])) + SDG: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[1, 0], [0, -1j]])) + H: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[1, 1], [1, -1]]) / np.sqrt(2)) + CZ: ClassVar[npt.NDArray[np.complex128]] = utils.lock( + np.asarray( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, -1], + ], + ) + ) + CNOT: ClassVar[npt.NDArray[np.complex128]] = utils.lock( + np.asarray( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + ], + ) + ) + SWAP: ClassVar[npt.NDArray[np.complex128]] = utils.lock( + np.asarray( + [ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + ], + ) + ) + CCX: ClassVar[npt.NDArray[np.complex128]] = utils.lock( + np.asarray( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0], + ], + ) + ) + @staticmethod def rx(theta: float) -> npt.NDArray[np.complex128]: """X rotation. diff --git a/graphix/pauli.py b/graphix/pauli.py index db6f4870..c9b3c764 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -8,8 +8,8 @@ import typing_extensions -from graphix._db import WellKnownMatrix from graphix.fundamentals import IXYZ, Axis, ComplexUnit, Sign, SupportsComplexCtor +from graphix.ops import Ops if TYPE_CHECKING: from collections.abc import Iterator @@ -81,13 +81,13 @@ def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix of the Pauli gate.""" co = complex(self.unit) if self.symbol == IXYZ.I: - return co * WellKnownMatrix.I + return co * Ops.I if self.symbol == IXYZ.X: - return co * WellKnownMatrix.X + return co * Ops.X if self.symbol == IXYZ.Y: - return co * WellKnownMatrix.Y + return co * Ops.Y if self.symbol == IXYZ.Z: - return co * WellKnownMatrix.Z + return co * Ops.Z typing_extensions.assert_never(self.symbol) def eigenstate(self, eigenvalue: int | Sign = 0) -> PlanarState: From d645c8c63e37f1c5e8cca7c8e766a97f8f67dc86 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:48:35 +0900 Subject: [PATCH 27/61] :recycle: Reuse enums --- graphix/_db.py | 65 +++++++++++++++++++++++---------------------- graphix/clifford.py | 4 +-- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/graphix/_db.py b/graphix/_db.py index d14654d3..405a0d53 100644 --- a/graphix/_db.py +++ b/graphix/_db.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import Literal, NamedTuple +from typing import NamedTuple import numpy as np from graphix import utils +from graphix.fundamentals import IXYZ, Sign from graphix.ops import Ops # 24 Unique 1-qubit Clifford gates @@ -131,47 +132,47 @@ CLIFFORD_CONJ = (0, 1, 2, 3, 5, 4, 6, 15, 12, 9, 10, 11, 8, 13, 14, 7, 20, 22, 23, 21, 16, 19, 17, 18) -class _CliffordMeasure(NamedTuple): +class _CM(NamedTuple): """NamedTuple just for documentation purposes.""" - pstr: Literal["X", "Y", "Z"] - sign: Literal[-1, +1] + pstr: IXYZ + sign: Sign -class _CliffordMeasureTuple(NamedTuple): - x: _CliffordMeasure - y: _CliffordMeasure - z: _CliffordMeasure +class _CMTuple(NamedTuple): + x: _CM + y: _CM + z: _CM # Conjugation of Pauli gates P with Clifford gate C, # i.e. C @ P @ C^dagger result in Pauli group, i.e. {\pm} \times {X, Y, Z}. # CLIFFORD_MEASURE contains the effect of Clifford conjugation of Pauli gates. CLIFFORD_MEASURE = ( - _CliffordMeasureTuple(_CliffordMeasure("X", +1), _CliffordMeasure("Y", +1), _CliffordMeasure("Z", +1)), - _CliffordMeasureTuple(_CliffordMeasure("X", +1), _CliffordMeasure("Y", -1), _CliffordMeasure("Z", -1)), - _CliffordMeasureTuple(_CliffordMeasure("X", -1), _CliffordMeasure("Y", +1), _CliffordMeasure("Z", -1)), - _CliffordMeasureTuple(_CliffordMeasure("X", -1), _CliffordMeasure("Y", -1), _CliffordMeasure("Z", +1)), - _CliffordMeasureTuple(_CliffordMeasure("Y", -1), _CliffordMeasure("X", +1), _CliffordMeasure("Z", +1)), - _CliffordMeasureTuple(_CliffordMeasure("Y", +1), _CliffordMeasure("X", -1), _CliffordMeasure("Z", +1)), - _CliffordMeasureTuple(_CliffordMeasure("Z", +1), _CliffordMeasure("Y", -1), _CliffordMeasure("X", +1)), - _CliffordMeasureTuple(_CliffordMeasure("X", +1), _CliffordMeasure("Z", -1), _CliffordMeasure("Y", +1)), - _CliffordMeasureTuple(_CliffordMeasure("Z", +1), _CliffordMeasure("Y", +1), _CliffordMeasure("X", -1)), - _CliffordMeasureTuple(_CliffordMeasure("Y", -1), _CliffordMeasure("X", -1), _CliffordMeasure("Z", -1)), - _CliffordMeasureTuple(_CliffordMeasure("Y", +1), _CliffordMeasure("X", +1), _CliffordMeasure("Z", -1)), - _CliffordMeasureTuple(_CliffordMeasure("Z", -1), _CliffordMeasure("Y", -1), _CliffordMeasure("X", -1)), - _CliffordMeasureTuple(_CliffordMeasure("Z", -1), _CliffordMeasure("Y", +1), _CliffordMeasure("X", +1)), - _CliffordMeasureTuple(_CliffordMeasure("X", -1), _CliffordMeasure("Z", -1), _CliffordMeasure("Y", -1)), - _CliffordMeasureTuple(_CliffordMeasure("X", -1), _CliffordMeasure("Z", +1), _CliffordMeasure("Y", +1)), - _CliffordMeasureTuple(_CliffordMeasure("X", +1), _CliffordMeasure("Z", +1), _CliffordMeasure("Y", -1)), - _CliffordMeasureTuple(_CliffordMeasure("Z", +1), _CliffordMeasure("X", +1), _CliffordMeasure("Y", +1)), - _CliffordMeasureTuple(_CliffordMeasure("Z", -1), _CliffordMeasure("X", +1), _CliffordMeasure("Y", -1)), - _CliffordMeasureTuple(_CliffordMeasure("Z", -1), _CliffordMeasure("X", -1), _CliffordMeasure("Y", +1)), - _CliffordMeasureTuple(_CliffordMeasure("Z", +1), _CliffordMeasure("X", -1), _CliffordMeasure("Y", -1)), - _CliffordMeasureTuple(_CliffordMeasure("Y", +1), _CliffordMeasure("Z", +1), _CliffordMeasure("X", +1)), - _CliffordMeasureTuple(_CliffordMeasure("Y", -1), _CliffordMeasure("Z", -1), _CliffordMeasure("X", +1)), - _CliffordMeasureTuple(_CliffordMeasure("Y", +1), _CliffordMeasure("Z", -1), _CliffordMeasure("X", -1)), - _CliffordMeasureTuple(_CliffordMeasure("Y", -1), _CliffordMeasure("Z", +1), _CliffordMeasure("X", -1)), + _CMTuple(_CM(IXYZ.X, Sign.PLUS), _CM(IXYZ.Y, Sign.PLUS), _CM(IXYZ.Z, Sign.PLUS)), + _CMTuple(_CM(IXYZ.X, Sign.PLUS), _CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.Z, Sign.MINUS)), + _CMTuple(_CM(IXYZ.X, Sign.MINUS), _CM(IXYZ.Y, Sign.PLUS), _CM(IXYZ.Z, Sign.MINUS)), + _CMTuple(_CM(IXYZ.X, Sign.MINUS), _CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.Z, Sign.PLUS)), + _CMTuple(_CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.X, Sign.PLUS), _CM(IXYZ.Z, Sign.PLUS)), + _CMTuple(_CM(IXYZ.Y, Sign.PLUS), _CM(IXYZ.X, Sign.MINUS), _CM(IXYZ.Z, Sign.PLUS)), + _CMTuple(_CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.X, Sign.PLUS)), + _CMTuple(_CM(IXYZ.X, Sign.PLUS), _CM(IXYZ.Z, Sign.MINUS), _CM(IXYZ.Y, Sign.PLUS)), + _CMTuple(_CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.Y, Sign.PLUS), _CM(IXYZ.X, Sign.MINUS)), + _CMTuple(_CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.X, Sign.MINUS), _CM(IXYZ.Z, Sign.MINUS)), + _CMTuple(_CM(IXYZ.Y, Sign.PLUS), _CM(IXYZ.X, Sign.PLUS), _CM(IXYZ.Z, Sign.MINUS)), + _CMTuple(_CM(IXYZ.Z, Sign.MINUS), _CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.X, Sign.MINUS)), + _CMTuple(_CM(IXYZ.Z, Sign.MINUS), _CM(IXYZ.Y, Sign.PLUS), _CM(IXYZ.X, Sign.PLUS)), + _CMTuple(_CM(IXYZ.X, Sign.MINUS), _CM(IXYZ.Z, Sign.MINUS), _CM(IXYZ.Y, Sign.MINUS)), + _CMTuple(_CM(IXYZ.X, Sign.MINUS), _CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.Y, Sign.PLUS)), + _CMTuple(_CM(IXYZ.X, Sign.PLUS), _CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.Y, Sign.MINUS)), + _CMTuple(_CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.X, Sign.PLUS), _CM(IXYZ.Y, Sign.PLUS)), + _CMTuple(_CM(IXYZ.Z, Sign.MINUS), _CM(IXYZ.X, Sign.PLUS), _CM(IXYZ.Y, Sign.MINUS)), + _CMTuple(_CM(IXYZ.Z, Sign.MINUS), _CM(IXYZ.X, Sign.MINUS), _CM(IXYZ.Y, Sign.PLUS)), + _CMTuple(_CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.X, Sign.MINUS), _CM(IXYZ.Y, Sign.MINUS)), + _CMTuple(_CM(IXYZ.Y, Sign.PLUS), _CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.X, Sign.PLUS)), + _CMTuple(_CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.Z, Sign.MINUS), _CM(IXYZ.X, Sign.PLUS)), + _CMTuple(_CM(IXYZ.Y, Sign.PLUS), _CM(IXYZ.Z, Sign.MINUS), _CM(IXYZ.X, Sign.MINUS)), + _CMTuple(_CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.X, Sign.MINUS)), ) # Decomposition of Clifford gates with H, S and Z. diff --git a/graphix/clifford.py b/graphix/clifford.py index 83a79024..6cd67b9d 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -18,7 +18,7 @@ CLIFFORD_MUL, CLIFFORD_TO_QASM3, ) -from graphix.fundamentals import IXYZ, ComplexUnit, Sign +from graphix.fundamentals import IXYZ, ComplexUnit from graphix.measurements import Domains from graphix.pauli import Pauli @@ -111,7 +111,7 @@ def measure(self, pauli: Pauli) -> Pauli: symbol, sign = table.z else: typing_extensions.assert_never(pauli.symbol) - return pauli.unit * Pauli(IXYZ[symbol], ComplexUnit.from_properties(sign=Sign(sign))) + return pauli.unit * Pauli(symbol, ComplexUnit.from_properties(sign=sign)) def commute_domains(self, domains: Domains) -> Domains: """ From e0b75bb0e5fae1d6170126949e30a078b0d4e490 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 00:34:58 +0900 Subject: [PATCH 28/61] :sparkles: Add try_from_matrix --- graphix/clifford.py | 28 ++++++++++++++++++++++++++-- tests/test_clifford.py | 12 +++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/graphix/clifford.py b/graphix/clifford.py index 6cd67b9d..9180115c 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -4,9 +4,11 @@ import copy import functools +import math from enum import Enum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any +import numpy as np import typing_extensions from graphix._db import ( @@ -23,7 +25,6 @@ from graphix.pauli import Pauli if TYPE_CHECKING: - import numpy as np import numpy.typing as npt @@ -69,6 +70,29 @@ def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix of the Clifford gate.""" return CLIFFORD[self.value] + @staticmethod + def try_from_matrix(mat: npt.NDArray[Any]) -> Clifford | None: + """Find the Clifford gate from the matrix. + + Return `None` if not found. + + Notes + ----- + Global phase is ignored. + """ + if mat.shape != (2, 2): + return None + for ci in Clifford: + mi = ci.matrix + for piv, piv_ in zip(mat.flat, mi.flat): + if math.isclose(abs(piv), 0): + continue + if math.isclose(abs(piv_), 0): + continue + if np.allclose(mat / piv, mi / piv_): + return ci + return None + def __repr__(self) -> str: """Return the Clifford expression on the form of HSZ decomposition.""" return " @ ".join([f"Clifford.{gate}" for gate in self.hsz]) diff --git a/tests/test_clifford.py b/tests/test_clifford.py index b8cf6962..d4d6ff26 100644 --- a/tests/test_clifford.py +++ b/tests/test_clifford.py @@ -1,9 +1,11 @@ from __future__ import annotations +import cmath import functools import itertools +import math import operator -from typing import Final +from typing import TYPE_CHECKING, Final import numpy as np import pytest @@ -12,6 +14,9 @@ from graphix.fundamentals import IXYZ, ComplexUnit, Sign from graphix.pauli import Pauli +if TYPE_CHECKING: + from numpy.random import Generator + _QASM3_DB: Final = { "id": Clifford.I, "x": Clifford.X, @@ -76,3 +81,8 @@ def test_measure(self, c: Clifford, p: Pauli) -> None: def test_qasm3(self, c: Clifford) -> None: cmul: Clifford = functools.reduce(operator.matmul, (_QASM3_DB[term] for term in reversed(c.qasm3))) assert cmul == c + + @pytest.mark.parametrize("c", Clifford) + def test_try_from_matrix(self, fx_rng: Generator, c: Clifford) -> None: + co = cmath.exp(2j * math.pi * fx_rng.uniform()) + assert Clifford.try_from_matrix(co * c.matrix) == c From 41f5f38a1da1608107db2718ea75915b24bdf18f Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 00:46:18 +0900 Subject: [PATCH 29/61] :white_check_mark: Update test --- tests/test_db.py | 79 ++++++++---------------------------------------- 1 file changed, 12 insertions(+), 67 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index f6202c83..4c5d8719 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -3,7 +3,6 @@ import itertools import numpy as np -import numpy.typing as npt import pytest from graphix._db import ( @@ -12,90 +11,36 @@ CLIFFORD_HSZ_DECOMPOSITION, CLIFFORD_MEASURE, CLIFFORD_MUL, - _CliffordMeasure, ) +from graphix.clifford import Clifford class TestCliffordDB: - @staticmethod - def classify_pauli(arr: npt.NDArray[np.complex128]) -> _CliffordMeasure: - """Compare the gate arr with Pauli gates and return the tuple of (Pauli string, sign). - - Parameters - ---------- - arr: np.array - 2x2 matrix. - - Returns - ------- - ind : _CliffordMeasure - """ - if np.allclose(CLIFFORD[1], arr): - return _CliffordMeasure("X", +1) - if np.allclose(-1 * CLIFFORD[1], arr): - return _CliffordMeasure("X", -1) - if np.allclose(CLIFFORD[2], arr): - return _CliffordMeasure("Y", +1) - if np.allclose(-1 * CLIFFORD[2], arr): - return _CliffordMeasure("Y", -1) - if np.allclose(CLIFFORD[3], arr): - return _CliffordMeasure("Z", +1) - if np.allclose(-1 * CLIFFORD[3], arr): - return _CliffordMeasure("Z", -1) - msg = "No Pauli found" - raise ValueError(msg) - - @staticmethod - def clifford_index(g: npt.NDArray[np.complex128]) -> int: - """Return the index of Clifford for a given 2x2 matrix. - - Compare the gate g with all Clifford gates (up to global phase) and return the matching index. - - Parameters - ---------- - g : 2x2 numpy array. - - Returns - ------- - i : index of Clifford gate - """ - for i in range(24): - ci = CLIFFORD[i] - # normalise global phase - norm = g[0, 1] / ci[0, 1] if ci[0, 0] == 0 else g[0, 0] / ci[0, 0] - # compare - if np.allclose(ci * norm, g): - return i - msg = "No Clifford found" - raise ValueError(msg) - @pytest.mark.parametrize(("i", "j"), itertools.product(range(24), range(3))) def test_measure(self, i: int, j: int) -> None: - conj = CLIFFORD[i].conjugate().T pauli = CLIFFORD[j + 1] - arr = np.matmul(np.matmul(conj, pauli), CLIFFORD[i]) - res = self.classify_pauli(arr) - assert res == CLIFFORD_MEASURE[i][j] + arr = CLIFFORD[i].conjugate().T @ pauli @ CLIFFORD[i] + sym, sgn = CLIFFORD_MEASURE[i][j] + arr_ = complex(sgn) * sym.matrix + assert np.allclose(arr, arr_) @pytest.mark.parametrize(("i", "j"), itertools.product(range(24), range(24))) def test_multiplication(self, i: int, j: int) -> None: - arr = np.matmul(CLIFFORD[i], CLIFFORD[j]) - assert CLIFFORD_MUL[i][j] == self.clifford_index(arr) + op = CLIFFORD[i] @ CLIFFORD[j] + assert Clifford.try_from_matrix(op) == Clifford(CLIFFORD_MUL[i][j]) @pytest.mark.parametrize("i", range(24)) def test_conjugation(self, i: int) -> None: - arr = CLIFFORD[i].conjugate().T - assert CLIFFORD_CONJ[i] == self.clifford_index(arr) + op = CLIFFORD[i].conjugate().T + assert Clifford.try_from_matrix(op) == Clifford(CLIFFORD_CONJ[i]) - @pytest.mark.parametrize("i", range(1, 24)) + @pytest.mark.parametrize("i", range(24)) def test_decomposition(self, i: int) -> None: op = np.eye(2, dtype=np.complex128) for j in CLIFFORD_HSZ_DECOMPOSITION[i]: - op = op @ CLIFFORD[j] - assert i == self.clifford_index(op) - + op @= CLIFFORD[j] + assert Clifford.try_from_matrix(op) == Clifford(i) -class TestDB: @pytest.mark.parametrize("i", range(24)) def test_safety(self, i: int) -> None: with pytest.raises(TypeError): From 98d5a225d907b7c8354bd0b123f526d6f42a649c Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 00:48:11 +0900 Subject: [PATCH 30/61] :wrench: Change file name --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51a7c68b..2f77d0bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ files = [ "graphix/pyzx.py", "graphix/rng.py", "graphix/states.py", - "graphix/type_utils.py", + "graphix/utils.py", "graphix/**/__init__.py", "graphix/_db.py", "noxfile.py", @@ -145,7 +145,7 @@ include = [ "graphix/pyzx.py", "graphix/rng.py", "graphix/states.py", - "graphix/type_utils.py", + "graphix/utils.py", "graphix/**/__init__.py", "graphix/_db.py", "noxfile.py", From 9fa414fec271ebf3d0de3e146d56db5428ab4bd0 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 00:56:15 +0900 Subject: [PATCH 31/61] :bulb: Update comments --- graphix/_db.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/graphix/_db.py b/graphix/_db.py index 405a0d53..3dd4f1d4 100644 --- a/graphix/_db.py +++ b/graphix/_db.py @@ -10,14 +10,14 @@ from graphix.fundamentals import IXYZ, Sign from graphix.ops import Ops -# 24 Unique 1-qubit Clifford gates -_C0 = Ops.I # identity +# 24 unique 1-qubit Clifford gates +_C0 = Ops.I # I _C1 = Ops.X # X _C2 = Ops.Y # Y _C3 = Ops.Z # Z _C4 = Ops.S # S = \sqrt{Z} -_C5 = Ops.SDG # S dagger -_C6 = Ops.H # Hadamard +_C5 = Ops.SDG # SDG = S^{\dagger} +_C6 = Ops.H # H _C7 = utils.lock(np.asarray([[1, -1j], [-1j, 1]]) / np.sqrt(2)) # \sqrt{iX} _C8 = utils.lock(np.asarray([[1, -1], [1, 1]]) / np.sqrt(2)) # \sqrt{iY} _C9 = utils.lock(np.asarray([[0, 1 - 1j], [-1 - 1j, 0]]) / np.sqrt(2)) # sqrt{I} @@ -37,7 +37,6 @@ _C23 = utils.lock(np.asarray([[-1 + 1j, 1 - 1j], [-1 - 1j, -1 - 1j]]) / 2) # I^(1/3) -# list of unique 1-qubit Clifford gates CLIFFORD = ( _C0, _C1, @@ -65,7 +64,7 @@ _C23, ) -# readable labels for the 1-qubit Clifford +# Human-readable labels CLIFFORD_LABEL = ( "I", "X", @@ -94,8 +93,7 @@ "I^{1/3}", ) -# Multiplying single-qubit Clifford gates result in a single-qubit Clifford gate. -# CLIFFORD_MUL provides the result of Clifford gate multiplications by Clifford index (see above). +# Clifford(CLIFFORD_MUL[i][j]) ~ CLIFFORD[i] @ CLIFFORD[j] (up to phase) CLIFFORD_MUL = ( (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23), (1, 0, 3, 2, 9, 10, 8, 15, 6, 4, 5, 12, 11, 14, 13, 7, 19, 18, 17, 16, 22, 23, 20, 21), @@ -123,17 +121,12 @@ (23, 22, 21, 20, 14, 15, 10, 8, 9, 7, 13, 5, 4, 6, 12, 11, 2, 3, 0, 1, 17, 16, 19, 18), ) -# Conjugation of Clifford gates result in a Clifford gate. -# CLIFFORD_CONJ provides the Clifford index of conjugated matrix. -# Example (S and S dagger): CLIFFORD_CONJ[4] = 5 -# WARNING: CLIFFORD[i].conj().T is not necessarily equal to -# CLIFFORD[CLIFFORD_CONJ[i]] in general: the phase may differ. -# For instance, CLIFFORD[7].conj().T = - CLIFFORD[CLIFFORD_CONJ[7]] +# Clifford(CLIFFORD_CONJ[i]) ~ CLIFFORD[i].H (up to phase) CLIFFORD_CONJ = (0, 1, 2, 3, 5, 4, 6, 15, 12, 9, 10, 11, 8, 13, 14, 7, 20, 22, 23, 21, 16, 19, 17, 18) class _CM(NamedTuple): - """NamedTuple just for documentation purposes.""" + """Pauli string and sign.""" pstr: IXYZ sign: Sign @@ -175,7 +168,7 @@ class _CMTuple(NamedTuple): _CMTuple(_CM(IXYZ.Y, Sign.MINUS), _CM(IXYZ.Z, Sign.PLUS), _CM(IXYZ.X, Sign.MINUS)), ) -# Decomposition of Clifford gates with H, S and Z. +# Decomposition of Clifford gates with H, S and Z (up to phase). CLIFFORD_HSZ_DECOMPOSITION = ( (0,), (6, 3, 6), From 065e4494a0a6196357b3d0ae6d88d08e7e4c1bdd Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 03:52:22 +0900 Subject: [PATCH 32/61] :children_crossing: Wrap by () --- graphix/clifford.py | 3 ++- tests/test_clifford.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/graphix/clifford.py b/graphix/clifford.py index 9180115c..35ed7041 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -95,7 +95,8 @@ def try_from_matrix(mat: npt.NDArray[Any]) -> Clifford | None: def __repr__(self) -> str: """Return the Clifford expression on the form of HSZ decomposition.""" - return " @ ".join([f"Clifford.{gate}" for gate in self.hsz]) + formula = " @ ".join([f"Clifford.{gate}" for gate in self.hsz]) + return f"({formula})" def __str__(self) -> str: """Return the name of the Clifford gate.""" diff --git a/tests/test_clifford.py b/tests/test_clifford.py index d4d6ff26..24948520 100644 --- a/tests/test_clifford.py +++ b/tests/test_clifford.py @@ -5,6 +5,7 @@ import itertools import math import operator +import re from typing import TYPE_CHECKING, Final import numpy as np @@ -44,7 +45,9 @@ def test_iteration(self) -> None: @pytest.mark.parametrize("c", Clifford) def test_repr(self, c: Clifford) -> None: - for term in repr(c).split(" @ "): + m = re.match(r"\((.*)\)", repr(c)) + assert m is not None + for term in m.group(1).split(" @ "): assert term in [ "Clifford.I", "Clifford.H", From f29920ae7736d3bd1a31e0157562a579ed68108a Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:12:15 +0900 Subject: [PATCH 33/61] :bulb: Add no cover pragma --- graphix/pauli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index c9b3c764..f47ee6d2 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -42,7 +42,7 @@ def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: return Pauli(IXYZ.Y, ComplexUnit.PLUS_J) if lr == (IXYZ.X, IXYZ.Z): return Pauli(IXYZ.Y, ComplexUnit.MINUS_J) - raise RuntimeError("Unreachable.") + raise RuntimeError("Unreachable.") # pragma: no cover @dataclasses.dataclass(frozen=True) From 9601baeae0511dc758c5313c9fb8f8d35a127e76 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:12:41 +0900 Subject: [PATCH 34/61] :white_check_mark: Add test --- tests/test_clifford.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_clifford.py b/tests/test_clifford.py index 24948520..ec551e04 100644 --- a/tests/test_clifford.py +++ b/tests/test_clifford.py @@ -89,3 +89,7 @@ def test_qasm3(self, c: Clifford) -> None: def test_try_from_matrix(self, fx_rng: Generator, c: Clifford) -> None: co = cmath.exp(2j * math.pi * fx_rng.uniform()) assert Clifford.try_from_matrix(co * c.matrix) == c + + def test_try_from_matrix_ng(self, fx_rng: Generator) -> None: + assert Clifford.try_from_matrix(np.zeros((2, 3))) is None + assert Clifford.try_from_matrix(fx_rng.normal(size=(2, 3))) is None From e29ce3be98b164d16a37b8876747efabb2c06d96 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:22:39 +0900 Subject: [PATCH 35/61] :recycle: Fix --- graphix/pauli.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index f47ee6d2..23d48be0 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -8,7 +8,7 @@ import typing_extensions -from graphix.fundamentals import IXYZ, Axis, ComplexUnit, Sign, SupportsComplexCtor +from graphix.fundamentals import IXYZ, Axis, ComplexUnit, SupportsComplexCtor from graphix.ops import Ops if TYPE_CHECKING: @@ -90,22 +90,22 @@ def matrix(self) -> npt.NDArray[np.complex128]: return co * Ops.Z typing_extensions.assert_never(self.symbol) - def eigenstate(self, eigenvalue: int | Sign = 0) -> PlanarState: + def eigenstate(self, b: int = 0) -> PlanarState: """Return the eigenstate of the Pauli.""" from graphix.states import BasicStates - if isinstance(eigenvalue, Sign): - # Normalize the eigenvalue - eigenvalue = 0 if eigenvalue == Sign.PLUS else 1 - + if b not in {0, 1}: + raise ValueError("b must be 0 or 1.") if self.symbol == IXYZ.X: - return BasicStates.PLUS if eigenvalue == 0 else BasicStates.MINUS + return BasicStates.PLUS if b == 0 else BasicStates.MINUS if self.symbol == IXYZ.Y: - return BasicStates.PLUS_I if eigenvalue == 0 else BasicStates.MINUS_I + return BasicStates.PLUS_I if b == 0 else BasicStates.MINUS_I if self.symbol == IXYZ.Z: - return BasicStates.ZERO if eigenvalue == 0 else BasicStates.ONE + return BasicStates.ZERO if b == 0 else BasicStates.ONE # Any state is eigenstate of the identity if self.symbol == IXYZ.I: + if b == 1: + raise ValueError("Unexpected eigenvalue for I.") return BasicStates.PLUS typing_extensions.assert_never(self.symbol) From dc5fcf4030d900bf44797144ffc34f008f3f32b0 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:22:46 +0900 Subject: [PATCH 36/61] :white_check_mark: Update tests --- tests/test_pauli.py | 16 +++++++++++++++- tests/test_statevec_backend.py | 8 +++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/test_pauli.py b/tests/test_pauli.py index 85c35ca1..0ad6d2c8 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from graphix.fundamentals import Axis, ComplexUnit +from graphix.fundamentals import Axis, ComplexUnit, Sign from graphix.pauli import Pauli @@ -75,3 +75,17 @@ def test_iterate_true(self) -> None: assert cmp[13] == 1j * Pauli.Z assert cmp[14] == -1 * Pauli.Z assert cmp[15] == -1j * Pauli.Z + + @pytest.mark.parametrize(("p", "b"), itertools.product(Pauli.iterate(include_unit=False), [0, 1])) + def test_eigenstate(self, p: Pauli, b: int) -> None: + if p == Pauli.I and b != 0: + pytest.skip("Invalid eigenstate for I.") + evec = p.eigenstate(b).get_statevector() + assert np.allclose(p.matrix @ evec, float(Sign.plus_if(b == 0)) * evec) + + def test_eigenstate_invalid(self) -> None: + with pytest.raises(ValueError): + _ = Pauli.I.eigenstate(1) + + with pytest.raises(ValueError): + _ = Pauli.I.eigenstate(2) diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index adbb89e3..b4fc696f 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -121,8 +121,8 @@ def test_deterministic_measure_one(self, fx_rng: Generator): coins = [fx_rng.choice([0, 1]), fx_rng.choice([0, 1])] expected_result = sum(coins) % 2 states = [ - Pauli.X.eigenstate(eigenvalue=coins[0]), - Pauli.Z.eigenstate(eigenvalue=coins[1]), + Pauli.X.eigenstate(coins[0]), + Pauli.Z.eigenstate(coins[1]), ] nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) @@ -193,9 +193,7 @@ def test_deterministic_measure_with_coin(self, fx_rng: Generator): n_neighbors = 10 coins = [fx_rng.choice([0, 1])] + [fx_rng.choice([0, 1]) for _ in range(n_neighbors)] expected_result = sum(coins) % 2 - states = [Pauli.X.eigenstate(eigenvalue=coins[0])] + [ - Pauli.Z.eigenstate(eigenvalue=coins[i + 1]) for i in range(n_neighbors) - ] + states = [Pauli.X.eigenstate(coins[0])] + [Pauli.Z.eigenstate(coins[i + 1]) for i in range(n_neighbors)] nodes = range(len(states)) backend.add_nodes(nodes=nodes, data=states) From da347a02fc03fed903fab4648b0293f804b76736 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:25:51 +0900 Subject: [PATCH 37/61] :recycle: Allow b = 1 for I --- graphix/pauli.py | 2 -- tests/test_pauli.py | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index 23d48be0..b5f85fb8 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -104,8 +104,6 @@ def eigenstate(self, b: int = 0) -> PlanarState: return BasicStates.ZERO if b == 0 else BasicStates.ONE # Any state is eigenstate of the identity if self.symbol == IXYZ.I: - if b == 1: - raise ValueError("Unexpected eigenvalue for I.") return BasicStates.PLUS typing_extensions.assert_never(self.symbol) diff --git a/tests/test_pauli.py b/tests/test_pauli.py index 0ad6d2c8..e7697c1e 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -78,14 +78,10 @@ def test_iterate_true(self) -> None: @pytest.mark.parametrize(("p", "b"), itertools.product(Pauli.iterate(include_unit=False), [0, 1])) def test_eigenstate(self, p: Pauli, b: int) -> None: - if p == Pauli.I and b != 0: - pytest.skip("Invalid eigenstate for I.") + ev = float(Sign.plus_if(b == 0)) if p != Pauli.I else 1 evec = p.eigenstate(b).get_statevector() - assert np.allclose(p.matrix @ evec, float(Sign.plus_if(b == 0)) * evec) + assert np.allclose(p.matrix @ evec, ev * evec) def test_eigenstate_invalid(self) -> None: - with pytest.raises(ValueError): - _ = Pauli.I.eigenstate(1) - with pytest.raises(ValueError): _ = Pauli.I.eigenstate(2) From 21ff8ef2d9e84461c89cbb629f374709032bcaf0 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:28:59 +0900 Subject: [PATCH 38/61] :white_check_mark: Change parameter --- tests/test_clifford.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_clifford.py b/tests/test_clifford.py index ec551e04..29a96de6 100644 --- a/tests/test_clifford.py +++ b/tests/test_clifford.py @@ -92,4 +92,4 @@ def test_try_from_matrix(self, fx_rng: Generator, c: Clifford) -> None: def test_try_from_matrix_ng(self, fx_rng: Generator) -> None: assert Clifford.try_from_matrix(np.zeros((2, 3))) is None - assert Clifford.try_from_matrix(fx_rng.normal(size=(2, 3))) is None + assert Clifford.try_from_matrix(fx_rng.normal(size=(2, 2))) is None From fc5f04a283292b6dd49712706d8d14f0042f71e0 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:33:48 +0900 Subject: [PATCH 39/61] :recycle: Move import to top level --- graphix/pauli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index b5f85fb8..2a8770f4 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -10,6 +10,7 @@ from graphix.fundamentals import IXYZ, Axis, ComplexUnit, SupportsComplexCtor from graphix.ops import Ops +from graphix.states import BasicStates if TYPE_CHECKING: from collections.abc import Iterator @@ -92,8 +93,6 @@ def matrix(self) -> npt.NDArray[np.complex128]: def eigenstate(self, b: int = 0) -> PlanarState: """Return the eigenstate of the Pauli.""" - from graphix.states import BasicStates - if b not in {0, 1}: raise ValueError("b must be 0 or 1.") if self.symbol == IXYZ.X: From 6ed4fd9ca5160fd6b5e0c00348538f267288630f Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:37:05 +0900 Subject: [PATCH 40/61] :bug: Change version range --- graphix/fundamentals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index b7d24c97..a64cc85e 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -19,7 +19,7 @@ import numpy.typing as npt -if sys.version_info >= (3, 9): +if sys.version_info >= (3, 10): SupportsComplexCtor = SupportsComplex | SupportsFloat | SupportsIndex | complex else: from typing import Union From 2d15149a7e6cd0cea538983ddc892182ea6d6ed7 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:39:41 +0900 Subject: [PATCH 41/61] :white_check_mark: Update tests --- graphix/fundamentals.py | 4 ++-- tests/test_fundamentals.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index a64cc85e..2a2058a6 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -21,7 +21,7 @@ if sys.version_info >= (3, 10): SupportsComplexCtor = SupportsComplex | SupportsFloat | SupportsIndex | complex -else: +else: # pragma: no cover from typing import Union SupportsComplexCtor = Union[SupportsComplex, SupportsFloat, SupportsIndex, complex] @@ -293,7 +293,7 @@ def polar(self, angle: float) -> tuple[float, float, float]: return (0, math.sin(angle), math.cos(angle)) if pp == (Axis.Z, Axis.X): return (math.sin(angle), 0, math.cos(angle)) - raise RuntimeError("Unreachable.") + raise RuntimeError("Unreachable.") # pragma: no cover @staticmethod def from_axes(a: Axis, b: Axis) -> Plane: diff --git a/tests/test_fundamentals.py b/tests/test_fundamentals.py index 4408e800..f8bbe3df 100644 --- a/tests/test_fundamentals.py +++ b/tests/test_fundamentals.py @@ -88,6 +88,7 @@ def test_try_from(self) -> None: assert ComplexUnit.try_from(1) == ComplexUnit.PLUS assert ComplexUnit.try_from(1.0) == ComplexUnit.PLUS assert ComplexUnit.try_from(1.0 + 0.0j) == ComplexUnit.PLUS + assert ComplexUnit.try_from(3) is None def test_from_properties(self) -> None: assert ComplexUnit.from_properties() == ComplexUnit.PLUS @@ -152,3 +153,11 @@ def test_from_axes(self) -> None: assert Plane.from_axes(Axis.Y, Axis.X) == Plane.XY assert Plane.from_axes(Axis.Z, Axis.Y) == Plane.YZ assert Plane.from_axes(Axis.Z, Axis.X) == Plane.XZ + + def test_from_axes_ng(self) -> None: + with pytest.raises(ValueError): + Plane.from_axes(Axis.X, Axis.X) + with pytest.raises(ValueError): + Plane.from_axes(Axis.Y, Axis.Y) + with pytest.raises(ValueError): + Plane.from_axes(Axis.Z, Axis.Z) From 7c7ff860d348ff334b6b2cd4ce98d125663eb5ab Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:46:10 +0900 Subject: [PATCH 42/61] :bug: Fix for older versions --- graphix/fundamentals.py | 2 +- tests/test_db.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index 2a2058a6..e1e5457a 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -92,7 +92,7 @@ def __rmul__(self, other: complex) -> complex: ... def __rmul__(self, other: complex) -> int | float | complex: """Multiply the sign with a number.""" - if isinstance(other, int | float | complex): + if isinstance(other, (int, float, complex)): return self.__mul__(other) return NotImplemented diff --git a/tests/test_db.py b/tests/test_db.py index 4c5d8719..9ea3df69 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -38,7 +38,7 @@ def test_conjugation(self, i: int) -> None: def test_decomposition(self, i: int) -> None: op = np.eye(2, dtype=np.complex128) for j in CLIFFORD_HSZ_DECOMPOSITION[i]: - op @= CLIFFORD[j] + op = op @ CLIFFORD[j] assert Clifford.try_from_matrix(op) == Clifford(i) @pytest.mark.parametrize("i", range(24)) From 04861fbfdb0a8caf62ad7be0ef62a9a4c88e9443 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:52:41 +0900 Subject: [PATCH 43/61] :bug: Remove invaild isinstance --- graphix/fundamentals.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index e1e5457a..52493161 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -182,9 +182,7 @@ def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> ComplexUnit: def __rmul__(self, other: SupportsComplexCtor) -> ComplexUnit: """Multiply the complex unit with a number.""" - if isinstance(other, SupportsComplexCtor): - return self.__mul__(other) - return NotImplemented + return self.__mul__(other) def __neg__(self) -> ComplexUnit: """Return the opposite of the complex unit.""" From b552772b8e0621738183408d5fcd575f76db16b4 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 20 Oct 2024 05:17:55 +0900 Subject: [PATCH 44/61] :memo: Update documentation --- docs/source/data.rst | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/source/data.rst b/docs/source/data.rst index 3360fd37..12cee4ae 100644 --- a/docs/source/data.rst +++ b/docs/source/data.rst @@ -27,14 +27,14 @@ This module defines standard data structure for pattern commands. .. autoclass:: MeasureUpdate -:mod:`graphix.pauli` module -+++++++++++++++++++++++++++ +:mod:`graphix.fundamentals` module +++++++++++++++++++++++++++++++++++ -This module defines standard data structure for Pauli operators, measurement planes and their transformations. +This module defines fundamental components of quantum computing. -.. automodule:: graphix.pauli +.. automodule:: graphix.fundamentals -.. currentmodule:: graphix.pauli +.. currentmodule:: graphix.fundamentals .. autoclass:: Axis :members: @@ -51,8 +51,16 @@ This module defines standard data structure for Pauli operators, measurement pla .. autoclass:: Plane :members: -.. autoclass:: Pauli +:mod:`graphix.pauli` module ++++++++++++++++++++++++++++ + +This module defines standard data structure for Pauli operators. + +.. automodule:: graphix.pauli +.. currentmodule:: graphix.pauli + +.. autoclass:: Pauli :mod:`graphix.instruction` module +++++++++++++++++++++++++++++++++ @@ -96,8 +104,3 @@ This module defines standard data structure for gate seqence (circuit model) use .. currentmodule:: graphix.states .. autoclass:: State - - - - - From b343bafcbce5ae76d3a212b28e109171dd909b85 Mon Sep 17 00:00:00 2001 From: "S.S." <66886825+EarlMilktea@users.noreply.github.com> Date: Thu, 24 Oct 2024 23:16:28 +0900 Subject: [PATCH 45/61] :memo: Update module docstring Co-authored-by: Shinichi Sunami <33350509+shinich1@users.noreply.github.com> --- graphix/measurements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix/measurements.py b/graphix/measurements.py index 40c68179..5536ad65 100644 --- a/graphix/measurements.py +++ b/graphix/measurements.py @@ -1,4 +1,4 @@ -"""MBQC measurements.""" +"""Data structure for single-qubit measurements in MBQC.""" from __future__ import annotations From 775a344662b6a8cb63a57a0c240ae3042f3cf119 Mon Sep 17 00:00:00 2001 From: "S.S." <66886825+EarlMilktea@users.noreply.github.com> Date: Sun, 27 Oct 2024 13:16:46 +0900 Subject: [PATCH 46/61] :memo: Update module docstring --- docs/source/data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/data.rst b/docs/source/data.rst index 12cee4ae..62c75fa7 100644 --- a/docs/source/data.rst +++ b/docs/source/data.rst @@ -30,7 +30,7 @@ This module defines standard data structure for pattern commands. :mod:`graphix.fundamentals` module ++++++++++++++++++++++++++++++++++ -This module defines fundamental components of quantum computing. +This module defines standard data structure for Pauli operators. .. automodule:: graphix.fundamentals From a8160e7f06645030b6f4a0b6effd0882ddbc6bd0 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:13:04 +0900 Subject: [PATCH 47/61] :children_crossing: Add __iter__ --- graphix/pauli.py | 8 +++++++- tests/test_pauli.py | 14 +++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index 2a8770f4..ef04a9e2 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -46,8 +46,14 @@ def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: raise RuntimeError("Unreachable.") # pragma: no cover +class _PauliMeta(type): + def __iter__(cls) -> Iterator[Pauli]: + """Iterate over all Pauli gates, including the unit.""" + return Pauli.iterate() + + @dataclasses.dataclass(frozen=True) -class Pauli: +class Pauli(metaclass=_PauliMeta): """Pauli gate: `u * {I, X, Y, Z}` where u is a complex unit. Pauli gates can be multiplied with other Pauli gates (with `@`), diff --git a/tests/test_pauli.py b/tests/test_pauli.py index e7697c1e..3f732a14 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -24,14 +24,14 @@ def test_axis(self) -> None: @pytest.mark.parametrize( ("u", "p"), - itertools.product(ComplexUnit, Pauli.iterate()), + itertools.product(ComplexUnit, Pauli), ) def test_unit_mul(self, u: ComplexUnit, p: Pauli) -> None: assert np.allclose((u * p).matrix, complex(u) * p.matrix) @pytest.mark.parametrize( ("a", "b"), - itertools.product(Pauli.iterate(), Pauli.iterate()), + itertools.product(Pauli, Pauli), ) def test_matmul(self, a: Pauli, b: Pauli) -> None: assert np.allclose((a @ b).matrix, a.matrix @ b.matrix) @@ -43,7 +43,7 @@ def test_str(self) -> None: assert str(-1 * Pauli.I) == "-IXYZ.I" assert str(-1j * Pauli.I) == "-1j * IXYZ.I" - @pytest.mark.parametrize("p", Pauli.iterate()) + @pytest.mark.parametrize("p", Pauli) def test_neg(self, p: Pauli) -> None: pneg = -p assert pneg == -p @@ -76,6 +76,14 @@ def test_iterate_true(self) -> None: assert cmp[14] == -1 * Pauli.Z assert cmp[15] == -1j * Pauli.Z + def test_iter_meta(self) -> None: + it = Pauli.iterate(include_unit=True) + it_ = iter(Pauli) + for p, p_ in zip(it, it_): + assert p == p_ + assert all(False for _ in it) + assert all(False for _ in it_) + @pytest.mark.parametrize(("p", "b"), itertools.product(Pauli.iterate(include_unit=False), [0, 1])) def test_eigenstate(self, p: Pauli, b: int) -> None: ev = float(Sign.plus_if(b == 0)) if p != Pauli.I else 1 From d3b995f073034a49b5763615417da1f8d6caa59e Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:25:22 +0900 Subject: [PATCH 48/61] :coffin: Remove dead duplicate --- tests/test_statevec_backend.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index b4fc696f..78df383d 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -81,8 +81,7 @@ def test_init_success(self, hadamardpattern, fx_rng: Generator) -> None: # random planar state rand_angle = fx_rng.random() * 2 * np.pi - rand_plane = fx_rng.choice(np.array([i for i in Plane])) - rand_plane = fx_rng.choice(np.array([i for i in Plane])) + rand_plane = fx_rng.choice(np.array(Plane)) state = PlanarState(rand_plane, rand_angle) backend = StatevectorBackend() backend.add_nodes(hadamardpattern.input_nodes, data=state) @@ -95,8 +94,7 @@ def test_init_success(self, hadamardpattern, fx_rng: Generator) -> None: def test_init_fail(self, hadamardpattern, fx_rng: Generator) -> None: rand_angle = fx_rng.random(2) * 2 * np.pi - rand_plane = fx_rng.choice(np.array([i for i in Plane]), 2) - rand_plane = fx_rng.choice(np.array([i for i in Plane]), 2) + rand_plane = fx_rng.choice(np.array(Plane), 2) state = PlanarState(rand_plane[0], rand_angle[0]) state2 = PlanarState(rand_plane[1], rand_angle[1]) From 6c708523306b5810709203ce73b94a4242732ee3 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:27:11 +0900 Subject: [PATCH 49/61] :recycle: Avoid abbrev. --- graphix/sim/base_backend.py | 6 +++--- graphix/sim/tensornet.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/graphix/sim/base_backend.py b/graphix/sim/base_backend.py index 5d11a961..2b7af5f4 100644 --- a/graphix/sim/base_backend.py +++ b/graphix/sim/base_backend.py @@ -187,16 +187,16 @@ def entangle_nodes(self, edge: tuple[int, int]) -> None: control = self.node_index.index(edge[1]) self.state.entangle((target, control)) - def measure(self, node: int, meas: Measurement) -> bool: + def measure(self, node: int, measurement: Measurement) -> bool: """Perform measurement of a node and trace out the qubit. Parameters ---------- node: int - meas: Measurement + measurement: Measurement """ loc = self.node_index.index(node) - result = perform_measure(loc, meas.plane, meas.angle, self.state, self.__rng, self.__pr_calc) + result = perform_measure(loc, measurement.plane, measurement.angle, self.state, self.__rng, self.__pr_calc) self.node_index.remove(node) self.state.remove_qubit(loc) return result diff --git a/graphix/sim/tensornet.py b/graphix/sim/tensornet.py index 3bc94fc3..8417bf09 100644 --- a/graphix/sim/tensornet.py +++ b/graphix/sim/tensornet.py @@ -144,7 +144,7 @@ def entangle_nodes(self, edge) -> None: elif self.graph_prep == "opt": pass - def measure(self, node: int, meas: Measurement) -> tuple[Backend, int]: + def measure(self, node: int, measurement: Measurement) -> tuple[Backend, int]: """Perform measurement of the node. In the context of tensornetwork, performing measurement equals to @@ -154,7 +154,7 @@ def measure(self, node: int, meas: Measurement) -> tuple[Backend, int]: ---------- node : int index of the node to measure - meas : Measurement + measurement : Measurement measure plane and angle """ if node in self._isolated_nodes: @@ -169,9 +169,9 @@ def measure(self, node: int, meas: Measurement) -> tuple[Backend, int]: result = self.__rng.choice([0, 1]) self.results[node] = result buffer = 2**0.5 - vec = PlanarState(meas.plane, meas.angle).get_statevector() + vec = PlanarState(measurement.plane, measurement.angle).get_statevector() if result: - vec = meas.plane.orth.matrix @ vec + vec = measurement.plane.orth.matrix @ vec proj_vec = vec * buffer self.state.measure_single(node, basis=proj_vec) return result From d19a3639323294e1e16ccec3f9602d5f22f9891a Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:39:41 +0900 Subject: [PATCH 50/61] :children_crossing: Improve __repr__ --- graphix/pauli.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index ef04a9e2..dd754acd 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -112,18 +112,28 @@ def eigenstate(self, b: int = 0) -> PlanarState: return BasicStates.PLUS typing_extensions.assert_never(self.symbol) - def __str__(self) -> str: - """Return a string representation of the Pauli (without module prefix).""" + def _repr_impl(self, prefix: str | None) -> str: + sym = self.symbol.name + if prefix is not None: + sym = f"{prefix}.{sym}" if self.unit == ComplexUnit.PLUS: - return str(self.symbol) + return sym if self.unit == ComplexUnit.MINUS: - return f"-{self.symbol}" + return f"-{sym}" if self.unit == ComplexUnit.PLUS_J: - return f"1j * {self.symbol}" + return f"1j * {sym}" if self.unit == ComplexUnit.MINUS_J: - return f"-1j * {self.symbol}" + return f"-1j * {sym}" typing_extensions.assert_never(self.unit) + def __repr__(self) -> str: + """Return a string representation of the Pauli.""" + return self._repr_impl(self.__class__.__name__) + + def __str__(self) -> str: + """Return a simplified string representation of the Pauli.""" + return self._repr_impl(None) + def __matmul__(self, other: Pauli) -> Pauli: """Return the product of two Paulis.""" if isinstance(other, Pauli): From 1aaf185343efc32efa559c9808b33ca58723897c Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:47:43 +0900 Subject: [PATCH 51/61] :white_check_mark: Update tests --- tests/test_pauli.py | 23 +++++++++++++++++------ tests/test_statevec_backend.py | 16 ++++++++-------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/tests/test_pauli.py b/tests/test_pauli.py index 3f732a14..fb74bafb 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -36,12 +36,23 @@ def test_unit_mul(self, u: ComplexUnit, p: Pauli) -> None: def test_matmul(self, a: Pauli, b: Pauli) -> None: assert np.allclose((a @ b).matrix, a.matrix @ b.matrix) - def test_str(self) -> None: - assert str(Pauli.I) == "IXYZ.I" - assert str(1 * Pauli.I) == "IXYZ.I" - assert str(1j * Pauli.I) == "1j * IXYZ.I" - assert str(-1 * Pauli.I) == "-IXYZ.I" - assert str(-1j * Pauli.I) == "-1j * IXYZ.I" + @pytest.mark.parametrize("p", Pauli.iterate(include_unit=False)) + def test_repr(self, p: Pauli) -> None: + pstr = f"Pauli.{p.symbol.name}" + assert repr(p) == pstr + assert repr(1 * p) == pstr + assert repr(1j * p) == f"1j * {pstr}" + assert repr(-1 * p) == f"-{pstr}" + assert repr(-1j * p) == f"-1j * {pstr}" + + @pytest.mark.parametrize("p", Pauli.iterate(include_unit=False)) + def test_str(self, p: Pauli) -> None: + pstr = p.symbol.name + assert str(p) == pstr + assert str(1 * p) == pstr + assert str(1j * p) == f"1j * {pstr}" + assert str(-1 * p) == f"-{pstr}" + assert str(-1j * p) == f"-1j * {pstr}" @pytest.mark.parametrize("p", Pauli) def test_neg(self, p: Pauli) -> None: diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index 78df383d..3a83ce20 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -126,9 +126,9 @@ def test_deterministic_measure_one(self, fx_rng: Generator): backend.add_nodes(nodes=nodes, data=states) backend.entangle_nodes(edge=(nodes[0], nodes[1])) - meas = Measurement(0, Plane.XY) + measurement = Measurement(0, Plane.XY) node_to_measure = backend.node_index[0] - result = backend.measure(node=node_to_measure, meas=meas) + result = backend.measure(node=node_to_measure, measurement=measurement) assert result == expected_result def test_deterministic_measure(self): @@ -143,9 +143,9 @@ def test_deterministic_measure(self): for i in range(1, n_neighbors + 1): backend.entangle_nodes(edge=(nodes[0], i)) - meas = Measurement(0, Plane.XY) + measurement = Measurement(0, Plane.XY) node_to_measure = backend.node_index[0] - result = backend.measure(node=node_to_measure, meas=meas) + result = backend.measure(node=node_to_measure, measurement=measurement) assert result == 0 assert list(backend.node_index) == list(range(1, n_neighbors + 1)) @@ -171,11 +171,11 @@ def test_deterministic_measure_many(self): backend.entangle_nodes(edge=(other, dummy)) # Same measurement for all traps - meas = Measurement(0, Plane.XY) + measurement = Measurement(0, Plane.XY) for trap in nodes[:n_traps]: node_to_measure = trap - result = backend.measure(node=node_to_measure, meas=meas) + result = backend.measure(node=node_to_measure, measurement=measurement) assert result == 0 assert list(backend.node_index) == list(range(n_traps, n_neighbors + n_traps + n_whatever)) @@ -197,8 +197,8 @@ def test_deterministic_measure_with_coin(self, fx_rng: Generator): for i in range(1, n_neighbors + 1): backend.entangle_nodes(edge=(nodes[0], i)) - meas = Measurement(0, Plane.XY) + measurement = Measurement(0, Plane.XY) node_to_measure = backend.node_index[0] - result = backend.measure(node=node_to_measure, meas=meas) + result = backend.measure(node=node_to_measure, measurement=measurement) assert result == expected_result assert list(backend.node_index) == list(range(1, n_neighbors + 1)) From a9b6469af64469145b3d4c2a0b08022a9ebf7ccc Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:57:31 +0900 Subject: [PATCH 52/61] :recycle: Update ComplexUnit --- graphix/fundamentals.py | 15 +++++++------ graphix/pauli.py | 16 ++++++------- tests/test_fundamentals.py | 46 +++++++++++++++++++------------------- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index 52493161..67d65833 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -118,10 +118,11 @@ class ComplexUnit(Enum): with Python constants 1, -1, 1j, -1j, and can be negated. """ - # HACK: Related to arg(z) - PLUS = 0 - PLUS_J = 1 - MINUS = 2 + # HACK: complex(u) == (1j) ** u.value for all u in ComplexUnit. + + ONE = 0 + J = 1 + MINUS_ONE = 2 MINUS_J = 3 @staticmethod @@ -134,11 +135,11 @@ def try_from(value: ComplexUnit | SupportsComplexCtor) -> ComplexUnit | None: except Exception: return None if value == 1: - return ComplexUnit.PLUS + return ComplexUnit.ONE if value == -1: - return ComplexUnit.MINUS + return ComplexUnit.MINUS_ONE if value == 1j: - return ComplexUnit.PLUS_J + return ComplexUnit.J if value == -1j: return ComplexUnit.MINUS_J return None diff --git a/graphix/pauli.py b/graphix/pauli.py index dd754acd..9ffb0911 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -32,15 +32,15 @@ def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: return Pauli() lr = (lhs, rhs) if lr == (IXYZ.X, IXYZ.Y): - return Pauli(IXYZ.Z, ComplexUnit.PLUS_J) + return Pauli(IXYZ.Z, ComplexUnit.J) if lr == (IXYZ.Y, IXYZ.X): return Pauli(IXYZ.Z, ComplexUnit.MINUS_J) if lr == (IXYZ.Y, IXYZ.Z): - return Pauli(IXYZ.X, ComplexUnit.PLUS_J) + return Pauli(IXYZ.X, ComplexUnit.J) if lr == (IXYZ.Z, IXYZ.Y): return Pauli(IXYZ.X, ComplexUnit.MINUS_J) if lr == (IXYZ.Z, IXYZ.X): - return Pauli(IXYZ.Y, ComplexUnit.PLUS_J) + return Pauli(IXYZ.Y, ComplexUnit.J) if lr == (IXYZ.X, IXYZ.Z): return Pauli(IXYZ.Y, ComplexUnit.MINUS_J) raise RuntimeError("Unreachable.") # pragma: no cover @@ -62,7 +62,7 @@ class Pauli(metaclass=_PauliMeta): """ symbol: IXYZ = IXYZ.I - unit: ComplexUnit = ComplexUnit.PLUS + unit: ComplexUnit = ComplexUnit.ONE I: ClassVar[Pauli] X: ClassVar[Pauli] Y: ClassVar[Pauli] @@ -116,11 +116,11 @@ def _repr_impl(self, prefix: str | None) -> str: sym = self.symbol.name if prefix is not None: sym = f"{prefix}.{sym}" - if self.unit == ComplexUnit.PLUS: + if self.unit == ComplexUnit.ONE: return sym - if self.unit == ComplexUnit.MINUS: + if self.unit == ComplexUnit.MINUS_ONE: return f"-{sym}" - if self.unit == ComplexUnit.PLUS_J: + if self.unit == ComplexUnit.J: return f"1j * {sym}" if self.unit == ComplexUnit.MINUS_J: return f"-1j * {sym}" @@ -162,7 +162,7 @@ def iterate(include_unit: bool = True) -> Iterator[Pauli]: ---------- include_unit (bool, optional): Include the unit in the iteration. Defaults to True. """ - us = tuple(ComplexUnit) if include_unit else (ComplexUnit.PLUS,) + us = tuple(ComplexUnit) if include_unit else (ComplexUnit.ONE,) for symbol in IXYZ: for unit in us: yield Pauli(symbol, unit) diff --git a/tests/test_fundamentals.py b/tests/test_fundamentals.py index f8bbe3df..a127a83e 100644 --- a/tests/test_fundamentals.py +++ b/tests/test_fundamentals.py @@ -84,16 +84,16 @@ def test_int(self) -> None: class TestComplexUnit: def test_try_from(self) -> None: - assert ComplexUnit.try_from(ComplexUnit.PLUS) == ComplexUnit.PLUS - assert ComplexUnit.try_from(1) == ComplexUnit.PLUS - assert ComplexUnit.try_from(1.0) == ComplexUnit.PLUS - assert ComplexUnit.try_from(1.0 + 0.0j) == ComplexUnit.PLUS + assert ComplexUnit.try_from(ComplexUnit.ONE) == ComplexUnit.ONE + assert ComplexUnit.try_from(1) == ComplexUnit.ONE + assert ComplexUnit.try_from(1.0) == ComplexUnit.ONE + assert ComplexUnit.try_from(1.0 + 0.0j) == ComplexUnit.ONE assert ComplexUnit.try_from(3) is None def test_from_properties(self) -> None: - assert ComplexUnit.from_properties() == ComplexUnit.PLUS - assert ComplexUnit.from_properties(is_imag=True) == ComplexUnit.PLUS_J - assert ComplexUnit.from_properties(sign=Sign.MINUS) == ComplexUnit.MINUS + assert ComplexUnit.from_properties() == ComplexUnit.ONE + assert ComplexUnit.from_properties(is_imag=True) == ComplexUnit.J + assert ComplexUnit.from_properties(sign=Sign.MINUS) == ComplexUnit.MINUS_ONE assert ComplexUnit.from_properties(sign=Sign.MINUS, is_imag=True) == ComplexUnit.MINUS_J @pytest.mark.parametrize(("sign", "is_imag"), itertools.product([Sign.PLUS, Sign.MINUS], [True, False])) @@ -102,15 +102,15 @@ def test_properties(self, sign: Sign, is_imag: bool) -> None: assert ComplexUnit.from_properties(sign=sign, is_imag=is_imag).is_imag == is_imag def test_complex(self) -> None: - assert complex(ComplexUnit.PLUS) == 1 - assert complex(ComplexUnit.PLUS_J) == 1j - assert complex(ComplexUnit.MINUS) == -1 + assert complex(ComplexUnit.ONE) == 1 + assert complex(ComplexUnit.J) == 1j + assert complex(ComplexUnit.MINUS_ONE) == -1 assert complex(ComplexUnit.MINUS_J) == -1j def test_str(self) -> None: - assert str(ComplexUnit.PLUS) == "1" - assert str(ComplexUnit.PLUS_J) == "1j" - assert str(ComplexUnit.MINUS) == "-1" + assert str(ComplexUnit.ONE) == "1" + assert str(ComplexUnit.J) == "1j" + assert str(ComplexUnit.MINUS_ONE) == "-1" assert str(ComplexUnit.MINUS_J) == "-1j" @pytest.mark.parametrize(("lhs", "rhs"), itertools.product(ComplexUnit, ComplexUnit)) @@ -118,18 +118,18 @@ def test_mul_self(self, lhs: ComplexUnit, rhs: ComplexUnit) -> None: assert complex(lhs * rhs) == complex(lhs) * complex(rhs) def test_mul_number(self) -> None: - assert ComplexUnit.PLUS * 1 == ComplexUnit.PLUS - assert 1 * ComplexUnit.PLUS == ComplexUnit.PLUS - assert ComplexUnit.PLUS * 1.0 == ComplexUnit.PLUS - assert 1.0 * ComplexUnit.PLUS == ComplexUnit.PLUS - assert ComplexUnit.PLUS * complex(1) == ComplexUnit.PLUS - assert complex(1) * ComplexUnit.PLUS == ComplexUnit.PLUS + assert ComplexUnit.ONE * 1 == ComplexUnit.ONE + assert 1 * ComplexUnit.ONE == ComplexUnit.ONE + assert ComplexUnit.ONE * 1.0 == ComplexUnit.ONE + assert 1.0 * ComplexUnit.ONE == ComplexUnit.ONE + assert ComplexUnit.ONE * complex(1) == ComplexUnit.ONE + assert complex(1) * ComplexUnit.ONE == ComplexUnit.ONE def test_neg(self) -> None: - assert -ComplexUnit.PLUS == ComplexUnit.MINUS - assert -ComplexUnit.PLUS_J == ComplexUnit.MINUS_J - assert -ComplexUnit.MINUS == ComplexUnit.PLUS - assert -ComplexUnit.MINUS_J == ComplexUnit.PLUS_J + assert -ComplexUnit.ONE == ComplexUnit.MINUS_ONE + assert -ComplexUnit.J == ComplexUnit.MINUS_J + assert -ComplexUnit.MINUS_ONE == ComplexUnit.ONE + assert -ComplexUnit.MINUS_J == ComplexUnit.J _PLANE_INDEX = {Axis.X: 0, Axis.Y: 1, Axis.Z: 2} From 26adaf71f614dd21a25ff86dbb579139c0f78103 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:03:09 +0900 Subject: [PATCH 53/61] :rewind: Use property --- graphix/clifford.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphix/clifford.py b/graphix/clifford.py index 35ed7041..20a0a8b8 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -3,7 +3,6 @@ from __future__ import annotations import copy -import functools import math from enum import Enum from typing import TYPE_CHECKING, Any @@ -65,7 +64,7 @@ class Clifford(Enum): _22 = 22 _23 = 23 - @functools.cached_property + @property def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix of the Clifford gate.""" return CLIFFORD[self.value] From 535c4f0cd82d23a52e66ab6ec63668ad733aa169 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:08:41 +0900 Subject: [PATCH 54/61] :children_crossing: Use longer name --- graphix/pauli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index 9ffb0911..61f66ec5 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -97,16 +97,16 @@ def matrix(self) -> npt.NDArray[np.complex128]: return co * Ops.Z typing_extensions.assert_never(self.symbol) - def eigenstate(self, b: int = 0) -> PlanarState: + def eigenstate(self, binary: int = 0) -> PlanarState: """Return the eigenstate of the Pauli.""" - if b not in {0, 1}: + if binary not in {0, 1}: raise ValueError("b must be 0 or 1.") if self.symbol == IXYZ.X: - return BasicStates.PLUS if b == 0 else BasicStates.MINUS + return BasicStates.PLUS if binary == 0 else BasicStates.MINUS if self.symbol == IXYZ.Y: - return BasicStates.PLUS_I if b == 0 else BasicStates.MINUS_I + return BasicStates.PLUS_I if binary == 0 else BasicStates.MINUS_I if self.symbol == IXYZ.Z: - return BasicStates.ZERO if b == 0 else BasicStates.ONE + return BasicStates.ZERO if binary == 0 else BasicStates.ONE # Any state is eigenstate of the identity if self.symbol == IXYZ.I: return BasicStates.PLUS From 611075d847b7ef914e7f01c7c392fccb1ee89751 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:19:44 +0900 Subject: [PATCH 55/61] :recycle: Move _matmul_impl inside class --- graphix/pauli.py | 51 +++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index 61f66ec5..386beb32 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -3,7 +3,6 @@ from __future__ import annotations import dataclasses -import functools from typing import TYPE_CHECKING, ClassVar import typing_extensions @@ -21,31 +20,6 @@ from graphix.states import PlanarState -@functools.lru_cache(maxsize=None) -def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: - """Return the product of two Paulis.""" - if lhs == IXYZ.I: - return Pauli(rhs) - if rhs == IXYZ.I: - return Pauli(lhs) - if lhs == rhs: - return Pauli() - lr = (lhs, rhs) - if lr == (IXYZ.X, IXYZ.Y): - return Pauli(IXYZ.Z, ComplexUnit.J) - if lr == (IXYZ.Y, IXYZ.X): - return Pauli(IXYZ.Z, ComplexUnit.MINUS_J) - if lr == (IXYZ.Y, IXYZ.Z): - return Pauli(IXYZ.X, ComplexUnit.J) - if lr == (IXYZ.Z, IXYZ.Y): - return Pauli(IXYZ.X, ComplexUnit.MINUS_J) - if lr == (IXYZ.Z, IXYZ.X): - return Pauli(IXYZ.Y, ComplexUnit.J) - if lr == (IXYZ.X, IXYZ.Z): - return Pauli(IXYZ.Y, ComplexUnit.MINUS_J) - raise RuntimeError("Unreachable.") # pragma: no cover - - class _PauliMeta(type): def __iter__(cls) -> Iterator[Pauli]: """Iterate over all Pauli gates, including the unit.""" @@ -134,10 +108,33 @@ def __str__(self) -> str: """Return a simplified string representation of the Pauli.""" return self._repr_impl(None) + @staticmethod + def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: + if lhs == IXYZ.I: + return Pauli(rhs) + if rhs == IXYZ.I: + return Pauli(lhs) + if lhs == rhs: + return Pauli() + lr = (lhs, rhs) + if lr == (IXYZ.X, IXYZ.Y): + return Pauli(IXYZ.Z, ComplexUnit.J) + if lr == (IXYZ.Y, IXYZ.X): + return Pauli(IXYZ.Z, ComplexUnit.MINUS_J) + if lr == (IXYZ.Y, IXYZ.Z): + return Pauli(IXYZ.X, ComplexUnit.J) + if lr == (IXYZ.Z, IXYZ.Y): + return Pauli(IXYZ.X, ComplexUnit.MINUS_J) + if lr == (IXYZ.Z, IXYZ.X): + return Pauli(IXYZ.Y, ComplexUnit.J) + if lr == (IXYZ.X, IXYZ.Z): + return Pauli(IXYZ.Y, ComplexUnit.MINUS_J) + raise RuntimeError("Unreachable.") # pragma: no cover + def __matmul__(self, other: Pauli) -> Pauli: """Return the product of two Paulis.""" if isinstance(other, Pauli): - return _matmul_impl(self.symbol, other.symbol) * (self.unit * other.unit) + return self._matmul_impl(self.symbol, other.symbol) * (self.unit * other.unit) return NotImplemented def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> Pauli: From 65de93955fa109b6fa95a415b68651b98120e92b Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:35:55 +0900 Subject: [PATCH 56/61] :recycle: Use NamedTuple --- graphix/measurements.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/graphix/measurements.py b/graphix/measurements.py index 5536ad65..89626387 100644 --- a/graphix/measurements.py +++ b/graphix/measurements.py @@ -4,6 +4,7 @@ import dataclasses import math +from typing import NamedTuple from graphix import utils from graphix.fundamentals import Axis, Plane, Sign @@ -17,8 +18,7 @@ class Domains: t_domain: set[int] -@dataclasses.dataclass(frozen=True) -class Measurement: +class Measurement(NamedTuple): """An MBQC measurement. :param angle: the angle of the measurement. Should be between [0, 2) @@ -45,8 +45,7 @@ def isclose(self, other: Measurement, rel_tol: float = 1e-09, abs_tol: float = 0 return math.isclose(self.angle, other.angle, rel_tol=rel_tol, abs_tol=abs_tol) and self.plane == other.plane -@dataclasses.dataclass(frozen=True) -class PauliMeasurement: +class PauliMeasurement(NamedTuple): """Pauli measurement.""" axis: Axis From 6bd1781de64376e28c7fed9f92f861b75084e388 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:37:44 +0900 Subject: [PATCH 57/61] :children_crossing: Omit () if singleton --- graphix/clifford.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphix/clifford.py b/graphix/clifford.py index 20a0a8b8..713e5e7a 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -95,6 +95,8 @@ def try_from_matrix(mat: npt.NDArray[Any]) -> Clifford | None: def __repr__(self) -> str: """Return the Clifford expression on the form of HSZ decomposition.""" formula = " @ ".join([f"Clifford.{gate}" for gate in self.hsz]) + if len(self.hsz) == 1: + return formula return f"({formula})" def __str__(self) -> str: From d7d6780714d6877e41c6b1aedd19233e4594e41b Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:47:22 +0900 Subject: [PATCH 58/61] :white_check_mark: Fix test --- tests/test_clifford.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_clifford.py b/tests/test_clifford.py index 29a96de6..d89c52ea 100644 --- a/tests/test_clifford.py +++ b/tests/test_clifford.py @@ -45,9 +45,10 @@ def test_iteration(self) -> None: @pytest.mark.parametrize("c", Clifford) def test_repr(self, c: Clifford) -> None: - m = re.match(r"\((.*)\)", repr(c)) - assert m is not None - for term in m.group(1).split(" @ "): + rep: str = repr(c) + m = re.match(r"\((.*)\)", rep) + rep = m.group(1) if m is not None else rep + for term in rep.split(" @ "): assert term in [ "Clifford.I", "Clifford.H", From c320ea977e0225769adb8f5e62d8a88e08ad49e3 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:01:42 +0900 Subject: [PATCH 59/61] :children_crossing: Rename arg --- graphix/pauli.py | 6 +++--- tests/test_pauli.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/graphix/pauli.py b/graphix/pauli.py index 386beb32..11af8915 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -152,14 +152,14 @@ def __neg__(self) -> Pauli: return dataclasses.replace(self, unit=-self.unit) @staticmethod - def iterate(include_unit: bool = True) -> Iterator[Pauli]: + def iterate(symbol_only: bool = False) -> Iterator[Pauli]: """Iterate over all Pauli gates. Parameters ---------- - include_unit (bool, optional): Include the unit in the iteration. Defaults to True. + symbol_only (bool, optional): Exclude the unit in the iteration. Defaults to False. """ - us = tuple(ComplexUnit) if include_unit else (ComplexUnit.ONE,) + us = (ComplexUnit.ONE,) if symbol_only else tuple(ComplexUnit) for symbol in IXYZ: for unit in us: yield Pauli(symbol, unit) diff --git a/tests/test_pauli.py b/tests/test_pauli.py index fb74bafb..13b22fc9 100644 --- a/tests/test_pauli.py +++ b/tests/test_pauli.py @@ -36,7 +36,7 @@ def test_unit_mul(self, u: ComplexUnit, p: Pauli) -> None: def test_matmul(self, a: Pauli, b: Pauli) -> None: assert np.allclose((a @ b).matrix, a.matrix @ b.matrix) - @pytest.mark.parametrize("p", Pauli.iterate(include_unit=False)) + @pytest.mark.parametrize("p", Pauli.iterate(symbol_only=True)) def test_repr(self, p: Pauli) -> None: pstr = f"Pauli.{p.symbol.name}" assert repr(p) == pstr @@ -45,7 +45,7 @@ def test_repr(self, p: Pauli) -> None: assert repr(-1 * p) == f"-{pstr}" assert repr(-1j * p) == f"-1j * {pstr}" - @pytest.mark.parametrize("p", Pauli.iterate(include_unit=False)) + @pytest.mark.parametrize("p", Pauli.iterate(symbol_only=True)) def test_str(self, p: Pauli) -> None: pstr = p.symbol.name assert str(p) == pstr @@ -59,16 +59,16 @@ def test_neg(self, p: Pauli) -> None: pneg = -p assert pneg == -p - def test_iterate_false(self) -> None: - cmp = list(Pauli.iterate(include_unit=False)) + def test_iterate_true(self) -> None: + cmp = list(Pauli.iterate(symbol_only=True)) assert len(cmp) == 4 assert cmp[0] == Pauli.I assert cmp[1] == Pauli.X assert cmp[2] == Pauli.Y assert cmp[3] == Pauli.Z - def test_iterate_true(self) -> None: - cmp = list(Pauli.iterate(include_unit=True)) + def test_iterate_false(self) -> None: + cmp = list(Pauli.iterate(symbol_only=False)) assert len(cmp) == 16 assert cmp[0] == Pauli.I assert cmp[1] == 1j * Pauli.I @@ -88,14 +88,14 @@ def test_iterate_true(self) -> None: assert cmp[15] == -1j * Pauli.Z def test_iter_meta(self) -> None: - it = Pauli.iterate(include_unit=True) + it = Pauli.iterate(symbol_only=False) it_ = iter(Pauli) for p, p_ in zip(it, it_): assert p == p_ assert all(False for _ in it) assert all(False for _ in it_) - @pytest.mark.parametrize(("p", "b"), itertools.product(Pauli.iterate(include_unit=False), [0, 1])) + @pytest.mark.parametrize(("p", "b"), itertools.product(Pauli.iterate(symbol_only=True), [0, 1])) def test_eigenstate(self, p: Pauli, b: int) -> None: ev = float(Sign.plus_if(b == 0)) if p != Pauli.I else 1 evec = p.eigenstate(b).get_statevector() From dbc596216c0db135093e2bb534dca449bff1858c Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Sat, 2 Nov 2024 01:30:47 +0900 Subject: [PATCH 60/61] :rewind: Revert annotation --- graphix/command.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/graphix/command.py b/graphix/command.py index a4045071..11c13c60 100644 --- a/graphix/command.py +++ b/graphix/command.py @@ -149,11 +149,9 @@ class MeasureUpdate: add_term: float @staticmethod - def compute(plane: Plane, s: int, t: int, clifford_gate: Clifford) -> MeasureUpdate: + def compute(plane: Plane, s: bool, t: bool, clifford_gate: Clifford) -> MeasureUpdate: """Compute the update for a given plane, signals and vertex operator.""" gates = list(map(Pauli.from_axis, plane.axes)) - s %= 2 - t %= 2 if s: clifford_gate = Clifford.X @ clifford_gate if t: From b40b7eafb4e74a0fac424deb96a778f2ce5d0cdb Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:23:35 +0900 Subject: [PATCH 61/61] :recycle: Use property --- graphix/fundamentals.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index 67d65833..1fecf0e1 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -3,7 +3,6 @@ from __future__ import annotations import enum -import functools import math import sys import typing @@ -198,7 +197,7 @@ class IXYZ(Enum): Y = enum.auto() Z = enum.auto() - @functools.cached_property + @property def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix representation.""" if self == IXYZ.I: @@ -219,7 +218,7 @@ class Axis(Enum): Y = enum.auto() Z = enum.auto() - @functools.cached_property + @property def matrix(self) -> npt.NDArray[np.complex128]: """Return the matrix representation.""" if self == Axis.X: