Linear Algebra Algorithms
Overview
The unitarylab_algorithms.linear_algebra package provides quantum algorithms for linear algebra tasks, including the Quantum Fourier Transform, linear system solving, non-unitary operator implementation, signal processing, and variational methods.
| Algorithm | Class | Task |
|---|---|---|
| QFT | QFTAlgorithm | Quantum Fourier Transform |
| HHL | HHLAlgorithm | Solve the linear system (phase-estimation approach) |
| LCU | LCUAlgorithm | Implement |
| QSP | QSPAlgorithm | QSP-based polynomial transformation |
| QSVT-QLSA | QSVTLinearSolverAlgorithm | QSVT-based linear solver |
| VQLS | VQLSAlgorithm | Variational quantum linear solver (NISQ) |
| AQC | AQCAlgorithm | Discrete adiabatic quantum linear solver (QLSP) |
Return Values and Error Handling
When an algorithm completes normally, it returns status, circuit_path, plot, circuit, and algorithm-specific results. If conditions such as matrix dimension, Hermiticity, or post-selection are not satisfied, a ValueError or RuntimeError is raised, which the caller can catch as needed. Numerical quality should be judged in combination with the error, residual, fidelity, or optimizer results.
Quantum Fourier Transform (QFT)
Background
The Quantum Fourier Transform is the quantum analogue of the discrete Fourier transform, mapping the computational basis state to:
QFT is a subroutine of Shor’s algorithm, QPE, HHL, and many other algorithms. This module also supports the inverse QFT (inverse=True).
Import
from unitarylab_algorithms import QFTAlgorithm.run() Parameters
def run(self, n: int, state: np.ndarray = None, inverse: bool = False,
backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]| Parameter | Type | Default | Description |
|---|---|---|---|
n | int | — (required) | Number of qubits |
state | np.ndarray | None | None | Initial state vector (defaults to ` |
inverse | bool | False | Whether to perform the inverse QFT |
Return Value
The actual output fields (merged into the final return dictionary):
{
"status": "ok",
"circuit_path": "....svg", # save_circuit(qc.decompose()), i.e. the fully decomposed circuit diagram
"plot": [{"format": "txt", "filename": "quantum_fourier_transform_algorithm_result.txt"}], # from save_txt(), a result text file rather than a circuit diagram
"circuit": <Circuit>,
"Final state": ..., # the final state vector obtained from simulation
"Expected state": ..., # the theoretical expected state computed via numpy's fft/ifft
"Verification error": ..., # the L2-norm error between the two (for logging/reference only, does not affect status)
"Computation time (s)": ...
}Example
import numpy as np
from unitarylab_algorithms import QFTAlgorithm
algo = QFTAlgorithm()
result = algo.run(n=4)
print(result['status']) # 'ok'
# Inverse QFT
result_inv = algo.run(n=4, inverse=True)Quick Demo
from unitarylab_algorithms.linear_algebra.qft.algorithm import test
test(n=4)Notes
- The
statusfield is always'ok'; theVerification erroris merely a comparison against the classicalnumpy.fft/numpy.ifftcomputation and does not affectstatus. - The saved circuit diagram uses the circuit after
qc.decompose()(all sub-gates are expanded and displayed), whereas thecircuitfield in the returned dictionary is the undecomposed original circuit object — the two have different structures.
HHL Algorithm for Solving Linear Systems
Background
The Harrow–Hassidim–Lloyd (HHL) algorithm solves the linear system with a time complexity of , where is the condition number of and is the matrix dimension. The optimal classical algorithm is .
The algorithm uses QPE to encode the eigenvalues of into a phase register, then applies a controlled rotation to compute the reciprocal of each eigenvalue, then un-computes the phase register, and finally performs post-selection on the ancilla qubit and the phase register to extract the solution vector.
Requirements:
- must be a Hermitian square matrix, and its dimension must be a power of 2
- cannot be an all-zero vector
- The eigenvalues of cannot all be approximately zero at the same time
Import
from unitarylab_algorithms import HHLAlgorithm.run() Parameters
def run(self, A: np.ndarray, b: np.ndarray, d: int,
backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]| Parameter | Type | Default | Description |
|---|---|---|---|
A | np.ndarray | — (required) | Hermitian coefficient matrix, dimension must be |
b | np.ndarray | — (required) | Right-hand side vector |
d | int | — (required) | Number of qubits in the phase register (precision ); recommended range in the web interface is [7, 15] |
The evolution time , the phase-grid starting point k_start, and whether to enable “signed phase mode” (automatically turned on when contains negative eigenvalues), among others, are all automatically determined by the algorithm based on the eigenspectrum of , and are not parameters the user can pass in.
Return Value
{
"status": "ok",
"circuit_path": "....svg",
"plot": [...],
"circuit": <Circuit>,
"Estimated solution (quantum)": ..., # the solution vector obtained via quantum post-selection
"Exact solution (classical)": ..., # the classical solution from np.linalg.solve(A, b)
"L2 error": ..., # the L2-norm difference between the two
"Post-selection probability": ..., # the post-selection success probability P(anc=1, phase=|0>)
"Computation time (s)": ...
}Example
import numpy as np
from unitarylab_algorithms import HHLAlgorithm
A = np.array([[0.8, 0], [0, 0.4]])
b = np.array([1, 2])
algo = HHLAlgorithm()
result = algo.run(A=A, b=b, d=11)
print(result['status'])Quick Demo
from unitarylab_algorithms.linear_algebra.hhl.algorithm import test
test(A=[[0.8, 0], [0, 0.4]], b=[1, 2], d=11)Notes
- A larger
dyields higher precision but significantly increases circuit depth; ifdis too small, the algorithm directly raises aValueError(e.g. “Phase bits d=… insufficient to resolve minimum phase”), rather than returning a failure status. - If the post-selection success probability is zero (i.e. the branch where
ancilla=1and the phase register is|0...0>never occurs), aValueError("Post-selection probability is zero...")is raised. - It is strongly recommended to use matrices with a small condition number (well-conditioned) for experimentation.
Linear Combination of Unitaries (LCU) Algorithm
Background
LCU implements the non-unitary operator by encoding the coefficients into an ancilla “prepare” register (number of qubits , where is the number of terms), and conditionally applying the unitaries on that register. LCU is a core primitive for Hamiltonian simulation, QSVT, and block encoding.
Import
from unitarylab_algorithms import LCUAlgorithm.run() Parameters
def run(self, alphas: List[float], unitaries: List[Circuit], n_sys: int,
initial_state: Circuit = None,
backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]| Parameter | Type | Default | Description |
|---|---|---|---|
alphas | List[float] | — (required) | Non-negative coefficient list , length must match unitaries |
unitaries | List[Circuit] | — (required) | List of unitary circuits |
n_sys | int | — (required) | Number of qubits in the system register |
initial_state | Circuit | None | None | Circuit that prepares the initial state of the system (optional; the number of qubits must equal n_sys, otherwise a ValueError is raised) |
Return Value
{
"status": "ok",
"circuit_path": "....svg",
"plot": [...],
"circuit": <Circuit>,
"Success probability": ..., # the probability that all ancilla qubits are measured as |0...0>
"Computation time (s)": ...,
"Result state": ... # the (unnormalized) state slice of the system register in the branch where the ancilla is |0...0>
}Example
from unitarylab_algorithms import LCUAlgorithm
# Build the unitary circuits (e.g. Pauli operators), then pass them in together with the coefficients
algo = LCUAlgorithm()
result = algo.run(alphas=[0.6, 0.4], unitaries=[U_I, U_X], n_sys=1)
print(result['status'])Quick Demo
from unitarylab_algorithms.linear_algebra.lcu.algorithm import test
test(n=1, alphas=[0.6, 0.4], paulis=['I', 'X'])test() automatically constructs the corresponding unitary circuits based on paulis (a list of Pauli strings, e.g. 'IX', 'ZZ'): when a coefficient is negative, its absolute value is taken and a global phase gate U.gp(np.angle(-1)) (representing the sign flip) is appended so that the term is correctly represented, rather than being skipped; only terms whose coefficient is exactly 0 are dropped outright and excluded from the circuit. The paulis parameter also supports being passed as a string separated by the half-width comma ,, the full-width (Chinese) comma ,, the half-width semicolon ;, or the full-width (Chinese) semicolon ; (format_pauli_list automatically converts it into a list); the current implementation does not support the Chinese enumeration comma 、.
Notes
is_success = success_prob > 1e-6is the success criterion actually computed internally in the source code; it is used only for log printing (“Valid” / “Very low success rate…”), and is not reflected in thestatusfield of the returned dictionary.- The number of qubits in the ancilla register is automatically determined by
n_anc = ceil(log2(m))(wheremis the length ofalphas), and does not need to be specified by the user.
Quantum Signal Processing (QSP, Linear Algebra Version)
Background
linear_algebra.qsp.QSPAlgorithm uses classical optimization (scipy.optimize.minimize, L-BFGS-B) to search for a set of phase sequences , and implements a polynomial approximation of for a given eigenvalue using a single-qubit circuit (alternately applying the signal rotation and the phase rotation ); it is a subroutine of QSVT and other advanced algorithms.
Naming Ambiguity Note: This module and
hamiltonian_simulation.qsp.QSPHSAlgorithmare two completely different classes — both source directories are namedqsp/, but they serve different purposes: this module (linear_algebra.qsp.QSPAlgorithm) only performs a approximation for a scalar eigenvalue on a single qubit, and is used to demonstrate/verify the QSP phase-finding procedure itself;hamiltonian_simulation.qsp.QSPHSAlgorithm, on the other hand, performs genuine time-evolution simulation for an arbitrary Hermitian matrix . Be sure to double-check the import path.
Import
from unitarylab_algorithms import QSPAlgorithm.run() Parameters
def run(self, t: float, d: int, x: float = 0.5,
backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]| Parameter | Type | Default | Description |
|---|---|---|---|
t | float | — (required) | Target evolution parameter; recommended range in the web interface is [0.1, 10.0] |
d | int | — (required) | Polynomial (phase) order; recommended range in the web interface is [5, 50] |
x | float | 0.5 | Test eigenvalue, must lie within [-1, 1] |
Return Value
{
"status": "ok",
"circuit_path": "....svg",
"plot": [...],
"circuit": <Circuit>,
"Estimated value": ..., # the complex amplitude obtained from circuit simulation
"Ideal value": ..., # the theoretical value np.cos(t * x)
"Absolute error": ...,
"Computation time (s)": ...
}Example
from unitarylab_algorithms import QSPAlgorithm
algo = QSPAlgorithm()
result = algo.run(t=1.0, d=10, x=0.5)
print(result['status'])Quick Demo
from unitarylab_algorithms.linear_algebra.qsp.algorithm import test
test(t=1.0, d=10, x=0.5)Notes
- The initial guess for the phase sequence search is
np.random.randn(d+1) * 0.1, and no random seed is set, so repeated calls with the same(t, d, x)may converge to different phase solutions and yield differentAbsolute errorvalues (the results are not fully reproducible); this is the same category of cause as the non-reproducibility issue with qDrift in the Hamiltonian simulation package. - L-BFGS-B is a local optimizer with no multi-start retry mechanism; when
dis small ortis large, it may converge to a poor local optimum, causingAbsolute errorto be large, thoughstatusis still always'ok'.
QSVT-Based Linear System Solver Algorithm
Background
Quantum Singular Value Transformation (QSVT) generalizes QSP to arbitrary matrices via block encoding, enabling polynomial transformations of singular values. The QSVT linear solver uses this framework to implement the matrix inverse function on the singular values of , solving with near-optimal query complexity.
Import
from unitarylab_algorithms import QSVTLinearSolverAlgorithm.run() Parameters
def run(self, A, b, epsilon,
backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]| Parameter | Type | Default | Description |
|---|---|---|---|
A | np.ndarray | — (required) | Coefficient matrix |
b | np.ndarray | — (required) | Right-hand side vector |
epsilon | float | — (required) | Target approximation accuracy; recommended range in the web interface is [1e-10, 1.0] |
Return Value
{
"status": "ok",
"circuit_path": "....svg",
"plot": [...],
"circuit": <Circuit>,
"Solution vector": ..., # the solution vector
"Scaling factor applied": ..., # the scaling factor applied internally by QSVTSolver
"Simulation time (s)": ...
}Example
import numpy as np
from unitarylab_algorithms import QSVTLinearSolverAlgorithm
A = np.array([[0.8, 0.1], [0.1, 0.6]])
b = np.array([1.0, 0.5])
algo = QSVTLinearSolverAlgorithm()
result = algo.run(A=A, b=b, epsilon=1e-3)
print(result['status'])Quick Demo
from unitarylab_algorithms.linear_algebra.qsvt_qlsa.algorithm import test
test(A=[[0.8, 0], [0, 0.4]], b=[1., 2.], epsilon=0.01)Notes
- The core logic of this algorithm (matrix validity checks, block encoding, QSVT phase angle solving) is fully delegated to the library’s internal
unitarylab.library.linear_solver.QSVTSolver;QSVTLinearSolverAlgorithm.run()itself is merely a thin wrapper and performs no explicit validation of A/b at the Python layer (e.g. whether they are Hermitian, whether dimensions match, etc.) — any validation failure surfaces as an exception raised internally byQSVTSolver. - The module-level
test()function has no independent docstring.
Variational Quantum Linear Solver (VQLS)
Background
VQLS is a variational algorithm suited for near-term quantum devices (NISQ), used to solve the linear system : it parameterizes the solution state with a hardware-efficient ansatz (RY-RZ rotations plus a ring of entangling CNOT layers), and iteratively optimizes the parameters by minimizing a residual-based cost function. The coefficient matrix is automatically Pauli-decomposed internally via unitarylab.library.pauli_operator.pauli_string_decomposition (), so the user does not need to supply the decomposition manually.
Import
from unitarylab_algorithms import VQLSAlgorithm.run() Parameters
def run(self, A: np.ndarray, b: np.ndarray, cost_function: str = "local_ht",
n_layers: int = 4, maxiter: int = 500, tol: float = 1e-6,
seed: int = 42, epsilon: Optional[float] = None,
backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]| Parameter | Type | Default | Description |
|---|---|---|---|
A | np.ndarray | — (required) | coefficient matrix |
b | np.ndarray | — (required) | -dimensional right-hand side vector |
cost_function | str | "local_ht" | "local_ht" (Hadamard-test circuit, slowest but closest to a real hardware path) / "local_classical" (classical matrix computation, fast) / "global" ($C_G = 1- |
n_layers | int | 4 | Number of ansatz layers (each layer is RY+RZ+a ring of CNOTs) |
maxiter | int | 500 | Maximum number of COBYLA iterations |
tol | float | 1e-6 | COBYLA convergence tolerance |
seed | int | 42 | Random seed for the initial parameters |
epsilon | float | None | None | Target solution error threshold; when set, stops early once (effective only for local_ht/local_classical; ignored in global mode) |
Return Value
{
"status": "ok",
"circuit_path": "....svg",
"plot": [...],
"circuit": <Circuit>,
"Fidelity": ..., # fidelity between the quantum solution and the classical solution; None if A is singular
"Ax Fidelity": ..., # fidelity between the normalized A|x_quantum> and |b>
"Cost Function": "local_ht" | "local_classical" | "global",
"Condition Number": ..., # np.linalg.cond(A)
"Solution State (Quantum)": ...,
"Solution State (Classical)": ..., # may be None (when A is singular)
"Computation Time (s)": ...,
"Cost History": [...], # the history of every cost-function evaluation
"Early Stopped": True | False
}Example
import numpy as np
from unitarylab_algorithms import VQLSAlgorithm
A = np.array([[1.5, 0.2], [0.2, 1.8]])
b = np.array([1.0, 0.5])
algo = VQLSAlgorithm()
result = algo.run(A=A, b=b, cost_function="local_ht", n_layers=4, maxiter=500)
print(result['status']) # always 'ok'Quick Demo
from unitarylab_algorithms.linear_algebra.vqls.algorithm import test
test(cost_function="local_ht", n_layers=4, maxiter=500, tol=1e-6, seed=42)
# When A and b are omitted, they default to [[1.5, 0.2], [0.2, 1.8]] and [1.0, 0.5] respectivelyNotes
- The optimizer’s convergence status is written to the saved result text; API callers can also combine
Fidelity,Cost History, andEarly Stoppedto judge the quality of the solution. - The web interface parameter names differ from the Python parameter names: the UI parameter names exposed in
parameters.jsonaremax_iterationsandtolerance, but the actual keyword arguments of.run()on the Python side aremaxiterandtol, respectively. - The web interface is missing the
A/b/seed/epsilonparameters: unlike HHL and QSVT-QLSA, VQLS’sparameters.jsonexposes only the four itemscost_function,n_layers,max_iterations, andtolerance, and does not provide input fields for the matrixA, the vectorb, the random seedseed, or the early-stopping thresholdepsilon. This means running VQLS via the current web interface uses the default built-in 2x2 example problem in the code, and the user cannot customize the linear system to be solved through the interface. - The
parameters.jsondefault value forn_layersis2, which does not match the Python.run()/test()default value of4. - When
Ais a singular matrix (np.linalg.solveraisesLinAlgError), bothFidelityandSolution State (Classical)returnNonerather than raising an error, and the algorithm still continues to output the quantum-side results. - The early-stopping mechanism has a known limitation: the source code comments explicitly note that COBYLA is implemented in Fortran, so a Python exception cannot actually interrupt its internal loop from within the cost-function callback. As a result, setting
_early_stop_flaginside_cost_with_stopdoes not cause COBYLA to exit immediately — it merely records the flag early for a later check, and the actual optimization may still keep running untilmaxiter(this behavior is acknowledged in the source code comments as a known design limitation).
Discrete Adiabatic Quantum Linear Solver (AQC)
Background
AQCAlgorithm implements the discrete adiabatic quantum linear system solver (QLSP): via Trotterized discrete adiabatic evolution, it solves on system qubits and 5 ancilla qubits. The algorithm performs an SVD block encoding of the coefficient matrix (normalized so that , then embedded into a unitary matrix), constructs the unitary that prepares using a Householder reflection, and then evolves step by step for steps according to the adiabatic schedule parameter , finally performing post-selection on the ancilla register to extract the solution vector.
This algorithm does not accept a user-supplied or : unlike the other solvers in this package (HHL, QSVT-QLSA, VQLS),
AQCAlgorithm.run()does not exposeA/bparameters at all — at runtime it first executesnp.random.seed(42), and then automatically generates a well-conditioned Hermitian matrixA = randn(N,N)(symmetrized and then with10·Iadded, to ensure diagonal dominance and a small condition number), together with a random right-hand-side vectorb = randn(N). Because the seed is fixed, theAandbgenerated for a givennare exactly the same on every run (reproducible), but the user cannot use it to solve a linear system of their own choosing — it can only be used to demonstrate/benchmark the discrete adiabatic solving procedure itself.
Import
from unitarylab_algorithms import AQCAlgorithm.run() Parameters
def run(self, n: int = 2, T: int = 0, p: float = 1.4,
backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]| Parameter | Type | Default | Description |
|---|---|---|---|
n | int | 2 | Number of system qubits (recommended 1–6); matrix dimension ; total number of qubits is (plus 5 fixed ancilla qubits) |
T | int | 0 | Number of adiabatic evolution steps; 0 means automatic selection, taking (rounded up to an even number), where is the condition number of the (internally generated) |
p | float | 1.4 | Adiabatic schedule parameter, must be > 1; recommended range in the web interface is [1.01, 5.0] |
Return Value
General case ():
{
"status": "ok",
"circuit_path": ["....svg", "....svg"], # [full circuit diagram, single-step slice circuit diagram] (isomorphic to the Trotter algorithm)
"plot": [...],
"circuit": <Circuit>,
"Quantum Solution (x)": ...,
"Classical Solution": ..., # the classical solution from np.linalg.solve(A, b)
"Residual Norm ||Ax-b||": ...,
"Error vs Classical (L2)": ...,
"Internal Scale Factor": ...,
"Post-selection Amplitude": ...,
"Simulation Time (s)": ...,
"Elapsed Time (s)": ...
}When the internally generated happens to have condition number (i.e. it is approximately a scalar multiple of the identity matrix), the algorithm takes a trivial fast path: it directly sets x = b (rescaled appropriately), skipping the quantum circuit evolution entirely. In this case, the structure of the returned dictionary differs slightly — it lacks the two entries Post-selection Amplitude and Simulation Time (s), and circuit_path is a single string rather than a list of length 2 (because only the initialization circuit, without the adiabatic evolution steps, is saved).
Example
from unitarylab_algorithms import AQCAlgorithm
algo = AQCAlgorithm()
result = algo.run(n=2, T=0, p=1.4) # T=0 → automatically choose the number of steps based on the condition number
print(result['status'])Quick Demo
from unitarylab_algorithms.linear_algebra.aqc.algorithm import test
test(n=2, T=0, p=1.4)Notes
- Post-selection projects onto the
|10000>branch of the ancilla register (4 qubits at0, 1 qubit at1); if the amplitude of this branch tends to zero, aRuntimeError("Post-selection returned a near-zero state...")is raised, suggesting an increase inTor indicating that the system is ill-conditioned. - When
Tis not specified (i.e.T=0), it is automatically set to and rounded to be even; if aTexplicitly passed in is odd, it will not be automatically adjusted. - Circuit size grows linearly with
T(11 sub-operations per step); when the full circuit is quite deep, the single-step slice diagramaqc_one_step.svgcan be viewed to understand the structure of a single step. - While saving the circuit diagram, the source code temporarily redirects
sys.stdoutto an in-memory buffer (to suppress noisy output during plotting), restoring it once complete — this is a normal implementation detail.
Version Reference
To import multiple algorithm classes from this module at once, you can batch-import directly from the top-level package:
from unitarylab_algorithms import (
HHLAlgorithm, LCUAlgorithm, QFTAlgorithm, QSPAlgorithm,
QSVTLinearSolverAlgorithm, VQLSAlgorithm, AQCAlgorithm,
)