Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quantum-kittens
GitHub Repository: quantum-kittens/platypus
Path: blob/main/notebooks/ch-paper-implementations/vqls.ipynb
3855 views
Kernel: Python 3

The 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 A\textbf{A}, such that Ax = b\textbf{A} |\textbf{x}\rangle \ = \ |\textbf{b}\rangle, where b|\textbf{b}\rangle is some known vector, the VQLS algorithm is theoretically able to find a normalized x|x\rangle that is proportional to x|\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 A\textbf{A}, which we have to decompose into a linear combination of unitaries with complex coefficients:

A = ncn AnA \ = \ \displaystyle\sum_{n} c_n \ A_n

Where each AnA_n is some unitary, and some unitary UU that prepares state b|\textbf{b}\rangle from 0|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 parameterized quantum circuit, then output to the classical optimizer. This allows us to search a parameter space for some set of parameters α\alpha, such that ψ(α) = xx|\psi(\alpha)\rangle \ = \ \frac{|\textbf{x}\rangle}{|| \textbf{x} ||}, where ψ(k)|\psi(k)\rangle is the output of out quantum circuit corresponding to some parameter set kk.

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:

alt text

So essentially, we start off with a qubit register, with each qubit initialized to 0|0\rangle. Our algorithm takes its inputs, then prepares and evaluates the cost function, starting with the creation of some ansatz V(α)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 x|\textbf{x}\rangle.

3. Qiskit Implementation

Fixed Hardware Ansatz

Let's start off by considering the ansatz V(α)V(\alpha), which is just a circuit that prepares some arbitrary state ψ(k)|\psi(k)\rangle. This allows us to "search" the state space by varying some set of parameters, kk. 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()
Image in a Jupyter notebook

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 RyRy gates allow us to search the state space, while the CZCZ 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 Φ = Aψ(k)|\Phi\rangle \ = \ \textbf{A} |\psi(k)\rangle is very close to b|\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:

HP = I  bbH_P \ = \ \mathbb{I} \ - \ |b\rangle \langle b|

Where we have:

CP = ΦHPΦ = Φ(I  bb)Φ = ΦΦ  ΦbbΦ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|b\rangle. We then subtract this from another number to get the desired low number when the inner product of Φ|\Phi\rangle and b|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 b|\textbf{b}\rangle. Thus, we replace Φ|\Phi\rangle with ΦΦΦ\frac{|\Phi\rangle}{\sqrt{\langle \Phi | \Phi \rangle}}:

C^P = ΦΦΦΦ  ΦbbΦΦΦ = 1  ΦbbΦΦΦ = 1  bΦ2ΦΦ\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 ψ(k)|\psi(k)\rangle with the ansatz. Now, we have two values to calculate in order to evaluate the cost function, namely bΦ2|\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 UU and some state ϕ|\phi\rangle, and we want to find the expectation value of UU with respect to the state, ϕUϕ\langle \phi | U | \phi \rangle, then we can evaluate the following circuit:



image1



Then, the probability of measuring the first qubit to be 00 is equal to 12(1 + ReU)\frac{1}{2} (1 \ + \ \text{Re}\langle U \rangle) and the probability of measuring 11 is 12(1  ReU)\frac{1}{2} (1 \ - \ \text{Re}\langle U \rangle), so subtracting the two probabilities gives us ReU\text{Re} \langle U \rangle. Luckily, the matrices we will be dealing with when we test this algorithm are completely real, so ReU = U\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:


0 + 12  ψ = 0  ψ + 1  ψ2\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:


0  ψ + 1  ψ2  0  ψ + 1  Uψ2\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:


0  ψ + 1  Uψ2  12 [0  ψ + 1  ψ + 0  Uψ  1  Uψ]\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]

 120  (I + U)ψ + 121  (I  U)ψ\Rightarrow \ \frac{1}{2} |0\rangle \ \otimes \ (\mathbb{I} \ + \ U)|\psi\rangle \ + \ \frac{1}{2} |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 00, we must take the inner product of the state vector with 0|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 11. Thus, we have:


P(0) = 14 ψ(I + U)(I + U)ψ = 14 ψ(I2 +U + U +UU)ψ = 14 ψ(2I +U + U)ψ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 U^{\dagger}) |\psi\rangle \ = \ \frac{1}{4} \ \langle \psi | (2\mathbb{I} \ + U \ + \ U^{\dagger}) |\psi\rangle

 14[2 + ψUψ + ψUψ] = 14[2 + (ψUψ) + ψUψ] = 12(1 + Re ψUψ)\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) = 12 (1  Re ψUψ)P(1) \ = \ \frac{1}{2} \ (1 \ - \ \text{Re} \ \langle \psi | U | \psi \rangle)

