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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Types of changes:
### Removed

### Fixed
- Fixed Clifford+T rebasing for exact `rx`, `ry`, and `rz` rotations at multiples of π/4. These gates now decompose instead of disappearing, while angles outside the exact basis raise `RebaseError` instead of producing an incorrect result. ([#428](https://github.com/qBraid/pyqasm/issues/428))

### Dependencies

Expand Down
56 changes: 49 additions & 7 deletions src/pyqasm/decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
Definition of the Decomposer class
"""

import math

import openqasm3.ast as qasm3_ast
from openqasm3.ast import BranchingStatement, QuantumGate

Expand Down Expand Up @@ -55,20 +57,60 @@ def process_gate_statement(cls, gate_name, statement, target_basis_set):
decomposition_rules, statement, gate_name
)
elif gate_name in {"rx", "ry", "rz"}:
# Approximate parameterized gates using Solovay-Kitaev
# Example -
# approx_gates = solovay_kitaev_algo(
# gate_name, statement.arguments[0].value, accuracy=0.01
# )
# return approx_gates
pass
processed_gates_list = cls._get_clifford_t_rotation(statement, gate_name)
else:
# Raise an error if the gate is not supported in the target basis set
error = f"Gate '{gate_name}' is not supported in the '{target_basis_set} set'."
raise RebaseError(error)

return processed_gates_list

@classmethod
def _get_clifford_t_rotation(cls, statement, gate_name):
"""Return an exact Clifford+T decomposition for a rotation gate.

Args:
statement: The rotation gate statement to decompose.
gate_name: The rotation gate name.

Returns:
list: The exact Clifford+T gate sequence.

Raises:
RebaseError: If the rotation is not an integer multiple of pi/4.
"""
angle = statement.arguments[0].value
quarter_turns = angle / (math.pi / 4)
rounded_quarter_turns = round(quarter_turns)

if not math.isclose(quarter_turns, rounded_quarter_turns, rel_tol=0.0, abs_tol=1e-10):
raise RebaseError(
f"Gate '{gate_name}' with angle {angle} cannot be represented exactly in the "
"Clifford+T basis."
)

normalized_turns = rounded_quarter_turns % 8
if normalized_turns == 0:
return []

if normalized_turns <= 4:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

N1 — the two-gate branches here are never exercised.

I instrumented _get_clifford_t_rotation and ran the suite: normalized_turns only ever takes 0, 1, 2 and 7. That leaves 3, 4, 5 and 6 untested — including both composite sequences, n=3 -> s; t; and n=5 -> sdg; tdg;, which is exactly where an off-by-one in the // 2 / % 2 split would hide. Every currently tested case is single-gate or empty. Could you add rz(3*pi/4) and rz(5*pi/4)?

Separately, the tests assert gate strings rather than equivalence — swap s for sdg in the ry sequence and the suite still passes once the expected string is edited to match. One test that reconstructs the unitary and compares it to the exact rotation up to global phase would guard the whole table at once, and numpy is already a dependency. I verified the table is correct today (99/99 over n = -16..16, plus crx/cry through the controlled path), so this is about keeping it that way rather than anything being wrong now.

phase_gates = ["s"] * (normalized_turns // 2)
phase_gates += ["t"] * (normalized_turns % 2)
else:
inverse_turns = 8 - normalized_turns
phase_gates = ["sdg"] * (inverse_turns // 2)
phase_gates += ["tdg"] * (inverse_turns % 2)

if gate_name == "rx":
gate_sequence = ["h", *phase_gates, "h"]
elif gate_name == "ry":
gate_sequence = ["sdg", "h", *phase_gates, "h", "s"]
else:
gate_sequence = phase_gates

rules = {gate_name: [{"gate": gate} for gate in gate_sequence]}
return cls._get_decomposed_gates(rules, statement, gate_name)

@classmethod
def process_branching_statement(cls, branching_statement, target_basis_set):
"""Process the branching statement based on the target basis set.
Expand Down
29 changes: 29 additions & 0 deletions tests/qasm2/test_rotation_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,39 @@

"""

from pyqasm.elements import BasisSet
from pyqasm.entrypoint import dumps, loads
from tests.utils import check_unrolled_qasm


def test_rebase_clifford_t_exact_rotation():
"""Test rebasing an exact rotation to the Clifford+T basis."""
qasm_in = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[1];
creg c[1];
ry(-pi/4) q[0];
measure q[0] -> c[0];
"""
expected_out = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[1];
creg c[1];
sdg q[0];
h q[0];
tdg q[0];
h q[0];
s q[0];
measure q[0] -> c[0];
"""

result = loads(qasm_in)
result.rebase(BasisSet.CLIFFORD_T)
check_unrolled_qasm(dumps(result), expected_out)


def test_convert_qasm_one_param():
"""Test converting qasm string from one-parameter gate"""

Expand Down
132 changes: 131 additions & 1 deletion tests/qasm3/test_rebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@

"""

import numpy as np
import pytest

from pyqasm.elements import BasisSet
from pyqasm.entrypoint import dumps, loads
from tests.utils import check_single_qubit_gate_op, check_unrolled_qasm
from pyqasm.exceptions import RebaseError
from tests.utils import (
assert_unitary_equal,
check_single_qubit_gate_op,
check_unrolled_qasm,
unitary_from_unrolled_ast,
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -170,6 +177,129 @@ def test_rebase_clifford_t(input_gates, decomposed_gates):
check_unrolled_qasm(dumps(result), expected_qasm)


@pytest.mark.parametrize(
"input_gate, decomposed_gates",
[
("rz(pi/4) q[0];", "t q[0];"),
("rz(-pi/4) q[0];", "tdg q[0];"),
("rz(pi/2) q[0];", "s q[0];"),
("rz(3*pi/4) q[0];", "s q[0];\nt q[0];"),
("rz(5*pi/4) q[0];", "sdg q[0];\ntdg q[0];"),
("rz(9*pi/4) q[0];", "t q[0];"),
(
"rx(pi/4) q[0];",
"""
h q[0];
t q[0];
h q[0];
""",
),
(
"ry(-pi/4) q[0];",
"""
sdg q[0];
h q[0];
tdg q[0];
h q[0];
s q[0];
""",
),
("rx(0) q[0];", ""),
],
)
def test_rebase_clifford_t_exact_rotations(input_gate, decomposed_gates):
"""Exact pi/4 rotations are preserved when rebasing to Clifford+T."""
qasm = f"""OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[1] c;
{input_gate}
c[0] = measure q[0];
"""

expected_qasm = f"""OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[1] c;
{decomposed_gates}
c[0] = measure q[0];
"""

result = loads(qasm)
result.rebase(BasisSet.CLIFFORD_T)
check_unrolled_qasm(dumps(result), expected_qasm)


@pytest.mark.parametrize(
"gate_name, pauli",
[
("rx", np.array([[0, 1], [1, 0]], dtype=complex)),
("ry", np.array([[0, -1j], [1j, 0]], dtype=complex)),
("rz", np.diag([1, -1]).astype(complex)),
],
)
@pytest.mark.parametrize("quarter_turns", range(8))
def test_rebase_clifford_t_rotation_unitary(gate_name: str, pauli: np.ndarray, quarter_turns: int):
"""Every exact quarter turn has the expected unitary up to global phase."""
qasm = f"""OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
{gate_name}({quarter_turns}*pi/4) q[0];
"""

result = loads(qasm)
result.rebase(BasisSet.CLIFFORD_T)

angle = quarter_turns * np.pi / 4
expected = np.cos(angle / 2) * np.eye(2) - 1j * np.sin(angle / 2) * pauli
assert_unitary_equal(unitary_from_unrolled_ast(result.unrolled_ast, 1), expected)


@pytest.mark.parametrize("gate_name", ["rx", "ry", "rz"])
def test_rebase_clifford_t_rejects_inexact_rotations(gate_name):
"""Rotations outside the exact basis fail instead of disappearing."""
qasm = f"""OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
{gate_name}(pi/3) q[0];
"""

with pytest.raises(
RebaseError,
match=rf"Gate '{gate_name}'.*cannot be represented exactly",
):
loads(qasm).rebase(BasisSet.CLIFFORD_T)


def test_rebase_clifford_t_rotation_in_branch():
"""Exact rotations inside conditional blocks are decomposed too."""
qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[1] c;
if (c[0] == true) {
ry(-pi/4) q[0];
}
"""

expected_qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[1] c;
if (c[0] == true) {
sdg q[0];
h q[0];
tdg q[0];
h q[0];
s q[0];
}
"""

result = loads(qasm)
result.rebase(BasisSet.CLIFFORD_T)
check_unrolled_qasm(dumps(result), expected_qasm)


def test_rebase_if():
"""Test converting a QASM3 program that contains if statements"""

Expand Down
Loading