Shot Adaptive Optimiser not Implemented Correctly in Pennylane?


I would like to use the ShotAdaptiveOptimiser for depth 1 QAOA. However, I get an error if the depth of the QAOA circuit is not greater or equal to 2. Below, I have reproduced the same error using the example code from the ShotAdaptiveOptimiser documentation page, by only modifying the number of StronglyEntanglingLayers to 1.

import pennylane as qml
from pennylane import numpy as npp

coeffs = [2, 4, -1, 5, 2]
obs = [
    qml.PauliX(0) @ qml.PauliX(1),
    qml.PauliY(0) @ qml.PauliY(1),
    qml.PauliZ(0) @ qml.PauliZ(1)
H = qml.Hamiltonian(coeffs, obs)
dev = qml.device("default.qubit", wires=2, shots=100)
cost = qml.ExpvalCost(qml.templates.StronglyEntanglingLayers, H, dev)

shape = qml.templates.StronglyEntanglingLayers.shape(n_layers=1, n_wires=2)
params = npp.random.random(shape)

opt = qml.ShotAdaptiveOptimizer(min_shots=10)
for i in range(6):
    params = opt.step(cost, params)
    print(f"Step {i}: cost = {cost(params):.2f}, shots_used = {opt.total_shots_used}")

Here is the output of running the code above:

[[[0.34656026 0.20603406 0.97567429]
  [0.89482299 0.70363518 0.84379755]]]
Step 0: cost = 1.14, shots_used = 120
IndexError                                Traceback (most recent call last)
c:\Code\Shot_Adaptive_Opt_Tests.ipynb Cell 2 in 1
     17 opt = qml.ShotAdaptiveOptimizer(min_shots=10)
     18 for i in range(6):
---> 19     params = opt.step(cost, params)
     20     print(f"Step {i}: cost = {cost(params):.2f}, shots_used = {opt.total_shots_used}")

File c:\ProgramData\Anaconda3\lib\site-packages\pennylane\optimize\, in ShotAdaptiveOptimizer.step(self, objective_fn, *args, **kwargs)
    444 self.total_shots_used += self.shots_used
    446 # compute the gradient, as well as the variance in the gradient,
    447 # using the number of shots determined by the array s.
--> 448 grads, grad_variances = self.compute_grad(objective_fn, args, kwargs)
    449 new_args = self.apply_grad(grads, args)
    451 if self.xi is None:

File c:\ProgramData\Anaconda3\lib\site-packages\pennylane\optimize\, in ShotAdaptiveOptimizer.compute_grad(self, objective_fn, args, kwargs)
    401 s = np.zeros_like(grad[0])
    403 for idx in p_ind:
--> 404     grad_slice = grad[(slice(0, self.s[i][idx]),) + idx]
    405     g[idx] = np.mean(grad_slice)
    406     s[idx] = np.var(grad_slice, ddof=1)

File c:\ProgramData\Anaconda3\lib\site-packages\pennylane\numpy\, in tensor.__getitem__(self, *args, **kwargs)
    186 def __getitem__(self, *args, **kwargs):
--> 187     item = super().__getitem__(*args, **kwargs)
    189     if not isinstance(item, tensor):
    190         item = tensor(item, requires_grad=self.requires_grad)

IndexError: too many indices for array: array is 2-dimensional, but 3 were indexed

The output of running qml.about() is the following:

Name: PennyLane
Version: 0.30.0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
License: Apache License 2.0
Location: c:\programdata\anaconda3\lib\site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml
Required-by: PennyLane-Lightning

Platform info:           Windows-10-10.0.19045-SP0
Python version:          3.9.15
Numpy version:           1.23.5
Scipy version:           1.10.1
Installed devices:
- lightning.qubit (PennyLane-Lightning-0.30.0)
- 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)
- (PennyLane-0.30.0)
- default.qubit.torch (PennyLane-0.30.0)
- default.qutrit (PennyLane-0.30.0)
- null.qubit (PennyLane-0.30.0)

I have tried modifying the ShotAdaptiveOptimiser class code in the pennylane package to get more information about the error root, and I could find out that the first time the class method compute_grad is executed, the class attribute self.s has the same shape as the params variable defined and printed in the code above, that is

[tensor([[[10, 10, 10],
         [10, 10, 10]]], dtype=int64, requires_grad=True)]

However, after the first iteration, the class attribute self.s has lost one axis, and looks like this

[tensor([[5, 5, 5],
        [5, 5, 5]], dtype=int64, requires_grad=True)]

Moreover, I also believe that the code in line 482 in which is

self.s[idx] = np.squeeze(np.int64(np.clip(s, min(2, self.min_shots), smax)))

should be

self.s[idx] = np.squeeze(np.int64(np.clip(s, max(2, self.min_shots), smax)))

so that the number of shots to use is always greater or equal to self.min_shots, which might be greater than 2. There must also be another error somewhere, because as you can see in the value of self.s after the first iteration, the number of shots to use is strictly less than 10, which was the minimum defined when the qml.ShotAdaptiveOptimizer object was created.

Thank you very much in advance.

Hey @pormelrog!

Excellent post :ok_hand:. Was able to replicate your error and appreciated you digging into it yourself :construction_worker_man:.

Do you mind making a bug report?

In the meantime, a temporary solution would be for you to do this in the source code (line 482 of

            self.s[idx] = (
                np.int64(np.clip(s, min(2, self.min_shots), smax)).squeeze()
                if len(self.s[idx]) > 1
                else np.int64(np.clip(s, min(2, self.min_shots), smax))

I’m not sure if this is the best longterm & efficient solution, so best to leave it to the developers to figure out :slight_smile:. They can reference your bug report though!

Hello Isaac,

I have created a bug report: [BUG] ShotAdaptiveOptimiser Number of Shots to Use Vector Missing An Axis after First Iteration · Issue #4207 · PennyLaneAI/pennylane · GitHub

As I mentioned in my post above (and also in the bug report), I also believe that the clipping is not done correctly, because according to the paper on which the AdaptiveShotOptimiser (iCANS1) is based, the elements in the vector self.s must all be greater or equal to self.min_shots. However, the code self.s[idx] = np.squeeze(np.int64(np.clip(s, min(2, self.min_shots), smax))) floors the elements at 2, and it must be replaced by self.s[idx] = np.squeeze(np.int64(np.clip(s, max(2, self.min_shots), smax))).

Let me know if you need more information, the bug report is missing anything or there is something I should do.

Kind regards,


Looks good! Thank you :grin:. I’ve notified the relevant people and we’ll look into it :slight_smile: