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,299 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ...demag import _simplex_volumes
|
|
9
|
+
from ..support import _si_unit, _simulation_compatibility_binding
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SimulationMeshMaterialMixin:
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
_simplex_volume_cache: Any | None
|
|
15
|
+
_volume_average_node_weights_cache: Any | None
|
|
16
|
+
_incident_cell_volume_sums_cache: Any | None
|
|
17
|
+
_demag_ms_values_cache: Any | None
|
|
18
|
+
_demag_volume_charge_scales_cache: Any | None
|
|
19
|
+
_nodal_ms_values_cache: Any | None
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
22
|
+
|
|
23
|
+
def _material_region_ids(self, material_name: str) -> np.ndarray:
|
|
24
|
+
region_ids = [
|
|
25
|
+
self.region_id_of_name[region_name]
|
|
26
|
+
for region_name, materials in self.mats_of_region_name.items()
|
|
27
|
+
if any(material.name == material_name for material in materials)
|
|
28
|
+
]
|
|
29
|
+
if not region_ids:
|
|
30
|
+
raise KeyError(f"Unknown material '{material_name}'.")
|
|
31
|
+
return np.asarray(region_ids, dtype=int)
|
|
32
|
+
|
|
33
|
+
def _field_average(
|
|
34
|
+
self,
|
|
35
|
+
data: np.ndarray,
|
|
36
|
+
*,
|
|
37
|
+
mat_name: str | None = None,
|
|
38
|
+
) -> np.ndarray | float:
|
|
39
|
+
if self.mesh is None:
|
|
40
|
+
return np.mean(data, axis=0)
|
|
41
|
+
if data.ndim == 1:
|
|
42
|
+
return self._scalar_field_average(data, mat_name=mat_name)
|
|
43
|
+
if data.ndim != 2:
|
|
44
|
+
return np.mean(data, axis=0)
|
|
45
|
+
if len(data) > 0 and np.all(data == data[0]):
|
|
46
|
+
return np.asarray(data[0], dtype=float)
|
|
47
|
+
|
|
48
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
49
|
+
points = self._mesh_points()
|
|
50
|
+
if (
|
|
51
|
+
len(data) != len(points)
|
|
52
|
+
or simplices.ndim != 2
|
|
53
|
+
or simplices.shape[1] != 4
|
|
54
|
+
or len(simplices) == 0
|
|
55
|
+
):
|
|
56
|
+
return np.mean(data, axis=0)
|
|
57
|
+
|
|
58
|
+
volumes, positive = self._simplex_volume_weights(points, simplices)
|
|
59
|
+
if mat_name is not None:
|
|
60
|
+
regions = np.asarray(self._require_mesh().regions, dtype=int)
|
|
61
|
+
positive = positive & np.isin(regions, self._material_region_ids(mat_name))
|
|
62
|
+
if not np.any(positive):
|
|
63
|
+
return np.mean(data, axis=0)
|
|
64
|
+
|
|
65
|
+
weights = (
|
|
66
|
+
self._volume_average_node_weights(points, simplices, len(data))
|
|
67
|
+
if mat_name is None
|
|
68
|
+
else None
|
|
69
|
+
)
|
|
70
|
+
if weights is None:
|
|
71
|
+
cell_values = np.mean(data[simplices[positive]], axis=1)
|
|
72
|
+
return np.sum(cell_values * volumes[positive, np.newaxis], axis=0) / float(
|
|
73
|
+
np.sum(volumes[positive])
|
|
74
|
+
)
|
|
75
|
+
return weights @ data
|
|
76
|
+
|
|
77
|
+
def _scalar_field_average(
|
|
78
|
+
self,
|
|
79
|
+
data: np.ndarray,
|
|
80
|
+
*,
|
|
81
|
+
mat_name: str | None = None,
|
|
82
|
+
) -> float:
|
|
83
|
+
if len(data) > 0 and np.all(data == data[0]):
|
|
84
|
+
return float(data[0])
|
|
85
|
+
|
|
86
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
87
|
+
points = self._mesh_points()
|
|
88
|
+
if len(data) != len(points) or simplices.ndim != 2 or simplices.shape[1] != 4:
|
|
89
|
+
return float(np.mean(data))
|
|
90
|
+
|
|
91
|
+
volumes, positive = self._simplex_volume_weights(points, simplices)
|
|
92
|
+
if mat_name is not None:
|
|
93
|
+
regions = np.asarray(self._require_mesh().regions, dtype=int)
|
|
94
|
+
positive = positive & np.isin(regions, self._material_region_ids(mat_name))
|
|
95
|
+
if not np.any(positive):
|
|
96
|
+
return float(np.mean(data))
|
|
97
|
+
|
|
98
|
+
weights = (
|
|
99
|
+
self._volume_average_node_weights(points, simplices, len(data))
|
|
100
|
+
if mat_name is None
|
|
101
|
+
else None
|
|
102
|
+
)
|
|
103
|
+
if weights is None:
|
|
104
|
+
cell_values = np.mean(data[simplices[positive]], axis=1)
|
|
105
|
+
return float(np.sum(cell_values * volumes[positive]) / float(np.sum(volumes[positive])))
|
|
106
|
+
return float(weights @ data)
|
|
107
|
+
|
|
108
|
+
def _simplex_volume_weights(
|
|
109
|
+
self,
|
|
110
|
+
points: np.ndarray,
|
|
111
|
+
simplices: np.ndarray,
|
|
112
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
113
|
+
token = self._mesh_geometry_token()
|
|
114
|
+
if self._simplex_volume_cache is not None:
|
|
115
|
+
cached_token, volumes, positive = self._simplex_volume_cache
|
|
116
|
+
if cached_token == token and len(volumes) == len(simplices):
|
|
117
|
+
return volumes, positive
|
|
118
|
+
|
|
119
|
+
volumes = _simulation_compatibility_binding(
|
|
120
|
+
"_simplex_volumes",
|
|
121
|
+
_simplex_volumes,
|
|
122
|
+
)(points, simplices)
|
|
123
|
+
positive = volumes > 0.0
|
|
124
|
+
self._simplex_volume_cache = (token, volumes, positive)
|
|
125
|
+
return volumes, positive
|
|
126
|
+
|
|
127
|
+
def _volume_average_node_weights(
|
|
128
|
+
self,
|
|
129
|
+
points: np.ndarray,
|
|
130
|
+
simplices: np.ndarray,
|
|
131
|
+
point_count: int,
|
|
132
|
+
) -> np.ndarray | None:
|
|
133
|
+
token = self._mesh_geometry_token()
|
|
134
|
+
if self._volume_average_node_weights_cache is not None:
|
|
135
|
+
cached_token, cached_point_count, weights = self._volume_average_node_weights_cache
|
|
136
|
+
if cached_token == token and cached_point_count == point_count:
|
|
137
|
+
return weights
|
|
138
|
+
|
|
139
|
+
volumes, positive = self._simplex_volume_weights(points, simplices)
|
|
140
|
+
if not np.any(positive):
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
weights = np.zeros(point_count, dtype=float)
|
|
144
|
+
positive_simplices = simplices[positive]
|
|
145
|
+
positive_volumes = volumes[positive]
|
|
146
|
+
np.add.at(weights, positive_simplices.flatten(), np.repeat(positive_volumes / 4.0, 4))
|
|
147
|
+
total_volume = float(np.sum(positive_volumes))
|
|
148
|
+
if total_volume <= 0.0:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
weights /= total_volume
|
|
152
|
+
self._volume_average_node_weights_cache = (token, point_count, weights)
|
|
153
|
+
return weights
|
|
154
|
+
|
|
155
|
+
def _incident_cell_volume_sums(
|
|
156
|
+
self,
|
|
157
|
+
points: np.ndarray,
|
|
158
|
+
simplices: np.ndarray,
|
|
159
|
+
volumes: np.ndarray,
|
|
160
|
+
) -> np.ndarray:
|
|
161
|
+
token = self._mesh_geometry_token()
|
|
162
|
+
point_count = len(points)
|
|
163
|
+
cell_count = len(simplices)
|
|
164
|
+
if self._incident_cell_volume_sums_cache is not None:
|
|
165
|
+
cached_token, cached_point_count, cached_cell_count, weights = (
|
|
166
|
+
self._incident_cell_volume_sums_cache
|
|
167
|
+
)
|
|
168
|
+
if (
|
|
169
|
+
cached_token == token
|
|
170
|
+
and cached_point_count == point_count
|
|
171
|
+
and cached_cell_count == cell_count
|
|
172
|
+
):
|
|
173
|
+
return weights
|
|
174
|
+
|
|
175
|
+
weights = np.zeros(point_count, dtype=float)
|
|
176
|
+
positive = volumes > 0.0
|
|
177
|
+
if np.any(positive):
|
|
178
|
+
positive_simplices = simplices[positive]
|
|
179
|
+
positive_volumes = volumes[positive]
|
|
180
|
+
for local_index in range(4):
|
|
181
|
+
np.add.at(
|
|
182
|
+
weights,
|
|
183
|
+
positive_simplices[:, local_index],
|
|
184
|
+
positive_volumes,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
self._incident_cell_volume_sums_cache = (token, point_count, cell_count, weights)
|
|
188
|
+
return weights
|
|
189
|
+
|
|
190
|
+
def _simplex_material(self, region_id: int) -> Any:
|
|
191
|
+
region_name = self.region_name_of_id.get(int(region_id))
|
|
192
|
+
raw_materials: Any = self.mats_of_region_name.get(region_name, []) if region_name else []
|
|
193
|
+
materials = cast(list[Any], raw_materials)
|
|
194
|
+
if not materials:
|
|
195
|
+
raise ValueError(f"Mesh region {region_id} has no configured magnetic material.")
|
|
196
|
+
if len(materials) != 1:
|
|
197
|
+
raise NotImplementedError(
|
|
198
|
+
"Multiple magnetic material subfields in one mesh region are not supported."
|
|
199
|
+
)
|
|
200
|
+
return materials[0]
|
|
201
|
+
|
|
202
|
+
def _simplex_material_ms(self, region_id: int) -> float:
|
|
203
|
+
return self._simplex_material(region_id).Ms.in_units_of(_si_unit("A/m"))
|
|
204
|
+
|
|
205
|
+
def _simplex_material_ms_values(self, regions: Sequence[int]) -> np.ndarray:
|
|
206
|
+
token = self._mesh_geometry_token()
|
|
207
|
+
self._ensure_demag_geometry_cache_token(token)
|
|
208
|
+
if self._demag_ms_values_cache is not None:
|
|
209
|
+
cached_token, cached_values = self._demag_ms_values_cache
|
|
210
|
+
if cached_token == token and len(cached_values) == len(regions):
|
|
211
|
+
return cached_values
|
|
212
|
+
|
|
213
|
+
region_ids = np.asarray(regions, dtype=int)
|
|
214
|
+
if region_ids.size == 0:
|
|
215
|
+
ms_values = np.zeros(0, dtype=float)
|
|
216
|
+
self._demag_ms_values_cache = (token, ms_values)
|
|
217
|
+
return ms_values
|
|
218
|
+
|
|
219
|
+
first_region = int(region_ids[0])
|
|
220
|
+
if np.all(region_ids == first_region):
|
|
221
|
+
ms_values = np.full(
|
|
222
|
+
len(region_ids),
|
|
223
|
+
self._simplex_material_ms(first_region),
|
|
224
|
+
dtype=float,
|
|
225
|
+
)
|
|
226
|
+
self._demag_ms_values_cache = (token, ms_values)
|
|
227
|
+
return ms_values
|
|
228
|
+
|
|
229
|
+
unique_region_ids, inverse = np.unique(region_ids, return_inverse=True)
|
|
230
|
+
ms_lookup = np.asarray(
|
|
231
|
+
[self._simplex_material_ms(int(region)) for region in unique_region_ids],
|
|
232
|
+
dtype=float,
|
|
233
|
+
)
|
|
234
|
+
ms_values = ms_lookup[inverse]
|
|
235
|
+
self._demag_ms_values_cache = (token, ms_values)
|
|
236
|
+
return ms_values
|
|
237
|
+
|
|
238
|
+
def _simplex_volume_charge_scales(self, regions: Sequence[int]) -> np.ndarray:
|
|
239
|
+
token = self._mesh_geometry_token()
|
|
240
|
+
self._ensure_demag_geometry_cache_token(token)
|
|
241
|
+
if self._demag_volume_charge_scales_cache is not None:
|
|
242
|
+
cached_token, cached_values = self._demag_volume_charge_scales_cache
|
|
243
|
+
if cached_token == token and len(cached_values) == len(regions):
|
|
244
|
+
return cached_values
|
|
245
|
+
|
|
246
|
+
region_ids = np.asarray(regions, dtype=int)
|
|
247
|
+
if region_ids.size == 0:
|
|
248
|
+
scales = np.zeros(0, dtype=float)
|
|
249
|
+
else:
|
|
250
|
+
unique_region_ids, inverse = np.unique(region_ids, return_inverse=True)
|
|
251
|
+
scale_lookup = np.asarray(
|
|
252
|
+
[
|
|
253
|
+
float(self._simplex_material(int(region)).scale_volume_charges)
|
|
254
|
+
for region in unique_region_ids
|
|
255
|
+
],
|
|
256
|
+
dtype=float,
|
|
257
|
+
)
|
|
258
|
+
scales = scale_lookup[inverse]
|
|
259
|
+
|
|
260
|
+
self._demag_volume_charge_scales_cache = (token, scales)
|
|
261
|
+
return scales
|
|
262
|
+
|
|
263
|
+
def _nodal_ms_values(self) -> np.ndarray:
|
|
264
|
+
if self.mesh is None:
|
|
265
|
+
raise RuntimeError("A mesh must be loaded before using material fields.")
|
|
266
|
+
|
|
267
|
+
token = self._mesh_geometry_token()
|
|
268
|
+
if self._nodal_ms_values_cache is not None:
|
|
269
|
+
cached_token, cached_values = self._nodal_ms_values_cache
|
|
270
|
+
if cached_token == token:
|
|
271
|
+
return cached_values
|
|
272
|
+
|
|
273
|
+
points = self._mesh_points()
|
|
274
|
+
simplices = np.asarray(self.mesh.simplices, dtype=int)
|
|
275
|
+
if simplices.size == 0 or simplices.ndim != 2 or simplices.shape[1] != 4:
|
|
276
|
+
default_ms = (
|
|
277
|
+
self.materials[0].Ms.in_units_of(_si_unit("A/m")) if self.materials else 0.0
|
|
278
|
+
)
|
|
279
|
+
nodal_ms = np.full(len(points), default_ms, dtype=float)
|
|
280
|
+
self._nodal_ms_values_cache = (token, nodal_ms)
|
|
281
|
+
return nodal_ms
|
|
282
|
+
|
|
283
|
+
regions = list(self.mesh.regions or [1] * len(simplices))
|
|
284
|
+
simplex_ms = self._simplex_material_ms_values(regions)
|
|
285
|
+
volumes, positive = self._simplex_volume_weights(points, simplices)
|
|
286
|
+
nodal_ms = np.zeros(len(points), dtype=float)
|
|
287
|
+
weights = self._incident_cell_volume_sums(points, simplices, volumes)
|
|
288
|
+
if np.any(positive):
|
|
289
|
+
positive_simplices = simplices[positive]
|
|
290
|
+
weighted_ms = simplex_ms[positive] * volumes[positive]
|
|
291
|
+
for local_index in range(4):
|
|
292
|
+
np.add.at(nodal_ms, positive_simplices[:, local_index], weighted_ms)
|
|
293
|
+
|
|
294
|
+
present = weights > 0.0
|
|
295
|
+
nodal_ms[present] /= weights[present]
|
|
296
|
+
if np.any(~present):
|
|
297
|
+
nodal_ms[~present] = simplex_ms[positive][0] if np.any(positive) else 0.0
|
|
298
|
+
self._nodal_ms_values_cache = (token, nodal_ms)
|
|
299
|
+
return nodal_ms
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ...backends import (
|
|
10
|
+
_load_rust_accelerator,
|
|
11
|
+
_selected_probe_geometry_backend,
|
|
12
|
+
)
|
|
13
|
+
from ...demag import _tetrahedral_barycentric_coordinates
|
|
14
|
+
from ..support import _simulation_compatibility_binding
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SimulationMeshProbeMixin:
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
_probe_geometry_cache_token: Any | None
|
|
20
|
+
_probe_tetrahedral_cache: Any | None
|
|
21
|
+
|
|
22
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
23
|
+
|
|
24
|
+
def _probe_is_within_mesh_bounds(self, pos: Sequence[float]) -> bool:
|
|
25
|
+
probe = np.asarray(pos, dtype=float)
|
|
26
|
+
if probe.shape != (3,):
|
|
27
|
+
raise ValueError(f"Probe position must be a 3-vector, got shape {probe.shape}.")
|
|
28
|
+
|
|
29
|
+
lower, upper, span = self._mesh_bounds()
|
|
30
|
+
tolerance = 1.0e-12 * span
|
|
31
|
+
return bool(np.all(probe >= lower - tolerance) and np.all(probe <= upper + tolerance))
|
|
32
|
+
|
|
33
|
+
def _probe_is_legacy_null_vertex(self, probe: np.ndarray, points: np.ndarray) -> bool:
|
|
34
|
+
"""Legacy nmag can return None at exact boundary mesh vertex 0."""
|
|
35
|
+
if len(points) == 0 or not np.array_equal(probe, points[0]):
|
|
36
|
+
return False
|
|
37
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
38
|
+
boundary_nodes = {
|
|
39
|
+
point_index
|
|
40
|
+
for _owner, face in self._boundary_faces_for_demag_mesh(points, simplices)
|
|
41
|
+
for point_index in face
|
|
42
|
+
}
|
|
43
|
+
return 0 in boundary_nodes
|
|
44
|
+
|
|
45
|
+
def _probe_tetrahedral_field(
|
|
46
|
+
self,
|
|
47
|
+
probe: np.ndarray,
|
|
48
|
+
nodal_values: np.ndarray,
|
|
49
|
+
) -> list[float] | None:
|
|
50
|
+
simplices, origins, inverse_matrices, lower, upper = self._probe_tetrahedral_geometry()
|
|
51
|
+
if len(simplices) == 0:
|
|
52
|
+
return None
|
|
53
|
+
tolerance = 1.0e-12
|
|
54
|
+
started = time.perf_counter()
|
|
55
|
+
candidate_indices = np.flatnonzero(
|
|
56
|
+
np.all(probe >= lower - tolerance, axis=1) & np.all(probe <= upper + tolerance, axis=1)
|
|
57
|
+
)
|
|
58
|
+
self._record_probe_timing(
|
|
59
|
+
"tetrahedral_candidate_search",
|
|
60
|
+
time.perf_counter() - started,
|
|
61
|
+
)
|
|
62
|
+
if len(candidate_indices) == 0:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
started = time.perf_counter()
|
|
66
|
+
offsets = probe - origins[candidate_indices]
|
|
67
|
+
local = np.einsum(
|
|
68
|
+
"nij,nj->ni",
|
|
69
|
+
inverse_matrices[candidate_indices],
|
|
70
|
+
offsets,
|
|
71
|
+
)
|
|
72
|
+
barycentric = np.column_stack(
|
|
73
|
+
[
|
|
74
|
+
1.0 - np.sum(local, axis=1),
|
|
75
|
+
local,
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
inside = np.all(barycentric >= -tolerance, axis=1) & np.all(
|
|
79
|
+
barycentric <= 1.0 + tolerance,
|
|
80
|
+
axis=1,
|
|
81
|
+
)
|
|
82
|
+
self._record_probe_timing(
|
|
83
|
+
"tetrahedral_barycentric",
|
|
84
|
+
time.perf_counter() - started,
|
|
85
|
+
)
|
|
86
|
+
if not np.any(inside):
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
started = time.perf_counter()
|
|
90
|
+
candidate_position = int(np.flatnonzero(inside)[0])
|
|
91
|
+
cache_index = int(candidate_indices[candidate_position])
|
|
92
|
+
simplex = simplices[cache_index]
|
|
93
|
+
clipped = np.minimum(np.maximum(barycentric[candidate_position], 0.0), 1.0)
|
|
94
|
+
result = (clipped @ nodal_values[simplex]).tolist()
|
|
95
|
+
self._record_probe_timing(
|
|
96
|
+
"tetrahedral_value_interpolation",
|
|
97
|
+
time.perf_counter() - started,
|
|
98
|
+
)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
def _probe_tetrahedral_geometry(
|
|
102
|
+
self,
|
|
103
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
104
|
+
total_started = time.perf_counter()
|
|
105
|
+
token = self._mesh_geometry_token()
|
|
106
|
+
if self._probe_geometry_cache_token == token and self._probe_tetrahedral_cache is not None:
|
|
107
|
+
self._record_probe_timing("tetrahedral_geometry_cache_hit", 0.0)
|
|
108
|
+
self._record_probe_timing(
|
|
109
|
+
"tetrahedral_geometry",
|
|
110
|
+
time.perf_counter() - total_started,
|
|
111
|
+
)
|
|
112
|
+
return self._probe_tetrahedral_cache
|
|
113
|
+
|
|
114
|
+
build_started = time.perf_counter()
|
|
115
|
+
backend = _selected_probe_geometry_backend(getattr(self, "config", None))
|
|
116
|
+
self._probe_tetrahedral_cache = (
|
|
117
|
+
self._probe_tetrahedral_geometry_rust()
|
|
118
|
+
if backend == "rust"
|
|
119
|
+
else self._probe_tetrahedral_geometry_python()
|
|
120
|
+
)
|
|
121
|
+
self._probe_geometry_cache_token = token
|
|
122
|
+
self._record_probe_timing(
|
|
123
|
+
f"tetrahedral_geometry_build:{backend}",
|
|
124
|
+
time.perf_counter() - build_started,
|
|
125
|
+
)
|
|
126
|
+
self._record_probe_timing(
|
|
127
|
+
"tetrahedral_geometry_build",
|
|
128
|
+
time.perf_counter() - build_started,
|
|
129
|
+
)
|
|
130
|
+
self._record_probe_timing(
|
|
131
|
+
"tetrahedral_geometry",
|
|
132
|
+
time.perf_counter() - total_started,
|
|
133
|
+
)
|
|
134
|
+
return self._probe_tetrahedral_cache
|
|
135
|
+
|
|
136
|
+
def _probe_tetrahedral_geometry_python(
|
|
137
|
+
self,
|
|
138
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
139
|
+
points = self._mesh_points()
|
|
140
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
141
|
+
if simplices.size == 0:
|
|
142
|
+
empty_simplices = np.zeros((0, 4), dtype=int)
|
|
143
|
+
empty_vectors = np.zeros((0, 3), dtype=float)
|
|
144
|
+
empty_matrices = np.zeros((0, 3, 3), dtype=float)
|
|
145
|
+
return (
|
|
146
|
+
empty_simplices,
|
|
147
|
+
empty_vectors,
|
|
148
|
+
empty_matrices,
|
|
149
|
+
empty_vectors,
|
|
150
|
+
empty_vectors,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
valid_simplices: list[np.ndarray] = []
|
|
154
|
+
origins: list[np.ndarray] = []
|
|
155
|
+
inverse_matrices: list[np.ndarray] = []
|
|
156
|
+
lower: list[np.ndarray] = []
|
|
157
|
+
upper: list[np.ndarray] = []
|
|
158
|
+
for simplex in simplices:
|
|
159
|
+
tetrahedron = points[simplex]
|
|
160
|
+
matrix = np.column_stack(
|
|
161
|
+
[
|
|
162
|
+
tetrahedron[1] - tetrahedron[0],
|
|
163
|
+
tetrahedron[2] - tetrahedron[0],
|
|
164
|
+
tetrahedron[3] - tetrahedron[0],
|
|
165
|
+
]
|
|
166
|
+
)
|
|
167
|
+
try:
|
|
168
|
+
inverse = np.linalg.inv(matrix)
|
|
169
|
+
except np.linalg.LinAlgError:
|
|
170
|
+
continue
|
|
171
|
+
valid_simplices.append(simplex)
|
|
172
|
+
origins.append(tetrahedron[0])
|
|
173
|
+
inverse_matrices.append(inverse)
|
|
174
|
+
lower.append(np.min(tetrahedron, axis=0))
|
|
175
|
+
upper.append(np.max(tetrahedron, axis=0))
|
|
176
|
+
|
|
177
|
+
if not valid_simplices:
|
|
178
|
+
empty_vectors = np.empty((0, 3), dtype=float)
|
|
179
|
+
return (
|
|
180
|
+
np.empty((0, 4), dtype=int),
|
|
181
|
+
empty_vectors,
|
|
182
|
+
np.empty((0, 3, 3), dtype=float),
|
|
183
|
+
empty_vectors,
|
|
184
|
+
empty_vectors,
|
|
185
|
+
)
|
|
186
|
+
return (
|
|
187
|
+
np.asarray(valid_simplices, dtype=int),
|
|
188
|
+
np.asarray(origins, dtype=float),
|
|
189
|
+
np.asarray(inverse_matrices, dtype=float),
|
|
190
|
+
np.asarray(lower, dtype=float),
|
|
191
|
+
np.asarray(upper, dtype=float),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _probe_tetrahedral_geometry_rust(
|
|
195
|
+
self,
|
|
196
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
197
|
+
rust_accel = _simulation_compatibility_binding(
|
|
198
|
+
"_load_rust_accelerator",
|
|
199
|
+
_load_rust_accelerator,
|
|
200
|
+
)("NmagConfig.accelerator['probe_geometry']")
|
|
201
|
+
geometry = rust_accel.build_probe_tetrahedral_geometry(
|
|
202
|
+
np.asarray(self._mesh_points(), dtype=np.float64),
|
|
203
|
+
np.asarray(self._require_mesh().simplices, dtype=np.int64),
|
|
204
|
+
)
|
|
205
|
+
valid_simplices, origins, inverse_matrices, lower, upper = geometry
|
|
206
|
+
return (
|
|
207
|
+
np.asarray(valid_simplices, dtype=int),
|
|
208
|
+
np.asarray(origins, dtype=float),
|
|
209
|
+
np.asarray(inverse_matrices, dtype=float),
|
|
210
|
+
np.asarray(lower, dtype=float),
|
|
211
|
+
np.asarray(upper, dtype=float),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _probe_tetrahedral_field_reference(
|
|
215
|
+
self,
|
|
216
|
+
probe: np.ndarray,
|
|
217
|
+
nodal_values: np.ndarray,
|
|
218
|
+
) -> list[float] | None:
|
|
219
|
+
"""Scalar reference path for checking cached probe interpolation."""
|
|
220
|
+
if self.mesh is None:
|
|
221
|
+
raise RuntimeError("A mesh must be loaded before probing fields.")
|
|
222
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
223
|
+
if simplices.size == 0:
|
|
224
|
+
return None
|
|
225
|
+
points = self._mesh_points()
|
|
226
|
+
for simplex in simplices:
|
|
227
|
+
barycentric = _tetrahedral_barycentric_coordinates(
|
|
228
|
+
points[simplex],
|
|
229
|
+
probe,
|
|
230
|
+
)
|
|
231
|
+
if barycentric is None:
|
|
232
|
+
continue
|
|
233
|
+
return (barycentric @ nodal_values[simplex]).tolist()
|
|
234
|
+
return None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from si.physical import SI
|
|
10
|
+
|
|
11
|
+
from ..checkpoint import read_checkpoint, runtime_state, save_checkpoint, validate_mesh
|
|
12
|
+
from ..dynamics import IntegratorStats
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimulationRestartMixin:
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
|
|
18
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
19
|
+
|
|
20
|
+
def get_restart_file_name(self) -> Path:
|
|
21
|
+
"""Return the default native checkpoint path beside simulation output."""
|
|
22
|
+
return self.writer.h5_filename.with_name(f"{self.name}_restart.h5")
|
|
23
|
+
|
|
24
|
+
def save_restart_file(self, filename: str | Path | None = None) -> Path:
|
|
25
|
+
"""Atomically save a complete native checkpoint.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
filename: Destination path, or ``None`` for the simulation's default
|
|
29
|
+
restart filename.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The checkpoint path.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
RuntimeError: If mesh or magnetization state is incomplete.
|
|
36
|
+
"""
|
|
37
|
+
destination = self.get_restart_file_name() if filename is None else Path(filename)
|
|
38
|
+
return save_checkpoint(self, destination)
|
|
39
|
+
|
|
40
|
+
def load_m_from_h5file(self, filename: str | Path) -> None:
|
|
41
|
+
"""Load only checkpoint magnetization into the configured simulation.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
filename: Native checkpoint created for the same mesh.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If the checkpoint or mesh is incompatible.
|
|
48
|
+
"""
|
|
49
|
+
point_count = len(self._mesh_points())
|
|
50
|
+
contents = read_checkpoint(Path(filename), point_count)
|
|
51
|
+
validate_mesh(self, contents)
|
|
52
|
+
self._fields["m"] = np.array(contents.magnetisation, dtype=float, copy=True)
|
|
53
|
+
self._invalidate_after_checkpoint_restore()
|
|
54
|
+
|
|
55
|
+
def load_restart_file(self, filename: str | Path | None = None) -> None:
|
|
56
|
+
"""Restore complete native state into a compatible loaded simulation.
|
|
57
|
+
|
|
58
|
+
The target simulation must already have the same mesh and compatible
|
|
59
|
+
materials. Magnetization, pinning, current density, applied field,
|
|
60
|
+
clock, integrator controls, and convergence state are restored.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
filename: Source path, or ``None`` for the default restart filename.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If checkpoint schema, mesh, materials, or dimensions are
|
|
67
|
+
incompatible.
|
|
68
|
+
"""
|
|
69
|
+
source = self.get_restart_file_name() if filename is None else Path(filename)
|
|
70
|
+
point_count = len(self._mesh_points())
|
|
71
|
+
contents = read_checkpoint(source, point_count)
|
|
72
|
+
runtime = runtime_state(self, contents)
|
|
73
|
+
|
|
74
|
+
self._fields["m"] = np.array(contents.magnetisation, dtype=float, copy=True)
|
|
75
|
+
self._fields["pin"] = np.array(contents.pinning, dtype=float, copy=True)
|
|
76
|
+
self._fields["H_ext"] = np.array(contents.external_field, dtype=float, copy=True)
|
|
77
|
+
if contents.current_density is None:
|
|
78
|
+
self._fields.pop("current_density", None)
|
|
79
|
+
else:
|
|
80
|
+
self._fields["current_density"] = np.array(
|
|
81
|
+
contents.current_density,
|
|
82
|
+
dtype=float,
|
|
83
|
+
copy=True,
|
|
84
|
+
)
|
|
85
|
+
self.clock = runtime.clock
|
|
86
|
+
self._integrator_config = runtime.integrator_config
|
|
87
|
+
self.stopping_dm_dt = runtime.stopping_dm_dt
|
|
88
|
+
self.max_time_reached = SI(runtime.maximum_time_seconds, "s")
|
|
89
|
+
self.max_dm_dt = runtime.maximum_dm_dt
|
|
90
|
+
self.convergence = runtime.convergence
|
|
91
|
+
self._restarting = False
|
|
92
|
+
self._invalidate_after_checkpoint_restore()
|
|
93
|
+
|
|
94
|
+
def _invalidate_after_checkpoint_restore(self) -> None:
|
|
95
|
+
self._invalidate_demag()
|
|
96
|
+
self._subfield_array_cache = None
|
|
97
|
+
self._subfield_average_cache = None
|
|
98
|
+
self._integrator = None
|
|
99
|
+
self._integrator_is_stale = True
|
|
100
|
+
self._integrator_rhs_evaluations = 0
|
|
101
|
+
self._integrator_effective_max_step_seconds = self._integrator_config.maximum_step_seconds
|
|
102
|
+
self._last_integrator_stats = IntegratorStats(status="restarted")
|
|
103
|
+
self._stage_wall_started = time.perf_counter()
|