1
$\begingroup$

I'm having a problem in IBM Qiskit with my qubit identities switching places during transpilation. I've been told by developers on the Qiskit slack server that there's currently no built-in way to fix this problem. I'm wondering if anyone has found one themselves.

The issue is that I'd like to perform an algorithm like so:

  1. Prepare in an initial state
  2. Apply a circuit U repeatedly n_iter times
  3. Measure

I also want this algorithm to:

  • Be reasonably efficient with gates (specifically noisy CNOTs)
  • Have the same error for U for each iteration of it

This turns out to be a headche. When U is complicated, it becomes difficult for the transpiler to efficiently decompose it to native gates. This difficulty goes up for U*U and U*U*U since they're longer. So if I were to transpile the whole algorithm at once (with many U's), it certainly won't give an efficient gate decomposition, and won't have the same error for each iteration of U.

The solution I thought would be reasonable is to transpile U on its own, then compose it with itself. But this hits a problem. The transpiler likes to switch qubit identities (and add global phase, and maybe more?) while searching for a more efficient circuit. So if I follow my algorithm as stated above, U will not line up correctly with the prep or measurement, and my results will be wrong.

I've been told by developers on the slack that there's currently no built-in way to fix this. The transpiler does not report a final_layout of the qubits.

I've been trying to determine the final_layout by hand by comparing the transpiler output to my intended circuit plus swap gates (up to a global phase), but I'm even failing at this.

Does anyone have either working code to determine the final_layout, or a better approach to implementing this algorithm?

$\endgroup$
  • $\begingroup$ If I understand correctly, then maybe you can try to add a barrier between each application of $U$. This will make sure Qiskit will only optimize the part within the barrier, which is just $U$, and it would be consistent. This will allow you to have the same error for $U$ for each iteration. Now, you can also specify the qubit layout that you want to use instead of letting Qiskit picking it for you. $\endgroup$ – KAJ226 Oct 30 at 4:35
  • $\begingroup$ @KAJ226 In my experience this approach neither makes the error consistent nor particularly close to minimal. I assume the randomness of the optimization is to blame for this. $\endgroup$ – Max Oct 30 at 6:01
  • 1
    $\begingroup$ Could you share some code? For example, what's the unitary U and what settings in transpiler have you tried? $\endgroup$ – tsgeorgios Oct 30 at 7:38
  • $\begingroup$ if you fix transpile(..., seed_transpiler=42, ...), does it help? $\endgroup$ – luciano Oct 30 at 11:27
  • $\begingroup$ @luciano No, I did try a bunch of fixed seeds to see if any would avoid the issue, but from manual searching couldn't find any. $\endgroup$ – Max Oct 30 at 18:16
0
$\begingroup$

If i understand your question correctly, i guess you could get, after transpiling $U$, the final layout from _layout property of a circuit and remap the qubits in initial state preparation circuit $ V $ to enforce this layout. If you want your measurement results to respect the initial ordering of qubits, you could re-order the classical register, i.e if qubit 0 is qubit 1 in the final circuit, store the measurement of qubit 1 in classical bit 0.

Finally, you can execute the circuit. The only catch is that you may need another transpilation step in case the layout is not compatible with $ V $ but if $U$ is much more 'complicated' than $V$, it should be fine.

Here is some working code.

from qiskit import transpile, execute, Aer, IBMQ

from qiskit.circuit import QuantumCircuit, ClassicalRegister
from qiskit.circuit.random import random_circuit

from qiskit.transpiler import PassManager
from qiskit.transpiler.passes.layout import ApplyLayout, SetLayout

from qiskit.quantum_info import Statevector
from qiskit.quantum_info.random import random_unitary

qasm  = Aer.get_backend('qasm_simulator')

IBMQ.load_account()
provider = IBMQ.get_provider(hub='ibm-q')
device = provider.get_backend('ibmq_santiago')

gates = device.configuration().basis_gates
coupling_map = device.configuration().coupling_map

n = 3
n_iter = 2

# init state
V = QuantumCircuit(n)
V.h(0)
for i in range(n - 1):
    V.cx(i, i + 1)
    
# random U
U = random_circuit(n, depth=5)

# full algorithm - no optimization - just for reference
qc = U.repeat(n_iter)
qc.compose(V, front=True, inplace=True)
qc.measure_all()

# transpile U
Utr = transpile(U, 
                basis_gates=gates, 
                coupling_map=coupling_map, 
                optimization_level=3)

layout = Utr._layout
mapping = layout.get_virtual_bits() # a map from virtual to physical qubits

# enforce layout in V
passes_ = [SetLayout(layout), ApplyLayout()]
pm = PassManager(passes_)
Vm = pm.run(V)

# full algorithm
circ = Utr.repeat(n_iter)
circ.compose(Vm, front=True, inplace=True)

# add measurements
cr = ClassicalRegister(n)
circ.add_register(cr)

# re-order classical registers 
for q in U.qubits:
    circ.measure(mapping[q], 
                 cr[q.index])


def simulate(qc):
    return execute(qc, qasm).result().get_counts()

print(simulate(qc))
---
{'000': 484, '001': 17, '010': 6, '011': 47, '100': 10, '110': 19, '111': 441}

print(simulate(circ))
---
{'000': 496, '001': 23, '010': 7, '011': 37, '100': 12, '101': 1, '110': 14, '111': 434}
| improve this answer | |
$\endgroup$
  • $\begingroup$ Thank you for the response, I didn't know one could access _layout, good to know. However it turns out I misunderstood the problem and that simply keeping a consistent layout wouldn't solve the problem -- see my answer. $\endgroup$ – Max Nov 17 at 1:17
0
$\begingroup$

I found a partial answer that I'll share to clarify what the problem is, and provide a hacky fix.

I finally realized that transpiling a circuit piece (without a measurement) doesn’t seem to execute the circuit on a permuted version of the qubits like I thought, but rather it ends up executing the circuit and then a permutation at the end. Comparing to what happens when measurements are present, it seems like transpile assumes measurements are present even if they aren’t. It then allows the measurement step to “absorb” some cnots in the form of swap gates, just swapping which measurement maps to what. This is smart when measurments are present, but when they aren’t it causes the circuit to just throw away swaps at the end of the circuit for no reason.

I also found a strange partial fix for this: It seems that if I transpile a second time after adding those missing swaps by hand, the second transpile won’t wrongly delete the swaps, and will partially absorb them into the circuit like I wanted. Unsure why this works.

EDIT: Here is code demonstrating the problem:

qc_foo = QuantumCircuit(n_q, n_q)
qubit_list = [0,1,2]
qc_foo.swap(0,1)
qc_foo.swap(0,2)
qc_foo.barrier()
qc_foo.measure(qubit_list, qubit_list)
qc_foo_trans = transpile(qc_foo, backend=sant, optimization_level=3)
qc_foo_trans.draw()

Notice the transpiled circuit has turned the 0-2 swap into a 1-2 swap, absorbing the difference into the measurement. Now comment out the measurement step and re-run. The 0-2 still becomes a 1-2, despite the lack of measurement, making the transpiled circuit incorrect.

A developer has also confirmed for me that this is likely a bug.

| improve this answer | |
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.