Skip to content

variational

eigenkit.core.variational

Variational optimisation: minimise <H>(θ) over the parameters of a circuit.

This is the training engine, independent of any framework, that the Variational Quantum Eigensolver (VQE) in chemistry, the quantum approximate optimisation algorithm (QAOA) in optimization, and any workflow of the form "prepare a parametrised state, measure <H>, optimise θ" share. It uses only NumPy and SciPy. It runs the NumPy forward pass of the core Circuit (compiled.run) and its analytic NumPy gradients (_gradient_jacobians: the backend's own gradient when it has one, such as the adjoint method of the NumPy simulator, and the parameter-shift rule otherwise). So it runs on any backend (NumPy, PennyLane, Qiskit, Braket, CUDA Quantum) without PyTorch. Even on PennyLane it uses the NumPy path, not the PyTorch QNode. SciPy is imported only at the first call.

Backends that cannot differentiate (gradient_method="none", for example a quantum processing unit (QPU) whose configuration leaves enable_gradients off) must use an optimiser that needs no gradient, such as "COBYLA", "Nelder-Mead", "Powell" or "spsa". These only need the forward evaluation.

VariationalResult dataclass

Bases: ValueEquality

Outcome of minimize_expectation().

The result is read-only. Assigning to a field raises FrozenInstanceError, and writing into an array field raises ValueError. This prevents a mistyped = (instead of ==) or an in-place edit from leaving a result whose scalar fields describe a different run from the array it holds. To get a writable copy of the array, use np.array(result.field).

Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute.

Attributes:

Name Type Description
optimal_value float

The lowest expectation value reached. When the observable is a Hamiltonian, it is the estimated ground-state energy.

optimal_parameters ndarray

The weights that produced this value, of shape (n_weights,).

n_iterations int

The number of iterations reported by the SciPy optimiser. When there is no such count (a custom callable, "spsa", or a SciPy method that does not report one), it is the number of cost evaluations, as in n_evaluations.

n_evaluations int

The number of circuit evaluations that the optimiser made. With a SciPy method it is usually higher than n_iterations, because a line search costs several evaluations per step. Evaluations made only for bookkeeping are not counted, so a custom optimiser that never called the cost function reports 0.

success bool

Whether the run converged. A SciPy optimiser reports it. A custom callable reports it only if it returns (parameters, converged), and otherwise it is inferred from whether the final cost is lower than the starting cost. A False with a plausible energy usually means that the maximum number of iterations was reached.

message str

The termination message of the optimiser. For a custom callable that reported nothing, it gives the cost before and after the run.

history tuple[float, ...]

The expectation value at every evaluation, in order. Plot it to see whether the run converged or stalled. It is a tuple, so nothing can be appended to it after the run.

__post_init__()

Make the array field read-only.

Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use np.array(result.optimal_parameters).

minimize_expectation(circuit, *, initial_parameters=None, optimizer='L-BFGS-B', max_iterations=None, tol=None, random_state=42, callback=None)

Minimise <H>(θ) for a circuit whose parameters are all variational.

Parameters:

Name Type Description Default
circuit Circuit

A core NumPy Circuit whose single observable is the Hamiltonian H and whose spec.n_inputs == 0 (every parameter is a variational weight). It works with any provider.

required
initial_parameters ndarray | None

The starting θ, of shape (n_weights,). When it is not given, it is drawn at random with the seed random_state.

None
optimizer Optimizer

The name of a scipy.optimize.minimize method (the default is "L-BFGS-B"), "spsa" for spsa(), or a callable optimizer(cost, theta0, gradient) -> parameters for a custom or framework optimiser. SPSA needs no gradient: each iteration evaluates the circuit twice for the gradient estimate and once more to record the history. A callable can instead return (parameters, converged) to report convergence itself. Otherwise, success is inferred from whether the final cost is lower than the starting cost, because a vector alone says nothing about convergence.

'L-BFGS-B'
max_iterations int | None

The maximum number of iterations. It is passed to SciPy as options={"maxiter": ...}, and to spsa() as maxiter, which is 100 when max_iterations is None.

None
tol float | None

The tolerance, passed to scipy.optimize.minimize.

None
random_state int | None

The seed for the random starting point, which also seeds the perturbations of "spsa". None draws an unseeded starting point.

42
callback Callable[[ndarray], None] | None

An optional function called after every iteration with the current parameters. It is passed to SciPy, and "spsa" also calls it.

None

Returns:

Type Description
VariationalResult

A VariationalResult with the optimal value, the parameters, and the history of the cost at every evaluation.

Example
from eigenkit import Circuit, Variational
from eigenkit.core.ir import Observable
from eigenkit.core.variational import minimize_expectation

# One qubit, one trainable rotation, H = Z. The minimum is -1.
circuit = Circuit(
    blocks=[Variational(depth=1)],
    observables=Observable.from_string("Z0"),
    provider="numpy",
)

result = minimize_expectation(circuit, random_state=42)

print(round(result.optimal_value, 6))        # -1.0
print(result.success, result.n_iterations)   # True 4