# 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.
"""A QubitMapper wrapper to transform from block-ordered to interleaved qubits."""
from __future__ import annotations
from qiskit.quantum_info import SparsePauliOp
from qiskit_nature.second_q.operators import FermionicOp
from .fermionic_mapper import FermionicMapper
[docs]class InterleavedQubitMapper(FermionicMapper):
    """A ``FermionicMapper`` wrapper returning interleaved-ordered operators.
    This class is intended to be used with fermionic systems. Furthermore, it is designed to work
    with ``FermionicMapper`` classes which map fermionic operators to the qubit space by site
    (unlike for example the :class:`~qiskit_nature.second_q.mappers.BravyiKitaevSuperFastMapper`
    which maps by interactions).
    .. warning::
       The mapper will _not_ perform any assertions on the wrapped ``FermionicMapper``. Thus,
       wrapping a :class:`~qiskit_nature.second_q.mappers.BravyiKitaevSuperFastMapper` is valid code
       which will indeed produce qubit operators for you. You will just not be able to interpret the
       order of the qubits in the same way.
    .. warning::
        The builtin two-qubit reduction of the :class:`.ParityMapper` will also not provide correct
        results when combined with this mapper. Again, this is not asserted so be aware of this
        pitfall.
        Thus, if you would like to reduce the number of qubits, you should instead look towards the
        :class:`.TaperedQubitMapper` which removes qubits based on all Z2-symmetries it detects in
        the operator.
    For site-based mappers, Qiskit Nature always arranges the qubits corresponding to the alpha-spin
    and beta-spin components in a blocked fashion. I.e. the first half of the qubit register
    corresponds to the alpha-spin components, and the second half to the beta-spin one, like so:
    .. code-block::
       a1, a2, ..., aN, b1, b2, ..., bN
    This class allows you to wrap such a ``FermionicMapper`` to produce qubit operators which have
    an interleaved order of qubits, instead. Taking the example from before, the outcome will be the
    following:
    .. code-block::
       a1, b1, a2, b2, ..., aN, bN
    .. note::
       This reordering is intended for an even total number of spin orbitals (i.e. the alpha-spin
       and beta-spin components should be identical in length; which they usually are). However,
       this is not asserted, so reordering a qubit operator label of odd length will still happen.
    Here is a very simple usage example:
    .. code-block:: python
       from qiskit_nature.second_q.mappers import JordanWignerMapper, InterleavedQubitMapper
       from qiskit_nature.second_q.operators import FermionicOp
       blocked_mapper = JordanWignerMapper()
       interleaved_mapper = InterleavedQubitMapper(blocked_mapper)
       ferm_op = FermionicOp({"+_0 -_1": 1}, num_spin_orbitals=4)
       blocked_op = blocked_mapper.map(ferm_op)
       # SparsePauliOp(['IIXY', 'IIYY', 'IIXX', 'IIYX'], coeffs=[-0.25j, 0.25, 0.25, 0.25j])
       print(interleaved_mapper.map(ferm_op))
       # SparsePauliOp(['IXIY', 'IYIY', 'IXIX', 'IYIX'], coeffs=[-0.25j, 0.25, 0.25, 0.25j])
    The following attributes can be set via the initializer but can also be read and updated once
    the ``InterleavedQubitMapper`` object has been constructed.
    Attributes:
        mapper (FermionicMapper): the actual mapper for mapping from :class:`.FermionicOp` to qubit
            operators.
    """
    def __init__(self, mapper: FermionicMapper):
        """
        Args:
            mapper: the actual ``FermionicMapper`` mapping :class:`.FermionicOp` to qubit operators.
        """
        self.mapper = mapper
    def _map_single(
        self, second_q_op: FermionicOp, *, register_length: int | None = None
    ) -> SparsePauliOp:
        if register_length is None:
            register_length = second_q_op.register_length
        interleaved_sec_op = second_q_op.permute_indices(
            list(range(0, register_length, 2)) + list(range(1, register_length, 2))
        )
        interleaved_op = self.mapper._map_single(
            interleaved_sec_op, register_length=register_length
        )
        return interleaved_op