Skip to content

Solving the Quantum Linear Systems Problem (QLSP) using AQC

View on GitHub Experiment in the IDE

Overview

Adiabatic Quantum Computing (AQC) leverages the adiabatic theorem to solve computational problems by gradually evolving a quantum system from an initial ground state to the ground state of a problem-specific Hamiltonian (see our AQC tutorial).

This tutorial focuses on applying the AQC approach to solve the Quantum Linear Systems Problem (QLSP), a cornerstone problem in quantum computing with significant applications in fields like machine learning, physics, and optimization.

Specifically, we aim to demonstrate how AQC can be utilized to approximate the solution to the QLSP [1]. This problem involves finding a quantum state that corresponds to the solution of a linear system of equations. The tutorial provides a structured overview of the QLSP, its mathematical formulation, and the steps needed to transform it into an eigenvalue problem, laying the foundation for solving it within the AQC framework.

The following demonstration we will follow the paper [1]. The notebook was written in collaboration with Prof. Lin Lin and Dr. Dong An, the authors of the paper.


1. Problem Statement

Given a Hermitian positive-definite matrix A and a vector |b, the goal is to approximate |x, the solution to the linear system A|x=|b, as a quantum state.

We are given:

  • Matrix ACN×N, an invertible Hermitian and positive-definite matrix with condition number κ and A2=1.

  • Vector |bCN, a normalized vector.

  • Target Error ϵ, specifying the desired accuracy.

The goal is to prepare a quantum state |xa, which is an ϵ-approximation of the normalized solution |x=A1|b/A1|b2. The approximation satisfies:

|xaxa||xx|2ϵ.

2. Transformation into AQC

The QLSP is converted into an equivalent eigenvalue problem to leverage quantum computation. This involves the following steps:

2.1 Constructing H0

Define:

H0=σxQb=$[0QbQb0]$,

where Qb=IN|bb| is a projection operator orthogonal to |b.

Key properties:

  • H0 is Hermitian.

  • The null space of H0: Null(H0)=span(|b~,|b¯), where

|b~=|0,b=[b0],|b¯=|1,b=[0b].

The null space of a matrix A is the set of all vectors x such that:

Ax=0.

With regards to eigenstates and eigenvalues: - The null space corresponds to the eigenspace of A associated with the eigenvalue 0. - Any vector in the null space is an eigenvector of A with eigenvalue 0.

2.2 Constructing H1

Define:

H1=σ+(AQb)+σ(QbA)=$[0AQbQbA0]$,

where σ±=12(σx±iσy).

Key properties:

  • If A|x|b, then QbA|x=Qb|b=0.

  • Null space of H1: Null(H1)=span(|x~,|b¯) , where

|x~=|0,x=[x0],

2.3 Adiabatic Interpolation

Construct an interpolation Hamiltonian:

H(f(s))=(1f(s))H0+f(s)H1,0s1,

where f(s) is a monotonic function mapping [0,1][0,1].

2.4 Spectral Gap

  • Qb is a projection operator, and the spectral gap between 0 and the rest of the eigenvalues of H0 is 1.

  • For H1, the gap between 0 and the rest of the eigenvalues is bounded from below by 1/κ. [1]

2.5 Adiabatic Evolution and Null Space

Notice that there is a degeneracy in the number of Null states (unlike the regular adiabatic algorithm usage where we are typically looking at a single grounde state)

Null(H1)=span(|x~,|b¯)

We also note that for any s, |b¯ is always in the null space of H(f(s)), i.e.,

|b¯Null(H(f(s))).

Therefore, there exist an additional statestate |x(s)~=|0|x(s), such that

Null(H(f(s)))={|x(s)~,|b¯}.

In particular:

  • At s=0, |x(0)~=|b~, the initial state.

  • At s=1, |x(1)~=|x~, the solution state.

Thus, |x(s)~ state represents the desired adiabatic path for the evolution [1].


3. AQC Approach to solve QLSP

The adiabatic quantum algorithm prepares the zero-energy state |x~ of H1 as follows:

  1. Initialize in the ground state of H0, i.e., |b~.

  2. Slowly evolve the system by varying f(s) from f(0)=0 to f(1)=1.

  3. At the end of the evolution, the system will approximate |x~, embedding |x - the solution of the QLSP.


