Use this file to discover all available pages before exploring further.
View on GitHub
Open this notebook in GitHub to run it yourself
The HHL algorithm[1], named after Harrow, Hassidim and Lloyd, is a fundamental quantum algorithm designed to solve a set of linear equations:
Ax=b,where A is an N×N matrix, and x and b are vectors of size N=2n.The algorithm prepares a quantum state ∣x⟩ that encodes the solution vector proportional to A−1b, starting from the input state b. It achieves this by applying Quantum Phase Estimation (QPE) to extract the eigenvalues of A, then performing a controlled rotation that effectively inverts these eigenvalues.The resulting amplitudes represent the solution state, which can be used to estimate properties of x efficiently for certain classes of matrices.
The algorithm treats the problem in the following way:
Input: A Hermitian matrix A of size 2n×2n, normalized vector b, ∣b∣=1 and given precision m.
Promise:A is Hermitian, sparse and well-conditioned. In addition, the eigenvalues of A lie between 1/κ and 1, where κ is the condition number of A. The entries of vector b can be loaded efficiently to a quantum state ∣b⟩.
Output: Solution vector encoded in the state ∣x⟩=∣A−1b⟩=∑j=02n−1λ~jβj∣uj⟩mem, where: λj are eigenphases of a unitary U=e2πiA, estimated up to m binary digits and βj are coefficients in expansion of initial state ∣b⟩ into linear combination of eigenvectors of A, such that ∣b⟩=∑j=02n−1βj∣uj⟩mem. The solution vector allows one to efficiently estimate xTMx for a Hermitian matrix M.
Complexity: HHL can find a quantum representation of the solution in polylogarithmic time, with a runtime of roughly
O(logNκ2/ϵ),where N=2n, and ϵ is the desired precision. In comparison, the best classical method requires polynomial time O(κN) (or O(κN) for positive definite matrices). Therefore, the quantum algorithm apparently represents an exponential speedup in problem size; however, this speedup depends on strong assumptions: A must be sparse, well-conditioned, and efficiently simulatable. Specifically, κ and 1/ϵ should be O(polylog(N)) (i.e., upper bounded by a polynomial expression with argument log(N)). Moreover, ∣b⟩ should be loaded efficiently, and our goal is either to only estimate observables like quantities of the form xTMx, only sample x or utilize x for further matrix computations [6].
Keywords: Fundamental quantum algorithm, Exponential speedup, Linear equation solver
Solving linear equations appears in many research, engineering, and design fields.For example, many physical and financial models, from fluid dynamics to portfolio optimization, are described by partial differential equations, which are typically treated by numerical schemes, most of which are eventually transformed to a set of linear equations.For simplicity, the demo below treats a use case where the matrix A has eigenvalues in the interval (0,1) and ∣b∣=1.Generalizations to other use cases, including the case where ∣b∣=1 and A is not a Hermitian matrix or not of size 2n×2n, are discussed at the end of this notebook.The discussion begins with a concise overview of the algorithm, followed by its definition and implementation using Classiq’s built-in functions. We conclude by comparing the exact implementation of the algorithm with an approximate method, which is often more practical for real-world systems requiring extensive computational resources.
Three quantum variables, coined here a “memory”, “estimator” and indicator are initialized with n, m, and 1 qubits respectively and the vector b is encoded into the memory variable:
∣0⟩mem∣0⟩est∣0⟩indSP∣b⟩mem∣0⟩est∣0⟩ind=j=0∑2n−1βj∣uj⟩mem∣0⟩est∣0⟩ind,where βj are the coefficients of b in the eigenbasis of A, and ∣uj⟩ eigenstates of U=exp(i2πA).
Quantum Phase Estimation is applied to the initial state,
QPEj=0∑2n−1βj∣uj⟩mem∣λ~j⟩est∣0⟩ind,where λ~j are the m bit approximations of the associated phases.This step can be achieved with roughly O(κ2s/ϵ) queries to A, where s is the sparsity of A, i.e., each row of A has at most s non-zero entries.
Controlled rotations are applied to indicator bit ∣0⟩ind with normalized constant C=2m1,
import numpy as np# Define matrix AA = np.array( [ [0.28, -0.01, 0.02, -0.1], [-0.01, 0.5, -0.22, -0.07], [0.02, -0.22, 0.43, -0.05], [-0.1, -0.07, -0.05, 0.42], ])# Define RHS vector bb = np.array([1, 2, 4, 3])# Normalize vector bb = b / np.linalg.norm(b)print("A =", A, "\n")print("b =", b)# Verify if the matrix is symmetric and has eigenvalues in (0,1)if not np.allclose(A, A.T, rtol=1e-6, atol=1e-6): raise Exception("The matrix is not symmetric")w, v = np.linalg.eig(A)for lam in w: if lam < 0 or lam > 1: raise Exception("Eigenvalues are not in (0,1)")# Binary representation of eigenvalues (classically calculated)m = 32 # Precision of a binary representation, e.g. 32 binary digitssign = lambda num: "-" if num < 0 else "" # Calculate sign of a numberbinary = lambda fraction: str( np.binary_repr(int(np.abs(fraction) * 2 ** (m))).zfill(m)).rstrip( "0") # Binary representation of a fractionprint()print("Eigenvalues:")for eig in sorted(w): print(f"{sign(eig)}0.{binary(eig.real)} =~ {eig.real}")
We next represent the Hamiltonian into the Pauli operators.This will allow evaluating U=ei2πA, which is utilized in the Quantum Phase Estimation (QPE) stage and consequently, in the associated classiq function code block.Having the matrix or the Hamiltonian defined, the unitary operator can be created with the qpe_flexible function by passing in the unitary_with_power argument a callable function that applies a unitary operation raised to a given power.The built-in function matrix_to_pauli_operator encodes the matrix A into a sum of Pauli strings, which then allows to encode the unitary matrix U with a product formula (Suzuki-Trotter).This is a classical pre-processing that can be achieved by various decomposition methods, for example, you can compare with method described in [2].The number of qubits is stored in the variable n.
from classiq import *hamiltonian = matrix_to_pauli_operator(A)n = hamiltonian.num_qubitsprint(f"Hamiltonian: {hamiltonian}")print("\nNumber of qubits for matrix representation =", n)
Output:
Hamiltonian: 0.4075*Pauli.I(0)*Pauli.I(1) + -0.05249999999999999*Pauli.Z(0)*Pauli.I(1) + -0.017499999999999988*Pauli.I(0)*Pauli.Z(1) + -0.057499999999999996*Pauli.Z(0)*Pauli.Z(1) + -0.030000000000000002*Pauli.X(0)*Pauli.I(1) + 0.02*Pauli.X(0)*Pauli.Z(1) + -0.025*Pauli.I(0)*Pauli.X(1) + 0.045000000000000005*Pauli.Z(0)*Pauli.X(1) + -0.16*Pauli.X(0)*Pauli.X(1) + -0.06*Pauli.Y(0)*Pauli.Y(1) Number of qubits for matrix representation = 2
The first step of the HHL algorithm involves loading the elements of the normalized RHS vector b, into a quantum variable:∣0⟩memSPi=0∑2n−1bi∣i⟩mem,where ∣i⟩ are the states in the computational basis.Below, we define the quantum function load_b using the built-in function prepare_amplitudes.This function loads the 2n elements of amplitudes, which correspond to the entries of the vector b, into the amplitudes of the state in the output variable memory.
The hhl function performs steps 2-4 of the algorithm.Steps 2 and 4 of the algorithm are achieved by applying the within_apply construct, where the within part corresponds to a flexible quantum phase estimation qpe_flexible.The apply involves the controlled rotations of step 3, which are performed with assign_amplitude_table function.The built in function qpe_flexible is given a function unitary_with_power which indicates how the powers U2k are performed within the phase estimation procedure. In the following implementation, the powers are performed by Hamiltonian evolution, utilizing the hamiltonian_evolution_with_power function.The normalization coefficient C ensures the amplitudes are normalized.Since eigenvalues of A are normalized to be between 1/κ and 1, the eigenvalues of the inverse matrix are between κ=1/λmin and 1. However, the amplitudes cannot surpass unity; we therefore choose C as a lower bound of the minimum eigenvalue C=1/2m.Arguments:
unitary_with_power: A callable function that applies a unitary operation raised to a power k.
precision: An integer representing the precision of the phase estimation process, (number of estimator qubits =m).
memory: An array representing the initial quantum state ∣b⟩ on which the matrix inversion is to be performed.
estimator: Stores the estimation of the phase.
indicator: An auxiliary qubit that stores the result of the inversion.
from classiq.qmod.symbolic import floor, log# Parameters for the initial state preparationamplitudes = b.tolist()# Parameters for the QPEprecision = 4@qfuncdef hhl( rhs_vector: CArray[CReal], precision: int, hamiltonian_evolution_with_power: QCallable[CInt, QArray], memory: Output[QArray], estimator: Output[QNum], indicator: Output[QBit],): # Allocate a quantum number for the phase with given precision allocate(precision, UNSIGNED, precision, estimator) # Prepare initial state load_b(amplitudes=amplitudes, memory=memory) # Allocate indicator allocate(indicator) # Perform quantum phase estimation and eigenvalue inversion within a quantum operation within_apply( lambda: qpe_flexible( unitary_with_power=lambda k: hamiltonian_evolution_with_power(k, memory), phase=estimator, ), lambda: assign_amplitude_table( lookup_table( lambda p: 0 if p == 0 else (1 / 2**estimator.size) / p, estimator ), estimator, indicator, ), )
We prepare for the quantum program execution stage by providing execution details and constructing a representation of HHL model from a function defined in Quantum Modelling Language Qmod.The resulting circuit is executed on a statevector simulator.For other available Classiq simulated backends see Execution on Classiq simulators.To define this part of the model, we set the backend preferences by placing ClassiqBackendPreferences with the backend name SIMULATOR_STATEVECTOR into the ExecutionPreferences.
After running the circuit a given number of times and collecting measurement results from the indicator and phase qubits we need to postprocess the data in order to find solution.Measurement values from circuit execution are denoted here by res_hhl.
If all eigenvalues are m-estimated (are represented by m binary digits), then the uncomputation by inverse-QPE creates the state:j=0∑2n−1βj∣uj⟩mem∣0⟩est(1−λ~j2C2∣0⟩ind+λ~jC∣1⟩ind)If there is an error in step 3 and at least one eigenvalue is not n-estimated, then after performing QPE†, the estimator variable does not reset to ∣0⟩est.This will further reduce the success probability ofIn the general case, we postselect only states that return 1 for indicator variable. In this tutorial, we will add another postselection condition on estimator variable, to return ∣0⟩est, in order to increase the accuracy of the solution result.Note: Postselection can improve HHL algorithm results at the cost of more executions. In the future, when two-way quantum computing is utilized, postselection might be replaced by adjoint-state preparation, as envisioned by Jarek Duda [4].The quantum_solution function defines a run over all the relevant strings holding the solution.The solution vector will be inserted into the variable qsol, after normalizing with C=1/2m.After the calculation we divide by C to obtain the elements of x.
To compare between the quantum and classical solutions we first need to isolate the global phase of the quantum solution.Let the outcome state be eiθ∣x⟩, satisfying A(eiθ∣x⟩)=eiθ∣b⟩. We can isolate the phase by taking the scalar product of Aeiθ∣x⟩ with ∣b⟩, i.e., ⟨b∣Aeiθx⟩=eiθ. We then evaluate θ, rotate by the appropriate angle, and take the real part.Note that only observables of the form ⟨x∣M∣x⟩ (which are independent of the global phase) are accessible by a quantum experiment.The global phase is purely a numerical inconvenience which arises in the comparison the enteries classical solution with the quantum amplitudes and has no physical meaning in the implementation of the algorithm on actual hardware.
import matplotlib.pyplot as pltdef quantum_solution_normalization(A, b, res_hhl, precision, disp=True): # Classical solution sol_classical = np.linalg.solve(A, b) if disp: print("Classical Solution: ", sol_classical) # Quantum solution with postselection size = len(b).bit_length() - 1 qsol = quantum_solution(res_hhl, size, precision) if disp: print("Quantum Solution: ", np.abs(qsol) / np.linalg.norm(qsol)) res = np.dot(A, qsol) global_phase = np.angle(np.vdot(res, b)) # Correct global phase and taking the real part qsol_corrected = np.real(np.exp(1j * global_phase) * qsol) return sol_classical, qsol_correcteddef show_solutions(A, b, res_hhl, precision, check=True, disp=True): # Classical solution and preprocessed quantum solution sol_classical, qsol_corrected = quantum_solution_normalization( A, b, res_hhl, QPE_SIZE, disp=disp ) # Verify is there is no functional error, which might come from changing endianness in Model or Execution if ( np.linalg.norm(sol_classical - qsol_corrected) / np.linalg.norm(sol_classical) > 0.1 and check ): raise Exception( "The HHL solution is too far from the classical one, please verify your algorithm" ) if disp: print("Corrected Quantum Solution: ", qsol_corrected) # Fidelity state_classical = sol_classical / np.linalg.norm(sol_classical) state_corrected = qsol_corrected / np.linalg.norm(qsol_corrected) fidelity = np.abs(np.dot(state_classical, state_corrected)) ** 2 print() print("Fidelity: ", f"{np.round(fidelity * 100,2)} %") if disp: plt.plot(sol_classical, "bo", label="Classical") plt.plot(qsol_corrected, "ro", label="HHL") plt.legend() plt.xlabel("$i$") plt.ylabel("$x_i$") plt.show()
In the upcoming sections, we will present two different examples of implementing Hamiltonian dynamics by defining the unitary operator using two methods: exact and approximated.
Example: Implementation of U with exact Hamiltonian Simulation
The phase estimation subroutine in the HHL algorithm involves a consecutive application of controlled powers of U gates, i.e, conditional C−U20,C−U21,…,C−U2m−1. In the present section, we implement these gates exactly and later compare the results to an approximate evaluation of these gates.While the implementation of exact Hamiltonian simulation provided here is not scalable for large systems due to the exponential growth in computational resources required, it serves as a valuable tool for debugging and studying small quantum systems.For sparse matrices, there is an efficient quantum algorithm for Hamiltonian simulation, see Ref. [7] and the technical notes section below.
The approximated Hamiltonian simulation is capable of handling larger and more complex systems, making it more suitable for real-world applications where exact solutions are computationally prohibitive.For the QPE we are going to use Classiq’s suzuki-trotter function of order one for Hamiltonian evolution e−iHt [3].This function is an approximated one, where its repetitions parameter controls its error.For a QPE algorithm with estimator size m a series of controlled-unitaries U2k with 0≤k≤n−1 are applied, for each of which we would like to pass a different repetitions parameter depth (to keep a roughly same error, the repetitions for approximating U=e2πi2kA is expected to be ∼2k times the repetitions of U=e2πiA). In Classiq this can be done by working with a qpe_flexible, and passing a “rule” for how to exponentiate each step within the QPE in repetitions parameter.
The parameters R0 and REPS_SCALING_FACTOR dictate the number of repititions in the Suzuki-Trotter approximation.The specific number of repititions depend on the Hamiltonian, and therefore, were chosen by trial and error.For other examples, one would need to use different values for these parameters, please compare with specific example in Flexible QPE tutorial.The relevant literature that discusses the errors of product formulas is available in Ref. [5].
We explored the HHL algorithm for solving linear systems using exact and approximated Hamiltonian simulations.The exact method, with a smaller circuit depth, is computationally less intensive but lacks scalability. In contrast, the approximated method, with a greater circuit depth, offers flexibility and can handle larger, more complex systems.This trade-off underscores the importance of selecting the appropriate method based on the problem’s size and complexity.
The condition on the sparsity of A arises from the following result, Ref. [7]: If A is a 2n×2n Hermitian s-spase matrix then one can implement an Hamiltonian simulation, e−iAt up to error ϵ in the spectral norm with
O(st∣∣A∣∣∞+loglog(1/ϵ)1/ϵ)queries to A and a total number of gates which is larger by a factor of n. Here, the infinity norm corresponds to the largest entry of A.
Before measurement we obtain the state
C∣A−1b⟩mem∣0⟩est∣1⟩ind+∣garbage⟩∣0⟩ind.Naively, one would require of order 1/C=O(2n) measurements to obtain a sample of the entries of ∣x⟩. However, utilizing quantum amplitude amplification one can obtain a quadratic speedup, sampling the state with an order of only O(2n/2) measurements.
We can replace the sparsity condition on A by any restriction allowing efficient Hamiltonian simulation.
The complexity of standard HHL algorithm is O(lognκ2s/ϵ).
The factor 1/ϵ has been improved by Childs et al. in [8] to log(κ/ϵ), and Ambainis introduced a generalization of amplitude amplification, which allows reducing the quadratic κ dependence to linear [9].
The use case treated above is a canonical one, assuming the following properties:
The RHS vector b is normalized.
The matrix A is an Hermitian one.
The matrix A is of size 2n×2n.
The eigenvalues of A are in the range (0,1).
However, any general problem that does not follow these conditions can be resolved as follows:
Normalize b and return the normalization factor
Symmetrize the problem as follows:
(0AAT0)(b0)=(0x).Note: This requires only a single additional qubit.
Complete the matrix dimension to the closest 2n with an identity matrix and the vector b will be completed with zeros.
(A00I)(b0)=(x0).
If the eigenvalues of A are in the range [−wmin,wmax] you can employ transformations to the exponentiated matrix that enters into the Hamiltonian simulation, and then undo them for extracting the results:
A~=wmin+wmaxA+wminI.The eigenvalues of this matrix lie in the interval [0,1), and are related to the eigenvalues of the original matrix viaλ=(wmin+wmax)λ~−wmin,with λ~ being an eigenvalue of A~ resulting from the QPE algorithm.This relation between eigenvalues is utilized in the pre and post-processing of the results or directly in the amplitude loading procedure.