diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 297588f69d36..d6451bdb7973 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -87,6 +87,7 @@ ResetAfterMeasureSimplification OptimizeCliffords NormalizeRXAngle + OptimizeAnnotated Calibration ============= @@ -232,6 +233,7 @@ from .optimization import ResetAfterMeasureSimplification from .optimization import OptimizeCliffords from .optimization import NormalizeRXAngle +from .optimization import OptimizeAnnotated # circuit analysis from .analysis import ResourceEstimation diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 0cb49abe61ed..84e81bfebdef 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -35,3 +35,4 @@ from .optimize_cliffords import OptimizeCliffords from .collect_cliffords import CollectCliffords from .normalize_rx_angle import NormalizeRXAngle +from .optimize_annotated import OptimizeAnnotated diff --git a/qiskit/transpiler/passes/optimization/optimize_annotated.py b/qiskit/transpiler/passes/optimization/optimize_annotated.py new file mode 100644 index 000000000000..65d06436cc5c --- /dev/null +++ b/qiskit/transpiler/passes/optimization/optimize_annotated.py @@ -0,0 +1,210 @@ +# 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. + +"""Optimize annotated operations on a circuit.""" + +from typing import Optional, List, Tuple + +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES +from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.circuit.annotated_operation import AnnotatedOperation, _canonicalize_modifiers +from qiskit.circuit import EquivalenceLibrary, ControlledGate, Operation, ControlFlowOp +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passes.utils import control_flow +from qiskit.transpiler.target import Target +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.exceptions import TranspilerError + + +class OptimizeAnnotated(TransformationPass): + """Optimization pass on circuits with annotated operations. + + Implemented optimizations: + + * For each annotated operation, converting the list of its modifiers to a canonical form. + For example, consecutively applying ``inverse()``, ``control(2)`` and ``inverse()`` + is equivalent to applying ``control(2)``. + + * Removing annotations when possible. + For example, ``AnnotatedOperation(SwapGate(), [InverseModifier(), InverseModifier()])`` + is equivalent to ``SwapGate()``. + + * Recursively combining annotations. + For example, if ``g1 = AnnotatedOperation(SwapGate(), InverseModifier())`` and + ``g2 = AnnotatedOperation(g1, ControlModifier(2))``, then ``g2`` can be replaced with + ``AnnotatedOperation(SwapGate(), [InverseModifier(), ControlModifier(2)])``. + + """ + + def __init__( + self, + target: Optional[Target] = None, + equivalence_library: Optional[EquivalenceLibrary] = None, + basis_gates: Optional[List[str]] = None, + recurse: bool = True, + ): + """ + OptimizeAnnotated initializer. + + Args: + target: Optional, the backend target to use for this pass. + equivalence_library: The equivalence library used + (instructions in this library will not be optimized by this pass). + basis_gates: Optional, target basis names to unroll to, e.g. `['u3', 'cx']` + (instructions in this list will not be optimized by this pass). + Ignored if ``target`` is also specified. + recurse: By default, when either ``target`` or ``basis_gates`` is specified, + the pass recursively descends into gate definitions (and the recursion is + not applied when neither is specified since such objects do not need to + be synthesized). Setting this value to ``False`` precludes the recursion in + every case. + """ + super().__init__() + + self._target = target + self._equiv_lib = equivalence_library + self._basis_gates = basis_gates + + self._top_level_only = not recurse or (self._basis_gates is None and self._target is None) + + if not self._top_level_only and self._target is None: + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} + self._device_insts = basic_insts | set(self._basis_gates) + + def run(self, dag: DAGCircuit): + """Run the OptimizeAnnotated pass on `dag`. + + Args: + dag: input dag. + + Returns: + Output dag with higher-level operations optimized. + + Raises: + TranspilerError: when something goes wrong. + + """ + dag, _ = self._run_inner(dag) + return dag + + def _run_inner(self, dag) -> Tuple[DAGCircuit, bool]: + """ + Optimizes annotated operations. + Returns True if did something. + """ + # Fast return + if self._top_level_only: + op_names = dag.count_ops(recurse=False) + if "annotated" not in op_names and not CONTROL_FLOW_OP_NAMES.intersection(op_names): + return dag, False + + # Handle control-flow + for node in dag.op_nodes(): + if isinstance(node.op, ControlFlowOp): + node.op = control_flow.map_blocks(self.run, node.op) + + # First, optimize every node in the DAG. + dag, opt1 = self._canonicalize(dag) + + opt2 = False + if not self._top_level_only: + # Second, recursively descend into definitions. + # Note that it is important to recurse only after the optimization methods have been run, + # as they may remove annotated gates. + dag, opt2 = self._recurse(dag) + + return dag, opt1 or opt2 + + def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]: + """ + Combines recursive annotated operations and canonicalizes modifiers. + Returns True if did something. + """ + + did_something = False + for node in dag.op_nodes(op=AnnotatedOperation): + modifiers = [] + cur = node.op + while isinstance(cur, AnnotatedOperation): + modifiers.extend(cur.modifiers) + cur = cur.base_op + canonical_modifiers = _canonicalize_modifiers(modifiers) + if len(canonical_modifiers) > 0: + # this is still an annotated operation + node.op.base_op = cur + node.op.modifiers = canonical_modifiers + else: + # no need for annotated operations + node.op = cur + did_something = True + return dag, did_something + + def _recursively_process_definitions(self, op: Operation) -> bool: + """ + Recursively applies optimizations to op's definition (or to op.base_op's + definition if op is an annotated operation). + Returns True if did something. + """ + + # If op is an annotated operation, we descend into its base_op + if isinstance(op, AnnotatedOperation): + return self._recursively_process_definitions(op.base_op) + + # Similar to HighLevelSynthesis transpiler pass, we do not recurse into a gate's + # `definition` for a gate that is supported by the target or in equivalence library. + + controlled_gate_open_ctrl = isinstance(op, ControlledGate) and op._open_ctrl + if not controlled_gate_open_ctrl: + inst_supported = ( + self._target.instruction_supported(operation_name=op.name) + if self._target is not None + else op.name in self._device_insts + ) + if inst_supported or (self._equiv_lib is not None and self._equiv_lib.has_entry(op)): + return False + + try: + # extract definition + definition = op.definition + except TypeError as err: + raise TranspilerError( + f"OptimizeAnnotated was unable to extract definition for {op.name}: {err}" + ) from err + except AttributeError: + # definition is None + definition = None + + if definition is None: + raise TranspilerError(f"OptimizeAnnotated was unable to optimize {op}.") + + definition_dag = circuit_to_dag(definition, copy_operations=False) + definition_dag, opt = self._run_inner(definition_dag) + + if opt: + # We only update a gate's definition if it was actually changed. + # This is important to preserve non-annotated singleton gates. + op.definition = dag_to_circuit(definition_dag) + + return opt + + def _recurse(self, dag) -> Tuple[DAGCircuit, bool]: + """ + Recursively handles gate definitions. + Returns True if did something. + """ + did_something = False + + for node in dag.op_nodes(): + opt = self._recursively_process_definitions(node.op) + did_something = did_something or opt + + return dag, did_something diff --git a/releasenotes/notes/add-optimize-annotated-pass-89ca1823e7109f81.yaml b/releasenotes/notes/add-optimize-annotated-pass-89ca1823e7109f81.yaml new file mode 100644 index 000000000000..8f06124b99fa --- /dev/null +++ b/releasenotes/notes/add-optimize-annotated-pass-89ca1823e7109f81.yaml @@ -0,0 +1,67 @@ +--- +features: + - | + Added a new transpiler pass, :class:`.OptimizeAnnotated` that optimizes annotated + operations on a quantum circuit. + + Consider the following example:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + InverseModifier, + ControlModifier, + ) + from qiskit.circuit.library import CXGate, SwapGate + from qiskit.transpiler.passes import OptimizeAnnotated + + # Create a quantum circuit with multiple annotated gates + gate1 = AnnotatedOperation( + SwapGate(), + [InverseModifier(), ControlModifier(2), InverseModifier(), ControlModifier(1)], + ) + gate2 = AnnotatedOperation( + SwapGate(), + [InverseModifier(), InverseModifier()] + ) + gate3 = AnnotatedOperation( + AnnotatedOperation(CXGate(), ControlModifier(2)), + ControlModifier(1) + ) + qc = QuantumCircuit(6) + qc.append(gate1, [3, 2, 4, 0, 5]) + qc.append(gate2, [1, 5]) + qc.append(gate3, [5, 4, 3, 2, 1]) + + # Optimize the circuit using OptimizeAnnotated transpiler pass + qc_optimized = OptimizeAnnotated()(qc) + + # This is how the optimized circuit should look like + gate1_expected = AnnotatedOperation(SwapGate(), ControlModifier(3)) + gate2_expected = SwapGate() + gate3_expected = AnnotatedOperation(CXGate(), ControlModifier(3)) + qc_expected = QuantumCircuit(6) + qc_expected.append(gate1_expected, [3, 2, 4, 0, 5]) + qc_expected.append(gate2_expected, [1, 5]) + qc_expected.append(gate3_expected, [5, 4, 3, 2, 1]) + + assert qc_optimized == qc_expected + + In the case of ``gate1``, the modifiers of the annotated swap gate are brought + into the canonical form: the two ``InverseModifier`` s cancel out, and the two + ``ControlModifier`` s are combined. In the case of ``gate2``, all the modifiers + get removed and the annotated operation is replaced by its base operation. + In the case of ``gate3``, multiple layers of annotations are combined into one. + + The constructor of :class:`.OptimizeAnnotated` pass accepts optional + arguments ``target``, ``equivalence_library``, ``basis_gates`` and ``recurse``. + When ``recurse`` is ``True`` (the default value) and when either ``target`` + or ``basis_gates`` are specified, the pass recursively descends into the gates + ``definition`` circuits, with the exception of gates that are already supported + by the target or that belong to the equivalence library. On the other hand, when + neither ``target`` nor ``basis_gates`` are specified, + or when ``recurse`` is set to ``False``, + the pass synthesizes only the "top-level" annotated operations, i.e. does not + recursively descend into the ``definition`` circuits. This behavior is consistent + with that of :class:`.HighLevelSynthesis` transpiler pass that needs to be called + in order to "unroll" the annotated operations into 1-qubit and 2-qubits gates. diff --git a/test/python/transpiler/test_optimize_annotated.py b/test/python/transpiler/test_optimize_annotated.py new file mode 100644 index 000000000000..2c80849e513f --- /dev/null +++ b/test/python/transpiler/test_optimize_annotated.py @@ -0,0 +1,195 @@ +# 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. + +"""Test OptimizeAnnotated pass""" + +from qiskit.circuit import QuantumCircuit, Gate +from qiskit.circuit.library import SwapGate, CXGate +from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + ControlModifier, + InverseModifier, + PowerModifier, +) +from qiskit.transpiler.passes import OptimizeAnnotated +from qiskit.test import QiskitTestCase + + +class TestOptimizeSwapBeforeMeasure(QiskitTestCase): + """Test optimizations related to annotated operations.""" + + def test_combine_modifiers(self): + """Test that the pass correctly combines modifiers.""" + gate1 = AnnotatedOperation( + SwapGate(), + [ + InverseModifier(), + ControlModifier(2), + PowerModifier(4), + InverseModifier(), + ControlModifier(1), + PowerModifier(-0.5), + ], + ) + gate2 = AnnotatedOperation(SwapGate(), [InverseModifier(), InverseModifier()]) + gate3 = AnnotatedOperation( + AnnotatedOperation(CXGate(), ControlModifier(2)), ControlModifier(1) + ) + gate4 = AnnotatedOperation( + AnnotatedOperation(SwapGate(), InverseModifier()), InverseModifier() + ) + gate5 = CXGate() + + gate1_expected = AnnotatedOperation( + SwapGate(), [InverseModifier(), PowerModifier(2), ControlModifier(3)] + ) + gate2_expected = SwapGate() + gate3_expected = AnnotatedOperation(CXGate(), ControlModifier(3)) + gate4_expected = SwapGate() + gate5_expected = CXGate() + + qc = QuantumCircuit(6) + qc.append(gate1, [3, 2, 4, 0, 5]) + qc.append(gate2, [1, 5]) + qc.append(gate3, [5, 4, 3, 2, 1]) + qc.append(gate4, [1, 2]) + qc.append(gate5, [4, 2]) + + qc_optimized = OptimizeAnnotated()(qc) + + qc_expected = QuantumCircuit(6) + qc_expected.append(gate1_expected, [3, 2, 4, 0, 5]) + qc_expected.append(gate2_expected, [1, 5]) + qc_expected.append(gate3_expected, [5, 4, 3, 2, 1]) + qc_expected.append(gate4_expected, [1, 2]) + qc_expected.append(gate5_expected, [4, 2]) + + self.assertEqual(qc_optimized, qc_expected) + + def test_optimize_definitions(self): + """Test that the pass descends into gate definitions when basis_gates are defined.""" + qc_def = QuantumCircuit(3) + qc_def.cx(0, 2) + qc_def.append(AnnotatedOperation(CXGate(), [InverseModifier(), InverseModifier()]), [0, 1]) + qc_def.append( + AnnotatedOperation( + SwapGate(), [InverseModifier(), ControlModifier(1), InverseModifier()] + ), + [0, 1, 2], + ) + + expected_qc_def_optimized = QuantumCircuit(3) + expected_qc_def_optimized.cx(0, 2) + expected_qc_def_optimized.cx(0, 1) + expected_qc_def_optimized.append( + AnnotatedOperation(SwapGate(), ControlModifier(1)), [0, 1, 2] + ) + + gate = Gate("custom_gate", 3, []) + gate.definition = qc_def + + qc = QuantumCircuit(4) + qc.h(0) + qc.append(gate, [0, 1, 3]) + + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + self.assertEqual(qc_optimized[1].operation.definition, expected_qc_def_optimized) + + def test_do_not_optimize_definitions_without_basis_gates(self): + """ + Test that the pass does not descend into gate definitions when neither the + target nor basis_gates are defined. + """ + qc_def = QuantumCircuit(3) + qc_def.cx(0, 2) + qc_def.append(AnnotatedOperation(CXGate(), [InverseModifier(), InverseModifier()]), [0, 1]) + qc_def.append( + AnnotatedOperation( + SwapGate(), [InverseModifier(), ControlModifier(1), InverseModifier()] + ), + [0, 1, 2], + ) + + gate = Gate("custom_gate", 3, []) + gate.definition = qc_def + + qc = QuantumCircuit(4) + qc.h(0) + qc.append(gate, [0, 1, 3]) + + qc_optimized = OptimizeAnnotated()(qc) + self.assertEqual(qc_optimized[1].operation.definition, qc_def) + + def test_do_not_optimize_definitions_without_recurse(self): + """ + Test that the pass does not descend into gate definitions when recurse is + False. + """ + qc_def = QuantumCircuit(3) + qc_def.cx(0, 2) + qc_def.append(AnnotatedOperation(CXGate(), [InverseModifier(), InverseModifier()]), [0, 1]) + qc_def.append( + AnnotatedOperation( + SwapGate(), [InverseModifier(), ControlModifier(1), InverseModifier()] + ), + [0, 1, 2], + ) + + gate = Gate("custom_gate", 3, []) + gate.definition = qc_def + + qc = QuantumCircuit(4) + qc.h(0) + qc.append(gate, [0, 1, 3]) + + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"], recurse=False)(qc) + self.assertEqual(qc_optimized[1].operation.definition, qc_def) + + def test_if_else(self): + """Test optimizations with if-else block.""" + + true_body = QuantumCircuit(3) + true_body.h(0) + true_body.append( + AnnotatedOperation(CXGate(), [InverseModifier(), InverseModifier()]), [0, 1] + ) + false_body = QuantumCircuit(3) + false_body.append( + AnnotatedOperation( + SwapGate(), [InverseModifier(), ControlModifier(1), InverseModifier()] + ), + [0, 1, 2], + ) + + qc = QuantumCircuit(3, 1) + qc.h(0) + qc.measure(0, 0) + qc.if_else((0, True), true_body, false_body, [0, 1, 2], []) + + qc_optimized = OptimizeAnnotated()(qc) + + expected_true_body_optimized = QuantumCircuit(3) + expected_true_body_optimized.h(0) + expected_true_body_optimized.append(CXGate(), [0, 1]) + expected_false_body_optimized = QuantumCircuit(3) + expected_false_body_optimized.append( + AnnotatedOperation(SwapGate(), ControlModifier(1)), [0, 1, 2] + ) + + expected_qc = QuantumCircuit(3, 1) + expected_qc.h(0) + expected_qc.measure(0, 0) + expected_qc.if_else( + (0, True), expected_true_body_optimized, expected_false_body_optimized, [0, 1, 2], [] + ) + + self.assertEqual(qc_optimized, expected_qc)