From 617f54a47abd7328b67b8bf80bcaaecb11ec4c9f Mon Sep 17 00:00:00 2001 From: Anthony-Gandon Date: Thu, 12 Jan 2023 08:43:50 +0100 Subject: [PATCH] Update API to allow QubitMapper where QubitConverter is used. (#999) * Failing tests but stable QubitConverter and QubitMapper can now be interverted * Modifications of the mappers map() * Update API for QubitMapper * Adds release note * Small corrections after first feedback. * Fix indentation: * Change for map_all to map and map to map_single. * Updated release note * Updated release note 2 * Small updates before pushing * small fix spell * First review of docstrings * Other docstrings * Make black * Fix mypy * Fix comments * fix * Rolled back the type expansion of the map and convert_match methods, added new tests to cover more cases * Implement the unwrapper in the _ListOrDict class * Fix spelling * Fix spell 2 --- .../excited_states_eigensolver.py | 37 ++-- .../excited_states_solver.py | 5 +- .../algorithms/excited_states_solvers/qeom.py | 92 +++++++--- .../qeom_electronic_ops_builder.py | 26 +-- .../qeom_vibrational_ops_builder.py | 19 ++- .../ground_state_eigensolver.py | 42 +++-- .../ground_state_solver.py | 14 +- .../minimum_eigensolver_factory.py | 6 +- .../numpy_minimum_eigensolver_factory.py | 6 +- .../vqe_ucc_factory.py | 5 +- .../vqe_uvcc_factory.py | 4 +- .../circuit/library/ansatzes/puccd.py | 10 +- .../circuit/library/ansatzes/succd.py | 9 +- .../second_q/circuit/library/ansatzes/ucc.py | 25 ++- .../circuit/library/ansatzes/uccsd.py | 10 +- .../second_q/circuit/library/ansatzes/uvcc.py | 21 ++- .../circuit/library/ansatzes/uvccsd.py | 9 +- .../circuit/library/bogoliubov_transform.py | 12 +- .../fermionic_gaussian_state.py | 18 +- .../library/initial_states/hartree_fock.py | 39 +++-- .../initial_states/slater_determinant.py | 13 +- .../circuit/library/initial_states/vscf.py | 27 +-- qiskit_nature/second_q/mappers/bksf.py | 2 +- .../second_q/mappers/bravyi_kitaev_mapper.py | 2 +- .../second_q/mappers/direct_mapper.py | 2 +- .../second_q/mappers/fermionic_mapper.py | 2 +- .../second_q/mappers/jordan_wigner_mapper.py | 2 +- .../second_q/mappers/linear_mapper.py | 2 +- .../second_q/mappers/logarithmic_mapper.py | 2 +- .../second_q/mappers/parity_mapper.py | 2 +- .../second_q/mappers/qubit_converter.py | 145 +++------------- .../second_q/mappers/qubit_mapper.py | 104 ++++++++++- qiskit_nature/second_q/mappers/spin_mapper.py | 3 +- .../second_q/mappers/vibrational_mapper.py | 3 +- .../second_q/problems/base_problem.py | 8 +- .../problems/electronic_structure_problem.py | 8 +- ...apper-qubitconverter-d237a7b596d50207.yaml | 13 ++ .../resources/__init__.py | 2 +- .../resources/expected_qeom_ops.py | 2 +- .../expected_transition_amplitudes.py | 2 +- .../test_excited_states_solvers.py | 61 ++++++- ...test_excited_states_solvers_auxiliaries.py | 2 +- .../test_qeom_electronic_ops.py | 36 +++- .../test_qeom_vibrational_ops.py | 34 +++- .../test_groundstate_eigensolver.py | 10 +- .../circuit/library/ansatzes/test_chc.py | 24 ++- .../circuit/library/ansatzes/test_puccd.py | 32 ++-- .../circuit/library/ansatzes/test_succd.py | 161 ++++++++++++------ .../circuit/library/ansatzes/test_ucc.py | 68 ++++++-- .../circuit/library/ansatzes/test_uccsd.py | 89 ++++++---- .../circuit/library/ansatzes/test_uvcc.py | 64 ++++--- .../test_fermionic_gaussian_state.py | 45 +++-- .../initial_states/test_hartree_fock.py | 54 +++++- .../initial_states/test_slater_determinant.py | 41 +++-- .../library/initial_states/test_vscf.py | 15 +- .../library/test_bogoliubov_transform.py | 36 +++- .../mappers/test_jordan_wigner_mapper.py | 44 +++++ test/second_q/mappers/test_qubit_converter.py | 19 +++ 58 files changed, 1099 insertions(+), 491 deletions(-) create mode 100644 releasenotes/notes/update-api-for-qubitmapper-qubitconverter-d237a7b596d50207.yaml diff --git a/qiskit_nature/second_q/algorithms/excited_states_solvers/excited_states_eigensolver.py b/qiskit_nature/second_q/algorithms/excited_states_solvers/excited_states_eigensolver.py index 601d7c77a9..ce89ede28c 100644 --- a/qiskit_nature/second_q/algorithms/excited_states_solvers/excited_states_eigensolver.py +++ b/qiskit_nature/second_q/algorithms/excited_states_solvers/excited_states_eigensolver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2022. +# (C) Copyright IBM 2020, 2023. # # 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 @@ -21,7 +21,7 @@ from qiskit.algorithms.eigensolvers import Eigensolver from qiskit.opflow import PauliSumOp -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.operators import SparseLabelOp from qiskit_nature.second_q.problems import BaseProblem from qiskit_nature.second_q.problems import EigenstateResult @@ -37,15 +37,14 @@ class ExcitedStatesEigensolver(ExcitedStatesSolver): def __init__( self, - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, solver: Union[Eigensolver, EigensolverFactory], ) -> None: """ Args: - qubit_converter: The ``QubitConverter`` to use for mapping and symmetry reduction. The - Z2 symmetries stored in this instance are the basis for the - commutativity information returned by this method. + qubit_converter: The ``QubitConverter`` or ``QubitMapper`` to use for mapping and symmetry + reduction. solver: Minimum Eigensolver or MESFactory object. """ self._qubit_converter = qubit_converter @@ -72,19 +71,27 @@ def get_qubit_operators( num_particles = getattr(problem, "num_particles", None) - main_operator = self._qubit_converter.convert( - main_second_q_op, - num_particles=num_particles, - sector_locator=problem.symmetry_sector_locator, - ) - aux_ops = self._qubit_converter.convert_match(aux_second_q_ops) + if isinstance(self._qubit_converter, QubitConverter): + main_operator = self._qubit_converter.convert( + main_second_q_op, + num_particles=num_particles, + sector_locator=problem.symmetry_sector_locator, + ) + aux_ops = self._qubit_converter.convert_match(aux_second_q_ops) + + else: + main_operator = self._qubit_converter.map(main_second_q_op) + aux_ops = self._qubit_converter.map(aux_second_q_ops) if aux_operators is not None: for name_aux, aux_op in aux_operators.items(): if isinstance(aux_op, SparseLabelOp): - converted_aux_op = self._qubit_converter.convert_match( - aux_op, suppress_none=True - ) + if isinstance(self._qubit_converter, QubitConverter): + converted_aux_op = self._qubit_converter.convert_match( + aux_op, suppress_none=True + ) + else: + converted_aux_op = self._qubit_converter.map(aux_op) else: converted_aux_op = aux_op if name_aux in aux_ops.keys(): diff --git a/qiskit_nature/second_q/algorithms/excited_states_solvers/excited_states_solver.py b/qiskit_nature/second_q/algorithms/excited_states_solvers/excited_states_solver.py index ebf99160da..f3b6533ed9 100644 --- a/qiskit_nature/second_q/algorithms/excited_states_solvers/excited_states_solver.py +++ b/qiskit_nature/second_q/algorithms/excited_states_solvers/excited_states_solver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2022. +# (C) Copyright IBM 2020, 2023. # # 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 @@ -56,7 +56,8 @@ def get_qubit_operators( problem: BaseProblem, aux_operators: Optional[dict[str, Union[SparseLabelOp, PauliSumOp]]] = None, ) -> Tuple[PauliSumOp, Optional[dict[str, PauliSumOp]]]: - """Gets the operator and auxiliary operators, and transforms the provided auxiliary operators. + """Gets the operator and auxiliary operators, and transforms the provided auxiliary operators + using a ``QubitConverter`` or ``QubitMapper``. If the user-provided ``aux_operators`` contain a name which clashes with an internally constructed auxiliary operator, then the corresponding internal operator will be overridden by the user-provided operator. diff --git a/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom.py b/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom.py index cf4fb2e527..c3b779c264 100644 --- a/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom.py +++ b/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom.py @@ -47,7 +47,7 @@ from qiskit_nature.second_q.algorithms.excited_states_solvers.excited_states_solver import ( ExcitedStatesSolver, ) -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.operators import SparseLabelOp from qiskit_nature.second_q.problems import ( BaseProblem, @@ -158,7 +158,7 @@ def __init__( self._untapered_qubit_op_main: QubitOperator | None = None @property - def qubit_converter(self) -> QubitConverter: + def qubit_converter(self) -> QubitConverter | QubitMapper: """Returns the qubit_converter object defined in the ground state solver.""" return self._gsc.qubit_converter @@ -193,21 +193,35 @@ def get_qubit_operators( # 1. Convert the main operator (hamiltonian) to QubitOperator and apply two qubit reduction num_particles = getattr(problem, "num_particles", None) - main_op = self.qubit_converter.convert_only( - main_operator, - num_particles=num_particles, - ) + + if isinstance(self.qubit_converter, QubitConverter): + main_op = self.qubit_converter.convert_only( + main_operator, + num_particles=num_particles, + ) + else: + main_op = self.qubit_converter.map(main_operator) + # aux_ops set to None if the solver does not support auxiliary operators. aux_ops = None if self.solver.supports_aux_operators(): - self.qubit_converter.force_match(num_particles=num_particles) - aux_ops = self.qubit_converter.convert_match(aux_second_q_operators) + if isinstance(self.qubit_converter, QubitConverter): + self.qubit_converter.force_match(num_particles=num_particles) + aux_ops = self.qubit_converter.convert_match(aux_second_q_operators) + + else: + aux_ops = self.qubit_converter.map(aux_second_q_operators) + cast(ListOrDictType[QubitOperator], aux_ops) if aux_operators is not None: for name, op in aux_operators.items(): if isinstance(op, (SparseLabelOp)): - converted_aux_op = self.qubit_converter.convert_match(op) + if isinstance(self.qubit_converter, QubitConverter): + converted_aux_op = self.qubit_converter.convert_match(op) + else: + converted_aux_op = self.qubit_converter.map(op) + else: converted_aux_op = op if name in aux_ops.keys(): @@ -224,12 +238,19 @@ def get_qubit_operators( # 2. Find the z2symmetries, set them in the qubit_converter, and apply the first step of the # tapering. - _, z2symmetries = self.qubit_converter.find_taper_op( - main_op, problem.symmetry_sector_locator - ) - self.qubit_converter.force_match(z2symmetries=z2symmetries) - untap_main_op = self.qubit_converter.convert_clifford(main_op) - untap_aux_ops = self.qubit_converter.convert_clifford(aux_ops) + if isinstance(self.qubit_converter, QubitConverter): + _, z2symmetries = self.qubit_converter.find_taper_op( + main_op, problem.symmetry_sector_locator + ) + self.qubit_converter.force_match(z2symmetries=z2symmetries) + untap_main_op = self.qubit_converter.convert_clifford(main_op) + untap_aux_ops = self.qubit_converter.convert_clifford(aux_ops) + else: + # TODO: Issue #974 sketches the construction of a Tapered Qubit Mapper which would implement + # the logic of the symmetries. Here, there should be a check for a Tapered Qubit Mapper and + # a similar logic that used above. + untap_main_op = main_op + untap_aux_ops = aux_ops # 4. If a MinimumEigensolverFactory was provided, then an additional call to get_solver() is # required. @@ -264,7 +285,10 @@ def solve( # 2. Run ground state calculation with fully tapered custom auxiliary operators # Note that the solve() method includes the `second_q' auxiliary operators - tap_aux_operators = self.qubit_converter.symmetry_reduce_clifford(untap_aux_ops) + if isinstance(self.qubit_converter, QubitConverter): + tap_aux_operators = self.qubit_converter.symmetry_reduce_clifford(untap_aux_ops) + else: + tap_aux_operators = untap_aux_ops groundstate_result = self._gsc.solve(problem, tap_aux_operators) ground_state = groundstate_result.eigenstates[0] @@ -383,9 +407,12 @@ def _build_one_sector(available_hopping_ops): right_op_2 = available_hopping_ops.get(f"Edag_{n_u}") to_be_computed_list.append((m_u, n_u, left_op_1, right_op_1, right_op_2)) - try: - z2_symmetries = self._gsc.qubit_converter.z2symmetries - except AttributeError: + if isinstance(self.qubit_converter, QubitConverter): + try: + z2_symmetries = self.qubit_converter.z2symmetries + except AttributeError: + z2_symmetries = Z2Symmetries([], [], []) + else: z2_symmetries = Z2Symmetries([], [], []) if not z2_symmetries.is_empty(): @@ -583,11 +610,17 @@ def _prepare_expansion_basis( size = int(len(list(excitation_indices.keys())) // 2) # Small workaround to apply two_qubit_reduction to a list with convert_match() - z2_symmetries = self.qubit_converter.z2symmetries - self.qubit_converter.force_match(z2symmetries=Z2Symmetries([], [], [])) - reduced_hopping_ops = self.qubit_converter.convert_match(hopping_operators) - self.qubit_converter.force_match(z2symmetries=z2_symmetries) - untap_hopping_ops = self.qubit_converter.convert_clifford(reduced_hopping_ops) + if isinstance(self.qubit_converter, QubitConverter): + num_particles = self.qubit_converter.num_particles + + reduced_hopping_ops = {} + for hopping_name, hopping_op in hopping_operators.items(): + reduced_hopping_ops[hopping_name] = self.qubit_converter._two_qubit_reduce( + hopping_op, num_particles + ) + untap_hopping_ops = self.qubit_converter.convert_clifford(reduced_hopping_ops) + else: + untap_hopping_ops = hopping_operators return untap_hopping_ops, type_of_commutativities, size @@ -699,7 +732,10 @@ def _build_excitation_operators( """ untap_hopping_ops, _, size = expansion_basis_data - tap_hopping_ops = self.qubit_converter.symmetry_reduce_clifford(untap_hopping_ops) + if isinstance(self.qubit_converter, QubitConverter): + tap_hopping_ops = self.qubit_converter.symmetry_reduce_clifford(untap_hopping_ops) + else: + tap_hopping_ops = untap_hopping_ops additionnal_measurements = estimate_observables( self._estimator, reference_state[0], tap_hopping_ops, reference_state[1] @@ -843,7 +879,11 @@ def _evaluate_observables_excited_states( ) # 3. Measure observables - tap_op_aux_op_dict = self.qubit_converter.symmetry_reduce_clifford(op_aux_op_dict) + if isinstance(self.qubit_converter, QubitConverter): + tap_op_aux_op_dict = self.qubit_converter.symmetry_reduce_clifford(op_aux_op_dict) + else: + tap_op_aux_op_dict = op_aux_op_dict + aux_measurements = estimate_observables( self._estimator, reference_state[0], tap_op_aux_op_dict, reference_state[1] ) diff --git a/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom_electronic_ops_builder.py b/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom_electronic_ops_builder.py index 055e40e6a3..28e35a0dea 100644 --- a/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom_electronic_ops_builder.py +++ b/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom_electronic_ops_builder.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -16,14 +16,14 @@ from typing import Callable, Dict, List, Tuple -from qiskit.opflow import PauliSumOp +from qiskit.opflow import PauliSumOp, Z2Symmetries from qiskit.tools import parallel_map from qiskit.utils import algorithm_globals from qiskit_nature import QiskitNatureError from qiskit_nature.second_q.circuit.library import UCC from qiskit_nature.second_q.operators import FermionicOp -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper def build_electronic_ops( @@ -36,7 +36,7 @@ def build_electronic_ops( [int, tuple[int, int]], list[tuple[tuple[int, ...], tuple[int, ...]]], ], - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, ) -> Tuple[ Dict[str, PauliSumOp], Dict[str, List[bool]], @@ -54,9 +54,10 @@ def build_electronic_ops( - and finally a callable which can be used to specify a custom list of excitations. For more details on how to write such a function refer to the default method, :meth:`generate_fermionic_excitations`. - qubit_converter: The ``QubitConverter`` to use for mapping and symmetry reduction. The Z2 - symmetries stored in this instance are the basis for the commutativity - information returned by this method. + qubit_converter: The ``QubitConverter`` or ``QubitMapper`` to use for mapping and symmetry + reduction. The Z2 symmetries stored in this instance are the basis for the commutativity + information returned by this method. These symmetries are set to `None` when a + ``QubitMapper`` is used. Returns: A tuple containing the hopping operators, the types of commutativities and the excitation @@ -100,7 +101,7 @@ def build_electronic_ops( def _build_single_hopping_operator( excitation: Tuple[Tuple[int, ...], Tuple[int, ...]], num_spatial_orbitals: int, - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, ) -> Tuple[PauliSumOp, List[bool]]: label = [] for occ in excitation[0]: @@ -109,8 +110,13 @@ def _build_single_hopping_operator( label.append(f"-_{unocc}") fer_op = FermionicOp({" ".join(label): 1.0}, num_spin_orbitals=2 * num_spatial_orbitals) - qubit_op = qubit_converter.convert_only(fer_op, qubit_converter.num_particles) - z2_symmetries = qubit_converter.z2symmetries + if isinstance(qubit_converter, QubitConverter): + qubit_op = qubit_converter.convert_only(fer_op, num_particles=qubit_converter.num_particles) + z2_symmetries = qubit_converter.z2symmetries + + else: + qubit_op = qubit_converter.map(fer_op) + z2_symmetries = Z2Symmetries([], [], []) commutativities = [] if not z2_symmetries.is_empty(): diff --git a/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom_vibrational_ops_builder.py b/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom_vibrational_ops_builder.py index f1502d1a4e..c8330ee9ef 100644 --- a/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom_vibrational_ops_builder.py +++ b/qiskit_nature/second_q/algorithms/excited_states_solvers/qeom_vibrational_ops_builder.py @@ -22,7 +22,7 @@ from qiskit_nature.second_q.circuit.library import UVCC from qiskit_nature.second_q.operators import VibrationalOp -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper def build_vibrational_ops( @@ -34,7 +34,7 @@ def build_vibrational_ops( [int, tuple[int, int]], list[tuple[tuple[int, ...], tuple[int, ...]]], ], - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, ) -> Tuple[ Dict[str, PauliSumOp], Dict[str, List[bool]], @@ -50,9 +50,9 @@ def build_vibrational_ops( - and finally a callable which can be used to specify a custom list of excitations. For more details on how to write such a function refer to the default method, :meth:`generate_vibrational_excitations`. - qubit_converter: The ``QubitConverter`` to use for mapping and symmetry reduction. The Z2 - symmetries stored in this instance are the basis for the commutativity - information returned by this method. + qubit_converter: The ``QubitConverter`` or ``QubitMapper`` to use for mapping and symmetry + reduction. Note that the ``QubitConverter`` will use its stored Z2 symmetries as basis for + the commutativity information returned by this method. Returns: Dict of hopping operators, dict of commutativity types and dict of excitation indices. """ @@ -91,7 +91,7 @@ def build_vibrational_ops( def _build_single_hopping_operator( excitation: Tuple[Tuple[int, ...], Tuple[int, ...]], num_modals: List[int], - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, ) -> PauliSumOp: label = [] for occ in excitation[0]: @@ -100,6 +100,11 @@ def _build_single_hopping_operator( label.append(f"-_{VibrationalOp.build_dual_index(num_modals, unocc)}") vibrational_op = VibrationalOp({" ".join(label): 1}, num_modals) - qubit_op: PauliSumOp = qubit_converter.convert_match(vibrational_op) + + qubit_op: PauliSumOp + if isinstance(qubit_converter, QubitConverter): + qubit_op = qubit_converter.convert_match(vibrational_op) + else: + qubit_op = qubit_converter.map(vibrational_op) return qubit_op diff --git a/qiskit_nature/second_q/algorithms/ground_state_solvers/ground_state_eigensolver.py b/qiskit_nature/second_q/algorithms/ground_state_solvers/ground_state_eigensolver.py index 440c4a26c3..0c1bce9d36 100644 --- a/qiskit_nature/second_q/algorithms/ground_state_solvers/ground_state_eigensolver.py +++ b/qiskit_nature/second_q/algorithms/ground_state_solvers/ground_state_eigensolver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2022. +# (C) Copyright IBM 2020, 2023. # # 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 @@ -19,7 +19,7 @@ from qiskit.algorithms.minimum_eigensolvers import MinimumEigensolver from qiskit_nature.second_q.operators import SparseLabelOp -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.problems import BaseProblem from qiskit_nature.second_q.problems import EigenstateResult @@ -34,13 +34,14 @@ class GroundStateEigensolver(GroundStateSolver): def __init__( self, - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, solver: MinimumEigensolver | MinimumEigensolverFactory, ) -> None: """ Args: - qubit_converter: A class that converts second quantized operator to qubit operator - according to a mapper it is initialized with. + qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` or + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance that converts a second + quantized operator to qubit operators and applies subsequent qubit reduction. solver: Minimum Eigensolver or MESFactory object, e.g. the VQEUCCSDFactory. """ super().__init__(qubit_converter) @@ -64,10 +65,6 @@ def solve( problem: A class encoding a problem to be solved. aux_operators: Additional auxiliary operators to evaluate. - Raises: - 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`). - Returns: An interpreted :class:`~.EigenstateResult`. For more information see also :meth:`~.BaseProblem.interpret`. @@ -92,20 +89,29 @@ def get_qubit_operators( num_particles = getattr(problem, "num_particles", None) - main_operator = self._qubit_converter.convert( - main_second_q_op, - num_particles=num_particles, - sector_locator=problem.symmetry_sector_locator, - ) - aux_ops = self._qubit_converter.convert_match(aux_second_q_ops) + if isinstance(self._qubit_converter, QubitConverter): + main_operator = self._qubit_converter.convert( + main_second_q_op, + num_particles=num_particles, + sector_locator=problem.symmetry_sector_locator, + ) + aux_ops = self._qubit_converter.convert_match(aux_second_q_ops) + else: + main_operator = self._qubit_converter.map(main_second_q_op) + aux_ops = self._qubit_converter.map(aux_second_q_ops) + if aux_operators is not None: for name_aux, aux_op in aux_operators.items(): if isinstance(aux_op, SparseLabelOp): - converted_aux_op = self._qubit_converter.convert_match( - aux_op, suppress_none=True - ) + if isinstance(self._qubit_converter, QubitConverter): + converted_aux_op = self._qubit_converter.convert_match( + aux_op, suppress_none=True + ) + else: + converted_aux_op = self._qubit_converter.map(aux_op) else: converted_aux_op = aux_op + if name_aux in aux_ops.keys(): LOGGER.warning( "The key '%s' was already taken by an internally constructed auxiliary " diff --git a/qiskit_nature/second_q/algorithms/ground_state_solvers/ground_state_solver.py b/qiskit_nature/second_q/algorithms/ground_state_solvers/ground_state_solver.py index 6bc5aa929f..4cad1b0d18 100644 --- a/qiskit_nature/second_q/algorithms/ground_state_solvers/ground_state_solver.py +++ b/qiskit_nature/second_q/algorithms/ground_state_solvers/ground_state_solver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2022. +# (C) Copyright IBM 2020, 2023. # # 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 @@ -21,7 +21,7 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit_nature.second_q.operators import SparseLabelOp -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.problems import BaseProblem from qiskit_nature.second_q.problems import EigenstateResult @@ -31,11 +31,12 @@ class GroundStateSolver(ABC): """The ground state calculation interface.""" - def __init__(self, qubit_converter: QubitConverter) -> None: + def __init__(self, qubit_converter: QubitConverter | QubitMapper) -> None: """ Args: - qubit_converter: A class that converts second quantized operator to qubit operator - according to a mapper it is initialized with. + qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` or + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance that converts a second + quantized operator to qubit operators and applies subsequent qubit reduction. """ self._qubit_converter = qubit_converter @@ -63,7 +64,8 @@ def get_qubit_operators( problem: BaseProblem, aux_operators: dict[str, SparseLabelOp | QubitOperator] | None = None, ) -> tuple[QubitOperator, dict[str, QubitOperator] | None]: - """Gets the operator and auxiliary operators, and transforms the provided auxiliary operators. + """Gets the operator and auxiliary operators, and transforms the provided auxiliary operators + using a ``QubitConverter`` or ``QubitMapper``. If the user-provided ``aux_operators`` contain a name which clashes with an internally constructed auxiliary operator, then the corresponding internal operator will be overridden by the user-provided operator. diff --git a/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/minimum_eigensolver_factory.py b/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/minimum_eigensolver_factory.py index 5a7708c074..9c6b7dc233 100644 --- a/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/minimum_eigensolver_factory.py +++ b/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/minimum_eigensolver_factory.py @@ -12,11 +12,13 @@ """The minimum eigensolver factory for ground state calculation algorithms.""" +from __future__ import annotations + from abc import ABC, abstractmethod from qiskit.algorithms.minimum_eigensolvers import MinimumEigensolver -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.problems import BaseProblem @@ -25,7 +27,7 @@ class MinimumEigensolverFactory(ABC): @abstractmethod def get_solver( - self, problem: BaseProblem, qubit_converter: QubitConverter + self, problem: BaseProblem, qubit_converter: QubitConverter | QubitMapper ) -> MinimumEigensolver: """Returns a minimum eigensolver, based on the qubit operator transformation. diff --git a/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/numpy_minimum_eigensolver_factory.py b/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/numpy_minimum_eigensolver_factory.py index e3951a1175..a5e683f9d2 100644 --- a/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/numpy_minimum_eigensolver_factory.py +++ b/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/numpy_minimum_eigensolver_factory.py @@ -12,8 +12,10 @@ """The NumPy minimum eigensolver factory for ground state calculation algorithms.""" +from __future__ import annotations + from qiskit.algorithms.minimum_eigensolvers import MinimumEigensolver, NumPyMinimumEigensolver -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.problems import BaseProblem from .minimum_eigensolver_factory import MinimumEigensolverFactory @@ -37,7 +39,7 @@ def __init__( self._minimum_eigensolver = NumPyMinimumEigensolver(**kwargs) def get_solver( - self, problem: BaseProblem, qubit_converter: QubitConverter + self, problem: BaseProblem, qubit_converter: QubitConverter | QubitMapper ) -> MinimumEigensolver: """Returns a NumPyMinimumEigensolver which possibly uses the default filter criterion provided by the ``problem``. diff --git a/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/vqe_ucc_factory.py b/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/vqe_ucc_factory.py index f3659c11d0..75b3f3f5e6 100644 --- a/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/vqe_ucc_factory.py +++ b/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/vqe_ucc_factory.py @@ -16,14 +16,13 @@ import logging import numpy as np - from qiskit.algorithms.minimum_eigensolvers import MinimumEigensolver, VQE from qiskit.algorithms.optimizers import Minimizer, Optimizer from qiskit.circuit import QuantumCircuit from qiskit.primitives import BaseEstimator from qiskit_nature.second_q.circuit.library import HartreeFock, UCC -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.problems import ( ElectronicStructureProblem, ) @@ -117,7 +116,7 @@ def initial_state(self, initial_state: QuantumCircuit | None) -> None: def get_solver( # type: ignore[override] self, problem: ElectronicStructureProblem, - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, ) -> MinimumEigensolver: """Returns a VQE with a UCC wavefunction ansatz, based on ``qubit_converter``. diff --git a/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/vqe_uvcc_factory.py b/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/vqe_uvcc_factory.py index c02be099a5..f71436f64b 100644 --- a/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/vqe_uvcc_factory.py +++ b/qiskit_nature/second_q/algorithms/ground_state_solvers/minimum_eigensolver_factories/vqe_uvcc_factory.py @@ -23,7 +23,7 @@ from qiskit.primitives import BaseEstimator from qiskit_nature.second_q.circuit.library import UVCC, VSCF -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.problems import ( VibrationalStructureProblem, ) @@ -114,7 +114,7 @@ def initial_point(self, initial_point: np.ndarray | InitialPoint | None) -> None def get_solver( # type: ignore[override] self, problem: VibrationalStructureProblem, - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, ) -> MinimumEigensolver: """Returns a VQE with a :class:`~.UVCC` wavefunction ansatz, based on ``qubit_converter``. diff --git a/qiskit_nature/second_q/circuit/library/ansatzes/puccd.py b/qiskit_nature/second_q/circuit/library/ansatzes/puccd.py index cb5dfc3ec8..613363eba3 100644 --- a/qiskit_nature/second_q/circuit/library/ansatzes/puccd.py +++ b/qiskit_nature/second_q/circuit/library/ansatzes/puccd.py @@ -19,7 +19,7 @@ from qiskit.circuit import QuantumCircuit from qiskit_nature import QiskitNatureError -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from .ucc import UCC from .utils.fermionic_excitation_generator import ( @@ -51,7 +51,7 @@ def __init__( self, num_spatial_orbitals: int | None = None, num_particles: tuple[int, int] | None = None, - qubit_converter: QubitConverter | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, reps: int = 1, initial_state: QuantumCircuit | None = None, @@ -63,8 +63,9 @@ def __init__( Args: num_spatial_orbitals: The number of spatial orbitals. num_particles: The tuple of the number of alpha- and beta-spin particles. - qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` instance - which takes care of mapping to a qubit operator. + qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` or + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance which takes care of mapping + to a qubit operator. reps: The number of times to repeat the evolved operators. initial_state: A ``QuantumCircuit`` object to prepend to the circuit. include_singles: enables the inclusion of single excitations per spin species. @@ -75,6 +76,7 @@ def __init__( Raises: QiskitNatureError: if the number of alpha and beta electrons is not equal. + """ self._validate_num_particles(num_particles) self._include_singles = include_singles diff --git a/qiskit_nature/second_q/circuit/library/ansatzes/succd.py b/qiskit_nature/second_q/circuit/library/ansatzes/succd.py index 1039ff4fd5..8bcabf1de8 100644 --- a/qiskit_nature/second_q/circuit/library/ansatzes/succd.py +++ b/qiskit_nature/second_q/circuit/library/ansatzes/succd.py @@ -23,7 +23,7 @@ from qiskit.circuit import QuantumCircuit from qiskit_nature import QiskitNatureError -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.operators import FermionicOp @@ -57,7 +57,7 @@ def __init__( self, num_spatial_orbitals: int | None = None, num_particles: tuple[int, int] | None = None, - qubit_converter: QubitConverter | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, reps: int = 1, initial_state: QuantumCircuit | None = None, @@ -69,8 +69,9 @@ def __init__( Args: num_spatial_orbitals: The number of spatial orbitals. num_particles: The tuple of the number of alpha- and beta-spin particles. - qubit_converter: The QubitConverter instance which takes care of mapping to a qubit - operator. + qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` or + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance which takes care of mapping + to a qubit operator. reps: The number of times to repeat the evolved operators. initial_state: A ``QuantumCircuit`` object to prepend to the circuit. include_singles: enables the inclusion of single excitations per spin species. diff --git a/qiskit_nature/second_q/circuit/library/ansatzes/ucc.py b/qiskit_nature/second_q/circuit/library/ansatzes/ucc.py index ac02860aaf..1e0af6a04d 100644 --- a/qiskit_nature/second_q/circuit/library/ansatzes/ucc.py +++ b/qiskit_nature/second_q/circuit/library/ansatzes/ucc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -24,7 +24,7 @@ from qiskit.opflow import PauliTrotterEvolution from qiskit_nature import QiskitNatureError -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.operators import FermionicOp, SparseLabelOp from .utils.fermionic_excitation_generator import generate_fermionic_excitations @@ -137,7 +137,7 @@ def __init__( list[tuple[tuple[int, ...], tuple[int, ...]]], ] | None = None, - qubit_converter: QubitConverter | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, alpha_spin: bool = True, beta_spin: bool = True, @@ -167,8 +167,9 @@ def __init__( to write such a callable refer to the default method :meth:`~qiskit_nature.second_q.circuit.library.ansatzes.utils.\ generate_fermionic_excitations`. - qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` instance - which takes care of mapping to a qubit operator. + qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` or + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance which takes care of mapping + to a qubit operator. alpha_spin: Boolean flag whether to include alpha-spin excitations. beta_spin: Boolean flag whether to include beta-spin excitations. max_spin_excitation: The largest number of excitations within a spin. E.g. you can set @@ -219,12 +220,12 @@ def __init__( _ = self.operators @property - def qubit_converter(self) -> QubitConverter: + def qubit_converter(self) -> QubitConverter | QubitMapper | None: """The qubit operator converter.""" return self._qubit_converter @qubit_converter.setter - def qubit_converter(self, conv: QubitConverter) -> None: + def qubit_converter(self, conv: QubitConverter | QubitMapper | None) -> None: """Sets the qubit operator converter.""" self._operators = None self._invalidate() @@ -310,7 +311,15 @@ def operators(self): # pylint: disable=invalid-overridden-method # inserting ``None`` as the result if an operator did not commute. To ensure that # the ``excitation_list`` is transformed identically to the operators, we retain # ``None`` for non-commuting operators in order to manually remove them in unison. - operators = self.qubit_converter.convert_match(excitation_ops, suppress_none=False) + if isinstance(self.qubit_converter, QubitConverter): + operators = self.qubit_converter.convert_match( + excitation_ops, suppress_none=False + ) + else: + # TODO: Issue #974 sketches the construction of a Tapered Qubit Mapper which would + # implement the logic of the symmetries. Here, there should be a check for a Tapered + # Qubit Mapper and a similar logic that used above. + operators = self.qubit_converter.map(excitation_ops) self._filter_operators(operators=operators) return super(UCC, self.__class__).operators.__get__(self) diff --git a/qiskit_nature/second_q/circuit/library/ansatzes/uccsd.py b/qiskit_nature/second_q/circuit/library/ansatzes/uccsd.py index 9f4aba9757..57140d7723 100644 --- a/qiskit_nature/second_q/circuit/library/ansatzes/uccsd.py +++ b/qiskit_nature/second_q/circuit/library/ansatzes/uccsd.py @@ -16,7 +16,7 @@ from __future__ import annotations from qiskit.circuit import QuantumCircuit -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from .ucc import UCC @@ -30,7 +30,7 @@ def __init__( self, num_spatial_orbitals: int | None = None, num_particles: tuple[int, int] | None = None, - qubit_converter: QubitConverter | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, reps: int = 1, initial_state: QuantumCircuit | None = None, @@ -41,8 +41,9 @@ def __init__( Args: num_spatial_orbitals: The number of spatial orbitals. num_particles: The tuple of the number of alpha- and beta-spin particles. - qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` instance - which takes care of mapping to a qubit operator. + qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` or + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance which takes care of mapping + to a qubit operator. reps: The number of times to repeat the evolved operators. initial_state: A ``QuantumCircuit`` object to prepend to the circuit. generalized: Boolean flag whether or not to use generalized excitations, which ignore @@ -50,6 +51,7 @@ def __init__( only determined from the number of spin orbitals and independent from the number of particles. preserve_spin: Boolean flag whether or not to preserve the particle spins. + """ super().__init__( num_spatial_orbitals=num_spatial_orbitals, diff --git a/qiskit_nature/second_q/circuit/library/ansatzes/uvcc.py b/qiskit_nature/second_q/circuit/library/ansatzes/uvcc.py index 0a678cbfa8..a8a16eb345 100644 --- a/qiskit_nature/second_q/circuit/library/ansatzes/uvcc.py +++ b/qiskit_nature/second_q/circuit/library/ansatzes/uvcc.py @@ -23,7 +23,7 @@ from qiskit.opflow import PauliTrotterEvolution from qiskit_nature import QiskitNatureError -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.operators import SparseLabelOp, VibrationalOp from .utils.vibration_excitation_generator import generate_vibration_excitations @@ -73,7 +73,7 @@ def __init__( list[tuple[tuple[int, ...], tuple[int, ...]]], ] | None = None, - qubit_converter: QubitConverter | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, reps: int = 1, initial_state: QuantumCircuit | None = None, @@ -97,8 +97,9 @@ def __init__( ``list[tuple[tuple[int, ...], tuple[int, ...]]]``. For more information on how to write such a callable refer to the default method :meth:`~qiskit_nature.\ second_q.circuit.library.ansatzes.utils.generate_vibration_excitations`. - qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` instance - which takes care of mapping to a qubit operator. + qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` or + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance which takes care of mapping + to a qubit operator. reps: The number of repetitions of basic module. initial_state: A ``QuantumCircuit`` object to prepend to the circuit. Note that this setting does *not* influence the ``excitations``. When relying on the default @@ -132,12 +133,12 @@ def __init__( _ = self.operators @property - def qubit_converter(self) -> QubitConverter | None: + def qubit_converter(self) -> QubitConverter | QubitMapper | None: """The qubit operator converter.""" return self._qubit_converter @qubit_converter.setter - def qubit_converter(self, conv: QubitConverter) -> None: + def qubit_converter(self, conv: QubitConverter | QubitMapper) -> None: """Sets the qubit operator converter.""" self._operators = None self._invalidate() @@ -206,7 +207,13 @@ def operators(self): # pylint: disable=invalid-overridden-method # inserting ``None`` as the result if an operator did not commute. To ensure that # the ``excitation_list`` is transformed identically to the operators, we retain # ``None`` for non-commuting operators in order to manually remove them in unison. - operators = self.qubit_converter.convert_match(excitation_ops, suppress_none=False) + if isinstance(self.qubit_converter, QubitConverter): + operators = self.qubit_converter.convert_match( + excitation_ops, suppress_none=False + ) + else: + operators = self.qubit_converter.map(excitation_ops) + valid_operators, valid_excitations = [], [] for op, ex in zip(operators, self._excitation_list): if op is not None: diff --git a/qiskit_nature/second_q/circuit/library/ansatzes/uvccsd.py b/qiskit_nature/second_q/circuit/library/ansatzes/uvccsd.py index ca198d1007..eadffe93ba 100644 --- a/qiskit_nature/second_q/circuit/library/ansatzes/uvccsd.py +++ b/qiskit_nature/second_q/circuit/library/ansatzes/uvccsd.py @@ -16,7 +16,7 @@ from __future__ import annotations from qiskit.circuit import QuantumCircuit -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from .uvcc import UVCC @@ -29,7 +29,7 @@ class UVCCSD(UVCC): def __init__( self, num_modals: list[int] | None = None, - qubit_converter: QubitConverter | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, reps: int = 1, initial_state: QuantumCircuit | None = None, @@ -38,8 +38,9 @@ def __init__( Args: num_modals: A list defining the number of modals per mode. E.g. for a 3-mode system with 4 modals per mode ``num_modals = [4, 4, 4]``. - qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` instance - which takes care of mapping to a qubit operator. + qubit_converter: The :class:`~qiskit_nature.second_q.mappers.QubitConverter` or + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance which takes care of mapping + to a qubit operator. reps: The number of times to repeat the evolved operators. initial_state: A ``QuantumCircuit`` object to prepend to the circuit. """ diff --git a/qiskit_nature/second_q/circuit/library/bogoliubov_transform.py b/qiskit_nature/second_q/circuit/library/bogoliubov_transform.py index 72c5e5b09a..9c93310a32 100644 --- a/qiskit_nature/second_q/circuit/library/bogoliubov_transform.py +++ b/qiskit_nature/second_q/circuit/library/bogoliubov_transform.py @@ -15,13 +15,12 @@ from __future__ import annotations from collections.abc import Iterator -from typing import Optional import numpy as np from qiskit import QuantumCircuit, QuantumRegister from qiskit.circuit import Gate, Qubit from qiskit.circuit.library import RZGate, XXPlusYYGate -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.mappers import JordanWignerMapper from qiskit_nature.utils import apply_matrix_to_slices, givens_matrix from qiskit_nature.utils.linalg import fermionic_gaussian_decomposition_jw @@ -133,7 +132,7 @@ class BogoliubovTransform(QuantumCircuit): def __init__( self, transformation_matrix: np.ndarray, - qubit_converter: Optional[QubitConverter] = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, validate: bool = True, rtol: float = 1e-5, @@ -145,7 +144,7 @@ def __init__( transformation_matrix: The matrix :math:`W` that specifies the coefficients of the new creation operators in terms of the original creation operators. Should be either :math:`N \times N` or :math:`N \times 2N`. - qubit_converter: The qubit converter. The default behavior is to create + qubit_converter: The ``QubitConverter`` or ``QubitMapper``. The default behavior is to create one using the call ``QubitConverter(JordanWignerMapper())``. validate: Whether to validate the inputs. rtol: Relative numerical tolerance for input validation. @@ -176,7 +175,10 @@ def __init__( register = QuantumRegister(n) super().__init__(register, **circuit_kwargs) - if isinstance(qubit_converter.mapper, JordanWignerMapper): + if ( + isinstance(qubit_converter, QubitConverter) + and isinstance(qubit_converter.mapper, JordanWignerMapper) + ) or (isinstance(qubit_converter, JordanWignerMapper)): operations = _bogoliubov_transform_jw(register, transformation_matrix) for gate, qubits in operations: self.append(gate, qubits) diff --git a/qiskit_nature/second_q/circuit/library/initial_states/fermionic_gaussian_state.py b/qiskit_nature/second_q/circuit/library/initial_states/fermionic_gaussian_state.py index f94b05f388..e7e122bb6e 100644 --- a/qiskit_nature/second_q/circuit/library/initial_states/fermionic_gaussian_state.py +++ b/qiskit_nature/second_q/circuit/library/initial_states/fermionic_gaussian_state.py @@ -12,12 +12,13 @@ """Fermionic Gaussian states.""" -from typing import Optional, Sequence +from __future__ import annotations + +from typing import Sequence import numpy as np from qiskit import QuantumCircuit, QuantumRegister -from qiskit_nature.second_q.mappers import QubitConverter -from qiskit_nature.second_q.mappers import JordanWignerMapper +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper, JordanWignerMapper from .utils.givens_rotations import _prepare_fermionic_gaussian_state_jw @@ -115,8 +116,8 @@ class FermionicGaussianState(QuantumCircuit): def __init__( self, transformation_matrix: np.ndarray, - occupied_orbitals: Optional[Sequence[int]] = None, - qubit_converter: QubitConverter = None, + occupied_orbitals: Sequence[int] | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, validate: bool = True, rtol: float = 1e-5, @@ -132,7 +133,7 @@ def __init__( of the operators :math:`\{b^\dagger_j\}` from the main body of the docstring of this function. The default behavior is to use the empty set of orbitals, which corresponds to a state with zero pseudo-particles. - qubit_converter: The qubit converter. The default behavior is to create + qubit_converter: The ``QubitConverter`` or ``QubitMapper``. The default behavior is to create one using the call ``QubitConverter(JordanWignerMapper())``. validate: Whether to validate the inputs. rtol: Relative numerical tolerance for input validation. @@ -164,7 +165,10 @@ def __init__( register = QuantumRegister(n) super().__init__(register, **circuit_kwargs) - if isinstance(qubit_converter.mapper, JordanWignerMapper): + if ( + isinstance(qubit_converter, QubitConverter) + and isinstance(qubit_converter.mapper, JordanWignerMapper) + ) or (isinstance(qubit_converter, JordanWignerMapper)): operations = _prepare_fermionic_gaussian_state_jw( register, transformation_matrix, occupied_orbitals ) diff --git a/qiskit_nature/second_q/circuit/library/initial_states/hartree_fock.py b/qiskit_nature/second_q/circuit/library/initial_states/hartree_fock.py index a7d908047e..e4551d9ad7 100644 --- a/qiskit_nature/second_q/circuit/library/initial_states/hartree_fock.py +++ b/qiskit_nature/second_q/circuit/library/initial_states/hartree_fock.py @@ -13,14 +13,13 @@ """Hartree-Fock initial state.""" from __future__ import annotations - import numpy as np from qiskit import QuantumRegister from qiskit.circuit.library import BlueprintCircuit from qiskit.opflow import PauliSumOp from qiskit.utils.validation import validate_min -from qiskit_nature.second_q.mappers import BravyiKitaevSuperFastMapper, QubitConverter +from qiskit_nature.second_q.mappers import BravyiKitaevSuperFastMapper, QubitConverter, QubitMapper from qiskit_nature.second_q.operators import FermionicOp @@ -31,14 +30,15 @@ def __init__( self, num_spatial_orbitals: int | None = None, num_particles: tuple[int, int] | None = None, - qubit_converter: QubitConverter | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, ) -> None: """ Args: num_spatial_orbitals: The number of spatial orbitals. num_particles: The number of particles as a tuple storing the number of alpha and beta-spin electrons in the first and second number, respectively. - qubit_converter: a :class:`~qiskit_nature.second_q.mappers.QubitConverter` instance. + qubit_converter: A :class:`~qiskit_nature.second_q.mappers.QubitConverter` or a + :class:`~qiskit_nature.second_q.mappers.QubitMapper` instance. Raises: NotImplementedError: If ``qubit_converter`` contains @@ -55,12 +55,12 @@ def __init__( self._reset_register() @property - def qubit_converter(self) -> QubitConverter: + def qubit_converter(self) -> QubitConverter | QubitMapper | None: """The qubit converter.""" return self._qubit_converter @qubit_converter.setter - def qubit_converter(self, conv: QubitConverter) -> None: + def qubit_converter(self, conv: QubitConverter | QubitMapper | None) -> None: """Sets the qubit converter.""" self._invalidate() self._qubit_converter = conv @@ -147,11 +147,17 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: raise ValueError("The qubit converter cannot be `None`.") return False - if isinstance(self.qubit_converter.mapper, BravyiKitaevSuperFastMapper): + mapper = ( + self.qubit_converter + if isinstance(self.qubit_converter, QubitMapper) + else self.qubit_converter.mapper + ) + + if isinstance(mapper, BravyiKitaevSuperFastMapper): if raise_on_failure: raise NotImplementedError( "Unsupported mapper in qubit converter: ", - type(self.qubit_converter.mapper), + type(mapper), ". See https://github.com/Qiskit/qiskit-nature/issues/537", ) return False @@ -197,7 +203,7 @@ def _build(self) -> None: def hartree_fock_bitstring_mapped( num_spatial_orbitals: int, num_particles: tuple[int, int], - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, *, match_convert: bool = True, ) -> list[bool]: @@ -207,7 +213,7 @@ def hartree_fock_bitstring_mapped( num_spatial_orbitals: The number of spatial orbitals, has a min. value of 1. num_particles: The number of particles as a tuple (alpha, beta) containing the number of alpha- and beta-spin electrons, respectively. - qubit_converter: A QubitConverter instance. + qubit_converter: A QubitConverter or QubitMapper instance. match_convert: Whether to use `convert_match` method of the qubit converter (default), or just do mapping and possibly two qubit reduction but no tapering. The latter is an advanced usage - e.g. if we are trying to auto-select the tapering sector @@ -227,11 +233,14 @@ def hartree_fock_bitstring_mapped( ) # map the `FermionicOp` to a qubit operator - qubit_op: PauliSumOp = ( - qubit_converter.convert_match(bitstr_op, check_commutes=False) - if match_convert - else qubit_converter.convert_only(bitstr_op, num_particles) - ) + qubit_op: PauliSumOp + if isinstance(qubit_converter, QubitConverter): + if match_convert: + qubit_op = qubit_converter.convert_match(bitstr_op, check_commutes=False) + else: + qubit_op = qubit_converter.convert_only(bitstr_op, num_particles) + else: + qubit_op = qubit_converter.map(bitstr_op) # We check the mapped operator `x` part of the paulis because we want to have particles # i.e. True, where the initial state introduced a creation (`+`) operator. diff --git a/qiskit_nature/second_q/circuit/library/initial_states/slater_determinant.py b/qiskit_nature/second_q/circuit/library/initial_states/slater_determinant.py index e24146050f..0dea507181 100644 --- a/qiskit_nature/second_q/circuit/library/initial_states/slater_determinant.py +++ b/qiskit_nature/second_q/circuit/library/initial_states/slater_determinant.py @@ -12,11 +12,13 @@ """Slater determinants.""" +from __future__ import annotations + from typing import Optional import numpy as np from qiskit import QuantumCircuit, QuantumRegister -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.mappers import JordanWignerMapper from .utils.givens_rotations import _prepare_slater_determinant_jw @@ -74,7 +76,7 @@ class SlaterDeterminant(QuantumCircuit): def __init__( self, transformation_matrix: np.ndarray, - qubit_converter: Optional[QubitConverter] = None, + qubit_converter: QubitConverter | QubitMapper | None = None, *, validate: bool = True, rtol: float = 1e-5, @@ -86,7 +88,7 @@ def __init__( transformation_matrix: The matrix :math:`Q` that specifies the coefficients of the new creation operators in terms of the original creation operators. The rows of the matrix must be orthonormal. - qubit_converter: The qubit converter. The default behavior is to create + qubit_converter: The ``QubitConverter`` or ``QubitMapper``. The default behavior is to create one using the call ``QubitConverter(JordanWignerMapper())``. validate: Whether to validate the inputs. rtol: Relative numerical tolerance for input validation. @@ -111,7 +113,10 @@ def __init__( register = QuantumRegister(n) super().__init__(register, **circuit_kwargs) - if isinstance(qubit_converter.mapper, JordanWignerMapper): + if ( + isinstance(qubit_converter, QubitConverter) + and isinstance(qubit_converter.mapper, JordanWignerMapper) + ) or (isinstance(qubit_converter, JordanWignerMapper)): operations = _prepare_slater_determinant_jw(register, transformation_matrix) for gate, qubits in operations: self.append(gate, qubits) diff --git a/qiskit_nature/second_q/circuit/library/initial_states/vscf.py b/qiskit_nature/second_q/circuit/library/initial_states/vscf.py index 021e8985c8..2c970fad7b 100644 --- a/qiskit_nature/second_q/circuit/library/initial_states/vscf.py +++ b/qiskit_nature/second_q/circuit/library/initial_states/vscf.py @@ -22,7 +22,7 @@ from qiskit.circuit.library import BlueprintCircuit from qiskit.opflow import PauliSumOp from qiskit_nature.second_q.mappers import DirectMapper -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.operators import VibrationalOp logger = logging.getLogger(__name__) @@ -42,14 +42,14 @@ class VSCF(BlueprintCircuit): def __init__( self, num_modals: list[int] | None = None, - qubit_converter: QubitConverter | None = None, + qubit_converter: QubitConverter | QubitMapper | None = None, ) -> None: """ Args: num_modals: Is a list defining the number of modals per mode. E.g. for a 3 modes system with 4 modals per mode num_modals = [4,4,4] - qubit_converter: a QubitConverter instance. This argument is currently being ignored - because only a single use-case is supported at the time of release: that of the + qubit_converter: a QubitConverter or QubitMapper instance. This argument is currently being + ignored because only a single use-case is supported at the time of release: that of the :class:`DirectMapper`. However, for future-compatibility of this functions signature, the argument has already been inserted. """ @@ -63,20 +63,21 @@ def __init__( ) @property - def qubit_converter(self) -> QubitConverter: + def qubit_converter(self) -> QubitConverter | QubitMapper | None: """The qubit converter.""" return self._qubit_converter @qubit_converter.setter - def qubit_converter(self, conv: QubitConverter) -> None: + def qubit_converter(self, conv: QubitConverter | QubitMapper | None) -> None: """Sets the qubit converter.""" self._invalidate() - if not isinstance(conv.mapper, DirectMapper): + mapper = conv if isinstance(conv, QubitMapper) else conv.mapper + if not isinstance(mapper, DirectMapper): logger.warning( "The only supported `QubitConverter` is one with a `DirectMapper` as the mapper " "instance. However you specified %s as an input, which will be ignored until more " "variants will be supported.", - type(conv.mapper), + type(mapper), ) conv = QubitConverter(DirectMapper()) self._qubit_converter = conv @@ -157,7 +158,7 @@ def _build(self) -> None: def vscf_bitstring_mapped( num_modals: list[int], - qubit_converter: QubitConverter, + qubit_converter: QubitConverter | QubitMapper, ) -> list[bool]: """Compute the bitstring representing the mapped VSCF initial state based on the given the number of modals per mode and qubit converter. @@ -165,7 +166,7 @@ def vscf_bitstring_mapped( Args: num_modals: A list defining the number of modals per mode. E.g. for a 3 modes system with 4 modals per mode num_modals = [4,4,4]. - qubit_converter: A QubitConverter instance. + qubit_converter: A QubitConverter or QubitMapper instance. Returns: The bitstring representing the mapped state of the VSCF initial state as array of bools. @@ -186,7 +187,11 @@ def vscf_bitstring_mapped( num_modals=num_modals, ) # map the `VibrationalOp` to a qubit operator - qubit_op: PauliSumOp = qubit_converter.convert_match(bitstr_op, check_commutes=False) + qubit_op: PauliSumOp + if isinstance(qubit_converter, QubitConverter): + qubit_op = qubit_converter.convert_match(bitstr_op, check_commutes=False) + else: + qubit_op = qubit_converter.map(bitstr_op) # We check the mapped operator `x` part of the paulis because we want to have particles # i.e. True, where the initial state introduced a creation (`+`) operator. diff --git a/qiskit_nature/second_q/mappers/bksf.py b/qiskit_nature/second_q/mappers/bksf.py index 07525a52ed..e7ac99e0ad 100644 --- a/qiskit_nature/second_q/mappers/bksf.py +++ b/qiskit_nature/second_q/mappers/bksf.py @@ -40,7 +40,7 @@ def __init__(self): """The BKSF mapping.""" super().__init__(allows_two_qubit_reduction=False) - def map(self, second_q_op: FermionicOp) -> PauliSumOp: + def _map_single(self, second_q_op: FermionicOp) -> PauliSumOp: if not isinstance(second_q_op, FermionicOp): raise TypeError("Type ", type(second_q_op), " not supported.") diff --git a/qiskit_nature/second_q/mappers/bravyi_kitaev_mapper.py b/qiskit_nature/second_q/mappers/bravyi_kitaev_mapper.py index 10caac3917..9bc88ec4d1 100644 --- a/qiskit_nature/second_q/mappers/bravyi_kitaev_mapper.py +++ b/qiskit_nature/second_q/mappers/bravyi_kitaev_mapper.py @@ -162,5 +162,5 @@ def flip_set(j, n): return pauli_table - def map(self, second_q_op: FermionicOp) -> PauliSumOp: + def _map_single(self, second_q_op: FermionicOp) -> PauliSumOp: return BravyiKitaevMapper.mode_based_mapping(second_q_op, second_q_op.register_length) diff --git a/qiskit_nature/second_q/mappers/direct_mapper.py b/qiskit_nature/second_q/mappers/direct_mapper.py index d1d911425d..6ebe48e2b5 100644 --- a/qiskit_nature/second_q/mappers/direct_mapper.py +++ b/qiskit_nature/second_q/mappers/direct_mapper.py @@ -49,5 +49,5 @@ def pauli_table(cls, nmodes: int) -> list[tuple[Pauli, Pauli]]: return pauli_table - def map(self, second_q_op: VibrationalOp) -> PauliSumOp: + def _map_single(self, second_q_op: VibrationalOp) -> PauliSumOp: return DirectMapper.mode_based_mapping(second_q_op, sum(second_q_op.num_modals)) diff --git a/qiskit_nature/second_q/mappers/fermionic_mapper.py b/qiskit_nature/second_q/mappers/fermionic_mapper.py index dfe7eafc18..72c76bdd6e 100644 --- a/qiskit_nature/second_q/mappers/fermionic_mapper.py +++ b/qiskit_nature/second_q/mappers/fermionic_mapper.py @@ -24,7 +24,7 @@ class FermionicMapper(QubitMapper): """Mapper of Fermionic Operator to Qubit Operator""" @abstractmethod - def map(self, second_q_op: FermionicOp) -> PauliSumOp: + def _map_single(self, second_q_op: FermionicOp) -> PauliSumOp: """Maps a :class:`~qiskit_nature.second_q.operators.FermionicOp` to a `PauliSumOp`. diff --git a/qiskit_nature/second_q/mappers/jordan_wigner_mapper.py b/qiskit_nature/second_q/mappers/jordan_wigner_mapper.py index f5dd5835cd..bbbc3d8212 100644 --- a/qiskit_nature/second_q/mappers/jordan_wigner_mapper.py +++ b/qiskit_nature/second_q/mappers/jordan_wigner_mapper.py @@ -47,5 +47,5 @@ def pauli_table(cls, nmodes: int) -> list[tuple[Pauli, Pauli]]: return pauli_table - def map(self, second_q_op: FermionicOp) -> PauliSumOp: + def _map_single(self, second_q_op: FermionicOp) -> PauliSumOp: return JordanWignerMapper.mode_based_mapping(second_q_op, second_q_op.register_length) diff --git a/qiskit_nature/second_q/mappers/linear_mapper.py b/qiskit_nature/second_q/mappers/linear_mapper.py index 499a284b73..25186e418f 100644 --- a/qiskit_nature/second_q/mappers/linear_mapper.py +++ b/qiskit_nature/second_q/mappers/linear_mapper.py @@ -33,7 +33,7 @@ def __init__(self): """The Linear spin-to-qubit mapping.""" super().__init__(allows_two_qubit_reduction=False) - def map(self, second_q_op: SpinOp) -> PauliSumOp: + def _map_single(self, second_q_op: SpinOp) -> PauliSumOp: qubit_ops_list: list[PauliSumOp] = [] diff --git a/qiskit_nature/second_q/mappers/logarithmic_mapper.py b/qiskit_nature/second_q/mappers/logarithmic_mapper.py index 79f7526309..64038f638c 100644 --- a/qiskit_nature/second_q/mappers/logarithmic_mapper.py +++ b/qiskit_nature/second_q/mappers/logarithmic_mapper.py @@ -72,7 +72,7 @@ def __init__(self, *, padding: float = 1, embed_upper: bool = True) -> None: self._padding = padding self._embed_upper = embed_upper - def map(self, second_q_op: SpinOp) -> PauliSumOp: + def _map_single(self, second_q_op: SpinOp) -> PauliSumOp: """Map spins to qubits using the Logarithmic encoding. Args: diff --git a/qiskit_nature/second_q/mappers/parity_mapper.py b/qiskit_nature/second_q/mappers/parity_mapper.py index 513fc7f21d..5bc230309e 100644 --- a/qiskit_nature/second_q/mappers/parity_mapper.py +++ b/qiskit_nature/second_q/mappers/parity_mapper.py @@ -54,5 +54,5 @@ def pauli_table(cls, nmodes: int) -> list[tuple[Pauli, Pauli]]: return pauli_table - def map(self, second_q_op: FermionicOp) -> PauliSumOp: + def _map_single(self, second_q_op: FermionicOp) -> PauliSumOp: return ParityMapper.mode_based_mapping(second_q_op, second_q_op.register_length) diff --git a/qiskit_nature/second_q/mappers/qubit_converter.py b/qiskit_nature/second_q/mappers/qubit_converter.py index 2f5f207676..8a94821632 100644 --- a/qiskit_nature/second_q/mappers/qubit_converter.py +++ b/qiskit_nature/second_q/mappers/qubit_converter.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -19,14 +19,9 @@ from typing import ( cast, Callable, - Dict, - Generator, - Generic, - Iterable, List, Optional, Tuple, - TypeVar, Union, ) @@ -39,53 +34,11 @@ from qiskit_nature import QiskitNatureError from qiskit_nature.second_q.operators import SparseLabelOp - -from .qubit_mapper import QubitMapper - -# pylint: disable=invalid-name -T = TypeVar("T") +from .qubit_mapper import QubitMapper, _ListOrDict logger = logging.getLogger(__name__) -class _ListOrDict(Dict, Iterable, Generic[T]): - """The ListOrDict utility class. - - This is a utility which allows seamless iteration of a `list` or `dict` object. - """ - - 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 - - class QubitConverter: """A converter from Second-Quantized to Qubit Operators. @@ -240,7 +193,7 @@ def convert( Returns: PauliSumOp qubit operator """ - qubit_op = self._map(second_q_op) + qubit_op = self._mapper.map(second_q_op) reduced_op = self._two_qubit_reduce(qubit_op, num_particles) tapered_op, z2symmetries = self.find_taper_op(reduced_op, sector_locator) @@ -268,7 +221,7 @@ def convert_only( Returns: PauliSumOp qubit operator """ - qubit_op = self._map(second_q_op) + qubit_op = self._mapper.map(second_q_op) reduced_op = self._two_qubit_reduce(qubit_op, num_particles) return reduced_op @@ -348,84 +301,40 @@ def convert_match( wrapped_second_q_ops: _ListOrDict[SparseLabelOp] = _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) - 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) - - tapered_ops = self._symmetry_reduce(reduced_ops, check_commutes) + for name, second_q_op in iter(wrapped_second_q_ops): + qubit_op: PauliSumOp = self._mapper.map(second_q_op) + reduced_op: PauliSumOp = self._two_qubit_reduce(qubit_op, self._num_particles) + reduced_ops[name] = reduced_op - returned_ops: Union[PauliSumOp, ListOrDictType[PauliSumOp]] + tapered_ops: _ListOrDict[PauliSumOp] = self._symmetry_reduce(reduced_ops, check_commutes) - if issubclass(wrapped_type, SparseLabelOp): - 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)) + returned_ops: Union[PauliSumOp, ListOrDictType[PauliSumOp]] = tapered_ops.unwrap( + wrapped_type, suppress_none=suppress_none + ) return returned_ops - def map( - self, - second_q_ops: SparseLabelOp | ListOrDictType[SparseLabelOp], - ) -> Union[PauliSumOp, ListOrDictType[PauliSumOp]]: - """A convenience method to map second quantized operators based on current mapper. - - Args: - second_q_ops: A second quantized operator, or list thereof - - Returns: - A qubit operator in the form of a PauliSumOp, or list thereof if a list of - second quantized operators was supplied - """ - if isinstance(second_q_ops, SparseLabelOp): - qubit_ops = self._map(second_q_ops) - else: - wrapped_type = type(second_q_ops) - - wrapped_second_q_ops: _ListOrDict[SparseLabelOp] = _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 - - def _map(self, second_q_op: SparseLabelOp | PauliSumOp) -> PauliSumOp: - if isinstance(second_q_op, PauliSumOp): - return second_q_op - elif self._sort_operators and isinstance(second_q_op, SparseLabelOp): - second_q_op = second_q_op.sort() - return self._mapper.map(second_q_op) - def _two_qubit_reduce( self, qubit_op: PauliSumOp, num_particles: Optional[Tuple[int, int]] ) -> PauliSumOp: reduced_op = qubit_op - if num_particles is not None: - if self._two_qubit_reduction and self._mapper.allows_two_qubit_reduction: - if qubit_op.num_qubits <= 2: - logger.warning( - "The original qubit operator only contains %s qubits! Skipping the requested " - "two-qubit reduction!", - qubit_op.num_qubits, - ) - return reduced_op + if ( + num_particles is not None + and self._two_qubit_reduction + and self._mapper.allows_two_qubit_reduction + ): + + two_q_reducer = TwoQubitReduction(num_particles) - two_q_reducer = TwoQubitReduction(num_particles) + if qubit_op.num_qubits <= 2: + logger.warning( + "The original qubit operator only contains %s qubits! Skipping the requested " + "two-qubit reduction!", + qubit_op.num_qubits, + ) + else: reduced_op = cast(PauliSumOp, two_q_reducer.convert(qubit_op)) return reduced_op @@ -602,7 +511,7 @@ def symmetry_reduce_clifford( def convert_clifford( self, qubit_ops: PauliSumOp | ListOrDictType[PauliSumOp], - ) -> _ListOrDict[PauliSumOp]: + ) -> PauliSumOp | ListOrDictType[PauliSumOp]: """ Applies the Clifford transformation from the current symmetry to all operators. diff --git a/qiskit_nature/second_q/mappers/qubit_mapper.py b/qiskit_nature/second_q/mappers/qubit_mapper.py index b65f1e14d6..5df278022b 100644 --- a/qiskit_nature/second_q/mappers/qubit_mapper.py +++ b/qiskit_nature/second_q/mappers/qubit_mapper.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -16,14 +16,78 @@ from abc import ABC, abstractmethod from functools import lru_cache +from typing import Union, TypeVar, Dict, Iterable, Generic, Tuple, Generator, Optional import numpy as np from qiskit.opflow import PauliSumOp from qiskit.quantum_info.operators import Pauli, SparsePauliOp +from qiskit.algorithms.list_or_dict import ListOrDict as ListOrDictType from qiskit_nature import QiskitNatureError from qiskit_nature.second_q.operators import SparseLabelOp +# 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. + """ + + 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 + + def unwrap(self, wrapped_type: type, suppress_none: bool = False) -> Dict | Iterable | T: + """Return the content of this class according to the initial type of the data before + the creation of the ListOrDict object. + + Args: + wrapped_type: Type of the data before the creation of the ListOrDict object. + suppress_none: If None values should be suppressed from the output list. + + Returns: + Content of the current class instance as a list, a dictionary or a single element. + """ + if wrapped_type == list: + if suppress_none: + return [op for _, op in iter(self) if op is not None] + else: + return [op for _, op in iter(self)] + if wrapped_type == dict: + return dict(iter(self)) + # only other case left is that it was a single operator to begin with: + return list(iter(self))[0][1] + class QubitMapper(ABC): """The interface for implementing methods which map from a `SparseLabelOp` to a @@ -49,7 +113,7 @@ def allows_two_qubit_reduction(self) -> bool: return self._allows_two_qubit_reduction @abstractmethod - def map(self, second_q_op: SparseLabelOp) -> PauliSumOp: + def _map_single(self, second_q_op: SparseLabelOp) -> PauliSumOp: """Maps a :class:`~qiskit_nature.second_q.operators.SparseLabelOp` to a `PauliSumOp`. @@ -61,6 +125,42 @@ def map(self, second_q_op: SparseLabelOp) -> PauliSumOp: """ raise NotImplementedError() + def map( + self, + second_q_ops: SparseLabelOp | ListOrDictType[SparseLabelOp], + suppress_none: bool = None, + ) -> PauliSumOp | ListOrDictType[PauliSumOp]: + """Maps a second quantized operator or a list, dict of second quantized operators based on + the current mapper. + + Args: + second_q_ops: A second quantized operator, or list thereof. + suppress_none: If None should be placed in the output list where an operator + did not commute with symmetry, to maintain order, or whether that should + be suppressed where the output list length may then be smaller than the input. + + Returns: + A qubit operator in the form of a PauliSumOp, or list (resp. dict) thereof if a list + (resp. dict) of second quantized operators was supplied. + """ + wrapped_type = type(second_q_ops) + + if issubclass(wrapped_type, SparseLabelOp): + second_q_ops = [second_q_ops] + suppress_none = False + + wrapped_second_q_ops: _ListOrDict[SparseLabelOp] = _ListOrDict(second_q_ops) + + qubit_ops: _ListOrDict = _ListOrDict() + for name, second_q_op in iter(wrapped_second_q_ops): + qubit_ops[name] = self._map_single(second_q_op) + + returned_ops: Union[PauliSumOp, ListOrDictType[PauliSumOp]] = qubit_ops.unwrap( + wrapped_type, suppress_none=suppress_none + ) + + return returned_ops + @classmethod @lru_cache(maxsize=32) def pauli_table(cls, nmodes: int) -> list[tuple[Pauli, Pauli]]: diff --git a/qiskit_nature/second_q/mappers/spin_mapper.py b/qiskit_nature/second_q/mappers/spin_mapper.py index 6e893711a9..fc5e3a25df 100644 --- a/qiskit_nature/second_q/mappers/spin_mapper.py +++ b/qiskit_nature/second_q/mappers/spin_mapper.py @@ -15,6 +15,7 @@ from abc import abstractmethod from qiskit.opflow import PauliSumOp + from qiskit_nature.second_q.operators import SpinOp from .qubit_mapper import QubitMapper @@ -24,7 +25,7 @@ class SpinMapper(QubitMapper): """Mapper of Spin Operator to Qubit Operator""" @abstractmethod - def map(self, second_q_op: SpinOp) -> PauliSumOp: + def _map_single(self, second_q_op: SpinOp) -> PauliSumOp: """Maps a :class:`~qiskit_nature.second_q.operators.SpinOp` to a `PauliSumOp`. Args: diff --git a/qiskit_nature/second_q/mappers/vibrational_mapper.py b/qiskit_nature/second_q/mappers/vibrational_mapper.py index 581bbd66fd..dfa1f056d9 100644 --- a/qiskit_nature/second_q/mappers/vibrational_mapper.py +++ b/qiskit_nature/second_q/mappers/vibrational_mapper.py @@ -15,6 +15,7 @@ from abc import abstractmethod from qiskit.opflow import PauliSumOp + from qiskit_nature.second_q.operators import VibrationalOp from .qubit_mapper import QubitMapper @@ -24,7 +25,7 @@ class VibrationalMapper(QubitMapper): """Mapper of Vibrational Operator to Qubit Operator""" @abstractmethod - def map(self, second_q_op: VibrationalOp) -> PauliSumOp: + def _map_single(self, second_q_op: VibrationalOp) -> PauliSumOp: """Maps a :class:`~qiskit_nature.second_q.operators.VibrationalOp` to a `PauliSumOp`. diff --git a/qiskit_nature/second_q/problems/base_problem.py b/qiskit_nature/second_q/problems/base_problem.py index 7e26c09e7c..f8b2df722b 100644 --- a/qiskit_nature/second_q/problems/base_problem.py +++ b/qiskit_nature/second_q/problems/base_problem.py @@ -22,7 +22,7 @@ from qiskit.algorithms.minimum_eigensolvers import MinimumEigensolverResult from qiskit.opflow import Z2Symmetries -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.operators import SparseLabelOp from qiskit_nature.second_q.hamiltonians import Hamiltonian @@ -78,7 +78,7 @@ def second_q_ops(self) -> tuple[SparseLabelOp, dict[str, SparseLabelOp]]: def symmetry_sector_locator( self, z2_symmetries: Z2Symmetries, - converter: QubitConverter, + converter: QubitConverter | QubitMapper, ) -> list[int] | None: # pylint: disable=unused-argument """Given the detected Z2Symmetries, it can determine the correct sector of the tapered @@ -86,8 +86,8 @@ def symmetry_sector_locator( Args: z2_symmetries: the z2 symmetries object. - converter: the qubit converter instance used for the operator conversion that - symmetries are to be determined for. + converter: the ``QubitConverter`` or ``QubitMapper`` instance used for the operator + conversion that symmetries are to be determined for. Returns: the sector of the tapered operators with the problem solution diff --git a/qiskit_nature/second_q/problems/electronic_structure_problem.py b/qiskit_nature/second_q/problems/electronic_structure_problem.py index b1f83c8a5c..c73528b194 100644 --- a/qiskit_nature/second_q/problems/electronic_structure_problem.py +++ b/qiskit_nature/second_q/problems/electronic_structure_problem.py @@ -27,7 +27,7 @@ from qiskit_nature.second_q.circuit.library.initial_states.hartree_fock import ( hartree_fock_bitstring_mapped, ) -from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper from qiskit_nature.second_q.hamiltonians import ElectronicEnergy from qiskit_nature.second_q.properties import Interpretable @@ -270,15 +270,15 @@ def filter_criterion(self, eigenstate, eigenvalue, aux_values): def symmetry_sector_locator( self, z2_symmetries: Z2Symmetries, - converter: QubitConverter, + converter: QubitConverter | QubitMapper, ) -> Optional[List[int]]: """Given the detected Z2Symmetries this determines the correct sector of the tapered operator that contains the ground state we need and returns that information. Args: z2_symmetries: the z2 symmetries object. - converter: the qubit converter instance used for the operator conversion that - symmetries are to be determined for. + converter: the ``QubitConverter`` or ``QubitMapper`` instance used for the operator + conversion that symmetries are to be determined for. Raises: QiskitNatureError: if the :attr:`num_particles` attribute is ``None``. diff --git a/releasenotes/notes/update-api-for-qubitmapper-qubitconverter-d237a7b596d50207.yaml b/releasenotes/notes/update-api-for-qubitmapper-qubitconverter-d237a7b596d50207.yaml new file mode 100644 index 0000000000..3800b22aa0 --- /dev/null +++ b/releasenotes/notes/update-api-for-qubitmapper-qubitconverter-d237a7b596d50207.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Updated the API to allow :class:`~qiskit_nature.second_q.mappers.QubitMapper` objects in places + where :class:`qiskit_nature.second_q.mappers.QubitConverter` were previously required. This addition + advances toward a future deprecation of :class:`~qiskit_nature.second_q.mappers.QubitConverter`. + All inputs of type :class:`~qiskit_nature.second_q.mappers.QubitConverter` now support + :class:`~qiskit_nature.second_q.mappers.QubitMapper` objects implementing a transformation from + second quantized operators to Pauli operators. Note that the mappers currently do not support + qubit reduction techniques. + - | + The method :meth:`~qiskit_nature.second_q.mappers.QubitMapper.map()` now supports individual operators + as well as lists and dictionaries of operators. diff --git a/test/second_q/algorithms/excited_state_solvers/resources/__init__.py b/test/second_q/algorithms/excited_state_solvers/resources/__init__.py index fdb172d367..2217e835e1 100644 --- a/test/second_q/algorithms/excited_state_solvers/resources/__init__.py +++ b/test/second_q/algorithms/excited_state_solvers/resources/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # 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 diff --git a/test/second_q/algorithms/excited_state_solvers/resources/expected_qeom_ops.py b/test/second_q/algorithms/excited_state_solvers/resources/expected_qeom_ops.py index b5e4df10f4..cd2d3cf2ca 100644 --- a/test/second_q/algorithms/excited_state_solvers/resources/expected_qeom_ops.py +++ b/test/second_q/algorithms/excited_state_solvers/resources/expected_qeom_ops.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # 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 diff --git a/test/second_q/algorithms/excited_state_solvers/resources/expected_transition_amplitudes.py b/test/second_q/algorithms/excited_state_solvers/resources/expected_transition_amplitudes.py index 6057d6fbdc..6656b471b5 100644 --- a/test/second_q/algorithms/excited_state_solvers/resources/expected_transition_amplitudes.py +++ b/test/second_q/algorithms/excited_state_solvers/resources/expected_transition_amplitudes.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # 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 diff --git a/test/second_q/algorithms/excited_state_solvers/test_excited_states_solvers.py b/test/second_q/algorithms/excited_state_solvers/test_excited_states_solvers.py index 8bf39fa475..e703b24c18 100644 --- a/test/second_q/algorithms/excited_state_solvers/test_excited_states_solvers.py +++ b/test/second_q/algorithms/excited_state_solvers/test_excited_states_solvers.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2022. +# (C) Copyright IBM 2020, 2023. # # 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 @@ -66,7 +66,8 @@ def setUp(self): -1.8427016 + 0.95788352, -1.8427016 + 1.5969296, ] - self.qubit_converter = QubitConverter(JordanWignerMapper()) + self.mapper = JordanWignerMapper() + self.qubit_converter = QubitConverter(self.mapper) self.electronic_structure_problem = self.driver.run() solver = NumPyEigensolver() @@ -185,6 +186,62 @@ def custom_filter_criterion(eigenstate, eigenvalue, aux_values): self._assert_energies(computed_energies, ref_energies, places=3) + @unittest.skipIf(not _optionals.HAS_PYSCF, "pyscf not available.") + def test_solver_compatibility_with_mappers(self): + """Test that solvers can use both QubitConverter and QubitMapper""" + + # pylint: disable=unused-argument + def filter_criterion(eigenstate, eigenvalue, aux_values): + return np.isclose(aux_values["ParticleNumber"][0], 2.0) + + with self.subTest("Excited states solver with qubit converter"): + solver = NumPyEigensolverFactory(filter_criterion=filter_criterion) + esc_converter = ExcitedStatesEigensolver(self.qubit_converter, solver) + results_converter = esc_converter.solve(self.electronic_structure_problem) + computed_energies_converter = [results_converter.computed_energies[0]] + # filter duplicates from list + for comp_energy in results_converter.computed_energies[1:]: + if not np.isclose(comp_energy, computed_energies_converter[-1]): + computed_energies_converter.append(comp_energy) + self._assert_energies(computed_energies_converter, self.reference_energies) + + with self.subTest("Excited states solver with qubit mapper"): + solver = NumPyEigensolverFactory(filter_criterion=filter_criterion) + esc_mapper = ExcitedStatesEigensolver(self.mapper, solver) + results_mapper = esc_mapper.solve(self.electronic_structure_problem) + # filter duplicates from list + computed_energies_mapper = [results_mapper.computed_energies[0]] + for comp_energy in results_mapper.computed_energies[1:]: + if not np.isclose(comp_energy, computed_energies_mapper[-1]): + computed_energies_mapper.append(comp_energy) + self._assert_energies(computed_energies_mapper, self.reference_energies) + + with self.subTest("QEOM with qubit converter"): + estimator = Estimator() + solver = VQEUCCFactory(estimator, UCCSD(), SLSQP()) + gsc_converter = GroundStateEigensolver(self.qubit_converter, solver) + esc_converter = QEOM(gsc_converter, estimator, "sd") + results_converter = esc_converter.solve(self.electronic_structure_problem) + # filter duplicates from list + computed_energies_converter = [results_converter.computed_energies[0]] + for comp_energy in results_converter.computed_energies[1:]: + if not np.isclose(comp_energy, computed_energies_converter[-1]): + computed_energies_converter.append(comp_energy) + self._assert_energies(computed_energies_converter, self.reference_energies) + + with self.subTest("QEOM with qubit mapper"): + estimator = Estimator() + solver = VQEUCCFactory(estimator, UCCSD(), SLSQP()) + gsc_mapper = GroundStateEigensolver(self.mapper, solver) + esc_mapper = QEOM(gsc_mapper, estimator, "sd") + results_mapper = esc_mapper.solve(self.electronic_structure_problem) + # filter duplicates from list + computed_energies_mapper = [results_mapper.computed_energies[0]] + for comp_energy in results_mapper.computed_energies[1:]: + if not np.isclose(comp_energy, computed_energies_mapper[-1]): + computed_energies_mapper.append(comp_energy) + self._assert_energies(computed_energies_mapper, self.reference_energies) + if __name__ == "__main__": unittest.main() diff --git a/test/second_q/algorithms/excited_state_solvers/test_excited_states_solvers_auxiliaries.py b/test/second_q/algorithms/excited_state_solvers/test_excited_states_solvers_auxiliaries.py index 22bee216e8..4ba53c014a 100644 --- a/test/second_q/algorithms/excited_state_solvers/test_excited_states_solvers_auxiliaries.py +++ b/test/second_q/algorithms/excited_state_solvers/test_excited_states_solvers_auxiliaries.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2022. +# (C) Copyright IBM 2020, 2023. # # 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 diff --git a/test/second_q/algorithms/excited_state_solvers/test_qeom_electronic_ops.py b/test/second_q/algorithms/excited_state_solvers/test_qeom_electronic_ops.py index 3e65dc4df2..4013be8786 100644 --- a/test/second_q/algorithms/excited_state_solvers/test_qeom_electronic_ops.py +++ b/test/second_q/algorithms/excited_state_solvers/test_qeom_electronic_ops.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -45,12 +45,13 @@ def setUp(self): basis="sto3g", ) - self.qubit_converter = QubitConverter(JordanWignerMapper()) + self.mapper = JordanWignerMapper() + self.qubit_converter = QubitConverter(self.mapper) self.electronic_structure_problem = self.driver.run() self.electronic_structure_problem.second_q_ops() def test_build_hopping_operators(self): - """Tests that the correct hopping operators are built.""" + """Tests that the correct hopping operator is built.""" hopping_operators, commutativities, indices = build_electronic_ops( self.electronic_structure_problem.num_spatial_orbitals, @@ -78,6 +79,35 @@ def test_build_hopping_operators(self): with self.subTest("excitation indices"): self.assertEqual(indices, expected_indices_electronic) + def test_build_hopping_operators_mapper(self): + """Tests that the correct hopping operator is built with a qubit mapper.""" + + hopping_operators, commutativities, indices = build_electronic_ops( + self.electronic_structure_problem.num_spatial_orbitals, + self.electronic_structure_problem.num_particles, + "sd", + self.mapper, + ) + + with self.subTest("hopping operators"): + self.assertEqual(hopping_operators.keys(), expected_hopping_operators_electronic.keys()) + for key, exp_key in zip( + hopping_operators.keys(), expected_hopping_operators_electronic.keys() + ): + self.assertEqual(key, exp_key) + val = hopping_operators[key].primitive + exp_val = expected_hopping_operators_electronic[exp_key] + if not val.equiv(exp_val): + print(val) + print(exp_val) + self.assertTrue(val.equiv(exp_val), msg=(val, exp_val)) + + with self.subTest("commutativities"): + self.assertEqual(commutativities, expected_commutativies_electronic) + + with self.subTest("excitation indices"): + self.assertEqual(indices, expected_indices_electronic) + if __name__ == "__main__": unittest.main() diff --git a/test/second_q/algorithms/excited_state_solvers/test_qeom_vibrational_ops.py b/test/second_q/algorithms/excited_state_solvers/test_qeom_vibrational_ops.py index 4268aab083..172d7f9a91 100644 --- a/test/second_q/algorithms/excited_state_solvers/test_qeom_vibrational_ops.py +++ b/test/second_q/algorithms/excited_state_solvers/test_qeom_vibrational_ops.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -41,7 +41,9 @@ class TestHoppingOpsBuilder(QiskitNatureTestCase): def setUp(self): super().setUp() algorithm_globals.random_seed = 8 - self.qubit_converter = QubitConverter(DirectMapper()) + + self.mapper = DirectMapper() + self.qubit_converter = QubitConverter(self.mapper) import sparse as sp # pylint: disable=import-error @@ -108,6 +110,34 @@ def test_build_hopping_operators(self): with self.subTest("excitation indices"): self.assertEqual(indices, expected_indices_vibrational) + def test_build_hopping_operators_mapper(self): + """Tests that the correct hopping operator is built with a qubit mapper.""" + + hopping_operators, commutativities, indices = build_vibrational_ops( + self.basis.num_modals, "sd", self.mapper + ) + + with self.subTest("hopping operators"): + self.assertEqual( + hopping_operators.keys(), expected_hopping_operators_vibrational.keys() + ) + for key, exp_key in zip( + hopping_operators.keys(), expected_hopping_operators_vibrational.keys() + ): + self.assertEqual(key, exp_key) + val = hopping_operators[key].primitive + exp_val = expected_hopping_operators_vibrational[exp_key] + if not val.equiv(exp_val): + print(val) + print(exp_val) + self.assertTrue(val.equiv(exp_val), msg=(val, exp_val)) + + with self.subTest("commutativities"): + self.assertEqual(commutativities, expected_commutativies_vibrational) + + with self.subTest("excitation indices"): + self.assertEqual(indices, expected_indices_vibrational) + if __name__ == "__main__": unittest.main() diff --git a/test/second_q/algorithms/ground_state_solvers/test_groundstate_eigensolver.py b/test/second_q/algorithms/ground_state_solvers/test_groundstate_eigensolver.py index 49b241da3f..8d5dc41b8c 100644 --- a/test/second_q/algorithms/ground_state_solvers/test_groundstate_eigensolver.py +++ b/test/second_q/algorithms/ground_state_solvers/test_groundstate_eigensolver.py @@ -54,7 +54,8 @@ def setUp(self): self.reference_energy = -1.1373060356951838 - self.qubit_converter = QubitConverter(JordanWignerMapper()) + self.mapper = JordanWignerMapper() + self.qubit_converter = QubitConverter(self.mapper) self.electronic_structure_problem = self.driver.run() self.num_spatial_orbitals = 2 @@ -82,6 +83,13 @@ def test_vqe_uccsd(self): res = calc.solve(self.electronic_structure_problem) self.assertAlmostEqual(res.total_energies[0], self.reference_energy, places=6) + def test_vqe_uccsd_mapper(self): + """Test VQE UCCSD case with QubitMapper""" + solver = VQEUCCFactory(Estimator(), UCC(excitations="d"), SLSQP()) + calc = GroundStateEigensolver(self.mapper, solver) + res = calc.solve(self.electronic_structure_problem) + self.assertAlmostEqual(res.total_energies[0], self.reference_energy, places=6) + def test_vqe_uccsd_with_callback(self): """Test VQE UCCSD with callback.""" diff --git a/test/second_q/circuit/library/ansatzes/test_chc.py b/test/second_q/circuit/library/ansatzes/test_chc.py index 19937dc7ee..70b4b4c2bb 100644 --- a/test/second_q/circuit/library/ansatzes/test_chc.py +++ b/test/second_q/circuit/library/ansatzes/test_chc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2022. +# (C) Copyright IBM 2019, 2023. # # 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 @@ -73,10 +73,6 @@ def test_chc_vscf(self): vibr_op = VibrationalOp(vibrational_op_labels, num_modals) - converter = QubitConverter(DirectMapper()) - - qubit_op = converter.convert_match(vibr_op) - init_state = VSCF(num_modals) num_qubits = sum(num_modals) @@ -88,12 +84,22 @@ def test_chc_vscf(self): ) optimizer = COBYLA(maxiter=1000) - algo = VQE(Estimator(), chc_ansatz, optimizer) - vqe_result = algo.compute_minimum_eigenvalue(qubit_op) - energy = vqe_result.optimal_value - self.assertAlmostEqual(energy, self.reference_energy, places=4) + mapper = DirectMapper() + converter = QubitConverter(mapper) + + with self.subTest("Qubit Converter object"): + qubit_op = converter.convert_match(vibr_op) + vqe_result = algo.compute_minimum_eigenvalue(qubit_op) + energy = vqe_result.optimal_value + self.assertAlmostEqual(energy, self.reference_energy, places=4) + + with self.subTest("Qubit Mapper object"): + qubit_op = mapper.map(vibr_op) + vqe_result = algo.compute_minimum_eigenvalue(qubit_op) + energy = vqe_result.optimal_value + self.assertAlmostEqual(energy, self.reference_energy, places=4) if __name__ == "__main__": diff --git a/test/second_q/circuit/library/ansatzes/test_puccd.py b/test/second_q/circuit/library/ansatzes/test_puccd.py index 6723b76272..ec75cf9065 100644 --- a/test/second_q/circuit/library/ansatzes/test_puccd.py +++ b/test/second_q/circuit/library/ansatzes/test_puccd.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -134,16 +134,26 @@ def test_raise_non_singlet(self): ) def test_puccd_ansatz_generalized(self, num_spatial_orbitals, num_particles, expect): """Tests the generalized PUCCD Ansatz.""" - converter = QubitConverter(JordanWignerMapper()) - - ansatz = PUCCD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - generalized=True, - ) - - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) + + with self.subTest("Qubit Converter object"): + ansatz = PUCCD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + generalized=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + + with self.subTest("Qubit Mapper object"): + ansatz = PUCCD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + generalized=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) if __name__ == "__main__": diff --git a/test/second_q/circuit/library/ansatzes/test_succd.py b/test/second_q/circuit/library/ansatzes/test_succd.py index 8af214a257..2747a198be 100644 --- a/test/second_q/circuit/library/ansatzes/test_succd.py +++ b/test/second_q/circuit/library/ansatzes/test_succd.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -56,15 +56,24 @@ class TestSUCCD(QiskitNatureTestCase): ) def test_succd_ansatz(self, num_spatial_orbitals, num_particles, expect): """Tests the SUCCD Ansatz.""" - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) - ansatz = SUCCD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - ) + with self.subTest("Qubit Converter object"): + ansatz = SUCCD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + with self.subTest("Qubit Mapper object"): + ansatz = SUCCD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) @unpack @data( @@ -101,16 +110,26 @@ def test_succd_ansatz_with_singles( self, num_spatial_orbitals, num_particles, include_singles, expect ): """Tests the SUCCD Ansatz with included single excitations.""" - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) - ansatz = SUCCD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - include_singles=include_singles, - ) + with self.subTest("Qubit Converter object"): + ansatz = SUCCD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + include_singles=include_singles, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + with self.subTest("Qubit Mapper object"): + ansatz = SUCCD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + include_singles=include_singles, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) def test_raise_non_singlet(self): """Test an error is raised when the number of alpha and beta electrons differ.""" @@ -146,16 +165,26 @@ def test_raise_non_singlet(self): ) def test_succd_ansatz_generalized(self, num_spatial_orbitals, num_particles, expect): """Tests the generalized SUCCD Ansatz.""" - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) - ansatz = SUCCD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - generalized=True, - ) + with self.subTest("Qubit Converter object"): + ansatz = SUCCD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + generalized=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + with self.subTest("Qubit Mapper object"): + ansatz = SUCCD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + generalized=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) @unpack @data( @@ -179,16 +208,26 @@ def test_succd_ansatz_generalized(self, num_spatial_orbitals, num_particles, exp ) def test_succ_mirror(self, num_spatial_orbitals, num_particles, expect): """Tests the `mirror` option of the SUCCD Ansatz.""" - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) - ansatz = SUCCD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - mirror=True, - ) + with self.subTest("Qubit Converter object"): + ansatz = SUCCD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + mirror=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + with self.subTest("Qubit Mapper object"): + ansatz = SUCCD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + mirror=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) @unpack @data( @@ -259,17 +298,28 @@ def test_succ_mirror_with_singles( self, num_spatial_orbitals, num_particles, include_singles, expect ): """Tests the succ_mirror Ansatz with included single excitations.""" - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) - ansatz = SUCCD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - include_singles=include_singles, - mirror=True, - ) + with self.subTest("Qubit Converter object"): + ansatz = SUCCD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + include_singles=include_singles, + mirror=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + with self.subTest("Qubit Mapper object"): + ansatz = SUCCD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + include_singles=include_singles, + mirror=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) @unpack @data( @@ -312,17 +362,28 @@ def test_succ_mirror_with_singles( ) def test_succ_mirror_ansatz_generalized(self, num_spatial_orbitals, num_particles, expect): """Tests the generalized succ_mirror Ansatz.""" - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) - ansatz = SUCCD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - generalized=True, - mirror=True, - ) + with self.subTest("Qubit Converter object"): + ansatz = SUCCD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + generalized=True, + mirror=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + with self.subTest("Qubit Mapper object"): + ansatz = SUCCD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + generalized=True, + mirror=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) if __name__ == "__main__": diff --git a/test/second_q/circuit/library/ansatzes/test_ucc.py b/test/second_q/circuit/library/ansatzes/test_ucc.py index 4f0c60a38f..8c94fa98a5 100644 --- a/test/second_q/circuit/library/ansatzes/test_ucc.py +++ b/test/second_q/circuit/library/ansatzes/test_ucc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -120,16 +120,26 @@ class TestUCC(QiskitNatureTestCase): ) def test_ucc_ansatz(self, excitations, num_spatial_orbitals, num_particles, expect): """Tests the UCC Ansatz.""" - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) - ansatz = UCC( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - excitations=excitations, - ) + with self.subTest("Qubit Converter object"): + ansatz = UCC( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + excitations=excitations, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + with self.subTest("Qubit Mapper object"): + ansatz = UCC( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + excitations=excitations, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) @unpack @data( @@ -179,17 +189,31 @@ def test_transpile_no_parameters(self): num_spatial_orbitals = 4 num_particles = (2, 2) - qubit_converter = QubitConverter(mapper=JordanWignerMapper()) - ansatz = UCC( - num_spatial_orbitals=num_spatial_orbitals, - num_particles=num_particles, - qubit_converter=qubit_converter, - excitations="s", - ) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) + + with self.subTest("Qubit Converter object"): + ansatz = UCC( + num_spatial_orbitals=num_spatial_orbitals, + num_particles=num_particles, + qubit_converter=converter, + excitations="s", + ) + + ansatz = transpile(ansatz, optimization_level=3) + self.assertEqual(ansatz.num_qubits, 8) + + with self.subTest("Qubit Mapper object"): + ansatz = UCC( + num_spatial_orbitals=num_spatial_orbitals, + num_particles=num_particles, + qubit_converter=mapper, + excitations="s", + ) - ansatz = transpile(ansatz, optimization_level=3) - self.assertEqual(ansatz.num_qubits, 8) + ansatz = transpile(ansatz, optimization_level=3) + self.assertEqual(ansatz.num_qubits, 8) def test_build_ucc(self): """Test building UCC""" @@ -276,6 +300,14 @@ def test_build_ucc(self): self.assertIsNotNone(ucc.operators) self.assertEqual(ucc.num_qubits, 4) + with self.subTest("Change qubit converter to qubit mapper"): + mapper = ParityMapper() + ucc.qubit_converter = mapper + self.assertIsNotNone(ucc.operators) + self.assertEqual(ucc.qubit_converter, mapper) + self.assertEqual(ucc.num_qubits, 6) + # TODO: PR #1018 Add test with parity mapper and two qubit reduction + if __name__ == "__main__": unittest.main() diff --git a/test/second_q/circuit/library/ansatzes/test_uccsd.py b/test/second_q/circuit/library/ansatzes/test_uccsd.py index 52a1645551..68db5bad6f 100644 --- a/test/second_q/circuit/library/ansatzes/test_uccsd.py +++ b/test/second_q/circuit/library/ansatzes/test_uccsd.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -101,15 +101,24 @@ class TestUCCSD(QiskitNatureTestCase): ) def test_uccsd_ansatz(self, num_spatial_orbitals, num_particles, expect): """Tests the UCCSD Ansatz.""" - converter = QubitConverter(JordanWignerMapper()) - - ansatz = UCCSD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - ) - - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) + + with self.subTest("Qubit Converter object"): + ansatz = UCCSD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + + with self.subTest("Qubit Mapper object"): + ansatz = UCCSD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) @unpack @data( @@ -137,16 +146,26 @@ def test_uccsd_ansatz(self, num_spatial_orbitals, num_particles, expect): ) def test_uccsd_ansatz_generalized(self, num_spatial_orbitals, num_particles, expect): """Tests the generalized UCCSD Ansatz.""" - converter = QubitConverter(JordanWignerMapper()) - - ansatz = UCCSD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - generalized=True, - ) - - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) + + with self.subTest("Qubit Converter object"): + ansatz = UCCSD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + generalized=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + + with self.subTest("Qubit Mapper object"): + ansatz = UCCSD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + generalized=True, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) @unpack @data( @@ -184,16 +203,26 @@ def test_uccsd_ansatz_generalized(self, num_spatial_orbitals, num_particles, exp ) def test_uccsd_ansatz_preserve_spin(self, num_spatial_orbitals, num_particles, expect): """Tests UCCSD Ansatz with spin flips.""" - converter = QubitConverter(JordanWignerMapper()) - - ansatz = UCCSD( - qubit_converter=converter, - num_particles=num_particles, - num_spatial_orbitals=num_spatial_orbitals, - preserve_spin=False, - ) - - assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) + + with self.subTest("Qubit Converter object"): + ansatz = UCCSD( + qubit_converter=converter, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + preserve_spin=False, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) + + with self.subTest("Qubit Mapper object"): + ansatz = UCCSD( + qubit_converter=mapper, + num_particles=num_particles, + num_spatial_orbitals=num_spatial_orbitals, + preserve_spin=False, + ) + assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect) if __name__ == "__main__": diff --git a/test/second_q/circuit/library/ansatzes/test_uvcc.py b/test/second_q/circuit/library/ansatzes/test_uvcc.py index 001035ed4b..0d1939209c 100644 --- a/test/second_q/circuit/library/ansatzes/test_uvcc.py +++ b/test/second_q/circuit/library/ansatzes/test_uvcc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -60,20 +60,32 @@ class TestUVCC(QiskitNatureTestCase): ) def test_ucc_ansatz(self, excitations, num_modals, expect): """Tests the UVCC Ansatz.""" - converter = QubitConverter(DirectMapper()) + mapper = DirectMapper() + converter = QubitConverter(mapper) - ansatz = UVCC(qubit_converter=converter, num_modals=num_modals, excitations=excitations) + with self.subTest("Qubit Converter object"): + ansatz = UVCC(qubit_converter=converter, num_modals=num_modals, excitations=excitations) + assert_ucc_like_ansatz(self, ansatz, num_modals, expect) - assert_ucc_like_ansatz(self, ansatz, num_modals, expect) + with self.subTest("Qubit Mapper object"): + ansatz = UVCC(qubit_converter=mapper, num_modals=num_modals, excitations=excitations) + assert_ucc_like_ansatz(self, ansatz, num_modals, expect) def test_transpile_no_parameters(self): """Test transpilation without parameters""" - qubit_converter = QubitConverter(mapper=DirectMapper()) + mapper = DirectMapper() + converter = QubitConverter(mapper) - ansatz = UVCC(qubit_converter=qubit_converter, num_modals=[2], excitations="s") - ansatz = transpile(ansatz, optimization_level=3) - self.assertEqual(ansatz.num_qubits, 2) + with self.subTest("Qubit Converter object"): + ansatz = UVCC(qubit_converter=converter, num_modals=[2], excitations="s") + ansatz = transpile(ansatz, optimization_level=3) + self.assertEqual(ansatz.num_qubits, 2) + + with self.subTest("Qubit Mapper object"): + ansatz = UVCC(qubit_converter=mapper, num_modals=[2], excitations="s") + ansatz = transpile(ansatz, optimization_level=3) + self.assertEqual(ansatz.num_qubits, 2) class TestUVCCVSCF(QiskitNatureTestCase): @@ -117,23 +129,27 @@ def test_uvcc_vscf(self): vibrational_op_labels = _create_labels(co2_2modes_2modals_2body) vibr_op = VibrationalOp(vibrational_op_labels, num_modals) - - converter = QubitConverter(DirectMapper()) - - qubit_op = converter.convert_match(vibr_op) - init_state = VSCF(num_modals) - - uvcc_ansatz = UVCC(num_modals, "sd", converter, initial_state=init_state) - - optimizer = COBYLA(maxiter=1000) - - algo = VQE(Estimator(), uvcc_ansatz, optimizer) - vqe_result = algo.compute_minimum_eigenvalue(qubit_op) - - energy = vqe_result.optimal_value - - self.assertAlmostEqual(energy, self.reference_energy, places=4) + mapper = DirectMapper() + converter = QubitConverter(mapper) + + with self.subTest("Qubit Converter object"): + qubit_op = converter.convert_match(vibr_op) + uvcc_ansatz = UVCC(num_modals, "sd", converter, initial_state=init_state) + optimizer = COBYLA(maxiter=1000) + algo = VQE(Estimator(), uvcc_ansatz, optimizer) + vqe_result = algo.compute_minimum_eigenvalue(qubit_op) + energy = vqe_result.optimal_value + self.assertAlmostEqual(energy, self.reference_energy, places=4) + + with self.subTest("Qubit Mapper object"): + qubit_op = mapper.map(vibr_op) + uvcc_ansatz = UVCC(num_modals, "sd", mapper, initial_state=init_state) + optimizer = COBYLA(maxiter=1000) + algo = VQE(Estimator(), uvcc_ansatz, optimizer) + vqe_result = algo.compute_minimum_eigenvalue(qubit_op) + energy = vqe_result.optimal_value + self.assertAlmostEqual(energy, self.reference_energy, places=4) def test_build_uvcc(self): """Test building UVCC""" diff --git a/test/second_q/circuit/library/initial_states/test_fermionic_gaussian_state.py b/test/second_q/circuit/library/initial_states/test_fermionic_gaussian_state.py index 7be33fadfe..a0992d00e0 100644 --- a/test/second_q/circuit/library/initial_states/test_fermionic_gaussian_state.py +++ b/test/second_q/circuit/library/initial_states/test_fermionic_gaussian_state.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -28,7 +28,9 @@ class TestFermionicGaussianState(QiskitNatureTestCase): def test_fermionic_gaussian_state(self): """Test preparing fermionic Gaussian states.""" n_orbitals = 5 - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) + quad_ham = random_quadratic_hamiltonian(n_orbitals, seed=5957) ( transformation_matrix, @@ -36,8 +38,6 @@ def test_fermionic_gaussian_state(self): transformed_constant, ) = quad_ham.diagonalizing_bogoliubov_transform() fermionic_op = quad_ham.second_q_op() - qubit_op = converter.convert(fermionic_op) - matrix = qubit_op.to_matrix() occupied_orbitals_lists = [ [], [0], @@ -47,13 +47,28 @@ def test_fermionic_gaussian_state(self): [1, 3, 4], range(n_orbitals), ] - for occupied_orbitals in occupied_orbitals_lists: - circuit = FermionicGaussianState( - transformation_matrix, occupied_orbitals, qubit_converter=converter - ) - final_state = np.array(Statevector(circuit)) - eig = np.sum(orbital_energies[occupied_orbitals]) + transformed_constant - np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-7) + + with self.subTest("Qubit Converter object"): + qubit_op = converter.convert(fermionic_op) + matrix = qubit_op.to_matrix() + for occupied_orbitals in occupied_orbitals_lists: + circuit = FermionicGaussianState( + transformation_matrix, occupied_orbitals, qubit_converter=converter + ) + final_state = np.array(Statevector(circuit)) + eig = np.sum(orbital_energies[occupied_orbitals]) + transformed_constant + np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-7) + + with self.subTest("Qubit Mapper object"): + qubit_op = mapper.map(fermionic_op) + matrix = qubit_op.to_matrix() + for occupied_orbitals in occupied_orbitals_lists: + circuit = FermionicGaussianState( + transformation_matrix, occupied_orbitals, qubit_converter=mapper + ) + final_state = np.array(Statevector(circuit)) + eig = np.sum(orbital_energies[occupied_orbitals]) + transformed_constant + np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-7) def test_no_side_effects(self): """Test that the routines don't mutate the input array.""" @@ -87,3 +102,11 @@ def test_unsupported_mapper(self): np.block([np.eye(2), np.zeros((2, 2))]), qubit_converter=QubitConverter(BravyiKitaevMapper()), ) + + def test_unsupported_mapper_no_converter(self): + """Test passing unsupported mapper fails gracefully when bypassing the qubit converter.""" + with self.assertRaisesRegex(NotImplementedError, "supported"): + _ = FermionicGaussianState( + np.block([np.eye(2), np.zeros((2, 2))]), + qubit_converter=BravyiKitaevMapper(), + ) diff --git a/test/second_q/circuit/library/initial_states/test_hartree_fock.py b/test/second_q/circuit/library/initial_states/test_hartree_fock.py index b8f503229b..752a54c68e 100644 --- a/test/second_q/circuit/library/initial_states/test_hartree_fock.py +++ b/test/second_q/circuit/library/initial_states/test_hartree_fock.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2022. +# (C) Copyright IBM 2020, 2023. # # 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 @@ -61,6 +61,15 @@ def test_raises_on_unsupported_mapper(self): ) state.draw() + def test_raises_on_unsupported_mapper_no_converter(self): + """Test if an error is raised for an unsupported mapper.""" + with self.assertRaises(NotImplementedError): + mapper = BravyiKitaevSuperFastMapper() + state = HartreeFock( + num_spatial_orbitals=2, num_particles=(1, 1), qubit_converter=mapper + ) + state.draw() + def test_qubits_4_jw_h2(self): """qubits 4 jw h2 test""" state = HartreeFock(2, (1, 1), QubitConverter(JordanWignerMapper())) @@ -170,6 +179,49 @@ def test_hf_bitstring_mapped(self): ] self.assertListEqual(bitstr, ref_notaper) + def test_hf_bitstring_mapped_with_qubitmapper(self): + """Mapped bitstring test for water with qubit mapper""" + + num_spatial_orbitals = 7 + num_particles = (5, 5) + mapper = ParityMapper() + converter = QubitConverter(mapper) + + ref_notaper_no_red = [ + True, + False, + True, + False, + True, + True, + True, + False, + True, + False, + True, + False, + False, + False, + ] + + with self.subTest("Qubit Converter object"): + bitstr = hartree_fock_bitstring_mapped( + num_spatial_orbitals=num_spatial_orbitals, + num_particles=num_particles, + qubit_converter=converter, + ) + self.assertListEqual(bitstr, ref_notaper_no_red) + + with self.subTest("Qubit Mapper object"): + bitstr = hartree_fock_bitstring_mapped( + num_spatial_orbitals=num_spatial_orbitals, + num_particles=num_particles, + qubit_converter=mapper, + ) + self.assertListEqual(bitstr, ref_notaper_no_red) + + # TODO: #1018 Add tests for the Parity mapper with two qubit reduction + if __name__ == "__main__": unittest.main() diff --git a/test/second_q/circuit/library/initial_states/test_slater_determinant.py b/test/second_q/circuit/library/initial_states/test_slater_determinant.py index 8e10389173..e7d3e92072 100644 --- a/test/second_q/circuit/library/initial_states/test_slater_determinant.py +++ b/test/second_q/circuit/library/initial_states/test_slater_determinant.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -28,7 +28,8 @@ class TestSlaterDeterminant(QiskitNatureTestCase): def test_slater_determinant(self): """Test preparing Slater determinants.""" n_orbitals = 5 - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) quad_ham = random_quadratic_hamiltonian(n_orbitals, num_conserving=True, seed=8839) ( transformation_matrix, @@ -36,15 +37,28 @@ def test_slater_determinant(self): transformed_constant, ) = quad_ham.diagonalizing_bogoliubov_transform() fermionic_op = quad_ham.second_q_op() - qubit_op = converter.convert(fermionic_op) - matrix = qubit_op.to_matrix() - for n_particles in range(n_orbitals + 1): - circuit = SlaterDeterminant( - transformation_matrix[:n_particles], qubit_converter=converter - ) - final_state = np.array(Statevector(circuit)) - eig = np.sum(orbital_energies[:n_particles]) + transformed_constant - np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-7) + + with self.subTest("Qubit Converter object"): + qubit_op = converter.convert(fermionic_op) + matrix = qubit_op.to_matrix() + for n_particles in range(n_orbitals + 1): + circuit = SlaterDeterminant( + transformation_matrix[:n_particles], qubit_converter=converter + ) + final_state = np.array(Statevector(circuit)) + eig = np.sum(orbital_energies[:n_particles]) + transformed_constant + np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-7) + + with self.subTest("Qubit Mapper object"): + qubit_op = mapper.map(fermionic_op) + matrix = qubit_op.to_matrix() + for n_particles in range(n_orbitals + 1): + circuit = SlaterDeterminant( + transformation_matrix[:n_particles], qubit_converter=mapper + ) + final_state = np.array(Statevector(circuit)) + eig = np.sum(orbital_energies[:n_particles]) + transformed_constant + np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-7) def test_no_side_effects(self): """Test that the routines don't mutate the input array.""" @@ -74,3 +88,8 @@ def test_unsupported_mapper(self): """Test passing unsupported mapper fails gracefully.""" with self.assertRaisesRegex(NotImplementedError, "supported"): _ = SlaterDeterminant(np.eye(2), qubit_converter=QubitConverter(BravyiKitaevMapper())) + + def test_unsupported_mapper_no_converter(self): + """Test passing unsupported mapper fails gracefully when bypassing the qubit converter.""" + with self.assertRaisesRegex(NotImplementedError, "supported"): + _ = SlaterDeterminant(np.eye(2), qubit_converter=BravyiKitaevMapper()) diff --git a/test/second_q/circuit/library/initial_states/test_vscf.py b/test/second_q/circuit/library/initial_states/test_vscf.py index 2f5f525473..af9a17fde1 100644 --- a/test/second_q/circuit/library/initial_states/test_vscf.py +++ b/test/second_q/circuit/library/initial_states/test_vscf.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2022. +# (C) Copyright IBM 2018, 2023. # # 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 @@ -62,6 +62,19 @@ def test_qubits_6_lazy_attribute_setting(self): self.assertEqual(ref, vscf) + def test_qubits_6_lazy_attribute_setting_no_converter(self): + """Test 2 modes 2 modal for the first mode and 4 modals for the second + with lazy attribute setting when bypassing the Qubit converter.""" + num_modals = [2, 4] + mapper = ParityMapper() + vscf = VSCF() + vscf.num_modals = num_modals + vscf.qubit_converter = mapper + ref = QuantumCircuit(6) + ref.x([0, 2]) + + self.assertEqual(ref, vscf) + if __name__ == "__main__": unittest.main() diff --git a/test/second_q/circuit/library/test_bogoliubov_transform.py b/test/second_q/circuit/library/test_bogoliubov_transform.py index ebce5bb618..d708174259 100644 --- a/test/second_q/circuit/library/test_bogoliubov_transform.py +++ b/test/second_q/circuit/library/test_bogoliubov_transform.py @@ -40,7 +40,8 @@ class TestBogoliubovTransform(QiskitNatureTestCase): @data((4, True), (5, True), (4, False), (5, False)) def test_bogoliubov_transform(self, n_orbitals, num_conserving): """Test Bogoliubov transform.""" - converter = QubitConverter(JordanWignerMapper()) + mapper = JordanWignerMapper() + converter = QubitConverter(mapper) hamiltonian = random_quadratic_hamiltonian( n_orbitals, num_conserving=num_conserving, seed=5740 ) @@ -49,14 +50,26 @@ def test_bogoliubov_transform(self, n_orbitals, num_conserving): orbital_energies, transformed_constant, ) = hamiltonian.diagonalizing_bogoliubov_transform() - matrix = converter.map(hamiltonian.second_q_op()).to_matrix() - bog_circuit = BogoliubovTransform(transformation_matrix, qubit_converter=converter) - for initial_state in range(2**n_orbitals): - state = Statevector.from_int(initial_state, dims=2**n_orbitals) - final_state = np.array(state.evolve(bog_circuit)) - occupied_orbitals = [i for i in range(n_orbitals) if initial_state >> i & 1] - eig = np.sum(orbital_energies[occupied_orbitals]) + transformed_constant - np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-8) + + with self.subTest("Qubit Converter object"): + matrix = converter.convert_only(hamiltonian.second_q_op()).to_matrix() + bog_circuit = BogoliubovTransform(transformation_matrix, qubit_converter=converter) + for initial_state in range(2**n_orbitals): + state = Statevector.from_int(initial_state, dims=2**n_orbitals) + final_state = np.array(state.evolve(bog_circuit)) + occupied_orbitals = [i for i in range(n_orbitals) if initial_state >> i & 1] + eig = np.sum(orbital_energies[occupied_orbitals]) + transformed_constant + np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-8) + + with self.subTest("Qubit Mapper object"): + matrix = mapper.map(hamiltonian.second_q_op()).to_matrix() + bog_circuit = BogoliubovTransform(transformation_matrix, qubit_converter=mapper) + for initial_state in range(2**n_orbitals): + state = Statevector.from_int(initial_state, dims=2**n_orbitals) + final_state = np.array(state.evolve(bog_circuit)) + occupied_orbitals = [i for i in range(n_orbitals) if initial_state >> i & 1] + eig = np.sum(orbital_energies[occupied_orbitals]) + transformed_constant + np.testing.assert_allclose(matrix @ final_state, eig * final_state, atol=1e-8) @data(4, 5) def test_bogoliubov_transform_compose_num_conserving(self, n_orbitals): @@ -141,3 +154,8 @@ def test_unsupported_mapper(self): """Test passing unsupported mapper fails gracefully.""" with self.assertRaisesRegex(NotImplementedError, "supported"): _ = BogoliubovTransform(np.eye(2), qubit_converter=QubitConverter(BravyiKitaevMapper())) + + def test_unsupported_mapper_no_converter(self): + """Test passing unsupported mapper fails gracefully when bypassing the qubit converter.""" + with self.assertRaisesRegex(NotImplementedError, "supported"): + _ = BogoliubovTransform(np.eye(2), qubit_converter=BravyiKitaevMapper()) diff --git a/test/second_q/mappers/test_jordan_wigner_mapper.py b/test/second_q/mappers/test_jordan_wigner_mapper.py index 478dbc6cd3..046f07ade2 100644 --- a/test/second_q/mappers/test_jordan_wigner_mapper.py +++ b/test/second_q/mappers/test_jordan_wigner_mapper.py @@ -100,6 +100,50 @@ def test_mapping_for_single_op(self): expected = SparsePauliOp.from_list([("X", 0.5 * a), ("Y", -0.5j * a)], dtype=object) self.assertEqual(JordanWignerMapper().map(op).primitive, expected) + def test_mapping_for_list_ops(self): + """Test for list of single register operator.""" + ops = [ + FermionicOp({"+_0": 1}, num_spin_orbitals=1), + FermionicOp({"-_0": 1}, num_spin_orbitals=1), + FermionicOp({"+_0 -_0": 1}, num_spin_orbitals=1), + FermionicOp({"-_0 +_0": 1}, num_spin_orbitals=1), + FermionicOp({"": 1}, num_spin_orbitals=1), + ] + expected = [ + PauliSumOp.from_list([("X", 0.5), ("Y", -0.5j)]), + PauliSumOp.from_list([("X", 0.5), ("Y", 0.5j)]), + PauliSumOp.from_list([("I", 0.5), ("Z", -0.5)]), + PauliSumOp.from_list([("I", 0.5), ("Z", 0.5)]), + PauliSumOp.from_list([("I", 1)]), + ] + + mapped_ops = JordanWignerMapper().map(ops) + self.assertEqual(len(mapped_ops), len(expected)) + for mapped_op, expected_op in zip(mapped_ops, expected): + self.assertEqual(mapped_op, expected_op) + + def test_mapping_for_dict_ops(self): + """Test for dict of single register operator.""" + ops = { + "+": FermionicOp({"+_0": 1}, num_spin_orbitals=1), + "-": FermionicOp({"-_0": 1}, num_spin_orbitals=1), + "N": FermionicOp({"+_0 -_0": 1}, num_spin_orbitals=1), + "E": FermionicOp({"-_0 +_0": 1}, num_spin_orbitals=1), + "I": FermionicOp({"": 1}, num_spin_orbitals=1), + } + expected = { + "+": PauliSumOp.from_list([("X", 0.5), ("Y", -0.5j)]), + "-": PauliSumOp.from_list([("X", 0.5), ("Y", 0.5j)]), + "N": PauliSumOp.from_list([("I", 0.5), ("Z", -0.5)]), + "E": PauliSumOp.from_list([("I", 0.5), ("Z", 0.5)]), + "I": PauliSumOp.from_list([("I", 1)]), + } + + mapped_ops = JordanWignerMapper().map(ops) + self.assertEqual(len(mapped_ops), len(expected)) + for k in mapped_ops.keys(): + self.assertEqual(mapped_ops[k], expected[k]) + if __name__ == "__main__": unittest.main() diff --git a/test/second_q/mappers/test_qubit_converter.py b/test/second_q/mappers/test_qubit_converter.py index 2426900330..3939aaefb5 100644 --- a/test/second_q/mappers/test_qubit_converter.py +++ b/test/second_q/mappers/test_qubit_converter.py @@ -87,6 +87,8 @@ def setUp(self): self.driver_result = driver.run() self.num_particles = self.driver_result.num_particles self.h2_op, _ = self.driver_result.second_q_ops() + self.mapper = ParityMapper() + self.qubit_conv = QubitConverter(self.mapper) def test_mapping_basic(self): """Test mapping to qubit operator""" @@ -272,6 +274,23 @@ def test_molecular_problem_sector_locator_z2_symmetry(self): ) self.assertEqual(qubit_op, TestQubitConverter.REF_H2_JW_TAPERED) + def test_compatibiliy_with_mappers(self): + """Test that QubitConverter.convert() is equivalent to QubitMapper.map() without any reduction""" + + with self.subTest("JordanWigner Mapper"): + mapper = JordanWignerMapper() + qubit_conv = QubitConverter(mapper) + qubit_op_converter = mapper.map(self.h2_op) + qubit_op_mapper = qubit_conv.convert(self.h2_op) + self.assertEqual(qubit_op_converter, qubit_op_mapper) + + with self.subTest("Parity Mapper"): + mapper = ParityMapper() + qubit_conv = QubitConverter(mapper) + qubit_op_converter = mapper.map(self.h2_op) + qubit_op_mapper = qubit_conv.convert(self.h2_op) + self.assertEqual(qubit_op_converter, qubit_op_mapper) + if __name__ == "__main__": unittest.main()