Skip to content

Commit

Permalink
Fix VQD for k>2 (#8989)
Browse files Browse the repository at this point in the history
* Fix vqd for k>2

Also ensure the optimal circuits from previous iterations are only constructed once and recognizable by the primitive caches.

* Remove empty parameter list

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit aab18fc)
  • Loading branch information
Cryoris authored and mergify[bot] committed Oct 27, 2022
1 parent c336cf5 commit 277472e
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 24 deletions.
40 changes: 23 additions & 17 deletions qiskit/algorithms/eigensolvers/vqd.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,16 @@ class VQD(VariationalAlgorithm, Eigensolver):
optimizer(Optimizer): A classical optimizer. Can either be a Qiskit optimizer or a callable
that takes an array as input and returns a Qiskit or SciPy optimization result.
k (int): the number of eigenvalues to return. Returns the lowest k eigenvalues.
betas (list[float]): beta parameters in the VQD paper.
betas (list[float]): Beta parameters in the VQD paper.
Should have length k - 1, with k the number of excited states.
These hyper-parameters balance the contribution of each overlap term to the cost
function and have a default value computed as the mean square sum of the
coefficients of the observable.
initial point (list[float]): An optional initial point (i.e. initial parameter values)
for the optimizer. If ``None`` then VQD will look to the ansatz for a preferred
point and if not will simply compute a random one.
callback (Callable[[int, np.ndarray, float, dict[str, Any]], None] | None):
a callback that can access the intermediate data
A callback that can access the intermediate data
during the optimization. Four parameter values are passed to the callback as
follows during each evaluation by the optimizer: the evaluation count,
the optimizer parameters for the ansatz, the estimated value, the estimation
Expand All @@ -124,16 +127,16 @@ def __init__(
ansatz: A parameterized circuit used as ansatz for the wave function.
optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable
that takes an array as input and returns a Qiskit or SciPy optimization result.
k: the number of eigenvalues to return. Returns the lowest k eigenvalues.
betas: beta parameters in the VQD paper.
k: The number of eigenvalues to return. Returns the lowest k eigenvalues.
betas: Beta parameters in the VQD paper.
Should have length k - 1, with k the number of excited states.
These hyperparameters balance the contribution of each overlap term to the cost
function and have a default value computed as the mean square sum of the
coefficients of the observable.
initial_point: an optional initial point (i.e. initial parameter values)
initial_point: An optional initial point (i.e. initial parameter values)
for the optimizer. If ``None`` then VQD will look to the ansatz for a preferred
point and if not will simply compute a random one.
callback: a callback that can access the intermediate data
callback: A callback that can access the intermediate data
during the optimization. Four parameter values are passed to the callback as
follows during each evaluation by the optimizer: the evaluation count,
the optimizer parameters for the ansatz, the estimated value,
Expand Down Expand Up @@ -238,11 +241,19 @@ def compute_eigenvalues(
if aux_operators is not None:
aux_values = []

# We keep a list of the bound circuits with optimal parameters, to avoid re-binding
# the same parameters to the ansatz if we do multiple steps
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]))

self._eval_count = 0
energy_evaluation = self._get_evaluate_energy(
step, operator, betas, prev_states=result.optimal_parameters
step, operator, betas, prev_states=prev_states
)

