From ab1447e5d3846fa49b9f005101af70a6a8f05339 Mon Sep 17 00:00:00 2001 From: Max Rossmannek Date: Wed, 1 Dec 2021 15:00:47 +0100 Subject: [PATCH] Dict-based `aux_operators` (#406) * [WIP] naive migration to dict-based aux_operators * [WIP] extract ListOrDict logic into class * Revert ListOrDict integration We need to properly deprecate the old function signature of `second_q_ops` before we can fully switch to dict-based aux operators. For this, I introduce a new keyword argument `return_list` which defaults to the old way of list-based aux operators. In the following commits I will add DeprecationWarnings announcing a change of this default as well as unittests to assert the correct behavior of the dict-based aux operators. * Add basic unittest for dict-based aux ops * Refactor * Extend aux_operators public extension to support dict too * Fix lint * Revert some unnecessary changes * Update docstrings * Fix spell * Remove unused import * Fix lint * Reuse ListOrDict-type alias from Terra * Remove BaseProblem.main_property_name setter This property should only ever be set during construction of a certain problem type. Removing the setter ensures this scenario. * Improve commutation debug message * Extract new aux_operators interface into global setting * Run black * Add DeprecationWarning for list-based aux_operators * Log warning instead of raising it * Raise error upon aux_operator name clash * Evaluate aux_operators at ground state during QEOM Co-authored-by: Manoel Marques --- .pylintdict | 1 + qiskit_nature/__init__.py | 10 ++- .../excited_states_eigensolver.py | 51 +++++++++-- .../excited_states_solver.py | 5 +- .../algorithms/excited_states_solvers/qeom.py | 14 ++- .../ground_state_solvers/adapt_vqe.py | 47 ++++++++-- .../ground_state_eigensolver.py | 49 +++++++++-- .../ground_state_solver.py | 3 +- .../second_quantization/qubit_converter.py | 88 ++++++++++++------- .../second_quantization/utils/__init__.py | 31 +++++++ .../second_quantization/utils/list_or_dict.py | 59 +++++++++++++ .../second_quantization/base_problem.py | 19 ++-- .../electronic_structure_problem.py | 31 ++++--- .../vibrational_structure_problem.py | 22 +++-- .../electronic/angular_momentum.py | 28 ++++-- .../electronic/dipole_moment.py | 77 ++++++++++------ .../electronic_structure_driver_result.py | 45 ++++++---- .../electronic/integrals/integral_property.py | 19 +++- .../electronic/magnetization.py | 28 ++++-- .../electronic/particle_number.py | 26 ++++-- .../second_quantized_property.py | 25 ++++-- .../vibrational/occupied_modals.py | 24 +++-- .../vibrational/vibrational_energy.py | 17 +++- .../vibrational_structure_driver_result.py | 34 ++++--- qiskit_nature/results/eigenstate_result.py | 14 ++- qiskit_nature/settings.py | 55 ++++++++++++ .../test_groundstate_eigensolver.py | 51 +++++++++++ 27 files changed, 686 insertions(+), 187 deletions(-) create mode 100644 qiskit_nature/converters/second_quantization/utils/__init__.py create mode 100644 qiskit_nature/converters/second_quantization/utils/list_or_dict.py create mode 100644 qiskit_nature/settings.py diff --git a/.pylintdict b/.pylintdict index 6ef311e6d8..561c4df2a8 100644 --- a/.pylintdict +++ b/.pylintdict @@ -22,6 +22,7 @@ arxiv asparagine aspartic atol +attr autosummary avogadro äquivalenzverbot diff --git a/qiskit_nature/__init__.py b/qiskit_nature/__init__.py index 1a870ce0a0..f10ba2dd6e 100644 --- a/qiskit_nature/__init__.py +++ b/qiskit_nature/__init__.py @@ -49,11 +49,19 @@ """ -from .version import __version__ +from qiskit.algorithms.minimum_eigen_solvers.minimum_eigen_solver import ( + ListOrDict as ListOrDictType, +) + from .exceptions import QiskitNatureError, UnsupportMethodError +from .settings import settings +from .version import __version__ + __all__ = [ "__version__", + "ListOrDictType", "QiskitNatureError", "UnsupportMethodError", + "settings", ] diff --git a/qiskit_nature/algorithms/excited_states_solvers/excited_states_eigensolver.py b/qiskit_nature/algorithms/excited_states_solvers/excited_states_eigensolver.py index 1260537506..141f348239 100644 --- a/qiskit_nature/algorithms/excited_states_solvers/excited_states_eigensolver.py +++ b/qiskit_nature/algorithms/excited_states_solvers/excited_states_eigensolver.py @@ -12,12 +12,14 @@ """The calculation of excited states via an Eigensolver algorithm""" -from typing import List, Union, Optional +from typing import Union, Optional from qiskit.algorithms import Eigensolver from qiskit.opflow import PauliSumOp +from qiskit_nature import ListOrDictType, QiskitNatureError from qiskit_nature.converters.second_quantization import QubitConverter +from qiskit_nature.converters.second_quantization.utils import ListOrDict from qiskit_nature.operators.second_quantization import SecondQuantizedOp from qiskit_nature.problems.second_quantization import BaseProblem from qiskit_nature.results import EigenstateResult @@ -58,7 +60,7 @@ def solver(self, solver: Union[Eigensolver, EigensolverFactory]) -> None: def solve( self, problem: BaseProblem, - aux_operators: Optional[List[Union[SecondQuantizedOp, PauliSumOp]]] = None, + aux_operators: Optional[ListOrDictType[Union[SecondQuantizedOp, PauliSumOp]]] = None, ) -> EigenstateResult: """Compute Ground and Excited States properties. @@ -67,8 +69,12 @@ def solve( aux_operators: Additional auxiliary operators to evaluate. Raises: - NotImplementedError: If an operator in ``aux_operators`` is not of type - ``FermionicOperator``. + ValueError: if the grouped property object returned by the driver does not contain a + main property as requested by the problem being solved (`problem.main_property_name`) + QiskitNatureError: if the user-provided `aux_operators` contain a name which clashes + with an internally constructed auxiliary operator. Note: the names used for the + internal auxiliary operators correspond to the `Property.name` attributes which + generated the respective operators. Returns: An interpreted :class:`~.EigenstateResult`. For more information see also @@ -79,19 +85,46 @@ def solve( # by the user but also additional ones from the transformation second_q_ops = problem.second_q_ops() + aux_second_q_ops: ListOrDictType[SecondQuantizedOp] + if isinstance(second_q_ops, list): + main_second_q_op = second_q_ops[0] + aux_second_q_ops = second_q_ops[1:] + elif isinstance(second_q_ops, dict): + name = problem.main_property_name + main_second_q_op = second_q_ops.pop(name, None) + if main_second_q_op is None: + raise ValueError( + f"The main `SecondQuantizedOp` associated with the {name} property cannot be " + "`None`." + ) + aux_second_q_ops = second_q_ops + main_operator = self._qubit_converter.convert( - second_q_ops[0], + main_second_q_op, num_particles=problem.num_particles, sector_locator=problem.symmetry_sector_locator, ) - aux_ops = self._qubit_converter.convert_match(second_q_ops[1:]) + aux_ops = self._qubit_converter.convert_match(aux_second_q_ops) if aux_operators is not None: - for aux_op in aux_operators: + wrapped_aux_operators: ListOrDict[Union[SecondQuantizedOp, PauliSumOp]] = ListOrDict( + aux_operators + ) + for name, aux_op in iter(wrapped_aux_operators): if isinstance(aux_op, SecondQuantizedOp): - aux_ops.append(self._qubit_converter.convert_match(aux_op, True)) + converted_aux_op = self._qubit_converter.convert_match(aux_op, True) else: - aux_ops.append(aux_op) + converted_aux_op = aux_op + if isinstance(aux_ops, list): + aux_ops.append(converted_aux_op) + elif isinstance(aux_ops, dict): + if name in aux_ops.keys(): + raise QiskitNatureError( + f"The key '{name}' is already taken by an internally constructed " + "auxliliary operator! Please use a different name for your custom " + "operator." + ) + aux_ops[name] = converted_aux_op if isinstance(self._solver, EigensolverFactory): # this must be called after transformation.transform diff --git a/qiskit_nature/algorithms/excited_states_solvers/excited_states_solver.py b/qiskit_nature/algorithms/excited_states_solvers/excited_states_solver.py index d1317fa568..9916ac0029 100644 --- a/qiskit_nature/algorithms/excited_states_solvers/excited_states_solver.py +++ b/qiskit_nature/algorithms/excited_states_solvers/excited_states_solver.py @@ -13,10 +13,11 @@ """ The excited states calculation interface """ from abc import ABC, abstractmethod -from typing import List, Optional, Union +from typing import Optional, Union from qiskit.opflow import PauliSumOp +from qiskit_nature import ListOrDictType from qiskit_nature.operators.second_quantization import SecondQuantizedOp from qiskit_nature.problems.second_quantization import BaseProblem from qiskit_nature.results import EigenstateResult @@ -29,7 +30,7 @@ class ExcitedStatesSolver(ABC): def solve( self, problem: BaseProblem, - aux_operators: Optional[List[Union[SecondQuantizedOp, PauliSumOp]]] = None, + aux_operators: Optional[ListOrDictType[Union[SecondQuantizedOp, PauliSumOp]]] = None, ) -> EigenstateResult: r"""Compute the excited states energies of the molecule that was supplied via the driver. diff --git a/qiskit_nature/algorithms/excited_states_solvers/qeom.py b/qiskit_nature/algorithms/excited_states_solvers/qeom.py index 4fd65e28bf..d90a62b8dd 100644 --- a/qiskit_nature/algorithms/excited_states_solvers/qeom.py +++ b/qiskit_nature/algorithms/excited_states_solvers/qeom.py @@ -30,6 +30,7 @@ PauliSumOp, ) +from qiskit_nature import ListOrDictType from qiskit_nature.operators.second_quantization import SecondQuantizedOp from qiskit_nature.problems.second_quantization import BaseProblem from qiskit_nature.results import EigenstateResult @@ -79,7 +80,7 @@ def excitations(self, excitations: Union[str, List[List[int]]]) -> None: def solve( self, problem: BaseProblem, - aux_operators: Optional[List[SecondQuantizedOp]] = None, + aux_operators: Optional[ListOrDictType[SecondQuantizedOp]] = None, ) -> EigenstateResult: """Run the excited-states calculation. @@ -102,13 +103,18 @@ def solve( ) # 1. Run ground state calculation - groundstate_result = self._gsc.solve(problem) + groundstate_result = self._gsc.solve(problem, aux_operators) # 2. Prepare the excitation operators + second_q_ops = problem.second_q_ops() + if isinstance(second_q_ops, list): + main_second_q_op = second_q_ops[0] + elif isinstance(second_q_ops, dict): + main_second_q_op = second_q_ops.pop(problem.main_property_name) + self._untapered_qubit_op_main = self._gsc.qubit_converter.convert_only( - problem.second_q_ops()[0], problem.num_particles + main_second_q_op, problem.num_particles ) - matrix_operators_dict, size = self._prepare_matrix_operators(problem) # 3. Evaluate eom operators diff --git a/qiskit_nature/algorithms/ground_state_solvers/adapt_vqe.py b/qiskit_nature/algorithms/ground_state_solvers/adapt_vqe.py index 62ecbe3f72..4fda3bcbe3 100644 --- a/qiskit_nature/algorithms/ground_state_solvers/adapt_vqe.py +++ b/qiskit_nature/algorithms/ground_state_solvers/adapt_vqe.py @@ -24,10 +24,12 @@ from qiskit.circuit import QuantumCircuit from qiskit.opflow import OperatorBase, PauliSumOp from qiskit.utils.validation import validate_min +from qiskit_nature import ListOrDictType from qiskit_nature.exceptions import QiskitNatureError from qiskit_nature.circuit.library import UCC from qiskit_nature.operators.second_quantization import SecondQuantizedOp from qiskit_nature.converters.second_quantization import QubitConverter +from qiskit_nature.converters.second_quantization.utils import ListOrDict from qiskit_nature.problems.second_quantization import BaseProblem from qiskit_nature.results import ElectronicStructureResult @@ -136,7 +138,7 @@ def _check_cyclicity(indices: List[int]) -> bool: def solve( self, problem: BaseProblem, - aux_operators: Optional[List[Union[SecondQuantizedOp, PauliSumOp]]] = None, + aux_operators: Optional[ListOrDictType[Union[SecondQuantizedOp, PauliSumOp]]] = None, ) -> "AdaptVQEResult": """Computes the ground state. @@ -147,6 +149,12 @@ def solve( Raises: QiskitNatureError: if a solver other than VQE or a ansatz other than UCCSD is provided or if the algorithm finishes due to an unforeseen reason. + ValueError: if the grouped property object returned by the driver does not contain a + main property as requested by the problem being solved (`problem.main_property_name`) + QiskitNatureError: if the user-provided `aux_operators` contain a name which clashes + with an internally constructed auxiliary operator. Note: the names used for the + internal auxiliary operators correspond to the `Property.name` attributes which + generated the respective operators. Returns: An AdaptVQEResult which is an ElectronicStructureResult but also includes runtime @@ -155,19 +163,46 @@ def solve( """ second_q_ops = problem.second_q_ops() + aux_second_q_ops: ListOrDictType[SecondQuantizedOp] + if isinstance(second_q_ops, list): + main_second_q_op = second_q_ops[0] + aux_second_q_ops = second_q_ops[1:] + elif isinstance(second_q_ops, dict): + name = problem.main_property_name + main_second_q_op = second_q_ops.pop(name, None) + if main_second_q_op is None: + raise ValueError( + f"The main `SecondQuantizedOp` associated with the {name} property cannot be " + "`None`." + ) + aux_second_q_ops = second_q_ops + self._main_operator = self._qubit_converter.convert( - second_q_ops[0], + main_second_q_op, num_particles=problem.num_particles, sector_locator=problem.symmetry_sector_locator, ) - aux_ops = self._qubit_converter.convert_match(second_q_ops[1:]) + aux_ops = self._qubit_converter.convert_match(aux_second_q_ops) if aux_operators is not None: - for aux_op in aux_operators: + wrapped_aux_operators: ListOrDict[Union[SecondQuantizedOp, PauliSumOp]] = ListOrDict( + aux_operators + ) + for name, aux_op in iter(wrapped_aux_operators): if isinstance(aux_op, SecondQuantizedOp): - aux_ops.append(self._qubit_converter.convert_match(aux_op, True)) + converted_aux_op = self._qubit_converter.convert_match(aux_op, True) else: - aux_ops.append(aux_op) + converted_aux_op = aux_op + if isinstance(aux_ops, list): + aux_ops.append(converted_aux_op) + elif isinstance(aux_ops, dict): + if name in aux_ops.keys(): + raise QiskitNatureError( + f"The key '{name}' is already taken by an internally constructed " + "auxliliary operator! Please use a different name for your custom " + "operator." + ) + aux_ops[name] = converted_aux_op if isinstance(self._solver, MinimumEigensolverFactory): vqe = self._solver.get_solver(problem, self._qubit_converter) diff --git a/qiskit_nature/algorithms/ground_state_solvers/ground_state_eigensolver.py b/qiskit_nature/algorithms/ground_state_solvers/ground_state_eigensolver.py index 54f0727c11..b6e8c96603 100644 --- a/qiskit_nature/algorithms/ground_state_solvers/ground_state_eigensolver.py +++ b/qiskit_nature/algorithms/ground_state_solvers/ground_state_eigensolver.py @@ -22,8 +22,10 @@ from qiskit.algorithms import MinimumEigensolver from qiskit.opflow import OperatorBase, PauliSumOp, StateFn, CircuitSampler +from qiskit_nature import ListOrDictType, QiskitNatureError from qiskit_nature.operators.second_quantization import SecondQuantizedOp from qiskit_nature.converters.second_quantization import QubitConverter +from qiskit_nature.converters.second_quantization.utils import ListOrDict from qiskit_nature.problems.second_quantization import BaseProblem from qiskit_nature.results import EigenstateResult from .ground_state_solver import GroundStateSolver @@ -65,7 +67,7 @@ def returns_groundstate(self) -> bool: def solve( self, problem: BaseProblem, - aux_operators: Optional[List[Union[SecondQuantizedOp, PauliSumOp]]] = None, + aux_operators: Optional[ListOrDictType[Union[SecondQuantizedOp, PauliSumOp]]] = None, ) -> EigenstateResult: """Compute Ground State properties. @@ -74,8 +76,12 @@ def solve( aux_operators: Additional auxiliary operators to evaluate. Raises: - NotImplementedError: If an operator in ``aux_operators`` is not of type - ``FermionicOperator``. + ValueError: if the grouped property object returned by the driver does not contain a + main property as requested by the problem being solved (`problem.main_property_name`) + QiskitNatureError: if the user-provided `aux_operators` contain a name which clashes + with an internally constructed auxiliary operator. Note: the names used for the + internal auxiliary operators correspond to the `Property.name` attributes which + generated the respective operators. Returns: An interpreted :class:`~.EigenstateResult`. For more information see also @@ -86,19 +92,46 @@ def solve( # user but also additional ones from the transformation second_q_ops = problem.second_q_ops() + aux_second_q_ops: ListOrDictType[SecondQuantizedOp] + if isinstance(second_q_ops, list): + main_second_q_op = second_q_ops[0] + aux_second_q_ops = second_q_ops[1:] + elif isinstance(second_q_ops, dict): + name = problem.main_property_name + main_second_q_op = second_q_ops.pop(name, None) + if main_second_q_op is None: + raise ValueError( + f"The main `SecondQuantizedOp` associated with the {name} property cannot be " + "`None`." + ) + aux_second_q_ops = second_q_ops + main_operator = self._qubit_converter.convert( - second_q_ops[0], + main_second_q_op, num_particles=problem.num_particles, sector_locator=problem.symmetry_sector_locator, ) - aux_ops = self._qubit_converter.convert_match(second_q_ops[1:]) + aux_ops = self._qubit_converter.convert_match(aux_second_q_ops) if aux_operators is not None: - for aux_op in aux_operators: + wrapped_aux_operators: ListOrDict[Union[SecondQuantizedOp, PauliSumOp]] = ListOrDict( + aux_operators + ) + for name, aux_op in iter(wrapped_aux_operators): if isinstance(aux_op, SecondQuantizedOp): - aux_ops.append(self._qubit_converter.convert_match(aux_op, True)) + converted_aux_op = self._qubit_converter.convert_match(aux_op, True) else: - aux_ops.append(aux_op) + converted_aux_op = aux_op + if isinstance(aux_ops, list): + aux_ops.append(converted_aux_op) + elif isinstance(aux_ops, dict): + if name in aux_ops.keys(): + raise QiskitNatureError( + f"The key '{name}' is already taken by an internally constructed " + "auxliliary operator! Please use a different name for your custom " + "operator." + ) + aux_ops[name] = converted_aux_op if isinstance(self._solver, MinimumEigensolverFactory): # this must be called after transformation.transform diff --git a/qiskit_nature/algorithms/ground_state_solvers/ground_state_solver.py b/qiskit_nature/algorithms/ground_state_solvers/ground_state_solver.py index 49e040055a..c116b1daf8 100644 --- a/qiskit_nature/algorithms/ground_state_solvers/ground_state_solver.py +++ b/qiskit_nature/algorithms/ground_state_solvers/ground_state_solver.py @@ -22,6 +22,7 @@ from qiskit.result import Result from qiskit.opflow import OperatorBase, PauliSumOp +from qiskit_nature import ListOrDictType from qiskit_nature.operators.second_quantization import SecondQuantizedOp from qiskit_nature.converters.second_quantization import QubitConverter from qiskit_nature.problems.second_quantization import BaseProblem @@ -43,7 +44,7 @@ def __init__(self, qubit_converter: QubitConverter) -> None: def solve( self, problem: BaseProblem, - aux_operators: Optional[List[Union[SecondQuantizedOp, PauliSumOp]]] = None, + aux_operators: Optional[ListOrDictType[Union[SecondQuantizedOp, PauliSumOp]]] = None, ) -> EigenstateResult: """Compute the ground state energy of the molecule that was supplied via the driver. diff --git a/qiskit_nature/converters/second_quantization/qubit_converter.py b/qiskit_nature/converters/second_quantization/qubit_converter.py index 1e4c3bebe6..29a0e00f56 100644 --- a/qiskit_nature/converters/second_quantization/qubit_converter.py +++ b/qiskit_nature/converters/second_quantization/qubit_converter.py @@ -21,11 +21,13 @@ from qiskit.opflow.converters import TwoQubitReduction from qiskit.opflow.primitive_ops import Z2Symmetries -from qiskit_nature import QiskitNatureError +from qiskit_nature import ListOrDictType, QiskitNatureError from qiskit_nature.mappers.second_quantization import QubitMapper from qiskit_nature.operators.second_quantization import SecondQuantizedOp +from .utils import ListOrDict + logger = logging.getLogger(__name__) @@ -245,10 +247,10 @@ def force_match( def convert_match( self, - second_q_ops: Union[SecondQuantizedOp, List[SecondQuantizedOp]], + second_q_ops: Union[SecondQuantizedOp, ListOrDictType[SecondQuantizedOp]], suppress_none: bool = False, check_commutes: bool = True, - ) -> Union[PauliSumOp, List[Optional[PauliSumOp]]]: + ) -> Union[PauliSumOp, ListOrDictType[PauliSumOp]]: """Convert further operators to match that done in :meth:`convert`, or as set by :meth:`force_match`. @@ -272,26 +274,42 @@ def convert_match( """ # To allow a single operator to be converted, but use the same logic that does the # actual conversions, we make a single entry list of it here and unwrap to return. - wrapped = False + wrapped_type = type(second_q_ops) + if isinstance(second_q_ops, SecondQuantizedOp): second_q_ops = [second_q_ops] - wrapped = True suppress_none = False # When only a single op we will return None back - qubit_ops = [self._map(second_q_op) for second_q_op in second_q_ops] - reduced_ops = [ - self._two_qubit_reduce(qubit_op, self._num_particles) for qubit_op in qubit_ops - ] - tapered_ops = self._symmetry_reduce(reduced_ops, suppress_none, check_commutes) + wrapped_second_q_ops: ListOrDict[SecondQuantizedOp] = ListOrDict(second_q_ops) + + qubit_ops: ListOrDict[PauliSumOp] = ListOrDict() + for name, second_q_op in iter(wrapped_second_q_ops): + qubit_ops[name] = self._map(second_q_op) - if wrapped: - tapered_ops = tapered_ops[0] + reduced_ops: ListOrDict[PauliSumOp] = ListOrDict() + for name, qubit_op in iter(qubit_ops): + reduced_ops[name] = self._two_qubit_reduce(qubit_op, self._num_particles) - return tapered_ops + tapered_ops = self._symmetry_reduce(reduced_ops, check_commutes) + + returned_ops: Union[PauliSumOp, ListOrDictType[PauliSumOp]] + + if issubclass(wrapped_type, SecondQuantizedOp): + returned_ops = list(iter(tapered_ops))[0][1] + elif wrapped_type == list: + if suppress_none: + returned_ops = [op for _, op in iter(tapered_ops) if op is not None] + else: + returned_ops = [op for _, op in iter(tapered_ops)] + elif wrapped_type == dict: + returned_ops = dict(iter(tapered_ops)) + + return returned_ops def map( - self, second_q_ops: Union[SecondQuantizedOp, List[SecondQuantizedOp]] - ) -> Union[PauliSumOp, List[Optional[PauliSumOp]]]: + self, + second_q_ops: Union[SecondQuantizedOp, ListOrDictType[SecondQuantizedOp]], + ) -> Union[PauliSumOp, ListOrDictType[PauliSumOp]]: """A convenience method to map second quantized operators based on current mapper. Args: @@ -304,7 +322,18 @@ def map( if isinstance(second_q_ops, SecondQuantizedOp): qubit_ops = self._map(second_q_ops) else: - qubit_ops = [self._map(second_q_op) for second_q_op in second_q_ops] + wrapped_type = type(second_q_ops) + + wrapped_second_q_ops: ListOrDict[SecondQuantizedOp] = ListOrDict(second_q_ops) + + qubit_ops = ListOrDict() + for name, second_q_op in iter(wrapped_second_q_ops): + qubit_ops[name] = self._map(second_q_op) + + if wrapped_type == list: + qubit_ops = [op for _, op in iter(qubit_ops)] + elif wrapped_type == dict: + qubit_ops = dict(iter(qubit_ops)) return qubit_ops @@ -393,10 +422,9 @@ def _find_taper_op( def _symmetry_reduce( self, - qubit_ops: List[PauliSumOp], - suppress_none: bool, + qubit_ops: ListOrDict[PauliSumOp], check_commutes: bool, - ) -> List[Optional[PauliSumOp]]: + ) -> ListOrDict[PauliSumOp]: if self._z2symmetries is None or self._z2symmetries.is_empty(): tapered_qubit_ops = qubit_ops @@ -406,24 +434,22 @@ def _symmetry_reduce( symmetry_ops = [] for symmetry in self._z2symmetries.symmetries: symmetry_ops.append(PauliSumOp.from_list([(symmetry.to_label(), 1.0)])) - commuted = [] - for i, qubit_op in enumerate(qubit_ops): + commuted = {} + for name, qubit_op in iter(qubit_ops): commutes = QubitConverter._check_commutes(symmetry_ops, qubit_op) - commuted.append(commutes) - logger.debug("Qubit operators commuted with symmetry %s", commuted) + commuted[name] = commutes + logger.debug("Qubit operator '%s' commuted with symmetry: %s", name, commutes) # Tapering values were set from prior convert so we go ahead and taper operators - tapered_qubit_ops = [] - for i, commutes in enumerate(commuted): + tapered_qubit_ops = ListOrDict() + for name, commutes in commuted.items(): if commutes: - tapered_qubit_ops.append(self._z2symmetries.taper(qubit_ops[i])) - elif not suppress_none: - tapered_qubit_ops.append(None) + tapered_qubit_ops[name] = self._z2symmetries.taper(qubit_ops[name]) else: logger.debug("Tapering operators whether they commute with symmetry or not:") - tapered_qubit_ops = [] - for qubit_op in qubit_ops: - tapered_qubit_ops.append(self._z2symmetries.taper(qubit_op)) + tapered_qubit_ops = ListOrDict() + for name, qubit_op in iter(qubit_ops): + tapered_qubit_ops[name] = self._z2symmetries.taper(qubit_ops[name]) return tapered_qubit_ops diff --git a/qiskit_nature/converters/second_quantization/utils/__init__.py b/qiskit_nature/converters/second_quantization/utils/__init__.py new file mode 100644 index 0000000000..f3405537bd --- /dev/null +++ b/qiskit_nature/converters/second_quantization/utils/__init__.py @@ -0,0 +1,31 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +""" +SecondQuantizedOp Converters (:mod:`qiskit_nature.converters.second_quantization.utils`) +======================================================================================== + +.. currentmodule:: qiskit_nature.converters.second_quantization.utils + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + ListOrDict + +""" + +from .list_or_dict import ListOrDict + +__all__ = [ + "ListOrDict", +] diff --git a/qiskit_nature/converters/second_quantization/utils/list_or_dict.py b/qiskit_nature/converters/second_quantization/utils/list_or_dict.py new file mode 100644 index 0000000000..bee65b11c2 --- /dev/null +++ b/qiskit_nature/converters/second_quantization/utils/list_or_dict.py @@ -0,0 +1,59 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +"""The ListOrDict utility class.""" + +from typing import Dict, Generator, Generic, Iterable, Optional, Tuple, TypeVar, Union +from qiskit_nature import ListOrDictType + +# pylint: disable=invalid-name +T = TypeVar("T") + + +class ListOrDict(Dict, Iterable, Generic[T]): + """The ListOrDict utility class. + + This is a utility which allows seamless iteration of a `list` or `dict` object. + This is mostly used for dealing with `aux_operators` which support both types since Qiskit Terra + version 0.19.0 + """ + + def __init__(self, values: Optional[ListOrDictType] = None): + """ + Args: + values: an optional object of `list` or `dict` type. + """ + if isinstance(values, list): + values = dict(enumerate(values)) + elif values is None: + values = {} + super().__init__(values) + + def __iter__(self) -> Generator[Tuple[Union[int, str], T], T, None]: + """Return the generator-iterator method.""" + return self._generator() + + def _generator(self) -> Generator[Tuple[Union[int, str], T], T, None]: + """Return generator method iterating the contents of this class. + + This generator yields the `(key, value)` pairs of the underlying dictionary. If this object + was constructed from a list, the keys in this generator are simply the numeric indices. + + This generator also supports overriding the yielded value upon receiving any value other + than `None` from a `send` [1] instruction. + + [1]: https://docs.python.org/3/reference/expressions.html#generator.send + """ + for key, value in self.items(): + new_value = yield (key, value) + if new_value is not None: + self[key] = new_value diff --git a/qiskit_nature/problems/second_quantization/base_problem.py b/qiskit_nature/problems/second_quantization/base_problem.py index 77f39e1a00..9d742a0275 100644 --- a/qiskit_nature/problems/second_quantization/base_problem.py +++ b/qiskit_nature/problems/second_quantization/base_problem.py @@ -19,12 +19,13 @@ import numpy as np from qiskit.opflow import PauliSumOp, Z2Symmetries -from qiskit_nature import QiskitNatureError +from qiskit_nature import ListOrDictType, QiskitNatureError from qiskit_nature.converters.second_quantization import QubitConverter from qiskit_nature.deprecation import DeprecatedType, deprecate_property from qiskit_nature.drivers import QMolecule, WatsonHamiltonian from qiskit_nature.drivers import BaseDriver as LegacyBaseDriver from qiskit_nature.drivers.second_quantization import BaseDriver +from qiskit_nature.operators.second_quantization import SecondQuantizedOp from qiskit_nature.properties.second_quantization import GroupedSecondQuantizedProperty from qiskit_nature.results import EigenstateResult from qiskit_nature.transformers import BaseTransformer as LegacyBaseTransformer @@ -96,6 +97,8 @@ def __init__( self._grouped_property: Optional[GroupedSecondQuantizedProperty] = None self._grouped_property_transformed: Optional[GroupedSecondQuantizedProperty] = None + self._main_property_name: str = "" + @property # type: ignore[misc] @deprecate_property( "0.2.0", @@ -130,18 +133,24 @@ def grouped_property_transformed(self) -> Optional[GroupedSecondQuantizedPropert object.""" return self._grouped_property_transformed + @property + def main_property_name(self) -> str: + """Returns the name of the property producing the main operator.""" + return self._main_property_name + @property def num_particles(self) -> Optional[Tuple[int, int]]: """Returns the number of particles, if available.""" return None @abstractmethod - def second_q_ops(self): - """Returns a list of `SecondQuantizedOp` created based on a driver and transformations - provided. + def second_q_ops(self) -> ListOrDictType[SecondQuantizedOp]: + """Returns the second quantized operators associated with this Property. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. Returns: - A list of `SecondQuantizedOp` in the following order: ... . + A `list` or `dict` of `SecondQuantizedOp` objects. """ raise NotImplementedError() diff --git a/qiskit_nature/problems/second_quantization/electronic/electronic_structure_problem.py b/qiskit_nature/problems/second_quantization/electronic/electronic_structure_problem.py index c82e896642..cbbe59e97d 100644 --- a/qiskit_nature/problems/second_quantization/electronic/electronic_structure_problem.py +++ b/qiskit_nature/problems/second_quantization/electronic/electronic_structure_problem.py @@ -20,6 +20,7 @@ from qiskit.opflow import PauliSumOp from qiskit.opflow.primitive_ops import Z2Symmetries +from qiskit_nature import ListOrDictType from qiskit_nature.circuit.library.initial_states.hartree_fock import hartree_fock_bitstring_mapped from qiskit_nature.drivers import QMolecule from qiskit_nature.drivers.second_quantization import ElectronicStructureDriver @@ -52,6 +53,7 @@ def __init__( transformers: A list of transformations to be applied to the driver result. """ super().__init__(driver, transformers) + self._main_property_name = "ElectronicEnergy" @property def num_particles(self) -> Tuple[int, int]: @@ -62,14 +64,17 @@ def num_spin_orbitals(self) -> int: """Returns the number of spin orbitals.""" return self._grouped_property_transformed.get_property("ParticleNumber").num_spin_orbitals - def second_q_ops(self) -> List[SecondQuantizedOp]: - """Returns a list of `SecondQuantizedOp` created based on a driver and transformations - provided. + def second_q_ops(self) -> ListOrDictType[SecondQuantizedOp]: + """Returns the second quantized operators associated with this Property. + + If the arguments are returned as a `list`, the operators are in the following order: the + Hamiltonian operator, total particle number operator, total angular momentum operator, total + magnetization operator, and (if available) x, y, z dipole operators. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. Returns: - A list of `SecondQuantizedOp` in the following order: Hamiltonian operator, - total particle number operator, total angular momentum operator, total magnetization - operator, and (if available) x, y, z dipole operators. + A `list` or `dict` of `SecondQuantizedOp` objects. """ driver_result = self.driver.run() @@ -98,9 +103,9 @@ def second_q_ops(self) -> List[SecondQuantizedOp]: self._grouped_property = driver_result self._grouped_property_transformed = self._transform(self._grouped_property) - second_quantized_ops_list = self._grouped_property_transformed.second_q_ops() + second_quantized_ops = self._grouped_property_transformed.second_q_ops() - return second_quantized_ops_list + return second_quantized_ops def hopping_qeom_ops( self, @@ -186,9 +191,15 @@ def get_default_filter_criterion( # pylint: disable=unused-argument def filter_criterion(self, eigenstate, eigenvalue, aux_values): # the first aux_value is the evaluated number of particles - num_particles_aux = aux_values[0][0] + try: + num_particles_aux = aux_values["ParticleNumber"][0] + except TypeError: + num_particles_aux = aux_values[0][0] # the second aux_value is the total angular momentum which (for singlets) should be zero - total_angular_momentum_aux = aux_values[1][0] + try: + total_angular_momentum_aux = aux_values["AngularMomentum"][0] + except TypeError: + total_angular_momentum_aux = aux_values[1][0] particle_number = cast( ParticleNumber, self.grouped_property_transformed.get_property(ParticleNumber) ) diff --git a/qiskit_nature/problems/second_quantization/vibrational/vibrational_structure_problem.py b/qiskit_nature/problems/second_quantization/vibrational/vibrational_structure_problem.py index 19be156b84..e77cf04328 100644 --- a/qiskit_nature/problems/second_quantization/vibrational/vibrational_structure_problem.py +++ b/qiskit_nature/problems/second_quantization/vibrational/vibrational_structure_problem.py @@ -19,6 +19,7 @@ from qiskit.algorithms import EigensolverResult, MinimumEigensolverResult from qiskit.opflow import PauliSumOp +from qiskit_nature import ListOrDictType from qiskit_nature.drivers import WatsonHamiltonian from qiskit_nature.drivers.second_quantization import VibrationalStructureDriver from qiskit_nature.operators.second_quantization import SecondQuantizedOp @@ -55,14 +56,18 @@ def __init__( super().__init__(bosonic_driver, transformers) self.num_modals = num_modals self.truncation_order = truncation_order + self._main_property_name = "VibrationalEnergy" - def second_q_ops(self) -> List[SecondQuantizedOp]: - """Returns a list of `SecondQuantizedOp` created based on a driver and transformations - provided. + def second_q_ops(self) -> ListOrDictType[SecondQuantizedOp]: + """Returns the second quantized operators created based on the driver and transformations. + + If the arguments are returned as a `list`, the operators are in the following order: the + Vibrational Hamiltonian operator, occupied modal operators for each mode. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. Returns: - A list of `SecondQuantizedOp` in the following order: Vibrational Hamiltonian operator, - occupied modal operators for each mode. + A `list` or `dict` of `SecondQuantizedOp` objects. """ driver_result = self.driver.run() @@ -101,9 +106,9 @@ def second_q_ops(self) -> List[SecondQuantizedOp]: basis = HarmonicBasis(num_modals) self._grouped_property_transformed.basis = basis - second_quantized_ops_list = self._grouped_property_transformed.second_q_ops() + second_quantized_ops = self._grouped_property_transformed.second_q_ops() - return second_quantized_ops_list + return second_quantized_ops def hopping_qeom_ops( self, @@ -194,7 +199,8 @@ def get_default_filter_criterion( def filter_criterion(self, eigenstate, eigenvalue, aux_values): # the first num_modes aux_value is the evaluated number of particles for the given mode for mode in range(self.grouped_property_transformed.num_modes): - if aux_values is None or not np.isclose(aux_values[mode][0], 1): + _key = str(mode) if isinstance(aux_values, dict) else mode + if aux_values is None or not np.isclose(aux_values[_key][0], 1): return False return True diff --git a/qiskit_nature/properties/second_quantization/electronic/angular_momentum.py b/qiskit_nature/properties/second_quantization/electronic/angular_momentum.py index b11b537c70..deb94a667f 100644 --- a/qiskit_nature/properties/second_quantization/electronic/angular_momentum.py +++ b/qiskit_nature/properties/second_quantization/electronic/angular_momentum.py @@ -19,6 +19,7 @@ import numpy as np +from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import QMolecule from qiskit_nature.operators.second_quantization import FermionicOp from qiskit_nature.results import EigenstateResult @@ -102,8 +103,14 @@ def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "AngularMoment qmol.num_molecular_orbitals * 2, ) - def second_q_ops(self) -> List[FermionicOp]: - """Returns a list containing the angular momentum operator.""" + def second_q_ops(self) -> ListOrDictType[FermionicOp]: + """Returns the second quantized angular momentum operator. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `FermionicOp` objects. + """ x_h1, x_h2 = _calc_s_x_squared_ints(self._num_spin_orbitals) y_h1, y_h2 = _calc_s_y_squared_ints(self._num_spin_orbitals) z_h1, z_h2 = _calc_s_z_squared_ints(self._num_spin_orbitals) @@ -112,9 +119,14 @@ def second_q_ops(self) -> List[FermionicOp]: h1_ints = OneBodyElectronicIntegrals(ElectronicBasis.SO, h_1) h2_ints = TwoBodyElectronicIntegrals(ElectronicBasis.SO, h_2) - return [(h1_ints.to_second_q_op() + h2_ints.to_second_q_op()).reduce()] - # TODO: refactor after closing https://github.com/Qiskit/qiskit-terra/issues/6772 + op = (h1_ints.to_second_q_op() + h2_ints.to_second_q_op()).reduce() + + if not settings.dict_aux_operators: + return [op] + + return {self.name: op} + def interpret(self, result: EigenstateResult) -> None: """Interprets an :class:`~qiskit_nature.results.EigenstateResult` in this property's context. @@ -127,13 +139,15 @@ def interpret(self, result: EigenstateResult) -> None: if not isinstance(result.aux_operator_eigenvalues, list): aux_operator_eigenvalues = [result.aux_operator_eigenvalues] else: - aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore + aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore[assignment] for aux_op_eigenvalues in aux_operator_eigenvalues: if aux_op_eigenvalues is None: continue - if aux_op_eigenvalues[1] is not None: - total_angular_momentum = aux_op_eigenvalues[1][0].real # type: ignore + _key = self.name if isinstance(aux_op_eigenvalues, dict) else 1 + + if aux_op_eigenvalues[_key] is not None: + total_angular_momentum = aux_op_eigenvalues[_key][0].real # type: ignore result.total_angular_momentum.append(total_angular_momentum) if expected is not None: diff --git a/qiskit_nature/properties/second_quantization/electronic/dipole_moment.py b/qiskit_nature/properties/second_quantization/electronic/dipole_moment.py index e8d3c25abe..e637a4d60f 100644 --- a/qiskit_nature/properties/second_quantization/electronic/dipole_moment.py +++ b/qiskit_nature/properties/second_quantization/electronic/dipole_moment.py @@ -14,12 +14,13 @@ from typing import Dict, List, Optional, Tuple, cast +from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import QMolecule from qiskit_nature.operators.second_quantization import FermionicOp from qiskit_nature.results import EigenstateResult -from ..second_quantized_property import LegacyDriverResult from ...grouped_property import GroupedProperty +from ..second_quantized_property import LegacyDriverResult from .bases import ElectronicBasis from .integrals import ElectronicIntegrals, IntegralProperty, OneBodyElectronicIntegrals from .types import ElectronicProperty @@ -203,11 +204,24 @@ def dipole_along_axis(axis, ao_ints, mo_ints, energy_shift): reverse_dipole_sign=qmol.reverse_dipole_sign, ) - def second_q_ops(self) -> List[FermionicOp]: - """Returns a list of dipole moment operators along all Cartesian axes.""" - return [dip.second_q_ops()[0] for dip in self._properties.values()] + def second_q_ops(self) -> ListOrDictType[FermionicOp]: + """Returns the second quantized dipole moment operators. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `FermionicOp` objects. + """ + ops: ListOrDictType[FermionicOp] + if not settings.dict_aux_operators: + ops = [dip.second_q_ops()[0] for dip in self._properties.values()] + return ops + + ops = {} + for prop in iter(self): + ops.update(prop.second_q_ops()) + return ops - # TODO: refactor after closing https://github.com/Qiskit/qiskit-terra/issues/6772 def interpret(self, result: EigenstateResult) -> None: """Interprets an :class:`~qiskit_nature.results.EigenstateResult` in this property's context. @@ -222,30 +236,37 @@ def interpret(self, result: EigenstateResult) -> None: if not isinstance(result.aux_operator_eigenvalues, list): aux_operator_eigenvalues = [result.aux_operator_eigenvalues] else: - aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore + aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore[assignment] + for aux_op_eigenvalues in aux_operator_eigenvalues: if aux_op_eigenvalues is None: continue - if len(aux_op_eigenvalues) >= 6: - dipole_moment = [] - for moment in aux_op_eigenvalues[3:6]: - if moment is not None: - dipole_moment += [moment[0].real] # type: ignore - else: - dipole_moment += [None] - - result.computed_dipole_moment.append(cast(DipoleTuple, tuple(dipole_moment))) - dipole_shifts: Dict[str, Dict[str, complex]] = {} - for prop in self._properties.values(): - for name, shift in prop._shift.items(): - if name not in dipole_shifts.keys(): - dipole_shifts[name] = {} - dipole_shifts[name][prop._axis] = shift - - result.extracted_transformer_dipoles.append( - { - name: cast(DipoleTuple, (shift["x"], shift["y"], shift["z"])) - for name, shift in dipole_shifts.items() - } - ) + if isinstance(aux_op_eigenvalues, list) and len(aux_op_eigenvalues) < 6: + continue + + axes_order = {"x": 0, "y": 1, "z": 2} + dipole_moment = [None] * 3 + for prop in iter(self): + moment: Optional[Tuple[complex, complex]] + try: + moment = aux_op_eigenvalues[axes_order[prop._axis] + 3] # type: ignore + except KeyError: + moment = aux_op_eigenvalues.get(prop.name, None) # type: ignore + if moment is not None: + dipole_moment[axes_order[prop._axis]] = moment[0].real # type: ignore + + result.computed_dipole_moment.append(cast(DipoleTuple, tuple(dipole_moment))) + dipole_shifts: Dict[str, Dict[str, complex]] = {} + for prop in self._properties.values(): + for name, shift in prop._shift.items(): + if name not in dipole_shifts.keys(): + dipole_shifts[name] = {} + dipole_shifts[name][prop._axis] = shift + + result.extracted_transformer_dipoles.append( + { + name: cast(DipoleTuple, (shift["x"], shift["y"], shift["z"])) + for name, shift in dipole_shifts.items() + } + ) diff --git a/qiskit_nature/properties/second_quantization/electronic/electronic_structure_driver_result.py b/qiskit_nature/properties/second_quantization/electronic/electronic_structure_driver_result.py index 4cfb3d4efd..85f540d681 100644 --- a/qiskit_nature/properties/second_quantization/electronic/electronic_structure_driver_result.py +++ b/qiskit_nature/properties/second_quantization/electronic/electronic_structure_driver_result.py @@ -14,6 +14,7 @@ from typing import List, Tuple, cast +from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import Molecule from qiskit_nature.drivers import QMolecule from qiskit_nature.operators.second_quantization import FermionicOp @@ -98,20 +99,32 @@ def from_legacy_driver_result( return ret - def second_q_ops(self) -> List[FermionicOp]: - """Returns the list of :class:`~qiskit_nature.operators.second_quantization.FermioncOp`s - given by the properties contained in this one.""" - ops: List[FermionicOp] = [] - # TODO: refactor after closing https://github.com/Qiskit/qiskit-terra/issues/6772 - for cls in [ - ElectronicEnergy, - ParticleNumber, - AngularMomentum, - Magnetization, - ElectronicDipoleMoment, - ]: - prop = self.get_property(cls) # type: ignore - if prop is None: - continue - ops.extend(prop.second_q_ops()) + def second_q_ops(self) -> ListOrDictType[FermionicOp]: + """Returns the second quantized operators associated with the properties in this group. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `FermionicOp` objects. + """ + ops: ListOrDictType[FermionicOp] + + if not settings.dict_aux_operators: + ops = [] + for cls in [ + ElectronicEnergy, + ParticleNumber, + AngularMomentum, + Magnetization, + ElectronicDipoleMoment, + ]: + prop = self.get_property(cls) # type: ignore + if prop is None: + continue + ops.extend(prop.second_q_ops()) + return ops + + ops = {} + for prop in iter(self): + ops.update(prop.second_q_ops()) return ops diff --git a/qiskit_nature/properties/second_quantization/electronic/integrals/integral_property.py b/qiskit_nature/properties/second_quantization/electronic/integrals/integral_property.py index 3491ed5e52..8b4f1a4128 100644 --- a/qiskit_nature/properties/second_quantization/electronic/integrals/integral_property.py +++ b/qiskit_nature/properties/second_quantization/electronic/integrals/integral_property.py @@ -14,6 +14,7 @@ from typing import Dict, List, Optional +from qiskit_nature import ListOrDictType, settings from qiskit_nature.operators.second_quantization import FermionicOp from qiskit_nature.results import EigenstateResult @@ -129,14 +130,26 @@ def integral_operator(self, density: OneBodyElectronicIntegrals) -> OneBodyElect f"The {self.__class__.__name__}.integral_operator is not implemented!" ) - def second_q_ops(self) -> List[FermionicOp]: - """Returns a list containing the Hamiltonian constructed by the stored electronic integrals.""" + def second_q_ops(self) -> ListOrDictType[FermionicOp]: + """Returns the second quantized operator constructed from the contained electronic integrals. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `FermionicOp` objects. + """ ints = None if ElectronicBasis.SO in self._electronic_integrals: ints = self._electronic_integrals[ElectronicBasis.SO] elif ElectronicBasis.MO in self._electronic_integrals: ints = self._electronic_integrals[ElectronicBasis.MO] - return [sum(int.to_second_q_op() for int in ints.values()).reduce()] # type: ignore + + op = sum(int.to_second_q_op() for int in ints.values()).reduce() # type: ignore[union-attr] + + if not settings.dict_aux_operators: + return [op] + + return {self.name: op} @classmethod def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "IntegralProperty": diff --git a/qiskit_nature/properties/second_quantization/electronic/magnetization.py b/qiskit_nature/properties/second_quantization/electronic/magnetization.py index 23ccad2288..2e9d257e7f 100644 --- a/qiskit_nature/properties/second_quantization/electronic/magnetization.py +++ b/qiskit_nature/properties/second_quantization/electronic/magnetization.py @@ -12,8 +12,9 @@ """The Magnetization property.""" -from typing import cast, List +from typing import cast +from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import QMolecule from qiskit_nature.operators.second_quantization import FermionicOp from qiskit_nature.results import EigenstateResult @@ -60,8 +61,14 @@ def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "Magnetization qmol.num_molecular_orbitals * 2, ) - def second_q_ops(self) -> List[FermionicOp]: - """Returns a list containing the magnetization operator.""" + def second_q_ops(self) -> ListOrDictType[FermionicOp]: + """Returns the second quantized magnetization operator. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `SecondQuantizedOp` objects. + """ op = FermionicOp( [ (f"N_{o}", 0.5 if o < self._num_spin_orbitals // 2 else -0.5) @@ -70,9 +77,12 @@ def second_q_ops(self) -> List[FermionicOp]: register_length=self._num_spin_orbitals, display_format="sparse", ) - return [op] - # TODO: refactor after closing https://github.com/Qiskit/qiskit-terra/issues/6772 + if not settings.dict_aux_operators: + return [op] + + return {self.name: op} + def interpret(self, result: EigenstateResult) -> None: """Interprets an :class:`~qiskit_nature.results.EigenstateResult` in this property's context. @@ -84,12 +94,14 @@ def interpret(self, result: EigenstateResult) -> None: if not isinstance(result.aux_operator_eigenvalues, list): aux_operator_eigenvalues = [result.aux_operator_eigenvalues] else: - aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore + aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore[assignment] for aux_op_eigenvalues in aux_operator_eigenvalues: if aux_op_eigenvalues is None: continue - if aux_op_eigenvalues[2] is not None: - result.magnetization.append(aux_op_eigenvalues[2][0].real) # type: ignore + _key = self.name if isinstance(aux_op_eigenvalues, dict) else 2 + + if aux_op_eigenvalues[_key] is not None: + result.magnetization.append(aux_op_eigenvalues[_key][0].real) # type: ignore else: result.magnetization.append(None) diff --git a/qiskit_nature/properties/second_quantization/electronic/particle_number.py b/qiskit_nature/properties/second_quantization/electronic/particle_number.py index f54e70d68c..fd840cdc1f 100644 --- a/qiskit_nature/properties/second_quantization/electronic/particle_number.py +++ b/qiskit_nature/properties/second_quantization/electronic/particle_number.py @@ -17,6 +17,7 @@ import numpy as np +from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import QMolecule from qiskit_nature.operators.second_quantization import FermionicOp from qiskit_nature.results import EigenstateResult @@ -158,16 +159,25 @@ def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "ParticleNumbe qmol.mo_occ_b, ) - def second_q_ops(self) -> List[FermionicOp]: - """Returns a list containing the particle number operator.""" + def second_q_ops(self) -> ListOrDictType[FermionicOp]: + """Returns the second quantized particle number operator. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `FermionicOp` objects. + """ op = FermionicOp( [(f"N_{o}", 1.0) for o in range(self._num_spin_orbitals)], register_length=self._num_spin_orbitals, display_format="sparse", ) - return [op] - # TODO: refactor after closing https://github.com/Qiskit/qiskit-terra/issues/6772 + if not settings.dict_aux_operators: + return [op] + + return {self.name: op} + def interpret(self, result: EigenstateResult) -> None: """Interprets an :class:`~qiskit_nature.results.EigenstateResult` in this property's context. @@ -180,13 +190,15 @@ def interpret(self, result: EigenstateResult) -> None: if not isinstance(result.aux_operator_eigenvalues, list): aux_operator_eigenvalues = [result.aux_operator_eigenvalues] else: - aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore + aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore[assignment] for aux_op_eigenvalues in aux_operator_eigenvalues: if aux_op_eigenvalues is None: continue - if aux_op_eigenvalues[0] is not None: - n_particles = aux_op_eigenvalues[0][0].real # type: ignore + _key = self.name if isinstance(aux_op_eigenvalues, dict) else 0 + + if aux_op_eigenvalues[_key] is not None: + n_particles = aux_op_eigenvalues[_key][0].real # type: ignore result.num_particles.append(n_particles) if not np.isclose( diff --git a/qiskit_nature/properties/second_quantization/second_quantized_property.py b/qiskit_nature/properties/second_quantization/second_quantized_property.py index ecf02cdf75..99533224fe 100644 --- a/qiskit_nature/properties/second_quantization/second_quantized_property.py +++ b/qiskit_nature/properties/second_quantization/second_quantized_property.py @@ -13,9 +13,9 @@ """The SecondQuantizedProperty base class.""" from abc import abstractmethod -from typing import Any, List, Type, TypeVar, Union +from typing import Any, Type, TypeVar, Union -from qiskit_nature import QiskitNatureError +from qiskit_nature import ListOrDictType, QiskitNatureError from qiskit_nature.drivers import QMolecule, WatsonHamiltonian from qiskit_nature.operators.second_quantization import SecondQuantizedOp @@ -34,8 +34,14 @@ class SecondQuantizedProperty(Property): """ @abstractmethod - def second_q_ops(self) -> List[SecondQuantizedOp]: - """Returns the list of second quantized operators associated with this Property.""" + def second_q_ops(self) -> ListOrDictType[SecondQuantizedOp]: + """Returns the second quantized operators associated with this Property. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `SecondQuantizedOp` objects. + """ @classmethod @abstractmethod @@ -74,8 +80,11 @@ class GroupedSecondQuantizedProperty(GroupedProperty[T], SecondQuantizedProperty second-quantized properties.""" @abstractmethod - def second_q_ops(self) -> List[SecondQuantizedOp]: - """ - Returns the list of second quantized operators given by the properties contained in this - group. + def second_q_ops(self) -> ListOrDictType[SecondQuantizedOp]: + """Returns the second quantized operators associated with the properties in this group. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `SecondQuantizedOp` objects. """ diff --git a/qiskit_nature/properties/second_quantization/vibrational/occupied_modals.py b/qiskit_nature/properties/second_quantization/vibrational/occupied_modals.py index 50be7dfd42..5251ed738a 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/occupied_modals.py +++ b/qiskit_nature/properties/second_quantization/vibrational/occupied_modals.py @@ -14,6 +14,7 @@ from typing import List, Optional, Tuple +from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import WatsonHamiltonian from qiskit_nature.operators.second_quantization import VibrationalOp from qiskit_nature.results import EigenstateResult @@ -58,13 +59,21 @@ def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "OccupiedModal return cls() - def second_q_ops(self) -> List[VibrationalOp]: - """Returns a list of operators each evaluating the occupied modal on a mode.""" + def second_q_ops(self) -> ListOrDictType[VibrationalOp]: + """Returns the second quantized operators indicating the occupied modals per mode. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `VibrationalOp` objects. + """ num_modals_per_mode = self.basis._num_modals_per_mode num_modes = len(num_modals_per_mode) - ops = [self._get_mode_op(mode) for mode in range(num_modes)] - return ops + if not settings.dict_aux_operators: + return [self._get_mode_op(mode) for mode in range(num_modes)] + + return {str(mode): self._get_mode_op(mode) for mode in range(num_modes)} def _get_mode_op(self, mode: int) -> VibrationalOp: """Constructs an operator to evaluate which modal of a given mode is occupied. @@ -96,15 +105,16 @@ def interpret(self, result: EigenstateResult) -> None: if not isinstance(result.aux_operator_eigenvalues, list): aux_operator_eigenvalues = [result.aux_operator_eigenvalues] else: - aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore + aux_operator_eigenvalues = result.aux_operator_eigenvalues # type: ignore[assignment] num_modes = len(self._basis._num_modals_per_mode) for aux_op_eigenvalues in aux_operator_eigenvalues: occ_modals = [] for mode in range(num_modes): - if aux_op_eigenvalues[mode] is not None: - occ_modals.append(aux_op_eigenvalues[mode][0].real) # type: ignore + _key = str(mode) if isinstance(aux_op_eigenvalues, dict) else mode + if aux_op_eigenvalues[_key] is not None: + occ_modals.append(aux_op_eigenvalues[_key][0].real) # type: ignore else: occ_modals.append(None) result.num_occupied_modals_per_mode.append(occ_modals) # type: ignore diff --git a/qiskit_nature/properties/second_quantization/vibrational/vibrational_energy.py b/qiskit_nature/properties/second_quantization/vibrational/vibrational_energy.py index e90708026a..4d7514137b 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/vibrational_energy.py +++ b/qiskit_nature/properties/second_quantization/vibrational/vibrational_energy.py @@ -14,6 +14,7 @@ from typing import cast, Dict, List, Optional, Tuple +from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import WatsonHamiltonian from qiskit_nature.operators.second_quantization import VibrationalOp from qiskit_nature.results import EigenstateResult @@ -132,15 +133,25 @@ def get_vibrational_integral(self, num_body_terms: int) -> Optional[VibrationalI """ return self._vibrational_integrals.get(num_body_terms, None) - def second_q_ops(self) -> List[VibrationalOp]: - """Returns a list containing the Hamiltonian constructed by the stored integrals.""" + def second_q_ops(self) -> ListOrDictType[VibrationalOp]: + """Returns the second quantized vibrational energy operator. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `VibrationalOp` objects. + """ ops = [] for num_body, ints in self._vibrational_integrals.items(): if self._truncation_order is not None and num_body > self._truncation_order: break ints.basis = self.basis ops.append(ints.to_second_q_op()) - return [sum(ops)] # type: ignore + + if not settings.dict_aux_operators: + return [sum(ops)] # type: ignore[list-item] + + return {self.name: sum(ops)} # type: ignore[dict-item] def interpret(self, result: EigenstateResult) -> None: """Interprets an :class:`~qiskit_nature.results.EigenstateResult` in this property's context. diff --git a/qiskit_nature/properties/second_quantization/vibrational/vibrational_structure_driver_result.py b/qiskit_nature/properties/second_quantization/vibrational/vibrational_structure_driver_result.py index f2d892cb58..ba6c85b3d8 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/vibrational_structure_driver_result.py +++ b/qiskit_nature/properties/second_quantization/vibrational/vibrational_structure_driver_result.py @@ -12,8 +12,9 @@ """The VibrationalStructureDriverResult class.""" -from typing import List, cast +from typing import cast +from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import WatsonHamiltonian from qiskit_nature.operators.second_quantization import VibrationalOp @@ -75,14 +76,25 @@ def from_legacy_driver_result( return ret - def second_q_ops(self) -> List[VibrationalOp]: - """Returns the list of :class:`~qiskit_nature.operators.second_quantization.VibrationalOp`s - given by the properties contained in this one.""" - ops: List[VibrationalOp] = [] - # TODO: make aux_ops a Dict? Then we don't need to hard-code the order of these properties. - for cls in [VibrationalEnergy, OccupiedModals]: - prop = self.get_property(cls) # type: ignore - if prop is None: - continue - ops.extend(prop.second_q_ops()) + def second_q_ops(self) -> ListOrDictType[VibrationalOp]: + """Returns the second quantized operators associated with the properties in this group. + + The actual return-type is determined by `qiskit_nature.settings.dict_aux_operators`. + + Returns: + A `list` or `dict` of `VibrationalOp` objects. + """ + ops: ListOrDictType[VibrationalOp] + if not settings.dict_aux_operators: + ops = [] + for cls in [VibrationalEnergy, OccupiedModals]: + prop = self.get_property(cls) # type: ignore + if prop is None: + continue + ops.extend(prop.second_q_ops()) + return ops + + ops = {} + for prop in iter(self): + ops.update(prop.second_q_ops()) return ops diff --git a/qiskit_nature/results/eigenstate_result.py b/qiskit_nature/results/eigenstate_result.py index de9dfdab22..d99b09841f 100644 --- a/qiskit_nature/results/eigenstate_result.py +++ b/qiskit_nature/results/eigenstate_result.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """Eigenstate results module.""" -from typing import Optional, List, Union +from typing import Optional, List, Tuple, Union import inspect import numpy as np @@ -22,6 +22,8 @@ from qiskit.algorithms import AlgorithmResult from qiskit.opflow import OperatorBase +from qiskit_nature import ListOrDictType + class EigenstateResult(AlgorithmResult): """The eigenstate result interface.""" @@ -44,7 +46,9 @@ def __init__(self) -> None: ] ] ] = None - self._aux_operator_eigenvalues: Optional[List[float]] = None + self._aux_operator_eigenvalues: Optional[ + List[ListOrDictType[Tuple[complex, complex]]] + ] = None self._raw_result: Optional[AlgorithmResult] = None @property @@ -129,12 +133,14 @@ def groundstate( return None @property - def aux_operator_eigenvalues(self) -> Optional[List[float]]: + def aux_operator_eigenvalues(self) -> Optional[List[ListOrDictType[Tuple[complex, complex]]]]: """return aux operator eigen values""" return self._aux_operator_eigenvalues @aux_operator_eigenvalues.setter - def aux_operator_eigenvalues(self, value: List[float]) -> None: + def aux_operator_eigenvalues( + self, value: List[ListOrDictType[Tuple[complex, complex]]] + ) -> None: """set aux operator eigen values""" self._aux_operator_eigenvalues = value diff --git a/qiskit_nature/settings.py b/qiskit_nature/settings.py new file mode 100644 index 0000000000..aa753987ae --- /dev/null +++ b/qiskit_nature/settings.py @@ -0,0 +1,55 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +"""Qiskit Nature Settings.""" + +import warnings + + +class QiskitNatureSettings: + """Global settings for Qiskit Nature.""" + + def __init__(self): + self._dict_aux_operators: bool = False + + @property + def dict_aux_operators(self) -> bool: + """Return whether `aux_operators` are dictionary- or list-based.""" + if not self._dict_aux_operators: + warnings.warn( + DeprecationWarning( + "List-based `aux_operators` are deprecated as of version 0.3.0 and support for " + "them will be removed no sooner than 3 months after the release. Instead, use " + "dict-based `aux_operators`. You can switch to the dict-based interface " + "immediately, by setting `qiskit_nature.settings.dict_aux_operators` to `True`." + ) + ) + + return self._dict_aux_operators + + @dict_aux_operators.setter + def dict_aux_operators(self, dict_aux_operators: bool) -> None: + """Set whether `aux_operators` are dictionary- or list-based.""" + if not dict_aux_operators: + warnings.warn( + DeprecationWarning( + "List-based `aux_operators` are deprecated as of version 0.3.0 and support for " + "them will be removed no sooner than 3 months after the release. Instead, use " + "dict-based `aux_operators`. You can switch to the dict-based interface " + "immediately, by setting `qiskit_nature.settings.dict_aux_operators` to `True`." + ) + ) + + self._dict_aux_operators = dict_aux_operators + + +settings = QiskitNatureSettings() diff --git a/test/algorithms/ground_state_solvers/test_groundstate_eigensolver.py b/test/algorithms/ground_state_solvers/test_groundstate_eigensolver.py index 227a9a8cb6..a77eefea76 100644 --- a/test/algorithms/ground_state_solvers/test_groundstate_eigensolver.py +++ b/test/algorithms/ground_state_solvers/test_groundstate_eigensolver.py @@ -29,6 +29,7 @@ from qiskit.test import slow_test from qiskit.utils import QuantumInstance, algorithm_globals +from qiskit_nature import settings from qiskit_nature.algorithms import ( GroundStateEigensolver, VQEUCCFactory, @@ -140,6 +141,56 @@ def test_aux_ops_reusability(self): frozenset(a.to_list()) == frozenset(b.to_list()) for a, b in zip(aux_ops, aux_ops_copy) ) + def test_dict_based_aux_ops(self): + """Test the `test_dict_based_aux_ops` variant""" + try: + settings.dict_aux_operators = True + solver = NumPyMinimumEigensolverFactory() + calc = GroundStateEigensolver(self.qubit_converter, solver) + res = calc.solve(self.electronic_structure_problem) + self.assertAlmostEqual(res.total_energies[0], self.reference_energy, places=6) + self.assertTrue( + np.all(isinstance(aux_op, dict) for aux_op in res.aux_operator_eigenvalues) + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["ParticleNumber"][0], 2.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["ParticleNumber"][1], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["AngularMomentum"][0], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["AngularMomentum"][1], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["Magnetization"][0], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["Magnetization"][1], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["DipoleMomentX"][0], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["DipoleMomentX"][1], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["DipoleMomentY"][0], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["DipoleMomentY"][1], 0.0, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["DipoleMomentZ"][0], -1.3889487, places=6 + ) + self.assertAlmostEqual( + res.aux_operator_eigenvalues[0]["DipoleMomentZ"][1], 0.0, places=6 + ) + finally: + settings.dict_aux_operators = False + def _setup_evaluation_operators(self): # first we run a ground state calculation solver = VQEUCCFactory(QuantumInstance(BasicAer.get_backend("statevector_simulator")))