From 2eec21a08a02f9fd53a58ba266a100758052c436 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 13 May 2021 22:02:36 +0200 Subject: [PATCH 01/19] make VQE stateless TODO deprecation --- .../algorithms/minimum_eigen_solvers/qaoa.py | 4 +- .../algorithms/minimum_eigen_solvers/vqe.py | 336 +++++++++++------- qiskit/algorithms/variational_algorithm.py | 13 +- 3 files changed, 215 insertions(+), 138 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/qaoa.py b/qiskit/algorithms/minimum_eigen_solvers/qaoa.py index 37085b3c4085..43c832fb0467 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_varform(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 1a6d08dc47ef..4b1b43ecac8e 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -38,7 +38,7 @@ 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 import QuantumInstance, algorithm_globals from ..optimizers import Optimizer, SLSQP from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigen_solver import MinimumEigensolver, MinimumEigensolverResult @@ -147,57 +147,80 @@ def __init__( if optimizer is None: optimizer = SLSQP() + if quantum_instance is not None: + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + # 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 + 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() + 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 = None + self._ret = None 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( + self._expectation = ExpectationFactory.build( operator=operator, backend=self.quantum_instance, include_custom=self._include_custom, ) - ) - 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""" + if isinstance(ansatz, QuantumCircuit): + # store the parameters + self._ansatz_params = sorted(ansatz.parameters, key=lambda p: p.name) + self._ansatz = ansatz + elif ansatz is None: + self._ansatz_params = None + self._ansatz = ansatz + else: + raise ValueError('Unsupported type "{}" of ansatz'.format(type(ansatz))) - @VariationalAlgorithm.quantum_instance.setter + @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 @@ -208,8 +231,7 @@ def expectation(self) -> ExpectationBase: @expectation.setter def expectation(self, exp: ExpectationBase) -> None: - self._set_expectation(exp) - self._user_valid_expectation = self._expectation is not None + self._expectation = exp def _check_operator_varform(self, operator: OperatorBase): """Check that the number of qubits of operator and ansatz match.""" @@ -226,10 +248,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) @@ -296,20 +323,17 @@ def construct_expectation( if operator is None: raise AlgorithmError("The operator was never provided.") - operator = self._check_operator(operator) + self._check_operator_varform(operator) - if isinstance(self.ansatz, QuantumCircuit): - param_dict = dict(zip(self._ansatz_params, parameter)) # type: Dict - wave_function = self.ansatz.assign_parameters(param_dict) - else: - wave_function = self.ansatz.construct_circuit(parameter) + param_dict = dict(zip(self._ansatz_params, parameter)) # type: Dict + wave_function = self.ansatz.assign_parameters(param_dict) # Expectation was never created , try to create one - if self._expectation is None: + if self.expectation is None: self._try_set_expectation_value_from_factory(operator) # If setting the expectation failed, raise an Error: - if self._expectation is None: + 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 " @@ -354,7 +378,7 @@ 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, aux_operators: List[OperatorBase], threshold: float = 1e-12) -> np.ndarray: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) @@ -366,24 +390,16 @@ def _eval_aux_ops(self, aux_operators: List[OperatorBase], threshold: float = 1e 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 + aux_operator_eigenvalues = np.array( + [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 + return aux_operator_eigenvalues def compute_minimum_eigenvalue( self, operator: OperatorBase, aux_operators: Optional[List[Optional[OperatorBase]]] = None @@ -392,13 +408,17 @@ 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_varform(operator) + + initial_point = _validate_initial_point(self.initial_point, + 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,67 +434,68 @@ 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 = self.get_energy_evaluation(operator) - # 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, + 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.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 + # TODO 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 + # self._ret.eigenstate = self.get_optimal_vector() + if aux_operators is not None: + aux_values = self._eval_aux_ops(aux_operators) + result.aux_operator_eigenvalues = aux_values[0] - self._ret.cost_function_evals = self._eval_count + # TODO delete as soon as get_optimal_vector etc are removed + self._ret = result + return result - return self._ret - - def _energy_evaluation( - self, parameters: Union[List[float], np.ndarray] - ) -> Union[float, List[float]]: + def get_energy_evaluation( + self, operator: OperatorBase + ) -> Callable[[np.ndarray], Union[float, List[float]]]: """Evaluate 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. Returns: Energy of the hamiltonian of each parameter. - Raises: RuntimeError: If the ansatz has no parameters. """ @@ -482,34 +503,39 @@ def _energy_evaluation( if self._ansatz.num_parameters == 0: raise RuntimeError("The ansatz cannot have 0 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 = self.construct_expectation(self._ansatz_params, operator) - 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()) + ) - end_time = time() - logger.info( - "Energy evaluation returned %s - %.5f (ms), eval count: %s", - means, - (end_time - start_time) * 1000, - self._eval_count, - ) + 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(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) + + 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] + return means if len(means) > 1 else means[0] + + return energy_evaluation def get_optimal_cost(self) -> float: """Get the minimal cost or energy found by the VQE.""" @@ -530,28 +556,34 @@ def get_optimal_circuit(self) -> QuantumCircuit: 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() + 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 optimal circuit.""" + from qiskit.utils.run_circuits import find_regs_by_name + + optimal_circuit = self.ansatz.bind_parameters(optimal_parameters) min_vector = {} - if self._quantum_instance.is_statevector: - ret = self._quantum_instance.execute(qc) - min_vector = ret.get_statevector(qc) + if self.quantum_instance.is_statevector: + ret = self._quantum_instance.execute(optimal_circuit) + min_vector = ret.get_statevector(optimal_circuit) 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) + c = ClassicalRegister(optimal_circuit.width(), name="c") + q = find_regs_by_name(optimal_circuit, "q") + optimal_circuit.add_register(c) + optimal_circuit.barrier(q) + optimal_circuit.measure(q, c) + ret = self.quantum_instance.execute(optimal_circuit) + counts = ret.get_counts(optimal_circuit) # normalize, just as done in CircuitSampler.sample_circuits - shots = self._quantum_instance._run_config.shots + shots = self.quantum_instance._run_config.shots min_vector = {b: (v / shots) ** 0.5 for (b, v) in counts.items()} + return min_vector @property @@ -578,3 +610,41 @@ 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""" + if self._eigenstate is None: + self._eigenstate = self._get_optimal_vector() + 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 + + # 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 + default_bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size + bounds = getattr(ansatz, "parameter_bounds", default_bounds) + + # 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 diff --git a/qiskit/algorithms/variational_algorithm.py b/qiskit/algorithms/variational_algorithm.py index 20493b72e666..df66c947b356 100644 --- a/qiskit/algorithms/variational_algorithm.py +++ b/qiskit/algorithms/variational_algorithm.py @@ -20,6 +20,7 @@ overridden to opt-out of this infrastructure but still meet the interface requirements. """ +import warnings from typing import Optional, Callable, Union, Dict import time import logging @@ -42,8 +43,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, @@ -63,6 +64,14 @@ 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 From ce7e81db7cdf710da0e55f2bab3acdf43754f8aa Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 14 May 2021 22:21:39 +0200 Subject: [PATCH 02/19] fix get initial point from ansatz --- .../algorithms/minimum_eigen_solvers/vqe.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 4b1b43ecac8e..62a0e8c05f62 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -151,10 +151,6 @@ def __init__( if not isinstance(quantum_instance, QuantumInstance): quantum_instance = QuantumInstance(quantum_instance) - # 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 - super().__init__() self._max_evals_grouped = max_evals_grouped @@ -174,12 +170,14 @@ def __init__( self._eval_time = None self._eval_count = None - self._ret = None self._optimizer.set_max_evals_grouped(max_evals_grouped) self._callback = callback logger.info(self.print_settings()) + # TODO remove this once the stateful methods are deleted + self._ret = None + def _try_set_expectation_value_from_factory(self, operator: OperatorBase) -> None: if operator is not None and self.quantum_instance is not None: self._expectation = ExpectationFactory.build( @@ -629,19 +627,22 @@ def _validate_initial_point(point, ansatz): # 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 - default_bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size - bounds = getattr(ansatz, "parameter_bounds", default_bounds) - - # 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) + if hasattr(ansatz, "preferred_init_points"): + point = ansatz.preferred_init_points + else: + # get bounds if ansatz has them set, otherwise use [-2pi, 2pi] for each parameter + default_bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size + bounds = getattr(ansatz, "parameter_bounds", default_bounds) + + # 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 ' From d6d3d81cb5ccf26f5b02197c9d5503887c12b538 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Sun, 16 May 2021 17:45:21 +0200 Subject: [PATCH 03/19] try fixing use of deprecate_function --- .../algorithms/minimum_eigen_solvers/vqe.py | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 62a0e8c05f62..a4605de08143 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -38,6 +38,7 @@ 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.deprecation import deprecate_function from qiskit.utils import QuantumInstance, algorithm_globals from ..optimizers import Optimizer, SLSQP from ..variational_algorithm import VariationalAlgorithm, VariationalResult @@ -535,6 +536,11 @@ def energy_evaluation(parameters): 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: @@ -543,6 +549,11 @@ 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: @@ -552,6 +563,11 @@ def get_optimal_circuit(self) -> QuantumCircuit: ) return self.ansatz.assign_parameters(self._ret.optimal_parameters) + @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.eigenvector.""") def get_optimal_vector(self) -> Union[List[float], Dict[str, int]]: """Get the simulation outcome of the optimal circuit.""" if self._ret.optimal_parameters is None: @@ -584,6 +600,11 @@ def _get_eigenstate(self, optimal_parameters) -> Union[List[float], Dict[str, in return min_vector + @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) -> List[float]: """The optimal parameters for the ansatz.""" @@ -625,24 +646,26 @@ def eigenstate(self, value: np.ndarray) -> None: 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: - if hasattr(ansatz, "preferred_init_points"): - point = ansatz.preferred_init_points - else: - # get bounds if ansatz has them set, otherwise use [-2pi, 2pi] for each parameter - default_bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size - bounds = getattr(ansatz, "parameter_bounds", default_bounds) - - # 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) + # 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 ' From 01ba50ce3bcac6786b3511db717a5aad1b4b3a49 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 31 May 2021 17:41:30 +0200 Subject: [PATCH 04/19] fix tests & black --- .../algorithms/minimum_eigen_solvers/vqe.py | 72 ++++++++++++------- qiskit/algorithms/variational_algorithm.py | 19 +++-- test/python/algorithms/test_vqe.py | 4 +- 3 files changed, 63 insertions(+), 32 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index a4605de08143..58ed4714f1db 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -182,10 +182,10 @@ def __init__( def _try_set_expectation_value_from_factory(self, operator: OperatorBase) -> None: if operator is not None and self.quantum_instance is not None: self._expectation = ExpectationFactory.build( - operator=operator, - backend=self.quantum_instance, - include_custom=self._include_custom, - ) + operator=operator, + backend=self.quantum_instance, + include_custom=self._include_custom, + ) @property def ansatz(self) -> Optional[QuantumCircuit]: @@ -377,7 +377,9 @@ 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) -> np.ndarray: + def _eval_aux_ops( + self, aux_operators: List[OperatorBase], threshold: float = 1e-12 + ) -> np.ndarray: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) @@ -395,9 +397,7 @@ def _eval_aux_ops(self, aux_operators: List[OperatorBase], threshold: float = 1e ] # 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 - aux_operator_eigenvalues = np.array( - [aux_operator_eigenvalues], dtype=object - ) + aux_operator_eigenvalues = np.array([aux_operator_eigenvalues], dtype=object) return aux_operator_eigenvalues def compute_minimum_eigenvalue( @@ -415,8 +415,9 @@ def compute_minimum_eigenvalue( # validation self._check_operator_varform(operator) - initial_point = _validate_initial_point(self.initial_point, - self.ansatz) + initial_point = _validate_initial_point(self.initial_point, self.ansatz) + + bounds = _validate_bounds(self.ansatz) # We need to handle the array entries being Optional i.e. having value None if aux_operators: @@ -452,6 +453,7 @@ def compute_minimum_eigenvalue( num_vars=len(initial_point), objective_function=energy_evaluation, gradient_function=gradient, + variable_bounds=bounds, initial_point=initial_point, ) eval_time = time() - start_time @@ -507,9 +509,7 @@ def get_energy_evaluation( 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()) - ) + 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) @@ -536,11 +536,13 @@ def energy_evaluation(parameters): return energy_evaluation - @deprecate_function(""" + @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.""") +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: @@ -549,11 +551,13 @@ def get_optimal_cost(self) -> float: ) return self._ret.optimal_value - @deprecate_function(""" + @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).""") +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: @@ -563,11 +567,13 @@ def get_optimal_circuit(self) -> QuantumCircuit: ) return self.ansatz.assign_parameters(self._ret.optimal_parameters) - @deprecate_function(""" -The VQE.get_optimal_cost method is deprecated as of Qiskit Terra 0.18.0 + @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.""") +queried as VQEResult.eigenvector.""" + ) def get_optimal_vector(self) -> Union[List[float], Dict[str, int]]: """Get the simulation outcome of the optimal circuit.""" if self._ret.optimal_parameters is None: @@ -600,11 +606,13 @@ def _get_eigenstate(self, optimal_parameters) -> Union[List[float], Dict[str, in return min_vector - @deprecate_function(""" + @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.""") +queried as VQEResult.optimal_point.""" + ) @property def optimal_params(self) -> List[float]: """The optimal parameters for the ansatz.""" @@ -668,7 +676,23 @@ def _validate_initial_point(point, ansatz): 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}).') + 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 df66c947b356..1dfaa091b889 100644 --- a/qiskit/algorithms/variational_algorithm.py +++ b/qiskit/algorithms/variational_algorithm.py @@ -64,13 +64,18 @@ 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) + 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: diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py index d1304d520400..304400a99fdc 100644 --- a/test/python/algorithms/test_vqe.py +++ b/test/python/algorithms/test_vqe.py @@ -206,7 +206,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.") From eee0cd4efb0a28a0d062993c87ed0ee60acf355c Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 31 May 2021 21:45:24 +0200 Subject: [PATCH 05/19] deprecate varalgo methods --- qiskit/algorithms/variational_algorithm.py | 74 +++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/variational_algorithm.py b/qiskit/algorithms/variational_algorithm.py index 1dfaa091b889..929477d5246f 100644 --- a/qiskit/algorithms/variational_algorithm.py +++ b/qiskit/algorithms/variational_algorithm.py @@ -31,7 +31,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 @@ -97,11 +97,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: @@ -111,11 +123,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): @@ -129,25 +153,55 @@ 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. Thus, the +initial_point property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) 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. Thus, the +initial_point property is deprecated as of Qiskit Terra 0.18.0 +and will be removed no sooner than 3 months after the releasedate.""" + ) 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, @@ -251,6 +305,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 ): @@ -272,6 +332,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()) @@ -302,6 +368,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 From ffd6b6d1494e398d4ec9656cd5ba363bedcdfea0 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 31 May 2021 21:45:40 +0200 Subject: [PATCH 06/19] sort params per circuit default --- .../algorithms/minimum_eigen_solvers/vqe.py | 48 +++++++++++-------- test/python/algorithms/test_vqe.py | 1 + 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 58ed4714f1db..6e822a72112c 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -105,6 +105,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: bool = False, ) -> None: """ @@ -140,6 +141,11 @@ 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: 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 ansatz is None: @@ -159,6 +165,9 @@ def __init__( self._expectation = expectation self._include_custom = include_custom + # 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 @@ -166,6 +175,7 @@ def __init__( self._initial_point = initial_point self._gradient = gradient self._quantum_instance = None + if quantum_instance is not None: self.quantum_instance = quantum_instance @@ -189,21 +199,18 @@ def _try_set_expectation_value_from_factory(self, operator: OperatorBase) -> Non @property def ansatz(self) -> Optional[QuantumCircuit]: - """Returns the ansatz""" + """Returns the ansatz.""" return self._ansatz @ansatz.setter def ansatz(self, ansatz: Optional[QuantumCircuit]): """Sets the ansatz""" - if isinstance(ansatz, QuantumCircuit): - # store the parameters - self._ansatz_params = sorted(ansatz.parameters, key=lambda p: p.name) - self._ansatz = ansatz - elif ansatz is None: - self._ansatz_params = None - self._ansatz = ansatz - else: - raise ValueError('Unsupported type "{}" of ansatz'.format(type(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) @property def quantum_instance(self) -> Optional[QuantumInstance]: @@ -222,6 +229,16 @@ def quantum_instance( quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend) ) + @property + 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) -> ExpectationBase: """The expectation value algorithm used to construct the expectation measurement from @@ -286,12 +303,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" @@ -466,7 +479,6 @@ def compute_minimum_eigenvalue( result.eigenvalue = opt_value + 0j result.eigenstate = self._get_eigenstate(result.optimal_parameters) - # TODO logger.info( "Optimization complete in %s seconds.\nFound opt_params %s in %s evals", eval_time, @@ -474,8 +486,6 @@ def compute_minimum_eigenvalue( self._eval_count, ) - # TODO - # self._ret.eigenstate = self.get_optimal_vector() if aux_operators is not None: aux_values = self._eval_aux_ops(aux_operators) result.aux_operator_eigenvalues = aux_values[0] diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py index 304400a99fdc..347137632519 100644 --- a/test/python/algorithms/test_vqe.py +++ b/test/python/algorithms/test_vqe.py @@ -164,6 +164,7 @@ def test_max_evals_grouped(self, optimizer, places, max_evals_grouped): 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) From 480d2a3f551e7e7717cfc4b9494b582b2d897bb8 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 3 Jun 2021 13:40:32 +0200 Subject: [PATCH 07/19] clarify deprecation message in varalgo.initialpoint --- qiskit/algorithms/variational_algorithm.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/variational_algorithm.py b/qiskit/algorithms/variational_algorithm.py index 929477d5246f..b6c9876e207b 100644 --- a/qiskit/algorithms/variational_algorithm.py +++ b/qiskit/algorithms/variational_algorithm.py @@ -177,9 +177,11 @@ def optimizer(self, optimizer: Optimizer): @property @deprecate_function( """ -The VariationalAlgorithm is reduced to an interface. Thus, the -initial_point property is deprecated as of Qiskit Terra 0.18.0 -and will be removed no sooner than 3 months after the releasedate.""" +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""" @@ -188,9 +190,11 @@ def initial_point(self) -> Optional[np.ndarray]: @initial_point.setter @deprecate_function( """ -The VariationalAlgorithm is reduced to an interface. Thus, the -initial_point property is deprecated as of Qiskit Terra 0.18.0 -and will be removed no sooner than 3 months after the releasedate.""" +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""" From 42993471965ef1b2aabe557bb8b84c4ececa2822 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 3 Jun 2021 13:58:24 +0200 Subject: [PATCH 08/19] 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 --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 6e822a72112c..60ba17dd2b50 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -180,7 +180,7 @@ def __init__( self.quantum_instance = quantum_instance self._eval_time = None - self._eval_count = None + self._eval_count = 0 self._optimizer.set_max_evals_grouped(max_evals_grouped) self._callback = callback From 06deb86162d1d1b82f6689c70cdd8be6146d5e71 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 3 Jun 2021 18:54:49 +0200 Subject: [PATCH 09/19] simplify get_eigenstate --- .../algorithms/minimum_eigen_solvers/vqe.py | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 60ba17dd2b50..3b85257a4936 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -391,13 +391,13 @@ def supports_aux_operators(cls) -> bool: return True def _eval_aux_ops( - self, aux_operators: List[OperatorBase], threshold: float = 1e-12 + self, parameters: np.ndarray, aux_operators: List[OperatorBase], 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_expect = aux_op_meas.compose(CircuitStateFn(self.ansatz.bind_parameters(parameters))) values = np.real(sampler.convert(aux_op_expect).eval()) # Discard values below threshold @@ -474,6 +474,7 @@ def compute_minimum_eigenvalue( 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 @@ -486,12 +487,13 @@ def compute_minimum_eigenvalue( self._eval_count, ) + # TODO delete as soon as get_optimal_vector etc are removed + self._ret = result + if aux_operators is not None: - aux_values = self._eval_aux_ops(aux_operators) + aux_values = self._eval_aux_ops(opt_params, aux_operators) result.aux_operator_eigenvalues = aux_values[0] - # TODO delete as soon as get_optimal_vector etc are removed - self._ret = result return result def get_energy_evaluation( @@ -594,27 +596,15 @@ def get_optimal_vector(self) -> Union[List[float], Dict[str, int]]: 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 optimal circuit.""" - from qiskit.utils.run_circuits import find_regs_by_name - + """Get the simulation outcome of the ansatz, provided with parameters.""" optimal_circuit = self.ansatz.bind_parameters(optimal_parameters) - min_vector = {} + state_fn = self._circuit_sampler.convert(StateFn(optimal_circuit)).eval() if self.quantum_instance.is_statevector: - ret = self._quantum_instance.execute(optimal_circuit) - min_vector = ret.get_statevector(optimal_circuit) + state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array else: - c = ClassicalRegister(optimal_circuit.width(), name="c") - q = find_regs_by_name(optimal_circuit, "q") - optimal_circuit.add_register(c) - optimal_circuit.barrier(q) - optimal_circuit.measure(q, c) - ret = self.quantum_instance.execute(optimal_circuit) - counts = ret.get_counts(optimal_circuit) - # 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( """ @@ -651,8 +641,6 @@ def cost_function_evals(self, value: int) -> None: @property def eigenstate(self) -> Optional[np.ndarray]: """return eigen state""" - if self._eigenstate is None: - self._eigenstate = self._get_optimal_vector() return self._eigenstate @eigenstate.setter From 070eb77086d4e630dfca7ef661e0aeb23911a795 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 9 Jun 2021 14:39:24 +0200 Subject: [PATCH 10/19] fix lint --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 3b85257a4936..feb902c0fb66 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -20,8 +20,7 @@ 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 From a9b431b870b28cf39639b4d5f6147390cb645ff1 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 9 Jun 2021 15:34:28 +0200 Subject: [PATCH 11/19] remote note on parameter order --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index feb902c0fb66..abd51c645cfb 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -83,14 +83,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__( From b6dcb5d770b9ae007205fbd4a4937412c579c287 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 10 Jun 2021 14:52:49 +0200 Subject: [PATCH 12/19] review suggestions * deprecate sort_parameters_by_name * check ansatz has 0 params in setter * rename varform -> ansatz --- .../algorithms/minimum_eigen_solvers/qaoa.py | 2 +- .../algorithms/minimum_eigen_solvers/vqe.py | 59 +++++++++++++------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/qaoa.py b/qiskit/algorithms/minimum_eigen_solvers/qaoa.py index 43c832fb0467..18689aab5fe0 100644 --- a/qiskit/algorithms/minimum_eigen_solvers/qaoa.py +++ b/qiskit/algorithms/minimum_eigen_solvers/qaoa.py @@ -125,7 +125,7 @@ def __init__( quantum_instance=quantum_instance, ) - def _check_operator_varform(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( diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index abd51c645cfb..71132cce5e16 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -17,6 +17,7 @@ from typing import Optional, List, Callable, Union, Dict import logging +import warnings from time import time import numpy as np @@ -96,7 +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: bool = False, + sort_parameters_by_name: Optional[bool] = None, ) -> None: """ @@ -132,13 +133,23 @@ 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: 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 + 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() @@ -195,14 +206,34 @@ def ansatz(self) -> Optional[QuantumCircuit]: @ansatz.setter def ansatz(self, ansatz: Optional[QuantumCircuit]): - """Sets the ansatz""" + """Sets the ansatz. + + Args: + ansatz: The parameterized circuit used as an ansatz. + + Raises: + ValueError: If the circuit is not parameterized (i.e. has 0 free parameters). + """ self._ansatz = ansatz if ansatz is not None: + if ansatz.num_parameters == 0: + raise ValueError("The ansatz must be parameterized, but has 0 free parameters.") + if self._sort_parameters_by_name: self._ansatz_params = sorted(ansatz.parameters, key=lambda p: p.name) else: self._ansatz_params = list(ansatz.parameters) + @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.""" @@ -240,7 +271,7 @@ def expectation(self) -> ExpectationBase: def expectation(self, exp: 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: @@ -326,7 +357,7 @@ def construct_expectation( if operator is None: raise AlgorithmError("The operator was never provided.") - self._check_operator_varform(operator) + self._check_operator_ansatz(operator) param_dict = dict(zip(self._ansatz_params, parameter)) # type: Dict wave_function = self.ansatz.assign_parameters(param_dict) @@ -417,7 +448,7 @@ def compute_minimum_eigenvalue( # this sets the size of the ansatz, so it must be called before the initial point # validation - self._check_operator_varform(operator) + self._check_operator_ansatz(operator) initial_point = _validate_initial_point(self.initial_point, self.ansatz) @@ -490,7 +521,7 @@ def compute_minimum_eigenvalue( def get_energy_evaluation( self, operator: OperatorBase ) -> Callable[[np.ndarray], Union[float, List[float]]]: - """Evaluate energy at given parameters for the ansatz. + """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. @@ -499,15 +530,9 @@ def get_energy_evaluation( Returns: Energy of the hamiltonian of each parameter. - - Raises: - RuntimeError: If the ansatz has no parameters. """ - num_parameters = self.ansatz.num_parameters - if self._ansatz.num_parameters == 0: - raise RuntimeError("The ansatz cannot have 0 parameters.") - expect_op = self.construct_expectation(self._ansatz_params, operator) + num_parameters = self.ansatz.num_parameters def energy_evaluation(parameters): parameter_sets = np.reshape(parameters, (-1, num_parameters)) From 62b2e99df74cc5c42c43b3fd018fa6eca35d6733 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 10 Jun 2021 17:11:58 +0200 Subject: [PATCH 13/19] fix tests --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 71132cce5e16..6360cb4aa566 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -211,14 +211,9 @@ def ansatz(self, ansatz: Optional[QuantumCircuit]): Args: ansatz: The parameterized circuit used as an ansatz. - Raises: - ValueError: If the circuit is not parameterized (i.e. has 0 free parameters). """ self._ansatz = ansatz if ansatz is not None: - if ansatz.num_parameters == 0: - raise ValueError("The ansatz must be parameterized, but has 0 free parameters.") - if self._sort_parameters_by_name: self._ansatz_params = sorted(ansatz.parameters, key=lambda p: p.name) else: @@ -530,9 +525,16 @@ def get_energy_evaluation( Returns: Energy of the hamiltonian of each parameter. + + Raises: + RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters). + """ - expect_op = self.construct_expectation(self._ansatz_params, operator) num_parameters = self.ansatz.num_parameters + if num_parameters == 0: + raise ValueError("The ansatz must be parameterized, but has 0 free parameters.") + + expect_op = self.construct_expectation(self._ansatz_params, operator) def energy_evaluation(parameters): parameter_sets = np.reshape(parameters, (-1, num_parameters)) From 2284c314c85be421c49e56ac9f8a243d22a98d57 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 10 Jun 2021 17:12:45 +0200 Subject: [PATCH 14/19] lint --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 6360cb4aa566..4e8ef476b695 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -532,7 +532,7 @@ def get_energy_evaluation( """ num_parameters = self.ansatz.num_parameters if num_parameters == 0: - raise ValueError("The ansatz must be parameterized, but has 0 free parameters.") + raise RuntimeError("The ansatz must be parameterized, but has 0 free parameters.") expect_op = self.construct_expectation(self._ansatz_params, operator) From a4c05ab0ba055b6baf2853883069619f5461dbf1 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 11 Jun 2021 08:27:58 +0200 Subject: [PATCH 15/19] 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 --- .../algorithms/minimum_eigen_solvers/vqe.py | 46 +++++++++---------- test/python/algorithms/test_vqe.py | 15 +++--- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 4e8ef476b695..8c57d685a30c 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -164,7 +164,8 @@ def __init__( self._max_evals_grouped = max_evals_grouped self._circuit_sampler = None # type: Optional[CircuitSampler] - self._expectation = expectation + self._input_expectation = expectation + self._factory_expectation = None self._include_custom = include_custom # set ansatz -- still supporting pre 0.18.0 sorting @@ -191,14 +192,6 @@ def __init__( # TODO remove this once the stateful methods are deleted self._ret = None - def _try_set_expectation_value_from_factory(self, operator: OperatorBase) -> None: - if operator is not None and self.quantum_instance is not None: - self._expectation = ExpectationFactory.build( - operator=operator, - backend=self.quantum_instance, - include_custom=self._include_custom, - ) - @property def ansatz(self) -> Optional[QuantumCircuit]: """Returns the ansatz.""" @@ -260,11 +253,14 @@ def initial_point(self, initial_point: np.ndarray): def expectation(self) -> ExpectationBase: """The expectation value algorithm used to construct the expectation measurement from the observable.""" - return self._expectation + if self._input_expectation is not None: + return self._input_expectation + + return self._factory_expectation @expectation.setter def expectation(self, exp: ExpectationBase) -> None: - self._expectation = exp + self._input_expectation = exp def _check_operator_ansatz(self, operator: OperatorBase): """Check that the number of qubits of operator and ansatz match.""" @@ -348,6 +344,8 @@ def construct_expectation( 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.") @@ -357,18 +355,6 @@ def construct_expectation( param_dict = dict(zip(self._ansatz_params, parameter)) # type: Dict wave_function = self.ansatz.assign_parameters(param_dict) - # Expectation was never created , try to create one - if self.expectation is None: - self._try_set_expectation_value_from_factory(operator) - - # 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." - ) - observable_meas = self.expectation.convert(StateFn(operator, is_measurement=True)) ansatz_circuit_op = CircuitStateFn(wave_function) return observable_meas.compose(ansatz_circuit_op).reduce() @@ -445,6 +431,20 @@ def compute_minimum_eigenvalue( # validation self._check_operator_ansatz(operator) + # if expectation was never created, try to create one + if self.expectation is None: + self._factory_expectation = ExpectationFactory.build( + operator=operator, + backend=self.quantum_instance, + include_custom=self._include_custom, + ) + 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." + ) + initial_point = _validate_initial_point(self.initial_point, self.ansatz) bounds = _validate_bounds(self.ansatz) diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py index 347137632519..c6f157650b1a 100644 --- a/test/python/algorithms/test_vqe.py +++ b/test/python/algorithms/test_vqe.py @@ -159,13 +159,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, - sort_parameters_by_name=True, - ) + 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) From 11ceff9590392b09b46643b9f581e88478ad015a Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 11 Jun 2021 10:49:14 +0200 Subject: [PATCH 16/19] move expectation factory to right place --- .../algorithms/minimum_eigen_solvers/vqe.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 8c57d685a30c..364eab3cd0b1 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -352,6 +352,20 @@ def construct_expectation( self._check_operator_ansatz(operator) + # if expectation was never created, try to create one + if self.expectation is None: + self._factory_expectation = ExpectationFactory.build( + operator=operator, + backend=self.quantum_instance, + include_custom=self._include_custom, + ) + 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) @@ -431,20 +445,6 @@ def compute_minimum_eigenvalue( # validation self._check_operator_ansatz(operator) - # if expectation was never created, try to create one - if self.expectation is None: - self._factory_expectation = ExpectationFactory.build( - operator=operator, - backend=self.quantum_instance, - include_custom=self._include_custom, - ) - 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." - ) - initial_point = _validate_initial_point(self.initial_point, self.ansatz) bounds = _validate_bounds(self.ansatz) From 91416864aae7bbe84edeba3d6a0109b72bd19197 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 15 Jun 2021 14:14:34 +0200 Subject: [PATCH 17/19] address #5746 --- .../algorithms/minimum_eigen_solvers/vqe.py | 72 ++++++++++++------- test/python/algorithms/test_vqe.py | 36 ++-------- .../python/opflow/test_expectation_factory.py | 41 +++++++++++ 3 files changed, 91 insertions(+), 58 deletions(-) create mode 100644 test/python/opflow/test_expectation_factory.py diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 364eab3cd0b1..fdb0d040dcc2 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -15,7 +15,7 @@ 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 @@ -164,8 +164,7 @@ def __init__( self._max_evals_grouped = max_evals_grouped self._circuit_sampler = None # type: Optional[CircuitSampler] - self._input_expectation = expectation - self._factory_expectation = None + self._expectation = expectation self._include_custom = include_custom # set ansatz -- still supporting pre 0.18.0 sorting @@ -253,10 +252,7 @@ def initial_point(self, initial_point: np.ndarray): def expectation(self) -> ExpectationBase: """The expectation value algorithm used to construct the expectation measurement from the observable.""" - if self._input_expectation is not None: - return self._input_expectation - - return self._factory_expectation + return self._expectation @expectation.setter def expectation(self, exp: ExpectationBase) -> None: @@ -329,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. @@ -337,10 +334,13 @@ 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. @@ -354,24 +354,25 @@ def construct_expectation( # if expectation was never created, try to create one if self.expectation is None: - self._factory_expectation = ExpectationFactory.build( + expectation = ExpectationFactory.build( operator=operator, backend=self.quantum_instance, include_custom=self._include_custom, ) - 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." - ) + else: + expectation = self.expectation 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, @@ -408,12 +409,16 @@ def supports_aux_operators(cls) -> bool: return True def _eval_aux_ops( - self, parameters: np.ndarray, aux_operators: List[OperatorBase], threshold: float = 1e-12 + 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_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()) @@ -445,6 +450,7 @@ def compute_minimum_eigenvalue( # 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) @@ -476,7 +482,9 @@ def compute_minimum_eigenvalue( gradient = self._gradient self._eval_count = 0 - energy_evaluation = self.get_energy_evaluation(operator) + energy_evaluation, expectation = self.get_energy_evaluation( + operator, return_expectation=True + ) start_time = time() opt_params, opt_value, nfev = self.optimizer.optimize( @@ -508,13 +516,15 @@ def compute_minimum_eigenvalue( self._ret = result if aux_operators is not None: - aux_values = self._eval_aux_ops(opt_params, aux_operators) + aux_values = self._eval_aux_ops(opt_params, aux_operators, expectation=expectation) result.aux_operator_eigenvalues = aux_values[0] return result def get_energy_evaluation( - self, operator: OperatorBase + 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. @@ -522,9 +532,14 @@ def get_energy_evaluation( Args: 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. + Energy 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). @@ -534,7 +549,9 @@ def get_energy_evaluation( if num_parameters == 0: raise RuntimeError("The ansatz must be parameterized, but has 0 free parameters.") - expect_op = self.construct_expectation(self._ansatz_params, operator) + expect_op, expectation = self.construct_expectation( + self._ansatz_params, operator, return_expectation=True + ) def energy_evaluation(parameters): parameter_sets = np.reshape(parameters, (-1, num_parameters)) @@ -546,7 +563,7 @@ def energy_evaluation(parameters): means = np.real(sampled_expect_op.eval()) if self._callback is not None: - variance = np.real(self.expectation.compute_variance(sampled_expect_op)) + 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 @@ -564,6 +581,9 @@ def energy_evaluation(parameters): return means if len(means) > 1 else means[0] + if return_expectation: + return energy_evaluation, expectation + return energy_evaluation @deprecate_function( diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py index c6f157650b1a..8684badc045b 100644 --- a/test/python/algorithms/test_vqe.py +++ b/test/python/algorithms/test_vqe.py @@ -35,7 +35,6 @@ from qiskit.exceptions import MissingOptionalLibraryError from qiskit.opflow import ( AerPauliExpectation, - ExpectationBase, Gradient, I, MatrixExpectation, @@ -413,26 +412,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.""" @@ -446,22 +425,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)) diff --git a/test/python/opflow/test_expectation_factory.py b/test/python/opflow/test_expectation_factory.py new file mode 100644 index 000000000000..47c9302e8c54 --- /dev/null +++ b/test/python/opflow/test_expectation_factory.py @@ -0,0 +1,41 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2021. +# +# 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 the expectation factory.""" + +import unittest +from test.python.opflow import QiskitOpflowTestCase + +from qiskit.opflow import PauliExpectation, AerPauliExpectation, ExpectationFactory, Z, I, X +from qiskit.utils import has_aer + + +if has_aer(): + from qiskit import Aer + + +class TestExpectationFactory(QiskitOpflowTestCase): + """Tests for the expectation factory.""" + + @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + def test_aer_simulator_pauli_sum(self): + """Test expectation selection with Aer's qasm_simulator.""" + backend = Aer.get_backend("aer_simulator") + op = 0.2 * (X ^ X) + 0.1 * (Z ^ I) + + with self.subTest("Defaults"): + expectation = ExpectationFactory.build(op, backend, include_custom=False) + self.assertIsInstance(expectation, PauliExpectation) + + with self.subTest("Include custom"): + expectation = ExpectationFactory.build(op, backend, include_custom=True) + self.assertIsInstance(expectation, AerPauliExpectation) From 77bb3bc160cff9ea171d235468c1d5e1c2796216 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 15 Jun 2021 16:17:08 +0200 Subject: [PATCH 18/19] fix typehint in expectation (add Optional) --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index fdb0d040dcc2..839c56d042d8 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -249,13 +249,13 @@ def initial_point(self, initial_point: np.ndarray): self._initial_point = initial_point @property - def expectation(self) -> ExpectationBase: + 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: + def expectation(self, exp: Optional[ExpectationBase]) -> None: self._input_expectation = exp def _check_operator_ansatz(self, operator: OperatorBase): From 8358340b67eca0737af5f7a3ed8a92e643a21939 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 16 Jun 2021 11:38:53 +0200 Subject: [PATCH 19/19] fix expectation setter --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 2 +- test/python/algorithms/test_vqe.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 839c56d042d8..9df4a8f7e7f3 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -256,7 +256,7 @@ def expectation(self) -> Optional[ExpectationBase]: @expectation.setter def expectation(self, exp: Optional[ExpectationBase]) -> None: - self._input_expectation = exp + self._expectation = exp def _check_operator_ansatz(self, operator: OperatorBase): """Check that the number of qubits of operator and ansatz match.""" diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py index 8684badc045b..c7fa92b917ee 100644 --- a/test/python/algorithms/test_vqe.py +++ b/test/python/algorithms/test_vqe.py @@ -381,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)