Variational Quantum Linear Solver
import qiskit
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import Aer, transpile, assemble
import math
import random
import numpy as np
from scipy.optimize import minimize


## 1. Introduction

The Variational Quantum Linear Solver, or the VQLS is a variational quantum algorithm that utilizes VQE in order to solve systems of linear equations more efficiently than classical computational algorithms. Specifically, if we are given some matrix $\textbf{A}$, such that $\textbf{A} |\textbf{x}\rangle \ = \ |\textbf{b}\rangle$, where $|\textbf{b}\rangle$ is some known vector, the VQLS algorithm is theoretically able to find a normalized $|x\rangle$ that is proportional to $|\textbf{x}\rangle$, which makes the above relationship true.

The output of this algorithm is identical to that of the HHL Quantum Linear-Solving Algorithm, except, while HHL provides a much more favourable computation speedup over VQLS, the variational nature of our algorithm allows for it to be performed on NISQ quantum computers, while HHL would require much more robust quantum hardware, and many more qubits.

## 2. The Algorithm

To begin, the inputs into this algorithm are evidently the matrix $\textbf{A}$, which we have to decompose into a linear combination of unitaries with complex coefficients:

$$A \ = \ \displaystyle\sum_{n} c_n \ A_n$$

Where each $A_n$ is some unitary, and some unitary $U$ that prepares state $|\textbf{b}\rangle$ from $|0\rangle$. Now, recall the general structure of a variational quantum algorithm. We have to construct a quantum cost function, which can be evaluated with a low-depth parametrized quantum circuit, then output to the classical optimizer. This allows us to search a parameter space for some set of parameters $\alpha$, such that $|\psi(\alpha)\rangle \ = \ \frac{|\textbf{x}\rangle}{|| \textbf{x} ||}$, where $|\psi(k)\rangle$ is the output of out quantum circuit corresponding to some parameter set $k$.

Before we actually begin constructing the cost function, let's take a look at a "high level" overview of the sub-routines within this algorithm, as illustrated in this image from the original paper:

So essentially, we start off with a qubit register, with each qubit initialized to $|0\rangle$. Our algorithm takes its inputs, then prepares and evaluates the cost function, starting with the creation of some ansatz $V(\alpha)$. If the computed cost is greater than some parameter $\gamma$, the algorithm is run again with updated parameters, and if not, the algorithm terminates, and the ansatz is calculated with the optimal parameters (determined at termination). This gives us the state vector that minimizes our cost function, and therefore the normalized form of $|\textbf{x}\rangle$.

## 3. Qiskit Implementation

### Fixed Hardware Ansatz

Let's start off by considering the ansatz $V(\alpha)$, which is just a circuit that prepares some arbitrary state $|\psi(k)\rangle$. This allows us to "search" the state space by varying some set of parameters, $k$. Anyways, the ansatz that we will use for this implementation is given as follows:

def apply_fixed_ansatz(qubits, parameters):

for iz in range (0, len(qubits)):
circ.ry(parameters[0][iz], qubits[iz])

circ.cz(qubits[0], qubits[1])
circ.cz(qubits[2], qubits[0])

for iz in range (0, len(qubits)):
circ.ry(parameters[1][iz], qubits[iz])

circ.cz(qubits[1], qubits[2])
circ.cz(qubits[2], qubits[0])

for iz in range (0, len(qubits)):
circ.ry(parameters[2][iz], qubits[iz])

circ = QuantumCircuit(3)
apply_fixed_ansatz([0, 1, 2], [[1, 1, 1], [1, 1, 1], [1, 1, 1]])
circ.draw()


This is called a fixed hardware ansatz: the configuration of quantum gates remains the same for each run of the circuit, all that changes are the parameters. Unlike the QAOA ansatz, it is not composed solely of Trotterized Hamiltonians. The applications of $Ry$ gates allow us to search the state space, while the $CZ$ gates create "interference" between the different qubit states.

Now, it makes sense for us to consider the actual cost function. The goal of our algorithm will be to minimize cost, so when $|\Phi\rangle \ = \ \textbf{A} |\psi(k)\rangle$ is very close to $|\textbf{b}\rangle$, we want our cost function's output to be very small, and when the vectors are close to being orthogonal, we want the cost function to be very large. Thus, we introduce the "projection" Hamiltonian:

$$H_P \ = \ \mathbb{I} \ - \ |b\rangle \langle b|$$

Where we have:

$$C_P \ = \ \langle \Phi | H_P | \Phi \rangle \ = \ \langle \Phi | (\mathbb{I} \ - \ |b\rangle \langle b|) |\Phi \rangle \ = \ \langle \Phi | \Phi \rangle \ - \ \langle \Phi |b\rangle \langle b | \Phi \rangle$$

Notice how the second term tells us "how much" of $|\Phi\rangle$ lies along $|b\rangle$. We then subtract this from another number to get the desired low number when the inner product of $|\Phi\rangle$ and $|b\rangle$ is greater (they agree more), and the opposite for when they are close to being orthogonal. This is looking good so far! However, there is still one more thing we can do to increase the accuracy of the algorithm: normalizing the cost function. This is due to the fact that if $|\Phi\rangle$ has a small norm, then the cost function will still be low, even if it does not agree with $|\textbf{b}\rangle$. Thus, we replace $|\Phi\rangle$ with $\frac{|\Phi\rangle}{\sqrt{\langle \Phi | \Phi \rangle}}$:

