nmag-python-3 0.0.2__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 (147) hide show
  1. anisotropy/__init__.py +28 -0
  2. anisotropy/anisotropy.py +13 -0
  3. anisotropy/evaluation.py +70 -0
  4. anisotropy/model.py +200 -0
  5. anisotropy/predefined.py +202 -0
  6. anisotropy/py.typed +1 -0
  7. anisotropy/values.py +84 -0
  8. mag_material/__init__.py +3 -0
  9. mag_material/mag_material.py +231 -0
  10. mag_material/parameters.py +150 -0
  11. mag_material/py.typed +1 -0
  12. nmag/__init__.py +36 -0
  13. nmag/backends.py +493 -0
  14. nmag/checkpoint.py +327 -0
  15. nmag/config.py +174 -0
  16. nmag/demag/__init__.py +39 -0
  17. nmag/demag/bem_operator.py +149 -0
  18. nmag/demag/geometry.py +134 -0
  19. nmag/demag/lindholm.py +133 -0
  20. nmag/demag/lindholm_fast.py +463 -0
  21. nmag/demag/linear.py +489 -0
  22. nmag/dynamics/__init__.py +255 -0
  23. nmag/output.py +27 -0
  24. nmag/parallel.py +45 -0
  25. nmag/py.typed +1 -0
  26. nmag/resources.py +38 -0
  27. nmag/simulation/__init__.py +519 -0
  28. nmag/simulation/anisotropy/__init__.py +5 -0
  29. nmag/simulation/anisotropy/fields.py +56 -0
  30. nmag/simulation/anisotropy/materials.py +138 -0
  31. nmag/simulation/demag/__init__.py +1 -0
  32. nmag/simulation/demag/bem/__init__.py +11 -0
  33. nmag/simulation/demag/bem/diagnostics.py +73 -0
  34. nmag/simulation/demag/bem/dirichlet.py +85 -0
  35. nmag/simulation/demag/bem/hierarchical.py +74 -0
  36. nmag/simulation/demag/bem/operator.py +292 -0
  37. nmag/simulation/demag/fem/__init__.py +11 -0
  38. nmag/simulation/demag/fem/assembly.py +114 -0
  39. nmag/simulation/demag/fem/charges.py +38 -0
  40. nmag/simulation/demag/fem/geometry.py +261 -0
  41. nmag/simulation/demag/fields/__init__.py +11 -0
  42. nmag/simulation/demag/fields/auxiliary.py +186 -0
  43. nmag/simulation/demag/fields/probe.py +77 -0
  44. nmag/simulation/demag/fields/recovery.py +207 -0
  45. nmag/simulation/demag/solver.py +17 -0
  46. nmag/simulation/dynamics/__init__.py +92 -0
  47. nmag/simulation/dynamics/advance.py +199 -0
  48. nmag/simulation/dynamics/integrator.py +263 -0
  49. nmag/simulation/exchange/__init__.py +13 -0
  50. nmag/simulation/exchange/coefficients.py +185 -0
  51. nmag/simulation/exchange/fields.py +106 -0
  52. nmag/simulation/exchange/llg_rhs.py +218 -0
  53. nmag/simulation/fields/__init__.py +19 -0
  54. nmag/simulation/fields/arrays.py +111 -0
  55. nmag/simulation/fields/availability.py +188 -0
  56. nmag/simulation/fields/averages.py +293 -0
  57. nmag/simulation/fields/derived.py +164 -0
  58. nmag/simulation/fields/maxangle.py +163 -0
  59. nmag/simulation/fields/probes.py +106 -0
  60. nmag/simulation/implicit_dynamics.py +221 -0
  61. nmag/simulation/mesh/__init__.py +13 -0
  62. nmag/simulation/mesh/geometry.py +153 -0
  63. nmag/simulation/mesh/materials.py +299 -0
  64. nmag/simulation/mesh/probe.py +234 -0
  65. nmag/simulation/restart.py +103 -0
  66. nmag/simulation/support.py +224 -0
  67. nmag_python_3-0.0.2.dist-info/METADATA +157 -0
  68. nmag_python_3-0.0.2.dist-info/RECORD +147 -0
  69. nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
  70. nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
  71. nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
  72. nmesh/__init__.py +130 -0
  73. nmesh/backend.py +286 -0
  74. nmesh/geometry/__init__.py +52 -0
  75. nmesh/geometry/boolean_operations.py +157 -0
  76. nmesh/geometry/primitives.py +453 -0
  77. nmesh/geometry/transform.py +126 -0
  78. nmesh/io/__init__.py +50 -0
  79. nmesh/io/ascii.py +132 -0
  80. nmesh/io/legacy_nmesh_hdf5.py +318 -0
  81. nmesh/io/meshio_support.py +170 -0
  82. nmesh/mesh_generation.py +182 -0
  83. nmesh/mesh_io.py +227 -0
  84. nmesh/mesh_model.py +147 -0
  85. nmesh/mesh_utilities.py +79 -0
  86. nmesh/mesher/__init__.py +21 -0
  87. nmesh/mesher/driver.py +146 -0
  88. nmesh/mesher/meshing_defaults.py +252 -0
  89. nmesh/mesher/meshing_parameters.py +185 -0
  90. nmesh/mesher/parity.py +21 -0
  91. nmesh/mesher/parity_canonical.py +142 -0
  92. nmesh/mesher/parity_comparison.py +191 -0
  93. nmesh/mesher/parity_metrics.py +114 -0
  94. nmesh/mesher/periodic.py +97 -0
  95. nmesh/mesher/relaxation/__init__.py +14 -0
  96. nmesh/mesher/relaxation/_constants.py +20 -0
  97. nmesh/mesher/relaxation/_types.py +15 -0
  98. nmesh/mesher/relaxation/density.py +170 -0
  99. nmesh/mesher/relaxation/engine/__init__.py +18 -0
  100. nmesh/mesher/relaxation/engine/state.py +155 -0
  101. nmesh/mesher/relaxation/engine/steps.py +248 -0
  102. nmesh/mesher/relaxation/engine/topology.py +230 -0
  103. nmesh/mesher/relaxation/forces/__init__.py +96 -0
  104. nmesh/mesher/relaxation/forces/jit.py +102 -0
  105. nmesh/mesher/relaxation/forces/neighbors.py +186 -0
  106. nmesh/mesher/relaxation/forces/simplex.py +302 -0
  107. nmesh/mesher/relaxation/forces/summary.py +207 -0
  108. nmesh/mesher/relaxation/forces/types.py +92 -0
  109. nmesh/mesher/relaxation/geometry/__init__.py +6 -0
  110. nmesh/mesher/relaxation/geometry/builder.py +154 -0
  111. nmesh/mesher/relaxation/geometry/model.py +194 -0
  112. nmesh/mesher/relaxation/seeding/__init__.py +74 -0
  113. nmesh/mesher/relaxation/seeding/periodic.py +88 -0
  114. nmesh/mesher/relaxation/seeding/points.py +88 -0
  115. nmesh/mesher/relaxation/seeding/sampling.py +142 -0
  116. nmesh/mesher/relaxation/topology/__init__.py +297 -0
  117. nmesh/mesher/relaxation/topology/finalize.py +78 -0
  118. nmesh/mesher/relaxation/topology/recovery.py +310 -0
  119. nmesh/mesher/sectioned_config.py +70 -0
  120. nmesh/nmesh.py +99 -0
  121. nmesh/py.typed +1 -0
  122. nmesh/utils/__init__.py +33 -0
  123. nmesh/utils/array_list_utils.py +128 -0
  124. nmesh/utils/constants.py +22 -0
  125. nmesh/utils/timing_memory_utils.py +51 -0
  126. nmesh/utils/types.py +13 -0
  127. si/constants.py +49 -0
  128. si/physical.py +722 -0
  129. si/py.typed +1 -0
  130. simulation/__init__.py +1 -0
  131. simulation/clock.py +237 -0
  132. simulation/data_writer.py +273 -0
  133. simulation/data_writer_collection.py +267 -0
  134. simulation/hysteresis.py +74 -0
  135. simulation/hysteresis_runner.py +286 -0
  136. simulation/hysteresis_schedule.py +180 -0
  137. simulation/inference/__init__.py +3 -0
  138. simulation/inference/inference.py +95 -0
  139. simulation/py.typed +1 -0
  140. simulation/quantity.py +88 -0
  141. simulation/simulation_core.py +458 -0
  142. throttler/__init__.py +3 -0
  143. throttler/py.typed +1 -0
  144. throttler/throttler.py +55 -0
  145. when/__init__.py +3 -0
  146. when/py.typed +1 -0
  147. when/when.py +416 -0
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import numpy as np
6
+
7
+ from ...backends import _load_rust_accelerator
8
+ from ..support import _simulation_compatibility_binding
9
+
10
+
11
+ class SimulationLlgRhsMixin:
12
+ if TYPE_CHECKING:
13
+
14
+ def __getattr__(self, name: str) -> Any: ...
15
+
16
+ def _llg_rhs_python(
17
+ self,
18
+ m: np.ndarray,
19
+ h_total: np.ndarray,
20
+ pin: np.ndarray,
21
+ ms_values: np.ndarray,
22
+ precession_coeff: float | np.ndarray,
23
+ damping_coeff: float | np.ndarray,
24
+ normalisation_coeff: float | np.ndarray,
25
+ dm_dcurrent: np.ndarray | None = None,
26
+ stt_adiabatic_coeff: float | np.ndarray = 0.0,
27
+ stt_nonadiabatic_coeff: float | np.ndarray = 0.0,
28
+ ) -> np.ndarray:
29
+ m, h_total, pin, ms_values = self._validated_llg_inputs(m, h_total, pin, ms_values)
30
+ precession_values, damping_values, normalisation_values = self._nodal_coefficients(
31
+ len(m),
32
+ (
33
+ ("precession", precession_coeff),
34
+ ("damping", damping_coeff),
35
+ ("normalisation", normalisation_coeff),
36
+ ),
37
+ )
38
+ rhs, mdotm = self._llg_field_rhs(
39
+ m,
40
+ h_total,
41
+ precession_values,
42
+ damping_values,
43
+ normalisation_values,
44
+ )
45
+ if dm_dcurrent is not None:
46
+ rhs += self._llg_stt_rhs(
47
+ m,
48
+ mdotm,
49
+ dm_dcurrent,
50
+ stt_adiabatic_coeff,
51
+ stt_nonadiabatic_coeff,
52
+ )
53
+ return ms_values[:, np.newaxis] * rhs * pin[:, np.newaxis]
54
+
55
+ @staticmethod
56
+ def _validated_llg_inputs(
57
+ m: np.ndarray,
58
+ h_total: np.ndarray,
59
+ pin: np.ndarray,
60
+ ms_values: np.ndarray,
61
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
62
+ m = np.asarray(m, dtype=float)
63
+ h_total = np.asarray(h_total, dtype=float)
64
+ pin = np.asarray(pin, dtype=float)
65
+ ms_values = np.asarray(ms_values, dtype=float)
66
+ if m.ndim != 2 or m.shape[1] != 3:
67
+ raise ValueError(f"m must have shape (n, 3), got {m.shape}.")
68
+ if h_total.shape != m.shape:
69
+ raise ValueError(f"h_total must have shape {m.shape}, got {h_total.shape}.")
70
+ if pin.shape != (len(m),):
71
+ raise ValueError(f"pin must have shape ({len(m)},), got {pin.shape}.")
72
+ if ms_values.shape != (len(m),):
73
+ raise ValueError(f"ms_values must have shape ({len(m)},), got {ms_values.shape}.")
74
+ return m, h_total, pin, ms_values
75
+
76
+ @staticmethod
77
+ def _nodal_coefficients(
78
+ point_count: int,
79
+ coefficients: tuple[tuple[str, float | np.ndarray], ...],
80
+ ) -> tuple[np.ndarray, ...]:
81
+ arrays: list[np.ndarray] = []
82
+ for name, coefficient in coefficients:
83
+ array = np.asarray(coefficient, dtype=float)
84
+ if array.ndim == 0:
85
+ array = np.full(point_count, float(array), dtype=float)
86
+ if array.shape != (point_count,):
87
+ raise ValueError(
88
+ f"{name} coefficient must have shape ({point_count},), got {array.shape}."
89
+ )
90
+ arrays.append(array)
91
+ return tuple(arrays)
92
+
93
+ @staticmethod
94
+ def _llg_field_rhs(
95
+ m: np.ndarray,
96
+ h_total: np.ndarray,
97
+ precession_values: np.ndarray,
98
+ damping_values: np.ndarray,
99
+ normalisation_values: np.ndarray,
100
+ ) -> tuple[np.ndarray, np.ndarray]:
101
+ mxh = np.cross(m, h_total)
102
+ mdoth = np.einsum("ij,ij->i", m, h_total)
103
+ mdotm = np.einsum("ij,ij->i", m, m)
104
+ damping = m * mdoth[:, np.newaxis] - h_total * mdotm[:, np.newaxis]
105
+ norm_error = (1.0 - mdotm)[:, np.newaxis] * m
106
+ rhs = (
107
+ precession_values[:, np.newaxis] * mxh
108
+ + damping_values[:, np.newaxis] * damping
109
+ + normalisation_values[:, np.newaxis] * norm_error
110
+ )
111
+ return rhs, mdotm
112
+
113
+ def _llg_stt_rhs(
114
+ self,
115
+ m: np.ndarray,
116
+ mdotm: np.ndarray,
117
+ dm_dcurrent: np.ndarray,
118
+ stt_adiabatic_coeff: float | np.ndarray,
119
+ stt_nonadiabatic_coeff: float | np.ndarray,
120
+ ) -> np.ndarray:
121
+ directional_derivative = np.asarray(dm_dcurrent, dtype=float)
122
+ if directional_derivative.shape != m.shape:
123
+ raise ValueError(
124
+ f"dm_dcurrent must have shape {m.shape}, got {directional_derivative.shape}."
125
+ )
126
+ adiabatic_values, nonadiabatic_values = self._nodal_coefficients(
127
+ len(m),
128
+ (
129
+ ("stt_adiabatic", stt_adiabatic_coeff),
130
+ ("stt_nonadiabatic", stt_nonadiabatic_coeff),
131
+ ),
132
+ )
133
+ mx_directional = np.cross(m, directional_derivative)
134
+ mdot_directional = np.einsum("ij,ij->i", m, directional_derivative)
135
+ mxmx_directional = (
136
+ m * mdot_directional[:, np.newaxis] - directional_derivative * mdotm[:, np.newaxis]
137
+ )
138
+ return (
139
+ adiabatic_values[:, np.newaxis] * mxmx_directional
140
+ + nonadiabatic_values[:, np.newaxis] * mx_directional
141
+ )
142
+
143
+ def _llg_rhs_rust(
144
+ self,
145
+ m: np.ndarray,
146
+ h_total: np.ndarray,
147
+ pin: np.ndarray,
148
+ ms_values: np.ndarray,
149
+ precession_coeff: float | np.ndarray,
150
+ damping_coeff: float | np.ndarray,
151
+ normalisation_coeff: float | np.ndarray,
152
+ dm_dcurrent: np.ndarray | None = None,
153
+ stt_adiabatic_coeff: float | np.ndarray = 0.0,
154
+ stt_nonadiabatic_coeff: float | np.ndarray = 0.0,
155
+ ) -> np.ndarray:
156
+ rust_accel = _simulation_compatibility_binding(
157
+ "_load_rust_accelerator",
158
+ _load_rust_accelerator,
159
+ )("NmagConfig.accelerator['llg']")
160
+ try:
161
+ llg_rhs = rust_accel.llg_rhs
162
+ except AttributeError as exc:
163
+ raise RuntimeError(
164
+ "NmagConfig.accelerator['llg']='rust' requires an nmag_accel build with llg_rhs. "
165
+ "Rebuild it with "
166
+ "`maturin develop --release --manifest-path rust/nmag_accel/Cargo.toml`."
167
+ ) from exc
168
+ coefficient_arrays = [
169
+ np.asarray(value, dtype=float)
170
+ for value in (
171
+ precession_coeff,
172
+ damping_coeff,
173
+ normalisation_coeff,
174
+ )
175
+ ]
176
+ if dm_dcurrent is None and all(array.ndim == 0 for array in coefficient_arrays):
177
+ result = llg_rhs(
178
+ np.asarray(m, dtype=float),
179
+ np.asarray(h_total, dtype=float),
180
+ np.asarray(pin, dtype=float),
181
+ np.asarray(ms_values, dtype=float),
182
+ *(float(array) for array in coefficient_arrays),
183
+ )
184
+ else:
185
+ try:
186
+ heterogeneous_rhs = (
187
+ rust_accel.llg_rhs_heterogeneous
188
+ if dm_dcurrent is None
189
+ else rust_accel.llg_rhs_stt_heterogeneous
190
+ )
191
+ except AttributeError as exc:
192
+ raise RuntimeError(
193
+ "NmagConfig.accelerator['llg']='rust' requires an nmag_accel build with "
194
+ "the heterogeneous LLG kernels. Rebuild the accelerator."
195
+ ) from exc
196
+ expanded = [
197
+ np.full(len(m), float(array), dtype=float) if array.ndim == 0 else array
198
+ for array in coefficient_arrays
199
+ ]
200
+ arguments: list[np.ndarray] = [
201
+ np.asarray(m, dtype=float),
202
+ np.asarray(h_total, dtype=float),
203
+ np.asarray(pin, dtype=float),
204
+ np.asarray(ms_values, dtype=float),
205
+ *expanded,
206
+ ]
207
+ if dm_dcurrent is not None:
208
+ stt_arrays = [
209
+ np.asarray(value, dtype=float)
210
+ for value in (stt_adiabatic_coeff, stt_nonadiabatic_coeff)
211
+ ]
212
+ expanded_stt = [
213
+ np.full(len(m), float(array), dtype=float) if array.ndim == 0 else array
214
+ for array in stt_arrays
215
+ ]
216
+ arguments.extend([np.asarray(dm_dcurrent, dtype=float), *expanded_stt])
217
+ result = heterogeneous_rhs(*arguments)
218
+ return np.asarray(result, dtype=float)
@@ -0,0 +1,19 @@
1
+ """Focused field-operation mixins exposed through one compatibility façade."""
2
+
3
+ from .arrays import SimulationFieldArrayMixin
4
+ from .availability import SimulationFieldAvailabilityMixin
5
+ from .averages import SimulationFieldAverageMixin
6
+ from .derived import SimulationFieldDerivedMixin
7
+ from .maxangle import SimulationFieldMaxangleMixin
8
+ from .probes import SimulationFieldProbeMixin
9
+
10
+
11
+ class SimulationFieldMixin(
12
+ SimulationFieldAvailabilityMixin,
13
+ SimulationFieldAverageMixin,
14
+ SimulationFieldMaxangleMixin,
15
+ SimulationFieldProbeMixin,
16
+ SimulationFieldArrayMixin,
17
+ SimulationFieldDerivedMixin,
18
+ ):
19
+ """Compose the public field API from focused implementation mixins."""
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ import numpy as np
9
+
10
+ from ..support import (
11
+ _NO_CONSTANT_AVERAGE,
12
+ DERIVED_FIELD_NAMES,
13
+ _copy_average_value,
14
+ )
15
+
16
+
17
+ class SimulationFieldArrayMixin:
18
+ if TYPE_CHECKING:
19
+ _subfield_array_cache: dict[str, np.ndarray] | None
20
+ _subfield_average_cache: dict[tuple[str, str | None], object] | None
21
+ _active_subfield_array_timings: dict[str, float] | None
22
+
23
+ def __getattr__(self, name: str) -> Any: ...
24
+
25
+ def _subfield_array(self, subfieldname: str) -> np.ndarray:
26
+ started = time.perf_counter()
27
+ if self._subfield_array_cache is not None and subfieldname in self._subfield_array_cache:
28
+ self._record_active_subfield_array_timing(
29
+ f"{subfieldname}:cache_hit",
30
+ time.perf_counter() - started,
31
+ )
32
+ return self._subfield_array_cache[subfieldname]
33
+
34
+ data = self._compute_subfield_array(subfieldname)
35
+ self._record_active_subfield_array_timing(
36
+ f"{subfieldname}:compute",
37
+ time.perf_counter() - started,
38
+ )
39
+ if self._subfield_array_cache is not None:
40
+ self._subfield_array_cache[subfieldname] = data
41
+ return data
42
+
43
+ def _record_active_subfield_array_timing(self, name: str, seconds: float) -> None:
44
+ timings = self._active_subfield_array_timings
45
+ if timings is not None:
46
+ timings[name] = timings.get(name, 0.0) + seconds
47
+
48
+ @contextmanager
49
+ def _record_active_subfield_array_timing_block(self, name: str) -> Generator[None]:
50
+ started = time.perf_counter()
51
+ try:
52
+ yield
53
+ finally:
54
+ self._record_active_subfield_array_timing(
55
+ name,
56
+ time.perf_counter() - started,
57
+ )
58
+
59
+ @contextmanager
60
+ def _subfield_array_cache_scope(self) -> Generator[None]:
61
+ previous_cache = self._subfield_array_cache
62
+ self._subfield_array_cache = previous_cache if previous_cache is not None else {}
63
+ try:
64
+ yield
65
+ finally:
66
+ self._subfield_array_cache = previous_cache
67
+
68
+ @contextmanager
69
+ def _subfield_average_cache_scope(self) -> Generator[None]:
70
+ previous_cache = self._subfield_average_cache
71
+ self._subfield_average_cache = previous_cache if previous_cache is not None else {}
72
+ try:
73
+ yield
74
+ finally:
75
+ self._subfield_average_cache = previous_cache
76
+
77
+ def _cached_subfield_average(
78
+ self,
79
+ key: tuple[str, str | None],
80
+ ) -> object:
81
+ cache = self._subfield_average_cache
82
+ if cache is None or key not in cache:
83
+ return _NO_CONSTANT_AVERAGE
84
+ return cache[key]
85
+
86
+ def _cache_subfield_average(
87
+ self,
88
+ key: tuple[str, str | None],
89
+ value: object,
90
+ ) -> None:
91
+ cache = self._subfield_average_cache
92
+ if cache is not None:
93
+ cache[key] = _copy_average_value(value)
94
+
95
+ def _compute_subfield_array(self, subfieldname: str) -> np.ndarray:
96
+ if subfieldname in DERIVED_FIELD_NAMES:
97
+ return self._derived_subfield_array(subfieldname)
98
+ if subfieldname == "H_demag":
99
+ return np.asarray(self._get_demag_nodal_field(), dtype=float)
100
+ if subfieldname == "H_ext":
101
+ h_ext = np.asarray(self._fields.get("H_ext", np.zeros(3)), dtype=float)
102
+ if h_ext.shape != (3,):
103
+ raise ValueError(f"H_ext must be a homogeneous 3-vector, got shape {h_ext.shape}.")
104
+ point_count = len(self.mesh.points) if self.mesh is not None else 1
105
+ return np.tile(h_ext, (point_count, 1))
106
+ if subfieldname not in self._fields:
107
+ raise KeyError(f"Unknown or unset subfield '{subfieldname}'.")
108
+ return np.asarray(self._fields[subfieldname], dtype=float)
109
+
110
+ def _is_subfield_available(self, subfieldname: str) -> bool:
111
+ return self.is_subfield_available(subfieldname)
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import numpy as np
6
+
7
+ from si.physical import SI
8
+
9
+ if TYPE_CHECKING:
10
+ from simulation.quantity import Quantity
11
+
12
+ from ..support import (
13
+ DERIVED_FIELD_NAMES,
14
+ _si_unit,
15
+ _simulation_compatibility_binding,
16
+ )
17
+
18
+ _known_field_quantities_cache: list[Quantity] | None = None
19
+ _known_quantities_by_name_cache: dict[str, Quantity] | None = None
20
+
21
+
22
+ def _known_field_quantities() -> list[Quantity]:
23
+ global _known_field_quantities_cache
24
+ if _known_field_quantities_cache is None:
25
+ from simulation.quantity import known_field_quantities
26
+
27
+ _known_field_quantities_cache = known_field_quantities
28
+ return _known_field_quantities_cache
29
+
30
+
31
+ def _known_quantities_by_name() -> dict[str, Quantity]:
32
+ global _known_quantities_by_name_cache
33
+ if _known_quantities_by_name_cache is None:
34
+ from simulation.quantity import known_quantities_by_name
35
+
36
+ _known_quantities_by_name_cache = known_quantities_by_name
37
+ return _known_quantities_by_name_cache
38
+
39
+
40
+ class SimulationFieldAvailabilityMixin:
41
+ if TYPE_CHECKING:
42
+
43
+ def __getattr__(self, name: str) -> Any: ...
44
+
45
+ def get_all_field_names(self) -> list[str]:
46
+ """Return the field names currently available for saving or access."""
47
+ has_mesh = self.mesh is not None
48
+ has_m = "m" in self._fields
49
+ has_mesh_and_m = has_mesh and has_m
50
+ has_tetrahedral_mesh = self._has_tetrahedral_mesh_or_empty()
51
+ return [
52
+ quantity.name
53
+ for quantity in _simulation_compatibility_binding(
54
+ "_known_field_quantities", _known_field_quantities
55
+ )()
56
+ if self._is_subfield_available_from_state(
57
+ quantity.name,
58
+ has_mesh=has_mesh,
59
+ has_m=has_m,
60
+ has_mesh_and_m=has_mesh_and_m,
61
+ has_tetrahedral_mesh=has_tetrahedral_mesh,
62
+ )
63
+ ]
64
+
65
+ def is_subfield_available(self, subfieldname: str) -> bool:
66
+ """Return whether the current model can provide a named field.
67
+
68
+ The check does not trigger expensive FEM/BEM work. Actual field access
69
+ can still raise a numerical error during calculation.
70
+
71
+ Args:
72
+ subfieldname: Candidate field name.
73
+
74
+ Returns:
75
+ True when the field is set or derivable from current state.
76
+ """
77
+ if subfieldname == "H_ext":
78
+ return True
79
+ if subfieldname not in DERIVED_FIELD_NAMES and subfieldname != "H_demag":
80
+ return subfieldname in self._fields
81
+
82
+ has_mesh = self.mesh is not None
83
+ has_m = "m" in self._fields
84
+ has_tetrahedral_mesh = self._has_tetrahedral_mesh_or_empty()
85
+ return self._is_subfield_available_from_state(
86
+ subfieldname,
87
+ has_mesh=has_mesh,
88
+ has_m=has_m,
89
+ has_mesh_and_m=has_mesh and has_m,
90
+ has_tetrahedral_mesh=has_tetrahedral_mesh,
91
+ )
92
+
93
+ def _is_subfield_available_from_state(
94
+ self,
95
+ subfieldname: str,
96
+ *,
97
+ has_mesh: bool,
98
+ has_m: bool,
99
+ has_mesh_and_m: bool,
100
+ has_tetrahedral_mesh: bool,
101
+ ) -> bool:
102
+ has_m_on_tetrahedral_mesh = self._has_m_on_tetrahedral_mesh(
103
+ has_mesh_and_m,
104
+ has_tetrahedral_mesh,
105
+ )
106
+ state_availability = {
107
+ "pin": has_mesh,
108
+ "H_anis": has_mesh_and_m,
109
+ "E_anis": has_mesh_and_m,
110
+ "H_exch": has_m_on_tetrahedral_mesh,
111
+ "E_exch": has_m_on_tetrahedral_mesh,
112
+ "dm_dcurrent": self._has_current_density(has_m_on_tetrahedral_mesh),
113
+ "dmdt": has_m_on_tetrahedral_mesh,
114
+ "H_total": has_m_on_tetrahedral_mesh,
115
+ "E_total": has_m_on_tetrahedral_mesh,
116
+ "E_demag": self._has_demag_magnetisation(has_mesh_and_m),
117
+ "phi": self._has_demag_magnetisation(has_mesh_and_m),
118
+ "rho": self._has_demag_magnetisation(has_mesh_and_m),
119
+ "M": has_mesh_and_m,
120
+ "E_ext": has_mesh_and_m,
121
+ "H_demag": self._has_demag_nodal_field(has_m, has_tetrahedral_mesh),
122
+ "H_ext": True,
123
+ }
124
+ if subfieldname in state_availability:
125
+ return state_availability[subfieldname]
126
+ return subfieldname not in DERIVED_FIELD_NAMES and subfieldname in self._fields
127
+
128
+ @staticmethod
129
+ def _has_m_on_tetrahedral_mesh(has_mesh_and_m: bool, has_tetrahedral_mesh: bool) -> bool:
130
+ return has_mesh_and_m and has_tetrahedral_mesh
131
+
132
+ def _has_current_density(self, has_m_on_tetrahedral_mesh: bool) -> bool:
133
+ return has_m_on_tetrahedral_mesh and "current_density" in self._fields
134
+
135
+ def _has_demag_magnetisation(self, has_mesh_and_m: bool) -> bool:
136
+ return self.do_demag and has_mesh_and_m
137
+
138
+ def _has_demag_nodal_field(self, has_m: bool, has_tetrahedral_mesh: bool) -> bool:
139
+ return self.do_demag and has_m and has_tetrahedral_mesh
140
+
141
+ def get_materials_of_field(self, field_name: str) -> list[Any]:
142
+ quantity = _simulation_compatibility_binding(
143
+ "_known_quantities_by_name", _known_quantities_by_name
144
+ )()[field_name]
145
+ if "?" in (quantity.signature or ""):
146
+ return self.materials
147
+ return []
148
+
149
+ def get_subfield(self, subfieldname: str, units: SI | None = None) -> Any:
150
+ """Return all nodal or cell values for one available field.
151
+
152
+ Args:
153
+ subfieldname: Name returned by :meth:`get_all_field_names`.
154
+ units: Optional compatible SI unit for returned numeric values.
155
+
156
+ Returns:
157
+ Field values as Python scalars or nested lists.
158
+
159
+ Raises:
160
+ KeyError: If the field is unknown, unset, or disabled.
161
+ """
162
+ data, field_units = self._subfield_data_and_units(subfieldname)
163
+ if units is None or field_units is None:
164
+ return data.tolist() if isinstance(data, np.ndarray) else data
165
+ return (np.asarray(data) * field_units.in_units_of(units)).tolist()
166
+
167
+ def _subfield_data_and_units(self, subfieldname: str) -> tuple[Any, SI | None]:
168
+ if subfieldname == "H_demag":
169
+ if not self.is_subfield_available(subfieldname):
170
+ raise KeyError("Subfield 'H_demag' is unavailable when demag is disabled.")
171
+ data = np.asarray(self._get_demag_nodal_field(), dtype=float)
172
+ return data, _si_unit("A/m")
173
+ if subfieldname in self._fields:
174
+ data = self._fields[subfieldname]
175
+ elif self.is_subfield_available(subfieldname):
176
+ data = self._subfield_array(subfieldname)
177
+ else:
178
+ raise KeyError(f"Unknown or unset subfield '{subfieldname}'.")
179
+ quantity = _simulation_compatibility_binding(
180
+ "_known_quantities_by_name", _known_quantities_by_name
181
+ )().get(subfieldname)
182
+ return data, None if quantity is None else quantity.units
183
+
184
+ def _has_tetrahedral_mesh_or_empty(self) -> bool:
185
+ if self.mesh is None:
186
+ return False
187
+ simplices = np.asarray(self.mesh.simplices)
188
+ return simplices.size == 0 or (simplices.ndim == 2 and simplices.shape[1] == 4)