Skip to content
This repository was archived by the owner on Sep 26, 2023. It is now read-only.

transpiler: wrap Rx angles to [0, π] #39

Merged
merged 5 commits into from
Mar 28, 2023
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 @@ -3,6 +3,7 @@
## Unreleased

* Add a Grover-based 3-SAT solver example #31
* Wrap single-qubit rotation angles to [0, π] instead of [-π, π] #39
* **Breaking change** Remove provider for legacy API #40
* Automatically load environment variables from `.env` files #42

Expand Down
8 changes: 8 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@

"""Pytest dynamic configuration."""

import hypothesis

hypothesis.settings.register_profile(
"default",
deadline=None, # Account for slower CI workers
print_blob=True, # Always print code to use with @reproduce_failure
)

pytest_plugins = [
"pytest_qiskit_aqt",
]
47 changes: 46 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ python = "^3.8"

python-dotenv = ">=1"
qiskit-aer = ">=0.11"
qiskit-terra = ">=0.23.2"
qiskit-terra = ">=0.23.3"
requests = ">=2"
tabulate = ">=0.9.0"
tweedledum = { version = ">=1", optional = true }
Expand All @@ -57,6 +57,7 @@ typing-extensions = ">=4.0.0"
black = "^23.1.0"
coverage = "^7.2.1"
dirty-equals = "^0.5.0"
hypothesis = "^6.70.0"
ipykernel = "^6.22.0"
isort = "^5.12.0"
jupyter-sphinx = "^0.4.0"
Expand Down
22 changes: 14 additions & 8 deletions qiskit_aqt_provider/transpiler_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,22 @@
from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin


class RewriteRxRyAsR(TransformationPass):
"""Rewrite all `RXGate` and `RYGate` instances as `RGate`. Wrap the rotation angle to [-π, π]."""
def rewrite_rx_as_r(theta: float) -> Instruction:
"""Instruction equivalent to Rx(θ) as R(θ, φ) with θ ∈ [0, π] and φ ∈ [0, 2π]."""

theta = math.atan2(math.sin(theta), math.cos(theta))
phi = math.pi if theta < 0.0 else 0.0
return RGate(abs(theta), phi)


class RewriteRxAsR(TransformationPass):
"""Rewrite Rx(θ) as R(θ, φ) with θ ∈ [0, π] and φ ∈ [0, 2π]."""

def run(self, dag: DAGCircuit) -> DAGCircuit:
for node in dag.gate_nodes():
if node.name in {"rx", "ry"}:
if node.name == "rx":
(theta,) = node.op.params
phi = math.pi / 2 if node.name == "ry" else 0.0
new_theta = math.atan2(math.sin(float(theta)), math.cos(float(theta)))
dag.substitute_node(node, RGate(new_theta, phi))
dag.substitute_node(node, rewrite_rx_as_r(float(theta)))
return dag


Expand All @@ -48,8 +54,8 @@ def pass_manager(
# This allows decomposing any run of rotations into the ZXZ form, taking
# advantage of the free Z rotations.
# Since the API expects R/RZ as single-qubit operations,
# we rewrite all RX/RY gates as R gates after optimizations have been performed.
RewriteRxRyAsR(),
# we rewrite all RX gates as R gates after optimizations have been performed.
RewriteRxAsR(),
]

return PassManager(passes)
Expand Down
85 changes: 61 additions & 24 deletions test/test_transpilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,60 +11,96 @@
# that they have been altered from the originals.

from math import pi
from typing import Final
from typing import Final, Union

import pytest
from hypothesis import assume, given
from hypothesis import strategies as st
from qiskit import QuantumCircuit, transpile
from qiskit.circuit.library import RXGate, RYGate

from qiskit_aqt_provider.aqt_provider import AQTProvider
from qiskit_aqt_provider.aqt_resource import AQTResource
from qiskit_aqt_provider.test.circuits import (
assert_circuits_equal,
assert_circuits_equivalent,
qft_circuit,
)
from qiskit_aqt_provider.transpiler_plugin import rewrite_rx_as_r


