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,293 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import numpy as np
7
+
8
+ from ..support import (
9
+ _NO_COMPOSED_AVERAGE,
10
+ _NO_CONSTANT_AVERAGE,
11
+ _copy_average_value,
12
+ )
13
+
14
+ _NO_ARRAY_AVERAGE = object()
15
+
16
+
17
+ class SimulationFieldAverageMixin:
18
+ if TYPE_CHECKING:
19
+ _active_subfield_array_timings: dict[str, float] | None
20
+
21
+ def __getattr__(self, name: str) -> Any: ...
22
+
23
+ def get_subfield_average(self, subfieldname: str, mat_name: str | None = None) -> Any:
24
+ """Return the volume-weighted average of an available field.
25
+
26
+ Args:
27
+ subfieldname: Field name, for example ``m``, ``H_total``, or
28
+ ``E_demag``.
29
+ mat_name: Restrict integration to tetrahedra belonging to this
30
+ material. Omit it for the complete magnetic volume.
31
+
32
+ Returns:
33
+ A scalar or component list, or ``None`` when the requested derived
34
+ quantity is not available for the current model.
35
+ """
36
+ timings: dict[str, float] = {}
37
+ self.last_subfield_average_timings_seconds = {}
38
+ total_started = time.perf_counter()
39
+ cache_key = (subfieldname, mat_name)
40
+ cached_average = self._cached_subfield_average(cache_key)
41
+ if cached_average is not _NO_CONSTANT_AVERAGE:
42
+ timings["cache_hit"] = time.perf_counter() - total_started
43
+ self._finish_subfield_average_timings(timings, total_started)
44
+ return _copy_average_value(cached_average)
45
+
46
+ started = time.perf_counter()
47
+ constant_average = self._constant_subfield_average(subfieldname)
48
+ if constant_average is not _NO_CONSTANT_AVERAGE:
49
+ timings["constant_average"] = time.perf_counter() - started
50
+ self._finish_subfield_average_timings(timings, total_started)
51
+ self._cache_subfield_average(cache_key, constant_average)
52
+ return constant_average
53
+
54
+ if subfieldname == "H_demag":
55
+ result = self._direct_demag_average(timings)
56
+ self._finish_subfield_average_timings(timings, total_started)
57
+ self._cache_subfield_average(cache_key, result)
58
+ return result
59
+
60
+ composed_average = self._try_composed_subfield_average(subfieldname, mat_name, timings)
61
+ if composed_average is not _NO_COMPOSED_AVERAGE:
62
+ result = np.asarray(composed_average, dtype=float).tolist()
63
+ self._finish_subfield_average_timings(timings, total_started)
64
+ self._cache_subfield_average(cache_key, result)
65
+ return result
66
+
67
+ average = self._array_subfield_average(subfieldname, mat_name, timings)
68
+ self._finish_subfield_average_timings(timings, total_started)
69
+ if average is _NO_ARRAY_AVERAGE:
70
+ return None
71
+ self._cache_subfield_average(cache_key, average)
72
+ return average
73
+
74
+ def _finish_subfield_average_timings(
75
+ self,
76
+ timings: dict[str, float],
77
+ total_started: float,
78
+ ) -> None:
79
+ timings["total"] = time.perf_counter() - total_started
80
+ self.last_subfield_average_timings_seconds = dict(sorted(timings.items()))
81
+
82
+ def _direct_demag_average(self, timings: dict[str, float]) -> list[float]:
83
+ subfield_timings: dict[str, float] = {}
84
+ previous_subfield_timings = self._active_subfield_array_timings
85
+ self._active_subfield_array_timings = subfield_timings
86
+ try:
87
+ started = time.perf_counter()
88
+ average = self._demag_cell_field_average()
89
+ timings["direct_cell_average"] = time.perf_counter() - started
90
+ finally:
91
+ if self._active_subfield_array_timings is subfield_timings:
92
+ self._active_subfield_array_timings = previous_subfield_timings
93
+ self._append_subfield_array_timings(timings, subfield_timings)
94
+ return np.asarray(average, dtype=float).tolist()
95
+
96
+ def _try_composed_subfield_average(
97
+ self,
98
+ subfieldname: str,
99
+ mat_name: str | None,
100
+ timings: dict[str, float],
101
+ ) -> object:
102
+ composed_timings: dict[str, float] = {}
103
+ subfield_timings: dict[str, float] = {}
104
+ previous_subfield_timings = self._active_subfield_array_timings
105
+ self._active_subfield_array_timings = subfield_timings
106
+ try:
107
+ started = time.perf_counter()
108
+ try:
109
+ average = self._composed_subfield_average(
110
+ subfieldname,
111
+ composed_timings,
112
+ mat_name=mat_name,
113
+ )
114
+ except KeyError:
115
+ average = _NO_COMPOSED_AVERAGE
116
+ timings["composed_average"] = time.perf_counter() - started
117
+ finally:
118
+ if self._active_subfield_array_timings is subfield_timings:
119
+ self._active_subfield_array_timings = previous_subfield_timings
120
+ if average is _NO_COMPOSED_AVERAGE:
121
+ timings.pop("composed_average")
122
+ return average
123
+ timings.update(composed_timings)
124
+ self._append_subfield_array_timings(timings, subfield_timings)
125
+ return average
126
+
127
+ def _array_subfield_average(
128
+ self,
129
+ subfieldname: str,
130
+ mat_name: str | None,
131
+ timings: dict[str, float],
132
+ ) -> Any | object:
133
+ subfield_timings: dict[str, float] = {}
134
+ previous_subfield_timings = self._active_subfield_array_timings
135
+ self._active_subfield_array_timings = subfield_timings
136
+ data: np.ndarray | None
137
+ try:
138
+ try:
139
+ started = time.perf_counter()
140
+ data = self._subfield_array(subfieldname)
141
+ timings["array"] = time.perf_counter() - started
142
+ except KeyError:
143
+ data = None
144
+ finally:
145
+ if self._active_subfield_array_timings is subfield_timings:
146
+ self._active_subfield_array_timings = previous_subfield_timings
147
+ self._append_subfield_array_timings(timings, subfield_timings)
148
+ if data is None or data.size == 0:
149
+ return _NO_ARRAY_AVERAGE
150
+ started = time.perf_counter()
151
+ average_value = self._field_average(data, mat_name=mat_name)
152
+ average = (
153
+ average_value.tolist()
154
+ if isinstance(average_value, np.ndarray)
155
+ else float(average_value)
156
+ )
157
+ timings["field_average"] = time.perf_counter() - started
158
+ return average
159
+
160
+ def _append_subfield_array_timings(
161
+ self,
162
+ timings: dict[str, float],
163
+ subfield_timings: dict[str, float],
164
+ ) -> None:
165
+ timings.update(
166
+ (f"subfield_array:{name}", seconds)
167
+ for name, seconds in sorted(subfield_timings.items())
168
+ )
169
+
170
+ def _constant_subfield_average(self, subfieldname: str) -> object:
171
+ if subfieldname == "H_ext":
172
+ return self._constant_h_ext_average()
173
+ if self.mesh is None:
174
+ return _NO_CONSTANT_AVERAGE
175
+ handlers = {
176
+ "pin": self._constant_pin_average,
177
+ "H_anis": self._constant_h_anis_average,
178
+ "E_anis": self._constant_e_anis_average,
179
+ "H_exch": self._constant_h_exch_average,
180
+ "E_exch": self._constant_e_exch_average,
181
+ }
182
+ handler = handlers.get(subfieldname)
183
+ return _NO_CONSTANT_AVERAGE if handler is None else handler()
184
+
185
+ def _constant_h_ext_average(self) -> object:
186
+ h_ext = np.asarray(self._fields.get("H_ext", np.zeros(3)), dtype=float)
187
+ return h_ext.tolist() if h_ext.shape == (3,) else _NO_CONSTANT_AVERAGE
188
+
189
+ def _constant_pin_average(self) -> object:
190
+ pin = self._fields.get("pin")
191
+ if pin is None:
192
+ return 1.0
193
+ pin_values = np.asarray(pin, dtype=float)
194
+ if pin_values.size > 0 and np.all(pin_values == pin_values[0]):
195
+ return float(pin_values[0])
196
+ return _NO_CONSTANT_AVERAGE
197
+
198
+ def _constant_h_anis_average(self) -> object:
199
+ return self._constant_anisotropy_average([0.0, 0.0, 0.0])
200
+
201
+ def _constant_e_anis_average(self) -> object:
202
+ return self._constant_anisotropy_average(0.0)
203
+
204
+ def _constant_anisotropy_average(self, zero_value: object) -> object:
205
+ if "m" not in self._fields or not self._anisotropy_is_zero_by_construction():
206
+ return _NO_CONSTANT_AVERAGE
207
+ return zero_value
208
+
209
+ def _constant_h_exch_average(self) -> object:
210
+ return [0.0, 0.0, 0.0] if self._exchange_is_zero_by_construction() else _NO_CONSTANT_AVERAGE
211
+
212
+ def _constant_e_exch_average(self) -> object:
213
+ return 0.0 if self._exchange_is_zero_by_construction() else _NO_CONSTANT_AVERAGE
214
+
215
+ def _composed_subfield_average(
216
+ self,
217
+ subfieldname: str,
218
+ timings: dict[str, float],
219
+ *,
220
+ mat_name: str | None = None,
221
+ ) -> object:
222
+ if subfieldname != "H_total":
223
+ return _NO_COMPOSED_AVERAGE
224
+ if self.mesh is None or "m" not in self._fields:
225
+ return _NO_COMPOSED_AVERAGE
226
+
227
+ total = np.asarray(
228
+ self._average_component_for_composition("H_ext", timings),
229
+ dtype=float,
230
+ )
231
+ if self.do_demag:
232
+ total = total + np.asarray(
233
+ self._average_component_for_composition("H_demag", timings),
234
+ dtype=float,
235
+ )
236
+ total = total + np.asarray(
237
+ self._average_component_for_composition(
238
+ "H_anis",
239
+ timings,
240
+ mat_name=mat_name,
241
+ ),
242
+ dtype=float,
243
+ )
244
+ total = total + np.asarray(
245
+ self._average_component_for_composition(
246
+ "H_exch",
247
+ timings,
248
+ mat_name=mat_name,
249
+ ),
250
+ dtype=float,
251
+ )
252
+ return total
253
+
254
+ def _average_component_for_composition(
255
+ self,
256
+ subfieldname: str,
257
+ timings: dict[str, float],
258
+ *,
259
+ mat_name: str | None = None,
260
+ ) -> np.ndarray:
261
+ cache_key = (subfieldname, mat_name)
262
+ cached_average = self._cached_subfield_average(cache_key)
263
+ if cached_average is not _NO_CONSTANT_AVERAGE:
264
+ timings[f"component_average:{subfieldname}:cache_hit"] = 0.0
265
+ return np.asarray(cached_average, dtype=float)
266
+
267
+ started = time.perf_counter()
268
+ constant_average = self._constant_subfield_average(subfieldname)
269
+ if constant_average is not _NO_CONSTANT_AVERAGE:
270
+ timings[f"component_average:{subfieldname}:constant"] = time.perf_counter() - started
271
+ self._cache_subfield_average(cache_key, constant_average)
272
+ return np.asarray(constant_average, dtype=float)
273
+
274
+ if subfieldname == "H_demag":
275
+ started = time.perf_counter()
276
+ average = self._demag_cell_field_average()
277
+ timings[f"component_average:{subfieldname}:direct_cell_average"] = (
278
+ time.perf_counter() - started
279
+ )
280
+ self._cache_subfield_average(cache_key, average)
281
+ return np.asarray(average, dtype=float)
282
+
283
+ started = time.perf_counter()
284
+ data = self._subfield_array(subfieldname)
285
+ timings[f"component_average:{subfieldname}:array"] = time.perf_counter() - started
286
+ if data.size == 0:
287
+ raise KeyError(f"Component field '{subfieldname}' is empty.")
288
+
289
+ started = time.perf_counter()
290
+ average = self._field_average(data, mat_name=mat_name)
291
+ timings[f"component_average:{subfieldname}:field_average"] = time.perf_counter() - started
292
+ self._cache_subfield_average(cache_key, average)
293
+ return np.asarray(average, dtype=float)
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import numpy as np
6
+
7
+ from ...backends import _selected_llg_backend
8
+ from ..support import (
9
+ LEGACY_DMDT_MAGNETISATION_SCALE,
10
+ MU0,
11
+ _simulation_compatibility_binding,
12
+ )
13
+
14
+
15
+ class SimulationFieldDerivedMixin:
16
+ if TYPE_CHECKING:
17
+
18
+ def __getattr__(self, name: str) -> Any: ...
19
+
20
+ def _derived_subfield_array(self, subfieldname: str) -> np.ndarray:
21
+ if self.mesh is None:
22
+ raise KeyError(f"Derived field '{subfieldname}' requires a mesh.")
23
+ point_count = len(self.mesh.points)
24
+ if subfieldname == "pin":
25
+ return self._derived_pin(point_count)
26
+ if "m" not in self._fields:
27
+ raise KeyError(f"Derived field '{subfieldname}' requires magnetisation.")
28
+
29
+ resolvers = {
30
+ "phi": self._derived_phi,
31
+ "rho": self._derived_rho,
32
+ "H_anis": self._derived_h_anis,
33
+ "H_exch": self._derived_h_exch,
34
+ "dm_dcurrent": self._derived_dm_dcurrent,
35
+ "dmdt": self._derived_dmdt,
36
+ "M": self._derived_magnetisation,
37
+ "H_total": self._derived_h_total,
38
+ "E_anis": self._derived_e_anis,
39
+ "E_exch": self._derived_e_exch,
40
+ "E_ext": self._derived_e_ext,
41
+ "E_demag": self._derived_e_demag,
42
+ "E_total": self._derived_e_total,
43
+ }
44
+ resolver = resolvers.get(subfieldname)
45
+ if resolver is None:
46
+ raise KeyError(f"Unknown or unset subfield '{subfieldname}'.") from None
47
+ return resolver(point_count)
48
+
49
+ def _derived_pin(self, point_count: int) -> np.ndarray:
50
+ return np.asarray(
51
+ self._fields.get("pin", np.ones(point_count, dtype=float)),
52
+ dtype=float,
53
+ )
54
+
55
+ def _derived_phi(self, point_count: int) -> np.ndarray:
56
+ if not self.do_demag:
57
+ return np.zeros(point_count, dtype=float)
58
+ phi, _rho, _volumes = self._get_demag_auxiliary_fields()
59
+ return phi
60
+
61
+ def _derived_rho(self, point_count: int) -> np.ndarray:
62
+ if not self.do_demag:
63
+ return np.zeros(point_count, dtype=float)
64
+ _phi, rho, _volumes = self._get_demag_auxiliary_fields()
65
+ return rho
66
+
67
+ def _derived_h_anis(self, point_count: int) -> np.ndarray:
68
+ field, _energy = self._get_anisotropy_fields()
69
+ return field
70
+
71
+ def _derived_h_exch(self, _point_count: int) -> np.ndarray:
72
+ return self._get_exchange_nodal_field()
73
+
74
+ def _derived_dm_dcurrent(self, _point_count: int) -> np.ndarray:
75
+ return self._get_dm_dcurrent()
76
+
77
+ def _derived_dmdt(self, _point_count: int) -> np.ndarray:
78
+ coefficients = self._nodal_material_coefficients()
79
+ m = np.asarray(self._fields["m"], dtype=float)
80
+ dmdt_scale_values = np.full(len(m), LEGACY_DMDT_MAGNETISATION_SCALE, dtype=float)
81
+ dm_dcurrent = (
82
+ self._subfield_array("dm_dcurrent") if "current_density" in self._fields else None
83
+ )
84
+ backend = self._llg_backend(len(m))
85
+ rhs = self._llg_rhs_rust if backend == "rust" else self._llg_rhs_python
86
+ with self._record_active_subfield_array_timing_block(f"dmdt_detail:llg_rhs:{backend}"):
87
+ return rhs(
88
+ m,
89
+ self._subfield_array("H_total"),
90
+ self._subfield_array("pin"),
91
+ dmdt_scale_values,
92
+ coefficients.precession,
93
+ coefficients.damping,
94
+ coefficients.normalisation,
95
+ dm_dcurrent,
96
+ coefficients.stt_adiabatic,
97
+ coefficients.stt_nonadiabatic,
98
+ )
99
+
100
+ def _llg_backend(self, point_count: int) -> str:
101
+ return _simulation_compatibility_binding("_selected_llg_backend", _selected_llg_backend)(
102
+ point_count, getattr(self, "config", None)
103
+ )
104
+
105
+ def _derived_magnetisation(self, _point_count: int) -> np.ndarray:
106
+ return np.asarray(self._fields["m"], dtype=float) * self._nodal_ms_values()[:, np.newaxis]
107
+
108
+ def _derived_h_total(self, _point_count: int) -> np.ndarray:
109
+ h_total = np.asarray(self._subfield_array("H_ext"), dtype=float)
110
+ if self.do_demag:
111
+ h_total = h_total + np.asarray(self._subfield_array("H_demag"), dtype=float)
112
+ return h_total + self._subfield_array("H_anis") + self._subfield_array("H_exch")
113
+
114
+ def _derived_e_anis(self, point_count: int) -> np.ndarray:
115
+ _field, energy = self._get_anisotropy_fields()
116
+ return energy
117
+
118
+ def _derived_e_exch(self, _point_count: int) -> np.ndarray:
119
+ return (
120
+ -0.5
121
+ * MU0
122
+ * self._nodal_ms_values()
123
+ * np.einsum(
124
+ "ij,ij->i",
125
+ self._subfield_array("H_exch"),
126
+ np.asarray(self._fields["m"], dtype=float),
127
+ )
128
+ )
129
+
130
+ def _derived_e_ext(self, _point_count: int) -> np.ndarray:
131
+ return -MU0 * np.einsum(
132
+ "ij,ij->i",
133
+ self._subfield_array("M"),
134
+ self._subfield_array("H_ext"),
135
+ )
136
+
137
+ def _derived_e_demag(self, point_count: int) -> np.ndarray:
138
+ if not self.do_demag:
139
+ return np.zeros(point_count, dtype=float)
140
+ return (
141
+ -0.5
142
+ * MU0
143
+ * np.einsum(
144
+ "ij,ij->i",
145
+ self._subfield_array("M"),
146
+ self._subfield_array("H_demag"),
147
+ )
148
+ )
149
+
150
+ def _derived_e_total(self, _point_count: int) -> np.ndarray:
151
+ return (
152
+ self._subfield_array("E_ext")
153
+ + self._subfield_array("E_anis")
154
+ + self._subfield_array("E_exch")
155
+ + self._subfield_array("E_demag")
156
+ )
157
+
158
+ def _exchange_is_zero_by_construction(self) -> bool:
159
+ if "m" not in self._fields:
160
+ return False
161
+ m = np.asarray(self._fields["m"], dtype=float)
162
+ if m.ndim != 2 or len(m) == 0:
163
+ return False
164
+ return bool(np.allclose(m, m[0], rtol=0.0, atol=1.0e-12))
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import numpy as np
7
+
8
+ from ...backends import (
9
+ _load_rust_accelerator,
10
+ _selected_maxangle_backend,
11
+ )
12
+ from ...demag import _normalised
13
+ from ..support import _simulation_compatibility_binding
14
+
15
+
16
+ class SimulationFieldMaxangleMixin:
17
+ if TYPE_CHECKING:
18
+
19
+ def __getattr__(self, name: str) -> Any: ...
20
+
21
+ def get_maxangle_average(self, field_name: str) -> float | None:
22
+ """Return the maximum neighboring magnetization angle in degrees.
23
+
24
+ Args:
25
+ field_name: Currently only ``"m"`` is supported.
26
+
27
+ Returns:
28
+ Maximum angle across mesh edges, or ``None`` when magnetization or
29
+ a suitable mesh is unavailable.
30
+ """
31
+ timings: dict[str, float] = {}
32
+ total_started = time.perf_counter()
33
+ self.last_maxangle_timings_seconds = {}
34
+ inputs = self._maxangle_inputs(field_name, timings)
35
+ if inputs is None:
36
+ return self._finish_maxangle(timings, total_started, None)
37
+ m, simplices = inputs
38
+ if self._is_uniform_magnetisation(m, timings):
39
+ return self._finish_maxangle(timings, total_started, 0.0)
40
+ backend = _simulation_compatibility_binding(
41
+ "_selected_maxangle_backend", _selected_maxangle_backend
42
+ )(getattr(self, "config", None))
43
+ if backend == "rust":
44
+ return self._finish_maxangle(
45
+ timings,
46
+ total_started,
47
+ self._timed_rust_maxangle(m, simplices, timings),
48
+ )
49
+ return self._finish_maxangle(
50
+ timings,
51
+ total_started,
52
+ self._timed_python_maxangle(m, timings),
53
+ )
54
+
55
+ def _finish_maxangle(
56
+ self,
57
+ timings: dict[str, float],
58
+ total_started: float,
59
+ value: float | None,
60
+ ) -> float | None:
61
+ timings["total"] = time.perf_counter() - total_started
62
+ self.last_maxangle_timings_seconds = dict(sorted(timings.items()))
63
+ return value
64
+
65
+ def _maxangle_inputs(
66
+ self,
67
+ field_name: str,
68
+ timings: dict[str, float] | None,
69
+ ) -> tuple[np.ndarray, np.ndarray] | None:
70
+ if field_name != "m" or "m" not in self._fields or self.mesh is None:
71
+ return None
72
+ started = time.perf_counter()
73
+ m = np.asarray(self._fields["m"], dtype=float)
74
+ simplices = np.asarray(self._require_mesh().simplices, dtype=int)
75
+ if timings is not None:
76
+ timings["input_arrays"] = time.perf_counter() - started
77
+ if m.ndim != 2 or m.shape[1] != 3 or simplices.ndim != 2 or simplices.shape[1] < 2:
78
+ return None
79
+ return (m, simplices) if len(m) > 0 else None
80
+
81
+ def _is_uniform_magnetisation(self, m: np.ndarray, timings: dict[str, float]) -> bool:
82
+ started = time.perf_counter()
83
+ is_uniform = bool(np.all(m == m[0]))
84
+ timings["uniform_check"] = time.perf_counter() - started
85
+ return is_uniform
86
+
87
+ def _timed_rust_maxangle(
88
+ self,
89
+ m: np.ndarray,
90
+ simplices: np.ndarray,
91
+ timings: dict[str, float],
92
+ ) -> float:
93
+ started = time.perf_counter()
94
+ maxangle = self._maxangle_average_rust(m, simplices)
95
+ timings["mesh_edges_and_angles:rust"] = time.perf_counter() - started
96
+ return maxangle
97
+
98
+ def _timed_python_maxangle(self, m: np.ndarray, timings: dict[str, float]) -> float:
99
+ started = time.perf_counter()
100
+ edges = self._mesh_edges()
101
+ timings["mesh_edges"] = time.perf_counter() - started
102
+ if len(edges) == 0:
103
+ return 0.0
104
+
105
+ started = time.perf_counter()
106
+ # set_m normalises stored magnetisation vectors, so maxangle can use
107
+ # the cached nodal rows directly instead of renormalising every edge.
108
+ edge_values = m[edges]
109
+ dots = np.einsum("ij,ij->i", edge_values[:, 0, :], edge_values[:, 1, :])
110
+ if dots.size == 0:
111
+ timings["edge_angles"] = time.perf_counter() - started
112
+ return 0.0
113
+ bounded_dots = np.minimum(np.maximum(dots, -1.0), 1.0)
114
+ maxangle = float(np.max(np.arccos(bounded_dots))) * 180.0 / np.pi
115
+ timings["edge_angles"] = time.perf_counter() - started
116
+ return maxangle
117
+
118
+ def _maxangle_average_rust(self, m: np.ndarray, simplices: np.ndarray) -> float:
119
+ rust_accel = _simulation_compatibility_binding(
120
+ "_load_rust_accelerator", _load_rust_accelerator
121
+ )("NmagConfig.accelerator['maxangle']")
122
+ try:
123
+ return float(rust_accel.maxangle_between_edges(m, simplices))
124
+ except AttributeError as exc:
125
+ raise RuntimeError(
126
+ "NmagConfig.accelerator['maxangle']='rust' requires an nmag_accel build with "
127
+ "maxangle_between_edges. Rebuild it with "
128
+ "`maturin develop --release --manifest-path rust/nmag_accel/Cargo.toml`."
129
+ ) from exc
130
+
131
+ def _maxangle_average_reference(self, field_name: str) -> float | None:
132
+ inputs = self._maxangle_inputs(field_name, None)
133
+ if inputs is None:
134
+ return None
135
+ m, simplices = inputs
136
+ if np.all(m == m[0]):
137
+ return 0.0
138
+ return self._reference_maxangle_from_simplices(m, simplices)
139
+
140
+ def _reference_maxangle_from_simplices(
141
+ self,
142
+ m: np.ndarray,
143
+ simplices: np.ndarray,
144
+ ) -> float:
145
+ max_angle = 0.0
146
+ seen_edges: set[tuple[int, int]] = set()
147
+ for simplex in simplices:
148
+ for left_position, left in enumerate(simplex):
149
+ for right in simplex[left_position + 1 :]:
150
+ edge = (min(int(left), int(right)), max(int(left), int(right)))
151
+ if edge in seen_edges:
152
+ continue
153
+ seen_edges.add(edge)
154
+ left_m = _simulation_compatibility_binding("_normalised", _normalised)(
155
+ m[edge[0]]
156
+ )
157
+ right_m = _simulation_compatibility_binding("_normalised", _normalised)(
158
+ m[edge[1]]
159
+ )
160
+ raw_dot = float(np.sum(left_m * right_m))
161
+ dot = min(1.0, max(-1.0, raw_dot))
162
+ max_angle = max(max_angle, float(np.arccos(dot)))
163
+ return max_angle * 180.0 / np.pi