Skip to the content.

QSE / KQD integration

qpubench models Krylov Quantum Diagonalization (KQD) and Quantum Subspace Expansion (QSE) in src/qpubench/schemas/mirrors/mqsdk_qse.py. The schemas cover the algorithm families implemented in MQSdk/qse.

Computing model: ComputingModel.GATE_BASED (KQD is an algorithmic technique on top of gate-based circuits — see KQDMethod, not a separate paradigm)


Algorithm overview

Three algorithm variants share the same pipeline structure:

Variant KQDMethod How subspace is built
Standard KQD HADAMARD_TEST Modified Hadamard test measures ⟨ψ_{I,m}|O|ψ_{J,n}⟩ for all (ref, power) pairs
Sample-based KQD SAMPLE_BASED_SQD Krylov circuits measured in Fock basis; bitstrings post-selected by particle number
Multi-reference MULTI_REF_HADAMARD / MULTI_REF_SQD d_refs reference states each seed their own Krylov series
All variants use Trotter time evolution U = e^{−iHdt} to generate the Krylov basis {U^k Φ⟩}.

Reference states

Néel states (spin chains)

from qpubench.schemas.mirrors.mqsdk_qse import NeelStateSpec, KQDReferenceSpec, KQDReferenceStateType

# Two complementary Néel states for antiferromagnetic chain
ref0 = KQDReferenceSpec(
    state_type=KQDReferenceStateType.NEEL,
    bitstring="1010101010",
    neel=NeelStateSpec(num_spins=10, shift=0),
    label="neel_shift0",
)
ref1 = KQDReferenceSpec(
    state_type=KQDReferenceStateType.NEEL,
    bitstring="0101010101",
    neel=NeelStateSpec(num_spins=10, shift=1),
    label="neel_shift1",
)

Slater determinants (chemistry)

from qpubench.schemas.mirrors.mqsdk_qse import SlaterDeterminantRef, KQDReferenceSpec, KQDReferenceStateType

# H2 in STO-3G: 2 active orbitals, 2 electrons (HF reference)
slater = SlaterDeterminantRef(
    ncas=2,         # 2 active spatial orbitals → 4 spin-orbitals
    occ_alpha=[0],  # α orbital 0 occupied
    occ_beta=[0],   # β orbital 0 occupied
)
print(slater.bitstring)      # "1010" (alpha_0=1, alpha_1=0, beta_0=1, beta_1=0)
print(slater.num_qubits)     # 4
print(slater.num_electrons)  # 2

ref_chem = KQDReferenceSpec(
    state_type=KQDReferenceStateType.SLATER_DET,
    bitstring=slater.bitstring,
    slater_det=slater,
    label="hf_reference",
)

Time evolution

from qpubench.schemas.mirrors.mqsdk_qse import KQDTimeEvolutionSpec, KrylovTimeEvolutionVariant
import math

# dt = π / ‖H‖₂ (spectral norm)
dt = math.pi / 3.0

te_spec = KQDTimeEvolutionSpec(
    dt=dt,
    num_trotter_steps=6,
    variant=KrylovTimeEvolutionVariant.EFFICIENT_ALTERNATING,
)

print(te_spec.dt_circ)   # dt / 6 — Trotter step size for each sub-circuit

EFFICIENT_ALTERNATING: alternating forward/reverse Rxyz blocks (default in qse).
LIE_TROTTER: Qiskit PauliEvolutionGate + LieTrotter synthesizer.


KQD configuration

from qpubench.schemas.mirrors.mqsdk_qse import KQDConfig, KQDMethod, RegularizationConfig, EigensolverMethod

config = KQDConfig(
    method=KQDMethod.HADAMARD_TEST,
    krylov_dim=6,
    num_references=2,
    dt=dt,
    num_trotter_steps=6,
    regularization=RegularizationConfig(
        threshold=1.0e-6,       # discard S eigenvalues below ε
        num_eigenvalues_k=4,
        solver=EigensolverMethod.SCIPY_EIGH,
    ),
)

Hadamard test path

The Hadamard test measures matrix elements of both the overlap matrix S and the projected Hamiltonian H:

from qpubench.schemas.mirrors.mqsdk_qse import (
    KrylovMatrixSpec, KrylovSubspaceMatrices,
    HadamardTestIterationResult,
    KrylovEigenResult,
)

# 6×6 subspace (krylov_dim=6, num_references=1)
dim = 6
S = KrylovMatrixSpec(
    label="S", dim=dim,
    matrix_real=[...],   # flattened 6×6 overlap matrix
    matrix_imag=[0.0] * 36,
)
H_mat = KrylovMatrixSpec(
    label="H", dim=dim,
    matrix_real=[...],   # flattened 6×6 projected Hamiltonian
    matrix_imag=[0.0] * 36,
)
matrices = KrylovSubspaceMatrices(
    S_matrix=S,
    H_matrix=H_mat,
    assembly_method="hadamard_test",
    krylov_dim=6,
)

