Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
- name: Setup requirements
run: pip install -r requirements.txt -r requirements-dev.txt

- name: Run pytest
run: pytest

- name: Run ruff-check
run: ruff check

Expand All @@ -34,6 +37,3 @@ jobs:

- name: Run mypy
run: mypy .

- name: Run pytest
run: pytest
3 changes: 3 additions & 0 deletions .gitignore

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe remove the . vscode that is local?

Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
*~
*.egg-info/
__pycache__/
.coverage
.vscode/
.mypy_cache/
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ universality of the gate set consisting of 𝔍(α) and ∧Z. This package
implements that transpilation mehtod in a straightforward and
principled way.

The package allows for transpilation of circuits with the following steps:

1. Convert circuit, defined by gates available in Graphix, to a set of J and ∧z gates.

2. Construct an open graph from the J and ∧z gates.

3. Find a flow for the open graph and convert it into a pattern.

Compared to the existing transpilation procedure in Graphix, this
implementation is more naive but also more transparent:

Expand Down
36 changes: 34 additions & 2 deletions graphix_jcz_transpiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,38 @@
Copyright (C) 2025, QAT team (ENS-PSL, Inria, CNRS).
"""

from graphix_jcz_transpiler.jcz_transpiler import transpile_jcz
from graphix_jcz_transpiler.jcz_transpiler import (
CZ,
InternalInstructionError,
J,
JCZInstructionKind,
circuit_to_open_graph,
decompose_ccx,
decompose_rx,
decompose_ry,
decompose_rz,
decompose_rzz,
decompose_swap,
decompose_y,
j_commands,
transpile_jcz,
transpile_jcz_open_graph,
)

__all__ = ["transpile_jcz"]
__all__ = [
"CZ",
"InternalInstructionError",
"J",
"JCZInstructionKind",
"circuit_to_open_graph",
"decompose_ccx",
"decompose_rx",
"decompose_ry",
"decompose_rz",
"decompose_rzz",
"decompose_swap",
"decompose_y",
"j_commands",
"transpile_jcz",
"transpile_jcz_open_graph",
]
149 changes: 128 additions & 21 deletions graphix_jcz_transpiler/jcz_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@
from math import pi
from typing import TYPE_CHECKING, ClassVar, Literal

import networkx as nx
from graphix import Pattern, command, instruction
from graphix.fundamentals import Plane
from graphix.instruction import InstructionKind
from graphix.measurements import Measurement
from graphix.opengraph import OpenGraph
from graphix.transpiler import Circuit, TranspileResult
from typing_extensions import TypeAlias, assert_never

if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Sequence

from graphix.parameters import ExpressionOrFloat
from graphix.parameter import ExpressionOrFloat


class JCZInstructionKind(Enum):
Expand Down Expand Up @@ -124,15 +128,17 @@ def decompose_rzz(instr: instruction.RZZ) -> list[instruction.CNOT | instruction

"""
return [
instruction.CNOT(control=instr.control, target=instr.target),
instruction.CNOT(target=instr.target, control=instr.control),
instruction.RZ(instr.target, instr.angle),
instruction.CNOT(control=instr.control, target=instr.target),
instruction.CNOT(target=instr.target, control=instr.control),
]


def decompose_cnot(instr: instruction.CNOT) -> list[instruction.H | CZ]:
"""Return a decomposition of the CNOT gate as H·∧z·H.

Vincent Danos, Elham Kashefi, Prakash Panangaden, The Measurement Calculus, 2007.

Args:
----
instr: the CNOT instruction to decompose.
Expand All @@ -152,6 +158,11 @@ def decompose_cnot(instr: instruction.CNOT) -> list[instruction.H | CZ]:
def decompose_swap(instr: instruction.SWAP) -> list[instruction.CNOT]:
"""Return a decomposition of the SWAP gate as CNOT(0, 1)·CNOT(1, 0)·CNOT(0, 1).

Michael A. Nielsen and Isaac L. Chuang,
Quantum Computation and Quantum Information,
Cambridge University Press, 2000
(p. 23 in the 10th Anniversary Edition).

Args:
----
instr: the SWAP instruction to decompose.
Expand All @@ -168,7 +179,7 @@ def decompose_swap(instr: instruction.SWAP) -> list[instruction.CNOT]:
]


