kmclab 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kmclab/square.py ADDED
@@ -0,0 +1,1078 @@
1
+ import numpy as np
2
+ from matplotlib.animation import FuncAnimation, PillowWriter
3
+ import matplotlib.cm as cm
4
+ from numpy.polynomial.polynomial import Polynomial
5
+ import matplotlib.pyplot as plt
6
+ import matplotlib.patches as patches
7
+ from scipy.stats import linregress
8
+ from __future__ import annotations
9
+
10
+
11
+
12
+ class square_kmc:
13
+
14
+ def __init__(self, n_atoms, n_defects, n_adsorbates, lattice_size, n_steps, defect_type = 1, adsorbates_freq = 0, seed=1):
15
+
16
+ np.random.seed(seed)
17
+ # what is you desired coverage= n_atom/lattice * lattice
18
+ self.n_atoms = n_atoms # Number of atoms
19
+ self.n_defects = n_defects
20
+ self.n_adsorbates= n_adsorbates
21
+ self.lattice_size = lattice_size # Lattice size
22
+ self.n_steps = n_steps # Number of steps
23
+ self.defect_type = defect_type
24
+ self.adsorbates_freq = adsorbates_freq
25
+ T = 300 # Temperature in Kelvin
26
+ k_B = 8.617e-5 # Boltzmann constant in eV/K
27
+ k_0 = 1
28
+ h = 4.1357e-15 #Planck Constant (eV.s)
29
+ self.len_vertical = 0.297e-3 # in micrometer
30
+ self.len_horizontal = 0.660e-3 # in micrometer
31
+
32
+ # DFT calculated vaues for diffusion on the stoichiometric surface in different directions (eV)
33
+ energy_barrier_north = 0.26
34
+ energy_barrier_south = 0.26
35
+ energy_barrier_east = 0.91
36
+ energy_barrier_west = 0.91
37
+ energy_barrier_northeast = 0.91
38
+ energy_barrier_northwest = 0.91
39
+ energy_barrier_southeast = 0.91
40
+ energy_barrier_southwest = 0.91
41
+
42
+ # DFT calculated vaues for diffusion out of trapping deffect in different directions (eV)
43
+ energy_barrier_trapping_defect_north = 0.99
44
+ energy_barrier_trapping_defect_south = 0.99
45
+ energy_barrier_trapping_defect_northeast = 0.99
46
+ energy_barrier_trapping_defect_northwest = 0.99
47
+ energy_barrier_trapping_defect_southeast = 0.99
48
+ energy_barrier_trapping_defect_southwest = 0.99
49
+
50
+ # DFT calculated vaues for diffusion out of trapping deffect in different directions (eV)
51
+ energy_barrier_blocking_defect_north = 0.99
52
+ energy_barrier_blocking_defect_south = 0.99
53
+ energy_barrier_blocking_defect_east = 0.99
54
+ energy_barrier_blocking_defect_west = 0.99
55
+ energy_barrier_blocking_defect_northeast = 0.99
56
+ energy_barrier_blocking_defect_northwest = 0.99
57
+ energy_barrier_blocking_defect_southeast = 0.99
58
+ energy_barrier_blocking_defect_southwest = 0.99
59
+
60
+
61
+ # DFT calculated vaues for diffusion over an adsorbate in different directions (eV)
62
+
63
+ energy_barrier_adsorbate_north = 0.72
64
+ energy_barrier_adsorbate_south = 0.72
65
+ energy_barrier_adsorbate_east = 0.72
66
+ energy_barrier_adsorbate_west = 0.72
67
+ energy_barrier_adsorbate_northeast = 0.72
68
+ energy_barrier_adsorbate_northwest = 0.72
69
+ energy_barrier_adsorbate_southeast = 0.72
70
+ energy_barrier_adsorbate_southwest = 0.72
71
+
72
+ # Calculate rate constants
73
+ rate_north = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_north / (k_B * T))
74
+ rate_south = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_south / (k_B * T))
75
+ rate_east = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_east / (k_B * T))
76
+ rate_west = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_west / (k_B * T))
77
+ rate_northeast = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_northeast / (k_B * T))
78
+ rate_northwest = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_northwest / (k_B * T))
79
+ rate_southeast = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_southeast / (k_B * T))
80
+ rate_southwest = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_southwest / (k_B * T))
81
+
82
+ rate_trapping_defect_north = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_trapping_defect_north / (k_B * T))
83
+ rate_trapping_defect_south = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_trapping_defect_south / (k_B * T))
84
+ rate_trapping_defect_northeast = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_trapping_defect_northeast / (k_B * T))
85
+ rate_trapping_defect_northwest = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_trapping_defect_northwest / (k_B * T))
86
+ rate_trapping_defect_southeast = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_trapping_defect_southeast / (k_B * T))
87
+ rate_trapping_defect_southwest = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_trapping_defect_southwest / (k_B * T))
88
+
89
+ rate_blocking_defect_north = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_blocking_defect_north / (k_B * T))
90
+ rate_blocking_defect_south = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_blocking_defect_south / (k_B * T))
91
+ rate_blocking_defect_east = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_blocking_defect_east / (k_B * T))
92
+ rate_blocking_defect_west = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_blocking_defect_west / (k_B * T))
93
+ rate_blocking_defect_northeast = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_blocking_defect_northeast / (k_B * T))
94
+ rate_blocking_defect_northwest = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_blocking_defect_northwest / (k_B * T))
95
+ rate_blocking_defect_southeast = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_blocking_defect_southeast / (k_B * T))
96
+ rate_blocking_defect_southwest = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_blocking_defect_southwest / (k_B * T))
97
+
98
+ rate_adsorbate_north = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_adsorbate_north / (k_B * T))
99
+ rate_adsorbate_south = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_adsorbate_south / (k_B * T))
100
+ rate_adsorbate_east = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_adsorbate_east / (k_B * T))
101
+ rate_adsorbate_west = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_adsorbate_west / (k_B * T))
102
+ rate_adsorbate_northeast = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_adsorbate_northeast / (k_B * T))
103
+ rate_adsorbate_northwest = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_adsorbate_northwest / (k_B * T))
104
+ rate_adsorbate_southeast = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_adsorbate_southeast / (k_B * T))
105
+ rate_adsorbate_southwest = k_0 * ((k_B * T)/h) * np.exp(-energy_barrier_adsorbate_southwest / (k_B * T))
106
+
107
+
108
+
109
+
110
+ self.moves = {
111
+ "north": (0, 1),
112
+ "south": (0, -1),
113
+ "east":(1, 0),
114
+ "west": (-1, 0),
115
+ "northeast": (1, 1),
116
+ "northwest": (-1, 1),
117
+ "southeast": (1, -1),
118
+ "southwest": (-1, -1)
119
+ }
120
+
121
+ self.move_rates = {
122
+ "north": rate_north,
123
+ "south": rate_south,
124
+ "east": rate_east,
125
+ "west": rate_west,
126
+ "northeast": rate_northeast,
127
+ "northwest": rate_northwest,
128
+ "southeast": rate_southeast,
129
+ "southwest": rate_southwest
130
+ }
131
+
132
+
133
+ self.move_rates_trapping_defects = {
134
+ "north": rate_trapping_defect_north,
135
+ "south": rate_trapping_defect_south,
136
+ "northeast": rate_trapping_defect_northeast,
137
+ "northwest": rate_trapping_defect_northwest,
138
+ "southeast": rate_trapping_defect_southeast,
139
+ "southwest": rate_trapping_defect_southwest
140
+ }
141
+
142
+ self.move_rates_blocking_defects = {
143
+ "north": rate_blocking_defect_north,
144
+ "south": rate_blocking_defect_south,
145
+ "east": rate_blocking_defect_east,
146
+ "west": rate_blocking_defect_west,
147
+ "northeast": rate_blocking_defect_northeast,
148
+ "northwest": rate_blocking_defect_northwest,
149
+ "southeast": rate_blocking_defect_southeast,
150
+ "southwest": rate_blocking_defect_southwest
151
+ }
152
+
153
+ self.move_rates_adsorbates = {
154
+ "north": rate_adsorbate_north,
155
+ "south": rate_adsorbate_south,
156
+ "east": rate_adsorbate_east,
157
+ "west": rate_adsorbate_west,
158
+ "northeast": rate_adsorbate_northeast,
159
+ "northwest": rate_adsorbate_northwest,
160
+ "southeast": rate_adsorbate_southeast,
161
+ "southwest": rate_adsorbate_southwest
162
+ }
163
+
164
+
165
+ self.square_lattice()
166
+
167
+ self.generate_defects()
168
+
169
+ self.init_atoms()
170
+
171
+ self.generate_adsorbates()
172
+
173
+ def run(self):
174
+
175
+ self.time = np.zeros(self.n_steps + 1)
176
+ self.msd = np.zeros(self.n_steps)
177
+ self.md = np.zeros(self.n_steps)
178
+ self.positions_over_time = []
179
+ self.positions_adsorbates_over_time = []
180
+
181
+ k_tot_rec = np.zeros(self.n_steps)
182
+
183
+ self.selected_moves_info = []
184
+
185
+ move_counts = {move: 0 for move in self.moves}
186
+ defects_sites = {site for pair in self.defects_pairs for site in pair}
187
+
188
+
189
+ if self.defect_type == 1:
190
+
191
+ for step in range(self.n_steps):
192
+
193
+ if self.adsorbates_freq != -1:
194
+ if step % int(self.adsorbates_freq) == 0:
195
+ self.generate_adsorbates()
196
+
197
+ self.positions_over_time.append(self.positions_atoms.copy()) # Store positions at each step
198
+ self.positions_adsorbates_over_time.append(self.positions_adsorbates.copy()) # Store hy positions at each step
199
+ atom_occupied_sites = {tuple(pos) for pos in self.positions_atoms} # Track occupied sites, position and occupoied sites are the same but different type
200
+ adsorbates_occupied_sites = {tuple(pos) for pos in self.positions_adsorbates}
201
+
202
+ defects_occ_sites = []
203
+ for atom_idx, (x, y) in enumerate(self.positions_atoms):
204
+ defect = False
205
+ for pair in self.defects_pairs:
206
+ if (x,y) in pair:
207
+ defect = True
208
+ break
209
+ if defect:
210
+ defects_occ_sites.append(pair[0])
211
+ defects_occ_sites.append(pair[1])
212
+
213
+ defects_occupied_sites = {tuple(pos) for pos in defects_occ_sites}
214
+
215
+ total_rate = 0
216
+ possible_moves = []
217
+
218
+ for atom_idx, (x, y) in enumerate(self.positions_atoms):
219
+
220
+ # is ov?
221
+ defect = False
222
+ for pair in self.defects_pairs:
223
+ if (x,y) in pair:
224
+ defect = True
225
+ break
226
+
227
+
228
+ if defect:
229
+
230
+ pair = self.find_pair((x,y), self.defects_pairs)
231
+ move_defects_predicted= self.move_defects(pair, self.lattice_size)
232
+
233
+ for dir_idx, (direction, (new_x, new_y)) in enumerate(move_defects_predicted.items()):
234
+
235
+ new_position = (new_x, new_y)
236
+
237
+
238
+ if (new_x, new_y) not in atom_occupied_sites and (new_x, new_y) not in defects_occupied_sites and (new_x, new_y) not in adsorbates_occupied_sites:
239
+ if direction == 'north':
240
+ dx = 0
241
+ dy = 1.5
242
+ total_rate += list(self.move_rates_trapping_defects.values())[dir_idx]
243
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_trapping_defects.values())[dir_idx], (new_x, new_y), dx, dy))
244
+ elif direction == 'south':
245
+ dx = 0
246
+ dy = - 1.5
247
+ total_rate += list(self.move_rates_trapping_defects.values())[dir_idx]
248
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_trapping_defects.values())[dir_idx],(new_x, new_y), dx, dy))
249
+
250
+ elif direction == 'northeast':
251
+ dx = 1
252
+ dy = 0.5
253
+ total_rate += list(self.move_rates_trapping_defects.values())[dir_idx]
254
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_trapping_defects.values())[dir_idx], (new_x, new_y), dx, dy))
255
+
256
+ elif direction == 'northwest':
257
+ dx = -1
258
+ dy = 0.5
259
+ total_rate += list(self.move_rates_trapping_defects.values())[dir_idx]
260
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_trapping_defects.values())[dir_idx], (new_x, new_y), dx, dy))
261
+
262
+ elif direction == 'southeast':
263
+ dx = 1
264
+ dy = -0.5
265
+ total_rate += list(self.move_rates_trapping_defects.values())[dir_idx]
266
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_trapping_defects.values())[dir_idx], (new_x, new_y), dx, dy))
267
+
268
+ elif direction == 'southwest':
269
+ dx = -1
270
+ dy = -0.5
271
+ total_rate += list(self.move_rates_trapping_defects.values())[dir_idx]
272
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_trapping_defects.values())[dir_idx], (new_x, new_y), dx, dy))
273
+
274
+
275
+
276
+
277
+ else:
278
+
279
+ for dir_idx, (dx, dy) in enumerate(list(self.moves.values())):
280
+
281
+ new_x, new_y = (x + dx) % self.lattice_size, (y + dy) % self.lattice_size
282
+ new_position = (new_x, new_y)
283
+
284
+ if new_position in adsorbates_occupied_sites:
285
+
286
+ dx = 2 * dx
287
+ dy = 2 * dy
288
+ new_x, new_y = (x + dx) % self.lattice_size, (y + dy) % self.lattice_size
289
+ new_position = (new_x, new_y)
290
+ if new_position not in atom_occupied_sites and (new_x, new_y) not in defects_occupied_sites and (new_x, new_y) not in adsorbates_occupied_sites:
291
+ total_rate += list(self.move_rates_adsorbates.values())[dir_idx]
292
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_adsorbates.values())[dir_idx], (new_x, new_y), dx, dy))
293
+
294
+ else:
295
+
296
+ if new_position not in atom_occupied_sites and (new_x, new_y) not in defects_occupied_sites:
297
+ total_rate += list(self.move_rates.values())[dir_idx]
298
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates.values())[dir_idx], (new_x, new_y), dx, dy))
299
+
300
+ rho1, rho2 = np.random.random(), np.random.random()
301
+
302
+ k_tot = total_rate
303
+
304
+ cumulative_rate = 0
305
+ for move_idx, (atom_idx, dir_idx, rate, new_pos, _, _) in enumerate(possible_moves):
306
+ cumulative_rate += rate
307
+ if rho1 * k_tot < cumulative_rate:
308
+ selected_move = move_idx
309
+ break
310
+
311
+ # Execute the selected process
312
+
313
+ atom_idx, dir_idx, rate, (new_x, new_y), dx, dy = possible_moves[selected_move]
314
+
315
+ if step % int(self.n_steps/10) == 0:
316
+ print(f'step = {step}')
317
+
318
+ # print(f'atom_{atom_idx} moves: dx = {dx}, dy = {dy}')
319
+ self.cumulative_vectors[atom_idx, 0] += dx
320
+ self.cumulative_vectors[atom_idx, 1] += dy
321
+ atom_occupied_sites.remove(self.positions_atoms[atom_idx])
322
+ self.positions_atoms[atom_idx] = (new_x, new_y)
323
+ atom_occupied_sites.add((new_x, new_y))
324
+
325
+ self.time[step + 1] = self.time[step] - (np.log(rho2) / k_tot)
326
+
327
+ selected_move_name = list(self.moves.keys())[dir_idx]
328
+ self.selected_moves_info.append((atom_idx, selected_move_name)) # Store the move details
329
+ move_counts[selected_move_name] += 1
330
+
331
+ displacements = np.sum((self.cumulative_vectors * np.array([self.len_horizontal, self.len_vertical])), axis=1)
332
+ squared_displacements = np.sum((self.cumulative_vectors * np.array([self.len_horizontal, self.len_vertical])) ** 2, axis=1)
333
+
334
+ k_tot_rec[step] = k_tot
335
+ self.md[step] = displacements.mean()
336
+ self.msd[step] = squared_displacements.mean()
337
+
338
+ return self.time[:-1], self.msd
339
+
340
+
341
+
342
+ if self.defect_type == 2:
343
+
344
+ for step in range(self.n_steps):
345
+
346
+ if self.adsorbates_freq != -1:
347
+ if step % int(self.adsorbates_freq) == 0:
348
+ self.generate_adsorbates()
349
+
350
+ self.positions_over_time.append(self.positions_atoms.copy()) # Store positions at each step
351
+ self.positions_adsorbates_over_time.append(self.positions_adsorbates.copy()) # Store hy positions at each step
352
+ atom_occupied_sites = {tuple(pos) for pos in self.positions_atoms} # Track occupied sites, position and occupoied sites are the same but different type
353
+ adsorbates_occupied_sites = {tuple(pos) for pos in self.positions_adsorbates}
354
+
355
+ defects_occ_sites = []
356
+ for atom_idx, (x, y) in enumerate(self.positions_atoms):
357
+ defect = False
358
+ for pair in self.defects_pairs:
359
+ if (x,y) in pair:
360
+ defect = True
361
+ break
362
+ if defect:
363
+ defects_occ_sites.append(pair[0])
364
+ defects_occ_sites.append(pair[1])
365
+
366
+ defects_occupied_sites = {tuple(pos) for pos in defects_occ_sites}
367
+
368
+ total_rate = 0
369
+ possible_moves = []
370
+
371
+ for atom_idx, (x, y) in enumerate(self.positions_atoms):
372
+
373
+
374
+ for dir_idx, (dx, dy) in enumerate(list(self.moves.values())):
375
+
376
+ new_x, new_y = (x + dx) % self.lattice_size, (y + dy) % self.lattice_size
377
+ new_position = (new_x, new_y)
378
+
379
+
380
+ if new_position in defects_sites:
381
+
382
+ if dir_idx == 0:
383
+ dx = 3 * dx
384
+ dy = 3 * dy
385
+ new_x, new_y = (x + dx) % self.lattice_size, (y + dy) % self.lattice_size
386
+ new_position = (new_x, new_y)
387
+ if new_position not in atom_occupied_sites and (new_x, new_y) not in defects_occupied_sites and (new_x, new_y) not in adsorbates_occupied_sites and (new_x, new_y) not in defects_sites:
388
+ total_rate += list(self.move_rates_blocking_defects.values())[dir_idx]
389
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_blocking_defects.values())[dir_idx], (new_x, new_y), dx, dy))
390
+
391
+ elif dir_idx == 1:
392
+
393
+ dx = 3 * dx
394
+ dy = 3 * dy
395
+ new_x, new_y = (x + dx) % self.lattice_size, (y + dy) % self.lattice_size
396
+ new_position = (new_x, new_y)
397
+ if new_position not in atom_occupied_sites and (new_x, new_y) not in defects_occupied_sites and (new_x, new_y) not in adsorbates_occupied_sites and (new_x, new_y) not in defects_sites:
398
+ total_rate += list(self.move_rates_blocking_defects.values())[dir_idx]
399
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_blocking_defects.values())[dir_idx], (new_x, new_y), dx, dy))
400
+
401
+
402
+
403
+ else:
404
+
405
+ dx = 2 * dx
406
+ dy = 2 * dy
407
+ new_x, new_y = (x + dx) % self.lattice_size, (y + dy) % self.lattice_size
408
+ new_position = (new_x, new_y)
409
+ if new_position not in atom_occupied_sites and (new_x, new_y) not in defects_occupied_sites and (new_x, new_y) not in adsorbates_occupied_sites and (new_x, new_y) not in defects_sites:
410
+ total_rate += list(self.move_rates_blocking_defects.values())[dir_idx]
411
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_blocking_defects.values())[dir_idx], (new_x, new_y), dx, dy))
412
+
413
+
414
+
415
+
416
+
417
+
418
+ elif new_position in adsorbates_occupied_sites:
419
+
420
+
421
+ dx = 2 * dx
422
+ dy = 2 * dy
423
+ new_x, new_y = (x + dx) % self.lattice_size, (y + dy) % self.lattice_size
424
+ new_position = (new_x, new_y)
425
+ if new_position not in atom_occupied_sites and (new_x, new_y) not in defects_occupied_sites and (new_x, new_y) not in adsorbates_occupied_sites:
426
+ total_rate += list(self.move_rates_adsorbates.values())[dir_idx]
427
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates_adsorbates.values())[dir_idx], (new_x, new_y), dx, dy))
428
+
429
+ else:
430
+
431
+
432
+ if new_position not in atom_occupied_sites and (new_x, new_y) not in defects_occupied_sites:
433
+ total_rate += list(self.move_rates.values())[dir_idx]
434
+ possible_moves.append((atom_idx, dir_idx, list(self.move_rates.values())[dir_idx], (new_x, new_y), dx, dy))
435
+
436
+ rho1, rho2 = np.random.random(), np.random.random()
437
+
438
+ k_tot = total_rate
439
+
440
+ cumulative_rate = 0
441
+ for move_idx, (atom_idx, dir_idx, rate, new_pos, _, _) in enumerate(possible_moves):
442
+ cumulative_rate += rate
443
+ if rho1 * k_tot < cumulative_rate:
444
+ selected_move = move_idx
445
+ break
446
+
447
+ # Execute the selected process
448
+
449
+ atom_idx, dir_idx, rate, (new_x, new_y), dx, dy = possible_moves[selected_move]
450
+
451
+ if step % int(self.n_steps/10) == 0:
452
+ print(f'step = {step}')
453
+
454
+ # print(f'atom_{atom_idx} moves: dx = {dx}, dy = {dy}')
455
+ self.cumulative_vectors[atom_idx, 0] += dx
456
+ self.cumulative_vectors[atom_idx, 1] += dy
457
+ atom_occupied_sites.remove(self.positions_atoms[atom_idx])
458
+ self.positions_atoms[atom_idx] = (new_x, new_y)
459
+ atom_occupied_sites.add((new_x, new_y))
460
+
461
+ self.time[step + 1] = self.time[step] - (np.log(rho2) / k_tot)
462
+
463
+ selected_move_name = list(self.moves.keys())[dir_idx]
464
+ self.selected_moves_info.append((atom_idx, selected_move_name)) # Store the move details
465
+ move_counts[selected_move_name] += 1
466
+
467
+ displacements = np.sum((self.cumulative_vectors * np.array([self.len_horizontal, self.len_vertical])), axis=1)
468
+ squared_displacements = np.sum((self.cumulative_vectors * np.array([self.len_horizontal, self.len_vertical])) ** 2, axis=1)
469
+
470
+ k_tot_rec[step] = k_tot
471
+ self.md[step] = displacements.mean()
472
+ self.msd[step] = squared_displacements.mean()
473
+
474
+ return self.time[:-1], self.msd
475
+
476
+
477
+ def square_lattice(self):
478
+ def generate_square_lattice(rows, cols):
479
+ square_lattice = []
480
+ for row in range(rows):
481
+ for col in range(cols):
482
+ square_lattice.append((col, row))
483
+ return square_lattice
484
+
485
+ self.square_lattice = generate_square_lattice(self.lattice_size, self.lattice_size)
486
+
487
+
488
+ def init_atoms(self):
489
+
490
+ self.positions_atoms = []
491
+ attempts = 0
492
+ max_attempts = 1000 # Avoid infinite loops
493
+
494
+ while len(self.positions_atoms) < self.n_atoms and attempts < max_attempts:
495
+
496
+ attempts += 1
497
+
498
+ i = np.random.choice(len(self.square_lattice), replace=False)
499
+ pos_atom= self.square_lattice[i]
500
+
501
+ # Check if atom overlaps with an oxygen vacancy (not allowed)
502
+ if pos_atom in map(tuple, self.positions_defects):
503
+ continue # Reject and try again
504
+
505
+ if pos_atom in map(tuple, np.array(self.positions_atoms)):
506
+ continue # Reject and try again
507
+
508
+ self.positions_atoms.append(pos_atom)
509
+
510
+ self.cumulative_vectors = np.zeros((self.n_atoms, 2), dtype=np.float64)
511
+
512
+ def move_defects(self, pair, lattice_size):
513
+ x = max(pair, key=lambda x: x[0])[0]
514
+ mx = max(pair, key=lambda x: x[1])[1]
515
+ mn = min(pair, key=lambda x: x[1])[1]
516
+ if mx == lattice_size - 1 and mn == 0:
517
+ mx = 0
518
+ mn = lattice_size - 1
519
+
520
+ new_pos = {
521
+ "north": ((x), (mx + 1) % lattice_size ),
522
+ "south": ((x), (mn - 1) % lattice_size),
523
+ "northeast": ((x + 1) % lattice_size, (mx) % lattice_size),
524
+ "northwest": ((x - 1) % lattice_size, (mx) % lattice_size),
525
+ "southeast": ((x + 1) % lattice_size, (mn) % lattice_size),
526
+ "southwest": ((x - 1) % lattice_size, (mn) % lattice_size)
527
+
528
+ }
529
+
530
+ return new_pos
531
+
532
+ def find_pair(self, element, pairs):
533
+ for pair in pairs:
534
+ if element == pair[0]:
535
+ return [pair[0], pair[1]]
536
+ elif element == pair[1]:
537
+ return [pair[0] , pair[1]]
538
+ return None
539
+
540
+ def generate_defects(self):
541
+
542
+
543
+ def is_valid_position(new_pos, existing_positions, lattice_size):
544
+ """
545
+ Checks if the new position is valid:
546
+ - It is not overlapping with an existing position.
547
+ - It is not a first nearest neighbor of any existing OV.
548
+ """
549
+ x, y = new_pos
550
+ neighbors = [
551
+ (x % lattice_size, (y + 1)), # Top
552
+ (x % lattice_size, (y - 1)), # Bottom
553
+ ((x - 1) % lattice_size, (y + 1) % lattice_size), #nw
554
+ ((x + 1) % lattice_size, (y + 1) % lattice_size), #ne
555
+ ((x - 1) % lattice_size, (y - 1) % lattice_size), #sw
556
+ ((x + 1) % lattice_size, (y - 1) % lattice_size), #se
557
+ (x % lattice_size, (y + 2)), # Top
558
+ (x % lattice_size, (y - 2)), # Bottom
559
+ ((x + 1) % lattice_size, (y + 2) % lattice_size), #ne
560
+ ((x + 1) % lattice_size, (y - 2) % lattice_size), #nw
561
+ ((x - 1) % lattice_size, (y - 2) % lattice_size), #sw
562
+ ((x - 1) % lattice_size, (y + 2) % lattice_size), #se
563
+ ((x - 1) % lattice_size, y % lattice_size), #w
564
+ ((x + 1) % lattice_size, y % lattice_size) #e
565
+ ]
566
+
567
+ return tuple(new_pos) not in existing_positions and all(tuple(pos) not in existing_positions for pos in neighbors)
568
+
569
+ def defects(lattice_size, n_defects):
570
+ max_possible_sites = lattice_size * lattice_size // 6 # Allow more vacancies
571
+ if n_defects > max_possible_sites:
572
+ raise ValueError(f"n_defects ({n_defects}) is too large for lattice_size={lattice_size}!")
573
+
574
+ if n_defects == 0:
575
+ return np.empty((0, 2), dtype=int) # Return an empty 2D array when no vacancies are needed
576
+
577
+ positions_base_defects = set()
578
+ attempts = 0
579
+ max_attempts = 20000 # Allow more attempts to fit more vacancies
580
+
581
+ y_values_even = np.arange(0, lattice_size -1, 1)
582
+ y_values_odd = np.arange(0, lattice_size -1, 1)
583
+
584
+ while len(positions_base_defects) < n_defects and attempts < max_attempts:
585
+ x = np.random.randint(0, lattice_size)
586
+ if x % 2 ==0:
587
+ y = np.random.choice(y_values_even)
588
+ new_pos = (x, y)
589
+ else:
590
+
591
+ y = np.random.choice(y_values_odd)
592
+ new_pos = (x, y)
593
+
594
+ if is_valid_position(new_pos, positions_base_defects, lattice_size):
595
+ positions_base_defects.add(new_pos)
596
+
597
+ attempts += 1
598
+
599
+ if len(positions_base_defects) < n_defects:
600
+ raise RuntimeError("Could not place all vdefects while avoiding overlaps and first-nearest neighbors.")
601
+
602
+ positions_base_defects = np.array(list(positions_base_defects))
603
+
604
+ # Compute the "above" positions (shifted by one row up)
605
+ positions_above_defects = np.column_stack((positions_base_defects[:, 0], ((positions_base_defects[:, 1] + 1) % lattice_size)))
606
+
607
+ # Combine base and above positions
608
+ positions_defects = np.vstack((positions_base_defects, positions_above_defects))
609
+
610
+ return positions_defects
611
+
612
+ self.positions_defects = defects(self.lattice_size, self.n_defects)
613
+
614
+ arr = self.positions_defects
615
+ self.defects_pairs = [[tuple(arr[j]), tuple(arr[int(j + 0.5 * len(arr))])] for j in range(int(0.5 * len(arr)))]
616
+
617
+
618
+
619
+ def generate_adsorbates(self):
620
+
621
+ self.positions_adsorbates = []
622
+ attempts = 0
623
+ max_attempts = 1000 # Avoid infinite loops
624
+
625
+ while len(self.positions_adsorbates) < self.n_adsorbates and attempts < max_attempts:
626
+
627
+ # print(f'attempt = {attempts}')
628
+ attempts += 1
629
+
630
+ i = np.random.choice(len(self.square_lattice), replace=False)
631
+ pos_adsorbates= self.square_lattice[i]
632
+
633
+ # Check if hydroxyl overlaps with an oxygen vacancy (not allowed)
634
+ if pos_adsorbates in map(tuple, self.positions_defects) or pos_adsorbates in map(tuple, self.positions_atoms):
635
+ continue # Reject and try again
636
+
637
+
638
+ # If all conditions are met, accept the position
639
+ self.positions_adsorbates.append(pos_adsorbates)
640
+
641
+ self.positions_adsorbates = np.array(self.positions_adsorbates)
642
+
643
+
644
+
645
+
646
+
647
+ def anim1panels(self, filename):
648
+
649
+
650
+ # ===================== Figure & Axes =====================
651
+ fig, ax_main = plt.subplots(figsize=(6, 6))
652
+ ax_main.set_xlim(-1, self.lattice_size + 1)
653
+ ax_main.set_ylim(-1, self.lattice_size + 1)
654
+ ax_main.set_xticks(range(self.lattice_size))
655
+ ax_main.set_yticks(range(self.lattice_size))
656
+ ax_main.grid(True, linestyle='--', linewidth=0.5)
657
+ ax_main.set_aspect('equal', adjustable='box') # ensure circles look like circles
658
+
659
+ # Background lattice (optional)
660
+ if hasattr(self, "square_lattice") and len(self.square_lattice) > 0:
661
+ hx, hy = zip(*self.square_lattice)
662
+ ax_main.scatter(hx, hy, s=20, color="gray", alpha=0.5, label="Lattice Sites")
663
+
664
+ title_text = ax_main.set_title("")
665
+
666
+ # ===================== OV Rectangles =====================
667
+ if hasattr(self, "defects_pairs") and self.defects_pairs:
668
+ for defect1, defect2 in self.defects_pairs:
669
+ x_min = min(defect1[0], defect2[0])
670
+ y_min = min(defect1[1], defect2[1])
671
+ if defect1[1] == defect2[1]: # horizontal
672
+ width, height = abs(defect1[0] - defect2[0]) + 1, 1
673
+ elif defect1[0] == defect2[0]: # vertical
674
+ width, height = 1, abs(defect1[1] - defect2[1]) + 1
675
+ else: # diagonal/block
676
+ width = abs(defect1[0] - defect2[0]) + 1
677
+ height = abs(defect1[1] - defect2[1]) + 1
678
+
679
+ rect = patches.Rectangle(
680
+ (x_min - 0.5, y_min - 0.5), width, height,
681
+ edgecolor='darkgray', facecolor=(245/255, 245/255, 245/255, 0.4), linewidth=2, zorder=3
682
+ )
683
+ ax_main.add_patch(rect)
684
+
685
+ # ===================== Hydroxyls (dynamic) =====================
686
+ adsorbates_circles = []
687
+ if hasattr(self, "positions_adsorbates_over_time") and len(self.positions_adsorbates_over_time) > 0:
688
+ adsorbates_n = len(self.positions_adsorbates_over_time[0])
689
+ for _ in range(adsorbates_n):
690
+ c = patches.Circle((0, 0), edgecolor='dimgray', radius=0.2, color='darkred', alpha=0.95, zorder=9)
691
+ ax_main.add_patch(c)
692
+ adsorbates_circles.append(c)
693
+
694
+ # ===================== Atom Gradient Setup =====================
695
+ # Baby blue gradient: bright center -> slightly darker edge
696
+ # Lightsteelblue gradient: center almost white → edge lightsteelblue
697
+ center_color = np.array([240/255, 245/255, 250/255, 1.0]) # very light (near white)
698
+ edge_color = np.array([176/255, 196/255, 222/255, 1.0]) #
699
+
700
+ def radial_gradient_image(resolution=256):
701
+ y, x = np.ogrid[-1:1:complex(0, resolution), -1:1:complex(0, resolution)]
702
+ r = np.sqrt(x*x + y*y)
703
+ r = np.clip(r, 0, 1)
704
+ grad = (1 - r)[..., None] * center_color + r[..., None] * edge_color # (H,W,4)
705
+ return grad.astype(float)
706
+
707
+ grad_img = radial_gradient_image(256)
708
+
709
+ atom_radius = 0.25
710
+ outline_color = 'midnightblue'
711
+ outline_width = 1.5
712
+
713
+ # Create atom visuals: (circle outline + clipped gradient image) per atom
714
+ atom_artists = [] # list of (circle_patch, image_artist)
715
+ for _ in range(self.n_atoms):
716
+ # Circle outline (above the gradient)
717
+ circle = patches.Circle(
718
+ (0, 0), radius=atom_radius,
719
+ edgecolor=outline_color, facecolor='none',
720
+ linewidth=outline_width, zorder=7
721
+ )
722
+ ax_main.add_patch(circle)
723
+
724
+ # Gradient image (under the outline), then clip to the circle
725
+ im = ax_main.imshow(
726
+ grad_img,
727
+ extent=[-atom_radius, atom_radius, -atom_radius, atom_radius],
728
+ origin='lower',
729
+ interpolation='bilinear',
730
+ zorder=6,
731
+ alpha=1.0
732
+ )
733
+ im.set_clip_path(circle) # critical: clip the image to the circle
734
+ im.set_clip_on(True)
735
+
736
+ atom_artists.append((circle, im))
737
+
738
+ # Transparent numbers centered inside atoms
739
+ labels = [
740
+ ax_main.text(
741
+ 0, 0, str(i),
742
+ fontsize=6, color='black',
743
+ ha='center', va='center',
744
+ fontweight='bold', alpha=1,
745
+ zorder=8
746
+ )
747
+ for i in range(self.n_atoms)
748
+ ]
749
+
750
+ # ===================== Update Function =====================
751
+ def update(frame):
752
+ # Move atoms
753
+ for i, (circle, im) in enumerate(atom_artists):
754
+ x, y = self.positions_over_time[frame][i]
755
+ circle.center = (x, y)
756
+ im.set_extent([x - atom_radius, x + atom_radius, y - atom_radius, y + atom_radius])
757
+ # (re)set clip path (safe across some backends)
758
+ im.set_clip_path(circle)
759
+ labels[i].set_position((x, y))
760
+
761
+ # Title
762
+ if hasattr(self, "selected_moves_info") and frame < len(self.selected_moves_info):
763
+ atom_idx, move_name = self.selected_moves_info[frame]
764
+ title_text.set_text(f"next: selected atom is atom_{atom_idx} and selected move is {move_name}")
765
+ else:
766
+ title_text.set_text("")
767
+
768
+ # Move hydroxyls
769
+ if adsorbates_circles:
770
+ for i, pos in enumerate(self.positions_adsorbates_over_time[frame]):
771
+ adsorbates_circles[i].center = (pos[0], pos[1])
772
+
773
+ # ===================== Animate (pause on last frame) =====================
774
+ interval_ms = 100
775
+ pause_frames = int(10_000 / interval_ms) # 10 sec
776
+ def update_with_pause(frame):
777
+ if frame < self.n_steps:
778
+ update(frame)
779
+ else:
780
+ update(self.n_steps - 1)
781
+
782
+ ani = FuncAnimation(fig, update_with_pause, frames=self.n_steps + pause_frames, interval=interval_ms)
783
+ ani.save(f"{filename}.gif", writer=PillowWriter(fps=20))
784
+ plt.show()
785
+
786
+
787
+
788
+
789
+
790
+ def anim2panels(self, filename):
791
+
792
+ fig, axes = plt.subplots(1, 2, figsize=(12, 6), gridspec_kw={'width_ratios': [2, 1]})
793
+ ax_main, ax_msd = axes[0], axes[1]
794
+
795
+ # ---- Main lattice view ----
796
+ ax_main.set_xlim(-1, self.lattice_size + 1)
797
+ ax_main.set_ylim(-1, self.lattice_size + 1)
798
+ ax_main.set_xticks(range(self.lattice_size))
799
+ ax_main.set_yticks(range(self.lattice_size))
800
+ ax_main.grid(True, linestyle='--', linewidth=0.5)
801
+ ax_main.set_aspect('equal', adjustable='box') # keep circles round
802
+
803
+ if hasattr(self, "square_lattice") and len(self.square_lattice) > 0:
804
+ hex_x, hex_y = zip(*self.square_lattice)
805
+ ax_main.scatter(hex_x, hex_y, s=20, color="gray", alpha=0.5, label="Lattice Sites")
806
+
807
+ title_text = ax_main.set_title("")
808
+
809
+ # ---- OV rectangles: whitesmoke fill with adjustable transparency ----
810
+ defect_alpha = 0.4 # <— tweak this for more/less transparent OV fill
811
+ if hasattr(self, "defects_pairs"):
812
+ for defect1, defect2 in self.defects_pairs:
813
+ x_min = min(defect1[0], defect2[0])
814
+ y_min = min(defect1[1], defect2[1])
815
+
816
+ if defect1[1] == defect2[1]: # Horizontal pair
817
+ width, height = abs(defect1[0] - defect2[0]) + 1, 1
818
+ elif defect1[0] == defect2[0]: # Vertical pair
819
+ width, height = 1, abs(defect1[1] - defect2[1]) + 1
820
+ else:
821
+ width = abs(defect1[0] - defect2[0]) + 1
822
+ height = abs(defect1[1] - defect2[1]) + 1
823
+
824
+ rectangle = patches.Rectangle(
825
+ (x_min - 0.5, y_min - 0.5), width, height,
826
+ edgecolor='gray',
827
+ facecolor='whitesmoke',
828
+ linewidth=1.5,
829
+ alpha=defect_alpha,
830
+ zorder=3
831
+ )
832
+ ax_main.add_patch(rectangle)
833
+
834
+ # ---- Hydroxyls (dynamic small red circles) ----
835
+ adsorbates_circles = []
836
+ if hasattr(self, "positions_adsorbates_over_time") and len(self.positions_adsorbates_over_time) > 0:
837
+ for _ in range(len(self.positions_adsorbates_over_time[0])):
838
+ c = patches.Circle((0, 0), linewidth=2.5, edgecolor='dimgray', radius=0.2, color='darkred', alpha=0.9, zorder=9)
839
+ ax_main.add_patch(c)
840
+ adsorbates_circles.append(c)
841
+
842
+ # ---- Atoms: lightsteelblue radial gradient + gray outline ----
843
+ # Gradient colors: center almost white -> edge lightsteelblue
844
+ center_color = np.array([240/255, 245/255, 250/255, 1.0]) # near white
845
+ edge_color = np.array([176/255, 196/255, 222/255, 1.0]) # lightsteelblue (#B0C4DE)
846
+
847
+ def radial_gradient_image(resolution=256):
848
+ y, x = np.ogrid[-1:1:complex(0, resolution), -1:1:complex(0, resolution)]
849
+ r = np.sqrt(x*x + y*y)
850
+ r = np.clip(r, 0, 1)
851
+ grad = (1 - r)[..., None] * center_color + r[..., None] * edge_color # (H,W,4)
852
+ return grad.astype(float)
853
+
854
+ grad_img = radial_gradient_image(256)
855
+ atom_radius = 0.25
856
+ outline_color = 'midnightblue'
857
+ outline_width = 1.5
858
+
859
+ # Create per-atom (circle outline + clipped gradient image)
860
+ atom_artists = [] # list of (circle_patch, image_artist)
861
+ for _ in range(self.n_atoms):
862
+ circle = patches.Circle(
863
+ (0, 0), radius=atom_radius,
864
+ edgecolor=outline_color, facecolor='none',
865
+ linewidth=outline_width, zorder=7
866
+ )
867
+ ax_main.add_patch(circle)
868
+
869
+ im = ax_main.imshow(
870
+ grad_img,
871
+ extent=[-atom_radius, atom_radius, -atom_radius, atom_radius],
872
+ origin='lower',
873
+ interpolation='bilinear',
874
+ zorder=6,
875
+ alpha=1.0
876
+ )
877
+ im.set_clip_path(circle) # clip gradient to the circle
878
+ im.set_clip_on(True)
879
+
880
+ atom_artists.append((circle, im))
881
+
882
+ # Atom labels: centered, semi-transparent
883
+ labels = [
884
+ ax_main.text(
885
+ 0, 0, str(i),
886
+ fontsize=6, color='black',
887
+ ha='center', va='center',
888
+ fontweight='bold', alpha=1,
889
+ zorder=8
890
+ )
891
+ for i in range(self.n_atoms)
892
+ ]
893
+
894
+ # ---- MSD plot setup ----
895
+ ax_msd.set_xlim(0, np.max(self.time) * 1.1)
896
+ ax_msd.set_ylim(0, np.max(self.msd) * 1.1)
897
+ ax_msd.set_xlabel("time (s)")
898
+ ax_msd.set_ylabel("MSD (µm²)")
899
+ ax_msd.grid(True, linestyle='--', linewidth=0.5)
900
+
901
+ (msd_line,) = ax_msd.plot([], [], color="#FFB6C1", label="MSD")
902
+ (fit_line,) = ax_msd.plot([], [], linestyle='--', color='midnightblue', label="Linear Fit")
903
+ slope_text = ax_msd.text(0.05, 0.1, "", transform=ax_msd.transAxes, fontsize=8, ha="left", va="top")
904
+
905
+ ax_msd.set_title("MSD over time")
906
+ ax_msd.legend()
907
+
908
+ # ---- Move stats (percentages) ----
909
+ move_labels = list(self.moves.keys())
910
+ move_texts = []
911
+ for i, move in enumerate(move_labels):
912
+ text = ax_msd.text(
913
+ 0.5, 0.9 - i * 0.1,
914
+ f"{move}: 0%",
915
+ transform=ax_msd.transAxes,
916
+ fontsize=10, ha="center", va="top"
917
+ )
918
+ move_texts.append(text)
919
+
920
+ # ---- Frame update ----
921
+ def update(frame):
922
+ # Atoms
923
+ for i, (circle, im) in enumerate(atom_artists):
924
+ x, y = self.positions_over_time[frame][i]
925
+ circle.center = (x, y)
926
+ im.set_extent([x - atom_radius, x + atom_radius, y - atom_radius, y + atom_radius])
927
+ im.set_clip_path(circle) # safe across backends
928
+ labels[i].set_position((x, y))
929
+
930
+ # Title
931
+ atom_idx, selected_move_name = self.selected_moves_info[frame]
932
+ title_text.set_text(f"next: selected atom is atom_{atom_idx} and selected move is {selected_move_name}")
933
+
934
+ # Hydroxyls
935
+ for i, pos in enumerate(self.positions_adsorbates_over_time[frame]):
936
+ adsorbates_circles[i].center = (pos[0], pos[1])
937
+
938
+ # MSD trace
939
+ msd_line.set_data(self.time[:frame], self.msd[:frame])
940
+
941
+ # Move percentages up to current frame
942
+ move_counts_up_to_frame = {m: 0 for m in move_labels}
943
+ for _, mname in self.selected_moves_info[:frame]:
944
+ move_counts_up_to_frame[mname] += 1
945
+ total_moves = max(1, sum(move_counts_up_to_frame.values()))
946
+ for i, m in enumerate(move_labels):
947
+ pct = 100.0 * move_counts_up_to_frame[m] / total_moves
948
+ move_texts[i].set_text(f"{m}: {pct:.1f}%")
949
+
950
+ # Linear fit for diffusion slope
951
+ if frame > 1:
952
+ x_fit = self.time[:frame]
953
+ y_fit = self.msd[:frame]
954
+ p = Polynomial.fit(x_fit, y_fit, 1).convert()
955
+ slope = p.coef[1]
956
+ fit_line.set_data(x_fit, p(x_fit))
957
+ slope_text.set_text(f"Diffusion = {slope/4:.3f}(µm²/s)")
958
+ else:
959
+ fit_line.set_data([], [])
960
+ slope_text.set_text("")
961
+
962
+ # ---- Animate with pause at end ----
963
+ interval_ms = 100
964
+ pause_frames = int(10_000 / interval_ms) # 10 sec hold
965
+ def update_with_pause(frame):
966
+ if frame < self.n_steps:
967
+ update(frame)
968
+ else:
969
+ update(self.n_steps - 1)
970
+
971
+ ani = FuncAnimation(fig, update_with_pause, frames=self.n_steps + pause_frames, interval=interval_ms)
972
+ ani.save(f"{filename}.gif", writer=PillowWriter(fps=20))
973
+ plt.show()
974
+
975
+
976
+
977
+
978
+
979
+
980
+ def msdplot(self, filename):
981
+
982
+ time = self.time[:-1]
983
+
984
+ ## Calculate overall diffusion coefficient
985
+ transient_cutoff = int(0 * self.n_steps) # Ignore the first 10% of steps
986
+ valid_time = time[transient_cutoff:] # Exclude transient region
987
+ valid_msd = self.msd[transient_cutoff:] # Exclude transient region
988
+
989
+ # Linear fit for MSD vs time
990
+ fit = Polynomial.fit(valid_time, valid_msd, 1).convert()
991
+ fit_line = fit(valid_time)
992
+ average_slope = fit.coef[1] # Slope of MSD vs time
993
+
994
+ # Diffusion coefficient
995
+ diffusion_coefficient_corrected = average_slope / 4
996
+
997
+ # Plot MSD vs. time with fit line
998
+ plt.figure(figsize=(8, 6))
999
+ plt.plot(time, self.msd, color="#89CFF0", label="Individual MSD Trajectory")
1000
+ plt.plot(valid_time, fit_line, linestyle="--", color="blue", label="Linear Fit")
1001
+ #plt.axvline(x=time[transient_cutoff], color='r', linestyle='--', label="Transient cutoff")
1002
+ plt.xlabel("Time (s)")
1003
+ plt.ylabel("MSD (µm²)")
1004
+ plt.legend()
1005
+ plt.title(f"Mean Squared Displacement vs Time - {self.n_steps} steps")
1006
+ plt.text(0.05 * max(time), 0.8 * max(self.msd), # Adjust placement (x, y) as needed
1007
+ f" Diffusion Coefficient = {diffusion_coefficient_corrected:.4f} µm²/s",
1008
+ fontsize=12, color='blue', bbox=dict(facecolor="white", alpha=0.5))
1009
+ plt.minorticks_on()
1010
+ plt.grid(True, which='major', linestyle='-', linewidth=0.6)
1011
+ plt.grid(True, which='minor', linestyle=':', linewidth=0.3, alpha=0.7)
1012
+ plt.savefig(f'{filename}.png', dpi = 600)
1013
+ plt.show()
1014
+
1015
+
1016
+
1017
+ def msd_histogram(self, n_seeds, msd_folder = "random_seeds/msd",
1018
+ time_folder = "random_seeds/time",
1019
+ save_folder = "random_seeds/average_msd.png",
1020
+ msd_trajs_color = "pink",
1021
+ msd_average_color = "#C71585"):
1022
+
1023
+
1024
+ msd_list = []
1025
+ time_list = []
1026
+
1027
+ for i in range(n_seeds):
1028
+ msd = np.load(f"{msd_folder}/rs_{i}.npy")
1029
+ time = np.load(f"{time_folder}/rs_{i}.npy")
1030
+ msd_list.append(msd)
1031
+ time_list.append(time)
1032
+
1033
+ # Convert lists to numpy arrays
1034
+ msd_array = np.array(msd_list)
1035
+ time_array = np.array(time_list)
1036
+
1037
+ # Compute average and standard deviation
1038
+ msd_avg = np.mean(msd_array, axis=0)
1039
+ msd_std = np.std(msd_array, axis=0)
1040
+ time_avg = np.mean(time_array, axis=0)
1041
+
1042
+ # Fit a linear function: MSD = a * Time + b
1043
+ # slope, intercept = np.polyfit(time_avg, msd_avg, 1)
1044
+ slope, intercept, r_value, p_value, slope_std_err = linregress(time_avg, msd_avg)
1045
+ fitted_line = slope * time_avg + intercept # Compute the fitted line
1046
+
1047
+ # Select 20 evenly spaced points for error bars
1048
+ num_error_points = 20
1049
+ indices = np.linspace(0, len(time_avg) - 1, num_error_points, dtype=int)
1050
+
1051
+ # Plot all MSD curves in light pink
1052
+ plt.figure(figsize=(8, 6))
1053
+ for i in range(n_seeds):
1054
+ plt.plot(time_array[i], msd_array[i], color=msd_trajs_color, alpha=0.5, linewidth=1)
1055
+
1056
+ # Plot average MSD with error bars at 20 selected points in dark pink
1057
+ plt.plot(time_avg, msd_avg, color=msd_average_color, linewidth=2, label="Average MSD")
1058
+ plt.errorbar(time_avg[indices], msd_avg[indices], yerr=msd_std[indices], fmt='o', color=msd_average_color, capsize=3)
1059
+
1060
+ # Plot fitted line
1061
+ #plt.plot(time_avg, fitted_line, linestyle="--", color="blue", linewidth=2, label=f"Linear Fit: Slope = {slope:.4f} ± {slope_std_err:.2f} (um²/s)")
1062
+ plt.plot(time_avg, fitted_line, linestyle="--", color="blue", linewidth=2, label="Linear Fit")
1063
+
1064
+ # Labels and legend
1065
+ plt.xlabel("Time(s)")
1066
+ plt.ylabel("MSD (µm²)")
1067
+ plt.title("T=300K - Lattice Size =10*10 - OH Coverage=0.5ML - OH Frequency=OFF")
1068
+
1069
+ # Display slope inside the plot
1070
+ plt.text(0.05 * max(time_avg), 0.8 * max(msd_avg), f"Diffusion Coefficient = {slope/4:.6f} ± {slope_std_err/4:.1e} (µm²/s)", fontsize=12, color="blue", bbox=dict(facecolor="white", alpha=0.5))
1071
+ plt.legend()
1072
+ plt.minorticks_on()
1073
+ plt.grid(True, which='major', linestyle='-', linewidth=0.6)
1074
+ plt.grid(True, which='minor', linestyle=':', linewidth=0.3, alpha=0.7)
1075
+ plt.savefig(save_folder, dpi = 600)
1076
+ plt.show()
1077
+
1078
+