# One Hadamard test measurement result
meas = HadamardTestIterationResult(
    circuit_label=[0, 3],    # reference 0, Krylov power 3
    observable_index=7,
    real_part=-0.4831,
    imag_part=0.0,
)

# Eigenvalue solve result
eigen = KrylovEigenResult(
    eigenvalues=[-4.258, -3.11, -2.01, -0.87, 0.45, 1.92],
    ground_state_energy=-4.258,
    S_eigenvalues=[0.99, 0.94, 0.85, 0.62, 0.31, 0.04],
    num_eigenvalues_discarded=0,
    krylov_dim_effective=6,
)

Sample-based path (SQD)

The SQD path measures Krylov circuits in the computational basis and accumulates bitstrings across steps:

from qpubench.schemas.mirrors.mqsdk_qse import (
    SQDPostselectionConfig, SQDStep, SQDConvergenceResult,
    KrylovBitstringCounts, CumulativeKrylovCounts,
)

postsel = SQDPostselectionConfig(num_ones=2, min_unique=5)

steps = [
    SQDStep(krylov_step=0, num_bitstrings=14, subspace_dim=14, energy_hartree=-1.10),
    SQDStep(krylov_step=1, num_bitstrings=22, subspace_dim=22, energy_hartree=-1.128),
    SQDStep(krylov_step=2, num_bitstrings=28, subspace_dim=28, energy_hartree=-1.135),
    SQDStep(krylov_step=3, num_bitstrings=31, subspace_dim=31, energy_hartree=-1.1371),
]

sqd_result = SQDConvergenceResult(
    steps=steps,
    final_energy=-1.1371,
    exact_energy=-1.1373,
)
print(f"Error: {sqd_result.error_mha:.3f} mHa")   # milli-Hartree

# Cumulative bitstring pool
pool = CumulativeKrylovCounts(
    cumulative_counts=[
        {"1010": 42, "0110": 38, "1001": 35},    # after step 0
        {"1010": 78, "0110": 71, "1001": 64, "0101": 18},  # after step 1
    ],
    postselection=postsel,
    num_references_pooled=1,
)

Cholesky decomposition

For molecular Hamiltonians, the two-electron integrals can be stored in low-rank Cholesky form:

from qpubench.schemas.mirrors.mqsdk_qse import CholeskyDecompositionSpec

chol = CholeskyDecompositionSpec(
    num_orbitals=4,     # H2 in cc-pVDZ: 4 spatial orbitals → 10 active
    eps=1.0e-6,         # pivoted Cholesky convergence threshold
    n_chol=12,          # number of Cholesky vectors retained
    max_cholesky=80,    # = 20 × num_orbitals
    accuracy=3.2e-7,    # max |V - LLᵀ| achieved
)

Full pipeline

from qpubench.schemas.mirrors.mqsdk_qse import KQDPipelineSpec
from qpubench.schemas.result import QuantumResult
from qpubench.schemas.primitives import ComputingModel

pipeline = KQDPipelineSpec(
    num_qubits=10,
    hamiltonian_label="heisenberg_chain_10",
    kqd_config=config,
    time_evolution=te_spec,
    reference_states=[ref0, ref1],
    krylov_matrices=matrices,
    hadamard_results=[meas],
    eigen_result=eigen,
    exact_energy=-4.2588,
    hf_energy=None,         # not applicable for spin chain
    cholesky_spec=None,
)

result = QuantumResult(
    computing_model=ComputingModel.GATE_BASED,
    vendor_results={"kqd_pipeline": pipeline},
)

Circuit family metadata

from qpubench.schemas.mirrors.mqsdk_qse import KrylovCircuitFamilySpec, KQDMethod

# Multi-reference Hadamard test: 2² × 6² = 144 circuits for (I,J,m,n)
fam = KrylovCircuitFamilySpec(
    method=KQDMethod.MULTI_REF_HADAMARD,
    num_qubits_system=10,
    krylov_dim=6,
    num_references=2,
    num_circuits=144,
    circuit_labels=[[i, j, m, n]
                    for i in range(2) for j in range(2)
                    for m in range(6) for n in range(6)],
    shots_per_circuit=2048,
    ancilla_qubits=1,
)

Backend

from qpubench.schemas.backend import BackendSpec

# Qiskit Aer for KQD (modality=KQD)
BackendSpec.qiskit_aer(method="statevector", num_qubits=12)
BackendSpec.qiskit_aer(method="matrix_product_state", num_qubits=40)

Noiseless statevector is the default for benchmarking; MPS extends reach to larger systems at the cost of bond dimension truncation.