Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/qasm-parser-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
graphix-qasm-parser @ git+https://github.com/TeamGraphix/graphix-qasm-parser@refs/pull/16/head
6 changes: 5 additions & 1 deletion .github/workflows/cov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ jobs:
python-version: ${{ env.python-version }}

- name: Run pytest
run: uv run --extra dev pytest --cov=./graphix --cov-report=xml --cov-report=term --doctest-modules
run: |
uv run --with-requirements .github/qasm-parser-requirements.txt \
--extra dev \
pytest --cov=./graphix --cov-report=xml --cov-report=term \
--doctest-modules

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- #591: `FixedBranchSelector` now passes its RNG parameter to its `default` branch selector.

- #595: `RZZ` gates are no longer incorrectly exported as `crz`; transpilation to `CNOT`-`RZ`-`CNOT` is provided.

- #596, #597: Pattern's `n_node` property updated after Pauli removal.

## [0.4] - 2026-08-18
Expand Down
7 changes: 3 additions & 4 deletions graphix/qasm3_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def circuit_to_qasm3_lines(circuit: Circuit, *, transpile: bool = True) -> Itera
An iterator over the OpenQASM 3.0 lines that represent the circuit.
"""
if transpile:
circuit = circuit.transpile_j_to_rzh().transpile_measurements_to_z_axis()
circuit = circuit.transpile_rzz().transpile_j_to_rzh().transpile_measurements_to_z_axis()
yield "OPENQASM 3;"
yield 'include "stdgates.inc";'
yield f"qubit[{circuit.width}] q;"
Expand Down Expand Up @@ -138,9 +138,8 @@ def instruction_to_qasm3(instruction: InstructionType) -> str:
case InstructionKind.CZ:
return qasm3_gate_call("cz", [qasm3_qubit(instruction.targets[i]) for i in (0, 1)])
case InstructionKind.RZZ:
angle = angle_to_qasm3(instruction.angle)
return qasm3_gate_call(
"crz", args=[angle], operands=[qasm3_qubit(instruction.control), qasm3_qubit(instruction.target)]
raise ValueError(
"RZZ gates must be decomposed before QASM3 export using `Circuit.transpile_rzz`, or setting `transpile=True`."
)
case InstructionKind.CCX:
return qasm3_gate_call(
Expand Down
11 changes: 11 additions & 0 deletions graphix/transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,17 @@ def transpile_j_to_rzh(self) -> Circuit:
new_circuit.add(instr)
return new_circuit

def transpile_rzz(self) -> Circuit:
"""Return an equivalent circuit where all RZZ gates have been replaced with OpenQASM gates."""
new_circuit = Circuit(self.width)
for instr in self.instruction:
match instr.kind:
case InstructionKind.RZZ:
new_circuit.extend(decompose_rzz(instr))
case _:
new_circuit.add(instr)
return new_circuit


def decompose_rzz(instr: instruction.RZZ) -> Iterator[instruction.CNOT | instruction.RZ]:
"""Yield a decomposition of RZZ(α) gate as CNOT(control, target)·Rz(target, α)·CNOT(control, target).
Expand Down
14 changes: 8 additions & 6 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def tests_all(session: Session) -> None:
"""Run the test suite with all dependencies."""
session.install(".[dev]")
# This dependency is added here to avoid circular dependencies
session.install("graphix-qasm-parser>=0.1.1")
session.install("-r", ".github/qasm-parser-requirements.txt")
run_pytest(session, doctest_modules=True, mpl=True)


