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,106 @@
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 si.physical import SI
10
+
11
+
12
+ class SimulationFieldProbeMixin:
13
+ if TYPE_CHECKING:
14
+
15
+ def __getattr__(self, name: str) -> Any: ...
16
+
17
+ def probe_subfield_siv(
18
+ self,
19
+ subfieldname: str,
20
+ pos: Sequence[float],
21
+ unit: SI | None = None,
22
+ ) -> Any:
23
+ """Probe a field at a position expressed in metres.
24
+
25
+ Demagnetization fields are linearly interpolated in the containing
26
+ tetrahedron. Magnetization uses the nearest mesh node, and a homogeneous
27
+ applied field is returned directly.
28
+
29
+ Args:
30
+ subfieldname: ``H_demag``, ``H_ext``, or ``m``.
31
+ pos: Three physical coordinates in metres.
32
+ unit: Reserved compatibility argument. Returned values use the
33
+ field's standard SI representation.
34
+
35
+ Returns:
36
+ Ordinary numeric components in SI, or ``None`` outside the mesh.
37
+
38
+ Raises:
39
+ KeyError: If the field name is not supported by this probe API.
40
+ """
41
+ if subfieldname == "H_demag":
42
+ total_started = time.perf_counter()
43
+ try:
44
+ started = time.perf_counter()
45
+ within_bounds = self._probe_is_within_mesh_bounds(pos)
46
+ self._record_probe_timing(
47
+ "bounds_check",
48
+ time.perf_counter() - started,
49
+ )
50
+ if not within_bounds:
51
+ return None
52
+
53
+ started = time.perf_counter()
54
+ points = self._mesh_points()
55
+ probe = np.asarray(pos, dtype=float)
56
+ self._record_probe_timing(
57
+ "prepare_probe",
58
+ time.perf_counter() - started,
59
+ )
60
+
61
+ started = time.perf_counter()
62
+ legacy_null_vertex = self._probe_is_legacy_null_vertex(probe, points)
63
+ self._record_probe_timing(
64
+ "legacy_null_vertex_check",
65
+ time.perf_counter() - started,
66
+ )
67
+ if legacy_null_vertex:
68
+ return None
69
+
70
+ started = time.perf_counter()
71
+ nodal_field = self._get_demag_nodal_field()
72
+ self._record_probe_timing(
73
+ "demag_nodal_field",
74
+ time.perf_counter() - started,
75
+ )
76
+
77
+ started = time.perf_counter()
78
+ result = self._probe_tetrahedral_field(probe, nodal_field)
79
+ self._record_probe_timing(
80
+ "tetrahedral_interpolation",
81
+ time.perf_counter() - started,
82
+ )
83
+ return result
84
+ finally:
85
+ self._record_probe_timing(
86
+ "total",
87
+ time.perf_counter() - total_started,
88
+ )
89
+ if subfieldname == "H_ext":
90
+ return self._fields.get("H_ext", np.zeros(3)).tolist()
91
+ if subfieldname != "m":
92
+ raise KeyError(f"Unknown or unset subfield '{subfieldname}'.")
93
+ if self.mesh is None or "m" not in self._fields:
94
+ return None
95
+
96
+ points = np.asarray(self.mesh.points, dtype=float)
97
+ if points.size == 0:
98
+ return None
99
+ probe = np.asarray(pos, dtype=float)
100
+ nearest_index = int(np.argmin(np.linalg.norm(points - probe, axis=1)))
101
+ return np.asarray(self._fields["m"][nearest_index], dtype=float).tolist()
102
+
103
+ def _record_probe_timing(self, name: str, value: float) -> None:
104
+ self.last_probe_timings_seconds[name] = (
105
+ self.last_probe_timings_seconds.get(name, 0.0) + value
106
+ )
@@ -0,0 +1,221 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+ import time
6
+ from typing import Any, Protocol
7
+
8
+ import numpy as np
9
+ from numpy.typing import NDArray
10
+
11
+ from ..backends import _load_rust_accelerator
12
+ from ..dynamics import IntegratorStats
13
+ from ..resources import available_memory_bytes as _available_memory_bytes
14
+ from .support import _si_unit, _simulation_compatibility_binding
15
+
16
+ FloatArray = NDArray[np.float64]
17
+ DIFFSOL_DENSE_MEMORY_LIMIT_ENV = "NMAG_DIFFSOL_DENSE_MEMORY_LIMIT_GIB"
18
+ DIFFSOL_DENSE_MATRIX_COPIES_ESTIMATE = 6
19
+ DIFFSOL_AVAILABLE_MEMORY_FRACTION = 0.8
20
+ DEFAULT_BDF_INITIAL_STEP_SECONDS = 1.0e-15
21
+ DEFAULT_BDF_MAXIMUM_STEPS = 1_000_000
22
+
23
+
24
+ class _BdfResult(Protocol):
25
+ state: list[float]
26
+ reached_time: float
27
+ accepted_steps: int
28
+ rejected_steps: int
29
+ rhs_evaluations: int
30
+ jacobian_vector_evaluations: int
31
+ nonlinear_iterations: int
32
+ nonlinear_failures: int
33
+ linear_solver_setups: int
34
+ last_step: float
35
+ max_dm_dt: float
36
+ converged: bool
37
+
38
+
39
+ def _estimated_dense_bdf_peak_bytes(state_size: int) -> int:
40
+ if state_size < 1:
41
+ raise ValueError("state_size must be positive.")
42
+ matrix_bytes = state_size * state_size * np.dtype(np.float64).itemsize
43
+ return DIFFSOL_DENSE_MATRIX_COPIES_ESTIMATE * matrix_bytes
44
+
45
+
46
+ def _diffsol_dense_memory_budget() -> tuple[int | None, str]:
47
+ configured = os.environ.get(DIFFSOL_DENSE_MEMORY_LIMIT_ENV)
48
+ if configured is not None:
49
+ value = configured.strip().lower()
50
+ if value == "unlimited":
51
+ return None, "explicit unlimited override"
52
+ try:
53
+ gibibytes = float(value)
54
+ except ValueError as error:
55
+ raise ValueError(
56
+ f"{DIFFSOL_DENSE_MEMORY_LIMIT_ENV} must be a positive number of GiB "
57
+ f"or 'unlimited', got {configured!r}."
58
+ ) from error
59
+ if not math.isfinite(gibibytes) or gibibytes <= 0.0:
60
+ raise ValueError(
61
+ f"{DIFFSOL_DENSE_MEMORY_LIMIT_ENV} must be a positive number of GiB "
62
+ f"or 'unlimited', got {configured!r}."
63
+ )
64
+ return int(gibibytes * 1024**3), "explicit environment budget"
65
+
66
+ available = _available_memory_bytes()
67
+ if available is None:
68
+ return None, "available memory could not be determined"
69
+ return int(available * DIFFSOL_AVAILABLE_MEMORY_FRACTION), "80% of available memory"
70
+
71
+
72
+ def _validate_diffsol_dense_memory(state_size: int) -> None:
73
+ estimated_bytes = _estimated_dense_bdf_peak_bytes(state_size)
74
+ budget_bytes, budget_source = _diffsol_dense_memory_budget()
75
+ if budget_bytes is None or estimated_bytes <= budget_bytes:
76
+ return
77
+
78
+ estimated_gib = estimated_bytes / 1024**3
79
+ budget_gib = budget_bytes / 1024**3
80
+ raise MemoryError(
81
+ f"The dense Diffsol solve for {state_size} state values is estimated to require "
82
+ f"approximately {estimated_gib:.2f} GiB, exceeding the {budget_gib:.2f} GiB "
83
+ f"budget ({budget_source}). Set {DIFFSOL_DENSE_MEMORY_LIMIT_ENV} to a reviewed "
84
+ "GiB budget or 'unlimited' to proceed. There is no fixed state-count limit."
85
+ )
86
+
87
+
88
+ def build_affine_field_operator(simulation: Any) -> FloatArray:
89
+ """Build the dense linear map from nodal m to demag plus exchange field."""
90
+ mesh_token = simulation._mesh_geometry_token()
91
+ cached = simulation._llg_affine_operator_cache
92
+ if cached is not None and cached[0] == mesh_token:
93
+ return cached[1]
94
+
95
+ original_m = np.asarray(simulation._fields["m"], dtype=np.float64)
96
+ state_size = int(original_m.size)
97
+ _validate_diffsol_dense_memory(state_size)
98
+
99
+ def linear_field(state: FloatArray) -> FloatArray:
100
+ simulation._fields["m"] = np.reshape(state, (-1, 3))
101
+ simulation._invalidate_demag()
102
+ field = np.asarray(simulation._get_exchange_nodal_field(), dtype=np.float64)
103
+ if simulation.do_demag:
104
+ field = field + np.asarray(simulation._get_demag_nodal_field(), dtype=np.float64)
105
+ return np.ravel(field)
106
+
107
+ try:
108
+ zero = np.zeros(state_size, dtype=np.float64)
109
+ baseline = linear_field(zero)
110
+ operator = np.empty((state_size, state_size), dtype=np.float64)
111
+ basis = np.zeros(state_size, dtype=np.float64)
112
+ for column in range(state_size):
113
+ basis[column] = 1.0
114
+ operator[:, column] = linear_field(basis) - baseline
115
+ basis[column] = 0.0
116
+ finally:
117
+ simulation._fields["m"] = np.array(original_m, copy=True)
118
+ simulation._invalidate_demag()
119
+
120
+ operator = np.ascontiguousarray(operator)
121
+ simulation._llg_affine_operator_cache = (mesh_token, operator)
122
+ return operator
123
+
124
+
125
+ def relax_with_diffsol(simulation: Any, H_applied: Any = None) -> None:
126
+ if simulation.mesh is None or "m" not in simulation._fields:
127
+ raise RuntimeError("A mesh and magnetisation are required before relaxation.")
128
+ if not simulation._anisotropy_is_zero_by_construction():
129
+ raise NotImplementedError(
130
+ "The experimental Diffsol backend does not support dynamic anisotropy."
131
+ )
132
+
133
+ simulation.do_next_stage(stage=simulation.clock.stage)
134
+ simulation.clock.stage_end = False
135
+ if H_applied is not None:
136
+ simulation.set_H_ext(H_applied)
137
+
138
+ started = time.perf_counter()
139
+ try:
140
+ operator = build_affine_field_operator(simulation)
141
+ point_count = len(simulation._fields["m"])
142
+ h_ext = np.asarray(simulation._fields.get("H_ext", np.zeros(3)), dtype=np.float64)
143
+ constant_field = np.ascontiguousarray(np.tile(h_ext, point_count))
144
+ pin = np.ascontiguousarray(simulation._subfield_array("pin"), dtype=np.float64)
145
+ c1, c2, c3 = simulation._llg_coefficients()
146
+ config = simulation.integrator_config
147
+ rust_accel = _simulation_compatibility_binding(
148
+ "_load_rust_accelerator", _load_rust_accelerator
149
+ )("NmagConfig(integrator_backend='diffsol')")
150
+ try:
151
+ integrate_llg_bdf = rust_accel.integrate_llg_bdf
152
+ except AttributeError as exc:
153
+ raise RuntimeError(
154
+ "NmagConfig(integrator_backend='diffsol') requires an nmag_accel build with the "
155
+ "Diffsol backend. Rebuild it with `./scripts/build-accelerator.sh --release`."
156
+ ) from exc
157
+
158
+ result: _BdfResult = integrate_llg_bdf(
159
+ np.ascontiguousarray(np.ravel(simulation._fields["m"]), dtype=np.float64),
160
+ operator,
161
+ constant_field,
162
+ pin,
163
+ c1,
164
+ c2,
165
+ c3,
166
+ 0.0,
167
+ simulation.max_time_reached.in_units_of(_si_unit("s")),
168
+ config.relative_tolerance,
169
+ config.absolute_tolerance,
170
+ min(DEFAULT_BDF_INITIAL_STEP_SECONDS, config.maximum_step_seconds),
171
+ config.maximum_step_seconds,
172
+ simulation.stopping_dm_dt,
173
+ 5,
174
+ simulation.convergence.required_checks,
175
+ DEFAULT_BDF_MAXIMUM_STEPS,
176
+ )
177
+ except Exception:
178
+ simulation._last_integrator_stats = IntegratorStats(
179
+ failed=True,
180
+ status="failed",
181
+ wall_seconds=time.perf_counter() - started,
182
+ )
183
+ raise
184
+ wall_seconds = time.perf_counter() - started
185
+ accepted_state = np.asarray(result.state, dtype=np.float64)
186
+ if not np.all(np.isfinite(accepted_state)):
187
+ raise FloatingPointError("Diffsol returned a non-finite magnetisation state.")
188
+
189
+ simulation._fields["m"] = np.reshape(accepted_state, (-1, 3))
190
+ simulation._invalidate_demag()
191
+ simulation._invalidate_integrator()
192
+ simulation.max_dm_dt = float(result.max_dm_dt)
193
+ simulation.clock.record_advance(
194
+ stage_time_seconds=float(result.reached_time),
195
+ accepted_steps=int(result.accepted_steps),
196
+ last_step_seconds=float(result.last_step),
197
+ wall_seconds=wall_seconds,
198
+ )
199
+ simulation.clock.convergence = bool(result.converged)
200
+ simulation.clock.stage_end = bool(result.converged)
201
+ simulation._last_integrator_stats = IntegratorStats(
202
+ accepted_steps=int(result.accepted_steps),
203
+ rhs_evaluations=int(result.rhs_evaluations),
204
+ rejected_steps=int(result.rejected_steps),
205
+ failed=False,
206
+ status="converged" if result.converged else "maximum_time_reached",
207
+ last_step_seconds=float(result.last_step),
208
+ simulated_seconds=float(result.reached_time),
209
+ wall_seconds=wall_seconds,
210
+ jacobian_vector_evaluations=int(result.jacobian_vector_evaluations),
211
+ nonlinear_iterations=int(result.nonlinear_iterations),
212
+ nonlinear_failures=int(result.nonlinear_failures),
213
+ linear_solver_setups=int(result.linear_solver_setups),
214
+ )
215
+ if not result.converged:
216
+ raise RuntimeError(
217
+ f"Diffsol reached {simulation.max_time_reached} without satisfying convergence."
218
+ )
219
+
220
+ simulation.save_data()
221
+ simulation.save_data(fields="all")
@@ -0,0 +1,13 @@
1
+ """Mesh-operation compatibility façade composed from focused mixins."""
2
+
3
+ from .geometry import SimulationMeshGeometryMixin
4
+ from .materials import SimulationMeshMaterialMixin
5
+ from .probe import SimulationMeshProbeMixin
6
+
7
+
8
+ class SimulationMeshMixin(
9
+ SimulationMeshGeometryMixin,
10
+ SimulationMeshProbeMixin,
11
+ SimulationMeshMaterialMixin,
12
+ ):
13
+ """Provide mesh caches, probing, and material-aware averaging."""
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import numpy as np
6
+
7
+ from ..support import _MESH_EDGE_SET_THRESHOLD
8
+
9
+
10
+ class SimulationMeshGeometryMixin:
11
+ if TYPE_CHECKING:
12
+
13
+ def __getattr__(self, name: str) -> Any: ...
14
+
15
+ def _invalidate_demag(self, *, clear_geometry: bool = False) -> None:
16
+ self._demag_cache_token = None
17
+ self._demag_dipoles = None
18
+ self._demag_nodal_cache = None
19
+ self._demag_cell_cache_token = None
20
+ self._demag_cell_field_cache = None
21
+ self._demag_aux_cache_token = None
22
+ self._demag_phi_cache = None
23
+ self._demag_rho_cache = None
24
+ self._demag_volumes_cache = None
25
+ self.last_demag_solve_diagnostics = {}
26
+ self.last_demag_internal_vectors = {}
27
+ self._exchange_cache_token = None
28
+ self._exchange_nodal_cache = None
29
+ self._anisotropy_fields_cache = None
30
+ if clear_geometry:
31
+ self._demag_geometry_cache_token = None
32
+ self._demag_boundary_faces_cache = None
33
+ self._demag_bem_cache = None
34
+ self._last_bem_operator_stats = None
35
+ self._demag_fem_geometry_cache = None
36
+ self._demag_gauge_factorization_cache = None
37
+ self._demag_dirichlet_factorization_cache = None
38
+ self._demag_ms_values_cache = None
39
+ self._demag_volume_charge_scales_cache = None
40
+ self._nodal_ms_values_cache = None
41
+ self._mesh_points_cache = None
42
+ self._mesh_bounds_cache = None
43
+ self._mesh_edge_cache = None
44
+ self._simplex_volume_cache = None
45
+ self._volume_average_node_weights_cache = None
46
+ self._incident_cell_volume_sums_cache = None
47
+ self._exchange_spectral_bound_cache = None
48
+ self._nodal_material_coefficients_cache = None
49
+ self._nodal_anisotropy_groups_cache = None
50
+ self._anisotropy_fields_cache = None
51
+ self._llg_affine_operator_cache = None
52
+ self._probe_geometry_cache_token = None
53
+ self._probe_tetrahedral_cache = None
54
+
55
+ def _mesh_points(self) -> np.ndarray:
56
+ token = self._mesh_geometry_token()
57
+ if self._mesh_points_cache is not None:
58
+ cached_token, cached_points = self._mesh_points_cache
59
+ if cached_token == token:
60
+ return cached_points
61
+
62
+ points = np.asarray(self._require_mesh().points, dtype=float)
63
+ if points.ndim != 2 or points.shape[1] != 3:
64
+ raise NotImplementedError("The demag MVP currently supports only 3D meshes.")
65
+ self._mesh_points_cache = (token, points)
66
+ return points
67
+
68
+ def _mesh_bounds(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
69
+ token = self._mesh_geometry_token()
70
+ if self._mesh_bounds_cache is not None:
71
+ cached_token, lower, upper, span = self._mesh_bounds_cache
72
+ if cached_token == token:
73
+ return lower, upper, span
74
+
75
+ points = self._mesh_points()
76
+ lower = np.min(points, axis=0)
77
+ upper = np.max(points, axis=0)
78
+ span = np.maximum(upper - lower, np.ones(3))
79
+ self._mesh_bounds_cache = (token, lower, upper, span)
80
+ return lower, upper, span
81
+
82
+ def _mesh_edges(self) -> np.ndarray:
83
+ token = self._mesh_geometry_token()
84
+ if self._mesh_edge_cache is not None:
85
+ cached_token, cached_edges = self._mesh_edge_cache
86
+ if cached_token == token:
87
+ return cached_edges
88
+
89
+ mesh = self._require_mesh()
90
+ simplices = np.asarray(mesh.simplices, dtype=int)
91
+ if simplices.ndim != 2 or simplices.shape[1] < 2 or len(simplices) == 0:
92
+ edges = np.zeros((0, 2), dtype=int)
93
+ else:
94
+ local_pairs = np.asarray(
95
+ [
96
+ (left, right)
97
+ for left in range(simplices.shape[1])
98
+ for right in range(left + 1, simplices.shape[1])
99
+ ],
100
+ dtype=int,
101
+ )
102
+ candidate_edge_count = len(simplices) * len(local_pairs)
103
+ if candidate_edge_count <= _MESH_EDGE_SET_THRESHOLD:
104
+ seen_edges: set[tuple[int, int]] = set()
105
+ for simplex in simplices:
106
+ for left_position, left in enumerate(simplex):
107
+ left_index = int(left)
108
+ for right in simplex[left_position + 1 :]:
109
+ right_index = int(right)
110
+ if left_index <= right_index:
111
+ seen_edges.add((left_index, right_index))
112
+ else:
113
+ seen_edges.add((right_index, left_index))
114
+ edges = np.asarray(sorted(seen_edges), dtype=int)
115
+ else:
116
+ expanded_edges = np.asarray(simplices[:, local_pairs], dtype=np.int64)
117
+ flat_edges = expanded_edges.flatten()
118
+ edges = np.column_stack((flat_edges[0::2], flat_edges[1::2]))
119
+ edges.sort(axis=1)
120
+ node_count = max(int(np.max(simplices)) + 1, len(mesh.points))
121
+ edge_codes = edges[:, 0].astype(np.int64, copy=False) * node_count + edges[
122
+ :, 1
123
+ ].astype(np.int64, copy=False)
124
+ unique_codes = np.unique(edge_codes)
125
+ edges = np.empty((len(unique_codes), 2), dtype=int)
126
+ edges[:, 0] = unique_codes // node_count
127
+ edges[:, 1] = unique_codes % node_count
128
+
129
+ self._mesh_edge_cache = (token, edges)
130
+ return edges
131
+
132
+ def _mesh_geometry_token(self) -> int:
133
+ return id(self._require_mesh().raw_mesh)
134
+
135
+ def _require_mesh(self) -> Any:
136
+ if self.mesh is None:
137
+ raise RuntimeError("A mesh must be loaded before using simulation fields.")
138
+ return self.mesh
139
+
140
+ def _ensure_demag_geometry_cache_token(self, token: int) -> None:
141
+ if self._demag_geometry_cache_token == token:
142
+ return
143
+ self._demag_geometry_cache_token = token
144
+ self._demag_boundary_faces_cache = None
145
+ self._demag_bem_cache = None
146
+ self._last_bem_operator_stats = None
147
+ self._demag_fem_geometry_cache = None
148
+ self._demag_ms_values_cache = None
149
+ self._demag_volume_charge_scales_cache = None
150
+ self._nodal_ms_values_cache = None
151
+ self._nodal_material_coefficients_cache = None
152
+ self._nodal_anisotropy_groups_cache = None
153
+ self._anisotropy_fields_cache = None