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

Add ElidePermutations transpiler pass #9523

Merged
merged 55 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
e90bdc7
Add ElideSwaps transpiler pass
mtreinish Feb 2, 2023
caa6a73
Update tests with optimization level 3
mtreinish Feb 2, 2023
e757164
Pass final layout from ElideSwap to output
mtreinish Feb 2, 2023
438e0a9
Move pass in opt level 3 earlier in stage and skip with explicit layout
mtreinish Feb 2, 2023
8f699e8
Doc and copy paste fixes
mtreinish Feb 2, 2023
8dfd4f7
Expand test coverage
mtreinish Feb 2, 2023
3b28f06
Update permutation tracking
mtreinish Feb 2, 2023
ec8af2d
Merge branch 'main' into elide-swaps
mtreinish Feb 2, 2023
599bc67
Generalize pass to support PermutationGate too
mtreinish Feb 9, 2023
ad3697c
Merge remote-tracking branch 'origin/main' into elide-swaps
mtreinish Aug 22, 2023
1d30a65
Fix permutation handling
mtreinish Aug 22, 2023
2f13d9d
Fix formatting
mtreinish Aug 22, 2023
5688293
Fix final layout handling for no initial layout
mtreinish Aug 22, 2023
955dec3
Improve documentation and log a warning if run post layout
mtreinish Aug 22, 2023
a364c08
Fix final layout handling with no ElideSwaps being run
mtreinish Aug 22, 2023
695f35b
Fix docs build
mtreinish Aug 23, 2023
39fd3bb
Merge branch 'main' into elide-swaps
mtreinish Sep 20, 2023
1ea19ee
Merge branch 'main' into elide-swaps
mtreinish Oct 5, 2023
95e7c11
Merge remote-tracking branch 'origin/main' into elide-swaps
mtreinish Oct 9, 2023
1d37611
Fix release note
mtreinish Oct 9, 2023
b27e52e
Fix typo
mtreinish Oct 9, 2023
9ece52c
Merge remote-tracking branch 'origin/main' into elide-swaps
mtreinish Oct 13, 2023
0f1df15
Add test for routing and elide permutations
mtreinish Oct 13, 2023
1aba9fc
Apply suggestions from code review
mtreinish Oct 16, 2023
723fd42
Rename test file to test_elide_permutations.py
mtreinish Oct 16, 2023
11b4156
Merge remote-tracking branch 'origin/main' into elide-swaps
mtreinish Oct 16, 2023
8ff19a6
Apply suggestions from code review
mtreinish Oct 17, 2023
40f4c4e
Merge branch 'main' into elide-swaps
mtreinish Dec 11, 2023
87ce691
Merge branch 'main' into elide-swaps
mtreinish Jan 17, 2024
c1fd952
Merge remote-tracking branch 'origin/main' into elide-swaps
mtreinish Mar 21, 2024
33e867d
Fix test import after rebase
mtreinish Mar 21, 2024
6f0f679
fixing failing test cases
sbrandhsn Mar 21, 2024
f698977
addresses kehas comments - thx
sbrandhsn Mar 21, 2024
31b23f2
Adding FinalyzeLayouts pass to pull the virtual circuit permutation f…
alexanderivrii Mar 26, 2024
32cc904
formatting
alexanderivrii Mar 26, 2024
960f592
Merge branch 'main' into elide-swaps
alexanderivrii Mar 27, 2024
e978874
Merge branch 'main' into elide-swaps
alexanderivrii Mar 28, 2024
b3c6252
partial rebase on top of 12057 + tests
alexanderivrii Mar 28, 2024
80cd91a
also need test_operator for partial rebase
alexanderivrii Mar 28, 2024
cc621f1
Merge branch 'main' into elide-swaps
alexanderivrii Mar 31, 2024
33ac77f
removing from transpiler flow for now; reworking elide tests
alexanderivrii Mar 31, 2024
ddfde6a
also adding permutation gate to the example
alexanderivrii Mar 31, 2024
aaa3365
also temporarily reverting test_transpiler.py
alexanderivrii Mar 31, 2024
15e9809
update to release notes
alexanderivrii Mar 31, 2024
2b203e1
minor fixes
alexanderivrii Mar 31, 2024
ba49503
Merge branch 'main' into elide-swaps
ElePT Apr 4, 2024
bb80234
Merge branch 'main' into elide-swaps
mtreinish May 1, 2024
5888c70
Apply suggestions from code review
mtreinish May 1, 2024
07c2966
Fix lint
mtreinish May 1, 2024
e270aa8
Update qiskit/transpiler/passes/optimization/elide_permutations.py
mtreinish May 1, 2024
f5b6778
Add test to test we skip after layout
mtreinish May 1, 2024
5cbc67d
Merge branch 'main' into elide-swaps
mtreinish May 1, 2024
e755bcd
Integrate FinalizeLayouts into the PassManager harness
mtreinish May 2, 2024
28263bf
Compose a potential existing virtual_permutation_layout
mtreinish May 2, 2024
3c967c3
Remove unused import
mtreinish May 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@
EchoRZXWeylDecomposition
ResetAfterMeasureSimplification
OptimizeCliffords
ElidePermutations
NormalizeRXAngle
OptimizeAnnotated
FinalizeLayouts

Calibration
=============
Expand Down Expand Up @@ -236,8 +238,10 @@
from .optimization import CollectCliffords
from .optimization import ResetAfterMeasureSimplification
from .optimization import OptimizeCliffords
from .optimization import ElidePermutations
from .optimization import NormalizeRXAngle
from .optimization import OptimizeAnnotated
from .optimization import FinalizeLayouts

# circuit analysis
from .analysis import ResourceEstimation
Expand Down
1 change: 0 additions & 1 deletion qiskit/transpiler/passes/layout/apply_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ def run(self, dag):
raise TranspilerError("The 'layout' must be full (with ancilla).")

post_layout = self.property_set["post_layout"]

q = QuantumRegister(len(layout), "q")

new_dag = DAGCircuit()
Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/layout/set_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def run(self, dag):
layout = None
else:
raise InvalidLayoutError(
f"SetLayout was intialized with the layout type: {type(self.layout)}"
f"SetLayout was initialized with the layout type: {type(self.layout)}"
)
self.property_set["layout"] = layout
return dag
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/optimization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,7 @@
from .reset_after_measure_simplification import ResetAfterMeasureSimplification
from .optimize_cliffords import OptimizeCliffords
from .collect_cliffords import CollectCliffords
from .elide_permutations import ElidePermutations
from .normalize_rx_angle import NormalizeRXAngle
from .optimize_annotated import OptimizeAnnotated
from .finalize_layouts import FinalizeLayouts
106 changes: 106 additions & 0 deletions qiskit/transpiler/passes/optimization/elide_permutations.py
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
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

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)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
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
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
self.property_set["virtual_permutation_layout"] = Layout(
{dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)}
)
Copy link
Member

Choose a reason for hiding this comment

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

I'd like to review properly once I'm back at work on Tuesday, but just as an immediate note: please can we consider not using Layout here? As both its name and the top PR comment says, the virtual_permutation is a permutation of virtual-to-virtual, whereas the Layout class is explicitly a mapping of virtual to physical qubits (i.e. Layout is a map from one Hilbert space to a different Hilbert space, whereas this is an operator within the same Hilbert space). The idx here are not physical qubits, they're just virtual-qubit indices, and I think we ought to separate the representations, especially given how confused we keep getting about Layout.

Copy link
Member

@jakelishman jakelishman Mar 31, 2024

Choose a reason for hiding this comment

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

By way of alternative, I mean I'd prefer just to store an int list of the permutation, which is how we store permutations in all other places (like PermutationGate).

Copy link
Contributor

Choose a reason for hiding this comment

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

Jake, I very much agree with you that using the word layout to represent anything else than virtual-to-physical is confusing. For instance, it took me a lot of time and an offline discussion with Matthew to understand that "final_layout" stands for the physical-to-physical permutation added during the routing stage. Having said that, I feel that at this point having both virtual-to-physical and physical-to-physical maps called layouts and internally represented using the Layout class, while having the virtual-to-virtual map being something else would be even more confusing. So not my favorite choice, but I am arguing for keeping the virtual-to-virtual map as a Layout as well.

It might be simplest to think of the word layout as of some mapping $f: A \rightarrow B$, where $A$ and $B$ can be anything. One nice thing is that you can easily compose mappings, e.g. composing the mappings $f:A\rightarrow B$ and $g:B\rightarrow C$ produces the mapping $g\circ f:A\rightarrow C$. The functionality to compose Layouts was recently implemented in PR #11399, and this simplifies the code that deals with Layouts by a lot.

In addition, we are now talking about multiple passes that can modify the virtual-to-virtual permutation: e.g., this ElidePermutations pass and the new StarPreRouting pass. To keep track of the combined permutation we can simply compose Layouts.

Copy link
Member

Choose a reason for hiding this comment

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

I think storing final_layout as a Layout was a mistake, and that was borne out in how nobody was able to use/understand TranspileLayout at first (both us - see all the issues we had in implementing Operator.from_circuit - and users) and we had to completely remake the public interface in #10835.

