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).
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 at the end of this page lists the full signatures
and defaults, with notes on every argument.
| Solver | Answers | Result carries |
|---|---|---|
HHL |
the solution x of Ax = b |
solution, euclidean_norm, residual, success_probability, readout_probability, raw_solution |
IQAE |
an unknown amplitude, with an interval | estimation, confidence_interval, n_rounds, n_oracle_calls |
PortfolioRiskEstimator |
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 and IQAE 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) in optimization, and the Variational Quantum
Eigensolver (VQE) in chemistry. The base class is defined in core and not in algorithms,
because QAOA and VQE depend only on core.
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.
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 compares the two
modes, and the worked example 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.
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.
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:
HHLsolves linear systems with the algorithm of Harrow, Hassidim and Lloyd (HHL).IQAEruns Iterative Quantum Amplitude Estimation (IQAE).PortfolioRiskEstimatorestimates 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 ofA, 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 modulealgorithms/_hamiltonian_sim.pyexplains what this means in practice. In short, a sparse matrix is not necessarily cheap to simulate here. - State preparation.
|b⟩is prepared withAmplitudeEncoding, which is treated as an oracle of unit cost. Loading general amplitudes costsO(N)gates, so a real speedup assumes thatbcomes 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, the2^n_langles, wheren_lis 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 |
4
|
t
|
float | None
|
The evolution time for |
None
|
epsilon
|
float
|
The relative eigenvalue cutoff, in |
0.01
|
provider
|
ProviderSpec
|
The backend to use: a registered name ( |
'numpy'
|
mode
|
str
|
How the circuit is built. This argument is keyword-only.
|
'reference'
|
trotter_steps
|
int | Literal['auto']
|
The number of Trotter steps for the base evolution time in
|
'auto'
|
trotter_order
|
int
|
The Trotter order, 1 or 2. The default is |
2
|
trotter_tolerance
|
float
|
The target for |
_AUTO_TROTTER_TOLERANCE
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Example
This example solves a 2 × 2 Hermitian system:
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:
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 |
required |
Returns:
| Type | Description |
|---|---|
HHLResult
|
An |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If the backend does not implement |
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
dataclass
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 |
euclidean_norm |
float
|
The norm |
residual |
float
|
The relative error |
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
|
The post-selected amplitudes as the circuit produced them. They are complex, and they come before the imaginary part is dropped and before the rescaling, so this is the only part of the output that comes directly from the quantum computation. The vector has unit norm, except when the size of the system is not a power of two. In that case it is a slice of a unit-norm state, and the Trotterised evolution leaks a small amount of weight into the zero padding. |
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 circuitAin place. The last wire (wires[-1]) must be the ancilla qubit:
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 theQubitUnitary,X,MCZandZgates. With a provider other than PennyLane, passshot_mode="synthetic"orshot_mode="auto", becauseshot_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 |
required |
alpha
|
float
|
The total failure probability. The algorithm applies a Bonferroni
correction, so the confidence interval of each round is valid at level
|
required |
n_qubits
|
int
|
The total number of qubits, including the ancilla. |
required |
provider
|
ProviderSpec
|
The backend to use: a registered name ( |
'numpy'
|
n_shots
|
int
|
The number of measurement shots per round. With
|
100
|
shot_mode
|
ShotMode
|
How the ancilla measurements are obtained. With |
'auto'
|
min_ratio
|
float
|
The minimum ratio |
2.0
|
random_state
|
int | None
|
The seed of the random number generator used for the shot
noise. An integer gives reproducible results, and |
42
|
Example
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 |
required |
Returns:
| Type | Description |
|---|---|
IQAEResult
|
An |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the selected provider does not support the gates that IQAE
needs ( |
IQAEResult
dataclass
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 |
confidence_interval |
tuple[float, float]
|
The |
n_rounds |
int
|
The number of rounds that the run executed. It can also be read
as |
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. |
n_iterations
property
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 |
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
|
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
|
provider
|
ProviderSpec
|
The backend that Note The Grover operator built internally uses the adjoint of an
|
'numpy'
|
n_shots
|
int
|
The number of shots per IQAE round. The default is |
100
|
random_state
|
int | None
|
The seed that makes the results reproducible. |
42
|
n_bisection_steps
|
int
|
The number of binary-search steps used to locate the
VaR with quantum estimates of the tail probability. With |
0
|
measure_tail_probability
|
bool
|
When |
True
|
distribution
|
str
|
The model of the loss distribution that
|
'gaussian'
|
Example
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
property
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
property
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
|
_seed_seq
|
SeedSequence | None
|
A private override of the random seed, used by |
None
|
Returns:
| Type | Description |
|---|---|
PortfolioRiskResult
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
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 |
required |
y
|
ndarray | None
|
Ignored, because this estimator is unsupervised. It is present for compatibility with the scikit-learn interface. |
None
|
weights
|
ndarray | None
|
A 1-D array of portfolio weights that sum to 1. If it is not given, all assets have equal weights. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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(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 |
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 |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If any level is below |
Warning
Each level runs IQAE once, so n levels need n separate quantum
circuits. For a single risk measure, call estimate() instead.
PortfolioRiskResult
dataclass
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 |
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 |
confidence_interval |
tuple[float, float]
|
The |
iqae_result |
IQAEResult | None
|
The full |