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.
- llg3d/__init__.py +3 -3
- llg3d/__main__.py +2 -2
- llg3d/benchmarks/__init__.py +1 -0
- llg3d/benchmarks/compare_commits.py +321 -0
- llg3d/benchmarks/efficiency.py +451 -0
- llg3d/benchmarks/utils.py +25 -0
- llg3d/element.py +118 -31
- llg3d/grid.py +51 -64
- llg3d/io.py +395 -0
- llg3d/main.py +36 -38
- llg3d/parameters.py +159 -49
- llg3d/post/__init__.py +1 -1
- llg3d/post/extract.py +105 -0
- llg3d/post/info.py +178 -0
- llg3d/post/m1_vs_T.py +90 -0
- llg3d/post/m1_vs_time.py +56 -0
- llg3d/post/process.py +82 -75
- llg3d/post/utils.py +38 -0
- llg3d/post/x_profiles.py +141 -0
- llg3d/py.typed +1 -0
- llg3d/solvers/__init__.py +153 -0
- llg3d/solvers/base.py +345 -0
- llg3d/solvers/experimental/__init__.py +9 -0
- llg3d/solvers/experimental/jax.py +361 -0
- llg3d/solvers/math_utils.py +41 -0
- llg3d/solvers/mpi.py +370 -0
- llg3d/solvers/numpy.py +126 -0
- llg3d/solvers/opencl.py +439 -0
- llg3d/solvers/profiling.py +38 -0
- {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/METADATA +6 -3
- llg3d-3.0.0.dist-info/RECORD +36 -0
- {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/WHEEL +1 -1
- llg3d-3.0.0.dist-info/entry_points.txt +9 -0
- llg3d/output.py +0 -108
- llg3d/post/plot_results.py +0 -65
- llg3d/post/temperature.py +0 -83
- llg3d/simulation.py +0 -104
- llg3d/solver/__init__.py +0 -45
- llg3d/solver/jax.py +0 -383
- llg3d/solver/mpi.py +0 -449
- llg3d/solver/numpy.py +0 -210
- llg3d/solver/opencl.py +0 -329
- llg3d/solver/solver.py +0 -93
- llg3d-2.0.0.dist-info/RECORD +0 -25
- llg3d-2.0.0.dist-info/entry_points.txt +0 -4
- {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/licenses/AUTHORS +0 -0
- {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {llg3d-2.0.0.dist-info → llg3d-3.0.0.dist-info}/top_level.txt +0 -0
llg3d/parameters.py
CHANGED
|
@@ -1,75 +1,185 @@
|
|
|
1
|
-
"""Parameters for the
|
|
1
|
+
"""Parameters for the simulation."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from argparse import Action
|
|
4
|
+
from dataclasses import asdict, dataclass, fields
|
|
5
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
Parameter = dict[str, str | int | float | bool] #: Type for a parameter
|
|
7
|
+
from . import solvers
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
# Type definitions for parameter names
|
|
10
|
+
ElementType = Literal["Cobalt", "Iron", "Nickel"]
|
|
11
|
+
SolverType = Literal["opencl", "mpi", "numpy", "jax"]
|
|
12
|
+
PrecisionType = Literal["single", "double"]
|
|
13
|
+
DeviceType = Literal["cpu", "gpu", "auto"]
|
|
14
|
+
InitType = Literal["0", "dw"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RunParameters:
|
|
19
|
+
"""
|
|
20
|
+
Store simulation parameters.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> params = RunParameters(element="Cobalt", N=1000)
|
|
24
|
+
>>> print(params)
|
|
25
|
+
element : Cobalt
|
|
26
|
+
N = 1000
|
|
27
|
+
dt = 1e-14
|
|
28
|
+
Jx = 300
|
|
29
|
+
Jy = 21
|
|
30
|
+
Jz = 21
|
|
31
|
+
dx = 1e-09
|
|
32
|
+
T = 1100
|
|
33
|
+
H_ext = 0.0
|
|
34
|
+
init_type : 0
|
|
35
|
+
result_file : run.npz
|
|
36
|
+
start_averaging = 2000
|
|
37
|
+
n_mean = 1
|
|
38
|
+
n_profile = 0
|
|
39
|
+
solver : numpy
|
|
40
|
+
precision : double
|
|
41
|
+
blocking = False
|
|
42
|
+
seed = 12345
|
|
43
|
+
device : auto
|
|
44
|
+
np = 1
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
element: ElementType = "Cobalt" #: Chemical element of the sample
|
|
48
|
+
N: int = 5000 #: Number of iterations
|
|
49
|
+
dt: float = 1.0e-14 #: Time step
|
|
50
|
+
Jx: int = 300 #: Number of points in x
|
|
51
|
+
Jy: int = 21 #: Number of points in y
|
|
52
|
+
Jz: int = 21 #: Number of points in z
|
|
53
|
+
dx: float = 1.0e-9 #: Step in x
|
|
54
|
+
T: float = 0.0 #: Temperature (K)
|
|
55
|
+
H_ext: float = 0.0 #: External field (A/m)
|
|
56
|
+
init_type: InitType = "0" #: Initialization type
|
|
57
|
+
result_file: str = "run.npz" #: Result file name
|
|
58
|
+
start_averaging: int = 4000 #: Start index of time average
|
|
59
|
+
n_mean: int = 1 #: Spatial average frequency (number of iterations)
|
|
60
|
+
n_profile: int = 0 #: x-profile save frequency (number of iterations)
|
|
61
|
+
solver: SolverType = "numpy" if solvers.size == 1 else "mpi" #: Solver to use
|
|
62
|
+
precision: PrecisionType = "double" #: Precision of the simulation
|
|
63
|
+
blocking: bool = False #: Use blocking communications
|
|
64
|
+
seed: int = 12345 #: Random seed for temperature fluctuations
|
|
65
|
+
device: DeviceType = "auto"
|
|
66
|
+
profiling: bool = False #: Enable profiling output
|
|
67
|
+
np: int = solvers.size #: Number of processes (for MPI solver)
|
|
68
|
+
|
|
69
|
+
def as_dict(self) -> dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Convert RunParameters to a dictionary.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dictionary representation of the parameters
|
|
75
|
+
"""
|
|
76
|
+
return asdict(self)
|
|
77
|
+
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
"""Return solver information in a readable format."""
|
|
80
|
+
# Compute the width of the longest parameter name
|
|
81
|
+
width = max([len(field.name) for field in fields(self)])
|
|
82
|
+
s = ""
|
|
83
|
+
for field in fields(self):
|
|
84
|
+
value = getattr(self, field.name)
|
|
85
|
+
# the seprator is ":" for strings and "=" for others
|
|
86
|
+
sep = ":" if isinstance(value, str) else "="
|
|
87
|
+
s += "{0:<{1}} {2} {3}\n".format(field.name, width, sep, value)
|
|
88
|
+
return s
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
DEFAULT_RUN_PARAMETERS = RunParameters()
|
|
92
|
+
DRP = DEFAULT_RUN_PARAMETERS #: Alias for default parameters
|
|
93
|
+
|
|
94
|
+
ParameterValue = str | int | float | bool
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ArgParameter(TypedDict):
|
|
98
|
+
"""Metadata for a simulation parameter."""
|
|
99
|
+
|
|
100
|
+
help: str
|
|
101
|
+
default: ParameterValue
|
|
102
|
+
choices: NotRequired[Any]
|
|
103
|
+
action: NotRequired[str | type[Action]]
|
|
104
|
+
type: NotRequired[type]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def lit_to_list(lit: Any) -> list[Any]:
|
|
108
|
+
"""
|
|
109
|
+
Convert a Literal type to a list of its possible values.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
lit: Literal type
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of possible values
|
|
116
|
+
"""
|
|
117
|
+
return list(lit.__args__)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
arg_parameters: dict[str, ArgParameter] = {
|
|
9
121
|
"element": {
|
|
10
122
|
"help": "Chemical element of the sample",
|
|
11
|
-
"default":
|
|
12
|
-
"choices":
|
|
123
|
+
"default": DRP.element,
|
|
124
|
+
"choices": lit_to_list(ElementType),
|
|
125
|
+
},
|
|
126
|
+
"N": {"help": "Number of time iterations", "default": DRP.N},
|
|
127
|
+
"dt": {"help": "Time step", "default": DRP.dt},
|
|
128
|
+
"Jx": {"help": "Number of points in x", "default": DRP.Jx},
|
|
129
|
+
"Jy": {"help": "Number of points in y", "default": DRP.Jy},
|
|
130
|
+
"Jz": {"help": "Number of points in z", "default": DRP.Jz},
|
|
131
|
+
"dx": {"help": "Step in x", "default": DRP.dx},
|
|
132
|
+
"T": {"help": "Temperature (K)", "default": DRP.T},
|
|
133
|
+
"H_ext": {"help": "External field (A/m)", "default": DRP.H_ext},
|
|
134
|
+
"init_type": {
|
|
135
|
+
"help": "Type of initialization ('0' for uniform, 'dw' for domain wall)",
|
|
136
|
+
"default": DRP.init_type,
|
|
137
|
+
"choices": lit_to_list(InitType),
|
|
138
|
+
},
|
|
139
|
+
"result_file": {
|
|
140
|
+
"help": "Name of the npz result file",
|
|
141
|
+
"default": DRP.result_file,
|
|
142
|
+
},
|
|
143
|
+
"start_averaging": {
|
|
144
|
+
"help": "Start index of time average",
|
|
145
|
+
"default": DRP.start_averaging,
|
|
13
146
|
},
|
|
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
147
|
"n_mean": {
|
|
24
148
|
"help": "Spatial average frequency (number of iterations)",
|
|
25
|
-
"default":
|
|
149
|
+
"default": DRP.n_mean,
|
|
26
150
|
},
|
|
27
151
|
"n_profile": {
|
|
28
152
|
"help": "x-profile save frequency (number of iterations)",
|
|
29
|
-
"default":
|
|
153
|
+
"default": DRP.n_profile,
|
|
30
154
|
},
|
|
31
155
|
"solver": {
|
|
32
156
|
"help": "Solver to use for the simulation",
|
|
33
|
-
"default":
|
|
34
|
-
"choices":
|
|
157
|
+
"default": DRP.solver,
|
|
158
|
+
"choices": lit_to_list(SolverType),
|
|
35
159
|
},
|
|
36
160
|
"precision": {
|
|
37
|
-
"help": "Precision of the
|
|
38
|
-
"default":
|
|
39
|
-
"choices":
|
|
161
|
+
"help": "Precision of the floating point (single or double)",
|
|
162
|
+
"default": DRP.precision,
|
|
163
|
+
"choices": lit_to_list(PrecisionType),
|
|
40
164
|
},
|
|
41
165
|
"blocking": {
|
|
42
|
-
"help": "Use blocking communications",
|
|
43
|
-
"default":
|
|
166
|
+
"help": "Use blocking communications (MPI solver only)",
|
|
167
|
+
"default": DRP.blocking,
|
|
44
168
|
"action": "store_true",
|
|
45
169
|
},
|
|
46
170
|
"seed": {
|
|
47
171
|
"help": "Random seed for temperature fluctuations",
|
|
48
|
-
"default":
|
|
172
|
+
"default": DRP.seed,
|
|
49
173
|
"type": int,
|
|
50
174
|
},
|
|
51
175
|
"device": {
|
|
52
|
-
"help": "Device to use
|
|
53
|
-
"default":
|
|
54
|
-
"
|
|
176
|
+
"help": "Device to use by the OpenCL solver",
|
|
177
|
+
"default": DRP.device,
|
|
178
|
+
"choices": lit_to_list(DeviceType),
|
|
55
179
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
180
|
+
"profiling": {
|
|
181
|
+
"help": "Enable profiling output (internal profiler)",
|
|
182
|
+
"default": DRP.profiling,
|
|
183
|
+
"action": "store_true",
|
|
184
|
+
},
|
|
185
|
+
} #: simulation CLI parameters
|
llg3d/post/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""Post-processing tools
|
|
1
|
+
"""Post-processing tools."""
|
llg3d/post/extract.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extract scalar values from .npz result files.
|
|
3
|
+
|
|
4
|
+
The extraction paths correspond to the hierarchical structure returned by
|
|
5
|
+
`load_results()` (see io.py), where metrics and observables are JSON-decoded
|
|
6
|
+
and stored under the 'results' key.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
|
|
10
|
+
.. code-block:: sh
|
|
11
|
+
|
|
12
|
+
llg3d.extract run.npz params/np results/metrics/total_time
|
|
13
|
+
|
|
14
|
+
This will extract the number of processes and total simulation time from the run.npz file.
|
|
15
|
+
Paths are separated by '/' and navigate through nested dictionaries and object attributes.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from llg3d.io import RunResults, load_results
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _navigate(value: object, levels: list[str]):
|
|
27
|
+
"""
|
|
28
|
+
Recursively navigate through nested structures using keys.
|
|
29
|
+
|
|
30
|
+
Tries subscript access (dicts/arrays) first, then attribute access (objects).
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
value: The current value to navigate.
|
|
34
|
+
levels: List of keys/attributes to navigate through.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The final value after navigating through all levels.
|
|
38
|
+
"""
|
|
39
|
+
if not levels:
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
k = levels[0]
|
|
43
|
+
try:
|
|
44
|
+
# Try dict/array subscript access
|
|
45
|
+
next_value = value[k] # type: ignore
|
|
46
|
+
except (TypeError, KeyError, IndexError):
|
|
47
|
+
# Fallback to attribute access
|
|
48
|
+
next_value = getattr(value, k)
|
|
49
|
+
|
|
50
|
+
return _navigate(next_value, levels[1:])
|
|
51
|
+
|
|
52
|
+
ExtractedValue = float | int | str | bool | np.integer | np.floating
|
|
53
|
+
|
|
54
|
+
def extract_values(npz_file: Path, *keys: str) -> list[ExtractedValue]:
|
|
55
|
+
"""
|
|
56
|
+
Extract scalar values from a .npz file.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
npz_file: Path to the .npz result file
|
|
60
|
+
*keys: tuple of keys of the scalar values to extract
|
|
61
|
+
(slash-separated for nested keys)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The list of extracted scalar values
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If one of the extracted values is not a scalar
|
|
68
|
+
"""
|
|
69
|
+
results: RunResults = load_results(npz_file)
|
|
70
|
+
values: list[ExtractedValue] = []
|
|
71
|
+
for key in keys:
|
|
72
|
+
# Split path into levels
|
|
73
|
+
levels: list[str] = key.split("/")
|
|
74
|
+
raw_value = _navigate(results, levels)
|
|
75
|
+
|
|
76
|
+
# Handle numpy scalars and arrays with shape ()
|
|
77
|
+
if isinstance(raw_value, np.ndarray):
|
|
78
|
+
if raw_value.shape != ():
|
|
79
|
+
raise ValueError(f"Extracted value for key '{key}' is not a scalar.")
|
|
80
|
+
value = raw_value.item() # Convert numpy scalar to Python scalar
|
|
81
|
+
else:
|
|
82
|
+
value = raw_value
|
|
83
|
+
|
|
84
|
+
if not isinstance(value, (float, int, str, bool, np.integer, np.floating)):
|
|
85
|
+
raise ValueError(f"Extracted value for key '{key}' is not a scalar.")
|
|
86
|
+
values.append(value)
|
|
87
|
+
return values
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main(): # pragma: no cover
|
|
91
|
+
"""Parse command line arguments and print simulation info."""
|
|
92
|
+
parser = argparse.ArgumentParser(
|
|
93
|
+
description="Extract scalar values from .npz result files.",
|
|
94
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument("filename", help="Path to the .npz result file", type=Path)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"keys",
|
|
99
|
+
nargs="+",
|
|
100
|
+
help="Key(s) of the scalar value(s) to extract (slash-separated for nested keys)",
|
|
101
|
+
)
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
|
|
104
|
+
values = extract_values(args.filename, *args.keys)
|
|
105
|
+
print(" ".join(map(str, values)))
|
llg3d/post/info.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Dump simulation results from a .npz file."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import textwrap
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..io import Metrics, Records, RunResults, format_profiling_table, load_results
|
|
10
|
+
|
|
11
|
+
INDENT = 4 * " "
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_array_memory(arr: np.ndarray) -> str:
|
|
15
|
+
"""Return a human-readable string of the array memory size."""
|
|
16
|
+
nbytes: float | int = arr.nbytes
|
|
17
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
18
|
+
if nbytes < 1024.0:
|
|
19
|
+
return f"{nbytes:.2f} {unit}"
|
|
20
|
+
nbytes /= 1024.0
|
|
21
|
+
return f"{nbytes:.2f} PB"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def summarize_array(arr: np.ndarray) -> str:
|
|
25
|
+
"""Return a summary of the array."""
|
|
26
|
+
s = f"""shape: {arr.shape}
|
|
27
|
+
dtype: {arr.dtype}
|
|
28
|
+
min: {arr.min():.6e}, max: {arr.max():.6e}
|
|
29
|
+
mean: {arr.mean():.6e}, std: {arr.std():.6e}
|
|
30
|
+
memory: {get_array_memory(arr)}
|
|
31
|
+
"""
|
|
32
|
+
s += np.array2string(
|
|
33
|
+
arr,
|
|
34
|
+
max_line_width=120,
|
|
35
|
+
threshold=50,
|
|
36
|
+
edgeitems=2,
|
|
37
|
+
formatter={"float": lambda x: f"{x:.3e}"},
|
|
38
|
+
)
|
|
39
|
+
return textwrap.indent(s, INDENT)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def dict_to_str(d: dict) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Convert a dictionary to a formatted string.
|
|
45
|
+
|
|
46
|
+
Right-aligns the values for better readability.
|
|
47
|
+
Skips empty dicts and empty lists.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
d: The dictionary to convert.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A formatted string representation of the dictionary.
|
|
54
|
+
"""
|
|
55
|
+
# Filter out empty dicts and empty lists
|
|
56
|
+
filtered = {
|
|
57
|
+
k: v for k, v in d.items() if not (isinstance(v, (dict, list)) and not v)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if not filtered:
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
lines = []
|
|
64
|
+
max_key_length = max(len(str(key)) for key in filtered.keys())
|
|
65
|
+
for key, value in filtered.items():
|
|
66
|
+
lines.append(f"{key}: {' ' * (max_key_length - len(str(key)))}{value}")
|
|
67
|
+
return textwrap.indent("\n".join(lines), INDENT)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _format_array(arr: np.ndarray, verbose: bool) -> str:
|
|
71
|
+
"""Format an array depending on verbosity."""
|
|
72
|
+
if verbose:
|
|
73
|
+
return summarize_array(arr)
|
|
74
|
+
else:
|
|
75
|
+
return textwrap.indent(f"shape: {arr.shape}, dtype: {arr.dtype}", INDENT)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _format_records(
|
|
79
|
+
records: Records | dict, verbose: bool, indent: int = 0
|
|
80
|
+
) -> list[str]:
|
|
81
|
+
"""Recursively format records section (nested dicts/arrays)."""
|
|
82
|
+
lines: list[str] = []
|
|
83
|
+
prefix = " " * indent
|
|
84
|
+
for key, value in records.items():
|
|
85
|
+
if isinstance(value, dict):
|
|
86
|
+
# Nested dict: recurse
|
|
87
|
+
lines.append(f"{prefix}{key}")
|
|
88
|
+
lines.extend(_format_records(value, verbose, indent + 4))
|
|
89
|
+
elif isinstance(value, np.ndarray):
|
|
90
|
+
# Numpy array: format accordingly
|
|
91
|
+
lines.append(f"{prefix}{key}")
|
|
92
|
+
lines.append(
|
|
93
|
+
textwrap.indent(_format_array(value, verbose), prefix + " " * 4)
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
# Other types: just print key and value
|
|
97
|
+
lines.append(f"{prefix}{key}: {value}")
|
|
98
|
+
return lines
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _format_metrics(metrics: Metrics) -> list[str]:
|
|
102
|
+
"""
|
|
103
|
+
Format metrics data, handling profiling_stats specially.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
metrics: Dictionary containing metrics data
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of formatted lines to append
|
|
110
|
+
"""
|
|
111
|
+
lines = []
|
|
112
|
+
|
|
113
|
+
# Extract metrics without profiling_stats
|
|
114
|
+
metrics_noprof = {k: v for k, v in metrics.items() if k != "profiling_stats"}
|
|
115
|
+
formatted = dict_to_str(metrics_noprof)
|
|
116
|
+
lines.append(formatted)
|
|
117
|
+
|
|
118
|
+
# Format profiling_stats as a table if present
|
|
119
|
+
if "profiling_stats" in metrics and metrics["profiling_stats"]:
|
|
120
|
+
lines.append("profiling_stats")
|
|
121
|
+
total_time = metrics.get("total_time")
|
|
122
|
+
lines.append(
|
|
123
|
+
textwrap.indent(
|
|
124
|
+
format_profiling_table(metrics["profiling_stats"], total_time), INDENT
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return lines
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_info(filename: Path, verbose: bool = False) -> str:
|
|
132
|
+
"""
|
|
133
|
+
Returns the simulation parameters and results from a .npz file.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
filename: Path to the .npz result file
|
|
137
|
+
verbose: If True, includes detailed array information
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A formatted string with simulation information
|
|
141
|
+
"""
|
|
142
|
+
run: RunResults = load_results(filename)
|
|
143
|
+
|
|
144
|
+
lines: list[str] = []
|
|
145
|
+
|
|
146
|
+
# Params
|
|
147
|
+
lines.append("params")
|
|
148
|
+
lines.append(dict_to_str(run.params.as_dict()))
|
|
149
|
+
|
|
150
|
+
# Metrics
|
|
151
|
+
lines.append("results/metrics")
|
|
152
|
+
lines.extend(_format_metrics(run.results["metrics"]))
|
|
153
|
+
|
|
154
|
+
# Observables
|
|
155
|
+
if "observables" in run.results:
|
|
156
|
+
observables = run.results["observables"]
|
|
157
|
+
lines.append("results/observables")
|
|
158
|
+
lines.append(dict_to_str(dict(observables)))
|
|
159
|
+
|
|
160
|
+
# Records
|
|
161
|
+
if "records" in run.results:
|
|
162
|
+
records = run.results["records"]
|
|
163
|
+
lines.append("results/records")
|
|
164
|
+
lines.extend(_format_records(records, verbose))
|
|
165
|
+
|
|
166
|
+
return "\n".join(lines)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main(): # pragma: no cover
|
|
170
|
+
"""Parse command line arguments and print simulation info."""
|
|
171
|
+
parser = argparse.ArgumentParser(
|
|
172
|
+
description=__doc__,
|
|
173
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
174
|
+
)
|
|
175
|
+
parser.add_argument("filename", help="Path to the .npz result file", type=Path)
|
|
176
|
+
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
|
177
|
+
args = parser.parse_args()
|
|
178
|
+
print(get_info(args.filename, args.verbose))
|
llg3d/post/m1_vs_T.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Plot the magnetization vs temperature and determine the Curie temperature."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import matplotlib
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
|
|
8
|
+
from .process import MagTempData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def plot_m1_vs_T(
|
|
12
|
+
mag_data: MagTempData,
|
|
13
|
+
image_filepath: Path | str | None = None,
|
|
14
|
+
show: bool = False,
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
Plots the data (T, <m_1>).
|
|
18
|
+
|
|
19
|
+
Interpolates the values, calculates the Curie temperature, exports to PNG.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
mag_data: Magnetization data object
|
|
23
|
+
image_filepath: Path to save the image
|
|
24
|
+
if None, the image will not be saved.
|
|
25
|
+
show: display the graph in a graphical window
|
|
26
|
+
"""
|
|
27
|
+
print(f"T_Curie = {mag_data.T_Curie:.0f} K")
|
|
28
|
+
if not show:
|
|
29
|
+
matplotlib.use("Agg") # Use non-interactive backend
|
|
30
|
+
|
|
31
|
+
fig, ax = plt.subplots()
|
|
32
|
+
fig.suptitle("Average magnetization vs Temperature")
|
|
33
|
+
params = mag_data.params
|
|
34
|
+
ax.set_title(
|
|
35
|
+
params["element"]
|
|
36
|
+
+ rf", ${params['Jx']}\times{params['Jy']}\times{params['Jz']}$"
|
|
37
|
+
rf" ($dx = ${params['dx']})",
|
|
38
|
+
fontdict={"size": 10},
|
|
39
|
+
)
|
|
40
|
+
ax.plot(
|
|
41
|
+
mag_data.temperature,
|
|
42
|
+
mag_data.m1_mean,
|
|
43
|
+
"o",
|
|
44
|
+
markerfacecolor="white",
|
|
45
|
+
label=f"{params['solver']} computations",
|
|
46
|
+
)
|
|
47
|
+
ax.plot(
|
|
48
|
+
mag_data.temperature_interp,
|
|
49
|
+
mag_data.interp(mag_data.temperature_interp),
|
|
50
|
+
label="interpolation (PCHIP)",
|
|
51
|
+
)
|
|
52
|
+
ax.annotate(
|
|
53
|
+
"$T_{{Curie}} = {:.0f} K$".format(mag_data.T_Curie),
|
|
54
|
+
xy=(mag_data.T_Curie, 0.1),
|
|
55
|
+
xytext=(mag_data.T_Curie + 20, 0.1 + 0.01),
|
|
56
|
+
)
|
|
57
|
+
# Draw lines to indicate T_Curie
|
|
58
|
+
ax_ymin = ax.get_ylim()[0]
|
|
59
|
+
ax_ymax = ax.get_ylim()[1]
|
|
60
|
+
y_T_Curie = (0.1 - ax_ymin) / (ax_ymax - ax_ymin)
|
|
61
|
+
ax.axvline(x=mag_data.T_Curie, ymax=y_T_Curie, color="k", linestyle="--")
|
|
62
|
+
ax_xmin = ax.get_xlim()[0]
|
|
63
|
+
x_T_Curie = (mag_data.T_Curie - ax_xmin) / (ax.get_xlim()[1] - ax_xmin)
|
|
64
|
+
ax.axhline(y=0.1, xmax=x_T_Curie, color="k", linestyle="--")
|
|
65
|
+
# Mark the Curie point
|
|
66
|
+
ax.plot(mag_data.T_Curie, 0.1, color="k", marker="s", markerfacecolor="white")
|
|
67
|
+
ax.set_xlabel("Temperature [K]")
|
|
68
|
+
ax.set_ylabel("Magnetization")
|
|
69
|
+
ax.grid()
|
|
70
|
+
ax.legend()
|
|
71
|
+
|
|
72
|
+
if show:
|
|
73
|
+
plt.show()
|
|
74
|
+
|
|
75
|
+
if image_filepath is not None:
|
|
76
|
+
fig.savefig(image_filepath, dpi=300)
|
|
77
|
+
print(f"Written to {image_filepath}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main(): # pragma: no cover
|
|
81
|
+
"""Parses the command line to execute processing functions."""
|
|
82
|
+
from .utils import get_cli_args
|
|
83
|
+
|
|
84
|
+
args = get_cli_args(
|
|
85
|
+
description=__doc__,
|
|
86
|
+
default_image_filepath=Path("m1_vs_T.png"),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
mag_data = MagTempData(*args.files)
|
|
90
|
+
plot_m1_vs_T(mag_data, image_filepath=args.image_filepath, show=args.show)
|
llg3d/post/m1_vs_time.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Plot m1 vs time from one or more result files."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from matplotlib import pyplot as plt
|
|
7
|
+
|
|
8
|
+
from ..io import RunResults, load_results
|
|
9
|
+
from .utils import get_cli_args
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def plot_m1_vs_time(
|
|
13
|
+
*files: Path, image_filepath: Path | str | None = None, show: bool = False
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Plot the results from the given files.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
*files: Paths to the result files.
|
|
20
|
+
image_filepath: Path to the output image file.
|
|
21
|
+
if None, the image will not be saved.
|
|
22
|
+
show: display the graph in a graphical window.
|
|
23
|
+
"""
|
|
24
|
+
fig, ax = plt.subplots()
|
|
25
|
+
for file in files:
|
|
26
|
+
print(f"Processing file: {file}")
|
|
27
|
+
result: RunResults = load_results(file)
|
|
28
|
+
xyz_average: np.ndarray = np.array(result.get_record("xyz_average"))
|
|
29
|
+
ax.plot(xyz_average[:, 0], xyz_average[:, 1], label=file)
|
|
30
|
+
|
|
31
|
+
ax.set_xlabel("time")
|
|
32
|
+
ax.set_ylabel(r"$<m_1>$")
|
|
33
|
+
ax.grid()
|
|
34
|
+
ax.legend()
|
|
35
|
+
ax.set_title(r"Space average of $m_1$ according to time")
|
|
36
|
+
|
|
37
|
+
if show:
|
|
38
|
+
plt.show()
|
|
39
|
+
|
|
40
|
+
if image_filepath is not None:
|
|
41
|
+
fig.savefig(image_filepath, dpi=300)
|
|
42
|
+
print(f"Written to {image_filepath}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main(): # pragma: no cover
|
|
46
|
+
"""Parse CLI arguments and call the plot function."""
|
|
47
|
+
args = get_cli_args(
|
|
48
|
+
description=__doc__,
|
|
49
|
+
default_image_filepath=Path("m1_vs_time.png"),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
plot_m1_vs_time(
|
|
53
|
+
*args.files,
|
|
54
|
+
image_filepath=args.image_filepath,
|
|
55
|
+
show=args.show,
|
|
56
|
+
)
|