How to retrieve circuit compiled for IBM backend

Hi,
With your help I’m able to run a circuit with amplitude encoding qml.AmplitudeEmbedding(.) on an ideal simulator and on IBM HW. How can I see what was the final gate-based circuit executed on the IBM?

I accept the PennyLane compiler may not yet have all the bells and whistles to handle the hardware specific compilation to the naitive gates, but when I submit a PennyLane job to IBM it must internally call the Qiskit transpiler for a selected real backend and Qiskit always generates the transpiled version of the circuit. In principle I can login to

and pull this transpiled circuit as QASM, given I know the IBM Job Id. But this forces me to use 2 frameworks and I’d like to do everything from PannyLane.

Q0: how can I retrieve IBM job Id within PennyLane framework?

Q1: is there a way to retrieve the circuit as-executed from

dev = qml.device('qiskit.ibmq', wires=num_wires, backend=backend, shots=shots)
qnode = qml.QNode(encoder, dev)
probTens = qnode(X)

The transpired circuit is included with the data returned by the executed IBM job.

Q2: (even more important) how can I pass the seed to the Qiskit transpiler from PannyLane?
This seed decides which physical qubits are used on a QPU. If the decision on the seed is left to Qiskit there is a possibility the same input circuit will map to different physical qubits in subsequent execution of QML iterations and then the training will have problems with convergence, since circuits executed on different qubits acquire different errors for every iteration.
Thanks
Jan

Hi @Jan_Balewski,
Thank you for your questions!
We have forwarded this question to members of our technical team who will be getting back to you within a week. Feel free to post any updates to your questions here in the thread in the meantime!

Hi @Jan_Balewski, thanks for your question! Unfortunately the final circuit as executed isn’t easily accessible after execution via the PennyLane device. It’s possible you could work around it by using some internal functions on the device that aren’t meant to be user-facing, but they are somewhat intertwined with the overall execution pipeline, so I think the simplest thing for now is to check on the IBMQ portal. Thank you for the feedback, it’s good to know what people would find useful!

In regard to your question about the job ID, you can pull the most recent job ID from the device as device._current_job.job_id. Please note that this is a private method rather than a user-facing one, and may change or be unsupported in the future.

Finally, you should be able to pass any kwargs you want used in transpilation to qml.device on initialization, and they will be used to transpile. This includes seed_transpiler and any other kwarg from the Qiskit compiler.transpile function. If you have trouble with this let me know!

1 Like

Hi Lillian,
Thanks for clarifying what is currently possible.
Attached is the end-to-end example code which prints transpiled GHZ circuit as executed on IBM Hanoi.
It extracts IBM job ID from PennyLane IBM-device after the circuit is executed and uses Qiskit to pull the transpiled circuit from the IBM database.
Additionally, I print the CX-depth and the total number of CX gates for the transpiled circuits - those are 2 crucial circuit properties deciding how good the results will be from the real HW.

The primary use case of analyzing a transpiled circuit is assessment, if it makes sense to run it on a given hardware. Since, Qiskit jobRes = job.result() requires a job to finish, which may take hours to days to learn if I made a bad choice, depending on how long the queue is.

So it is a good starting point, but eventually device.transpile(circuit) would be a more desirable solution, w/o need to execute the circuit on the HW. (It could call Qiskit transpiler under the hood).
I’m good for now,
thanks again for your assistance
Jan

import pennylane as qml
num_wires = 5
from qiskit_ibm_provider import IBMProvider
provider = IBMProvider()
backend = provider.get_backend('ibm_hanoi')
dev = qml.device('qiskit.ibmq', wires=num_wires, backend=backend, shots=200)

def ghz(dev):
    @qml.qnode(dev)
    def circuit():
        n=num_wires
        qml.Hadamard(wires=0)
        for i in range(1, n):  
            qml.CNOT(wires=[0,i])
        return qml.probs(wires=range(n))
    return circuit
circ = ghz(dev)
print('backend:',backend)
print(qml.draw(circ)())

backend: <IBMBackend(‘ibm_hanoi’)>

0: ──H─╭●─╭●─╭●─╭●─┤ ╭Probs
1: ────╰X─│──│──│──┤ ├Probs
2: ───────╰X─│──│──┤ ├Probs
3: ──────────╰X─│──┤ ├Probs
4: ─────────────╰X─┤ ╰Probs

Run the job and circuit retrieval

probTens = circ()  # run circuit on IBM device
print('probs:',probTens)
jid=dev._current_job.job_id()
print('IBM job ID',jid) 
job = provider.retrieve_job(jid)
# Retrieve the results from the job
jobRes = job.result()
resL=jobRes.results  
nqc=len(resL)  # number of circuit in the job
counts=jobRes.get_counts()
if nqc==1: counts=[counts]  # this is poor design
print('M: got %d circ+results'%nqc, 'JID=',jid)
for ic in range(nqc):
    qc=job.circuits()[ic]  # transpiled circuit 
    resHead=resL[ic].header # auxiliary info about used hardware
    print('\nM: circ=%d %s'%(ic,qc.name))
    print('counts:',counts[ic])
    q2_depth=qc.depth(filter_function=lambda x: x.operation.num_qubits > 1)
    print('circuit q2_depth=%d , gate count:'%q2_depth,dict(qc.count_ops()))
    print(qc.draw(output="text", idle_wires=False,fold=120))
    print('result header: ',end=''); print(resHead)

Transpiled circuit depth. In particular the ideal circuit have only 4 CX gates, but the final one has 10 , so it will perform worse on real HW.
circuit q2_depth=11 , gate count: {'cx': 10, 'measure': 5, 'rz': 2, 'sx': 1, 'barrier': 1}
Portion of transpiled circuit , including mapping to physical qubits and swaps.

global phase: π/4
                                                                                              ┌───┐                    »
       Qubit(QuantumRegister(5, 'q'), 2) -> 0 ────────────────────────────────────────────────┤ X ├────────────────────»
                                                                               ┌───┐     ┌───┐└─┬─┘          ┌───┐     »
       Qubit(QuantumRegister(5, 'q'), 3) -> 1 ─────────────────────────────────┤ X ├──■──┤ X ├──■────■────■──┤ X ├──■──»
                                                                               └─┬─┘  │  └─┬─┘       │  ┌─┴─┐└─┬─┘┌─┴─┐»
Qubit(QuantumRegister(22, 'ancilla'), 3) -> 2 ───────────────────────────────────┼────┼────┼─────────┼──┤ X ├──■──┤ X ├»
                                                                                 │    │    │         │  └───┘     └───┘»
       Qubit(QuantumRegister(5, 'q'), 4) -> 3 ───────────────────────────────────┼────┼────┼─────────┼─────────────────»
                                              ┌─────────┐┌────┐┌─────────┐       │  ┌─┴─┐  │       ┌─┴─┐               »
       Qubit(QuantumRegister(5, 'q'), 0) -> 4 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π/2) ├──■────■──┤ X ├──■───────┤ X ├───────────────»
                                              └─────────┘└────┘└─────────┘┌─┴─┐     └───┘          └───┘               »
       Qubit(QuantumRegister(5, 'q'), 1) -> 7 ────────────────────────────┤ X ├────────────────────────────────────────»
                                                                          └───┘                                        »
                                         c: 5/═════════════════════════════════════════════════════════════════════════»
                                                                                                                       »
1 Like