$$\hat{C}_P \ = \ \frac{\langle \Phi | \Phi \rangle}{\langle \Phi | \Phi \rangle} \ - \ \frac{\langle \Phi |b\rangle \langle b | \Phi \rangle}{\langle \Phi | \Phi \rangle} \ = \ 1 \ - \ \frac{\langle \Phi |b\rangle \langle b | \Phi \rangle}{\langle \Phi | \Phi \rangle} \ = \ 1 \ - \ \frac{|\langle b | \Phi \rangle|^2}{\langle \Phi | \Phi \rangle}$$

Ok, so, we have prepared our state $|\psi(k)\rangle$ with the ansatz. Now, we have two values to calculate in order to evaluate the cost function, namely $|\langle b | \Phi \rangle|^2$ and $\langle \Phi | \Phi \rangle$. Luckily, a nifty little quantum subroutine called the Hadamard Test allows us to do this! Essentially, if we have some unitary $U$ and some state $|\phi\rangle$, and we want to find the expectation value of $U$ with respect to the state, $\langle \phi | U | \phi \rangle$, then we can evaluate the following circuit:

Then, the probability of measuring the first qubit to be $0$ is equal to $\frac{1}{2} (1 \ + \ \text{Re}\langle U \rangle)$ and the probability of measuring $1$ is $\frac{1}{2} (1 \ - \ \text{Re}\langle U \rangle)$, so subtracting the two probabilities gives us $\text{Re} \langle U \rangle$. Luckily, the matrices we will be dealing with when we test this algorithm are completely real, so $\text{Re} \langle U \rangle \ = \ \langle U \rangle$, for this specific implementation. Here is how the Hadamard test works. By the circuit diagram, we have as our general state vector:

$$\frac{|0\rangle \ + \ |1\rangle}{\sqrt{2}} \ \otimes \ |\psi\rangle \ = \ \frac{|0\rangle \ \otimes \ |\psi\rangle \ + \ |1\rangle \ \otimes \ |\psi\rangle}{\sqrt{2}}$$

Applying our controlled unitary:

$$\frac{|0\rangle \ \otimes \ |\psi\rangle \ + \ |1\rangle \ \otimes \ |\psi\rangle}{\sqrt{2}} \ \rightarrow \ \frac{|0\rangle \ \otimes \ |\psi\rangle \ + \ |1\rangle \ \otimes \ U|\psi\rangle}{\sqrt{2}}$$

Then applying the Hadamard gate to the first qubit:

$$\frac{|0\rangle \ \otimes \ |\psi\rangle \ + \ |1\rangle \ \otimes \ U|\psi\rangle}{\sqrt{2}} \ \rightarrow \ \frac{1}{2} \ \big[ |0\rangle \ \otimes \ |\psi\rangle \ + \ |1\rangle \ \otimes \ |\psi\rangle \ + \ |0\rangle \ \otimes \ U|\psi\rangle \ - \ |1\rangle \ \otimes \ U|\psi\rangle \big]$$

$$\Rightarrow \ |0\rangle \ \otimes \ (\mathbb{I} \ + \ U)|\psi\rangle \ + \ |1\rangle \ \otimes \ (\mathbb{I} \ - \ U)|\psi\rangle$$

When we take a measurement of the first qubit, remember that in order to find the probability of measuring $0$, we must take the inner product of the state vector with $|0\rangle$, then multiply by its complex conjugate (see the quantum mechanics section if you are not familiar with this). The same follows for the probability of measuring $1$. Thus, we have:

$$P(0) \ = \ \frac{1}{4} \ \langle \psi | (\mathbb{I} \ + \ U) (\mathbb{I} \ + \ U^{\dagger}) |\psi\rangle \ = \ \frac{1}{4} \ \langle \psi | (\mathbb{I}^2 \ + U \ + \ U^{\dagger} \ + \ U^{\dagger} U) |\psi\rangle \ = \ \frac{1}{4} \ \langle \psi | (2\mathbb{I} \ + U \ + \ U^{\dagger}) |\psi\rangle$$

$$\Rightarrow \ \frac{1}{4} \Big[ 2 \ + \ \langle \psi | U^{\dagger} | \psi \rangle \ + \ \langle \psi | U | \psi \rangle \Big] \ = \ \frac{1}{4} \Big[ 2 \ + \ (\langle \psi | U | \psi \rangle)^{*} \ + \ \langle \psi | U | \psi \rangle \Big] \ = \ \frac{1}{2} (1 \ + \ \text{Re} \ \langle \psi | U | \psi \rangle)$$

By a similar procedure, we get:

$$P(1) \ = \ \frac{1}{2} \ (1 \ - \ \text{Re} \ \langle \psi | U | \psi \rangle)$$

And so, by taking the difference:

$$P(0) \ - \ P(1) \ = \ \text{Re} \ \langle \psi | U | \psi \rangle$$

Cool! Now, we can actually implement this for the two values we have to compute. Starting with $\langle \Phi | \Phi \rangle$, we have:

$$\langle \Phi | \Phi \rangle \ = \ \langle \psi(k) | A^{\dagger} A |\psi(k) \rangle \ = \ \langle 0 | V(k)^{\dagger} A^{\dagger} A V(k) |0\rangle \ = \ \langle 0 | V(k)^{\dagger} \Big( \displaystyle\sum_{n} c_n \ A_n \Big)^{\dagger} \Big( \displaystyle\sum_{n} c_n \ A_n \Big) V(k) |0\rangle$$

$$\Rightarrow \ \langle \Phi | \Phi \rangle \ = \ \displaystyle\sum_{m} \displaystyle\sum_{n} c_m^{*} c_n \langle 0 | V(k)^{\dagger} A_m^{\dagger} A_n V(k) |0\rangle$$