start_time = time()
Expand Down Expand Up @@ -304,15 +315,15 @@ def _get_evaluate_energy(
step: int,
operator: BaseOperator | PauliSumOp,
betas: Sequence[float],
prev_states: list[np.ndarray] | None = None,
prev_states: list[QuantumCircuit] | None = None,
) -> Callable[[np.ndarray], float | list[float]]:
"""Returns a function handle to evaluate the ansatz's energy for any given parameters.
This is the objective function to be passed to the optimizer that is used for evaluation.
Args:
step: level of energy being calculated. 0 for ground, 1 for first excited state...
operator: The operator whose energy to evaluate.
prev_states: List of parameters from previous rounds of optimization.
prev_states: List of optimal circuits from previous rounds of optimization.
Returns:
A callable that computes and returns the energy of the hamiltonian
Expand All @@ -336,10 +347,6 @@ def _get_evaluate_energy(

self._check_operator_ansatz(operator)

prev_circs = []
for state in range(step - 1):
prev_circs.append(self.ansatz.bind_parameters(prev_states[state]))

def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float:

try:
Expand All @@ -355,10 +362,9 @@ def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float:
if step > 1:
# Compute overlap cost
fidelity_job = self.fidelity.run(
[self.ansatz] * len(prev_circs),
prev_circs,
[parameters] * len(prev_circs),
[prev_states[:-1]],
[self.ansatz] * (step - 1),
prev_states,
[parameters] * (step - 1),
)
costs = fidelity_job.result().fidelities

Expand Down
5 changes: 5 additions & 0 deletions releasenotes/notes/fix-vqd-kgt2-1ed95de3e32102c1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
Fixed the :class:`~.eigensolvers.VQD` if more than ``k=2`` eigenvalues are computed.
Previously the code failed due to an internal type mismatch, but now runs as expected.
22 changes: 15 additions & 7 deletions test/python/algorithms/eigensolvers/test_vqd.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def setUp(self):
algorithm_globals.random_seed = self.seed

self.h2_energy = -1.85727503
self.h2_energy_excited = [-1.85727503, -1.24458455]
self.h2_energy_excited = [-1.85727503, -1.24458455, -0.88272215, -0.22491125]

self.ryrz_wavefunction = TwoLocal(
rotation_blocks=["ry", "rz"], entanglement_blocks="cz", reps=1
Expand Down Expand Up @@ -88,7 +88,7 @@ def test_basic_operator(self, op):

with self.subTest(msg="test eigenvalue"):
np.testing.assert_array_almost_equal(
result.eigenvalues.real, self.h2_energy_excited, decimal=1
result.eigenvalues.real, self.h2_energy_excited[:2], decimal=1
)

with self.subTest(msg="test dimension of optimal point"):
Expand All @@ -108,6 +108,14 @@ def test_basic_operator(self, op):
)
np.testing.assert_array_almost_equal(job.result().values, result.eigenvalues, 6)

def test_full_spectrum(self):
"""Test obtaining all eigenvalues."""
vqd = VQD(self.estimator, self.fidelity, self.ryrz_wavefunction, optimizer=L_BFGS_B(), k=4)
result = vqd.compute_eigenvalues(H2_PAULI)
np.testing.assert_array_almost_equal(
result.eigenvalues.real, self.h2_energy_excited, decimal=2
)

@data(H2_PAULI, H2_OP)
def test_mismatching_num_qubits(self, op):
"""Ensuring circuit and operator mismatch is caught"""
Expand Down Expand Up @@ -198,7 +206,7 @@ def test_vqd_optimizer(self, op):
def run_check():
result = vqd.compute_eigenvalues(operator=op)
np.testing.assert_array_almost_equal(
result.eigenvalues.real, self.h2_energy_excited, decimal=3
result.eigenvalues.real, self.h2_energy_excited[:2], decimal=3
)

run_check()
Expand Down Expand Up @@ -226,7 +234,7 @@ def test_aux_operators_list(self, op):
# Start with an empty list
result = vqd.compute_eigenvalues(op, aux_operators=[])
np.testing.assert_array_almost_equal(
result.eigenvalues.real, self.h2_energy_excited, decimal=2
result.eigenvalues.real, self.h2_energy_excited[:2], decimal=2
)
self.assertIsNone(result.aux_operators_evaluated)

Expand All @@ -236,7 +244,7 @@ def test_aux_operators_list(self, op):
aux_ops = [aux_op1, aux_op2]
result = vqd.compute_eigenvalues(op, aux_operators=aux_ops)
np.testing.assert_array_almost_equal(
result.eigenvalues.real, self.h2_energy_excited, decimal=2
result.eigenvalues.real, self.h2_energy_excited[:2], decimal=2
)
self.assertEqual(len(result.aux_operators_evaluated), 2)
# expectation values
Expand All @@ -250,7 +258,7 @@ def test_aux_operators_list(self, op):
extra_ops = [*aux_ops, None, 0]
result = vqd.compute_eigenvalues(op, aux_operators=extra_ops)
np.testing.assert_array_almost_equal(
result.eigenvalues.real, self.h2_energy_excited, decimal=2
result.eigenvalues.real, self.h2_energy_excited[:2], decimal=2
)
self.assertEqual(len(result.aux_operators_evaluated), 2)
# expectation values
Expand Down Expand Up @@ -278,7 +286,7 @@ def test_aux_operators_dict(self, op):
# Start with an empty dictionary
result = vqd.compute_eigenvalues(op, aux_operators={})
np.testing.assert_array_almost_equal(
result.eigenvalues.real, self.h2_energy_excited, decimal=2
result.eigenvalues.real, self.h2_energy_excited[:2], decimal=2
)
self.assertIsNone(result.aux_operators_evaluated)

Expand Down

0 comments on commit 277472e

Please sign in to comment.