State preparation
The StatePreparation algorithm in QDK/Chemistry constructs quantum circuits that load classical representations of target wavefunctions onto qubits.
Following QDK/Chemistry’s algorithm design principles, it takes a Wavefunction instance as input and produces a Circuit as output.
The output circuit, when executed, prepares the qubit register in a state that encodes the input wavefunction.
Overview
The StatePreparation module provides tools for constructing quantum circuits that load classical representations of wavefunctions (e.g., a Slater determinant or a linear combination thereof, represented by the Wavefunction class) onto qubits. It supports multiple approaches for state preparation, allowing users to choose the method best suited to their problem. Each approach is designed to efficiently encode quantum states for chemistry applications.
For details on individual methods and their technical implementations, see the Available implementations section below.
Using the StatePreparation
Note
This algorithm is currently available only in the Python API.
This section demonstrates how to create, configure, and run a state preparation.
The run method returns a circuit object that, when executed, loads the input wavefunction onto a qubit register.
Input requirements
The StatePreparation requires the following input:
- Wavefunction
A
Wavefunctioninstance containing the quantum state to be loaded onto qubits. This is typically obtained from a multi-configuration calculation using the MultiConfigurationCalculator. The method with which this encoding is achieved is implementation dependent.
Creating a state preparation algorithm
from qdk_chemistry.algorithms import create
from qdk_chemistry.data import AlgorithmRef
# Create a StatePreparation instance
sparse_prep = create("state_prep", "sparse_isometry")
dense_prep = create("state_prep", "dense_pure_state")
regular_prep = create("state_prep", "qiskit_regular_isometry")
Configuring settings
Settings can be modified using the settings() object.
See Available implementations below for implementation-specific options.
# Transpilation is configured on the algorithm that emits the Qiskit circuit.
regular_prep.settings().set("transpile", True)
regular_prep.settings().set("basis_gates", ["rz", "cz", "sdg", "h"])
regular_prep.settings().set("transpile_optimization_level", 3)
# Select other dense preparation method for sparse isometry
sparse_isometry_with_qiskit_dense_prep = create(
"state_prep",
"sparse_isometry",
dense_state_prep=AlgorithmRef(
"state_prep",
"qiskit_regular_isometry",
transpile=True,
basis_gates=["rz", "cz", "sdg", "h"],
transpile_optimization_level=3,
),
)
Running the calculation
Once configured, the StatePreparation can be used to generate a quantum circuit from a Wavefunction.
import numpy as np
from qdk_chemistry.data import Structure
# Specify a structure
coords = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4]])
symbols = ["H", "H"]
structure = Structure(coords, symbols=symbols)
# Run scf
scf_solver = create("scf_solver")
E_scf, wfn_scf = scf_solver.run(
structure, charge=0, spin_multiplicity=1, basis_or_guess="sto-3g"
)
# Compute the Hamiltonian
hamiltonian_constructor = create("hamiltonian_constructor")
hamiltonian = hamiltonian_constructor.run(wfn_scf.get_orbitals())
# Compute CAS wavefunction
cas_solver = create("multi_configuration_calculator", "macis_cas")
E_cas, wfn_cas = cas_solver.run(hamiltonian, 1, 1)
# Construct the circuit
regular_circuit = regular_prep.run(wfn_cas)
sparse_circuit = sparse_prep.run(wfn_cas)
dense_circuit = dense_prep.run(wfn_cas)
print(f"Regular isometry circuit:\n{regular_circuit.get_qiskit_circuit()}")
print(f"Sparse isometry circuit:\n{sparse_circuit.get_qsharp_circuit()}")
print(f"Dense pure state circuit:\n{dense_circuit.get_qsharp_circuit()}")
Available implementations
QDK/Chemistry’s StatePreparation provides a unified interface for state preparation methods.
You can discover available implementations programmatically:
from qdk_chemistry.algorithms import registry
print(registry.available("state_prep"))
# ['sparse_isometry', 'dense_pure_state', 'qiskit_regular_isometry']
Sparse Isometry
Factory name: "sparse_isometry"
This method is an optimized approach that leverages sparsity in the target wavefunction. It is a modification of the original sparse isometry work in [MIC21], and is native to QDK/Chemistry, described in [CBG+26]. By working only with the non-zero amplitudes, it substantially reduces circuit depth and gate count compared with dense methods, and is especially efficient for wavefunctions with sparse amplitude structure.
How it works
A wavefunction built from \(d\) determinants is written as
where each \(b_j\) is the occupation bitstring of one determinant on \(n\) qubits. For chemically relevant states \(d \ll 2^{n}\), so all but a vanishing fraction of the \(2^{n}\) amplitudes are zero. Dense methods still pay for every one of them; the sparse isometry pays only for the \(d\) that matter.
The support is collected into a binary matrix \(M \in \mathrm{GF}(2)^{n \times d}\) whose columns are the determinant bitstrings. The algorithm then proceeds in four steps:
Reduce. Gaussian elimination over \(\mathrm{GF}(2)\) brings \(M\) to row echelon form of rank \(r \le n\). Elimination is preceded by two simplifications: duplicate rows are cancelled against each other, and all-ones rows are cleared. When the reduced matrix is diagonal, an additional cascade removes one further row, so the final reduced width may be smaller than \(r\).
Record. Every row operation used in the reduction is tracked. A row addition over \(\mathrm{GF}(2)\) is exactly a CNOT on the qubit register and a row negation is exactly an
Xgate, so the elimination sequence is itself a Clifford circuit \(E\) satisfying \(E \cdot M = M_{\mathrm{ref}}\).Prepare. The amplitudes \(c_j\) are loaded onto the remaining reduced rows by a nested state-preparation algorithm selected with the
dense_state_prepsetting.Recovery. Replaying the recorded operations in reverse applies \(E^{-1}\), mapping the reduced basis states back onto the original determinant bitstrings \(b_j\).
The cost is therefore governed by \(d\) and the reduced width rather than by \(2^{n}\), and step 3 is the only part that is exponential in that smaller register.
Binary encoding
Setting binary_encoding to True replaces step 3 when \(m = \lceil \log_2 d \rceil\) is smaller than the number of rows left after reduction. It constructs a one-to-one map from the \(d\) determinants into the \(2^m\) basis states of an \(m\)-qubit dense register, so the nested preparation runs on that smaller amplitude vector.
The compression circuit is synthesized in two stages:
Diagonal encoding. The pivot block of the reduced matrix is an identity, i.e. a unary encoding that spends one qubit per determinant. A staircase of CNOT gates normalizes it, and a divide-and-conquer cascade of CNOT and Toffoli gates then folds the unary pattern into a binary counter, collapsing the pivot columns onto the \(m\) dense rows.
Non-pivot processing. The remaining columns carry no pivot and are handled in power-of-two batches. Each batch emits an address-controlled lookup block that writes the correct binary label into the dense register conditioned on the sparse indicator rows, and then clears those rows. The synthesizer costs a single lookup against a split into smaller chunks and keeps whichever needs fewer Toffoli gates.
Once both stages complete, every sparse row is guaranteed to hold \(\left| 0 \right\rangle\), so the state lives entirely in the dense register. The amplitudes are prepared there, and the whole compression circuit is inverted to scatter back to the full register.
Lookup blocks add CCZ operations and need helper qubits. Rather than allocating fresh ancillas, the synthesizer first borrows idle system qubits — those absent from the reduced support — and allocates additional qubits only when that pool is exhausted. The smaller amplitude-loading register therefore does not imply fewer qubits or non-Clifford gates for the complete circuit.
Binary encoding applies only when \(m < n_{\mathrm{rows}}\), where \(n_{\mathrm{rows}}\) is the width left after reduction. Otherwise the algorithm transparently falls back to the standard path described above.
# Compress the reduced subspace with binary encoding
binary_encoded_prep = create("state_prep", "sparse_isometry", binary_encoding=True)
# Select the algorithm that prepares the compressed dense register
binary_encoded_prep.settings().set(
"dense_state_prep", AlgorithmRef("state_prep", "dense_pure_state")
)
Note
Setting measurement_based_uncompute to True uncomputes the helper qubits of each lookup block by measurement and a classically controlled correction rather than by Toffoli gates. This trades Toffoli count for mid-circuit measurement and feedforward, so the resulting circuit requires a target profile that supports adaptive execution.
Settings
Setting |
Type |
Description |
|---|---|---|
|
bool |
Compress the reduced subspace with binary encoding instead of preparing it directly. Best effort: |
|
AlgorithmRef |
State preparation algorithm used for the dense subspace. Default is |
|
bool |
Allow anti-controls as well as controls in the lookup blocks. Default is True. |
|
bool |
Uncompute lookup helper qubits by measurement instead of Toffoli gates. Default is False. |
This algorithm declares no transpilation settings of its own. Transpilation applies only when
the nested dense_state_prep algorithm emits a Qiskit circuit, and is configured on that
algorithm:
# Transpilation is configured on the algorithm that emits the Qiskit circuit.
regular_prep.settings().set("transpile", True)
regular_prep.settings().set("basis_gates", ["rz", "cz", "sdg", "h"])
regular_prep.settings().set("transpile_optimization_level", 3)
# Select other dense preparation method for sparse isometry
sparse_isometry_with_qiskit_dense_prep = create(
"state_prep",
"sparse_isometry",
dense_state_prep=AlgorithmRef(
"state_prep",
"qiskit_regular_isometry",
transpile=True,
basis_gates=["rz", "cz", "sdg", "h"],
transpile_optimization_level=3,
),
)
Dense Pure State
Factory name: "dense_pure_state"
This method expands the wavefunction into its full amplitude vector and synthesizes that vector exactly. Synthesis is delegated to PreparePureStateD from the Q# standard library, which follows the construction of Shende, Bullock, and Markov [SBM06].
Each determinant’s coefficient is placed at the index given by its occupation bitstring, giving a dense vector of \(2^{n}\) real amplitudes that is handed to PreparePureStateD.
Requirements
The coefficients must be real; a wavefunction with a non-zero imaginary part is rejected. The register is limited to 32 qubits, which bounds the size of the dense amplitude vector.
Settings
This implementation exposes no settings.
Regular Isometry
Factory name: "qiskit_regular_isometry"
This method uses regular isometry synthesis via Qiskit, implementing the isometry-based approach proposed by Matthias Christandl [ICK+16]. It provides a general solution for state preparation, and is suitable for cases where a dense representation is required or preferred. Like Dense Pure State it synthesizes the full amplitude vector, but it is provided through the plugin system and returns an OpenQASM circuit, which makes it the natural choice for Qiskit-based workflows.
Settings
Setting |
Type |
Description |
|---|---|---|
|
list[str] |
Basis gates for transpilation. Default is [“x”, “y”, “z”, “cx”, “cz”, “id”, “h”, “s”, “sdg”, “rz”]. |
|
bool |
Whether to transpile the circuit. Default is True. |
|
int |
Optimization level for transpilation (0-3). Default is 0. |
For more details on how QDK/Chemistry interfaces with external packages, see the plugin system documentation.
Further reading
The above examples can be downloaded as a complete Python script.
ExpectationEstimator: Estimate the energy of prepared states
QubitMapper: Map Hamiltonians to qubit operators
Settings: Configuration settings for algorithms
Factory Pattern: Understanding algorithm creation