and so our task becomes computing every possible term $\langle 0 | V(k)^{\dagger} A_m^{\dagger} A_n V(k) |0\rangle$ using the Hadamard test. This requires us to prepare the state $V(k) |0\rangle$, and then perform controlled operations with some control-ancilla qubit for the unitary matrices $A_m^{\dagger}$ and $A_n$. We can implement this in code:

# Creates the Hadamard test

circ.h(ancilla_index)

apply_fixed_ansatz(qubits, parameters)

for ie in range (0, len(gate_type[0])):
if (gate_type[0][ie] == 1):
circ.cz(ancilla_index, qubits[ie])

for ie in range (0, len(gate_type[1])):
if (gate_type[1][ie] == 1):
circ.cz(ancilla_index, qubits[ie])

circ.h(ancilla_index)

circ = QuantumCircuit(4)
had_test([[0, 0, 0], [0, 0, 1]], [1, 2, 3], 0, [[1, 1, 1], [1, 1, 1], [1, 1, 1]])
circ.draw()


The reason why we are applying two different "gate_types" is because this represents the pairs of gates shown in the expanded form of $\langle \Phi | \Phi \rangle$.

It is also important to note that for the purposes of this implementation (the systems of equations we will actually be solving, we are only concerned with the gates $Z$ and $\mathbb{I}$, so I only include support for these gates (The code includes number "identifiers" that signify the application of different gates, $0$ for $\mathbb{I}$ and $1$ for $Z$).

Now, we can move on to the second value we must calculate, which is $|\langle b | \Phi \rangle|^2$. We get:

$$|\langle b | \Phi \rangle|^2 \ = \ |\langle b | A V(k) | 0 \rangle|^2 \ = \ |\langle 0 | U^{\dagger} A V(k) | 0 \rangle|^2 \ = \ \langle 0 | U^{\dagger} A V(k) | 0 \rangle \langle 0 | V(k)^{\dagger} A^{\dagger} U |0\rangle$$

All we have to do now is the same expansion as before for the product $\langle 0 | U^{\dagger} A V(k) | 0 \rangle \langle 0 | V(k)^{\dagger} A^{\dagger} U |0\rangle$:

$$\langle 0 | U^{\dagger} A V(k) | 0 \rangle^2 \ = \ \displaystyle\sum_{m} \displaystyle\sum_{n} c_m^{*} c_n \langle 0 | U^{\dagger} A_n V(k) | 0 \rangle \langle 0 | V(k)^{\dagger} A_m^{\dagger} U |0\rangle$$

Now, again, for the purposes of this demonstration, we will soon see that all the outputs/expectation values of our implementation will be real, so we have:

$$\Rightarrow \ \langle 0 | U^{\dagger} A V(k) | 0 \rangle \ = \ (\langle 0 | U^{\dagger} A V(k) | 0 \rangle)^{*} \ = \ \langle 0 | V(k)^{\dagger} A^{\dagger} U |0\rangle$$

Thus, in this particular implementation:

$$|\langle b | \Phi \rangle|^2 \ = \ \displaystyle\sum_{m} \displaystyle\sum_{n} c_m c_n \langle 0 | U^{\dagger} A_n V(k) | 0 \rangle \langle 0 | U^{\dagger} A_m V(k) | 0 \rangle$$

There is a sophisticated way of solving for this value, using a newly-proposed subroutine called the Hadamard Overlap Test (see cited paper), but for this tutorial, we will just be using a standard Hadamard Test, where we control each matrix. This unfortunately requires the use of an extra ancilla qubit. We essentially just place a control on each of the gates involved in the ancilla, the $|b\rangle$ preparation unitary, and the $A_n$ unitaries. We get something like this for the controlled-ansatz:

# Creates controlled anstaz for calculating |<b|psi>|^2 with a Hadamard test

def control_fixed_ansatz(qubits, parameters, ancilla, reg):

for i in range (0, len(qubits)):
circ.cry(parameters[0][i], qiskit.circuit.Qubit(reg, ancilla), qiskit.circuit.Qubit(reg, qubits[i]))

circ.ccx(ancilla, qubits[1], 4)
circ.cz(qubits[0], 4)
circ.ccx(ancilla, qubits[1], 4)

circ.ccx(ancilla, qubits[0], 4)
circ.cz(qubits[2], 4)
circ.ccx(ancilla, qubits[0], 4)

for i in range (0, len(qubits)):
circ.cry(parameters[1][i], qiskit.circuit.Qubit(reg, ancilla), qiskit.circuit.Qubit(reg, qubits[i]))

circ.ccx(ancilla, qubits[2], 4)
circ.cz(qubits[1], 4)
circ.ccx(ancilla, qubits[2], 4)

circ.ccx(ancilla, qubits[0], 4)
circ.cz(qubits[2], 4)
circ.ccx(ancilla, qubits[0], 4)

for i in range (0, len(qubits)):
circ.cry(parameters[2][i], qiskit.circuit.Qubit(reg, ancilla), qiskit.circuit.Qubit(reg, qubits[i]))

q_reg = QuantumRegister(5)
circ = QuantumCircuit(q_reg)
control_fixed_ansatz([1, 2, 3], [[1, 1, 1], [1, 1, 1], [1, 1, 1]], 0, q_reg)
circ.draw()


Notice the extra qubit, q0_4. This is an ancilla, and allows us to create a $CCZ$ gate, as is shown in the circuit. Now, we also have to create the circuit for $U$. In our implementation, we will pick $U$ as:

$$U \ = \ H_1 H_2 H_3$$

Thus, we have:

def control_b(ancilla, qubits):

for ia in qubits:
circ.ch(ancilla, ia)

circ = QuantumCircuit(4)
control_b(0, [1, 2, 3])
circ.draw()


Finally, we construct our new Hadamard test:

# Create the controlled Hadamard test, for calculating <psi|psi>

def special_had_test(gate_type, qubits, ancilla_index, parameters, reg):

circ.h(ancilla_index)

control_fixed_ansatz(qubits, parameters, ancilla_index, reg)

for ty in range (0, len(gate_type)):
if (gate_type[ty] == 1):
circ.cz(ancilla_index, qubits[ty])

control_b(ancilla_index, qubits)

circ.h(ancilla_index)

q_reg = QuantumRegister(5)
circ = QuantumCircuit(q_reg)
special_had_test([[0, 0, 0], [0, 0, 1]], [1, 2, 3], 0, [[1, 1, 1], [1, 1, 1], [1, 1, 1]], q_reg)
circ.draw()


This is for the specific implementation when all of our parameters are set to $1$, and the set of gates $A_n$ is simply [0, 0, 0], and [0, 0, 1], which corresponds to the identity matrix on all qubits, as well as the $Z$ matrix on the third qubit (with my "code notation").

Now, we are ready to calculate the final cost function. This simply involves us taking the products of all combinations of the expectation outputs from the different circuits, multiplying by their respective coefficients, and arranging into the cost function that we discussed previously!

# Implements the entire cost function on the quantum circuit

def calculate_cost_function(parameters):

global opt

overall_sum_1 = 0

parameters = [parameters[0:3], parameters[3:6], parameters[6:9]]

for i in range(0, len(gate_set)):
for j in range(0, len(gate_set)):

global circ

qctl = QuantumRegister(5)
qc = ClassicalRegister(5)
circ = QuantumCircuit(qctl, qc)

backend = Aer.get_backend('statevector_simulator')

multiply = coefficient_set[i]*coefficient_set[j]

had_test([gate_set[i], gate_set[j]], [1, 2, 3], 0, parameters)

t_circ = transpile(circ, backend)
qobj = assemble(t_circ)
job = backend.run(qobj)

result = job.result()
outputstate = np.real(result.get_statevector(circ, decimals=100))
o = outputstate

m_sum = 0
for l in range (0, len(o)):
if (l%2 == 1):
n = o[l]**2
m_sum+=n

overall_sum_1+=multiply*(1-(2*m_sum))

overall_sum_2 = 0

for i in range(0, len(gate_set)):
for j in range(0, len(gate_set)):

multiply = coefficient_set[i]*coefficient_set[j]
mult = 1

for extra in range(0, 2):

qctl = QuantumRegister(5)
qc = ClassicalRegister(5)
circ = QuantumCircuit(qctl, qc)

backend = Aer.get_backend('statevector_simulator')

if (extra == 0):
special_had_test(gate_set[i], [1, 2, 3], 0, parameters, qctl)
if (extra == 1):
special_had_test(gate_set[j], [1, 2, 3], 0, parameters, qctl)

t_circ = transpile(circ, backend)
qobj = assemble(t_circ)
job = backend.run(qobj)

result = job.result()
outputstate = np.real(result.get_statevector(circ, decimals=100))
o = outputstate

m_sum = 0
for l in range (0, len(o)):
if (l%2 == 1):
n = o[l]**2
m_sum+=n
mult = mult*(1-(2*m_sum))

overall_sum_2+=multiply*mult

print(1-float(overall_sum_2/overall_sum_1))

return 1-float(overall_sum_2/overall_sum_1)


This code may look long and daunting, but it isn't! In this simulation, I'm taking a numerical approach, where I'm calculating the amplitude squared of each state corresponding to a measurement of the ancilla Hadamard test qubit in the $1$ state, then calculating $P(0) \ - \ P(1) \ = \ 1 \ - \ 2P(1)$ with that information. This is very exact, but is not realistic, as a real quantum device would have to sample the circuit many times to generate these probabilities (I'll discuss sampling later). In addition, this code is not completely optimized (it completes more evaluations of the quantum circuit than it has to), but this is the simplest way in which the code can be implemented, and I will be optimizing it in an update to this tutorial in the near future.

