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 aSymmetriesobject.
- 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=Trueon the lattice factories) minimizes the number of auxiliary qubits and stabilizers. Useverstraete_cirac()with aLatticeGraph.
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
MajoranaMappingdirectly 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 thebase_encodingname describe the same encoding. Cross-backend eigenvalue tests in the test suite verify this for every supported factory × backend combination. However, if aMajoranaMappingis 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 mappedQubitOperator. Backends must first run the base transform (typically usingmapping.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 bycreate_hubbard_hamiltonian()andcreate_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 |
|---|---|---|
|
double |
Threshold for pruning small Pauli coefficients. Default: |
|
double |
Threshold for filtering small integrals before transformation. Default: |
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
The above examples can be downloaded as a complete Python script.
StatePreparation: Prepare quantum circuits from wavefunctions
ExpectationEstimator: Estimate energies using the qubit operator
Settings: Configuration settings for algorithms
Factory Pattern: Understanding algorithm creation