And so, by taking the difference:


P(0)  P(1) = Re ψUψ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:


ΦΦ = ψ(k)AAψ(k) = 0V(k)AAV(k)0 = 0V(k)(ncn An)(ncn An)V(k)0\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

 ΦΦ = mncmcn0V(k)AmAnV(k)0\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 0V(k)AmAnV(k)0\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)0V(k) |0\rangle, and then perform controlled operations with some control-auxiliary qubits for the unitary matrices AmA_m^{\dagger} and AnA_n. We can implement this in code:

# Creates the Hadamard test def had_test(gate_type, qubits, auxiliary_index, parameters): circ.h(auxiliary_index) apply_fixed_ansatz(qubits, parameters) for ie in range (0, len(gate_type[0])): if (gate_type[0][ie] == 1): circ.cz(auxiliary_index, qubits[ie]) for ie in range (0, len(gate_type[1])): if (gate_type[1][ie] == 1): circ.cz(auxiliary_index, qubits[ie]) circ.h(auxiliary_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()
Image in a Jupyter notebook

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 ZZ and I\mathbb{I}, so I only include support for these gates (The code includes number "identifiers" that signify the application of different gates, 00 for I\mathbb{I} and 11 for ZZ).

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


bΦ2 = bAV(k)02 = 0UAV(k)02 = 0UAV(k)00V(k)AU0|\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 0UAV(k)00V(k)AU0\langle 0 | U^{\dagger} A V(k) | 0 \rangle \langle 0 | V(k)^{\dagger} A^{\dagger} U |0\rangle:


0UAV(k)02 = mncmcn0UAnV(k)00V(k)AmU0\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:

 0UAV(k)0 = (0UAV(k)0) = 0V(k)AU0\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:


bΦ2 = mncmcn0UAnV(k)00UAmV(k)0|\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 auxiliary qubit. We essentially just place a control on each of the gates involved in the auxiliary, the b|b\rangle preparation unitary, and the AnA_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, auxiliary, reg): for i in range (0, len(qubits)): circ.cry(parameters[0][i], qiskit.circuit.Qubit(reg, auxiliary), qiskit.circuit.Qubit(reg, qubits[i])) circ.ccx(auxiliary, qubits[1], 4) circ.cz(qubits[0], 4) circ.ccx(auxiliary, qubits[1], 4) circ.ccx(auxiliary, qubits[0], 4) circ.cz(qubits[2], 4) circ.ccx(auxiliary, qubits[0], 4) for i in range (0, len(qubits)): circ.cry(parameters[1][i], qiskit.circuit.Qubit(reg, auxiliary), qiskit.circuit.Qubit(reg, qubits[i])) circ.ccx(auxiliary, qubits[2], 4) circ.cz(qubits[1], 4) circ.ccx(auxiliary, qubits[2], 4) circ.ccx(auxiliary, qubits[0], 4) circ.cz(qubits[2], 4) circ.ccx(auxiliary, qubits[0], 4) for i in range (0, len(qubits)): circ.cry(parameters[2][i], qiskit.circuit.Qubit(reg, auxiliary), 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()
Image in a Jupyter notebook

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


U = H1H2H3U \ = \ H_1 H_2 H_3

Thus, we have:

def control_b(auxiliary, qubits): for ia in qubits: circ.ch(auxiliary, ia) circ = QuantumCircuit(4) control_b(0, [1, 2, 3]) circ.draw()
Image in a Jupyter notebook

Finally, we construct our new Hadamard test:

# Create the controlled Hadamard test, for calculating <psi|psi> def special_had_test(gate_type, qubits, auxiliary_index, parameters, reg): circ.h(auxiliary_index) control_fixed_ansatz(qubits, parameters, auxiliary_index, reg) for ty in range (0, len(gate_type)): if (gate_type[ty] == 1): circ.cz(auxiliary_index, qubits[ty]) control_b(auxiliary_index, qubits) circ.h(auxiliary_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()
Image in a Jupyter notebook

This is for the specific implementation when all of our parameters are set to 11, and the set of gates AnA_n is simply [0, 0, 0], and [0, 0, 1], which corresponds to the identity matrix on all qubits, as well as the ZZ 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('aer_simulator') multiply = coefficient_set[i]*coefficient_set[j] had_test([gate_set[i], gate_set[j]], [1, 2, 3], 0, parameters) circ.save_statevector() 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('aer_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.save_statevector() 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 auxiliary Hadamard test qubit in the 11 state, then calculating P(0)  P(1) = 1  2P(1)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.45Z3 + 0.55IA \ = \ 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 k1000 k  {0, 3000}\frac{k}{1000} \ k \ \in \ \{0, \ 3000\}, which is initially chosen randomly. We will run the optimizer for 200200 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 AA to our optimal vector ψo|\psi\rangle_o, normalize it, then calculate the inner product squared of this vector and the solution vector, b|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) circ.save_statevector() backend = Aer.get_backend('aer_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]]) a3 = np.add(a1, a2) 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.9753247736417425 0.9725945550265833 0.9881829990974598 0.9735044788914573 0.9825622239819491 0.9521695287639483 0.9641289169551646 0.9810923281260829 0.9506232525200021 0.9135183424692581 0.6486338854003744 0.9610175757157511 0.7991390772450623 0.43223993362193414 0.6179931214419391 0.5312002393298162 0.525836936124962 0.46527244753985697 0.6224972170955259 0.5143843349578765 0.43529571733363925 0.5145921656664549 0.4576434629159343 0.5218968092870584 0.4228665017032789 0.42821012326099617 0.4366400232342279 0.4202400788693508 0.4434622363905891 0.4139678746995452 0.46766355063054077 0.4191258671158191 0.4104719013219754 0.43498107174427003 0.41007298758174093 0.3901887342467515 0.3683580896883727 0.35842144288815236 0.35150446199465446 0.35392876717646304 0.35230006315713736 0.3534006550263027 0.33835238410241164 0.3216035044279426 0.3215518345284333 0.3207907399598038 0.3112290192956739 0.3185780125966089 0.3113724779705289 0.3261278500230791 0.32516039759118687 0.2882497071733243 0.28394115929307884 0.28620710473841526 0.30120380764331245 0.30620212892578147 0.2877690935915189 0.30264869398313643 0.3139140596177764 0.29372535974661884 0.2881267175415161 0.26905142846520025 0.2725080841331642 0.26850546258942465 0.2608379362343609 0.25355152433399997 0.25383724578038247 0.2509407788690101 0.27268592467380015 0.24724504158911442 0.2367294160138349 0.2372405143086621 0.2226294588769837 0.2079082977666672 0.19882751119553477 0.18298900195319479 0.17432949697834188 0.16500038603225942 0.17580104945243757 0.15376369088972397 0.15376377943270902 0.16156308308210898 0.149806975093015 0.15280240619698315 0.14605120052260046 0.1389891557636317 0.1716084714059224 0.13404941229463407 0.13182943280227177 0.1367646834200359 0.12173523658032326 0.12931651470676475 0.12426333152161351 0.1116455126208864 0.10432656055820932 0.09942809024950561 0.10504713664226428 0.12919370688821552 0.10196725091516101 0.09734302672948492 0.11126416586158872 0.09706033273934289 0.09463922878397568 0.11178503822001529 0.09559297077893802 0.07597783771536537 0.1424131709526466 0.07877598020866705 0.1283196954308533 0.08052055061050356 0.0729047675490091 0.07076930246800228 0.07185867674764079 0.07053481041875131 0.06989740074229545 0.08455925027664313 0.06822772929731169 0.07398996669796964 0.07163628992232451 0.06846544645110575 0.07199891626110921 0.06817093948753183 0.0684222796019931 0.06882846370215978 0.06909980283293682 0.06610471164884224 0.06566313925065481 0.06649164898506499 0.06504806145473963 0.06359495216270505 0.06433708944764527 0.06334136785064393 0.06290666798481614 0.06314292881818284 0.06260646245030832 0.06355578667372974 0.06056653432453718 0.05919753928241167 0.058170444217219175 0.057414594500709626 0.056865602881367994 0.056766828462060936 0.05718595968921192 0.056758026764732517 0.05462350203372912 0.053044552028650105 0.0520973449952431 0.052097669432554006 0.05169896301015997 0.051675821441459546 0.051968411102925605 0.05140579660782307 0.05146190447248955 0.051205096447826226 0.050617781039401066 0.049391261050200974 0.048835822838440124 0.04892543843503128 0.04844443508729446 0.04656407700066045 0.04562086016181688 0.045328480101730295 0.045183520180439585 0.044837669761571775 0.04448960478920816 0.0437610601647026 0.04281841218070137 0.04474382357815987 0.04309240801504444 0.041516662767065116 0.04048850999779885 0.03973094533445598 0.03996980891395707 0.04032986673432215 0.03931515549768805 0.03965454921033884 0.03802694301057796 0.03893352231650171 0.03805594431865511 0.03675914009598702 0.0376769437650295 0.03727987128868604 0.03740350186584995 0.0368570716292419 0.035585123301985044 0.03528870194795641 0.03531004711262742 0.035313914641810995 0.03503851517908274 0.03594022115774986 0.03479428654712802 0.03403041762599002 0.03464331809071253 0.034325867424921275 0.03513193859368824 0.03423294071087679 0.03421540510324084 0.03313082520780297 0.03302579556573393 0.03306141835888243 fun: 0.03302579556573393 maxcv: 0.0 message: 'Maximum number of function evaluations has been exceeded.' nfev: 200 status: 2 success: False x: array([ 3.19103414, 2.34621137, 1.10649596, -1.09328699, 1.60020466, 1.07590907, -0.18699078, 2.74374942, 2.93056934]) (0.966974204438782+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 ψo|\psi\rangle_o and b|b\rangle are very similar!

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


A = 0.55I + 0.225Z2 + 0.225Z3A \ = \ 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) circ.save_statevector() backend = Aer.get_backend('aer_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]]) a3 = np.add(np.add(a2, a0), a1) 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.9999460828525244 0.9459946827146493 0.9611080883892018 0.9986736901796653 0.8627167836305789 0.7636826343213896 0.6927375036664214 0.7841621204047367 0.5046315033575315 0.959888886828774 0.24095357295379194 0.35412388684428997 0.2505029068901229 0.3536716767705401 0.2846319005740705 0.4048963732696784 0.23416675529632336 0.7117425709133748 0.32659425977015777 0.6036240929410239 0.3757328602087735 0.4207636501789066 0.27734279605351964 0.22368449622617337 0.41382485417779624 0.17562503520702089 0.2051599498294221 0.2010130050064799 0.23587847720487543 0.17146079523470736 0.16895134121827926 0.25063566907395096 0.1652529483489017 0.23771021331831899 0.1810456734523036 0.18023204435638107 0.1725708525795212 0.16406963124569918 0.18584412569912834 0.17683187863702776 0.1735623719273831 0.1550019397217035 0.14806604658635425 0.1507993144872969 0.1490106926352196 0.14432686872870737 0.14404729342162104 0.14622471395510495 0.13570818588218603 0.1356225931350501 0.13200452186569378 0.12750747035753818 0.13070779041253122 0.12877593275011923 0.12374296337694235 0.12484589971032944 0.12521564695558285 0.12413459710477981 0.11897639010541661 0.12039147423458374 0.12126319661473617 0.12393782478950521 0.11779169005307055 0.11396485517123245 0.11556638728798307 0.11461738141559685 0.11602691969805257 0.11470415743222362 0.11334605893180849 0.11486601148061337 0.11435201908257508 0.11044789680438138 0.10984355504305898 0.1107608621336662 0.11009369935176039 0.10666947762664791 0.10511372700142474 0.10575526422822235 0.10447175325333324 0.10228855181546703 0.1033959194938675 0.1029578651158185 0.09963625975177148 0.09885243842558411 0.10288363070882056 0.0992717216790826 0.09684659140265273 0.09514744804743414 0.09480820571528747 0.0948134403536246 0.09528051549952554 0.09428016968933806 0.09651671356293257 0.0935585640991401 0.09118435888457954 0.08945284901003236 0.08663670708316762 0.08513398805372907 0.08258027255682154 0.08188137548967267 0.08040224687457864 0.07797420808252442 0.07849678408947225 0.07765780316171755 0.08107532961962949 0.07674754478818968 0.07481340066885611 0.07838451941080737 0.0738594324717563 0.07481914112221955 0.07386024942022384 0.07349330718036184 0.07413570276992332 0.07379611672505959 0.07336811830692302 0.07352046440449445 0.07345550855784255 0.0724344505299721 0.07131743090002529 0.07052456831403142 0.06994705560548609 0.07065273525938554 0.06996355336428073 0.07053176239605008 0.06974122393004811 0.07010494236634801 0.06892999687519175 0.06747217325124477 0.06820985036832383 0.06766036762407734 0.06692360880886039 0.06610624692331324 0.06552676987490225 0.06605265551337647 0.06609588796877819 0.0659587013296713 0.06649546083522961 0.06643398604204998 0.06373353828697759 0.06284452836480003 0.0622423847785295 0.0627198146786283 0.06250458422181959 0.0629864420664954 0.0623013445248094 0.06102757361156719 0.060363489156702754 0.059761013697851584 0.059529926594358074 0.05919151336820261 0.057451782554649555 0.05695095071027223 0.056031851466833205 0.055064399881422865 0.054726666092292264 0.0552910411942642 0.05438687098014283 0.05515414759342385 0.05366328237089557 0.05456663366119463 0.053649543425571156 0.05260588437310276 0.05332145686013989 0.052696896625595846 0.05260106957763555 0.05196392958239704 0.05229431028196152 0.05189287612581628 0.051288361575417385 0.05093149150362464 0.05035310384468894 0.04994514051097865 0.04923976788184781 0.048676220094953426 0.04806504566800085 0.04755653834162965 0.0474374497817327 0.04732513386151105 0.04720891168832286 0.04716291980885301 0.0465808704843611 0.04597590562471554 0.045370423544424 0.04506277972386796 0.044740613474143376 0.04434751022573735 0.044599606239073886 0.04465438453199788 0.04405580071371318 0.0441243234799803 0.043512165375859 0.04293196866243332 0.04258618284607574 0.043159533998749944 0.04271402333241059 0.04243364647557679 0.042071270785810855 0.042170434546728974 0.041492238079240296 0.0414481331200941 fun: 0.0414481331200941 maxcv: 0.0 message: 'Maximum number of function evaluations has been exceeded.' nfev: 200 status: 2 success: False x: array([1.6090495 , 2.06886548, 1.38891826, 1.73735143, 3.00379065, 1.54485063, 2.27911576, 1.68438631, 1.22546245]) (0.9585518668702525-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 AA, with the same matrix UU:

#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('aer_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('aer_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) circ.save_statevector() backend = Aer.get_backend('aer_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]]) a3 = np.add(np.add(a2, a0), a1) 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.11200545382922444 0.09019590673257383 0.084420044535651 0.0935489449253396 0.13053393753196751 0.09775368517057148 0.0878815157716909 0.12603424565199128 0.0921710376654844 0.08183249981959462 0.04946167511271249 0.05328022155561529 0.06271262831552882 0.043848791220488015 0.046331550823754175 0.046896535590745914 0.04445014861604002 0.04454286004965602 0.038789335517067536 0.037074449481376504 0.04105201192834784 0.04169246663147208 0.03320238011771148 0.033637102688513054 0.035932890967591335 0.03684190158379119 0.03238579537972208 0.03214710518463426 0.03142620960111109 0.0317210292755119 0.032923352873836076 0.032765263541752576 0.03227625764794939 0.03261315885676541 0.03261324427960599 0.03132719835219744 0.03176833609501506 0.03176172925445275 0.03175634689747042 0.031651448450159436 0.0311419734656998 0.03188024061783645 0.031616112768612536 0.030690632433164833 0.03127350054411926 0.031385372951870494 0.03151376147612972 0.03132120455186127 0.031435279311042286 0.03152866121433162 0.03074654201293947 0.03149934924841069 0.03124451940566364 0.031122047906554373 0.031857328510763616 0.03131994648293024 0.03150027766910801 0.031814149823458315 0.031554728687447686 0.0305464887686121 0.031205326223329166 0.03096744904519888 0.032194352131026616 0.03134873742564126 0.03142619670205038 0.030951978306888006 0.0321422663274199 0.031210715424008773 0.031824715700533024 0.031478259401082065 0.030959414165635524 0.031838566011639036 0.03141118564770495 0.03160712560267187 0.030951191397417976 0.03098863573948729 0.03138241512575268 0.0310593060064851 0.030815264459396086 0.031685252451904344 0.03148243846255294 0.031654859371498145 0.032014563263113804 0.031209064668742403 0.03169399478974655 0.032076060479113044 0.031054761463022773 0.030917034481992456 0.03172707349616066 0.031243342627772064 0.030728111831267335 0.03043416091446094 0.030957126603245233 0.031028176606096136 0.031193013357644572 0.031083654673426553 0.03112310910654137 0.031717587729768404 0.03127494856462032 0.03156330582171374 0.031625147392619346 0.030973059338494036 0.031148095173009316 0.031327071264894646 0.031298982655805885 0.031917216119208947 0.031165960915188196 fun: 0.031165960915188196 maxcv: 0.0 message: 'Optimization terminated successfully.' nfev: 107 status: 1 success: True x: array([ 3.60955579, 1.06944298, 1.6183139 , -1.19248748, 0.11895816, 2.13073899, -0.32517705, 2.45498693, 2.13752322]) (0.740802845445537+0j)

So as you can see, not amazing, our solution is still off by a fairly significant margin ( 3%~3\% 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.tools.jupyter %qiskit_version_table