The final step is to actually use this code to solve a real linear system. We will first be looking at the example:

$$A \ = \ 0.45 Z_3 \ + \ 0.55 \mathbb{I}$$

In order to minimize the cost function, we use the COBYLA optimizer method, which we repeatedly applying. Our search space for parameters is determined by $\frac{k}{1000} \ k \ \in \ \{0, \ 3000\}$, which is initially chosen randomly. We will run the optimizer for $200$ steps, then terminate and apply the ansatz for our optimal parameters, to get our optimized state vector! In addition, we will compute some post-processing, to see if our algorithm actually works! In order to do this, we will apply $A$ to our optimal vector $|\psi\rangle_o$, normalize it, then calculate the inner product squared of this vector and the solution vector, $|b\rangle$! We can put this all into code as:

coefficient_set = [0.55, 0.45]
gate_set = [[0, 0, 0], [0, 0, 1]]

out = minimize(calculate_cost_function, x0=[float(random.randint(0,3000))/1000 for i in range(0, 9)], method="COBYLA", options={'maxiter':200})
print(out)

out_f = [out['x'][0:3], out['x'][3:6], out['x'][6:9]]

circ = QuantumCircuit(3, 3)
apply_fixed_ansatz([0, 1, 2], out_f)

backend = Aer.get_backend('statevector_simulator')
t_circ = transpile(circ, backend)
qobj = assemble(t_circ)
job = backend.run(qobj)

