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/__init__.py +5 -0
- kmclab/hex.py +1072 -0
- kmclab/square.py +1078 -0
- kmclab-0.1.0.dist-info/METADATA +49 -0
- kmclab-0.1.0.dist-info/RECORD +7 -0
- kmclab-0.1.0.dist-info/WHEEL +5 -0
- kmclab-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
|