Shot Adaptive Optimizer limitations

Hello! I have some questions concerning the qml.ShotAdaptiveOptimizer.

  1. In the documentation there’s the following note: “The shot adaptive optimizer only supports single QNode objects as objective functions.”
    Is there a plan to extend it to other possible objective functions?
    Example: commonly used QML losses where there’s a classical post-processing of multiple QNode objects.
    Otherwise which is the main limitation/reason behind this limitation?

  2. When using a qiskit backend it returns an error, could this be connected to the aforementioned problem?

Thanks a lot in advance!
Best

Hi @mai ,

Welcome to the Forum and thanks for your questions!

I’m not sure if part 1 is in the roadmap but I can check.

For both questions could you please provide the following information? It can help us dig deeper into the problem and possible solutions:

a. The output of qml.about()
b. A minimal (but self-contained) working example
c. The full error traceback

If you’re not sure what these mean then make sure to check out this video.

I look forward to seeing your answers!

Hi,
thanks a lot for your answer. I would be really interested into part 1, which is my main concern. In the meantime thanks again, here I’m sharing the required details for part 2:

a)

Name: PennyLane
Version: 0.36.0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location:.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-qiskit, PennyLane_Lightning

Platform info:           macOS-13.2.1-arm64-arm-64bit
Python version:          3.11.6
Numpy version:           1.26.4
Scipy version:           1.13.0
Installed devices:
- default.clifford (PennyLane-0.36.0)
- default.gaussian (PennyLane-0.36.0)
- default.mixed (PennyLane-0.36.0)
- default.qubit (PennyLane-0.36.0)
- default.qubit.autograd (PennyLane-0.36.0)
- default.qubit.jax (PennyLane-0.36.0)
- default.qubit.legacy (PennyLane-0.36.0)
- default.qubit.tf (PennyLane-0.36.0)
- default.qubit.torch (PennyLane-0.36.0)
- default.qutrit (PennyLane-0.36.0)
- default.qutrit.mixed (PennyLane-0.36.0)
- null.qubit (PennyLane-0.36.0)
- qiskit.aer (PennyLane-qiskit-0.36.0)
- qiskit.basicaer (PennyLane-qiskit-0.36.0)
- qiskit.basicsim (PennyLane-qiskit-0.36.0)
- qiskit.ibmq (PennyLane-qiskit-0.36.0)
- qiskit.ibmq.circuit_runner (PennyLane-qiskit-0.36.0)
- qiskit.ibmq.sampler (PennyLane-qiskit-0.36.0)
- qiskit.remote (PennyLane-qiskit-0.36.0)
- lightning.qubit (PennyLane_Lightning-0.36.0)

b) This is basically the qml.ShotAdaptiveOptimizer example found in the documentation but with a “qiskit.aer” device:


from pennylane import numpy as np
import pennylane as qml

coeffs = [2, 4, -1, 5, 2]
obs = [
  qml.X(1),
  qml.Z(1),
  qml.X(0) @ qml.X(1),
  qml.Y(0) @ qml.Y(1),
  qml.Z(0) @ qml.Z(1)
]
H = qml.Hamiltonian(coeffs, obs)
dev = qml.device('qiskit.aer', wires=2,shots=100)

@qml.qnode(dev)
def cost(weights):
    qml.StronglyEntanglingLayers(weights, wires=range(2))
    return qml.expval(H)

shape = qml.templates.StronglyEntanglingLayers.shape(n_layers=2, n_wires=2)
params = np.random.random(shape)
opt = qml.ShotAdaptiveOptimizer(min_shots=10, term_sampling="weighted_random_sampling")
for i in range(60):
   params = opt.step(cost, params)
   print(f"Step {i}: cost = {cost(params):.2f}, shots_used = {opt.total_shots_used}")

c)

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
.venv/adaptiveShot_pennylane_example.ipynb Cell 1 line 2
     22 opt = qml.ShotAdaptiveOptimizer(min_shots=10, term_sampling="weighted_random_sampling")
     23 for i in range(60):
---> 24    params = opt.step(cost, params)
     25    print(f"Step {i}: cost = {cost(params):.2f}, shots_used = {opt.total_shots_used}")

File ~.venv/lib/python3.11/site-packages/pennylane/optimize/shot_adaptive.py:421, in ShotAdaptiveOptimizer.step(self, objective_fn, *args, **kwargs)
    417 self.total_shots_used += self.shots_used
    419 # compute the gradient, as well as the variance in the gradient,
    420 # using the number of shots determined by the array s.
