Skip to content

Commit

Permalink
Improve mappers, add ModeBasedMapper (#1301)
Browse files Browse the repository at this point in the history
* Implement ModeBasedMapper no caching

* Added caching and release note

* Update release note to pass spell check

* fix typing

* fix typing for Python <3.10

* suggestions by mrossinek plus changes needed to pass pylint

* Update improve-mappers-b55cb0ca5fd656e4.yaml

---------

Co-authored-by: Steve Wood <[email protected]>
Co-authored-by: Max Rossmannek <[email protected]>
  • Loading branch information
3 people authored Apr 17, 2024
1 parent 61a5cb6 commit 295788f
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 130 deletions.
3 changes: 3 additions & 0 deletions qiskit_nature/second_q/mappers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
:nosignatures:
QubitMapper
ModeBasedMapper
FermionicOp Mappers
+++++++++++++++++++
Expand Down Expand Up @@ -101,6 +102,7 @@
from .logarithmic_mapper import LogarithmicMapper
from .direct_mapper import DirectMapper
from .qubit_mapper import QubitMapper
from .mode_based_mapper import ModeBasedMapper
from .interleaved_qubit_mapper import InterleavedQubitMapper
from .tapered_qubit_mapper import TaperedQubitMapper

Expand All @@ -116,4 +118,5 @@
"QubitMapper",
"InterleavedQubitMapper",
"TaperedQubitMapper",
"ModeBasedMapper",
]
11 changes: 7 additions & 4 deletions qiskit_nature/second_q/mappers/bravyi_kitaev_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@
from qiskit.quantum_info.operators import Pauli

from .fermionic_mapper import FermionicMapper
from .mode_based_mapper import ModeBasedMapper, PauliType


class BravyiKitaevMapper(FermionicMapper):
class BravyiKitaevMapper(FermionicMapper, ModeBasedMapper):
"""The Bravyi-Kitaev fermion-to-qubit mapping."""

@classmethod
def pauli_table(self, register_length: int) -> list[tuple[PauliType, PauliType]]:
return self._pauli_table(register_length)

