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
nmag/backends.py ADDED
@@ -0,0 +1,493 @@
1
+ """Backend selection for Nmag's optional acceleration and memory policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import os
7
+ from types import ModuleType
8
+
9
+ from .config import DEMAG_BEM_STORAGE_ENV, NmagConfig, RustKernel
10
+ from .resources import available_memory_bytes
11
+
12
+ RUST_ACCELERATOR_API_VERSION = 2
13
+ MEMORY_MODE_ENV = "NMAG_MEMORY_MODE"
14
+ MEMORY_MODES = frozenset({"auto", "low"})
15
+ AUTO_DENSE_MEMORY_FRACTION = 0.2
16
+
17
+ DEMAG_BEM_STORAGE_BACKEND_ENV = DEMAG_BEM_STORAGE_ENV
18
+ DEMAG_BEM_STORAGE_BACKENDS = frozenset({"auto", "dense", "hierarchical", "matrix-free"})
19
+ DEMAG_BEM_AUTO_MATRIX_FREE_MIN_BOUNDARY_NODES_ENV = (
20
+ "NMAG_DEMAG_BEM_AUTO_MATRIX_FREE_MIN_BOUNDARY_NODES"
21
+ )
22
+ DEFAULT_DEMAG_BEM_AUTO_MATRIX_FREE_MIN_BOUNDARY_NODES = 2048
23
+ DEMAG_FEM_MATRIX_BACKEND_ENV = "NMAG_DEMAG_FEM_MATRIX_BACKEND"
24
+ DEMAG_FEM_MATRIX_BACKENDS = frozenset({"auto", "dense", "sparse"})
25
+ DEMAG_FEM_AUTO_SPARSE_MIN_POINTS_ENV = "NMAG_DEMAG_FEM_AUTO_SPARSE_MIN_POINTS"
26
+ DEFAULT_DEMAG_FEM_AUTO_SPARSE_MIN_POINTS = 2048
27
+ DEMAG_LINEAR_SOLVER_BACKEND_ENV = "NMAG_DEMAG_LINEAR_SOLVER_BACKEND"
28
+ DEMAG_LINEAR_SOLVER_BACKENDS = frozenset({"auto", "numpy", "scipy"})
29
+ DEMAG_LINEAR_SOLVER_AUTO_SCIPY_MIN_SIZE_ENV = "NMAG_DEMAG_LINEAR_SOLVER_AUTO_SCIPY_MIN_SIZE"
30
+ DEFAULT_DEMAG_LINEAR_SOLVER_AUTO_SCIPY_MIN_SIZE = 128
31
+ DEMAG_SOLVE_CONDITION_DIAGNOSTICS_ENV = "NMAG_DEMAG_SOLVE_CONDITION_DIAGNOSTICS"
32
+ DEMAG_INTERNAL_TRACE_ENV = "NMAG_DEMAG_INTERNAL_TRACE"
33
+ DEMAG_DENSE_MAX_POINTS_ENV = "NMAG_DEMAG_DENSE_MAX_POINTS"
34
+ LEAST_SQUARES_RELATIVE_RESIDUAL_TOLERANCE = 1.0e-10
35
+
36
+ DEMAG_FEM_GEOMETRY_RUST_MIN_CELLS = 2048
37
+ DEMAG_FEM_ASSEMBLY_RUST_MIN_CELLS = 2048
38
+ DEMAG_NODAL_RECOVERY_RUST_MIN_CELLS = 2048
39
+ DEMAG_CELL_AVERAGE_RUST_MIN_CELLS = 2048
40
+ LLG_RUST_MIN_POINTS = 2048
41
+
42
+
43
+ def _default_config() -> NmagConfig:
44
+ """Return the environment-derived default for standalone helper calls."""
45
+
46
+ return NmagConfig.from_environment()
47
+
48
+
49
+ def _policy(config: NmagConfig | None) -> NmagConfig:
50
+ return _default_config() if config is None else config
51
+
52
+
53
+ def _rust_accelerator_available() -> bool:
54
+ try:
55
+ accelerator = importlib.import_module("nmag_accel")
56
+ except ImportError:
57
+ return False
58
+ return getattr(accelerator, "API_VERSION", None) == RUST_ACCELERATOR_API_VERSION
59
+
60
+
61
+ def _load_rust_accelerator(required_by: str = "NmagConfig.accelerator") -> ModuleType:
62
+ try:
63
+ accelerator = importlib.import_module("nmag_accel")
64
+ except ImportError as exc:
65
+ raise RuntimeError(
66
+ f"{required_by}=rust requires the optional nmag_accel extension. Build it with "
67
+ "`maturin develop --release --manifest-path rust/nmag_accel/Cargo.toml`."
68
+ ) from exc
69
+
70
+ actual_version = getattr(accelerator, "API_VERSION", None)
71
+ if actual_version != RUST_ACCELERATOR_API_VERSION:
72
+ raise RuntimeError(
73
+ f"{required_by}=rust found an incompatible nmag_accel extension "
74
+ f"(API_VERSION={actual_version!r}, expected {RUST_ACCELERATOR_API_VERSION}). "
75
+ "Rebuild it with `maturin develop --release --manifest-path "
76
+ "rust/nmag_accel/Cargo.toml`."
77
+ )
78
+ return accelerator
79
+
80
+
81
+ def _select_rust_backend(
82
+ *,
83
+ config: NmagConfig | None,
84
+ kernel: RustKernel,
85
+ fallback: str,
86
+ item_count: int | None = None,
87
+ rust_min_items: int | None = None,
88
+ ) -> str:
89
+ """Resolve one optional Rust kernel without process-global selectors."""
90
+
91
+ mode = _policy(config).accelerator_mode_for(kernel)
92
+ if mode == "off":
93
+ return fallback
94
+ if mode == "rust":
95
+ _load_rust_accelerator(f"NmagConfig.accelerator[{kernel.value!r}]")
96
+ return "rust"
97
+ if not _rust_accelerator_available():
98
+ return fallback
99
+ if (
100
+ item_count is not None
101
+ and rust_min_items is not None
102
+ and item_count < rust_min_items
103
+ ):
104
+ return fallback
105
+ return "rust"
106
+
107
+
108
+ def _rust_selection_metadata(
109
+ *,
110
+ config: NmagConfig | None,
111
+ kernel: RustKernel,
112
+ selected_backend: str,
113
+ fallback: str,
114
+ item_count: int | None = None,
115
+ rust_min_items: int | None = None,
116
+ ) -> dict[str, object]:
117
+ policy = _policy(config)
118
+ requested = policy.accelerator_mode_for(kernel)
119
+ available = _rust_accelerator_available()
120
+ if requested == "off":
121
+ reason = "accelerator_off"
122
+ elif requested == "rust":
123
+ reason = "explicit_rust_request"
124
+ elif not available:
125
+ reason = "rust_accelerator_unavailable"
126
+ elif rust_min_items is not None and item_count is not None and item_count < rust_min_items:
127
+ reason = "item_count_below_rust_min"
128
+ elif selected_backend == fallback:
129
+ reason = "auto_fallback"
130
+ else:
131
+ reason = "rust_accelerator_available"
132
+ metadata: dict[str, object] = {
133
+ "requested": requested,
134
+ "selected": selected_backend,
135
+ "rust_available": available,
136
+ "selection_reason": reason,
137
+ }
138
+ if item_count is not None:
139
+ metadata["item_count"] = int(item_count)
140
+ if rust_min_items is not None:
141
+ metadata["rust_min_items"] = int(rust_min_items)
142
+ return metadata
143
+
144
+
145
+ def _selected_integrator_backend(config: NmagConfig | None = None) -> str:
146
+ policy = _policy(config)
147
+ backend = policy.integrator_backend
148
+ if backend == "diffsol" and _selected_memory_mode() == "low":
149
+ raise ValueError(
150
+ "NmagConfig(integrator_backend='diffsol') constructs a dense affine operator "
151
+ f"and is incompatible with {MEMORY_MODE_ENV}=low; use scipy instead."
152
+ )
153
+ if backend == "diffsol" and policy.accelerator == "off":
154
+ raise ValueError(
155
+ "NmagConfig(integrator_backend='diffsol') requires the Rust accelerator; "
156
+ "accelerator='off' disables it."
157
+ )
158
+ return backend
159
+
160
+
161
+ def _selected_memory_mode() -> str:
162
+ mode = os.environ.get(MEMORY_MODE_ENV, "auto").strip().lower()
163
+ if mode not in MEMORY_MODES:
164
+ choices = ", ".join(sorted(MEMORY_MODES))
165
+ raise ValueError(f"Unsupported {MEMORY_MODE_ENV}={mode!r}; choose one of {choices}.")
166
+ return mode
167
+
168
+
169
+ def _auto_prefers_low_memory_storage(
170
+ *, estimated_dense_bytes: int, item_count: int | None, fallback_min_items: int
171
+ ) -> bool:
172
+ available = available_memory_bytes()
173
+ if available is not None:
174
+ return estimated_dense_bytes > available * AUTO_DENSE_MEMORY_FRACTION
175
+ return item_count is not None and item_count >= fallback_min_items
176
+
177
+
178
+ def _selected_lindholm_bem_backend(config: NmagConfig | None = None) -> str:
179
+ return _select_rust_backend(
180
+ config=config, kernel=RustKernel.LINDHOLM_BEM, fallback="numba"
181
+ )
182
+
183
+
184
+ def _lindholm_bem_backend_selection_metadata(config: NmagConfig | None = None) -> dict[str, object]:
185
+ return _rust_selection_metadata(
186
+ config=config,
187
+ kernel=RustKernel.LINDHOLM_BEM,
188
+ selected_backend=_selected_lindholm_bem_backend(config),
189
+ fallback="numba",
190
+ )
191
+
192
+
193
+ def _selected_probe_geometry_backend(config: NmagConfig | None = None) -> str:
194
+ return _select_rust_backend(
195
+ config=config, kernel=RustKernel.PROBE_GEOMETRY, fallback="python"
196
+ )
197
+
198
+
199
+ def _probe_geometry_backend_selection_metadata(config: NmagConfig | None = None) -> dict[str, object]:
200
+ return _rust_selection_metadata(
201
+ config=config,
202
+ kernel=RustKernel.PROBE_GEOMETRY,
203
+ selected_backend=_selected_probe_geometry_backend(config),
204
+ fallback="python",
205
+ )
206
+
207
+
208
+ def _selected_demag_fem_geometry_backend(
209
+ cell_count: int | None = None, config: NmagConfig | None = None
210
+ ) -> str:
211
+ return _select_rust_backend(
212
+ config=config,
213
+ kernel=RustKernel.FEM_GEOMETRY,
214
+ fallback="python",
215
+ item_count=cell_count,
216
+ rust_min_items=DEMAG_FEM_GEOMETRY_RUST_MIN_CELLS,
217
+ )
218
+
219
+
220
+ def _demag_fem_geometry_backend_selection_metadata(
221
+ cell_count: int | None = None, config: NmagConfig | None = None
222
+ ) -> dict[str, object]:
223
+ return _rust_selection_metadata(
224
+ config=config,
225
+ kernel=RustKernel.FEM_GEOMETRY,
226
+ selected_backend=_selected_demag_fem_geometry_backend(cell_count, config),
227
+ fallback="python",
228
+ item_count=cell_count,
229
+ rust_min_items=DEMAG_FEM_GEOMETRY_RUST_MIN_CELLS,
230
+ )
231
+
232
+
233
+ def _selected_demag_boundary_face_backend(config: NmagConfig | None = None) -> str:
234
+ return _select_rust_backend(
235
+ config=config, kernel=RustKernel.BOUNDARY_FACES, fallback="python"
236
+ )
237
+
238
+
239
+ def _demag_boundary_face_backend_selection_metadata(
240
+ config: NmagConfig | None = None,
241
+ ) -> dict[str, object]:
242
+ return _rust_selection_metadata(
243
+ config=config,
244
+ kernel=RustKernel.BOUNDARY_FACES,
245
+ selected_backend=_selected_demag_boundary_face_backend(config),
246
+ fallback="python",
247
+ )
248
+
249
+
250
+ def _selected_demag_fem_assembly_backend(
251
+ cell_count: int | None = None, config: NmagConfig | None = None
252
+ ) -> str:
253
+ return _select_rust_backend(
254
+ config=config,
255
+ kernel=RustKernel.FEM_ASSEMBLY,
256
+ fallback="python",
257
+ item_count=cell_count,
258
+ rust_min_items=DEMAG_FEM_ASSEMBLY_RUST_MIN_CELLS,
259
+ )
260
+
261
+
262
+ def _demag_fem_assembly_backend_selection_metadata(
263
+ cell_count: int | None = None, config: NmagConfig | None = None
264
+ ) -> dict[str, object]:
265
+ return _rust_selection_metadata(
266
+ config=config,
267
+ kernel=RustKernel.FEM_ASSEMBLY,
268
+ selected_backend=_selected_demag_fem_assembly_backend(cell_count, config),
269
+ fallback="python",
270
+ item_count=cell_count,
271
+ rust_min_items=DEMAG_FEM_ASSEMBLY_RUST_MIN_CELLS,
272
+ )
273
+
274
+
275
+ def _selected_demag_nodal_recovery_backend(
276
+ cell_count: int | None = None, config: NmagConfig | None = None
277
+ ) -> str:
278
+ return _select_rust_backend(
279
+ config=config,
280
+ kernel=RustKernel.NODAL_RECOVERY,
281
+ fallback="python",
282
+ item_count=cell_count,
283
+ rust_min_items=DEMAG_NODAL_RECOVERY_RUST_MIN_CELLS,
284
+ )
285
+
286
+
287
+ def _demag_nodal_recovery_backend_selection_metadata(
288
+ cell_count: int | None = None, config: NmagConfig | None = None
289
+ ) -> dict[str, object]:
290
+ return _rust_selection_metadata(
291
+ config=config,
292
+ kernel=RustKernel.NODAL_RECOVERY,
293
+ selected_backend=_selected_demag_nodal_recovery_backend(cell_count, config),
294
+ fallback="python",
295
+ item_count=cell_count,
296
+ rust_min_items=DEMAG_NODAL_RECOVERY_RUST_MIN_CELLS,
297
+ )
298
+
299
+
300
+ def _selected_demag_cell_average_backend(
301
+ cell_count: int | None = None, config: NmagConfig | None = None
302
+ ) -> str:
303
+ return _select_rust_backend(
304
+ config=config,
305
+ kernel=RustKernel.CELL_AVERAGE,
306
+ fallback="python",
307
+ item_count=cell_count,
308
+ rust_min_items=DEMAG_CELL_AVERAGE_RUST_MIN_CELLS,
309
+ )
310
+
311
+
312
+ def _demag_cell_average_backend_selection_metadata(
313
+ cell_count: int | None = None, config: NmagConfig | None = None
314
+ ) -> dict[str, object]:
315
+ return _rust_selection_metadata(
316
+ config=config,
317
+ kernel=RustKernel.CELL_AVERAGE,
318
+ selected_backend=_selected_demag_cell_average_backend(cell_count, config),
319
+ fallback="python",
320
+ item_count=cell_count,
321
+ rust_min_items=DEMAG_CELL_AVERAGE_RUST_MIN_CELLS,
322
+ )
323
+
324
+
325
+ def _selected_llg_backend(
326
+ point_count: int | None = None, config: NmagConfig | None = None
327
+ ) -> str:
328
+ return _select_rust_backend(
329
+ config=config,
330
+ kernel=RustKernel.LLG,
331
+ fallback="python",
332
+ item_count=point_count,
333
+ rust_min_items=LLG_RUST_MIN_POINTS,
334
+ )
335
+
336
+
337
+ def _llg_backend_selection_metadata(
338
+ point_count: int | None = None, config: NmagConfig | None = None
339
+ ) -> dict[str, object]:
340
+ return _rust_selection_metadata(
341
+ config=config,
342
+ kernel=RustKernel.LLG,
343
+ selected_backend=_selected_llg_backend(point_count, config),
344
+ fallback="python",
345
+ item_count=point_count,
346
+ rust_min_items=LLG_RUST_MIN_POINTS,
347
+ )
348
+
349
+
350
+ def _selected_maxangle_backend(config: NmagConfig | None = None) -> str:
351
+ return _select_rust_backend(
352
+ config=config, kernel=RustKernel.MAXANGLE, fallback="python"
353
+ )
354
+
355
+
356
+ def _maxangle_backend_selection_metadata(config: NmagConfig | None = None) -> dict[str, object]:
357
+ return _rust_selection_metadata(
358
+ config=config,
359
+ kernel=RustKernel.MAXANGLE,
360
+ selected_backend=_selected_maxangle_backend(config),
361
+ fallback="python",
362
+ )
363
+
364
+
365
+ def _demag_bem_auto_matrix_free_min_boundary_nodes() -> int:
366
+ return _positive_environment_int(
367
+ DEMAG_BEM_AUTO_MATRIX_FREE_MIN_BOUNDARY_NODES_ENV,
368
+ DEFAULT_DEMAG_BEM_AUTO_MATRIX_FREE_MIN_BOUNDARY_NODES,
369
+ "boundary-node count",
370
+ )
371
+
372
+
373
+ def _demag_fem_auto_sparse_min_points() -> int:
374
+ return _positive_environment_int(
375
+ DEMAG_FEM_AUTO_SPARSE_MIN_POINTS_ENV,
376
+ DEFAULT_DEMAG_FEM_AUTO_SPARSE_MIN_POINTS,
377
+ "point count",
378
+ )
379
+
380
+
381
+ def _demag_linear_solver_auto_scipy_min_size() -> int:
382
+ return _positive_environment_int(
383
+ DEMAG_LINEAR_SOLVER_AUTO_SCIPY_MIN_SIZE_ENV,
384
+ DEFAULT_DEMAG_LINEAR_SOLVER_AUTO_SCIPY_MIN_SIZE,
385
+ "dense dimension",
386
+ )
387
+
388
+
389
+ def _positive_environment_int(name: str, default: int, description: str) -> int:
390
+ raw_value = os.environ.get(name, str(default))
391
+ try:
392
+ minimum = int(raw_value)
393
+ except ValueError as exc:
394
+ raise ValueError(
395
+ f"Unsupported {name}={raw_value!r}; expected a positive integer {description}."
396
+ ) from exc
397
+ if minimum <= 0:
398
+ raise ValueError(
399
+ f"Unsupported {name}={raw_value!r}; expected a positive integer {description}."
400
+ )
401
+ return minimum
402
+
403
+
404
+ def _selected_demag_bem_storage_backend(
405
+ boundary_node_count: int | None = None,
406
+ config: NmagConfig | None = None,
407
+ ) -> str:
408
+ backend = (
409
+ os.environ.get(DEMAG_BEM_STORAGE_BACKEND_ENV, "auto").strip().lower()
410
+ if config is None
411
+ else config.demag_bem_storage
412
+ )
413
+ if backend not in DEMAG_BEM_STORAGE_BACKENDS:
414
+ choices = ", ".join(sorted(DEMAG_BEM_STORAGE_BACKENDS))
415
+ raise ValueError(f"Unsupported {DEMAG_BEM_STORAGE_BACKEND_ENV}={backend!r}; choose one of {choices}.")
416
+ if backend != "auto":
417
+ return backend
418
+ if _selected_memory_mode() == "low":
419
+ return "matrix-free"
420
+ count = boundary_node_count or 0
421
+ if _auto_prefers_low_memory_storage(
422
+ estimated_dense_bytes=count * count * 8,
423
+ item_count=boundary_node_count,
424
+ fallback_min_items=_demag_bem_auto_matrix_free_min_boundary_nodes(),
425
+ ):
426
+ mode = _policy(config).accelerator_mode_for(RustKernel.LINDHOLM_BEM)
427
+ if mode == "off" or (mode == "auto" and not _rust_accelerator_available()):
428
+ return "matrix-free"
429
+ if mode == "rust":
430
+ _load_rust_accelerator("NmagConfig.accelerator['lindholm_bem']")
431
+ return "hierarchical"
432
+ return "dense"
433
+
434
+
435
+ def _selected_demag_fem_matrix_backend(point_count: int | None = None) -> str:
436
+ backend = os.environ.get(DEMAG_FEM_MATRIX_BACKEND_ENV, "auto").strip().lower()
437
+ if backend not in DEMAG_FEM_MATRIX_BACKENDS:
438
+ choices = ", ".join(sorted(DEMAG_FEM_MATRIX_BACKENDS))
439
+ raise ValueError(f"Unsupported {DEMAG_FEM_MATRIX_BACKEND_ENV}={backend!r}; choose one of {choices}.")
440
+ if backend != "auto":
441
+ return backend
442
+ if _selected_memory_mode() == "low":
443
+ return "sparse"
444
+ count = point_count or 0
445
+ if _auto_prefers_low_memory_storage(
446
+ estimated_dense_bytes=count * count * 8 * 4,
447
+ item_count=point_count,
448
+ fallback_min_items=_demag_fem_auto_sparse_min_points(),
449
+ ):
450
+ return "sparse"
451
+ return "dense"
452
+
453
+
454
+ def _selected_demag_linear_solver_backend(dense_dimension: int | None = None) -> str:
455
+ backend = os.environ.get(DEMAG_LINEAR_SOLVER_BACKEND_ENV, "auto").strip().lower()
456
+ if backend not in DEMAG_LINEAR_SOLVER_BACKENDS:
457
+ choices = ", ".join(sorted(DEMAG_LINEAR_SOLVER_BACKENDS))
458
+ raise ValueError(f"Unsupported {DEMAG_LINEAR_SOLVER_BACKEND_ENV}={backend!r}; choose one of {choices}.")
459
+ if backend != "auto":
460
+ return backend
461
+ if dense_dimension is not None and dense_dimension >= _demag_linear_solver_auto_scipy_min_size():
462
+ return "scipy"
463
+ return "numpy"
464
+
465
+
466
+ def _demag_linear_solver_backend_selection_metadata(
467
+ dense_dimension: int | None = None,
468
+ ) -> dict[str, object]:
469
+ selected = _selected_demag_linear_solver_backend(dense_dimension)
470
+ requested = os.environ.get(DEMAG_LINEAR_SOLVER_BACKEND_ENV, "auto").strip().lower()
471
+ metadata: dict[str, object] = {"requested": requested, "selected": selected}
472
+ if dense_dimension is not None:
473
+ metadata["dense_dimension"] = int(dense_dimension)
474
+ metadata["auto_scipy_min_size"] = _demag_linear_solver_auto_scipy_min_size()
475
+ return metadata
476
+
477
+
478
+ def _demag_solve_condition_diagnostics_enabled() -> bool:
479
+ return _boolean_environment(DEMAG_SOLVE_CONDITION_DIAGNOSTICS_ENV)
480
+
481
+
482
+ def _demag_internal_trace_enabled() -> bool:
483
+ return _boolean_environment(DEMAG_INTERNAL_TRACE_ENV)
484
+
485
+
486
+ def _boolean_environment(name: str) -> bool:
487
+ raw_value = os.environ.get(name, "0")
488
+ value = raw_value.strip().lower()
489
+ if value in {"1", "true", "yes", "on"}:
490
+ return True
491
+ if value in {"0", "false", "no", "off", ""}:
492
+ return False
493
+ raise ValueError(f"Unsupported {name}={raw_value!r}; choose a boolean value.")