Optimizers¶
Divi provides built-in support for optimizing quantum programs using a range of optimization methods, each suited to different problem types and user requirements.
All optimizers can be accessed through the divi.qprog.optimizers module. Scipy-based optimizers rely on the ScipyMethod enum to specify the optimizer used.
Monte Carlo Optimization¶
The Monte Carlo [1] method in Divi is a stochastic global optimization approach. It works by randomly sampling the parameter space and selecting configurations that minimize the target cost function. This method is particularly useful when:
The optimization landscape is rugged or non-convex.
Gradients are not available or are unreliable.
A rough global search is preferred before local refinement.
Monte Carlo optimization can help identify promising regions in high-dimensional parameter spaces before applying more refined methods.
Configure MonteCarloOptimizer by passing population_size (the number of parameter sets evaluated per iteration) and optionally n_best_sets (how many top-performing sets are carried to the next iteration) to its constructor. The read-only n_param_sets property then reflects the configured population size.
SciPy Optimizers¶
Divi provides several SciPy-based optimizers through the ScipyOptimizer class:
Nelder-Mead¶
Nelder-Mead [2] is a gradient-free, simplex-based optimization algorithm. It is ideal for local optimization in low to moderate dimensional spaces. The method iteratively refines a simplex (a geometrical figure defined by a set of parameter vectors) by evaluating cost function values and applying operations such as reflection, expansion, and contraction.
Use Nelder-Mead when:
Your problem is continuous but noisy.
Gradients are unavailable or expensive to compute.
You are tuning parameters in a relatively low-dimensional space.
from divi.qprog.optimizers import ScipyOptimizer, ScipyMethod
optimizer = ScipyOptimizer(method=ScipyMethod.NELDER_MEAD)
L-BFGS-B¶
L-BFGS-B (Limited-memory Broyden–Fletcher–Goldfarb–Shanno with Bound constraints) [3] is a quasi-Newton method that leverages gradient information to efficiently converge to a local minimum. In Divi, gradient calculation is performed using the parameter shift rule, a technique well-suited to quantum circuits that allows for analytical gradient computation by evaluating the function at shifted parameter values.
Divi computes these parameter shifts in parallel, significantly reducing wall-clock time for gradient evaluations.
Use L-BFGS-B when:
You require fast convergence to a local minimum.
Your cost function is smooth and differentiable.
Note
On small or barren-plateau circuits the parameter-shift gradient can be
near-zero at the start, so L-BFGS-B may hit its gtol and stop after a
couple of iterations (a flat, uninformative loss trajectory). If that
happens, switch to a gradient-free optimizer (COBYLA, Nelder-Mead) or the
Monte Carlo optimizer for a more illustrative run.
optimizer = ScipyOptimizer(method=ScipyMethod.L_BFGS_B)
COBYLA¶
COBYLA (Constrained Optimization BY Linear Approximations) [4] is a gradient-free, local optimization method—like Nelder-Mead—that supports nonlinear inequality constraints. It constructs successive linear approximations of the objective function and constraints, iteratively refining the solution within a trust region.
Use COBYLA when:
Your optimization problem includes constraints.
Gradients are inaccessible or too noisy.
You seek a reliable optimizer for low to moderate-dimensional spaces.
COBYLA is also a good choice of optimizer when trying out QAOA for a new problem/experimenting, but your mileage may vary.
optimizer = ScipyOptimizer(method=ScipyMethod.COBYLA)
PyMOO Optimizers¶
Divi also supports evolutionary algorithms through PyMOO:
CMA-ES (Covariance Matrix Adaptation Evolution Strategy)¶
CMA-ES [5] is a stochastic, derivative-free method for numerical optimization of non-linear or non-convex continuous optimization problems.
from divi.qprog.optimizers import PymooOptimizer, PymooMethod
optimizer = PymooOptimizer(method=PymooMethod.CMAES)
Differential Evolution¶
Differential Evolution [6] is a method that optimizes a problem by iteratively trying to improve a candidate solution with regard to a given measure of quality.
optimizer = PymooOptimizer(method=PymooMethod.DE)
Quantum Natural Gradient¶
The QNGOptimizer performs regularized natural-gradient descent:
where \(\nabla L\) is the parameter-shift gradient and \(G\) is a positive-semidefinite metric tensor. Preconditioning by the inverse metric rescales the gradient to follow the geometry of the quantum state manifold, which can dramatically reduce the number of iterations needed to converge compared to vanilla gradient descent — particularly in circuits with many parameters or pronounced curvature variation.
Metric estimators. The optimizer is metric-agnostic: the metric is
produced by an injected MetricEstimator
strategy. Swapping the estimator changes the metric without touching the
optimizer itself. Two estimators are provided:
PullbackMetricEstimator(default) — Hamiltonian-aware pullback metric, \(G_{ij} = \sum_r a_r^2 \,(\partial_i \langle P_r \rangle)(\partial_j \langle P_r \rangle)\), computed from the per-Pauli-term expectation gradients of the loss observable \(H = \sum_r a_r P_r\). The gradient and metric share one parameter-shift measurement pass (a single memoized evaluation), so there is no extra circuit overhead relative to a standard gradient step. Compatible with VQE, QAOA, andCustomVQAprograms whose loss is the expectation value of the cost Hamiltonian. Rejects programs with a classical loss objective (PCE) or a supervised data-bound loss.FubiniStudyMetricEstimator— block-diagonal Fubini–Study metric (quantum geometric tensor), computed from the covariance of the Hermitian generators of each layer’s parametric gates. Independent of the loss observable, so it applies to any program with a Pauli-rotation ansatz — including PCE. Provides onlymetric_fn; the gradient falls back to the program’s own parameter-shift rule. Rejects ansatze that use unsupported or composite-angle gates, and data-bound programs.
Solver options. The optimizer regularizes the metric before inverting to prevent divergence along flat directions:
solver="tikhonov"(default) — solves \((G + \lambda I)\,\delta = \nabla L\) via a Cholesky-based symmetric solve, exploiting the PSD structure.solver="pinv"— applies the Moore–Penrose pseudo-inverse of the raw (undamped) metric with cutoffrcond.
Usage is the same as any optimizer — pass an instance via the
optimizer= argument and call run():
from divi.qprog import VQE
from divi.qprog.optimizers import QNGOptimizer
vqe = VQE(
molecule=molecule,
backend=backend,
optimizer=QNGOptimizer(step_size=0.1, regularization=1e-3),
max_iterations=10,
)
vqe.run()
To switch to the Fubini–Study metric, inject a different estimator:
from divi.qprog.optimizers import FubiniStudyMetricEstimator, QNGOptimizer
optimizer = QNGOptimizer(
metric_estimator=FubiniStudyMetricEstimator(),
)
Note
QNG does not support checkpointing (supports_checkpointing is
False). Passing checkpoint_config with a checkpoint directory to
run() raises a ValueError upfront. The variational algorithm
already checkpoints the parameter history, so optimizer-level state is
not needed.
Note
Both metric estimators compute the metric exactly per iteration and require additional circuit evaluations beyond a plain gradient step. QNG is best suited to simulators or small-to-moderate problems where the measurement overhead is acceptable. For large hardware runs with tight shot budgets, standard gradient-based optimizers may be more practical.
Note
QNG with QDrift. When a QAOA program uses a stochastic
QDrift trotterization, the gradient and the
metric must be evaluated on the same sampled Hamiltonian — otherwise the
natural-gradient step mixes mismatched operators. Divi guarantees this: the
cost, gradient, and metric pipelines all draw the same QDrift batch within
one optimizer evaluation (the sample is keyed on an internal per-evaluation
counter) and resample on the next. The draw is reproducible from the
QDrift(seed=...) you provide; with no seed it is still consistent within
each evaluation but varies across runs. n_hamiltonians_per_iteration
controls how many independent samples are averaged per evaluation — higher
values reduce variance at a proportional increase in circuits.
Use QNG when:
You are running VQE, QAOA, or PCE on a simulator and want faster parameter convergence relative to vanilla gradient descent.
Your landscape has strong curvature variation across parameters.
You are using the pullback metric and want gradient + metric from a single measurement pass.
Simultaneous Perturbation (SPSA / QN-SPSA)¶
SPSA and its quantum-natural variant estimate their search direction from a constant number of cost evaluations per step, independent of the number of parameters. This makes them attractive for many-parameter, shot-noisy circuits where the parameter-shift rule — which scales with the parameter count — is prohibitively expensive.
SPSA¶
The SPSAOptimizer [7] approximates the
gradient from just two cost evaluations per step by perturbing all
parameters simultaneously along a random Bernoulli \(\pm 1\) direction
\(h\):
with decaying gains \(a_k = a/(A + k + 1)^\alpha\) and
\(c_k = c/(k + 1)^\gamma\). The two perturbed points are evaluated as a
single batch, so a stochastic cost (e.g. a QDrift
QAOA cost) scores both against the same sampled Hamiltonian. SPSA is
gradient-free: it ignores any parameter-shift gradient the algorithm would
otherwise compute.
from divi.qprog.optimizers import SPSAOptimizer
optimizer = SPSAOptimizer(learning_rate=0.2, c=0.2)
A good starting point is to set c near the standard deviation of the cost’s
shot noise (so the finite difference clears the noise floor) and tune
learning_rate from there. resamplings averages several gradient samples
per step to reduce variance (at proportional cost). The optional blocking
guard performs look-ahead blocking — it evaluates the candidate’s loss and
rejects the step if it would worsen the loss by more than blocking_tol times
the std of the recent window, otherwise accepting it. This prevents a single bad
estimate (or a divergent preconditioned step in QN-SPSA) from corrupting the run,
at the cost of one extra evaluation per step (plus one to seed the baseline).
blocking_tol — not resamplings — is the knob that absorbs cost noise in
that single-evaluation accept/reject decision.
Note
When blocking is off and a run diverges (most likely QN-SPSA in high
dimensions), best-iterate tracking would otherwise return an early finite
iterate with no signal that the run blew up. The optimizers emit a one-time
UserWarning when the loss grows by more than 1000× its starting value —
if you see it, enable blocking, raise regularization, or lower
learning_rate.
Note
The perturbation directions are drawn from a random generator, so runs vary
by default. Pass rng= (a numpy.random.Generator) for reproducibility.
The loss reported to the history is the average of the two perturbed
evaluations — a free but O(c_k²)-biased estimate of f(θ); set
exact_loss=True to spend one extra unperturbed evaluation per step for the
exact value (used for the recorded loss and best-iterate selection). Note that
a QDrift(seed=...) seed fixes only the Hamiltonian sampling, not these
perturbation directions.
QN-SPSA¶
The QNSPSAOptimizer [8] combines the
SPSA gradient with a metric-preconditioned update, so it follows the geometry of
the state manifold like QNGOptimizer while
keeping a constant per-step circuit budget. Like QNG, the metric backend is
pluggable via the metric_estimator argument:
StochasticFidelityMetricEstimator(default) — the faithful QN-SPSA metric, estimated from state-overlap fidelities \(F(\theta_1, \theta_2) = |\langle\psi(\theta_1)|\psi(\theta_2)\rangle|^2\) (measured as the all-zeros probability of the compute-uncompute circuit \(U(\theta_1)\,U(\theta_2)^\dagger\)) using two random directions and four overlap evaluations per step. The samples are accumulated into a running average seeded at the identity, conditioned as \(|\bar g| + \beta I\), and used to precondition the SPSA gradient. Because the overlap depends only on the ansatz state — not the loss observable — it applies to any qiskit-invertible ansatz, and rejects data-bound programs (and any ansatz qiskit cannot invert). For QDrift QAOA the metric is built from the fixed cost-ansatz realization captured at construction (it does not re-sample per evaluation), so it stays consistent across the run.FubiniStudyMetricEstimatororPullbackMetricEstimator— use the estimator’s exact metric (as in QNG) while keeping the SPSA gradient. The metric cost then scales with the parameter count rather than staying constant.
from divi.qprog.optimizers import QNSPSAOptimizer
# Faithful stochastic-fidelity metric (default)
optimizer = QNSPSAOptimizer(learning_rate=0.01, c=0.2, regularization=1e-3)
# Or reuse an exact metric with the SPSA gradient
from divi.qprog.optimizers import FubiniStudyMetricEstimator
optimizer = QNSPSAOptimizer(
metric_estimator=FubiniStudyMetricEstimator(),
)
QN-SPSA preconditions the step by the (inverse) metric, so it typically uses a
smaller raw learning_rate than plain SPSA. The constant per-step cost buys a
noisier metric estimate than QNG’s exact metric. In high dimensions this noisy,
low-rank metric estimate can occasionally drive a divergent step; raise
resamplings (less metric noise), increase regularization, or enable
blocking (which rejects such steps outright) if the optimization is unstable.
Note
Like QNG, neither SPSA nor QN-SPSA supports checkpointing
(supports_checkpointing is False): their only persistent state is the
parameter vector, which the variational algorithm already records. The
per-step gains, blocking history, and running-average metric are recomputed
each run.
Use SPSA / QN-SPSA when:
Your circuit has many parameters and parameter-shift gradients are too costly.
The cost is shot-noisy and you want a method designed around stochastic evaluations.
(QN-SPSA) You want metric-aware updates at a constant per-step circuit cost, trading the exact metric for a stochastic estimate.
QUIVER (Adaptive Directional Gradients)¶
The QUIVEROptimizer [9] reconstructs the
full gradient from V random Rademacher (\(\pm 1\)) directional
derivatives, independent of the parameter count \(N\):
costing \(2V\) evaluations per step. A single direction (\(V = 1\))
recovers SPSA; \(V = N\) recovers the full parameter-shift gradient — so
V dials between cheap-but-noisy and expensive-but-precise gradient estimates.
QUIVER also adapts V and the per-direction shot count M each step
(iCANS/gCANS-style), spending more directions when the gradient estimate is noisy
relative to its magnitude, and more shots when measurement noise dominates:
Vis driven by the spread of theVdirectional samples — it needs no backend variance, so it adapts on any cost function.Mis driven by the measurement-variance estimate the cost closure exposes on shot-based backends. On native-expectation-value backends (no shot counts) it falls back to a fixedMandV-from-spread only.
from divi.qprog.optimizers import QUIVEROptimizer
optimizer = QUIVEROptimizer(learning_rate=0.1, epsilon=0.1, V_init=2)
Set derivative_mode="parameter_shift" to use a \(\pi/2\) directional shift
instead of the finite-difference step epsilon; set adapt_V=False /
adapt_M=False to pin a fixed budget. QUIVER shares SPSA’s blocking and
exact_loss options, is gradient-free, and does not support checkpointing.
Note
An adapting M delivers its per-evaluation shot budget to the backend as
explicit per-circuit shot groups, which disables circuit-template batching for
that submission. On template-capable backends (e.g. the Qoro cloud) this trades
template reuse for shot adaptivity — prefer adapt_M=False there if
submission overhead dominates, and reserve adapt_M for local shot-based
simulators. adapt_M also assumes a uniform per-group shot count, so it is
not combined with a configured shot_distribution (a warning is emitted).
Use QUIVER when:
You want SPSA’s constant-cost appeal but with a tunable accuracy/cost trade-off via
V.You want the per-step measurement budget to adapt automatically to shot noise.
Grid Search¶
The GridSearchOptimizer performs an exhaustive evaluation of every
point on a user-defined parameter grid and returns the best-performing
combination. It is designed for low-dimensional parameter spaces (1–3
parameters) where you want full visibility into the loss landscape.
Use Grid Search when:
You have a small number of variational parameters (e.g. QAOA with 1 layer: γ and β).
You want to visualize the loss landscape.
You need a deterministic, reproducible sweep.
You want to warm-start a variational optimizer from the best grid point.
import numpy as np
from divi.qprog.optimizers import GridSearchOptimizer
# Auto-generate a 20×20 grid over [0, 2π] × [0, π]
optimizer = GridSearchOptimizer(
param_ranges=[(0, 2 * 3.14159), (0, 3.14159)],
grid_points=20,
)
# Or supply an explicit grid
optimizer = GridSearchOptimizer(
param_grid=np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
)
The grid is evaluated in a single pass regardless of max_iterations.
A warning is issued if max_iterations > 1 is supplied.
Note
Grid search scales as grid_points ** n_params, so it becomes
impractical beyond ~3 parameters. For higher dimensions, use
MonteCarloOptimizer or CMA-ES instead.
Choosing the Right Optimizer¶
For :class:`~divi.qprog.algorithms.VQE`:
QNG (pullback): Best when the landscape is smooth and you want faster per-iteration progress than L-BFGS-B at the cost of additional metric measurements; requires a Hamiltonian-expectation-value loss (not PCE)
QN-SPSA: Metric-aware updates like QNG but at a constant per-step circuit cost; best for many-parameter circuits where exact gradients/metrics are too expensive
SPSA: Gradient-free with two evaluations per step; best for many-parameter, shot-noisy circuits
QUIVER: Forward-gradient generalization of SPSA with a tunable direction count
Vand adaptive shot allocation; best when you want to trade gradient accuracy against measurement budget on shot-based backendsL-BFGS-B: Best for smooth, differentiable landscapes with good initial parameters
Monte Carlo: Excellent for exploration and avoiding local minima
COBYLA: Good for constrained problems or when gradients are unreliable
Nelder-Mead: Robust choice for noisy or discontinuous landscapes
For :class:`~divi.qprog.algorithms.QAOA`:
Grid Search: Best for 1–2 layer QAOA where you want full landscape visibility
QNG (pullback): Accelerates convergence in simulator runs; requires a cost Hamiltonian expectation loss
QN-SPSA / SPSA: Best for deep, many-layer QAOA on shot-noisy backends, where the constant per-step cost beats parameter-shift gradients
QUIVER: When you want SPSA’s low per-step cost on shot-noisy QAOA but with a tunable direction count and adaptive shot allocation
COBYLA: Often the best starting point for
QAOAproblemsNelder-Mead: Good for noisy landscapes and parameter initialization
Monte Carlo: Excellent for global exploration and avoiding barren plateaus
L-BFGS-B: Use when you have good initial parameters and smooth landscapes
For PyMOO Optimizers:
CMA-ES: Excellent for high-dimensional parameter spaces and when you need robust global optimization. Particularly effective for
VQEwith many parameters.Differential Evolution: Good for multimodal optimization landscapes and when you need to escape local minima. Works well for
QAOAparameter optimization.
For Hyperparameter Sweeps:
Monte Carlo: Best for initial exploration across parameter ranges
L-BFGS-B: Use for fine-tuning after Monte Carlo exploration
Nelder-Mead: Robust fallback when other methods fail
CMA-ES: Excellent for high-dimensional sweeps with many parameters
Quantum-Specific Considerations:
Barren Plateaus: Use
MonteCarloOptimizeror CMA-ES to avoid getting trapped in flat regionsParameter Initialization: Start with small random values (typically [-0.1, 0.1]) for better convergence
Circuit Depth: Deeper circuits benefit from more robust optimizers like CMA-ES or
MonteCarloOptimizerNoise Resilience: Nelder-Mead and COBYLA are more robust to quantum noise than gradient-based methods
Natural Gradient: Use
QNGOptimizeron simulators when the circuit has pronounced curvature variation and you want metric-aware updates; not recommended for large hardware runs where extra metric measurements are expensive
Early Stopping¶
Long-running optimizations can waste resources once convergence has effectively
stalled. Divi’s EarlyStopping controller lets you
terminate the loop automatically based on configurable criteria.
Pass an EarlyStopping instance to any variational algorithm:
from divi.qprog import VQE, EarlyStopping
from divi.qprog.optimizers import ScipyOptimizer, ScipyMethod
vqe = VQE(
molecule=molecule,
backend=backend,
optimizer=ScipyOptimizer(method=ScipyMethod.COBYLA),
max_iterations=200,
early_stopping=EarlyStopping(
patience=10,
min_delta=1e-5,
),
)
vqe.run()
print(f"Stopped at iteration {vqe.current_iteration}")
print(f"Reason: {vqe.stop_reason}") # e.g. "patience_exceeded"
print(f"Converged: {vqe.optimize_result.success}") # False for early stop
Stopping Criteria¶
Three criteria are available and are evaluated in priority order after every iteration. The first one that fires stops the loop.
Patience (always active) — Stop when the cost has not improved by at least
min_deltaforpatienceconsecutive iterations.EarlyStopping(patience=10, min_delta=1e-4)
Gradient norm (optional) — Stop when the L2 norm of the gradient falls below
grad_norm_threshold. Only effective with gradient-based optimizers such asScipyOptimizer(method=ScipyMethod.L_BFGS_B).EarlyStopping(patience=10, grad_norm_threshold=1e-6)
Cost variance (optional) — Stop when the rolling variance of the last
variance_windowcost values drops belowvariance_threshold. Useful for noisy landscapes where cost oscillates but no longer trends downward.EarlyStopping( patience=10, variance_window=20, variance_threshold=1e-8, )
All three criteria can be enabled simultaneously; the first one that triggers will stop the loop.
After the Run¶
After run() completes, use stop_reason
to determine why optimization ended:
None— optimization ran tomax_iterationswithout triggering early stopping"patience_exceeded"— cost plateaued"gradient_below_threshold"— gradient vanished"cost_variance_settled"— cost variance settled
The optimize_result attribute
is always populated and its message field includes the stop reason.
Inspecting Optimizer Results¶
After running a variational algorithm, the loss history is available directly
on the program object via
min_losses_per_iteration and
losses_history — see
reading results in core concepts for their
semantics and types.
Beyond the loss history, you can inspect the raw result object returned by the
underlying optimizer
via the optimize_result property.
This exposes optimizer-specific diagnostics such as:
nfev– number of cost-function evaluationsnjev– number of Jacobian (gradient) evaluations (gradient-based optimizers)nit– number of iterations completedsuccess– whether the optimizer convergedmessage– convergence or termination message
program.run()
result = program.optimize_result
if result is not None:
print(f"Function evaluations: {result.nfev}")
print(f"Converged: {result.success}")
Note
optimize_result is always populated after run() completes.
When optimization converges normally, success is True.
When early stopping or cancellation terminates the run, success is
False and the message field describes the reason. The available
attributes depend on the optimizer; see scipy.optimize.OptimizeResult
for the full specification.
Next Steps¶
tutorials/ — runnable examples
Ground-State Energy Estimation with VQE and Combinatorial Optimization with QAOA and PCE — algorithm-specific guidance
Program Ensembles and Workflows — optimizers in large-scale sweeps and ensembles