> ## 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.

# Configuration recovery

> How SKQD repairs noisy measurement bitstrings using orbital occupancies.

The `configuration_recovery` flag in [`SKQDOptions`](helpers.md#haiqu.sdk.skqd.SKQDOptions) controls how the SQD post-processing loop in [`haiqu.postprocess_skqd`](postprocess_skqd.md) 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.

```python theme={null}
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 $\binom{4}{2}^2 = 36$ valid configurations out of $2^8 = 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:

```python theme={null}
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:

1. 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.
2. Count how many bits are wrong (e.g., one too few for `n_beta = 2`).
3. 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.

<Note>
  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.
</Note>

## 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.

```python theme={null}
opts = SKQDOptions(
    samples_per_batch=100,
    num_batches=5,
    max_iterations=15,
    symmetrize_spin=True,
    configuration_recovery=True,  # enable recovery
    seed=42,
)
```

## References

* Yu, J. et al. *Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization*. [arXiv:2501.09702](https://arxiv.org/abs/2501.09702) (2025).
* Robledo-Moreno, J. et al. *Chemistry Beyond Exact Solutions on a Quantum-Centric Supercomputer*. [arXiv:2405.05068](https://arxiv.org/abs/2405.05068) (2024) — the original SQD configuration-recovery procedure.
* IBM Quantum tutorial: [Sample-based Krylov quantum diagonalization of a fermionic lattice model](https://quantum.cloud.ibm.com/docs/en/tutorials/sample-based-krylov-quantum-diagonalization).
* Implementation: [`qiskit_addon_sqd.configuration_recovery.recover_configurations`](https://github.com/Qiskit/qiskit-addon-sqd/blob/main/qiskit_addon_sqd/configuration_recovery.py).