--> 421 grads, grad_variances = self.compute_grad(objective_fn, args, kwargs)
    422 new_args = self.apply_grad(grads, args)
    424 if self.xi is None:

File ~.venv/lib/python3.11/site-packages/pennylane/optimize/shot_adaptive.py:358, in ShotAdaptiveOptimizer.compute_grad(self, objective_fn, args, kwargs)
    345 r"""Compute gradient of the objective function, as well as the variance of the gradient,
    346 at the given point.
    347 
   (...)
    355     :math:`\nabla f(x^{(t)})` and the variance of the gradient
    356 """
    357 if isinstance(objective_fn, qml.QNode) or hasattr(objective_fn, "device"):
--> 358     grads = self._single_shot_qnode_gradients(objective_fn, args, kwargs)
    359 else:
    360     raise ValueError(
    361         "The objective function must be encoded as a single QNode object for the shot "
    362         "adaptive optimizer. "
    363     )

File ~.venv/lib/python3.11/site-packages/pennylane/optimize/shot_adaptive.py:323, in ShotAdaptiveOptimizer._single_shot_qnode_gradients(self, qnode, args, kwargs)
    320     self.check_learning_rate(coeffs)
    322 if self.term_sampling == "weighted_random_sampling":
--> 323     return self.qnode_weighted_random_sampling(
    324         qnode, coeffs, observables, self.max_shots, self.trainable_args, *args, **kwargs
    325     )
    326 if self.term_sampling is not None:
    327     raise ValueError(
    328         f"Unknown Hamiltonian term sampling method {self.term_sampling}. "
    329         "Only term_sampling='weighted_random_sampling' and "
    330         "term_sampling=None currently supported."
    331     )

File ~.venv/lib/python3.11/site-packages/pennylane/optimize/shot_adaptive.py:263, in ShotAdaptiveOptimizer.qnode_weighted_random_sampling(qnode, coeffs, observables, shots, argnums, *args, **kwargs)
    260 else:
    261     cost = qnode
--> 263 jacs = qml.jacobian(cost, argnum=argnums)(*args, **kwargs, shots=new_shots)
    265 if s == 1:
    266     jacs = [np.expand_dims(j, 0) for j in jacs]

File ~.venv/lib/python3.11/site-packages/pennylane/_grad.py:455, in jacobian.<locals>._jacobian_function(*args, **kwargs)
    449 if not _argnum:
    450     warnings.warn(
    451         "Attempted to differentiate a function with no trainable parameters. "
    452         "If this is unintended, please add trainable parameters via the "
    453         "'requires_grad' attribute or 'argnum' keyword."
    454     )
--> 455 jac = tuple(_jacobian(func, arg)(*args, **kwargs) for arg in _argnum)
    457 return jac[0] if unpack else jac

File ~.venv/lib/python3.11/site-packages/pennylane/_grad.py:455, in <genexpr>(.0)
    449 if not _argnum:
    450     warnings.warn(
    451         "Attempted to differentiate a function with no trainable parameters. "
    452         "If this is unintended, please add trainable parameters via the "
    453         "'requires_grad' attribute or 'argnum' keyword."
    454     )
--> 455 jac = tuple(_jacobian(func, arg)(*args, **kwargs) for arg in _argnum)
    457 return jac[0] if unpack else jac

File ~.venv/lib/python3.11/site-packages/autograd/wrap_util.py:20, in unary_to_nary.<locals>.nary_operator.<locals>.nary_f(*args, **kwargs)
     18 else:
     19     x = tuple(args[i] for i in argnum)
---> 20 return unary_operator(unary_f, x, *nary_op_args, **nary_op_kwargs)

File ~.venv/lib/python3.11/site-packages/autograd/differential_operators.py:60, in jacobian(fun, x)
     50 @unary_to_nary
     51 def jacobian(fun, x):
     52     """
     53     Returns a function which computes the Jacobian of `fun` with respect to
     54     positional argument number `argnum`, which must be a scalar or array. Unlike
   (...)
     58     (out1, out2, ...) then the Jacobian has shape (out1, out2, ..., in1, in2, ...).
     59     """
