From cdc5d3fc7e51bd4a2e485ad13f4f7b3fb44e5d03 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 17 Jun 2021 10:46:26 +0200 Subject: [PATCH] Stateless ``VQE`` (Qiskit/qiskit-terra#6418) * make VQE stateless TODO deprecation * fix get initial point from ansatz * try fixing use of deprecate_function * fix tests & black * deprecate varalgo methods * sort params per circuit default * clarify deprecation message in varalgo.initialpoint * initialize eval_count with 0 instead of None it doesn't really matter, having it None was more a safeguard, but adaptVQE breaks if this is None and not 0 * simplify get_eigenstate * fix lint * remote note on parameter order * review suggestions * deprecate sort_parameters_by_name * check ansatz has 0 params in setter * rename varform -> ansatz * fix tests * lint * keep previous behaviour of expectation meaning that if a user didn't set it, we re-construct the expectation for each new run of VQE * move expectation factory to right place * address Qiskit/qiskit-terra#5746 * fix typehint in expectation (add Optional) * fix expectation setter Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../minimum_eigen_solvers/qaoa.py | 4 +- .../minimum_eigen_solvers/vqe.py | 524 +++++++++++------- qiskit_algorithms/variational_algorithm.py | 96 +++- test/test_vqe.py | 55 +- 4 files changed, 449 insertions(+), 230 deletions(-) diff --git a/qiskit_algorithms/minimum_eigen_solvers/qaoa.py b/qiskit_algorithms/minimum_eigen_solvers/qaoa.py index 37085b3c..18689aab 100644 --- a/qiskit_algorithms/minimum_eigen_solvers/qaoa.py +++ b/qiskit_algorithms/minimum_eigen_solvers/qaoa.py @@ -125,14 +125,12 @@ def __init__( quantum_instance=quantum_instance, ) - def _check_operator(self, operator: OperatorBase) -> OperatorBase: + def _check_operator_ansatz(self, operator: OperatorBase) -> OperatorBase: # Recreates a circuit based on operator parameter. if operator.num_qubits != self.ansatz.num_qubits: self.ansatz = QAOAAnsatz( operator, self._reps, initial_state=self._initial_state, mixer_operator=self._mixer ) - operator = super()._check_operator(operator) - return operator @property def initial_state(self) -> Optional[QuantumCircuit]: diff --git a/qiskit_algorithms/minimum_eigen_solvers/vqe.py b/qiskit_algorithms/minimum_eigen_solvers/vqe.py index 48941cfc..58673a4c 100755 --- a/qiskit_algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit_algorithms/minimum_eigen_solvers/vqe.py @@ -15,13 +15,13 @@ See https://arxiv.org/abs/1304.3061 """ -from typing import Optional, List, Callable, Union, Dict +from typing import Optional, List, Callable, Union, Dict, Tuple import logging +import warnings from time import time import numpy as np -from qiskit import ClassicalRegister, QuantumCircuit -from qiskit.circuit import Parameter +from qiskit.circuit import QuantumCircuit, Parameter from qiskit.circuit.library import RealAmplitudes from qiskit.providers import BaseBackend from qiskit.providers import Backend @@ -38,7 +38,8 @@ from qiskit.opflow.gradients import GradientBase from qiskit.utils.validation import validate_min from qiskit.utils.backend_utils import is_aer_provider -from qiskit.utils.quantum_instance import QuantumInstance +from qiskit.utils.deprecation import deprecate_function +from qiskit.utils import QuantumInstance, algorithm_globals from ..optimizers import Optimizer, SLSQP from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigen_solver import MinimumEigensolver, MinimumEigensolverResult @@ -83,14 +84,6 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): 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`. - .. note:: - - The VQE stores the parameters of ``ansatz`` sorted by name to map the values - provided by the optimizer to the circuit. This is done to ensure reproducible results, - for example such that running the optimization twice with same random seeds yields the - same result. Also, the ``optimal_point`` of the result object can be used as initial - point of another VQE run by passing it as ``initial_point`` to the initializer. - """ def __init__( @@ -104,6 +97,7 @@ def __init__( max_evals_grouped: int = 1, callback: Optional[Callable[[int, np.ndarray, float, float], None]] = None, quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None, + sort_parameters_by_name: Optional[bool] = None, ) -> None: """ @@ -139,79 +133,132 @@ def __init__( 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 + sort_parameters_by_name: Deprecated. If True, the initial point is bound to the ansatz + parameters strictly sorted by name instead of the default circuit order. That means + that the ansatz parameters are e.g. sorted as ``x[0] x[1] x[10] x[2] ...`` instead + of ``x[0] x[1] x[2] ... x[10]``. Set this to ``True`` to obtain the behavior prior + to Qiskit Terra 0.18.0. """ validate_min("max_evals_grouped", max_evals_grouped, 1) + + if sort_parameters_by_name is not None: + warnings.warn( + "The ``sort_parameters_by_name`` attribute is deprecated and will be " + "removed no sooner than 3 months after the release date of Qiskit Terra " + "0.18.0.", + DeprecationWarning, + stacklevel=2, + ) + if ansatz is None: ansatz = RealAmplitudes() if optimizer is None: optimizer = SLSQP() - # set the initial point to the preferred parameters of the ansatz - if initial_point is None and hasattr(ansatz, "preferred_init_points"): - initial_point = ansatz.preferred_init_points + if quantum_instance is not None: + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + + super().__init__() self._max_evals_grouped = max_evals_grouped self._circuit_sampler = None # type: Optional[CircuitSampler] self._expectation = expectation - self._user_valid_expectation = self._expectation is not None self._include_custom = include_custom - self._expect_op = None - super().__init__( - ansatz=ansatz, - optimizer=optimizer, - cost_fn=self._energy_evaluation, - gradient=gradient, - initial_point=initial_point, - quantum_instance=quantum_instance, - ) - self._ret = VQEResult() + # set ansatz -- still supporting pre 0.18.0 sorting + self._sort_parameters_by_name = sort_parameters_by_name + self._ansatz_params = None + self._ansatz = None + self.ansatz = ansatz + + self._optimizer = optimizer + self._initial_point = initial_point + self._gradient = gradient + self._quantum_instance = None + + if quantum_instance is not None: + self.quantum_instance = quantum_instance + self._eval_time = None + self._eval_count = 0 self._optimizer.set_max_evals_grouped(max_evals_grouped) self._callback = callback - self._eval_count = 0 logger.info(self.print_settings()) - def _try_set_expectation_value_from_factory(self, operator: OperatorBase) -> None: - if operator is not None and self.quantum_instance is not None: - self._set_expectation( - ExpectationFactory.build( - operator=operator, - backend=self.quantum_instance, - include_custom=self._include_custom, - ) - ) + # TODO remove this once the stateful methods are deleted + self._ret = None - def _set_expectation(self, exp: ExpectationBase) -> None: - self._expectation = exp - self._user_valid_expectation = False - self._expect_op = None + @property + def ansatz(self) -> Optional[QuantumCircuit]: + """Returns the ansatz.""" + return self._ansatz + + @ansatz.setter + def ansatz(self, ansatz: Optional[QuantumCircuit]): + """Sets the ansatz. + + Args: + ansatz: The parameterized circuit used as an ansatz. + + """ + self._ansatz = ansatz + if ansatz is not None: + if self._sort_parameters_by_name: + self._ansatz_params = sorted(ansatz.parameters, key=lambda p: p.name) + else: + self._ansatz_params = list(ansatz.parameters) - @VariationalAlgorithm.quantum_instance.setter + @property + def gradient(self) -> Optional[Union[GradientBase, Callable]]: + """Returns the gradient.""" + return self._gradient + + @gradient.setter + def gradient(self, gradient: Optional[Union[GradientBase, Callable]]): + """Sets the gradient.""" + self._gradient = gradient + + @property + def quantum_instance(self) -> Optional[QuantumInstance]: + """Returns quantum instance.""" + return self._quantum_instance + + @quantum_instance.setter def quantum_instance( self, quantum_instance: Union[QuantumInstance, BaseBackend, Backend] ) -> None: """set quantum_instance""" - super(VQE, self.__class__).quantum_instance.__set__(self, quantum_instance) - + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + self._quantum_instance = quantum_instance self._circuit_sampler = CircuitSampler( - self._quantum_instance, param_qobj=is_aer_provider(self._quantum_instance.backend) + quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend) ) @property - def expectation(self) -> ExpectationBase: + def initial_point(self) -> Optional[np.ndarray]: + """Returns initial point""" + return self._initial_point + + @initial_point.setter + def initial_point(self, initial_point: np.ndarray): + """Sets initial point""" + self._initial_point = initial_point + + @property + def expectation(self) -> Optional[ExpectationBase]: """The expectation value algorithm used to construct the expectation measurement from the observable.""" return self._expectation @expectation.setter - def expectation(self, exp: ExpectationBase) -> None: - self._set_expectation(exp) - self._user_valid_expectation = self._expectation is not None + def expectation(self, exp: Optional[ExpectationBase]) -> None: + self._expectation = exp - def _check_operator_varform(self, operator: OperatorBase): + def _check_operator_ansatz(self, operator: OperatorBase): """Check that the number of qubits of operator and ansatz match.""" if operator is not None and self.ansatz is not None: if operator.num_qubits != self.ansatz.num_qubits: @@ -226,10 +273,15 @@ def _check_operator_varform(self, operator: OperatorBase): "number of qubits using `num_qubits`." ) from ex - @VariationalAlgorithm.optimizer.setter # type: ignore + @property + def optimizer(self) -> Optional[Optimizer]: + """Returns optimizer""" + return self._optimizer + + @optimizer.setter def optimizer(self, optimizer: Optimizer): """Sets optimizer""" - super(VQE, self.__class__).optimizer.__set__(self, optimizer) # type: ignore + self._optimizer = optimizer if optimizer is not None: optimizer.set_max_evals_grouped(self._max_evals_grouped) @@ -260,12 +312,8 @@ def print_settings(self): ) ret += "{}".format(self.setting) ret += "===============================================================\n" - if hasattr(self._ansatz, "setting"): - ret += "{}".format(self._ansatz.setting) - elif hasattr(self._ansatz, "print_settings"): - ret += "{}".format(self._ansatz.print_settings()) - elif isinstance(self._ansatz, QuantumCircuit): - ret += "ansatz is a custom circuit" + if self.ansatz is not None: + ret += "{}".format(self.ansatz.draw(output="text")) else: ret += "ansatz has not been set" ret += "===============================================================\n" @@ -277,7 +325,8 @@ def construct_expectation( self, parameter: Union[List[float], List[Parameter], np.ndarray], operator: OperatorBase, - ) -> OperatorBase: + return_expectation: bool = False, + ) -> Union[OperatorBase, Tuple[OperatorBase, ExpectationBase]]: r""" Generate the ansatz circuit and expectation value measurement, and return their runnable composition. @@ -285,40 +334,45 @@ def construct_expectation( Args: parameter: Parameters for the ansatz circuit. operator: Qubit operator of the Observable + return_expectation: If True, return the ``ExpectationBase`` expectation converter used + in the construction of the expectation value. Useful e.g. to compute the standard + deviation of the expectation value. Returns: The Operator equalling the measurement of the ansatz :class:`StateFn` by the - Observable's expectation :class:`StateFn`. + Observable's expectation :class:`StateFn`, and, optionally, the expectation converter. Raises: AlgorithmError: If no operator has been provided. + AlgorithmError: If no expectation is passed and None could be inferred via the + ExpectationFactory. """ if operator is None: raise AlgorithmError("The operator was never provided.") - operator = self._check_operator(operator) + self._check_operator_ansatz(operator) - if isinstance(self.ansatz, QuantumCircuit): - param_dict = dict(zip(self._ansatz_params, parameter)) # type: Dict - wave_function = self.ansatz.assign_parameters(param_dict) + # if expectation was never created, try to create one + if self.expectation is None: + expectation = ExpectationFactory.build( + operator=operator, + backend=self.quantum_instance, + include_custom=self._include_custom, + ) else: - wave_function = self.ansatz.construct_circuit(parameter) - - # Expectation was never created , try to create one - if self._expectation is None: - self._try_set_expectation_value_from_factory(operator) + expectation = self.expectation - # If setting the expectation failed, raise an Error: - if self._expectation is None: - raise AlgorithmError( - "No expectation set and could not automatically set one, please " - "try explicitly setting an expectation or specify a backend so it " - "can be chosen automatically." - ) + param_dict = dict(zip(self._ansatz_params, parameter)) # type: Dict + wave_function = self.ansatz.assign_parameters(param_dict) - observable_meas = self.expectation.convert(StateFn(operator, is_measurement=True)) + observable_meas = expectation.convert(StateFn(operator, is_measurement=True)) ansatz_circuit_op = CircuitStateFn(wave_function) - return observable_meas.compose(ansatz_circuit_op).reduce() + expect_op = observable_meas.compose(ansatz_circuit_op).reduce() + + if return_expectation: + return expect_op, expectation + + return expect_op def construct_circuit( self, @@ -354,36 +408,32 @@ def extract_circuits(op): def supports_aux_operators(cls) -> bool: return True - def _eval_aux_ops(self, aux_operators: List[OperatorBase], threshold: float = 1e-12) -> None: + def _eval_aux_ops( + self, + parameters: np.ndarray, + aux_operators: List[OperatorBase], + expectation: ExpectationBase, + threshold: float = 1e-12, + ) -> np.ndarray: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) - aux_op_meas = self.expectation.convert(StateFn(ListOp(aux_operators), is_measurement=True)) - aux_op_expect = aux_op_meas.compose(CircuitStateFn(self.get_optimal_circuit())) + aux_op_meas = expectation.convert(StateFn(ListOp(aux_operators), is_measurement=True)) + aux_op_expect = aux_op_meas.compose(CircuitStateFn(self.ansatz.bind_parameters(parameters))) values = np.real(sampler.convert(aux_op_expect).eval()) # Discard values below threshold aux_op_results = values * (np.abs(values) > threshold) # Deal with the aux_op behavior where there can be Nones or Zero qubit Paulis in the list _aux_op_nones = [op is None for op in aux_operators] - self._ret.aux_operator_eigenvalues = [ + aux_operator_eigenvalues = [ None if is_none else [result] for (is_none, result) in zip(_aux_op_nones, aux_op_results) ] # As this has mixed types, since it can included None, it needs to explicitly pass object # data type to avoid numpy 1.19 warning message about implicit conversion being deprecated - self._ret.aux_operator_eigenvalues = np.array( - [self._ret.aux_operator_eigenvalues], dtype=object - ) - - def _check_operator(self, operator: OperatorBase) -> OperatorBase: - """set operator""" - self._expect_op = None - self._check_operator_varform(operator) - # Expectation was not passed by user, try to create one - if not self._user_valid_expectation: - self._try_set_expectation_value_from_factory(operator) - return operator + aux_operator_eigenvalues = np.array([aux_operator_eigenvalues], dtype=object) + return aux_operator_eigenvalues def compute_minimum_eigenvalue( self, operator: OperatorBase, aux_operators: Optional[List[Optional[OperatorBase]]] = None @@ -392,13 +442,19 @@ def compute_minimum_eigenvalue( if self.quantum_instance is None: raise AlgorithmError( - "A QuantumInstance or Backend " "must be supplied to run the quantum algorithm." + "A QuantumInstance or Backend must be supplied to run the quantum algorithm." ) + self.quantum_instance.circuit_summary = True - if operator is None: - raise AlgorithmError("The operator was never provided.") + # this sets the size of the ansatz, so it must be called before the initial point + # validation + self._check_operator_ansatz(operator) + + # set an expectation for this algorithm run (will be reset to None at the end) + initial_point = _validate_initial_point(self.initial_point, self.ansatz) + + bounds = _validate_bounds(self.ansatz) - operator = self._check_operator(operator) # We need to handle the array entries being Optional i.e. having value None if aux_operators: zero_op = I.tensorpower(operator.num_qubits) * 0.0 @@ -414,103 +470,129 @@ def compute_minimum_eigenvalue( else: aux_operators = None - self._quantum_instance.circuit_summary = True + # Convert the gradient operator into a callable function that is compatible with the + # optimization routine. + if isinstance(self._gradient, GradientBase): + gradient = self._gradient.gradient_wrapper( + ~StateFn(operator) @ StateFn(self._ansatz), + bind_params=self._ansatz_params, + backend=self._quantum_instance, + ) + else: + gradient = self._gradient self._eval_count = 0 + energy_evaluation, expectation = self.get_energy_evaluation( + operator, return_expectation=True + ) - # Convert the gradient operator into a callable function that is compatible with the - # optimization routine. - if self._gradient: - if isinstance(self._gradient, GradientBase): - self._gradient = self._gradient.gradient_wrapper( - ~StateFn(operator) @ StateFn(self._ansatz), - bind_params=self._ansatz_params, - backend=self._quantum_instance, - ) - if not self._expect_op: - self._expect_op = self.construct_expectation(self._ansatz_params, operator) - vqresult = self.find_minimum( - initial_point=self.initial_point, - ansatz=self.ansatz, - cost_fn=self._energy_evaluation, - gradient_fn=self._gradient, - optimizer=self.optimizer, + start_time = time() + opt_params, opt_value, nfev = self.optimizer.optimize( + num_vars=len(initial_point), + objective_function=energy_evaluation, + gradient_function=gradient, + variable_bounds=bounds, + initial_point=initial_point, ) + eval_time = time() - start_time - self._ret = VQEResult() - self._ret.combine(vqresult) + result = VQEResult() + result.optimal_point = opt_params + result.optimal_parameters = dict(zip(self._ansatz_params, opt_params)) + result.optimal_value = opt_value + result.cost_function_evals = nfev + result.optimizer_time = eval_time + result.eigenvalue = opt_value + 0j + result.eigenstate = self._get_eigenstate(result.optimal_parameters) - if vqresult.optimizer_evals is not None and self._eval_count >= vqresult.optimizer_evals: - self._eval_count = vqresult.optimizer_evals - self._eval_time = vqresult.optimizer_time logger.info( "Optimization complete in %s seconds.\nFound opt_params %s in %s evals", - self._eval_time, - vqresult.optimal_point, + eval_time, + result.optimal_point, self._eval_count, ) - self._ret.eigenvalue = vqresult.optimal_value + 0j - self._ret.eigenstate = self.get_optimal_vector() - self._ret.eigenvalue = self.get_optimal_cost() - if aux_operators: - self._eval_aux_ops(aux_operators) - self._ret.aux_operator_eigenvalues = self._ret.aux_operator_eigenvalues[0] + # TODO delete as soon as get_optimal_vector etc are removed + self._ret = result - self._ret.cost_function_evals = self._eval_count + if aux_operators is not None: + aux_values = self._eval_aux_ops(opt_params, aux_operators, expectation=expectation) + result.aux_operator_eigenvalues = aux_values[0] - return self._ret + return result - def _energy_evaluation( - self, parameters: Union[List[float], np.ndarray] - ) -> Union[float, List[float]]: - """Evaluate energy at given parameters for the ansatz. + def get_energy_evaluation( + self, + operator: OperatorBase, + return_expectation: bool = False, + ) -> Callable[[np.ndarray], Union[float, List[float]]]: + """Returns a function handle to evaluates the energy at given parameters for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. Args: - parameters: The parameters for the ansatz. + operator: The operator whose energy 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: - Energy of the hamiltonian of each parameter. + Returns: + Energy of the hamiltonian of each parameter, and, optionally, the expectation + converter. Raises: - RuntimeError: If the ansatz has no parameters. + RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters). + """ num_parameters = self.ansatz.num_parameters - if self._ansatz.num_parameters == 0: - raise RuntimeError("The ansatz cannot have 0 parameters.") + if num_parameters == 0: + raise RuntimeError("The ansatz must be parameterized, but has 0 free parameters.") - parameter_sets = np.reshape(parameters, (-1, num_parameters)) - # Create dict associating each parameter with the lists of parameterization values for it - param_bindings = dict( - zip(self._ansatz_params, parameter_sets.transpose().tolist()) - ) # type: Dict + expect_op, expectation = self.construct_expectation( + self._ansatz_params, operator, return_expectation=True + ) - start_time = time() - sampled_expect_op = self._circuit_sampler.convert(self._expect_op, params=param_bindings) - means = np.real(sampled_expect_op.eval()) - - if self._callback is not None: - variance = np.real(self._expectation.compute_variance(sampled_expect_op)) - estimator_error = np.sqrt(variance / self.quantum_instance.run_config.shots) - for i, param_set in enumerate(parameter_sets): - self._eval_count += 1 - self._callback(self._eval_count, param_set, means[i], estimator_error[i]) - else: - self._eval_count += len(means) + def energy_evaluation(parameters): + parameter_sets = np.reshape(parameters, (-1, num_parameters)) + # Create dict associating each parameter with the lists of parameterization values for it + param_bindings = dict(zip(self._ansatz_params, parameter_sets.transpose().tolist())) + + start_time = time() + sampled_expect_op = self._circuit_sampler.convert(expect_op, params=param_bindings) + means = np.real(sampled_expect_op.eval()) + + if self._callback is not None: + variance = np.real(expectation.compute_variance(sampled_expect_op)) + estimator_error = np.sqrt(variance / self.quantum_instance.run_config.shots) + for i, param_set in enumerate(parameter_sets): + self._eval_count += 1 + self._callback(self._eval_count, param_set, means[i], estimator_error[i]) + else: + self._eval_count += len(means) + + end_time = time() + logger.info( + "Energy evaluation returned %s - %.5f (ms), eval count: %s", + means, + (end_time - start_time) * 1000, + self._eval_count, + ) - end_time = time() - logger.info( - "Energy evaluation returned %s - %.5f (ms), eval count: %s", - means, - (end_time - start_time) * 1000, - self._eval_count, - ) + return means if len(means) > 1 else means[0] + + if return_expectation: + return energy_evaluation, expectation - return means if len(means) > 1 else means[0] + return energy_evaluation + @deprecate_function( + """ +The VQE.get_optimal_cost method is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate. +This information is part of the returned result object and can be +queried as VQEResult.eigenvalue.""" + ) def get_optimal_cost(self) -> float: """Get the minimal cost or energy found by the VQE.""" if self._ret.optimal_point is None: @@ -519,6 +601,13 @@ def get_optimal_cost(self) -> float: ) return self._ret.optimal_value + @deprecate_function( + """ +The VQE.get_optimal_circuit method is deprecated as of Qiskit Terra +0.18.0 and will be removed no sooner than 3 months after the releasedate. +This information is part of the returned result object and can be +queried as VQEResult.ansatz.bind_parameters(VQEResult.optimal_point).""" + ) def get_optimal_circuit(self) -> QuantumCircuit: """Get the circuit with the optimal parameters.""" if self._ret.optimal_point is None: @@ -528,32 +617,40 @@ def get_optimal_circuit(self) -> QuantumCircuit: ) return self.ansatz.assign_parameters(self._ret.optimal_parameters) + @deprecate_function( + """ +The VQE.get_optimal_vector method is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate. +This information is part of the returned result object and can be +queried as VQEResult.eigenvector.""" + ) def get_optimal_vector(self) -> Union[List[float], Dict[str, int]]: """Get the simulation outcome of the optimal circuit.""" - from qiskit.utils.run_circuits import find_regs_by_name - - if self._ret.optimal_point is None: + if self._ret.optimal_parameters is None: raise AlgorithmError( - "Cannot find optimal vector before running the " "algorithm to find optimal params." + "Cannot find optimal circuit before running the " + "algorithm to find optimal vector." ) - qc = self.get_optimal_circuit() - min_vector = {} - if self._quantum_instance.is_statevector: - ret = self._quantum_instance.execute(qc) - min_vector = ret.get_statevector(qc) + return self._get_eigenstate(self._ret.optimal_parameters) + + def _get_eigenstate(self, optimal_parameters) -> Union[List[float], Dict[str, int]]: + """Get the simulation outcome of the ansatz, provided with parameters.""" + optimal_circuit = self.ansatz.bind_parameters(optimal_parameters) + state_fn = self._circuit_sampler.convert(StateFn(optimal_circuit)).eval() + if self.quantum_instance.is_statevector: + state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array else: - c = ClassicalRegister(qc.width(), name="c") - q = find_regs_by_name(qc, "q") - qc.add_register(c) - qc.barrier(q) - qc.measure(q, c) - ret = self._quantum_instance.execute(qc) - counts = ret.get_counts(qc) - # normalize, just as done in CircuitSampler.sample_circuits - shots = self._quantum_instance._run_config.shots - min_vector = {b: (v / shots) ** 0.5 for (b, v) in counts.items()} - return min_vector + state = state_fn.to_dict_fn().primitive # SparseVectorStateFn -> DictStateFn -> dict + return state + + @deprecate_function( + """ +The VQE.optimal_params property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate. +This information is part of the returned result object and can be +queried as VQEResult.optimal_point.""" + ) @property def optimal_params(self) -> np.ndarray: """The optimal parameters for the ansatz.""" @@ -578,3 +675,60 @@ def cost_function_evals(self) -> Optional[int]: def cost_function_evals(self, value: int) -> None: """Sets number of cost function evaluations""" self._cost_function_evals = value + + @property + def eigenstate(self) -> Optional[np.ndarray]: + """return eigen state""" + return self._eigenstate + + @eigenstate.setter + def eigenstate(self, value: np.ndarray) -> None: + """set eigen state""" + self._eigenstate = 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/qiskit_algorithms/variational_algorithm.py b/qiskit_algorithms/variational_algorithm.py index 168d42d0..a10eb5de 100644 --- a/qiskit_algorithms/variational_algorithm.py +++ b/qiskit_algorithms/variational_algorithm.py @@ -26,6 +26,7 @@ (``qiskit.utils.algorithm_globals.random_seed = seed``). """ +import warnings from typing import Optional, Callable, Union, Dict import time import logging @@ -36,7 +37,7 @@ from qiskit.providers import BaseBackend from qiskit.providers import Backend from qiskit.opflow.gradients import GradientBase -from qiskit.utils import QuantumInstance, algorithm_globals +from qiskit.utils import QuantumInstance, algorithm_globals, deprecate_function from .algorithm_result import AlgorithmResult from .optimizers import Optimizer, SLSQP @@ -48,8 +49,8 @@ class VariationalAlgorithm: def __init__( self, - ansatz: QuantumCircuit, - optimizer: Optimizer, + ansatz: Optional[QuantumCircuit] = None, + optimizer: Optional[Optimizer] = None, cost_fn: Optional[Callable] = None, gradient: Optional[Union[GradientBase, Callable]] = None, initial_point: Optional[np.ndarray] = None, @@ -69,6 +70,19 @@ def __init__( Raises: ValueError: for invalid input """ + if any( + arg is not None + for arg in [ansatz, optimizer, cost_fn, gradient, initial_point, quantum_instance] + ): + warnings.warn( + "The VariationalAlgorithm class has been reduced to an abstract " + "interface. Passing any arguments to the initializer is deprecated as of " + "Qiskit Terra 0.18.0 and will be unsupported no sooner than 3 months " + "after the release date.", + DeprecationWarning, + stacklevel=2, + ) + self._quantum_instance = None if quantum_instance: self.quantum_instance = quantum_instance @@ -89,11 +103,23 @@ def __init__( self._parameterized_circuits = None @property + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +quantum_instance property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def quantum_instance(self) -> Optional[QuantumInstance]: """Returns quantum instance.""" return self._quantum_instance @quantum_instance.setter + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +quantum_instance property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def quantum_instance( self, quantum_instance: Union[QuantumInstance, BaseBackend, Backend] ) -> None: @@ -103,11 +129,23 @@ def quantum_instance( self._quantum_instance = quantum_instance @property + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +ansatz property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def ansatz(self) -> Optional[QuantumCircuit]: """Returns the ansatz""" return self._ansatz @ansatz.setter + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +ansatz property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def ansatz(self, ansatz: Optional[QuantumCircuit]): """Sets the ansatz""" if isinstance(ansatz, QuantumCircuit): @@ -121,25 +159,59 @@ def ansatz(self, ansatz: Optional[QuantumCircuit]): raise ValueError('Unsupported type "{}" of ansatz'.format(type(ansatz))) @property + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +optimizer property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def optimizer(self) -> Optional[Optimizer]: """Returns optimizer""" return self._optimizer @optimizer.setter + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +optimizer property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def optimizer(self, optimizer: Optimizer): """Sets optimizer""" self._optimizer = optimizer @property + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. The +initial_point property will be made abstract no sooner than +3 months after the release date of Qiskit Terra 0.18.0. You should +make a concrete implementation in any derived class and not rely on +the implementation here which will be removed.""" + ) def initial_point(self) -> Optional[np.ndarray]: """Returns initial point""" return self._initial_point @initial_point.setter + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. The +initial_point property will be made abstract no sooner than +3 months after the release date of Qiskit Terra 0.18.0. You should +make a concrete implementation in any derived class and not rely on +the implementation here which will be removed.""" + ) def initial_point(self, initial_point: np.ndarray): """Sets initial point""" self._initial_point = initial_point + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +find_minimum method is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def find_minimum( self, initial_point: Optional[np.ndarray] = None, @@ -244,6 +316,12 @@ def find_minimum( return result + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +get_prob_vector_for_params method is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def get_prob_vector_for_params( self, construct_circuit_fn, params_s, quantum_instance, construct_circuit_args=None ): @@ -265,6 +343,12 @@ def get_prob_vector_for_params( probs_s.append(self.get_probabilities_for_counts(counts)) return np.array(probs_s) + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +get_probabilities_for_counts method is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def get_probabilities_for_counts(self, counts): """get probabilities for counts""" shots = sum(counts.values()) @@ -295,6 +379,12 @@ def optimal_params(self): """returns optimal parameters""" raise NotImplementedError() + @deprecate_function( + """ +The VariationalAlgorithm is reduced to an interface. Thus, the +cleanup_paramterized_circuits method is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) def cleanup_parameterized_circuits(self): """set parameterized circuits to None""" self._parameterized_circuits = None diff --git a/test/test_vqe.py b/test/test_vqe.py index d1304d52..c7fa92b9 100644 --- a/test/test_vqe.py +++ b/test/test_vqe.py @@ -35,7 +35,6 @@ from qiskit.exceptions import MissingOptionalLibraryError from qiskit.opflow import ( AerPauliExpectation, - ExpectationBase, Gradient, I, MatrixExpectation, @@ -159,12 +158,14 @@ def test_missing_varform_params(self): @unpack def test_max_evals_grouped(self, optimizer, places, max_evals_grouped): """VQE Optimizers test""" - vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=optimizer, - max_evals_grouped=max_evals_grouped, - quantum_instance=self.statevector_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=self.ryrz_wavefunction, + optimizer=optimizer, + max_evals_grouped=max_evals_grouped, + quantum_instance=self.statevector_simulator, + sort_parameters_by_name=True, + ) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=places) @@ -206,7 +207,9 @@ def test_qasm_aux_operators_normalized(self): zip(sorted(wavefunction.parameters, key=lambda p: p.name), opt_params) ) - optimal_vector = vqe.get_optimal_vector() + with self.assertWarns(DeprecationWarning): + optimal_vector = vqe.get_optimal_vector() + self.assertAlmostEqual(sum([v ** 2 for v in optimal_vector.values()]), 1.0, places=4) @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") @@ -378,6 +381,7 @@ def test_reuse(self): with self.assertRaises(AlgorithmError): _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + vqe.expectation = MatrixExpectation() vqe.quantum_instance = self.statevector_simulator with self.subTest(msg="assert VQE works once all info is available"): result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) @@ -409,26 +413,6 @@ def run_check(): vqe.optimizer = L_BFGS_B() run_check() - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") - def test_vqe_expectation_select(self): - """Test expectation selection with Aer's qasm_simulator.""" - backend = Aer.get_backend("aer_simulator") - - with self.subTest("Defaults"): - vqe = VQE(quantum_instance=backend) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertIsInstance(vqe.expectation, PauliExpectation) - - with self.subTest("Include custom"): - vqe = VQE(include_custom=True, quantum_instance=backend) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertIsInstance(vqe.expectation, AerPauliExpectation) - - with self.subTest("Set explicitly"): - vqe = VQE(expectation=AerPauliExpectation(), quantum_instance=backend) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertIsInstance(vqe.expectation, AerPauliExpectation) - @data(MatrixExpectation(), None) def test_backend_change(self, user_expectation): """Test that VQE works when backend changes.""" @@ -442,22 +426,15 @@ def test_backend_change(self, user_expectation): if user_expectation is not None: with self.subTest("User expectation kept."): self.assertEqual(vqe.expectation, user_expectation) - else: - with self.subTest("Expectation created."): - self.assertIsInstance(vqe.expectation, ExpectationBase) - try: - vqe.quantum_instance = BasicAer.get_backend("qasm_simulator") - except Exception as ex: # pylint: disable=broad-except - self.fail("Failed to change backend. Error: '{}'".format(str(ex))) - return + vqe.quantum_instance = BasicAer.get_backend("qasm_simulator") + + # works also if no expectation is set, since it will be determined automatically result1 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + if user_expectation is not None: with self.subTest("Change backend with user expectation, it is kept."): self.assertEqual(vqe.expectation, user_expectation) - else: - with self.subTest("Change backend without user expectation, one created."): - self.assertIsInstance(vqe.expectation, ExpectationBase) with self.subTest("Check results."): self.assertEqual(len(result0.optimal_point), len(result1.optimal_point))