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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Map material anisotropy models onto geometric mesh nodes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from anisotropy import AnisotropyModel, PredefinedAnisotropy, anisotropy_signature_values
|
|
11
|
+
|
|
12
|
+
from ..support import _si_unit
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class NodalAnisotropyGroup:
|
|
17
|
+
model: AnisotropyModel
|
|
18
|
+
order: int | None
|
|
19
|
+
nodes: np.ndarray
|
|
20
|
+
saturation_magnetisation: float
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SimulationAnisotropyMaterialMixin:
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
_nodal_anisotropy_groups_cache: tuple[int, tuple[NodalAnisotropyGroup, ...]] | None
|
|
26
|
+
|
|
27
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
28
|
+
|
|
29
|
+
def _nodal_anisotropy_groups(self) -> tuple[NodalAnisotropyGroup, ...]:
|
|
30
|
+
token = self._mesh_geometry_token()
|
|
31
|
+
cached = self._nodal_anisotropy_groups_cache
|
|
32
|
+
if cached is not None and cached[0] == token:
|
|
33
|
+
return cached[1]
|
|
34
|
+
|
|
35
|
+
point_count = len(self._mesh_points())
|
|
36
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=np.int64)
|
|
37
|
+
regions = np.asarray(
|
|
38
|
+
self._require_mesh().regions or [1] * len(simplices),
|
|
39
|
+
dtype=np.int64,
|
|
40
|
+
)
|
|
41
|
+
assignments: list[tuple[np.ndarray, Any, float] | None] = [None] * point_count
|
|
42
|
+
|
|
43
|
+
if len(simplices) == 0:
|
|
44
|
+
if self.materials:
|
|
45
|
+
material = self.materials[0]
|
|
46
|
+
signature = anisotropy_signature_values(
|
|
47
|
+
material.anisotropy,
|
|
48
|
+
material.anisotropy_order,
|
|
49
|
+
)
|
|
50
|
+
ms = float(material.Ms.in_units_of(_si_unit("A/m")))
|
|
51
|
+
assignments = [(signature, material, ms)] * point_count
|
|
52
|
+
else:
|
|
53
|
+
if len(regions) != len(simplices):
|
|
54
|
+
raise ValueError("Mesh regions must contain one entry per simplex.")
|
|
55
|
+
for region in np.unique(regions):
|
|
56
|
+
material = self._simplex_material(int(region))
|
|
57
|
+
signature = anisotropy_signature_values(
|
|
58
|
+
material.anisotropy,
|
|
59
|
+
material.anisotropy_order,
|
|
60
|
+
)
|
|
61
|
+
ms = float(material.Ms.in_units_of(_si_unit("A/m")))
|
|
62
|
+
nodes = np.unique(simplices[regions == region].ravel())
|
|
63
|
+
for node in nodes:
|
|
64
|
+
previous = assignments[int(node)]
|
|
65
|
+
if previous is not None:
|
|
66
|
+
previous_signature, _previous_material, previous_ms = previous
|
|
67
|
+
if not self._anisotropy_materials_compatible(
|
|
68
|
+
_previous_material,
|
|
69
|
+
material,
|
|
70
|
+
previous_signature,
|
|
71
|
+
signature,
|
|
72
|
+
) or not np.isclose(previous_ms, ms, rtol=1.0e-12, atol=0.0):
|
|
73
|
+
raise NotImplementedError(
|
|
74
|
+
"A mesh node shared by materials with different anisotropy or Ms "
|
|
75
|
+
"requires material-specific magnetisation DOFs; node "
|
|
76
|
+
f"{int(node)} includes region {int(region)}."
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
assignments[int(node)] = (signature, material, ms)
|
|
80
|
+
|
|
81
|
+
if self.materials:
|
|
82
|
+
fallback = self.materials[0]
|
|
83
|
+
fallback_signature = anisotropy_signature_values(
|
|
84
|
+
fallback.anisotropy,
|
|
85
|
+
fallback.anisotropy_order,
|
|
86
|
+
)
|
|
87
|
+
fallback_ms = float(fallback.Ms.in_units_of(_si_unit("A/m")))
|
|
88
|
+
assignments = [
|
|
89
|
+
assignment or (fallback_signature, fallback, fallback_ms)
|
|
90
|
+
for assignment in assignments
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
grouped_nodes: dict[bytes, list[int]] = {}
|
|
94
|
+
grouped_materials: dict[bytes, tuple[Any, float]] = {}
|
|
95
|
+
for node, assignment in enumerate(assignments):
|
|
96
|
+
if assignment is None:
|
|
97
|
+
continue
|
|
98
|
+
signature, material, ms = assignment
|
|
99
|
+
key = signature.tobytes() + np.float64(ms).tobytes()
|
|
100
|
+
grouped_nodes.setdefault(key, []).append(node)
|
|
101
|
+
grouped_materials[key] = (material, ms)
|
|
102
|
+
|
|
103
|
+
groups = tuple(
|
|
104
|
+
NodalAnisotropyGroup(
|
|
105
|
+
model=grouped_materials[key][0].anisotropy,
|
|
106
|
+
order=grouped_materials[key][0].anisotropy_order,
|
|
107
|
+
nodes=np.asarray(nodes, dtype=np.int64),
|
|
108
|
+
saturation_magnetisation=grouped_materials[key][1],
|
|
109
|
+
)
|
|
110
|
+
for key, nodes in grouped_nodes.items()
|
|
111
|
+
)
|
|
112
|
+
self._nodal_anisotropy_groups_cache = (token, groups)
|
|
113
|
+
return groups
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _anisotropy_materials_compatible(
|
|
117
|
+
first: Any,
|
|
118
|
+
second: Any,
|
|
119
|
+
first_signature: np.ndarray,
|
|
120
|
+
second_signature: np.ndarray,
|
|
121
|
+
) -> bool:
|
|
122
|
+
first_model = first.anisotropy
|
|
123
|
+
second_model = second.anisotropy
|
|
124
|
+
if first_model is None or second_model is None:
|
|
125
|
+
return first_model is second_model
|
|
126
|
+
if isinstance(first_model, PredefinedAnisotropy) and isinstance(
|
|
127
|
+
second_model,
|
|
128
|
+
PredefinedAnisotropy,
|
|
129
|
+
):
|
|
130
|
+
return bool(
|
|
131
|
+
np.allclose(
|
|
132
|
+
first_signature,
|
|
133
|
+
second_signature,
|
|
134
|
+
rtol=1.0e-12,
|
|
135
|
+
atol=1.0e-12,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return first_model is second_model and first.anisotropy_order == second.anisotropy_order
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Demagnetization solver, field, and geometry implementation modules."""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""BEM demagnetization compatibility façade composed from focused mixins."""
|
|
2
|
+
|
|
3
|
+
from .dirichlet import SimulationDemagBemDirichletMixin
|
|
4
|
+
from .operator import SimulationDemagBemOperatorMixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SimulationDemagBemMixin(
|
|
8
|
+
SimulationDemagBemOperatorMixin,
|
|
9
|
+
SimulationDemagBemDirichletMixin,
|
|
10
|
+
):
|
|
11
|
+
"""Provide Lindholm BEM construction and Dirichlet extension."""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ....demag.bem_operator import (
|
|
9
|
+
BemOperatorStats,
|
|
10
|
+
HierarchicalLindholmBemOperator,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def bem_operator_stats(
|
|
17
|
+
*,
|
|
18
|
+
requested_backend: str,
|
|
19
|
+
effective_backend: str,
|
|
20
|
+
fallback_reason: str | None,
|
|
21
|
+
operator: Any,
|
|
22
|
+
boundary_node_count: int,
|
|
23
|
+
boundary_faces: int,
|
|
24
|
+
setup_seconds: float,
|
|
25
|
+
) -> BemOperatorStats:
|
|
26
|
+
dense_equivalent = boundary_node_count**2 * np.dtype(np.float64).itemsize
|
|
27
|
+
if isinstance(operator, HierarchicalLindholmBemOperator):
|
|
28
|
+
rust_operator = operator.rust_operator
|
|
29
|
+
dense_blocks = int(rust_operator.dense_blocks)
|
|
30
|
+
low_rank_blocks = int(rust_operator.low_rank_blocks)
|
|
31
|
+
maximum_rank = int(rust_operator.maximum_rank)
|
|
32
|
+
mean_rank = float(rust_operator.mean_rank)
|
|
33
|
+
sampled_error = float(rust_operator.sampled_relative_error)
|
|
34
|
+
storage_bytes = operator.storage_bytes
|
|
35
|
+
else:
|
|
36
|
+
dense_blocks = 1 if isinstance(operator, np.ndarray) and operator.size else 0
|
|
37
|
+
low_rank_blocks = 0
|
|
38
|
+
maximum_rank = 0
|
|
39
|
+
mean_rank = 0.0
|
|
40
|
+
sampled_error = 0.0
|
|
41
|
+
storage_bytes = (
|
|
42
|
+
int(operator.nbytes) if isinstance(operator, np.ndarray) else operator.storage_bytes
|
|
43
|
+
)
|
|
44
|
+
return BemOperatorStats(
|
|
45
|
+
requested_backend=requested_backend,
|
|
46
|
+
effective_backend=effective_backend,
|
|
47
|
+
fallback_reason=fallback_reason,
|
|
48
|
+
boundary_nodes=boundary_node_count,
|
|
49
|
+
boundary_faces=boundary_faces,
|
|
50
|
+
setup_seconds=setup_seconds,
|
|
51
|
+
storage_bytes=storage_bytes,
|
|
52
|
+
dense_equivalent_bytes=dense_equivalent,
|
|
53
|
+
dense_blocks=dense_blocks,
|
|
54
|
+
low_rank_blocks=low_rank_blocks,
|
|
55
|
+
maximum_rank=maximum_rank,
|
|
56
|
+
mean_rank=mean_rank,
|
|
57
|
+
sampled_relative_error=sampled_error,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def log_bem_operator_selection(stats: BemOperatorStats, selected_backend: str) -> None:
|
|
62
|
+
fallback = (
|
|
63
|
+
f" after {selected_backend} fallback: {stats.fallback_reason}"
|
|
64
|
+
if stats.fallback_reason
|
|
65
|
+
else ""
|
|
66
|
+
)
|
|
67
|
+
logger.info(
|
|
68
|
+
"Lindholm BEM selected %s storage%s (%.3f MiB, %.3f dense ratio)",
|
|
69
|
+
stats.effective_backend,
|
|
70
|
+
fallback,
|
|
71
|
+
stats.storage_bytes / (1024 * 1024),
|
|
72
|
+
stats.compression_ratio,
|
|
73
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ....backends import _selected_demag_linear_solver_backend
|
|
8
|
+
from ....demag import _solve_linear_system
|
|
9
|
+
from ....demag.linear import _factorize_scipy, _is_sparse_matrix, _solve_sparse_linear_system
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SimulationDemagBemDirichletMixin:
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
_demag_dirichlet_factorization_cache: Any | None
|
|
15
|
+
|
|
16
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
17
|
+
|
|
18
|
+
def _dirichlet_extension_from_boundary(
|
|
19
|
+
self,
|
|
20
|
+
stiffness: np.ndarray,
|
|
21
|
+
boundary_nodes: np.ndarray,
|
|
22
|
+
boundary_values: np.ndarray,
|
|
23
|
+
) -> np.ndarray:
|
|
24
|
+
with self._record_active_subfield_array_timing_block(
|
|
25
|
+
"demag_auxiliary:dirichlet_extension:index",
|
|
26
|
+
):
|
|
27
|
+
phi = np.zeros(stiffness.shape[0], dtype=float)
|
|
28
|
+
phi[boundary_nodes] = boundary_values
|
|
29
|
+
boundary_set = set(int(index) for index in boundary_nodes)
|
|
30
|
+
interior_nodes = np.asarray(
|
|
31
|
+
[index for index in range(stiffness.shape[0]) if index not in boundary_set],
|
|
32
|
+
dtype=int,
|
|
33
|
+
)
|
|
34
|
+
if len(interior_nodes) == 0:
|
|
35
|
+
self._record_active_subfield_array_timing(
|
|
36
|
+
"demag_auxiliary:dirichlet_extension:no_interior",
|
|
37
|
+
0.0,
|
|
38
|
+
)
|
|
39
|
+
return phi
|
|
40
|
+
|
|
41
|
+
with self._record_active_subfield_array_timing_block(
|
|
42
|
+
"demag_auxiliary:dirichlet_extension:slice",
|
|
43
|
+
):
|
|
44
|
+
if _is_sparse_matrix(stiffness):
|
|
45
|
+
interior_stiffness = stiffness[interior_nodes][:, interior_nodes]
|
|
46
|
+
boundary_stiffness = stiffness[interior_nodes][:, boundary_nodes]
|
|
47
|
+
else:
|
|
48
|
+
interior_stiffness = stiffness[np.ix_(interior_nodes, interior_nodes)]
|
|
49
|
+
boundary_stiffness = stiffness[np.ix_(interior_nodes, boundary_nodes)]
|
|
50
|
+
with self._record_active_subfield_array_timing_block(
|
|
51
|
+
"demag_auxiliary:dirichlet_extension:rhs",
|
|
52
|
+
):
|
|
53
|
+
interior_rhs = -boundary_stiffness @ boundary_values
|
|
54
|
+
if _is_sparse_matrix(interior_stiffness):
|
|
55
|
+
phi[interior_nodes] = _solve_sparse_linear_system(
|
|
56
|
+
interior_stiffness,
|
|
57
|
+
interior_rhs,
|
|
58
|
+
record_timing=self._record_active_subfield_array_timing,
|
|
59
|
+
timing_prefix="demag_auxiliary:dirichlet_extension",
|
|
60
|
+
)
|
|
61
|
+
elif _selected_demag_linear_solver_backend(stiffness.shape[0]) == "scipy":
|
|
62
|
+
cached = self._demag_dirichlet_factorization_cache
|
|
63
|
+
if cached is None:
|
|
64
|
+
factorization = _factorize_scipy(interior_stiffness)
|
|
65
|
+
self._demag_dirichlet_factorization_cache = (
|
|
66
|
+
interior_nodes,
|
|
67
|
+
boundary_nodes,
|
|
68
|
+
factorization,
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
cached_interior, cached_boundary, factorization = cached
|
|
72
|
+
if not (
|
|
73
|
+
np.array_equal(cached_interior, interior_nodes)
|
|
74
|
+
and np.array_equal(cached_boundary, boundary_nodes)
|
|
75
|
+
):
|
|
76
|
+
raise RuntimeError("Cached demag boundary partition does not match the mesh.")
|
|
77
|
+
phi[interior_nodes] = factorization.solve(interior_rhs)
|
|
78
|
+
else:
|
|
79
|
+
phi[interior_nodes] = _solve_linear_system(
|
|
80
|
+
interior_stiffness,
|
|
81
|
+
interior_rhs,
|
|
82
|
+
record_timing=self._record_active_subfield_array_timing,
|
|
83
|
+
timing_prefix="demag_auxiliary:dirichlet_extension",
|
|
84
|
+
)
|
|
85
|
+
return phi
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ....backends import _load_rust_accelerator, _selected_lindholm_bem_backend
|
|
9
|
+
from ....demag.bem_operator import HierarchicalLindholmBemOperator
|
|
10
|
+
from ....resources import available_memory_bytes
|
|
11
|
+
from ...support import _simulation_compatibility_binding
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_hierarchical_lindholm_operator(
|
|
17
|
+
simulation: Any,
|
|
18
|
+
points: np.ndarray,
|
|
19
|
+
simplices: np.ndarray,
|
|
20
|
+
boundary_faces: list[tuple[int, tuple[int, int, int]]],
|
|
21
|
+
) -> tuple[np.ndarray, Any, str | None]:
|
|
22
|
+
config = simulation.config
|
|
23
|
+
if _selected_lindholm_bem_backend(config) != "rust":
|
|
24
|
+
boundary_nodes, operator = simulation._build_lindholm_bem_operator(
|
|
25
|
+
points,
|
|
26
|
+
simplices,
|
|
27
|
+
boundary_faces,
|
|
28
|
+
)
|
|
29
|
+
return boundary_nodes, operator, "Rust accelerator is unavailable or disabled"
|
|
30
|
+
|
|
31
|
+
rust_accelerator = _simulation_compatibility_binding(
|
|
32
|
+
"_load_rust_accelerator",
|
|
33
|
+
_load_rust_accelerator,
|
|
34
|
+
)()
|
|
35
|
+
if not hasattr(rust_accelerator, "build_lindholm_hmatrix"):
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
"The installed nmag_accel does not provide hierarchical Lindholm BEM; "
|
|
38
|
+
"rebuild it with ./scripts/build-accelerator.sh --release."
|
|
39
|
+
)
|
|
40
|
+
boundary_nodes, local_index_by_point, face_nodes = simulation._lindholm_bem_boundary_index(
|
|
41
|
+
points,
|
|
42
|
+
boundary_faces,
|
|
43
|
+
)
|
|
44
|
+
hierarchical = config.hierarchical_bem
|
|
45
|
+
available = available_memory_bytes()
|
|
46
|
+
memory_budget = (
|
|
47
|
+
int(available * hierarchical.memory_fraction)
|
|
48
|
+
if available is not None
|
|
49
|
+
else np.iinfo(np.uintp).max
|
|
50
|
+
)
|
|
51
|
+
try:
|
|
52
|
+
rust_operator = rust_accelerator.build_lindholm_hmatrix(
|
|
53
|
+
np.ascontiguousarray(points, dtype=np.float64),
|
|
54
|
+
np.ascontiguousarray(simplices, dtype=np.int64),
|
|
55
|
+
np.ascontiguousarray(face_nodes, dtype=np.int64),
|
|
56
|
+
np.ascontiguousarray(boundary_nodes, dtype=np.int64),
|
|
57
|
+
np.ascontiguousarray(local_index_by_point, dtype=np.int64),
|
|
58
|
+
relative_tolerance=hierarchical.relative_tolerance,
|
|
59
|
+
admissibility_eta=hierarchical.admissibility_eta,
|
|
60
|
+
leaf_size=hierarchical.leaf_size,
|
|
61
|
+
max_rank=hierarchical.max_rank,
|
|
62
|
+
validation_vectors=hierarchical.validation_vectors,
|
|
63
|
+
validation_rows=hierarchical.validation_rows,
|
|
64
|
+
memory_budget_bytes=memory_budget,
|
|
65
|
+
)
|
|
66
|
+
except (MemoryError, ValueError) as exc:
|
|
67
|
+
logger.info("Hierarchical Lindholm BEM rejected; using exact matrix-free BEM: %s", exc)
|
|
68
|
+
boundary_nodes, operator = simulation._build_lindholm_bem_operator(
|
|
69
|
+
points,
|
|
70
|
+
simplices,
|
|
71
|
+
boundary_faces,
|
|
72
|
+
)
|
|
73
|
+
return boundary_nodes, operator, str(exc)
|
|
74
|
+
return boundary_nodes, HierarchicalLindholmBemOperator(rust_operator), None
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ....backends import (
|
|
9
|
+
_load_rust_accelerator,
|
|
10
|
+
_selected_demag_bem_storage_backend,
|
|
11
|
+
_selected_lindholm_bem_backend,
|
|
12
|
+
)
|
|
13
|
+
from ....demag import (
|
|
14
|
+
_boundary_node_solid_angles,
|
|
15
|
+
_build_lindholm_bem_matrix_fast,
|
|
16
|
+
_lindholm_triangle_contributions,
|
|
17
|
+
_precompute_lindholm_face_geometry,
|
|
18
|
+
)
|
|
19
|
+
from ....demag.bem_operator import MatrixFreeLindholmBemOperator
|
|
20
|
+
from ...support import _simulation_compatibility_binding
|
|
21
|
+
from .diagnostics import bem_operator_stats, log_bem_operator_selection
|
|
22
|
+
from .hierarchical import build_hierarchical_lindholm_operator
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SimulationDemagBemOperatorMixin:
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
_demag_bem_cache: Any | None
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
30
|
+
|
|
31
|
+
def _lindholm_bem_for_demag_mesh(
|
|
32
|
+
self,
|
|
33
|
+
points: np.ndarray,
|
|
34
|
+
simplices: np.ndarray,
|
|
35
|
+
boundary_faces: list[tuple[int, tuple[int, int, int]]],
|
|
36
|
+
) -> tuple[np.ndarray, Any]:
|
|
37
|
+
cache_started = time.perf_counter()
|
|
38
|
+
token = self._mesh_geometry_token()
|
|
39
|
+
self._ensure_demag_geometry_cache_token(token)
|
|
40
|
+
boundary_node_count = len({node for _, face in boundary_faces for node in face})
|
|
41
|
+
config = getattr(self, "config", None)
|
|
42
|
+
storage_backend = _selected_demag_bem_storage_backend(boundary_node_count, config)
|
|
43
|
+
if self._demag_bem_cache is not None:
|
|
44
|
+
cached_backend, boundary_nodes, operator, stats = self._demag_bem_cache
|
|
45
|
+
if cached_backend == storage_backend:
|
|
46
|
+
cache_kind = "matrix" if storage_backend == "dense" else "operator"
|
|
47
|
+
self._last_bem_operator_stats = stats
|
|
48
|
+
self._record_active_subfield_array_timing(
|
|
49
|
+
f"demag_auxiliary:bem_{cache_kind}_cache_hit",
|
|
50
|
+
time.perf_counter() - cache_started,
|
|
51
|
+
)
|
|
52
|
+
return boundary_nodes, operator
|
|
53
|
+
|
|
54
|
+
fallback_reason: str | None = None
|
|
55
|
+
effective_backend = storage_backend
|
|
56
|
+
if storage_backend == "hierarchical":
|
|
57
|
+
boundary_nodes, bem, fallback_reason = build_hierarchical_lindholm_operator(
|
|
58
|
+
self,
|
|
59
|
+
points,
|
|
60
|
+
simplices,
|
|
61
|
+
boundary_faces,
|
|
62
|
+
)
|
|
63
|
+
if fallback_reason is not None:
|
|
64
|
+
effective_backend = "matrix-free"
|
|
65
|
+
elif storage_backend == "matrix-free":
|
|
66
|
+
boundary_nodes, bem = self._build_lindholm_bem_operator(
|
|
67
|
+
points,
|
|
68
|
+
simplices,
|
|
69
|
+
boundary_faces,
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
boundary_nodes, bem = self._build_lindholm_bem_matrix(
|
|
73
|
+
points,
|
|
74
|
+
simplices,
|
|
75
|
+
boundary_faces,
|
|
76
|
+
)
|
|
77
|
+
elapsed = time.perf_counter() - cache_started
|
|
78
|
+
requested_backend = getattr(config, "demag_bem_storage", storage_backend)
|
|
79
|
+
stats = bem_operator_stats(
|
|
80
|
+
requested_backend=requested_backend,
|
|
81
|
+
effective_backend=effective_backend,
|
|
82
|
+
fallback_reason=fallback_reason,
|
|
83
|
+
operator=bem,
|
|
84
|
+
boundary_node_count=len(boundary_nodes),
|
|
85
|
+
boundary_faces=len(boundary_faces),
|
|
86
|
+
setup_seconds=elapsed,
|
|
87
|
+
)
|
|
88
|
+
log_bem_operator_selection(stats, storage_backend)
|
|
89
|
+
self._last_bem_operator_stats = stats
|
|
90
|
+
self._demag_boundary_faces_cache = boundary_faces
|
|
91
|
+
self._demag_bem_cache = (storage_backend, boundary_nodes, bem, stats)
|
|
92
|
+
return boundary_nodes, cast(Any, bem)
|
|
93
|
+
|
|
94
|
+
def _build_lindholm_bem_operator(
|
|
95
|
+
self,
|
|
96
|
+
points: np.ndarray,
|
|
97
|
+
simplices: np.ndarray,
|
|
98
|
+
boundary_faces: list[tuple[int, tuple[int, int, int]]],
|
|
99
|
+
) -> tuple[np.ndarray, MatrixFreeLindholmBemOperator]:
|
|
100
|
+
boundary_nodes, local_index_by_point, face_nodes = self._lindholm_bem_boundary_index(
|
|
101
|
+
points,
|
|
102
|
+
boundary_faces,
|
|
103
|
+
)
|
|
104
|
+
(
|
|
105
|
+
face_points,
|
|
106
|
+
zeta_vectors,
|
|
107
|
+
eta_vectors,
|
|
108
|
+
edge_lengths,
|
|
109
|
+
corner_cosines,
|
|
110
|
+
denominator_factors,
|
|
111
|
+
) = _precompute_lindholm_face_geometry(points, face_nodes)
|
|
112
|
+
rust_accelerator = None
|
|
113
|
+
if _selected_lindholm_bem_backend(getattr(self, "config", None)) == "rust":
|
|
114
|
+
rust_accelerator = _simulation_compatibility_binding(
|
|
115
|
+
"_load_rust_accelerator",
|
|
116
|
+
_load_rust_accelerator,
|
|
117
|
+
)()
|
|
118
|
+
if not hasattr(rust_accelerator, "apply_lindholm_bem_matrix_free"):
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
"The installed nmag_accel does not provide matrix-free Lindholm BEM; "
|
|
121
|
+
"rebuild it with ./scripts/build-accelerator.sh --release."
|
|
122
|
+
)
|
|
123
|
+
return boundary_nodes, MatrixFreeLindholmBemOperator(
|
|
124
|
+
points=np.ascontiguousarray(points, dtype=np.float64),
|
|
125
|
+
simplices=np.ascontiguousarray(simplices, dtype=np.int64),
|
|
126
|
+
boundary_faces=np.ascontiguousarray(face_nodes, dtype=np.int64),
|
|
127
|
+
face_points=np.ascontiguousarray(face_points, dtype=np.float64),
|
|
128
|
+
zeta_vectors=np.ascontiguousarray(zeta_vectors, dtype=np.float64),
|
|
129
|
+
eta_vectors=np.ascontiguousarray(eta_vectors, dtype=np.float64),
|
|
130
|
+
edge_lengths=np.ascontiguousarray(edge_lengths, dtype=np.float64),
|
|
131
|
+
corner_cosines=np.ascontiguousarray(corner_cosines, dtype=np.float64),
|
|
132
|
+
denominator_factors=np.ascontiguousarray(denominator_factors, dtype=np.float64),
|
|
133
|
+
boundary_nodes=np.ascontiguousarray(boundary_nodes, dtype=np.int64),
|
|
134
|
+
local_index_by_point=np.ascontiguousarray(local_index_by_point, dtype=np.int64),
|
|
135
|
+
solid_angles=np.ascontiguousarray(
|
|
136
|
+
_boundary_node_solid_angles(points, simplices),
|
|
137
|
+
dtype=np.float64,
|
|
138
|
+
),
|
|
139
|
+
rust_accelerator=rust_accelerator,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _build_lindholm_bem_matrix(
|
|
143
|
+
self,
|
|
144
|
+
points: np.ndarray,
|
|
145
|
+
simplices: np.ndarray,
|
|
146
|
+
boundary_faces: list[tuple[int, tuple[int, int, int]]],
|
|
147
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
148
|
+
backend = _selected_lindholm_bem_backend(getattr(self, "config", None))
|
|
149
|
+
if backend == "python":
|
|
150
|
+
return self._build_lindholm_bem_matrix_python(
|
|
151
|
+
points,
|
|
152
|
+
simplices,
|
|
153
|
+
boundary_faces,
|
|
154
|
+
)
|
|
155
|
+
if backend == "rust":
|
|
156
|
+
return self._build_lindholm_bem_matrix_rust(
|
|
157
|
+
points,
|
|
158
|
+
simplices,
|
|
159
|
+
boundary_faces,
|
|
160
|
+
)
|
|
161
|
+
return self._build_lindholm_bem_matrix_numba(
|
|
162
|
+
points,
|
|
163
|
+
simplices,
|
|
164
|
+
boundary_faces,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _lindholm_bem_boundary_index(
|
|
168
|
+
self,
|
|
169
|
+
points: np.ndarray,
|
|
170
|
+
boundary_faces: list[tuple[int, tuple[int, int, int]]],
|
|
171
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
172
|
+
boundary_nodes = np.asarray(
|
|
173
|
+
sorted({point_index for _, face in boundary_faces for point_index in face}),
|
|
174
|
+
dtype=np.int64,
|
|
175
|
+
)
|
|
176
|
+
local_index_by_point = np.full(len(points), -1, dtype=np.int64)
|
|
177
|
+
local_index_by_point[boundary_nodes] = np.arange(len(boundary_nodes), dtype=np.int64)
|
|
178
|
+
face_nodes = np.asarray([face for _, face in boundary_faces], dtype=np.int64)
|
|
179
|
+
return boundary_nodes, local_index_by_point, face_nodes
|
|
180
|
+
|
|
181
|
+
def _build_lindholm_bem_matrix_python(
|
|
182
|
+
self,
|
|
183
|
+
points: np.ndarray,
|
|
184
|
+
simplices: np.ndarray,
|
|
185
|
+
boundary_faces: list[tuple[int, tuple[int, int, int]]],
|
|
186
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
187
|
+
with self._record_active_subfield_array_timing_block(
|
|
188
|
+
"demag_auxiliary:lindholm_bem:index",
|
|
189
|
+
):
|
|
190
|
+
boundary_nodes, local_index_by_point, _face_nodes = self._lindholm_bem_boundary_index(
|
|
191
|
+
points,
|
|
192
|
+
boundary_faces,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
with self._record_active_subfield_array_timing_block(
|
|
196
|
+
"demag_auxiliary:lindholm_bem:python",
|
|
197
|
+
):
|
|
198
|
+
bem = np.zeros((len(boundary_nodes), len(boundary_nodes)), dtype=float)
|
|
199
|
+
solid_angles = _boundary_node_solid_angles(points, simplices)
|
|
200
|
+
|
|
201
|
+
for row, observer_index in enumerate(boundary_nodes):
|
|
202
|
+
observer = points[int(observer_index)]
|
|
203
|
+
for _, face in boundary_faces:
|
|
204
|
+
p0, p1, p2 = points[list(face)]
|
|
205
|
+
normal = np.cross(p1 - p0, p2 - p0)
|
|
206
|
+
normal_length = float(np.linalg.norm(normal))
|
|
207
|
+
if normal_length == 0.0:
|
|
208
|
+
continue
|
|
209
|
+
contributions = _lindholm_triangle_contributions(
|
|
210
|
+
observer,
|
|
211
|
+
p0,
|
|
212
|
+
p1,
|
|
213
|
+
p2,
|
|
214
|
+
normal / normal_length,
|
|
215
|
+
)
|
|
216
|
+
for point_index, contribution in zip(face, contributions): # noqa: B905
|
|
217
|
+
bem[row, int(local_index_by_point[int(point_index)])] += contribution
|
|
218
|
+
bem[row, row] += solid_angles[int(observer_index)] / (4.0 * np.pi) - 1.0
|
|
219
|
+
return boundary_nodes, bem
|
|
220
|
+
|
|
221
|
+
def _build_lindholm_bem_matrix_numba(
|
|
222
|
+
self,
|
|
223
|
+
points: np.ndarray,
|
|
224
|
+
simplices: np.ndarray,
|
|
225
|
+
boundary_faces: list[tuple[int, tuple[int, int, int]]],
|
|
226
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
227
|
+
with self._record_active_subfield_array_timing_block(
|
|
228
|
+
"demag_auxiliary:lindholm_bem:index",
|
|
229
|
+
):
|
|
230
|
+
boundary_nodes, local_index_by_point, face_nodes = self._lindholm_bem_boundary_index(
|
|
231
|
+
points,
|
|
232
|
+
boundary_faces,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
with self._record_active_subfield_array_timing_block(
|
|
236
|
+
"demag_auxiliary:lindholm_bem:numba",
|
|
237
|
+
):
|
|
238
|
+
(
|
|
239
|
+
face_points,
|
|
240
|
+
zeta_vectors,
|
|
241
|
+
eta_vectors,
|
|
242
|
+
edge_lengths,
|
|
243
|
+
corner_cosines,
|
|
244
|
+
denominator_factors,
|
|
245
|
+
) = _precompute_lindholm_face_geometry(np.asarray(points, dtype=np.float64), face_nodes)
|
|
246
|
+
bem = _build_lindholm_bem_matrix_fast(
|
|
247
|
+
np.asarray(points, dtype=np.float64),
|
|
248
|
+
np.asarray(simplices, dtype=np.int64),
|
|
249
|
+
face_nodes,
|
|
250
|
+
face_points,
|
|
251
|
+
zeta_vectors,
|
|
252
|
+
eta_vectors,
|
|
253
|
+
edge_lengths,
|
|
254
|
+
corner_cosines,
|
|
255
|
+
denominator_factors,
|
|
256
|
+
np.asarray(boundary_nodes, dtype=np.int64),
|
|
257
|
+
local_index_by_point,
|
|
258
|
+
)
|
|
259
|
+
return boundary_nodes, bem
|
|
260
|
+
|
|
261
|
+
def _build_lindholm_bem_matrix_rust(
|
|
262
|
+
self,
|
|
263
|
+
points: np.ndarray,
|
|
264
|
+
simplices: np.ndarray,
|
|
265
|
+
boundary_faces: list[tuple[int, tuple[int, int, int]]],
|
|
266
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
267
|
+
with self._record_active_subfield_array_timing_block(
|
|
268
|
+
"demag_auxiliary:lindholm_bem:index",
|
|
269
|
+
):
|
|
270
|
+
boundary_nodes, local_index_by_point, face_nodes = self._lindholm_bem_boundary_index(
|
|
271
|
+
points,
|
|
272
|
+
boundary_faces,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
with self._record_active_subfield_array_timing_block(
|
|
276
|
+
"demag_auxiliary:lindholm_bem:rust",
|
|
277
|
+
):
|
|
278
|
+
rust_accel = _simulation_compatibility_binding(
|
|
279
|
+
"_load_rust_accelerator",
|
|
280
|
+
_load_rust_accelerator,
|
|
281
|
+
)()
|
|
282
|
+
bem = np.asarray(
|
|
283
|
+
rust_accel.build_lindholm_bem_matrix(
|
|
284
|
+
np.asarray(points, dtype=np.float64),
|
|
285
|
+
np.asarray(simplices, dtype=np.int64),
|
|
286
|
+
face_nodes,
|
|
287
|
+
np.asarray(boundary_nodes, dtype=np.int64),
|
|
288
|
+
local_index_by_point,
|
|
289
|
+
),
|
|
290
|
+
dtype=float,
|
|
291
|
+
)
|
|
292
|
+
return boundary_nodes, bem
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""FEM demagnetization compatibility façade composed from focused mixins."""
|
|
2
|
+
|
|
3
|
+
from .assembly import SimulationDemagFemAssemblyMixin
|
|
4
|
+
from .geometry import SimulationDemagFemGeometryMixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SimulationDemagFemMixin(
|
|
8
|
+
SimulationDemagFemAssemblyMixin,
|
|
9
|
+
SimulationDemagFemGeometryMixin,
|
|
10
|
+
):
|
|
11
|
+
"""Provide FEM assembly, geometry, and boundary-face operations."""
|