4. Goals

  • Set up a QLSP example: Derive H0, H1 and define the interpolation Hamiltonian

  • Quantum Circuit Design: Implement Hamiltonian simulation for H(f(s)).

  • Evaluate results: Compare quantum simulation results with the numeric calculation.


Let’s begin with the mathematical setup and proceed to implementation!

Setting up a QLSP example where A is a 4x4 matrix:

Setting an example

For simplicity, we first assume A is Hermitian and positive definite

import numpy as np

# Define matrix A and vector b
A = np.array([[4, 1, 2, 0], [1, 3, 0, 1], [2, 0, 3, 1], [0, 1, 1, 2]])
b = np.array([12, 10, 17, 26])

As a purely mathematical pre-processing step, we will calculate the condition number k for A.

In practical scenarios, the condition number is often approximated or known beforehand based on external factors or prior knowledge.

import numpy as np


def compute_condition_number(A):
    """
    Computes the condition number (κ) of a matrix A.

    Parameters:
    A (numpy.ndarray): Input square matrix.

    Returns:
    float: The condition number of A.
    """
    try:
        # Compute the norm of A (2-norm, largest singular value)
        norm_A = np.linalg.norm(A, 2)

        # Compute the inverse of A
        A_inv = np.linalg.inv(A)

        # Compute the norm of A^-1 (2-norm)
        norm_A_inv = np.linalg.norm(A_inv, 2)

        # Compute the condition number κ
        condition_number = norm_A * norm_A_inv

        return condition_number
    except np.linalg.LinAlgError:
        return float("inf")  # Return infinity if the matrix is singular


condition_number = compute_condition_number(A)
print("Condition number:", condition_number)
Condition number: 14.472337634948623

Construct H0 and H1:

The setup_QLSP function prepares the necessary Hamiltonians and normalized components to solve the Quantum Linear Systems Problem (QLSP).

The built-in function matrix_to_hamiltonian, used within setup_QLSP function, encodes the hamitonian matrix into a sum of Pauli strings that will be used to exponentiate the hamiltonians with a product formula (Suzuki-Trotter) in the next step.

from classiq import *
from classiq.execution import *


def setup_QLSP(A, b):
    # Normalize A
    norm_A = np.linalg.norm(A, "fro")
    A_normalized = A / norm_A

    # Normalize vector b
    b_normalized = b / np.linalg.norm(b)

    # Create the outer product of b
    outer_product_b = np.outer(b_normalized, b_normalized)

    # Define the identity matrix I with the same size as b
    identity_matrix = np.eye(len(b))

    # Compute Qb = I - outer_product_b
    Qb = identity_matrix - outer_product_b

    # Define the Pauli-X (σx) and Pauli-Y (σy) matrices
    pauli_x = np.array([[0, 1], [1, 0]])

    pauli_y = np.array([[0, -1j], [1j, 0]])

    # Define Pauli plus and minus operators
    pauli_plus = 0.5 * (pauli_x + 1j * pauli_y)
    pauli_minus = 0.5 * (pauli_x - 1j * pauli_y)

    # Compute the tensor product of Pauli-X and Qb
    H0 = np.kron(pauli_x, Qb)

    # Compute A*Qb and Qb*A
    A_Qb = np.dot(A, Qb)
    Qb_A = np.dot(Qb, A)

    # Compute the tensor products
    tensor_plus = np.kron(pauli_plus, A_Qb)
    tensor_minus = np.kron(pauli_minus, Qb_A)

    # Define H1 as the sum of the two tensor products
    H1 = tensor_plus + tensor_minus

    HO_HAMILTONIAN = matrix_to_hamiltonian(H0)
    H1_HAMILTONIAN = matrix_to_hamiltonian(H1)

    return H0, H1, HO_HAMILTONIAN, H1_HAMILTONIAN, A_normalized, b_normalized


# Setup

H0, H1, HO_HAMILTONIAN, H1_HAMILTONIAN, A_normalized, b_normalized = setup_QLSP(A, b)

Define the interpolation Hamiltonian:

For the sake of simplicity, we will first use the "vanilla AQC" linear scheduling function f(s(t))=s(t)=t/T. to define the time-dependent interpolated Hamiltonian, where T is the total evolution time.

As t progresses from 0 to T, f(s) satisfies the [0,1][0,1] mapping.

# Define the time-dependent Interpolated Hamiltonian, where T is the total evolution time


