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

LookaheadSwap never swaps back #10145

Closed
fuglede opened this issue May 23, 2023 · 6 comments
Closed

LookaheadSwap never swaps back #10145

fuglede opened this issue May 23, 2023 · 6 comments
Labels
bug Something isn't working

Comments

@fuglede
Copy link

fuglede commented May 23, 2023

Environment

  • Qiskit Terra version: 0.24.0
  • Python version: 3.10.10
  • Operating system: Linux 6.1.0-9-amd64 SMP PREEMPT_DYNAMIC Debian 6.1.27-1 (2023-05-08) x86_64 GNU/Linux

What is happening?

The pass qiskit.transpiler.passes.routing.lookahead_swap.LookaheadSwap produces circuits that are not equivalent to the input circuit, since it never swaps back qubits. This becomes an issue if you want to compose multiple such circuits, if you don't want to keep track of the swaps yourself somehow, or just in general if you want to count the cost of swapping back.

But maybe this is just working as intended; a note somewhere in the docs might be useful in that case. Moreover, if the intention is that one manually swaps back at the end, the final permutation should be available somehow (either through the pass itself, or when using it through, say, qiskit.compiler.transpile).

How can we reproduce the issue?

from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator
from qiskit.transpiler import CouplingMap
from qiskit.transpiler.passes.routing.lookahead_swap import LookaheadSwap

c1 = QuantumCircuit(3)
c1.cnot(0, 2)
c2 = LookaheadSwap(CouplingMap([[0, 1], [1, 2]]))(c1)
print(Operator(c1).equiv(Operator(c2)))  # False
c2.swap(0, 1)  # Manually swap back, shouldn't be necessary
print(Operator(c1).equiv(Operator(c2)))  # True

What should happen?

It shouldn't be necessary to add the c2.swap(0, 1) by hand.

Any suggestions?

No response

@fuglede fuglede added the bug Something isn't working label May 23, 2023
@fuglede
Copy link
Author

fuglede commented May 23, 2023

The same thing happens with BasicSwap, SabreSwap, and StochasticSwap as well.

@jakelishman
Copy link
Member

This is expected behaviour - if you look at QuantumCircuit.layout.final_layout after a call to transpile, you should find the reverse layout that you're looking for. The pass LayoutTransformation can calculate apply necessary swap maps to move from one layout to another, which might help you too.

We do this because the situation you're asking for is more computationally expensive and generally unnecessary for execution of a complete circuit on hardware; the compiler tracks the virtual->physical layout throughout the circuit execution (it changes over the course of a circuit, typically), and ensures that gates and measures are inserted on the correct physical qubits. The compiler is only designed to operate on complete circuits, not subcomponents of one, and the final qubit layout is unobservable - only measurement results are, and we ensure that we map measures into the correct bits.

@fuglede
Copy link
Author

fuglede commented May 23, 2023

Makes sense, thanks for spelling that out.

@fuglede fuglede closed this as completed May 23, 2023
@fuglede
Copy link
Author

fuglede commented May 24, 2023

I did notice that QuantumCircuit.layout.final_layout is not available after calling LookaheadSwap (or the others), so there one has to do something else.

In the case of transpile, it looks like it's sometimes there, but for some reason not always. For example, I get the following:

c1 = QuantumCircuit(3)
c1.cnot(0, 2)
coupling_map = CouplingMap([[0, 1], [1, 2]])
c2 = transpile(c1, coupling_map=coupling_map)
print(c2.layout.final_layout is None)  # True

When I use transpile on a bigger circuit, it does show up, and I can use LayoutTransformation to add the "missing" SWAP; in particular, I can do something like

coupling_map = CouplingMap([[0, 1], [1, 2]])
c2 = transpile(c1, coupling_map=coupling_map)
to_layout = Layout.generate_trivial_layout(*c2.qregs)
c3 = LayoutTransformation(coupling_map, c2.layout.final_layout, to_layout)(c2)
print(Operator(c1).equiv(Operator(c2)))  # False
print(Operator(c1).equiv(Operator(c3)))  # Depends on input

so that takes care of that. But I might have expected c1 to always be operator equivalent to c3 (and AFAICT, not even up to scalar multiplication), but apparently that's not the case; is that expected too? In the cases I have where it fails, I do still get equivalence of statevectors though, so maybe that's all that's expected.

@jakelishman
Copy link
Member

jakelishman commented Jun 6, 2023

Sorry for the very slow reply here. If you run a transpiler routing pass manually, then the final layout will be present in the property_set attribute of the pass. If you use a PassManager (or transpile), that should automatically populate it for you, if a routing pass was run. Currently, we'll end up with final_layout=None if no routing was required. That's not an ideal situation from an API-design perspective and I think it would be reasonable for us to consider changing it, but in the immediate term, that's what None means.

@jakelishman
Copy link
Member

For your second question about Operator, yes that's also expected behaviour; when you make an operator of a transpiled circuit, your input and output Hilbert spaces will be the physical qubits, not the virtual qubits that are represented on them (because a key part of a quantum compilation is specifically rewriting the virtual operations in terms of physical qubits only). This means that the operator is ordered based on physical qubits, not virtual ones.

In your examples, the transpiler is also almost certainly not requiring any routing (hence final_layout is None), because there's a way of choosing the initial virtual -> physical mapping that requires no swaps. In your c1, if you choose {v0: p0, v1: p2, v2: p1} or {v0: p1, v1: p0, v2: p2}, you can do the CX gate without needing to swap anything over. The transpiler will pick between those two randomly in your situation. If you want to fix the initial virtual -> physical mapping to some particular value, you should specify initial_layout in the call to transpile.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants