Source code for graphix.clifford
"""24 Unique single-qubit Clifford gates and their multiplications, conjugations and Pauli conjugations."""
from __future__ import annotations
import copy
import math
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Any
import networkx as nx
import numpy as np
import typing_extensions
from graphix._db import (
CLIFFORD,
CLIFFORD_CONJ,
CLIFFORD_HSZ_DECOMPOSITION,
CLIFFORD_MEASURE,
CLIFFORD_MUL,
CLIFFORD_PAULI_DECOMPOSITION,
CLIFFORD_TO_QASM3,
)
from graphix.fundamentals import Axis, ComplexUnit, I
from graphix.pauli import Pauli
if TYPE_CHECKING:
import numpy.typing as npt
from graphix import OpenGraph, PauliMeasurement
from graphix.pretty_print import OutputFormat
[docs]
@dataclass
class Domains:
"""Represent :math:`X^sZ^t` where s and t are XOR of results from given sets of indices."""
s_domain: set[int]
t_domain: set[int]
[docs]
class Clifford(Enum):
"""Clifford gate."""
# MEMO: Cannot use ClassVar here
I: Clifford
X: Clifford
Y: Clifford
Z: Clifford
S: Clifford
SDG: Clifford
H: Clifford
_0 = 0
_1 = 1
_2 = 2
_3 = 3
_4 = 4
_5 = 5
_6 = 6
_7 = 7
_8 = 8
_9 = 9
_10 = 10
_11 = 11
_12 = 12
_13 = 13
_14 = 14
_15 = 15
_16 = 16
_17 = 17
_18 = 18
_19 = 19
_20 = 20
_21 = 21
_22 = 22
_23 = 23
@property
def matrix(self) -> npt.NDArray[np.complex128]:
"""Return the matrix of the Clifford gate."""
return CLIFFORD[self.value]
[docs]
@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, strict=True):
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."""
formula = " @ ".join([f"Clifford.{gate}" for gate in self.hsz])
if len(self.hsz) == 1:
return formula
return f"({formula})"
def __str__(self) -> str:
"""Return a string representation of a Clifford in the default output format.
Example
-------
>>> from graphix import Clifford
>>> print(Clifford.SDG @ Clifford.H @ Clifford.SDG @ Clifford.Y)
S H Sdg
"""
return self.draw()
[docs]
def draw(self, output: OutputFormat | None = None) -> str:
"""Return a string representation of a Clifford.
Parameters
----------
clifford : Clifford
The statevector to format.
output : OutputFormat | None
Desired formatting style (``ASCII``, ``LaTeX`` or ``Unicode``).
Returns
-------
str
The formatted Clifford.
"""
from graphix.pretty_print import clifford_to_str # noqa: PLC0415
return clifford_to_str(self, output)
@property
def conj(self) -> Clifford:
"""Return the conjugate of the Clifford gate."""
return Clifford(CLIFFORD_CONJ[self.value])
@property
def hsz(self) -> list[Clifford]:
"""Return a decomposition of the Clifford gate with the gates ``H``, ``S``, ``Z``."""
return [Clifford(i) for i in CLIFFORD_HSZ_DECOMPOSITION[self.value]]
@property
def qasm3(self) -> tuple[str, ...]:
"""Return a decomposition of the Clifford gate as qasm3 gates."""
return CLIFFORD_TO_QASM3[self.value]
def __matmul__(self, other: Clifford) -> Clifford:
"""Multiplication within the Clifford group (modulo unit factor)."""
if isinstance(other, Clifford):
return Clifford(CLIFFORD_MUL[self.value][other.value])
return NotImplemented
[docs]
def measure_axis(self, axis: Axis) -> Pauli:
"""Compute C† P C with P the Pauli +axis."""
table = CLIFFORD_MEASURE[self.value]
match axis:
case Axis.X:
symbol, sign = table.x
case Axis.Y:
symbol, sign = table.y
case Axis.Z:
symbol, sign = table.z
case _:
typing_extensions.assert_never(axis)
return Pauli(symbol, ComplexUnit.from_properties(sign=sign))
[docs]
def measure(self, pauli: Pauli) -> Pauli:
"""Compute C† P C."""
if pauli.symbol == I:
return copy.deepcopy(pauli)
new_pauli = self.measure_axis(pauli.symbol)
return pauli.unit * new_pauli
[docs]
def commute_domains(self, domains: Domains) -> Domains:
r"""
Commute :math:`X^sZ^t` with :math:`C`.
Given :math:`X^sZ^t`, return :math:`X^{s'}Z^{t'}` such that :math:`X^sZ^tC = CX^{s'}Z^{t'}`.
Note that applying the method to :meth:`conj` computes the reverse commutation:
indeed, :math:`C^\dag X^s Z^t = (X^s Z^t C)^\dag = (C X^{s'} Z^{t'})^\dag = X^{s'}Z^{t'}C^\dag`.
"""
s_domain = domains.s_domain.copy()
t_domain = domains.t_domain.copy()
for gate in self.hsz:
match gate:
case Clifford.I:
pass
case Clifford.H:
t_domain, s_domain = s_domain, t_domain
case Clifford.S:
t_domain ^= s_domain
case Clifford.Z:
pass
case _: # pragma: no cover
raise RuntimeError(f"{gate} should be either I, H, S or Z.")
return Domains(s_domain, t_domain)
[docs]
def to_opengraph(self) -> OpenGraph[PauliMeasurement]:
"""Return a local-Clifford-free open graph equivalent to the Clifford gate."""
from graphix import OpenGraph # noqa: PLC0415
decomposition = CLIFFORD_PAULI_DECOMPOSITION[self.value]
n = len(decomposition)
return OpenGraph(
graph=nx.path_graph(n + 1),
input_nodes=[0],
output_nodes=[n],
measurements=dict(enumerate(decomposition)),
)
Clifford.I = Clifford(0)
Clifford.X = Clifford(1)
Clifford.Y = Clifford(2)
Clifford.Z = Clifford(3)
Clifford.S = Clifford(4)
Clifford.SDG = Clifford(5)
Clifford.H = Clifford(6)