def hamiltonian_t(H0, H1, t, T):
    s = t / T
    return ((1 - s) * H0) + (s * H1)

Spectral gap analysis:

From the quantum adiabatic theorem [3, Theorem 3] the formula for the adiabatic error bound η at any point s is:

η(s)=C{H(1)(0)2TΔ2(0)+H(1)(s)2TΔ2(f(s))+1T0s[H(2)(s)2Δ2(f(s))+H(1)(s)22Δ3(f(s))]ds}.

See Appendix A for a detailed explanation of the components.

The formula shows that the adiabatic error is minimized when:

  1. The total runtime T is large (slow evolution).

  2. The spectral gap Δ is large (well-separated ground and excited states).

  3. The derivatives H(1) and H(2) are small (smooth Hamiltonian changes).

The following function plots the spectral gap Δ evolution:

import matplotlib.pyplot as plt


def plot_eigenvalues_evolution(ylim=None):
    time_steps = np.linspace(0, 1, 100)  # Discrete time steps

    # Store eigenvalues at each time step
    eigenvalues = []

    # Calculate eigenvalues across all time steps
    for t in time_steps:
        H_t = hamiltonian_t(H0, H1, t, 1)
        eigvals = np.linalg.eigvalsh(H_t)  # Sorted real eigenvalues
        eigenvalues.append(eigvals)

    # Convert eigenvalues list to a NumPy array for easier manipulation
    eigenvalues = np.array(eigenvalues)

    # Add small offsets to separate close eigenvalues visually
    offsets = np.linspace(
        -0.05, 0.05, eigenvalues.shape[1]
    )  # Small offsets for each eigenvalue line

    # Plot the eigenvalues across time steps
    plt.figure(figsize=(10, 6))
    for i in range(eigenvalues.shape[1]):
        plt.plot(time_steps, eigenvalues[:, i] + offsets[i], label=f"Eigenvalue {i+1}")

    # Highlight degenerate eigenvalues (if any)
    for step_idx, t in enumerate(time_steps):
        unique_vals, counts = np.unique(eigenvalues[step_idx], return_counts=True)

    # Apply y-axis limits if provided
    if ylim:
        plt.ylim(ylim)

    # Customize the plot
    plt.xlabel("Time (t)", fontsize=12)
    plt.ylabel("Eigenvalues", fontsize=12)
    plt.title("Eigenvalues Evolution Across $s$", fontsize=14)
    plt.grid()

    # Move the legend to the side
    plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0)
    plt.tight_layout()

    # Show the plot
    plt.show()
plot_eigenvalues_evolution()

png

Should we want to focus on the spectral gap from the Null states, we can zoom-in on our plot:

plot_eigenvalues_evolution(ylim=(-1.5, 1.5))

png

We can visually observe from the above that although the spectral gap does change throughout s it still stays quite large in our example, so we can choose T accordingly. Without going into details we will choose a simple value for T (represented as TOTAL_EVOLUTION_TIME). However, if we simply assume H(1)2, H(2)2 are bounded by constants, and use the worst-case bound that Δκ1, it can be shown that in order to have η(1)ϵ, the runtime of vanilla AQC is Tκ3/ϵ.

TOTAL_EVOLUTION_TIME = 7

AQC Implementation:

Since \(|ψT(s)=Texp(iT0sH(f(s))ds)|ψT(0) ,\) where T is the time-ordering operator, it is sufficient to implement an efficient time-dependent Hamiltonian simulation of H(f(s)).

One straightforward approach to achieve this is by using the Trotter splitting method. The lowest order approximation takes the form:

Texp(iT0sH(f(s))ds)m=1Mexp(iThH(f(sm)))

which can further be approximated as:

m=1Mexp(iTh(1f(sm))H0)exp(iThf(sm)H1)

where \(h=s/M,sm=mh.\)

It is proved in [2] that the error of such an approximation is

O(poly(log(N))T2/M),

which indicates that to achieve an ϵ-approximation, it suffices to choose

M=O(poly(log(N))T(ϵ)2/ϵ).

(Note that in our case T also dependents on ϵ).

Building the Quantum model:

Using the Classiq platform, we implement the adiabatic path with Suzuki-Trotter decomposition for Hamiltonian exponentiation.

In our model T is represented by the TOTAL_EVOLUTION_TIME and M is represented by NUM_STEPS.