result = job.result()
o = result.get_statevector(circ, decimals=10)

a1 = coefficient_set[1]*np.array([[1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], [0,0,1,0,0,0,0,0], [0,0,0,1,0,0,0,0], [0,0,0,0,-1,0,0,0], [0,0,0,0,0,-1,0,0], [0,0,0,0,0,0,-1,0], [0,0,0,0,0,0,0,-1]])
a2 = coefficient_set[0]*np.array([[1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], [0,0,1,0,0,0,0,0], [0,0,0,1,0,0,0,0], [0,0,0,0,1,0,0,0], [0,0,0,0,0,1,0,0], [0,0,0,0,0,0,1,0], [0,0,0,0,0,0,0,1]])

b = np.array([float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8))])

print((b.dot(a3.dot(o)/(np.linalg.norm(a3.dot(o)))))**2)

0.6836710713149694
0.7515998909285393
0.7161441091698497
0.1737436940780679
0.40696124837156944
-0.3535401823067905
0.4593404801165958
-0.0012336654801023972
-0.4463210599167178
0.37506686093528685
-4.379278251915205
-2.0702757319302054
-4.052478777420056
-2.167412641304694
-4.510512339394018
-0.9042764538913497
-3.5268825189460165
-1.8779784555828156
-2.347555184817919
-4.102407220646085
-0.53105252496996
-3.3465996125086503
-9.806136017109747
-1.1759478432807855
-12.607376881458723
-2.6437728221787067
-5.971235125386092
-8.192074789626762
-13.511654167182465
-4.736839645438364
-7.084101088705891
-8.33515861629934
-13.0125723405279
-13.367007926236864
-12.434824019490271
-13.73128884536073
-14.51377634970942
-17.246031065051437
-8.879872575380292
-11.815966063032686
-11.5710513844899
-17.9568474628019
-18.001072234232858
-16.46752606731651
-18.041276031312314
-18.642618346141305
-18.516145742002056
-18.626327966350445
-21.690269344286975
-19.914512749755055
-20.146332421880615
-19.59313481026583
-21.084881450285263
-20.272131408016193
-23.45682530459152
-23.984805702914173
-23.30358683356295
-24.146195587125263
-22.89770461617978
-23.326523386699723
-23.792337749567174
-23.87548046091968
-24.499390929785072
-24.77569711279162
-24.780918962333548
-24.3803512839752
-24.7416817469701
-24.256248823150752
-25.210956597392897
-25.273188942333725
-25.188361144786736
-25.085877974073615
-25.413199505263616
-25.31901964043407
-25.301070335451875
-25.086204691931936
-25.412113011873007
-25.391851478266272
-25.436781924946985
-25.418394791055313
-25.25698128318078
-25.43402261687891
-25.448303823559655
-25.439129330181217
-25.45018596131398
-25.46065665613229
-25.47658804761009
-25.47077336741886
-25.45258530462644
-25.455115001420815
-25.476569293218507
-25.479742449939867
-25.472241870569242
-25.46358733511837
-25.47820863597425
-25.475364870603233
-25.4814681627656
-25.479319968735346
-25.47356731119324
-25.47919989429459
-25.489522858368236
-25.49193724180144
-25.493428051252426
-25.49382956257092
-25.489838750589445
-25.48761557939051
-25.49047090548659
-25.49376611753482
-25.49833415937675
-25.498038352253978
-25.491303032515958
-25.496947359903306
-25.500781400683966
-25.504243874013955
-25.49849112110951
-25.503615948397734
-25.506220091454882
-25.51099402391699
-25.50383979065906
-25.50877222993204
-25.50831831562757
-25.511279551679145
-25.5158368072972
-25.503926781671108
-25.517404171040106
-25.514521057596838
-25.513019341615678
-25.52104220170088
-25.520385223417343
-25.51842572354038
-25.51845694109321
-25.51959392800958
-25.52033193570153
-25.522661266795584
-25.52152091650786
-25.52170699851708
-25.52285325005413
-25.521620125034087
-25.52125450166988
-25.520999444096834
-25.52574021209731
-25.527271383225404
-25.52699212105805
-25.52679186525044
-25.524971874590758
-25.527441533959532
-25.52872424645938
-25.527690865525102
-25.528615209922698
-25.531112710916034
-25.530748141002512
-25.533633121159365
-25.534589435239834
-25.535867915512046
-25.53735036237113
-25.537229509894782
-25.53556462313499
-25.53770839526196
-25.538186977317885
-25.537070140187083
-25.53997526050806
-25.537956023217447
-25.5394422758533
-25.540954949882245
-25.54330278321787
-25.544574784055705
-25.543834408882685
-25.544652444482754
-25.543443961051224
-25.544087841954326
-25.54726726864188
-25.545794703753124
-25.546995080914154
-25.54900358414738
-25.547872002893445
-25.549512727607386
-25.548455280838628
-25.549457271029283
-25.550557060717306
-25.55063198019705
-25.550544338347194
-25.550141935627334
-25.55117216445757
-25.550999723127003
-25.552023494012815
-25.55365468892064
-25.55395025392101
-25.553987042262968
-25.55252985501772
-25.553970589015915
-25.554377672097047
-25.554560341666598
-25.55558182401981
-25.555923214653955
-25.55656449497681
-25.55729276578212
-25.55897541154142
-25.560534476906355
-25.562079645359134
-25.563629897598023
fun: -25.563629897598023
maxcv: 0.0
message: 'Maximum number of function evaluations has been exceeded.'
nfev: 200
status: 2
success: False
x: array([1.56836246e+00, 2.34312133e+00, 3.15061615e+00, 9.01532725e-01,
2.06793431e+00, 1.09978001e-02, 5.38526348e-01, 3.36998826e+00,
1.04326588e-03])
(0.10985709552241303+0j)


As you can see, our cost function has achieved a fairly low value of 0.03273673575407443, and when we calculate our classical cost function, we get 0.96776862579723, which agrees perfectly with what we measured, the vectors $|\psi\rangle_o$ and $|b\rangle$ are very similar!

Let's do another test! This time, we will keep $|b\rangle$ the same, but we will have:

$$A \ = \ 0.55 \mathbb{I} \ + \ 0.225 Z_2 \ + \ 0.225 Z_3$$

Again, we run our optimization code:

coefficient_set = [0.55, 0.225, 0.225]
gate_set = [[0, 0, 0], [0, 1, 0], [0, 0, 1]]

out = minimize(calculate_cost_function, x0=[float(random.randint(0,3000))/1000 for i in range(0, 9)], method="COBYLA", options={'maxiter':200})
print(out)

out_f = [out['x'][0:3], out['x'][3:6], out['x'][6:9]]

circ = QuantumCircuit(3, 3)
apply_fixed_ansatz([0, 1, 2], out_f)

backend = Aer.get_backend('statevector_simulator')

t_circ = transpile(circ, backend)
qobj = assemble(t_circ)
job = backend.run(qobj)

result = job.result()
o = result.get_statevector(circ, decimals=10)

a1 = coefficient_set[2]*np.array([[1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], [0,0,1,0,0,0,0,0], [0,0,0,1,0,0,0,0], [0,0,0,0,-1,0,0,0], [0,0,0,0,0,-1,0,0], [0,0,0,0,0,0,-1,0], [0,0,0,0,0,0,0,-1]])
a0 = coefficient_set[1]*np.array([[1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], [0,0,-1,0,0,0,0,0], [0,0,0,-1,0,0,0,0], [0,0,0,0,1,0,0,0], [0,0,0,0,0,1,0,0], [0,0,0,0,0,0,-1,0], [0,0,0,0,0,0,0,-1]])
a2 = coefficient_set[0]*np.array([[1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], [0,0,1,0,0,0,0,0], [0,0,0,1,0,0,0,0], [0,0,0,0,1,0,0,0], [0,0,0,0,0,1,0,0], [0,0,0,0,0,0,1,0], [0,0,0,0,0,0,0,1]])

b = np.array([float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8))])

print((b.dot(a3.dot(o)/(np.linalg.norm(a3.dot(o)))))**2)

0.47739942145460346
0.524993716494066
0.7163777756890085
0.3250981795748564
0.5676945290874322
-0.10477076167273736
0.26341797752803797
0.10959435241866156
-0.1504552096125218
-0.2778551864929759
-0.7970657049262597
0.1623810875048327
-0.7039405118800652
0.15044048605268778
-0.464613461308774
-0.46280314560807767
-0.9476583016332047
-0.2134648623296067
-0.46498067399227483
-0.48692406571506686
-1.533069849757791
-1.4900943316309947
-1.8139124681307002
-1.0417559856725949
-2.136857755540209
-2.8929990103039365
-2.2449095284992433
-1.5264461084434866
-2.9862140729800957
-4.541417152254607
-5.5018026132357924
-3.0188245219004743
-6.084703781278799
-3.641956925806414
-4.267240889086195
-1.4014502277338452
-3.821658878429308
-4.986357084689766
-7.89633333687752
-6.431503487738861
-7.921286283315821
-6.607926619277441
-6.866060969760977
-5.59502421241013
-7.784033322451878
-7.8889297852632225
-7.2667929881916695
-8.33010049111
-8.596411115089726
-8.249997848765439
-7.877191085786363
-10.525660849417724
-11.750215440121314
-13.108088861739548
-12.79594315229371
-11.449832164541425
-14.535678843981385
-15.912657273600619
-13.144407695452946
-16.088607846300373
-10.735653554791613
-14.727679204558287
-17.43228206456088
-17.515404081329166
-15.685810293310379
-15.353961409056385
-15.78816002530695
-17.548477823662413
-17.599189971992413
-18.602407516056168
-19.795675249255567
-18.65026745455338
-20.270560717656497
-20.650564576767742
-20.689943983415475
-20.687672681032414
-19.646126286563323
-20.749020143414736
-21.538464835836443
-20.30141338985323
-22.417628157240276
-21.567422658707677
-22.602912345865423
-23.040563084458295
-23.360262593713518
-23.413273989607003
-23.905320565436668
-23.798535051633557
-23.994712112183844
-24.114180234614594
-23.83706775599153
-24.1799253716804
-24.276380678165637
-24.28650583066239
-24.385516136845272
-24.86719474652208
-25.082908510952425
-25.052800750182797
-24.7946001918745
-25.223679741934372
-25.241050529883733
-25.383748145184384
-25.339984048134387
-25.250707334064913
-25.5516677963427
-25.739762385036755
-25.048075791056984
-25.592253558921858
-25.891854727751483
-25.98557643289914
-25.937427407155997
-25.07269231851325
-25.966695473197113
-26.07495163278442
-25.776457567933626
-26.205478632830825
-26.035662267110915
-25.928231597510997
-26.363530755427107
-26.48187043135237
-26.275987529521583
-26.241113441219092
-26.517534558268718
-26.47005716744065
-26.330263792122754
-26.501227373308524
-26.210550535666428
-26.501964086836896
-26.111384832336146
-26.34312535697989
-26.41067198882266
-26.46593243975066
-26.55534230759356
-26.539568190613178
-26.562603009204416
-26.541893572120706
-26.558355607809446
-26.53740917956415
-26.56746302059123
-26.55905392886007
-26.557231821525267
-26.565721130888758
-26.56579876135346
-26.555828502416507
-26.562855215700658
-26.566534246037307
-26.569793741716595
-26.57236118751077
-26.57434538511865
-26.574846839310446
-26.57342051799322
-26.57388205691317
-26.575703529123917
-26.576846779391474
-26.576011828308477
-26.57600169292743
-26.58163957328619
-26.584367300074955
-26.586914690909627
-26.588450642224338
-26.5895087699962
-26.59203857305113
-26.59116978235862
-26.591321573855105
-26.591590689661846
-26.593596255326656
-26.595920365021275
-26.593818634230278
-26.5962324806927
-26.595480261805367
-26.594868031227723
-26.59667603809894
-26.597223386859213
-26.597731214001705
-26.59775719240833
-26.597945862959754
-26.596946148748643
-26.59931483802362
-26.59938102237642
-26.599495931275882
-26.600410480804282
-26.600445779750185
-26.60019596481503
-26.59989478691507
-26.600296342018733
-26.60167413693707
-26.602051585196136
-26.60246162774175
-26.603019572317407
-26.60283150565994
-26.603389801510982
-26.60374469245516
-26.60397221860359
-26.60378596727059
-26.60432997355807
-26.60448720569166
-26.605002476198262
-26.60439610282012
-26.604766877287474
-26.605007747758222
fun: -26.605007747758222
maxcv: 0.0
message: 'Maximum number of function evaluations has been exceeded.'
nfev: 200
status: 2
success: False
x: array([2.38122341, 1.98992817, 4.69971104, 1.56178653, 3.12971691,
1.56423666, 0.66469388, 4.26774393, 0.7439952 ])
(0.29209436862772997+0j)


