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/demag/linear.py ADDED
@@ -0,0 +1,489 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import time
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+
12
+ from ..backends import (
13
+ DEMAG_DENSE_MAX_POINTS_ENV,
14
+ LEAST_SQUARES_RELATIVE_RESIDUAL_TOLERANCE,
15
+ _demag_solve_condition_diagnostics_enabled,
16
+ _selected_demag_linear_solver_backend,
17
+ )
18
+
19
+ LOGGER = logging.getLogger(__name__)
20
+ _scipy_linalg_cache: Any | None = None
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class ScipyLUFactorization:
25
+ """Reusable LU factorization for a geometry-dependent dense system."""
26
+
27
+ lu: np.ndarray
28
+ pivots: np.ndarray
29
+
30
+ def solve(self, rhs: np.ndarray) -> np.ndarray:
31
+ return np.asarray(
32
+ _scipy_linalg().lu_solve(
33
+ (self.lu, self.pivots),
34
+ np.asarray(rhs, dtype=float),
35
+ check_finite=False,
36
+ ),
37
+ dtype=float,
38
+ )
39
+
40
+
41
+ def _factorize_scipy(matrix: np.ndarray) -> ScipyLUFactorization:
42
+ lu, pivots = _scipy_linalg().lu_factor(
43
+ np.asarray(matrix, dtype=float),
44
+ check_finite=False,
45
+ )
46
+ return ScipyLUFactorization(np.asarray(lu, dtype=float), np.asarray(pivots, dtype=int))
47
+
48
+
49
+ def _factorize_gauge_fixed(matrix: np.ndarray) -> ScipyLUFactorization:
50
+ constraint = np.ones(matrix.shape[0], dtype=float)
51
+ augmented = np.block(
52
+ [
53
+ [matrix, constraint[:, np.newaxis]],
54
+ [constraint[np.newaxis, :], np.zeros((1, 1), dtype=float)],
55
+ ]
56
+ )
57
+ return _factorize_scipy(augmented)
58
+
59
+
60
+ def __getattr__(name: str) -> Any:
61
+ if name == "_SCIPY_LINALG":
62
+ return _scipy_linalg_cache
63
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
64
+
65
+
66
+ def _scipy_linalg() -> Any:
67
+ global _scipy_linalg_cache
68
+ if _scipy_linalg_cache is None:
69
+ from scipy import linalg
70
+
71
+ _scipy_linalg_cache = linalg
72
+ return _scipy_linalg_cache
73
+
74
+
75
+ def _demag_dense_max_points() -> int | None:
76
+ raw_value = os.environ.get(DEMAG_DENSE_MAX_POINTS_ENV)
77
+ if raw_value is None:
78
+ return None
79
+ try:
80
+ value = int(raw_value)
81
+ except ValueError as error:
82
+ raise ValueError(
83
+ f"{DEMAG_DENSE_MAX_POINTS_ENV} must be a positive integer, got {raw_value!r}."
84
+ ) from error
85
+ if value < 1:
86
+ raise ValueError(
87
+ f"{DEMAG_DENSE_MAX_POINTS_ENV} must be a positive integer, got {raw_value!r}."
88
+ )
89
+ return value
90
+
91
+
92
+ def _validate_dense_demag_size(point_count: int) -> None:
93
+ maximum = _demag_dense_max_points()
94
+ if maximum is None or point_count <= maximum:
95
+ return
96
+ estimated_gib = point_count * point_count * 8 / (1024**3)
97
+ raise MemoryError(
98
+ "Dense demag FEM assembly refused a mesh with "
99
+ f"{point_count} points (one dense matrix is approximately {estimated_gib:.2f} GiB). "
100
+ f"The current safety limit is {maximum}; change {DEMAG_DENSE_MAX_POINTS_ENV} only "
101
+ "when the required memory is known to be available."
102
+ )
103
+
104
+
105
+ def _scipy_sparse_modules() -> tuple[Any, Any]:
106
+ from scipy import sparse
107
+ from scipy.sparse import linalg as sparse_linalg
108
+
109
+ return sparse, sparse_linalg
110
+
111
+
112
+ def _is_sparse_matrix(matrix: Any) -> bool:
113
+ sparse, _sparse_linalg = _scipy_sparse_modules()
114
+ return bool(sparse.issparse(matrix))
115
+
116
+
117
+ def _solve_sparse_gauge_fixed(
118
+ matrix: Any,
119
+ rhs: np.ndarray,
120
+ *,
121
+ record_timing: Callable[[str, float], None] | None = None,
122
+ timing_prefix: str = "linear_solver:gauge",
123
+ ) -> np.ndarray:
124
+ _sparse, sparse_linalg = _scipy_sparse_modules()
125
+ started = time.perf_counter()
126
+ point_count = int(matrix.shape[0])
127
+ diagonal = np.asarray(matrix.diagonal(), dtype=float)
128
+ matrix_scale = float(np.max(np.abs(diagonal))) if diagonal.size else 1.0
129
+ if matrix_scale <= np.finfo(float).tiny:
130
+ return np.zeros_like(rhs, dtype=float)
131
+
132
+ scaled_matrix = matrix / matrix_scale
133
+ scaled_rhs = np.asarray(rhs, dtype=float) / matrix_scale
134
+
135
+ def apply_constrained_matrix(vector: np.ndarray) -> np.ndarray:
136
+ return np.asarray(scaled_matrix @ vector, dtype=float) + np.mean(vector)
137
+
138
+ constrained = sparse_linalg.LinearOperator(
139
+ matrix.shape,
140
+ matvec=apply_constrained_matrix,
141
+ dtype=np.dtype(float),
142
+ )
143
+ inverse_diagonal = 1.0 / (diagonal / matrix_scale + 1.0 / max(point_count, 1))
144
+
145
+ def apply_preconditioner(vector: np.ndarray) -> np.ndarray:
146
+ return inverse_diagonal * vector
147
+
148
+ preconditioner = sparse_linalg.LinearOperator(
149
+ matrix.shape,
150
+ matvec=apply_preconditioner,
151
+ dtype=np.dtype(float),
152
+ )
153
+ solution, status = sparse_linalg.cg(
154
+ constrained,
155
+ scaled_rhs,
156
+ rtol=1.0e-12,
157
+ atol=1.0e-14,
158
+ maxiter=max(1000, 10 * point_count),
159
+ M=preconditioner,
160
+ )
161
+ if int(status) != 0:
162
+ result = sparse_linalg.lsmr(
163
+ constrained,
164
+ scaled_rhs,
165
+ atol=1.0e-12,
166
+ btol=1.0e-12,
167
+ conlim=0.0,
168
+ maxiter=max(1000, 10 * point_count),
169
+ )
170
+ solution = result[0]
171
+ if int(result[1]) not in {0, 1, 2}:
172
+ raise np.linalg.LinAlgError(
173
+ "Sparse gauge solve did not converge "
174
+ f"(CG status {int(status)}, LSMR status {int(result[1])})."
175
+ )
176
+ solution = np.asarray(solution, dtype=float)
177
+ solution -= np.mean(solution)
178
+ if record_timing is not None:
179
+ record_timing(f"{timing_prefix}:sparse_iterative", time.perf_counter() - started)
180
+ return solution
181
+
182
+
183
+ def _solve_sparse_linear_system(
184
+ matrix: Any,
185
+ rhs: np.ndarray,
186
+ *,
187
+ record_timing: Callable[[str, float], None] | None = None,
188
+ timing_prefix: str = "linear_solver",
189
+ ) -> np.ndarray:
190
+ _sparse, sparse_linalg = _scipy_sparse_modules()
191
+ started = time.perf_counter()
192
+ diagonal = np.asarray(matrix.diagonal(), dtype=float)
193
+ matrix_scale = float(np.max(np.abs(diagonal))) if diagonal.size else 1.0
194
+ if matrix_scale <= np.finfo(float).tiny:
195
+ return np.zeros_like(rhs, dtype=float)
196
+ scaled_matrix = matrix / matrix_scale
197
+ scaled_rhs = np.asarray(rhs, dtype=float) / matrix_scale
198
+ scaled_diagonal = diagonal / matrix_scale
199
+ inverse_diagonal = np.ones_like(scaled_diagonal)
200
+ nonzero = np.abs(scaled_diagonal) > np.finfo(float).tiny
201
+ inverse_diagonal[nonzero] = 1.0 / scaled_diagonal[nonzero]
202
+
203
+ def apply_preconditioner(vector: np.ndarray) -> np.ndarray:
204
+ return inverse_diagonal * vector
205
+
206
+ preconditioner = sparse_linalg.LinearOperator(
207
+ matrix.shape,
208
+ matvec=apply_preconditioner,
209
+ dtype=np.dtype(float),
210
+ )
211
+ solution, status = sparse_linalg.cg(
212
+ scaled_matrix,
213
+ scaled_rhs,
214
+ rtol=1.0e-12,
215
+ atol=0.0,
216
+ maxiter=max(1000, 10 * int(matrix.shape[0])),
217
+ M=preconditioner,
218
+ )
219
+ if int(status) != 0:
220
+ fallback = sparse_linalg.lsmr(
221
+ scaled_matrix,
222
+ scaled_rhs,
223
+ atol=1.0e-12,
224
+ btol=1.0e-12,
225
+ maxiter=max(1000, 10 * int(matrix.shape[0])),
226
+ )
227
+ solution = fallback[0]
228
+ if int(fallback[1]) not in {0, 1, 2}:
229
+ raise np.linalg.LinAlgError(
230
+ "Sparse linear solve did not converge "
231
+ f"(CG status {int(status)}, LSMR status {int(fallback[1])})."
232
+ )
233
+ if record_timing is not None:
234
+ record_timing(f"{timing_prefix}:sparse_iterative", time.perf_counter() - started)
235
+ return np.asarray(solution, dtype=float)
236
+
237
+
238
+ def _validated_least_squares_solution(
239
+ matrix: np.ndarray,
240
+ rhs: np.ndarray,
241
+ solution: np.ndarray,
242
+ ) -> np.ndarray:
243
+ residual = matrix @ solution - rhs
244
+ rhs_norm = float(np.linalg.norm(rhs))
245
+ residual_norm = float(np.linalg.norm(residual))
246
+ relative_residual = residual_norm / rhs_norm if rhs_norm else residual_norm
247
+ if not np.isfinite(relative_residual) or (
248
+ relative_residual > LEAST_SQUARES_RELATIVE_RESIDUAL_TOLERANCE
249
+ ):
250
+ raise np.linalg.LinAlgError(
251
+ "Singular demag system has no accurate least-squares solution: "
252
+ f"relative residual={relative_residual:.3e}."
253
+ )
254
+ LOGGER.warning(
255
+ "Using a least-squares solution for a singular demag system (relative residual %.3e).",
256
+ relative_residual,
257
+ )
258
+ return solution
259
+
260
+
261
+ def _max_abs(values: np.ndarray) -> float:
262
+ return float(np.max(np.abs(values))) if values.size else 0.0
263
+
264
+
265
+ def _l2_norm(values: np.ndarray) -> float:
266
+ return float(np.linalg.norm(values.ravel())) if values.size else 0.0
267
+
268
+
269
+ def _safe_relative_residual(numerator: np.ndarray, scale: np.ndarray) -> float:
270
+ numerator_l2 = _l2_norm(numerator)
271
+ scale_l2 = _l2_norm(scale)
272
+ if scale_l2 == 0.0:
273
+ return 0.0 if numerator_l2 == 0.0 else float("inf")
274
+ return float(numerator_l2 / scale_l2)
275
+
276
+
277
+ def _dense_condition_number(matrix: np.ndarray) -> float:
278
+ if matrix.size == 0:
279
+ return 0.0
280
+ return float(np.linalg.cond(np.asarray(matrix, dtype=float)))
281
+
282
+
283
+ def _solve_gauge_fixed(
284
+ matrix: np.ndarray,
285
+ rhs: np.ndarray,
286
+ *,
287
+ record_timing: Callable[[str, float], None] | None = None,
288
+ timing_prefix: str = "linear_solver:gauge",
289
+ ) -> np.ndarray:
290
+ augmented, augmented_rhs = _gauge_augmented_system(matrix, rhs, record_timing, timing_prefix)
291
+ return _solve_dense_system(
292
+ augmented,
293
+ augmented_rhs,
294
+ record_timing=record_timing,
295
+ timing_prefix=timing_prefix,
296
+ )[:-1]
297
+
298
+
299
+ def _gauge_augmented_system(
300
+ matrix: np.ndarray,
301
+ rhs: np.ndarray,
302
+ record_timing: Callable[[str, float], None] | None,
303
+ timing_prefix: str,
304
+ ) -> tuple[np.ndarray, np.ndarray]:
305
+ started = time.perf_counter()
306
+ try:
307
+ constraint = np.ones(len(rhs), dtype=float)
308
+ augmented = np.block(
309
+ [
310
+ [matrix, constraint[:, np.newaxis]],
311
+ [constraint[np.newaxis, :], np.zeros((1, 1), dtype=float)],
312
+ ]
313
+ )
314
+ augmented_rhs = np.concatenate([rhs, [0.0]])
315
+ finally:
316
+ if record_timing is not None:
317
+ record_timing(f"{timing_prefix}:setup", time.perf_counter() - started)
318
+ return augmented, augmented_rhs
319
+
320
+
321
+ def _solve_dense_system(
322
+ matrix: np.ndarray,
323
+ rhs: np.ndarray,
324
+ *,
325
+ record_timing: Callable[[str, float], None] | None,
326
+ timing_prefix: str,
327
+ ) -> np.ndarray:
328
+ backend = _selected_demag_linear_solver_backend(int(matrix.shape[0]))
329
+ if backend == "scipy":
330
+ return _solve_dense_scipy(matrix, rhs, record_timing, timing_prefix)
331
+ return _solve_dense_numpy(matrix, rhs, record_timing, timing_prefix)
332
+
333
+
334
+ def _solve_dense_scipy(
335
+ matrix: np.ndarray,
336
+ rhs: np.ndarray,
337
+ record_timing: Callable[[str, float], None] | None,
338
+ timing_prefix: str,
339
+ ) -> np.ndarray:
340
+ started = time.perf_counter()
341
+ linalg = _scipy_linalg()
342
+ if record_timing is not None:
343
+ record_timing(f"{timing_prefix}:scipy_import", time.perf_counter() - started)
344
+ started = time.perf_counter()
345
+ try:
346
+ result = linalg.solve(matrix, rhs, assume_a="sym", check_finite=False)
347
+ if record_timing is not None:
348
+ record_timing(f"{timing_prefix}:scipy_solve", time.perf_counter() - started)
349
+ return result
350
+ except np.linalg.LinAlgError:
351
+ if record_timing is not None:
352
+ record_timing(
353
+ f"{timing_prefix}:scipy_solve_failed",
354
+ time.perf_counter() - started,
355
+ )
356
+ started = time.perf_counter()
357
+ result = _validated_least_squares_solution(
358
+ matrix,
359
+ rhs,
360
+ linalg.lstsq(matrix, rhs, check_finite=False)[0],
361
+ )
362
+ if record_timing is not None:
363
+ record_timing(f"{timing_prefix}:scipy_lstsq", time.perf_counter() - started)
364
+ return result
365
+
366
+
367
+ def _solve_linear_system(
368
+ matrix: np.ndarray,
369
+ rhs: np.ndarray,
370
+ *,
371
+ record_timing: Callable[[str, float], None] | None = None,
372
+ timing_prefix: str = "linear_solver",
373
+ ) -> np.ndarray:
374
+ return _solve_dense_system(
375
+ matrix,
376
+ rhs,
377
+ record_timing=record_timing,
378
+ timing_prefix=timing_prefix,
379
+ )
380
+
381
+
382
+ def _solve_dense_numpy(
383
+ matrix: np.ndarray,
384
+ rhs: np.ndarray,
385
+ record_timing: Callable[[str, float], None] | None,
386
+ timing_prefix: str,
387
+ ) -> np.ndarray:
388
+ started = time.perf_counter()
389
+ try:
390
+ result = np.linalg.solve(matrix, rhs)
391
+ if record_timing is not None:
392
+ record_timing(f"{timing_prefix}:numpy_solve", time.perf_counter() - started)
393
+ return result
394
+ except np.linalg.LinAlgError:
395
+ if record_timing is not None:
396
+ record_timing(
397
+ f"{timing_prefix}:numpy_solve_failed",
398
+ time.perf_counter() - started,
399
+ )
400
+ started = time.perf_counter()
401
+ result = _validated_least_squares_solution(
402
+ matrix,
403
+ rhs,
404
+ np.linalg.lstsq(matrix, rhs, rcond=None)[0],
405
+ )
406
+ if record_timing is not None:
407
+ record_timing(f"{timing_prefix}:numpy_lstsq", time.perf_counter() - started)
408
+ return result
409
+
410
+
411
+ def _demag_solve_diagnostics(
412
+ stiffness: Any,
413
+ divergence: np.ndarray,
414
+ boundary_nodes: np.ndarray,
415
+ phi1: np.ndarray,
416
+ phi2_boundary: np.ndarray,
417
+ phi2: np.ndarray,
418
+ phi: np.ndarray,
419
+ ) -> dict[str, float | int]:
420
+ phi1_residual = np.asarray(stiffness @ phi1, dtype=float) - divergence
421
+ phi1_centered_residual = (
422
+ phi1_residual - np.mean(phi1_residual) if phi1_residual.size else phi1_residual
423
+ )
424
+ divergence_centered = divergence - np.mean(divergence) if divergence.size else divergence
425
+
426
+ boundary_mask = np.zeros(len(phi), dtype=bool)
427
+ boundary_mask[boundary_nodes] = True
428
+ interior_nodes = np.flatnonzero(~boundary_mask)
429
+ interior_rows = (
430
+ stiffness[interior_nodes]
431
+ if _is_sparse_matrix(stiffness)
432
+ else stiffness[np.ix_(interior_nodes, np.arange(len(phi)))]
433
+ )
434
+ dirichlet_residual = (
435
+ np.asarray(interior_rows @ phi2, dtype=float)
436
+ if interior_nodes.size
437
+ else np.empty(0, dtype=float)
438
+ )
439
+ dirichlet_scale = (
440
+ np.asarray(np.abs(interior_rows) @ np.abs(phi2), dtype=float)
441
+ if interior_nodes.size
442
+ else np.empty(0, dtype=float)
443
+ )
444
+
445
+ diagnostics: dict[str, float | int] = {
446
+ "node_count": int(len(phi)),
447
+ "boundary_node_count": int(len(boundary_nodes)),
448
+ "gauge_augmented_size": int(len(divergence) + 1),
449
+ "divergence_sum_abs": abs(float(np.sum(divergence))),
450
+ "phi1_gauge_sum_abs": abs(float(np.sum(phi1))),
451
+ "phi1_centered_residual_max_abs": _max_abs(phi1_centered_residual),
452
+ "phi1_centered_residual_l2": _l2_norm(phi1_centered_residual),
453
+ "phi1_centered_residual_relative_l2": _safe_relative_residual(
454
+ phi1_centered_residual,
455
+ divergence_centered,
456
+ ),
457
+ "dirichlet_interior_node_count": int(interior_nodes.size),
458
+ "dirichlet_residual_max_abs": _max_abs(dirichlet_residual),
459
+ "dirichlet_residual_l2": _l2_norm(dirichlet_residual),
460
+ "dirichlet_residual_relative_l2": _safe_relative_residual(
461
+ dirichlet_residual,
462
+ dirichlet_scale,
463
+ ),
464
+ "phi1_max_abs": _max_abs(phi1),
465
+ "phi2_boundary_max_abs": _max_abs(phi2_boundary),
466
+ "phi2_max_abs": _max_abs(phi2),
467
+ "phi_max_abs": _max_abs(phi),
468
+ }
469
+ if _demag_solve_condition_diagnostics_enabled():
470
+ if _is_sparse_matrix(stiffness):
471
+ diagnostics["condition_numbers_skipped_for_sparse_storage"] = 1
472
+ else:
473
+ constraint = np.ones(len(divergence), dtype=float)
474
+ augmented_stiffness = np.block(
475
+ [
476
+ [stiffness, constraint[:, np.newaxis]],
477
+ [constraint[np.newaxis, :], np.zeros((1, 1), dtype=float)],
478
+ ]
479
+ )
480
+ diagnostics["gauge_augmented_condition_number"] = _dense_condition_number(
481
+ augmented_stiffness,
482
+ )
483
+ diagnostics["stiffness_condition_number"] = _dense_condition_number(stiffness)
484
+ diagnostics["dirichlet_interior_condition_number"] = (
485
+ _dense_condition_number(stiffness[np.ix_(interior_nodes, interior_nodes)])
486
+ if interior_nodes.size
487
+ else 0.0
488
+ )
489
+ return diagnostics