In an ideal world, we'd walk back final_layout but we only didn't in #10835 because it would have been a breaking change so we just left it. I'd really prefer that we didn't expand that bad situation. I wouldn't say we represent physical-to-physical permutations everywhere as Layouts at all, we just do it in this one place that imo we already recognised as a mistake.

I'm also not super wild that we merged #11399 - to me, that PR has a mistake even in its top comment where is even suggests that a Layout can have an inverse. A permutation can have an inverse, but a layout can't - a Layout doesn't have a "forwards" and "backwards", it has a "virtual" side and a "physical" side, and every part of the data structure, public method and argument typing says that.

Copy link
Contributor

@alexanderivrii alexanderivrii Apr 1, 2024

Choose a reason for hiding this comment

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

I am fine with any decision but I do find it so much easier to think about all of this (including how to guarantee that unitary equivalence is preserved) when I think of a layout as of an invertible mapping. For better or for worse, even the "layout" (in the property set terminology) aka the "initial_layout" (in TranspileLayout terminilogy) is an invertible mapping from ancilla-expanded virtual qubits to physical qubits. I would argue that this is also a mistake (I mean the ancilla-expanded part of this), but this is another thing that we are stuck with for now. But if we do work with invertible mappings, having the inverse mapping is very natural and really helps writing shorter cleaner code. At this point I feel it would be significantly more confusing if we did not repeat the same mistake once again. :)

Copy link
Member

@jakelishman jakelishman Apr 1, 2024

Choose a reason for hiding this comment

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

Oh, also, in case this is the part we're missing each other on: I wouldn't expect to be able to directly compose the result of the permutation that I'm talkin about with the permutation induced by a routing pass, because we're expecting ElideSwaps to run on a virtual circuit, and routing runs on a physical circuit, so the permutations apply to two different Hilbert spaces.

In my version of the transpiler pipeline, ApplyLayout would convert the virtual permutation to a physical permutation (likewise FullAncillaAllocation would pad it to fill width, etc, just like these passes will do to operators when/if we transpiler EstimatorPubs), and then they'd be directly composable, because only then would they be acting on the same spaces. There would be no need for FinalizeLayout.

Layout keeps confusing the matter because it makes us think the "virtual qubits" are always the same thing in layouts at any transpiler pass, but the misuse of Layout and the existence of FullAncillaAllocatiom makes that not at all true. The two *_virtual_layout methods of TranspileLayout do produce layouts with the same virtual qubits, which is why they actually directly interact cleanly.

Copy link
Contributor

@alexanderivrii alexanderivrii Apr 1, 2024

Choose a reason for hiding this comment

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

So, would it be possible to have the circuit's TranspileLayout instance available to every transpiler pass (and not just at the end of the run) with every pass being able to update the fields of the TranspileLayout and not the "layout" and "final_layout" attributes of the property set? (And if these are needed for backward compatibility, then somehow retrieve them from the TranspileLayout instead)?

