Creating QNodes for large numbers of circuits that only differ by number of wires

Take the following simple quantum function:

def my_quantum_function(params, wires=None):

    for i in range(wires):
        random_rot = params[i]
        qml.RY(random_rot, wires=i)
        
    qml.broadcast(qml.CZ, wires=range(wires), pattern="chain")
     
    return qml.expval(qml.PauliZ(0))

Let’s say I want to make QNode objects using this function for wires=2 and wires=3. My understanding is that I can either leave the function as it is and write,

params = [np.random.uniform(0, 2*np.pi) for _ in range(wires)]

dev1 = qml.device("default.qubit", wires=2)
circuit1 = qml.QNode(my_quantum_function, dev1)
result1 = circuit1(params, wires=2)

dev2 = qml.device("default.qubit", wires=3)
circuit2 = qml.QNode(my_quantum_function, dev2)
result2 = circuit2(params, wires=3)

or, I can make two copies of the function and use the QNode decorator:

dev1 = qml.device("default.qubit", wires=2)
dev2 = qml.device("default.qubit", wires=2)

@qml.qnode(dev1)
def circuit1(params):

    wires = 2
    for i in range(wires):
        random_rot = params[i]
        qml.RY(random_rot, wires=i)
        
    qml.broadcast(qml.CZ, wires=range(wires), pattern="chain")
     
    return qml.expval(qml.PauliZ(0))

@qml.qnode(dev2)
def circuit2(params):

    wires = 3
    for i in range(wires):
        random_rot = params[i]
        qml.RY(random_rot, wires=i)
        
    qml.broadcast(qml.CZ, wires=range(wires), pattern="chain")
     
    return qml.expval(qml.PauliZ(0))

params = [np.random.uniform(0, 2*np.pi) for _ in range(wires)]
result1 = circuit1(params)
result2 = circuit2(params)

Fair enough. But let’s say I wanted to create QNode objects using this function for wires=2,3,4,…,20. What is the most idiomatic way to do this? I know QNodes collections is a way to define families of QNodes, but this still requires explicitly defining all N devices you need, which doesn’t end up saving any time, it seems. Right now what I’m doing is using the top function I gave and a for-loop, defining a new device/QNode on each iteration:

n_wires = [i+2 for i in range(19)]
for n in n_wires:
    params = [np.random.uniform(0, 2*np.pi) for _ in range(n)]
    dev = qml.device("default.qubit", wires=n)
    circuit = qml.QNode(my_quantum_function, dev)
    result = circuit(thetas, wires=n)
    ...
    # Do some computations
    ...

Using the QNode decorator is the recommended approach, and QNodes can be set to mutable, but I can’t find an example in the documentation where you can adjust the number of wires on the fly. Maybe there isn’t a more elegant way to do this, but I thought I would ask!

Hi @ryanhill1,

Welcome to the forum!

I’d say that your approach of having a quantum function that depends on the number of wires, creating separate QNodes and devices is a good one. PennyLane doesn’t have any further conveniences for such a use case, other than creating a QNodeCollection which you’ve mentioned.

When considering the exact use case, each of these quantum functions would actually be equivalent to running a quantum circuit of size N=2..20 on a quantum device that is capable of simulating exactly N qubits.

The implied alternative to this would have to involve creating a single quantum device with 20 qubits, running the quantum circuits one by one on that device and resetting the device in between circuit runs (can be achieved by calling dev.reset()). Both for simulators and for hardware devices it can be costly to have unused ancillae subsystems. One simulator that could be interesting is the The Wavefunction device from the PennyLane-Forest plugin, which only keeps track of the active subsystems in memory.

It could be interesting to see in which cases a tradeoff could be made between the two approaches.

1 Like