Source code for qiskit_nature.second_q.mappers.qubit_mapper

# This code is part of a Qiskit project.
#
# (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
# 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.

"""Qubit Mapper interface."""

from __future__ import annotations

from abc import ABC
from functools import lru_cache
from typing import TypeVar, Dict, Iterable, Generic, Generator

import numpy as np
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: ListOrDictType | None = 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[int | str, T], T, None]:
        """Return the generator-iterator method."""
        return self._generator()

    def _generator(self) -> Generator[tuple[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

    @classmethod
    def wrap(cls, objects: dict | list | T) -> tuple[_ListOrDict, type]:
        """Wraps the provided objects into a ``_ListOrDict`` instance.

        Args:
            objects: a dict, list or single object instance to be wrapped.

        Returns:
            A tuple consisting of the constructed ``_ListOrDict`` and the original type that was
            wrapped.
        """
        wrapped_type = type(objects)

        if not issubclass(wrapped_type, (dict, list)):
            objects = [objects]

        wrapped_objects = cls(objects)

        return wrapped_objects, wrapped_type

    def unwrap(self, wrapped_type: type, *, suppress_none: bool = True) -> 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.

        Returns:
            Content of the current class instance as a list, a dictionary or a single element.
        """

        def _qubit_op_type_wrapper(qubit_op: SparsePauliOp | None):
            if qubit_op is None:
                return None
            return qubit_op

        if wrapped_type == list:
            if suppress_none:
                return [_qubit_op_type_wrapper(op) for _, op in iter(self) if op is not None]
            else:
                return [_qubit_op_type_wrapper(op) for _, op in iter(self)]
        if wrapped_type == dict:
            if suppress_none:
                return {key: _qubit_op_type_wrapper(op) for key, op in iter(self) if op is not None}
            else:
                return {key: _qubit_op_type_wrapper(op) for key, op in iter(self)}
        # only other case left is that it was a single operator to begin with:
        return _qubit_op_type_wrapper(list(iter(self))[0][1])


[docs]class QubitMapper(ABC): """The interface for implementing methods which map from a ``SparseLabelOp`` to a qubit operator in the form of a ``SparsePauliOp``. """ def _map_single( self, second_q_op: SparseLabelOp, *, 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. """ return self.mode_based_mapping(second_q_op, register_length=register_length)
[docs] def map( self, second_q_ops: SparseLabelOp | ListOrDictType[SparseLabelOp], *, register_length: int | None = None, ) -> SparsePauliOp | ListOrDictType[SparsePauliOp]: """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. register_length: when provided, this will be used to overwrite the ``register_length`` attribute of the ``SparseLabelOp`` being mapped. This is possible because the ``register_length`` is considered a lower bound in a ``SparseLabelOp``. Returns: A qubit operator in the form of a ``SparsePauliOp``, or list (resp. dict) thereof if a list (resp. dict) of second quantized operators was supplied. """ wrapped_second_q_ops, wrapped_type = _ListOrDict.wrap(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, register_length=register_length) returned_ops = qubit_ops.unwrap(wrapped_type) # Note the output of the mapping will never be None for standard mappers other than the # TaperedQubitMapper. return returned_ops
[docs] @classmethod @lru_cache(maxsize=32) def pauli_table(cls, register_length: int) -> list[tuple[Pauli, Pauli]]: """Generates a Pauli-lookup table mapping from modes to pauli pairs. The generated table is processed by :meth:`.QubitMapper.sparse_pauli_operators`. Args: register_length: the register length for which to generate the table. Returns: A list of tuples in which the first and second Pauli operator the real and imaginary Pauli strings, respectively. """
[docs] @classmethod @lru_cache(maxsize=32) def sparse_pauli_operators( cls, register_length: int ) -> tuple[list[SparsePauliOp], list[SparsePauliOp]]: # pylint: disable=unused-argument """Generates the cached :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 cls.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)
[docs] @classmethod def mode_based_mapping( cls, 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 = cls.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