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,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)
|