Visualizing Variational Landscapes

The divi.viz module provides loss-landscape scans and analysis tools for variational programs. Use it to inspect the local geometry of an objective around a chosen parameter vector, compare minima, study curvature, and find minimum-energy paths between solutions.

Divi’s scan API is inspired by orqviz (Zapata Engineering, Apache-2.0; see also arXiv:2111.04695), but the implementation is specific to Divi’s program model, batching, and pipeline execution. Beyond orqviz-compatible scan geometry, divi.viz adds Hessian eigenvalue analysis, Fourier power spectra, gradient overlays, 3D rendering, trajectory overlays on PCA scans, and the Nudged Elastic Band algorithm — all evaluated through Divi’s batched cost pipeline.

Choosing a Scan Type

I want to…

Use

Result type

See the landscape around my optimum

scan_1d() / scan_2d()

Scan1DResult / Scan2DResult

See the landscape the optimizer explored

scan_pca()

PCAScanResult

Compare two solutions along the connecting line

scan_interp_1d() / scan_interp_2d()

Scan1DResult / Scan2DResult

Identify curvature directions at a point

compute_hessian()

HessianResult

Find frequency structure in a landscape

fourier_analysis_2d()

Fourier2DResult

Find the energy barrier between two minima

run_neb()

NEBResult

Every scan function returns a result object with a .plot() method for quick visualization. Two-dimensional results also have .plot_3d() for surface rendering.

Basic Scans

The standalone API is useful when you want to make the scan call explicit:

import numpy as np
import pennylane as qp
from qiskit.circuit.library import RYGate, RZGate

from divi.backends import MaestroSimulator
from divi.qprog import GenericLayerAnsatz, VQE
from divi.qprog.optimizers import ScipyMethod, ScipyOptimizer
from divi.viz import scan_1d, scan_2d

hamiltonian = -1.0 * qp.Z(0) + 0.5 * qp.Z(0) @ qp.Z(1)

vqe = VQE(
    hamiltonian=hamiltonian,
    ansatz=GenericLayerAnsatz([RYGate, RZGate]),
    n_layers=2,
    optimizer=ScipyOptimizer(method=ScipyMethod.COBYLA),
    max_iterations=12,
    backend=MaestroSimulator(shots=5000),
)
vqe.run()

line = scan_1d(vqe, n_points=31, rng=0)
plane = scan_2d(vqe, grid_shape=(21, 21), rng=0)

line.plot(show=True)    # line chart of objective vs offset
plane.plot(show=True)   # filled contour plot

line.plot opens a matplotlib figure with the scanned offset on the x-axis and the objective value on the y-axis — useful for spotting barren plateaus or local minima along a single direction. plane.plot renders a filled contour plot over the two scan axes with the optimum at the center, giving a 2-D picture of the local loss landscape. Both return the underlying matplotlib.figure.Figure and matplotlib.axes.Axes so you can customize the rendering.

If center is omitted, the scan is centered on program.best_params from a previous optimization run.

By default, scalar spans use \((-\pi, \pi)\) along each scan axis. Omitted directions are filled with random vectors (in 2D, the second axis is orthogonal to the first). Pass an integer or numpy.random.Generator as rng for reproducible slices.

Direction vectors are normalized to unit length by default, so span offsets are in parameter-space Euclidean distance: offset=1.0 moves exactly 1.0 in parameter space regardless of the direction’s original length. Set normalize_directions=False to use the raw direction vector, where offset=1.0 moves by the full (unnormalized) direction. This matches the orqviz convention.

Using program.viz

Supported variational programs also expose scan and analysis functions through a convenience wrapper:

scan = vqe.viz.scan_2d(grid_shape=(25, 25), rng=0)
fig, ax = scan.plot(show=True)

The wrapper exposes scan_1d, scan_2d, scan_pca, scan_interp_1d, scan_interp_2d, compute_hessian, and run_neb. Utility functions like periodic_trajectory_wrap() and fourier_analysis_2d() operate on results or arrays and should be imported directly from divi.viz.

PCA Scans

A random-direction scan shows a slice of the landscape that may miss the structure the optimizer navigated. PCA scans address this: they build scan directions from the principal components of the optimization trajectory, showing the landscape along the directions that captured the most variance during optimization. With few iterations, best-so-far points can lie almost on a line in parameter space (common with COBYLA early on), so give the run enough steps that the trajectory spans two independent directions — as in tutorials/visualization/viz_qaoa_pce_comparison.py.

Use scan_pca() with parameter vectors from the optimization history:

from divi.viz import scan_pca

samples = np.vstack(vqe.param_history(mode="best_per_iteration"))
pca_scan = scan_pca(
    vqe,
    samples=samples,
    center=vqe.best_params,
    grid_shape=(21, 21),
    offset=0.2,   # expand grid 0.2 beyond the sample cloud (default is 1.0)
)
pca_scan.plot(show=True)   # cell heatmap with sample dots

samples must have shape (n_samples, n_params) and span at least two independent directions. After run(), a typical choice is stacking program.param_history(mode="best_per_iteration") (one best row per callback; avoids folding an entire population into PCA) or program.param_history(mode="all_evaluated") when you want every evaluated member.

Omit center to anchor the affine plane at the sample mean (orqviz default). Pass center=program.best_params to translate the plane so the same PC directions pass through the optimum.

Periodic Wrapping for PCA

