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