Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve mappers, add ModeBasedMapper #1301

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this because otherwise mypy fails due to this issue: python/mypy#7509


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.
#
# 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:`.QubitMapper.sparse_pauli_operators`.
grossardt marked this conversation as resolved.
Show resolved Hide resolved

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