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/parameters.py CHANGED
@@ -1,75 +1,185 @@
1
- """Parameters for the LLG3D simulation."""
1
+ """Parameters for the simulation."""
2
2
 
3
- import numpy as np
3
+ from argparse import Action
4
+ from dataclasses import asdict, dataclass, fields
5
+ from typing import Any, Literal, NotRequired, TypedDict
4
6
 
5
- # Parameters: default value and description
6
- Parameter = dict[str, str | int | float | bool] #: Type for a parameter
7
+ from . import solvers
7
8
 
8
- parameters: dict[str, Parameter] = {
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": "Cobalt",
12
- "choices": ["Cobalt", "Iron", "Nickel"],
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": 1,
149
+ "default": DRP.n_mean,
26
150
  },
27
151
  "n_profile": {
28
152
  "help": "x-profile save frequency (number of iterations)",
29
- "default": 0,
153
+ "default": DRP.n_profile,
30
154
  },
31
155
  "solver": {
32
156
  "help": "Solver to use for the simulation",
33
- "default": "numpy",
34
- "choices": ["opencl", "mpi", "numpy", "jax"],
157
+ "default": DRP.solver,
158
+ "choices": lit_to_list(SolverType),
35
159
  },
36
160
  "precision": {
37
- "help": "Precision of the simulation (single or double)",
38
- "default": "double",
39
- "choices": ["single", "double"],
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": False,
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": 12345,
172
+ "default": DRP.seed,
49
173
  "type": int,
50
174
  },
51
175
  "device": {
52
- "help": "Device to use ('cpu', 'gpu', or 'auto')",
53
- "default": "auto",
54
- "type": str,
176
+ "help": "Device to use by the OpenCL solver",
177
+ "default": DRP.device,
178
+ "choices": lit_to_list(DeviceType),
55
179
  },
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
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 for LLG3D."""
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)
@@ -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
+ )