ExcitationSolve integration
ExcitationSolve (pip install excitationsolve, Communications Physics 2025, doi:10.1038/s42005-025-02375-9) is a gradient-free VQE optimizer for excitation-operator ansätze (G³ = G). It fits a 2nd-order Fourier series to 5 energy probe points per parameter and locates the global minimum analytically via the companion matrix method.
Schemas: src/qpubench/schemas/mirrors/dlr_excitation_solve.py
Tracking the upstream release (checked against the main branch, July 2026):
- The follow-up paper Haas et al. 2026 (arXiv:2602.10776) adds operator-selection and warm-start strategies for the adaptive variant. Recorded via
ExcitationAdaptResult.operator_selection(free-text strategy label, e.g."max_gradient"or"warm_start") andAdaptVQEStep.optimal_theta. optimal_theta/optimal_theta_pyscfcompute the analytic optimal parameter (and its energy lowering) for a single double excitation applied to a Hartree-Fock reference — a cheap warm start, toggled byExcitationSolveConfig.warm_start_double_excitations.- The new upstream
parameter_occoptimizer argument (per-parameter occurrence / ordering hint) is carried onExcitationSolveConfig.parameter_occ.
Core algorithm summary
1D mode (default):
- For each parameter
θ_i, evaluate energy at 5 shifts:{θ_i + k·π/2 : k=0,1,2,3,4}. - Fit
E(θ) = a₀ + a₁cos(θ/2) + b₁sin(θ/2) + a₂cos(θ) + b₂sin(θ)(5 coefficients). - Find global minimum analytically via companion matrix.
- Update
θ_i. Sweep through all parameters until convergence.
2D mode: jointly optimise two parameters using 25 probe points and 25 Fourier basis terms.
ADAPT mode (ExcitationAdaptiveOptimizer): selects the best operator from a pool each step by testing 5 energy probes for each candidate.
Configuration
from qpubench.schemas import ExcitationSolveConfig, ExcitationSolveMode, AlgorithmSpec
# Optimizer config (maps to ExcitationSolveQiskit constructor)
cfg = ExcitationSolveConfig(
maxiter=200,
tol=1e-10,
num_samples=5, # ≥5 for 1D; exactly 25 for 2D; 5 per candidate for ADAPT
hf_energy=-1.1175, # optional HF reference for chemical accuracy tracking
save_parameters=True,
mode=ExcitationSolveMode.ONE_D,
parameter_occ=None, # optional per-parameter ordering hint
warm_start_double_excitations=False, # analytic optimal_theta HF warm start
)
# In AlgorithmSpec for use with BenchmarkRunner
spec = AlgorithmSpec(
name="ExcitationSolve",
optimizer="excitation_solve",
use_analytic_grad=False, # gradient-free by design
opt_maxiter=200, # = ExcitationSolveConfig.maxiter
opt_thresh=1e-10, # = ExcitationSolveConfig.tol
num_samples=5, # = ExcitationSolveConfig.num_samples
)
Recording a 1D optimisation run
from qpubench.schemas import (
ExcitationSolveSweep, ExcitationSolveIteration,
ExcitationSolveResult, ParameterSample, VQAConfig, VQAResult,
)
import numpy as np
# --- Record one parameter sweep (for parameter index 0) ---
shifts = np.array([0, np.pi/2, -np.pi/2, np.pi, -np.pi])
energies = np.array([-1.10, -1.05, -1.08, -1.12, -1.09]) # from circuit evaluations
sweep = ExcitationSolveSweep(
parameter_index=0,
samples=[ParameterSample(parameter_variation=s, energy_sample=e)
for s, e in zip(shifts.tolist(), energies.tolist())],
optimized_parameter=2.31,
optimized_energy=-1.136,
fourier_coefficients=[…], # a₀, a₁, b₁, a₂, b₂ from np.linalg.solve
)
# --- Record one iteration (one sweep over all parameters) ---
iteration = ExcitationSolveIteration(
iteration=3,
energy=-1.1355,
nfev=75, # cumulative function evaluations
delta_energy=1.2e-4,
parameters=[2.31, -1.05, 0.78], # full parameter vector (if save_parameters=True)
sweeps=[sweep],
)
# --- Full optimizer result (returned by minimize()) ---
result = ExcitationSolveResult(
optimized_parameters=[2.31, -1.05, 0.78],
final_energy=-1.1361,
n_function_evaluations=375,
n_iterations=25,
converged=True,
hf_energy=-1.1175,
history=[iteration, …],
config=cfg,
)
# Convergence trace
print(result.convergence_values()) # [energy per iteration]
print(result.nfev_history()) # [cumulative nfev per iteration]
print(result.energy_error_vs_hf) # |final - hf|
# Bridge to qpubench
quantum_result = result.to_quantum_result()
vqa = VQAConfig(
problem_type="chemistry",
molecule="H2",
algorithm="ExcitationSolve",
optimizer="excitation_solve",
)
vqa_result = VQAResult(
num_parameters=3,
nfev=result.n_function_evaluations,
hf_energy=result.hf_energy,
convergence_values=result.convergence_values(),
final_eigenvalue=result.final_energy,
ground_truth=-1.1373,
)
Recording ADAPT-VQE runs
from qpubench.schemas import AdaptVQEStep, ExcitationAdaptResult
# Each call to optimizer.step_and_cost() produces one AdaptVQEStep
step = AdaptVQEStep(
step_index=2,
prior_cost=-1.10,
max_gradient=0.042, # maximum energy improvement across pool
selected_operator="SingleExcitation(0->2)",
n_pool_evaluated=12, # operators tested this step
n_function_evaluations=60, # = n_pool_evaluated * 5
drain_pool=True,
optimal_theta=0.113, # analytic HF warm-start value, if used
)
adapt_result = ExcitationAdaptResult(
steps=[step, …],
final_energy=-1.136,
final_parameters=[0.31, -0.18, 0.52],
n_operators_added=3,
converged=True,
config=cfg,
operator_selection="warm_start", # Haas et al. 2026 strategy label
)
print(adapt_result.grad_norm_history()) # max_gradient per step
print(adapt_result.energy_history()) # prior_cost per step
quantum_result = adapt_result.to_quantum_result()
2D mode
Use ExcitationSolveMode.TWO_D and set num_samples=25. Record the sweep with parameter_indices:
cfg_2d = ExcitationSolveConfig(mode=ExcitationSolveMode.TWO_D, num_samples=25)
sweep_2d = ExcitationSolveSweep(
parameter_indices=[0, 1], # two parameters jointly
samples=[
ParameterSample(parameter_variation=[s1, s2], energy_sample=e)
for (s1, s2), e in zip(shifts_2d, energies_2d)
],
optimized_parameter=[1.57, -0.78], # 2-element list
optimized_energy=-1.137,
fourier_coefficients=[…], # 25 tensor-product basis coefficients
)
Fewer than 5 samples (noisy circuit)
When num_samples > 5, ExcitationSolve performs a least-squares fit instead of an exact 5-point reconstruction. Record this the same way — just pass more ParameterSample entries:
cfg_noisy = ExcitationSolveConfig(num_samples=9) # 9 probe points per parameter