Qubit mapping

The QubitMapper algorithm in QDK/Chemistry transforms electronic-structure Hamiltonians into qubit operators suitable for quantum computation. Following QDK/Chemistry’s algorithm design principles, it takes a Hamiltonian instance as input and produces a QubitOperator instance as output. This transformation is essential for executing quantum chemistry algorithms on quantum hardware.

Overview

The QubitMapper algorithm converts fermionic Hamiltonians into qubit-operator representations composed of Pauli strings. This transformation preserves the operator algebra, particle-number constraints, and antisymmetry required by fermionic statistics. The resulting qubit operator is mathematically equivalent to the original fermionic Hamiltonian but is now in a form that can be executed on quantum hardware or simulated by quantum algorithms.

Note

Core energy handling: The core energy (nuclear repulsion + frozen orbital contributions) from the input Hamiltonian is not included in the output QubitOperator. To compute total energies, add hamiltonian.get_core_energy() to expectation values computed from the QubitOperator.

Supported encodings

Different encoding strategies produce mathematically equivalent qubit operators but with different Pauli-string structures. The choice of encoding can affect circuit depth and measurement requirements on quantum hardware. Not every implementation supports all encodings — see Available implementations for details.

Jordan-Wigner [JW28]

Encodes each fermionic mode in a single qubit whose state directly represents the orbital occupation. Fermionic antisymmetry is enforced through a Z-string on all lower-indexed qubits.

Bravyi-Kitaev [SRL12]

Distributes both occupation and parity information across qubits using a binary-tree (Fenwick tree) structure, reducing the average Pauli-string weight to O(log n).

Parity [SRL12]

Encodes qubits with cumulative electron-number parities of the orbitals.

Symmetry-conserving Bravyi-Kitaev [BGMT17]

Exploits particle-number and spin-parity symmetries to reduce the qubit count by 2. Use symmetry_conserving_bravyi_kitaev() with a Symmetries object.

Bravyi-Kitaev tree [HavlivcekCT+17]

A tree-based variant of the Bravyi-Kitaev transformation that uses a different qubit indexing strategy.

Verstraete-Cirac [VC05]

An auxiliary qubit encoding that eliminates non-local Z-strings by locally tracking parity. It operates on any connected LatticeGraph; an optional Hamiltonian-path reordering (dfs_ordering=True on the lattice factories) minimizes the number of auxiliary qubits and stabilizers. Use verstraete_cirac() with a LatticeGraph.

Using the QubitMapper

Note

This algorithm is currently available only in the Python API.

This section demonstrates how to create, configure, and run a qubit mapping. The run method requires a MajoranaMapping as its second argument, which specifies the fermion-to-qubit encoding to use. It returns a QubitOperator object containing the Pauli-string representation.

Input requirements

The QubitMapper requires the following inputs:

Hamiltonian

A Hamiltonian instance containing the fermionic one- and two-electron integrals. This is typically constructed using the HamiltonianConstructor algorithm.

The Hamiltonian defines the fermionic operators that will be transformed into qubit (Pauli) operators using the selected encoding strategy.

MajoranaMapping

A MajoranaMapping instance specifying the fermion-to-qubit encoding. Built-in factory methods are available for standard encodings (e.g., MajoranaMapping.jordan_wigner(num_modes=n)), or a custom encoding can be constructed from a Pauli-string table.

Note

Different encoding strategies produce mathematically equivalent qubit operators but with different Pauli-string structures. The choice of encoding can affect circuit depth and measurement requirements on quantum hardware. See Supported encodings above for descriptions.

Creating a mapper

from qdk_chemistry.algorithms import create
from qdk_chemistry.data import MajoranaMapping

# Create a QubitMapper instance
qubit_mapper = create("qubit_mapper")

Configuring settings

Settings can be modified using the settings() object. See Available implementations below for implementation-specific options.

# Optional: configure numerical thresholds
qubit_mapper.settings().set("threshold", 1e-12)
qubit_mapper.settings().set("integral_threshold", 1e-12)

