-
Notifications
You must be signed in to change notification settings - Fork 0
/
simulation.py
378 lines (281 loc) · 13.4 KB
/
simulation.py
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
import numpy as np
import math
class Simulation:
"""
METHODS
__init__: Initialises a simulation object, sets up initial state, unitary matrix and observation matrix.
get_initial_state: Returns the initial state vector describing the system.
get_current_state: Returns the current state vector describing the system.
get_unitary_matrix: Returns the unitary matrix describing evolution.
get_obs_matrix: Returns the observation matrix.
apply_cycle: Applies one unitary evolultion cycle.
apply_obs: Returns the probability that the current state has a bound state.
check_unitary: Checks is a matrix is unitary.
check_norm: Cehcks if a vector is normalised.
"""
def __init__(self, N = 3, L_sites = 6, theta = np.pi, phi = 2 * np.pi / 3, theta_p = 0, phi_p = 2 * np.pi / 3, debug = False):
"""
Initialises a simulation object, sets up initial state, unitary matrix and observation matrix.
INPUTS
N: Number of excited states. Positive integer 1-L.
L_sites: Number of sites along the main chain. Positive even integer 4, 6, 8 or 10.
theta: Kinetic term for main chain fSim. Radians.
phi: Interaction term for main chain fSim. Radians.
theta_p: Kinetic term for decorator fSim. Radians.
phi_p: Interaction term for decorator fSim. Radians.
debug: Option to turn debugging on. Boolean.
OUTPUTS
None
"""
self._N = N # The number of excited qbits
self._L_sites = L_sites # Has to be larger than 2, needs to be even
self._theta = theta # The amplitude representing kinetic / hopping energy in radians
self._phi = phi # The phase angle representing interaction strength in radians
self._theta_p = theta_p # Theta term for decorators
self._phi_p = phi_p # Phi term for decorators
self.debug = debug # If debug is True it will check that matrices are unitary and vectors and normalised
if self._N > self._L_sites:
raise Exception("You cannot have more excited states then there are sites")
if self._L_sites < 4:
raise Exception("You need to have at least 4 sites")
if self._L_sites % 2 == 1:
raise Exception("It has to be an even number of sites")
if self._N < 0:
raise Exception("You cannot have a negative number of excited states")
self.__initial_state = self.__create_initial_state() # Gets the initial state of the system
self.__current_state = self.__initial_state # At the start current state is equal to inital state
self.__unitary_matrix = self.__create_unitary_matrix() # Gets the unitary evolution matrix
self.__obs_matrix = self.__create_obs_matrix() # Gets the matrix that performs the observation
def get_initial_state(self):
"""
Returns the initial state vector describing the system.
INPUTS
None
OUTPUTS
Initial state vector
"""
return self.__initial_state
def get_current_state(self):
"""
Returns the current state vector describing the system.
INPUTS
None
OUTPUTS
Current state vector
"""
return self.__current_state
def get_unitary_matrix(self):
"""
Returns the unitary matrix describing evolution.
INPUTS
None
OUTPUTS
Unitary matrix
"""
return self.__unitary_matrix
def get_obs_matrix(self):
"""
Returns the observation matrix.
INPUTS
None
OUTPUTS
Observation matrix
"""
return self.__obs_matrix
def apply_cycle(self):
"""
Applies one unitary evolultion cycle.
INPUTS
None
OUTPUTS
None
"""
self.__current_state = np.matmul(self.__unitary_matrix, self.__current_state) # Pre multiplies the state by the unitary matrix
if self.debug == True: # Checks result is normalised
if self.check_norm(self.__current_state) == False:
raise Exception("The vector is not normalised")
def apply_obs(self):
"""
Returns the probability that the current state has a bound state.
INPUTS
None
OUTPUTS
Bound state probability of the current state
"""
state_t = np.transpose(self.__current_state)
state_h = np.conjugate(state_t) # Finds the conjugate tranpose of the current state
probability = np.dot(state_h, np.dot(self.__obs_matrix, self.__current_state)) # Applys the observation to the current state
return np.real(probability) # Returns the probability
def __create_initial_state(self):
"""
Creates the inital state vector describing the system.
INPUTS
None
OUTPUTS
Initial state vector
"""
mid_point = math.ceil(self._L_sites / 2) - 1 # Finds the lower midpoint of the sites
odd = (self._L_sites % 2)
start = (mid_point - math.floor((self._N - 1 + odd) / 2))
end = self._L_sites - self._N - start
self._circuit = start * "0" + self._N * "1" + end * "0" # Creates a bit string of 0's with N 1's in the middle
for i in range(0, self._L_sites):
if i % 2 == 1:
self._circuit = self._circuit[:(i + 1 + math.floor(i/2))] + "0" + self._circuit[(i + 1 + math.floor(i/2)):] # Inserts decorators, need to account for index increasing each addition
# Creates the basis vector of all 0's with a 1 positioned at the correct index
index = int(self._circuit, 2)
state = np.zeros(2 ** int( 1.5 *self._L_sites)) # 1.5 times because there is one decorator for every other main site
state[index] = 1
return state
def __create_unitary_matrix(self):
"""
Creates the unitary matrix that describes system evolution.
INPUTS
None
OUTPUTS
Unitary matrix
"""
# Sets up inital matrices that will act on odd, even and decorator sites
dim = 2 ** int(1.5 * self._L_sites) # Dimensions of the final matrix
odd_matrix = np.identity(dim)
even_matrix = np.identity(dim)
dec_matrix = np.identity(dim)
# i is the site (starting at zero) where the fsim gate acts on
for i in range(0, self._L_sites):
if i % 2 == 1: # Decorations case
m_before = np.identity(2 ** int(1.5 * i))
m_middle = 1 # Nothing in middle
m_after = np.identity(2 ** int(math.ceil(1.5 * (self._L_sites - i) - 2))) # Note ceil not floor
sub_dec_matrix = self.__create_sub_matrices(self._theta_p, self._phi_p, m_before, m_middle, m_after)
if self.debug == True: # Checks the sub matrix is unitary
if self.check_unitary(sub_matrix) == False:
raise Exception("The decoration matrix at index " + str(i) + " is not unitary")
# For the case when the fsim is acting on the edges
if i == self._L_sites - 1:
# Get the matrix in the middle as such
m_before = 1 # Nothing before
m_middle = np.identity(2 ** int(1.5 * self._L_sites - 3)) # Minus 3 because of the "edges" that it acts on and then an identity to the RHS
m_after = np.identity(2)
sub_matrix = self.__create_sub_matrices(self._theta, self._phi, m_before, m_middle, m_after)
else: # odd and even case
m_before = np.identity(2 ** int(1.5 * i))
m_middle = np.identity(2 ** (i % 2)) # % handles odd and even
m_after = np.identity(2 ** int(1.5 * (self._L_sites - i) - 2)) # int floors and handles odd and even
sub_matrix = self.__create_sub_matrices(self._theta, self._phi, m_before, m_middle, m_after)
if self.debug == True: # Checks the sub matrix is unitary
if self.check_unitary(sub_matrix) == False:
raise Exception("The sub_matrix with fsim at index " + str(i) + " is not unitary")
if i % 2 == 0: # If even, multiply to the even matrix
even_matrix = np.matmul(even_matrix, sub_matrix)
else: # If odd, multiply to the odd matrix
odd_matrix = np.matmul(odd_matrix, sub_matrix)
dec_matrix = np.matmul(dec_matrix, sub_dec_matrix)
unitary_matrix = np.matmul(np.matmul(dec_matrix, odd_matrix), even_matrix) # Multiply the odd and even matrices together
if self.debug == True: # Check the unitary evolution matrix is unitary
if self.check_unitary(unitary_matrix) == False:
raise Exception("The unitary_matrix is not unitary")
return unitary_matrix
def __create_obs_matrix(self):
"""
Creates the observation matrix.
INPUTS
None
OUTPUTS
Observational matrix
"""
dim = 2 ** int(1.5 * self._L_sites)
obs_matrix = np.zeros((dim, dim)) # Creates a matrix of all zeros with dimensions of the observation matrix
# Consider all excited, L_sites = N? Might already be handled
for i in range(0, self._L_sites):
counter = self._L_sites - self._N - i # Calculate a counter used for positioning 0's and 1's in the bit string
if counter >= 0: # For the case where the last state is 0's
bit_string = i * "0" + self._N * "1" + counter * "0"
else: # For the case where last state is a 1
bit_string = (0 - counter) * "1" + (i + counter) * "0" + (self._N + counter) * "1"
for j in range(0, self._L_sites):
if j % 2 == 1:
bit_string = bit_string[:(j + 1 + math.floor(j/2))] + "0" + bit_string[(j + 1 + math.floor(j/2)):] # Inserts decorators, need to account for index increasing each addition
index = int(bit_string, 2)
obs_matrix[index, index] = 1 # Inserts 1's into the correct index in the observation matrix
return obs_matrix
def __create_sub_matrices(self, theta, phi, m_before, m_middle, m_after):
# Sets up the swap matrices we need to use
p = np.array([
[1, 0],
[0, 0]
])
q = np.array([
[0, 0],
[0, 1]
])
sigma_x = np.array([
[0, 1],
[1, 0]
])
sigma_y = np.array([
[0, -1j],
[1j, 0]
])
sigma_z = np.array([
[1, 0],
[0, -1]
])
dim = 2 ** int(1.5 * self._L_sites)
# Breakes the fsim down into swap matrices
m_1 = self.__large_kron(p, m_before, m_middle, m_after)
m_2 = np.exp(1j * phi) * self.__large_kron(q, m_before, m_middle, m_after)
m_3 = 0.5 * np.cos(theta) * (np.identity(dim) - self.__large_kron(sigma_z, m_before, m_middle, m_after))
m_4 = 0.5 * 1j * np.sin(theta) * (self.__large_kron(sigma_x, m_before, m_middle, m_after) + self.large_kron(sigma_y, m_before, m_middle, m_after))
sub_matrix = m_1 + m_2 + m_3 + m_4 # Adds all these matrcies together
return sub_matrix
def __large_kron(self, m_swap, m_before, m_middle, m_after):
"""
Takes the tensor product of of m_before, m_swap, m_middle, m_swap and m_after.
INPUTS
m_swap: The relevent swap matrix.
m_before: An identity matrix that goes first in the tensor product.
m_middle: An identity matrix that goes in the middle of the tesnor product.
m_after: An identity matrix that goes last in the tensor product.
OUTPUTS
The tensor product of these matrices
"""
return np.kron(np.kron(np.kron(np.kron(m_before, m_swap), m_middle), m_swap), m_after)
@staticmethod
def check_unitary(matrix):
"""
Checks if a matrix is unitary
INPUTS
matrix: The matrix we want to check if unitary.
OUTPUTS
A boolean denoting whether the matrix is unitary.
"""
if matrix.shape[0] != matrix.shape[1]: # Check if square matrix
return False
# Finds the conjugate tranpose
matrix_t = np.transpose(matrix)
matrix_h = np.conjugate(matrix_t)
result = np.matmul(matrix_h, matrix) # Multiply conjugate tranpose by original
result = result.round(10) # Round each element to 10 decimal places to account for floating point errors
# Check if result is real and equals the identity matrix
size = result.shape[0]
if np.all(np.imag(result) == 0) and np.allclose(result, np.identity(size)):
return True
return False
@staticmethod
def check_norm(vector):
"""
Checks if a vector is normalised.
INPUTS
vector: The vector we want to check if normalised.
OUTPUTS
A boolean denoting whether the vector is normalised.
"""
# Finds the conjugate tranpose
vector_t = np.transpose(vector)
vector_h = np.conjugate(vector_t)
dot = np.vdot(vector_t, vector) # Performs dot product of conjuage tranpose and the vector
dot = dot.round(10) # Round each element to 10 decimal places to account for floating point errors
# Checks result is equal to 1
if np.imag(dot) == 0 and np.real(dot) == 1:
return True
return False