nmag-python-3 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. anisotropy/__init__.py +28 -0
  2. anisotropy/anisotropy.py +13 -0
  3. anisotropy/evaluation.py +70 -0
  4. anisotropy/model.py +200 -0
  5. anisotropy/predefined.py +202 -0
  6. anisotropy/py.typed +1 -0
  7. anisotropy/values.py +84 -0
  8. mag_material/__init__.py +3 -0
  9. mag_material/mag_material.py +231 -0
  10. mag_material/parameters.py +150 -0
  11. mag_material/py.typed +1 -0
  12. nmag/__init__.py +36 -0
  13. nmag/backends.py +493 -0
  14. nmag/checkpoint.py +327 -0
  15. nmag/config.py +174 -0
  16. nmag/demag/__init__.py +39 -0
  17. nmag/demag/bem_operator.py +149 -0
  18. nmag/demag/geometry.py +134 -0
  19. nmag/demag/lindholm.py +133 -0
  20. nmag/demag/lindholm_fast.py +463 -0
  21. nmag/demag/linear.py +489 -0
  22. nmag/dynamics/__init__.py +255 -0
  23. nmag/output.py +27 -0
  24. nmag/parallel.py +45 -0
  25. nmag/py.typed +1 -0
  26. nmag/resources.py +38 -0
  27. nmag/simulation/__init__.py +519 -0
  28. nmag/simulation/anisotropy/__init__.py +5 -0
  29. nmag/simulation/anisotropy/fields.py +56 -0
  30. nmag/simulation/anisotropy/materials.py +138 -0
  31. nmag/simulation/demag/__init__.py +1 -0
  32. nmag/simulation/demag/bem/__init__.py +11 -0
  33. nmag/simulation/demag/bem/diagnostics.py +73 -0
  34. nmag/simulation/demag/bem/dirichlet.py +85 -0
  35. nmag/simulation/demag/bem/hierarchical.py +74 -0
  36. nmag/simulation/demag/bem/operator.py +292 -0
  37. nmag/simulation/demag/fem/__init__.py +11 -0
  38. nmag/simulation/demag/fem/assembly.py +114 -0
  39. nmag/simulation/demag/fem/charges.py +38 -0
  40. nmag/simulation/demag/fem/geometry.py +261 -0
  41. nmag/simulation/demag/fields/__init__.py +11 -0
  42. nmag/simulation/demag/fields/auxiliary.py +186 -0
  43. nmag/simulation/demag/fields/probe.py +77 -0
  44. nmag/simulation/demag/fields/recovery.py +207 -0
  45. nmag/simulation/demag/solver.py +17 -0
  46. nmag/simulation/dynamics/__init__.py +92 -0
  47. nmag/simulation/dynamics/advance.py +199 -0
  48. nmag/simulation/dynamics/integrator.py +263 -0
  49. nmag/simulation/exchange/__init__.py +13 -0
  50. nmag/simulation/exchange/coefficients.py +185 -0
  51. nmag/simulation/exchange/fields.py +106 -0
  52. nmag/simulation/exchange/llg_rhs.py +218 -0
  53. nmag/simulation/fields/__init__.py +19 -0
  54. nmag/simulation/fields/arrays.py +111 -0
  55. nmag/simulation/fields/availability.py +188 -0
  56. nmag/simulation/fields/averages.py +293 -0
  57. nmag/simulation/fields/derived.py +164 -0
  58. nmag/simulation/fields/maxangle.py +163 -0
  59. nmag/simulation/fields/probes.py +106 -0
  60. nmag/simulation/implicit_dynamics.py +221 -0
  61. nmag/simulation/mesh/__init__.py +13 -0
  62. nmag/simulation/mesh/geometry.py +153 -0
  63. nmag/simulation/mesh/materials.py +299 -0
  64. nmag/simulation/mesh/probe.py +234 -0
  65. nmag/simulation/restart.py +103 -0
  66. nmag/simulation/support.py +224 -0
  67. nmag_python_3-0.0.2.dist-info/METADATA +157 -0
  68. nmag_python_3-0.0.2.dist-info/RECORD +147 -0
  69. nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
  70. nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
  71. nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
  72. nmesh/__init__.py +130 -0
  73. nmesh/backend.py +286 -0
  74. nmesh/geometry/__init__.py +52 -0
  75. nmesh/geometry/boolean_operations.py +157 -0
  76. nmesh/geometry/primitives.py +453 -0
  77. nmesh/geometry/transform.py +126 -0
  78. nmesh/io/__init__.py +50 -0
  79. nmesh/io/ascii.py +132 -0
  80. nmesh/io/legacy_nmesh_hdf5.py +318 -0
  81. nmesh/io/meshio_support.py +170 -0
  82. nmesh/mesh_generation.py +182 -0
  83. nmesh/mesh_io.py +227 -0
  84. nmesh/mesh_model.py +147 -0
  85. nmesh/mesh_utilities.py +79 -0
  86. nmesh/mesher/__init__.py +21 -0
  87. nmesh/mesher/driver.py +146 -0
  88. nmesh/mesher/meshing_defaults.py +252 -0
  89. nmesh/mesher/meshing_parameters.py +185 -0
  90. nmesh/mesher/parity.py +21 -0
  91. nmesh/mesher/parity_canonical.py +142 -0
  92. nmesh/mesher/parity_comparison.py +191 -0
  93. nmesh/mesher/parity_metrics.py +114 -0
  94. nmesh/mesher/periodic.py +97 -0
  95. nmesh/mesher/relaxation/__init__.py +14 -0
  96. nmesh/mesher/relaxation/_constants.py +20 -0
  97. nmesh/mesher/relaxation/_types.py +15 -0
  98. nmesh/mesher/relaxation/density.py +170 -0
  99. nmesh/mesher/relaxation/engine/__init__.py +18 -0
  100. nmesh/mesher/relaxation/engine/state.py +155 -0
  101. nmesh/mesher/relaxation/engine/steps.py +248 -0
  102. nmesh/mesher/relaxation/engine/topology.py +230 -0
  103. nmesh/mesher/relaxation/forces/__init__.py +96 -0
  104. nmesh/mesher/relaxation/forces/jit.py +102 -0
  105. nmesh/mesher/relaxation/forces/neighbors.py +186 -0
  106. nmesh/mesher/relaxation/forces/simplex.py +302 -0
  107. nmesh/mesher/relaxation/forces/summary.py +207 -0
  108. nmesh/mesher/relaxation/forces/types.py +92 -0
  109. nmesh/mesher/relaxation/geometry/__init__.py +6 -0
  110. nmesh/mesher/relaxation/geometry/builder.py +154 -0
  111. nmesh/mesher/relaxation/geometry/model.py +194 -0
  112. nmesh/mesher/relaxation/seeding/__init__.py +74 -0
  113. nmesh/mesher/relaxation/seeding/periodic.py +88 -0
  114. nmesh/mesher/relaxation/seeding/points.py +88 -0
  115. nmesh/mesher/relaxation/seeding/sampling.py +142 -0
  116. nmesh/mesher/relaxation/topology/__init__.py +297 -0
  117. nmesh/mesher/relaxation/topology/finalize.py +78 -0
  118. nmesh/mesher/relaxation/topology/recovery.py +310 -0
  119. nmesh/mesher/sectioned_config.py +70 -0
  120. nmesh/nmesh.py +99 -0
  121. nmesh/py.typed +1 -0
  122. nmesh/utils/__init__.py +33 -0
  123. nmesh/utils/array_list_utils.py +128 -0
  124. nmesh/utils/constants.py +22 -0
  125. nmesh/utils/timing_memory_utils.py +51 -0
  126. nmesh/utils/types.py +13 -0
  127. si/constants.py +49 -0
  128. si/physical.py +722 -0
  129. si/py.typed +1 -0
  130. simulation/__init__.py +1 -0
  131. simulation/clock.py +237 -0
  132. simulation/data_writer.py +273 -0
  133. simulation/data_writer_collection.py +267 -0
  134. simulation/hysteresis.py +74 -0
  135. simulation/hysteresis_runner.py +286 -0
  136. simulation/hysteresis_schedule.py +180 -0
  137. simulation/inference/__init__.py +3 -0
  138. simulation/inference/inference.py +95 -0
  139. simulation/py.typed +1 -0
  140. simulation/quantity.py +88 -0
  141. simulation/simulation_core.py +458 -0
  142. throttler/__init__.py +3 -0
  143. throttler/py.typed +1 -0
  144. throttler/throttler.py +55 -0
  145. when/__init__.py +3 -0
  146. when/py.typed +1 -0
  147. when/when.py +416 -0
@@ -0,0 +1,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()