From fa9ba3e41d46c847d77f85c3e75901659a471545 Mon Sep 17 00:00:00 2001 From: Tony Coder <407243179@qq.com> Date: Wed, 28 Oct 2026 13:36:13 +0800 Subject: [PATCH] fix: decompose ccx, id and p when rebasing to ROTATIONAL_CX / CLIFFORD_T rebase() raised RebaseError for ccx (kept as a single gate by unroll), for id, and for the p gates that the qelib1 c3x expands into. - add a 6-CNOT Toffoli rule for both basis sets, addressing the third operand through a new AppliedQubit.QUBIT3 - drop id in both basis sets - map p to rz (equal up to global phase); for Clifford+T reuse the exact rotation path, which still rejects non pi/4 multiples - pick rule qubits by AppliedQubit value instead of a two-qubit if/else Signed-off-by: Tony Coder <407243179@qq.com> --- CHANGELOG.md | 1 + src/pyqasm/decomposer.py | 28 +++++++----- src/pyqasm/maps/decomposition_rules.py | 47 ++++++++++++++++++-- tests/qasm3/test_rebase.py | 61 ++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97693292..c14cfada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Types of changes: ### Removed ### Fixed +- Fixed `rebase` failing on `ccx`, `id`, and the `p` gates produced by unrolling `c3x` for the `ROTATIONAL_CX` / `CLIFFORD_T` basis sets ([#468](https://github.com/qBraid/pyqasm/pull/468), [#467](https://github.com/qBraid/pyqasm/issues/467)). - `loads()` syntax errors now name the offending token, line and column, e.g. `Failed to parse OpenQASM string: syntax error near ';' at line 3, column 13`. Before, the reason after the colon was empty ([#460](https://github.com/qBraid/pyqasm/pull/460)). - Fixed the matplotlib printer's controlled-gate lookup for `c3x` and `c4x` ([#458](https://github.com/qBraid/pyqasm/pull/458), [#322](https://github.com/qBraid/pyqasm/issues/322)). - Fixed `loads("")` / whitespace-only input raising `AttributeError` instead of `ValidationError`. Empty and whitespace programs now raise `ValidationError` like other parse failures. ([#451](https://github.com/qBraid/pyqasm/pull/451), [#437](https://github.com/qBraid/pyqasm/issues/437)) diff --git a/src/pyqasm/decomposer.py b/src/pyqasm/decomposer.py index d65dd17c..334000d4 100644 --- a/src/pyqasm/decomposer.py +++ b/src/pyqasm/decomposer.py @@ -16,13 +16,15 @@ Definition of the Decomposer class """ +import copy import math import openqasm3.ast as qasm3_ast from openqasm3.ast import BranchingStatement, QuantumGate +from pyqasm.elements import BasisSet from pyqasm.exceptions import RebaseError -from pyqasm.maps.decomposition_rules import DECOMPOSITION_RULES, AppliedQubit +from pyqasm.maps.decomposition_rules import DECOMPOSITION_RULES from pyqasm.maps.gates import BASIS_GATE_MAP, fresh_qubits @@ -56,7 +58,18 @@ def process_gate_statement(cls, gate_name, statement, target_basis_set): processed_gates_list = cls._get_decomposed_gates( decomposition_rules, statement, gate_name ) - elif gate_name in {"rx", "ry", "rz"}: + elif gate_name == "p" and target_basis_set == BasisSet.ROTATIONAL_CX: + # p(theta) = exp(i*theta/2) * rz(theta), so it is rz(theta) up to a global phase + processed_gates_list = [ + qasm3_ast.QuantumGate( + modifiers=[], + name=qasm3_ast.Identifier(name="rz"), + arguments=copy.deepcopy(statement.arguments), + qubits=fresh_qubits(*statement.qubits), + ) + ] + elif gate_name in {"rx", "ry", "rz", "p"}: + # p is handled like rz here (equal up to a global phase) 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 @@ -196,14 +209,7 @@ def _get_qubits_for_gate(cls, qubits, rule): list: The ordered qubits to be used for the gate operation. """ if "controll_bit" in rule: - if rule["controll_bit"] == AppliedQubit.QUBIT1: - qubits = [qubits[0], qubits[1]] - else: - qubits = [qubits[1], qubits[0]] - + qubits = [qubits[rule["controll_bit"].value], qubits[rule["target_bit"].value]] elif "target_bit" in rule: - if rule["target_bit"] == AppliedQubit.QUBIT1: - qubits = [qubits[0]] - else: - qubits = [qubits[1]] + qubits = [qubits[rule["target_bit"].value]] return qubits diff --git a/src/pyqasm/maps/decomposition_rules.py b/src/pyqasm/maps/decomposition_rules.py index 3a17c961..666a5073 100644 --- a/src/pyqasm/maps/decomposition_rules.py +++ b/src/pyqasm/maps/decomposition_rules.py @@ -25,8 +25,9 @@ class AppliedQubit(Enum): """Enum to represent the qubits that are involved in the decomposition of a gate.""" - QUBIT1 = 0 # Control qubit - QUBIT2 = 1 # Target qubit + QUBIT1 = 0 + QUBIT2 = 1 + QUBIT3 = 2 # Decomposition rules for the gates in the basis sets. @@ -37,12 +38,13 @@ class AppliedQubit(Enum): # - gate: The name of the gate to be applied. # - param: The parameter of the gate. # - target_bit: The target qubit of the gate. -# This key is only used for decomposition of gates that operate on two qubits. +# This key is only used for decomposition of multi-qubit gates. # - controll_bit: The control qubit of the gate. -# This key is only used for gates that operate on two qubits. +# This key is only used for multi-qubit gates. # DECOMPOSITION_RULES = { BasisSet.ROTATIONAL_CX: { + "id": [], "x": [{"gate": "rx", "param": CONSTANTS_MAP["pi"]}], "y": [{"gate": "ry", "param": CONSTANTS_MAP["pi"]}], "z": [{"gate": "rz", "param": CONSTANTS_MAP["pi"]}], @@ -67,8 +69,28 @@ class AppliedQubit(Enum): {"gate": "cx", "controll_bit": AppliedQubit.QUBIT2, "target_bit": AppliedQubit.QUBIT1}, {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2}, ], + "ccx": [ + {"gate": "ry", "param": CONSTANTS_MAP["pi"] / 2, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "rx", "param": CONSTANTS_MAP["pi"], "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT2, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "rz", "param": -CONSTANTS_MAP["pi"] / 4, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "rz", "param": CONSTANTS_MAP["pi"] / 4, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT2, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "rz", "param": -CONSTANTS_MAP["pi"] / 4, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "rz", "param": CONSTANTS_MAP["pi"] / 4, "target_bit": AppliedQubit.QUBIT2}, + {"gate": "rz", "param": CONSTANTS_MAP["pi"] / 4, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "ry", "param": CONSTANTS_MAP["pi"] / 2, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "rx", "param": CONSTANTS_MAP["pi"], "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2}, + {"gate": "rz", "param": CONSTANTS_MAP["pi"] / 4, "target_bit": AppliedQubit.QUBIT1}, + {"gate": "rz", "param": -CONSTANTS_MAP["pi"] / 4, "target_bit": AppliedQubit.QUBIT2}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2}, + ], }, BasisSet.CLIFFORD_T: { + "id": [], "x": [{"gate": "h"}, {"gate": "s"}, {"gate": "s"}, {"gate": "h"}], "y": [ {"gate": "s"}, @@ -98,6 +120,23 @@ class AppliedQubit(Enum): {"gate": "cx", "controll_bit": AppliedQubit.QUBIT2, "target_bit": AppliedQubit.QUBIT1}, {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2}, ], + "ccx": [ + {"gate": "h", "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT2, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "tdg", "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "t", "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT2, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "tdg", "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT3}, + {"gate": "t", "target_bit": AppliedQubit.QUBIT2}, + {"gate": "t", "target_bit": AppliedQubit.QUBIT3}, + {"gate": "h", "target_bit": AppliedQubit.QUBIT3}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2}, + {"gate": "t", "target_bit": AppliedQubit.QUBIT1}, + {"gate": "tdg", "target_bit": AppliedQubit.QUBIT2}, + {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2}, + ], }, } diff --git a/tests/qasm3/test_rebase.py b/tests/qasm3/test_rebase.py index 787c0eed..d408f02f 100644 --- a/tests/qasm3/test_rebase.py +++ b/tests/qasm3/test_rebase.py @@ -18,15 +18,18 @@ """ import numpy as np +import openqasm3.ast as qasm3_ast import pytest from pyqasm.elements import BasisSet from pyqasm.entrypoint import dumps, loads from pyqasm.exceptions import RebaseError +from pyqasm.maps.gates import BASIS_GATE_MAP from tests.utils import ( assert_unitary_equal, check_single_qubit_gate_op, check_unrolled_qasm, + mcx_unitary, unitary_from_unrolled_ast, ) @@ -479,3 +482,61 @@ def test_rebase_qasm_module_methods(): result.reverse_qubit_order() print(dumps(result)) check_unrolled_qasm(dumps(result), expected_reverse_qubit_order_qasm) + + +def _gate_names(module) -> list[str]: + """Return the names of all QuantumGate statements in the module's unrolled AST.""" + return [ + stmt.name.name + for stmt in module.unrolled_ast.statements + if isinstance(stmt, qasm3_ast.QuantumGate) + ] + + +@pytest.mark.parametrize("basis", [BasisSet.ROTATIONAL_CX, BasisSet.CLIFFORD_T]) +@pytest.mark.parametrize( + "ccx_stmt, target", + [ + ("ccx q[0], q[1], q[2];", None), + ("ccx q[2], q[0], q[1];", 1), + ], +) +def test_rebase_ccx(basis, ccx_stmt, target): + """Rebasing ccx emits only basis gates and preserves the Toffoli unitary.""" + qasm = f"""OPENQASM 3.0; + include "stdgates.inc"; + qubit[3] q; + {ccx_stmt} + """ + result = loads(qasm).rebase(basis) + gates = _gate_names(result) + assert gates and set(gates).issubset(BASIS_GATE_MAP[basis]) + assert_unitary_equal( + unitary_from_unrolled_ast(result.unrolled_ast, 3), + mcx_unitary(3, target=target), + ) + + +@pytest.mark.parametrize("basis", [BasisSet.ROTATIONAL_CX, BasisSet.CLIFFORD_T]) +def test_rebase_id(basis): + """Rebasing id succeeds and emits no gate statements.""" + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + id q[0]; + """ + result = loads(qasm).rebase(basis) + assert not _gate_names(result) + + +def test_rebase_qasm2_c3x_rotational_cx(): + """Rebasing QASM 2 c3x to ROTATIONAL_CX emits only basis gates with matching unitary.""" + qasm = """OPENQASM 2.0; + include "qelib1.inc"; + qreg q[4]; + c3x q[0], q[1], q[2], q[3]; + """ + result = loads(qasm).rebase(BasisSet.ROTATIONAL_CX) + gates = _gate_names(result) + assert gates and set(gates).issubset(BASIS_GATE_MAP[BasisSet.ROTATIONAL_CX]) + assert_unitary_equal(unitary_from_unrolled_ast(result.unrolled_ast, 4), mcx_unitary(4))