Drawing Quantum Circuit from a list of Operations and their Adjoints

Hello,

TLDR: Circuit Drawer function draws just the second half of the list of Pennylane operations.

I am trying to create a “circuit drawer” function that takes in a list of Pennylane operations, and then queues the operations then draws them.
The code is:

@qml.qnode(qml.device('default.qubit'))
def circuit_drawer(circ_ops):
    for op in circ_ops:
        # print(f"Queueing operation: {op}")
        op.queue()
    
    return qml.state()

pennylane_circ_drawer = qml.draw_mpl(circuit_drawer, decimals=4)

Given, I have a list of PennyLane operations and their adjoints to invert the circuit in the following manner:
base_circ_adjoint = base_circ_ops + [qml.adjoint(op) for op in base_circ_ops[::-1]]

However, when I attempt to draw the circuit, I can only see the adjoint operations in the image.

pennylane_circ_drawer(base_circ_adjoint)

Moreover, when I try to call the circuit_drawer function to review the states, I do not see a |0\rangle state but the following:
[ 0.5+0.j 0. +0.j 0.5+0.j 0. +0.j 0. +0.j -0.5+0.j 0. +0.j 0.5+0.j 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. +0.j]

which follows the diagram circuit’s math.

I can confirm list of operations has all the operations, as seen from the print statement inside the queuing loop.

Queueing operation: CZ(wires=[1, 4])
Queueing operation: CNOT(wires=[2, 1])
Queueing operation: Z(0)
Queueing operation: H(4)
Queueing operation: Z(2)
Queueing operation: Z(4)
Queueing operation: CNOT(wires=[4, 1])
Queueing operation: H(2)
Queueing operation: Z(0)
Queueing operation: Z(0)
Queueing operation: Adjoint(Z(0))
Queueing operation: Adjoint(Z(0))
Queueing operation: Adjoint(H(2))
Queueing operation: Adjoint(CNOT(wires=[4, 1]))
Queueing operation: Adjoint(Z(4))
Queueing operation: Adjoint(Z(2))
Queueing operation: Adjoint(H(4))
Queueing operation: Adjoint(Z(0))
Queueing operation: Adjoint(CNOT(wires=[2, 1]))
Queueing operation: Adjoint(CZ(wires=[1, 4]))

Can you please help me understand what am I doing incorrectly here?

Thanks!

EDIT:
This is not an issue if I give it just the base_circ_ops list. I can see and confirm the correct outputs.

print(circuit_drawer(base_circ_ops))
pennylane_circ_drawer(base_circ_ops)
Queueing operation: CZ(wires=[1, 4])
Queueing operation: CNOT(wires=[2, 1])
Queueing operation: Z(0)
Queueing operation: H(4)
Queueing operation: Z(2)
Queueing operation: Z(4)
Queueing operation: CNOT(wires=[4, 1])
Queueing operation: H(2)
Queueing operation: Z(0)
Queueing operation: Z(0)
[ 0.5+0.j  0. -0.j  0.5+0.j  0. -0.j  0. +0.j  0. -0.j  0. +0.j  0. -0.j
  0. +0.j  0. -0.j  0. +0.j  0. -0.j -0.5+0.j  0. -0.j -0.5+0.j  0. -0.j]

Hi @ashutiw2k ,

The problem here is that you should use qml.apply instead of op.queue().

Here’s a code example using apply.

import pennylane as qml

@qml.qnode(qml.device('default.qubit'))
def circuit_drawer(circ_ops):
    for op in circ_ops:
        qml.apply(op) # Use qml.apply instead of op.queue()
    
    return qml.state()

pennylane_circ_drawer = qml.draw_mpl(circuit_drawer, decimals=4)

base_circ_ops = [qml.CZ(wires=[0, 1]), qml.CNOT(wires=[1, 2])]
base_circ_adjoint = base_circ_ops + [qml.adjoint(op) for op in base_circ_ops[::-1]] 

# Draw the circuits
pennylane_circ_drawer(base_circ_ops)
pennylane_circ_drawer(base_circ_adjoint)

I hope this helps!

Hi @CatalinaAlbornoz , thank you for your reply.

Unfortunately, this does not work either. Upon running the provided code block, here are my figures:


As before, the original base_circuit_ops are drawn correctly, but the second diagram only shows the adjoint operations, despite the initial and adjoint operations being present in the base_circ_adjoint list.

I think I read somewhere yesterday that Pennylane internally “binds” an instance of an operation/gate to a specific Qnode, such that it can not be “executed” on a second/different Qnode - is there something similar going on here?

I don’t think that should be the case, as if I draw just the base_circ_adjoint list (after restarting the Jupyter environment), only the adjoint operations are drawn, like before.

Hi @ashutiw2k ,

I understand your issue now and there’s indeed something strange going on here.
This is not related to PennyLane binding things to a specific qnode, this looks like an issue with joining the two lists of operations. For now the solution is to simply write out the full list of operations, but I’ll keep looking into how to actually fix the core problem.

import pennylane as qml

@qml.qnode(qml.device('default.qubit'))
def circuit_drawer(circ_ops):
    for op in circ_ops:
        qml.apply(op) # Use qml.apply instead of op.queue()
    
    return qml.state()

pennylane_circ_drawer = qml.draw_mpl(circuit_drawer, decimals=4)

base_circ_ops = [qml.CZ(wires=[0, 1]), qml.CNOT(wires=[1, 2])]
base_circ_adjoint = base_circ_ops + [qml.adjoint(op) for op in base_circ_ops[::-1]]
base_circ_adjoint_2 = [qml.CZ(wires=[0, 1]), qml.CNOT(wires=[1, 2])] + [qml.adjoint(op) for op in base_circ_ops[::-1]]

# Draw the circuits
pennylane_circ_drawer(base_circ_adjoint)
pennylane_circ_drawer(base_circ_adjoint_2);

Let me know if this quick workaround is enough for you for now. Thanks again for flagging this issue!

Hi @ashutiw2k ,

It turns out that this might be related to this issue where PennyLane basically thinks you added the gates by accident (since you have both the operator and the adjoint right after). One temporary solution is to create a deepcopy of the operations. PennyLane then considers them as two separate things and adds everything as expected.

import pennylane as qml
import copy

@qml.qnode(qml.device('default.qubit'))
def circuit_drawer(circ_ops):
    for op in circ_ops:
        qml.apply(op) # Use qml.apply instead of op.queue()
    
    return qml.state()

pennylane_circ_drawer = qml.draw_mpl(circuit_drawer, decimals=4)

base_circ_ops = [qml.CZ(wires=[0, 1]), qml.CNOT(wires=[1, 2])]
adjoint_ops = [qml.adjoint(op) for op in base_circ_ops[::-1]]

base_circ_ops2 = copy.deepcopy(base_circ_ops) # Create a deepcopy of the list (a shallow copy doesn't solve the issue)

base_circ_adjoint = base_circ_ops + adjoint_ops # This skips base_circ_ops when drawing
base_circ_adjoint_deepcopy = base_circ_ops2 + adjoint_ops # This works

# Draw the circuits
pennylane_circ_drawer(base_circ_adjoint)
pennylane_circ_drawer(base_circ_adjoint_deepcopy)

Let me know if this works for you!

Hi @CatalinaAlbornoz ,
Yes, this does indeed work.
Thank you for your help!

1 Like

Hi,

For anyone else looking at this issue, it seems that it’s now fixe with this PR.