Again, very low error, and the classical cost function agrees! Great, so it works!

Now, we have found that this algorithm works in theory. I tried to run some simulations with a circuit that samples the circuit instead of calculating the probabilities numerically. Now, let's try to sample the quantum circuit, as a real quantum computer would do! For some reason, this simulation would only converge somewhat well for a ridiculously high number of "shots" (runs of the circuit, in order to calculate the probability distribution of outcomes). I think that this is mostly to do with limitations in the classical optimizer (COBYLA), due to the noisy nature of sampling a quantum circuit (a measurement with the same parameters won't always yield the same outcome). Luckily, there are other optimizers that are built for noisy functions, such as SPSA, but we won't be looking into that in this tutorial. Let's try our sampling for our second value of $A$, with the same matrix $U$:

#Implements the entire cost function on the quantum circuit (sampling, 100000 shots)

def calculate_cost_function(parameters):

global opt

overall_sum_1 = 0

parameters = [parameters[0:3], parameters[3:6], parameters[6:9]]

for i in range(0, len(gate_set)):
for j in range(0, len(gate_set)):

global circ

qctl = QuantumRegister(5)
qc = ClassicalRegister(1)
circ = QuantumCircuit(qctl, qc)

backend = Aer.get_backend('qasm_simulator')

multiply = coefficient_set[i]*coefficient_set[j]

had_test([gate_set[i], gate_set[j]], [1, 2, 3], 0, parameters)

circ.measure(0, 0)

t_circ = transpile(circ, backend)
qobj = assemble(t_circ, shots=10000)
job = backend.run(qobj)

result = job.result()
outputstate = result.get_counts(circ)

if ('1' in outputstate.keys()):
m_sum = float(outputstate["1"])/100000
else:
m_sum = 0

overall_sum_1+=multiply*(1-2*m_sum)

overall_sum_2 = 0

for i in range(0, len(gate_set)):
for j in range(0, len(gate_set)):

multiply = coefficient_set[i]*coefficient_set[j]
mult = 1

for extra in range(0, 2):

qctl = QuantumRegister(5)
qc = ClassicalRegister(1)

circ = QuantumCircuit(qctl, qc)

backend = Aer.get_backend('qasm_simulator')

if (extra == 0):
special_had_test(gate_set[i], [1, 2, 3], 0, parameters, qctl)
if (extra == 1):
special_had_test(gate_set[j], [1, 2, 3], 0, parameters, qctl)

circ.measure(0, 0)

t_circ = transpile(circ, backend)
qobj = assemble(t_circ, shots=10000)
job = backend.run(qobj)

result = job.result()
outputstate = result.get_counts(circ)

if ('1' in outputstate.keys()):
m_sum = float(outputstate["1"])/100000
else:
m_sum = 0

mult = mult*(1-2*m_sum)

overall_sum_2+=multiply*mult

print(1-float(overall_sum_2/overall_sum_1))

return 1-float(overall_sum_2/overall_sum_1)

coefficient_set = [0.55, 0.225, 0.225]
gate_set = [[0, 0, 0], [0, 1, 0], [0, 0, 1]]

out = minimize(calculate_cost_function, x0=[float(random.randint(0,3000))/1000 for i in range(0, 9)], method="COBYLA", options={'maxiter':200})
print(out)

