diff --git a/CHANGELOG.md b/CHANGELOG.md index 8afc3a69..21f90f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/pyqasm/decomposer.py b/src/pyqasm/decomposer.py index 147cee07..d65dd17c 100644 --- a/src/pyqasm/decomposer.py +++ b/src/pyqasm/decomposer.py @@ -16,6 +16,8 @@ Definition of the Decomposer class """ +import math + import openqasm3.ast as qasm3_ast from openqasm3.ast import BranchingStatement, QuantumGate @@ -55,13 +57,7 @@ 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'." @@ -69,6 +65,52 @@ def process_gate_statement(cls, gate_name, statement, target_basis_set): 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: + 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. diff --git a/tests/qasm2/test_rotation_gates.py b/tests/qasm2/test_rotation_gates.py index 8f0a24eb..218e366e 100644 --- a/tests/qasm2/test_rotation_gates.py +++ b/tests/qasm2/test_rotation_gates.py @@ -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""" diff --git a/tests/qasm3/test_rebase.py b/tests/qasm3/test_rebase.py index 79a245c5..ecd41863 100644 --- a/tests/qasm3/test_rebase.py +++ b/tests/qasm3/test_rebase.py @@ -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( @@ -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"""