Source code for qiskit_nature.second_q.mappers.tapered_qubit_mapper

# 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.

"""The Tapered Qubit Mapper."""

from __future__ import annotations

import logging
from typing import cast

from qiskit_algorithms.list_or_dict import ListOrDict as ListOrDictType
from qiskit.quantum_info.analysis.z2_symmetries import Z2Symmetries
from qiskit.quantum_info.operators import SparsePauliOp

from qiskit_nature.second_q.operators import SparseLabelOp

from .qubit_mapper import QubitMapper, _ListOrDict

logger = logging.getLogger(__name__)


[docs]class TaperedQubitMapper(QubitMapper): """The wrapper around qubit mappers implementing the logic to reduce the size of a problem (operator) based on mathematical ``Z2Symmetries`` that can be automatically detected in the operator. The following attributes can be read and updated once the ``TaperedQubitMapper`` object has been constructed. Attributes: mapper: Object defining the mapping of second quantized operators to Pauli operators. z2symmetries: Symmetries to use to reduce the Pauli operators. """ def __init__( self, mapper: QubitMapper, z2symmetries: Z2Symmetries = Z2Symmetries([], [], []), ): """ Args: mapper: ``QubitMapper`` object implementing the mapping of second quantized operators to Pauli operators. z2symmetries: ``Z2Symmetries`` object defining the symmetries to use to reduce the Pauli operators. Raises: ValueError: If the input mapper is already a ``TaperedQubitMapper``. """ super().__init__() if isinstance(mapper, TaperedQubitMapper): raise ValueError( "TaperedQubitMapper cannot be nested in another TaperedQubitMapper. " "If you want to update your TaperedQubitMapper instance please " "build a new one starting from the standard mappers." ) self.mapper: QubitMapper = mapper self.z2symmetries = z2symmetries def _map_clifford_single( self, second_q_op: SparseLabelOp, *, register_length: int | None = None ) -> SparsePauliOp: mapped_op = self.mapper.map(second_q_op, register_length=register_length) converted_op = self.z2symmetries.convert_clifford(mapped_op) return converted_op def _taper_clifford_single(self, converted_op: SparsePauliOp) -> SparsePauliOp: # Mappers do not apply symmetry reduction if the tapering values were not set to specify the # eigen-sector in which lies the solution. if self.z2symmetries.tapering_values is None: return converted_op else: tapered_op = cast(SparsePauliOp, self.z2symmetries.taper_clifford(converted_op)) return tapered_op def _map_single( self, second_q_op: SparseLabelOp, *, register_length: int | None = None ) -> SparsePauliOp: converted_op = self._map_clifford_single(second_q_op, register_length=register_length) tapered_op = self._taper_clifford_single(converted_op) return tapered_op
[docs] def map_clifford( 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 internal mapper. Then, composes all mapped pauli operators with the clifford operations defined by the internal ``Z2Symmetries`` to prepare for the symmetry reduction. This composition gives isospectral operators and exposes redundant qubits for later tapering. Args: second_q_ops: A second quantized operator, or list (resp. dict) thereof. 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: 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[SparsePauliOp] = _ListOrDict() for name, second_q_op in iter(wrapped_second_q_ops): qubit_ops[name] = self._map_clifford_single( second_q_op, register_length=register_length ) returned_ops = qubit_ops.unwrap(wrapped_type) return returned_ops
[docs] def taper_clifford( self, pauli_ops: SparsePauliOp | ListOrDictType[SparsePauliOp], *, check_commutes: bool = True, suppress_none: bool = True, ) -> SparsePauliOp | None | ListOrDictType[SparsePauliOp | None]: """Applies the symmetry reduction on a ``SparsePauliOp`` or a list (resp. dict). This method implies that the second quantized operators were already mapped to Pauli operators and composed with the clifford operations defined in the symmetry, for example using the ``map_clifford`` method. Args: pauli_ops: A pauli operator already evolved with the symmetry clifford operations. check_commutes: If the commutativity of operators with symmetries must be checked before any calculation. suppress_none: If None should be placed in the output list where an operator did not commute with symmetry, to maintain order, or whether that should be suppressed where the output list length may then be smaller than the input. 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_pauli_ops, wrapped_type = _ListOrDict.wrap(pauli_ops) qubit_ops: _ListOrDict[SparsePauliOp] if self.z2symmetries.is_empty(): qubit_ops = wrapped_pauli_ops else: qubit_ops = _ListOrDict() for name, pauli_op in iter(wrapped_pauli_ops): if check_commutes and not self._check_commutes(pauli_op): qubit_ops[name] = None else: qubit_ops[name] = self._taper_clifford_single(pauli_op) returned_ops: SparsePauliOp | ListOrDictType[SparsePauliOp] = qubit_ops.unwrap( wrapped_type, suppress_none=suppress_none ) return returned_ops
[docs] def map( self, second_q_ops: SparseLabelOp | ListOrDictType[SparseLabelOp], *, register_length: int | None = None, ) -> SparsePauliOp | None | ListOrDictType[SparsePauliOp | None]: """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. """ # NOTE: we do not rely on the `_map_single` method here because we want to ensure that # `check_commutes=True` is set. This is not done via `_map_single` because the correct # filtering of `None` values can only be done on the level of `taper_clifford` pauli_ops = self.map_clifford(second_q_ops, register_length=register_length) # This choice of keyword arguments ensures that the output does not contain None. tapered_ops = self.taper_clifford(pauli_ops, check_commutes=True, suppress_none=True) return tapered_ops
def _check_commutes(self, qubit_op: SparsePauliOp) -> bool: logger.debug("Checking operator commutes with symmetries:") # We use sq_paulis instead of symmetries because the qubit operator was already composed with the # cliffords defined in the symmetry. converted_symmetries = self.z2symmetries._sq_paulis commuting_rows = qubit_op.paulis.commutes_with_all(converted_symmetries) commutes = len(commuting_rows) == qubit_op.size logger.debug(" '%s' commutes: %s", id(qubit_op), commutes) return commutes