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/opencl.py ADDED
@@ -0,0 +1,330 @@
1
+ """LLG3D Solver using OpenCL."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import pyopencl as cl
8
+ from pyopencl import clrandom
9
+ from pyopencl import array as clarray
10
+
11
+ from ..output import progress_bar, get_output_files, close_output_files
12
+ from ..grid import Grid
13
+ from ..element import Element, Cobalt
14
+
15
+
16
+ def get_context_and_device(
17
+ device_selection: str = "auto",
18
+ ) -> tuple[cl.Context, cl.Device]:
19
+ """
20
+ Get the OpenCL context and device.
21
+
22
+ Args:
23
+ device_selection:
24
+
25
+ - ``"auto"``: Let OpenCL choose automatically
26
+ - ``"cpu"``: Select CPU device
27
+ - ``"gpu"``: Select first available GPU
28
+ - ``"gpu:N"``: Select specific GPU by index (e.g., ``"gpu:0"``, ``"gpu:1"``)
29
+
30
+ Returns:
31
+ - The OpenCL context
32
+ - The OpenCL device
33
+ """
34
+ if device_selection == "auto":
35
+ context = cl.create_some_context(interactive=False)
36
+ device = context.devices[0]
37
+ return context, device
38
+
39
+ # Get all platforms and devices
40
+ platforms = cl.get_platforms()
41
+ all_devices = []
42
+
43
+ for platform in platforms:
44
+ all_devices.extend(platform.get_devices())
45
+
46
+ if not all_devices:
47
+ raise RuntimeError("No OpenCL devices found")
48
+
49
+ # Filter devices based on selection
50
+ if device_selection == "cpu":
51
+ cpu_devices = [d for d in all_devices if d.type & cl.device_type.CPU]
52
+ if not cpu_devices:
53
+ raise RuntimeError("No CPU devices found")
54
+ selected_device = cpu_devices[0]
55
+ elif device_selection == "gpu":
56
+ gpu_devices = [d for d in all_devices if d.type & cl.device_type.GPU]
57
+ if not gpu_devices:
58
+ raise RuntimeError("No GPU devices found")
59
+ selected_device = gpu_devices[0]
60
+ elif device_selection.startswith("gpu:"):
61
+ gpu_devices = [d for d in all_devices if d.type & cl.device_type.GPU]
62
+ if not gpu_devices:
63
+ raise RuntimeError("No GPU devices found")
64
+
65
+ gpu_index = int(device_selection.split(":")[1])
66
+ if gpu_index >= len(gpu_devices):
67
+ raise RuntimeError(
68
+ f"GPU index {gpu_index} not available. Found {len(gpu_devices)} GPU(s)"
69
+ )
70
+ selected_device = gpu_devices[gpu_index]
71
+ else:
72
+ raise ValueError(f"Invalid device selection: {device_selection}")
73
+
74
+ # Create context with selected device
75
+ context = cl.Context([selected_device])
76
+ print(f"Selected OpenCL device: {selected_device.name} ({selected_device.type})")
77
+
78
+ return context, selected_device
79
+
80
+
81
+ def get_precision(device: cl.Device, precision: str) -> np.dtype:
82
+ """
83
+ Get the numpy float type based on the precision.
84
+
85
+ Args:
86
+ device: OpenCL device
87
+ precision: Precision of the simulation (single or double)
88
+
89
+ Returns:
90
+ The numpy float type (float32 or float64)
91
+
92
+ Raises:
93
+ RuntimeError: If double precision is asked while the device does not support it
94
+ """
95
+ # Check that cl device supports double precision
96
+ if precision == "double" and not device.double_fp_config:
97
+ raise RuntimeError("The selected device does not support double precision.")
98
+
99
+ return np.float64 if precision == "double" else np.float32
100
+
101
+
102
+ class Program:
103
+ """Class to manage the OpenCL kernels for the LLG3D simulation."""
104
+
105
+ def __init__(self, g: Grid, context: cl.Context, np_float: np.dtype):
106
+ self.grid = g
107
+ self.context = context
108
+ self.np_float = np_float
109
+ self.cl_program = self._get_built_program()
110
+
111
+ def _get_built_program(self) -> cl.Program:
112
+ """
113
+ Return the OpenCL program built from the source code.
114
+
115
+ Returns:
116
+ The OpenCL program object
117
+ """
118
+ opencl_code = (Path(__file__).parent / "llg3d.cl").read_text()
119
+ build_options = "-D USE_DOUBLE_PRECISION" if self.np_float == np.float64 else ""
120
+ build_options += (
121
+ f" -D NX={self.grid.Jx} -D NY={self.grid.Jy} -D NZ={self.grid.Jz}"
122
+ )
123
+ return cl.Program(self.context, opencl_code).build(options=build_options)
124
+
125
+ def get_kernel(self, kernel_name: str, arg_types: list = [None]) -> cl.Kernel:
126
+ """
127
+ Returns the specified kernel by name.
128
+
129
+ Args:
130
+ kernel_name: Name of the kernel to retrieve
131
+ arg_types: List of argument types for the kernel
132
+
133
+ Returns:
134
+ The OpenCL kernel object
135
+ """
136
+ kernel: cl.Kernel = getattr(self.cl_program, kernel_name)
137
+ kernel.set_arg_types(arg_types)
138
+ return kernel
139
+
140
+
141
+ def simulate(
142
+ N: int,
143
+ Jx: int,
144
+ Jy: int,
145
+ Jz: int,
146
+ dx: float,
147
+ T: float,
148
+ H_ext: float,
149
+ dt: float,
150
+ start_averaging: int,
151
+ n_mean: int,
152
+ n_profile: int,
153
+ element_class: Element,
154
+ precision: str,
155
+ seed: int,
156
+ device: str = "auto",
157
+ **_,
158
+ ) -> tuple[float, str, float]:
159
+ """
160
+ Simulates the system over N iterations.
161
+
162
+ Args:
163
+ N: Number of iterations
164
+ Jx: Number of grid points in x direction
165
+ Jy: Number of grid points in y direction
166
+ Jz: Number of grid points in z direction
167
+ dx: Grid spacing
168
+ T: Temperature in Kelvin
169
+ H_ext: External magnetic field strength
170
+ dt: Time step for the simulation
171
+ start_averaging: Number of iterations for averaging
172
+ n_mean: Number of iterations for integral output
173
+ n_profile: Number of iterations for profile output
174
+ element_class: Element of the sample (default: Cobalt)
175
+ precision: Precision of the simulation (single or double)
176
+ seed: Random seed for temperature fluctuations
177
+ device: Device to use ('cpu', 'gpu', 'gpu:0', 'gpu:1', etc., or 'auto')
178
+
179
+ Returns:
180
+ - The grid object
181
+ - The time taken for the simulation
182
+ - The output filename
183
+ """
184
+ context, opencl_device = get_context_and_device(device)
185
+ np_float = get_precision(opencl_device, precision)
186
+
187
+ g = Grid(Jx, Jy, Jz, dx)
188
+ print(g)
189
+
190
+ e = element_class(T, H_ext, g, dt)
191
+ if not isinstance(e, Cobalt):
192
+ raise NotImplementedError(
193
+ f"Element is {type(e)} but only {Cobalt} is supported at the moment."
194
+ )
195
+ print(f"CFL = {e.get_CFL()}")
196
+
197
+ # --- Initialization ---
198
+
199
+ def theta_init(shape):
200
+ """Initialization of theta."""
201
+ return np.zeros(shape, dtype=np_float)
202
+
203
+ def phi_init(t, shape):
204
+ """Initialization of phi."""
205
+ return np.zeros(shape, dtype=np_float) + e.gamma_0 * H_ext * t
206
+
207
+ m_n = np.zeros((3,) + g.dims, dtype=np_float)
208
+
209
+ theta = theta_init(g.dims)
210
+ phi = phi_init(0, g.dims)
211
+
212
+ m_n[0] = np.cos(theta)
213
+ m_n[1] = np.sin(theta) * np.cos(phi)
214
+ m_n[2] = np.sin(theta) * np.sin(phi)
215
+
216
+ queue = cl.CommandQueue(context)
217
+
218
+ program = Program(g, context, np_float)
219
+ slope_kernel = program.get_kernel("slope", [None] * 3 + [np_float] * 8)
220
+ update_1_kernel = program.get_kernel("update_1", [None] * 3 + [np_float])
221
+ update_2_kernel = program.get_kernel("update_2", [None] * 4 + [np_float])
222
+ normalize_kernel = program.get_kernel("normalize")
223
+
224
+ # Create a CL array for m1 component in order to compute averages
225
+ d_m1 = clarray.empty(queue, g.ntot, np_float)
226
+ copy_m1_kernel = program.get_kernel("copy_m1", [None, None])
227
+
228
+ mf = cl.mem_flags
229
+ mem_size = m_n.nbytes
230
+
231
+ d_m_n = cl.Buffer(context, mf.READ_WRITE | mf.COPY_HOST_PTR, hostbuf=m_n)
232
+ d_R_alea = cl.array.Array(queue, (3,) + g.dims, np_float)
233
+ d_m_np1 = cl.Buffer(context, mf.READ_WRITE, mem_size)
234
+ d_s_pre = cl.Buffer(context, mf.READ_WRITE, mem_size)
235
+ d_s_cor = cl.Buffer(context, mf.READ_WRITE, mem_size)
236
+
237
+ rng = clrandom.PhiloxGenerator(context, seed=seed)
238
+
239
+ f_mean, f_profiles, output_filenames = get_output_files(g, T, n_mean, n_profile)
240
+
241
+ t = 0.0
242
+ m1_average = 0.0
243
+
244
+ start_time = time.perf_counter()
245
+
246
+ for n in progress_bar(range(1, N + 1), "Iteration : ", 40):
247
+ t += dt
248
+
249
+ rng.fill_normal(d_R_alea)
250
+ queue.finish() # ensure the array is filled
251
+
252
+ # Prediction phase
253
+
254
+ # calculate s_i_pre from m_i^n
255
+ slope_kernel(
256
+ queue,
257
+ g.dims,
258
+ None,
259
+ d_m_n,
260
+ d_R_alea.data,
261
+ d_s_pre,
262
+ g.dx,
263
+ g.dy,
264
+ g.dz,
265
+ e.coeff_1,
266
+ e.coeff_2,
267
+ e.coeff_3,
268
+ e.coeff_4,
269
+ e.lambda_G,
270
+ )
271
+
272
+ # m_i^n+1 = m_i^n + dt * s_i_pre
273
+ update_1_kernel(queue, (3 * g.ntot,), None, d_m_n, d_m_np1, d_s_pre, dt)
274
+ queue.finish()
275
+
276
+ # # Correction phase
277
+
278
+ # calculate s_i_cor from m_i^n+1
279
+ slope_kernel(
280
+ queue,
281
+ g.dims,
282
+ None,
283
+ d_m_np1,
284
+ d_R_alea.data,
285
+ d_s_cor,
286
+ g.dx,
287
+ g.dy,
288
+ g.dz,
289
+ e.coeff_1,
290
+ e.coeff_2,
291
+ e.coeff_3,
292
+ e.coeff_4,
293
+ e.lambda_G,
294
+ )
295
+ # m_i^n+1 = m_i^n + dt * (s_i_pre + s_i_cor) / 2
296
+ # Update using the corrected values
297
+ update_2_kernel(
298
+ queue, (3 * g.ntot,), None, d_m_n, d_m_np1, d_s_pre, d_s_cor, dt
299
+ )
300
+ queue.finish()
301
+
302
+ # Normalization
303
+ normalize_kernel(queue, (g.ntot,), None, d_m_np1).wait()
304
+
305
+ # Swap the buffers for the next iteration
306
+ d_m_n, d_m_np1 = d_m_np1, d_m_n
307
+
308
+ # Space average of m_1 using the midpoint method with OpenCL sum
309
+ if n_mean != 0 and n % n_mean == 0:
310
+ # Copy only the first component from d_m_n to d_m1 with weights applied
311
+ # d_m_n contains [m1, m2, m3] interleaved, we want only m1
312
+ copy_m1_kernel(queue, g.dims, None, d_m_n, d_m1.data)
313
+
314
+ # Use PyOpenCL array sum to compute the weighted sum
315
+ weighted_sum = clarray.sum(d_m1).get()
316
+ m1_mean = weighted_sum / g.ncell
317
+
318
+ if n >= start_averaging:
319
+ m1_average += m1_mean * n_mean
320
+
321
+ f_mean.write(f"{t:10.8e} {m1_mean:10.8e}\n")
322
+
323
+ total_time = time.perf_counter() - start_time
324
+
325
+ close_output_files(f_mean, f_profiles)
326
+
327
+ if n > start_averaging:
328
+ m1_average /= N - start_averaging
329
+
330
+ return total_time, output_filenames, m1_average
llg3d/solver/solver.py ADDED
@@ -0,0 +1,89 @@
1
+ """Common functions for the LLG3D solver."""
2
+
3
+ import numpy as np
4
+
5
+ from ..grid import Grid
6
+ from ..element import Element
7
+
8
+
9
+ def cross_product(a: np.ndarray, b: np.ndarray) -> np.ndarray:
10
+ r"""
11
+ Compute cross product :math:`a \times b`.
12
+
13
+ This implementation is faster than np.cross for large arrays.
14
+
15
+ Args:
16
+ a: First vector (shape (3, nx, ny, nz))
17
+ b: Second vector (shape (3, nx, ny, nz))
18
+
19
+ Returns:
20
+ Cross product :math:`a \times b` (shape (3, nx, ny, nz))
21
+ """
22
+ return np.stack(
23
+ [
24
+ a[1] * b[2] - a[2] * b[1], # x-component
25
+ a[2] * b[0] - a[0] * b[2], # y-component
26
+ a[0] * b[1] - a[1] * b[0], # z-component
27
+ ],
28
+ axis=0,
29
+ )
30
+
31
+
32
+ def compute_H_anisotropy(e: Element, m: np.ndarray) -> np.ndarray:
33
+ """
34
+ Compute the anisotropy field.
35
+
36
+ Args:
37
+ e: Element object
38
+ m: Magnetization array (shape (3, nx, ny, nz)).
39
+
40
+ Returns:
41
+ Anisotropy field array (shape (3, nx, ny, nz))
42
+ """
43
+ m1, m2, m3 = m
44
+
45
+ m1m1 = m1 * m1
46
+ m2m2 = m2 * m2
47
+ m3m3 = m3 * m3
48
+
49
+ if e.anisotropy == "uniaxial":
50
+ aniso_1 = m1
51
+ aniso_2 = np.zeros_like(m1)
52
+ aniso_3 = np.zeros_like(m1)
53
+
54
+ if e.anisotropy == "cubic":
55
+ aniso_1 = -(1 - m1m1 + m2m2 * m3m3) * m1
56
+ aniso_2 = -(1 - m2m2 + m1m1 * m3m3) * m2
57
+ aniso_3 = -(1 - m3m3 + m1m1 * m2m2) * m3
58
+
59
+ return e.coeff_2 * np.stack([aniso_1, aniso_2, aniso_3], axis=0)
60
+
61
+
62
+ def space_average(g: Grid, m: np.ndarray, copy: bool = True) -> float:
63
+ """
64
+ Returns the spatial average of m with shape (g.dims) using the midpoint method.
65
+
66
+ Args:
67
+ g: Grid object
68
+ m: Array to be integrated
69
+ copy: If True, copy m to avoid modifying its value
70
+
71
+ Returns:
72
+ Spatial average of m
73
+ """
74
+ # copy m to avoid modifying its value
75
+ mm = m.copy() if copy else m
76
+
77
+ # on the edges, we divide the contribution by 2
78
+ # x
79
+ mm[0, :, :] /= 2
80
+ mm[-1, :, :] /= 2
81
+ # y
82
+ mm[:, 0, :] /= 2
83
+ mm[:, -1, :] /= 2
84
+ # z
85
+ mm[:, :, 0] /= 2
86
+ mm[:, :, -1] /= 2
87
+
88
+ average = mm.sum() / g.ncell
89
+ return average
@@ -1,36 +1,28 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: llg3d
3
- Version: 1.4.1
4
- Summary: Solveur pour l'équation de Landau-Lifshitz-Gilbert stochastique en 3D
3
+ Version: 2.0.1
4
+ Summary: A solver for the stochastic Landau-Lifshitz-Gilbert equation in 3D
5
5
  Author-email: Clémentine Courtès <clementine.courtes@math.unistra.fr>, Matthieu Boileau <matthieu.boileau@math.unistra.fr>
6
6
  Project-URL: Homepage, https://gitlab.math.unistra.fr/llg3d/llg3d
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
- Requires-Python: >=3.6
10
+ Requires-Python: >=3.9
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  License-File: AUTHORS
14
14
  Requires-Dist: numpy
15
- Requires-Dist: mpi4py
16
15
  Requires-Dist: matplotlib
17
16
  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
- Requires-Dist: sphinxcontrib-bibtex ; extra == 'doc'
29
- Requires-Dist: ipython ; extra == 'doc'
30
- Provides-Extra: test
31
- Requires-Dist: pytest ; extra == 'test'
32
- Requires-Dist: pytest-cov ; extra == 'test'
33
- Requires-Dist: pytest-mpi ; extra == 'test'
17
+ Provides-Extra: mpi
18
+ Requires-Dist: mpi4py; extra == "mpi"
19
+ Provides-Extra: opencl
20
+ Requires-Dist: pyopencl; extra == "opencl"
21
+ Requires-Dist: mako; extra == "opencl"
22
+ Provides-Extra: jax
23
+ Requires-Dist: jax[cuda]; sys_platform != "darwin" and extra == "jax"
24
+ Requires-Dist: jax[cpu]; sys_platform == "darwin" and extra == "jax"
25
+ Dynamic: license-file
34
26
 
35
27
  # LLG3D: A solver for the stochastic Landau-Lifshitz-Gilbert equation in 3D
36
28
 
@@ -40,6 +32,6 @@ Requires-Dist: pytest-mpi ; extra == 'test'
40
32
  [![Doc](https://img.shields.io/badge/doc-sphinx-blue)](https://llg3d.pages.math.unistra.fr/llg3d/)
41
33
  [![SWH](https://archive.softwareheritage.org/badge/origin/https://gitlab.math.unistra.fr/llg3d/llg3d/)](https://archive.softwareheritage.org/browse/origin/?origin_url=https://gitlab.math.unistra.fr/llg3d/llg3d)
42
34
 
43
- LLG3D is written in Python and utilizes the MPI library for parallelizing computations.
35
+ LLG3D is written in Python and may run in parallel using MPI, OpenCL or JAX.
44
36
 
45
37
  See the [documentation](https://llg3d.pages.math.unistra.fr/llg3d/).
@@ -0,0 +1,25 @@
1
+ llg3d/__init__.py,sha256=YhFE4dB9NPWdcadzg14xMgOao1PqDgzxU2iFKEp7Vjs,187
2
+ llg3d/__main__.py,sha256=3D1q7AG5vU6gr-V0iuo5oNYl-Og2SvJ4YaBTdqOVVaw,115
3
+ llg3d/element.py,sha256=B2GDv5lTNTgvbIqSThEaA2o3hQRkhQdIgXg-MxzeSuc,4304
4
+ llg3d/grid.py,sha256=goXO-sh9wJ2YDqe3IerqE6NlvUFacp328RfXHHW6bFY,3848
5
+ llg3d/main.py,sha256=8ltKUT9jA3u1E0cEUjxemRfRPhERnIm_i5L6sP0_G0I,1888
6
+ llg3d/output.py,sha256=HCL1V1oehbV2OERwbS0WCG00W1kNeNQpgcP33LIrGWU,2934
7
+ llg3d/parameters.py,sha256=VoAkm8v6MiOkwRaHupWgHVTQaSDSGIKz5jWnRT8PBx0,2442
8
+ llg3d/simulation.py,sha256=8RbAzLhlhCEZZCGxqNEczxGvfrLsnxKbRCkfpy1cCtU,3429
9
+ llg3d/post/__init__.py,sha256=rX0jQIXGQYzFRoXSsPu2oEDB4YWK5IxFs0cJqXnQb8g,39
10
+ llg3d/post/plot_results.py,sha256=fmCxeKmRfpHa_sNHMqczYNWQOCLJzxfRDQL22AeeJcM,1550
11
+ llg3d/post/process.py,sha256=YKOUIgjAOD3q6_qoxzWVSC0WHBofRZJafwWzdMXWQXA,3569
12
+ llg3d/post/temperature.py,sha256=SHf3s1LHIY0SBDKLoSMGSKo1e7EXcKeMXZdlEsChCBk,2150
13
+ llg3d/solver/__init__.py,sha256=fqd1wZkIAi0sMiOQg0tbfLInMDjTCFkRF5NA_FV1wyU,901
14
+ llg3d/solver/jax.py,sha256=VKn7YanidgXhpoIKEJy0sL0HZ1wV3a9F1uWmbAmWOi0,12026
15
+ llg3d/solver/mpi.py,sha256=hJSXbZxpmi3z6PyMxyvuSBHFaqDv4S_C2FnAwAgI_r4,13041
16
+ llg3d/solver/numpy.py,sha256=3trvsn2W-CjGdFF70oQZ6Jw8avX47OJ1DighschMcHE,6040
17
+ llg3d/solver/opencl.py,sha256=EEExOmSUDs0UPt-E4XvPv4NA_S4SuIqw5Uo9p4KghcM,10222
18
+ llg3d/solver/solver.py,sha256=MXscOjO0-RUIqOir1D23_8xCFw8ulDADTFQwQbMkBvc,2142
19
+ llg3d-2.0.1.dist-info/licenses/AUTHORS,sha256=vhJ88HikYvOrGiB_l1xH2X6hyn9ZJafx6mpoMYNhh1I,297
20
+ llg3d-2.0.1.dist-info/licenses/LICENSE,sha256=aFxTGAyyve8nM9T2jWTarJzQhdSSC3MbbN1heNAev9c,1062
21
+ llg3d-2.0.1.dist-info/METADATA,sha256=J7kKUK6YL7cIKhHmEJLMud6rzr93AgCGozbmHIYVwPY,1948
22
+ llg3d-2.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ llg3d-2.0.1.dist-info/entry_points.txt,sha256=kjuf09uDEuROnDeuHkVen3oAS4ThEPS084TpHmQAbJ8,133
24
+ llg3d-2.0.1.dist-info/top_level.txt,sha256=cBZ0roaXt3CAXqYojuO84lGPCtWuLlXxLGLYRKmHZy0,6
25
+ llg3d-2.0.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ llg3d = llg3d.main:main
3
+ llg3d.plot_results = llg3d.post.plot_results:main
4
+ llg3d.post = llg3d.post.temperature:main