def decompose_y(instr: instruction.Y) -> Iterable[instruction.X | instruction.Z]:
def decompose_y(instr: instruction.Y) -> list[instruction.X | instruction.Z]:
"""Return a decomposition of the Y gate as X·Z.

Args:
Expand All @@ -180,7 +191,7 @@ def decompose_y(instr: instruction.Y) -> Iterable[instruction.X | instruction.Z]
the decomposition.

"""
return reversed([instruction.X(instr.target), instruction.Z(instr.target)])
return list(reversed([instruction.X(instr.target), instruction.Z(instr.target)]))


def decompose_rx(instr: instruction.RX) -> list[J]:
Expand Down Expand Up @@ -220,7 +231,7 @@ def decompose_ry(instr: instruction.RY) -> list[J]:
return [J(target=instr.target, angle=angle) for angle in reversed((0, pi / 2, instr.angle, -pi / 2))]


def decompose_rz(instr: instruction.RZ) -> Iterable[J]:
def decompose_rz(instr: instruction.RZ) -> list[J]:
"""Return a J decomposition of the RZ gate.

The Rz(α) gate is decomposed into H·J(α) (that is to say, J(0)·J(α)).
Expand All @@ -238,7 +249,7 @@ def decompose_rz(instr: instruction.RZ) -> Iterable[J]:
return [J(target=instr.target, angle=angle) for angle in reversed((0, instr.angle))]


def instruction_to_jcz(instr: JCZInstruction) -> Iterable[J | CZ]:
def instruction_to_jcz(instr: JCZInstruction) -> Sequence[J | CZ]:
"""Return a J-∧z decomposition of the instruction.

Args:
Expand Down Expand Up @@ -282,7 +293,7 @@ def instruction_to_jcz(instr: JCZInstruction) -> Iterable[J | CZ]:
assert_never(instr.kind)


def instruction_list_to_jcz(instrs: Iterable[JCZInstruction]) -> list[J | CZ]:
def instruction_list_to_jcz(instrs: Sequence[JCZInstruction]) -> list[J | CZ]:
"""Return a J-∧z decomposition of the sequence of instructions.

Args:
Expand All @@ -297,14 +308,22 @@ def instruction_list_to_jcz(instrs: Iterable[JCZInstruction]) -> list[J | CZ]:
return [jcz_instr for instr in instrs for jcz_instr in instruction_to_jcz(instr)]


class IllformedPatternError(Exception):
class IllformedCircuitError(Exception):
"""Raised if the circuit is ill-formed."""

def __init__(self) -> None:
"""Build the exception."""
super().__init__("Ill-formed pattern")


class CircuitWithMeasurementError(Exception):
"""Raised if the circuit contains measurements."""

def __init__(self) -> None:
"""Build the exception."""
super().__init__("Circuits containing measurements are not supported by the transpiler.")


class InternalInstructionError(Exception):
"""Raised if the circuit contains internal _XC or _ZC instructions."""

Expand All @@ -313,6 +332,29 @@ def __init__(self, instr: instruction.Instruction) -> None:
super().__init__(f"Internal instruction: {instr}")


def j_commands(current_node: int, next_node: int, angle: ExpressionOrFloat) -> list[command.Command]:
"""Return the MBQC pattern commands for a J gate.

Args:
----
current_node: the current node.
next_node: the next node.
angle: the angle of the J gate.
domain: the domain the X correction is based on.

Returns:
-------
the MBQC pattern commands for a J gate as a list

"""
return [
command.N(node=next_node),
command.E(nodes=(current_node, next_node)),
command.M(node=current_node, angle=(angle / pi) + 0.0), # Avoids -0.0
command.X(node=next_node, domain={current_node}),
]


def transpile_jcz(circuit: Circuit) -> TranspileResult:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment for line 374: why do we expect the circuit to contain _XCor _ZC instructions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't, but unfortunately the instructions _XC and _ZC exist in graphix (they are used internally by the original transpiler, so I hope they will disappear eventually).

"""Transpile a circuit via a J-∧z decomposition.

Expand All @@ -326,8 +368,8 @@ def transpile_jcz(circuit: Circuit) -> TranspileResult:

