-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Add ElidePermutations pass to optimization level 3 #12111
Changes from 48 commits
e90bdc7
caa6a73
e757164
438e0a9
8f699e8
8dfd4f7
3b28f06
ec8af2d
599bc67
ad3697c
1d30a65
2f13d9d
5688293
955dec3
a364c08
695f35b
39fd3bb
1ea19ee
95e7c11
1d37611
b27e52e
9ece52c
0f1df15
1aba9fc
723fd42
11b4156
8ff19a6
40f4c4e
87ce691
c1fd952
33e867d
6f0f679
f698977
31b23f2
32cc904
960f592
e978874
b3c6252
80cd91a
cc621f1
33ac77f
ddfde6a
aaa3365
15e9809
2b203e1
7a90b70
9edf432
c935fea
6649551
76ee567
0c6eece
032039c
a2de408
efe602d
90f20e7
83e52ff
b4ced45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,6 +81,9 @@ def __init__( | |
a Numpy array of shape (2**N, 2**N) qubit systems will be used. If | ||
the input operator is not an N-qubit operator, it will assign a | ||
single subsystem with dimension specified by the shape of the input. | ||
Note that two operators initialized via this method are only considered equivalent if they | ||
match up to their canonical qubit order (or: permutation). See :meth:`.Operator.from_circuit` | ||
to specify a different qubit permutation. | ||
""" | ||
op_shape = None | ||
if isinstance(data, (list, np.ndarray)): | ||
|
@@ -391,8 +394,7 @@ def from_circuit( | |
Returns: | ||
Operator: An operator representing the input circuit | ||
""" | ||
dimension = 2**circuit.num_qubits | ||
op = cls(np.eye(dimension)) | ||
|
||
if layout is None: | ||
if not ignore_set_layout: | ||
layout = getattr(circuit, "_layout", None) | ||
|
@@ -403,27 +405,38 @@ def from_circuit( | |
initial_layout=layout, | ||
input_qubit_mapping={qubit: index for index, qubit in enumerate(circuit.qubits)}, | ||
) | ||
|
||
initial_layout = layout.initial_layout if layout is not None else None | ||
|
||
if final_layout is None: | ||
if not ignore_set_layout and layout is not None: | ||
final_layout = getattr(layout, "final_layout", None) | ||
|
||
qargs = None | ||
# If there was a layout specified (either from the circuit | ||
# or via user input) use that to set qargs to permute qubits | ||
# based on that layout | ||
if layout is not None: | ||
physical_to_virtual = layout.initial_layout.get_physical_bits() | ||
qargs = [ | ||
layout.input_qubit_mapping[physical_to_virtual[physical_bit]] | ||
for physical_bit in range(len(physical_to_virtual)) | ||
] | ||
# Convert circuit to an instruction | ||
instruction = circuit.to_instruction() | ||
op._append_instruction(instruction, qargs=qargs) | ||
# If final layout is set permute output indices based on layout | ||
from qiskit.synthesis.permutation.permutation_utils import _inverse_pattern | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kind of nit: Since we are always going to execute this line of code, shouldn't this be at the top of the function? Right after the docstring? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see. But my concern is more towards moving this to top of the function |
||
|
||
if initial_layout is not None: | ||
input_qubits = [None] * len(layout.input_qubit_mapping) | ||
for q, p in layout.input_qubit_mapping.items(): | ||
input_qubits[p] = q | ||
|
||
initial_permutation = initial_layout.to_permutation(input_qubits) | ||
initial_permutation_inverse = _inverse_pattern(initial_permutation) | ||
|
||
if final_layout is not None: | ||
perm_pattern = [final_layout._v2p[v] for v in circuit.qubits] | ||
op = op.apply_permutation(perm_pattern, front=False) | ||
final_permutation = final_layout.to_permutation(circuit.qubits) | ||
final_permutation_inverse = _inverse_pattern(final_permutation) | ||
|
||
op = Operator(circuit) | ||
|
||
if initial_layout: | ||
op = op.apply_permutation(initial_permutation, True) | ||
|
||
if final_layout: | ||
op = op.apply_permutation(final_permutation_inverse, False) | ||
|
||
if initial_layout: | ||
op = op.apply_permutation(initial_permutation_inverse, False) | ||
|
||
return op | ||
|
||
def is_unitary(self, atol=None, rtol=None): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# This code is part of Qiskit. | ||
# | ||
# (C) Copyright IBM 2023 | ||
# | ||
# 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. | ||
|
||
|
||
"""Remove any swap gates in the circuit by pushing it through into a qubit permutation.""" | ||
|
||
import logging | ||
|
||
from qiskit.circuit.library.standard_gates import SwapGate | ||
from qiskit.circuit.library.generalized_gates import PermutationGate | ||
from qiskit.transpiler.basepasses import TransformationPass | ||
from qiskit.transpiler.layout import Layout | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ElidePermutations(TransformationPass): | ||
r"""Remove permutation operations from a pre-layout circuit | ||
|
||
This pass is intended to be run before a layout (mapping virtual qubits | ||
to physical qubits) is set during the transpilation pipeline. This | ||
pass iterates over the :class:`~.DAGCircuit` and when a :class:`~.SwapGate` | ||
or :class:`~.PermutationGate` are encountered it permutes the virtual qubits in | ||
the circuit and removes the swap gate. This will effectively remove any | ||
:class:`~SwapGate`\s or :class:`~PermutationGate` in the circuit prior to running | ||
layout. If this pass is run after a layout has been set it will become a no-op | ||
(and log a warning) as this optimization is not sound after physical qubits are | ||
selected and there are connectivity constraints to adhere to. | ||
|
||
For tracking purposes this pass sets 3 values in the property set if there | ||
are any :class:`~.SwapGate` or :class:`~.PermutationGate` objects in the circuit | ||
and the pass isn't a no-op. | ||
|
||
* ``original_layout``: The trivial :class:`~.Layout` for the input to this pass being run | ||
* ``original_qubit_indices``: The mapping of qubit objects to positional indices for the state | ||
of the circuit as input to this pass. | ||
* ``virtual_permutation_layout``: A :class:`~.Layout` object mapping input qubits to the output | ||
state after eliding permutations. | ||
|
||
These three properties are needed for the transpiler to track the permutations in the out | ||
:attr:`.QuantumCircuit.layout` attribute. The elision of permutations is equivalent to a | ||
``final_layout`` set by routing and all three of these attributes are needed in the case | ||
""" | ||
|
||
def run(self, dag): | ||
"""Run the ElidePermutations pass on ``dag``. | ||
|
||
Args: | ||
dag (DAGCircuit): the DAG to be optimized. | ||
|
||
Returns: | ||
DAGCircuit: the optimized DAG. | ||
""" | ||
if self.property_set["layout"] is not None: | ||
logger.warning( | ||
"ElidePermutations is not valid after a layout has been set. This indicates " | ||
"an invalid pass manager construction." | ||
) | ||
return dag | ||
|
||
op_count = dag.count_ops() | ||
if op_count.get("swap", 0) == 0 and op_count.get("permutation", 0) == 0: | ||
return dag | ||
|
||
new_dag = dag.copy_empty_like() | ||
qubit_mapping = list(range(len(dag.qubits))) | ||
|
||
def _apply_mapping(qargs): | ||
return tuple(dag.qubits[qubit_mapping[dag.find_bit(qubit).index]] for qubit in qargs) | ||
|
||
for node in dag.topological_op_nodes(): | ||
if not isinstance(node.op, (SwapGate, PermutationGate)): | ||
new_dag.apply_operation_back(node.op, _apply_mapping(node.qargs), node.cargs) | ||
elif getattr(node.op, "condition", None) is not None: | ||
new_dag.apply_operation_back(node.op, _apply_mapping(node.qargs), node.cargs) | ||
elif isinstance(node.op, SwapGate): | ||
index_0 = dag.find_bit(node.qargs[0]).index | ||
index_1 = dag.find_bit(node.qargs[1]).index | ||
qubit_mapping[index_1], qubit_mapping[index_0] = ( | ||
qubit_mapping[index_0], | ||
qubit_mapping[index_1], | ||
) | ||
elif isinstance(node.op, PermutationGate): | ||
starting_indices = [qubit_mapping[dag.find_bit(qarg).index] for qarg in node.qargs] | ||
pattern = node.op.params[0] | ||
pattern_indices = [qubit_mapping[idx] for idx in pattern] | ||
for i, j in zip(starting_indices, pattern_indices): | ||
qubit_mapping[i] = j | ||
input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} | ||
self.property_set["original_layout"] = Layout(input_qubit_mapping) | ||
if self.property_set["original_qubit_indices"] is None: | ||
self.property_set["original_qubit_indices"] = input_qubit_mapping | ||
# ToDo: check if this exists; then compose | ||
self.property_set["virtual_permutation_layout"] = Layout( | ||
{dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)} | ||
) | ||
return new_dag |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# 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. | ||
|
||
|
||
"""Finalize layout-related attributes.""" | ||
|
||
import logging | ||
|
||
from qiskit.transpiler.basepasses import AnalysisPass | ||
from qiskit.transpiler.layout import Layout | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class FinalizeLayouts(AnalysisPass): | ||
"""Finalize 'layout' and 'final_layout' attributes, taking 'virtual_permutation_layout' | ||
into account when exists. | ||
""" | ||
|
||
def run(self, dag): | ||
"""Run the FinalizeLayouts pass on ``dag``. | ||
|
||
Args: | ||
dag (DAGCircuit): the DAG circuit. | ||
""" | ||
|
||
if ( | ||
virtual_permutation_layout := self.property_set.get("virtual_permutation_layout", None) | ||
) is None: | ||
return | ||
|
||
self.property_set.pop("virtual_permutation_layout") | ||
|
||
# virtual_permutation_layout is usually created before extending the layout with ancillas, | ||
# so we extend the permutation to be identity on ancilla qubits | ||
original_qubit_indices = self.property_set.get("original_qubit_indices", None) | ||
for oq in original_qubit_indices: | ||
if oq not in virtual_permutation_layout: | ||
virtual_permutation_layout[oq] = original_qubit_indices[oq] | ||
|
||
t_qubits = dag.qubits | ||
|
||
if (t_initial_layout := self.property_set.get("layout", None)) is None: | ||
t_initial_layout = Layout(dict(enumerate(t_qubits))) | ||
|
||
if (t_final_layout := self.property_set.get("final_layout", None)) is None: | ||
t_final_layout = Layout(dict(enumerate(t_qubits))) | ||
|
||
# Ordered list of original qubits | ||
original_qubits_reverse = {v: k for k, v in original_qubit_indices.items()} | ||
original_qubits = [] | ||
for i in range(len(original_qubits_reverse)): | ||
original_qubits.append(original_qubits_reverse[i]) | ||
|
||
virtual_permutation_layout_inv = virtual_permutation_layout.inverse( | ||
original_qubits, original_qubits | ||
) | ||
|
||
t_initial_layout_inv = t_initial_layout.inverse(original_qubits, t_qubits) | ||
|
||
# ToDo: this can possibly be made simpler | ||
new_final_layout = t_initial_layout_inv | ||
new_final_layout = new_final_layout.compose(virtual_permutation_layout_inv, original_qubits) | ||
new_final_layout = new_final_layout.compose(t_initial_layout, original_qubits) | ||
new_final_layout = new_final_layout.compose(t_final_layout, t_qubits) | ||
|
||
self.property_set["layout"] = t_initial_layout | ||
self.property_set["final_layout"] = new_final_layout |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,7 @@ | |
from qiskit.transpiler.passes import PulseGates | ||
from qiskit.transpiler.passes import ContainsInstruction | ||
from qiskit.transpiler.passes import VF2PostLayout | ||
from qiskit.transpiler.passes import FinalizeLayouts | ||
from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason | ||
from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason | ||
from qiskit.transpiler.exceptions import TranspilerError | ||
|
@@ -407,6 +408,13 @@ def _direction_condition(property_set): | |
return pre_opt | ||
|
||
|
||
def generate_post_op_passmanager(): | ||
"""Generates the post-optimization pass manager.""" | ||
out = PassManager() | ||
out.append(FinalizeLayouts()) | ||
return out | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Highly unimportant, but just to point out that this can also be spelled (No need to change it, just commenting in case it's shorter for you in the future.) |
||
|
||
|
||
def generate_translation_passmanager( | ||
target, | ||
basis_gates=None, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,6 +107,8 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa | |
"scheduling", scheduling_method, pass_manager_config, optimization_level=3 | ||
) | ||
|
||
post_optimization = common.generate_post_op_passmanager() | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we ought to include the layout finalisation in every optimisation level, even if by our presets it won't have any work to do. It just helps catch the case of somebody using (say) an O1 pass manager but injecting Since we don't have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since #9523 moved the responsibility of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, this is much-much cleaner than having a separate |
||
return StagedPassManager( | ||
pre_init=pre_init, | ||
init=init, | ||
|
@@ -115,5 +117,6 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa | |
translation=translation, | ||
pre_optimization=pre_optimization, | ||
optimization=optimization, | ||
post_optimization=post_optimization, | ||
scheduling=sched, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
features: | ||
- | | ||
The transpiler passes :class:`~.ElidePermutations` and :class:`~.FinalizeLayouts` | ||
run by default with optimization level 3. Intuitively, removing | ||
:class:`~.SwapGate`\s and :class:`~qiskit.circuit.library.PermutationGate`\s | ||
in a virtual circuit is almost always beneficial, as it makes the circuit shorter | ||
and easier to route. As :class:`~.OptimizeSwapBeforeMeasure` is a special case | ||
of :class:`~.ElidePermutations`, it has been removed from optimization level 3. |
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.
nit: We can rewrite this as
inital_layout = layout and layout.initial_layout
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.
Your way is also valid, so it's mostly just a question of preference.
is not None
checks are very explicit about what's being expected, whereas the implicit truthiness and pass-through of Python's Boolean expressions is maybe a little less well-known (and technically allows more things to pass through the false-y test without an error).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.
Thats understandable. Fine with keeping it this way for code readability 👍