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/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