Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix VQD for k>2 #8989

Merged
merged 3 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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