## `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`

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.                                           |
| `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. |
| `tol`                | \`float                       | None\`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | The tolerance, passed to scipy.optimize.minimize.                                                                                                        |
| `random_state`       | \`int                         | None\`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | The seed for the random starting point, which also seeds the perturbations of "spsa". None draws an unseeded starting point.                             |
| `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.                          |

Returns:

| Type                | Description                                                                                                  |
| ------------------- | ------------------------------------------------------------------------------------------------------------ |
| `VariationalResult` | A VariationalResult with the optimal value, the parameters, and the history of the cost at every evaluation. |

Example

```python
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
```
