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,114 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import numpy as np
6
+
7
+ from ....backends import (
8
+ _load_rust_accelerator,
9
+ _selected_demag_fem_assembly_backend,
10
+ )
11
+ from ...support import _simulation_compatibility_binding
12
+ from .charges import volume_charge_scaling_correction
13
+
14
+
15
+ class SimulationDemagFemAssemblyMixin:
16
+ if TYPE_CHECKING:
17
+
18
+ def __getattr__(self, name: str) -> Any: ...
19
+
20
+ def _assemble_demag_fem_system(
21
+ self,
22
+ points: np.ndarray,
23
+ simplices: np.ndarray,
24
+ m: np.ndarray,
25
+ ms_values: np.ndarray,
26
+ volume_charge_scales: np.ndarray,
27
+ ) -> tuple[Any, np.ndarray, np.ndarray]:
28
+ point_count = len(points)
29
+ stiffness, gradients_by_cell, volumes = self._demag_fem_geometry_for_mesh(
30
+ points,
31
+ simplices,
32
+ )
33
+ assembly_backend = _selected_demag_fem_assembly_backend(
34
+ len(simplices), getattr(self, "config", None)
35
+ )
36
+ with self._record_active_subfield_array_timing_block(
37
+ f"demag_auxiliary:assemble_fem:divergence:{assembly_backend}",
38
+ ):
39
+ if assembly_backend == "rust":
40
+ divergence = self._assemble_demag_fem_divergence_rust(
41
+ simplices,
42
+ gradients_by_cell,
43
+ volumes,
44
+ m,
45
+ ms_values,
46
+ point_count,
47
+ )
48
+ else:
49
+ divergence = self._assemble_demag_fem_divergence_python(
50
+ simplices,
51
+ gradients_by_cell,
52
+ volumes,
53
+ m,
54
+ ms_values,
55
+ point_count,
56
+ )
57
+ divergence += volume_charge_scaling_correction(
58
+ simplices,
59
+ gradients_by_cell,
60
+ volumes,
61
+ m,
62
+ ms_values,
63
+ volume_charge_scales,
64
+ point_count,
65
+ )
66
+ return stiffness, divergence, volumes
67
+
68
+ def _assemble_demag_fem_divergence_python(
69
+ self,
70
+ simplices: np.ndarray,
71
+ gradients_by_cell: np.ndarray,
72
+ volumes: np.ndarray,
73
+ m: np.ndarray,
74
+ ms_values: np.ndarray,
75
+ point_count: int,
76
+ ) -> np.ndarray:
77
+ divergence = np.zeros(point_count, dtype=float)
78
+ if len(simplices) == 0:
79
+ return divergence
80
+
81
+ cell_m = m[simplices] * ms_values[:, np.newaxis, np.newaxis]
82
+ average_m = np.mean(cell_m, axis=1)
83
+ cell_divergence = volumes[:, np.newaxis] * np.einsum(
84
+ "cij,cj->ci",
85
+ gradients_by_cell,
86
+ average_m,
87
+ )
88
+ np.add.at(divergence, simplices, cell_divergence)
89
+ return divergence
90
+
91
+ def _assemble_demag_fem_divergence_rust(
92
+ self,
93
+ simplices: np.ndarray,
94
+ gradients_by_cell: np.ndarray,
95
+ volumes: np.ndarray,
96
+ m: np.ndarray,
97
+ ms_values: np.ndarray,
98
+ point_count: int,
99
+ ) -> np.ndarray:
100
+ rust_accel = _simulation_compatibility_binding(
101
+ "_load_rust_accelerator",
102
+ _load_rust_accelerator,
103
+ )("NmagConfig.accelerator['fem_assembly']")
104
+ return np.asarray(
105
+ rust_accel.build_demag_fem_divergence(
106
+ np.asarray(simplices, dtype=np.int64),
107
+ np.asarray(gradients_by_cell, dtype=np.float64),
108
+ np.asarray(volumes, dtype=np.float64),
109
+ np.asarray(m, dtype=np.float64),
110
+ np.asarray(ms_values, dtype=np.float64),
111
+ int(point_count),
112
+ ),
113
+ dtype=float,
114
+ )
@@ -0,0 +1,38 @@
1
+ """Finite-element separation of demagnetization volume charges."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+
8
+ def volume_charge_scaling_correction(
9
+ simplices: np.ndarray,
10
+ gradients_by_cell: np.ndarray,
11
+ volumes: np.ndarray,
12
+ m: np.ndarray,
13
+ ms_values: np.ndarray,
14
+ volume_charge_scales: np.ndarray,
15
+ point_count: int,
16
+ ) -> np.ndarray:
17
+ """Return the weak-source correction for material volume-charge scales.
18
+
19
+ The standard FEM assembly is the combined surface and volume source
20
+ ``integral(grad(test) . M)``. Integration by parts identifies the volume
21
+ part on a linear tetrahedron as ``-volume * div(M) / 4`` at every local
22
+ node. Scaling that term separately preserves the unscaled surface charge.
23
+ """
24
+ correction = np.zeros(point_count, dtype=float)
25
+ if len(simplices) == 0 or np.all(volume_charge_scales == 1.0):
26
+ return correction
27
+
28
+ cell_m = m[simplices] * ms_values[:, np.newaxis, np.newaxis]
29
+ cell_divergence = np.einsum("cij,cij->c", gradients_by_cell, cell_m)
30
+ local_correction = (
31
+ -(volume_charge_scales - 1.0) * volumes * cell_divergence / simplices.shape[1]
32
+ )
33
+ np.add.at(
34
+ correction,
35
+ simplices,
36
+ np.broadcast_to(local_correction[:, np.newaxis], simplices.shape),
37
+ )
38
+ return correction
@@ -0,0 +1,261 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import numpy as np
6
+
7
+ from ....backends import (
8
+ _load_rust_accelerator,
9
+ _selected_demag_boundary_face_backend,
10
+ _selected_demag_fem_geometry_backend,
11
+ _selected_demag_fem_matrix_backend,
12
+ )
13
+ from ....demag import (
14
+ _oriented_boundary_faces,
15
+ _validate_dense_demag_size,
16
+ _validate_tetrahedral_cells,
17
+ )
18
+ from ...support import _simulation_compatibility_binding
19
+
20
+
21
+ class SimulationDemagFemGeometryMixin:
22
+ if TYPE_CHECKING:
23
+ _demag_fem_geometry_cache: Any | None
24
+ _demag_boundary_faces_cache: Any | None
25
+
26
+ def __getattr__(self, name: str) -> Any: ...
27
+
28
+ def _demag_fem_geometry_for_mesh(
29
+ self,
30
+ points: np.ndarray,
31
+ simplices: np.ndarray,
32
+ ) -> tuple[Any, np.ndarray, np.ndarray]:
33
+ token = self._mesh_geometry_token()
34
+ self._ensure_demag_geometry_cache_token(token)
35
+ matrix_backend = _selected_demag_fem_matrix_backend(len(points))
36
+ if self._demag_fem_geometry_cache is not None:
37
+ cached_backend, stiffness, gradients, volumes = self._demag_fem_geometry_cache
38
+ if cached_backend == matrix_backend:
39
+ return stiffness, gradients, volumes
40
+
41
+ if matrix_backend == "sparse":
42
+ stiffness, gradients, volumes = self._demag_fem_geometry_for_mesh_sparse(
43
+ points,
44
+ simplices,
45
+ )
46
+ self._demag_fem_geometry_cache = (
47
+ matrix_backend,
48
+ stiffness,
49
+ gradients,
50
+ volumes,
51
+ )
52
+ return stiffness, gradients, volumes
53
+
54
+ backend = _selected_demag_fem_geometry_backend(
55
+ len(simplices), getattr(self, "config", None)
56
+ )
57
+ with self._record_active_subfield_array_timing_block(
58
+ f"demag_auxiliary:assemble_fem:geometry:{backend}",
59
+ ):
60
+ if backend == "rust":
61
+ geometry = self._demag_fem_geometry_for_mesh_rust(
62
+ points,
63
+ simplices,
64
+ )
65
+ else:
66
+ geometry = self._demag_fem_geometry_for_mesh_python(
67
+ points,
68
+ simplices,
69
+ )
70
+ stiffness, gradients, volumes = geometry
71
+ self._demag_fem_geometry_cache = (matrix_backend, stiffness, gradients, volumes)
72
+ return stiffness, gradients, volumes
73
+
74
+ def _demag_fem_geometry_for_mesh_sparse(
75
+ self,
76
+ points: np.ndarray,
77
+ simplices: np.ndarray,
78
+ ) -> tuple[Any, np.ndarray, np.ndarray]:
79
+ from scipy import sparse
80
+
81
+ _validate_tetrahedral_cells(points, simplices)
82
+ point_count = len(points)
83
+ if len(simplices) == 0:
84
+ return (
85
+ sparse.csr_matrix((point_count, point_count), dtype=float),
86
+ np.empty((0, 4, 3), dtype=float),
87
+ np.empty(0, dtype=float),
88
+ )
89
+
90
+ cell_points = points[simplices]
91
+ matrices = np.concatenate(
92
+ [np.ones((len(simplices), 4, 1), dtype=float), cell_points],
93
+ axis=2,
94
+ )
95
+ inverses = np.linalg.inv(matrices)
96
+ gradients = np.transpose(inverses[:, 1:, :], (0, 2, 1))
97
+ volumes = np.abs(np.linalg.det(cell_points[:, 1:] - cell_points[:, [0]])) / 6.0
98
+ local_stiffness = volumes[:, np.newaxis, np.newaxis] * np.einsum(
99
+ "cik,cjk->cij",
100
+ gradients,
101
+ gradients,
102
+ )
103
+ rows = np.broadcast_to(simplices[:, :, np.newaxis], local_stiffness.shape).ravel()
104
+ columns = np.broadcast_to(simplices[:, np.newaxis, :], local_stiffness.shape).ravel()
105
+ stiffness = sparse.coo_matrix(
106
+ (local_stiffness.ravel(), (rows, columns)),
107
+ shape=(point_count, point_count),
108
+ ).tocsr()
109
+ stiffness.sum_duplicates()
110
+ return stiffness, gradients, volumes
111
+
112
+ def _demag_fem_geometry_for_mesh_python(
113
+ self,
114
+ points: np.ndarray,
115
+ simplices: np.ndarray,
116
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
117
+ point_count = len(points)
118
+ _validate_dense_demag_size(point_count)
119
+ _validate_tetrahedral_cells(points, simplices)
120
+ stiffness = np.zeros((point_count, point_count), dtype=float)
121
+ gradients_by_cell = np.zeros((len(simplices), 4, 3), dtype=float)
122
+ volumes = np.zeros(len(simplices), dtype=float)
123
+
124
+ if len(simplices) == 0:
125
+ return stiffness, gradients_by_cell, volumes
126
+
127
+ cell_points = points[simplices]
128
+ matrices = np.concatenate(
129
+ [
130
+ np.ones((len(simplices), 4, 1), dtype=float),
131
+ cell_points,
132
+ ],
133
+ axis=2,
134
+ )
135
+ try:
136
+ inverses = np.linalg.inv(matrices)
137
+ except np.linalg.LinAlgError:
138
+ return self._demag_fem_geometry_for_mesh_loop(
139
+ points,
140
+ simplices,
141
+ stiffness,
142
+ gradients_by_cell,
143
+ volumes,
144
+ )
145
+
146
+ gradients_by_cell[:] = np.transpose(inverses[:, 1:, :], (0, 2, 1))
147
+ volumes[:] = np.abs(np.linalg.det(cell_points[:, 1:] - cell_points[:, [0]])) / 6.0
148
+ positive = volumes > 0.0
149
+ if np.any(positive):
150
+ local_stiffness = volumes[positive, np.newaxis, np.newaxis] * np.einsum(
151
+ "cik,cjk->cij",
152
+ gradients_by_cell[positive],
153
+ gradients_by_cell[positive],
154
+ )
155
+ positive_simplices = simplices[positive]
156
+ np.add.at(
157
+ stiffness,
158
+ (
159
+ positive_simplices[:, :, np.newaxis],
160
+ positive_simplices[:, np.newaxis, :],
161
+ ),
162
+ local_stiffness,
163
+ )
164
+
165
+ return stiffness, gradients_by_cell, volumes
166
+
167
+ def _demag_fem_geometry_for_mesh_rust(
168
+ self,
169
+ points: np.ndarray,
170
+ simplices: np.ndarray,
171
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
172
+ _validate_dense_demag_size(len(points))
173
+ _validate_tetrahedral_cells(points, simplices)
174
+ rust_accel = _simulation_compatibility_binding(
175
+ "_load_rust_accelerator",
176
+ _load_rust_accelerator,
177
+ )("NmagConfig.accelerator['fem_geometry']")
178
+ stiffness, gradients_by_cell, volumes = rust_accel.build_demag_fem_geometry(
179
+ np.asarray(points, dtype=np.float64),
180
+ np.asarray(simplices, dtype=np.int64),
181
+ )
182
+ return (
183
+ np.asarray(stiffness, dtype=float),
184
+ np.asarray(gradients_by_cell, dtype=float),
185
+ np.asarray(volumes, dtype=float),
186
+ )
187
+
188
+ def _demag_fem_geometry_for_mesh_loop(
189
+ self,
190
+ points: np.ndarray,
191
+ simplices: np.ndarray,
192
+ stiffness: np.ndarray,
193
+ gradients_by_cell: np.ndarray,
194
+ volumes: np.ndarray,
195
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
196
+ for cell_index, simplex in enumerate(simplices):
197
+ cell_points = points[simplex]
198
+ matrix = np.column_stack([np.ones(4), cell_points])
199
+ try:
200
+ inverse = np.asarray(np.linalg.inv(matrix), dtype=float)
201
+ except np.linalg.LinAlgError as error:
202
+ raise ValueError(f"Demag FEM cell {cell_index} is singular.") from error
203
+ gradients = np.transpose(inverse[1:, :])
204
+ volume = abs(float(np.linalg.det(cell_points[1:] - cell_points[[0]]))) / 6.0
205
+ if volume <= 0.0:
206
+ raise ValueError(f"Demag FEM cell {cell_index} has zero volume.")
207
+ volumes[cell_index] = volume
208
+ gradients_by_cell[cell_index] = gradients
209
+ stiffness[np.ix_(simplex, simplex)] += volume * (gradients @ np.transpose(gradients))
210
+
211
+ return stiffness, gradients_by_cell, volumes
212
+
213
+ def _boundary_faces_for_demag_mesh(
214
+ self,
215
+ points: np.ndarray,
216
+ simplices: np.ndarray,
217
+ ) -> list[tuple[int, tuple[int, int, int]]]:
218
+ token = self._mesh_geometry_token()
219
+ self._ensure_demag_geometry_cache_token(token)
220
+ if self._demag_boundary_faces_cache is not None:
221
+ return self._demag_boundary_faces_cache
222
+
223
+ backend = _selected_demag_boundary_face_backend(getattr(self, "config", None))
224
+ with self._record_active_subfield_array_timing_block(
225
+ f"demag_auxiliary:boundary_faces:{backend}",
226
+ ):
227
+ if backend == "rust":
228
+ boundary_faces = self._boundary_faces_for_demag_mesh_rust(
229
+ points,
230
+ simplices,
231
+ )
232
+ else:
233
+ boundary_faces = _simulation_compatibility_binding(
234
+ "_oriented_boundary_faces",
235
+ _oriented_boundary_faces,
236
+ )(points, simplices)
237
+ self._demag_boundary_faces_cache = boundary_faces
238
+ self._demag_bem_cache = None
239
+ self._last_bem_operator_stats = None
240
+ return boundary_faces
241
+
242
+ def _boundary_faces_for_demag_mesh_rust(
243
+ self,
244
+ points: np.ndarray,
245
+ simplices: np.ndarray,
246
+ ) -> list[tuple[int, tuple[int, int, int]]]:
247
+ rust_accel = _simulation_compatibility_binding(
248
+ "_load_rust_accelerator",
249
+ _load_rust_accelerator,
250
+ )("NmagConfig.accelerator['boundary_faces']")
251
+ owners, faces = rust_accel.build_oriented_boundary_faces(
252
+ np.asarray(points, dtype=np.float64),
253
+ np.asarray(simplices, dtype=np.int64),
254
+ )
255
+ owner_array = np.asarray(owners, dtype=int).flatten()
256
+ face_values = np.asarray(faces, dtype=int).flatten()
257
+ face_array = np.column_stack((face_values[0::3], face_values[1::3], face_values[2::3]))
258
+ return [
259
+ (int(owner), (int(face[0]), int(face[1]), int(face[2])))
260
+ for owner, face in zip(owner_array, face_array, strict=True)
261
+ ]
@@ -0,0 +1,11 @@
1
+ """Demag field compatibility façade composed from recovery and auxiliary mixins."""
2
+
3
+ from .auxiliary import SimulationDemagAuxiliaryMixin
4
+ from .recovery import SimulationDemagRecoveryMixin
5
+
6
+
7
+ class SimulationDemagFieldsMixin(
8
+ SimulationDemagRecoveryMixin,
9
+ SimulationDemagAuxiliaryMixin,
10
+ ):
11
+ """Provide recovered demag fields and FEM/BEM scalar-potential solves."""
@@ -0,0 +1,186 @@
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
+ _demag_internal_trace_enabled,
10
+ _selected_demag_linear_solver_backend,
11
+ )
12
+ from ....demag import _demag_solve_diagnostics, _solve_gauge_fixed
13
+ from ....demag.linear import _factorize_gauge_fixed, _is_sparse_matrix, _solve_sparse_gauge_fixed
14
+ from ...support import LEGACY_RHO_BOXED_VOLUME_M3
15
+
16
+
17
+ class SimulationDemagAuxiliaryMixin:
18
+ if TYPE_CHECKING:
19
+ _demag_phi_cache: Any | None
20
+ _demag_rho_cache: Any | None
21
+ _demag_volumes_cache: Any | None
22
+ _demag_gauge_factorization_cache: Any | None
23
+
24
+ def __getattr__(self, name: str) -> Any: ...
25
+
26
+ def _get_demag_auxiliary_fields(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
27
+ cache_started = time.perf_counter()
28
+ token = self._demag_token()
29
+ if (
30
+ self._demag_aux_cache_token == token
31
+ and self._demag_phi_cache is not None
32
+ and self._demag_rho_cache is not None
33
+ and self._demag_volumes_cache is not None
34
+ ):
35
+ self._record_active_subfield_array_timing(
36
+ "demag_auxiliary:cache_hit",
37
+ time.perf_counter() - cache_started,
38
+ )
39
+ return self._demag_phi_cache, self._demag_rho_cache, self._demag_volumes_cache
40
+
41
+ with self._record_active_subfield_array_timing_block(
42
+ "demag_auxiliary:mesh_inputs",
43
+ ):
44
+ points = self._mesh_points()
45
+ simplices = np.asarray(self._require_mesh().simplices, dtype=int)
46
+ if simplices.size == 0:
47
+ phi = np.zeros(len(points), dtype=float)
48
+ rho = np.zeros(len(points), dtype=float)
49
+ volumes = np.zeros(0, dtype=float)
50
+ else:
51
+ if simplices.ndim != 2 or simplices.shape[1] != 4:
52
+ raise NotImplementedError("The demag MVP currently supports tetrahedral 3D meshes.")
53
+ with self._record_active_subfield_array_timing_block(
54
+ "demag_auxiliary:boundary_faces",
55
+ ):
56
+ boundary_faces = self._boundary_faces_for_demag_mesh(points, simplices)
57
+ if not boundary_faces:
58
+ phi = np.zeros(len(points), dtype=float)
59
+ rho = np.zeros(len(points), dtype=float)
60
+ volumes = np.zeros(len(simplices), dtype=float)
61
+ else:
62
+ with self._record_active_subfield_array_timing_block(
63
+ "demag_auxiliary:material_inputs",
64
+ ):
65
+ m = np.asarray(self._fields["m"], dtype=float)
66
+ regions = list(self._require_mesh().regions or [1] * len(simplices))
67
+ ms_values = self._simplex_material_ms_values(regions)
68
+ volume_charge_scales = self._simplex_volume_charge_scales(regions)
69
+ with self._record_active_subfield_array_timing_block(
70
+ "demag_auxiliary:fem_bem_total",
71
+ ):
72
+ phi, rho, volumes = self._fem_bem_auxiliary_at_nodes(
73
+ points,
74
+ simplices,
75
+ boundary_faces,
76
+ m,
77
+ ms_values,
78
+ volume_charge_scales,
79
+ )
80
+
81
+ self._demag_aux_cache_token = token
82
+ self._demag_phi_cache = phi
83
+ self._demag_rho_cache = rho
84
+ self._demag_volumes_cache = volumes
85
+ return phi, rho, volumes
86
+
87
+ def _fem_bem_auxiliary_at_nodes(
88
+ self,
89
+ points: np.ndarray,
90
+ simplices: np.ndarray,
91
+ boundary_faces: list[tuple[int, tuple[int, int, int]]],
92
+ m: np.ndarray,
93
+ ms_values: np.ndarray,
94
+ volume_charge_scales: np.ndarray,
95
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
96
+ with self._record_active_subfield_array_timing_block(
97
+ "demag_auxiliary:assemble_fem",
98
+ ):
99
+ stiffness, divergence, volumes = self._assemble_demag_fem_system(
100
+ points,
101
+ simplices,
102
+ m,
103
+ ms_values,
104
+ volume_charge_scales,
105
+ )
106
+ with self._record_active_subfield_array_timing_block("demag_auxiliary:rho_box"):
107
+ rho = self._scalar_cofield_to_legacy_boxed_field(
108
+ divergence,
109
+ len(points),
110
+ )
111
+ with self._record_active_subfield_array_timing_block(
112
+ "demag_auxiliary:gauge_solve",
113
+ ):
114
+ if _is_sparse_matrix(stiffness):
115
+ phi1 = _solve_sparse_gauge_fixed(
116
+ stiffness,
117
+ divergence,
118
+ record_timing=self._record_active_subfield_array_timing,
119
+ timing_prefix="demag_auxiliary:gauge_solve",
120
+ )
121
+ elif _selected_demag_linear_solver_backend(stiffness.shape[0]) == "scipy":
122
+ if self._demag_gauge_factorization_cache is None:
123
+ self._demag_gauge_factorization_cache = _factorize_gauge_fixed(stiffness)
124
+ augmented_rhs = np.concatenate([divergence, [0.0]])
125
+ phi1 = self._demag_gauge_factorization_cache.solve(augmented_rhs)[:-1]
126
+ else:
127
+ phi1 = _solve_gauge_fixed(
128
+ stiffness,
129
+ divergence,
130
+ record_timing=self._record_active_subfield_array_timing,
131
+ timing_prefix="demag_auxiliary:gauge_solve",
132
+ )
133
+ with self._record_active_subfield_array_timing_block(
134
+ "demag_auxiliary:lindholm_bem",
135
+ ):
136
+ boundary_nodes, bem = self._lindholm_bem_for_demag_mesh(
137
+ points,
138
+ simplices,
139
+ boundary_faces,
140
+ )
141
+ phi2 = np.zeros(len(points), dtype=float)
142
+ phi2_boundary = np.zeros(len(boundary_nodes), dtype=float)
143
+ if len(boundary_nodes) > 0:
144
+ with self._record_active_subfield_array_timing_block(
145
+ "demag_auxiliary:bem_multiply",
146
+ ):
147
+ phi2_boundary = bem @ phi1[boundary_nodes]
148
+ with self._record_active_subfield_array_timing_block(
149
+ "demag_auxiliary:dirichlet_extension",
150
+ ):
151
+ phi2 = self._dirichlet_extension_from_boundary(
152
+ stiffness,
153
+ boundary_nodes,
154
+ phi2_boundary,
155
+ )
156
+ self.last_demag_solve_diagnostics = _demag_solve_diagnostics(
157
+ stiffness,
158
+ divergence,
159
+ boundary_nodes,
160
+ phi1,
161
+ phi2_boundary,
162
+ phi2,
163
+ phi1 + phi2,
164
+ )
165
+ if _demag_internal_trace_enabled():
166
+ self.last_demag_internal_vectors = {
167
+ "rho": np.array(rho, copy=True),
168
+ "phi1": np.array(phi1, copy=True),
169
+ "phi2": np.array(phi2, copy=True),
170
+ "phi": np.array(phi1 + phi2, copy=True),
171
+ }
172
+ return phi1 + phi2, rho, volumes
173
+
174
+ def _scalar_cofield_to_legacy_boxed_field(
175
+ self,
176
+ cofield: np.ndarray,
177
+ point_count: int,
178
+ ) -> np.ndarray:
179
+ if cofield.size == 0:
180
+ return np.zeros(point_count, dtype=float)
181
+
182
+ nodal = np.zeros(point_count, dtype=float)
183
+ # Legacy Nmag's simulation unit system always boxes rho with the
184
+ # fixed 1 nm base volume, rather than the loaded mesh's unit_length.
185
+ nodal[: len(cofield)] = cofield / LEGACY_RHO_BOXED_VOLUME_M3
186
+ return nodal
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import numpy as np
7
+
8
+ from ....demag import _mean_edge_length, _simplex_volumes
9
+ from ...support import MU0_OVER_4PI, _simulation_compatibility_binding
10
+
11
+
12
+ class SimulationDemagProbeMixin:
13
+ if TYPE_CHECKING:
14
+ _demag_dipoles: tuple[np.ndarray, np.ndarray, float] | None
15
+
16
+ def __getattr__(self, name: str) -> Any: ...
17
+
18
+ def _cell_demag_from_potential(
19
+ self,
20
+ points: np.ndarray,
21
+ simplices: np.ndarray,
22
+ phi: np.ndarray,
23
+ ) -> np.ndarray:
24
+ _, gradients_by_cell, _ = self._demag_fem_geometry_for_mesh(points, simplices)
25
+ return -np.einsum(
26
+ "ci,cij->cj",
27
+ phi[simplices],
28
+ gradients_by_cell,
29
+ )
30
+
31
+ def _build_demag_dipoles(self) -> tuple[np.ndarray, np.ndarray, float]:
32
+ if "m" not in self._fields:
33
+ raise RuntimeError("Magnetisation must be set before using demag fields.")
34
+
35
+ points = self._mesh_points()
36
+ mesh = self._require_mesh()
37
+ simplices = np.asarray(mesh.simplices, dtype=int)
38
+ if simplices.size == 0:
39
+ return np.empty((0, 3)), np.empty((0, 3)), 1.0
40
+ if simplices.ndim != 2 or simplices.shape[1] != 4:
41
+ raise NotImplementedError("The demag MVP currently supports tetrahedral 3D meshes.")
42
+
43
+ token = (id(mesh.raw_mesh), id(self._fields["m"]))
44
+ if self._demag_cache_token == token and self._demag_dipoles is not None:
45
+ return self._demag_dipoles
46
+
47
+ m = np.asarray(self._fields["m"], dtype=float)
48
+ centroids = np.mean(points[simplices], axis=1)
49
+ volumes = _simulation_compatibility_binding(
50
+ "_simplex_volumes",
51
+ _simplex_volumes,
52
+ )(points, simplices)
53
+ regions = list(mesh.regions or [1] * len(simplices))
54
+ ms_values = np.asarray(
55
+ [self._simplex_material_ms(region) for region in regions], dtype=float
56
+ )
57
+ cell_m = np.mean(m[simplices], axis=1)
58
+ dipole_moments = cell_m * (ms_values * volumes)[:, np.newaxis]
59
+ softening = 0.25 * _mean_edge_length(points, simplices)
60
+ self._demag_cache_token = token
61
+ self._demag_dipoles = (centroids, dipole_moments, softening)
62
+ return self._demag_dipoles
63
+
64
+ def _probe_demag_at(self, pos: Sequence[float]) -> np.ndarray:
65
+ centroids, dipole_moments, softening = self._build_demag_dipoles()
66
+ if len(centroids) == 0:
67
+ return np.zeros(3, dtype=float)
68
+
69
+ r = np.asarray(pos, dtype=float) - centroids
70
+ r2 = np.sum(r * r, axis=1) + softening * softening
71
+ r3 = r2**1.5
72
+ r5 = r2**2.5
73
+ mdotr = np.sum(dipole_moments * r, axis=1)
74
+ terms = (3.0 * r * mdotr[:, np.newaxis] / r5[:, np.newaxis]) - (
75
+ dipole_moments / r3[:, np.newaxis]
76
+ )
77
+ return MU0_OVER_4PI * np.sum(terms, axis=0)