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.
Haiqu.postprocess_skqd(results, num_shots, h1e, h2e, norb, nelec, skqd_options)
Submit SKQD postprocessing (SQD diagonalization) as an async job. Takes the raw probability distributions from all Krylov circuit executions and the Hamiltonian tensors, submits them to the Haiqu server for classical SQD diagonalization on a background worker, and returns a job handle. Calljob.result() to block until the worker finishes and get
an SKQDResult, or job.progress() for live updates.
The caller is responsible for passing the correct tensors in the
same basis used for circuit generation. For example, if circuits
were built in momentum basis (SIAM), pass momentum-basis tensors.
- Parameters:
- results (list *[*dict *[*str , float ] ]) — List of probability distribution dicts (bitstring -> float) from all Krylov circuit executions, as returned by haiqu.run().result().
- num_shots (int) — Number of shots used per circuit execution.
- h1e — One-body Hamiltonian tensor, shape (norb, norb). Accepts numpy arrays or nested lists. Must be in the same basis as the circuits.
- h2e — Two-body Hamiltonian tensor, shape (norb, norb, norb, norb). Accepts numpy arrays or nested lists. Must be in the same basis as the circuits.
- norb (int) — Number of spatial orbitals.
- nelec (tuple *[*int , int ]) — Tuple of (n_alpha, n_beta) electron counts.
- skqd_options (SKQDOptions) — SKQDOptions with SQD parameters (samples_per_batch, num_batches, max_iterations, symmetrize_spin, configuration_recovery, seed).
- Returns:
Job handle.
: Call
job.result()to retrieve anSKQDResultcontaining the best ground-stateenergy, the CI subspace dimension, the CIamplitudesand determinant strings (ci_strs_a,ci_strs_b), per-orbitalorbital_occupancies_alpha/_beta, and the per-iterationiteration_history.job.infoexposes the SKQD output payload (same fields asSKQDResult). Usejob.progress()for live status updates andhelp(job.result)for the full description of result andinfocontents. - Return type: SKQDJobModel
Examples
How it works
This section traces whatpostprocess_skqd actually does to your
hardware results, step by step, on the same small SIAM model used in the
Configuration recovery tutorial
(norb=4, nelec=(2, 2), 8 qubits).
The setup: you’ve already built five Krylov circuits at evolution times
with
build_siam_momentum_basis_krylov_circuits, run each with 1000 shots
on hardware (or a simulator), and now hold five probability
distributions over 8-bit measurement outcomes. Each dist_i is a
plain Python dict mapping bitstring strings to probabilities, e.g.
postprocess_skqd(results=[dist_0, dist_1, dist_2, dist_3, dist_4], ...).
The next sections describe what happens on the worker between the call
and result.energy.
Step 0: merge the five distributions into one pool
The worker doesn’t care which Krylov circuit produced which bitstring. It only cares about the union of bitstrings that came out of the Krylov subspace, weighted by how often they were measured. So the first thing it does is convert each distribution back into a multiset of samples and concatenate them. For eachdist_i, every bitstring b with probability p is
reproduced round(p × num_shots) times in a list, and the five lists
are concatenated into a single BitArray of about 5000 samples.
If the same bitstring appears in several circuits — which is typical,
the dominant ground-state determinants show up at every Krylov index —
its counts simply accumulate. For example, 00110011 might have
probabilities across the five
circuits, contributing roughly
samples to the pool.
The Krylov-circuit-of-origin information is intentionally discarded
here. SQD only needs the empirical probability distribution over
electron configurations; the role of the Krylov circuits is upstream —
they prepare states whose measurement distribution spans the relevant
subspace.
Iteration 1
Of the ~5000 merged samples, the ones with the wrong electron count get discarded. With 8 qubits at ~1% per-qubit readout-error rate, roughly 84% of samples land in-sector (have exactly two ones in each half). So we go from ~5000 to ~4200 samples and from ~80 unique bitstrings to ~36 (or fewer, if some valid configurations weren’t sampled).num_batches × samples_per_batch = 5 × 100 = 500
bitstrings from the in-sector pool, with replacement, weighted by count.
The five batches are independent random draws and almost always differ
from each other.
current_occupancies.
Iteration 2: what changes
The same four-step sequence runs again, but with two pieces of memory now in play.- The bitstring pool may shift. With
configuration_recovery=False(the default) the pool is the same in-sector samples as iteration 1. Withconfiguration_recovery=True, the worker re-runsrecover_configurationsagainst the new occupancies , repairing the previously out-of-sector samples back into the (2, 2) sector and adding them to the pool. See Configuration recovery for the exact bit-flip rule. - Subsamples are fresh. The RNG state has advanced, so even with the same pool the five batches draw different bitstrings.
- Carryover is prepended. Each batch’s CI string list is now
(include + carryover_from_iter_1 + new_samples)deduplicated (fermion.py:360-361). The high-weight determinants from iteration 1 are guaranteed to be in iteration 2’s basis regardless of what the random subsamples bring in.
result.iteration_history — for example, a typical
trajectory: each round adds the
strings carryover preserved plus whatever new high-weight strings the
fresh random subsamples surface, until new draws stop adding anything
new and the subspace plateaus.
Convergence
The loop checks two conditions at the end of each iteration (fermion.py:384-394):- the energy difference from the previous iteration is below
energy_tol, and - the maximum change in any orbital occupancy is below
occupancies_tol.
max_iterations. The function returns the best result seen across all
iterations, not necessarily the last one.
For our small SIAM, convergence happens at iteration 2 — energy and
occupancies haven’t moved since iteration 1.
What the result looks like
iteration_history is the place to look when diagnosing convergence —
if energy keeps falling at the last iteration, you ran out of
max_iterations before convergence and should raise it; if subspace
dimension keeps growing without the energy improving, your batches are
too small or carryover_threshold is too low.
See also
- Configuration recovery — what changes
when
configuration_recovery=True, with worked bit-flip examples. - Yu, J. et al. Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization. arXiv:2501.09702 (2025).
- Implementation:
“qiskit_addon_sqd.fermion.diagonalize_fermionic_hamiltonian
<[https://github.com/Qiskit/qiskit-addon-sqd/blob/main/qiskit_addon_sqd/fermion.py](https://github.com/Qiskit/qiskit-addon-sqd/blob/main/qiskit_addon_sqd/fermion.py)>\__.