sfSuperfermion docs
Guides

Gradients & VQE

5 gradient methods, VQE optimization, QAOA, and custom training loops.

Two Ways to Compute Gradients

Fast pathsf.State.grad() in Rust:

state = sf.simulate(ansatz, params=params)
dag = ansatz.bind(params).to_ir()
grads = state.grad(obs, dag, params)

Framework path — through PyTorch, JAX, or TensorFlow autograd (see ML Frameworks).

5 Gradient Methods

MethodScalingNoise-robustBest for
AdjointO(1)NoDeep circuits, many params
Parameter-shiftO(N)NoShallow circuits
SPSAO(1)YesNoisy, many params
QNGO(N²)NoFast convergence
RiemannianO(N²)NoNatural gradient via QFIM

Adjoint

One forward + one backward pass regardless of parameter count. The default — fastest for most circuits.

from superfermion.qml.gradient.adjoint import adjoint_grad_vector

grads = adjoint_grad_vector(ansatz, params, obs)

Up to 800x faster than parameter-shift for deep circuits (UCCSD, deep SU(2)).

Parameter-Shift

Evaluates the circuit at shifted parameter values. O(N) scaling but exact.

from superfermion.qml.gradient.parameter_shift import parameter_shift_grad_vector

grads = parameter_shift_grad_vector(ansatz, params, obs)

SPSA (Simultaneous Perturbation Stochastic Approximation)

Stochastic gradient estimation. O(1) scaling, noise-robust.

from superfermion.qml.gradient.spsa import spsa_gradient

grads = spsa_gradient(ansatz, params, obs, num_samples=100)

Quantum Natural Gradient

Uses the Quantum Fisher Information Matrix for metric-aware optimization. Converges in fewer iterations.

from superfermion.qml.gradient.qng import quantum_natural_gradient

grads = quantum_natural_gradient(ansatz, params, obs)

Riemannian Gradient

Natural gradient on the Riemannian manifold of quantum states.

from superfermion.qml.gradient.riemannian import riemannian_gradient

grads = riemannian_gradient(ansatz, params, obs)

VQE — Variational Quantum Eigensolver

Manual loop

import superfermion as sf
import numpy as np

H = sf.Hamiltonian([
    sf.PauliString("II", coeff=-1.0523),
    sf.PauliString("IZ", coeff=0.3979),
    sf.PauliString("ZI", coeff=-0.3979),
    sf.PauliString("ZZ", coeff=-0.0112),
    sf.PauliString("XX", coeff=0.1809),
])

ansatz = (sf.Circuit(2)
    .ry(sf.param("t0"), 0).ry(sf.param("t1"), 1)
    .cnot(0, 1)
    .ry(sf.param("t2"), 0).ry(sf.param("t3"), 1))

params = {f"t{i}": np.random.uniform(0, np.pi) for i in range(4)}
obs = H.to_sparse_list()
lr = 0.1

for step in range(100):
    state = sf.simulate(ansatz, params=params)
    energy = state.expectation(obs)

    dag = ansatz.bind(params).to_ir()
    grads = state.grad(obs, dag, params)

    for k in params:
        params[k] -= lr * grads.get(k, 0.0)

    if step % 20 == 0:
        print(f"Step {step:3d}: energy = {energy:.8f}")

print(f"Final energy: {energy:.8f}")

Built-in VQE class

from superfermion.algorithms.variational import VQE

vqe = VQE(ansatz, obs)
result = vqe.optimize(
    init_params=params,
    max_iterations=100,
    method="L-BFGS-B",    # scipy optimizer
    grad_method="adjoint",
)
print(f"Energy: {result.energy:.8f}")
print(f"Optimal params: {result.optimal_params}")

QAOA

from superfermion.algorithms.variational import QAOA
import networkx as nx

# MaxCut on a random graph
G = nx.random_regular_graph(3, 8)

qaoa = QAOA(G, p=3)  # p = number of layers
result = qaoa.optimize(max_iterations=100)
print(f"MaxCut approximation ratio: {result.energy / nx.maxcut_value(G):.4f}")

Custom Training Loop with SciPy

from scipy.optimize import minimize

def cost_function(param_array):
    params = {f"t{i}": v for i, v in enumerate(param_array)}
    state = sf.simulate(ansatz, params=params)
    return state.expectation(obs)

result = minimize(cost_function, x0=np.zeros(4), method="L-BFGS-B")
print(f"Optimal: {result.fun:.8f} at {result.x}")

Quantum Fisher Information Matrix

state = sf.simulate(ansatz, params=params)
dag = ansatz.bind(params).to_ir()
qfim = state.qfim(dag, params)  # n_params × n_params matrix

print(qfim.shape)
print(f"Condition number: {np.linalg.cond(qfim):.2f}")

The QFIM measures the sensitivity of the quantum state to parameter changes. Used by QNG and Riemannian gradient methods.

Tips

  • Start with adjoint — it's the fastest for most circuits
  • Use SPSA for noisy hardware — it tolerates shot noise and hardware noise
  • Switch to QNG if convergence is slow — fewer iterations, but each is more expensive
  • Monitor gradient norms — if they vanish, you're at a barren plateau; try different initial params

On this page