Running a VQA with Qiskit Runtime

Hello,

I am trying to run a VQA on IBM’s quantum computers through the Qiskit plugin, but I’m not sure how to do so. I have an algorithm that, on every iteration step, runs a bunch of different (related) circuits and accumulates the values for a single calculation of the cost function. I saw that Pennylane supports a few primitives like circuit_runner that would let you run the entire optimization with a single job to the quantum computer, but I’m not sure how to prevent my code from spawning over 30 jobs per iteration.

Here is my code run on a toy problem:

import pennylane as qml
from pennylane import numpy as np

import matplotlib.pyplot as plt
# Number of system qubits; this determines the size of the matrix A and vector b
n_qubits = 3

print(type(n_qubits))

# Number of quantum measurements performed to get a probability distribution of results
n_shots = 10**6

# Total number of qubits; here we add an ancillary qubit
tot_qubits = n_qubits + 1

# Index of ancillary qubit (Python lists are 0-indexed)
ancilla_idx = n_qubits

# Number of optimization steps
steps = 30

# Learning rate of optimization algorithm
eta = 0.8

# Initial spread of random quantum weights
q_delta = 0.001

# Seed for RNG
rng_seed = 0
# Coefficients of the linear combination A = c_0 A_0 + c_1 A_1 ...
c = np.array([1.0, 0.2, 0.2])
print(type(c))
print(len(c))



def U_b():
    """Unitary matrix rotating the ground state to the problem vector |b> = U_b |0>."""
    
    # This loop applies the Hadamard operator on each wire
    for idx in range(n_qubits):
        qml.Hadamard(wires=idx)


# This function gives the components of A
def CA(idx):
    """Controlled versions of the unitary components A_l of the problem matrix A."""
    if idx == 0:
        # Identity operation
        None

    elif idx == 1:
        # CNOT gate is the same as controlled Pauli-X gate
        qml.CNOT(wires=[ancilla_idx, 0])
        qml.CZ(wires=[ancilla_idx, 1])

    elif idx == 2:
        qml.CNOT(wires=[ancilla_idx, 0])

def variational_block(weights):
    """Variational circuit mapping the ground state |0> to the ansatz state |x>."""
    # We first prepare an equal superposition of all the states of the computational basis.
    for idx in range(n_qubits):
        qml.Hadamard(wires=idx)

    # A very minimal variational circuit.
    for idx, element in enumerate(weights):
        qml.RY(element, wires=idx)
# TODO: this is what we vary to access different devices (like IBM, Rigetti etc.)
# Loads a particular quantum device
# 'default.qubit' is a simple state simulator
# 'wires' is a aparamter specifying the number of wires (subsytems) to initialise the device with
dev_mu = qml.device("default.qubit", wires=tot_qubits)


# Quantum node contains a quantum function (in this case, local_hadamard_test) and the computational 
# device it's executed on (in this case, dev_mu)
@qml.qnode(dev_mu, interface="autograd")
# For this function, l, lp, j are the indices for the mu coefficients
def local_hadamard_test(weights, l=None, lp=None, j=None, part=None):

    # First Hadamard gate applied to the ancillary qubit.
    qml.Hadamard(wires=ancilla_idx)

    # For estimating the imaginary part of the coefficient "mu", we must add a "-i"
    # phase gate.
    if part == "Im" or part == "im":
        qml.PhaseShift(-np.pi / 2, wires=ancilla_idx)

    # Variational circuit generating a guess for the solution vector |x>
    variational_block(weights)

    # Controlled application of the unitary component A_l of the problem matrix A.
    CA(l)

    # Adjoint of the unitary U_b associated to the problem vector |b>.
    # In this specific example Adjoint(U_b) = U_b.
    U_b()

    # Controlled Z operator at position j. If j = -1, apply the identity.
    if j != -1:
        qml.CZ(wires=[ancilla_idx, j])

    # Unitary U_b associated to the problem vector |b>.
    U_b()

    # Controlled application of Adjoint(A_lp).
    # In this specific example Adjoint(A_lp) = A_lp.
    CA(lp)

    # Second Hadamard gate applied to the ancillary qubit.
    qml.Hadamard(wires=ancilla_idx)

    # Expectation value of Z for the ancillary qubit.
    return qml.expval(qml.PauliZ(wires=ancilla_idx))
