Source code for qdk_chemistry.algorithms.qubit_mapper.qdk_qubit_mapper

"""QDK native qubit mapper using Majorana-level C++ building blocks.

This module provides the QdkQubitMapper class for transforming electronic structure
Hamiltonians to qubit operators. The encoding is specified by a
:class:`~qdk_chemistry.data.MajoranaMapping` passed to ``run()``, making the
mapper encoding-agnostic.
"""

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from qdk_chemistry._core.data import (
    majorana_map_hamiltonian,
    sparse_pauli_word_to_label,
)
from qdk_chemistry.algorithms.qubit_mapper.qubit_mapper import QubitMapper, QubitMapperSettings
from qdk_chemistry.data.enums.fermion_mode_order import FermionModeOrder
from qdk_chemistry.data.qubit_operator import QubitOperator
from qdk_chemistry.utils import Logger

if TYPE_CHECKING:
    from qdk_chemistry.data import Hamiltonian, MajoranaMapping

__all__ = ["QdkQubitMapper", "QdkQubitMapperSettings"]


[docs] class QdkQubitMapperSettings(QubitMapperSettings): """Settings configuration for a QdkQubitMapper. Settings: threshold (double, default=1e-12): Threshold for pruning small Pauli coefficients. integral_threshold (double, default=1e-12): Threshold for filtering small integrals. """
[docs] def __init__(self) -> None: """Initialize QdkQubitMapperSettings.""" Logger.trace_entering() super().__init__() self._set_default( "threshold", "double", 1e-12, "Threshold for pruning small Pauli coefficients", ) self._set_default( "integral_threshold", "double", 1e-12, "Threshold for filtering small integrals (improves performance)", )
[docs] class QdkQubitMapper(QubitMapper): """QDK native qubit mapper using Majorana-level C++ engine. This is a **table-driven** backend: it reads the Pauli-string table from the :class:`~qdk_chemistry.data.MajoranaMapping` and passes it directly to the C++ ``majorana_map_hamiltonian`` engine. Any valid ``MajoranaMapping`` works — built-in (Jordan-Wigner, Bravyi-Kitaev, parity, BK-tree, SCBK) or custom user-defined tables. The mapping's ``name`` and ``base_encoding`` are used only for metadata on the output :class:`~qdk_chemistry.data.QubitOperator`, not for dispatch. Both restricted (RHF) and unrestricted (UHF) Hamiltonians are supported. For unrestricted systems, the engine handles all four spin-channel ERI blocks (aa, ab, ba, bb) independently. The two-body integrals are consumed in the native storage format of the Hamiltonian's container (the C++ engine dispatches on the container type): * :class:`~qdk_chemistry.data.SparseHamiltonianContainer`: the engine iterates only the stored non-zero ``(p, q, r, s)`` entries, so both memory and runtime improve for the mostly-zero integrals of lattice / model Hamiltonians. * :class:`~qdk_chemistry.data.CholeskyHamiltonianContainer`: the three-center factors are kept in their ``O(N**2 * naux)`` form and the auxiliary index is contracted one ``(pq|.)`` row at a time, so the dense ``N**4`` tensor is never built and peak additional memory is a single ``N**2`` row. * All other containers use the dense engine path. In all cases the result is numerically equivalent (term-by-term, to within ``1e-12``) to the dense path. The mapper uses canonical blocked spin-orbital ordering internally: qubits 0..N-1 for alpha spin, qubits N..2N-1 for beta spin (where N is the number of spatial orbitals). Use ``QubitOperator.to_interleaved()`` for alternative qubit orderings. Examples: >>> from qdk_chemistry.algorithms import create >>> from qdk_chemistry.data import MajoranaMapping >>> mapper = create("qubit_mapper") >>> mapping = MajoranaMapping.jordan_wigner(num_modes=n_spin_orbitals) >>> qh = mapper.run(hamiltonian, mapping) """
[docs] def __init__( self, threshold: float = 1e-12, integral_threshold: float = 1e-12, ) -> None: """Initialize the QdkQubitMapper with default settings. Args: threshold: Threshold for pruning small Pauli coefficients. Default: 1e-12. integral_threshold: Threshold for filtering small integrals. Default: 1e-12. """ super().__init__() self._settings = QdkQubitMapperSettings() self._settings.set("threshold", threshold) self._settings.set("integral_threshold", integral_threshold)
[docs] def name(self) -> str: """Return the algorithm name.""" return "qdk"
def _run_impl( self, hamiltonian: Hamiltonian, mapping: MajoranaMapping, ) -> QubitOperator: """Transform a fermionic Hamiltonian to a qubit operator (table-driven). This backend passes the C++ ``MajoranaMapping`` directly to the native mapper. The ``base_encoding`` name is used only for metadata on the output, not for dispatch. If *mapping* carries tapering metadata, the base encoding is extracted first, mapped, and tapering is applied to the result via :meth:`~QubitMapper._taper_result`. Args: hamiltonian: The fermionic Hamiltonian with one-body and two-body integrals. mapping: The Majorana-to-Pauli encoding (table is consumed directly by the C++ engine). Returns: QubitOperator: The qubit operator with Pauli strings and coefficients. """ Logger.trace_entering() # Strip tapering — the C++ engine maps the base encoding only base_mapping = mapping.without_tapering() if mapping.tapering else mapping threshold = float(self.settings().get("threshold")) integral_threshold = float(self.settings().get("integral_threshold")) n_spatial = hamiltonian.get_one_body_integrals()[0].shape[0] n_spin_orbitals = 2 * n_spatial spin_symmetric = hamiltonian.get_orbitals().is_restricted() if base_mapping.num_modes != n_spin_orbitals: raise ValueError( f"MajoranaMapping has {base_mapping.num_modes} modes but the Hamiltonian has " f"{n_spin_orbitals} spin-orbitals (2 x {n_spatial} spatial orbitals). " f"Use MajoranaMapping.jordan_wigner(num_modes={n_spin_orbitals}) or equivalent." ) # The C++ engine dispatches on the container type: sparse and Cholesky # containers feed their native sparse / low-rank two-body integrals # straight into the engine (the dense N^4 tensor is never # materialized); all other containers use the dense engine path. words, coefficients = majorana_map_hamiltonian( base_mapping, hamiltonian, spin_symmetric, threshold, integral_threshold, ) n_qubits = base_mapping.num_qubits pauli_strings = [sparse_pauli_word_to_label(word, n_qubits) for word in words] Logger.debug(f"Generated {len(pauli_strings)} Pauli terms for {n_qubits} qubits") qh = QubitOperator( pauli_strings=pauli_strings, coefficients=np.array(coefficients, dtype=complex), encoding=base_mapping.base_encoding, fermion_mode_order=FermionModeOrder.BLOCKED, ) return self._taper_result(qh, mapping)