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.
- anisotropy/__init__.py +28 -0
- anisotropy/anisotropy.py +13 -0
- anisotropy/evaluation.py +70 -0
- anisotropy/model.py +200 -0
- anisotropy/predefined.py +202 -0
- anisotropy/py.typed +1 -0
- anisotropy/values.py +84 -0
- mag_material/__init__.py +3 -0
- mag_material/mag_material.py +231 -0
- mag_material/parameters.py +150 -0
- mag_material/py.typed +1 -0
- nmag/__init__.py +36 -0
- nmag/backends.py +493 -0
- nmag/checkpoint.py +327 -0
- nmag/config.py +174 -0
- nmag/demag/__init__.py +39 -0
- nmag/demag/bem_operator.py +149 -0
- nmag/demag/geometry.py +134 -0
- nmag/demag/lindholm.py +133 -0
- nmag/demag/lindholm_fast.py +463 -0
- nmag/demag/linear.py +489 -0
- nmag/dynamics/__init__.py +255 -0
- nmag/output.py +27 -0
- nmag/parallel.py +45 -0
- nmag/py.typed +1 -0
- nmag/resources.py +38 -0
- nmag/simulation/__init__.py +519 -0
- nmag/simulation/anisotropy/__init__.py +5 -0
- nmag/simulation/anisotropy/fields.py +56 -0
- nmag/simulation/anisotropy/materials.py +138 -0
- nmag/simulation/demag/__init__.py +1 -0
- nmag/simulation/demag/bem/__init__.py +11 -0
- nmag/simulation/demag/bem/diagnostics.py +73 -0
- nmag/simulation/demag/bem/dirichlet.py +85 -0
- nmag/simulation/demag/bem/hierarchical.py +74 -0
- nmag/simulation/demag/bem/operator.py +292 -0
- nmag/simulation/demag/fem/__init__.py +11 -0
- nmag/simulation/demag/fem/assembly.py +114 -0
- nmag/simulation/demag/fem/charges.py +38 -0
- nmag/simulation/demag/fem/geometry.py +261 -0
- nmag/simulation/demag/fields/__init__.py +11 -0
- nmag/simulation/demag/fields/auxiliary.py +186 -0
- nmag/simulation/demag/fields/probe.py +77 -0
- nmag/simulation/demag/fields/recovery.py +207 -0
- nmag/simulation/demag/solver.py +17 -0
- nmag/simulation/dynamics/__init__.py +92 -0
- nmag/simulation/dynamics/advance.py +199 -0
- nmag/simulation/dynamics/integrator.py +263 -0
- nmag/simulation/exchange/__init__.py +13 -0
- nmag/simulation/exchange/coefficients.py +185 -0
- nmag/simulation/exchange/fields.py +106 -0
- nmag/simulation/exchange/llg_rhs.py +218 -0
- nmag/simulation/fields/__init__.py +19 -0
- nmag/simulation/fields/arrays.py +111 -0
- nmag/simulation/fields/availability.py +188 -0
- nmag/simulation/fields/averages.py +293 -0
- nmag/simulation/fields/derived.py +164 -0
- nmag/simulation/fields/maxangle.py +163 -0
- nmag/simulation/fields/probes.py +106 -0
- nmag/simulation/implicit_dynamics.py +221 -0
- nmag/simulation/mesh/__init__.py +13 -0
- nmag/simulation/mesh/geometry.py +153 -0
- nmag/simulation/mesh/materials.py +299 -0
- nmag/simulation/mesh/probe.py +234 -0
- nmag/simulation/restart.py +103 -0
- nmag/simulation/support.py +224 -0
- nmag_python_3-0.0.2.dist-info/METADATA +157 -0
- nmag_python_3-0.0.2.dist-info/RECORD +147 -0
- nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
- nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
- nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
- nmesh/__init__.py +130 -0
- nmesh/backend.py +286 -0
- nmesh/geometry/__init__.py +52 -0
- nmesh/geometry/boolean_operations.py +157 -0
- nmesh/geometry/primitives.py +453 -0
- nmesh/geometry/transform.py +126 -0
- nmesh/io/__init__.py +50 -0
- nmesh/io/ascii.py +132 -0
- nmesh/io/legacy_nmesh_hdf5.py +318 -0
- nmesh/io/meshio_support.py +170 -0
- nmesh/mesh_generation.py +182 -0
- nmesh/mesh_io.py +227 -0
- nmesh/mesh_model.py +147 -0
- nmesh/mesh_utilities.py +79 -0
- nmesh/mesher/__init__.py +21 -0
- nmesh/mesher/driver.py +146 -0
- nmesh/mesher/meshing_defaults.py +252 -0
- nmesh/mesher/meshing_parameters.py +185 -0
- nmesh/mesher/parity.py +21 -0
- nmesh/mesher/parity_canonical.py +142 -0
- nmesh/mesher/parity_comparison.py +191 -0
- nmesh/mesher/parity_metrics.py +114 -0
- nmesh/mesher/periodic.py +97 -0
- nmesh/mesher/relaxation/__init__.py +14 -0
- nmesh/mesher/relaxation/_constants.py +20 -0
- nmesh/mesher/relaxation/_types.py +15 -0
- nmesh/mesher/relaxation/density.py +170 -0
- nmesh/mesher/relaxation/engine/__init__.py +18 -0
- nmesh/mesher/relaxation/engine/state.py +155 -0
- nmesh/mesher/relaxation/engine/steps.py +248 -0
- nmesh/mesher/relaxation/engine/topology.py +230 -0
- nmesh/mesher/relaxation/forces/__init__.py +96 -0
- nmesh/mesher/relaxation/forces/jit.py +102 -0
- nmesh/mesher/relaxation/forces/neighbors.py +186 -0
- nmesh/mesher/relaxation/forces/simplex.py +302 -0
- nmesh/mesher/relaxation/forces/summary.py +207 -0
- nmesh/mesher/relaxation/forces/types.py +92 -0
- nmesh/mesher/relaxation/geometry/__init__.py +6 -0
- nmesh/mesher/relaxation/geometry/builder.py +154 -0
- nmesh/mesher/relaxation/geometry/model.py +194 -0
- nmesh/mesher/relaxation/seeding/__init__.py +74 -0
- nmesh/mesher/relaxation/seeding/periodic.py +88 -0
- nmesh/mesher/relaxation/seeding/points.py +88 -0
- nmesh/mesher/relaxation/seeding/sampling.py +142 -0
- nmesh/mesher/relaxation/topology/__init__.py +297 -0
- nmesh/mesher/relaxation/topology/finalize.py +78 -0
- nmesh/mesher/relaxation/topology/recovery.py +310 -0
- nmesh/mesher/sectioned_config.py +70 -0
- nmesh/nmesh.py +99 -0
- nmesh/py.typed +1 -0
- nmesh/utils/__init__.py +33 -0
- nmesh/utils/array_list_utils.py +128 -0
- nmesh/utils/constants.py +22 -0
- nmesh/utils/timing_memory_utils.py +51 -0
- nmesh/utils/types.py +13 -0
- si/constants.py +49 -0
- si/physical.py +722 -0
- si/py.typed +1 -0
- simulation/__init__.py +1 -0
- simulation/clock.py +237 -0
- simulation/data_writer.py +273 -0
- simulation/data_writer_collection.py +267 -0
- simulation/hysteresis.py +74 -0
- simulation/hysteresis_runner.py +286 -0
- simulation/hysteresis_schedule.py +180 -0
- simulation/inference/__init__.py +3 -0
- simulation/inference/inference.py +95 -0
- simulation/py.typed +1 -0
- simulation/quantity.py +88 -0
- simulation/simulation_core.py +458 -0
- throttler/__init__.py +3 -0
- throttler/py.typed +1 -0
- throttler/throttler.py +55 -0
- when/__init__.py +3 -0
- when/py.typed +1 -0
- 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
|