Repository: Qiskit/qiskit

Issue:
VQD fails when using SPSA optimizer
### Environment

- **Qiskit Terra version**: 0.23.0
- **Python version**: 3.9
- **Operating system**: Linux


### What is happening?

When calling `compute_eignevalues` it fails as follows

```
  File ".../qiskit/primitives/base/base_primitive.py", line 108, in _validate_parameter_values
    raise TypeError("Invalid parameter values, expected Sequence[Sequence[float]].")
TypeError: Invalid parameter values, expected Sequence[Sequence[float]].
```

This was raised on external Slack for Qiskit https://qiskit.slack.com/archives/CB6C24TPB/p1675060749792269

The code below, that reproduces the above, is a cut down version of that as supplied in the thread there.

### How can we reproduce the issue?

```
from qiskit.primitives import Estimator, Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.algorithms.optimizers import COBYLA, SPSA
from qiskit.algorithms.eigensolvers import VQD
from qiskit.algorithms.state_fidelities import ComputeUncompute

op = SparsePauliOp(["I", "X"], coeffs=[2.42, 0.3])

ansatz = QuantumCircuit(1)
ansatz.rz(Parameter('theta'), 0)
ansatz.ry(Parameter('phi'), 0)

#optimizer = COBYLA(maxiter=60)
optimizer = SPSA(maxiter=60)


vqd = VQD(estimator=Estimator(),
          fidelity=ComputeUncompute(Sampler()),
          ansatz=ansatz,
          k=2,
          optimizer=optimizer)

result = vqd.compute_eigenvalues(operator=op)

print(result)
```

If the above optimizer is change to COBYLA then it runs to completion

### What should happen?

It should work with SPSA and not raise an error

### Any suggestions?

This comes about I think from this https://github.com/Qiskit/qiskit-terra/blob/06f5ac1e8d7cbf4dcd157c127911f83c1e43e6ae/qiskit/algorithms/eigensolvers/vqd.py#L275-L285 where we allow the optimizer to batch points where it can. SPSA will do this for calibration (and for each gradient step later). Disabling the code which does that setting (ie forcing it to do things one at a time) allows it to work.



Discussion:
FYI setting max_evals_grouped on the optimizer, as below, in the above code fails too (though not quite the same way) - by default `max_evals_grouped` is 1 but setting it higher allows points to be batched (grouped) together which it will do when using its internal default finite diff gradient.

```
optimizer = SLSQP(maxiter=60, max_evals_grouped=10)
```
I think I have identified the cause of this issue. VQD is missing some parameter batching functionality. I will follow up with a bugfix PR as soon as I find the bandwidth.

Resolving patch:
diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py
--- a/qiskit/algorithms/eigensolvers/vqd.py
+++ b/qiskit/algorithms/eigensolvers/vqd.py
@@ -1,6 +1,6 @@
 # This code is part of Qiskit.
 #
-# (C) Copyright IBM 2022.
+# (C) Copyright IBM 2022, 2023.
 #
 # This code is licensed under the Apache License, Version 2.0. You may
 # obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -193,7 +193,6 @@ def compute_eigenvalues(
         operator: BaseOperator | PauliSumOp,
         aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None,
     ) -> VQDResult:
-
         super().compute_eigenvalues(operator, aux_operators)
 
         # this sets the size of the ansatz, so it must be called before the initial point
