diff --git a/.pylintdict b/.pylintdict index 1bf007d13..608370663 100644 --- a/.pylintdict +++ b/.pylintdict @@ -51,6 +51,7 @@ bergholm berlin bitstring bksf +bo bogoliubov bohr boltzmann @@ -442,6 +443,7 @@ posteriori pqrs pre prebuilt +prefactor prepend prepended preprint diff --git a/qiskit_nature/second_q/mappers/__init__.py b/qiskit_nature/second_q/mappers/__init__.py index 784545e44..ab9d67a5c 100644 --- a/qiskit_nature/second_q/mappers/__init__.py +++ b/qiskit_nature/second_q/mappers/__init__.py @@ -58,6 +58,7 @@ :nosignatures: BosonicLinearMapper + BosonicLogarithmicMapper VibrationalOp Mappers +++++++++++++++++++++ @@ -99,6 +100,7 @@ from .parity_mapper import ParityMapper from .linear_mapper import LinearMapper from .bosonic_linear_mapper import BosonicLinearMapper +from .bosonic_logarithmic_mapper import BosonicLogarithmicMapper from .logarithmic_mapper import LogarithmicMapper from .direct_mapper import DirectMapper from .qubit_mapper import QubitMapper @@ -114,6 +116,7 @@ "ParityMapper", "LinearMapper", "BosonicLinearMapper", + "BosonicLogarithmicMapper", "LogarithmicMapper", "QubitMapper", "InterleavedQubitMapper", diff --git a/qiskit_nature/second_q/mappers/bosonic_linear_mapper.py b/qiskit_nature/second_q/mappers/bosonic_linear_mapper.py index e366968c7..6430d1a93 100644 --- a/qiskit_nature/second_q/mappers/bosonic_linear_mapper.py +++ b/qiskit_nature/second_q/mappers/bosonic_linear_mapper.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023. +# (C) Copyright IBM 2023, 2024. # # 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 @@ -85,7 +85,7 @@ class BosonicLinearMapper(BosonicMapper): def _map_single( self, second_q_op: BosonicOp, *, register_length: int | None = None ) -> SparsePauliOp: - """Maps a :class:`~qiskit_nature.second_q.operators.SparseLabelOp` to a``SparsePauliOp``. + """Maps a :class:`~qiskit_nature.second_q.operators.SparseLabelOp` to a ``SparsePauliOp``. Args: second_q_op: the ``SparseLabelOp`` to be mapped. @@ -95,6 +95,9 @@ def _map_single( Returns: The qubit operator corresponding to the problem-Hamiltonian in the qubit space. + + Raises: + ValueError: if any term in the bosonic operator is not in the form `+_k` or `-_k`. """ if register_length is None: register_length = second_q_op.num_modes @@ -108,7 +111,10 @@ def _map_single( bos_op_to_pauli_op = SparsePauliOp(["I" * qubit_register_length], coeffs=[1.0]) for op, idx in terms: if op not in ("+", "-"): - break + raise ValueError( + f"Invalid bosonic operator: `{op}_{idx}`." + "All bosonic operators must have the following shape: `+_k` or `-_k`." + ) pauli_expansion: list[SparsePauliOp] = [] # Now we are dealing with a single bosonic operator. We have to perform the linear mapper for n_k in range(self.max_occupation): diff --git a/qiskit_nature/second_q/mappers/bosonic_logarithmic_mapper.py b/qiskit_nature/second_q/mappers/bosonic_logarithmic_mapper.py new file mode 100644 index 000000000..0e6d3d591 --- /dev/null +++ b/qiskit_nature/second_q/mappers/bosonic_logarithmic_mapper.py @@ -0,0 +1,241 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The Logarithmic Mapper for Bosons.""" + +from __future__ import annotations +import operator +import math +import logging + +from functools import reduce, lru_cache + +import numpy as np + +from qiskit.quantum_info import SparsePauliOp + +from qiskit_nature.second_q.operators import BosonicOp +from .bosonic_mapper import BosonicMapper + +logger = logging.getLogger(__name__) + + +class BosonicLogarithmicMapper(BosonicMapper): + """The Logarithmic boson-to-qubit Mapper. + + This mapper generates a logarithmic encoding of the Bosonic operator :math:`b_k^\\dagger, b_k` to + qubit operators (linear combinations of pauli strings). + In this logarithmic encoding the number of qubits necessary to represent a bosonic mode is + determined by the max occupation :math:`n_k^{max}` of the mode (meaning the number of states used + in the expansion of the mode, or equivalently the state at which the maximum excitation can take + place). The number of qubits is given by: + :math:`\\lceil\\log_2(n_k^{max} + 1)\\rceil`. + + .. note:: + A consequence of the rounding up for determining the number of required qubits is that the + actual max occupation is often larger than the one selected by the user. For example, if the + user selects :math:`n_k^{max} = 2`, then the number of required qubits is + :math:`\\lceil\\log_2(3)\\rceil = 2`. If we now compute the max occupation for 2 qubits, we + get :math:`2^2 - 1 = 3`, which is larger than the user-selected max occupation. The user should + expect that the actual max occupation is always larger than or equal to the one selected. + If the code changes the max occupation, a warning will appear in the logs. + + The mode :math:`|k\\rangle` is then mapped to the occupation number vector + :math:`|0_{n_k^{max}}, 0_{n_k^{max} - 1},..., 0_{n_k + 1}, 1_{n_k}, 0_{n_k - 1},..., 0_{0_k}\\rangle` + + This class implements the equation (34) and (35) of Reference [1]. + + .. math:: + b_k^\\dagger = \\sum_{n_k = 0}^{2^{N_q}-2}\\left(\\sqrt{n_k + 1}|n+1\\rangle\\langle n|\\right) + + b_k = \\sum_{n_k = 1}^{2^{N_q}-1}\\left(\\sqrt{n_k}|n-1\\rangle\\langle n|\\right) + + where :math:`N_q` is the number of qubits used to represent each mode + (given by :math:`\\lceil\\log_2(n_k^{max} + 1)\\rceil`). This implementation first computes each + :math:`|n+1\\rangle\\langle n|` and :math:`|n-1\\rangle\\langle n|` in a binary representation + and then uses equation (37) from Reference [1] to map to the Pauli operators. + + The length of the qubit register is: + + .. code-block:: python + + BosonicOp.num_modes * math.ceil(numpy.log2(BosonicLogarithmicMapper.max_occupation + 1)) + + Below is an example of how one can use this mapper: + + .. code-block:: python + + from qiskit_nature.second_q.mappers import BosonicLogarithmicMapper + from qiskit_nature.second_q.operators import BosonicOp + + mapper = BosonicLogarithmicMapper(max_occupation=2) + qubit_op = mapper.map(BosonicOp({'+_0 -_0': 1}, num_modes=1)) + + .. note:: + Since this mapper truncates the maximum occupation of a bosonic state as represented in the + qubit register, the commutation relations after the mapping differ from the standard ones. + Please refer to Section 4, equation 22 of Reference [2] for more details. + + References: + [1] Bo Peng et al., Quantum Simulation of Boson-Related Hamiltonians: Techniques, Effective + Hamiltonian Construction, and Error Analysis, Arxiv https://doi.org/10.48550/arXiv.2307.06580 + + [2] R. Somma et al., Quantum Simulations of Physics Problems, Arxiv + https://doi.org/10.48550/arXiv.quant-ph/0304063 + """ + + def __init__(self, max_occupation: int) -> None: + # Compute the actual max occupation from the one selected by the user + self.number_of_qubits_per_mode: int = ( + 1 if max_occupation == 0 else math.ceil(np.log2(max_occupation + 1)) + ) + max_calculated_occupation = 2**self.number_of_qubits_per_mode - 1 + if max_occupation != max_calculated_occupation: + logger.warning( + f"The user requested a max occupation of {max_occupation}, but the actual " + + f"max occupation is {max_calculated_occupation}." + ) + super().__init__(max_calculated_occupation) + + @property + def number_of_qubits_per_mode(self) -> int: + """The minimum number of qubits required to represent a bosonic mode given a max occupation.""" + return self._number_of_qubits_per_mode + + @number_of_qubits_per_mode.setter + def number_of_qubits_per_mode(self, num_qubits: int) -> None: + if num_qubits < 1: + raise ValueError(f"The number of qubits must be at least 1, and not {num_qubits}.") + self._number_of_qubits_per_mode: int = num_qubits + + def _map_single( + self, second_q_op: BosonicOp, *, register_length: int | None = None + ) -> SparsePauliOp: + """Maps a :class:`~qiskit_nature.second_q.operators.SparseLabelOp` to a ``SparsePauliOp``. + + Args: + second_q_op: the ``SparseLabelOp`` to be mapped. + register_length: when provided, this will be used to overwrite the ``register_length`` + attribute of the operator being mapped. This is possible because the + ``register_length`` is considered a lower bound in a ``SparseLabelOp``. + + Returns: + The qubit operator corresponding to the problem-Hamiltonian in the qubit space. + + Raises: + ValueError: if any term in the bosonic operator is not in the form `+_k` or `-_k`. + """ + if register_length is None: + register_length = second_q_op.num_modes + + # The actual register length is the number of qubits per mode times the number of modes + qubit_register_length: int = register_length * self.number_of_qubits_per_mode + # Create a Pauli operator, which we will fill in this method + pauli_op: list[SparsePauliOp] = [] + # Then we loop over all the terms of the bosonic operator + for terms, coeff in second_q_op.terms(): + # Then loop over each term (terms -> List[Tuple[string, int]]) + bos_op_to_pauli_op = SparsePauliOp(["I" * qubit_register_length], coeffs=[1.0]) + # Loop over the operators in the term + for op, idx in terms: + if op not in ("+", "-"): + raise ValueError( + f"Invalid bosonic operator: `{op}_{idx}`." + "All bosonic operators must have the following shape: `+_k` or `-_k`." + ) + pauli_expansion: list[SparsePauliOp] = [] + # Define the index of the mode in the qubit register + mode_index_in_register: int = idx * self.number_of_qubits_per_mode + # Now we start mapping the operator. First, define the range of the sum + terms_range = ( + range(2**self.number_of_qubits_per_mode - 1) + if op == "+" + else range(1, 2**self.number_of_qubits_per_mode) + ) + for n in terms_range: + # In each iteration we deal with a term of the form sqrt(n+1)*|n+1> SparsePauliOp: + """This method builds the Qiskit Pauli operators of one of the operators: + I_+ = I + Z, I_- = I - Z, S_+ = X + iY and S_- = X - iY. + + Args: + qubit_idx: the register index of the qubit on which the operator is acting. + register_length: the length of the qubit register.\n + qubit_operator: the operator to be mapped. Possible values are: + - '00', which corresponds to '|0><0|' or 'I+' + - '11', which corresponds to '|1><1|' or 'I-' + - '01', which corresponds to '|0><1|' or 'S+' + - '10', which corresponds to '|1><0|' or 'S-' + + Returns: + A SparsePauliOp representing the Pauli operator. + """ + if qubit_operator == "00": # I+ + return SparsePauliOp.from_sparse_list( + [("", [], 0.5), ("Z", [qubit_idx], 0.5)], num_qubits=register_length + ) + if qubit_operator == "11": # I- + return SparsePauliOp.from_sparse_list( + [("", [], 0.5), ("Z", [qubit_idx], -0.5)], num_qubits=register_length + ) + if qubit_operator == "01": # S+ + return SparsePauliOp.from_sparse_list( + [("X", [qubit_idx], 0.5), ("Y", [qubit_idx], 0.5j)], num_qubits=register_length + ) + if qubit_operator == "10": # S- + return SparsePauliOp.from_sparse_list( + [("X", [qubit_idx], 0.5), ("Y", [qubit_idx], -0.5j)], num_qubits=register_length + ) + raise ValueError( + f"Invalid operator {qubit_operator}. Possible values are '00', '11', '01' and '10'." + ) diff --git a/releasenotes/notes/bosonic-logarithmic-mapper-4b1f24c4ca16cf8b.yaml b/releasenotes/notes/bosonic-logarithmic-mapper-4b1f24c4ca16cf8b.yaml new file mode 100644 index 000000000..cd7392095 --- /dev/null +++ b/releasenotes/notes/bosonic-logarithmic-mapper-4b1f24c4ca16cf8b.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + This release introduces a new mapper for the :class:`.BosonicOp`, the :class:`.BosonicLogarithmicMapper`. + It is more efficient both in terms of the number of qubits required and, for some operations, in the number of Pauli strings generated due + to the binary encoding of the bosonic operator. For other operations, such as the hopping term + :math:`b^\dagger_i b_j`, the number of Pauli strings generated is bigger than the linear mapper. + This mapper is based on this `paper `_. + Below is an example of how one can use this mapper, assuming the existence of some :class:`.BosonicOp` instance called ``bos_op``: + + .. code-block:: python + + from qiskit_nature.second_q.mappers import BosonicLogarithmicMapper + mapper = BosonicLogarithmicMapper(max_occupation=2) + qubit_op = mapper.map(bos_op) diff --git a/test/second_q/mappers/test_bosonic_logarithmic_mapper.py b/test/second_q/mappers/test_bosonic_logarithmic_mapper.py new file mode 100644 index 000000000..cdbd4f5ff --- /dev/null +++ b/test/second_q/mappers/test_bosonic_logarithmic_mapper.py @@ -0,0 +1,270 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Test Bosonic Logarithmic Mapper """ + +import unittest + +from test import QiskitNatureTestCase + +from ddt import ddt, data, unpack +import numpy as np + +from qiskit.quantum_info import SparsePauliOp +from qiskit_nature.second_q.operators import BosonicOp +from qiskit_nature.second_q.mappers import BosonicLogarithmicMapper + + +@ddt +class TestBosonicLogarithmicMapper(QiskitNatureTestCase): + """Test Bosonic Logarithmic Mapper""" + + # Define some useful coefficients + sq_2 = np.sqrt(2) + sq_3 = np.sqrt(3) + sq_5 = np.sqrt(5) + sq_6 = np.sqrt(6) + sq_7 = np.sqrt(7) + + bos_op1 = BosonicOp({"+_0": 1}) + # Using: max_occupation = 3 (number_of_qubits_per_mode = 2) + # fmt: off + ref_qubit_op1_nq2 = 0.25 * SparsePauliOp( + ["IX", "IY", "ZX", "ZY", "XX", "XY", "YX", "YY"], + coeffs=[1 + sq_3, -1j * (1 + sq_3), 1 - sq_3, -1j * (1 - sq_3), + sq_2, 1j * sq_2, -1j * sq_2, sq_2,] + ) + # fmt: on + # Using: max_occupation = 7 (number_of_qubits_per_mode = 3) + # fmt: off + ref_qubit_op1_nq3 = 0.125 * SparsePauliOp( + ["IIX", "IIY", "IZX", "IZY", + "ZIX", "ZIY", "ZZX", "ZZY", + "IXX", "IXY", "IYX", "IYY", + "ZXX", "ZXY", "ZYX", "ZYY", + "XXX", "XXY", "XYX", "XYY", + "YXX", "YXY", "YYX", "YYY"], + coeffs=[ + 1+sq_3+sq_5+sq_7, -1j*(1+sq_3+sq_5+sq_7), 1-sq_3+sq_5-sq_7, -1j*(1-sq_3+sq_5-sq_7), + 1+sq_3-sq_5-sq_7, -1j*(1+sq_3-sq_5-sq_7), 1-sq_3-sq_5+sq_7, -1j*(1-sq_3-sq_5+sq_7), + sq_2 + sq_6, 1j * (sq_2 + sq_6), -1j * (sq_2 + sq_6), sq_2 + sq_6, + sq_2 - sq_6, 1j * (sq_2 - sq_6), -1j * (sq_2 - sq_6), sq_2 - sq_6, + 2, 2j, 2j, -2, + -2j, 2, 2, 2j], + ) + # fmt: on + + bos_op2 = BosonicOp({"-_0": 1}) + # Using: max_occupation = 3 (number_of_qubits_per_mode = 2) + # fmt: off + ref_qubit_op2_nq2 = 0.25 * SparsePauliOp( + ["IX", "IY", "ZX", "ZY", "XX", "XY", "YX", "YY"], + coeffs=[ + 1 + sq_3, 1j * (1 + sq_3), 1 - sq_3, 1j * (1 - sq_3), sq_2, -1j * sq_2, 1j * sq_2, sq_2], + ) + # fmt: on + # Using: max_occupation = 7 (number_of_qubits_per_mode = 3) + # fmt: off + ref_qubit_op2_nq3 = 0.125 * SparsePauliOp( + ["IIX", "IIY", "IZX", "IZY", + "ZIX", "ZIY", "ZZX", "ZZY", + "IXX", "IXY", "IYX", "IYY", + "ZXX", "ZXY", "ZYX", "ZYY", + "XXX", "XXY", "XYX", "XYY", + "YXX", "YXY", "YYX", "YYY"], + coeffs=[ + 1+sq_3+sq_5+sq_7, 1j*(1+sq_3+sq_5+sq_7), 1-sq_3+sq_5-sq_7, 1j*(1-sq_3+sq_5-sq_7), + 1+sq_3-sq_5-sq_7, 1j*(1+sq_3-sq_5-sq_7), 1-sq_3-sq_5+sq_7, 1j*(1-sq_3-sq_5+sq_7), + sq_2 + sq_6, -1j * (sq_2 + sq_6), 1j * (sq_2 + sq_6), sq_2 + sq_6, + sq_2 - sq_6, -1j * (sq_2 - sq_6), 1j * (sq_2 - sq_6), sq_2 - sq_6, + 2, -2j, -2j, -2, + 2j, 2, 2, -2j], + ) + # fmt: on + + bos_op3 = BosonicOp({"+_1": 1}) + # Using: max_occupation = 3 (number_of_qubits_per_mode = 2) + # fmt: off + ref_qubit_op3_nq2 = 0.25 * SparsePauliOp( + ["IXII", "IYII", "ZXII", "ZYII", "XXII", "XYII", "YXII", "YYII"], + coeffs=[ + 1 + sq_3, -1j * (1 + sq_3), 1 - sq_3, -1j * (1 - sq_3), sq_2, 1j * sq_2, -1j * sq_2, sq_2], + ) + # fmt: on + # Using: max_occupation = 7 (number_of_qubits_per_mode = 3) + # fmt: off + ref_qubit_op3_nq3 = 0.125 * SparsePauliOp( + ["IIXIII", "IIYIII", "IZXIII", "IZYIII", + "ZIXIII", "ZIYIII", "ZZXIII", "ZZYIII", + "IXXIII", "IXYIII", "IYXIII", "IYYIII", + "ZXXIII", "ZXYIII", "ZYXIII", "ZYYIII", + "XXXIII", "XXYIII", "XYXIII", "XYYIII", + "YXXIII", "YXYIII", "YYXIII", "YYYIII"], + coeffs=[ + 1+sq_3+sq_5+sq_7, -1j*(1+sq_3+sq_5+sq_7), 1-sq_3+sq_5-sq_7, -1j*(1-sq_3+sq_5-sq_7), + 1+sq_3-sq_5-sq_7, -1j*(1+sq_3-sq_5-sq_7), 1-sq_3-sq_5+sq_7, -1j*(1-sq_3-sq_5+sq_7), + sq_2 + sq_6, 1j * (sq_2 + sq_6), -1j * (sq_2 + sq_6), sq_2 + sq_6, + sq_2 - sq_6, 1j * (sq_2 - sq_6), -1j * (sq_2 - sq_6), sq_2 - sq_6, + 2, 2j, 2j, -2, + -2j, 2, 2, 2j], + ) + # fmt: on + + bos_op4 = BosonicOp({"-_1": 1}) + # Using: max_occupation = 3 (number_of_qubits_per_mode = 2) + # fmt: off + ref_qubit_op4_nq2 = 0.25 * SparsePauliOp( + ["IXII", "IYII", "ZXII", "ZYII", "XXII", "XYII", "YXII", "YYII"], + coeffs=[ + 1 + sq_3, 1j * (1 + sq_3), 1 - sq_3, 1j * (1 - sq_3), sq_2, -1j * sq_2, 1j * sq_2, sq_2], + ) + # fmt: on + # Using: max_occupation = 7 (number_of_qubits_per_mode = 3) + # fmt: off + ref_qubit_op4_nq3 = 0.125 * SparsePauliOp( + ["IIXIII", "IIYIII", "IZXIII", "IZYIII", + "ZIXIII", "ZIYIII", "ZZXIII", "ZZYIII", + "IXXIII", "IXYIII", "IYXIII", "IYYIII", + "ZXXIII", "ZXYIII", "ZYXIII", "ZYYIII", + "XXXIII", "XXYIII", "XYXIII", "XYYIII", + "YXXIII", "YXYIII", "YYXIII", "YYYIII"], + coeffs=[ + 1+sq_3+sq_5+sq_7, 1j*(1+sq_3+sq_5+sq_7), 1-sq_3+sq_5-sq_7, 1j*(1-sq_3+sq_5-sq_7), + 1+sq_3-sq_5-sq_7, 1j*(1+sq_3-sq_5-sq_7), 1-sq_3-sq_5+sq_7, 1j*(1-sq_3-sq_5+sq_7), + sq_2 + sq_6, -1j * (sq_2 + sq_6), 1j * (sq_2 + sq_6), sq_2 + sq_6, + sq_2 - sq_6, -1j * (sq_2 - sq_6), 1j * (sq_2 - sq_6), sq_2 - sq_6, + 2, -2j, -2j, -2, + 2j, 2, 2, -2j], + ) + # fmt: on + + bos_op5 = BosonicOp({"+_0 -_0": 1}) + # Using: max_occupation = 3 (number_of_qubits_per_mode = 2) + ref_qubit_op5_nq2 = 0.5 * SparsePauliOp(["II", "IZ", "ZI"], coeffs=[3, -1, -2]) + # Using: max_occupation = 7 (number_of_qubits_per_mode = 3) + ref_qubit_op5_nq3 = 0.5 * SparsePauliOp(["III", "IIZ", "IZI", "ZII"], coeffs=[7, -1, -2, -4]) + + bos_op6 = BosonicOp({"-_0 +_0": 1}) + # Using: max_occupation = 3 (number_of_qubits_per_mode = 2) + ref_qubit_op6_nq2 = 0.5 * SparsePauliOp(["II", "IZ", "ZZ"], coeffs=[3, 1, -2]) + # Using: max_occupation = 7 (number_of_qubits_per_mode = 3) + ref_qubit_op6_nq3 = 0.5 * SparsePauliOp( + ["III", "IIZ", "IZZ", "ZII", "ZIZ", "ZZI", "ZZZ"], coeffs=[7, 1, -2, -2, -2, -2, 2] + ) + + bos_op7 = BosonicOp({"+_0 -_1": 1}) + bos_op8 = BosonicOp({"-_1 +_0": 1}) + # Using: max_occupation = 3 (number_of_qubits_per_mode = 2) + # fmt: off + ref_qubit_op7_8_nq2 = 0.0625 * SparsePauliOp( + ["IXIX", "IYIX", "IXIY", "IYIY", + "ZXIX", "ZYIX", "ZXIY", "ZYIY", + "XXIX", "XYIX", "YXIX", "YYIX", + "XXIY", "XYIY", "YXIY", "YYIY", + "IXZX", "IYZX", "IXZY", "IYZY", + "ZXZX", "ZYZX", "ZXZY", "ZYZY", + "XXZX", "XYZX", "YXZX", "YYZX", + "XXZY", "XYZY", "YXZY", "YYZY", + "IXXX", "IXXY", "IXYX", "IXYY", + "IYXX", "IYXY", "IYYX", "IYYY", + "ZXXX", "ZXXY", "ZXYX", "ZXYY", + "ZYXX", "ZYXY", "ZYYX", "ZYYY", + "XXXX", "XYXX", "YXXX", "YYXX", + "XXXY", "XYXY", "YXXY", "YYXY", + "XXYX", "XYYX", "YXYX", "YYYX", + "XXYY", "XYYY", "YXYY", "YYYY", + ], + coeffs=[ + 4 + 2 * sq_3, 1j * (4 + 2 * sq_3), -1j * (4 + 2 * sq_3), 4 + 2 * sq_3, + -2, -2j, 2j, -2, + sq_2 + sq_6, -1j * (sq_2 + sq_6), 1j * (sq_2 + sq_6), sq_2 + sq_6, + -1j * (sq_2 + sq_6), -(sq_2 + sq_6), sq_2 + sq_6, -1j * (sq_2 + sq_6), + -2, -2j, 2j, -2, + 4 - 2 * sq_3, 1j * (4 - 2 * sq_3), -1j * (4 - 2 * sq_3), 4 - 2 * sq_3, + sq_2 - sq_6, -1j * (sq_2 - sq_6), 1j * (sq_2 - sq_6), sq_2 - sq_6, + -1j * (sq_2 - sq_6), -(sq_2 - sq_6), sq_2 - sq_6, -1j * (sq_2 - sq_6), + sq_2 + sq_6, 1j * (sq_2 + sq_6), -1j * (sq_2 + sq_6), sq_2 + sq_6, + 1j * (sq_2 + sq_6), -(sq_2 + sq_6), sq_2 + sq_6, 1j * (sq_2 + sq_6), + sq_2 - sq_6, 1j * (sq_2 - sq_6), -1j * (sq_2 - sq_6), sq_2 - sq_6, + 1j * (sq_2 - sq_6), -(sq_2 - sq_6), sq_2 - sq_6, 1j * (sq_2 - sq_6), + 2, -2j, 2j, 2, + 2j, 2, -2, 2j, + -2j, -2, 2, -2j, + 2, -2j, 2j, 2], + ) + # fmt: on + # Using: max_occupation = 7 (number_of_qubits_per_mode = 3) + # Computing this analytically is too complex, as it would result in hundreds of Pauli terms. + # Thus we compute the reference by composing previously mapped operators. The correctness of + # this reference is ensured by the fact that the previous operators are covered in some of + # the unit tests in this test class. + ref_qubit_op7_8_nq3 = ref_qubit_op4_nq3.compose( + BosonicLogarithmicMapper(max_occupation=7).map(BosonicOp({"+_0": 1}, num_modes=2)) + ).simplify() + + bos_op9 = BosonicOp({"+_0 +_0": 1}) + # Using: max_occupation = 3 (number_of_qubits_per_mode = 2) + ref_qubit_op9_nq2 = ( + 0.0625 + * 4 + * SparsePauliOp( + ["XI", "YI", "XZ", "YZ"], + coeffs=[sq_2 + sq_6, -1j * (sq_2 + sq_6), sq_2 - sq_6, -1j * (sq_2 - sq_6)], + ) + ) + # Using: max_occupation = 7 (number_of_qubits_per_mode = 3) + # Computing this analytically is too complex. Thus we compute the reference by composing previously + # mapped operators. The correctness of this reference is ensured by the fact that the previous + # operators are covered in some of the unit tests in this test class. + ref_qubit_op9_nq3 = ref_qubit_op1_nq3.compose(ref_qubit_op1_nq3).simplify() + + # Test max_occupation = 3 (number_of_qubits_per_mode = 2) + @data( + (bos_op1, ref_qubit_op1_nq2), + (bos_op2, ref_qubit_op2_nq2), + (bos_op3, ref_qubit_op3_nq2), + (bos_op4, ref_qubit_op4_nq2), + (bos_op5, ref_qubit_op5_nq2), + (bos_op6, ref_qubit_op6_nq2), + (bos_op7, ref_qubit_op7_8_nq2), + (bos_op8, ref_qubit_op7_8_nq2), + (bos_op9, ref_qubit_op9_nq2), + ) + @unpack + def test_mapping_max_occupation_3(self, bos_op, ref_qubit_op): + """Test mapping to qubit operator""" + mapper = BosonicLogarithmicMapper(max_occupation=3) + qubit_op = mapper.map(bos_op) + self.assertEqualSparsePauliOp(qubit_op, ref_qubit_op) + + # Test max_occupation = 7 (number_of_qubits_per_mode = 3) + @data( + (bos_op1, ref_qubit_op1_nq3), + (bos_op2, ref_qubit_op2_nq3), + (bos_op3, ref_qubit_op3_nq3), + (bos_op4, ref_qubit_op4_nq3), + (bos_op5, ref_qubit_op5_nq3), + (bos_op6, ref_qubit_op6_nq3), + (bos_op7, ref_qubit_op7_8_nq3), + (bos_op8, ref_qubit_op7_8_nq3), + (bos_op9, ref_qubit_op9_nq3), + ) + @unpack + def test_mapping_max_occupation_7(self, bos_op, ref_qubit_op): + """Test mapping to qubit operator""" + mapper = BosonicLogarithmicMapper(max_occupation=7) + qubit_op = mapper.map(bos_op) + self.assertEqualSparsePauliOp(qubit_op, ref_qubit_op) + + +if __name__ == "__main__": + unittest.main()