Repository: Qiskit/qiskit

Issue:
Make ``VQE`` stateless
### Summary

Make the `VQE` algorithm stateless.

### What is the expected enhancement

All our algorithms are stateless, i.e. they don't store/depend on previous executions. The `VQE` still has methods that allow access to the last solved system, which should be strictly left to the results object. Furthermore the computation depends on an internal state being set, which should be changed to a procedural, stateless workflow.


Discussion:

Resolving patch:
diff --git a/qiskit/algorithms/minimum_eigen_solvers/qaoa.py b/qiskit/algorithms/minimum_eigen_solvers/qaoa.py
--- 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
--- 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
--- 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


Test patch:
diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py
--- 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,
@@ -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))
diff --git a/test/python/opflow/test_expectation_factory.py b/test/python/opflow/test_expectation_factory.py
new file mode 100644
--- /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)