@staticmethod
@lru_cache(maxsize=32)
def pauli_table(cls, register_length: int) -> list[tuple[Pauli, Pauli]]:
# pylint: disable=unused-argument
def _pauli_table(register_length: int) -> list[tuple[PauliType, PauliType]]:
def parity_set(j, n):
"""
Computes the parity set of the j-th orbital in n modes.
Expand Down
11 changes: 7 additions & 4 deletions qiskit_nature/second_q/mappers/direct_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,22 @@
from qiskit.quantum_info.operators import Pauli

from .vibrational_mapper import VibrationalMapper
from .mode_based_mapper import ModeBasedMapper, PauliType


class DirectMapper(VibrationalMapper):
class DirectMapper(VibrationalMapper, ModeBasedMapper):
"""The Direct mapper.
This mapper maps a :class:`~.VibrationalOp` to a qubit operator. In doing so, each modal of the
``VibrationalOp`` gets mapped to a single qubit.
"""

@classmethod
def pauli_table(self, register_length: int) -> list[tuple[PauliType, PauliType]]:
return self._pauli_table(register_length)

@staticmethod
@lru_cache(maxsize=32)
def pauli_table(cls, register_length: int) -> list[tuple[Pauli, Pauli]]:
# pylint: disable=unused-argument
def _pauli_table(register_length: int) -> list[tuple[PauliType, PauliType]]:
pauli_table = []

for i in range(register_length):
Expand Down
11 changes: 7 additions & 4 deletions qiskit_nature/second_q/mappers/jordan_wigner_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@
from qiskit.quantum_info.operators import Pauli

from .fermionic_mapper import FermionicMapper
from .mode_based_mapper import ModeBasedMapper, PauliType


class JordanWignerMapper(FermionicMapper):
class JordanWignerMapper(FermionicMapper, ModeBasedMapper):
"""The Jordan-Wigner fermion-to-qubit mapping."""

@classmethod
def pauli_table(self, register_length: int) -> list[tuple[PauliType, PauliType]]:
return self._pauli_table(register_length)

@staticmethod
@lru_cache(maxsize=32)
def pauli_table(cls, register_length: int) -> list[tuple[Pauli, Pauli]]:
# pylint: disable=unused-argument
def _pauli_table(register_length: int) -> list[tuple[PauliType, PauliType]]:
pauli_table = []

for i in range(register_length):
Expand Down
2 changes: 1 addition & 1 deletion qiskit_nature/second_q/mappers/logarithmic_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def _logarithmic_encoding(
op.chop()
spin_op_encoding.append(op)

return tuple(spin_op_encoding)
return (spin_op_encoding[0], spin_op_encoding[1], spin_op_encoding[2], spin_op_encoding[3])

def _embed_matrix(
self,
Expand Down
143 changes: 143 additions & 0 deletions qiskit_nature/second_q/mappers/mode_based_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# This code is part of a Qiskit project.
#
# (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
# 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.

"""Mode Based Mapper."""

from __future__ import annotations

from typing import Union
from abc import abstractmethod

import numpy as np
from qiskit.quantum_info.operators import Pauli, PauliList, SparsePauliOp

from qiskit_nature import QiskitNatureError
from qiskit_nature.second_q.operators import SparseLabelOp
from qiskit_nature.second_q.mappers.qubit_mapper import QubitMapper

# Types that can be data for a SparsePauliOp
PauliType = Union[PauliList, SparsePauliOp, Pauli, list, str]


class ModeBasedMapper(QubitMapper):
"""Mapper from ``SparseLabelOp`` to a qubit operator using a Pauli table."""

def _map_single(
self, second_q_op: SparseLabelOp, *, register_length: int | None = None
) -> SparsePauliOp:
return self.mode_based_mapping(second_q_op, register_length=register_length)

@abstractmethod
def pauli_table(self, register_length: int) -> list[tuple[PauliType, PauliType]]:
r"""Generates a Pauli-lookup table mapping from modes to Pauli operators or pairs of Pauli
operators.
This table is a list of tuples :math:`(P, Q)` of two Pauli operators, corresponding to the
real part :math:`P` and imaginary part :math:`Q` for the respective mode index. These Pauli
operators are used to construct the creation and annihilation operators
:math:`(P \pm i Q)/2`.
The generated table is processed by :meth:`.sparse_pauli_operators`.
Args:
register_length: the register length for which to generate the table.
Returns:
A list of tuples of two Pauli string operators.
"""

def sparse_pauli_operators(
self, register_length: int
) -> tuple[list[SparsePauliOp], list[SparsePauliOp]]:
# pylint: disable=unused-argument
"""Generates the :class:`.SparsePauliOp` terms.
This uses :meth:`.QubitMapper.pauli_table` to construct a list of operators used to
translate the second-quantization symbols into qubit operators.
Args:
register_length: the register length for which to generate the operators.
Returns:
Two lists stored in a tuple, consisting of the creation and annihilation operators,
applied on the individual modes.
"""
times_creation_op = []
times_annihilation_op = []

for paulis in self.pauli_table(register_length):
real_part = SparsePauliOp(paulis[0], coeffs=[0.5])
imag_part = SparsePauliOp(paulis[1], coeffs=[0.5j])

# The creation operator is given by 0.5*(X - 1j*Y)
creation_op = real_part - imag_part
times_creation_op.append(creation_op)

# The annihilation operator is given by 0.5*(X + 1j*Y)
annihilation_op = real_part + imag_part
times_annihilation_op.append(annihilation_op)

return (times_creation_op, times_annihilation_op)

def mode_based_mapping(
self,
second_q_op: SparseLabelOp,
register_length: int | None = None,
) -> SparsePauliOp:
# pylint: disable=unused-argument
"""Utility method to map a ``SparseLabelOp`` to a qubit operator using a pauli table.
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.
Returns:
The qubit operator corresponding to the problem-Hamiltonian in the qubit space.
Raises:
QiskitNatureError: If number length of pauli table does not match the number
of operator modes, or if the operator has unexpected label content
"""
if register_length is None:
register_length = second_q_op.register_length

times_creation_op, times_annihilation_op = self.sparse_pauli_operators(register_length)

# make sure ret_op_list is not empty by including a zero op
ret_op_list = [SparsePauliOp("I" * register_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]))

# 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 == "+":
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()
# catch any disallowed labels
else:
raise QiskitNatureError(
f"FermionicOp label included '{char}'. Allowed characters: I, N, E, +, -"
)
ret_op_list.append(ret_op)

sparse_op = SparsePauliOp.sum(ret_op_list).simplify()
return sparse_op
14 changes: 8 additions & 6 deletions qiskit_nature/second_q/mappers/parity_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from __future__ import annotations

import logging

from functools import lru_cache

import numpy as np
Expand All @@ -25,11 +24,12 @@

from qiskit_nature.second_q.operators import FermionicOp
from .fermionic_mapper import FermionicMapper
from .mode_based_mapper import ModeBasedMapper, PauliType

logger = logging.getLogger(__name__)


class ParityMapper(FermionicMapper):
class ParityMapper(FermionicMapper, ModeBasedMapper):
"""The Parity fermion-to-qubit mapping.
When using this mapper, :attr:`num_particles` can optionally be used to apply an additional step
Expand Down Expand Up @@ -72,10 +72,12 @@ def num_particles(self, value: tuple[int, int] | None) -> None:
par_2 = 1 if num_alpha % 2 == 0 else -1
self._tapering_values = [par_2, par_1]

@classmethod
def pauli_table(self, register_length: int) -> list[tuple[PauliType, PauliType]]:
return self._pauli_table(register_length)

@staticmethod
@lru_cache(maxsize=32)
def pauli_table(cls, register_length: int) -> list[tuple[Pauli, Pauli]]:
# pylint: disable=unused-argument
def _pauli_table(register_length: int) -> list[tuple[PauliType, PauliType]]:
pauli_table = []

for i in range(register_length):
Expand Down Expand Up @@ -129,7 +131,7 @@ def _two_qubit_reduce(self, operator: SparsePauliOp) -> SparsePauliOp:
def _map_single(
self, second_q_op: FermionicOp, *, register_length: int | None = None
) -> SparsePauliOp:
mapped_op = ParityMapper.mode_based_mapping(second_q_op, register_length=register_length)
mapped_op = self.mode_based_mapping(second_q_op, register_length=register_length)

reduced_op = mapped_op
if self.num_particles is not None:
Expand Down
Loading

0 comments on commit 295788f

Please sign in to comment.