In the Trotter implementation M scales as O(T2) with respect to the runtime T, therefore our rough choice of M:

NUM_STEPS = 50

We are now ready to build our quantum model.

The function adiabatic_evolution_qfunc implements the adiabatic path with Suzuki-Trotter decomposition for Hamiltonian exponentiation:

@qfunc(generative=True)
def adiabatic_evolution_qfunc(
    H0: CArray[PauliTerm],
    H1: CArray[PauliTerm],
    evolution_time: CInt,
    num_steps: CInt,
    qba: QArray[QBit],
):
    # Time step for each increment
    delta_t = evolution_time / num_steps
    for step in range(num_steps):
        t = step * delta_t
        suzuki_trotter(
            HO_HAMILTONIAN,
            evolution_coefficient=delta_t * (1 - t / evolution_time),
            order=1,
            repetitions=1,
            qbv=qba,
        )
        suzuki_trotter(
            H1_HAMILTONIAN,
            evolution_coefficient=delta_t * (t / evolution_time),
            order=1,
            repetitions=10,
            qbv=qba,
        )

To solve the QLSP we first prepare H0 zero state |b~ and than initiate the evolution:

def get_model(H0, H1, b, evolution_time, num_steps):

    @qfunc
    def main(qba: Output[QArray[QBit]]):
        prepare_state(
            probabilities=(np.abs(np.kron(np.array([1, 0]), b)) ** 2).tolist(),
            bound=0,
            out=qba,
        )
        adiabatic_evolution_qfunc(H0, H1, evolution_time, num_steps, qba)

    execution_preferences = ExecutionPreferences(
        num_shots=1,
        backend_preferences=ClassiqBackendPreferences(
            backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR
        ),
    )
    return create_model(main, execution_preferences=execution_preferences)


qmod = get_model(
    HO_HAMILTONIAN, H1_HAMILTONIAN, b_normalized, TOTAL_EVOLUTION_TIME, NUM_STEPS
)

Synthesize, verify and execute

Synthesize the model into a Quantum Program, verify it and execute it on a state vector simulator:**

qprog = synthesize(qmod)
write_qmod(qmod, "solving_qlsp_with_aqc", decimal_precision=5)
show(qprog)

result_state_vector = execute(qprog).result_value().state_vector
Opening: https://platform.classiq.io/circuit/2rwe34neCtLDeNlc1hOH6HYVXdn?version=0.66.0

Evaluate results:

def plot_state_probabilities(title, x, color="b"):
    # Ensure x is a numpy array and normalized
    x = np.array(x)

    # Calculate probabilities
    probabilities = np.abs(x) ** 2

    # Create labels for the states
    labels = [f"|{i}>" for i in range(len(x))]

    # Plot the probabilities
    plt.bar(labels, probabilities, color=color, alpha=0.7)
    plt.xlabel("States")
    plt.ylabel("Probabilities")
    plt.title(title)
    plt.xticks(rotation=45)
    plt.ylim(0, 1)
    plt.show()


def compare_states(
    state1, state1_label, state2, state1_labe2, color1="gold", color2="b"
):
    # Plot a histogram of each state probabilities
    plot_state_probabilities(state1_label, state1, color1)
    plot_state_probabilities(state1_labe2, state2, color2)

    # Check the overlap between states
    overlap = np.abs(np.vdot(state1, state2)) ** 2
    print(f"Similarity of results: {overlap:.4f}")
# Print the solution vector x
x = np.linalg.solve(A_normalized, b_normalized)
print("Solution vector x:")
normalized_x = x / np.linalg.norm(x)
print(normalized_x)

# Convert dictionary values to complex numbers
print("State vector:")
state_vector = np.array([complex(value) for value in result_state_vector.values()])
print(state_vector)

compare_states(
    state_vector,
    "Quantum simulator state_vector",
    np.kron(np.array([1, 0]), normalized_x),
    "Classical solution to normalized_x",
)
Solution vector x:
[ 0.32300564 -0.23491319 -0.22423532  0.88893288]
State vector:
[ 0.01631432-3.08220039e-01j -0.01056133+1.99531093e-01j
 -0.01705876+3.22284489e-01j  0.04545515-8.58766326e-01j
  0.13241748+7.00895691e-03j -0.02525213-1.33661442e-03j
  0.00707045+3.74244064e-04j -0.05315043-2.81329231e-03j]

