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