algorithms
eigenkit.algorithms.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.
eigenkit.algorithms.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.
eigenkit.algorithms.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 ( |
eigenkit.algorithms.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. |
eigenkit.algorithms.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
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 |
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.
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 |
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. |
save(path)
Save a fitted model to a .eigenkit bundle.
The bundles of models that hold scikit-learn objects (QSVC, QSVR, the
reservoir estimators and the time-series estimators) contain joblib
pickles, so loading one can run code. Share them only with people who
trust you.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
The path of the bundle. The suffix |
required |
load(path, *, provider=None)
classmethod
Load a fitted model from a .eigenkit bundle.
Load only bundles that you trust. A bundle can contain joblib pickles, and
unpickling runs code, as with torch.load or joblib.load.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
The path of the bundle. |
required |
provider
|
ProviderSpec | None
|
A provider that replaces the saved one. |
None
|
Returns:
| Type | Description |
|---|---|
'BaseQuantumEstimator'
|
The loaded estimator. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the method is called on a subclass and the bundle holds
a different class. |
eigenkit.algorithms.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 |