diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py index 6b48a30090ed..a3e4020338d0 100644 --- a/qiskit/algorithms/eigensolvers/vqd.py +++ b/qiskit/algorithms/eigensolvers/vqd.py @@ -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 @@ -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, @@ -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() @@ -304,7 +315,7 @@ 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. @@ -312,7 +323,7 @@ def _get_evaluate_energy( 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 @@ -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: @@ -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 diff --git a/releasenotes/notes/fix-vqd-kgt2-1ed95de3e32102c1.yaml b/releasenotes/notes/fix-vqd-kgt2-1ed95de3e32102c1.yaml new file mode 100644 index 000000000000..64303afc6735 --- /dev/null +++ b/releasenotes/notes/fix-vqd-kgt2-1ed95de3e32102c1.yaml @@ -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. diff --git a/test/python/algorithms/eigensolvers/test_vqd.py b/test/python/algorithms/eigensolvers/test_vqd.py index f51ecb92147a..b3b2b232de42 100644 --- a/test/python/algorithms/eigensolvers/test_vqd.py +++ b/test/python/algorithms/eigensolvers/test_vqd.py @@ -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 @@ -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"): @@ -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""" @@ -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() @@ -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) @@ -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 @@ -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 @@ -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)