Expand Down Expand Up @@ -97,7 +97,7 @@ class ReverseDependency:
[
ReverseDependency("https://github.com/thierry-martinez/graphix-symbolic", branch="in-place_methods"),
ReverseDependency("https://github.com/thierry-martinez/graphix-stim-backend", branch="rename-simulate"),
ReverseDependency("https://github.com/TeamGraphix/graphix-qasm-parser"),
ReverseDependency("https://github.com/TeamGraphix/graphix-qasm-parser", branch="refs/pull/16/head"),
ReverseDependency(
"https://github.com/thierry-martinez/graphix-ibmq", doctest_modules=False, branch="rename-simulate"
),
Expand Down Expand Up @@ -127,11 +127,13 @@ def tests_reverse_dependencies(session: Session, package: ReverseDependency) ->
session.install("nox")
with TemporaryDirectory() as tmpdir:
with session.cd(tmpdir):
if package.branch is None:
session.run("git", "clone", package.repository, external=True)
else:
session.run("git", "clone", "-b", package.branch, package.repository, external=True)
session.run("git", "clone", package.repository, external=True)
with session.cd(dirname):
if package.branch is not None:
# Use `git fetch` instead of `git clone -b` to support
# special refs such as `refs/pull/N/head`
session.run("git", "fetch", "origin", package.branch, external=True)
session.run("git", "checkout", "--detach", "FETCH_HEAD", external=True)
# graphix installation fails without constraint on numba
session.install(package.install_target, "numba>=0.65.1")
# Note that `session.cd` is used as a context manager above,
Expand Down
39 changes: 17 additions & 22 deletions tests/test_qasm3_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pytest
from numpy.random import PCG64, Generator

from graphix import Circuit, instruction
from graphix import Circuit
from graphix.fundamentals import ANGLE_PI, Axis
from graphix.qasm3_exporter import angle_to_qasm3, circuit_to_qasm3, pattern_to_qasm3
from graphix.random_objects import rand_circuit
Expand All @@ -23,23 +23,6 @@ def test_angle_to_qasm3(check: tuple[float, str]) -> None:
assert angle_to_qasm3(angle) == expected


def test_measurement() -> None:
# Measurements are not supported yet by the parser.
# https://github.com/TeamGraphix/graphix-qasm-parser/issues/3
# The best we can do is to check if the measurement instruction
# is exported as expected.
circuit = Circuit(1, instr=[instruction.M(target=0, axis=Axis.Z)])
qasm = circuit_to_qasm3(circuit)
assert (
qasm
== """OPENQASM 3;
include "stdgates.inc";
qubit[1] q;
bit[1] b;
b[0] = measure q[0];"""
)


@pytest.mark.parametrize("jumps", range(1, 11))
def test_to_qasm3_random_circuit(fx_bg: PCG64, jumps: int) -> None:
"""Check the export to OpenQASM 3 without validating the result.
Expand All @@ -61,13 +44,25 @@ def test_to_qasm3_random_circuit(fx_bg: PCG64, jumps: int) -> None:
_qasm3 = pattern_to_qasm3(pattern)


def test_to_qasm3_failures() -> None:
circuit = Circuit(2)
def test_to_qasm3_measure_on_x_axis() -> None:
circuit = Circuit(1)
circuit.m(0, Axis.X)
with pytest.raises(ValueError, match="OpenQASM3 only supports measurements on Z axis"):
circuit_to_qasm3(circuit, transpile=False)
circuit = circuit.transpile_measurements_to_z_axis()
circuit.j(1, 0.25)
_qasm3 = circuit_to_qasm3(circuit)


def test_to_qasm3_j() -> None:
circuit = Circuit(1)
circuit.j(0, 0.25)
with pytest.raises(ValueError, match="J gates must be decomposed before QASM3 export"):
circuit_to_qasm3(circuit, transpile=False)
_qasm3 = circuit_to_qasm3(circuit)


def test_to_qasm3_rzz() -> None:
circuit = Circuit(2)
circuit.rzz(0, 1, 0.25)
with pytest.raises(ValueError, match="RZZ gates must be decomposed before QASM3 export"):
circuit_to_qasm3(circuit, transpile=False)
_qasm3 = circuit_to_qasm3(circuit)
9 changes: 7 additions & 2 deletions tests/test_qasm3_exporter_to_graphix_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from numpy.random import PCG64, Generator

from graphix import Circuit, Instruction
from graphix.fundamentals import ANGLE_PI
from graphix.fundamentals import ANGLE_PI, Axis
from graphix.instruction import InstructionKind
from graphix.qasm3_exporter import circuit_to_qasm3
from graphix.random_objects import rand_circuit
Expand Down Expand Up @@ -50,7 +50,7 @@ def test_circuit_to_qasm3(fx_bg: PCG64, jumps: int) -> None:

@pytest.mark.parametrize("instruction", ALL_INSTRUCTIONS)
def test_instruction_to_qasm3(instruction: InstructionType) -> None:
if instruction.kind == InstructionKind.M:
if instruction.kind in {InstructionKind.RZZ, InstructionKind.M}:
pytest.skip()
check_round_trip(Circuit(3, instr=[instruction]))

Expand All @@ -67,3 +67,8 @@ def test_j_to_qasm3_failure() -> None:
circuit = Circuit(3, instr=[Instruction.J(target=0, angle=ANGLE_PI / 4)])
with pytest.raises(ValueError):
circuit_to_qasm3(circuit, transpile=False)


def test_measurement() -> None:
circuit = Circuit(1, instr=[Instruction.M(target=0, axis=Axis.Z)])
check_round_trip(circuit)
Loading