# Algorithms

`eigenkit.algorithms` contains three standalone quantum algorithms. `HHL` solves linear systems with the algorithm of Harrow, Hassidim and Lloyd (HHL). `IQAE` runs Iterative Quantum Amplitude Estimation (IQAE). `PortfolioRiskEstimator` estimates portfolio risk and is built on IQAE. The first two are single-call solvers. You configure a solver in its constructor, call one method, and get back a frozen result dataclass. There is no `fit`/`predict` cycle and no fitted state to keep. `PortfolioRiskEstimator` works differently, because it is first fitted to a price history (see [below](#portfolioriskestimator-var-and-cvar)).

All three default to `provider="numpy"`, the built-in simulator that every installation includes. Two of the modes of `IQAE`, a callable oracle and `shot_mode="provider"`, need `provider="pennylane"` and the `pennylane` extra. The [API reference](#api-reference) at the end of this page lists the full signatures and defaults, with notes on every argument.

| Solver                                                           | Answers                                                          | Result carries                                                                                         |
| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| [`HHL`](#hhl-linear-systems)                                     | the solution `x` of `Ax = b`                                     | `solution`, `euclidean_norm`, `residual`, `success_probability`, `readout_probability`, `raw_solution` |
| [`IQAE`](#iqae-amplitude-estimation)                             | an unknown amplitude, with an interval                           | `estimation`, `confidence_interval`, `n_rounds`, `n_oracle_calls`                                      |
| [`PortfolioRiskEstimator`](#portfolioriskestimator-var-and-cvar) | the Value at Risk (VaR) and the Conditional Value at Risk (CVaR) | `var`, `cvar`, `tail_probability`, `confidence_interval`, `iqae_result`                                |

Each result is a frozen dataclass, so its fields can be read but not changed.

The theory behind these two algorithms is explained in the [HHL](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md) and [IQAE](https://docs.kit.eigenorbit.com/algorithms/iqae/index.md) chapters of the Algorithms Explained section.

### What counts as a solver

A solver inherits from BaseQuantumSolver. It has a `solve()` method that takes a problem and returns a result, with no training phase and no fitted state. Four classes are solvers: `HHL` and `IQAE` on this page, the Quantum Approximate Optimization Algorithm ([`QAOA`](https://docs.kit.eigenorbit.com/user-guide/optimization/index.md)) in `optimization`, and the Variational Quantum Eigensolver ([`VQE`](https://docs.kit.eigenorbit.com/user-guide/chemistry/index.md)) in `chemistry`. The base class is defined in `core` and not in `algorithms`, because `QAOA` and `VQE` depend only on `core`.

```python
from eigenkit.core import BaseQuantumSolver
from eigenkit import HHL

isinstance(HHL(), BaseQuantumSolver)  # True
```

`solve()` is an abstract method, so a solver class that does not implement it cannot be instantiated. The base class also stores the `provider` attribute for all four solvers. Each subclass sets its own default in its signature and passes it to the base class, and the default is `"numpy"` for all four.

`PortfolioRiskEstimator` does not inherit from `BaseQuantumSolver`, by design. It follows the scikit-learn estimator pattern, with `fit()`, `estimate()` and `var_at()` and no `solve()` method, so `isinstance` returns `False` for it. This is the expected result when you check whether an object can be given a problem directly.

## HHL: Linear systems

`HHL` solves `Ax = b`. If `A` is Hermitian, it is used directly, even when it is not positive semidefinite. Any other invertible square matrix is converted to the normal equations `A†Ax = A†b`, which have the same solution and a Hermitian system matrix.

```python
import numpy as np
from eigenkit import HHL

A = np.array([[1.0, 0.5], [0.5, 1.5]])
b = np.array([1.0, 0.0])

result = HHL(n_eigenvalue_qubits=3, provider="numpy").solve(A, b)

print(result.solution.real)  # [ 1.1888 -0.3789]
print(np.linalg.solve(A, b))  # [ 1.2    -0.4   ]  the classical answer
print(result.residual)  # 0.026
print(result.success_probability)  # 0.7282
```

The solution is returned as a complex array because it is read from a statevector. Use `.real` when the system is real.

The circuit that `solve()` builds has the same stages in both modes. `|b⟩` is prepared on the input register. Phase estimation writes the eigenvalues of `A` into the clock register, whose size is set by `n_eigenvalue_qubits`. The `1/x` rotation turns each eigenvalue into its reciprocal on the ancilla, and `QPE+`, the adjoint of quantum phase estimation (QPE), resets the clock register. The `mode` argument changes how the phase-estimation step is built, but not these stages. The answer is read from the branch where the ancilla is in the useful state and the clock register has returned to zero. This is why the backend must provide a statevector, and `readout_probability` measures the weight of that branch. `success_probability` counts only the ancilla, over every clock state.

Two arguments control the accuracy. `n_eigenvalue_qubits` sets the size of the clock register, which determines how finely the spectrum is resolved. A finer resolution needs a larger circuit. The example above uses `3` qubits and gets within about `1 %` of the classical answer. `epsilon` is an eigenvalue cutoff. Eigenvalues below it are treated as noise and dropped, which prevents a nearly singular system from producing very large values. The evolution time `t` is chosen automatically from the spectrum, and we recommend keeping the automatic value. If `t` is too large, phase estimation aliases. The solver detects this and reports it, instead of returning a wrong answer.

A third argument, `mode`, chooses which circuit is built. The default, `mode="reference"`, computes the eigenvalues classically and applies `e^{iAt}` as one dense unitary. It is exact and fast, but its cost does not reflect how HHL scales. `mode="honest"` builds the same algorithm from elementary gates instead. The evolution is Trotterised from the Pauli decomposition of `A`, and the evolution time comes from a Gershgorin bound, so no circuit parameter depends on an eigendecomposition. This mode needs at least two clock qubits, and `trotter_steps` (`"auto"` by default), `trotter_order` and `trotter_tolerance` control the Trotter grid. The [HHL page](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md) compares the two modes, and the [worked example](https://docs.kit.eigenorbit.com/examples/hhl-honest/index.md) measures the cost of each one.

The result has six fields. `residual` is the best measure of the quality of a run. It is the relative error `‖Ax − b‖ / ‖b‖`, computed against the original system, so it measures the answer itself and not the algorithm's own estimate of it. `success_probability` matters for anything beyond a simulator. It is the probability that the ancilla lands in the useful branch, so its reciprocal is roughly the number of times a real device would have to repeat the state preparation. `readout_probability` is the narrower physical rate: the probability that both the ancilla and the clock register land where the solution is read, which is what an estimate from shots needs. `raw_solution` holds the post-selected amplitudes produced by the circuit, before the classical rescaling. It is the only part of the output that comes directly from the quantum computation.

The circuit reads the full statevector and post-selects, so the backend must provide a statevector. The numpy simulator, PennyLane `default.qubit` and `lightning.qubit`, and Qiskit Aer all do. Shot-based backends and quantum processing units (QPUs) raise `NotImplementedError`.

## IQAE: Amplitude estimation

`IQAE` estimates `a = |⟨ψ₁|A|0⟩|²`, the probability that a state-preparation circuit ends in the marked state. The estimate comes with a confidence interval, so you do not have to infer one from repeated runs.

The oracle is the state preparation `A` itself. It can be given as a `CircuitSpec` built with `CircuitBuilder`, or as a PennyLane callable `oracle(wires)`, which needs `provider="pennylane"`. **The last qubit is the ancilla.** The oracle must have no trainable weights and no inputs, and its number of qubits must equal `n_qubits`.

```python
import numpy as np
from eigenkit import CircuitBuilder
from eigenkit import IQAE

# RY(pi/3) on the ancilla prepares an amplitude of sin^2(pi/6) = 0.25 exactly.
qc = CircuitBuilder(n_qubits=2)
qc.ry(np.pi / 3, qubit=1)
oracle = qc.build()

result = IQAE(epsilon=0.02, alpha=0.05, n_qubits=2, random_state=0).solve(oracle)

print(result.estimation)  # 0.246
print(result.confidence_interval)  # (0.2407, 0.2513)  contains the true 0.25
print(result.n_rounds)  # 4
print(result.n_oracle_calls)  # 2600
```

This is the operator that IQAE repeats. One repetition applies the state preparation `A` (the oracle you pass, drawn as `CU` in the figure), then a phase flip on the marked state, then `A†`, then the diffusion operator. Each repetition rotates more amplitude onto the marked state. The adaptive rule of Grinko et al. decides how the number of repetitions grows from one round to the next, and `n_oracle_calls` counts the applications of `A` and `A†` over the whole run. The Hadamard layer at the start comes from Grover search, where `A` prepares the uniform superposition. In IQAE, the initial state is whatever your oracle prepares.

`epsilon`, `alpha` and `n_qubits` are required and have no default values. `epsilon` is the half-width of the interval that you are willing to accept, `alpha` is the probability that the interval is allowed to miss the true value, and `n_qubits` is the number of qubits of the oracle, including the ancilla.

Set `random_state` whenever a run must be repeatable, because the shot noise is seeded from it.

The result contains the estimate, its interval, and two cost counters. Use `n_oracle_calls` when comparing against classical sampling, because the advantage of the algorithm is its convergence rate: as `epsilon` gets smaller, the error decreases roughly as `1/n_oracle_calls`, compared with `1/√n` when drawing `n` classical samples.

## PortfolioRiskEstimator: VaR and CVaR

`PortfolioRiskEstimator` takes a price history, fits a loss distribution to it, and reports the Value at Risk (VaR) and the Conditional Value at Risk (CVaR). The tail probability behind these numbers is estimated with `IQAE`, which is the quantum part of the method. By default, VaR and CVaR are computed classically from the fitted bins. A quantum bisection, described below, is optional.

Not financial advice

The estimates come from a model fitted to the price history you pass in, and they carry no guarantee. They are not investment advice, and they are not meant to be the only basis of a decision about risk.

`fit` takes `X` of shape `(n_timesteps, n_assets)`, containing closing prices and not returns. Portfolio weights are optional, and all assets are weighted equally by default.

```python
import numpy as np
from eigenkit import PortfolioRiskEstimator

rng = np.random.default_rng(0)
returns = rng.normal(0.0005, 0.02, size=(250, 3))
prices = 100 * np.exp(np.cumsum(returns, axis=0))

estimator = PortfolioRiskEstimator(n_qubits=5, provider="numpy", random_state=0)
estimator.fit(prices, weights=np.array([0.5, 0.3, 0.2]))

risk = estimator.estimate(confidence_level=0.95)

print(risk.var)  # 0.0237   loses at most 2.37% on 95% of days
print(risk.cvar)  # 0.0262   average loss on the worst 5%
print(risk.tail_probability)  # 0.0659   IQAE estimate of the tail mass
```

`n_qubits` sets the resolution. One qubit is the ancilla and the others discretise the loss distribution, so `5` qubits give `16` bins. With coarse bins, the VaR changes in visible steps. If the result looks like a round number, increase `n_qubits` first.

Two arguments decide how much of the computation is quantum. With the default `n_bisection_steps=0`, VaR and CVaR are the classical values from the bins, and only the tail probability is estimated on the circuit. A positive value runs a binary search that calls IQAE at every step to find the VaR threshold. `measure_tail_probability` controls the final IQAE call. Turn it off when you only need the classical values and do not want to run the circuit at all.

The result reports `var` and `cvar` as fractions of the portfolio value, with the estimated `tail_probability` and a confidence interval for it. The interval applies to the tail probability, not to the VaR. The nested `iqae_result` contains the underlying IQAE estimate, and is `None` when the final IQAE call was skipped.

`var_at()` is the array version of `estimate()`. It takes a list of confidence levels and returns the VaR at each one. For the fit above, `var_at([0.90, 0.95, 0.99])` returns `[0.0144, 0.0237, 0.0330]`, and the middle value matches `estimate(0.95).var`.

The method is not called `predict()`, on purpose. In scikit-learn, `predict` takes samples from the same feature space that `fit` received, and this method takes confidence levels instead. Calling `predict()` raises an error that points to `var_at()`.

A confidence level below `0.5` is valid (the VaR at `0.5` is the median loss), but such values also look like a series of returns or normalised features, so `var_at()` issues a warning in that case. Passing such an array is the only mistake that this method does not reject with an error. A price series is rejected because prices are not in `(0, 1)`, and a price matrix is rejected because it is not 1-D.

## API reference

### `eigenkit.algorithms`

General-purpose quantum algorithms.

This package contains three algorithms:

- HHL solves linear systems with the algorithm of Harrow, Hassidim and Lloyd (HHL).
- IQAE runs Iterative Quantum Amplitude Estimation (IQAE).
- PortfolioRiskEstimator estimates the Value at Risk (VaR) and the Conditional Value at Risk (CVaR) of a portfolio with IQAE.

`HHL` and `IQAE` inherit from BaseQuantumSolver, the base class for solvers that work in a single call and keep no fitted state. The Quantum Approximate Optimization Algorithm (`QAOA`) and the Variational Quantum Eigensolver (`VQE`) also inherit from it, in their own packages. The base class is defined in `core` because `QAOA` and `VQE` depend only on `core`. `PortfolioRiskEstimator` does not inherit from it, by design. It is a scikit-learn-style estimator, registered in the estimator registry of `eigenkit.qml` so that it can be saved and loaded. It therefore needs scikit-learn and is imported on first use; `HHL` and `IQAE` need only the core install.

#### `HHL`

Bases: `BaseQuantumSolver`

Quantum linear solver based on the algorithm of Harrow, Hassidim and Lloyd (HHL).

Solves `Ax = b` for any invertible square matrix `A`. If `A` is Hermitian, it is used directly, even when it is not positive semidefinite. Any other matrix is converted to the normal equations `A†Ax = A†b`, which have the same solution and a Hermitian, positive semidefinite system matrix. The cost of this conversion is that the condition number `κ` is squared.

The circuit itself inverts the eigenvalues. It applies controlled `RY` rotations to the ancilla, with one fixed angle for each state of the clock register, so the reciprocal `1/λ` of each eigenvalue is encoded in the amplitudes without any classical division by the eigenvalues. The readout is different: it uses the simulator's full statevector and keeps only the part where the ancilla is in `|1⟩` and the clock register is in `|0⟩`. HHL produces the quantum state `|x⟩`, so reading a classical vector from it is only possible on a simulator. See HHLResult.

Warning

The backend must provide `statevector()` on its compiled circuit. The built-in numpy simulator, PennyLane `default.qubit` and `lightning.qubit`, and Qiskit Aer all do. A custom backend registered with register() must implement it, otherwise `solve()` raises `NotImplementedError`.

**Scope and scaling.** HHL is efficient only when three conditions hold: `A` is a sum of a few short Pauli terms, `A` is well conditioned, and the state `|b⟩` can be prepared efficiently.

- *Hamiltonian simulation.* `e^{iAt}` is Trotterised from the Pauli decomposition of `A`, so its cost follows the number of Pauli terms. This means sparsity in the Pauli basis, which is different from sparsity of the matrix. The module `algorithms/_hamiltonian_sim.py` explains what this means in practice. In short, a sparse matrix is not necessarily cheap to simulate here.
- *State preparation.* `|b⟩` is prepared with AmplitudeEncoding, which is treated as an oracle of unit cost. Loading general amplitudes costs `O(N)` gates, so a real speedup assumes that `b` comes from a formula, from an earlier quantum routine, or from a quantum random-access memory (qRAM).
- *Conditioning.* The condition number `κ` is never an input. It only bounds the achievable success probability and the size of the clock register.
- *Inversion angles.* The gates are elementary (see `_uniformly_controlled_ry()`), so the circuit built in `"honest"` mode contains no unitary-matrix gate. However, the `2^n_l` angles, where `n_l` is the number of clock qubits, are listed one by one instead of being computed by the circuit. The classical description of the circuit therefore has size `Θ(2^n_l)`. The circuit can be built from elementary gates, but its description is not compact.

Solvers for dense matrices, for example the one of Wossnig, Zhao and Prakash, reach a quadratic speedup when the spectral norm is bounded. They need a specialised memory architecture that this package does not provide.

**Real and complex inputs.** For real `A` and `b`, the readout discards the imaginary part. The leading error of the Trotter eigenvectors enters the raw vector as a purely imaginary component, so discarding the imaginary part removes it. This matters at `trotter_order=1`, where it improves the convergence by one order: the measured error falls as `steps⁻²` for real inputs, and as `steps⁻¹` for a complex Hermitian `A` or a complex `b`, which keep that error. At `trotter_order=2`, the default, the first-order commutator already cancels by construction, so there is nothing left to remove, and real and complex inputs both converge as `steps⁻²`. This advantage was not designed. It depends on the test for real inputs in solve(), so a change to that test changes the convergence rate at order 1.

Parameters:

| Name                  | Type           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Default                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `n_eigenvalue_qubits` | `int`          | The number of qubits in the clock register. Eigenvalues are resolved into 2\*\*n_eigenvalue_qubits buckets. The value must be at least 1, and at least 2 in "honest" mode. More qubits give a finer resolution of the spectrum and a larger circuit. The default is 4.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `4`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `t`                   | \`float        | None\`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | The evolution time for U = e^{iAt}. If given, it must be positive and finite. If None, it is chosen automatically: from the spectrum in "reference" mode, and from a Gershgorin bound in "honest" mode. The default is None.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `epsilon`             | `float`        | The relative eigenvalue cutoff, in \[0, 1). For a Hermitian A, buckets whose estimated eigenvalue is below epsilon * λ_max are treated as zero. For the normal equations, the cutoff applies to the singular values instead, and it is implemented as epsilon² on the squared spectrum. If no eigenvalue is above the cutoff, solve() raises an error. The default is 0.01.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `0.01`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `provider`            | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. It must provide statevector(). The default is "numpy".                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `'numpy'`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `mode`                | `str`          | How the circuit is built. This argument is keyword-only. "reference" (the default): the eigenvalues are computed classically with eigvalsh, e^{iAt} is a dense QubitUnitary computed with expm, and t is tuned to the known spectrum. This mode is fast and exact, but the O(N³) precomputation and the dense unitary mean that its cost does not follow the scaling of HHL. "honest": the circuit is built as it would be on hardware. No eigendecomposition sets any circuit parameter: t comes from a Gershgorin bound, the scaling constant C of the inversion comes from the resolution of the clock register (see \_honest_qpe_params()), and e^{iAt} is Trotterised into elementary gates. This mode requires n_eigenvalue_qubits >= 2. Input validation still runs an O(N³) rank check. This check makes the solver safer to use and is not part of the algorithm; see solve(). | `'reference'`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `trotter_steps`       | \`int          | Literal['auto']\`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | The number of Trotter steps for the base evolution time in "honest" mode. It is ignored in "reference" mode. With "auto", the default, the solver picks the smallest number of steps for which a computable bound on the Trotter drift ‖A_eff − A‖ stays below trotter_tolerance × C, which is below the eigenvalue resolution of the clock register. This matters because, with a fixed step size, the circuit is exact HHL for an effective matrix A_eff whose eigenvectors are rotated even when its eigenvalues are exact. That changes the direction of the answer while every check based on the spectrum reports success. An integer turns off the automatic choice, and the solver warns if the drift bound then exceeds C/2. Above that value, an extreme eigenvalue can be captured by the Nyquist bucket or wrap around the branch cut. The automatic choice is limited by a gate budget, and the solver says so when the limit is reached. The default is "auto". |
| `trotter_order`       | `int`          | The Trotter order, 1 or 2. The default is 2. The symmetric product of order 2 is a palindrome, so the first-order commutator cancels, the error falls from O(τ) to O(τ²), and A_eff stays real for a real A. At order 1, A_eff picks up Y-type terms, and a real system returns a complex state. Order 1 is kept for cost comparisons.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `2`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `trotter_tolerance`   | `float`        | The target for trotter_steps="auto". This argument is keyword-only. The drift bound must not exceed trotter_tolerance × C. The default, 0.25, keeps the drift at a quarter of a bucket, below the C/2 threshold at which the Nyquist bucket can capture an eigenvalue. At order 2, the cost grows as steps ∝ 1/√tolerance.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `_AUTO_TROTTER_TOLERANCE`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |

Raises:

| Type         | Description                                                                                                |
| ------------ | ---------------------------------------------------------------------------------------------------------- |
| `ValueError` | If n_eigenvalue_qubits is below 1, if t is not a positive finite number, or if epsilon is outside \[0, 1). |

Example

This example solves a `2 × 2` Hermitian system:

```python
import numpy as np
from eigenkit import HHL

A = np.array([[2, 1], [1, 2]], dtype=float)
b = np.array([1, 0], dtype=float)
result = HHL(n_eigenvalue_qubits=6).solve(A, b)
print(result.solution)
print(result.residual)
```

This example solves the same system on a faster simulator:

```python
from eigenkit import HHL, PennyLaneConfig

solver = HHL(
    n_eigenvalue_qubits=6,
    provider=PennyLaneConfig(device_name="lightning.qubit"),
)
result = solver.solve(A, b)
```

##### `solve(A, b)`

Solve the linear system `Ax = b`.

Parameters:

| Name | Type      | Description                                                                                                             | Default    |
| ---- | --------- | ----------------------------------------------------------------------------------------------------------------------- | ---------- |
| `A`  | `ndarray` | A real or complex square matrix, which must be invertible. A non-Hermitian matrix is converted to the normal equations. | *required* |
| `b`  | `ndarray` | The right-hand side, of length A.shape[0].                                                                              | *required* |

Returns:

| Type        | Description                                                                                                                                             |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HHLResult` | An HHLResult. Its solution is read from the simulator's statevector and completed with a classical least-squares rescaling. See the notes on HHLResult. |

Raises:

| Type                  | Description                                                                                                                                        |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ValueError`          | If A is not square or is singular, if b has the wrong length, is zero or is not finite, or if t causes aliasing in quantum phase estimation (QPE). |
| `NotImplementedError` | If the backend does not implement statevector(), or if it returns something else, such as a density matrix.                                        |

Note

Validation runs one `O(N³)` singular value decomposition (SVD) to reject a singular `A` and to compute a condition number for the warnings. No quantity derived from it reaches `t`, `C`, the rotation angles or the number of Trotter steps, and changing it would leave the circuit exactly the same. `mode="honest"` therefore keeps its defining property, but `solve()` itself still does `O(N³)` work.

#### `HHLResult`

Bases: `ValueEquality`

Output of HHL.solve().

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 arrays it holds. To get a writable copy of an 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                                                                                                                                             |
| --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `solution`            | `ndarray` | The best-fit x with Ax ≈ b. It is float64 for real inputs, and complex128 when A or b has a non-zero imaginary part.                                    |
| `euclidean_norm`      | `float`   | The norm ‖solution‖ of the returned solution. It is computed after the classical least-squares rescaling, so it is not a norm that HHL itself measures. |
| `residual`            | `float`   | The relative error ‖Ax − b‖ / ‖b‖, computed with the original A and b.                                                                                  |
| `success_probability` | `float`   | The probability that the ancilla is in                                                                                                                  |
| `readout_probability` | `float`   | The probability of the joint event from which the solution is read: the ancilla in                                                                      |
| `raw_solution`        | \`ndarray | None\`                                                                                                                                                  |

Note

HHL produces the quantum state `|x⟩`, not a vector of numbers. The full array in this result is read from the simulator's statevector, which only a simulator can provide. On hardware, only expectation values can be measured, and extracting all `N` components of `|x⟩` costs `O(N)` measurements, which cancels the speedup. In practice, `|x⟩` is passed to another quantum routine, or used to estimate one observable. Recovering the sign of `1/λ` needs a separate protocol. All of this applies to both values of `mode`.

Warning

`solution` is completed by two classical steps that hardware cannot perform. For real inputs, the imaginary part is discarded. The magnitude and the sign then come from a least-squares projection, `x = raw · ⟨A·raw, b⟩ / ‖A·raw‖²`, which uses the full classical `A` and `b`. Only the direction of `raw` comes from the quantum computation. The rescaling can change `residual` by an order of magnitude, and for an indefinite `A` its sign correction is not reliable, because `⟨Ax, b⟩ ≥ 0` does not imply `⟨x, A⁻¹b⟩ ≥ 0`.

##### `__post_init__()`

Make the array fields 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.solution)`.

Both array fields are frozen. `raw_solution` is optional, and `freeze_arrays` would turn a `None` into a zero-dimensional object array, so it is only frozen when the run recorded one.

#### `IQAE`

Bases: `BaseQuantumSolver`

Iterative Quantum Amplitude Estimation (IQAE).

Estimates `a = |⟨ψ₁|A|0⟩|²` to an additive precision `epsilon`, with a failure probability of at most `alpha`. It uses the Grover operator `Q = A S₀ A† S_ψ₁`, built from the oracle `A` that you provide.

The oracle can be given in two forms:

- A **PennyLane callable** `oracle(wires)`, which applies the circuit `A` in place. The last wire (`wires[-1]`) must be the ancilla qubit:

```python
def my_oracle(wires):
    qml.RY(theta, wires=wires[0])
    qml.CNOT(wires=[wires[0], wires[-1]])
```

The matrix of the circuit is extracted internally with `qml.matrix`, so this form requires `provider="pennylane"` and the `pennylane` extra.

- A CircuitSpec, which works on any backend that supports the `QubitUnitary`, `X`, `MCZ` and `Z` gates. With a provider other than PennyLane, pass `shot_mode="synthetic"` or `shot_mode="auto"`, because `shot_mode="provider"` works only with PennyLane.

Parameters:

| Name           | Type           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Default                                                                                                                                         |
| -------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `epsilon`      | `float`        | The additive precision to reach, that is, the half-width of the final confidence interval for a. It must be in (0, π/8).                                                                                                                                                                                                                                                                                                                                                                                                                                                          | *required*                                                                                                                                      |
| `alpha`        | `float`        | The total failure probability. The algorithm applies a Bonferroni correction, so the confidence interval of each round is valid at level alpha / T, where T = ceil(log2(π / (8 * epsilon))) is the maximum number of rounds. It must be in (0, 1).                                                                                                                                                                                                                                                                                                                                | *required*                                                                                                                                      |
| `n_qubits`     | `int`          | The total number of qubits, including the ancilla.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | *required*                                                                                                                                      |
| `provider`     | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. The default is "numpy", the built-in simulator.                                                                                                                                                                                                                                                                                                                                                                                                         | `'numpy'`                                                                                                                                       |
| `n_shots`      | `int`          | The number of measurement shots per round. With shot_mode="synthetic", which is used on analytic backends, it sets the size of a seeded binomial draw on top of the exact probability of the ancilla. With shot_mode="provider" or "auto", it is the number of samples per round on the hardware or simulator, unless the provider config sets its own shots, which takes precedence. For example, a device configured for 50 shots samples 50 times, whatever the value of n_shots. To control sampling on those backends, set shots on the provider config. The default is 100. | `100`                                                                                                                                           |
| `shot_mode`    | `ShotMode`     | How the ancilla measurements are obtained. With "auto", the default, the provider's shot count is used when the backend config sets a finite shots, and synthetic binomial noise is used otherwise. "provider" always samples from the backend. "synthetic" always uses exact expectation values plus seeded binomial noise.                                                                                                                                                                                                                                                      | `'auto'`                                                                                                                                        |
| `min_ratio`    | `float`        | The minimum ratio K\_{i+1} / K_i between the Grover powers of consecutive rounds. It must be greater than 1.                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `2.0`                                                                                                                                           |
| `random_state` | \`int          | None\`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | The seed of the random number generator used for the shot noise. An integer gives reproducible results, and None uses a non-deterministic seed. |

Example

```python
import numpy as np
from eigenkit import CircuitBuilder
from eigenkit.algorithms import IQAE

# RY(pi/3) on the ancilla prepares an amplitude of sin^2(pi/6) = 0.25.
qc = CircuitBuilder(n_qubits=2)
qc.ry(np.pi / 3, qubit=1)
oracle = qc.build()

iqae = IQAE(epsilon=0.02, alpha=0.05, n_qubits=2, random_state=0)
result = iqae.solve(oracle)

print(result.estimation)           # 0.246
print(result.confidence_interval)  # (0.2407, 0.2513), which contains 0.25
print(result.n_oracle_calls)       # 2600
```

##### `solve(oracle)`

Estimate the amplitude `a = |⟨ψ₁|A|0⟩|²`.

Parameters:

| Name     | Type       | Description   | Default                                                                                                                                                                                      |
| -------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `oracle` | \`Callable | CircuitSpec\` | Either a PennyLane callable oracle(wires) that builds the state-preparation circuit A, or a CircuitSpec produced by CircuitBuilder. In both cases, the last qubit is treated as the ancilla. |

Returns:

| Type         | Description                                                                             |
| ------------ | --------------------------------------------------------------------------------------- |
| `IQAEResult` | An IQAEResult with the point estimate, the confidence interval and the resource counts. |

Raises:

| Type         | Description                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------------- |
| `ValueError` | If the selected provider does not support the gates that IQAE needs (QubitUnitary, X, MCZ and Z). |

#### `IQAEResult`

Bases: `ValueEquality`

Outcome of a completed Iterative Quantum Amplitude Estimation (IQAE) run.

The result is read-only. Assigning to a field raises `FrozenInstanceError`, so a mistyped `=` (instead of `==`) cannot change a result. This type holds no arrays, so nothing else needs protecting.

Results compare by value. They are not hashable, so that all result types behave in the same way: the types that hold arrays cannot be hashed both correctly and efficiently.

Attributes:

| Name                  | Type                  | Description                                                                                                                                                                                                                                                                  |
| --------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `estimation`          | `float`               | The point estimate of the amplitude a.                                                                                                                                                                                                                                       |
| `confidence_interval` | `tuple[float, float]` | The (lower, upper) Clopper-Pearson interval at the requested confidence level 1 - alpha.                                                                                                                                                                                     |
| `n_rounds`            | `int`                 | The number of rounds that the run executed. It can also be read as n_iterations, the name that the other result types use.                                                                                                                                                   |
| `n_oracle_calls`      | `int`                 | The total number of oracle calls over all rounds.                                                                                                                                                                                                                            |
| `success`             | `bool`                | Whether the interval reached the requested precision. This is the same test that ends the loop over rounds, and the one reported by the warning that the run did not converge. False means that the run stopped at max_rounds with an interval still wider than 2 * epsilon. |

##### `n_iterations`

The number of rounds, under the name that the other results use.

`n_rounds` is the name that IQAE uses, and it stays available. This property lets one loop read the number of iterations from any result.

Returns:

| Type  | Description                                 |
| ----- | ------------------------------------------- |
| `int` | The number of rounds that the run executed. |

#### `PortfolioRiskEstimator`

Bases: `BaseQuantumEstimator`

Quantum estimator of portfolio risk.

It estimates the Value at Risk (VaR) and the Conditional Value at Risk (CVaR) of a portfolio. The estimator can use IQAE to estimate the tail probability on a quantum circuit. The oracle for IQAE is built from the fitted return distribution with amplitude encoding.

Note

This is not a standard scikit-learn regressor. The risk at a confidence level comes from `estimate()` and `var_at()`, which take confidence levels and not feature vectors, and the estimator has no meaningful scikit-learn `score()`. It inherits only from BaseQuantumEstimator.

Not financial advice

The estimates come from a model fitted to the price history you pass in, and they carry no guarantee. They are not investment advice.

Parameters:

| Name                       | Type           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Default                                       |
| -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `n_qubits`                 | `int`          | The total number of qubits, including the ancilla. The loss distribution is discretised into 2^(n_qubits - 1) bins. The minimum is 2: one data qubit and the ancilla.                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `6`                                           |
| `epsilon`                  | `float`        | The additive precision of the IQAE estimate of the tail probability.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `0.01`                                        |
| `alpha`                    | `float`        | The failure probability of IQAE. The confidence level is 1 − alpha.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `0.05`                                        |
| `horizon_days`             | `int`          | The investment horizon, in trading days, used to scale daily log-returns to the target horizon. The default is 1, which gives daily risk measures.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `1`                                           |
| `provider`                 | `ProviderSpec` | The backend that IQAE uses: a registered name ("pennylane", "qiskit", "braket" and so on), a config instance, or a backend object. The default is "numpy". Note The Grover operator built internally uses the adjoint of an AmplitudeEncoding block, so the provider must support the adjoint of a state preparation and provide a statevector. The built-in "numpy" simulator and PennyLane both do, and their results agree to the last digit. A backend that does not raises an error when the operator is built. Calling IQAE directly with a CircuitSpec based on QubitUnitary avoids this requirement.                    | `'numpy'`                                     |
| `n_shots`                  | `int`          | The number of shots per IQAE round. The default is 100.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `100`                                         |
| `random_state`             | \`int          | None\`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | The seed that makes the results reproducible. |
| `n_bisection_steps`        | `int`          | The number of binary-search steps used to locate the VaR with quantum estimates of the tail probability. With 0, the default, the VaR and CVaR are computed classically from the fitted bins. A positive value calls \_quantum_var_bisection(), which refines the threshold by running IQAE at each step of the bisection.                                                                                                                                                                                                                                                                                                      | `0`                                           |
| `measure_tail_probability` | `bool`         | When True, the default, estimate() runs IQAE once after the VaR and CVaR are computed, and reports the quantum estimate of the tail in tail_probability and confidence_interval. Set it to False to skip that final IQAE call when only the classical VaR and CVaR are needed and n_bisection_steps=0.                                                                                                                                                                                                                                                                                                                          | `True`                                        |
| `distribution`             | `str`          | The model of the loss distribution that fit() uses. "gaussian" (the default) fits the mean μ and the standard deviation σ of the scaled log-returns, and integrates the Gaussian cumulative distribution function (CDF) over each bin. The distribution is truncated at ±6σ. A VaR beyond that point is capped at the limit, and a RuntimeWarning is issued. "empirical" places each historical return in the nearest bin of equal width, without assuming a parametric form. "t" fits a Student-t distribution by maximum-likelihood estimation, and integrates its CDF over each bin. It has heavier tails than the Gaussian. | `'gaussian'`                                  |

Example

```python
import numpy as np
from eigenkit.algorithms import PortfolioRiskEstimator

rng = np.random.default_rng(0)
returns = rng.normal(0.0005, 0.02, size=(250, 3))
prices = 100 * np.exp(np.cumsum(returns, axis=0))

estimator = PortfolioRiskEstimator(
    n_qubits=5, provider="numpy", random_state=0
)
estimator.fit(prices, weights=np.array([0.5, 0.3, 0.2]))

risk = estimator.estimate(confidence_level=0.95)

print(risk.var)               # 0.0237: at most a 2.37% loss on 95% of days
print(risk.cvar)              # 0.0262: the average loss on the worst 5%
print(risk.tail_probability)  # 0.0659: the IQAE estimate of the tail mass
```

##### `distribution_params`

Summary statistics of the fitted return distribution.

The value is `None` if `fit()` has not been called. Otherwise it always contains `"mu"` (the mean) and `"sigma"` (the standard deviation) of the scaled log-returns, whatever the `distribution` model. For `"t"`, the degrees of freedom are not stored here, because they are an intermediate result of the maximum-likelihood fit and not part of the state of the estimator.

##### `is_fitted`

Whether `fit()` has completed successfully.

##### `estimate(confidence_level=0.95, *, _seed_seq=None)`

Estimate the VaR and CVaR at `confidence_level`.

The VaR and CVaR come from the discretised loss distribution. When `n_bisection_steps > 0`, the VaR is refined by a bisection that uses IQAE. Otherwise, the classical quantiles of the bins are used. Unless `measure_tail_probability=False`, a final IQAE run estimates `P(loss ≥ VaR)`, for checking and diagnostics.

Parameters:

| Name               | Type           | Description                                                                                          | Default                                                                                                                                                                                                        |
| ------------------ | -------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `confidence_level` | `float`        | The probability level of the risk measures, for example 0.95 for the 95 % VaR. It must be in (0, 1). | `0.95`                                                                                                                                                                                                         |
| `_seed_seq`        | \`SeedSequence | None\`                                                                                               | A private override of the random seed, used by var_at() so that the shot noise of the IQAE runs for different confidence levels is independent. The default is None, which derives the seed from random_state. |

Returns:

| Type                  | Description            |
| --------------------- | ---------------------- |
| `PortfolioRiskResult` | A PortfolioRiskResult. |

Raises:

| Type           | Description                                                                      |
| -------------- | -------------------------------------------------------------------------------- |
| `RuntimeError` | If fit() has not been called.                                                    |
| `ValueError`   | If the selected provider cannot compile the oracle for the tail that IQAE needs. |

##### `fit(X, y=None, *, weights=None)`

Fit the return distribution to historical price data.

Parameters:

| Name      | Type      | Description                                                                                                                                                        | Default                                                                                                           |
| --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `X`       | `ndarray` | A 2-D array of shape (T, n_assets) with daily closing prices, where T must be at least 2. A 1-D array of length T is accepted for a portfolio with a single asset. | *required*                                                                                                        |
| `y`       | \`ndarray | None\`                                                                                                                                                             | Ignored, because this estimator is unsupervised. It is present for compatibility with the scikit-learn interface. |
| `weights` | \`ndarray | None\`                                                                                                                                                             | A 1-D array of portfolio weights that sum to 1. If it is not given, all assets have equal weights.                |

Raises:

| Type         | Description                                                                                                                            |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `ValueError` | If distribution is not one of the supported values. This can happen when distribution is changed with set_params() after construction. |

##### `predict(X)`

Not supported. Use `var_at()` instead.

The estimator base class requires a `predict` method. In scikit-learn, `predict` takes samples from the same feature space that `fit` received, but the risk at a confidence level is a different question, which `var_at()` answers.

Parameters:

| Name | Type      | Description | Default    |
| ---- | --------- | ----------- | ---------- |
| `X`  | `ndarray` | Ignored.    | *required* |

Raises:

| Type                  | Description                             |
| --------------------- | --------------------------------------- |
| `NotImplementedError` | Always. The message points to var_at(). |

##### `var_at(confidence_levels)`

Return the VaR at each confidence level.

This is the array version of `estimate()`. It runs IQAE once for each level and returns only the `var` of each result.

Parameters:

| Name                | Type      | Description                                                                                                                                      | Default    |
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `confidence_levels` | `ndarray` | A 1-D array of confidence levels in (0, 1), for example [0.95, 0.99]. A scalar or an (n, 1) column is also accepted, and it is read as n levels. | *required* |

Returns:

| Type      | Description                                                                                 |
| --------- | ------------------------------------------------------------------------------------------- |
| `ndarray` | An array of VaR values, as fractions of the portfolio value lost, with one value per level. |

Raises:

| Type             | Description                                                   |
| ---------------- | ------------------------------------------------------------- |
| `ValueError`     | If the argument is not 1-D after a single column is squeezed. |
| `NotFittedError` | If fit() has not been called.                                 |

Warns:

| Type          | Description                                                                                                                                                                                       |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UserWarning` | If any level is below 0.5. Such a level is valid, but values below 0.5 are also what a price series or a feature matrix looks like, and this warning catches the mistake of passing one of those. |

Warning

Each level runs IQAE once, so `n` levels need `n` separate quantum circuits. For a single risk measure, call `estimate()` instead.

Example

```python
estimator.var_at([0.90, 0.95, 0.99])
```

#### `PortfolioRiskResult`

Bases: `ValueEquality`

Outcome of a PortfolioRiskEstimator run.

The result is read-only. Assigning to a field raises `FrozenInstanceError`, so a mistyped `=` (instead of `==`) cannot change a result. This type holds no arrays, so nothing else needs protecting.

Results compare by value. They are not hashable, so that all result types behave in the same way: the types that hold arrays cannot be hashed both correctly and efficiently.

Attributes:

| Name                  | Type                  | Description                                                                                                                                   |
| --------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `var`                 | `float`               | The Value at Risk (VaR) at the requested confidence_level, as a fraction of the portfolio value lost. For example, 0.12 means a loss of 12 %. |
| `cvar`                | `float`               | The Conditional Value at Risk (CVaR), also called Expected Shortfall, at the same confidence level.                                           |
| `tail_probability`    | `float`               | The IQAE point estimate of P(loss ≥ VaR).                                                                                                     |
| `confidence_interval` | `tuple[float, float]` | The (lower, upper) Clopper-Pearson interval for the tail probability, at level 1 - alpha.                                                     |
| `iqae_result`         | \`IQAEResult          | None\`                                                                                                                                        |
