-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
e56018d
First installment to transpiler pass that optimizes annotated operati…
alexanderivrii 14cac72
supporting control flow
alexanderivrii 0841f75
lint fixes
alexanderivrii bd550bd
release notes
alexanderivrii d724aaa
suggestions from code review
alexanderivrii 945de26
option to override recursion
alexanderivrii e67f37b
fast return and style
alexanderivrii 1b390e5
fast return only when no need to recurse
alexanderivrii fb7e279
adding test for recurse=False
alexanderivrii f911c52
docs and reno improvements
alexanderivrii File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
210 changes: 210 additions & 0 deletions
210
qiskit/transpiler/passes/optimization/optimize_annotated.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
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 |
67 changes: 67 additions & 0 deletions
67
releasenotes/notes/add-optimize-annotated-pass-89ca1823e7109f81.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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):
basis_gates
nortarget
is specified, then we do not recursively descent into gate definitions, regardless of therecurse
option. This is consistent with the behavior ofHighLevelSynthesis
pass (and previously ofUnrollCustomDefinitions
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.basis_gates
ortarget
is specified, andrecurse
has the default value ofTrue
, then we do the recursion.recurse
toFalse
.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 withinHighLevelSynthesis
itself, i.e. whenHighLevelSynthesis
needs to synthesize aDAGCircuit
(possibly as part of its own recursion), it can first callOptimizeAnnotated
on this dag circuit withrecurse=False
. What do you think?There was a problem hiding this comment.
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 theinit
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.