Quantum gate parameters are typically \(2\pi\)-periodic. When an optimization trajectory crosses the period boundary, PCA can see an artificial jump and produce distorted landscapes. Use periodic_trajectory_wrap() before feeding samples to scan_pca():

from divi.viz import periodic_trajectory_wrap, scan_pca

raw_samples = np.vstack(vqe.param_history(mode="best_per_iteration"))
samples = periodic_trajectory_wrap(raw_samples)
pca_scan = scan_pca(vqe, samples=samples, center=vqe.best_params)

The function iterates forward through the rows, wrapping each row relative to its predecessor so that no single-step jump exceeds half a period.

Trajectory Overlay

Pass show_trajectory=True to plot() to draw the optimization path as a connected line on top of the heatmap:

pca_scan.plot(show=True, show_trajectory=True)

Start and end points are marked with distinct markers. You can combine show_trajectory with show_samples (scatter dots, enabled by default).

Interpolation Scans

Use scan_interp_1d() to evaluate the objective along the straight line between two parameter vectors — for example, to check if there is a barrier between two local minima:

from divi.viz import scan_interp_1d

result = scan_interp_1d(program, theta_1=vqe.best_params, theta_2=other_params)
result.plot(show=True)   # line chart with t on the x-axis

The offsets in the result are the interpolation parameter t ranging from 0 (theta_1) to 1 (theta_2).

scan_interp_2d() extends this to 2D: the x-direction is the interpolation vector and the y-direction is orthogonal (random if omitted):

from divi.viz import scan_interp_2d

result = scan_interp_2d(program, theta_1, theta_2, grid_shape=(21, 21))
result.plot(show=True)   # filled contour plot

Default spans are (-0.5, 1.5) along x (extending the interpolation line) and (-0.5, 0.5) along y.

Hessian Analysis

compute_hessian() computes the matrix of second partial derivatives at a parameter point. Eigenvalues reveal local curvature; eigenvectors are natural choices as scan directions.

By default, the parameter-shift rule is used (exact for standard quantum gates). Pass gradient_method=GradientMethod.FINITE_DIFFERENCE for a finite-difference fallback with configurable eps:

from divi.viz import GradientMethod

# Exact (parameter-shift, default):
hess = program.viz.compute_hessian()

# Finite-difference alternative:
hess = program.viz.compute_hessian(gradient_method=GradientMethod.FINITE_DIFFERENCE, eps=1e-4)

Via program.viz, the center defaults to best_params:

hess = program.viz.compute_hessian()  # at best_params

# Eigenvalues are sorted ascending (smallest first).
print("Eigenvalues:", hess.eigenvalues)
# Inverse condition number: values near 1 indicate isotropic curvature.
print("Condition ratio:", hess.eigenvalues[0] / hess.eigenvalues[-1])

# Use steepest-curvature directions for a 2D scan.
d1, d2 = hess.top_eigenvectors(k=2)
scan = program.viz.scan_2d(direction_x=d1, direction_y=d2)
scan.plot(show=True)

The total number of evaluations is \(2n^2 + 1\) where n is the number of parameters. For a 2-layer QAOA with 4 parameters per layer (n = 8), that is 129 evaluations; for 20 parameters, 801.

Fourier Analysis

fourier_analysis_2d() applies a 2D FFT to any Scan2DResult or PCAScanResult and returns the power spectrum:

from divi.viz import fourier_analysis_2d

spectrum = fourier_analysis_2d(plane)
spectrum.plot(show=True)   # heatmap of the power spectrum

The power spectrum reveals dominant frequency components in the loss landscape.

Nudged Elastic Band (NEB)

Warning

NEB is experimental. Convergence is sensitive to hyperparameters.

Use NEB when you have two candidate solutions and want to characterize the energy barrier between them — a low barrier suggests the solutions are connected, while a high barrier suggests distinct basins.

run_neb() finds a minimum-energy path by relaxing a chain of images via gradient descent perpendicular to the chain tangent. Like compute_hessian(), it accepts a gradient_method parameter (defaults to PARAMETER_SHIFT):

from divi.viz import run_neb

result = run_neb(program, theta_1, theta_2, n_pivots=12, n_steps=50)
result.plot(show=True)   # energy profile along the relaxed path

Set convergence_tol to stop early when the maximum pivot displacement falls below a threshold:

result = run_neb(program, theta_1, theta_2, convergence_tol=1e-4)

3D Surface Plots

Both Scan2DResult and PCAScanResult have a plot_3d() method for matplotlib 3D surface rendering:

plane.plot_3d(show=True)

Gradient Overlay

Pass show_gradients=True to any 2D .plot() call to overlay a quiver plot of the numerical gradient (computed via numpy.gradient() on the grid, no extra evaluations):

plane.plot(show=True, show_gradients=True)

Batching Behavior

Scans evaluate all grid points in a single batched call through the program’s existing cost pipeline. This means scan evaluations reuse the same backend, batching, and circuit-compilation behavior as normal optimization. A 41x41 grid sends 1,681 parameter sets in one batch, not 1,681 separate evaluations.

Supported Programs

Scans apply to fixed-parameter-space subclasses of VariationalQuantumAlgorithm, including VQE, QAOA, PCE, and CustomVQA.

Limitations:

  • IterativeQAOA is excluded because its parameter space is not fixed across iterations.

  • PCE preserves the same backend rules as ordinary cost evaluation. In particular, hard CVaR mode still requires a sampling backend.