# Computes the mu coefficients
def mu(weights, l=None, lp=None, j=None):
    """Generates the coefficients to compute the "local" cost function C_L."""

    mu_real = local_hadamard_test(weights, l=l, lp=lp, j=j, part="Re")
    mu_imag = local_hadamard_test(weights, l=l, lp=lp, j=j, part="Im")

    return mu_real + 1.0j * mu_imag
def psi_norm(weights):
    """Returns the normalization constant <psi|psi>, where |psi> = A |x>."""
    norm = 0.0

    for l in range(0, len(c)):
        for lp in range(0, len(c)):
            norm = norm + c[l] * np.conj(c[lp]) * mu(weights, l, lp, -1)

    return abs(norm)
def cost_loc(weights):
    """Local version of the cost function. Tends to zero when A|x> is proportional to |b>."""
    mu_sum = 0.0

    for l in range(0, len(c)):
        for lp in range(0, len(c)):
            for j in range(0, n_qubits):
                mu_sum = mu_sum + c[l] * np.conj(c[lp]) * mu(weights, l, lp, j)

    mu_sum = abs(mu_sum)

    # Cost function C_L
    return 0.5 - 0.5 * mu_sum / (n_qubits * psi_norm(weights))
np.random.seed(rng_seed)
w = q_delta * np.random.randn(n_qubits, requires_grad=True)

opt = qml.GradientDescentOptimizer(eta)

cost_history = []
for it in range(steps):
    w, cost = opt.step_and_cost(cost_loc, w)
    print("Step {:3d}       Cost_L = {:9.7f}".format(it, cost))
    cost_history.append(cost)

If you want help with diagnosing an error, please put the full error message below:

qml.about():

Name: PennyLane
Version: 0.30.0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml
Required-by: PennyLane-Lightning

Platform info:           macOS-12.6-x86_64-i386-64bit
Python version:          3.11.6
Numpy version:           1.23.5
Scipy version:           1.10.1
Installed devices:
- default.gaussian (PennyLane-0.30.0)
- default.mixed (PennyLane-0.30.0)
- default.qubit (PennyLane-0.30.0)
- default.qubit.autograd (PennyLane-0.30.0)
- default.qubit.jax (PennyLane-0.30.0)
- default.qubit.tf (PennyLane-0.30.0)
- default.qubit.torch (PennyLane-0.30.0)
- default.qutrit (PennyLane-0.30.0)
- null.qubit (PennyLane-0.30.0)
- lightning.qubit (PennyLane-Lightning-0.30.0)

Hi @jkwan314, welcome back to the Forum!

I see that you’re using an older version of PennyLane.

Both PennyLane and Qiskit have evolved a lot in the past few months so everything you see in the PennyLane documentation works with version 0.33.

My recommendation would be to upgrade PennyLane and the PennyLane-Qiskit plugin using python -m pip install --upgrade pennylane pennylane-qiskit.

You may need to update Qiskit and its subpackages in a similar way.

This upgrade will require that you probably change some things in your code.

Regarding the job issue you can try to do broadcasting to send everything as a single job. Below is an example.

Remember to always load your account at the beginning.

# Import your favourite library
import pennylane as qml

# Import Numpy from PennyLane
from pennylane import numpy as np

# Import the library that you need in order to use your IBM account
import qiskit_ibm_provider

IBM_token = #'Your Token Goes Here'

try:
    qiskit_ibm_provider.IBMProvider()
except:
    qiskit_ibm_provider.IBMProvider.save_account(token=IBM_token, overwrite=True)

Here’s an example without broadcasting. Note that theta is a single number.

# Choose the device you want to use.
# dev = qml.device("lightning.qubit", wires=2, shots=1000)
dev = qml.device('qiskit.aer', wires=2, shots=1000)
# dev = qml.device('qiskit.ibmq', wires=2, shots=1000, backend='ibmq_qasm_simulator')
# dev = qml.device('qiskit.ibmq', wires=2, shots=1000, backend='ibm_lagos')


