diff --git a/CHANGELOG.md b/CHANGELOG.md index c14cfada..745a3745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Types of changes: ### Removed ### Fixed +- Decompose `p`, `phaseshift`, and `u1` (`U1`) gates into a single `rz(theta)` rotation instead of `h; rx(theta); h` ([#470](https://github.com/qBraid/pyqasm/pull/470), [#469](https://github.com/qBraid/pyqasm/issues/469)). - 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)). diff --git a/docs/gate_decompositions.md b/docs/gate_decompositions.md index f4357db0..d833384d 100644 --- a/docs/gate_decompositions.md +++ b/docs/gate_decompositions.md @@ -113,20 +113,20 @@ q_1: ┤ X ├┤ Rz(π) ├┤ X ├ └───┘└───────┘└───┘ ``` -## [Phaseshift Gate](../src/pyqasm/maps/gates.py#L249) +## [Phaseshift Gate](../src/pyqasm/maps/gates.py#L300) -The phaseshift gate is implemented as a decomposition of other gates using the following qiskit decomposition: +The phaseshift gate is implemented as a single `rz` rotation (equivalent up to a global phase) using the following qiskit decomposition: ```python In [10]: q = QuantumCircuit(1) In [11]: q.p(theta,0) -In [12]: new_qc = transpile(q, basis_gates=['rx','h']) +In [12]: new_qc = transpile(q, basis_gates=['rz']) In [13]: print(new_qc) Out[13]: - - ┌───┐┌───────────┐┌───┐ -q: ┤ H ├┤ Rx(theta) ├┤ H ├ - └───┘└───────────┘└───┘ +global phase: theta/2 + ┌───────────┐ +q: ┤ Rz(theta) ├ + └───────────┘ ``` ## [CSWAP Gate](../src/pyqasm/maps/gates.py#L264) diff --git a/src/pyqasm/maps/gates.py b/src/pyqasm/maps/gates.py index a0d8ab42..90cc3161 100644 --- a/src/pyqasm/maps/gates.py +++ b/src/pyqasm/maps/gates.py @@ -299,17 +299,13 @@ def zz_gate( def phaseshift_gate(theta: int | float, qubit: IndexedIdentifier) -> list[QuantumGate]: """ - Implements the phase shift gate as a decomposition of other gates. + Implements the phase shift gate as a single RZ rotation (equivalent up to a global phase). Reference : See https://github.com/qBraid/pyqasm/tree/main/docs/gate_decompositions.md#phaseshift-gate for decomposition details """ - result: list[QuantumGate] = [] - result.extend(one_qubit_gate_op("h", qubit)) - result.extend(one_qubit_rotation_op("rx", theta, qubit)) - result.extend(one_qubit_gate_op("h", qubit)) - return result + return one_qubit_rotation_op("rz", theta, qubit) def cswap_gate( diff --git a/tests/qasm2/test_rotation_gates.py b/tests/qasm2/test_rotation_gates.py index 218e366e..d0b33f93 100644 --- a/tests/qasm2/test_rotation_gates.py +++ b/tests/qasm2/test_rotation_gates.py @@ -18,9 +18,37 @@ """ +import numpy as np +import pytest + from pyqasm.elements import BasisSet from pyqasm.entrypoint import dumps, loads -from tests.utils import check_unrolled_qasm +from tests.utils import assert_unitary_equal, check_unrolled_qasm, unitary_from_unrolled_ast + + +@pytest.mark.parametrize("gate_name", ["p", "u1"]) +def test_phaseshift_gates_unroll_to_rz_qasm2(gate_name): + """Test that p and u1 in OpenQASM 2 unroll to a single rz gate.""" + theta = 0.5 + qasm_in = f""" +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +{gate_name}({theta}) q[0]; +""" + expected_out = f""" +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +rz({theta}) q[0]; +""" + result = loads(qasm_in) + result.unroll() + check_unrolled_qasm(dumps(result), expected_out) + assert_unitary_equal( + unitary_from_unrolled_ast(result.unrolled_ast, 1), + np.diag([1.0, np.exp(1j * theta)]), + ) def test_rebase_clifford_t_exact_rotation(): diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index 34e3ae95..28fc819e 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -670,7 +670,7 @@ def test_qasm3_depth_branching_for_external_gates(): @pytest.mark.parametrize( ["input_qasm_str", "before_decompose", "after_decompose"], - [(QASM3_DECOMPOSE_GATE_DEPTH, 2, 25), (QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH, 2, 25)], + [(QASM3_DECOMPOSE_GATE_DEPTH, 2, 21), (QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH, 2, 21)], ) def test_gate_depth_decomposable_gates(input_qasm_str, before_decompose, after_decompose): result = loads(input_qasm_str) diff --git a/tests/qasm3/test_device_qubits.py b/tests/qasm3/test_device_qubits.py index 2fc71687..4d197b41 100644 --- a/tests/qasm3/test_device_qubits.py +++ b/tests/qasm3/test_device_qubits.py @@ -190,21 +190,13 @@ def test_gates(): rz(4.71238898038469) __PYQASM_QUBITS__[0]; rx(1.5707963267948966) __PYQASM_QUBITS__[0]; rz(3.141592653589793) __PYQASM_QUBITS__[0]; - h __PYQASM_QUBITS__[0]; - rx(0.7853981633974483) __PYQASM_QUBITS__[0]; - h __PYQASM_QUBITS__[0]; + rz(0.7853981633974483) __PYQASM_QUBITS__[0]; cx __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[0]; - h __PYQASM_QUBITS__[0]; - rx(-0.7853981633974483) __PYQASM_QUBITS__[0]; - h __PYQASM_QUBITS__[0]; + rz(-0.7853981633974483) __PYQASM_QUBITS__[0]; cx __PYQASM_QUBITS__[4], __PYQASM_QUBITS__[0]; - h __PYQASM_QUBITS__[0]; - rx(0.7853981633974483) __PYQASM_QUBITS__[0]; - h __PYQASM_QUBITS__[0]; + rz(0.7853981633974483) __PYQASM_QUBITS__[0]; cx __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[0]; - h __PYQASM_QUBITS__[0]; - rx(-0.7853981633974483) __PYQASM_QUBITS__[0]; - h __PYQASM_QUBITS__[0]; + rz(-0.7853981633974483) __PYQASM_QUBITS__[0]; rz(3.141592653589793) __PYQASM_QUBITS__[0]; rx(1.5707963267948966) __PYQASM_QUBITS__[0]; rz(4.71238898038469) __PYQASM_QUBITS__[0]; diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index aaadd780..60aa63bd 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + """ Module containing unit tests for unrolling quantum gates. @@ -981,3 +983,79 @@ def test_physical_qubit_duplicate_detection(): """ with pytest.raises(ValidationError, match=r"Duplicate qubit"): loads(qasm_input).validate() + + +@pytest.mark.parametrize("gate_name", ["p", "phaseshift", "u1"]) +def test_phaseshift_gates_unroll_to_rz(gate_name): + """p, phaseshift, and u1 unroll to a single rz(theta) gate with matching unitary.""" + theta = 0.7 + qasm3_string = f""" + OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + {gate_name}({theta}) q[0]; + """ + expected_qasm = f""" + OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + rz({theta}) q[0]; + """ + result = loads(qasm3_string) + result.unroll() + check_unrolled_qasm(dumps(result), expected_qasm) + assert_unitary_equal( + unitary_from_unrolled_ast(result.unrolled_ast, 1), + np.diag([1.0, np.exp(1j * theta)]), + ) + + +_CSX_UNITARY = np.array( + [ + [1, 0, 0, 0], + [0, 0.5 * (1 + 1j), 0, 0.5 * (1 - 1j)], + [0, 0, 1, 0], + [0, 0.5 * (1 - 1j), 0, 0.5 * (1 + 1j)], + ], + dtype=complex, +) + +_RCCX_UNITARY = np.array( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, -1j], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, -1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 1j, 0, 0, 0, 0], + ], + dtype=complex, +) + + +@pytest.mark.parametrize( + "body, num_qubits, expected_num_gates, expected_unitary", + [ + ("csx q[0], q[1];", 2, 15, _CSX_UNITARY), + ("rccx q[0], q[1], q[2];", 3, 17, _RCCX_UNITARY), + ], +) +def test_csx_and_rccx_decomposition_unitary(body, num_qubits, expected_num_gates, expected_unitary): + """csx and rccx decompose via single-rz phaseshift_gate and preserve their unitary.""" + header = f'OPENQASM 3;\ninclude "stdgates.inc";\nqubit[{num_qubits}] q;\n' + module = loads(header + body) + module.unroll() + + assert_unitary_equal( + unitary_from_unrolled_ast(module.unrolled_ast, num_qubits), + expected_unitary, + ) + gate_names = [ + stmt.name.name + for stmt in module.unrolled_ast.statements + if isinstance(stmt, qasm3_ast.QuantumGate) + ] + assert "h" not in gate_names + assert len(gate_names) == expected_num_gates diff --git a/tests/qasm3/test_rebase.py b/tests/qasm3/test_rebase.py index d408f02f..f5600c26 100644 --- a/tests/qasm3/test_rebase.py +++ b/tests/qasm3/test_rebase.py @@ -233,6 +233,34 @@ def test_rebase_clifford_t_exact_rotations(input_gate, decomposed_gates): check_unrolled_qasm(dumps(result), expected_qasm) +@pytest.mark.parametrize( + "input_gate, decomposed_gates", + [ + ("p(pi/4) q[0];", "t q[0];"), + ("p(pi/2) q[0];", "s q[0];"), + ("phaseshift(pi/4) q[0];", "t q[0];"), + ("u1(pi/2) q[0];", "s q[0];"), + ], +) +def test_rebase_clifford_t_phaseshift(input_gate, decomposed_gates): + """Phase-shift gates rebase directly to single Clifford+T phase gates.""" + qasm = f"""OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + {input_gate} + """ + + expected_qasm = f"""OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + {decomposed_gates} + """ + + result = loads(qasm) + result.rebase(BasisSet.CLIFFORD_T) + check_unrolled_qasm(dumps(result), expected_qasm) + + @pytest.mark.parametrize( "gate_name, pauli", [ diff --git a/tests/utils.py b/tests/utils.py index a55f4685..464ca2a6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -484,12 +484,7 @@ def check_u3_gate_op(unrolled_ast, num_gates, qubit_list, param_list): def _check_phase_shift_gate_op(unrolled_ast, num_gates, qubit_list, param_list): - num_h_gates = 2 * num_gates - h_qubit_list = [qubit for qubit in qubit_list for _ in range(2)] - num_rx_gates = 1 * num_gates - - check_single_qubit_gate_op(unrolled_ast, num_h_gates, h_qubit_list, "h") - check_single_qubit_rotation_op(unrolled_ast, num_rx_gates, qubit_list, param_list, "rx") + check_single_qubit_rotation_op(unrolled_ast, num_gates, qubit_list, param_list, "rz") def check_measure_op(unrolled_ast, num_ops, meas_pairs):