Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New OptimizeAnnotated transpiler pass #11476

Merged
merged 10 commits into from
Jan 30, 2024
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
ResetAfterMeasureSimplification
OptimizeCliffords
NormalizeRXAngle
OptimizeAnnotated

Calibration
=============
Expand Down Expand Up @@ -233,6 +234,7 @@
from .optimization import ResetAfterMeasureSimplification
from .optimization import OptimizeCliffords
from .optimization import NormalizeRXAngle
from .optimization import OptimizeAnnotated

# circuit analysis
from .analysis import ResourceEstimation
Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/optimization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
210 changes: 210 additions & 0 deletions qiskit/transpiler/passes/optimization/optimize_annotated.py
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think having an explicit option to recurse into definitions is useful or not? Right now it's only done implicitly based on the presence of a Target or basis gates. But do you think there is value in having an override for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about this, such an option could definitely be useful and is added in fb7e279.

The current logic is the following (please see if you agree with this):

  • If neither basis_gates nor target is specified, then we do not recursively descent into gate definitions, regardless of the recurse option. This is consistent with the behavior of HighLevelSynthesis pass (and previously of UnrollCustomDefinitions pass) which in this case does not descend into the definitions either, i.e. we shouldn't optimize what we are not going to synthesize.
  • If either basis_gates or target is specified, and recurse has the default value of True, then we do the recursion.
  • But we can override the recursion by setting recurse to False.

This also ties in to the question of where this pass should sit in the transpilation pipeline. I was also thinking about the init stage (just before running high level synthesis). Alternatively, if you think that having yet another recursive pass would be slow, we could use it from within HighLevelSynthesis itself, i.e. when HighLevelSynthesis needs to synthesize a DAGCircuit (possibly as part of its own recursion), it can first call OptimizeAnnotated on this dag circuit with recurse=False. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this logic makes sense to me. I think leaving this as a standalone pass makes sense, just as it seems like a slightly different building block in a pipeline that high level synthesis. However, I do agree there is probably some concern around performance if we're going to run with recurse=True in the init stage and I like your suggestion of calling it internally when it makes sense. I think when we investigate integrating this into the preset pass managers as part of 1.1.0 we can look at all those details.

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
alexanderivrii marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -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.