llg3d 2.0.0__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 +3 -1
- llg3d/__main__.py +2 -2
- llg3d/element.py +22 -16
- llg3d/grid.py +7 -10
- llg3d/main.py +6 -5
- llg3d/output.py +9 -10
- llg3d/parameters.py +3 -3
- llg3d/post/__init__.py +1 -1
- llg3d/post/plot_results.py +4 -8
- llg3d/post/process.py +18 -13
- llg3d/post/temperature.py +6 -13
- llg3d/simulation.py +5 -14
- llg3d/solver/__init__.py +2 -2
- llg3d/solver/jax.py +44 -40
- llg3d/solver/mpi.py +25 -24
- llg3d/solver/numpy.py +11 -14
- llg3d/solver/opencl.py +31 -30
- llg3d/solver/solver.py +7 -11
- {llg3d-2.0.0.dist-info → llg3d-2.0.1.dist-info}/METADATA +2 -2
- llg3d-2.0.1.dist-info/RECORD +25 -0
- llg3d-2.0.0.dist-info/RECORD +0 -25
- {llg3d-2.0.0.dist-info → llg3d-2.0.1.dist-info}/WHEEL +0 -0
- {llg3d-2.0.0.dist-info → llg3d-2.0.1.dist-info}/entry_points.txt +0 -0
- {llg3d-2.0.0.dist-info → llg3d-2.0.1.dist-info}/licenses/AUTHORS +0 -0
- {llg3d-2.0.0.dist-info → llg3d-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {llg3d-2.0.0.dist-info → llg3d-2.0.1.dist-info}/top_level.txt +0 -0
llg3d/__init__.py
CHANGED
llg3d/__main__.py
CHANGED
llg3d/element.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""Module containing the definition of the chemical elements."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
@@ -11,8 +11,16 @@ mu_0 = 4 * np.pi * 1.0e-7 #: Vacuum permeability :math:`[H.m^{-1}]`
|
|
|
11
11
|
gamma = 1.76e11 #: Gyromagnetic ratio :math:`[rad.s^{-1}.T^{-1}]`
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class Element:
|
|
15
|
-
"""
|
|
14
|
+
class Element(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Abstract class for chemical elements.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
T: Temperature in Kelvin
|
|
20
|
+
H_ext: External magnetic field strength
|
|
21
|
+
g: Grid object representing the simulation grid
|
|
22
|
+
dt: Time step for the simulation
|
|
23
|
+
"""
|
|
16
24
|
|
|
17
25
|
A = 0.0
|
|
18
26
|
K = 0.0
|
|
@@ -22,14 +30,6 @@ class Element:
|
|
|
22
30
|
anisotropy: str = ""
|
|
23
31
|
|
|
24
32
|
def __init__(self, T: float, H_ext: float, g: Grid, dt: float) -> None:
|
|
25
|
-
"""Initializes the Element class with given parameters
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
T: Temperature in Kelvin
|
|
29
|
-
H_ext: External magnetic field strength
|
|
30
|
-
g: Grid object representing the simulation grid
|
|
31
|
-
dt: Time step for the simulation
|
|
32
|
-
"""
|
|
33
33
|
self.H_ext = H_ext
|
|
34
34
|
self.g = g
|
|
35
35
|
self.dt = dt
|
|
@@ -52,16 +52,16 @@ class Element:
|
|
|
52
52
|
|
|
53
53
|
def get_CFL(self) -> float:
|
|
54
54
|
"""
|
|
55
|
-
Returns the value of the CFL
|
|
55
|
+
Returns the value of the CFL.
|
|
56
56
|
|
|
57
57
|
Returns:
|
|
58
|
-
|
|
58
|
+
The CFL value
|
|
59
59
|
"""
|
|
60
60
|
return self.dt * self.coeff_1 / self.g.dx**2
|
|
61
61
|
|
|
62
62
|
def to_dict(self) -> dict:
|
|
63
63
|
"""
|
|
64
|
-
Export element parameters to a dictionary for JAX JIT compatibility
|
|
64
|
+
Export element parameters to a dictionary for JAX JIT compatibility.
|
|
65
65
|
|
|
66
66
|
Returns:
|
|
67
67
|
Dictionary containing element parameters needed for computations
|
|
@@ -81,6 +81,8 @@ class Element:
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
class Cobalt(Element):
|
|
84
|
+
"""Cobalt element."""
|
|
85
|
+
|
|
84
86
|
A = 30.0e-12 #: Exchange constant :math:`[J.m^{-1}]`
|
|
85
87
|
K = 520.0e3 #: Anisotropy constant :math:`[J.m^{-3}]`
|
|
86
88
|
lambda_G = 0.5 #: Damping parameter :math:`[1]`
|
|
@@ -90,6 +92,8 @@ class Cobalt(Element):
|
|
|
90
92
|
|
|
91
93
|
|
|
92
94
|
class Iron(Element):
|
|
95
|
+
"""Iron element."""
|
|
96
|
+
|
|
93
97
|
A = 21.0e-12 #: Exchange constant :math:`[J.m^{-1}]`
|
|
94
98
|
K = 48.0e3 #: Anisotropy constant :math:`[J.m^{-3}]`
|
|
95
99
|
lambda_G = 0.5 #: Damping parameter :math:`[1]`
|
|
@@ -99,6 +103,8 @@ class Iron(Element):
|
|
|
99
103
|
|
|
100
104
|
|
|
101
105
|
class Nickel(Element):
|
|
106
|
+
"""Nickel element."""
|
|
107
|
+
|
|
102
108
|
A = 9.0e-12 #: Exchange constant :math:`[J.m^{-1}]`
|
|
103
109
|
K = -5.7e3 #: Anisotropy constant :math:`[J.m^{-3}]`
|
|
104
110
|
lambda_G = 0.5 #: Damping parameter :math:`[1]`
|
llg3d/grid.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Module to define the grid for the simulation
|
|
3
|
-
"""
|
|
1
|
+
"""Module to define the grid for the simulation."""
|
|
4
2
|
|
|
5
3
|
from dataclasses import dataclass
|
|
6
4
|
import numpy as np
|
|
@@ -10,7 +8,7 @@ from .solver import rank, size
|
|
|
10
8
|
|
|
11
9
|
@dataclass
|
|
12
10
|
class Grid:
|
|
13
|
-
"""Stores grid data"""
|
|
11
|
+
"""Stores grid data."""
|
|
14
12
|
|
|
15
13
|
# Parameter values correspond to the global grid
|
|
16
14
|
Jx: int #: number of points in x direction
|
|
@@ -19,7 +17,7 @@ class Grid:
|
|
|
19
17
|
dx: float #: grid spacing in x direction
|
|
20
18
|
|
|
21
19
|
def __post_init__(self) -> None:
|
|
22
|
-
"""Compute grid characteristics"""
|
|
20
|
+
"""Compute grid characteristics."""
|
|
23
21
|
self.dy = self.dz = self.dx # Setting dx = dy = dz
|
|
24
22
|
self.Lx = (self.Jx - 1) * self.dx
|
|
25
23
|
self.Ly = (self.Jy - 1) * self.dy
|
|
@@ -35,8 +33,7 @@ class Grid:
|
|
|
35
33
|
self.ncell = (self.Jx - 1) * (self.Jy - 1) * (self.Jz - 1)
|
|
36
34
|
|
|
37
35
|
def __str__(self) -> str:
|
|
38
|
-
"""Print grid information"""
|
|
39
|
-
|
|
36
|
+
"""Print grid information."""
|
|
40
37
|
header = "\t\t".join(("x", "y", "z"))
|
|
41
38
|
s = f"""\
|
|
42
39
|
\t{header}
|
|
@@ -55,7 +52,7 @@ ncell = {self.ncell:d}
|
|
|
55
52
|
self, T: float, name: str = "m1_mean", extension: str = "txt"
|
|
56
53
|
) -> str:
|
|
57
54
|
"""
|
|
58
|
-
Returns the output file name for a given temperature
|
|
55
|
+
Returns the output file name for a given temperature.
|
|
59
56
|
|
|
60
57
|
Args:
|
|
61
58
|
T: temperature
|
|
@@ -97,7 +94,7 @@ ncell = {self.ncell:d}
|
|
|
97
94
|
|
|
98
95
|
def to_dict(self) -> dict:
|
|
99
96
|
"""
|
|
100
|
-
Export grid parameters to a dictionary for JAX JIT compatibility
|
|
97
|
+
Export grid parameters to a dictionary for JAX JIT compatibility.
|
|
101
98
|
|
|
102
99
|
Returns:
|
|
103
100
|
Dictionary containing grid parameters needed for computations
|
|
@@ -114,7 +111,7 @@ ncell = {self.ncell:d}
|
|
|
114
111
|
|
|
115
112
|
def get_laplacian_coeff(self) -> tuple[float, float, float, float]:
|
|
116
113
|
"""
|
|
117
|
-
Returns the coefficients for the laplacian computation
|
|
114
|
+
Returns the coefficients for the laplacian computation.
|
|
118
115
|
|
|
119
116
|
Returns:
|
|
120
117
|
Tuple of coefficients (dx2_inv, dy2_inv, dz2_inv, center_coeff)
|
llg3d/main.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Define a CLI for the llg3d package.
|
|
3
|
-
"""
|
|
1
|
+
"""Define a CLI for the llg3d package."""
|
|
4
2
|
|
|
5
3
|
import argparse
|
|
6
4
|
|
|
@@ -20,6 +18,7 @@ else:
|
|
|
20
18
|
def parse_args(args: list[str] | None) -> argparse.Namespace:
|
|
21
19
|
"""
|
|
22
20
|
Argument parser for llg3d.
|
|
21
|
+
|
|
23
22
|
Automatically adds arguments from the parameter dictionary.
|
|
24
23
|
|
|
25
24
|
Returns:
|
|
@@ -44,9 +43,11 @@ def parse_args(args: list[str] | None) -> argparse.Namespace:
|
|
|
44
43
|
|
|
45
44
|
def main(arg_list: list[str] = None):
|
|
46
45
|
"""
|
|
47
|
-
Evaluates the command line and runs the simulation
|
|
48
|
-
"""
|
|
46
|
+
Evaluates the command line and runs the simulation.
|
|
49
47
|
|
|
48
|
+
Args:
|
|
49
|
+
arg_list: List of command line arguments
|
|
50
|
+
"""
|
|
50
51
|
args = parse_args(arg_list)
|
|
51
52
|
|
|
52
53
|
if size > 1 and args.solver != "mpi":
|
llg3d/output.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Utility functions for LLG3D
|
|
3
|
-
"""
|
|
1
|
+
"""Utility functions for LLG3D."""
|
|
4
2
|
|
|
5
3
|
import json
|
|
6
4
|
import sys
|
|
@@ -10,9 +8,11 @@ from .solver import rank
|
|
|
10
8
|
from .grid import Grid
|
|
11
9
|
|
|
12
10
|
|
|
13
|
-
def progress_bar(
|
|
11
|
+
def progress_bar(
|
|
12
|
+
it: Iterable, prefix: str = "", size: int = 60, out: TextIO = sys.stdout
|
|
13
|
+
):
|
|
14
14
|
"""
|
|
15
|
-
Displays a progress bar
|
|
15
|
+
Displays a progress bar.
|
|
16
16
|
|
|
17
17
|
(Source: https://stackoverflow.com/a/34482761/16593179)
|
|
18
18
|
|
|
@@ -22,14 +22,13 @@ def progress_bar(it: Iterable, prefix: str = "", size: int = 60, out: TextIO = s
|
|
|
22
22
|
size: Size of the progress bar (number of characters)
|
|
23
23
|
out: Output stream (default is sys.stdout)
|
|
24
24
|
"""
|
|
25
|
-
|
|
26
25
|
count = len(it)
|
|
27
26
|
|
|
28
27
|
def show(j):
|
|
29
28
|
x = int(size * j / count)
|
|
30
29
|
if rank == 0:
|
|
31
30
|
print(
|
|
32
|
-
f"{prefix}[{
|
|
31
|
+
f"{prefix}[{'█' * x}{('.' * (size - x))}] {j}/{count}",
|
|
33
32
|
end="\r",
|
|
34
33
|
file=out,
|
|
35
34
|
flush=True,
|
|
@@ -48,7 +47,7 @@ def progress_bar(it: Iterable, prefix: str = "", size: int = 60, out: TextIO = s
|
|
|
48
47
|
|
|
49
48
|
def write_json(json_file: str, run: dict):
|
|
50
49
|
"""
|
|
51
|
-
Writes the run dictionary to a JSON file
|
|
50
|
+
Writes the run dictionary to a JSON file.
|
|
52
51
|
|
|
53
52
|
Args:
|
|
54
53
|
json_file: Name of the JSON file
|
|
@@ -60,7 +59,7 @@ def write_json(json_file: str, run: dict):
|
|
|
60
59
|
|
|
61
60
|
def get_output_files(g: Grid, T: float, n_mean: int, n_profile: int) -> tuple:
|
|
62
61
|
"""
|
|
63
|
-
Open files and list them
|
|
62
|
+
Open files and list them.
|
|
64
63
|
|
|
65
64
|
Args:
|
|
66
65
|
g: Grid object
|
|
@@ -95,7 +94,7 @@ def get_output_files(g: Grid, T: float, n_mean: int, n_profile: int) -> tuple:
|
|
|
95
94
|
|
|
96
95
|
def close_output_files(f_mean: TextIO, f_profiles: list[TextIO] = None):
|
|
97
96
|
"""
|
|
98
|
-
Close all output files
|
|
97
|
+
Close all output files.
|
|
99
98
|
|
|
100
99
|
Args:
|
|
101
100
|
f_mean: file handler for storing m space integral over time
|
llg3d/parameters.py
CHANGED
|
@@ -58,13 +58,13 @@ parameters: dict[str, Parameter] = {
|
|
|
58
58
|
|
|
59
59
|
def get_parameter_list(parameters: dict) -> str:
|
|
60
60
|
"""
|
|
61
|
-
Returns parameter values as a string
|
|
61
|
+
Returns parameter values as a string.
|
|
62
62
|
|
|
63
63
|
Args:
|
|
64
|
-
|
|
64
|
+
parameters: Dictionary of parameters parsed by argparse
|
|
65
65
|
|
|
66
66
|
Returns:
|
|
67
|
-
|
|
67
|
+
Formatted string of parameters
|
|
68
68
|
"""
|
|
69
69
|
width = max([len(name) for name in parameters])
|
|
70
70
|
s = ""
|
llg3d/post/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""Post-processing tools for LLG3D."""
|
|
1
|
+
"""Post-processing tools for LLG3D."""
|
llg3d/post/plot_results.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Plot 1D curves from several files
|
|
2
|
+
Plot 1D curves from several files.
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
5
|
|
|
@@ -25,7 +25,6 @@ def plot(*files: tuple[str], output_file: str = DEFAULT_OUTPUT_FILE):
|
|
|
25
25
|
files (tuple[str]): Paths to the result files.
|
|
26
26
|
output_file (str): Path to the output image file.
|
|
27
27
|
"""
|
|
28
|
-
|
|
29
28
|
fig, ax = plt.subplots()
|
|
30
29
|
for file in files:
|
|
31
30
|
if not file.endswith(".txt"):
|
|
@@ -43,12 +42,9 @@ def plot(*files: tuple[str], output_file: str = DEFAULT_OUTPUT_FILE):
|
|
|
43
42
|
|
|
44
43
|
|
|
45
44
|
def main():
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
parser.add_argument(
|
|
50
|
-
"files", nargs="+", type=str, help="Path to the result files."
|
|
51
|
-
)
|
|
45
|
+
"""Main function to parse arguments and call the plot function."""
|
|
46
|
+
parser = argparse.ArgumentParser(description="Plot results from one or more files.")
|
|
47
|
+
parser.add_argument("files", nargs="+", type=str, help="Path to the result files.")
|
|
52
48
|
parser.add_argument(
|
|
53
49
|
"--output",
|
|
54
50
|
"-o",
|
llg3d/post/process.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Post-processes a set of runs
|
|
4
|
-
|
|
3
|
+
Post-processes a set of runs.
|
|
4
|
+
|
|
5
|
+
Runs are grouped into a `run.json` file or into a set of SLURM job arrays:
|
|
5
6
|
|
|
6
7
|
1. Extracts result data,
|
|
7
8
|
2. Plots the computed average magnetization against temperature,
|
|
8
9
|
3. Interpolates the computed points using cubic splines,
|
|
9
|
-
4. Determines the Curie temperature as the value corresponding to the minimal (negative)
|
|
10
|
+
4. Determines the Curie temperature as the value corresponding to the minimal (negative)
|
|
11
|
+
slope of the interpolated curve.
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
import json
|
|
@@ -17,14 +19,11 @@ from scipy.interpolate import interp1d
|
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class MagData:
|
|
20
|
-
"""
|
|
21
|
-
Class to handle magnetization data and interpolation according to temperature
|
|
22
|
-
"""
|
|
22
|
+
"""Class to handle magnetization data and interpolation according to temperature."""
|
|
23
23
|
|
|
24
24
|
n_interp = 200
|
|
25
25
|
|
|
26
26
|
def __init__(self, job_dir: Path = None, run_file: Path = Path("run.json")) -> None:
|
|
27
|
-
|
|
28
27
|
if job_dir:
|
|
29
28
|
self.parentpath = job_dir
|
|
30
29
|
data, self.run = self.process_slurm_jobs()
|
|
@@ -65,16 +64,18 @@ class MagData:
|
|
|
65
64
|
run = json.load(f)
|
|
66
65
|
# Adding temperature and averaging value to the data list
|
|
67
66
|
data.extend(
|
|
68
|
-
[
|
|
67
|
+
[
|
|
68
|
+
[float(T), res["m1_mean"]]
|
|
69
|
+
for T, res in run["results"].items()
|
|
70
|
+
]
|
|
69
71
|
)
|
|
70
72
|
except FileNotFoundError:
|
|
71
|
-
print(f"Warning: {json_filename} file not found
|
|
73
|
+
print(f"Warning: {json_filename} file not found in {jobdir.as_posix()}")
|
|
72
74
|
|
|
73
75
|
data.sort() # Sorting by increasing temperatures
|
|
74
76
|
|
|
75
77
|
return np.array(data), run
|
|
76
78
|
|
|
77
|
-
|
|
78
79
|
def process_json(json_filepath: Path) -> tuple[np.array, dict]:
|
|
79
80
|
"""
|
|
80
81
|
Reads the run.json file and extracts result data.
|
|
@@ -98,8 +99,12 @@ class MagData:
|
|
|
98
99
|
@property
|
|
99
100
|
def T_Curie(self) -> float:
|
|
100
101
|
"""
|
|
101
|
-
Return the Curie temperature
|
|
102
|
-
|
|
102
|
+
Return the Curie temperature.
|
|
103
|
+
|
|
104
|
+
It is defined as the temperature at which the magnetization is below 0.1.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
float: Curie temperature
|
|
103
108
|
"""
|
|
104
109
|
i_max = np.where(0.1 - self.interp(self.T) > 0)[0].min()
|
|
105
|
-
return self.T[i_max]
|
|
110
|
+
return self.T[i_max]
|
llg3d/post/temperature.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Plot the magnetization vs temperature and determine the Curie temperature.
|
|
4
|
-
"""
|
|
2
|
+
"""Plot the magnetization vs temperature and determine the Curie temperature."""
|
|
5
3
|
|
|
6
4
|
import argparse
|
|
7
5
|
from pathlib import Path
|
|
@@ -13,17 +11,14 @@ from .process import MagData
|
|
|
13
11
|
|
|
14
12
|
def plot_m_vs_T(m: MagData, show: bool):
|
|
15
13
|
"""
|
|
16
|
-
Plots the data (T, <m>)
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
Plots the data (T, <m>).
|
|
15
|
+
|
|
16
|
+
Interpolates the values, calculates the Curie temperature, exports to PNG.
|
|
19
17
|
|
|
20
18
|
Args:
|
|
21
|
-
|
|
22
|
-
parentdir: path to the directory containing the runs
|
|
23
|
-
run: descriptive dictionary of the run
|
|
19
|
+
m: Magnetization data object
|
|
24
20
|
show: display the graph in a graphical window
|
|
25
21
|
"""
|
|
26
|
-
|
|
27
22
|
print(f"T_Curie = {m.T_Curie:.0f} K")
|
|
28
23
|
|
|
29
24
|
fig, ax = plt.subplots()
|
|
@@ -56,9 +51,7 @@ def plot_m_vs_T(m: MagData, show: bool):
|
|
|
56
51
|
|
|
57
52
|
|
|
58
53
|
def main():
|
|
59
|
-
"""
|
|
60
|
-
Parses the command line to execute processing functions
|
|
61
|
-
"""
|
|
54
|
+
"""Parses the command line to execute processing functions."""
|
|
62
55
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
63
56
|
parser.add_argument("--job_dir", type=Path, help="Slurm main job directory")
|
|
64
57
|
parser.add_argument(
|
llg3d/simulation.py
CHANGED
|
@@ -23,17 +23,14 @@ from .output import write_json
|
|
|
23
23
|
class Simulation:
|
|
24
24
|
"""
|
|
25
25
|
Class to encapsulate the simulation logic.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
params: Dictionary of simulation parameters.
|
|
26
29
|
"""
|
|
27
30
|
|
|
28
31
|
json_file = "run.json" #: JSON file to store the results
|
|
29
32
|
|
|
30
33
|
def __init__(self, params: dict[str, Parameter]):
|
|
31
|
-
"""
|
|
32
|
-
Initializes the simulation with parameters.
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
params: Dictionary of simulation parameters.
|
|
36
|
-
"""
|
|
37
34
|
self.params: dict[str, Parameter] = params.copy() #: simulation parameters
|
|
38
35
|
self.simulate: callable = self._get_simulate_function_from_name(
|
|
39
36
|
self.params["solver"]
|
|
@@ -46,9 +43,7 @@ class Simulation:
|
|
|
46
43
|
self.params["element_class"] = get_element_class(params["element"])
|
|
47
44
|
|
|
48
45
|
def run(self):
|
|
49
|
-
"""
|
|
50
|
-
Runs the simulation and store the results.
|
|
51
|
-
"""
|
|
46
|
+
"""Runs the simulation and store the results."""
|
|
52
47
|
self.total_time, self.filenames, self.m1_mean = self.simulate(**self.params)
|
|
53
48
|
|
|
54
49
|
def _get_simulate_function_from_name(self, name: str) -> callable:
|
|
@@ -62,19 +57,15 @@ class Simulation:
|
|
|
62
57
|
callable: The simulation function
|
|
63
58
|
|
|
64
59
|
Example:
|
|
65
|
-
|
|
66
60
|
>>> simulate = self.get_simulate_function_from_name("mpi")
|
|
67
61
|
|
|
68
62
|
Will return the `simulate` function from the `llg3d.solver.mpi` module.
|
|
69
63
|
"""
|
|
70
|
-
|
|
71
64
|
module = __import__(f"llg3d.solver.{name}", fromlist=["simulate"])
|
|
72
65
|
return inspect.getattr_static(module, "simulate")
|
|
73
66
|
|
|
74
67
|
def save(self):
|
|
75
|
-
"""
|
|
76
|
-
Saves the results of the simulation to a JSON file.
|
|
77
|
-
"""
|
|
68
|
+
"""Saves the results of the simulation to a JSON file."""
|
|
78
69
|
params = self.params.copy() # save the parameters
|
|
79
70
|
del params["element_class"] # remove class object before serialization
|
|
80
71
|
if rank == 0:
|
llg3d/solver/__init__.py
CHANGED
llg3d/solver/jax.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
LLG3D solver using XLA compilation
|
|
3
|
-
"""
|
|
1
|
+
"""LLG3D solver using XLA compilation."""
|
|
4
2
|
|
|
5
3
|
import os
|
|
6
4
|
import time
|
|
@@ -20,8 +18,8 @@ def compute_H_anisotropy(
|
|
|
20
18
|
m: jnp.ndarray, coeff_2: float, anisotropy: int
|
|
21
19
|
) -> jnp.ndarray:
|
|
22
20
|
"""
|
|
23
|
-
Compute anisotropy field (JIT compiled)
|
|
24
|
-
|
|
21
|
+
Compute anisotropy field (JIT compiled).
|
|
22
|
+
|
|
25
23
|
Args:
|
|
26
24
|
m: Magnetization array (shape (3, nx, ny, nz))
|
|
27
25
|
coeff_2: Coefficient for anisotropy
|
|
@@ -30,7 +28,6 @@ def compute_H_anisotropy(
|
|
|
30
28
|
Returns:
|
|
31
29
|
Anisotropy field array (shape (3, nx, ny, nz))
|
|
32
30
|
"""
|
|
33
|
-
|
|
34
31
|
m1, m2, m3 = m
|
|
35
32
|
|
|
36
33
|
m1m1 = m1 * m1
|
|
@@ -69,11 +66,17 @@ def compute_H_anisotropy(
|
|
|
69
66
|
|
|
70
67
|
@jax.jit
|
|
71
68
|
def laplacian3D(
|
|
72
|
-
m_i: jnp.ndarray,
|
|
69
|
+
m_i: jnp.ndarray,
|
|
70
|
+
dx2_inv: float,
|
|
71
|
+
dy2_inv: float,
|
|
72
|
+
dz2_inv: float,
|
|
73
|
+
center_coeff: float,
|
|
73
74
|
) -> jnp.ndarray:
|
|
74
75
|
"""
|
|
75
|
-
Compute Laplacian for a single component with Neumann boundary conditions
|
|
76
|
-
|
|
76
|
+
Compute Laplacian for a single component with Neumann boundary conditions.
|
|
77
|
+
|
|
78
|
+
(JIT compiled)
|
|
79
|
+
|
|
77
80
|
Args:
|
|
78
81
|
m_i: Single component of magnetization (shape (nx, ny, nz))
|
|
79
82
|
dx2_inv: Inverse of squared grid spacing in x direction
|
|
@@ -94,12 +97,10 @@ def laplacian3D(
|
|
|
94
97
|
|
|
95
98
|
|
|
96
99
|
@jax.jit
|
|
97
|
-
def compute_laplacian(
|
|
98
|
-
m: jnp.ndarray, dx: float, dy: float, dz: float
|
|
99
|
-
) -> jnp.ndarray:
|
|
100
|
+
def compute_laplacian(m: jnp.ndarray, dx: float, dy: float, dz: float) -> jnp.ndarray:
|
|
100
101
|
"""
|
|
101
|
-
Compute 3D Laplacian with Neumann boundary conditions (JIT compiled)
|
|
102
|
-
|
|
102
|
+
Compute 3D Laplacian with Neumann boundary conditions (JIT compiled).
|
|
103
|
+
|
|
103
104
|
Args:
|
|
104
105
|
m: Magnetization array (shape (3, nx, ny, nz))
|
|
105
106
|
dx: Grid spacing in x direction
|
|
@@ -111,7 +112,7 @@ def compute_laplacian(
|
|
|
111
112
|
"""
|
|
112
113
|
dx2_inv, dy2_inv, dz2_inv = 1 / dx**2, 1 / dy**2, 1 / dz**2
|
|
113
114
|
center_coeff = -2 * (dx2_inv + dy2_inv + dz2_inv)
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
return jnp.stack(
|
|
116
117
|
[
|
|
117
118
|
laplacian3D(m[0], dx2_inv, dy2_inv, dz2_inv, center_coeff),
|
|
@@ -125,44 +126,44 @@ def compute_laplacian(
|
|
|
125
126
|
@jax.jit
|
|
126
127
|
def compute_space_average_jax(m1: jnp.ndarray) -> float:
|
|
127
128
|
"""
|
|
128
|
-
Compute space average using midpoint method on GPU (JIT compiled)
|
|
129
|
-
|
|
129
|
+
Compute space average using midpoint method on GPU (JIT compiled).
|
|
130
|
+
|
|
130
131
|
Args:
|
|
131
132
|
m1: First component of magnetization (shape (nx, ny, nz))
|
|
132
|
-
|
|
133
|
+
|
|
133
134
|
Returns:
|
|
134
135
|
Space average of m1
|
|
135
136
|
"""
|
|
136
137
|
# Get dimensions directly from the array shape
|
|
137
138
|
Jx, Jy, Jz = m1.shape
|
|
138
|
-
|
|
139
|
+
|
|
139
140
|
# Create 3D coordinate grids using the shape
|
|
140
141
|
i_coords = jnp.arange(Jx)
|
|
141
|
-
j_coords = jnp.arange(Jy)
|
|
142
|
+
j_coords = jnp.arange(Jy)
|
|
142
143
|
k_coords = jnp.arange(Jz)
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
# Create 3D coordinate grids
|
|
145
|
-
ii, jj, kk = jnp.meshgrid(i_coords, j_coords, k_coords, indexing=
|
|
146
|
-
|
|
146
|
+
ii, jj, kk = jnp.meshgrid(i_coords, j_coords, k_coords, indexing="ij")
|
|
147
|
+
|
|
147
148
|
# Apply midpoint weights (0.5 on edges, 1.0 elsewhere)
|
|
148
149
|
weights = jnp.ones_like(m1)
|
|
149
|
-
weights = jnp.where((ii == 0) | (ii == Jx-1), weights * 0.5, weights)
|
|
150
|
-
weights = jnp.where((jj == 0) | (jj == Jy-1), weights * 0.5, weights)
|
|
151
|
-
weights = jnp.where((kk == 0) | (kk == Jz-1), weights * 0.5, weights)
|
|
152
|
-
|
|
150
|
+
weights = jnp.where((ii == 0) | (ii == Jx - 1), weights * 0.5, weights)
|
|
151
|
+
weights = jnp.where((jj == 0) | (jj == Jy - 1), weights * 0.5, weights)
|
|
152
|
+
weights = jnp.where((kk == 0) | (kk == Jz - 1), weights * 0.5, weights)
|
|
153
|
+
|
|
153
154
|
# Compute weighted sum and normalize
|
|
154
155
|
weighted_sum = jnp.sum(weights * m1)
|
|
155
|
-
|
|
156
|
+
|
|
156
157
|
# Compute ncell from the weights (this is the effective cell count)
|
|
157
158
|
ncell = jnp.sum(weights)
|
|
158
|
-
|
|
159
|
+
|
|
159
160
|
return weighted_sum / ncell
|
|
160
161
|
|
|
161
162
|
|
|
162
163
|
@jax.jit
|
|
163
164
|
def cross_product(a: jnp.ndarray, b: jnp.ndarray) -> jnp.ndarray:
|
|
164
165
|
r"""
|
|
165
|
-
Compute cross product :math:`a \times b` (JIT compiled)
|
|
166
|
+
Compute cross product :math:`a \times b` (JIT compiled).
|
|
166
167
|
|
|
167
168
|
Args:
|
|
168
169
|
a: First vector (shape (3, nx, ny, nz))
|
|
@@ -181,11 +182,12 @@ def compute_slope(
|
|
|
181
182
|
g_params: dict, e_params: dict, m: jnp.ndarray, R_random: jnp.ndarray
|
|
182
183
|
) -> jnp.ndarray:
|
|
183
184
|
"""
|
|
184
|
-
JIT-compiled version of compute_slope_jax using modular sub-functions
|
|
185
|
+
JIT-compiled version of compute_slope_jax using modular sub-functions.
|
|
185
186
|
|
|
186
187
|
Args:
|
|
187
188
|
g_params: Grid parameters dict (dx, dy, dz)
|
|
188
|
-
e_params: Element parameters dict (coeff_1, coeff_2, coeff_3, lambda_G,
|
|
189
|
+
e_params: Element parameters dict (coeff_1, coeff_2, coeff_3, lambda_G,
|
|
190
|
+
anisotropy)
|
|
189
191
|
m: Magnetization array (shape (3, nx, ny, nz))
|
|
190
192
|
R_random: Random field array (shape (3, nx, ny, nz))
|
|
191
193
|
|
|
@@ -234,7 +236,7 @@ def simulate(
|
|
|
234
236
|
**_,
|
|
235
237
|
) -> tuple[float, str, float]:
|
|
236
238
|
"""
|
|
237
|
-
Simulates the system for N iterations using JAX
|
|
239
|
+
Simulates the system for N iterations using JAX.
|
|
238
240
|
|
|
239
241
|
Args:
|
|
240
242
|
N: Number of iterations
|
|
@@ -258,7 +260,6 @@ def simulate(
|
|
|
258
260
|
- The output filenames
|
|
259
261
|
- The average magnetization
|
|
260
262
|
"""
|
|
261
|
-
|
|
262
263
|
# Configure JAX
|
|
263
264
|
if device == "auto":
|
|
264
265
|
# Let JAX choose the best available device
|
|
@@ -276,7 +277,10 @@ def simulate(
|
|
|
276
277
|
os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_id)
|
|
277
278
|
print(f"Set CUDA_VISIBLE_DEVICES={gpu_id}")
|
|
278
279
|
else:
|
|
279
|
-
|
|
280
|
+
cuda_visible_devices = os.environ["CUDA_VISIBLE_DEVICES"]
|
|
281
|
+
print(
|
|
282
|
+
f"Using external CUDA_VISIBLE_DEVICES={cuda_visible_devices}"
|
|
283
|
+
)
|
|
280
284
|
|
|
281
285
|
# Set precision
|
|
282
286
|
if precision == "double":
|
|
@@ -305,11 +309,11 @@ def simulate(
|
|
|
305
309
|
|
|
306
310
|
# --- Initialization ---
|
|
307
311
|
def theta_init(shape):
|
|
308
|
-
"""Initialization of theta"""
|
|
312
|
+
"""Initialization of theta."""
|
|
309
313
|
return jnp.zeros(shape, dtype=jnp_float)
|
|
310
314
|
|
|
311
315
|
def phi_init(t, shape):
|
|
312
|
-
"""Initialization of phi"""
|
|
316
|
+
"""Initialization of phi."""
|
|
313
317
|
return jnp.zeros(shape, dtype=jnp_float) + e.gamma_0 * H_ext * t
|
|
314
318
|
|
|
315
319
|
m_n = jnp.zeros((3,) + dims, dtype=jnp_float)
|
|
@@ -328,16 +332,16 @@ def simulate(
|
|
|
328
332
|
|
|
329
333
|
# === JIT WARMUP: Pre-compile all functions to exclude compilation time ===
|
|
330
334
|
print("Warming up JIT compilation...")
|
|
331
|
-
|
|
335
|
+
|
|
332
336
|
# Generate dummy random field for warmup
|
|
333
337
|
warmup_key = random.PRNGKey(42)
|
|
334
338
|
R_warmup = e.coeff_4 * random.normal(warmup_key, (3,) + dims, dtype=jnp_float)
|
|
335
|
-
|
|
339
|
+
|
|
336
340
|
# Warmup all JIT functions with actual data shapes
|
|
337
341
|
_ = compute_slope(g_params, e_params, m_n, R_warmup)
|
|
338
342
|
if n_mean != 0:
|
|
339
343
|
_ = compute_space_average_jax(m_n[0])
|
|
340
|
-
|
|
344
|
+
|
|
341
345
|
# Force compilation and execution to complete
|
|
342
346
|
jax.block_until_ready(m_n)
|
|
343
347
|
print("JIT warmup completed.")
|
llg3d/solver/mpi.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
LLG3D Solver using MPI
|
|
2
|
+
LLG3D Solver using MPI.
|
|
3
3
|
|
|
4
4
|
The parallelization is done in the x direction.
|
|
5
5
|
"""
|
|
@@ -22,9 +22,9 @@ def get_boundaries_x(
|
|
|
22
22
|
g: Grid, m: np.ndarray, blocking: bool = False
|
|
23
23
|
) -> tuple[np.ndarray, np.ndarray, MPI.Request, MPI.Request]:
|
|
24
24
|
"""
|
|
25
|
-
Returns the boundaries asynchronously
|
|
26
|
-
|
|
27
|
-
with calculations
|
|
25
|
+
Returns the boundaries asynchronously.
|
|
26
|
+
|
|
27
|
+
Allows overlapping communication time of boundaries with calculations.
|
|
28
28
|
|
|
29
29
|
Args:
|
|
30
30
|
g: Grid object
|
|
@@ -37,7 +37,6 @@ def get_boundaries_x(
|
|
|
37
37
|
- request_start: Request for start boundary
|
|
38
38
|
- request_end: Request for end boundary
|
|
39
39
|
"""
|
|
40
|
-
|
|
41
40
|
# Extract slices for Neumann boundary conditions
|
|
42
41
|
|
|
43
42
|
m_i_x_start = np.empty((1, g.Jy, g.Jz))
|
|
@@ -80,6 +79,7 @@ def laplacian3D(
|
|
|
80
79
|
) -> np.ndarray:
|
|
81
80
|
"""
|
|
82
81
|
Returns the Laplacian of m_i in 3D.
|
|
82
|
+
|
|
83
83
|
We start by calculating contributions in y and z, to wait
|
|
84
84
|
for the end of communications in x.
|
|
85
85
|
|
|
@@ -97,7 +97,6 @@ def laplacian3D(
|
|
|
97
97
|
Returns:
|
|
98
98
|
Laplacian of m_i (shape (nx, ny, nz))
|
|
99
99
|
"""
|
|
100
|
-
|
|
101
100
|
# Extract slices for Neumann boundary conditions
|
|
102
101
|
m_i_y_start = m_i[:, 1:2, :]
|
|
103
102
|
m_i_y_end = m_i[:, -2:-1, :]
|
|
@@ -143,7 +142,7 @@ def compute_laplacian(
|
|
|
143
142
|
boundaries: tuple[np.ndarray, np.ndarray, MPI.Request, MPI.Request],
|
|
144
143
|
) -> np.ndarray:
|
|
145
144
|
"""
|
|
146
|
-
Compute the laplacian of m in 3D
|
|
145
|
+
Compute the laplacian of m in 3D.
|
|
147
146
|
|
|
148
147
|
Args:
|
|
149
148
|
g: Grid object
|
|
@@ -153,7 +152,6 @@ def compute_laplacian(
|
|
|
153
152
|
Returns:
|
|
154
153
|
Laplacian of m (shape (3, nx, ny, nz))
|
|
155
154
|
"""
|
|
156
|
-
|
|
157
155
|
dx2_inv, dy2_inv, dz2_inv, center_coeff = g.get_laplacian_coeff()
|
|
158
156
|
|
|
159
157
|
return np.stack(
|
|
@@ -174,7 +172,7 @@ def compute_slope(
|
|
|
174
172
|
boundaries: tuple[np.ndarray, np.ndarray, MPI.Request, MPI.Request],
|
|
175
173
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
176
174
|
"""
|
|
177
|
-
Compute the slope of the LLG equation
|
|
175
|
+
Compute the slope of the LLG equation.
|
|
178
176
|
|
|
179
177
|
Args:
|
|
180
178
|
g: Grid object
|
|
@@ -186,7 +184,6 @@ def compute_slope(
|
|
|
186
184
|
Returns:
|
|
187
185
|
Slope array (shape (3, nx, ny, nz))
|
|
188
186
|
"""
|
|
189
|
-
|
|
190
187
|
# Precalculate terms used multiple times
|
|
191
188
|
|
|
192
189
|
H_aniso = compute_H_anisotropy(e, m)
|
|
@@ -205,8 +202,8 @@ def compute_slope(
|
|
|
205
202
|
|
|
206
203
|
def space_average(g: Grid, m: np.ndarray) -> float:
|
|
207
204
|
"""
|
|
208
|
-
Returns the spatial average of m with shape (g.dims)
|
|
209
|
-
|
|
205
|
+
Returns the spatial average of m with shape (g.dims) using the midpoint method.
|
|
206
|
+
|
|
210
207
|
Performs the local sum on each process and then reduces it to process 0.
|
|
211
208
|
|
|
212
209
|
Args:
|
|
@@ -216,7 +213,6 @@ def space_average(g: Grid, m: np.ndarray) -> float:
|
|
|
216
213
|
Returns:
|
|
217
214
|
Spatial average of m
|
|
218
215
|
"""
|
|
219
|
-
|
|
220
216
|
# Make a copy of m to avoid modifying its value
|
|
221
217
|
mm = m.copy()
|
|
222
218
|
|
|
@@ -242,16 +238,14 @@ def space_average(g: Grid, m: np.ndarray) -> float:
|
|
|
242
238
|
|
|
243
239
|
def integral_yz(m: np.ndarray) -> np.ndarray:
|
|
244
240
|
"""
|
|
245
|
-
Returns the spatial average of
|
|
246
|
-
in y and z of m of shape (g.dims) using the midpoint method
|
|
241
|
+
Returns the spatial average of m using the midpoint method along y and z.
|
|
247
242
|
|
|
248
243
|
Args:
|
|
249
244
|
m: Array to be integrated
|
|
250
245
|
|
|
251
246
|
Returns:
|
|
252
|
-
np.ndarray: Spatial average of m in y and z
|
|
247
|
+
np.ndarray: Spatial average of m in y and z of shape (g.dims[0],)
|
|
253
248
|
"""
|
|
254
|
-
|
|
255
249
|
# Make a copy of m to avoid modifying its value
|
|
256
250
|
mm = m.copy()
|
|
257
251
|
|
|
@@ -267,26 +261,25 @@ def integral_yz(m: np.ndarray) -> np.ndarray:
|
|
|
267
261
|
|
|
268
262
|
def profile(m: np.ndarray, m_xprof: np.ndarray):
|
|
269
263
|
"""
|
|
270
|
-
Retrieves the x profile of the average of m in y and z
|
|
264
|
+
Retrieves the x profile of the average of m in y and z.
|
|
271
265
|
|
|
272
266
|
Args:
|
|
273
267
|
m: Array to be integrated
|
|
274
268
|
m_xprof: Array to store the x profile
|
|
275
269
|
"""
|
|
276
|
-
|
|
277
270
|
# Gather m in mglob
|
|
278
271
|
m_mean_yz = integral_yz(m)
|
|
279
272
|
comm.Gather(m_mean_yz, m_xprof)
|
|
280
273
|
|
|
281
274
|
|
|
282
275
|
def theta_init(t: float, g: Grid) -> np.ndarray:
|
|
283
|
-
"""Initialization of theta"""
|
|
276
|
+
"""Initialization of theta."""
|
|
284
277
|
x, y, z = g.get_mesh(local=True)
|
|
285
278
|
return np.zeros(g.dims)
|
|
286
279
|
|
|
287
280
|
|
|
288
281
|
def phi_init(t: float, g: Grid, e: Element) -> np.ndarray:
|
|
289
|
-
"""Initialization of phi"""
|
|
282
|
+
"""Initialization of phi."""
|
|
290
283
|
# return np.zeros(shape) + e.coeff_3 * t
|
|
291
284
|
return np.zeros(g.dims) + e.gamma_0 * e.H_ext * t
|
|
292
285
|
|
|
@@ -309,7 +302,7 @@ def simulate(
|
|
|
309
302
|
**_,
|
|
310
303
|
) -> tuple[float, str, float]:
|
|
311
304
|
"""
|
|
312
|
-
Simulates the system for N iterations
|
|
305
|
+
Simulates the system for N iterations.
|
|
313
306
|
|
|
314
307
|
Args:
|
|
315
308
|
N: Number of iterations
|
|
@@ -332,7 +325,6 @@ def simulate(
|
|
|
332
325
|
- The time taken for the simulation
|
|
333
326
|
- The output filename
|
|
334
327
|
"""
|
|
335
|
-
|
|
336
328
|
if Jx % size != 0:
|
|
337
329
|
if rank == 0:
|
|
338
330
|
print(
|
|
@@ -433,7 +425,7 @@ def simulate(
|
|
|
433
425
|
|
|
434
426
|
|
|
435
427
|
class ArgumentParser(argparse.ArgumentParser):
|
|
436
|
-
"""An argument parser compatible with MPI"""
|
|
428
|
+
"""An argument parser compatible with MPI."""
|
|
437
429
|
|
|
438
430
|
def _print_message(self, message, file=None):
|
|
439
431
|
if rank == 0 and message:
|
|
@@ -442,6 +434,15 @@ class ArgumentParser(argparse.ArgumentParser):
|
|
|
442
434
|
file.write(message)
|
|
443
435
|
|
|
444
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
|
+
"""
|
|
445
446
|
if message:
|
|
446
447
|
self._print_message(message, sys.stderr)
|
|
447
448
|
comm.barrier()
|
llg3d/solver/numpy.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
LLG3D Solver using NumPy
|
|
3
|
-
"""
|
|
1
|
+
"""LLG3D Solver using NumPy."""
|
|
4
2
|
|
|
5
3
|
import time
|
|
6
4
|
|
|
@@ -16,7 +14,7 @@ def laplacian3D(
|
|
|
16
14
|
m_i: np.ndarray, dx2_inv: float, dy2_inv: float, dz2_inv: float, center_coeff: float
|
|
17
15
|
) -> np.ndarray:
|
|
18
16
|
"""
|
|
19
|
-
Returns the laplacian of m in 3D
|
|
17
|
+
Returns the laplacian of m in 3D.
|
|
20
18
|
|
|
21
19
|
Args:
|
|
22
20
|
m_i: Magnetization array (shape (nx, ny, nz))
|
|
@@ -28,7 +26,6 @@ def laplacian3D(
|
|
|
28
26
|
Returns:
|
|
29
27
|
Laplacian of m (shape (nx, ny, nz))
|
|
30
28
|
"""
|
|
31
|
-
|
|
32
29
|
m_i_padded = np.pad(m_i, ((1, 1), (1, 1), (1, 1)), mode="reflect")
|
|
33
30
|
|
|
34
31
|
laplacian = (
|
|
@@ -42,7 +39,7 @@ def laplacian3D(
|
|
|
42
39
|
|
|
43
40
|
def compute_laplacian(g: Grid, m: np.ndarray) -> np.ndarray:
|
|
44
41
|
"""
|
|
45
|
-
Compute the laplacian of m in 3D
|
|
42
|
+
Compute the laplacian of m in 3D.
|
|
46
43
|
|
|
47
44
|
Args:
|
|
48
45
|
g: Grid object
|
|
@@ -51,7 +48,6 @@ def compute_laplacian(g: Grid, m: np.ndarray) -> np.ndarray:
|
|
|
51
48
|
Returns:
|
|
52
49
|
Laplacian of m (shape (3, nx, ny, nz))
|
|
53
50
|
"""
|
|
54
|
-
|
|
55
51
|
dx2_inv, dy2_inv, dz2_inv, center_coeff = g.get_laplacian_coeff()
|
|
56
52
|
|
|
57
53
|
return np.stack(
|
|
@@ -68,17 +64,17 @@ def compute_slope(
|
|
|
68
64
|
g: Grid, e: Element, m: np.ndarray, R_random: np.ndarray
|
|
69
65
|
) -> np.ndarray:
|
|
70
66
|
"""
|
|
71
|
-
Compute the slope of the LLG equation
|
|
67
|
+
Compute the slope of the LLG equation.
|
|
68
|
+
|
|
72
69
|
Args:
|
|
73
70
|
g: Grid object
|
|
74
71
|
e: Element object
|
|
75
72
|
m: Magnetization array (shape (3, nx, ny, nz))
|
|
76
|
-
R_random: Random field array (shape (3, nx, ny, nz))
|
|
73
|
+
R_random: Random field array (shape (3, nx, ny, nz)).
|
|
77
74
|
|
|
78
75
|
Returns:
|
|
79
76
|
Slope array (shape (3, nx, ny, nz))
|
|
80
77
|
"""
|
|
81
|
-
|
|
82
78
|
H_aniso = compute_H_anisotropy(e, m)
|
|
83
79
|
|
|
84
80
|
laplacian_m = compute_laplacian(g, m)
|
|
@@ -111,13 +107,14 @@ def simulate(
|
|
|
111
107
|
**_,
|
|
112
108
|
) -> tuple[float, str, float]:
|
|
113
109
|
"""
|
|
114
|
-
Simulates the system for N iterations
|
|
110
|
+
Simulates the system for N iterations.
|
|
115
111
|
|
|
116
112
|
Args:
|
|
117
113
|
N: Number of iterations
|
|
118
114
|
Jx: Number of grid points in x direction
|
|
119
115
|
Jy: Number of grid points in y direction
|
|
120
|
-
Jz: Number of grid points in z direction
|
|
116
|
+
Jz: Number of grid points in z direction
|
|
117
|
+
dx: Grid spacing
|
|
121
118
|
T: Temperature in Kelvin
|
|
122
119
|
H_ext: External magnetic field strength
|
|
123
120
|
dt: Time step for the simulation
|
|
@@ -152,11 +149,11 @@ def simulate(
|
|
|
152
149
|
# --- Initialization ---
|
|
153
150
|
|
|
154
151
|
def theta_init(shape):
|
|
155
|
-
"""Initialization of theta"""
|
|
152
|
+
"""Initialization of theta."""
|
|
156
153
|
return np.zeros(shape, dtype=np_float)
|
|
157
154
|
|
|
158
155
|
def phi_init(t, shape):
|
|
159
|
-
"""Initialization of phi"""
|
|
156
|
+
"""Initialization of phi."""
|
|
160
157
|
return np.zeros(shape, dtype=np_float) + e.gamma_0 * H_ext * t
|
|
161
158
|
|
|
162
159
|
m_n = np.zeros((3,) + dims, dtype=np_float)
|
llg3d/solver/opencl.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
LLG3D Solver using OpenCL
|
|
3
|
-
"""
|
|
1
|
+
"""LLG3D Solver using OpenCL."""
|
|
4
2
|
|
|
5
3
|
import time
|
|
6
4
|
from pathlib import Path
|
|
@@ -15,15 +13,17 @@ from ..grid import Grid
|
|
|
15
13
|
from ..element import Element, Cobalt
|
|
16
14
|
|
|
17
15
|
|
|
18
|
-
def get_context_and_device(
|
|
16
|
+
def get_context_and_device(
|
|
17
|
+
device_selection: str = "auto",
|
|
18
|
+
) -> tuple[cl.Context, cl.Device]:
|
|
19
19
|
"""
|
|
20
|
-
Get the OpenCL context and device
|
|
20
|
+
Get the OpenCL context and device.
|
|
21
21
|
|
|
22
22
|
Args:
|
|
23
23
|
device_selection:
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
- ``"auto"``: Let OpenCL choose automatically
|
|
26
|
-
- ``"cpu"``: Select CPU device
|
|
26
|
+
- ``"cpu"``: Select CPU device
|
|
27
27
|
- ``"gpu"``: Select first available GPU
|
|
28
28
|
- ``"gpu:N"``: Select specific GPU by index (e.g., ``"gpu:0"``, ``"gpu:1"``)
|
|
29
29
|
|
|
@@ -35,17 +35,17 @@ def get_context_and_device(device_selection: str = "auto") -> tuple[cl.Context,
|
|
|
35
35
|
context = cl.create_some_context(interactive=False)
|
|
36
36
|
device = context.devices[0]
|
|
37
37
|
return context, device
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
# Get all platforms and devices
|
|
40
40
|
platforms = cl.get_platforms()
|
|
41
41
|
all_devices = []
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
for platform in platforms:
|
|
44
44
|
all_devices.extend(platform.get_devices())
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
if not all_devices:
|
|
47
47
|
raise RuntimeError("No OpenCL devices found")
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
# Filter devices based on selection
|
|
50
50
|
if device_selection == "cpu":
|
|
51
51
|
cpu_devices = [d for d in all_devices if d.type & cl.device_type.CPU]
|
|
@@ -61,26 +61,29 @@ def get_context_and_device(device_selection: str = "auto") -> tuple[cl.Context,
|
|
|
61
61
|
gpu_devices = [d for d in all_devices if d.type & cl.device_type.GPU]
|
|
62
62
|
if not gpu_devices:
|
|
63
63
|
raise RuntimeError("No GPU devices found")
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
gpu_index = int(device_selection.split(":")[1])
|
|
66
66
|
if gpu_index >= len(gpu_devices):
|
|
67
|
-
raise RuntimeError(
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"GPU index {gpu_index} not available. Found {len(gpu_devices)} GPU(s)"
|
|
69
|
+
)
|
|
68
70
|
selected_device = gpu_devices[gpu_index]
|
|
69
71
|
else:
|
|
70
72
|
raise ValueError(f"Invalid device selection: {device_selection}")
|
|
71
|
-
|
|
73
|
+
|
|
72
74
|
# Create context with selected device
|
|
73
75
|
context = cl.Context([selected_device])
|
|
74
76
|
print(f"Selected OpenCL device: {selected_device.name} ({selected_device.type})")
|
|
75
|
-
|
|
77
|
+
|
|
76
78
|
return context, selected_device
|
|
77
79
|
|
|
78
80
|
|
|
79
81
|
def get_precision(device: cl.Device, precision: str) -> np.dtype:
|
|
80
82
|
"""
|
|
81
|
-
Get the numpy float type based on the precision
|
|
83
|
+
Get the numpy float type based on the precision.
|
|
82
84
|
|
|
83
85
|
Args:
|
|
86
|
+
device: OpenCL device
|
|
84
87
|
precision: Precision of the simulation (single or double)
|
|
85
88
|
|
|
86
89
|
Returns:
|
|
@@ -97,9 +100,7 @@ def get_precision(device: cl.Device, precision: str) -> np.dtype:
|
|
|
97
100
|
|
|
98
101
|
|
|
99
102
|
class Program:
|
|
100
|
-
"""
|
|
101
|
-
Class to manage the OpenCL kernels for the LLG3D simulation
|
|
102
|
-
"""
|
|
103
|
+
"""Class to manage the OpenCL kernels for the LLG3D simulation."""
|
|
103
104
|
|
|
104
105
|
def __init__(self, g: Grid, context: cl.Context, np_float: np.dtype):
|
|
105
106
|
self.grid = g
|
|
@@ -109,12 +110,11 @@ class Program:
|
|
|
109
110
|
|
|
110
111
|
def _get_built_program(self) -> cl.Program:
|
|
111
112
|
"""
|
|
112
|
-
Return the OpenCL program built from the source code
|
|
113
|
+
Return the OpenCL program built from the source code.
|
|
113
114
|
|
|
114
115
|
Returns:
|
|
115
116
|
The OpenCL program object
|
|
116
117
|
"""
|
|
117
|
-
|
|
118
118
|
opencl_code = (Path(__file__).parent / "llg3d.cl").read_text()
|
|
119
119
|
build_options = "-D USE_DOUBLE_PRECISION" if self.np_float == np.float64 else ""
|
|
120
120
|
build_options += (
|
|
@@ -157,13 +157,13 @@ def simulate(
|
|
|
157
157
|
**_,
|
|
158
158
|
) -> tuple[float, str, float]:
|
|
159
159
|
"""
|
|
160
|
-
Simulates the system over N iterations
|
|
160
|
+
Simulates the system over N iterations.
|
|
161
161
|
|
|
162
162
|
Args:
|
|
163
163
|
N: Number of iterations
|
|
164
164
|
Jx: Number of grid points in x direction
|
|
165
165
|
Jy: Number of grid points in y direction
|
|
166
|
-
Jz: Number of grid points in z direction
|
|
166
|
+
Jz: Number of grid points in z direction
|
|
167
167
|
dx: Grid spacing
|
|
168
168
|
T: Temperature in Kelvin
|
|
169
169
|
H_ext: External magnetic field strength
|
|
@@ -181,7 +181,6 @@ def simulate(
|
|
|
181
181
|
- The time taken for the simulation
|
|
182
182
|
- The output filename
|
|
183
183
|
"""
|
|
184
|
-
|
|
185
184
|
context, opencl_device = get_context_and_device(device)
|
|
186
185
|
np_float = get_precision(opencl_device, precision)
|
|
187
186
|
|
|
@@ -190,17 +189,19 @@ def simulate(
|
|
|
190
189
|
|
|
191
190
|
e = element_class(T, H_ext, g, dt)
|
|
192
191
|
if not isinstance(e, Cobalt):
|
|
193
|
-
raise NotImplementedError(
|
|
192
|
+
raise NotImplementedError(
|
|
193
|
+
f"Element is {type(e)} but only {Cobalt} is supported at the moment."
|
|
194
|
+
)
|
|
194
195
|
print(f"CFL = {e.get_CFL()}")
|
|
195
196
|
|
|
196
197
|
# --- Initialization ---
|
|
197
198
|
|
|
198
199
|
def theta_init(shape):
|
|
199
|
-
"""Initialization of theta"""
|
|
200
|
+
"""Initialization of theta."""
|
|
200
201
|
return np.zeros(shape, dtype=np_float)
|
|
201
202
|
|
|
202
203
|
def phi_init(t, shape):
|
|
203
|
-
"""Initialization of phi"""
|
|
204
|
+
"""Initialization of phi."""
|
|
204
205
|
return np.zeros(shape, dtype=np_float) + e.gamma_0 * H_ext * t
|
|
205
206
|
|
|
206
207
|
m_n = np.zeros((3,) + g.dims, dtype=np_float)
|
|
@@ -219,7 +220,7 @@ def simulate(
|
|
|
219
220
|
update_1_kernel = program.get_kernel("update_1", [None] * 3 + [np_float])
|
|
220
221
|
update_2_kernel = program.get_kernel("update_2", [None] * 4 + [np_float])
|
|
221
222
|
normalize_kernel = program.get_kernel("normalize")
|
|
222
|
-
|
|
223
|
+
|
|
223
224
|
# Create a CL array for m1 component in order to compute averages
|
|
224
225
|
d_m1 = clarray.empty(queue, g.ntot, np_float)
|
|
225
226
|
copy_m1_kernel = program.get_kernel("copy_m1", [None, None])
|
|
@@ -309,11 +310,11 @@ def simulate(
|
|
|
309
310
|
# Copy only the first component from d_m_n to d_m1 with weights applied
|
|
310
311
|
# d_m_n contains [m1, m2, m3] interleaved, we want only m1
|
|
311
312
|
copy_m1_kernel(queue, g.dims, None, d_m_n, d_m1.data)
|
|
312
|
-
|
|
313
|
+
|
|
313
314
|
# Use PyOpenCL array sum to compute the weighted sum
|
|
314
315
|
weighted_sum = clarray.sum(d_m1).get()
|
|
315
316
|
m1_mean = weighted_sum / g.ncell
|
|
316
|
-
|
|
317
|
+
|
|
317
318
|
if n >= start_averaging:
|
|
318
319
|
m1_average += m1_mean * n_mean
|
|
319
320
|
|
llg3d/solver/solver.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Common functions for the LLG3D solver
|
|
3
|
-
"""
|
|
1
|
+
"""Common functions for the LLG3D solver."""
|
|
4
2
|
|
|
5
3
|
import numpy as np
|
|
6
4
|
|
|
@@ -21,7 +19,6 @@ def cross_product(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
|
21
19
|
Returns:
|
|
22
20
|
Cross product :math:`a \times b` (shape (3, nx, ny, nz))
|
|
23
21
|
"""
|
|
24
|
-
|
|
25
22
|
return np.stack(
|
|
26
23
|
[
|
|
27
24
|
a[1] * b[2] - a[2] * b[1], # x-component
|
|
@@ -34,14 +31,15 @@ def cross_product(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
|
34
31
|
|
|
35
32
|
def compute_H_anisotropy(e: Element, m: np.ndarray) -> np.ndarray:
|
|
36
33
|
"""
|
|
37
|
-
Compute the anisotropy field
|
|
34
|
+
Compute the anisotropy field.
|
|
35
|
+
|
|
38
36
|
Args:
|
|
39
37
|
e: Element object
|
|
40
|
-
m: Magnetization array (shape (3, nx, ny, nz))
|
|
38
|
+
m: Magnetization array (shape (3, nx, ny, nz)).
|
|
39
|
+
|
|
41
40
|
Returns:
|
|
42
41
|
Anisotropy field array (shape (3, nx, ny, nz))
|
|
43
42
|
"""
|
|
44
|
-
|
|
45
43
|
m1, m2, m3 = m
|
|
46
44
|
|
|
47
45
|
m1m1 = m1 * m1
|
|
@@ -63,8 +61,7 @@ def compute_H_anisotropy(e: Element, m: np.ndarray) -> np.ndarray:
|
|
|
63
61
|
|
|
64
62
|
def space_average(g: Grid, m: np.ndarray, copy: bool = True) -> float:
|
|
65
63
|
"""
|
|
66
|
-
Returns the spatial average of m with shape (g.dims)
|
|
67
|
-
using the midpoint method
|
|
64
|
+
Returns the spatial average of m with shape (g.dims) using the midpoint method.
|
|
68
65
|
|
|
69
66
|
Args:
|
|
70
67
|
g: Grid object
|
|
@@ -72,9 +69,8 @@ def space_average(g: Grid, m: np.ndarray, copy: bool = True) -> float:
|
|
|
72
69
|
copy: If True, copy m to avoid modifying its value
|
|
73
70
|
|
|
74
71
|
Returns:
|
|
75
|
-
|
|
72
|
+
Spatial average of m
|
|
76
73
|
"""
|
|
77
|
-
|
|
78
74
|
# copy m to avoid modifying its value
|
|
79
75
|
mm = m.copy() if copy else m
|
|
80
76
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llg3d
|
|
3
|
-
Version: 2.0.
|
|
4
|
-
Summary:
|
|
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
|
|
@@ -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,,
|
llg3d-2.0.0.dist-info/RECORD
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
llg3d/__init__.py,sha256=7Bcs0le_-tmgNMoIH8KaKJRqHjrzfNLu6RMP6c8T-Jo,160
|
|
2
|
-
llg3d/__main__.py,sha256=wZ2BSBj_ZgnfGNmYZkeDVrfMPpzLOEmjmSnXrjnwfzo,96
|
|
3
|
-
llg3d/element.py,sha256=3umDR1CxRjMfaBKjDPp35RxG24cSagONwzWkY5doxuI,4290
|
|
4
|
-
llg3d/grid.py,sha256=Vkj0K9lkYZLlmSk6-S3BOupKh6JtxrWTABiK06GZHsk,3844
|
|
5
|
-
llg3d/main.py,sha256=A4d1_6NfPNfHptPoitE772zZT4akeF0zFVXM5Gu1Tjo,1829
|
|
6
|
-
llg3d/output.py,sha256=llPZ7lQ1bKwfe-A_hBYEn0QWf0A2aFjHneJAv-djVJk,2921
|
|
7
|
-
llg3d/parameters.py,sha256=Jg1sDPWLaUeDMFFJBdb3YmN7ljgsQ-8qDXMpXUUqjGI,2437
|
|
8
|
-
llg3d/simulation.py,sha256=XtmB8vYaQbFsx9sQPXqGQs1m-iGTPqRpis1Y67W5hO0,3551
|
|
9
|
-
llg3d/post/__init__.py,sha256=PUQUkQURiZrnZmfGWJK5YCqk5Wv1iqo_IzczftqgJVQ,38
|
|
10
|
-
llg3d/post/plot_results.py,sha256=NEhIEpVouQO0kf5Gsq5CLsDtnQtBuC0hxQ8IWII04Fg,1507
|
|
11
|
-
llg3d/post/process.py,sha256=yIM4LuhqSckfXCBTToPSnRS7qSswJ8tk3DNzJ-wQerU,3422
|
|
12
|
-
llg3d/post/temperature.py,sha256=Zz7za7Af3zQAxuSVznpCf5Ive9da4dvOK-2U2-Vwhuc,2271
|
|
13
|
-
llg3d/solver/__init__.py,sha256=cAXNyd8XZrdkfutNI0vgyMG4YueJiRL3Mqbf8nI7jvo,886
|
|
14
|
-
llg3d/solver/jax.py,sha256=o-c8Vs6T6hAxn0eFchHwBjry8QOWr3MK-d2Rs7XpRRA,11965
|
|
15
|
-
llg3d/solver/mpi.py,sha256=HINbUbVxLvqrM0yKMdg8sz_28eZ3TXOScaSNYvSgovg,12850
|
|
16
|
-
llg3d/solver/numpy.py,sha256=vqQIMatv84obDcNjoCmKRtuRmOBxRAjwfxmKPH1h5YM,6035
|
|
17
|
-
llg3d/solver/opencl.py,sha256=ZkZrjzsQryrYDqoJbGLq2E7uJHaxAeXJUEebOLaNlWw,10221
|
|
18
|
-
llg3d/solver/solver.py,sha256=7m5iU1JtZfdRW6l20tnRElU4sPsmm-gqgNujyqe1BuE,2152
|
|
19
|
-
llg3d-2.0.0.dist-info/licenses/AUTHORS,sha256=vhJ88HikYvOrGiB_l1xH2X6hyn9ZJafx6mpoMYNhh1I,297
|
|
20
|
-
llg3d-2.0.0.dist-info/licenses/LICENSE,sha256=aFxTGAyyve8nM9T2jWTarJzQhdSSC3MbbN1heNAev9c,1062
|
|
21
|
-
llg3d-2.0.0.dist-info/METADATA,sha256=4wnGvwrqKOLrZcsb5XIPKWBhWt1-diU3-f9LSUdbRP4,1952
|
|
22
|
-
llg3d-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
-
llg3d-2.0.0.dist-info/entry_points.txt,sha256=kjuf09uDEuROnDeuHkVen3oAS4ThEPS084TpHmQAbJ8,133
|
|
24
|
-
llg3d-2.0.0.dist-info/top_level.txt,sha256=cBZ0roaXt3CAXqYojuO84lGPCtWuLlXxLGLYRKmHZy0,6
|
|
25
|
-
llg3d-2.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|