diff --git a/.pylintdict b/.pylintdict index 608370663..10edb01ff 100644 --- a/.pylintdict +++ b/.pylintdict @@ -282,6 +282,7 @@ jcf jcp jernigan ji +jiang jk jl jordan @@ -291,6 +292,7 @@ json jupyter jw kagome +kalev kanav ket kitaev @@ -361,6 +363,7 @@ moller molssi morse mpl +mruczkiewicz mul multi multigraph @@ -381,6 +384,7 @@ neq networkx neuropeptide neutron +neven nicholas nielsen nisq @@ -664,11 +668,16 @@ xcfun xdata xy xyz +xzy ydata yx +yxz yy +yzx zeitschrift zi zmatrix zsh +zxy +zyx zz diff --git a/qiskit_nature/second_q/mappers/__init__.py b/qiskit_nature/second_q/mappers/__init__.py index ab9d67a5c..cf7463656 100644 --- a/qiskit_nature/second_q/mappers/__init__.py +++ b/qiskit_nature/second_q/mappers/__init__.py @@ -38,6 +38,14 @@ BravyiKitaevSuperFastMapper JordanWignerMapper ParityMapper + TernaryTreeMapper + +**Note:** :class:`~qiskit_nature.second_q.mappers.TernaryTreeMapper` maps +:class:`~qiskit_nature.second_q.operators.MajoranaOp` to +:class:`~qiskit.quantum_info.SparsePauliOp`. In order to use it on a +:class:`~qiskit_nature.second_q.operators.FermionicOp`, convert to a +:class:`~qiskit_nature.second_q.operators.MajoranaOp` first using +:code:`MajoranaOp.from_fermionic_op()`. **Interleaved Qubit-Ordering:** If you want to generate qubit operators where the alpha-spin and beta-spin components are mapped to the qubit register in an interleaved (rather than the default @@ -98,6 +106,7 @@ from .bravyi_kitaev_mapper import BravyiKitaevMapper from .jordan_wigner_mapper import JordanWignerMapper from .parity_mapper import ParityMapper +from .ternary_tree_mapper import TernaryTreeMapper from .linear_mapper import LinearMapper from .bosonic_linear_mapper import BosonicLinearMapper from .bosonic_logarithmic_mapper import BosonicLogarithmicMapper @@ -114,6 +123,7 @@ "DirectMapper", "JordanWignerMapper", "ParityMapper", + "TernaryTreeMapper", "LinearMapper", "BosonicLinearMapper", "BosonicLogarithmicMapper", diff --git a/qiskit_nature/second_q/mappers/bosonic_linear_mapper.py b/qiskit_nature/second_q/mappers/bosonic_linear_mapper.py index 6430d1a93..d05e0f0ea 100644 --- a/qiskit_nature/second_q/mappers/bosonic_linear_mapper.py +++ b/qiskit_nature/second_q/mappers/bosonic_linear_mapper.py @@ -29,7 +29,7 @@ class BosonicLinearMapper(BosonicMapper): """The Linear boson-to-qubit mapping. This mapper generates a linear encoding of the Bosonic operator :math:`b_k^\\dagger, b_k` to qubit - operators (linear combinations of pauli strings). + operators (linear combinations of Pauli strings). In this linear encoding each bosonic mode is represented via :math:`n_k^{max} + 1` qubits, where :math:`n_k^{max}` is the max occupation 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). diff --git a/qiskit_nature/second_q/mappers/bosonic_mapper.py b/qiskit_nature/second_q/mappers/bosonic_mapper.py index 5f12fea0d..7165abb6b 100644 --- a/qiskit_nature/second_q/mappers/bosonic_mapper.py +++ b/qiskit_nature/second_q/mappers/bosonic_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 diff --git a/qiskit_nature/second_q/mappers/majorana_mapper.py b/qiskit_nature/second_q/mappers/majorana_mapper.py new file mode 100644 index 000000000..6a884682e --- /dev/null +++ b/qiskit_nature/second_q/mappers/majorana_mapper.py @@ -0,0 +1,33 @@ +# 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. + +"""Majorana Mapper.""" + +from __future__ import annotations + +from qiskit.quantum_info import SparsePauliOp + +from qiskit_nature.second_q.operators import MajoranaOp + +from .qubit_mapper import ListOrDictType, QubitMapper + + +class MajoranaMapper(QubitMapper): + """Mapper of Majorana Operator to Qubit Operator""" + + def map( + self, + second_q_ops: MajoranaOp | ListOrDictType[MajoranaOp], + *, + register_length: int | None = None, + ) -> SparsePauliOp | ListOrDictType[SparsePauliOp]: + return super().map(second_q_ops, register_length=register_length) diff --git a/qiskit_nature/second_q/mappers/mode_based_mapper.py b/qiskit_nature/second_q/mappers/mode_based_mapper.py index e5d76db80..79593a0a8 100644 --- a/qiskit_nature/second_q/mappers/mode_based_mapper.py +++ b/qiskit_nature/second_q/mappers/mode_based_mapper.py @@ -61,7 +61,7 @@ def sparse_pauli_operators( # pylint: disable=unused-argument """Generates the :class:`.SparsePauliOp` terms. - This uses :meth:`.QubitMapper.pauli_table` to construct a list of operators used to + This uses :meth:`.pauli_table` to construct a list of operators used to translate the second-quantization symbols into qubit operators. Args: @@ -113,22 +113,21 @@ def mode_based_mapping( register_length = second_q_op.register_length times_creation_op, times_annihilation_op = self.sparse_pauli_operators(register_length) + mapped_string_length = times_creation_op[0].num_qubits # make sure ret_op_list is not empty by including a zero op - ret_op_list = [SparsePauliOp("I" * register_length, coeffs=[0])] + ret_op_list = [SparsePauliOp("I" * mapped_string_length, coeffs=[0])] for terms, coeff in second_q_op.terms(): # 1. Initialize an operator list with the identity scaled by the `coeff` - ret_op = SparsePauliOp("I" * register_length, coeffs=np.array([coeff])) + ret_op = SparsePauliOp("I" * mapped_string_length, coeffs=np.array([coeff])) # Go through the label and replace the fermion operators by their qubit-equivalent, then # save the respective Pauli string in the pauli_str list. for term in terms: char = term[0] - if char == "": - break position = int(term[1]) - if char == "+": + if char in ("+", ""): # "" for MajoranaOp, creator = annihilator ret_op = ret_op.compose(times_creation_op[position], front=True).simplify() elif char == "-": ret_op = ret_op.compose(times_annihilation_op[position], front=True).simplify() diff --git a/qiskit_nature/second_q/mappers/ternary_tree_mapper.py b/qiskit_nature/second_q/mappers/ternary_tree_mapper.py new file mode 100644 index 000000000..ed8374e6e --- /dev/null +++ b/qiskit_nature/second_q/mappers/ternary_tree_mapper.py @@ -0,0 +1,127 @@ +# 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. + +"""Ternary Tree Mapper.""" + +from __future__ import annotations + +from functools import lru_cache + +from qiskit.quantum_info.operators import SparsePauliOp + +from .majorana_mapper import MajoranaMapper +from .mode_based_mapper import ModeBasedMapper, PauliType + + +class TernaryTreeMapper(MajoranaMapper, ModeBasedMapper): + """Ternary Tree fermion-to-qubit mapping. + + As described by Jiang, Kalev, Mruczkiewicz, and Neven, Quantum 4, 276 (2020), + preprint at `arXiv:1910.10746 `_. + + This is a mapper for :class:`~qiskit_nature.second_q.operators.MajoranaOp`. + For mapping :class:`~qiskit_nature.second_q.operators.FermionicOp` convert + to a Majorana operator first: + + .. code-block:: + + from qiskit_nature.second_q.operators import FermionicOp, MajoranaOp + from qiskit_nature.second_q.mappers import TernaryTreeMapper + fermionic_op = FermionicOp(...) + majorana_op = MajoranaOp.from_fermionic_op(fermionic_op) + majorana_op = majorana_op.index_order().simplify() + pauli_op = TernaryTreeMapper().map(majorana_op) + + """ + + _pauli_priority: str = "ZXY" + + def __init__(self, pauli_priority: str = "ZXY"): + """ + Use the Pauli priority argument (one of XYZ, XZY, YXZ, YZX, ZXY, ZYX) to influence which + Pauli operators appear most frequently in the Pauli strings. The default is 'ZXY', due to + the fact that the Z gate is usually the most directly supported gate. + + Args: + pauli_priority (str) : Priority with which Pauli operators are assigned. + + Raises: + ValueError: if pauli_priority is not one of XYZ, XZY, YXZ, YZX, ZXY, ZYX + """ + super().__init__() + self._pauli_priority = pauli_priority.upper() + if self._pauli_priority not in ("XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX"): + raise ValueError("Pauli priority must be one of XYZ, XZY, YXZ, YZX, ZXY, ZYX") + + def pauli_table(self, register_length: int) -> list[tuple[PauliType, PauliType]]: + """This method is implemented for ``TernaryTreeMapper`` only for compatibility. + ``TernaryTreeMapper.map`` only uses the ``sparse_pauli_operators`` method which + overrides the corresponding method in `ModeBasedMapper`. + """ + pauli_table = [] + for pauli in self._pauli_table(self._pauli_priority, register_length)[1]: + # creator/annihilator are constructed as (real +/- imaginary) / 2 + # for Majorana ops (self adjoint) we have imaginary = 0 and need + # to multiply the operators from _pauli_table by 2 to get the correct pre-factor + pauli_table.append((2 * pauli[0], SparsePauliOp([""]))) + return pauli_table + + def _pauli_string_length(self, register_length: int) -> int: + return self._pauli_table(self._pauli_priority, register_length)[0] + + @staticmethod + @lru_cache(maxsize=32) + def _pauli_table( + pauli_priority: str, register_length: int + ) -> tuple[int, list[tuple[PauliType]]]: + tree_height = 0 + while 3 ** (tree_height + 1) <= register_length + 1: + tree_height += 1 + add_nodes = register_length + 1 - 3**tree_height + + pauli_x, pauli_y, pauli_z = tuple(pauli_priority) + pauli_list: list[tuple[str, list, int]] = [("", [], 1)] + qubit_index = 0 + for _ in range(tree_height): + new_pauli_list = [] + for paulis, qubits, coeff in pauli_list: + new_pauli_list.append((paulis + pauli_x, qubits + [qubit_index], coeff)) + new_pauli_list.append((paulis + pauli_y, qubits + [qubit_index], coeff)) + new_pauli_list.append((paulis + pauli_z, qubits + [qubit_index], coeff)) + qubit_index += 1 + pauli_list = new_pauli_list + while add_nodes > 0: + paulis, qubits, coeff = pauli_list.pop(0) + pauli_list.append((paulis + pauli_x, qubits + [qubit_index], coeff)) + pauli_list.append((paulis + pauli_y, qubits + [qubit_index], coeff)) + add_nodes -= 1 + if add_nodes > 0: + pauli_list.append((paulis + pauli_z, qubits + [qubit_index], coeff)) + add_nodes -= 1 + qubit_index += 1 + + num_qubits = qubit_index + pauli_list.pop() # only 2n of the 2n+1 operators are independent + pauli_ops = [(SparsePauliOp.from_sparse_list([pauli], num_qubits),) for pauli in pauli_list] + return num_qubits, pauli_ops + + def sparse_pauli_operators( + self, register_length: int + ) -> tuple[list[SparsePauliOp], list[SparsePauliOp]]: + times_creation_annihiliation_op = [] + + for paulis in self._pauli_table(self._pauli_priority, register_length)[1]: + # For Majorana ops (self adjoint) pauli_table is a list of 1-element tuples + creation_op = SparsePauliOp(paulis[0], coeffs=[1]) + times_creation_annihiliation_op.append(creation_op) + + return (times_creation_annihiliation_op, times_creation_annihiliation_op) diff --git a/releasenotes/notes/add-ternary-tree-mapper-dad9b02ae4af54d2.yaml b/releasenotes/notes/add-ternary-tree-mapper-dad9b02ae4af54d2.yaml new file mode 100644 index 000000000..44e81bad6 --- /dev/null +++ b/releasenotes/notes/add-ternary-tree-mapper-dad9b02ae4af54d2.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds a new mapper class, :class:`~qiskit_nature.second_q.mappers.TernaryTreeMapper` + along with a new parent class :class:`~qiskit_nature.second_q.mappers.MajoranaMapper` + implementing the Ternary Tree based mapping algorithm + (cf. `arXiv:1910.10746 `_) that maps a `MajoranaOp` + to a `SparsePauliOp`. diff --git a/test/second_q/mappers/test_ternary_tree_mapper.py b/test/second_q/mappers/test_ternary_tree_mapper.py new file mode 100644 index 000000000..a10f9d043 --- /dev/null +++ b/test/second_q/mappers/test_ternary_tree_mapper.py @@ -0,0 +1,185 @@ +# 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 Ternary Tree Mapper """ + +import unittest +from test import QiskitNatureTestCase + +import numpy as np +from qiskit.circuit import Parameter +from qiskit.quantum_info import SparsePauliOp +from qiskit_algorithms import NumPyEigensolver + +import qiskit_nature.optionals as _optionals +from qiskit_nature.second_q.drivers import PySCFDriver +from qiskit_nature.second_q.mappers import TernaryTreeMapper +from qiskit_nature.second_q.operators import MajoranaOp + + +class TestTernaryTreeMapper(QiskitNatureTestCase): + """Test Ternary Tree Mapper""" + + REF_H2 = SparsePauliOp.from_list( + [ + ("IIII", -0.81054798160031430), + ("ZIII", -0.22575349071287365), + ("IZII", +0.17218393211855787), + ("ZZII", +0.12091263243164174), + ("IIZI", -0.22575349071287362), + ("ZIZI", +0.17464343053355980), + ("IZZI", +0.16614543242281926), + ("IIIZ", +0.17218393211855818), + ("ZIIZ", +0.16614543242281926), + ("IZIZ", +0.16892753854646372), + ("IIZZ", +0.12091263243164174), + ("XXXX", +0.04523279999117751), + ("YYXX", +0.04523279999117751), + ("XXYY", +0.04523279999117751), + ("YYYY", +0.04523279999117751), + ] + ) + + @unittest.skipIf(not _optionals.HAS_PYSCF, "pyscf not available.") + def test_mapping(self): + """Test spectrum of H2 molecule.""" + driver = PySCFDriver() + driver_result = driver.run() + fermionic_op, _ = driver_result.second_q_ops() + majorana_op = MajoranaOp.from_fermionic_op(fermionic_op) + mapper = TernaryTreeMapper() + qubit_op = mapper.map(majorana_op) + result = NumPyEigensolver().compute_eigenvalues(qubit_op).eigenvalues + expected = NumPyEigensolver().compute_eigenvalues(TestTernaryTreeMapper.REF_H2).eigenvalues + self.assertTrue(np.isclose(result, expected)) + + def test_pauli_table(self): + """Test that Pauli table satisfies Majorana anticommutation relations.""" + for num_modes in range(1, 10): + pauli_tab = [] + for pauli in TernaryTreeMapper("XYZ").pauli_table(num_modes): + self.assertEqualSparsePauliOp(pauli[1], SparsePauliOp([""])) + pauli_tab.append(pauli[0] / 2) + self.assertEqual(len(pauli_tab), num_modes) + identity = SparsePauliOp("I" * pauli_tab[0].num_qubits) + for i, first in enumerate(pauli_tab): + anticommutator = (first.dot(first) + first.dot(first)).simplify() + self.assertEqual(anticommutator, 2 * identity) + for j in range(i): + second = pauli_tab[j] + anticommutator = (first.dot(second) + second.dot(first)).simplify() + self.assertEqual(anticommutator, 0 * identity) + + def test_mapping_for_single_op(self): + """Test for double register operator.""" + with self.subTest("test all ops for num_modes=4"): + num_modes = 4 + ops = ["_0", "_1", "_2", "_3"] + expected = ["IY", "IZ", "XX", "YX"] + for op, pauli_string in zip(ops, expected): + majorana_op = MajoranaOp({op: 1}, num_modes=num_modes) + expected_op = SparsePauliOp.from_list([(pauli_string, 1)]) + mapped = TernaryTreeMapper("XYZ").map(majorana_op) + self.assertEqualSparsePauliOp(mapped, expected_op) + + with self.subTest("test all ops for num_modes=8"): + num_modes = 8 + ops = ["_0", "_1", "_2", "_3", "_4", "_5", "_6", "_7"] + expected = ["IIXX", "IIYX", "IIZX", "IXIY", "IYIY", "IZIY", "XIIZ", "YIIZ"] + for op, pauli_string in zip(ops, expected): + majorana_op = MajoranaOp({op: 1}, num_modes=num_modes) + expected_op = SparsePauliOp.from_list([(pauli_string, 1)]) + mapped = TernaryTreeMapper("XYZ").map(majorana_op) + self.assertEqualSparsePauliOp(mapped, expected_op) + + with self.subTest("test parameters"): + a = Parameter("a") + op = MajoranaOp({"_0": a}, num_modes=2) + expected = SparsePauliOp.from_list([("X", a)], dtype=object) + qubit_op = TernaryTreeMapper("XYZ").map(op) + self.assertEqual(qubit_op, expected) + + with self.subTest("test empty operator"): + op = MajoranaOp({}, num_modes=2) + expected = SparsePauliOp.from_list([("I", 0)]) + qubit_op = TernaryTreeMapper("XYZ").map(op) + self.assertEqual(qubit_op, expected) + + with self.subTest("test constant operator"): + op = MajoranaOp({"": 2.2}, num_modes=2) + expected = SparsePauliOp.from_list([("I", 2.2)]) + qubit_op = TernaryTreeMapper("XYZ").map(op) + self.assertEqual(qubit_op, expected) + + def test_mapping_for_list_ops(self): + """Test for list of single register operator.""" + ops = [ + MajoranaOp({"_0": 1}, num_modes=2), + MajoranaOp({"_1": 1}, num_modes=2), + MajoranaOp({"_0 _1": 1}, num_modes=2), + ] + expected = [ + SparsePauliOp.from_list([("X", 1)]), + SparsePauliOp.from_list([("Y", 1)]), + SparsePauliOp.from_list([("Z", 1j)]), + ] + + mapped_ops = TernaryTreeMapper("XYZ").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 = { + "gamma0": MajoranaOp({"_0": 1}, num_modes=2), + "gamma1": MajoranaOp({"_1": 1}, num_modes=2), + "gamma0 gamma1": MajoranaOp({"_0 _1": 1}, num_modes=2), + } + expected = { + "gamma0": SparsePauliOp.from_list([("X", 1)]), + "gamma1": SparsePauliOp.from_list([("Y", 1)]), + "gamma0 gamma1": SparsePauliOp.from_list([("Z", 1j)]), + } + + mapped_ops = TernaryTreeMapper("XYZ").map(ops) + self.assertEqual(len(mapped_ops), len(expected)) + for k in mapped_ops.keys(): + self.assertEqual(mapped_ops[k], expected[k]) + + def test_mapping_overwrite_reg_len(self): + """Test overwriting the register length.""" + op = MajoranaOp({"_0 _1": 1}, num_modes=2) + expected = MajoranaOp({"_0 _1": 1}, num_modes=3) + mapper = TernaryTreeMapper("XYZ") + self.assertEqual(mapper.map(op, register_length=3), mapper.map(expected)) + + def test_mapping_pauli_priority(self): + """Test different settings for Pauli priority.""" + ops = [MajoranaOp({"_0": 1}, num_modes=2), MajoranaOp({"_1": 1}, num_modes=2)] + strings = "XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX" + for string in strings: + with self.subTest(string): + mapped = TernaryTreeMapper(string).map(ops) + expected = [ + SparsePauliOp.from_list([(string[0], 1)]), + SparsePauliOp.from_list([(string[1], 1)]), + ] + self.assertEqual(mapped, expected) + + with self.subTest("invalid"): + with self.assertRaises(ValueError): + TernaryTreeMapper("ABC") + + +if __name__ == "__main__": + unittest.main()