png

png

Similarity of results: 0.9672

By comparing the quantum-computed results with the mathematically expected solution, we observe a good alignment - showcasing the potential of the AQC approach for solving linear problems.

We can observe the runtime of the above implementation by analyzing the depth parameter from the transpiled circuit data of our quantum program

print("Program depth:", QuantumProgram.from_qprog(qprog).transpiled_circuit.depth)
Program depth: 33308

For alternative QLSP configurations, selecting different values for T and M will be necessary, and these choices will naturally impact the circuit depth. For instance::

# Define matrix A and vector b
A = np.array(
    [
        [3.9270525, 1.06841123, 2.09661281, -0.10400811],
        [1.06841123, 2.93584295, -0.0906049, 1.09754032],
        [2.09661281, -0.0906049, 2.87204449, 1.13774997],
        [-0.10400811, 1.09754032, 1.13774997, 1.85170585],
    ]
)
b = np.array([12, 10, 17, 26])

condition_number = compute_condition_number(A)
print("Condition number:", condition_number)

H0, H1, HO_HAMILTONIAN, H1_HAMILTONIAN, A_normalized, b_normalized = setup_QLSP(A, b)
Condition number: 5722470278.425136
plot_eigenvalues_evolution(ylim=(-1.5, 1.5))

png

Although the condition number is higher, the spectral gap remains relatively large. However, using the same values of T and M as chosen above will result in increased error:

qmod = get_model(
    HO_HAMILTONIAN, H1_HAMILTONIAN, b_normalized, TOTAL_EVOLUTION_TIME, NUM_STEPS
)
qprog = synthesize(qmod)
show(qprog)
result_state_vector = execute(qprog).result_value().state_vector
Opening: https://platform.classiq.io/circuit/2rweHDjGqIVTawYYsk3Uwic8S9J?version=0.66.0
# Print the solution vector x
x = np.linalg.solve(A_normalized, b_normalized)
print("Solution vector x:")
normalized_x = x / np.linalg.norm(x)
print(normalized_x)

# Convert dictionary values to complex numbers
print("State vector:")
state_vector = np.array([complex(value) for value in result_state_vector.values()])
print(state_vector)

compare_states(
    state_vector,
    "Quantum simulator state_vector",
    np.kron(np.array([1, 0]), normalized_x),
    "Classical solution to normalized_x",
)
Solution vector x:
[ 0.42009163 -0.39396804 -0.55637591  0.59896415]
State vector:
[-0.02338635-0.37524301j  0.02140731+0.34348851j  0.04152884+0.6663462j
 -0.02503025-0.40161994j  0.28004751-0.01745346j -0.13146114+0.00819308j
 -0.18310263+0.01141155j  0.05280268-0.00329083j]

png

png

Similarity of results: 0.8209
print("Program depth:", QuantumProgram.from_qprog(qprog).transpiled_circuit.depth)
Program depth: 33308

Choosing T ans M accordingly will improve results but affect the overall runtime:

TOTAL_EVOLUTION_TIME = 10
NUM_STEPS = 100
qmod = get_model(
    HO_HAMILTONIAN, H1_HAMILTONIAN, b_normalized, TOTAL_EVOLUTION_TIME, NUM_STEPS
)
qprog = synthesize(qmod)
show(qprog)
result_state_vector = execute(qprog).result_value().state_vector
Opening: https://platform.classiq.io/circuit/2rwei3h5uCQJMppqbZQUFzX2v9y?version=0.66.0
# Print the solution vector x
x = np.linalg.solve(A_normalized, b_normalized)
print("Solution vector x:")
normalized_x = x / np.linalg.norm(x)
print(normalized_x)

# Convert dictionary values to complex numbers
print("State vector:")
state_vector = np.array([complex(value) for value in result_state_vector.values()])
print(state_vector)

compare_states(
    state_vector,
    "Quantum simulator state_vector",
    np.kron(np.array([1, 0]), normalized_x),
    "Classical solution to normalized_x",
)
Solution vector x:
[ 0.42009163 -0.39396804 -0.55637591  0.59896415]
State vector:
[-0.44075799-0.27456092j  0.2986469 +0.18603581j  0.45432412+0.28301165j
 -0.45996295-0.28652425j  0.06242484-0.10021182j -0.02261307+0.0363012j
 -0.05203803+0.08353766j  0.01609333-0.02583494j]