@pytest.mark.parametrize(
"angle,expected_angle",
"input_theta,output_theta,output_phi",
[
(pi / 3, pi / 3),
(7 * pi / 5, -3 * pi / 5),
(25 * pi, -pi),
(22 * pi / 3, -2 * pi / 3),
(pi / 3, pi / 3, 0.0),
(-pi / 3, pi / 3, pi),
(7 * pi / 5, 3 * pi / 5, pi),
(25 * pi, pi, pi),
(22 * pi / 3, 2 * pi / 3, pi),
],
)
def test_rx_wrap_angle(
angle: float, expected_angle: float, offline_simulator_no_noise: AQTResource
def test_rx_rewrite_example(
input_theta: float,
output_theta: float,
output_phi: float,
) -> None:
"""Check that transpiled rotation gate angles are wrapped to [-π,π]."""
qc = QuantumCircuit(1)
qc.rx(angle, 0)
"""Snapshot test for the Rx(θ) → R(θ, φ) rule."""

result = QuantumCircuit(1)
result.append(rewrite_rx_as_r(input_theta), (0,))

expected = QuantumCircuit(1)
expected.r(expected_angle, 0, 0)
expected.r(output_theta, output_phi, 0)

result = transpile(qc, offline_simulator_no_noise, optimization_level=3)
assert isinstance(result, QuantumCircuit)
reference = QuantumCircuit(1)
reference.rx(input_theta, 0)

assert_circuits_equal(result, expected)
assert_circuits_equivalent(result, reference)


def test_rx_r_rewrite_simple(offline_simulator_no_noise: AQTResource) -> None:
"""Check that Rx gates are rewritten as R gates."""
@given(theta=st.floats(allow_nan=False, min_value=-1000 * pi, max_value=1000 * pi))
@pytest.mark.parametrize("optimization_level", [1, 2, 3])
@pytest.mark.parametrize("test_gate", [RXGate, RYGate])
def test_rx_ry_rewrite_transpile(
theta: float,
optimization_level: int,
test_gate: Union[RXGate, RYGate],
) -> None:
"""Test the rewrite rule: Rx(θ), Ry(θ) → R(θ, φ), θ ∈ [0, π], φ ∈ [0, 2π]."""

assume(abs(theta) > pi / 200)

# we only need the backend's transpiler target for this test
backend = AQTProvider("").get_resource("default", "offline_simulator_no_noise")

qc = QuantumCircuit(1)
qc.rx(pi / 2, 0)
qc.append(test_gate(theta), (0,))

expected = QuantumCircuit(1)
expected.r(pi / 2, 0, 0)
trans_qc = transpile(qc, backend, optimization_level=optimization_level)
assert isinstance(trans_qc, QuantumCircuit)

result = transpile(qc, offline_simulator_no_noise, optimization_level=3)
assert isinstance(result, QuantumCircuit) # only got one circuit back
assert_circuits_equivalent(trans_qc, qc)

assert_circuits_equal(result, expected)
assert set(trans_qc.count_ops()) <= set(backend.configuration().basis_gates)

num_r = trans_qc.count_ops().get("r")
assume(num_r is not None)
assert num_r == 1

for operation in trans_qc.data:
instruction = operation[0]
if instruction.name == "r":
theta, phi = instruction.params
assert 0 <= float(theta) <= pi
assert 0 <= float(phi) <= 2 * pi
break
else: # pragma: no cover
pytest.fail("No R gates in transpiled circuit.")


def test_decompose_1q_rotations_simple(offline_simulator_no_noise: AQTResource) -> None:
"""Check that runs of single-qubit rotations are optimized as a ZXZ."""
def test_decompose_1q_rotations_example(offline_simulator_no_noise: AQTResource) -> None:
"""Snapshot test for the efficient rewrite of single-qubit rotation runs as ZXZ."""
qc = QuantumCircuit(1)
qc.rx(pi / 2, 0)
qc.ry(pi / 2, 0)
Expand All @@ -77,6 +113,7 @@ def test_decompose_1q_rotations_simple(offline_simulator_no_noise: AQTResource)
assert isinstance(result, QuantumCircuit) # only got one circuit back

assert_circuits_equal(result, expected)
assert_circuits_equivalent(result, expected)


RXX_ANGLES: Final = [
Expand Down