Skip to content

Commit

Permalink
Fix VQD with SPSA optimizer (#9538)
Browse files Browse the repository at this point in the history
* Add parameter batching

* Add spsa to tests

* Add reno

* Remove batch size from evals

* Apply suggestions from code review

* Apply suggestion from code review

* Update tests

* Delete Untitled.ipynb

* Fix batch size SPSA

* Small fixes

* fix batching

* update test

* Update reno

* Change SPSA test

* Apply reno suggestion

Co-authored-by: Julien Gacon <gaconju@gmail.com>

---------

Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 17, 2023
1 parent ecb6f9a commit 061aee2
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 19 deletions.
27 changes: 14 additions & 13 deletions qiskit/algorithms/eigensolvers/vqd.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -226,7 +225,6 @@ def compute_eigenvalues(
aux_operators = None

if self.betas is None:

if isinstance(operator, PauliSumOp):
operator = operator.coeff * operator.primitive

Expand Down Expand Up @@ -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]))
Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand All @@ -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 = []
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
fixes:
- |
Fixed a bug in the :class:`~.eigensolvers.VQD` algorithm where
the energy evaluation function could not process batches of parameters, making it
incompatible with optimizers with ``max_evals_grouped>1``.
See `#9500 <https://github.com/Qiskit/qiskit-terra/issues/9500>`__.
20 changes: 14 additions & 6 deletions test/python/algorithms/eigensolvers/test_vqd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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."""
Expand Down

0 comments on commit 061aee2

Please sign in to comment.