# Create a QNode with 2 entangled qubits
@qml.qnode(dev)
def circuit(theta):
    qml.RX(theta,wires=0)
    qml.CNOT(wires=[0,1])
    return qml.probs()

theta = np.array(1.0,requires_grad=True)
# Draw your circuit
qml.draw_mpl(circuit,style='pennylane')(theta)

# Run your circuit
print('Probabilities of measuring 00, 01, 10 , and 11:', circuit(theta))

Now here’s an example with broadcasting. Note that everywhere except in the QNode I’ve changed theta into thetas, and it’s now an array of values.

# Choose the device you want to use.
# dev = qml.device("lightning.qubit", wires=2, shots=1000)
dev = qml.device('qiskit.aer', wires=2, shots=1000)
# dev = qml.device('qiskit.ibmq', wires=2, shots=1000, backend='ibmq_qasm_simulator')
# dev = qml.device('qiskit.ibmq', wires=2, shots=1000, backend='ibm_lagos')


# Create a QNode with 2 entangled qubits
@qml.qnode(dev)
def circuit(theta):
    qml.RX(theta,wires=0)
    qml.CNOT(wires=[0,1])
    return qml.probs()

thetas = np.array([1.0,2.0],requires_grad=True)
# Draw your circuit
qml.draw_mpl(circuit,style='pennylane')(thetas)

# Run your circuit
print('Probabilities of measuring 00, 01, 10 , and 11:', circuit(thetas))

This should help you adapt your code to run it on IBM devices and reduce the number of jobs needed.

Please let me know if you have any further questions!

By the way,

We have a very small survey for PennyLane v0.33 , and it would be awesome if you’d give us some feedback and tell us about your needs. Thank you! :grin:

1 Like

Hi @CatalinaAlbornoz ,

Thanks for the prompt reply. Just to make sure I’m understanding your solution correctly, you are suggesting that I run a single job where I broadcast the parameters (ie. running the same circuit on each parameter)? The reason I’m clarifying is that if you look at my local_hadamard_test function, the circuit I’m trying to run is actually different for each “parameter” I’m trying to accumulate.

Hi @jkwan314,

My solution wouldn’t work in every single case so I’m not sure whether it works in your exact situation.

Did you try it out?

Hi @CatalinaAlbornoz,

Broadcasting sounds like a great idea, but I don’t think it’s particularly applicable for my use case. If my understanding is correct, we can apply the same circuit to multiple parameters. The issue is, the gates in my circuit changes for each term I’m trying to accumulate (by change I mean small changes like adding / removing a single S gate, or changing the target qubit of CZ etc.). That wouldn’t work with broadcasting.

That said, I thought of a way I could reduce the number of training epochs through using broadcast to implement something akin to a batched gradient descent. Here’s the process I was thinking of:

  1. run circuit(thetas)
  2. Somehow aggregrate the broadcasted outputs
  3. gradient descent

I tried running it off the sample code you gave:

# Import your favourite library
import pennylane as qml

# Import Numpy from PennyLane
from pennylane import numpy as np

# Import the library that you need in order to use your IBM account
# import qiskit_ibm_provider

# IBM_token = #'Your Token Goes Here'

# try:
#     qiskit_ibm_provider.IBMProvider()
# except:
#     qiskit_ibm_provider.IBMProvider.save_account(token=IBM_token, overwrite=True)
# Choose the device you want to use.
# dev = qml.device("lightning.qubit", wires=2, shots=1000)
dev = qml.device('qiskit.aer', wires=2, shots=1000)
# dev = qml.device('qiskit.ibmq', wires=2, shots=1000, backend='ibmq_qasm_simulator')
# dev = qml.device('qiskit.ibmq', wires=2, shots=1000, backend='ibm_lagos')


# Create a QNode with 2 entangled qubits
@qml.qnode(dev)
def circuit(theta):
    qml.RX(theta,wires=0)
    qml.CNOT(wires=[0,1])
    return qml.expval(qml.PauliZ(0))

thetas = np.array([1.0,2.0],requires_grad=True)
# Draw your circuit
qml.draw_mpl(circuit,style='pennylane')(thetas)