png

png

Similarity of results: 0.9587
print("Program depth:", QuantumProgram.from_qprog(qprog).transpiled_circuit.depth)
Program depth: 67208

As expected, the overall circuit depth increased to achieve a high degree of similarity in the results.

*Although the results are satisfying, a more optimal approach can be implemented based on the discrete adiabatic theorem [[4](#DISCRETE)]*

Important to note: The above implementation example aims to provide an intuitive understanding of the principles behind solving quantum linear solver problems with the adiabatic quantum evolution and the associated error bounds. However, for simplicity and accessibility, several aspects of the implementation are not optimal:

  • Suzuki-Trotter Decomposition: We utilized the first order Suzuki-Trotter approximation for time evolution. As such (as mentioned above), a more optimal approach based on the discrete adiabatic theorem [4] could be implemented.
  • Schedule Function: In the above example we used the vanilla AQC scheduling function. Using more sophisticated scheduling functions as suggested in [1] will improve runtime.
  • Brute-Force Encoding: The encoding of Pauli operators in this tutorial is direct and unoptimized, scaling exponentially with system size. Other encoding techniques (such as suggested in [4]) will be more efficient.

These choices were made to prioritize conceptual clarity over computational efficiency. Next step would be to apply state pf the art techniques and show improvement in gate complexity and runtime.

Appendix A: Explanation of the adiabatic error bound components

The adiabatic error bound η(s) is defined by the quantum adiabatic theorem [3, Theorem 3] as:

η(s)=C{H(1)(0)2TΔ2(0)+H(1)(s)2TΔ2(f(s))+1T0s[H(2)(s)2Δ2(f(s))+H(1)(s)22Δ3(f(s))]ds}.

Detailed Explanation of Components:

  1. η(s):

  2. Represents the adiabatic error bound at a specific point s[0,1].

  3. Quantifies the deviation of the quantum state from the ground state during evolution.

  4. C:

  5. A proportionality constant that depends on system-specific properties, such as the dimensionality and scaling of norms.

  6. H(1)(0):

  7. The operator norm of the first derivative of the Hamiltonian, H(s), evaluated at s=0.

  8. Indicates how quickly the Hamiltonian changes initially.

  9. T:

  10. The total runtime of the adiabatic evolution.

  11. Larger T values reduce the error, as slower evolution aids adiabaticity.

  12. Δ(0):

  13. The spectral gap at s=0, defined as the energy difference between the ground state and the first excited state of H(s).

  14. Larger gaps improve the adiabatic process.

  15. H(1)(s):

  16. The operator norm of the first derivative of H(s) at an intermediate point s.

  17. Reflects how fast the Hamiltonian is changing during evolution.

  18. Δ(f(s)):

  19. The spectral gap at the point s, mapped via the function f(s).

  20. H(2)(s):

  21. The operator norm of the second derivative of the Hamiltonian, H(2)(s), at a point s.

  22. Captures the curvature or acceleration of the Hamiltonian's evolution.

  23. 0sds:

  24. An integral from 0 to s, summing contributions of the Hamiltonian’s derivatives over the path.

  25. Accounts for cumulative effects of the Hamiltonian's changes during evolution.

  26. Δ2(f(s)) and Δ3(f(s)):

    • Higher powers of the spectral gap at s.

    • Larger gaps (in Δ2 and Δ3) significantly reduce the adiabatic error.

[1]: An, D. and Lin, L. “Quantum Linear System Solver Based on Time-Optimal Adiabatic Quantum Com- puting and Quantum Approximate Optimization Algorithm.” ACM Trans. Quantum Comput. 3 (2022). arXiv:1909.05500..

[2]: Wim van Dam, Michele Mosca, and Umesh Vazirani. 2001. How powerful is adiabatic quantum computation? In Proceedings 42nd IEEE Symposium on Foundations of Computer Science. IEEE, Piscataway, NJ, 279–287

[3]: Sabine Jansen, Mary-Beth Ruskai, and Ruedi Seiler. 2007. Bounds for the adiabatic approximation with applications to quantum computation. J. Math. Phys. 48, 10 (2007), 102111

[4]: The discrete adiabatic quantum linear system solver has lower constant factors than the randomized adiabatic solver Pedro C.S. Costa, Dong An, Ryan Babbush, Dominic Berry