Running the calculation


from qdk_chemistry.data import Structure

# Read a molecular structure from inline XYZ file
structure = Structure.from_xyz("""\
3
Water molecule
O    0.000000    0.000000    0.000000
H    0.758602    0.000000    0.504284
H   -0.758602    0.000000    0.504284
""")

# Perform an SCF calculation to generate initial orbitals
scf_solver = create("scf_solver")
_, wfn_hf = scf_solver.run(
    structure, charge=0, spin_multiplicity=1, basis_or_guess="cc-pvdz"
)

# Select an active space
num_active_orbitals = 6
active_space_selector = create(
    "active_space_selector",
    algorithm_name="qdk_valence",
    num_active_electrons=4,
    num_active_orbitals=num_active_orbitals,
)
active_wfn = active_space_selector.run(wfn_hf)
active_orbitals = active_wfn.get_orbitals()

# Construct Hamiltonian in the active space
hamiltonian_constructor = create("hamiltonian_constructor")
hamiltonian = hamiltonian_constructor.run(active_orbitals)

# Determine the number of spin-orbitals in the active space
n_spin_orbitals = 2 * num_active_orbitals

# Choose an encoding
mapping = MajoranaMapping.jordan_wigner(num_modes=n_spin_orbitals)

# Map the fermionic Hamiltonian to a qubit Hamiltonian
qubit_hamiltonian = qubit_mapper.run(hamiltonian, mapping)
print(f"Qubit Hamiltonian has {qubit_hamiltonian.num_qubits} qubits")

Available implementations

QDK/Chemistry’s QubitMapper provides a unified interface for qubit mapping methods. You can discover available implementations programmatically:

from qdk_chemistry.algorithms import registry

print(registry.available("qubit_mapper"))
# ['qdk', 'qiskit']

Details for extending implementations

Implementations fall into two groups. They use the MajoranaMapping argument differently:

Table-driven backends (QDK native)

Read the Pauli-string table from the MajoranaMapping directly and pass it to the C++ mapping engine. Any valid table works, including custom encodings that have no standard name.

Third-party backends (OpenFermion, Qiskit)

Ignore the Pauli table. They read base_encoding — a string like "jordan-wigner" or "bravyi-kitaev-tree" — and pass it to their own library to select the matching transform. The qubit operator is then built from scratch using the third-party library’s own fermion-to-qubit code.

This distinction has practical consequences:

  • Custom mappings (user-defined Pauli tables) work with the QDK backend but cannot be used with third-party backends, which have no way to interpret an arbitrary table.

  • Consistency is assumed, not verified. Factory-produced mappings (e.g. MajoranaMapping.jordan_wigner()) guarantee that the Pauli table and the base_encoding name describe the same encoding. Cross-backend eigenvalue tests in the test suite verify this for every supported factory × backend combination. However, if a MajoranaMapping is manually built with a table that does not match its name, a third-party backend will silently use the wrong transform.

  • Tapering is each backend’s responsibility. The base class provides a _taper_result() helper that applies tapering and qubit relabeling to an already mapped QubitOperator. Backends must first run the base transform (typically using mapping.without_tapering()) and then call _taper_result() on the output. All shipped backends use this helper, but third-party backends are free to handle tapering however they choose.

QDK

Factory name: "qdk"

Native QDK/Chemistry qubit mapping implementation built on the PauliOperator expression layer. This is a table-driven backend: it reads the Pauli-string table from the MajoranaMapping and passes it directly to the C++ mapping engine. Any valid MajoranaMapping works — factory-produced or custom user-defined tables. The mapping’s name and base_encoding are used only for metadata on the output, not to select a transform.

Supported encodings: Jordan-Wigner, Bravyi-Kitaev, Bravyi-Kitaev tree, Parity, SCBK, Verstraete-Cirac, and any custom encoding