Raises:
------
IllformedPatternError: if the pattern is ill-formed.
InternalInstructionError: if the circuit contains internal _XC or _ZC instructions.
IllformedCircuitError: if the circuit has underdefined instructions.

"""
indices: list[int | None] = list(range(circuit.width))
Expand All @@ -347,26 +389,91 @@ def transpile_jcz(circuit: Circuit) -> TranspileResult:
if instr_jcz.kind == JCZInstructionKind.J:
target = indices[instr_jcz.target]
if target is None:
raise IllformedPatternError
raise IllformedCircuitError
ancilla = n_nodes
n_nodes += 1
pattern.extend(
[
command.N(node=ancilla),
command.E(nodes=(target, ancilla)),
command.M(node=target, angle=-instr_jcz.angle / pi),
command.X(node=ancilla, domain={target}),
],
)
pattern.extend(j_commands(target, ancilla, -instr_jcz.angle))
indices[instr_jcz.target] = ancilla
continue
if instr_jcz.kind == JCZInstructionKind.CZ:
t0, t1 = instr_jcz.targets
i0, i1 = indices[t0], indices[t1]
if i0 is None or i1 is None:
raise IllformedPatternError
raise IllformedCircuitError
pattern.extend([command.E(nodes=(i0, i1))])
continue
assert_never(instr_jcz.kind)
pattern.reorder_output_nodes([i for i in indices if i is not None])
return TranspileResult(pattern, tuple(classical_outputs))


def circuit_to_open_graph(circuit: Circuit) -> OpenGraph[Measurement]:
"""Transpile a circuit via a J-∧z-like decomposition to an open graph.

Args:
----
circuit: the circuit to transpile.

Returns:
-------
the result of the transpilation: an open graph.

Raises:
------
IllformedCircuitError: if the pattern is ill-formed (operation on already measured node)
InternalInstructionError: if the circuit contains internal _XC or _ZC instructions.
CircuitWithMeasurementError: if the circuit contains measurements.

"""
indices: list[int | None] = list(range(circuit.width))
n_nodes = circuit.width
measurements: dict[int, Measurement] = {}
inputs = list(range(n_nodes))
graph: nx.Graph[int] = nx.Graph() # type: ignore[name-defined,attr-defined]
graph.add_nodes_from(inputs)
for instr in circuit.instruction:
if instr.kind == InstructionKind.M:
raise CircuitWithMeasurementError
# Use == for mypy
if instr.kind == InstructionKind._XC or instr.kind == InstructionKind._ZC: # noqa: PLR1714, SLF001
raise InternalInstructionError(instr)
for instr_jcz in instruction_to_jcz(instr):
if instr_jcz.kind == JCZInstructionKind.J:
target = indices[instr_jcz.target]
if target is None:
raise IllformedCircuitError
ancilla = n_nodes
n_nodes += 1
graph.add_node(ancilla)
graph.add_edge(target, ancilla)
measurements[target] = Measurement(-instr_jcz.angle / pi, plane=Plane.XY)
indices[instr_jcz.target] = ancilla
continue
if instr_jcz.kind == JCZInstructionKind.CZ:
t0, t1 = instr_jcz.targets
i0, i1 = indices[t0], indices[t1]
if i0 is None or i1 is None:
raise IllformedCircuitError
graph.add_edge(i0, i1)
continue
assert_never(instr_jcz.kind)
outputs = [i for i in indices if i is not None]
return OpenGraph(graph=graph, input_nodes=inputs, output_nodes=outputs, measurements=measurements)


def transpile_jcz_open_graph(circuit: Circuit) -> TranspileResult:
"""Transpile a circuit via a J-∧z-like decomposition to a pattern.

Currently fails due to overuse of memory in conversion from open graph to pattern, assumed in the causal flow step.

Args:
----
circuit: the circuit to transpile.

Returns:
-------
the result of the transpilation: a pattern.

"""
og = circuit_to_open_graph(circuit)
return TranspileResult(og.to_pattern(), tuple(og.measurements.keys()))
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
name = "graphix_jcz_transpiler"
version = "1"

[tool.setuptools]
packages = ["graphix_jcz_transpiler"]
[tool.setuptools.packages.find]
include = ["graphix_jcz_transpiler"]

[tool.ruff]
line-length = 120
Expand Down
Loading