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.
- anisotropy/__init__.py +28 -0
- anisotropy/anisotropy.py +13 -0
- anisotropy/evaluation.py +70 -0
- anisotropy/model.py +200 -0
- anisotropy/predefined.py +202 -0
- anisotropy/py.typed +1 -0
- anisotropy/values.py +84 -0
- mag_material/__init__.py +3 -0
- mag_material/mag_material.py +231 -0
- mag_material/parameters.py +150 -0
- mag_material/py.typed +1 -0
- nmag/__init__.py +36 -0
- nmag/backends.py +493 -0
- nmag/checkpoint.py +327 -0
- nmag/config.py +174 -0
- nmag/demag/__init__.py +39 -0
- nmag/demag/bem_operator.py +149 -0
- nmag/demag/geometry.py +134 -0
- nmag/demag/lindholm.py +133 -0
- nmag/demag/lindholm_fast.py +463 -0
- nmag/demag/linear.py +489 -0
- nmag/dynamics/__init__.py +255 -0
- nmag/output.py +27 -0
- nmag/parallel.py +45 -0
- nmag/py.typed +1 -0
- nmag/resources.py +38 -0
- nmag/simulation/__init__.py +519 -0
- nmag/simulation/anisotropy/__init__.py +5 -0
- nmag/simulation/anisotropy/fields.py +56 -0
- nmag/simulation/anisotropy/materials.py +138 -0
- nmag/simulation/demag/__init__.py +1 -0
- nmag/simulation/demag/bem/__init__.py +11 -0
- nmag/simulation/demag/bem/diagnostics.py +73 -0
- nmag/simulation/demag/bem/dirichlet.py +85 -0
- nmag/simulation/demag/bem/hierarchical.py +74 -0
- nmag/simulation/demag/bem/operator.py +292 -0
- nmag/simulation/demag/fem/__init__.py +11 -0
- nmag/simulation/demag/fem/assembly.py +114 -0
- nmag/simulation/demag/fem/charges.py +38 -0
- nmag/simulation/demag/fem/geometry.py +261 -0
- nmag/simulation/demag/fields/__init__.py +11 -0
- nmag/simulation/demag/fields/auxiliary.py +186 -0
- nmag/simulation/demag/fields/probe.py +77 -0
- nmag/simulation/demag/fields/recovery.py +207 -0
- nmag/simulation/demag/solver.py +17 -0
- nmag/simulation/dynamics/__init__.py +92 -0
- nmag/simulation/dynamics/advance.py +199 -0
- nmag/simulation/dynamics/integrator.py +263 -0
- nmag/simulation/exchange/__init__.py +13 -0
- nmag/simulation/exchange/coefficients.py +185 -0
- nmag/simulation/exchange/fields.py +106 -0
- nmag/simulation/exchange/llg_rhs.py +218 -0
- nmag/simulation/fields/__init__.py +19 -0
- nmag/simulation/fields/arrays.py +111 -0
- nmag/simulation/fields/availability.py +188 -0
- nmag/simulation/fields/averages.py +293 -0
- nmag/simulation/fields/derived.py +164 -0
- nmag/simulation/fields/maxangle.py +163 -0
- nmag/simulation/fields/probes.py +106 -0
- nmag/simulation/implicit_dynamics.py +221 -0
- nmag/simulation/mesh/__init__.py +13 -0
- nmag/simulation/mesh/geometry.py +153 -0
- nmag/simulation/mesh/materials.py +299 -0
- nmag/simulation/mesh/probe.py +234 -0
- nmag/simulation/restart.py +103 -0
- nmag/simulation/support.py +224 -0
- nmag_python_3-0.0.2.dist-info/METADATA +157 -0
- nmag_python_3-0.0.2.dist-info/RECORD +147 -0
- nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
- nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
- nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
- nmesh/__init__.py +130 -0
- nmesh/backend.py +286 -0
- nmesh/geometry/__init__.py +52 -0
- nmesh/geometry/boolean_operations.py +157 -0
- nmesh/geometry/primitives.py +453 -0
- nmesh/geometry/transform.py +126 -0
- nmesh/io/__init__.py +50 -0
- nmesh/io/ascii.py +132 -0
- nmesh/io/legacy_nmesh_hdf5.py +318 -0
- nmesh/io/meshio_support.py +170 -0
- nmesh/mesh_generation.py +182 -0
- nmesh/mesh_io.py +227 -0
- nmesh/mesh_model.py +147 -0
- nmesh/mesh_utilities.py +79 -0
- nmesh/mesher/__init__.py +21 -0
- nmesh/mesher/driver.py +146 -0
- nmesh/mesher/meshing_defaults.py +252 -0
- nmesh/mesher/meshing_parameters.py +185 -0
- nmesh/mesher/parity.py +21 -0
- nmesh/mesher/parity_canonical.py +142 -0
- nmesh/mesher/parity_comparison.py +191 -0
- nmesh/mesher/parity_metrics.py +114 -0
- nmesh/mesher/periodic.py +97 -0
- nmesh/mesher/relaxation/__init__.py +14 -0
- nmesh/mesher/relaxation/_constants.py +20 -0
- nmesh/mesher/relaxation/_types.py +15 -0
- nmesh/mesher/relaxation/density.py +170 -0
- nmesh/mesher/relaxation/engine/__init__.py +18 -0
- nmesh/mesher/relaxation/engine/state.py +155 -0
- nmesh/mesher/relaxation/engine/steps.py +248 -0
- nmesh/mesher/relaxation/engine/topology.py +230 -0
- nmesh/mesher/relaxation/forces/__init__.py +96 -0
- nmesh/mesher/relaxation/forces/jit.py +102 -0
- nmesh/mesher/relaxation/forces/neighbors.py +186 -0
- nmesh/mesher/relaxation/forces/simplex.py +302 -0
- nmesh/mesher/relaxation/forces/summary.py +207 -0
- nmesh/mesher/relaxation/forces/types.py +92 -0
- nmesh/mesher/relaxation/geometry/__init__.py +6 -0
- nmesh/mesher/relaxation/geometry/builder.py +154 -0
- nmesh/mesher/relaxation/geometry/model.py +194 -0
- nmesh/mesher/relaxation/seeding/__init__.py +74 -0
- nmesh/mesher/relaxation/seeding/periodic.py +88 -0
- nmesh/mesher/relaxation/seeding/points.py +88 -0
- nmesh/mesher/relaxation/seeding/sampling.py +142 -0
- nmesh/mesher/relaxation/topology/__init__.py +297 -0
- nmesh/mesher/relaxation/topology/finalize.py +78 -0
- nmesh/mesher/relaxation/topology/recovery.py +310 -0
- nmesh/mesher/sectioned_config.py +70 -0
- nmesh/nmesh.py +99 -0
- nmesh/py.typed +1 -0
- nmesh/utils/__init__.py +33 -0
- nmesh/utils/array_list_utils.py +128 -0
- nmesh/utils/constants.py +22 -0
- nmesh/utils/timing_memory_utils.py +51 -0
- nmesh/utils/types.py +13 -0
- si/constants.py +49 -0
- si/physical.py +722 -0
- si/py.typed +1 -0
- simulation/__init__.py +1 -0
- simulation/clock.py +237 -0
- simulation/data_writer.py +273 -0
- simulation/data_writer_collection.py +267 -0
- simulation/hysteresis.py +74 -0
- simulation/hysteresis_runner.py +286 -0
- simulation/hysteresis_schedule.py +180 -0
- simulation/inference/__init__.py +3 -0
- simulation/inference/inference.py +95 -0
- simulation/py.typed +1 -0
- simulation/quantity.py +88 -0
- simulation/simulation_core.py +458 -0
- throttler/__init__.py +3 -0
- throttler/py.typed +1 -0
- throttler/throttler.py +55 -0
- when/__init__.py +3 -0
- when/py.typed +1 -0
- 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
|