Oh yeah the whole reason to invent the FinalizeTranspiler pass was that SetLayout and ApplyLayout do not always run (iirc they do not run when no coupling map or target is specified). I thought there is a deep reason why we might want to skip running these passes, but if not, we could indeed delegate the job of lifting the permutation of virtual qubits to a permutation of physical qubits to the ApplyLayout pass (which was actually Matthew's original suggestion).

Copy link
Member

Choose a reason for hiding this comment

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

No, TranspileLayout will never be available to any transpiler pass - it's not created until PassManager.run is converting the final DAGCircuit back to a QuantumCircuit. We can track whatever we like in order to build it at the end, though.

If ApplyLayout never runs, then the circuit never swaps from being virtual to physical; the output remains a virtual circuit. That's expected to me - ApplyLayout's job is to convert virtual into physical. I don't think that's a problem here - it would just mean that the only method that ought to do anything on TranspileLayout should be routing_permutation (which I guess is slightly misnamed now - it's not only routing); for all the others, there is no Layout, so {initial,final}_{virtual,index}_layout() would be whatever they are when the circuit wasn't laid out.

I was mostly saving this for tomorrow, but since I'm here, let me sketch out how I'd make sense of permutations and the responsibility splits all the way through the transpiler to avoid needing extra stuff on Layout. I think that this actually along the lines of what Matt was originally intending with final_layout, except I think the use of the Layout class totally confused things and then we got all lost in the weeds.

  1. Any pass that involves rewriting the meaning of qubit indices part way through the circuit (so ElideSwaps and all the routing passes, but not the layout passes) can be thought of as taking a circuit c to a new circuit c2 that's implicitly followed by a PermutationGate(perm2) acting on dag.qubits, so that matrix(c) == matrix(PermutationGate(perm2)) @ matrix(c2).
  2. Crucially, my list perm2 in the above isn't a Layout, it's directly linked to indices into whatever dag.qubits is at the current point in the pipeline. We store perm2 in PropertySet["final_permutation"] (note: we could equally calculate and store PermutationGate(perm2).inverse().pattern, i.e. store either the additive or the subtractive part. We can choose whatever convention is more convenient for composition.)
  3. If a second pass now runs on c2 that elides more swaps and so maps c2 to (c3, perm3), the full action of the transpilation pipeline is such that matrix(c) == matrix(PermutationGate(perm2)) @ matrix(PermutationGate(perm3)) @ matrix(c3). Note that perm2 is applied after perm3, because when the second pass ran, it only considered c2 and not c2 & perm2, so its "undo" permutation perm3 gets applied before perm2. Clearly, then the complete permutation that we want to store in the PropertySet is just the effect of applying perm3 followed by the effect of applying perm2 - a straightforward permutation composition. Every pass that produces a permutation is responsible for updating the final permutation with that composition (we can supply a helper method that's just Permutation.compose as I had it above, plus handling for if the second permutation is None).
  4. Now, if we run a pass that actually changes dag.qubits in any way (i.e. any layout pass, or any ancilla-expansion pass), those passes are clearly responsible for updating final_permutation so that the invariant of "dag.apply_operation_back(PermutationGate(property_set["final_permutation"]), dag.qubits)) is how to undo the mid-circuit permutation" remains correct - i.e. they're responsible for updating the stored permutation so it stays in sync with dag.qubits, since they're changing dag.qubits.
  5. Finally, on the output, because we maintained this clean invariant on property_set["final_permutation"], when it comes time to save the permutation at the end, we don't actually have to do any work. It doesn't matter whether any layout pass ran, or whether one or more virtual-qubit swap elision passes ran, or any physical-qubit swap mapping passes (etc etc etc) ran - we never need to worry about what the permutation refers to, because it's always whatever happens to be in dag.qubits and it's always (implicitly) applied as PermutationGate(perm) on all of dag.qubits to undo the mapping.

In that form, it really doesn't matter if ApplyLayout actually runs or not. If it does, it's responsible for updating the permutation to match indices into dag.qubits, just like it's responsible for updating every gate on the circuit. If it doesn't, then nothing ever changed the meaning of indices into dag.qubits, so the permutation still has the correct meaning when we reach the end of the transpiler pipeline. We can then map that to the awkward final_layout form in order to built a TranspileLayout - I think that's Layout(zip(dag.qubits, property_set["final_permutation"])) (regardless of whether routing ran or not), but I don't 100% remember how final_layout currently works. That should then automatically work with TranspileLayout.routing_permutation.

(Fwiw, if we do do this, then I'd tweak my suggestion in #12057 (comment) to use TranspileLayout.routing_permutation() instead, since that might sometimes be set without initial_index_layout() or final_index_layout() having values.)

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks Jake, I am now very much in favor of the changes that you are proposing. If we think of the "final_layout" not as of a layout (i.e. not as of a map $V\to P$) and not as of a routing permutation of physical qubits (i.e. not as of a map $P\to P$), but as representing a permutation that is implicitly applied at the end of the current circuit, then everything can be realized just you have described and we don't need to explicitly store the "virtual_permutation_layout" or introduce the FinalizeLayouts pass. Great! This is actually how I was thinking about what's going on all along (i.e. a circuit with an implicit permutation gate at the end), but then tried to fit this into how I understood the "layout" / "final layout" framework.

However, in order to avoid the same confusion in the future, can we actually replace "final layout" from Layout class by an actual permutation (both choices as a list of integers e.g. [2, 3, 1, 0] or as a permutation gate PermutationGate([2, 3, 1, 0]) look fine for me) and not as of an object of type Layout? I am afraid that technically this change may not be backward-compatible, however these are somewhat of internal transpiler objects, and in theory the user should only be working with the final TranspileLayout. What do you think?

On the topic, where in Qiskit do we document what each property set attribute really means? For instance, I don't see anything relevant when searching for final_layout.

Aside: when talking about permutations we have two different conventions going on. IIRC, a routing_permutation in TranspileLayout of the form [2, 3, 1, 0] means 0 -> 2, 1 -> 3, 2 -> 1, 3 -> 0 while for PermutationGate([2, 3, 1, 0]) the convention is 2->0, 2->1, 1->2, 0->3. The two are inverse of each other, but it's still annoying to recall which one is used when.

I completely agree on the details of composing permutation and applying permutations to layouts. However, I want a common place that implements these methods, rather than having many passes duplicating the same code. This is why I was hoping that the passes could work with TranspileLayout class which already has (or should have) relevant usability functions.

I think we also agree that every pass implicitly takes the triple (current dag, current layout, current implicit permutation) and is responsible for producing the triple (new dag, new layout, new implicit permutation) which should be unitary-equivalent (using the Operator.from_circuit logic) provided that the circuit only consists of unitary operations and there is no ancilla expansion.

Back to the backwards-compatibility question and documentation, we would not be telling the writers of say routing passes (that can be written as external routing plugins) to make sure to update the "layout" and "final_layout" attributes. Is this backward-compatible? Where do or should we document this?

Copy link
Member

@jakelishman jakelishman Apr 4, 2024

Choose a reason for hiding this comment

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

For backwards compatibility, I think something like this could work:

  • make final_permutation a different property to final_layout with the type we want it to have
  • have Qiskit-internal passes not set final_layout, but only set final_permutation
  • during TranspileLayout construction at the end of the circuit, if final_layout is set, then assume that it ran during a user-defined routing stage and compose its implied permutation with final_permutation. We may well get that wrong if we ran a swap-elision pass after a user routing pass, but I think that's very unlikely.
  • routing and elision passes would never update layout (since that's just the initial layout), only final_permutation.

For different conventions between routing_permutation and PermutationGate: that's part of what I was getting at with "we can either store the inverse or the forwards permutation". You could argue that they're the same permutation, it's just one is stored as an additive (apply this to a circuit to undo the induced permutation) and one is subtractive (this is the induced permutation, so apply the inverse of it to undo it). We can make final_permutation store whichever of those matches the convention of routing_permutation already - as you say, you can easily convert one to the other - so we're consistent with ourselves.

return new_dag
77 changes: 77 additions & 0 deletions qiskit/transpiler/passes/optimization/finalize_layouts.py
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:
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the t_ prefix stand for here? For readability, can we write it out?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what his intent was with the prefix either, but we can always clean it up in a follow up PR.


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
1 change: 0 additions & 1 deletion qiskit/transpiler/preset_passmanagers/builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
)
)
init.append(CommutativeCancellation())

else:
raise TranspilerError(f"Invalid optimization level {optimization_level}")
return init
Expand Down
47 changes: 47 additions & 0 deletions releasenotes/notes/add-elide-swaps-b0a4c373c9af1efd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
features:
- |
Added a new optimization transpiler pass, :class:`~.ElidePermutations`,
which is designed to run prior to the :ref:`layout_stage` and will
optimize away any :class:`~.SwapGate`\s and
:class:`~qiskit.circuit.library.PermutationGate`\s
in a circuit by permuting virtual
qubits. For example, taking a circuit with :class:`~.SwapGate`\s:

.. plot::

mtreinish marked this conversation as resolved.
Show resolved Hide resolved
from qiskit.circuit import QuantumCircuit

qc = QuantumCircuit(3)
qc.h(0)
qc.swap(0, 1)
qc.swap(2, 0)
qc.cx(1, 0)
qc.measure_all()
qc.draw("mpl")

will remove the swaps when the pass is run:

.. plot::
:include-source:

from qiskit.transpiler.passes import ElidePermutations
from qiskit.circuit import QuantumCircuit

qc = QuantumCircuit(3)
qc.h(0)
qc.swap(0, 1)
qc.swap(2, 0)
qc.cx(1, 0)
qc.measure_all()

ElidePermutations()(qc).draw("mpl")

The pass also sets the ``virtual_permutation_layout`` property set, storing
the permutation of the virtual qubits that was optimized away.

- |
Added a new optimization transpiler pass, :class:`~.FinalizeLayouts`,
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
which is designed to run after the :ref:`routing_stage` and update the
``final_layout`` property set based on ``layout`` and
``virtual_permutation_layout``.
Copy link
Contributor

Choose a reason for hiding this comment

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

From your discussion above @jakelishman , it sounds to me like we ought to consider keeping any new properties private until we i.e. refactor to storing final_permutation.

Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm fine not documenting it explicitly as part of the API although at the end of the day the pass doesn't do much in the absence of a virtual permutation layout. So it's kind of hard to keep it private as part of the pass. I don't think it's adding an extra burden realistically to support it for the remainder of 1.x if we pivot to something else in the future we have to do this already with final_layout.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, part of us not being able to get the final_permutation landed properly for 1.1 is that basically have to support this for the whole 1.x cycle now, but that was priced into the decision - it shouldn't be too painful, and we'd have needed a 2.0 release to make a truly clean break from external routing passes anyway.

Loading
Loading