out_f = [out['x'][0:3], out['x'][3:6], out['x'][6:9]]

circ = QuantumCircuit(3, 3)
apply_fixed_ansatz([0, 1, 2], out_f)

backend = Aer.get_backend('statevector_simulator')

t_circ = transpile(circ, backend)
qobj = assemble(t_circ)
job = backend.run(qobj)

result = job.result()
o = result.get_statevector(circ, decimals=10)

a1 = coefficient_set[2]*np.array([[1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], [0,0,1,0,0,0,0,0], [0,0,0,1,0,0,0,0], [0,0,0,0,-1,0,0,0], [0,0,0,0,0,-1,0,0], [0,0,0,0,0,0,-1,0], [0,0,0,0,0,0,0,-1]])
a0 = coefficient_set[1]*np.array([[1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], [0,0,-1,0,0,0,0,0], [0,0,0,-1,0,0,0,0], [0,0,0,0,1,0,0,0], [0,0,0,0,0,1,0,0], [0,0,0,0,0,0,-1,0], [0,0,0,0,0,0,0,-1]])
a2 = coefficient_set[0]*np.array([[1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], [0,0,1,0,0,0,0,0], [0,0,0,1,0,0,0,0], [0,0,0,0,1,0,0,0], [0,0,0,0,0,1,0,0], [0,0,0,0,0,0,1,0], [0,0,0,0,0,0,0,1]])

b = np.array([float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8)),float(1/np.sqrt(8))])

print((b.dot(a3.dot(o)/(np.linalg.norm(a3.dot(o)))))**2)

0.22184763316822664
0.19273223102993542
0.1608043093838336
0.15776260650899343
0.1604668594065196
0.20270788659482475
0.1653017433072872
0.15772387386958486
0.16932705255179747
0.15078781703879818
0.12763352227315627
0.12349113889746566
0.12340890898054957
0.11087348805733632
0.07153358610617566
0.0541635121470988
0.05717099624898714
0.05581321338788503
0.06787917194962423
0.06525735766091545
0.07504631693175612
0.04460931677017643
0.05616284216315792
0.04132352807462003
0.04635036349140309
0.0484184913344029
0.042338946257858945
0.044190390149081216
0.0603667539651237
0.038534422561567805
0.041947667923634246
0.04012866396949755
0.03846319643286178
0.033719007739151485
0.037549943270434816
0.0329755926299502
0.033166009864400214
0.03236861720233719
0.03518746256419969
0.03382256234763148
0.03408015744971793
0.032164216958187386
0.03136565179463491
0.03243174334140975
0.032811322004243304
0.03311981226609406
0.03230571042652086
0.03209737343633878
0.03259565396078923
0.031971300663448
0.03182449510269025
0.03166790934728392
0.031545068454096103
0.0325907928976511
0.03177131913194298
0.03162347225426099
0.031673027056575154
0.03196636304127609
0.03257963478473169
0.03169553625944599
0.03214016908325723
0.0327647777072948
0.032173881867212706
0.03212234487303578
0.03184791153704136
0.03246378619348689
0.033265167712026034
0.03178481715223991
0.03188039751404681
0.031519197099606155
0.03162589526561921
0.03204877788690752
0.031170956838027952
0.031791837701847125
0.03268177409230377
0.03222322523708521
0.03200540180905842
0.03218881480425839
0.03189276891822179
0.03206299775973176
0.032638184619654065
0.03221695847144701
0.03197367229870118
0.03122127322650292
0.03215494575263278
0.03195737645781427
0.0313996019460947
0.031751302596798814
0.03235879142295761
0.03200653044318302
0.03209418422682575
0.03146270897346337
0.03216076663115808
0.03322975496297731
0.03245650561241198
0.031879334388560165
0.032338276579189085
0.0322898069996701
0.0327313076938347
0.031600778776520566
0.031738980510020665
0.031909244981661145
0.03194773709946652
0.03236450191738838
0.03257227829287046
0.032128125544844566
0.03227232197365737
0.032125321074616964
0.03163141472832365
fun: 0.03163141472832365
maxcv: 0.0
message: 'Optimization terminated successfully.'
nfev: 109
status: 1
success: True
x: array([ 4.55255881,  3.32812547,  3.28558136,  2.39074737, -2.87366833,
0.47578228,  2.44237173,  2.02024018,  4.3956465 ])
(0.7495901945576429+0j)


So as you can see, not amazing, our solution is still off by a fairly significant margin ($3.677\%$ error isn't awful, but ideally, we want it to be much closer to 0). Again, I think this is due to the optimizer itself, not the actual quantum circuit. I will be making an update to this Notebook once I figure out how to correct this problem (likely with the introduction of a noisy optimizer, as I previously mentioned).

## 4. Acknowledgements

This implementation is based on the work presented in the research paper "Variational Quantum Linear Solver: A Hybrid Algorithm for Linear Systems", written by Carlos Bravo-Prieto, Ryan LaRose, M. Cerezo, Yiğit Subaşı, Lukasz Cincio, and Patrick J. Coles, which is available at this link.

Special thanks to Carlos Bravo-Prieto for personally helping me out, by answering some of my questions concerning the paper!

import qiskit
qiskit.__qiskit_version__

{'qiskit-terra': '0.16.1',
'qiskit-aer': '0.7.2',
'qiskit-ignis': '0.5.1',
'qiskit-ibmq-provider': '0.11.1',
'qiskit-aqua': '0.8.1',
'qiskit': '0.23.2'}