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

Commit

Permalink
transpiler: wrap Rx angles to [0, π] (#39)
Browse files Browse the repository at this point in the history
* transpiler: wrap Rx angles to [0, π], fuzzing test
  • Loading branch information
airwoodix authored Mar 28, 2023
1 parent c72cdff commit 31913d2
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 34 deletions.
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

0 comments on commit 31913d2

Please sign in to comment.