Skip to content

Commit

Permalink
Add plugin pass to support non-calibrated rzz angles (#2043)
Browse files Browse the repository at this point in the history
* add plugin pass to support non-calibrated rzz angles

* add release note

* update phase check logic

* add more document

* add more angle test

* support control flow

* Update qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py

Co-authored-by: Yael Ben-Haim <[email protected]>

* Update qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py

Co-authored-by: Yael Ben-Haim <[email protected]>

* Update qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py

Co-authored-by: Yael Ben-Haim <[email protected]>

* Update documentation wording

Co-authored-by: Yael Ben-Haim <[email protected]>

* update test to require the same phase

* fix quad2 no wrap

* fix quad3 no wrap

* apply a global phase of pi for a 2pi angle offset

* black

* bug fix

* wrote test_fractional_plugin

* made the _quad functions static methods of the class

* Update release-notes/unreleased/2043.feat.rst

Co-authored-by: Will Shanks <[email protected]>

* deleted a test

---------

Co-authored-by: Yael Ben-Haim <[email protected]>
Co-authored-by: Will Shanks <[email protected]>
Co-authored-by: Kevin Tian <[email protected]>
  • Loading branch information
4 people authored Dec 4, 2024
1 parent 7d5f3ac commit d88028b
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 2 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ dependencies = [
[project.entry-points."qiskit.transpiler.translation"]
ibm_backend = "qiskit_ibm_runtime.transpiler.plugin:IBMTranslationPlugin"
ibm_dynamic_circuits = "qiskit_ibm_runtime.transpiler.plugin:IBMDynamicTranslationPlugin"
ibm_fractional = "qiskit_ibm_runtime.transpiler.plugin:IBMFractionalTranslationPlugin"

[project.urls]
documentation = "https://docs.quantum.ibm.com/"
Expand Down
4 changes: 3 additions & 1 deletion qiskit_ibm_runtime/ibm_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,9 @@ def close_session(self) -> None:

def get_translation_stage_plugin(self) -> str:
"""Return the default translation stage plugin name for IBM backends."""
return "ibm_dynamic_circuits"
if not self.options.use_fractional_gates:
return "ibm_dynamic_circuits"
return "ibm_fractional"


class IBMRetiredBackend(IBMBackend):
Expand Down
1 change: 1 addition & 0 deletions qiskit_ibm_runtime/transpiler/passes/basis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
"""Passes to layout circuits to IBM backend's instruction sets."""

from .convert_id_to_delay import ConvertIdToDelay
from .fold_rzz_angle import FoldRzzAngle
246 changes: 246 additions & 0 deletions qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Pass to wrap Rzz gate angle in calibrated range of 0-pi/2."""

from typing import Tuple
from math import pi

from qiskit.converters import dag_to_circuit, circuit_to_dag
from qiskit.circuit.library.standard_gates import RZZGate, RZGate, XGate, GlobalPhaseGate
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.circuit import Qubit, ControlFlowOp
from qiskit.dagcircuit import DAGCircuit
from qiskit.transpiler.basepasses import TransformationPass

import numpy as np


class FoldRzzAngle(TransformationPass):
"""Fold Rzz gate angle into calibrated range of 0-pi/2 with
local gate tweaks.
In the IBM Quantum ISA, the instruction Rzz(theta) has
valid "theta" value of [0, pi/2] and any instruction outside
this range becomes a non-ISA operation for the quantum backend.
The transpiler pass discovers such non-ISA Rzz gates
and folds the gate angle into the calibrated range
with addition of single qubit gates while preserving
logical equivalency of the input quantum circuit.
Added local gates might be efficiently merged into
neighboring single qubit gates by the following single qubit
optimization passes.
This pass allows the Qiskit users to naively use the Rzz gates
with angle of arbitrary real numbers.
.. note::
This pass doesn't transform the circuit when the
Rzz gate angle is an unbound parameter.
In this case, the user must assign a gate angle before
transpilation, or be responsible for choosing parameters
from the calibrated range of [0, pi/2].
"""

def run(self, dag: DAGCircuit) -> DAGCircuit:
self._run_inner(dag)
return dag

def _run_inner(self, dag: DAGCircuit) -> bool:
"""Mutate the input dag to fix non-ISA Rzz angles.
Return true if the dag was modified."""
modified = False
for node in dag.op_nodes():
if isinstance(node.op, ControlFlowOp):
modified_blocks = False
new_blocks = []
for block in node.op.blocks:
block_dag = circuit_to_dag(block)
if self._run_inner(block_dag):
# Store circuit with Rzz modification
new_blocks.append(dag_to_circuit(block_dag))
modified_blocks = True
else:
# Keep original circuit to save memory
new_blocks.append(block)
if modified_blocks:
dag.substitute_node(
node,
node.op.replace_blocks(new_blocks),
inplace=True,
)
modified = True
continue

if not isinstance(node.op, RZZGate):
continue

angle = node.op.params[0]
if isinstance(angle, ParameterExpression) or 0 <= angle <= pi / 2:
# Angle is an unbound parameter or a calibrated value.
continue

# Modify circuit around Rzz gate to address non-ISA angles.
modified = True
wrap_angle = np.angle(np.exp(1j * angle))
if 0 <= wrap_angle <= pi / 2:
# In the first quadrant.
replace = self._quad1(wrap_angle, node.qargs)
elif pi / 2 < wrap_angle <= pi:
# In the second quadrant.
replace = self._quad2(wrap_angle, node.qargs)
elif -pi <= wrap_angle <= -pi / 2:
# In the third quadrant.
replace = self._quad3(wrap_angle, node.qargs)
elif -pi / 2 < wrap_angle < 0:
# In the forth quadrant.
replace = self._quad4(wrap_angle, node.qargs)
else:
raise RuntimeError("Unreacheable.")
if pi < angle % (4 * pi) < 3 * pi:
replace.apply_operation_back(GlobalPhaseGate(pi))
dag.substitute_node_with_dag(node, replace)
return modified

@staticmethod
def _quad1(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit:
"""Handle angle between [0, pi/2].
Circuit is not transformed - the Rzz gate is calibrated for the angle.
Returns:
A new dag with the same Rzz gate.
"""
new_dag = DAGCircuit()
new_dag.add_qubits(qubits=qubits)
new_dag.apply_operation_back(
RZZGate(angle),
qargs=qubits,
check=False,
)
return new_dag

@staticmethod
def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit:
"""Handle angle between (pi/2, pi].
Circuit is transformed into the following form:
┌───────┐┌───┐ ┌───┐
q_0: ┤ Rz(π) ├┤ X ├─■──────────┤ X ├
├───────┤└───┘ │ZZ(π - θ) └───┘
q_1: ┤ Rz(π) ├──────■───────────────
└───────┘
Returns:
New dag to replace Rzz gate.
"""
new_dag = DAGCircuit()
new_dag.add_qubits(qubits=qubits)
new_dag.apply_operation_back(GlobalPhaseGate(pi / 2))
new_dag.apply_operation_back(
RZGate(pi),
qargs=(qubits[0],),
cargs=(),
check=False,
)
new_dag.apply_operation_back(
RZGate(pi),
qargs=(qubits[1],),
check=False,
)
if not np.isclose(new_angle := (pi - angle), 0.0):
new_dag.apply_operation_back(
XGate(),
qargs=(qubits[0],),
check=False,
)
new_dag.apply_operation_back(
RZZGate(new_angle),
qargs=qubits,
check=False,
)
new_dag.apply_operation_back(
XGate(),
qargs=(qubits[0],),
check=False,
)
return new_dag

@staticmethod
def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit:
"""Handle angle between [-pi, -pi/2].
Circuit is transformed into following form:
┌───────┐
q_0: ┤ Rz(π) ├─■───────────────
├───────┤ │ZZ(π - Abs(θ))
q_1: ┤ Rz(π) ├─■───────────────
└───────┘
Returns:
New dag to replace Rzz gate.
"""
new_dag = DAGCircuit()
new_dag.add_qubits(qubits=qubits)
new_dag.apply_operation_back(GlobalPhaseGate(-pi / 2))
new_dag.apply_operation_back(
RZGate(pi),
qargs=(qubits[0],),
check=False,
)
new_dag.apply_operation_back(
RZGate(pi),
qargs=(qubits[1],),
check=False,
)
if not np.isclose(new_angle := (pi - np.abs(angle)), 0.0):
new_dag.apply_operation_back(
RZZGate(new_angle),
qargs=qubits,
check=False,
)
return new_dag

@staticmethod
def _quad4(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit:
"""Handle angle between (-pi/2, 0).
Circuit is transformed into following form:
┌───┐ ┌───┐
q_0: ┤ X ├─■───────────┤ X ├
└───┘ │ZZ(Abs(θ)) └───┘
q_1: ──────■────────────────
Returns:
New dag to replace Rzz gate.
"""
new_dag = DAGCircuit()
new_dag.add_qubits(qubits=qubits)
new_dag.apply_operation_back(
XGate(),
qargs=(qubits[0],),
check=False,
)
new_dag.apply_operation_back(
RZZGate(abs(angle)),
qargs=qubits,
check=False,
)
new_dag.apply_operation_back(
XGate(),
qargs=(qubits[0],),
check=False,
)
return new_dag
41 changes: 40 additions & 1 deletion qiskit_ibm_runtime/transpiler/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
from qiskit.transpiler.preset_passmanagers import common
from qiskit.transpiler.passes import ConvertConditionsToIfOps

from qiskit_ibm_runtime.transpiler.passes.basis.convert_id_to_delay import (
from qiskit_ibm_runtime.transpiler.passes.basis import (
ConvertIdToDelay,
FoldRzzAngle,
)


Expand Down Expand Up @@ -96,3 +97,41 @@ def pass_manager(
plugin_passes += [ConvertConditionsToIfOps()]

return PassManager(plugin_passes) + translator_pm


class IBMFractionalTranslationPlugin(PassManagerStagePlugin):
"""A translation stage plugin for targeting Qiskit circuits
to IBM Quantum systems with fractional gate support.
Currently coexistence of fractional gate operations and
dynamic circuits is not assumed.
"""

def pass_manager(
self,
pass_manager_config: PassManagerConfig,
optimization_level: Optional[int] = None,
) -> PassManager:
"""Build IBMTranslationPlugin PassManager."""

translator_pm = common.generate_translation_passmanager(
target=pass_manager_config.target,
basis_gates=pass_manager_config.basis_gates,
approximation_degree=pass_manager_config.approximation_degree,
coupling_map=pass_manager_config.coupling_map,
backend_props=pass_manager_config.backend_properties,
unitary_synthesis_method=pass_manager_config.unitary_synthesis_method,
unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config,
hls_config=pass_manager_config.hls_config,
)

instruction_durations = pass_manager_config.instruction_durations
pre_passes = []
post_passes = []
target = pass_manager_config.target or pass_manager_config.basis_gates
if instruction_durations and not "id" in target:
pre_passes.append(ConvertIdToDelay(instruction_durations))
if "rzz" in target:
# Apply this pass after SU4 is translated.
post_passes.append(FoldRzzAngle())
return PassManager(pre_passes) + translator_pm + PassManager(post_passes)
9 changes: 9 additions & 0 deletions release-notes/unreleased/2043.feat.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Added a new transpiler translation plugin :class:`~.IBMFractionalTranslationPlugin`
and a pass :class:`~.FoldRzzAngle`.
This plugin is automatically applied for backends
retrieved with the ``use_fractional_gates`` opt-in,
and the folding pass is added when the backend target includes the ``RZZ`` gate.

The new pass modifies the input quantum circuit, so that all ``RZZ`` gates in the
circuit have an angle parameter within [0, pi/2] which is supported
by IBM Quantum processors.
Loading

0 comments on commit d88028b

Please sign in to comment.