-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathVQE.idr
298 lines (234 loc) · 12.6 KB
/
VQE.idr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
module VQE
import Data.Nat
import Data.Vect
import Unitary
import Control.Linear.LIO
import Lemmas
import QStateT
import Injection
import LinearTypes
import Complex
import System.Random
import QuantumOp
import RandomUtilities
%default total
-- Example of code where quantum and classical information interact
-- The VQE (Variational Quantum Eigensolver) algorithm finds the lowest eigenvalue for an Hamiltonian operator
-- More precisely, we suppose that the eigenstate corresponding to the lowest eigenvalue is given by some ansatz
-- that depends on some parameters, and we optimize the value of the parameters, to find the eigenvalue.
-- The next lines of code explain how the ansatz is given in general. The ansatz has some depth p, and all parameters
-- the ansatz is depending on are given by two matrices of angles, represented by the RotationAnglesMatrix type.
||| Type for the matrices of rotation angles
|||
||| Matrix of size (n+1) * m
public export
RotationAnglesMatrix : Nat -> Nat -> Type
RotationAnglesMatrix n m = Vect (S n) (Vect m Double)
||| Unitary operator used for linear entanglement
|||
||| n -- The arity of the operator
export
linearEntanglement : (n : Nat) -> Unitary n
linearEntanglement 0 = IdGate
linearEntanglement 1 = IdGate
linearEntanglement (S (S n)) =
let circ = IdGate {n = 1} # linearEntanglement (S n)
in circ . (CNOT 0 1 IdGate)
||| Unitary operator, tensor product of phase gates
|||
||| n -- The arity of the operator
||| phases -- The vector of phases
tensorRzs : {n : Nat} -> (phases : Vect n Double) -> Unitary n
tensorRzs phases = tensorMapSimple (map (\phase => RzGate phase) phases)
||| Unitary operator, tensor product of Ry gates
|||
||| n -- The arity of the operator
||| phases -- The vector of phases for the Ry gates
tensorRys : {n : Nat} -> (phases : Vect n Double) -> Unitary n
tensorRys phases = tensorMapSimple (map (\phase => RyGate phase) phases)
||| The overall unitary operator for VQE, the ansatz
|||
||| n -- The arity of the operator
||| depth -- The depth of the ansatz, i.e. the number of repetitions of the pattern
||| phasesRy -- The vector of phases for all the Ry rotations
||| phasesRz -- The vector of phases for all the phase gates
||| output -- The overall unitary operator for VQE, the ansatz
export
ansatz : (n : Nat) -> (depth : Nat) ->
(phasesRy : RotationAnglesMatrix depth n) ->
(phasesRz : RotationAnglesMatrix depth n) ->
Unitary n
ansatz n 0 [phaseRy] [phaseRz] = tensorRzs phaseRz . tensorRys phaseRy
ansatz n (S d) (phaseRy :: phasesRy) (phaseRz :: phasesRz) =
let circ1 = ansatz n d phasesRy phasesRz
in circ1 . linearEntanglement n . tensorRzs phaseRz . tensorRys phaseRy
------------- THE HAMILTONIAN -------------------------------------------------------
-- The ansatz gives us some vector |psi>, that depends on some parameters
-- We want to find the minimum value of <psi|H|psi> over all possible parameters
-- To do that, we need a way to compute <psi|H|psi>.
-- This is easy if H is an tensor product of Pauli Matrices. In this case, one can build a circuit U_H
-- such that, if we build the circuit U_H|psi> and measure the first qubit, then
-- <psi|H|psi> is equal to x - y where x (resp.y) is the probability of measuring 0 (resp. 1)
-- More generally, if H is a sum of tensor product of Pauli Matrices, say H = a_1 H_1 + ... a_k H_k
-- then one can obtain <psi|H|psi> by computing separately each <psi|H_i|psi> using the previous idea
-- As a consequence, for all intent and purposes here, we suppose that Hamiltonians are given by linear combinations of
-- tensor product of Pauli Matrices.
-- This is represented by the type Hamiltonian below, that is a list of pairs (coefficient, tensor product of pauli matrix)
||| Data type for the the Pauli matrices X,Y,Z and Identity
public export
data PauliAtomic : Type where
PauliI : PauliAtomic
PauliX : PauliAtomic
PauliY : PauliAtomic
PauliZ : PauliAtomic
||| Tensor product of n Pauli matrices represented as a vector
||| n -- number of qubits
public export
PauliBasis : Nat -> Type
PauliBasis n = Vect n PauliAtomic
||| Decomposition of Hamiltonian as sum of Pauli matrices
||| Hamiltonian = a_1P_1 + a_2P_2 + ... + a_kP_k
||| n -- number of qubits
public export
Hamiltonian : Nat -> Type
Hamiltonian n = List (Double, PauliBasis n)
-- Now the next lines of code explain how to obtain the value of <psi|H|psi>, first for a tensor product of Pauli matrices, then more generally.
-- First here is the definition of U_H, the unitary s.t. measuring U_H|psi> makes us able to compute <psi|H|psi>:
--------------------- COMPUTE THE ENERGY ------------------------------
-- The value of the energy <psi|H|psi> can be computed by applying a unitary operator U_H to |psi> and mesuring the first qubit
-- Here the operator U_H uses one more qubits than H. It is possible to use the exact same number of qubits, but the code is easier this way.
||| With a tensor product of Pauli matrices, compute the corresponding unitary operator U_H
|||
||| n -- number of qubits
||| h -- vector that represents the tensor product of Pauli matrices
||| output -- U_h
encodingUnitary : {n : Nat} -> (h : PauliBasis n) -> Unitary (S n)
encodingUnitary [] = IdGate
encodingUnitary {n = S k} (PauliI :: xs) = rewrite sym $ lemmaplusOneRight k in (encodingUnitary xs) # IdGate {n=1}
encodingUnitary {n = S k} (PauliX :: xs) =
let p1 = lemmakLTSk k
in rewrite sym $ lemmaplusOneRight k in CNOT (S k) 0 (H (S k) ((encodingUnitary xs) # IdGate {n=1}))
encodingUnitary {n = S k} (PauliY :: xs) =
let p1 = lemmakLTSk k
in rewrite sym $ lemmaplusOneRight k in CNOT (S k) 0 (H (S k) (S (S k) ((encodingUnitary xs) # IdGate {n=1})))
encodingUnitary {n = S k} (PauliZ :: xs) =
let p1 = lemmakLTSk k
in rewrite sym $ lemmaplusOneRight k in CNOT (S k) 0 ((encodingUnitary xs) # IdGate {n=1})
-- the next function computes <psi|H|psi>, for H a tensor product of Pauli Matrices
-- This is the function that needs to run the quantum circuit
-- As explained above, to obtain <psi|H|psi>, we produce a circuit for U_H|psi> and then measure the first qubit
-- The value of <psi|H|psi> is then equal to the probability of measuring 0 minus the probability of measuring 1
-- the following code runs the whole experiment nSamples times,
-- and records the number of time we measure 0 and the number of times we measure 1 and returns the difference between the two quantities
-- if nSamples is big enough, then <psi|H|psi> is therefore well approximated by the result of this function divided by the number of samples.
||| Computes an approximation of <psi|H|psi> when H is a tensor product of Pauli Matrices
||| More precisely, <psi|H|psi> is equal to the result of this function divided by nSamples
|||
||| n -- number of qubits
||| p -- tensor product of Pauli matrices
||| nSamples -- the number of time we sample
||| circuit -- the state |psi>
||| output -- computed energy
computeEnergyPauli : QuantumOp t => (n : Nat) -> (p : PauliBasis n) -> (nSamples : Nat) -> (circuit : Unitary n) -> IO Double
computeEnergyPauli n p 0 circuit = pure 0
computeEnergyPauli n p (S nSamples) circuit = do
let encodingCircuit = encodingUnitary p
(b :: _) <- run (do
qs <- newQubits {t} (S n)
qs <- applyUnitary qs ( (IdGate {n=1}) # circuit)
qs <- applyUnitary qs encodingCircuit
measureAll qs
)
rest <- computeEnergyPauli {t} n p nSamples circuit
if (not b) then pure $ 1 + rest else pure $ rest - 1
-- the next function computes <psi|H|psi> for a general hamiltonian H, expressed as a linear combination of tensor product of Pauli Matrices
-- we just have to call the function computeEnergyPauli for each element inside the linear combination:
||| Compute an approximation of <psi|H|psi> for a general Hamiltonian H
|||
||| n -- number of qubits
||| h -- the hamiltonian of the problem
||| nSamples -- the number of time we sample for each component of H
||| circuit -- the state |psi>
||| output -- computed energy
computeEnergy : QuantumOp t => (n : Nat) -> (h : Hamiltonian n) -> (nSamples : Nat) -> (circuit : Unitary n) -> IO Double
computeEnergy n [] nSamples circuit = pure 0
computeEnergy n ((r, p) :: hs) nSamples circuit = do
res1 <- computeEnergy {t} n hs nSamples circuit
res2 <- computeEnergyPauli {t} n p nSamples circuit
pure $ res1 + r*res2/(cast nSamples)
--------------------- CLASSICAL OPTIMISATION ------------------------
--- The energy that is compute by the function computeEnergy is fed to the function classicalOptimisation before
--- That will produces new parameters for the ansatz to try, based on the value of <psi|H|psi> on all previous runs
--- This function should use general optimisation techniques, like for instance the Nelder-Mead method
--- For simplicity, we opted instead for a function that returns random parameters
||| Generate a matrix of size (n+1) * m of random Double
export
randomMatrix : (n : Nat) -> (m : Nat) -> IO (RotationAnglesMatrix n m)
randomMatrix 0 m = do
xs <- randomVect m
pure [xs]
randomMatrix (S n) m = do
xs <- randomVect m
ys <- randomMatrix n m
pure (xs :: ys)
||| The (probabilistic) classical optimisation procedure for VQE.
||| IO output allows us to use probabilistic optimisation procedures.
||| Given all previously observed information, determine new rotation angles for the next VQE run.
||| We ask for the function to be able to return also parameters for the very first ansatz (that is, the function
||| will be called for the first time with an empty vector of previous information)
||| Remark: we randomly generate the next rotation angles for simplicity.
|||
||| k -- number of previous iterations of the algorithm
||| n -- arity of the ansatz circuit
||| depth -- depth of the ansatz circuit
||| h -- hamiltonian of the problem
||| previous_info -- previously used parameters and measurement outcomes
||| output -- new rotation angles for the next run of VQE
classicalOptimisation : {n : Nat} -> (depth : Nat) ->
(h : Hamiltonian n) ->
(previous_info : Vect k (RotationAnglesMatrix depth n, RotationAnglesMatrix depth n, Double)) ->
IO (RotationAnglesMatrix depth n, RotationAnglesMatrix depth n)
classicalOptimisation depth h previous_info = do
phasesRy <- randomMatrix depth n
phasesRz <- randomMatrix depth n
pure (phasesRy, phasesRz)
-----------------------PUTTING QUANTUM AND CLASSICAL PARTS TOGETHER -------------------------
-- We now put everything together
-- This function will run the whole process k times, computing <psi|H|psi> at each step, and calling the classical optimiser to obtain
-- the next value of |psi> to test
||| Helper function for VQE
||| n -- number of qubits
||| h -- the hamiltonian of the problem
||| nSamples -- number of times we sample to compute <psi|H|psi>
||| k -- number of iterations of the algorithm
||| depth -- depth of the ansatz circuit
||| output -- all observed information : rotation angles and computed energies
VQE': QuantumOp t =>
(n : Nat) -> (h : Hamiltonian n) -> (nSamples : Nat) -> (k : Nat) -> (depth : Nat) ->
IO (Vect k (RotationAnglesMatrix depth n, RotationAnglesMatrix depth n, Double))
VQE' n h nSamples 0 depth = pure []
VQE' n h nSamples (S k) depth = do
previous_info <- VQE' {t} n h nSamples k depth
(phasesRy, phasesRz) <- classicalOptimisation depth h previous_info
let circuit = ansatz n depth phasesRy phasesRz
energy <- computeEnergy {t} n h nSamples circuit
pure $ (phasesRy, phasesRz, energy) :: previous_info
--- The VQE algorithm just calls the previous function, and returns the minimum value of <psi|H|psi> that was observed
--- Note that, in practice, if the classical Optimiser does their job correctly, the minimum value of <psi|H|psi>
--- is very likely to be obtained in the last iteration. As we use a random function instead of an optimiser, this is
--- not true, so we take the minimum among all runs.
||| VQE algorithm
||| n -- number of qubits
||| h -- the hamiltonian of the problem
||| nSamples -- number of times we sample to compute <psi|H|psi>
||| k -- number of iterations of the algorithm
||| depth -- depth of the ansatz circuit
||| output -- the lowest computed energy
export
VQE : QuantumOp t =>
(n : Nat) -> (h : Hamiltonian n) -> (nSamples : Nat) -> (k : Nat) -> (depth : Nat) ->
IO Double
VQE n h nSamples k depth = do
observed_info <- VQE' {t=t} n h nSamples (S k) depth
let energies = map (\(_, _, r) => r) observed_info
pure $ foldl min (head energies) energies