The native mapper uses blocked spin-orbital ordering internally (alpha orbitals first, then beta orbitals). Use QubitOperator.to_interleaved() for alternative qubit orderings if needed.

Both restricted (RHF) and unrestricted (UHF) Hamiltonians are supported.

Custom encodings can be defined by constructing a MajoranaMapping from a Pauli-string table.

Container-aware fast paths

The "qdk" backend consumes two-body integrals directly from the underlying HamiltonianContainer without ever materializing a dense \(N^4\) two-body tensor when the container stores its integrals in a compressed form:

  • SparseHamiltonianContainer — the mapping loop iterates over only the stored non-zero (p, q, r, s) integrals rather than the full \(O(N^4)\) index space, skipping the zeros that dominate lattice/model Hamiltonians (e.g. those produced by create_hubbard_hamiltonian() and create_ppp_hamiltonian()). This improves both memory and runtime, since neither the dense tensor nor the zero entries are ever touched. Stored entries are canonicalized under the 8-fold integral symmetry before mapping, so the result does not depend on which symmetry-related permutations of an integral the container stores, nor on their order.

  • CholeskyHamiltonianContainer — the three-center (Cholesky / density-fitted) factors are kept in their \(O(N^2 \cdot n_\text{aux})\) form and the auxiliary index is contracted in integral space, one (pq|.) row at a time (a vectorized matrix-vector product per orbital pair). The dense four-center tensor is never built and peak additional memory is a single \(N^2\)-length row, making this path suitable for systems whose dense ERI tensor does not fit in memory.

In all cases the result is a QubitOperator that is numerically equivalent — term-by-term, to within 1e-12 — to the dense CanonicalFourCenterHamiltonianContainer path for the same integrals. The behaviour of run() and the shape of the returned operator are unchanged, and the selection is fully automatic based on the container type. The CanonicalFourCenterHamiltonianContainer continues to use the dense path.

Settings

Setting

Type

Description

threshold

double

Threshold for pruning small Pauli coefficients. Default: 1e-12

integral_threshold

double

Threshold for filtering small integrals before transformation. Default: 1e-12

Example

from qdk_chemistry.algorithms import create as create_algorithm
from qdk_chemistry.data import MajoranaMapping

# Create a native QDK QubitMapper instance
qdk_mapper = create_algorithm("qubit_mapper", "qdk")

# Optional: configure thresholds for numerical precision
qdk_mapper.settings().set("threshold", 1e-12)
qdk_mapper.settings().set("integral_threshold", 1e-12)

# Choose an encoding (Jordan-Wigner, Bravyi-Kitaev, or Parity)
mapping = MajoranaMapping.jordan_wigner(num_modes=n_spin_orbitals)

# Map the fermionic Hamiltonian to a qubit Hamiltonian
qdk_qubit_hamiltonian = qdk_mapper.run(hamiltonian, mapping)
print(f"QDK mapper produced {len(qdk_qubit_hamiltonian.pauli_strings)} Pauli terms")

Qiskit

Factory name: "qiskit"

Qubit mapping implementation integrated through the Qiskit plugin. This is a third-party backend: it reads mapping.base_encoding to select a Qiskit Nature mapper class and ignores the Pauli table (see Details for extending implementations).

Supported base encodings: Jordan-Wigner, Bravyi-Kitaev, Parity

Both restricted (RHF) and unrestricted (UHF) Hamiltonians are supported.

Settings

This implementation has no configurable settings.

OpenFermion

Factory name: "openfermion"

Qubit mapping implementation integrated through the OpenFermion plugin. This is a third-party backend: it reads mapping.base_encoding to select an OpenFermion transform function and ignores the Pauli table (see Details for extending implementations).

Supported base encodings: Jordan-Wigner, Bravyi-Kitaev, Bravyi-Kitaev tree

Both restricted (RHF) and unrestricted (UHF) Hamiltonians are supported.

Settings

This implementation has no configurable settings. The encoding strategy is determined entirely by the MajoranaMapping provided to run().

Further reading