---> 60     vjp, ans = _make_vjp(fun, x)
     61     ans_vspace = vspace(ans)
     62     jacobian_shape = ans_vspace.shape + vspace(x).shape

File ~.venv/lib/python3.11/site-packages/autograd/core.py:10, in make_vjp(fun, x)
      8 def make_vjp(fun, x):
      9     start_node = VJPNode.new_root()
---> 10     end_value, end_node =  trace(start_node, fun, x)
     11     if end_node is None:
     12         def vjp(g): return vspace(x).zeros()

File ~.venv/lib/python3.11/site-packages/autograd/tracer.py:10, in trace(start_node, fun, x)
      8 with trace_stack.new_trace() as t:
      9     start_box = new_box(x, t, start_node)
---> 10     end_box = fun(start_box)
     11     if isbox(end_box) and end_box._trace == start_box._trace:
     12         return end_box._value, end_box._node

File ~.venv/lib/python3.11/site-packages/autograd/wrap_util.py:15, in unary_to_nary.<locals>.nary_operator.<locals>.nary_f.<locals>.unary_f(x)
     13 else:
     14     subargs = subvals(args, zip(argnum, x))
---> 15 return fun(*subargs, **kwargs)

File ~.venv/lib/python3.11/site-packages/pennylane/optimize/shot_adaptive.py:258, in ShotAdaptiveOptimizer.qnode_weighted_random_sampling.<locals>.cost(*args, **kwargs)
    256 def cost(*args, **kwargs):
    257     # pylint: disable=cell-var-from-loop
--> 258     return qml.math.stack(qnode(*args, **kwargs))

File ~.venv/lib/python3.11/site-packages/pennylane/math/multi_dispatch.py:151, in multi_dispatch.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    148 interface = interface or get_interface(*dispatch_args)
    149 kwargs["like"] = interface
--> 151 return fn(*args, **kwargs)

File ~.venv/lib/python3.11/site-packages/pennylane/math/multi_dispatch.py:497, in stack(values, axis, like)
    468 """Stack a sequence of tensors along the specified axis.
    469 
    470 .. warning::
   (...)
    494        [5.00e+00, 8.00e+00, 1.01e+02]], dtype=float32)>
    495 """
    496 values = np.coerce(values, like=like)
--> 497 return np.stack(values, axis=axis, like=like)

File ~.venv/lib/python3.11/site-packages/autoray/autoray.py:81, in do(fn, like, *args, **kwargs)
     79 backend = _choose_backend(fn, args, kwargs, like=like)
     80 func = get_lib_fn(backend, fn)
---> 81 return func(*args, **kwargs)

File ~.venv/lib/python3.11/site-packages/numpy/core/shape_base.py:445, in stack(arrays, axis, out, dtype, casting)
    443 arrays = [asanyarray(arr) for arr in arrays]
    444 if not arrays:
--> 445     raise ValueError('need at least one array to stack')
    447 shapes = {arr.shape for arr in arrays}
    448 if len(shapes) != 1:

ValueError: need at least one array to stack

Hi @mai ,

About question 1, I got an answer from my colleague Tom. It’s not currently on the roadmap but it’s good to know that there’s a need for it! This might motivate adding it to the roadmap in the future. I think the restriction is because it’s clear how to distribute the shots when it’s just a QNode measuring the expectation value of a Hamiltonian. If you have lots of QNodes and classical processing, it’s not clear how shots are distributed.

Of course if you need this functionality you can always make a Fork of PennyLane and add it in your own fork.

About question 2, I can replicate your error. I’m not sure what’s causing it but will investigate.

Ok, thanks a lot again!

Hi @mai ,

It seems that there’s some issue between qml.ShotAdaptiveOptimizer and qiskit.aer. It works well with GradientDescentOptimizer though. Would this be enough for you? Or do you need qml.ShotAdaptiveOptimizer to work with qiskit.aer urgently?

By the way, I’ve created a bug report here. Thank you for making us aware of this issue!

Hello @CatalinaAlbornoz,

Yes, I was interested in ShotAdaptive and that is why I asked for information, I have previously experimented with qiskit.aer backend with other optimizers and there was no problem in those cases.

Thanks again for your support!

Hi @mai , new update.

It turns out that the problem is due to qiskit.aer not supporting shot vectors. Unfortunately since the issue comes from Qiskit itself we cannot fix it, but we’ll add a better error message so that anyone who wants to use this in the future knows what’s happening.