# Run your circuit
print('expval', circuit(thetas))
def cost(circuit, thetas):
    c = circuit(thetas)
    return np.mean(c) # collapses the gradient tape??
opt = qml.GradientDescentOptimizer(0.01)
cost_history = []
for it in range(10):
# for it in range(steps):
    thetas, cost = opt.step_and_cost(lambda thetas: cost(circuit, thetas), thetas)
    
    # clear_output(wait=True)

    print("Step {:3d}       Cost_L = {:9.7f}".format(it, cost), flush=True)
    cost_history.append(cost)

But I’m getting the following error:

TypeError                                 Traceback (most recent call last)
/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/Jeffrey_test.ipynb セル 6 line 4
      1 cost_history = []
      2 for it in range(10):
      3 # for it in range(steps):
----> 4     thetas, cost = opt.step_and_cost(lambda thetas: cost(circuit, thetas), thetas)
      6     # clear_output(wait=True)
      8     print("Step {:3d}       Cost_L = {:9.7f}".format(it, cost), flush=True)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/optimize/gradient_descent.py:59, in GradientDescentOptimizer.step_and_cost(self, objective_fn, grad_fn, *args, **kwargs)
     39 def step_and_cost(self, objective_fn, *args, grad_fn=None, **kwargs):
     40     """Update trainable arguments with one step of the optimizer and return the corresponding
     41     objective function value prior to the step.
     42 
   (...)
     56         If single arg is provided, list [array] is replaced by array.
     57     """
---> 59     g, forward = self.compute_grad(objective_fn, args, kwargs, grad_fn=grad_fn)
     60     new_args = self.apply_grad(g, args)
     62     if forward is None:

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/optimize/gradient_descent.py:117, in GradientDescentOptimizer.compute_grad(objective_fn, args, kwargs, grad_fn)
     99 r"""Compute gradient of the objective function at the given point and return it along with
    100 the objective function forward pass (if available).
...
----> 4     thetas, cost = opt.step_and_cost(lambda thetas: cost(circuit, thetas), thetas)
      6     # clear_output(wait=True)
      8     print("Step {:3d}       Cost_L = {:9.7f}".format(it, cost), flush=True)

TypeError: 'numpy.float64' object is not callable

qml.about():

Name: PennyLane
Version: 0.33.1
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane-Lightning, PennyLane-qiskit

Platform info:           macOS-12.6-x86_64-i386-64bit
Python version:          3.11.6
Numpy version:           1.23.5
Scipy version:           1.10.1
Installed devices:
- default.gaussian (PennyLane-0.33.1)
- default.mixed (PennyLane-0.33.1)
- default.qubit (PennyLane-0.33.1)
- default.qubit.autograd (PennyLane-0.33.1)
- default.qubit.jax (PennyLane-0.33.1)
- default.qubit.legacy (PennyLane-0.33.1)
- default.qubit.tf (PennyLane-0.33.1)
- default.qubit.torch (PennyLane-0.33.1)
- default.qutrit (PennyLane-0.33.1)
- null.qubit (PennyLane-0.33.1)
- lightning.qubit (PennyLane-Lightning-0.33.1)
- qiskit.aer (PennyLane-qiskit-0.33.1)
- qiskit.basicaer (PennyLane-qiskit-0.33.1)
- qiskit.ibmq (PennyLane-qiskit-0.33.1)
- qiskit.ibmq.circuit_runner (PennyLane-qiskit-0.33.1)
- qiskit.ibmq.sampler (PennyLane-qiskit-0.33.1)
- qiskit.remote (PennyLane-qiskit-0.33.1)

Hi @jkwan314,

The issue is that you’re calling two things cost. The output of your optimization is the new value of the parameters and the previous value of the cost so I recommend changing the name of this variable to cost_prev as shown below and you will see no errors anymore.

# for it in range(steps):
    thetas, cost_prev = opt.step_and_cost(lambda thetas: cost(circuit, thetas), thetas)
    
    # clear_output(wait=True)

    print("Step {:3d}       Cost_L = {:9.7f}".format(it, cost_prev), flush=True)
    cost_history.append(cost_prev)
1 Like