llg3d 1.4.1__py3-none-any.whl → 2.0.1__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 +6 -1
- llg3d/__main__.py +6 -0
- llg3d/element.py +134 -0
- llg3d/grid.py +123 -0
- llg3d/main.py +67 -0
- llg3d/output.py +107 -0
- llg3d/parameters.py +75 -0
- llg3d/post/__init__.py +1 -1
- llg3d/post/plot_results.py +61 -0
- llg3d/post/process.py +18 -13
- llg3d/post/temperature.py +6 -14
- llg3d/simulation.py +95 -0
- llg3d/solver/__init__.py +45 -0
- llg3d/solver/jax.py +387 -0
- llg3d/solver/mpi.py +450 -0
- llg3d/solver/numpy.py +207 -0
- llg3d/solver/opencl.py +330 -0
- llg3d/solver/solver.py +89 -0
- {llg3d-1.4.1.dist-info → llg3d-2.0.1.dist-info}/METADATA +14 -22
- llg3d-2.0.1.dist-info/RECORD +25 -0
- {llg3d-1.4.1.dist-info → llg3d-2.0.1.dist-info}/WHEEL +1 -1
- llg3d-2.0.1.dist-info/entry_points.txt +4 -0
- llg3d/llg3d.py +0 -742
- llg3d/llg3d_seq.py +0 -447
- llg3d-1.4.1.dist-info/RECORD +0 -13
- llg3d-1.4.1.dist-info/entry_points.txt +0 -3
- {llg3d-1.4.1.dist-info → llg3d-2.0.1.dist-info/licenses}/AUTHORS +0 -0
- {llg3d-1.4.1.dist-info → llg3d-2.0.1.dist-info/licenses}/LICENSE +0 -0
- {llg3d-1.4.1.dist-info → llg3d-2.0.1.dist-info}/top_level.txt +0 -0
llg3d/solver/mpi.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLG3D Solver using MPI.
|
|
3
|
+
|
|
4
|
+
The parallelization is done in the x direction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from mpi4py import MPI
|
|
13
|
+
|
|
14
|
+
from .. import comm, rank, size, status
|
|
15
|
+
from ..output import progress_bar, get_output_files, close_output_files
|
|
16
|
+
from ..grid import Grid
|
|
17
|
+
from ..element import Element
|
|
18
|
+
from .solver import cross_product, compute_H_anisotropy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_boundaries_x(
|
|
22
|
+
g: Grid, m: np.ndarray, blocking: bool = False
|
|
23
|
+
) -> tuple[np.ndarray, np.ndarray, MPI.Request, MPI.Request]:
|
|
24
|
+
"""
|
|
25
|
+
Returns the boundaries asynchronously.
|
|
26
|
+
|
|
27
|
+
Allows overlapping communication time of boundaries with calculations.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
g: Grid object
|
|
31
|
+
m: Magnetization array (shape (3, nx, ny, nz))
|
|
32
|
+
blocking: Whether to use blocking communication for boundaries
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
- m_i_x_start: Start boundary in x direction
|
|
36
|
+
- m_i_x_end: End boundary in x direction
|
|
37
|
+
- request_start: Request for start boundary
|
|
38
|
+
- request_end: Request for end boundary
|
|
39
|
+
"""
|
|
40
|
+
# Extract slices for Neumann boundary conditions
|
|
41
|
+
|
|
42
|
+
m_i_x_start = np.empty((1, g.Jy, g.Jz))
|
|
43
|
+
m_i_x_end = np.empty_like(m_i_x_start)
|
|
44
|
+
|
|
45
|
+
# Prepare ring communication:
|
|
46
|
+
# Even if procs 0 and size - 1 shouldn't receive anything from left
|
|
47
|
+
# and right respectively, it's simpler to express it like this
|
|
48
|
+
right = (rank + 1) % size
|
|
49
|
+
left = (rank - 1 + size) % size
|
|
50
|
+
|
|
51
|
+
if blocking:
|
|
52
|
+
# Wait for boundaries to be available
|
|
53
|
+
comm.Sendrecv(
|
|
54
|
+
m[:1, :, :], dest=left, sendtag=0, recvbuf=m_i_x_end, source=right
|
|
55
|
+
)
|
|
56
|
+
comm.Sendrecv(
|
|
57
|
+
m[-1:, :, :], dest=right, sendtag=1, recvbuf=m_i_x_start, source=left
|
|
58
|
+
)
|
|
59
|
+
return m_i_x_start, m_i_x_end, None, None
|
|
60
|
+
else:
|
|
61
|
+
request_start = comm.Irecv(m_i_x_start, source=left, tag=201)
|
|
62
|
+
request_end = comm.Irecv(m_i_x_end, source=right, tag=202)
|
|
63
|
+
comm.Isend(m[-1:, :, :], dest=right, tag=201)
|
|
64
|
+
comm.Isend(m[:1, :, :], dest=left, tag=202)
|
|
65
|
+
|
|
66
|
+
return m_i_x_start, m_i_x_end, request_start, request_end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def laplacian3D(
|
|
70
|
+
m_i: np.ndarray,
|
|
71
|
+
dx2_inv: float,
|
|
72
|
+
dy2_inv: float,
|
|
73
|
+
dz2_inv: float,
|
|
74
|
+
center_coeff: float,
|
|
75
|
+
m_i_x_start: np.ndarray,
|
|
76
|
+
m_i_x_end: np.ndarray,
|
|
77
|
+
request_end: MPI.Request,
|
|
78
|
+
request_start: MPI.Request,
|
|
79
|
+
) -> np.ndarray:
|
|
80
|
+
"""
|
|
81
|
+
Returns the Laplacian of m_i in 3D.
|
|
82
|
+
|
|
83
|
+
We start by calculating contributions in y and z, to wait
|
|
84
|
+
for the end of communications in x.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
m_i: i-component of the magnetization array (shape (nx, ny, nz))
|
|
88
|
+
dx2_inv: inverse of the squared grid spacing in x direction
|
|
89
|
+
dy2_inv: inverse of the squared grid spacing in y direction
|
|
90
|
+
dz2_inv: inverse of the squared grid spacing in z direction
|
|
91
|
+
center_coeff: center coefficient for the Laplacian
|
|
92
|
+
m_i_x_start: start boundary in x direction
|
|
93
|
+
m_i_x_end: end boundary in x direction
|
|
94
|
+
request_start: request for start boundary
|
|
95
|
+
request_end: request for end boundary
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Laplacian of m_i (shape (nx, ny, nz))
|
|
99
|
+
"""
|
|
100
|
+
# Extract slices for Neumann boundary conditions
|
|
101
|
+
m_i_y_start = m_i[:, 1:2, :]
|
|
102
|
+
m_i_y_end = m_i[:, -2:-1, :]
|
|
103
|
+
|
|
104
|
+
m_i_z_start = m_i[:, :, 1:2]
|
|
105
|
+
m_i_z_end = m_i[:, :, -2:-1]
|
|
106
|
+
|
|
107
|
+
m_i_y_plus = np.concatenate((m_i[:, 1:, :], m_i_y_end), axis=1)
|
|
108
|
+
m_i_y_minus = np.concatenate((m_i_y_start, m_i[:, :-1, :]), axis=1)
|
|
109
|
+
m_i_z_plus = np.concatenate((m_i[:, :, 1:], m_i_z_end), axis=2)
|
|
110
|
+
m_i_z_minus = np.concatenate((m_i_z_start, m_i[:, :, :-1]), axis=2)
|
|
111
|
+
|
|
112
|
+
laplacian = (
|
|
113
|
+
dy2_inv * (m_i_y_plus + m_i_y_minus)
|
|
114
|
+
+ dz2_inv * (m_i_z_plus + m_i_z_minus)
|
|
115
|
+
+ center_coeff * m_i
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Wait for x-boundaries to be available (communications completed)
|
|
119
|
+
try:
|
|
120
|
+
request_end.Wait(status)
|
|
121
|
+
request_start.Wait(status)
|
|
122
|
+
except AttributeError:
|
|
123
|
+
pass # Blocking case
|
|
124
|
+
|
|
125
|
+
# For extreme procs, apply Neumann boundary conditions in x
|
|
126
|
+
if rank == size - 1:
|
|
127
|
+
m_i_x_end = m_i[-2:-1, :, :]
|
|
128
|
+
if rank == 0:
|
|
129
|
+
m_i_x_start = m_i[1:2, :, :]
|
|
130
|
+
|
|
131
|
+
m_i_x_plus = np.concatenate((m_i[1:, :, :], m_i_x_end), axis=0)
|
|
132
|
+
m_i_x_minus = np.concatenate((m_i_x_start, m_i[:-1, :, :]), axis=0)
|
|
133
|
+
|
|
134
|
+
laplacian += dx2_inv * (m_i_x_plus + m_i_x_minus)
|
|
135
|
+
|
|
136
|
+
return laplacian
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def compute_laplacian(
|
|
140
|
+
g: Grid,
|
|
141
|
+
m: np.ndarray,
|
|
142
|
+
boundaries: tuple[np.ndarray, np.ndarray, MPI.Request, MPI.Request],
|
|
143
|
+
) -> np.ndarray:
|
|
144
|
+
"""
|
|
145
|
+
Compute the laplacian of m in 3D.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
g: Grid object
|
|
149
|
+
m: Magnetization array (shape (3, nx, ny, nz))
|
|
150
|
+
boundaries: Boundaries for x direction
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Laplacian of m (shape (3, nx, ny, nz))
|
|
154
|
+
"""
|
|
155
|
+
dx2_inv, dy2_inv, dz2_inv, center_coeff = g.get_laplacian_coeff()
|
|
156
|
+
|
|
157
|
+
return np.stack(
|
|
158
|
+
[
|
|
159
|
+
laplacian3D(m[0], dx2_inv, dy2_inv, dz2_inv, center_coeff, *boundaries[0]),
|
|
160
|
+
laplacian3D(m[1], dx2_inv, dy2_inv, dz2_inv, center_coeff, *boundaries[1]),
|
|
161
|
+
laplacian3D(m[2], dx2_inv, dy2_inv, dz2_inv, center_coeff, *boundaries[2]),
|
|
162
|
+
],
|
|
163
|
+
axis=0,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def compute_slope(
|
|
168
|
+
e: Element,
|
|
169
|
+
g: Grid,
|
|
170
|
+
m: np.ndarray,
|
|
171
|
+
R_random: np.ndarray,
|
|
172
|
+
boundaries: tuple[np.ndarray, np.ndarray, MPI.Request, MPI.Request],
|
|
173
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
174
|
+
"""
|
|
175
|
+
Compute the slope of the LLG equation.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
g: Grid object
|
|
179
|
+
e: Element object
|
|
180
|
+
m: Magnetization array (shape (3, nx, ny, nz))
|
|
181
|
+
R_random: Random field array (shape (3, nx, ny, nz))
|
|
182
|
+
boundaries: Boundaries for x direction
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Slope array (shape (3, nx, ny, nz))
|
|
186
|
+
"""
|
|
187
|
+
# Precalculate terms used multiple times
|
|
188
|
+
|
|
189
|
+
H_aniso = compute_H_anisotropy(e, m)
|
|
190
|
+
|
|
191
|
+
laplacian_m = compute_laplacian(g, m, boundaries)
|
|
192
|
+
R_eff = e.coeff_1 * laplacian_m + R_random + H_aniso
|
|
193
|
+
R_eff[0] += e.coeff_3
|
|
194
|
+
|
|
195
|
+
m_cross_R_eff = cross_product(m, R_eff)
|
|
196
|
+
m_cross_m_cross_R_eff = cross_product(m, m_cross_R_eff)
|
|
197
|
+
|
|
198
|
+
s = -(m_cross_R_eff + e.lambda_G * m_cross_m_cross_R_eff)
|
|
199
|
+
|
|
200
|
+
return s
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def space_average(g: Grid, m: np.ndarray) -> float:
|
|
204
|
+
"""
|
|
205
|
+
Returns the spatial average of m with shape (g.dims) using the midpoint method.
|
|
206
|
+
|
|
207
|
+
Performs the local sum on each process and then reduces it to process 0.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
g: Grid object
|
|
211
|
+
m: Array to be integrated (shape (x, y, z))
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Spatial average of m
|
|
215
|
+
"""
|
|
216
|
+
# Make a copy of m to avoid modifying its value
|
|
217
|
+
mm = m.copy()
|
|
218
|
+
|
|
219
|
+
# On y and z edges, divide the contribution by 2
|
|
220
|
+
mm[:, 0, :] /= 2
|
|
221
|
+
mm[:, -1, :] /= 2
|
|
222
|
+
mm[:, :, 0] /= 2
|
|
223
|
+
mm[:, :, -1] /= 2
|
|
224
|
+
|
|
225
|
+
# On x edges (only on extreme procs), divide the contribution by 2
|
|
226
|
+
if rank == 0:
|
|
227
|
+
mm[0] /= 2
|
|
228
|
+
if rank == size - 1:
|
|
229
|
+
mm[-1] /= 2
|
|
230
|
+
local_sum = mm.sum()
|
|
231
|
+
|
|
232
|
+
# Sum across all processes gathered by process 0
|
|
233
|
+
global_sum = comm.reduce(local_sum)
|
|
234
|
+
|
|
235
|
+
# Spatial average is the global sum divided by the number of cells
|
|
236
|
+
return global_sum / g.ncell if rank == 0 else 0.0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def integral_yz(m: np.ndarray) -> np.ndarray:
|
|
240
|
+
"""
|
|
241
|
+
Returns the spatial average of m using the midpoint method along y and z.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
m: Array to be integrated
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
np.ndarray: Spatial average of m in y and z of shape (g.dims[0],)
|
|
248
|
+
"""
|
|
249
|
+
# Make a copy of m to avoid modifying its value
|
|
250
|
+
mm = m.copy()
|
|
251
|
+
|
|
252
|
+
# On y and z edges, divide the contribution by 2
|
|
253
|
+
mm[:, 0, :] /= 2
|
|
254
|
+
mm[:, -1, :] /= 2
|
|
255
|
+
mm[:, :, 0] /= 2
|
|
256
|
+
mm[:, :, -1] /= 2
|
|
257
|
+
|
|
258
|
+
n_cell_yz = (mm.shape[1] - 1) * (mm.shape[2] - 1)
|
|
259
|
+
return mm.sum(axis=(1, 2)) / n_cell_yz
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def profile(m: np.ndarray, m_xprof: np.ndarray):
|
|
263
|
+
"""
|
|
264
|
+
Retrieves the x profile of the average of m in y and z.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
m: Array to be integrated
|
|
268
|
+
m_xprof: Array to store the x profile
|
|
269
|
+
"""
|
|
270
|
+
# Gather m in mglob
|
|
271
|
+
m_mean_yz = integral_yz(m)
|
|
272
|
+
comm.Gather(m_mean_yz, m_xprof)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def theta_init(t: float, g: Grid) -> np.ndarray:
|
|
276
|
+
"""Initialization of theta."""
|
|
277
|
+
x, y, z = g.get_mesh(local=True)
|
|
278
|
+
return np.zeros(g.dims)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def phi_init(t: float, g: Grid, e: Element) -> np.ndarray:
|
|
282
|
+
"""Initialization of phi."""
|
|
283
|
+
# return np.zeros(shape) + e.coeff_3 * t
|
|
284
|
+
return np.zeros(g.dims) + e.gamma_0 * e.H_ext * t
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def simulate(
|
|
288
|
+
N: int,
|
|
289
|
+
Jx: int,
|
|
290
|
+
Jy: int,
|
|
291
|
+
Jz: int,
|
|
292
|
+
dx: float,
|
|
293
|
+
T: float,
|
|
294
|
+
H_ext: float,
|
|
295
|
+
dt: float,
|
|
296
|
+
start_averaging: int,
|
|
297
|
+
n_mean: int,
|
|
298
|
+
n_profile: int,
|
|
299
|
+
element_class: Element,
|
|
300
|
+
seed: int,
|
|
301
|
+
blocking: bool,
|
|
302
|
+
**_,
|
|
303
|
+
) -> tuple[float, str, float]:
|
|
304
|
+
"""
|
|
305
|
+
Simulates the system for N iterations.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
N: Number of iterations
|
|
309
|
+
Jx: Number of grid points in x direction
|
|
310
|
+
Jy: Number of grid points in y direction
|
|
311
|
+
Jz: Number of grid points in z direction
|
|
312
|
+
dx: Grid spacing
|
|
313
|
+
T: Temperature in Kelvin
|
|
314
|
+
H_ext: External magnetic field strength
|
|
315
|
+
dt: Time step for the simulation
|
|
316
|
+
start_averaging: Number of iterations for averaging
|
|
317
|
+
n_mean: Number of iterations for integral output
|
|
318
|
+
n_profile: Number of iterations for profile output
|
|
319
|
+
element_class: Element of the sample (default: Cobalt)
|
|
320
|
+
blocking: Whether to use blocking communication for boundaries
|
|
321
|
+
seed: Random seed for temperature fluctuations
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
- The grid object
|
|
325
|
+
- The time taken for the simulation
|
|
326
|
+
- The output filename
|
|
327
|
+
"""
|
|
328
|
+
if Jx % size != 0:
|
|
329
|
+
if rank == 0:
|
|
330
|
+
print(
|
|
331
|
+
f"Error: Jx must be divisible by the number of processes"
|
|
332
|
+
f"({Jx = }, np = {size})"
|
|
333
|
+
)
|
|
334
|
+
comm.barrier()
|
|
335
|
+
MPI.Finalize()
|
|
336
|
+
exit(2)
|
|
337
|
+
|
|
338
|
+
# Initialize a sequence of random seeds
|
|
339
|
+
# See: https://numpy.org/doc/stable/reference/random/parallel.html
|
|
340
|
+
ss = np.random.SeedSequence(seed)
|
|
341
|
+
|
|
342
|
+
# Deploy size x SeedSequence to pass to child processes
|
|
343
|
+
child_seeds = ss.spawn(size)
|
|
344
|
+
streams = [np.random.default_rng(s) for s in child_seeds]
|
|
345
|
+
rng = streams[rank]
|
|
346
|
+
|
|
347
|
+
# Create the grid
|
|
348
|
+
g = Grid(Jx, Jy, Jz, dx)
|
|
349
|
+
|
|
350
|
+
if rank == 0:
|
|
351
|
+
print(g)
|
|
352
|
+
|
|
353
|
+
e = element_class(T, H_ext, g, dt)
|
|
354
|
+
if rank == 0:
|
|
355
|
+
print(f"CFL = {e.get_CFL()}")
|
|
356
|
+
|
|
357
|
+
m_n = np.zeros((3,) + g.dims)
|
|
358
|
+
|
|
359
|
+
theta = theta_init(0, g)
|
|
360
|
+
phi = phi_init(0, g, e)
|
|
361
|
+
|
|
362
|
+
m_n[0] = np.cos(theta)
|
|
363
|
+
m_n[1] = np.sin(theta) * np.cos(phi)
|
|
364
|
+
m_n[2] = np.sin(theta) * np.sin(phi)
|
|
365
|
+
|
|
366
|
+
m_xprof = np.zeros(g.Jx) # global coordinates
|
|
367
|
+
|
|
368
|
+
f_mean, f_profiles, output_filenames = get_output_files(g, T, n_mean, n_profile)
|
|
369
|
+
|
|
370
|
+
t = 0.0
|
|
371
|
+
m1_average = 0.0
|
|
372
|
+
|
|
373
|
+
start_time = time.perf_counter()
|
|
374
|
+
|
|
375
|
+
for n in progress_bar(range(1, N + 1), "Iteration: ", 40):
|
|
376
|
+
t += dt
|
|
377
|
+
|
|
378
|
+
# Prediction phase
|
|
379
|
+
x_boundaries = [
|
|
380
|
+
get_boundaries_x(g, m_n[i], blocking=blocking) for i in range(3)
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
# adding randomness: effect of temperature
|
|
384
|
+
R_random = e.coeff_4 * rng.standard_normal((3, *g.dims))
|
|
385
|
+
|
|
386
|
+
s_pre = compute_slope(e, g, m_n, R_random, x_boundaries)
|
|
387
|
+
m_pre = m_n + dt * s_pre
|
|
388
|
+
|
|
389
|
+
# correction phase
|
|
390
|
+
x_boundaries = [
|
|
391
|
+
get_boundaries_x(g, m_pre[i], blocking=blocking) for i in range(3)
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
s_cor = compute_slope(e, g, m_pre, R_random, x_boundaries)
|
|
395
|
+
m_n += dt * 0.5 * (s_pre + s_cor)
|
|
396
|
+
|
|
397
|
+
# renormalize to verify the constraint of being on the sphere
|
|
398
|
+
norm = np.sqrt(m_n[0] ** 2 + m_n[1] ** 2 + m_n[2] ** 2)
|
|
399
|
+
m_n /= norm
|
|
400
|
+
|
|
401
|
+
# Export the average of m1 to a file
|
|
402
|
+
if n_mean != 0 and n % n_mean == 0:
|
|
403
|
+
m1_mean = space_average(g, m_n[0])
|
|
404
|
+
if rank == 0:
|
|
405
|
+
if n >= start_averaging:
|
|
406
|
+
m1_average += m1_mean * n_mean
|
|
407
|
+
f_mean.write(f"{t:10.8e} {m1_mean:10.8e}\n")
|
|
408
|
+
# Export the x profiles of the averaged m_i in y and z
|
|
409
|
+
if n_profile != 0 and n % n_profile == 0:
|
|
410
|
+
for i in 0, 1, 2:
|
|
411
|
+
profile(m_n[i], m_xprof)
|
|
412
|
+
if rank == 0:
|
|
413
|
+
# add an x profile to the file
|
|
414
|
+
np.save(f_profiles[i], m_xprof)
|
|
415
|
+
|
|
416
|
+
total_time = time.perf_counter() - start_time
|
|
417
|
+
|
|
418
|
+
if rank == 0:
|
|
419
|
+
close_output_files(f_mean, f_profiles)
|
|
420
|
+
|
|
421
|
+
if n > start_averaging:
|
|
422
|
+
m1_average /= N - start_averaging
|
|
423
|
+
|
|
424
|
+
return total_time, output_filenames, m1_average
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class ArgumentParser(argparse.ArgumentParser):
|
|
428
|
+
"""An argument parser compatible with MPI."""
|
|
429
|
+
|
|
430
|
+
def _print_message(self, message, file=None):
|
|
431
|
+
if rank == 0 and message:
|
|
432
|
+
if file is None:
|
|
433
|
+
file = sys.stderr
|
|
434
|
+
file.write(message)
|
|
435
|
+
|
|
436
|
+
def exit(self, status=0, message=None):
|
|
437
|
+
"""
|
|
438
|
+
Exit the program using MPI finalize.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
status: Exit status code
|
|
442
|
+
message: Optional exit message
|
|
443
|
+
file: Output file (default: stderr)
|
|
444
|
+
|
|
445
|
+
"""
|
|
446
|
+
if message:
|
|
447
|
+
self._print_message(message, sys.stderr)
|
|
448
|
+
comm.barrier()
|
|
449
|
+
MPI.Finalize()
|
|
450
|
+
exit(status)
|
llg3d/solver/numpy.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""LLG3D Solver using NumPy."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..output import progress_bar, get_output_files, close_output_files
|
|
8
|
+
from ..grid import Grid
|
|
9
|
+
from ..element import Element
|
|
10
|
+
from .solver import space_average, cross_product, compute_H_anisotropy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def laplacian3D(
|
|
14
|
+
m_i: np.ndarray, dx2_inv: float, dy2_inv: float, dz2_inv: float, center_coeff: float
|
|
15
|
+
) -> np.ndarray:
|
|
16
|
+
"""
|
|
17
|
+
Returns the laplacian of m in 3D.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
m_i: Magnetization array (shape (nx, ny, nz))
|
|
21
|
+
dx2_inv: Inverse of the square of the grid spacing in x direction
|
|
22
|
+
dy2_inv: Inverse of the square of the grid spacing in y direction
|
|
23
|
+
dz2_inv: Inverse of the square of the grid spacing in z direction
|
|
24
|
+
center_coeff: Coefficient for the center point in the laplacian
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Laplacian of m (shape (nx, ny, nz))
|
|
28
|
+
"""
|
|
29
|
+
m_i_padded = np.pad(m_i, ((1, 1), (1, 1), (1, 1)), mode="reflect")
|
|
30
|
+
|
|
31
|
+
laplacian = (
|
|
32
|
+
dx2_inv * (m_i_padded[2:, 1:-1, 1:-1] + m_i_padded[:-2, 1:-1, 1:-1])
|
|
33
|
+
+ dy2_inv * (m_i_padded[1:-1, 2:, 1:-1] + m_i_padded[1:-1, :-2, 1:-1])
|
|
34
|
+
+ dz2_inv * (m_i_padded[1:-1, 1:-1, 2:] + m_i_padded[1:-1, 1:-1, :-2])
|
|
35
|
+
+ center_coeff * m_i
|
|
36
|
+
)
|
|
37
|
+
return laplacian
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def compute_laplacian(g: Grid, m: np.ndarray) -> np.ndarray:
|
|
41
|
+
"""
|
|
42
|
+
Compute the laplacian of m in 3D.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
g: Grid object
|
|
46
|
+
m: Magnetization array (shape (3, nx, ny, nz))
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Laplacian of m (shape (3, nx, ny, nz))
|
|
50
|
+
"""
|
|
51
|
+
dx2_inv, dy2_inv, dz2_inv, center_coeff = g.get_laplacian_coeff()
|
|
52
|
+
|
|
53
|
+
return np.stack(
|
|
54
|
+
[
|
|
55
|
+
laplacian3D(m[0], dx2_inv, dy2_inv, dz2_inv, center_coeff),
|
|
56
|
+
laplacian3D(m[1], dx2_inv, dy2_inv, dz2_inv, center_coeff),
|
|
57
|
+
laplacian3D(m[2], dx2_inv, dy2_inv, dz2_inv, center_coeff),
|
|
58
|
+
],
|
|
59
|
+
axis=0,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def compute_slope(
|
|
64
|
+
g: Grid, e: Element, m: np.ndarray, R_random: np.ndarray
|
|
65
|
+
) -> np.ndarray:
|
|
66
|
+
"""
|
|
67
|
+
Compute the slope of the LLG equation.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
g: Grid object
|
|
71
|
+
e: Element object
|
|
72
|
+
m: Magnetization array (shape (3, nx, ny, nz))
|
|
73
|
+
R_random: Random field array (shape (3, nx, ny, nz)).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Slope array (shape (3, nx, ny, nz))
|
|
77
|
+
"""
|
|
78
|
+
H_aniso = compute_H_anisotropy(e, m)
|
|
79
|
+
|
|
80
|
+
laplacian_m = compute_laplacian(g, m)
|
|
81
|
+
R_eff = e.coeff_1 * laplacian_m + R_random + H_aniso
|
|
82
|
+
R_eff[0] += e.coeff_3
|
|
83
|
+
|
|
84
|
+
m_cross_R_eff = cross_product(m, R_eff)
|
|
85
|
+
m_cross_m_cross_R_eff = cross_product(m, m_cross_R_eff)
|
|
86
|
+
|
|
87
|
+
s = -(m_cross_R_eff + e.lambda_G * m_cross_m_cross_R_eff)
|
|
88
|
+
|
|
89
|
+
return s
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def simulate(
|
|
93
|
+
N: int,
|
|
94
|
+
Jx: int,
|
|
95
|
+
Jy: int,
|
|
96
|
+
Jz: int,
|
|
97
|
+
dx: float,
|
|
98
|
+
T: float,
|
|
99
|
+
H_ext: float,
|
|
100
|
+
dt: float,
|
|
101
|
+
start_averaging: int,
|
|
102
|
+
n_mean: int,
|
|
103
|
+
n_profile: int,
|
|
104
|
+
element_class: Element,
|
|
105
|
+
precision: str,
|
|
106
|
+
seed: int,
|
|
107
|
+
**_,
|
|
108
|
+
) -> tuple[float, str, float]:
|
|
109
|
+
"""
|
|
110
|
+
Simulates the system for N iterations.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
N: Number of iterations
|
|
114
|
+
Jx: Number of grid points in x direction
|
|
115
|
+
Jy: Number of grid points in y direction
|
|
116
|
+
Jz: Number of grid points in z direction
|
|
117
|
+
dx: Grid spacing
|
|
118
|
+
T: Temperature in Kelvin
|
|
119
|
+
H_ext: External magnetic field strength
|
|
120
|
+
dt: Time step for the simulation
|
|
121
|
+
start_averaging: Number of iterations for averaging
|
|
122
|
+
n_mean: Number of iterations for integral output
|
|
123
|
+
n_profile: Number of iterations for profile output
|
|
124
|
+
element_class: Element of the sample (default: Cobalt)
|
|
125
|
+
precision: Precision of the simulation (single or double)
|
|
126
|
+
seed: Random seed for temperature fluctuations
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
- The time taken for the simulation
|
|
130
|
+
- The output filenames
|
|
131
|
+
- The average magnetization
|
|
132
|
+
"""
|
|
133
|
+
np_float = np.float64 if precision == "double" else np.float32
|
|
134
|
+
# Initialize a sequence of random seeds
|
|
135
|
+
# See: https://numpy.org/doc/stable/reference/random/parallel.html#seedsequence-spawning
|
|
136
|
+
ss = np.random.SeedSequence(seed)
|
|
137
|
+
|
|
138
|
+
# Deploy size x SeedSequence to be passed to child processes
|
|
139
|
+
child_seeds = ss.spawn(1)
|
|
140
|
+
rng = np.random.default_rng(child_seeds[0])
|
|
141
|
+
|
|
142
|
+
g = Grid(Jx, Jy, Jz, dx)
|
|
143
|
+
|
|
144
|
+
dims = g.dims
|
|
145
|
+
|
|
146
|
+
e = element_class(T, H_ext, g, dt)
|
|
147
|
+
print(f"CFL = {e.get_CFL()}")
|
|
148
|
+
|
|
149
|
+
# --- Initialization ---
|
|
150
|
+
|
|
151
|
+
def theta_init(shape):
|
|
152
|
+
"""Initialization of theta."""
|
|
153
|
+
return np.zeros(shape, dtype=np_float)
|
|
154
|
+
|
|
155
|
+
def phi_init(t, shape):
|
|
156
|
+
"""Initialization of phi."""
|
|
157
|
+
return np.zeros(shape, dtype=np_float) + e.gamma_0 * H_ext * t
|
|
158
|
+
|
|
159
|
+
m_n = np.zeros((3,) + dims, dtype=np_float)
|
|
160
|
+
|
|
161
|
+
theta = theta_init(dims)
|
|
162
|
+
phi = phi_init(0, dims)
|
|
163
|
+
|
|
164
|
+
m_n[0] = np.cos(theta)
|
|
165
|
+
m_n[1] = np.sin(theta) * np.cos(phi)
|
|
166
|
+
m_n[2] = np.sin(theta) * np.sin(phi)
|
|
167
|
+
|
|
168
|
+
f_mean, f_profiles, output_filenames = get_output_files(g, T, n_mean, n_profile)
|
|
169
|
+
|
|
170
|
+
t = 0.0
|
|
171
|
+
m1_average = 0.0
|
|
172
|
+
|
|
173
|
+
start_time = time.perf_counter()
|
|
174
|
+
|
|
175
|
+
for n in progress_bar(range(1, N + 1), "Iteration : ", 40):
|
|
176
|
+
t += dt
|
|
177
|
+
|
|
178
|
+
# Adding randomness: temperature effect
|
|
179
|
+
R_random = e.coeff_4 * rng.standard_normal((3,) + dims, dtype=np_float)
|
|
180
|
+
|
|
181
|
+
# Prediction phase
|
|
182
|
+
s_pre = compute_slope(g, e, m_n, R_random)
|
|
183
|
+
m_pre = m_n + dt * s_pre
|
|
184
|
+
|
|
185
|
+
# Correction phase
|
|
186
|
+
s_cor = compute_slope(g, e, m_pre, R_random)
|
|
187
|
+
m_n += dt * 0.5 * (s_pre + s_cor)
|
|
188
|
+
|
|
189
|
+
# We renormalize to check the constraint of being on the sphere
|
|
190
|
+
norm = np.sqrt(m_n[0] ** 2 + m_n[1] ** 2 + m_n[2] ** 2)
|
|
191
|
+
m_n /= norm
|
|
192
|
+
|
|
193
|
+
# Export the average of m1 to a file
|
|
194
|
+
if n_mean != 0 and n % n_mean == 0:
|
|
195
|
+
m1_mean = space_average(g, m_n[0])
|
|
196
|
+
if n >= start_averaging:
|
|
197
|
+
m1_average += m1_mean * n_mean
|
|
198
|
+
f_mean.write(f"{t:10.8e} {m1_mean:10.8e}\n")
|
|
199
|
+
|
|
200
|
+
total_time = time.perf_counter() - start_time
|
|
201
|
+
|
|
202
|
+
close_output_files(f_mean, f_profiles)
|
|
203
|
+
|
|
204
|
+
if n > start_averaging:
|
|
205
|
+
m1_average /= N - start_averaging
|
|
206
|
+
|
|
207
|
+
return total_time, output_filenames, m1_average
|