llg3d 2.0.1__py3-none-any.whl → 3.1.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 (46) hide show
  1. llg3d/__init__.py +2 -4
  2. llg3d/benchmarks/__init__.py +1 -0
  3. llg3d/benchmarks/compare_commits.py +321 -0
  4. llg3d/benchmarks/efficiency.py +451 -0
  5. llg3d/benchmarks/utils.py +25 -0
  6. llg3d/element.py +98 -17
  7. llg3d/grid.py +48 -58
  8. llg3d/io.py +395 -0
  9. llg3d/main.py +32 -35
  10. llg3d/parameters.py +159 -49
  11. llg3d/post/__init__.py +1 -1
  12. llg3d/post/extract.py +112 -0
  13. llg3d/post/info.py +192 -0
  14. llg3d/post/m1_vs_T.py +107 -0
  15. llg3d/post/m1_vs_time.py +81 -0
  16. llg3d/post/process.py +87 -85
  17. llg3d/post/utils.py +38 -0
  18. llg3d/post/x_profiles.py +161 -0
  19. llg3d/py.typed +1 -0
  20. llg3d/solvers/__init__.py +153 -0
  21. llg3d/solvers/base.py +345 -0
  22. llg3d/solvers/experimental/__init__.py +9 -0
  23. llg3d/{solver → solvers/experimental}/jax.py +117 -143
  24. llg3d/solvers/math_utils.py +41 -0
  25. llg3d/solvers/mpi.py +370 -0
  26. llg3d/solvers/numpy.py +126 -0
  27. llg3d/solvers/opencl.py +439 -0
  28. llg3d/solvers/profiling.py +38 -0
  29. {llg3d-2.0.1.dist-info → llg3d-3.1.0.dist-info}/METADATA +5 -2
  30. llg3d-3.1.0.dist-info/RECORD +36 -0
  31. {llg3d-2.0.1.dist-info → llg3d-3.1.0.dist-info}/WHEEL +1 -1
  32. llg3d-3.1.0.dist-info/entry_points.txt +9 -0
  33. llg3d/output.py +0 -107
  34. llg3d/post/plot_results.py +0 -61
  35. llg3d/post/temperature.py +0 -76
  36. llg3d/simulation.py +0 -95
  37. llg3d/solver/__init__.py +0 -45
  38. llg3d/solver/mpi.py +0 -450
  39. llg3d/solver/numpy.py +0 -207
  40. llg3d/solver/opencl.py +0 -330
  41. llg3d/solver/solver.py +0 -89
  42. llg3d-2.0.1.dist-info/RECORD +0 -25
  43. llg3d-2.0.1.dist-info/entry_points.txt +0 -4
  44. {llg3d-2.0.1.dist-info → llg3d-3.1.0.dist-info}/licenses/AUTHORS +0 -0
  45. {llg3d-2.0.1.dist-info → llg3d-3.1.0.dist-info}/licenses/LICENSE +0 -0
  46. {llg3d-2.0.1.dist-info → llg3d-3.1.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
- parameters: Dictionary of parameters parsed by argparse
65
-
66
- Returns:
67
- 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,112 @@
1
+ """
2
+ Extract scalar values from .npz result files.
3
+
4
+ Use the ``llg3d.extract`` command line tool to extract scalar values from .npz result
5
+ files:
6
+
7
+ .. command-output:: llg3d.extract --help
8
+
9
+
10
+ Extract the total execution time from a ``run.npz`` file:
11
+
12
+ .. command-output:: llg3d.extract run.npz results/metrics/total_time
13
+ :cwd: ../execute
14
+
15
+
16
+ Extract both the total execution time and the time per iteration:
17
+
18
+ .. command-output:: llg3d.extract run.npz results/metrics/total_time \
19
+ results/metrics/time_per_ite
20
+ :cwd: ../execute
21
+ """
22
+
23
+ import argparse
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+
28
+ from llg3d.io import RunResults, load_results
29
+
30
+
31
+ def _navigate(value: object, levels: list[str]):
32
+ """
33
+ Recursively navigate through nested structures using keys.
34
+
35
+ Tries subscript access (dicts/arrays) first, then attribute access (objects).
36
+
37
+ Args:
38
+ value: The current value to navigate.
39
+ levels: List of keys/attributes to navigate through.
40
+
41
+ Returns:
42
+ The final value after navigating through all levels.
43
+ """
44
+ if not levels:
45
+ return value
46
+
47
+ k = levels[0]
48
+ try:
49
+ # Try dict/array subscript access
50
+ next_value = value[k] # type: ignore
51
+ except (TypeError, KeyError, IndexError):
52
+ # Fallback to attribute access
53
+ next_value = getattr(value, k)
54
+
55
+ return _navigate(next_value, levels[1:])
56
+
57
+
58
+ ExtractedValue = float | int | str | bool | np.integer | np.floating
59
+
60
+
61
+ def extract_values(npz_file: Path, *keys: str) -> list[ExtractedValue]:
62
+ """
63
+ Extract scalar values from a .npz file.
64
+
65
+ Args:
66
+ npz_file: Path to the .npz result file
67
+ *keys: tuple of keys of the scalar values to extract
68
+ (slash-separated for nested keys)
69
+
70
+ Returns:
71
+ The list of extracted scalar values
72
+
73
+ Raises:
74
+ ValueError: If one of the extracted values is not a scalar
75
+ """
76
+ results: RunResults = load_results(npz_file)
77
+ values: list[ExtractedValue] = []
78
+ for key in keys:
79
+ # Split path into levels
80
+ levels: list[str] = key.split("/")
81
+ raw_value = _navigate(results, levels)
82
+
83
+ # Handle numpy scalars and arrays with shape ()
84
+ if isinstance(raw_value, np.ndarray):
85
+ if raw_value.shape != ():
86
+ raise ValueError(f"Extracted value for key '{key}' is not a scalar.")
87
+ value = raw_value.item() # Convert numpy scalar to Python scalar
88
+ else:
89
+ value = raw_value
90
+
91
+ if not isinstance(value, (float, int, str, bool, np.integer, np.floating)):
92
+ raise ValueError(f"Extracted value for key '{key}' is not a scalar.")
93
+ values.append(value)
94
+ return values
95
+
96
+
97
+ def main(): # pragma: no cover
98
+ """Parse command line arguments and print simulation info."""
99
+ parser = argparse.ArgumentParser(
100
+ description="Extract scalar values from .npz result files.",
101
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
102
+ )
103
+ parser.add_argument("filename", help="Path to the .npz result file", type=Path)
104
+ parser.add_argument(
105
+ "keys",
106
+ nargs="+",
107
+ help="Key(s) of the scalar value(s) to extract (slash-separated for nested keys)",
108
+ )
109
+ args = parser.parse_args()
110
+
111
+ values = extract_values(args.filename, *args.keys)
112
+ print(" ".join(map(str, values)))
llg3d/post/info.py ADDED
@@ -0,0 +1,192 @@
1
+ """
2
+ Dump simulation results from a .npz file.
3
+
4
+ To browse the content of the ``run.npz`` file, use the ``llg3d.info`` command:
5
+
6
+ .. command-output:: llg3d.info run.npz
7
+ :cwd: ../execute/
8
+
9
+ The numpy arrays cans be previewed with more detailed information using the ``--verbose``
10
+ option:
11
+
12
+ .. command-output:: llg3d.info run.npz --verbose
13
+ :cwd: ../execute/
14
+
15
+ """
16
+
17
+ import argparse
18
+ import textwrap
19
+ from pathlib import Path
20
+
21
+ import numpy as np
22
+
23
+ from ..io import Metrics, Records, RunResults, format_profiling_table, load_results
24
+
25
+ INDENT = 4 * " "
26
+
27
+
28
+ def get_array_memory(arr: np.ndarray) -> str:
29
+ """Return a human-readable string of the array memory size."""
30
+ nbytes: float | int = arr.nbytes
31
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
32
+ if nbytes < 1024.0:
33
+ return f"{nbytes:.2f} {unit}"
34
+ nbytes /= 1024.0
35
+ return f"{nbytes:.2f} PB"
36
+
37
+
38
+ def summarize_array(arr: np.ndarray) -> str:
39
+ """Return a summary of the array."""
40
+ s = f"""shape: {arr.shape}
41
+ dtype: {arr.dtype}
42
+ min: {arr.min():.6e}, max: {arr.max():.6e}
43
+ mean: {arr.mean():.6e}, std: {arr.std():.6e}
44
+ memory: {get_array_memory(arr)}
45
+ """
46
+ s += np.array2string(
47
+ arr,
48
+ max_line_width=120,
49
+ threshold=50,
50
+ edgeitems=2,
51
+ formatter={"float": lambda x: f"{x:.3e}"},
52
+ )
53
+ return textwrap.indent(s, INDENT)
54
+
55
+
56
+ def dict_to_str(d: dict) -> str:
57
+ """
58
+ Convert a dictionary to a formatted string.
59
+
60
+ Right-aligns the values for better readability.
61
+ Skips empty dicts and empty lists.
62
+
63
+ Args:
64
+ d: The dictionary to convert.
65
+
66
+ Returns:
67
+ A formatted string representation of the dictionary.
68
+ """
69
+ # Filter out empty dicts and empty lists
70
+ filtered = {
71
+ k: v for k, v in d.items() if not (isinstance(v, (dict, list)) and not v)
72
+ }
73
+
74
+ if not filtered:
75
+ return ""
76
+
77
+ lines = []
78
+ max_key_length = max(len(str(key)) for key in filtered.keys())
79
+ for key, value in filtered.items():
80
+ lines.append(f"{key}: {' ' * (max_key_length - len(str(key)))}{value}")
81
+ return textwrap.indent("\n".join(lines), INDENT)
82
+
83
+
84
+ def _format_array(arr: np.ndarray, verbose: bool) -> str:
85
+ """Format an array depending on verbosity."""
86
+ if verbose:
87
+ return summarize_array(arr)
88
+ else:
89
+ return textwrap.indent(f"shape: {arr.shape}, dtype: {arr.dtype}", INDENT)
90
+
91
+
92
+ def _format_records(
93
+ records: Records | dict, verbose: bool, indent: int = 0
94
+ ) -> list[str]:
95
+ """Recursively format records section (nested dicts/arrays)."""
96
+ lines: list[str] = []
97
+ prefix = " " * indent
98
+ for key, value in records.items():
99
+ if isinstance(value, dict):
100
+ # Nested dict: recurse
101
+ lines.append(f"{prefix}{key}")
102
+ lines.extend(_format_records(value, verbose, indent + 4))
103
+ elif isinstance(value, np.ndarray):
104
+ # Numpy array: format accordingly
105
+ lines.append(f"{prefix}{key}")
106
+ lines.append(
107
+ textwrap.indent(_format_array(value, verbose), prefix + " " * 4)
108
+ )
109
+ else:
110
+ # Other types: just print key and value
111
+ lines.append(f"{prefix}{key}: {value}")
112
+ return lines
113
+
114
+
115
+ def _format_metrics(metrics: Metrics) -> list[str]:
116
+ """
117
+ Format metrics data, handling profiling_stats specially.
118
+
119
+ Args:
120
+ metrics: Dictionary containing metrics data
121
+
122
+ Returns:
123
+ List of formatted lines to append
124
+ """
125
+ lines = []
126
+
127
+ # Extract metrics without profiling_stats
128
+ metrics_noprof = {k: v for k, v in metrics.items() if k != "profiling_stats"}
129
+ formatted = dict_to_str(metrics_noprof)
130
+ lines.append(formatted)
131
+
132
+ # Format profiling_stats as a table if present
133
+ if "profiling_stats" in metrics and metrics["profiling_stats"]:
134
+ lines.append("profiling_stats")
135
+ total_time = metrics.get("total_time")
136
+ lines.append(
137
+ textwrap.indent(
138
+ format_profiling_table(metrics["profiling_stats"], total_time), INDENT
139
+ )
140
+ )
141
+
142
+ return lines
143
+
144
+
145
+ def get_info(filename: Path, verbose: bool = False) -> str:
146
+ """
147
+ Returns the simulation parameters and results from a .npz file.
148
+
149
+ Args:
150
+ filename: Path to the .npz result file
151
+ verbose: If True, includes detailed array information
152
+
153
+ Returns:
154
+ A formatted string with simulation information
155
+ """
156
+ run: RunResults = load_results(filename)
157
+
158
+ lines: list[str] = []
159
+
160
+ # Params
161
+ lines.append("params")
162
+ lines.append(dict_to_str(run.params.as_dict()))
163
+
164
+ # Metrics
165
+ lines.append("results/metrics")
166
+ lines.extend(_format_metrics(run.results["metrics"]))
167
+
168
+ # Observables
169
+ if "observables" in run.results:
170
+ observables = run.results["observables"]
171
+ lines.append("results/observables")
172
+ lines.append(dict_to_str(dict(observables)))
173
+
174
+ # Records
175
+ if "records" in run.results:
176
+ records = run.results["records"]
177
+ lines.append("results/records")
178
+ lines.extend(_format_records(records, verbose))
179
+
180
+ return "\n".join(lines)
181
+
182
+
183
+ def main(): # pragma: no cover
184
+ """Parse command line arguments and print simulation info."""
185
+ parser = argparse.ArgumentParser(
186
+ description=__doc__,
187
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
188
+ )
189
+ parser.add_argument("filename", help="Path to the .npz result file", type=Path)
190
+ parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
191
+ args = parser.parse_args()
192
+ print(get_info(args.filename, args.verbose))
llg3d/post/m1_vs_T.py ADDED
@@ -0,0 +1,107 @@
1
+ """
2
+ Plot the magnetization vs temperature and determine the Curie temperature.
3
+
4
+ Use the ``llg3d.m1_vs_T`` command line tool to plot the average magnetization versus
5
+ temperature from multiple result files:
6
+
7
+ .. command-output:: llg3d.m1_vs_T -h
8
+ :cwd: ../execute/temperatures
9
+
10
+ When calling the tool on a selection of result files:
11
+
12
+ .. command-output:: llg3d.m1_vs_T run_*.npz -i m1_vs_T.png
13
+ :cwd: ../execute/temperatures
14
+ :shell:
15
+
16
+ .. image:: ../execute/temperatures/m1_vs_T.png
17
+ :alt: Magnetization versus temperature for multiple files
18
+ """
19
+
20
+ from pathlib import Path
21
+
22
+ import matplotlib
23
+ import matplotlib.pyplot as plt
24
+
25
+ from .process import MagTempData
26
+
27
+
28
+ def plot_m1_vs_T(
29
+ mag_data: MagTempData,
30
+ image_filepath: Path | str | None = None,
31
+ show: bool = False,
32
+ ):
33
+ """
34
+ Plots the data (T, <m_1>).
35
+
36
+ Interpolates the values, calculates the Curie temperature, exports to PNG.
37
+
38
+ Args:
39
+ mag_data: Magnetization data object
40
+ image_filepath: Path to save the image
41
+ if None, the image will not be saved.
42
+ show: display the graph in a graphical window
43
+ """
44
+ print(f"T_Curie = {mag_data.T_Curie:.0f} K")
45
+ if not show:
46
+ matplotlib.use("Agg") # Use non-interactive backend
47
+
48
+ fig, ax = plt.subplots()
49
+ fig.suptitle("Average magnetization vs Temperature")
50
+ params = mag_data.params
51
+ ax.set_title(
52
+ params["element"]
53
+ + rf", ${params['Jx']}\times{params['Jy']}\times{params['Jz']}$"
54
+ rf" ($dx = ${params['dx']})",
55
+ fontdict={"size": 10},
56
+ )
57
+ ax.plot(
58
+ mag_data.temperature,
59
+ mag_data.m1_mean,
60
+ "o",
61
+ markerfacecolor="white",
62
+ label=f"{params['solver']} computations",
63
+ )
64
+ ax.plot(
65
+ mag_data.temperature_interp,
66
+ mag_data.interp(mag_data.temperature_interp),
67
+ label="interpolation (PCHIP)",
68
+ )
69
+ ax.annotate(
70
+ "$T_{{Curie}} = {:.0f} K$".format(mag_data.T_Curie),
71
+ xy=(mag_data.T_Curie, 0.1),
72
+ xytext=(mag_data.T_Curie + 20, 0.1 + 0.01),
73
+ )
74
+ # Draw lines to indicate T_Curie
75
+ ax_ymin = ax.get_ylim()[0]
76
+ ax_ymax = ax.get_ylim()[1]
77
+ y_T_Curie = (0.1 - ax_ymin) / (ax_ymax - ax_ymin)
78
+ ax.axvline(x=mag_data.T_Curie, ymax=y_T_Curie, color="k", linestyle="--")
79
+ ax_xmin = ax.get_xlim()[0]
80
+ x_T_Curie = (mag_data.T_Curie - ax_xmin) / (ax.get_xlim()[1] - ax_xmin)
81
+ ax.axhline(y=0.1, xmax=x_T_Curie, color="k", linestyle="--")
82
+ # Mark the Curie point
83
+ ax.plot(mag_data.T_Curie, 0.1, color="k", marker="s", markerfacecolor="white")
84
+ ax.set_xlabel("Temperature [K]")
85
+ ax.set_ylabel("Magnetization")
86
+ ax.grid()
87
+ ax.legend()
88
+
89
+ if show:
90
+ plt.show()
91
+
92
+ if image_filepath is not None:
93
+ fig.savefig(image_filepath, dpi=300)
94
+ print(f"Written to {image_filepath}")
95
+
96
+
97
+ def main(): # pragma: no cover
98
+ """Parses the command line to execute processing functions."""
99
+ from .utils import get_cli_args
100
+
101
+ args = get_cli_args(
102
+ description="Plot the magnetization vs temperature and determine the Curie temperature.",
103
+ default_image_filepath=Path("m1_vs_T.png"),
104
+ )
105
+
106
+ mag_data = MagTempData(*args.files)
107
+ plot_m1_vs_T(mag_data, image_filepath=args.image_filepath, show=args.show)