Source code for qiskit_nature.second_q.mappers.parity_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.

"""The Parity Mapper."""

from __future__ import annotations

import logging

from functools import lru_cache

import numpy as np

from qiskit.quantum_info.analysis.z2_symmetries import Z2Symmetries
from qiskit.quantum_info.operators import Pauli, PauliList, SparsePauliOp

from qiskit_nature.second_q.operators import FermionicOp
from .fermionic_mapper import FermionicMapper

logger = logging.getLogger(__name__)


[docs]class ParityMapper(FermionicMapper): """The Parity fermion-to-qubit mapping. When using this mapper, :attr:`num_particles` can optionally be used to apply an additional step of reduction after the mapping to pauli operators. The two-qubit reduction tapers two qubits (middle and last qubit) because the spin orbitals are ordered in two spin sectors (block spin order). Based on the provided number of particles this allows the automatic selection of the correct symmetry sector. .. warning:: Combing this additional two-qubit reduction with the :class:`.InterleavedQubitMapper` will **not** yield the intended result. While the code will work, the hard-coded indices of the qubits which are removed will alter the Hamiltonian in a non-physical way, resulting in a physically incorrect answer. In such a case you should rely on the :class:`.TaperedQubitMapper`, instead. """ def __init__(self, num_particles: tuple[int, int] | None = None): """ Args: num_particles: the number of particles. For more details refer to the class docstring. """ self._tapering_values: list | None = None self.num_particles = num_particles @property def num_particles(self) -> tuple[int, int] | None: """Get number of particles.""" return self._num_particles @num_particles.setter def num_particles(self, value: tuple[int, int] | None) -> None: """Set number of particles.""" self._num_particles = value self._tapering_values = None if self._num_particles is not None: num_alpha = self._num_particles[0] num_beta = self._num_particles[1] par_1 = 1 if (num_alpha + num_beta) % 2 == 0 else -1 par_2 = 1 if num_alpha % 2 == 0 else -1 self._tapering_values = [par_2, par_1]
[docs] @classmethod @lru_cache(maxsize=32) def pauli_table(cls, register_length: int) -> list[tuple[Pauli, Pauli]]: # pylint: disable=unused-argument pauli_table = [] for i in range(register_length): a_z: list[int] | np.ndarray = [0] * (i - 1) + [1] if i > 0 else [] a_x: list[int] | np.ndarray = [0] * (i - 1) + [0] if i > 0 else [] b_z: list[int] | np.ndarray = [0] * (i - 1) + [0] if i > 0 else [] b_x: list[int] | np.ndarray = [0] * (i - 1) + [0] if i > 0 else [] a_z = np.asarray(a_z + [0] + [0] * (register_length - i - 1), dtype=bool) a_x = np.asarray(a_x + [1] + [1] * (register_length - i - 1), dtype=bool) b_z = np.asarray(b_z + [1] + [0] * (register_length - i - 1), dtype=bool) b_x = np.asarray(b_x + [1] + [1] * (register_length - i - 1), dtype=bool) pauli_table.append((Pauli((a_z, a_x)), Pauli((b_z, b_x)))) return pauli_table
def _two_qubit_reduce(self, operator: SparsePauliOp) -> SparsePauliOp: """ Applies the two qubit reduction to the operator. This method hard codes the ``Z2Symmetries`` corresponding to the spin orbitals ordering. The tapering values required to identify the eigen sector of the problem are calculated when :attr:`num_particles` is set. Args: operator: To be tapered operator. Returns: A new operator whose qubit number is reduced by 2. """ num_qubits = operator.num_qubits last_idx = num_qubits - 1 mid_idx = num_qubits // 2 - 1 sq_list = [mid_idx, last_idx] # build symmetries, sq_paulis: symmetries, sq_paulis = [], [] for idx in sq_list: pauli_str = ["I"] * num_qubits pauli_str[idx] = "Z" z_sym = "".join(pauli_str)[::-1] symmetries.append(z_sym) pauli_str[idx] = "X" sq_pauli = "".join(pauli_str)[::-1] sq_paulis.append(sq_pauli) symmetries = PauliList(symmetries) sq_paulis = PauliList(sq_paulis) z2_symmetries = Z2Symmetries(symmetries, sq_paulis, sq_list, self._tapering_values) return z2_symmetries.taper(operator) 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) reduced_op = mapped_op if self.num_particles is not None: if mapped_op.num_qubits > 2: reduced_op = self._two_qubit_reduce(mapped_op) else: logger.warning( "The original qubit operator only contains %s qubits! " "Skipping the requested two-qubit reduction!", mapped_op.num_qubits, ) return reduced_op