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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
28 changes: 17 additions & 11 deletions src/pyqasm/decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
47 changes: 43 additions & 4 deletions src/pyqasm/maps/decomposition_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"]}],
Expand All @@ -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"},
Expand Down Expand Up @@ -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},
],
},
}

Expand Down
61 changes: 61 additions & 0 deletions tests/qasm3/test_rebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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))
Loading