From 6cc5ca25ad8b4ee41ab8d6b34c72f57ca91476c1 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Fri, 31 Jul 2026 12:05:17 +0200 Subject: [PATCH] Fix pretty-printing of Clifford gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements `clifford_to_str`, which relies on the QASM3 decomposition (into `I`, `X`, `Y`, `Z`, `S`, `S†`, and `H`), since it is more compact than the HSZ decomposition. `CLIFFORD_LABEL` is removed from `_db` as names were ambiguous. The QASM3 decomposition has been made optimal and is now tested. All members of the `Clifford` class now appear in the documentation and docstrings are fixed. --- docs/source/clifford.rst | 13 +----- graphix/_db.py | 91 ++++++++++++++-------------------------- graphix/clifford.py | 46 +++++++++++++++----- graphix/pretty_print.py | 52 +++++++++++++++++++++++ tests/test_db.py | 49 ++++++++++++++++++++++ 5 files changed, 169 insertions(+), 82 deletions(-) diff --git a/docs/source/clifford.rst b/docs/source/clifford.rst index f769e7e8e..6c6bac47a 100644 --- a/docs/source/clifford.rst +++ b/docs/source/clifford.rst @@ -10,15 +10,4 @@ Miscellaneous modules .. currentmodule:: graphix.clifford .. autoclass:: graphix.clifford.Clifford - -.. data:: graphix.cliffford.CLIFFORD - - list of 24 unique single-qubit Clifford operators as numpy array. - -.. data:: graphix.clifford.CLIFFORD_MUL - - the matrix multiplication of single-qubit Clifford gates, expressed as a mapping of CLIFFORD indices. This is possible because multiplication of two Clifford gates result in a Clifford gate. - -.. data:: graphix.clifford.CLIFFORD_MEASURE - - The mapping of Pauli operators under conjugation by single-qubit Clifford gates, expressed as a mapping into Pauli operator indices (in CLIFFORD) and sign. + :members: diff --git a/graphix/_db.py b/graphix/_db.py index 5d7346430..621011591 100644 --- a/graphix/_db.py +++ b/graphix/_db.py @@ -16,26 +16,26 @@ _C1 = Ops.X # X _C2 = Ops.Y # Y _C3 = Ops.Z # Z -_C4 = Ops.S # S = \sqrt{Z} -_C5 = Ops.SDG # SDG = S^{\dagger} +_C4 = Ops.S # S +_C5 = Ops.SDG # S† _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} -_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) +_C7 = utils.lock(np.asarray([[1, -1j], [-1j, 1]]) / np.sqrt(2)) # S† H S† +_C8 = utils.lock(np.asarray([[1, -1], [1, 1]]) / np.sqrt(2)) # H Z +_C9 = utils.lock(np.asarray([[0, 1 - 1j], [-1 - 1j, 0]]) / np.sqrt(2)) # S† X +_C10 = utils.lock(np.asarray([[0, -1 - 1j], [1 - 1j, 0]]) / np.sqrt(2)) # S X +_C11 = utils.lock(np.asarray([[1, -1], [-1, -1]]) / np.sqrt(2)) # H Y +_C12 = utils.lock(np.asarray([[-1, -1], [1, -1]]) / np.sqrt(2)) # H X +_C13 = utils.lock(np.asarray([[1j, -1], [1, -1j]]) / np.sqrt(2)) # S† H S +_C14 = utils.lock(np.asarray([[1j, 1], [-1, -1j]]) / np.sqrt(2)) # S H S† +_C15 = utils.lock(np.asarray([[-1, -1j], [-1j, -1]]) / np.sqrt(2)) # S H S +_C16 = utils.lock(np.asarray([[-1 + 1j, 1 + 1j], [-1 + 1j, -1 - 1j]]) / 2) # H S† +_C17 = utils.lock(np.asarray([[-1 + 1j, -1 - 1j], [1 - 1j, -1 - 1j]]) / 2) # H S† X +_C18 = utils.lock(np.asarray([[1 + 1j, 1 - 1j], [-1 - 1j, 1 - 1j]]) / 2) # H S X +_C19 = utils.lock(np.asarray([[-1 - 1j, 1 - 1j], [-1 - 1j, -1 + 1j]]) / 2) # H S +_C20 = utils.lock(np.asarray([[-1 - 1j, -1 - 1j], [1 - 1j, -1 + 1j]]) / 2) # S H +_C21 = utils.lock(np.asarray([[-1 + 1j, -1 + 1j], [1 + 1j, -1 - 1j]]) / 2) # S† H +_C22 = utils.lock(np.asarray([[1 + 1j, -1 - 1j], [1 - 1j, 1 - 1j]]) / 2) # S H Y +_C23 = utils.lock(np.asarray([[-1 + 1j, 1 - 1j], [-1 - 1j, -1 - 1j]]) / 2) # S† H Y CLIFFORD = ( @@ -65,35 +65,6 @@ _C23, ) -# Human-readable labels -CLIFFORD_LABEL = ( - "I", - "X", - "Y", - "Z", - "S", - "Sdagger", - "H", - r"\sqrt{iX}", - r"\sqrt{iY}", - r"\sqrt{I}", - r"\sqrt{-I}", - r"\sqrt{I}", - r"\sqrt{-iY}", - r"\sqrt{-I}", - r"\sqrt{-I}", - r"\sqrt{-iX}", - "I^{1/3}", - "I^{1/3}", - "I^{1/3}", - "I^{1/3}", - "I^{1/3}", - "I^{1/3}", - "I^{1/3}", - "I^{1/3}", - "I^{1/3}", -) - # 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), @@ -210,22 +181,22 @@ class _CMTuple(NamedTuple): ("sdg",), ("h",), ("sdg", "h", "sdg"), - ("h", "x"), - ("sdg", "y"), - ("sdg", "x"), - ("h", "y"), - ("h", "z"), - ("sdg", "h", "sdg", "y"), + ("z", "h"), + ("x", "sdg"), + ("x", "s"), + ("y", "h"), + ("x", "h"), + ("s", "h", "sdg"), ("sdg", "h", "s"), - ("sdg", "h", "sdg", "x"), + ("s", "h", "s"), ("sdg", "h"), - ("sdg", "h", "y"), - ("sdg", "h", "z"), - ("sdg", "h", "x"), + ("x", "sdg", "h"), + ("x", "s", "h"), + ("s", "h"), ("h", "s"), ("h", "sdg"), - ("h", "x", "sdg"), - ("h", "x", "s"), + ("y", "h", "s"), + ("y", "h", "sdg"), ) CLIFFORD_PAULI_DECOMPOSITION = ( diff --git a/graphix/clifford.py b/graphix/clifford.py index ec5b93fc2..ae0fd45b7 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -16,7 +16,6 @@ CLIFFORD, CLIFFORD_CONJ, CLIFFORD_HSZ_DECOMPOSITION, - CLIFFORD_LABEL, CLIFFORD_MEASURE, CLIFFORD_MUL, CLIFFORD_PAULI_DECOMPOSITION, @@ -29,11 +28,12 @@ import numpy.typing as npt from graphix import OpenGraph, PauliMeasurement + from graphix.pretty_print import OutputFormat @dataclass class Domains: - """Represent `X^sZ^t` where s and t are XOR of results from given sets of indices.""" + """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] @@ -112,8 +112,34 @@ def __repr__(self) -> str: return f"({formula})" def __str__(self) -> str: - """Return the name of the Clifford gate.""" - return CLIFFORD_LABEL[self.value] + """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() + + 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: @@ -122,7 +148,7 @@ def conj(self) -> Clifford: @property def hsz(self) -> list[Clifford]: - """Return a decomposition of the Clifford gate with the gates `H`, `S`, `Z`.""" + """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 @@ -158,13 +184,13 @@ def measure(self, pauli: Pauli) -> Pauli: return pauli.unit * new_pauli def commute_domains(self, domains: Domains) -> Domains: - """ - Commute `X^sZ^t` with `C`. + r""" + Commute :math:`X^sZ^t` with :math:`C`. - Given `X^sZ^t`, return `X^s'Z^t'` such that `X^sZ^tC = CX^s'Z^t'`. + 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 `self.conj` computes the reverse commutation: - indeed, `C†X^sZ^t = (X^sZ^tC)† = (CX^s'Z^t')† = X^s'Z^t'C†`. + 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() diff --git a/graphix/pretty_print.py b/graphix/pretty_print.py index 5623602d7..67cc699a5 100644 --- a/graphix/pretty_print.py +++ b/graphix/pretty_print.py @@ -15,6 +15,7 @@ from typing_extensions import assert_never from graphix import command +from graphix._db import CLIFFORD_TO_QASM3 from graphix.fundamentals import AbstractMeasurement, Axis, Plane, Sign, angle_to_rad, rad_to_angle from graphix.measurements import BlochMeasurement, PauliMeasurement from graphix.parameter import AffineExpression @@ -23,6 +24,7 @@ from collections.abc import Container, Iterable, Mapping, Sequence from collections.abc import Set as AbstractSet + from graphix import Clifford from graphix.command import Node from graphix.flow.core import PauliFlow, XZCorrections from graphix.fundamentals import Angle @@ -996,3 +998,53 @@ def density_matrix_to_str( widths = [max(len(row[col]) for row in rows) for col in range(len(rows[0]))] if rows else [] lines = [" ".join(entry.rjust(widths[col]) for col, entry in enumerate(row)) for row in rows] return "\n".join(f"[ {line} ]" for line in lines) + + +QASM3_INSTR_TO_STR = { + OutputFormat.ASCII: { + "id": "I", + "x": "X", + "y": "Y", + "z": "Z", + "s": "S", + "sdg": "Sdg", + "h": "H", + }, + OutputFormat.Unicode: { + "id": "I", + "x": "X", + "y": "Y", + "z": "Z", + "s": "S", + "sdg": "S†", + "h": "H", + }, + OutputFormat.LaTeX: { + "id": "I", + "x": "X", + "y": "Y", + "z": "Z", + "s": "S", + "sdg": r"S^\dag", + "h": "H", + }, +} + + +def clifford_to_str(clifford: Clifford, output: OutputFormat | None = None) -> str: + r"""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. + """ + output = ensure_output_format(output) + return " ".join(QASM3_INSTR_TO_STR[output][instr] for instr in reversed(CLIFFORD_TO_QASM3[clifford.value])) diff --git a/tests/test_db.py b/tests/test_db.py index 5b983b351..56802388d 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,6 +1,8 @@ from __future__ import annotations import itertools +from functools import reduce +from operator import matmul from typing import TYPE_CHECKING import networkx as nx @@ -15,6 +17,7 @@ CLIFFORD_MEASURE, CLIFFORD_MUL, CLIFFORD_PAULI_DECOMPOSITION, + CLIFFORD_TO_QASM3, ) from graphix.clifford import Clifford from graphix.opengraph import OpenGraphError @@ -131,3 +134,49 @@ def unwrap(v: T | None) -> T: def test_generate_clifford_pauli_decomposition(fx_rng: Generator) -> None: clifford_pauli_decomposition = generate_clifford_pauli_decomposition(fx_rng) assert clifford_pauli_decomposition == CLIFFORD_PAULI_DECOMPOSITION + + +QASM3_BASIS = { + Clifford.I: "id", + Clifford.X: "x", + Clifford.Y: "y", + Clifford.Z: "z", + Clifford.S: "s", + Clifford.SDG: "sdg", + Clifford.H: "h", +} + +QASM3_TO_CLIFFORD = {instr: clifford for clifford, instr in QASM3_BASIS.items()} + + +def test_clifford_to_qasm3() -> None: + for clifford, decomposition in zip(Clifford, CLIFFORD_TO_QASM3, strict=True): + clifford2 = reduce(matmul, (QASM3_TO_CLIFFORD[instr] for instr in reversed(decomposition))) + assert clifford == clifford2 + + +def generate_qasm3_decomposition() -> tuple[tuple[str, ...], ...]: + decompositions: list[tuple[str, ...] | None] = [None] * len(Clifford) + new_decompositions: dict[Clifford, tuple[str, ...]] = { + clifford: (instr,) for clifford, instr in QASM3_BASIS.items() + } + while new_decompositions: + current_decompositions = new_decompositions + new_decompositions = {} + for clifford, decomposition in current_decompositions.items(): + decompositions[clifford.value] = decomposition + for clifford, decomposition in current_decompositions.items(): + for instr_clifford, instr in QASM3_BASIS.items(): + result = instr_clifford @ clifford + if decompositions[result.value] is not None: + continue + result_decomposition = (*decomposition, instr) + decompositions[result.value] = result_decomposition + new_decompositions[result] = result_decomposition + assert all(decomposition is not None for decomposition in decompositions) + return tuple(map(unwrap, decompositions)) + + +def test_clifford_to_qasm3_optimal() -> None: + qasm3_decomposition = generate_qasm3_decomposition() + assert qasm3_decomposition == CLIFFORD_TO_QASM3