Gradients & VQE
5 gradient methods, VQE optimization, QAOA, and custom training loops.
Two Ways to Compute Gradients
Fast path — sf.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
| Method | Scaling | Noise-robust | Best for |
|---|---|---|---|
| Adjoint | O(1) | No | Deep circuits, many params |
| Parameter-shift | O(N) | No | Shallow circuits |
| SPSA | O(1) | Yes | Noisy, many params |
| QNG | O(N²) | No | Fast convergence |
| Riemannian | O(N²) | No | Natural 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