llg3d 1.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.
- llg3d/__init__.py +1 -0
- llg3d/llg3d.py +742 -0
- llg3d/llg3d_seq.py +447 -0
- llg3d/post/__init__.py +0 -0
- llg3d/post/process.py +105 -0
- llg3d/post/temperature.py +84 -0
- llg3d-1.1.0.dist-info/AUTHORS +3 -0
- llg3d-1.1.0.dist-info/LICENSE +22 -0
- llg3d-1.1.0.dist-info/METADATA +42 -0
- llg3d-1.1.0.dist-info/RECORD +13 -0
- llg3d-1.1.0.dist-info/WHEEL +5 -0
- llg3d-1.1.0.dist-info/entry_points.txt +3 -0
- llg3d-1.1.0.dist-info/top_level.txt +1 -0
llg3d/llg3d_seq.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solver for the stochastic Landau-Lifshitz-Gilbert equation in 3D
|
|
3
|
+
(sequential version for history)
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
# Create a random number generator by setting the seed
|
|
14
|
+
rng = np.random.default_rng(0)
|
|
15
|
+
|
|
16
|
+
# Initialize a sequence of random seeds
|
|
17
|
+
# See: https://numpy.org/doc/stable/reference/random/parallel.html#seedsequence-spawning
|
|
18
|
+
ss = np.random.SeedSequence(12345)
|
|
19
|
+
|
|
20
|
+
# Deploy size x SeedSequence to be passed to child processes
|
|
21
|
+
child_seeds = ss.spawn(1)
|
|
22
|
+
rng = np.random.default_rng(child_seeds[0])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Parameters: default value and description
|
|
26
|
+
parameters = {
|
|
27
|
+
"N": (500, "Number of temporal iterations"),
|
|
28
|
+
"dt": (1.0e-14, "Time step"),
|
|
29
|
+
"Jx": (300, "Number of points in x"),
|
|
30
|
+
"Jy": (21, "Number of points in y"),
|
|
31
|
+
"Jz": (21, "Number of points in z"),
|
|
32
|
+
"Lx": (2.99e-7, "Length in x"),
|
|
33
|
+
"Ly": (1.0e-8, "Length in y"),
|
|
34
|
+
"Lz": (1.0e-8, "Length in z"),
|
|
35
|
+
"T": (1100, "Temperature"),
|
|
36
|
+
"H_ext": (0.0, "External field"),
|
|
37
|
+
"n_average": (2000, "Starting index of temporal averaging"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def progress_bar(it, prefix="", size=60, out=sys.stdout):
|
|
42
|
+
"""
|
|
43
|
+
Displays a progress bar
|
|
44
|
+
(Source: https://stackoverflow.com/a/34482761/16593179)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
count = len(it)
|
|
48
|
+
|
|
49
|
+
def show(j):
|
|
50
|
+
x = int(size * j / count)
|
|
51
|
+
print(
|
|
52
|
+
f"{prefix}[{u'█'*x}{('.'*(size-x))}] {j}/{count}",
|
|
53
|
+
end="\r",
|
|
54
|
+
file=out,
|
|
55
|
+
flush=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
show(0)
|
|
59
|
+
for i, item in enumerate(it):
|
|
60
|
+
yield item
|
|
61
|
+
# To avoid slowing down the computation, we do not display at every iteration
|
|
62
|
+
if i % 5 == 0:
|
|
63
|
+
show(i + 1)
|
|
64
|
+
show(i + 1)
|
|
65
|
+
print("\n", flush=True, file=out)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Grid:
|
|
70
|
+
"""Stores grid data"""
|
|
71
|
+
|
|
72
|
+
# Parameters refer to the entire grid
|
|
73
|
+
Jx: int
|
|
74
|
+
Jy: int
|
|
75
|
+
Jz: int
|
|
76
|
+
Lx: float
|
|
77
|
+
Ly: float
|
|
78
|
+
Lz: float
|
|
79
|
+
|
|
80
|
+
def __post_init__(self) -> None:
|
|
81
|
+
"""Calculates grid characteristics"""
|
|
82
|
+
self.dx = self.Lx / (self.Jx - 1)
|
|
83
|
+
self.dy = self.Ly / (self.Jy - 1)
|
|
84
|
+
self.dz = self.Lz / (self.Jz - 1)
|
|
85
|
+
# Shape of the local array for the process
|
|
86
|
+
self.dims = self.Jx, self.Jy, self.Jz
|
|
87
|
+
# Volume of a grid cell
|
|
88
|
+
self.dV = self.dx * self.dy * self.dz
|
|
89
|
+
# Total volume
|
|
90
|
+
self.V = self.Lx * self.Ly * self.Lz
|
|
91
|
+
# Total number of points
|
|
92
|
+
self.ntot = self.Jx * self.Jy * self.Jz
|
|
93
|
+
self.ncell = (self.Jx - 1) * (self.Jy - 1) * (self.Jz - 1)
|
|
94
|
+
|
|
95
|
+
def __repr__(self):
|
|
96
|
+
s = "\t" + "\t\t".join(("x", "y", "z")) + "\n"
|
|
97
|
+
s += f"J =\t{self.Jx}\t\t{self.Jy}\t\t{self.Jz}\n"
|
|
98
|
+
s += f"L =\t{self.Lx}\t\t{self.Ly}\t\t{self.Lz}\n"
|
|
99
|
+
s += f"d =\t{self.dx:.08e}\t{self.dy:.08e}\t{self.dz:.08e}\n\n"
|
|
100
|
+
s += f"dV = {self.dV:.08e}\n"
|
|
101
|
+
s += f"V = {self.V:.08e}\n"
|
|
102
|
+
s += f"ntot = {self.ntot:d}\n"
|
|
103
|
+
|
|
104
|
+
return s
|
|
105
|
+
|
|
106
|
+
def get_filename(self, T: float) -> str:
|
|
107
|
+
"""Returns the output file name for a given temperature"""
|
|
108
|
+
suffix = f"T{int(T)}_{self.Jx}x{self.Jy}x{self.Jz}"
|
|
109
|
+
return f"m1_integral_space_{suffix}.txt"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Element:
|
|
113
|
+
"""Abstract class for an element"""
|
|
114
|
+
|
|
115
|
+
A = 0.0
|
|
116
|
+
K = 0.0
|
|
117
|
+
gamma = 0.0
|
|
118
|
+
mu_0 = 0.0
|
|
119
|
+
k_B = 0.0
|
|
120
|
+
lambda_G = 0.0
|
|
121
|
+
M_s = 0.0
|
|
122
|
+
a_eff = 0.0
|
|
123
|
+
|
|
124
|
+
def __init__(self, T: float, H_ext, g: Grid, dt: float) -> None:
|
|
125
|
+
self.g = g
|
|
126
|
+
self.dt = dt
|
|
127
|
+
self.gamma_0 = self.gamma * self.mu_0
|
|
128
|
+
|
|
129
|
+
# --- Characteristic scales ---
|
|
130
|
+
self.coeff_1 = self.gamma_0 * 2.0 * self.A / (self.mu_0 * self.M_s)
|
|
131
|
+
self.coeff_2 = self.gamma_0 * 2.0 * self.K / (self.mu_0 * self.M_s)
|
|
132
|
+
self.coeff_3 = self.gamma_0 * H_ext
|
|
133
|
+
|
|
134
|
+
# corresponds to the temperature actually put into the random field
|
|
135
|
+
T_simu = T * self.g.dx / self.a_eff
|
|
136
|
+
# calculation of the random field related to temperature
|
|
137
|
+
# (we only take the volume over one mesh)
|
|
138
|
+
h_alea = np.sqrt(
|
|
139
|
+
2
|
|
140
|
+
* self.lambda_G
|
|
141
|
+
* self.k_B
|
|
142
|
+
/ (self.gamma_0 * self.mu_0 * self.M_s * self.g.dV)
|
|
143
|
+
)
|
|
144
|
+
H_alea = h_alea * np.sqrt(T_simu) * np.sqrt(1.0 / self.dt)
|
|
145
|
+
self.coeff_4 = H_alea * self.gamma_0
|
|
146
|
+
|
|
147
|
+
def get_CFL(self) -> float:
|
|
148
|
+
"""Returns the value of the CFL"""
|
|
149
|
+
return self.dt * self.coeff_1 / self.g.dx**2
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Cobalt(Element):
|
|
153
|
+
A = 30.0e-12
|
|
154
|
+
K = 520.0e3
|
|
155
|
+
gamma = 1.76e11
|
|
156
|
+
mu_0 = 1.26e-6
|
|
157
|
+
k_B = 1.38e-23
|
|
158
|
+
# #mu_B=9.27e-24
|
|
159
|
+
lambda_G = 0.5
|
|
160
|
+
M_s = 1400.0e3
|
|
161
|
+
a_eff = 0.25e-9
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Iron(Element):
|
|
165
|
+
A = 21.0e-12
|
|
166
|
+
K = 48.0e3
|
|
167
|
+
gamma = 1.76e11
|
|
168
|
+
mu_0 = 1.26e-6
|
|
169
|
+
gamma_0 = gamma * mu_0 # 2.34e+5
|
|
170
|
+
k_B = 1.38e-23
|
|
171
|
+
# mu_B=9.27e-24
|
|
172
|
+
lambda_G = 0.5
|
|
173
|
+
M_s = 1700.0e3
|
|
174
|
+
a_eff = 0.286e-9
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def calculate_laplacian(e: Element, g: Grid, m):
|
|
178
|
+
"""Returns the laplacian of m (* coeff_1) in 3D"""
|
|
179
|
+
|
|
180
|
+
# Extract slices for Neumann boundary conditions
|
|
181
|
+
m_start_x = m[1:2, :, :]
|
|
182
|
+
m_end_x = m[-2:-1, :, :]
|
|
183
|
+
|
|
184
|
+
m_start_y = m[:, 1:2, :]
|
|
185
|
+
m_end_y = m[:, -2:-1, :]
|
|
186
|
+
|
|
187
|
+
m_start_z = m[:, :, 1:2]
|
|
188
|
+
m_end_z = m[:, :, -2:-1]
|
|
189
|
+
|
|
190
|
+
laplacian = (
|
|
191
|
+
(
|
|
192
|
+
np.concatenate((m[1:, :, :], m_end_x), axis=0)
|
|
193
|
+
+ np.concatenate((m_start_x, m[:-1, :, :]), axis=0)
|
|
194
|
+
)
|
|
195
|
+
/ g.dx ** 2
|
|
196
|
+
+ (
|
|
197
|
+
np.concatenate((m[:, 1:, :], m_end_y), axis=1)
|
|
198
|
+
+ np.concatenate((m_start_y, m[:, :-1, :]), axis=1)
|
|
199
|
+
)
|
|
200
|
+
/ g.dy ** 2
|
|
201
|
+
+ (
|
|
202
|
+
np.concatenate((m[:, :, 1:], m_end_z), axis=2)
|
|
203
|
+
+ np.concatenate((m_start_z, m[:, :, :-1]), axis=2)
|
|
204
|
+
)
|
|
205
|
+
/ g.dz ** 2
|
|
206
|
+
- 2 * (1 / g.dx ** 2 + 1 / g.dy ** 2 + 1 / g.dz ** 2) * m
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return e.coeff_1 * laplacian
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def calculate_si(
|
|
213
|
+
e: Element, m1, m2, m3, laplacian_m1, laplacian_m2, laplacian_m3, R_alea
|
|
214
|
+
):
|
|
215
|
+
"""Returns the s_i = a_i + b_i"""
|
|
216
|
+
|
|
217
|
+
# Precalculate terms that appear multiple times
|
|
218
|
+
|
|
219
|
+
R_1 = laplacian_m1 + e.coeff_2 * m1 + e.coeff_3 + e.coeff_4 * R_alea[0]
|
|
220
|
+
R_2 = laplacian_m2 + e.coeff_4 * R_alea[1]
|
|
221
|
+
R_3 = laplacian_m3 + e.coeff_4 * R_alea[2]
|
|
222
|
+
|
|
223
|
+
l_G_m1m2 = e.lambda_G * m1 * m2
|
|
224
|
+
l_G_m1m3 = e.lambda_G * m1 * m3
|
|
225
|
+
l_G_m2m3 = e.lambda_G * m2 * m3
|
|
226
|
+
|
|
227
|
+
m1m1 = m1 * m1
|
|
228
|
+
m2m2 = m2 * m2
|
|
229
|
+
m3m3 = m3 * m3
|
|
230
|
+
|
|
231
|
+
s1 = (
|
|
232
|
+
(-m2 - l_G_m1m3) * R_3
|
|
233
|
+
+ +(m3 - l_G_m1m2) * R_2
|
|
234
|
+
+ +e.lambda_G * (m2m2 + m3m3) * R_1
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
s2 = (
|
|
238
|
+
(-m3 - l_G_m1m2) * R_1
|
|
239
|
+
+ (m1 - l_G_m2m3) * R_3
|
|
240
|
+
+ e.lambda_G * (m1m1 + m3m3) * R_2
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
s3 = (
|
|
244
|
+
(-m1 - l_G_m2m3) * R_2
|
|
245
|
+
+ (m2 - l_G_m1m3) * R_1
|
|
246
|
+
+ e.lambda_G * (m1m1 + m2m2) * R_3
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return s1, s2, s3
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def integral(g, m: np.ndarray) -> float:
|
|
253
|
+
"""
|
|
254
|
+
Returns the spatial average of m with shape (g.dims)
|
|
255
|
+
using the midpoint method
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
# copy m to avoid modifying its value
|
|
259
|
+
mm = m.copy()
|
|
260
|
+
|
|
261
|
+
# on the edges, we divide the contribution by 2
|
|
262
|
+
# x
|
|
263
|
+
mm[0, :, :] /= 2
|
|
264
|
+
mm[-1, :, :] /= 2
|
|
265
|
+
# y
|
|
266
|
+
mm[:, 0, :] /= 2
|
|
267
|
+
mm[:, -1, :] /= 2
|
|
268
|
+
# z
|
|
269
|
+
mm[:, :, 0] /= 2
|
|
270
|
+
mm[:, :, -1] /= 2
|
|
271
|
+
|
|
272
|
+
return mm.sum() / g.ncell
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def simulate(N, Jx, Jy, Jz, Lx, Ly, Lz, T, H_ext, dt, n_average, element):
|
|
277
|
+
"""Simulates the system over N iterations"""
|
|
278
|
+
|
|
279
|
+
g = Grid(Jx=Jx, Jy=Jy, Jz=Jz, Lx=Lx, Ly=Ly, Lz=Lz)
|
|
280
|
+
print(g)
|
|
281
|
+
|
|
282
|
+
dims = g.dims
|
|
283
|
+
|
|
284
|
+
e = element(T, H_ext, g, dt)
|
|
285
|
+
print(f"CFL = {e.get_CFL()}")
|
|
286
|
+
|
|
287
|
+
# --- Initialization ---
|
|
288
|
+
|
|
289
|
+
def theta_init(shape):
|
|
290
|
+
"""Initialization of theta"""
|
|
291
|
+
return np.zeros(shape)
|
|
292
|
+
|
|
293
|
+
def phi_init(t, shape):
|
|
294
|
+
"""Initialization of phi"""
|
|
295
|
+
return np.zeros(shape) + e.gamma_0 * H_ext * t
|
|
296
|
+
|
|
297
|
+
m1 = np.zeros((2,) + dims)
|
|
298
|
+
m2 = np.zeros_like(m1)
|
|
299
|
+
m3 = np.zeros_like(m1)
|
|
300
|
+
|
|
301
|
+
theta = theta_init(dims)
|
|
302
|
+
phi = phi_init(0, dims)
|
|
303
|
+
|
|
304
|
+
m1[0] = np.cos(theta)
|
|
305
|
+
m2[0] = np.sin(theta) * np.cos(phi)
|
|
306
|
+
m3[0] = np.sin(theta) * np.sin(phi)
|
|
307
|
+
|
|
308
|
+
# Output file
|
|
309
|
+
f = open(g.get_filename(T), "w")
|
|
310
|
+
|
|
311
|
+
t = 0.0
|
|
312
|
+
m1_mean = 0.0
|
|
313
|
+
|
|
314
|
+
start_time = time.perf_counter()
|
|
315
|
+
|
|
316
|
+
for n in progress_bar(range(1, N + 1), "Iteration : ", 40):
|
|
317
|
+
t += dt
|
|
318
|
+
|
|
319
|
+
# Adding randomness: temperature effect
|
|
320
|
+
R_alea = rng.standard_normal((3,) + dims)
|
|
321
|
+
|
|
322
|
+
# Prediction phase
|
|
323
|
+
|
|
324
|
+
laplacian_m1 = calculate_laplacian(e, g, m1[0])
|
|
325
|
+
laplacian_m2 = calculate_laplacian(e, g, m2[0])
|
|
326
|
+
laplacian_m3 = calculate_laplacian(e, g, m3[0])
|
|
327
|
+
|
|
328
|
+
s1_pre, s2_pre, s3_pre = calculate_si(
|
|
329
|
+
e, m1[0], m2[0], m3[0], laplacian_m1, laplacian_m2, laplacian_m3, R_alea
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Update
|
|
333
|
+
m1[1] = m1[0] + dt * s1_pre
|
|
334
|
+
m2[1] = m2[0] + dt * s2_pre
|
|
335
|
+
m3[1] = m3[0] + dt * s3_pre
|
|
336
|
+
|
|
337
|
+
# Correction phase
|
|
338
|
+
|
|
339
|
+
laplacian_m1 = calculate_laplacian(e, g, m1[1])
|
|
340
|
+
laplacian_m2 = calculate_laplacian(e, g, m2[1])
|
|
341
|
+
laplacian_m3 = calculate_laplacian(e, g, m3[1])
|
|
342
|
+
|
|
343
|
+
s1_cor, s2_cor, s3_cor = calculate_si(
|
|
344
|
+
e, m1[1], m2[1], m3[1], laplacian_m1, laplacian_m2, laplacian_m3, R_alea
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Update
|
|
348
|
+
m1[1] = m1[0] + dt * 0.5 * (s1_pre + s1_cor)
|
|
349
|
+
m2[1] = m2[0] + dt * 0.5 * (s2_pre + s2_cor)
|
|
350
|
+
m3[1] = m3[0] + dt * 0.5 * (s3_pre + s3_cor)
|
|
351
|
+
|
|
352
|
+
# We renormalize to check the constraint of being on the sphere
|
|
353
|
+
norm = np.sqrt(m1[1] ** 2 + m2[1] ** 2 + m3[1] ** 2)
|
|
354
|
+
m1[1] /= norm
|
|
355
|
+
m2[1] /= norm
|
|
356
|
+
m3[1] /= norm
|
|
357
|
+
|
|
358
|
+
m1[0] = m1[1]
|
|
359
|
+
m2[0] = m2[1]
|
|
360
|
+
m3[0] = m3[1]
|
|
361
|
+
|
|
362
|
+
# Midpoint method
|
|
363
|
+
m1_integral = integral(g, m1[0])
|
|
364
|
+
if n >= n_average:
|
|
365
|
+
m1_mean += m1_integral
|
|
366
|
+
|
|
367
|
+
f.write(f"{t:10.8e} {m1_integral:10.8e}\n")
|
|
368
|
+
|
|
369
|
+
m1_mean /= N - n_average
|
|
370
|
+
|
|
371
|
+
print(f"Output in {g.get_filename(T)}")
|
|
372
|
+
f.close()
|
|
373
|
+
|
|
374
|
+
print(f"{t = :e} T_f = {N * dt}")
|
|
375
|
+
if m1_mean != 0.0:
|
|
376
|
+
print(f"{m1_mean = :e}")
|
|
377
|
+
|
|
378
|
+
return g, (time.perf_counter() - start_time)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def check_solution(g: Grid, T: float) -> bool:
|
|
382
|
+
"""
|
|
383
|
+
Verifies that the solution is identical to m1_integral_space_T1100_ref.txt,
|
|
384
|
+
obtained with the reference code using np.random.default_rng(0)
|
|
385
|
+
"""
|
|
386
|
+
filename = g.get_filename(T)
|
|
387
|
+
ref_filename = Path(filename).stem + "_ref.txt"
|
|
388
|
+
try:
|
|
389
|
+
with open(filename) as f:
|
|
390
|
+
with open(ref_filename) as f_ref:
|
|
391
|
+
for line, line_ref in zip(f, f_ref):
|
|
392
|
+
assert line == line_ref
|
|
393
|
+
print("Check OK")
|
|
394
|
+
return True
|
|
395
|
+
except AssertionError:
|
|
396
|
+
# The solution is not identical: we calculate the L2 norm of the error
|
|
397
|
+
data = np.loadtxt(filename)
|
|
398
|
+
ref = np.loadtxt(ref_filename)
|
|
399
|
+
nmax = min(data.shape[0], ref.shape[0])
|
|
400
|
+
error = np.linalg.norm(data[:nmax, 1] - ref[:nmax, 1], ord=2)
|
|
401
|
+
norm = np.linalg.norm(ref[:nmax, 1], ord=2)
|
|
402
|
+
print(f"Relative error (L2 norm): {error/norm = :08e}")
|
|
403
|
+
return False
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def parse_args(args) -> argparse.Namespace:
|
|
407
|
+
"""Argument parser for llg3d_seq"""
|
|
408
|
+
parser = argparse.ArgumentParser(
|
|
409
|
+
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
410
|
+
)
|
|
411
|
+
parser.add_argument(
|
|
412
|
+
"-c", "--check", action="store_true", help="Check against the reference"
|
|
413
|
+
)
|
|
414
|
+
parser.add_argument(
|
|
415
|
+
"-element", type=str, default="Cobalt", help="Element of the sample"
|
|
416
|
+
)
|
|
417
|
+
# Add arguments from the parameters dictionary
|
|
418
|
+
for name, data in parameters.items():
|
|
419
|
+
value, description = data
|
|
420
|
+
parser.add_argument(
|
|
421
|
+
f"-{name}", type=type(value), help=description, default=value
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return parser.parse_args(args)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def main(args_main=None):
|
|
428
|
+
"""Evaluates the command line and starts the simulation"""
|
|
429
|
+
args = parse_args(args_main)
|
|
430
|
+
|
|
431
|
+
check = args.check
|
|
432
|
+
del args.check
|
|
433
|
+
N = args.N
|
|
434
|
+
|
|
435
|
+
# Convert the element object from the string
|
|
436
|
+
if "element" in args:
|
|
437
|
+
vars(args)["element"] = globals()[args.element]
|
|
438
|
+
grid, total_time = simulate(**vars(args))
|
|
439
|
+
|
|
440
|
+
print(f"{N = } iterations")
|
|
441
|
+
print(f"total_time [s] = {total_time:.03f}")
|
|
442
|
+
print(f"temps/ite [s/ite] = {total_time / N:.03e}")
|
|
443
|
+
if check:
|
|
444
|
+
check_solution(grid, args.T)
|
|
445
|
+
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
main()
|
llg3d/post/__init__.py
ADDED
|
File without changes
|
llg3d/post/process.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Post-processes a set of runs grouped into a `run.json` file or
|
|
4
|
+
into a set of SLURM job arrays:
|
|
5
|
+
|
|
6
|
+
1. Extracts result data,
|
|
7
|
+
2. Plots the computed average magnetization against temperature,
|
|
8
|
+
3. Interpolates the computed points using cubic splines,
|
|
9
|
+
4. Determines the Curie temperature as the value corresponding to the minimal (negative) slope of the interpolated curve.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from scipy.interpolate import interp1d
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MagData:
|
|
20
|
+
"""
|
|
21
|
+
Class to handle magnetization data and interpolation according to temperature
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
n_interp = 200
|
|
25
|
+
|
|
26
|
+
def __init__(self, job_dir: Path = None, run_file: Path = Path("run.json")) -> None:
|
|
27
|
+
|
|
28
|
+
if job_dir:
|
|
29
|
+
self.parentpath = job_dir
|
|
30
|
+
data, self.run = self.process_slurm_jobs()
|
|
31
|
+
elif run_file:
|
|
32
|
+
self.parentpath = run_file.parent
|
|
33
|
+
data, self.run = self.process_json(run_file)
|
|
34
|
+
|
|
35
|
+
self.temperature = data[:, 0]
|
|
36
|
+
self.m1_mean = data[:, 1]
|
|
37
|
+
self.interp = interp1d(self.temperature, self.m1_mean, kind="cubic")
|
|
38
|
+
self.T = np.linspace(
|
|
39
|
+
self.temperature.min(), self.temperature.max(), self.n_interp
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def process_slurm_jobs(self) -> tuple[np.array, dict]:
|
|
43
|
+
"""
|
|
44
|
+
Iterates through calculation directories to assemble data.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
parentdir (str): path to the directory containing the runs
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
tuple: (data, run) where data is a numpy array (T, <m>) and run
|
|
51
|
+
is a descriptive dictionary of the run
|
|
52
|
+
"""
|
|
53
|
+
json_filename = "run.json"
|
|
54
|
+
|
|
55
|
+
# List of run directories
|
|
56
|
+
jobdirs = [f for f in self.parentpath.iterdir() if f.is_dir()]
|
|
57
|
+
if len(jobdirs) == 0:
|
|
58
|
+
exit(f"No job directories found in {self.parentpath}")
|
|
59
|
+
data = []
|
|
60
|
+
# Iterating through run directories
|
|
61
|
+
for jobdir in jobdirs:
|
|
62
|
+
try:
|
|
63
|
+
# Reading the JSON file
|
|
64
|
+
with open(jobdir / json_filename) as f:
|
|
65
|
+
run = json.load(f)
|
|
66
|
+
# Adding temperature and averaging value to the data list
|
|
67
|
+
data.extend(
|
|
68
|
+
[[float(T), res["m1_mean"]] for T, res in run["results"].items()]
|
|
69
|
+
)
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
print(f"Warning: {json_filename} file not found " f"in {jobdir.as_posix()}")
|
|
72
|
+
|
|
73
|
+
data.sort() # Sorting by increasing temperatures
|
|
74
|
+
|
|
75
|
+
return np.array(data), run
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def process_json(json_filepath: Path) -> tuple[np.array, dict]:
|
|
79
|
+
"""
|
|
80
|
+
Reads the run.json file and extracts result data.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
json_filepath: path to the run.json file
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
tuple: (data, run) where data is a numpy array (T, <m>) and run
|
|
87
|
+
is a descriptive dictionary of the run
|
|
88
|
+
"""
|
|
89
|
+
with open(json_filepath) as f:
|
|
90
|
+
run = json.load(f)
|
|
91
|
+
|
|
92
|
+
data = [[int(T), res["m1_mean"]] for T, res in run["results"].items()]
|
|
93
|
+
|
|
94
|
+
data.sort() # Sorting by increasing temperatures
|
|
95
|
+
|
|
96
|
+
return np.array(data), run
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def T_Curie(self) -> float:
|
|
100
|
+
"""
|
|
101
|
+
Return the Curie temperature defined as the temperature at
|
|
102
|
+
which the magnetization is below 0.1
|
|
103
|
+
"""
|
|
104
|
+
i_max = np.where(0.1 - self.interp(self.T) > 0)[0].min()
|
|
105
|
+
return self.T[i_max]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Plot the magnetization vs temperature and determine the Curie temperature.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .process import MagData
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plot_m_vs_T(m: MagData, show: bool):
|
|
16
|
+
"""
|
|
17
|
+
Plots the data (T, <m>), interpolates the values,
|
|
18
|
+
calculates the Curie temperature.
|
|
19
|
+
Exports to PNG.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
data: numpy array (T, <m>)
|
|
23
|
+
parentdir: path to the directory containing the runs
|
|
24
|
+
run: descriptive dictionary of the run
|
|
25
|
+
show: display the graph in a graphical window
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
print(f"T_Curie = {m.T_Curie:.0f} K")
|
|
29
|
+
|
|
30
|
+
fig, ax = plt.subplots()
|
|
31
|
+
fig.suptitle("Average magnetization vs Temperature")
|
|
32
|
+
params = m.run["params"]
|
|
33
|
+
ax.set_title(
|
|
34
|
+
params["element"]
|
|
35
|
+
+ rf", ${params['Jx']}\times{params['Jy']}\times{params['Jz']}$"
|
|
36
|
+
rf" ($dx = ${params['dx']})",
|
|
37
|
+
fontdict={"size": 10},
|
|
38
|
+
)
|
|
39
|
+
ax.plot(m.temperature, m.m1_mean, "o", label="computed")
|
|
40
|
+
ax.plot(m.T, m.interp(m.T), label="interpolated (cubic)")
|
|
41
|
+
ax.annotate(
|
|
42
|
+
"$T_{{Curie}} = {:.0f} K$".format(m.T_Curie),
|
|
43
|
+
xy=(m.T_Curie, m.interp(m.T_Curie)),
|
|
44
|
+
xytext=(m.T_Curie + 20, m.interp(m.T_Curie) + 0.01),
|
|
45
|
+
)
|
|
46
|
+
ax.axvline(x=m.T_Curie, color="k")
|
|
47
|
+
ax.set_xlabel("Temperature [K]")
|
|
48
|
+
ax.set_ylabel("Magnetization")
|
|
49
|
+
ax.legend()
|
|
50
|
+
|
|
51
|
+
if show:
|
|
52
|
+
plt.show()
|
|
53
|
+
|
|
54
|
+
image_filename = m.parentpath / "m1_mean.png"
|
|
55
|
+
fig.savefig(image_filename)
|
|
56
|
+
print(f"Image saved in {image_filename}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
"""
|
|
61
|
+
Parses the command line to execute processing functions
|
|
62
|
+
"""
|
|
63
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
64
|
+
parser.add_argument("--job_dir", type=Path, help="Slurm main job directory")
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--run_file", type=Path, default="run.json", help="Path to the run.json file"
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"-s",
|
|
70
|
+
"--show",
|
|
71
|
+
action="store_true",
|
|
72
|
+
default=False,
|
|
73
|
+
help="Display the graph in a graphical window",
|
|
74
|
+
)
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
if args.job_dir:
|
|
77
|
+
m = MagData(job_dir=args.job_dir)
|
|
78
|
+
else:
|
|
79
|
+
m = MagData(run_file=args.run_file)
|
|
80
|
+
plot_m_vs_T(m, args.show)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 IRMA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: llg3d
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Solveur pour l'équation de Landau-Lifshitz-Gilbert stochastique en 3D
|
|
5
|
+
Author-email: IRMA <matthieu.boileau@math.unistra.fr>
|
|
6
|
+
Project-URL: Homepage, https://gitlab.math.unistra.fr/llg3d/llg3d
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
License-File: AUTHORS
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: mpi4py
|
|
16
|
+
Requires-Dist: matplotlib
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Provides-Extra: doc
|
|
19
|
+
Requires-Dist: Sphinx >=7.2.2 ; extra == 'doc'
|
|
20
|
+
Requires-Dist: myst-parser ; extra == 'doc'
|
|
21
|
+
Requires-Dist: furo ; extra == 'doc'
|
|
22
|
+
Requires-Dist: nbsphinx ; extra == 'doc'
|
|
23
|
+
Requires-Dist: sphinx-copybutton ; extra == 'doc'
|
|
24
|
+
Requires-Dist: sphinx-autobuild ; extra == 'doc'
|
|
25
|
+
Requires-Dist: sphinx-prompt ; extra == 'doc'
|
|
26
|
+
Requires-Dist: sphinx-last-updated-by-git ; extra == 'doc'
|
|
27
|
+
Requires-Dist: sphinxcontrib-programoutput ; extra == 'doc'
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
30
|
+
Requires-Dist: pytest-cov ; extra == 'test'
|
|
31
|
+
Requires-Dist: pytest-mpi ; extra == 'test'
|
|
32
|
+
|
|
33
|
+
# LLG3D: A solver for the stochastic Landau-Lifshitz-Gilbert equation in 3D
|
|
34
|
+
|
|
35
|
+
[](https://gitlab.math.unistra.fr/llg3d/llg3d/-/commits/main)
|
|
36
|
+
[](https://llg3d.pages.math.unistra.fr/llg3d/coverage)
|
|
37
|
+
[](https://gitlab.math.unistra.fr/llg3d/llg3d/-/releases)
|
|
38
|
+
[](https://llg3d.pages.math.unistra.fr/llg3d/)
|
|
39
|
+
|
|
40
|
+
LLG3D is written in Python and utilizes the MPI library for parallelizing computations.
|
|
41
|
+
|
|
42
|
+
See the [documentation](https://llg3d.pages.math.unistra.fr/llg3d/).
|