From fdf7f98765e33162e4938a0cf567301bfa8d90cd Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 08:48:49 -0400 Subject: [PATCH 01/30] Add SSVQE again to resolve conflicts --- qiskit/algorithms/__init__.py | 4 + qiskit/algorithms/eigensolvers/__init__.py | 5 + qiskit/algorithms/eigensolvers/ssvqe.py | 573 ++++++++++++++++++ .../algorithms/eigensolvers/test_ssvqe.py | 415 +++++++++++++ 4 files changed, 997 insertions(+) create mode 100644 qiskit/algorithms/eigensolvers/ssvqe.py create mode 100644 test/python/algorithms/eigensolvers/test_ssvqe.py diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 3f44364dc26d..71ec7861c698 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -95,6 +95,8 @@ NumPyEigensolver VQD VQDResult + SSVQE + SSVQEResult Eigensolvers ------------ @@ -399,6 +401,8 @@ "HamiltonianPhaseEstimationResult", "VQD", "VQDResult", + "SSVQE", + "SSVQEResult", "PhaseEstimationScale", "PhaseEstimation", "PhaseEstimationResult", diff --git a/qiskit/algorithms/eigensolvers/__init__.py b/qiskit/algorithms/eigensolvers/__init__.py index 2934d6c2a340..d0a8eb712d2d 100644 --- a/qiskit/algorithms/eigensolvers/__init__.py +++ b/qiskit/algorithms/eigensolvers/__init__.py @@ -26,6 +26,7 @@ Eigensolver NumPyEigensolver VQD + SSVQE Results ======= @@ -36,12 +37,14 @@ EigensolverResult NumPyEigensolverResult VQDResult + SSVQEResult """ from .numpy_eigensolver import NumPyEigensolver, NumPyEigensolverResult from .eigensolver import Eigensolver, EigensolverResult from .vqd import VQD, VQDResult +from .ssvqe import SSVQE, SSVQEResult __all__ = [ "NumPyEigensolver", @@ -50,4 +53,6 @@ "EigensolverResult", "VQD", "VQDResult", + "SSVQE", + "SSVQEResult" ] diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py new file mode 100644 index 000000000000..140327a45252 --- /dev/null +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -0,0 +1,573 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2022. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The Subspace Search Variational Quantum Eigensolver algorithm. +See https://arxiv.org/abs/1810.09434 +""" + +from __future__ import annotations + +import logging +import warnings +from time import time +from collections.abc import Callable, Sequence + +import numpy as np + +from qiskit.algorithms.gradients import BaseEstimatorGradient +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library import RealAmplitudes +from qiskit.opflow import PauliSumOp +from qiskit.primitives import BaseEstimator +from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils import algorithm_globals +from qiskit.quantum_info import Statevector + +from ..exceptions import AlgorithmError +from ..list_or_dict import ListOrDict +from ..optimizers import Optimizer, Minimizer, OptimizerResult +from ..variational_algorithm import VariationalAlgorithm, VariationalResult +from .eigensolver import Eigensolver, EigensolverResult + +from ..observables_evaluator import estimate_observables + +logger = logging.getLogger(__name__) + + +class SSVQE(VariationalAlgorithm, Eigensolver): + r"""The Subspace Search Variational Quantum Eigensolver algorithm. + `SSVQE ` is a quantum algorithm that uses a + variational technique to find + the low-lying eigenvalues of the Hamiltonian :math:`H` of a given system. + SSVQE can be seen as a natural generalization of VQE. Whereas VQE + minimizes the expectation value of :math:`H` with respect to one ansatz state, + SSVQE takes a set of mutually orthogonal input states, applies the same parameterized + ansatz to all of them, then minimizes a weighted sum + of the expectation values of :math:`H` with respect to these states. + An instance of SSVQE requires defining three algorithmic sub-components: + + An integer k denoting the number of eigenstates that the algorithm will attempt to find, + A trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, and one of the classical + :mod:`~qiskit.algorithms.optimizers`. The ansatz is varied, via its set of parameters, by the + optimizer, such that it works towards a set of mutually orthogonal states, as determined by the + parameters applied to the ansatz, that will result in the minimum weighted sum of expectation values + being measured of the input operator (Hamiltonian) with respect to these states. The weights + given to this list of expectation values is given by *weight_vector*. + An optional array of parameter values, via the *initial_point*, may be provided as the + starting point for the search of the low-lying eigenvalues. This feature is particularly useful + such as when there are reasons to believe that the solution point is close to a particular + point. The length of the *initial_point* list value must match the number of the parameters + expected by the ansatz being used. If the *initial_point* is left at the default + of ``None``, then SSVQE will look to the ansatz for a preferred value, based on its + given initial state. If the ansatz returns ``None``, + then a random point will be generated within the parameter bounds set, as per above. + If the ansatz provides ``None`` as the lower bound, then SSVQE + will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` + as the upper bound, the default value will be :math:`2\pi`. + + An optional list of initial states, via the *initial_states*, may also be provided. Choosing + these states appropriately is a critical part of the algorithm. They must be mutually orthogonal + because this is how the algorithm enforces the mutual orthogonality of the solution states. If + the *initial_states* is left as ``None``, then SSVQE will automatically generate a list of + computational basis states and use these as the initial states. For many physically-motivated + problems, it is advised to not rely on these default values as doing so can easily result in + an unphysical solution being returned. For example, if one wishes to find the low-lying + excited states of a molecular Hamiltonian, then we expect the output states to belong to + a particular particle-number subspace. If an ansatz that preserves particle number such as + :class:`UCCSD` is used, then states belonging to the incorrect particle number subspace + will be returned if the *initial_states* are not in the correct particle number subspace. + A similar statement can often be made for the spin-magnetization quantum number. + + The optimizer can either be one of Qiskit's optimizers, such as + :class:`~qiskit.algorithms.optimizers.SPSA` or a callable with the following signature: + .. note:: + The callable _must_ have the argument names ``fun, x0, jac, bounds`` as indicated + in the following code block. + .. code-block::python + from qiskit.algorithms.optimizers import OptimizerResult + def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: + # Note that the callable *must* have these argument names! + # Args: + # fun (callable): the function to minimize + # x0 (np.ndarray): the initial point for the optimization + # jac (callable, optional): the gradient of the objective function + # bounds (list, optional): a list of tuples specifying the parameter bounds + result = OptimizerResult() + result.x = # optimal parameters + result.fun = # optimal function value + return result + The above signature also allows to directly pass any SciPy minimizer, for instance as + .. code-block::python + from functools import partial + from scipy.optimize import minimize + optimizer = partial(minimize, method="L-BFGS-B") + """ + + def __init__( + self, + estimator: BaseEstimator, + k: int | None = 2, + ansatz: QuantumCircuit | None = None, + optimizer: Optimizer | Minimizer = None, + initial_point: Sequence[float] = None, + initial_states: Sequence[ + QuantumCircuit + ] = None, # Set of initial orthogonal states expressed as a list of QuantumCircuit objects. + weight_vector: Sequence[float] + | Sequence[int] = None, # set of weight factors to be used in the cost function + gradient: BaseEstimatorGradient | None = None, # don't forget to change this later. + callback: Callable[[int, np.ndarray, float, float], None] | None = None, + ) -> None: + """ + Args: + k: The number of eigenstates that the algorithm will attempt to find. + 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. + initial_point: An optional initial point (i.e. initial parameter values) + for the optimizer. If ``None`` then VQE will look to the ansatz for a preferred + point and if not will simply compute a random one. + initial_states: An optional list of mutually orthogonal initial states. + If ``None``, then SSVQE will set these to be a list of mutually orthogonal + computational basis states. + weight_vector: An optional list or array of real positive numbers with length + equal to the value of *num_states* to be used in the weighted energy summation + objective function. This fixes the ordering of the returned eigenstate/eigenvalue + pairs. If ``None``, then SSVQE will default to [n, n-1, ..., 1] for `k` = n. + gradient: An optional gradient function or operator for optimizer. + expectation: The Expectation converter for taking the average value of the + Observable over the ansatz state function. When ``None`` (the default) an + :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select + an appropriate expectation based on the operator and backend. When using Aer + qasm_simulator backend, with paulis, it is however much faster to leverage custom + Aer function for the computation but, although VQE performs much faster + with it, the outcome is ideal, with no shot noise, like using a state vector + simulator. If you are just looking for the quickest performance when choosing Aer + qasm_simulator and the lack of shot noise is not an issue then set `include_custom` + parameter here to ``True`` (defaults to ``False``). + include_custom: When `expectation` parameter here is None setting this to ``True`` will + allow the factory to include the custom Aer pauli expectation. + max_evals_grouped: Max number of evaluations performed simultaneously. Signals the + given optimizer that more than one set of parameters can be supplied so that + potentially the expectation values can be computed in parallel. Typically this is + possible when a finite difference gradient is used by the optimizer such that + multiple points to compute the gradient can be passed and if computed in parallel + improve overall execution time. Deprecated if a gradient operator or function is + given. + 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 for its current set of parameters as it works towards the minimum. + These are: the evaluation count, the optimizer parameters for the + ansatz, the evaluated mean and the evaluated standard deviation.` + quantum_instance: Quantum Instance or Backend + """ + + super().__init__() + + self.k = k + self.initial_states = initial_states + if weight_vector is not None: + self.weight_vector = weight_vector + else: + self.weight_vector = [self.k - n for n in range(self.k)] + self.ansatz = ansatz + self.optimizer = optimizer + self.initial_point = initial_point + self.gradient = gradient + self.callback = callback + self.estimator = estimator + + @property + def initial_point(self) -> Sequence[float] | None: + """Returns initial point""" + return self._initial_point + + @initial_point.setter + def initial_point(self, initial_point: Sequence[float] | None): + """Sets initial point""" + self._initial_point = initial_point + + @property + def callback(self) -> Callable[[int, np.ndarray, float, float], None] | None: + """Returns callback""" + return self._callback + + @callback.setter + def callback(self, callback: Callable[[int, np.ndarray, float, float], None] | None): + """Sets callback""" + self._callback = callback + + @classmethod + def supports_aux_operators(cls) -> bool: + return True + + def compute_eigenvalues( + self, + operator: BaseOperator | PauliSumOp, + aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, + ) -> EigensolverResult: + + ansatz = self._check_operator_ansatz(operator) + + initial_point = _validate_initial_point(self.initial_point, ansatz) + + initial_states = self._check_operator_initial_states(self.initial_states, operator) + + bounds = _validate_bounds(ansatz) + + initialized_ansatz_list = [initial_states[n].compose(ansatz) for n in range(self.k)] + + start_time = time() + + evaluate_weighted_energy_sum = self._get_evaluate_weighted_energy_sum( + initialized_ansatz_list, operator + ) + + if self.gradient is not None: # need to implement _get_evaluate_gradient + evaluate_gradient = self._get_evalute_gradient(initialized_ansatz_list, operator) + else: + evaluate_gradient = None + + if aux_operators: + zero_op = PauliSumOp.from_list([("I" * self.ansatz.num_qubits, 0)]) + + # Convert the None and zero values when aux_operators is a list. + # Drop None and convert zero values when aux_operators is a dict. + if isinstance(aux_operators, list): + key_op_iterator = enumerate(aux_operators) + converted = [zero_op] * len(aux_operators) + else: + key_op_iterator = aux_operators.items() + converted = {} + for key, op in key_op_iterator: + if op is not None: + converted[key] = zero_op if op == 0 else op + + aux_operators = converted + + else: + aux_operators = None + + start_time = time() + + if callable(self.optimizer): + optimizer_result = self.optimizer( + fun=evaluate_weighted_energy_sum, + x0=initial_point, + jac=evaluate_gradient, + bounds=bounds, + ) + else: + optimizer_result = self.optimizer.minimize( + fun=evaluate_weighted_energy_sum, + x0=initial_point, + jac=evaluate_gradient, + bounds=bounds, + ) + + optimizer_time = time() - start_time + + logger.info( + "Optimization complete in %s seconds.\nFound opt_params %s", + optimizer_time, + optimizer_result.x, + ) + + if aux_operators is not None: + bound_ansatz_list = [ + initialized_ansatz_list[n].bind_parameters(optimizer_result.x) + for n in range(self.k) + ] + + aux_values_list = [ + estimate_observables( + self.estimator, + bound_ansatz_list[n], + aux_operators, + ) + for n in range(self.k) + ] + else: + aux_values_list = None + + return self._build_ssvqe_result( + optimizer_result, aux_values_list, optimizer_time, operator, initialized_ansatz_list + ) + + def _get_evaluate_weighted_energy_sum( + self, + initialized_ansatz_list: list[QuantumCircuit], + operator: BaseOperator | PauliSumOp, + ) -> tuple[Callable[[np.ndarray], float | list[float]], dict]: + """Returns a function handle to evaluates the weighted energy sum at given parameters + for the ansatz. This is the objective function to be passed to the optimizer + that is used for evaluation. + Args: + operator: The operator whose energy levels to evaluate. + return_expectation: If True, return the ``ExpectationBase`` expectation converter used + in the construction of the expectation value. Useful e.g. to evaluate other + operators with the same expectation value converter. + Returns: + Weighted energy sum of the hamiltonian of each parameter, and, optionally, the expectation + converter. + Raises: + RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters). + """ + num_parameters = initialized_ansatz_list[0].num_parameters + + eval_count = 0 + + def evaluate_weighted_energy_sum(parameters): + nonlocal eval_count + parameters = np.reshape(parameters, (-1, num_parameters)).tolist() + batchsize = len(parameters) + + try: + job = self.estimator.run( + [initialized_ansatz_list[m] for n in range(batchsize) for m in range(self.k)], + [operator] * self.k * batchsize, + [parameters[n] for n in range(batchsize) for m in range(self.k)], + ) + result = job.result() + values = result.values + + energies = np.reshape(values, (batchsize, self.k)) + weighted_energy_sums = np.dot(energies, self.weight_vector).tolist() + energies = energies.tolist() + + except Exception as exc: + raise AlgorithmError("The primitive job to evaluate the energy failed!") from exc + + if self.callback is not None: + metadata = result.metadata + for params, energies_value, metadata in zip(parameters, energies, metadata): + eval_count += 1 + self.callback(eval_count, params, energies_value, metadata) + + return ( + weighted_energy_sums[0] if len(weighted_energy_sums) == 1 else weighted_energy_sums + ) + + return evaluate_weighted_energy_sum + + def _get_evalute_gradient( # check this implementation + self, + initialized_ansatz_list: list[QuantumCircuit], + operator: BaseOperator | PauliSumOp, + ) -> tuple[Callable[[np.ndarray], np.ndarray]]: + """Get a function handle to evaluate the gradient at given parameters for the ansatz. + Args: + initialized_ansatz_list: The list of initialized ansatz preparing the quantum states. + operator: The operator whose energy to evaluate. + Returns: + A function handle to evaluate the gradient at given parameters for the initialized + ansatz list. + Raises: + AlgorithmError: If the primitive job to evaluate the gradient fails. + """ + + def evaluate_gradient(parameters): + # broadcasting not required for the estimator gradients + try: + job = self.gradient.run( + initialized_ansatz_list, + [operator] * self.k, + [parameters for n in range(self.k)], + ) + energy_gradients = job.result().gradients + weighted_energy_sum_gradient = sum( + [self.weight_vector[n] * energy_gradients[n] for n in range(self.k)] + ) + except Exception as exc: + raise AlgorithmError("The primitive job to evaluate the gradient failed!") from exc + + return weighted_energy_sum_gradient + + return evaluate_gradient + + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: + """Check that the number of qubits of operator and ansatz match and that the ansatz is + parameterized. + """ + # set defaults + if self.ansatz is None: + ansatz = RealAmplitudes(num_qubits=operator.num_qubits, reps=6) + else: + ansatz = self.ansatz + + if operator.num_qubits != ansatz.num_qubits: + try: + logger.info( + "Trying to resize ansatz to match operator on %s qubits.", operator.num_qubits + ) + ansatz.num_qubits = operator.num_qubits + except AttributeError as error: + raise AlgorithmError( + "The number of qubits of the ansatz does not match the " + "operator, and the ansatz does not allow setting the " + "number of qubits using `num_qubits`." + ) from error + + if ansatz.num_parameters == 0: + raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") + + return ansatz + + def _check_operator_initial_states( + self, list_of_states: Sequence[QuantumCircuit] | None, operator: BaseOperator | PauliSumOp + ) -> QuantumCircuit: + + """Check that the number of qubits of operator and all the initial states match.""" + + if list_of_states is None: + initial_states = [QuantumCircuit(operator.num_qubits) for n in range(self.k)] + for n in range(self.k): + initial_states[n].initialize(Statevector.from_int(n, 2**operator.num_qubits)) + + warnings.warn( + "No initial states have been provided to SSVQE, so they have been set to " + "a subset of the computational basis states.This may result in unphysical " + "results for some problems." + ) + + else: + initial_states = list_of_states + + for initial_state in initial_states: + if operator.num_qubits != initial_state.num_qubits: + try: + logger.info( + "Trying to resize initial state to match operator on %s qubits.", + operator.num_qubits, + ) + initial_state.num_qubits = operator.num_qubits + except AttributeError as error: + raise AlgorithmError( + "The number of qubits of the initial state does not match the " + "operator, and the initial state does not allow setting the " + "number of qubits using `num_qubits`." + ) from error + + return initial_states + + def _eval_aux_ops( + self, + ansatz: QuantumCircuit, + aux_operators: ListOrDict[BaseOperator | PauliSumOp], + ) -> ListOrDict[tuple(complex, complex)]: + """Compute auxiliary operator eigenvalues.""" + + if isinstance(aux_operators, dict): + aux_ops = list(aux_operators.values()) + else: + aux_ops = aux_operators + + num_aux_ops = len(aux_ops) + aux_job = self.estimator.run([ansatz] * num_aux_ops, aux_ops) + aux_values = aux_job.result().values + aux_values = list(zip(aux_values, [0] * len(aux_values))) + + if isinstance(aux_operators, dict): + aux_values = dict(zip(aux_operators.keys(), aux_values)) + + return aux_values + + def _build_ssvqe_result( + self, + optimizer_result: OptimizerResult, + aux_operators_evaluated: ListOrDict[tuple[complex, tuple[complex, int]]], + optimizer_time: float, + operator: BaseOperator | PauliSumOp, + initialized_ansatz_list: Sequence[QuantumCircuit], + ) -> SSVQEResult: + result = SSVQEResult() + result.eigenvalues = ( + self.estimator.run( + initialized_ansatz_list, [operator] * self.k, [optimizer_result.x] * self.k + ) + .result() + .values + ) + result.cost_function_evals = optimizer_result.nfev + result.optimal_point = optimizer_result.x + result.optimal_parameters = dict(zip(self.ansatz.parameters, optimizer_result.x)) + result.optimal_value = optimizer_result.fun + result.optimizer_time = optimizer_time + result.aux_operators_evaluated = aux_operators_evaluated + result.optimizer_result = optimizer_result + + return result + + +class SSVQEResult(VariationalResult, EigensolverResult): + """SSVQE Result.""" + + def __init__(self) -> None: + super().__init__() + self._cost_function_evals = None + + @property + def cost_function_evals(self) -> int: + """Returns number of cost optimizer evaluations""" + return self._cost_function_evals + + @cost_function_evals.setter + def cost_function_evals(self, value: int) -> None: + """Sets number of cost function evaluations""" + self._cost_function_evals = value + + +def _validate_initial_point(point, ansatz): + expected_size = ansatz.num_parameters + + # try getting the initial point from the ansatz + if point is None and hasattr(ansatz, "preferred_init_points"): + point = ansatz.preferred_init_points + # if the point is None choose a random initial point + + if point is None: + # get bounds if ansatz has them set, otherwise use [-2pi, 2pi] for each parameter + bounds = getattr(ansatz, "parameter_bounds", None) + if bounds is None: + bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size + + # replace all Nones by [-2pi, 2pi] + lower_bounds = [] + upper_bounds = [] + for lower, upper in bounds: + lower_bounds.append(lower if lower is not None else -2 * np.pi) + upper_bounds.append(upper if upper is not None else 2 * np.pi) + + # sample from within bounds + point = algorithm_globals.random.uniform(lower_bounds, upper_bounds) + + elif len(point) != expected_size: + raise ValueError( + f"The dimension of the initial point ({len(point)}) does not match the " + f"number of parameters in the circuit ({expected_size})." + ) + + return point + + +def _validate_bounds(ansatz): + if hasattr(ansatz, "parameter_bounds") and ansatz.parameter_bounds is not None: + bounds = ansatz.parameter_bounds + if len(bounds) != ansatz.num_parameters: + raise ValueError( + f"The number of bounds ({len(bounds)}) does not match the number of " + f"parameters in the circuit ({ansatz.num_parameters})." + ) + else: + bounds = [(None, None)] * ansatz.num_parameters + + return bounds diff --git a/test/python/algorithms/eigensolvers/test_ssvqe.py b/test/python/algorithms/eigensolvers/test_ssvqe.py new file mode 100644 index 000000000000..3d057fc037ff --- /dev/null +++ b/test/python/algorithms/eigensolvers/test_ssvqe.py @@ -0,0 +1,415 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022 +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Test SSVQE """ + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase + +from functools import partial +import numpy as np +from ddt import data, ddt + +from qiskit import QuantumCircuit +from qiskit.algorithms.eigensolvers import SSVQE +from qiskit.algorithms import AlgorithmError +from qiskit.algorithms.optimizers import ( + COBYLA, + L_BFGS_B, + SLSQP, + CG, + TNC, + P_BFGS, + GradientDescent, + OptimizerResult, +) + +from qiskit.algorithms.gradients import ParamShiftEstimatorGradient +from qiskit.circuit.library import TwoLocal, RealAmplitudes +from qiskit.opflow import PauliSumOp +from qiskit.primitives import Sampler, Estimator +from qiskit.algorithms.state_fidelities import ComputeUncompute +from qiskit.utils import algorithm_globals +from qiskit.quantum_info.operators import Operator + + +def _mock_optimizer(fun, point, jac=None, bounds=None, inputs=None) -> OptimizerResult: + """A mock of a callable that can be used as minimizer in SSVQE.""" + result = OptimizerResult() + result.x = np.zeros_like(point) + result.fun = fun(result.x) + result.nit = 0 + + if inputs is not None: + inputs.update({"fun": fun, "x0": point, "jac": jac, "bounds": bounds}) + return result + + +I = PauliSumOp.from_list([("I", 1)]) # pylint: disable=invalid-name +X = PauliSumOp.from_list([("X", 1)]) # pylint: disable=invalid-name +Z = PauliSumOp.from_list([("Z", 1)]) # pylint: disable=invalid-name + +H2_PAULI = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) +) + +H2_OP = Operator(H2_PAULI.to_matrix()) + + +@ddt +class TestSSVQE(QiskitAlgorithmsTestCase): + """Test SSVQE""" + + def setUp(self): + super().setUp() + self.seed = 50 + algorithm_globals.random_seed = self.seed + + self.h2_energy = -1.85727503 + self.h2_energy_excited = [-1.85727503, -1.24458455] + + self.ryrz_wavefunction = TwoLocal( + rotation_blocks=["ry", "rz"], entanglement_blocks="cz", reps=1 + ) + self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") + + self.estimator = Estimator() + self.estimator_shots = Estimator(options={"shots": 2048, "seed": self.seed}) + self.fidelity = ComputeUncompute(Sampler()) + self.betas = [50, 50] + + @data(H2_PAULI, H2_OP) + def test_basic_operator(self, op): + """Test SSVQE without aux_operators.""" + wavefunction = self.ryrz_wavefunction + ssvqe = SSVQE( + estimator=self.estimator, + ansatz=wavefunction, + optimizer=COBYLA(), + ) + + result = ssvqe.compute_eigenvalues(operator=op) + + with self.subTest(msg="test eigenvalue"): + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=1 + ) + + with self.subTest(msg="test dimension of optimal point"): + self.assertEqual(len(result.optimal_point), 8) + + with self.subTest(msg="assert cost_function_evals is set"): + self.assertIsNotNone(result.cost_function_evals) + + with self.subTest(msg="assert optimizer_time is set"): + self.assertIsNotNone(result.optimizer_time) + + @data(H2_PAULI, H2_OP) + def test_mismatching_num_qubits(self, op): + """Ensuring circuit and operator mismatch is caught""" + wavefunction = QuantumCircuit(1) + optimizer = SLSQP(maxiter=50) + ssvqe = SSVQE(estimator=self.estimator, k=1, ansatz=wavefunction, optimizer=optimizer) + with self.assertRaises(AlgorithmError): + _ = ssvqe.compute_eigenvalues(operator=op) + + @data(H2_PAULI, H2_OP) + def test_missing_varform_params(self, op): + """Test specifying a variational form with no parameters raises an error.""" + circuit = QuantumCircuit(op.num_qubits) + ssvqe = SSVQE(estimator=self.estimator, ansatz=circuit, optimizer=SLSQP(), k=1) + with self.assertRaises(AlgorithmError): + ssvqe.compute_eigenvalues(operator=op) + + @data(H2_PAULI, H2_OP) + def test_callback(self, op): + """Test the callback on SSVQE.""" + history = {"eval_count": [], "parameters": [], "mean_energies": [], "metadata": []} + + def store_intermediate_result(eval_count, parameters, mean, metadata): + history["eval_count"].append(eval_count) + history["parameters"].append(parameters) + history["mean_energies"].append(mean) + history["metadata"].append(metadata) + + optimizer = COBYLA(maxiter=3) + wavefunction = self.ry_wavefunction + + ssvqe = SSVQE( + estimator=self.estimator, + ansatz=wavefunction, + optimizer=optimizer, + callback=store_intermediate_result, + ) + + ssvqe.compute_eigenvalues(operator=op) + + self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) + # self.assertTrue(all(isinstance(mean, float) for mean in history["mean_energies"])) + self.assertTrue( + all( + isinstance(mean, float) + for mean_list in history["mean_energies"] + for mean in mean_list + ) + ) + self.assertTrue(all(isinstance(metadata, dict) for metadata in history["metadata"])) + for params in history["parameters"]: + self.assertTrue(all(isinstance(param, float) for param in params)) + + ref_eval_count = [1, 2, 3] + ref_mean = [[-1.07, -1.44], [-1.45, -1.06], [-1.37, -0.94]] + + np.testing.assert_array_almost_equal(history["eval_count"], ref_eval_count, decimal=0) + np.testing.assert_array_almost_equal(history["mean_energies"], ref_mean, decimal=2) + + @data(H2_PAULI, H2_OP) + def test_ssvqe_optimizer(self, op): + """Test running same SSVQE twice to re-use optimizer, then switch optimizer""" + ssvqe = SSVQE( + estimator=self.estimator, ansatz=RealAmplitudes(reps=6), optimizer=SLSQP(), k=2 + ) + + def run_check(): + result = ssvqe.compute_eigenvalues(operator=op) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=3 + ) + + run_check() + + with self.subTest("Optimizer re-use"): + run_check() + + with self.subTest("Optimizer replace"): + ssvqe.optimizer = L_BFGS_B() + run_check() + + @data(H2_PAULI, H2_OP) + def test_aux_operators_list(self, op): + """Test list-based aux_operators.""" + wavefunction = self.ry_wavefunction + ssvqe = SSVQE(estimator=self.estimator, ansatz=wavefunction, optimizer=SLSQP(), k=2) + + # Start with an empty list + result = ssvqe.compute_eigenvalues(op, aux_operators=[]) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=2 + ) + self.assertIsNone(result.aux_operators_evaluated) + + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] + result = ssvqe.compute_eigenvalues(op, aux_operators=aux_ops) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=2 + ) + self.assertEqual(len(result.aux_operators_evaluated), 2) + # expectation values + self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2, places=2) + self.assertAlmostEqual(result.aux_operators_evaluated[0][1][0], 0, places=2) + # metadata + self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) + + # Go again with additional None and zero operators + extra_ops = [*aux_ops, None, 0] + result = ssvqe.compute_eigenvalues(op, aux_operators=extra_ops) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=2 + ) + self.assertEqual(len(result.aux_operators_evaluated), 2) + # expectation values + self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2, places=2) + self.assertAlmostEqual(result.aux_operators_evaluated[0][1][0], 0, places=2) + self.assertEqual(result.aux_operators_evaluated[0][2][0], 0.0) + self.assertEqual(result.aux_operators_evaluated[0][3][0], 0.0) + # metadata + self.assertIsInstance(result.aux_operators_evaluated[0][0][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][3][1], dict) + + @data(H2_PAULI, H2_OP) + def test_aux_operators_dict(self, op): + """Test dictionary compatibility of aux_operators""" + wavefunction = self.ry_wavefunction + ssvqe = SSVQE(estimator=self.estimator, ansatz=wavefunction, optimizer=SLSQP()) + + # Start with an empty dictionary + result = ssvqe.compute_eigenvalues(op, aux_operators={}) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=2 + ) + self.assertIsNone(result.aux_operators_evaluated) + + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + result = ssvqe.compute_eigenvalues(op, aux_operators=aux_ops) + self.assertEqual(len(result.eigenvalues), 2) + self.assertEqual(result.eigenvalues.dtype, np.float64) + self.assertAlmostEqual(result.eigenvalues[0], -1.85727503, 2) + self.assertEqual(len(result.aux_operators_evaluated), 2) + self.assertEqual(len(result.aux_operators_evaluated[0]), 2) + # expectation values + self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op2"][0], 0, places=1) + # metadata + self.assertIsInstance(result.aux_operators_evaluated[0]["aux_op1"][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0]["aux_op2"][1], dict) + + # Go again with additional None and zero operators + extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} + result = ssvqe.compute_eigenvalues(op, aux_operators=extra_ops) + self.assertEqual(len(result.eigenvalues), 2) + self.assertEqual(result.eigenvalues.dtype, np.float64) + self.assertAlmostEqual(result.eigenvalues[0], -1.85727503, places=5) + self.assertEqual(len(result.aux_operators_evaluated), 2) + self.assertEqual(len(result.aux_operators_evaluated[0]), 3) + # expectation values + self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op2"][0], 0.0, places=2) + self.assertEqual(result.aux_operators_evaluated[0]["zero_operator"][0], 0.0) + self.assertTrue("None_operator" not in result.aux_operators_evaluated[0].keys()) + # metadata + self.assertIsInstance(result.aux_operators_evaluated[0]["aux_op1"][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0]["aux_op2"][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0]["zero_operator"][1], dict) + + @data(H2_PAULI, H2_OP) + def test_aux_operator_std_dev(self, op): + """Test non-zero standard deviations of aux operators.""" + wavefunction = self.ry_wavefunction + ssvqe = SSVQE( + estimator=self.estimator, + ansatz=wavefunction, + initial_point=[ + 1.70256666, + -5.34843975, + -0.39542903, + 5.99477786, + -2.74374986, + -4.85284669, + 0.2442925, + -1.51638917, + ], + optimizer=COBYLA(maxiter=0), + ) + + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] + result = ssvqe.compute_eigenvalues(op, aux_operators=aux_ops) + self.assertEqual(len(result.aux_operators_evaluated), 2) + # expectation values + self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2.0, places=1) + self.assertAlmostEqual( + result.aux_operators_evaluated[0][1][0], 0.0019531249999999445, places=1 + ) + # metadata + self.assertIsInstance(result.aux_operators_evaluated[0][0][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) + + # Go again with additional None and zero operators + aux_ops = [*aux_ops, None, 0] + result = ssvqe.compute_eigenvalues(op, aux_operators=aux_ops) + self.assertEqual(len(result.aux_operators_evaluated[0]), 4) + # expectation values + self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2.0, places=1) + self.assertAlmostEqual( + result.aux_operators_evaluated[0][1][0], 0.0019531249999999445, places=1 + ) + self.assertEqual(result.aux_operators_evaluated[0][2][0], 0.0) + self.assertEqual(result.aux_operators_evaluated[0][3][0], 0.0) + # metadata + self.assertIsInstance(result.aux_operators_evaluated[0][0][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][2][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][3][1], dict) + + @data( + CG(), + L_BFGS_B(), + P_BFGS(), + SLSQP(), + TNC(), + ) + def test_with_gradient(self, optimizer): + """Test SSVQE using gradient primitive.""" + estimator = Estimator() + ssvqe = SSVQE( + k=2, + estimator=estimator, + ansatz=self.ry_wavefunction, + optimizer=optimizer, + gradient=ParamShiftEstimatorGradient(estimator), + ) + result = ssvqe.compute_eigenvalues(operator=H2_PAULI) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=5 + ) + + def test_gradient_passed(self): + """Test the gradient is properly passed into the optimizer.""" + inputs = {} + estimator = Estimator() + ssvqe = SSVQE( + k=2, + estimator=estimator, + ansatz=RealAmplitudes(), + optimizer=partial(_mock_optimizer, inputs=inputs), + gradient=ParamShiftEstimatorGradient(estimator), + ) + _ = ssvqe.compute_eigenvalues(operator=H2_PAULI) + + self.assertIsNotNone(inputs["jac"]) + + def test_gradient_run(self): + """Test using the gradient to calculate the minimum.""" + estimator = Estimator() + ssvqe = SSVQE( + k=2, + estimator=estimator, + ansatz=RealAmplitudes(), + optimizer=GradientDescent(maxiter=200, learning_rate=0.1), + gradient=ParamShiftEstimatorGradient(estimator), + ) + result = ssvqe.compute_eigenvalues(operator=H2_PAULI) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=5 + ) + + def test_max_evals_grouped(self): + """Test with SLSQP with max_evals_grouped.""" + optimizer = SLSQP(maxiter=50, max_evals_grouped=5) + ssvqe = SSVQE( + k=2, + estimator=Estimator(), + ansatz=RealAmplitudes(reps=6), + optimizer=optimizer, + ) + result = ssvqe.compute_eigenvalues(operator=H2_PAULI) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited, decimal=5 + ) + + +if __name__ == "__main__": + unittest.main() From 26e3dfe9ce4062f7de1e290c7b32b72b8ecfbb66 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 10:44:49 -0400 Subject: [PATCH 02/30] fix tests and linting --- qiskit/algorithms/eigensolvers/__init__.py | 2 +- test/python/algorithms/eigensolvers/test_ssvqe.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/__init__.py b/qiskit/algorithms/eigensolvers/__init__.py index d0a8eb712d2d..9b247bd77b6b 100644 --- a/qiskit/algorithms/eigensolvers/__init__.py +++ b/qiskit/algorithms/eigensolvers/__init__.py @@ -54,5 +54,5 @@ "VQD", "VQDResult", "SSVQE", - "SSVQEResult" + "SSVQEResult", ] diff --git a/test/python/algorithms/eigensolvers/test_ssvqe.py b/test/python/algorithms/eigensolvers/test_ssvqe.py index 3d057fc037ff..99e936a8bce8 100644 --- a/test/python/algorithms/eigensolvers/test_ssvqe.py +++ b/test/python/algorithms/eigensolvers/test_ssvqe.py @@ -42,15 +42,15 @@ from qiskit.quantum_info.operators import Operator -def _mock_optimizer(fun, point, jac=None, bounds=None, inputs=None) -> OptimizerResult: +def _mock_optimizer(fun, x0, jac=None, bounds=None, inputs=None) -> OptimizerResult: """A mock of a callable that can be used as minimizer in SSVQE.""" result = OptimizerResult() - result.x = np.zeros_like(point) + result.x = np.zeros_like(x0) result.fun = fun(result.x) result.nit = 0 if inputs is not None: - inputs.update({"fun": fun, "x0": point, "jac": jac, "bounds": bounds}) + inputs.update({"fun": fun, "x0": x0, "jac": jac, "bounds": bounds}) return result From a6782c78532cb5d9d7ee5e827a53e964e9bcbab8 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 13:36:31 -0400 Subject: [PATCH 03/30] attempt to fix docs --- qiskit/algorithms/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 71ec7861c698..3f44364dc26d 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -95,8 +95,6 @@ NumPyEigensolver VQD VQDResult - SSVQE - SSVQEResult Eigensolvers ------------ @@ -401,8 +399,6 @@ "HamiltonianPhaseEstimationResult", "VQD", "VQDResult", - "SSVQE", - "SSVQEResult", "PhaseEstimationScale", "PhaseEstimation", "PhaseEstimationResult", From 9aec61a2363e39efdce8b16e441542b4e127db80 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 13:46:15 -0400 Subject: [PATCH 04/30] fix test_ssvqe linting --- test/python/algorithms/eigensolvers/test_ssvqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/algorithms/eigensolvers/test_ssvqe.py b/test/python/algorithms/eigensolvers/test_ssvqe.py index 99e936a8bce8..568ea3734cb9 100644 --- a/test/python/algorithms/eigensolvers/test_ssvqe.py +++ b/test/python/algorithms/eigensolvers/test_ssvqe.py @@ -41,7 +41,7 @@ from qiskit.utils import algorithm_globals from qiskit.quantum_info.operators import Operator - +# pylint: disable=invalid-name def _mock_optimizer(fun, x0, jac=None, bounds=None, inputs=None) -> OptimizerResult: """A mock of a callable that can be used as minimizer in SSVQE.""" result = OptimizerResult() From 804150c3e015f6e0533f17c225505316eb159eae Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 15:30:42 -0400 Subject: [PATCH 05/30] add release note --- qiskit/algorithms/eigensolvers/__init__.py | 2 +- .../add-ssvqe-algorithm-6c395267cdd03798.yaml | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml diff --git a/qiskit/algorithms/eigensolvers/__init__.py b/qiskit/algorithms/eigensolvers/__init__.py index 9b247bd77b6b..d0a8eb712d2d 100644 --- a/qiskit/algorithms/eigensolvers/__init__.py +++ b/qiskit/algorithms/eigensolvers/__init__.py @@ -54,5 +54,5 @@ "VQD", "VQDResult", "SSVQE", - "SSVQEResult", + "SSVQEResult" ] diff --git a/releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml b/releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml new file mode 100644 index 000000000000..3c25e3892f90 --- /dev/null +++ b/releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml @@ -0,0 +1,38 @@ +--- +features: + - | + Adds the Subspace Search Variational Quantum Eigensolver (:class:`SSVQE`) + algorithm. SSVQE is a generalization of the Variational Quantum Eigensolver + (VQE) which aims to find the low-lying eigenvalue/eigenvector pairs of a + Hermitian operator :math:`H`. It takes a set of mutually orthogonal input + states, applies a parameterized ansatz (:class:`QuantumCircuit`) to all of + them, then minimizes a weighted sum of the expectation values of :math:`H` + with respect to these parameterized states using a classical optimizer + (:class:`qiskit.algorithms.optimizers.Optimizer`). + For example: + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.opflow import Z + from qiskit.primitives import Estimator + from qiskit.circuit.library import RealAmplitudes + from qiskit.algorithms.optimizers import SPSA + from qiskit.algorithms.eigensolvers import SSVQE + + operator = Z^Z + input_states = [QuantumCircuit(2), QuantumCircuit(2)] + input_states[0].x(0) + input_states[1].x(1) + + ssvqe_instance = SSVQE(k=2, + estimator=Estimator(), + optimizer=SPSA(), + ansatz=RealAmplitudes(2), + initial_states=input_states) + + result = ssvqe_instance.compute_eigenvalues(operator) + + + + From 7e15520f12e8fdaecbde65d1ee40489b32bef325 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 19:45:47 -0400 Subject: [PATCH 06/30] add checks and tests for initial states/weights --- qiskit/algorithms/eigensolvers/ssvqe.py | 57 ++++++++++--------- .../algorithms/eigensolvers/test_ssvqe.py | 29 ++++++++++ 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 140327a45252..af0c027d6795 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -121,11 +121,12 @@ def __init__( initial_point: Sequence[float] = None, initial_states: Sequence[ QuantumCircuit - ] = None, # Set of initial orthogonal states expressed as a list of QuantumCircuit objects. + ] = None, weight_vector: Sequence[float] - | Sequence[int] = None, # set of weight factors to be used in the cost function - gradient: BaseEstimatorGradient | None = None, # don't forget to change this later. + | Sequence[int] = None, + gradient: BaseEstimatorGradient | None = None, callback: Callable[[int, np.ndarray, float, float], None] | None = None, + check_input_states_orthogonality: bool = True ) -> None: """ Args: @@ -144,47 +145,31 @@ def __init__( objective function. This fixes the ordering of the returned eigenstate/eigenvalue pairs. If ``None``, then SSVQE will default to [n, n-1, ..., 1] for `k` = n. gradient: An optional gradient function or operator for optimizer. - expectation: The Expectation converter for taking the average value of the - Observable over the ansatz state function. When ``None`` (the default) an - :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select - an appropriate expectation based on the operator and backend. When using Aer - qasm_simulator backend, with paulis, it is however much faster to leverage custom - Aer function for the computation but, although VQE performs much faster - with it, the outcome is ideal, with no shot noise, like using a state vector - simulator. If you are just looking for the quickest performance when choosing Aer - qasm_simulator and the lack of shot noise is not an issue then set `include_custom` - parameter here to ``True`` (defaults to ``False``). - include_custom: When `expectation` parameter here is None setting this to ``True`` will - allow the factory to include the custom Aer pauli expectation. - max_evals_grouped: Max number of evaluations performed simultaneously. Signals the - given optimizer that more than one set of parameters can be supplied so that - potentially the expectation values can be computed in parallel. Typically this is - possible when a finite difference gradient is used by the optimizer such that - multiple points to compute the gradient can be passed and if computed in parallel - improve overall execution time. Deprecated if a gradient operator or function is - given. 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 for its current set of parameters as it works towards the minimum. These are: the evaluation count, the optimizer parameters for the ansatz, the evaluated mean and the evaluated standard deviation.` - quantum_instance: Quantum Instance or Backend + check_input_states_orthogonality: A boolean that sets whether or not to check + that the value of initial_states passed consists of a mutually orthogonal + set of states. If ``True``, then SSVQE will check that these states are mutually + orthogonal and return an error if they are not. This is set to ``True`` by default, + but setting this to ``False`` may be desirable for larger numbers of qubits to avoid + exponentially large computational overhead before the simulation even starts. """ super().__init__() self.k = k self.initial_states = initial_states - if weight_vector is not None: - self.weight_vector = weight_vector - else: - self.weight_vector = [self.k - n for n in range(self.k)] + self.weight_vector = weight_vector self.ansatz = ansatz self.optimizer = optimizer self.initial_point = initial_point self.gradient = gradient self.callback = callback self.estimator = estimator + self.check_initial_states_orthogonal = check_input_states_orthogonality @property def initial_point(self) -> Sequence[float] | None: @@ -226,6 +211,8 @@ def compute_eigenvalues( initialized_ansatz_list = [initial_states[n].compose(ansatz) for n in range(self.k)] + self.weight_vector = self._check_weight_vector(self.weight_vector) + start_time = time() evaluate_weighted_energy_sum = self._get_evaluate_weighted_energy_sum( @@ -328,6 +315,7 @@ def _get_evaluate_weighted_energy_sum( def evaluate_weighted_energy_sum(parameters): nonlocal eval_count + #nonlocal weights parameters = np.reshape(parameters, (-1, num_parameters)).tolist() batchsize = len(parameters) @@ -441,6 +429,12 @@ def _check_operator_initial_states( else: initial_states = list_of_states + if self.check_initial_states_orthogonal is True: + stacked_states_array = np.hstack([np.asarray(Statevector(state)) for state in initial_states]) + if not np.isclose(stacked_states_array.transpose() @ stacked_states_array, np.eye(self.k)): + raise AlgorithmError( + "The set of initial states provided is not mutually orthogonal." + ) for initial_state in initial_states: if operator.num_qubits != initial_state.num_qubits: @@ -459,6 +453,15 @@ def _check_operator_initial_states( return initial_states + def _check_weight_vector(self, weight_vector: Sequence[float]) -> Sequence[float]: + if weight_vector is None: + weight_vector = [self.k - n for n in range(self.k)] + elif len(weight_vector) != self.k: + raise AlgorithmError( + "The number of weights provided does not match the number of states.") + + return weight_vector + def _eval_aux_ops( self, ansatz: QuantumCircuit, diff --git a/test/python/algorithms/eigensolvers/test_ssvqe.py b/test/python/algorithms/eigensolvers/test_ssvqe.py index 568ea3734cb9..bf76f5b10ac5 100644 --- a/test/python/algorithms/eigensolvers/test_ssvqe.py +++ b/test/python/algorithms/eigensolvers/test_ssvqe.py @@ -126,6 +126,35 @@ def test_mismatching_num_qubits(self, op): with self.assertRaises(AlgorithmError): _ = ssvqe.compute_eigenvalues(operator=op) + @data(H2_PAULI, H2_OP) + def test_initial_states_mismatching_num_qubits(self, op): + """Ensuring initial states and operator mismatch is caught""" + initial_state = [QuantumCircuit(1)] + wavefunction = QuantumCircuit(2) + optimizer = SLSQP(maxiter=50) + ssvqe = SSVQE(estimator=self.estimator, k=1, ansatz=wavefunction, optimizer=optimizer, initial_states=initial_state) + with self.assertRaises(AlgorithmError): + _ = ssvqe.compute_eigenvalues(operator=op) + + @data(H2_PAULI, H2_OP) + def test_mismatching_weights(self, op): + """Ensuring incorrect number of weights is caught""" + optimizer = SLSQP(maxiter=50) + ssvqe = SSVQE(estimator=self.estimator, k=2, weight_vector=[3,2,1], optimizer=optimizer) + with self.assertRaises(AlgorithmError): + _ = ssvqe.compute_eigenvalues(operator=op) + + @data(H2_PAULI, H2_OP) + def test_initial_states_nonorthogonal(self, op): + """Ensuring non-orthogonality of input initial states is caught""" + initial_states = [QuantumCircuit(2), QuantumCircuit(2)] + initial_states[0].x(0) + initial_states[1].x(0) + optimizer = SLSQP(maxiter=50) + ssvqe = SSVQE(estimator=self.estimator, k=1, optimizer=optimizer, initial_states=initial_states) + with self.assertRaises(AlgorithmError): + _ = ssvqe.compute_eigenvalues(operator=op) + @data(H2_PAULI, H2_OP) def test_missing_varform_params(self, op): """Test specifying a variational form with no parameters raises an error.""" From 4e0bdd8e0988d707d1efad3eb1511e0a70c4d609 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 19:56:36 -0400 Subject: [PATCH 07/30] add checks and tests for initial states/weights --- qiskit/algorithms/eigensolvers/__init__.py | 2 +- qiskit/algorithms/eigensolvers/ssvqe.py | 22 ++++++++++--------- .../algorithms/eigensolvers/test_ssvqe.py | 14 +++++++++--- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/__init__.py b/qiskit/algorithms/eigensolvers/__init__.py index d0a8eb712d2d..9b247bd77b6b 100644 --- a/qiskit/algorithms/eigensolvers/__init__.py +++ b/qiskit/algorithms/eigensolvers/__init__.py @@ -54,5 +54,5 @@ "VQD", "VQDResult", "SSVQE", - "SSVQEResult" + "SSVQEResult", ] diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index af0c027d6795..769951c6cd65 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -119,14 +119,11 @@ def __init__( ansatz: QuantumCircuit | None = None, optimizer: Optimizer | Minimizer = None, initial_point: Sequence[float] = None, - initial_states: Sequence[ - QuantumCircuit - ] = None, - weight_vector: Sequence[float] - | Sequence[int] = None, + initial_states: Sequence[QuantumCircuit] = None, + weight_vector: Sequence[float] | Sequence[int] = None, gradient: BaseEstimatorGradient | None = None, callback: Callable[[int, np.ndarray, float, float], None] | None = None, - check_input_states_orthogonality: bool = True + check_input_states_orthogonality: bool = True, ) -> None: """ Args: @@ -315,7 +312,7 @@ def _get_evaluate_weighted_energy_sum( def evaluate_weighted_energy_sum(parameters): nonlocal eval_count - #nonlocal weights + # nonlocal weights parameters = np.reshape(parameters, (-1, num_parameters)).tolist() batchsize = len(parameters) @@ -430,8 +427,12 @@ def _check_operator_initial_states( else: initial_states = list_of_states if self.check_initial_states_orthogonal is True: - stacked_states_array = np.hstack([np.asarray(Statevector(state)) for state in initial_states]) - if not np.isclose(stacked_states_array.transpose() @ stacked_states_array, np.eye(self.k)): + stacked_states_array = np.hstack( + [np.asarray(Statevector(state)) for state in initial_states] + ) + if not np.isclose( + stacked_states_array.transpose() @ stacked_states_array, np.eye(self.k) + ): raise AlgorithmError( "The set of initial states provided is not mutually orthogonal." ) @@ -458,7 +459,8 @@ def _check_weight_vector(self, weight_vector: Sequence[float]) -> Sequence[float weight_vector = [self.k - n for n in range(self.k)] elif len(weight_vector) != self.k: raise AlgorithmError( - "The number of weights provided does not match the number of states.") + "The number of weights provided does not match the number of states." + ) return weight_vector diff --git a/test/python/algorithms/eigensolvers/test_ssvqe.py b/test/python/algorithms/eigensolvers/test_ssvqe.py index bf76f5b10ac5..4ba3b0f8043a 100644 --- a/test/python/algorithms/eigensolvers/test_ssvqe.py +++ b/test/python/algorithms/eigensolvers/test_ssvqe.py @@ -132,7 +132,13 @@ def test_initial_states_mismatching_num_qubits(self, op): initial_state = [QuantumCircuit(1)] wavefunction = QuantumCircuit(2) optimizer = SLSQP(maxiter=50) - ssvqe = SSVQE(estimator=self.estimator, k=1, ansatz=wavefunction, optimizer=optimizer, initial_states=initial_state) + ssvqe = SSVQE( + estimator=self.estimator, + k=1, + ansatz=wavefunction, + optimizer=optimizer, + initial_states=initial_state, + ) with self.assertRaises(AlgorithmError): _ = ssvqe.compute_eigenvalues(operator=op) @@ -140,7 +146,7 @@ def test_initial_states_mismatching_num_qubits(self, op): def test_mismatching_weights(self, op): """Ensuring incorrect number of weights is caught""" optimizer = SLSQP(maxiter=50) - ssvqe = SSVQE(estimator=self.estimator, k=2, weight_vector=[3,2,1], optimizer=optimizer) + ssvqe = SSVQE(estimator=self.estimator, k=2, weight_vector=[3, 2, 1], optimizer=optimizer) with self.assertRaises(AlgorithmError): _ = ssvqe.compute_eigenvalues(operator=op) @@ -151,7 +157,9 @@ def test_initial_states_nonorthogonal(self, op): initial_states[0].x(0) initial_states[1].x(0) optimizer = SLSQP(maxiter=50) - ssvqe = SSVQE(estimator=self.estimator, k=1, optimizer=optimizer, initial_states=initial_states) + ssvqe = SSVQE( + estimator=self.estimator, k=1, optimizer=optimizer, initial_states=initial_states + ) with self.assertRaises(AlgorithmError): _ = ssvqe.compute_eigenvalues(operator=op) From 777891a373cd39ee021c93861650f5a160e6f4ae Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 21:35:34 -0400 Subject: [PATCH 08/30] temporary change to troubleshoot docs --- qiskit/algorithms/eigensolvers/__init__.py | 2 -- qiskit/algorithms/eigensolvers/ssvqe.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/__init__.py b/qiskit/algorithms/eigensolvers/__init__.py index 9b247bd77b6b..6ab06c300ec7 100644 --- a/qiskit/algorithms/eigensolvers/__init__.py +++ b/qiskit/algorithms/eigensolvers/__init__.py @@ -26,7 +26,6 @@ Eigensolver NumPyEigensolver VQD - SSVQE Results ======= @@ -37,7 +36,6 @@ EigensolverResult NumPyEigensolverResult VQDResult - SSVQEResult """ diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 769951c6cd65..c776c2316fbf 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -312,7 +312,6 @@ def _get_evaluate_weighted_energy_sum( def evaluate_weighted_energy_sum(parameters): nonlocal eval_count - # nonlocal weights parameters = np.reshape(parameters, (-1, num_parameters)).tolist() batchsize = len(parameters) @@ -455,6 +454,7 @@ def _check_operator_initial_states( return initial_states def _check_weight_vector(self, weight_vector: Sequence[float]) -> Sequence[float]: + """Check that the number of weights matches the number of states.""" if weight_vector is None: weight_vector = [self.k - n for n in range(self.k)] elif len(weight_vector) != self.k: From 34debd0172fcbbe016999e6827516cc86458e42c Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Thu, 29 Sep 2022 22:35:03 -0400 Subject: [PATCH 09/30] add autosummary back --- qiskit/algorithms/eigensolvers/__init__.py | 2 ++ qiskit/algorithms/eigensolvers/ssvqe.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/eigensolvers/__init__.py b/qiskit/algorithms/eigensolvers/__init__.py index 6ab06c300ec7..9b247bd77b6b 100644 --- a/qiskit/algorithms/eigensolvers/__init__.py +++ b/qiskit/algorithms/eigensolvers/__init__.py @@ -26,6 +26,7 @@ Eigensolver NumPyEigensolver VQD + SSVQE Results ======= @@ -36,6 +37,7 @@ EigensolverResult NumPyEigensolverResult VQDResult + SSVQEResult """ diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index c776c2316fbf..9c3a7021db70 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -45,7 +45,7 @@ class SSVQE(VariationalAlgorithm, Eigensolver): r"""The Subspace Search Variational Quantum Eigensolver algorithm. - `SSVQE ` is a quantum algorithm that uses a + `SSVQE `__ is a quantum algorithm that uses a variational technique to find the low-lying eigenvalues of the Hamiltonian :math:`H` of a given system. SSVQE can be seen as a natural generalization of VQE. Whereas VQE From 166b6f697c2e13b44baea2a60714e78926cc2a75 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Fri, 30 Sep 2022 00:39:56 -0400 Subject: [PATCH 10/30] fix docstrings --- qiskit/algorithms/eigensolvers/ssvqe.py | 111 ++++++++++-------------- 1 file changed, 45 insertions(+), 66 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 9c3a7021db70..e3299f2d58d2 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -11,7 +11,8 @@ # that they have been altered from the originals. """The Subspace Search Variational Quantum Eigensolver algorithm. -See https://arxiv.org/abs/1810.09434 + +See https://arxiv.org/abs/1810.09434. """ from __future__ import annotations @@ -45,71 +46,49 @@ class SSVQE(VariationalAlgorithm, Eigensolver): r"""The Subspace Search Variational Quantum Eigensolver algorithm. - `SSVQE `__ is a quantum algorithm that uses a - variational technique to find - the low-lying eigenvalues of the Hamiltonian :math:`H` of a given system. - SSVQE can be seen as a natural generalization of VQE. Whereas VQE - minimizes the expectation value of :math:`H` with respect to one ansatz state, - SSVQE takes a set of mutually orthogonal input states, applies the same parameterized - ansatz to all of them, then minimizes a weighted sum - of the expectation values of :math:`H` with respect to these states. - An instance of SSVQE requires defining three algorithmic sub-components: - - An integer k denoting the number of eigenstates that the algorithm will attempt to find, - A trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, and one of the classical - :mod:`~qiskit.algorithms.optimizers`. The ansatz is varied, via its set of parameters, by the - optimizer, such that it works towards a set of mutually orthogonal states, as determined by the - parameters applied to the ansatz, that will result in the minimum weighted sum of expectation values - being measured of the input operator (Hamiltonian) with respect to these states. The weights - given to this list of expectation values is given by *weight_vector*. - An optional array of parameter values, via the *initial_point*, may be provided as the - starting point for the search of the low-lying eigenvalues. This feature is particularly useful - such as when there are reasons to believe that the solution point is close to a particular - point. The length of the *initial_point* list value must match the number of the parameters - expected by the ansatz being used. If the *initial_point* is left at the default - of ``None``, then SSVQE will look to the ansatz for a preferred value, based on its - given initial state. If the ansatz returns ``None``, - then a random point will be generated within the parameter bounds set, as per above. - If the ansatz provides ``None`` as the lower bound, then SSVQE - will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` - as the upper bound, the default value will be :math:`2\pi`. - - An optional list of initial states, via the *initial_states*, may also be provided. Choosing - these states appropriately is a critical part of the algorithm. They must be mutually orthogonal - because this is how the algorithm enforces the mutual orthogonality of the solution states. If - the *initial_states* is left as ``None``, then SSVQE will automatically generate a list of - computational basis states and use these as the initial states. For many physically-motivated - problems, it is advised to not rely on these default values as doing so can easily result in - an unphysical solution being returned. For example, if one wishes to find the low-lying - excited states of a molecular Hamiltonian, then we expect the output states to belong to - a particular particle-number subspace. If an ansatz that preserves particle number such as - :class:`UCCSD` is used, then states belonging to the incorrect particle number subspace - will be returned if the *initial_states* are not in the correct particle number subspace. - A similar statement can often be made for the spin-magnetization quantum number. - - The optimizer can either be one of Qiskit's optimizers, such as - :class:`~qiskit.algorithms.optimizers.SPSA` or a callable with the following signature: - .. note:: - The callable _must_ have the argument names ``fun, x0, jac, bounds`` as indicated - in the following code block. - .. code-block::python - from qiskit.algorithms.optimizers import OptimizerResult - def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: - # Note that the callable *must* have these argument names! - # Args: - # fun (callable): the function to minimize - # x0 (np.ndarray): the initial point for the optimization - # jac (callable, optional): the gradient of the objective function - # bounds (list, optional): a list of tuples specifying the parameter bounds - result = OptimizerResult() - result.x = # optimal parameters - result.fun = # optimal function value - return result - The above signature also allows to directly pass any SciPy minimizer, for instance as - .. code-block::python - from functools import partial - from scipy.optimize import minimize - optimizer = partial(minimize, method="L-BFGS-B") + `SSVQE `__ is a quantum algorithm + that uses a variational technique to find the low-lying eigenvalues + of the Hamiltonian :math:`H` of a given system. SSVQE can be seen as + a natural generalization of VQE. Whereas VQE minimizes the expectation + value of :math:`H` with respect to one ansatz state, SSVQE takes a set + of mutually orthogonal input states, applies the same parameterized ansatz + to all of them, then minimizes a weighted sum of the expectation values of + :math:`H` with respect to these states. An instance of SSVQE requires defining + three algorithmic sub-components: + + An integer k denoting the number of eigenstates that the algorithm will attempt + to find, a trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, and + one of the classical :mod:`~qiskit.algorithms.optimizers`. The ansatz is varied, + via its set of parameters, by the optimizer, such that it works towards a set of + mutually orthogonal states, as determined by the parameters applied to the ansatz, + that will result in the minimum weighted sum of expectation values being measured + of the input operator (Hamiltonian) with respect to these states. The weights given + to this list of expectation values is given by *weight_vector*. An optional array of + parameter values, via the *initial_point*, may be provided as the starting point for + the search of the low-lying eigenvalues. This feature is particularly useful such as + when there are reasons to believe that the solution point is close to a particular + point. The length of the *initial_point* list value must match the number of the + parameters expected by the ansatz being used. If the *initial_point* is left at the + default of ``None``, then SSVQE will look to the ansatz for a preferred value, based + on its given initial state. If the ansatz returns ``None``, then a random point will + be generated within the parameter bounds set, as per above. If the ansatz provides + ``None`` as the lower bound, then SSVQE will default it to :math:`-2\pi`; similarly, + if the ansatz returns ``None`` as the upper bound, the default value will be :math:`2\pi`. + + An optional list of initial states, via the *initial_states*, may also be provided. + Choosing these states appropriately is a critical part of the algorithm. They must + be mutually orthogonal because this is how the algorithm enforces the mutual + orthogonality of the solution states. If the *initial_states* is left as ``None``, + then SSVQE will automatically generate a list of computational basis states and use + these as the initial states. For many physically-motivated problems, it is advised + to not rely on these default values as doing so can easily result in an unphysical + solution being returned. For example, if one wishes to find the low-lying excited + states of a molecular Hamiltonian, then we expect the output states to belong to a + particular particle-number subspace. If an ansatz that preserves particle number + such as :class:`UCCSD` is used, then states belonging to the incorrect particle + number subspace will be returned if the *initial_states* are not in the correct + particle number subspace. A similar statement can often be made for the + spin-magnetization quantum number. """ def __init__( From b8be010781bdbb56b9d2269571a2b2726ea81c22 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Fri, 30 Sep 2022 13:37:32 -0400 Subject: [PATCH 11/30] adjust docstrings/ reduce duplicate code --- qiskit/algorithms/eigensolvers/ssvqe.py | 80 ++++++++++++++++--------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index e3299f2d58d2..d660401b0673 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -89,6 +89,32 @@ class SSVQE(VariationalAlgorithm, Eigensolver): number subspace will be returned if the *initial_states* are not in the correct particle number subspace. A similar statement can often be made for the spin-magnetization quantum number. + + The following attributes can be set via the initializer but can also be read and + updated once the SSVQE object has been constructed. + + Attributes: + estimator (BaseEstimator): The primitive instance used to perform the expectation + estimation of observables. + k (int): The number of eigenstates that SSVQE will attempt to find. + ansatz (QuantumCircuit): A parameterized circuit used as an ansatz for the + wave function. + optimizer (Optimizer): A classical optimizer, which can be either a Qiskit optimizer + or a callable that takes an array as input and returns a Qiskit or SciPy optimization + result. + gradient (BaseEstimatorGradient | None): An optional estimator gradient to be used with the + optimizer. + callback (Callable[[int, np.ndarray, np.ndarray, dict[str, Any]], None] | None): A callback that + can access the intermediate data at each optimization step. These data are: the + evaluation count, the optimizer parameters for the ansatz, the evaluated mean energies, and the + metadata dictionary. + weight_vector (Sequence[float]): A 1D array of real positive-valued numbers to assign + as weights to each of the expectation values. If ``None``, then SSVQE will default + to [k, k-1, ..., 1]. + initial_states (Sequence[QuantumCircuit]): An optional list of mutually orthogonal initial states. + If ``None``, then SSVQE will set these to be a list of mutually orthogonal + computational basis states. + """ def __init__( @@ -357,6 +383,25 @@ def evaluate_gradient(parameters): return evaluate_gradient + def _check_circuit_num_qubits( + self, operator: BaseOperator | PauliSumOp, circuit: QuantumCircuit, circuit_type: str + ) -> QuantumCircuit: + """Check that the number of qubits for the circuit passed matches the number of qubits of the operator.""" + if operator.num_qubits != circuit.num_qubits: + try: + logger.info( + f"Trying to resize {circuit_type} to match operator on %s qubits.", + operator.num_qubits, + ) + circuit.num_qubits = operator.num_qubits + except AttributeError as error: + raise AlgorithmError( + f"The number of qubits of the {circuit_type} does not match the " + f"operator, and the {circuit_type} does not allow setting the " + "number of qubits using `num_qubits`." + ) from error + return circuit + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. @@ -367,18 +412,9 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum else: ansatz = self.ansatz - if operator.num_qubits != ansatz.num_qubits: - try: - logger.info( - "Trying to resize ansatz to match operator on %s qubits.", operator.num_qubits - ) - ansatz.num_qubits = operator.num_qubits - except AttributeError as error: - raise AlgorithmError( - "The number of qubits of the ansatz does not match the " - "operator, and the ansatz does not allow setting the " - "number of qubits using `num_qubits`." - ) from error + ansatz = self._check_circuit_num_qubits( + operator=operator, circuit=ansatz, circuit_type="ansatz" + ) if ansatz.num_parameters == 0: raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") @@ -398,8 +434,8 @@ def _check_operator_initial_states( warnings.warn( "No initial states have been provided to SSVQE, so they have been set to " - "a subset of the computational basis states.This may result in unphysical " - "results for some problems." + "a subset of the computational basis states. This may result in unphysical " + "results for some chemistry and physics problems." ) else: @@ -416,19 +452,9 @@ def _check_operator_initial_states( ) for initial_state in initial_states: - if operator.num_qubits != initial_state.num_qubits: - try: - logger.info( - "Trying to resize initial state to match operator on %s qubits.", - operator.num_qubits, - ) - initial_state.num_qubits = operator.num_qubits - except AttributeError as error: - raise AlgorithmError( - "The number of qubits of the initial state does not match the " - "operator, and the initial state does not allow setting the " - "number of qubits using `num_qubits`." - ) from error + initial_state = self._check_circuit_num_qubits( + operator=operator, circuit=initial_state, circuit_type="initial state" + ) return initial_states From 3752340aa17b79639fd15650937e2c4f4bca789b Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Fri, 30 Sep 2022 14:03:31 -0400 Subject: [PATCH 12/30] adjust docstrings --- qiskit/algorithms/eigensolvers/ssvqe.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index d660401b0673..d818e875b427 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -147,11 +147,9 @@ def __init__( objective function. This fixes the ordering of the returned eigenstate/eigenvalue pairs. If ``None``, then SSVQE will default to [n, n-1, ..., 1] for `k` = n. gradient: An optional gradient function or operator for optimizer. - 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 for its current set of parameters as it works towards the minimum. - These are: the evaluation count, the optimizer parameters for the - ansatz, the evaluated mean and the evaluated standard deviation.` + callback: A callback that can access the intermediate data at each optimization step. + These data are: the evaluation count, the optimizer ansatz parameters, + the evaluated mean energies, and the metadata dictionary. check_input_states_orthogonality: A boolean that sets whether or not to check that the value of initial_states passed consists of a mutually orthogonal set of states. If ``True``, then SSVQE will check that these states are mutually From 19ccaf03dc1acf7046638e448a6a03b3f923e62d Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Fri, 30 Sep 2022 14:49:27 -0400 Subject: [PATCH 13/30] adjust docstrings --- qiskit/algorithms/eigensolvers/ssvqe.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index d818e875b427..7f64b5910bba 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -104,7 +104,7 @@ class SSVQE(VariationalAlgorithm, Eigensolver): result. gradient (BaseEstimatorGradient | None): An optional estimator gradient to be used with the optimizer. - callback (Callable[[int, np.ndarray, np.ndarray, dict[str, Any]], None] | None): A callback that + callback (Callable[[int, np.ndarray, Sequence[float], dict[str, Any]], None] | None): A function that can access the intermediate data at each optimization step. These data are: the evaluation count, the optimizer parameters for the ansatz, the evaluated mean energies, and the metadata dictionary. @@ -127,7 +127,7 @@ def __init__( initial_states: Sequence[QuantumCircuit] = None, weight_vector: Sequence[float] | Sequence[int] = None, gradient: BaseEstimatorGradient | None = None, - callback: Callable[[int, np.ndarray, float, float], None] | None = None, + callback: Callable[[int, np.ndarray, Sequence[float], float], None] | None = None, check_input_states_orthogonality: bool = True, ) -> None: """ @@ -181,16 +181,6 @@ def initial_point(self, initial_point: Sequence[float] | None): """Sets initial point""" self._initial_point = initial_point - @property - def callback(self) -> Callable[[int, np.ndarray, float, float], None] | None: - """Returns callback""" - return self._callback - - @callback.setter - def callback(self, callback: Callable[[int, np.ndarray, float, float], None] | None): - """Sets callback""" - self._callback = callback - @classmethod def supports_aux_operators(cls) -> bool: return True From 8114914236604b2ea4ff52168d6a976149d31bd1 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Fri, 30 Sep 2022 16:49:14 -0400 Subject: [PATCH 14/30] fix docs/linting --- qiskit/algorithms/eigensolvers/ssvqe.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 7f64b5910bba..bc277577acd7 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -104,16 +104,16 @@ class SSVQE(VariationalAlgorithm, Eigensolver): result. gradient (BaseEstimatorGradient | None): An optional estimator gradient to be used with the optimizer. - callback (Callable[[int, np.ndarray, Sequence[float], dict[str, Any]], None] | None): A function that - can access the intermediate data at each optimization step. These data are: the - evaluation count, the optimizer parameters for the ansatz, the evaluated mean energies, and the - metadata dictionary. + callback (Callable[[int, np.ndarray, Sequence[float], dict[str, Any]], None] | None): A + function that can access the intermediate data at each optimization step. These data are + the evaluation count, the optimizer parameters for the ansatz, the evaluated mean + energies, and the metadata dictionary. weight_vector (Sequence[float]): A 1D array of real positive-valued numbers to assign as weights to each of the expectation values. If ``None``, then SSVQE will default to [k, k-1, ..., 1]. - initial_states (Sequence[QuantumCircuit]): An optional list of mutually orthogonal initial states. - If ``None``, then SSVQE will set these to be a list of mutually orthogonal - computational basis states. + initial_states (Sequence[QuantumCircuit]): An optional list of mutually orthogonal + initial states. If ``None``, then SSVQE will set these to be a list of mutually + orthogonal computational basis states. """ @@ -374,19 +374,22 @@ def evaluate_gradient(parameters): def _check_circuit_num_qubits( self, operator: BaseOperator | PauliSumOp, circuit: QuantumCircuit, circuit_type: str ) -> QuantumCircuit: - """Check that the number of qubits for the circuit passed matches the number of qubits of the operator.""" + """Check that the number of qubits for the circuit passed matches + the number of qubits of the operator. + """ if operator.num_qubits != circuit.num_qubits: try: logger.info( - f"Trying to resize {circuit_type} to match operator on %s qubits.", + "Trying to resize %s to match operator on %s qubits.", + circuit_type, operator.num_qubits, ) circuit.num_qubits = operator.num_qubits except AttributeError as error: raise AlgorithmError( - f"The number of qubits of the {circuit_type} does not match the " + f"The number of qubits of the {circuit_type} does not match the ", f"operator, and the {circuit_type} does not allow setting the " - "number of qubits using `num_qubits`." + "number of qubits using `num_qubits`.", ) from error return circuit From e0d671f4c3e389ac174916e28090bd2197406f14 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Fri, 30 Sep 2022 19:10:35 -0400 Subject: [PATCH 15/30] add estimator to args in docstrings --- qiskit/algorithms/eigensolvers/ssvqe.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index bc277577acd7..48b17992073d 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -132,6 +132,7 @@ def __init__( ) -> None: """ Args: + estimator: The estimator primitive. k: The number of eigenstates that the algorithm will attempt to find. ansatz: A parameterized circuit used as Ansatz for the wave function. optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable From 10d98d781f45e9fc1c364d36a598425eae1ed704 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 14:45:30 -0400 Subject: [PATCH 16/30] adjust order of attributes in docstring --- qiskit/algorithms/eigensolvers/ssvqe.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 48b17992073d..24f9e8cc2c6e 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -104,17 +104,16 @@ class SSVQE(VariationalAlgorithm, Eigensolver): result. gradient (BaseEstimatorGradient | None): An optional estimator gradient to be used with the optimizer. + initial_states (Sequence[QuantumCircuit]): An optional list of mutually orthogonal + initial states. If ``None``, then SSVQE will set these to be a list of mutually + orthogonal computational basis states. + weight_vector (Sequence[float]): A 1D array of real positive-valued numbers to assign + as weights to each of the expectation values. If ``None``, then SSVQE will default + to [k, k-1, ..., 1]. callback (Callable[[int, np.ndarray, Sequence[float], dict[str, Any]], None] | None): A function that can access the intermediate data at each optimization step. These data are the evaluation count, the optimizer parameters for the ansatz, the evaluated mean energies, and the metadata dictionary. - weight_vector (Sequence[float]): A 1D array of real positive-valued numbers to assign - as weights to each of the expectation values. If ``None``, then SSVQE will default - to [k, k-1, ..., 1]. - initial_states (Sequence[QuantumCircuit]): An optional list of mutually orthogonal - initial states. If ``None``, then SSVQE will set these to be a list of mutually - orthogonal computational basis states. - """ def __init__( From ff2c76fe077b4ec2f082a49d40225ee34eb76bd5 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 14:50:48 -0400 Subject: [PATCH 17/30] adjust attributes in docstring --- qiskit/algorithms/eigensolvers/ssvqe.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 24f9e8cc2c6e..85daaa686e8c 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -114,6 +114,12 @@ class SSVQE(VariationalAlgorithm, Eigensolver): function that can access the intermediate data at each optimization step. These data are the evaluation count, the optimizer parameters for the ansatz, the evaluated mean energies, and the metadata dictionary. + check_input_states_orthogonality: A boolean that sets whether or not to check + that the value of initial_states passed consists of a mutually orthogonal + set of states. If ``True``, then SSVQE will check that these states are mutually + orthogonal and return an error if they are not. This is set to ``True`` by default, + but setting this to ``False`` may be desirable for larger numbers of qubits to avoid + exponentially large computational overhead before the simulation even starts. """ def __init__( From 2aef67646c8d65f8b708732475558eeb1c0f5cf2 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 14:56:38 -0400 Subject: [PATCH 18/30] remove redundant start_time --- qiskit/algorithms/eigensolvers/ssvqe.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 85daaa686e8c..834664f55922 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -209,8 +209,6 @@ def compute_eigenvalues( self.weight_vector = self._check_weight_vector(self.weight_vector) - start_time = time() - evaluate_weighted_energy_sum = self._get_evaluate_weighted_energy_sum( initialized_ansatz_list, operator ) From 249bc41e9c5af2c49faa66544ef4db55de3765d1 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 15:06:59 -0400 Subject: [PATCH 19/30] remove old comment --- qiskit/algorithms/eigensolvers/ssvqe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 834664f55922..45717f35f97e 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -289,7 +289,7 @@ def _get_evaluate_weighted_energy_sum( initialized_ansatz_list: list[QuantumCircuit], operator: BaseOperator | PauliSumOp, ) -> tuple[Callable[[np.ndarray], float | list[float]], dict]: - """Returns a function handle to evaluates the weighted energy sum at given parameters + """Returns a function handle to evaluate the weighted energy sum at given parameters for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. Args: @@ -340,7 +340,7 @@ def evaluate_weighted_energy_sum(parameters): return evaluate_weighted_energy_sum - def _get_evalute_gradient( # check this implementation + def _get_evalute_gradient( self, initialized_ansatz_list: list[QuantumCircuit], operator: BaseOperator | PauliSumOp, From 3db7006ad4d39cfb6e1f3bbc505552399d36e3f7 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 15:17:57 -0400 Subject: [PATCH 20/30] chanage Sequence to list of QuantumCircuits --- qiskit/algorithms/eigensolvers/ssvqe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 45717f35f97e..f62ca54a9acd 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -129,7 +129,7 @@ def __init__( ansatz: QuantumCircuit | None = None, optimizer: Optimizer | Minimizer = None, initial_point: Sequence[float] = None, - initial_states: Sequence[QuantumCircuit] = None, + initial_states: list[QuantumCircuit] = None, weight_vector: Sequence[float] | Sequence[int] = None, gradient: BaseEstimatorGradient | None = None, callback: Callable[[int, np.ndarray, Sequence[float], float], None] | None = None, @@ -417,7 +417,7 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum return ansatz def _check_operator_initial_states( - self, list_of_states: Sequence[QuantumCircuit] | None, operator: BaseOperator | PauliSumOp + self, list_of_states: list[QuantumCircuit] | None, operator: BaseOperator | PauliSumOp ) -> QuantumCircuit: """Check that the number of qubits of operator and all the initial states match.""" @@ -492,7 +492,7 @@ def _build_ssvqe_result( aux_operators_evaluated: ListOrDict[tuple[complex, tuple[complex, int]]], optimizer_time: float, operator: BaseOperator | PauliSumOp, - initialized_ansatz_list: Sequence[QuantumCircuit], + initialized_ansatz_list: list[QuantumCircuit], ) -> SSVQEResult: result = SSVQEResult() result.eigenvalues = ( From bb917799d074baab523a2dcf94115c24ae2a4494 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 15:51:20 -0400 Subject: [PATCH 21/30] wrap aux operator evaluation in try block --- qiskit/algorithms/eigensolvers/ssvqe.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index f62ca54a9acd..52f77213a1f3 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -477,12 +477,17 @@ def _eval_aux_ops( aux_ops = aux_operators num_aux_ops = len(aux_ops) - aux_job = self.estimator.run([ansatz] * num_aux_ops, aux_ops) - aux_values = aux_job.result().values - aux_values = list(zip(aux_values, [0] * len(aux_values))) - if isinstance(aux_operators, dict): - aux_values = dict(zip(aux_operators.keys(), aux_values)) + try: + aux_job = self.estimator.run([ansatz] * num_aux_ops, aux_ops) + aux_values = aux_job.result().values + aux_values = list(zip(aux_values, [0] * len(aux_values))) + + if isinstance(aux_operators, dict): + aux_values = dict(zip(aux_operators.keys(), aux_values)) + + except Exception as exc: + raise AlgorithmError("The primitive job to evaluate the gradient failed!") from exc return aux_values From c16b14328c80fdd5328f209338f9c4e7cd984527 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 15:52:25 -0400 Subject: [PATCH 22/30] edit error message --- qiskit/algorithms/eigensolvers/ssvqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 52f77213a1f3..5411af7c7cb0 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -487,7 +487,7 @@ def _eval_aux_ops( aux_values = dict(zip(aux_operators.keys(), aux_values)) except Exception as exc: - raise AlgorithmError("The primitive job to evaluate the gradient failed!") from exc + raise AlgorithmError("The primitive job to evaluate the aux operator values failed!") from exc return aux_values From d3f7f02b57efec5b340840209897aef8d25f765c Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 15:56:00 -0400 Subject: [PATCH 23/30] wrap eigenvalue evaluations in try block --- qiskit/algorithms/eigensolvers/ssvqe.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 5411af7c7cb0..9c04cab6d05e 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -500,13 +500,19 @@ def _build_ssvqe_result( initialized_ansatz_list: list[QuantumCircuit], ) -> SSVQEResult: result = SSVQEResult() - result.eigenvalues = ( - self.estimator.run( - initialized_ansatz_list, [operator] * self.k, [optimizer_result.x] * self.k + + try: + result.eigenvalues = ( + self.estimator.run( + initialized_ansatz_list, [operator] * self.k, [optimizer_result.x] * self.k + ) + .result() + .values ) - .result() - .values - ) + + except Exception as exc: + raise AlgorithmError("The primitive job to evaluate the gradient failed!") from exc + result.cost_function_evals = optimizer_result.nfev result.optimal_point = optimizer_result.x result.optimal_parameters = dict(zip(self.ansatz.parameters, optimizer_result.x)) From eb1c1d3b149d4b471fc96d3b13c75a8c46f8038f Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 16:05:51 -0400 Subject: [PATCH 24/30] edit error message --- qiskit/algorithms/eigensolvers/ssvqe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 9c04cab6d05e..2e32a85ffae8 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -511,8 +511,8 @@ def _build_ssvqe_result( ) except Exception as exc: - raise AlgorithmError("The primitive job to evaluate the gradient failed!") from exc - + raise AlgorithmError("The primitive job to evaluate the eigenvalues failed!") from exc + result.cost_function_evals = optimizer_result.nfev result.optimal_point = optimizer_result.x result.optimal_parameters = dict(zip(self.ansatz.parameters, optimizer_result.x)) From ab4cd2e0b7f72e05302e70d9599838596726a4f3 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 16:13:40 -0400 Subject: [PATCH 25/30] adjust release note --- releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml b/releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml index 3c25e3892f90..a3208c72a5c8 100644 --- a/releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml +++ b/releasenotes/notes/add-ssvqe-algorithm-6c395267cdd03798.yaml @@ -14,13 +14,13 @@ features: .. code-block:: python from qiskit import QuantumCircuit - from qiskit.opflow import Z + from qiskit.quantum_info import Pauli from qiskit.primitives import Estimator from qiskit.circuit.library import RealAmplitudes from qiskit.algorithms.optimizers import SPSA from qiskit.algorithms.eigensolvers import SSVQE - operator = Z^Z + operator = Pauli("ZZ") input_states = [QuantumCircuit(2), QuantumCircuit(2)] input_states[0].x(0) input_states[1].x(1) From 4c15a162130ea87611a7c3787a1b4ac3fd8fbf9d Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 16:16:03 -0400 Subject: [PATCH 26/30] try removing _almost from testing --- test/python/algorithms/eigensolvers/test_ssvqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/algorithms/eigensolvers/test_ssvqe.py b/test/python/algorithms/eigensolvers/test_ssvqe.py index 4ba3b0f8043a..9509c87d9eb5 100644 --- a/test/python/algorithms/eigensolvers/test_ssvqe.py +++ b/test/python/algorithms/eigensolvers/test_ssvqe.py @@ -210,7 +210,7 @@ def store_intermediate_result(eval_count, parameters, mean, metadata): ref_eval_count = [1, 2, 3] ref_mean = [[-1.07, -1.44], [-1.45, -1.06], [-1.37, -0.94]] - np.testing.assert_array_almost_equal(history["eval_count"], ref_eval_count, decimal=0) + np.testing.assert_array_equal(history["eval_count"], ref_eval_count) np.testing.assert_array_almost_equal(history["mean_energies"], ref_mean, decimal=2) @data(H2_PAULI, H2_OP) From a428057ea59e1221b2ae44910de7251df901b4cd Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 19 Oct 2022 16:18:14 -0400 Subject: [PATCH 27/30] linting --- qiskit/algorithms/eigensolvers/ssvqe.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 2e32a85ffae8..12d42f488487 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -487,7 +487,9 @@ def _eval_aux_ops( aux_values = dict(zip(aux_operators.keys(), aux_values)) except Exception as exc: - raise AlgorithmError("The primitive job to evaluate the aux operator values failed!") from exc + raise AlgorithmError( + "The primitive job to evaluate the aux operator values failed!" + ) from exc return aux_values @@ -511,7 +513,7 @@ def _build_ssvqe_result( ) except Exception as exc: - raise AlgorithmError("The primitive job to evaluate the eigenvalues failed!") from exc + raise AlgorithmError("The primitive job to evaluate the eigenvalues failed!") from exc result.cost_function_evals = optimizer_result.nfev result.optimal_point = optimizer_result.x From 694e9045cc5bc8f2f17d0e9562e412cc3b9c1bb3 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Wed, 30 Nov 2022 18:13:08 -0500 Subject: [PATCH 28/30] implement suggested changes --- qiskit/algorithms/eigensolvers/ssvqe.py | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 12d42f488487..6ea2227a8523 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -63,22 +63,22 @@ class SSVQE(VariationalAlgorithm, Eigensolver): mutually orthogonal states, as determined by the parameters applied to the ansatz, that will result in the minimum weighted sum of expectation values being measured of the input operator (Hamiltonian) with respect to these states. The weights given - to this list of expectation values is given by *weight_vector*. An optional array of - parameter values, via the *initial_point*, may be provided as the starting point for + to this list of expectation values is given by ``weight_vector``. An optional array of + parameter values, via the ``initial_point``, may be provided as the starting point for the search of the low-lying eigenvalues. This feature is particularly useful such as when there are reasons to believe that the solution point is close to a particular - point. The length of the *initial_point* list value must match the number of the - parameters expected by the ansatz being used. If the *initial_point* is left at the + point. The length of the ``initial_point`` list value must match the number of the + parameters expected by the ansatz being used. If the ``initial_point`` is left at the default of ``None``, then SSVQE will look to the ansatz for a preferred value, based on its given initial state. If the ansatz returns ``None``, then a random point will be generated within the parameter bounds set, as per above. If the ansatz provides ``None`` as the lower bound, then SSVQE will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` as the upper bound, the default value will be :math:`2\pi`. - An optional list of initial states, via the *initial_states*, may also be provided. + An optional list of initial states, via the ``initial_states``, may also be provided. Choosing these states appropriately is a critical part of the algorithm. They must be mutually orthogonal because this is how the algorithm enforces the mutual - orthogonality of the solution states. If the *initial_states* is left as ``None``, + orthogonality of the solution states. If the ``initial_states`` is left as ``None``, then SSVQE will automatically generate a list of computational basis states and use these as the initial states. For many physically-motivated problems, it is advised to not rely on these default values as doing so can easily result in an unphysical @@ -86,7 +86,7 @@ class SSVQE(VariationalAlgorithm, Eigensolver): states of a molecular Hamiltonian, then we expect the output states to belong to a particular particle-number subspace. If an ansatz that preserves particle number such as :class:`UCCSD` is used, then states belonging to the incorrect particle - number subspace will be returned if the *initial_states* are not in the correct + number subspace will be returned if the ``initial_states`` are not in the correct particle number subspace. A similar statement can often be made for the spin-magnetization quantum number. @@ -127,10 +127,10 @@ def __init__( estimator: BaseEstimator, k: int | None = 2, ansatz: QuantumCircuit | None = None, - optimizer: Optimizer | Minimizer = None, - initial_point: Sequence[float] = None, - initial_states: list[QuantumCircuit] = None, - weight_vector: Sequence[float] | Sequence[int] = None, + optimizer: Optimizer | Minimizer | None = None, + initial_point: Sequence[float] | None = None, + initial_states: list[QuantumCircuit] | None = None, + weight_vector: Sequence[float] | Sequence[int] | None = None, gradient: BaseEstimatorGradient | None = None, callback: Callable[[int, np.ndarray, Sequence[float], float], None] | None = None, check_input_states_orthogonality: bool = True, @@ -149,7 +149,7 @@ def __init__( If ``None``, then SSVQE will set these to be a list of mutually orthogonal computational basis states. weight_vector: An optional list or array of real positive numbers with length - equal to the value of *num_states* to be used in the weighted energy summation + equal to the value of ``num_states`` to be used in the weighted energy summation objective function. This fixes the ordering of the returned eigenstate/eigenvalue pairs. If ``None``, then SSVQE will default to [n, n-1, ..., 1] for `k` = n. gradient: An optional gradient function or operator for optimizer. @@ -157,7 +157,7 @@ def __init__( These data are: the evaluation count, the optimizer ansatz parameters, the evaluated mean energies, and the metadata dictionary. check_input_states_orthogonality: A boolean that sets whether or not to check - that the value of initial_states passed consists of a mutually orthogonal + that the value of ``initial_states`` passed consists of a mutually orthogonal set of states. If ``True``, then SSVQE will check that these states are mutually orthogonal and return an error if they are not. This is set to ``True`` by default, but setting this to ``False`` may be desirable for larger numbers of qubits to avoid @@ -293,15 +293,15 @@ def _get_evaluate_weighted_energy_sum( for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. Args: - operator: The operator whose energy levels to evaluate. - return_expectation: If True, return the ``ExpectationBase`` expectation converter used - in the construction of the expectation value. Useful e.g. to evaluate other - operators with the same expectation value converter. + initialized_anastz_list: A list consisting of copies of the ansatz initialized + in the initial states. + operator: The operator whose expectation value with respect to each of the + states in ``initialzed_ansatz_list`` is being measured. Returns: - Weighted energy sum of the hamiltonian of each parameter, and, optionally, the expectation - converter. + Weighted expectation value sum of the operator for each parameter. Raises: - RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters). + AlgorithmError: If the primitive job to evaluate the weighted energy + sum fails. """ num_parameters = initialized_ansatz_list[0].num_parameters From 2c287def0f0a24d0f479bc2ed7027b04ca72c5b1 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Tue, 6 Dec 2022 17:00:47 -0500 Subject: [PATCH 29/30] modify SSVQE docstring --- qiskit/algorithms/eigensolvers/ssvqe.py | 66 +++++++++++++++++++------ 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index 6ea2227a8523..ca03d8af2a4a 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -46,21 +46,30 @@ class SSVQE(VariationalAlgorithm, Eigensolver): r"""The Subspace Search Variational Quantum Eigensolver algorithm. - `SSVQE `__ is a quantum algorithm - that uses a variational technique to find the low-lying eigenvalues + `SSVQE `__ is a hybrid quantum-classical + algorithm that uses a variational technique to find the low-lying eigenvalues of the Hamiltonian :math:`H` of a given system. SSVQE can be seen as a natural generalization of VQE. Whereas VQE minimizes the expectation - value of :math:`H` with respect to one ansatz state, SSVQE takes a set - of mutually orthogonal input states, applies the same parameterized ansatz - to all of them, then minimizes a weighted sum of the expectation values of - :math:`H` with respect to these states. An instance of SSVQE requires defining - three algorithmic sub-components: - - An integer k denoting the number of eigenstates that the algorithm will attempt - to find, a trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, and - one of the classical :mod:`~qiskit.algorithms.optimizers`. The ansatz is varied, - via its set of parameters, by the optimizer, such that it works towards a set of - mutually orthogonal states, as determined by the parameters applied to the ansatz, + value of :math:`H` with respect to the ansatz state, SSVQE takes a set + of mutually orthogonal input states :math:`\{| \psi_{i}} \rangle\}_{i=0}^{k-1}`, + applies the same parameterized ansatz circuit :math:`U(\vec\theta)` to all of them, + then minimizes a weighted sum of the expectation values of :math:`H` with respect to these states: + + .. math:: + + \min_{\vec\theta} \sum_{i=0}^{k-1} w_{i} \langle\psi_{i} (\vec\theta)|H| \psi{i}(\vec\theta) \rangle + + where :math:`|\psi{i} (\vec\theta)\rangle` is shorthand for :math:`U (\vec\theta)| \psi_{i} \rangle` + and :math:`\{ w_{i} \}_{i=0}^{k-1}` are the components of the ``weight_vector``. + + An instance of SSVQE requires defining four algorithmic sub-components: + + An :attr:`estimator` to compute the expectation values of operators, an integer ``k`` denoting + the number of eigenstates that the algorithm will attempt to find, an ansatz which is a + :class:`QuantumCircuit`, and one of the classical :mod:`~qiskit.algorithms.optimizers`. + + The ansatz is varied, via its set of parameters, by the optimizer, such that it works towards + a set of mutually orthogonal states, as determined by the parameters applied to the ansatz, that will result in the minimum weighted sum of expectation values being measured of the input operator (Hamiltonian) with respect to these states. The weights given to this list of expectation values is given by ``weight_vector``. An optional array of @@ -90,6 +99,31 @@ class SSVQE(VariationalAlgorithm, Eigensolver): particle number subspace. A similar statement can often be made for the spin-magnetization quantum number. + A minimal example of how one may initialize an instance of ``SSVQE`` and use it + to compute the low-lying eigenvalues of an operator: + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.quantum_info import Pauli + from qiskit.primitives import Estimator + from qiskit.circuit.library import RealAmplitudes + from qiskit.algorithms.optimizers import SPSA + from qiskit.algorithms.eigensolvers import SSVQE + + operator = Pauli("ZZ") + input_states = [QuantumCircuit(2), QuantumCircuit(2)] + input_states[0].x(0) + input_states[1].x(1) + + ssvqe_instance = SSVQE(k=2, + estimator=Estimator(), + optimizer=SPSA(), + ansatz=RealAmplitudes(2), + initial_states=input_states) + + result = ssvqe_instance.compute_eigenvalues(operator) + The following attributes can be set via the initializer but can also be read and updated once the SSVQE object has been constructed. @@ -117,9 +151,9 @@ class SSVQE(VariationalAlgorithm, Eigensolver): check_input_states_orthogonality: A boolean that sets whether or not to check that the value of initial_states passed consists of a mutually orthogonal set of states. If ``True``, then SSVQE will check that these states are mutually - orthogonal and return an error if they are not. This is set to ``True`` by default, - but setting this to ``False`` may be desirable for larger numbers of qubits to avoid - exponentially large computational overhead before the simulation even starts. + orthogonal and return an :class:`AlgorithmError` if they are not. + This is set to ``True`` by default, but setting this to ``False`` may be desirable + for larger numbers of qubits to avoid exponentially large computational overhead. """ def __init__( From 4bdba5c0f2d9f394bdb2eff07e7b63a7a3166210 Mon Sep 17 00:00:00 2001 From: Joel Bierman Date: Tue, 6 Dec 2022 21:07:54 -0500 Subject: [PATCH 30/30] fixing linting --- qiskit/algorithms/eigensolvers/ssvqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/eigensolvers/ssvqe.py b/qiskit/algorithms/eigensolvers/ssvqe.py index ca03d8af2a4a..f77dfd9904a6 100644 --- a/qiskit/algorithms/eigensolvers/ssvqe.py +++ b/qiskit/algorithms/eigensolvers/ssvqe.py @@ -57,7 +57,7 @@ class SSVQE(VariationalAlgorithm, Eigensolver): .. math:: - \min_{\vec\theta} \sum_{i=0}^{k-1} w_{i} \langle\psi_{i} (\vec\theta)|H| \psi{i}(\vec\theta) \rangle + \min_{\vec\theta}\sum_{i=0}^{k-1} w_{i}\langle\psi_{i}(\vec\theta)|H|\psi{i}(\vec\theta)\rangle where :math:`|\psi{i} (\vec\theta)\rangle` is shorthand for :math:`U (\vec\theta)| \psi_{i} \rangle` and :math:`\{ w_{i} \}_{i=0}^{k-1}` are the components of the ``weight_vector``.