llg3d 2.0.0__py3-none-any.whl → 3.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.
Files changed (48) hide show
  1. llg3d/__init__.py +3 -3
  2. llg3d/__main__.py +2 -2
  3. llg3d/benchmarks/__init__.py +1 -0
  4. llg3d/benchmarks/compare_commits.py +321 -0
  5. llg3d/benchmarks/efficiency.py +451 -0
  6. llg3d/benchmarks/utils.py +25 -0
  7. llg3d/element.py +118 -31
  8. llg3d/grid.py +51 -64
  9. llg3d/io.py +395 -0
  10. llg3d/main.py +36 -38
  11. llg3d/parameters.py +159 -49
  12. llg3d/post/__init__.py +1 -1
  13. llg3d/post/extract.py +105 -0
  14. llg3d/post/info.py +178 -0
  15. llg3d/post/m1_vs_T.py +90 -0
  16. llg3d/post/m1_vs_time.py +56 -0
  17. llg3d/post/process.py +82 -75
  18. llg3d/post/utils.py +38 -0
  19. llg3d/post/x_profiles.py +141 -0
  20. llg3d/py.typed +1 -0
  21. llg3d/solvers/__init__.py +153 -0
  22. llg3d/solvers/base.py +345 -0
  23. llg3d/solvers/experimental/__init__.py +9 -0
  24. llg3d/solvers/experimental/jax.py +361 -0
  25. llg3d/solvers/math_utils.py +41 -0
  26. llg3d/solvers/mpi.py +370 -0
  27. llg3d/solvers/numpy.py +126 -0
  28. llg3d/solvers/opencl.py +439 -0
  29. llg3d/solvers/profiling.py +38 -0
  30. {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/METADATA +6 -3
  31. llg3d-3.0.0.dist-info/RECORD +36 -0
  32. {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/WHEEL +1 -1
  33. llg3d-3.0.0.dist-info/entry_points.txt +9 -0
  34. llg3d/output.py +0 -108
  35. llg3d/post/plot_results.py +0 -65
  36. llg3d/post/temperature.py +0 -83
  37. llg3d/simulation.py +0 -104
  38. llg3d/solver/__init__.py +0 -45
  39. llg3d/solver/jax.py +0 -383
  40. llg3d/solver/mpi.py +0 -449
  41. llg3d/solver/numpy.py +0 -210
  42. llg3d/solver/opencl.py +0 -329
  43. llg3d/solver/solver.py +0 -93
  44. llg3d-2.0.0.dist-info/RECORD +0 -25
  45. llg3d-2.0.0.dist-info/entry_points.txt +0 -4
  46. {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/licenses/AUTHORS +0 -0
  47. {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/licenses/LICENSE +0 -0
  48. {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/top_level.txt +0 -0
llg3d/post/process.py CHANGED
@@ -1,105 +1,112 @@
1
- #!/usr/bin/env python3
2
1
  """
3
- Post-processes a set of runs grouped into a `run.json` file or
4
- into a set of SLURM job arrays:
2
+ Post-processes a set of runs.
5
3
 
6
4
  1. Extracts result data,
7
5
  2. Plots the computed average magnetization against temperature,
8
- 3. Interpolates the computed points using cubic splines,
9
- 4. Determines the Curie temperature as the value corresponding to the minimal (negative) slope of the interpolated curve.
6
+ 3. Interpolates the computed points using a PCHIP interpolator,
7
+ 4. Determines the Curie temperature as the value below which the magnetization
8
+ drops under 0.1.
10
9
  """
11
10
 
12
- import json
13
11
  from pathlib import Path
14
12
 
13
+
15
14
  import numpy as np
16
- from scipy.interpolate import interp1d
15
+ from scipy.interpolate import PchipInterpolator
16
+ from scipy.optimize import brentq
17
17
 
18
+ from llg3d.io import load_results
18
19
 
19
- class MagData:
20
- """
21
- Class to handle magnetization data and interpolation according to temperature
22
- """
23
20
 
24
- n_interp = 200
21
+ class MagTempData:
22
+ """
23
+ Handle magnetization data using the npz format.
25
24
 
26
- def __init__(self, job_dir: Path = None, run_file: Path = Path("run.json")) -> None:
25
+ - Extracts result data,
26
+ - Interpolates the computed points using a PCHIP interpolator,
27
+ - Determines the Curie temperature as the value below which the magnetization
28
+ drops under 0.1.
27
29
 
28
- if job_dir:
29
- self.parentpath = job_dir
30
- data, self.run = self.process_slurm_jobs()
31
- elif run_file:
32
- self.parentpath = run_file.parent
33
- data, self.run = self.process_json(run_file)
30
+ Args:
31
+ *files: Paths to the result .npz files
32
+ """
34
33
 
35
- self.temperature = data[:, 0]
36
- self.m1_mean = data[:, 1]
37
- self.interp = interp1d(self.temperature, self.m1_mean, kind="cubic")
38
- self.T = np.linspace(
34
+ n_interp = 200 #: number of interpolation points
35
+
36
+ def __init__(self, *files: Path | str) -> None:
37
+ #: list of result files
38
+ self.files: list[Path] = [Path(file) for file in files]
39
+ self.params: dict = {} #: common parameters of the runs
40
+ # Extract data from the runs
41
+ data = self._process_jobs()
42
+ self.temperature = data[:, 0] #: temperatures from the runs
43
+ self.m1_mean = data[:, 1] #: mean magnetization
44
+ # Use PCHIP interpolator which preserves positivity and monotonicity
45
+ #: interpolated magnetization function
46
+ self.interp = PchipInterpolator(self.temperature, self.m1_mean)
47
+ self.temperature_interp = np.linspace(
39
48
  self.temperature.min(), self.temperature.max(), self.n_interp
40
- )
49
+ ) #: finer temperature grid for interpolation
41
50
 
42
- def process_slurm_jobs(self) -> tuple[np.array, dict]:
51
+ @property
52
+ def T_Curie(self) -> float:
43
53
  """
44
- Iterates through calculation directories to assemble data.
54
+ Return the Curie temperature.
45
55
 
46
- Args:
47
- parentdir (str): path to the directory containing the runs
56
+ It is defined as the temperature at which the magnetization equals 0.1,
57
+ found using a root-finding algorithm for precision.
48
58
 
49
59
  Returns:
50
- tuple: (data, run) where data is a numpy array (T, <m>) and run
51
- is a descriptive dictionary of the run
52
- """
53
- json_filename = "run.json"
54
-
55
- # List of run directories
56
- jobdirs = [f for f in self.parentpath.iterdir() if f.is_dir()]
57
- if len(jobdirs) == 0:
58
- exit(f"No job directories found in {self.parentpath}")
59
- data = []
60
- # Iterating through run directories
61
- for jobdir in jobdirs:
62
- try:
63
- # Reading the JSON file
64
- with open(jobdir / json_filename) as f:
65
- run = json.load(f)
66
- # Adding temperature and averaging value to the data list
67
- data.extend(
68
- [[float(T), res["m1_mean"]] for T, res in run["results"].items()]
69
- )
70
- except FileNotFoundError:
71
- print(f"Warning: {json_filename} file not found " f"in {jobdir.as_posix()}")
72
-
73
- data.sort() # Sorting by increasing temperatures
60
+ float: Curie temperature
74
61
 
75
- return np.array(data), run
76
-
77
-
78
- def process_json(json_filepath: Path) -> tuple[np.array, dict]:
62
+ Raises:
63
+ ValueError: If the magnetization never crosses 0.1 in the dataset
79
64
  """
80
- Reads the run.json file and extracts result data.
81
-
82
- Args:
83
- json_filepath: path to the run.json file
65
+ # Check if magnetization ever crosses 0.1
66
+ T_min = self.temperature.min()
67
+ T_max = self.temperature.max()
68
+ m1_min = self.interp(T_min)
69
+ m1_max = self.interp(T_max)
70
+
71
+ # If 0.1 is never reached
72
+ if (m1_min < 0.1 and m1_max < 0.1) or (m1_min > 0.1 and m1_max > 0.1):
73
+ raise ValueError(
74
+ f"Magnetization never crosses 0.1 in the dataset. "
75
+ f"Range: [{m1_min:.4f}, {m1_max:.4f}]"
76
+ )
77
+
78
+ # Find the exact temperature where m = 0.1 using Brent's method
79
+ T_curie = brentq(lambda T: float(self.interp(T)) - 0.1, T_min, T_max)
80
+ return float(T_curie)
81
+
82
+ def _process_jobs(self) -> np.ndarray:
83
+ """
84
+ Iterates through calculation directories to assemble data.
84
85
 
85
86
  Returns:
86
- tuple: (data, run) where data is a numpy array (T, <m>) and run
87
- is a descriptive dictionary of the run
87
+ data a numpy array (T, <m>)
88
+
89
+ Raises:
90
+ ValueError: If any file does not end with .npz
88
91
  """
89
- with open(json_filepath) as f:
90
- run = json.load(f)
92
+ for file in self.files:
93
+ if not file.name.endswith(".npz"):
94
+ raise ValueError(f"File {file} should end with .npz")
95
+ # Get parameters from the first run file
91
96
 
92
- data = [[int(T), res["m1_mean"]] for T, res in run["results"].items()]
97
+ first_results = load_results(self.files[0])
98
+ self.params = first_results.params.as_dict() # Store common parameters
99
+ data = []
100
+ # Iterating through run directories
101
+ for file in self.files:
102
+ print(f"Processing file: {file}")
103
+ run_results = load_results(file)
104
+ params = run_results.params.as_dict()
105
+ m1_mean = np.nan
106
+ if "observables" in run_results.results:
107
+ m1_mean = run_results.results["observables"].get("m1_mean", np.nan)
108
+ data.append([params["T"], m1_mean])
93
109
 
94
110
  data.sort() # Sorting by increasing temperatures
95
111
 
96
- return np.array(data), run
97
-
98
- @property
99
- def T_Curie(self) -> float:
100
- """
101
- Return the Curie temperature defined as the temperature at
102
- which the magnetization is below 0.1
103
- """
104
- i_max = np.where(0.1 - self.interp(self.T) > 0)[0].min()
105
- return self.T[i_max]
112
+ return np.array(data)
llg3d/post/utils.py ADDED
@@ -0,0 +1,38 @@
1
+ """Command-line argument parsing for post-processing scripts."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+
7
+ def get_cli_args(
8
+ description: str | None, default_image_filepath: Path
9
+ ) -> argparse.Namespace:
10
+ """
11
+ Parse command-line arguments for post-processing scripts.
12
+
13
+ Args:
14
+ description: Description of the script for the help message.
15
+ default_image_filepath: Default path to save the output image.
16
+
17
+ Returns:
18
+ Parsed command-line arguments.
19
+ """
20
+ parser = argparse.ArgumentParser(
21
+ description=description, formatter_class=argparse.ArgumentDefaultsHelpFormatter
22
+ )
23
+ parser.add_argument("files", nargs="+", type=Path, help="Path to the result files.")
24
+ parser.add_argument(
25
+ "-i",
26
+ "--image_filepath",
27
+ type=Path,
28
+ default=default_image_filepath,
29
+ help="Path to save the image",
30
+ )
31
+ parser.add_argument(
32
+ "-s",
33
+ "--show",
34
+ action="store_true",
35
+ default=False,
36
+ help="Display the plot (omit to disable display in non-interactive runs).",
37
+ )
38
+ return parser.parse_args()
@@ -0,0 +1,141 @@
1
+ """Plot x-profiles of magnetization from a result file."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from matplotlib import pyplot as plt
7
+
8
+ from ..grid import Grid
9
+ from ..io import RunResults, load_results
10
+ from .extract import extract_values
11
+
12
+
13
+ def parse_slice(slice_str: str) -> slice:
14
+ """
15
+ Parse a string representation of a slice into a slice object.
16
+
17
+ Handles formats like 'start:stop:step', 'start:stop', ':stop', '::step', etc.
18
+
19
+ Args:
20
+ slice_str: String representation of the slice.
21
+
22
+ Returns:
23
+ A slice object corresponding to the input string.
24
+ """
25
+ parts = slice_str.split(":")
26
+ # Convert parts to int or None
27
+ args = [int(p) if p else None for p in parts]
28
+ return slice(*args)
29
+
30
+
31
+ def plot_x_profiles(
32
+ file: Path,
33
+ image_filepath: Path | str | None = None,
34
+ show: bool = False,
35
+ time_slice: str = ":",
36
+ m_index: int = 0,
37
+ ):
38
+ """
39
+ Plot the results from the given files.
40
+
41
+ Args:
42
+ file: Path to the result file.
43
+ image_filepath: Path to the output image file.
44
+ if None, the image will not be saved.
45
+ show: display the graph in a graphical window.
46
+ time_slice: String representing the time slice to plot (e.g. ":-5", "::10").
47
+ m_index: Index of the magnetization component to plot (0, 1, or 2).
48
+ """
49
+ fig, ax = plt.subplots()
50
+ result: RunResults = load_results(file)
51
+ grid_params = extract_values(
52
+ file, "params/Jx", "params/Jy", "params/Jz", "params/dx"
53
+ )
54
+ grid = Grid(*grid_params) # type: ignore
55
+ x_coords = grid.get_x_coords(local=False)
56
+ x_profiles = result.get_record("x_profiles")
57
+
58
+ # Parse and apply slice
59
+ sl = parse_slice(time_slice)
60
+ times = x_profiles["t"][sl]
61
+ m_profiles = x_profiles[f"m{m_index}"][sl]
62
+
63
+ for i, time in enumerate(times):
64
+ ax.plot(x_coords, m_profiles[i], label=f"t = {time:.3e} s")
65
+ ax.set_xlabel("x")
66
+ ax.set_ylabel(rf"$m_{{{m_index}}}$")
67
+ ax.grid()
68
+ ax.legend()
69
+ ax.set_title(rf"Longitudinal profiles of magnetization $m_{{{m_index + 1}}}$")
70
+
71
+ if show:
72
+ plt.show()
73
+
74
+ if image_filepath is not None:
75
+ fig.savefig(image_filepath, dpi=300)
76
+ print(f"Written to {image_filepath}")
77
+
78
+
79
+ def get_cli_args(
80
+ description: str | None, default_image_filepath: Path
81
+ ) -> argparse.Namespace:
82
+ """
83
+ Parse command-line arguments for post-processing scripts.
84
+
85
+ Args:
86
+ description: Description of the script for the help message.
87
+ default_image_filepath: Default path to save the output image.
88
+
89
+ Returns:
90
+ Parsed command-line arguments.
91
+ """
92
+ parser = argparse.ArgumentParser(
93
+ description=description, formatter_class=argparse.ArgumentDefaultsHelpFormatter
94
+ )
95
+ parser.add_argument("file", type=Path, help="Path to the result file.")
96
+ parser.add_argument(
97
+ "-i",
98
+ "--image_filepath",
99
+ type=Path,
100
+ default=default_image_filepath,
101
+ help="Path to save the image",
102
+ )
103
+ parser.add_argument(
104
+ "-s",
105
+ "--show",
106
+ action="store_true",
107
+ default=False,
108
+ help="Display the plot (omit to disable display in non-interactive runs).",
109
+ )
110
+ parser.add_argument(
111
+ "-t",
112
+ "--time",
113
+ type=str,
114
+ default=":",
115
+ help="Slice for time steps (e.g., '-5:' for last 5, '::10' for every 10th).",
116
+ )
117
+ parser.add_argument(
118
+ "-m",
119
+ "--m_index",
120
+ type=int,
121
+ choices=[1, 2, 3],
122
+ default=1,
123
+ help="Index of the magnetization component to plot (1, 2, or 3).",
124
+ )
125
+ return parser.parse_args()
126
+
127
+
128
+ def main(): # pragma: no cover
129
+ """Parse CLI arguments and call the plot function."""
130
+ args = get_cli_args(
131
+ description=__doc__,
132
+ default_image_filepath=Path("x_profile.png"),
133
+ )
134
+
135
+ plot_x_profiles(
136
+ args.file,
137
+ image_filepath=args.image_filepath,
138
+ show=args.show,
139
+ time_slice=args.time,
140
+ m_index=args.m_index,
141
+ )
llg3d/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker file to indicate llg3d is PEP 561 type-annotated
@@ -0,0 +1,153 @@
1
+ """
2
+ Define various types of solvers.
3
+
4
+ Example:
5
+ To initialize one of the solver classes:
6
+
7
+ >>> from llg3d.parameters import RunParameters
8
+ >>> from llg3d.solvers.numpy import NumpySolver
9
+ >>> solver = NumpySolver(**RunParameters(solver="numpy").as_dict())
10
+ """
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ import os
15
+ import importlib.util
16
+
17
+ if TYPE_CHECKING:
18
+ from .base import BaseSolver
19
+
20
+
21
+ def get_size() -> int:
22
+ """
23
+ Return the number of parallel MPI processes.
24
+
25
+ Use environment variables to avoid initializing MPI unnecessarily.
26
+
27
+ Returns:
28
+ Number of MPI processes if in an MPI environment, else 1.
29
+ """
30
+ # Open MPI
31
+ if "OMPI_COMM_WORLD_SIZE" in os.environ:
32
+ return int(os.environ["OMPI_COMM_WORLD_SIZE"])
33
+
34
+ # MPICH/Intel MPI
35
+ if "PMI_SIZE" in os.environ:
36
+ return int(os.environ["PMI_SIZE"])
37
+
38
+ # SLURM
39
+ if "SLURM_NTASKS" in os.environ:
40
+ return int(os.environ["SLURM_NTASKS"])
41
+
42
+ return 1
43
+
44
+
45
+ def get_rank() -> int:
46
+ """
47
+ Return the rank of the current MPI process.
48
+
49
+ Use environment variables to avoid initializing MPI unnecessarily.
50
+
51
+ Returns:
52
+ Rank of the current MPI process if in an MPI environment, else 0.
53
+ """
54
+ # PMIx
55
+ if "PMIX_RANK" in os.environ:
56
+ return int(os.environ["PMIX_RANK"])
57
+
58
+ # Open MPI
59
+ if "OMPI_COMM_WORLD_RANK" in os.environ:
60
+ return int(os.environ["OMPI_COMM_WORLD_RANK"])
61
+
62
+ # MPICH/Intel MPI
63
+ if "PMI_RANK" in os.environ:
64
+ return int(os.environ["PMI_RANK"])
65
+
66
+ # SLURM
67
+ if "SLURM_PROCID" in os.environ:
68
+ return int(os.environ["SLURM_PROCID"])
69
+
70
+ return 0
71
+
72
+
73
+ __all__ = [
74
+ "rank",
75
+ "size",
76
+ "comm",
77
+ "status",
78
+ "mpi_initialized",
79
+ "get_size",
80
+ "get_rank",
81
+ "get_solver_class",
82
+ "LIB_AVAILABLE",
83
+ ]
84
+
85
+ LIB_AVAILABLE: dict[str, bool] = {}
86
+
87
+ # Check for other solver availability
88
+ for lib in "pyopencl", "jax", "mpi4py":
89
+ LIB_AVAILABLE[lib] = importlib.util.find_spec(lib, package=__package__) is not None
90
+
91
+
92
+ # MPI library: initialize dummy variables at first:
93
+ # it prevents from initializing the MPI communicator if not needed
94
+ class _DummyComm:
95
+ pass
96
+
97
+
98
+ class _DummyStatus:
99
+ pass
100
+
101
+
102
+ comm = _DummyComm()
103
+ rank = get_rank()
104
+ size = get_size()
105
+ status = _DummyStatus()
106
+ mpi_initialized = False
107
+
108
+
109
+ def get_solver_class(solver_name: str) -> "type[BaseSolver]":
110
+ """
111
+ Get the solver class based on the solver name.
112
+
113
+ Args:
114
+ solver_name: Name of the solver ("mpi", "numpy", "opencl", "jax")
115
+
116
+ Returns:
117
+ The solver class
118
+
119
+ Raises:
120
+ ValueError: If the selected solver is not compatible with MPI
121
+ or if the solver name is unknown
122
+
123
+ Example:
124
+ >>> Solver = get_solver_class("numpy")
125
+ >>> Solver.__name__
126
+ "NumpySolver"
127
+ """
128
+ if size > 1 and solver_name != "mpi":
129
+ raise ValueError(f"Solver method '{solver_name}' is not compatible with MPI.")
130
+
131
+ Solver: type[BaseSolver]
132
+ if solver_name == "mpi":
133
+ from .mpi import MPISolver
134
+
135
+ Solver = MPISolver
136
+ elif solver_name == "numpy":
137
+ from .numpy import NumpySolver
138
+
139
+ Solver = NumpySolver
140
+
141
+ elif solver_name == "opencl":
142
+ from .opencl import OpenCLSolver
143
+
144
+ Solver = OpenCLSolver
145
+
146
+ elif solver_name == "jax":
147
+ from .experimental.jax import JaxSolver
148
+
149
+ Solver = JaxSolver
150
+ else:
151
+ raise ValueError(f"Unknown solver method '{solver_name}'.")
152
+
153
+ return Solver