@@ -226,7 +225,6 @@ def compute_eigenvalues(
             aux_operators = None
 
         if self.betas is None:
-
             if isinstance(operator, PauliSumOp):
                 operator = operator.coeff * operator.primitive
 
@@ -254,7 +252,6 @@ def compute_eigenvalues(
         prev_states = []
 
         for step in range(1, self.k + 1):
-
             # update list of optimal circuits
             if step > 1:
                 prev_states.append(self.ansatz.bind_parameters(result.optimal_points[-1]))
@@ -365,22 +362,28 @@ def _get_evaluate_energy(
         self._check_operator_ansatz(operator)
 
         def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float:
+            # handle broadcasting: ensure parameters is of shape [array, array, ...]
+            if len(parameters.shape) == 1:
+                parameters = np.reshape(parameters, (-1, num_parameters))
+            batch_size = len(parameters)
 
             estimator_job = self.estimator.run(
-                circuits=[self.ansatz], observables=[operator], parameter_values=[parameters]
+                batch_size * [self.ansatz], batch_size * [operator], parameters
             )
 
-            total_cost = 0
+            total_cost = np.zeros(batch_size)
+
             if step > 1:
                 # compute overlap cost
+                batched_prev_states = [state for state in prev_states for _ in range(batch_size)]
                 fidelity_job = self.fidelity.run(
-                    [self.ansatz] * (step - 1),
-                    prev_states,
-                    [parameters] * (step - 1),
+                    batch_size * [self.ansatz] * (step - 1),
+                    batched_prev_states,
+                    np.tile(parameters, (step - 1, 1)),
                 )
-
                 costs = fidelity_job.result().fidelities
 
+                costs = np.reshape(costs, (step - 1, -1))
                 for state, cost in enumerate(costs):
                     total_cost += np.real(betas[state] * cost)
 
@@ -394,7 +397,7 @@ def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float:
 
             if self.callback is not None:
                 metadata = estimator_result.metadata
-                for params, value, meta in zip([parameters], values, metadata):
+                for params, value, meta in zip(parameters, values, metadata):
                     self._eval_count += 1
                     self.callback(self._eval_count, params, value, meta, step)
             else:
@@ -406,7 +409,6 @@ def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float:
 
     @staticmethod
     def _build_vqd_result() -> VQDResult:
-
         result = VQDResult()
         result.optimal_points = []
         result.optimal_parameters = []
@@ -420,7 +422,6 @@ def _build_vqd_result() -> VQDResult:
 
     @staticmethod
     def _update_vqd_result(result, opt_result, eval_time, ansatz) -> VQDResult:
-
         result.optimal_points.append(opt_result.x)
         result.optimal_parameters.append(dict(zip(ansatz.parameters, opt_result.x)))
         result.optimal_values.append(opt_result.fun)


Test patch:
diff --git a/test/python/algorithms/eigensolvers/test_vqd.py b/test/python/algorithms/eigensolvers/test_vqd.py
--- a/test/python/algorithms/eigensolvers/test_vqd.py
+++ b/test/python/algorithms/eigensolvers/test_vqd.py
@@ -19,13 +19,9 @@
 from ddt import data, ddt
 
 from qiskit import QuantumCircuit
-from qiskit.algorithms.eigensolvers import VQD
+from qiskit.algorithms.eigensolvers import VQD, VQDResult
 from qiskit.algorithms import AlgorithmError
-from qiskit.algorithms.optimizers import (
-    COBYLA,
-    L_BFGS_B,
-    SLSQP,
-)
+from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B, SLSQP, SPSA
 from qiskit.algorithms.state_fidelities import ComputeUncompute
 from qiskit.circuit.library import TwoLocal, RealAmplitudes
 from qiskit.opflow import PauliSumOp
@@ -207,6 +203,7 @@ def store_intermediate_result(eval_count, parameters, mean, metadata, step):
     @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI)
     def test_vqd_optimizer(self, op):
         """Test running same VQD twice to re-use optimizer, then switch optimizer"""
+
         vqd = VQD(
             estimator=self.estimator,
             fidelity=self.fidelity,
@@ -231,6 +228,17 @@ def run_check():
             vqd.optimizer = L_BFGS_B()
             run_check()
 
+        with self.subTest("Batched optimizer replace"):
+            vqd.optimizer = SLSQP(maxiter=60, max_evals_grouped=10)
+            run_check()
+
+        with self.subTest("SPSA replace"):
+            # SPSA takes too long to converge, so we will
+            # only check that it runs with no errors.
+            vqd.optimizer = SPSA(maxiter=5, learning_rate=0.01, perturbation=0.01)
+            result = vqd.compute_eigenvalues(operator=op)
+            self.assertIsInstance(result, VQDResult)
+
     @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI)
     def test_aux_operators_list(self, op):
         """Test list-based aux_operators."""