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
- 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)).
Expand Down
14 changes: 7 additions & 7 deletions docs/gate_decompositions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 2 additions & 6 deletions src/pyqasm/maps/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
30 changes: 29 additions & 1 deletion tests/qasm2/test_rotation_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion tests/qasm3/test_depth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 4 additions & 12 deletions tests/qasm3/test_device_qubits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
78 changes: 78 additions & 0 deletions tests/qasm3/test_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
28 changes: 28 additions & 0 deletions tests/qasm3/test_rebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand Down
7 changes: 1 addition & 6 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading