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/__init__.py CHANGED
@@ -1 +1,4 @@
1
- __version__ = "1.4.1"
1
+ from .solver import LIB_AVAILABLE, rank, size, comm, status
2
+
3
+ __version__ = "2.0.0"
4
+ __all__ = ["LIB_AVAILABLE", "rank", "size", "comm", "status", "__version__"]
llg3d/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ # Enable to use llg3d as a module
2
+
3
+ from .main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
llg3d/element.py ADDED
@@ -0,0 +1,128 @@
1
+ r"""
2
+ Module containing the definition of the chemical elements
3
+ """
4
+
5
+ import numpy as np
6
+
7
+ from .grid import Grid
8
+
9
+ k_B = 1.38e-23 #: Boltzmann constant :math:`[J.K^{-1}]`
10
+ mu_0 = 4 * np.pi * 1.0e-7 #: Vacuum permeability :math:`[H.m^{-1}]`
11
+ gamma = 1.76e11 #: Gyromagnetic ratio :math:`[rad.s^{-1}.T^{-1}]`
12
+
13
+
14
+ class Element:
15
+ """Abstract class for chemical elements"""
16
+
17
+ A = 0.0
18
+ K = 0.0
19
+ lambda_G = 0.0
20
+ M_s = 0.0
21
+ a_eff = 0.0
22
+ anisotropy: str = ""
23
+
24
+ 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
+ self.H_ext = H_ext
34
+ self.g = g
35
+ self.dt = dt
36
+ self.gamma_0 = gamma * mu_0 #: Rescaled gyromagnetic ratio [mA^-1.s^-1]
37
+
38
+ # --- Characteristic Scales ---
39
+ self.coeff_1 = self.gamma_0 * 2.0 * self.A / (mu_0 * self.M_s)
40
+ self.coeff_2 = self.gamma_0 * 2.0 * self.K / (mu_0 * self.M_s)
41
+ self.coeff_3 = self.gamma_0 * H_ext
42
+
43
+ # corresponds to the temperature actually put into the random field
44
+ T_simu = T * self.g.dx / self.a_eff
45
+ # calculation of the random field related to temperature
46
+ # (we only take the volume over one mesh)
47
+ h_alea = np.sqrt(
48
+ 2 * self.lambda_G * k_B / (self.gamma_0 * mu_0 * self.M_s * self.g.dV)
49
+ )
50
+ H_alea = h_alea * np.sqrt(T_simu) * np.sqrt(1.0 / self.dt)
51
+ self.coeff_4 = H_alea * self.gamma_0
52
+
53
+ def get_CFL(self) -> float:
54
+ """
55
+ Returns the value of the CFL
56
+
57
+ Returns:
58
+ float: The CFL value
59
+ """
60
+ return self.dt * self.coeff_1 / self.g.dx**2
61
+
62
+ def to_dict(self) -> dict:
63
+ """
64
+ Export element parameters to a dictionary for JAX JIT compatibility
65
+
66
+ Returns:
67
+ Dictionary containing element parameters needed for computations
68
+ """
69
+ # Map anisotropy string to integer for JIT compatibility
70
+ aniso_map = {"uniaxial": 0, "cubic": 1}
71
+
72
+ return {
73
+ "coeff_1": self.coeff_1,
74
+ "coeff_2": self.coeff_2,
75
+ "coeff_3": self.coeff_3,
76
+ "coeff_4": self.coeff_4,
77
+ "lambda_G": self.lambda_G,
78
+ "anisotropy": aniso_map[self.anisotropy],
79
+ "gamma_0": self.gamma_0,
80
+ }
81
+
82
+
83
+ class Cobalt(Element):
84
+ A = 30.0e-12 #: Exchange constant :math:`[J.m^{-1}]`
85
+ K = 520.0e3 #: Anisotropy constant :math:`[J.m^{-3}]`
86
+ lambda_G = 0.5 #: Damping parameter :math:`[1]`
87
+ M_s = 1400.0e3 #: Saturation magnetization :math:`[A.m^{-1}]`
88
+ a_eff = 0.25e-9 #: Effective lattice constant :math:`[m]`
89
+ anisotropy = "uniaxial" #: Type of anisotropy (e.g., "uniaxial", "cubic")
90
+
91
+
92
+ class Iron(Element):
93
+ A = 21.0e-12 #: Exchange constant :math:`[J.m^{-1}]`
94
+ K = 48.0e3 #: Anisotropy constant :math:`[J.m^{-3}]`
95
+ lambda_G = 0.5 #: Damping parameter :math:`[1]`
96
+ M_s = 1700.0e3 #: Saturation magnetization :math:`[A.m^{-1}]`
97
+ a_eff = 0.286e-9 #: Effective lattice constant :math:`[m]`
98
+ anisotropy = "cubic" #: Type of anisotropy (e.g., "uniaxial", "cubic")
99
+
100
+
101
+ class Nickel(Element):
102
+ A = 9.0e-12 #: Exchange constant :math:`[J.m^{-1}]`
103
+ K = -5.7e3 #: Anisotropy constant :math:`[J.m^{-3}]`
104
+ lambda_G = 0.5 #: Damping parameter :math:`[1]`
105
+ M_s = 490.0e3 #: Saturation magnetization :math:`[A.m^{-1}]`
106
+ a_eff = 0.345e-9 #: Effective lattice constant :math:`[m]`
107
+ anisotropy = "cubic" #: Type of anisotropy (e.g., "uniaxial", "cubic")
108
+
109
+
110
+ def get_element_class(element_name: str | type[Element]) -> type[Element]:
111
+ """
112
+ Get the class of the chemical element by its name.
113
+
114
+ Args:
115
+ element_name: The name of the element or its class
116
+
117
+ Returns:
118
+ The class of the element
119
+
120
+ Raises:
121
+ ValueError: If the element is not found
122
+ """
123
+ if isinstance(element_name, type):
124
+ return element_name
125
+ for cls in Element.__subclasses__():
126
+ if cls.__name__ == element_name:
127
+ return cls
128
+ raise ValueError(f"Element '{element_name}' not found in {__file__}.")
llg3d/grid.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ Module to define the grid for the simulation
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ import numpy as np
7
+
8
+ from .solver import rank, size
9
+
10
+
11
+ @dataclass
12
+ class Grid:
13
+ """Stores grid data"""
14
+
15
+ # Parameter values correspond to the global grid
16
+ Jx: int #: number of points in x direction
17
+ Jy: int #: number of points in y direction
18
+ Jz: int #: number of points in z direction
19
+ dx: float #: grid spacing in x direction
20
+
21
+ def __post_init__(self) -> None:
22
+ """Compute grid characteristics"""
23
+ self.dy = self.dz = self.dx # Setting dx = dy = dz
24
+ self.Lx = (self.Jx - 1) * self.dx
25
+ self.Ly = (self.Jy - 1) * self.dy
26
+ self.Lz = (self.Jz - 1) * self.dz
27
+ # shape of the local array to the process
28
+ self.dims = self.Jx // size, self.Jy, self.Jz
29
+ # elemental volume of a cell
30
+ self.dV = self.dx * self.dy * self.dz
31
+ # total volume
32
+ self.V = self.Lx * self.Ly * self.Lz
33
+ # total number of points
34
+ self.ntot = self.Jx * self.Jy * self.Jz
35
+ self.ncell = (self.Jx - 1) * (self.Jy - 1) * (self.Jz - 1)
36
+
37
+ def __str__(self) -> str:
38
+ """Print grid information"""
39
+
40
+ header = "\t\t".join(("x", "y", "z"))
41
+ s = f"""\
42
+ \t{header}
43
+ J\t{self.Jx}\t\t{self.Jy}\t\t{self.Jz}
44
+ L\t{self.Lx:.08e}\t{self.Ly:.08e}\t{self.Lz:.08e}
45
+ d\t{self.dx:.08e}\t{self.dy:.08e}\t{self.dz:.08e}
46
+
47
+ dV = {self.dV:.08e}
48
+ V = {self.V:.08e}
49
+ ntot = {self.ntot:d}
50
+ ncell = {self.ncell:d}
51
+ """
52
+ return s
53
+
54
+ def get_filename(
55
+ self, T: float, name: str = "m1_mean", extension: str = "txt"
56
+ ) -> str:
57
+ """
58
+ Returns the output file name for a given temperature
59
+
60
+ Args:
61
+ T: temperature
62
+ name: file name
63
+ extension: file extension
64
+
65
+ Returns:
66
+ file name
67
+
68
+ >>> g = Grid(Jx=300, Jy=21, Jz=21, dx=1.e-9)
69
+ >>> g.get_filename(1100)
70
+ 'm1_mean_T1100_300x21x21.txt'
71
+ """
72
+ suffix = f"T{int(T)}_{self.Jx}x{self.Jy}x{self.Jz}"
73
+ return f"{name}_{suffix}.{extension}"
74
+
75
+ def get_mesh(
76
+ self, local: bool = True, dtype=np.float64
77
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
78
+ """
79
+ Returns a meshgrid of the coordinates.
80
+
81
+ Args:
82
+ local: if True, returns the local coordinates,
83
+ otherwise the global coordinates
84
+ dtype: data type of the coordinates)
85
+
86
+ Returns:
87
+ tuple of 3D arrays with the coordinates
88
+ """
89
+ x_global = np.linspace(0, self.Lx, self.Jx, dtype=dtype) # global coordinates
90
+ y = np.linspace(0, self.Ly, self.Jy, dtype=dtype)
91
+ z = np.linspace(0, self.Lz, self.Jz, dtype=dtype)
92
+ if local:
93
+ x_local = np.split(x_global, size)[rank] # local coordinates
94
+ return np.meshgrid(x_local, y, z, indexing="ij")
95
+ else:
96
+ return np.meshgrid(x_global, y, z, indexing="ij")
97
+
98
+ def to_dict(self) -> dict:
99
+ """
100
+ Export grid parameters to a dictionary for JAX JIT compatibility
101
+
102
+ Returns:
103
+ Dictionary containing grid parameters needed for computations
104
+ """
105
+ return {
106
+ "dx": self.dx,
107
+ "dy": self.dy,
108
+ "dz": self.dz,
109
+ "Jx": self.Jx,
110
+ "Jy": self.Jy,
111
+ "Jz": self.Jz,
112
+ "dV": self.dV,
113
+ }
114
+
115
+ def get_laplacian_coeff(self) -> tuple[float, float, float, float]:
116
+ """
117
+ Returns the coefficients for the laplacian computation
118
+
119
+ Returns:
120
+ Tuple of coefficients (dx2_inv, dy2_inv, dz2_inv, center_coeff)
121
+ """
122
+ dx2_inv = 1 / self.dx**2
123
+ dy2_inv = 1 / self.dy**2
124
+ dz2_inv = 1 / self.dz**2
125
+ center_coeff = -2 * (dx2_inv + dy2_inv + dz2_inv)
126
+ return dx2_inv, dy2_inv, dz2_inv, center_coeff
llg3d/main.py ADDED
@@ -0,0 +1,66 @@
1
+ """
2
+ Define a CLI for the llg3d package.
3
+ """
4
+
5
+ import argparse
6
+
7
+
8
+ from . import rank, size, LIB_AVAILABLE
9
+ from .parameters import parameters, get_parameter_list
10
+ from .simulation import Simulation
11
+
12
+ if LIB_AVAILABLE["mpi4py"]:
13
+ # Use the MPI version of the ArgumentParser
14
+ from .solver.mpi import ArgumentParser
15
+ else:
16
+ # Use the original version of the ArgumentParser
17
+ from argparse import ArgumentParser
18
+
19
+
20
+ def parse_args(args: list[str] | None) -> argparse.Namespace:
21
+ """
22
+ Argument parser for llg3d.
23
+ Automatically adds arguments from the parameter dictionary.
24
+
25
+ Returns:
26
+ argparse.Namespace: Parsed arguments
27
+ """
28
+ parser = ArgumentParser(
29
+ description=__doc__,
30
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
31
+ )
32
+
33
+ if size > 1:
34
+ parameters["solver"]["default"] = "mpi"
35
+
36
+ # Automatically add arguments from the parameter dictionary
37
+ for name, parameter in parameters.items():
38
+ if "action" not in parameter:
39
+ parameter["type"] = type(parameter["default"])
40
+ parser.add_argument(f"--{name}", **parameter)
41
+
42
+ return parser.parse_args(args)
43
+
44
+
45
+ def main(arg_list: list[str] = None):
46
+ """
47
+ Evaluates the command line and runs the simulation
48
+ """
49
+
50
+ args = parse_args(arg_list)
51
+
52
+ if size > 1 and args.solver != "mpi":
53
+ raise ValueError(f"Solver method {args.solver} is not compatible with MPI.")
54
+ if args.solver == "mpi" and not LIB_AVAILABLE["mpi4py"]:
55
+ raise ValueError(
56
+ "The MPI solver method requires to install the mpi4py package, "
57
+ "for example using pip: pip install mpi4py"
58
+ )
59
+
60
+ if rank == 0:
61
+ # Display parameters as a list
62
+ print(get_parameter_list(vars(args)))
63
+
64
+ simulation = Simulation(vars(args))
65
+ simulation.run()
66
+ simulation.save()
llg3d/output.py ADDED
@@ -0,0 +1,108 @@
1
+ """
2
+ Utility functions for LLG3D
3
+ """
4
+
5
+ import json
6
+ import sys
7
+ from typing import Iterable, TextIO
8
+
9
+ from .solver import rank
10
+ from .grid import Grid
11
+
12
+
13
+ def progress_bar(it: Iterable, prefix: str = "", size: int = 60, out: TextIO = sys.stdout):
14
+ """
15
+ Displays a progress bar
16
+
17
+ (Source: https://stackoverflow.com/a/34482761/16593179)
18
+
19
+ Args:
20
+ it: Iterable object to iterate over
21
+ prefix: Prefix string for the progress bar
22
+ size: Size of the progress bar (number of characters)
23
+ out: Output stream (default is sys.stdout)
24
+ """
25
+
26
+ count = len(it)
27
+
28
+ def show(j):
29
+ x = int(size * j / count)
30
+ if rank == 0:
31
+ print(
32
+ f"{prefix}[{u'█'*x}{('.'*(size-x))}] {j}/{count}",
33
+ end="\r",
34
+ file=out,
35
+ flush=True,
36
+ )
37
+
38
+ show(0)
39
+ for i, item in enumerate(it):
40
+ yield item
41
+ # To avoid slowing down the computation, we do not display at every iteration
42
+ if i % 5 == 0:
43
+ show(i + 1)
44
+ show(i + 1)
45
+ if rank == 0:
46
+ print("\n", flush=True, file=out)
47
+
48
+
49
+ def write_json(json_file: str, run: dict):
50
+ """
51
+ Writes the run dictionary to a JSON file
52
+
53
+ Args:
54
+ json_file: Name of the JSON file
55
+ run: Dictionary containing the run information
56
+ """
57
+ with open(json_file, "w") as f:
58
+ json.dump(run, f, indent=4)
59
+
60
+
61
+ def get_output_files(g: Grid, T: float, n_mean: int, n_profile: int) -> tuple:
62
+ """
63
+ Open files and list them
64
+
65
+ Args:
66
+ g: Grid object
67
+ T: temperature
68
+ n_mean: Number of iterations for integral output
69
+ n_profile: Number of iterations for profile output
70
+
71
+ Returns:
72
+ - a file handler for storing m space integral over time
73
+ - a file handler for storing x-profiles of m_i
74
+ - a list of output filenames
75
+ """
76
+ f_mean = None
77
+ f_profiles = None
78
+ output_filenames = []
79
+ if n_mean != 0:
80
+ output_filenames.append(g.get_filename(T, extension="txt"))
81
+ if n_profile != 0:
82
+ output_filenames.extend(
83
+ [g.get_filename(T, name=f"m{i + 1}", extension="npy") for i in range(3)]
84
+ )
85
+ if rank == 0:
86
+ if n_mean != 0:
87
+ f_mean = open(output_filenames[0], "w") # integral of m1
88
+ if n_profile != 0:
89
+ f_profiles = [
90
+ open(output_filename, "wb") for output_filename in output_filenames[1:]
91
+ ] # x profiles of m_i
92
+
93
+ return f_mean, f_profiles, output_filenames
94
+
95
+
96
+ def close_output_files(f_mean: TextIO, f_profiles: list[TextIO] = None):
97
+ """
98
+ Close all output files
99
+
100
+ Args:
101
+ f_mean: file handler for storing m space integral over time
102
+ f_profiles: file handlers for storing x-profiles of m_i
103
+ """
104
+ if f_mean is not None:
105
+ f_mean.close()
106
+ if f_profiles is not None:
107
+ for f_profile in f_profiles:
108
+ f_profile.close()
llg3d/parameters.py ADDED
@@ -0,0 +1,75 @@
1
+ """Parameters for the LLG3D simulation."""
2
+
3
+ import numpy as np
4
+
5
+ # Parameters: default value and description
6
+ Parameter = dict[str, str | int | float | bool] #: Type for a parameter
7
+
8
+ parameters: dict[str, Parameter] = {
9
+ "element": {
10
+ "help": "Chemical element of the sample",
11
+ "default": "Cobalt",
12
+ "choices": ["Cobalt", "Iron", "Nickel"],
13
+ },
14
+ "N": {"help": "Number of time iterations", "default": 5000},
15
+ "dt": {"help": "Time step", "default": 1.0e-14},
16
+ "Jx": {"help": "Number of points in x", "default": 300},
17
+ "Jy": {"help": "Number of points in y", "default": 21},
18
+ "Jz": {"help": "Number of points in z", "default": 21},
19
+ "dx": {"help": "Step in x", "default": 1.0e-9},
20
+ "T": {"help": "Temperature (K)", "default": 0.0},
21
+ "H_ext": {"help": "External field (A/m)", "default": 0.0 / (4 * np.pi * 1.0e-7)},
22
+ "start_averaging": {"help": "Start index of time average", "default": 4000},
23
+ "n_mean": {
24
+ "help": "Spatial average frequency (number of iterations)",
25
+ "default": 1,
26
+ },
27
+ "n_profile": {
28
+ "help": "x-profile save frequency (number of iterations)",
29
+ "default": 0,
30
+ },
31
+ "solver": {
32
+ "help": "Solver to use for the simulation",
33
+ "default": "numpy",
34
+ "choices": ["opencl", "mpi", "numpy", "jax"],
35
+ },
36
+ "precision": {
37
+ "help": "Precision of the simulation (single or double)",
38
+ "default": "double",
39
+ "choices": ["single", "double"],
40
+ },
41
+ "blocking": {
42
+ "help": "Use blocking communications",
43
+ "default": False,
44
+ "action": "store_true",
45
+ },
46
+ "seed": {
47
+ "help": "Random seed for temperature fluctuations",
48
+ "default": 12345,
49
+ "type": int,
50
+ },
51
+ "device": {
52
+ "help": "Device to use ('cpu', 'gpu', or 'auto')",
53
+ "default": "auto",
54
+ "type": str,
55
+ },
56
+ } #: simulation parameters
57
+
58
+
59
+ def get_parameter_list(parameters: dict) -> str:
60
+ """
61
+ Returns parameter values as a string
62
+
63
+ Args:
64
+ d: Dictionary of parameters parsed by argparse
65
+
66
+ Returns:
67
+ str: Formatted string of parameters
68
+ """
69
+ width = max([len(name) for name in parameters])
70
+ s = ""
71
+ for name, value in parameters.items():
72
+ # the seprator is ":" for strings and "=" for others
73
+ sep = ":" if isinstance(value, str) else "="
74
+ s += "{0:<{1}} {2} {3}\n".format(name, width, sep, value)
75
+ return s
@@ -0,0 +1,65 @@
1
+ """
2
+ Plot 1D curves from several files
3
+
4
+ Usage:
5
+
6
+ python plot_results.py file1.txt
7
+ or
8
+ python plot_results.py file1.txt file2.txt file3.txt
9
+
10
+ """
11
+
12
+ import argparse
13
+ from matplotlib import pyplot as plt
14
+ import numpy as np
15
+
16
+
17
+ DEFAULT_OUTPUT_FILE = "results.png"
18
+
19
+
20
+ def plot(*files: tuple[str], output_file: str = DEFAULT_OUTPUT_FILE):
21
+ """
22
+ Plot the results from the given files.
23
+
24
+ Args:
25
+ files (tuple[str]): Paths to the result files.
26
+ output_file (str): Path to the output image file.
27
+ """
28
+
29
+ fig, ax = plt.subplots()
30
+ for file in files:
31
+ if not file.endswith(".txt"):
32
+ raise ValueError(f"File {file} does not end with .txt")
33
+ data = np.loadtxt(file)
34
+ ax.plot(data[:, 0], data[:, 1], label=file)
35
+
36
+ ax.set_xlabel("time")
37
+ ax.set_ylabel(r"$<m_1>$")
38
+ ax.legend()
39
+ ax.set_title(r"Space average of $m_1$ according to time")
40
+ fig.savefig(output_file)
41
+ print(f"Written to {output_file}")
42
+ plt.show()
43
+
44
+
45
+ def main():
46
+ parser = argparse.ArgumentParser(
47
+ description="Plot results from one or more files."
48
+ )
49
+ parser.add_argument(
50
+ "files", nargs="+", type=str, help="Path to the result files."
51
+ )
52
+ parser.add_argument(
53
+ "--output",
54
+ "-o",
55
+ type=str,
56
+ default=DEFAULT_OUTPUT_FILE,
57
+ help=f"Path to the output image file (default: {DEFAULT_OUTPUT_FILE}).",
58
+ )
59
+ args = parser.parse_args()
60
+
61
+ plot(*args.files, output_file=args.output)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
llg3d/post/temperature.py CHANGED
@@ -7,7 +7,6 @@ import argparse
7
7
  from pathlib import Path
8
8
 
9
9
  import matplotlib.pyplot as plt
10
- import numpy as np
11
10
 
12
11
  from .process import MagData
13
12
 
llg3d/simulation.py ADDED
@@ -0,0 +1,104 @@
1
+ """
2
+ Define the Simulation class.
3
+
4
+ Example usage:
5
+
6
+ >>> from llg3d.main import Simulation
7
+ >>> from llg3d.parameters import parameters
8
+ >>> run_parameters = {name: value["default"] for name, value in parameters.items()}
9
+ >>> sim = Simulation(run_parameters)
10
+ >>> sim.run()
11
+ >>> sim.save()
12
+
13
+ """
14
+
15
+ import inspect
16
+
17
+ from . import rank, size
18
+ from .element import get_element_class
19
+ from .parameters import Parameter
20
+ from .output import write_json
21
+
22
+
23
+ class Simulation:
24
+ """
25
+ Class to encapsulate the simulation logic.
26
+ """
27
+
28
+ json_file = "run.json" #: JSON file to store the results
29
+
30
+ 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
+ self.params: dict[str, Parameter] = params.copy() #: simulation parameters
38
+ self.simulate: callable = self._get_simulate_function_from_name(
39
+ self.params["solver"]
40
+ ) #: simulation function imported from the solver module
41
+ self.total_time: None | float = None #: total simulation time
42
+ self.filenames: list[str] = [] #: list of output filenames
43
+ self.m1_mean: None | float = None #: space and time average of m1
44
+ self.params["np"] = size # Add a parameter for the number of processes
45
+ # Reference the element class from the element string
46
+ self.params["element_class"] = get_element_class(params["element"])
47
+
48
+ def run(self):
49
+ """
50
+ Runs the simulation and store the results.
51
+ """
52
+ self.total_time, self.filenames, self.m1_mean = self.simulate(**self.params)
53
+
54
+ def _get_simulate_function_from_name(self, name: str) -> callable:
55
+ """
56
+ Retrieves the simulation function for a given solver name.
57
+
58
+ Args:
59
+ name: Name of the solver
60
+
61
+ Returns:
62
+ callable: The simulation function
63
+
64
+ Example:
65
+
66
+ >>> simulate = self.get_simulate_function_from_name("mpi")
67
+
68
+ Will return the `simulate` function from the `llg3d.solver.mpi` module.
69
+ """
70
+
71
+ module = __import__(f"llg3d.solver.{name}", fromlist=["simulate"])
72
+ return inspect.getattr_static(module, "simulate")
73
+
74
+ def save(self):
75
+ """
76
+ Saves the results of the simulation to a JSON file.
77
+ """
78
+ params = self.params.copy() # save the parameters
79
+ del params["element_class"] # remove class object before serialization
80
+ if rank == 0:
81
+ results = {"total_time": self.total_time}
82
+ # Export the integral of m1
83
+ if len(self.filenames) > 0:
84
+ results["integral_file"] = self.filenames[0]
85
+ print(f"Integral of m1 in {self.filenames[0]}")
86
+ # Export the x-profiles of m1, m2 and m3
87
+ for i, filename in enumerate(self.filenames[1:]):
88
+ results[f"xprofile_m{i}"] = filename
89
+ print(f"x-profile of m{i} in {filename}")
90
+
91
+ print(
92
+ f"""\
93
+ N iterations = {params["N"]}
94
+ total_time [s] = {self.total_time:.03f}
95
+ time/ite [s/iter] = {self.total_time / params["N"]:.03e}\
96
+ """
97
+ )
98
+ # Export the mean of m1
99
+ if params["N"] > params["start_averaging"]:
100
+ print(f"m1_mean = {self.m1_mean:e}")
101
+ results["m1_mean"] = float(self.m1_mean)
102
+
103
+ write_json(self.json_file, {"params": params, "results": results})
104
+ print(f"Summary in {self.json_file}")