Documentation Index
Fetch the complete documentation index at: https://docs.haiqu.ai/llms.txt
Use this file to discover all available pages before exploring further.
The configuration_recovery flag in SKQDOptions controls how the SQD post-processing loop in haiqu.postprocess_skqd handles measurement bitstrings that hardware noise pushed out of the target particle-number sector. This page walks through what the flag does on a small Single-Impurity Anderson Model (SIAM), so you can decide whether to enable it.
The system
We use the same SIAM that ships with the simple SKQD example: four spatial orbitals (one impurity, three bath sites), at half-filling.
from haiqu.sdk.skqd import siam_hamiltonian, get_orbital_rotation, rotate_basis
norb = 4
nelec = (2, 2) # (n_alpha, n_beta)
h1e_site, h2e_site = siam_hamiltonian(norb=norb, t=1.0, U=10.0, V=1.0, mu=-5.0)
orbital_rotation = get_orbital_rotation(norb)
h1e, h2e = rotate_basis(h1e_site, h2e_site, orbital_rotation.T.conj())
The Krylov circuits that feed postprocess_skqd run on 2 * norb = 8 qubits.
Bitstring layout
Each shot returns 8 bits. We split them into two halves: the left four bits encode beta (spin-down) occupations, the right four encode alpha (spin-up). Within each half, position 0 is the rightmost bit (orbital 0), then orbital 1, orbital 2, orbital 3 going left:
beta orbitals alpha orbitals
┌─────────────┐ ┌─────────────┐
b₃ b₂ b₁ b₀ a₃ a₂ a₁ a₀
So the bitstring 00110011 means:
- Beta: orbitals 0 and 1 occupied (rightmost two bits of the left half).
- Alpha: orbitals 0 and 1 occupied (rightmost two bits of the right half).
This is one of the dominant ground-state determinants for our SIAM in the momentum basis. (With occupancy 0.5 on orbitals 1 and 2 — see below — there are actually four equally-weighted dominant determinants; we’ll come back to the others in the worked examples.)
Valid configurations
Because nelec = (2, 2), a valid bitstring has exactly two ones in each half. There are (24)2=36 valid configurations out of 28=256 total. A handful of examples:
| Bitstring | Beta orbitals | Alpha orbitals | Valid? |
|---|
00110011 | 0, 1 | 0, 1 | yes |
01010011 | 0, 2 | 0, 1 | yes |
00111100 | 0, 1 | 2, 3 | yes |
00010011 | 0 | 0, 1 | no (n_beta = 1) |
10110011 | 0, 1, 3 | 0, 1 | no (n_beta = 3) |
What hardware noise does
Read-out noise flips bits independently with some probability per qubit. A perfect shot of 00110011 can land as 00010011 (one beta bit flipped 1→0) or 10110011 (one beta bit flipped 0→1). Both bitstrings are out of the (2, 2) sector.
The fraction of out-of-sector shots grows with the number of qubits and the per-qubit error rate. For 8 qubits at 1% per-qubit read-out error, roughly 8% of shots fall outside the sector; for 40 qubits at the same rate, it’s roughly 33%.
Default behavior: postselection
With configuration_recovery=False (the default), each iteration of the SQD loop discards every out-of-sector shot. The bigger your system, the more shots you waste:
from haiqu.sdk.skqd import SKQDOptions
opts = SKQDOptions(configuration_recovery=False) # default
What configuration recovery does
With configuration_recovery=True, from the second iteration onward the loop repairs out-of-sector bitstrings instead of discarding them, using the orbital occupancies measured at the previous iteration.
For our SIAM at U/t=10, the converged orbital occupancies (alpha and beta both) are:
| Orbital | Occupancy |
|---|
| 0 | 0.99 |
| 1 | 0.50 |
| 2 | 0.50 |
| 3 | 0.01 |
Read this as: orbital 0 is “almost always occupied” in the ground state, orbital 3 is “almost always empty,” and orbitals 1 and 2 each carry half an electron on average across the ground-state distribution.
The recovery procedure handles the alpha and beta halves independently. For each half:
- Compute a per-bit flip probability for every position. A 0 at a high-occupancy orbital is very likely to be flipped to 1; a 1 at a low-occupancy orbital is very likely to be flipped to 0; bits at orbitals near the expected density (
nelec / norb = 0.5) get a small floor probability.
- Count how many bits are wrong (e.g., one too few for
n_beta = 2).
- Pick exactly that many bits to flip, weighted by the flip probabilities from step 1.
The result is guaranteed to land in the (n_alpha, n_beta) sector and is biased toward configurations consistent with the current orbital occupancies.
For our SIAM occupancies, the per-orbital flip probabilities work out to:
| Orbital | Occupancy | p(flip 0→1) | p(flip 1→0) |
|---|
| 0 | 0.99 | 0.98 | 0.0002 |
| 1 | 0.50 | 0.01 | 0.01 |
| 2 | 0.50 | 0.01 | 0.01 |
| 3 | 0.01 | 0.0002 | 0.98 |
These are the building blocks for the worked examples below.
Worked example 1: 00010011 → 00110011
Starting bitstring: 00010011. Beta half is 0001, so only beta orbital 0 is occupied — n_beta = 1, target is 2, so one beta 0 needs to flip to 1.
The 0-bits in beta are at orbitals 1, 2, 3. Their 0→1 flip probabilities (normalized within the 0-bit set) are:
| Orbital | Occupancy | p(flip 0→1) | Normalized |
|---|
| 1 | 0.50 | 0.01 | 0.495 |
| 2 | 0.50 | 0.01 | 0.495 |
| 3 | 0.01 | 0.0002 | 0.010 |
Recovery picks orbital 1 or orbital 2 with about equal probability (~49.5% each), and orbital 3 only about 1% of the time. Both 00110011 and 01010011 are equally-weighted dominant ground-state determinants, so either outcome lands in the high-weight part of the subspace; we essentially never recover into the much higher-energy configuration with orbital 3 occupied.
Worked example 2: 10110011 → 00110011
Starting bitstring: 10110011. Beta half is 1011 — three orbitals occupied (0, 1, 3) — so n_beta = 3, one beta 1 needs to flip to 0.
The 1-bits in beta are at orbitals 0, 1, 3. Their 1→0 flip probabilities (normalized within the 1-bit set) are:
| Orbital | Occupancy | p(flip 1→0) | Normalized |
|---|
| 0 | 0.99 | 0.0002 | 0.0002 |
| 1 | 0.50 | 0.01 | 0.010 |
| 3 | 0.01 | 0.98 | 0.990 |
Recovery picks orbital 3 about 99% of the time. The recovered beta is 0011, putting the bitstring back to 00110011 — one of the dominant ground-state determinants.
In words: orbital 3 was “supposed to be empty” (occupancy 0.01) but the noisy bitstring had it occupied; recovery confidently turns it off.
Why iteration 1 has no recovery
The procedure needs orbital occupancies, which only become available after the first diagonalization. Iteration 1 always falls back to plain Hamming-weight postselection; recovery kicks in from iteration 2 onward and continues until the loop converges or max_iterations is reached.
This is a self-consistent loop: each iteration’s diagonalization produces occupancies that feed the next iteration’s recovery, which produces a different subspace, which produces refined occupancies, and so on.
When to enable it
| Setting | configuration_recovery |
|---|
| Noiseless simulator | False (default) |
| Hardware run, large system, high noise | True |
| Hardware run, small system, low reject % | False is usually fine |
The benefit grows with the fraction of out-of-sector samples. For a 40-qubit hardware run, recovery is typically the difference between converging cleanly and failing to. For a 4-qubit toy model on a simulator, it usually does nothing.
opts = SKQDOptions(
samples_per_batch=100,
num_batches=5,
max_iterations=15,
symmetrize_spin=True,
configuration_recovery=True, # enable recovery
seed=42,
)
References