nmag-python-3 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. anisotropy/__init__.py +28 -0
  2. anisotropy/anisotropy.py +13 -0
  3. anisotropy/evaluation.py +70 -0
  4. anisotropy/model.py +200 -0
  5. anisotropy/predefined.py +202 -0
  6. anisotropy/py.typed +1 -0
  7. anisotropy/values.py +84 -0
  8. mag_material/__init__.py +3 -0
  9. mag_material/mag_material.py +231 -0
  10. mag_material/parameters.py +150 -0
  11. mag_material/py.typed +1 -0
  12. nmag/__init__.py +36 -0
  13. nmag/backends.py +493 -0
  14. nmag/checkpoint.py +327 -0
  15. nmag/config.py +174 -0
  16. nmag/demag/__init__.py +39 -0
  17. nmag/demag/bem_operator.py +149 -0
  18. nmag/demag/geometry.py +134 -0
  19. nmag/demag/lindholm.py +133 -0
  20. nmag/demag/lindholm_fast.py +463 -0
  21. nmag/demag/linear.py +489 -0
  22. nmag/dynamics/__init__.py +255 -0
  23. nmag/output.py +27 -0
  24. nmag/parallel.py +45 -0
  25. nmag/py.typed +1 -0
  26. nmag/resources.py +38 -0
  27. nmag/simulation/__init__.py +519 -0
  28. nmag/simulation/anisotropy/__init__.py +5 -0
  29. nmag/simulation/anisotropy/fields.py +56 -0
  30. nmag/simulation/anisotropy/materials.py +138 -0
  31. nmag/simulation/demag/__init__.py +1 -0
  32. nmag/simulation/demag/bem/__init__.py +11 -0
  33. nmag/simulation/demag/bem/diagnostics.py +73 -0
  34. nmag/simulation/demag/bem/dirichlet.py +85 -0
  35. nmag/simulation/demag/bem/hierarchical.py +74 -0
  36. nmag/simulation/demag/bem/operator.py +292 -0
  37. nmag/simulation/demag/fem/__init__.py +11 -0
  38. nmag/simulation/demag/fem/assembly.py +114 -0
  39. nmag/simulation/demag/fem/charges.py +38 -0
  40. nmag/simulation/demag/fem/geometry.py +261 -0
  41. nmag/simulation/demag/fields/__init__.py +11 -0
  42. nmag/simulation/demag/fields/auxiliary.py +186 -0
  43. nmag/simulation/demag/fields/probe.py +77 -0
  44. nmag/simulation/demag/fields/recovery.py +207 -0
  45. nmag/simulation/demag/solver.py +17 -0
  46. nmag/simulation/dynamics/__init__.py +92 -0
  47. nmag/simulation/dynamics/advance.py +199 -0
  48. nmag/simulation/dynamics/integrator.py +263 -0
  49. nmag/simulation/exchange/__init__.py +13 -0
  50. nmag/simulation/exchange/coefficients.py +185 -0
  51. nmag/simulation/exchange/fields.py +106 -0
  52. nmag/simulation/exchange/llg_rhs.py +218 -0
  53. nmag/simulation/fields/__init__.py +19 -0
  54. nmag/simulation/fields/arrays.py +111 -0
  55. nmag/simulation/fields/availability.py +188 -0
  56. nmag/simulation/fields/averages.py +293 -0
  57. nmag/simulation/fields/derived.py +164 -0
  58. nmag/simulation/fields/maxangle.py +163 -0
  59. nmag/simulation/fields/probes.py +106 -0
  60. nmag/simulation/implicit_dynamics.py +221 -0
  61. nmag/simulation/mesh/__init__.py +13 -0
  62. nmag/simulation/mesh/geometry.py +153 -0
  63. nmag/simulation/mesh/materials.py +299 -0
  64. nmag/simulation/mesh/probe.py +234 -0
  65. nmag/simulation/restart.py +103 -0
  66. nmag/simulation/support.py +224 -0
  67. nmag_python_3-0.0.2.dist-info/METADATA +157 -0
  68. nmag_python_3-0.0.2.dist-info/RECORD +147 -0
  69. nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
  70. nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
  71. nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
  72. nmesh/__init__.py +130 -0
  73. nmesh/backend.py +286 -0
  74. nmesh/geometry/__init__.py +52 -0
  75. nmesh/geometry/boolean_operations.py +157 -0
  76. nmesh/geometry/primitives.py +453 -0
  77. nmesh/geometry/transform.py +126 -0
  78. nmesh/io/__init__.py +50 -0
  79. nmesh/io/ascii.py +132 -0
  80. nmesh/io/legacy_nmesh_hdf5.py +318 -0
  81. nmesh/io/meshio_support.py +170 -0
  82. nmesh/mesh_generation.py +182 -0
  83. nmesh/mesh_io.py +227 -0
  84. nmesh/mesh_model.py +147 -0
  85. nmesh/mesh_utilities.py +79 -0
  86. nmesh/mesher/__init__.py +21 -0
  87. nmesh/mesher/driver.py +146 -0
  88. nmesh/mesher/meshing_defaults.py +252 -0
  89. nmesh/mesher/meshing_parameters.py +185 -0
  90. nmesh/mesher/parity.py +21 -0
  91. nmesh/mesher/parity_canonical.py +142 -0
  92. nmesh/mesher/parity_comparison.py +191 -0
  93. nmesh/mesher/parity_metrics.py +114 -0
  94. nmesh/mesher/periodic.py +97 -0
  95. nmesh/mesher/relaxation/__init__.py +14 -0
  96. nmesh/mesher/relaxation/_constants.py +20 -0
  97. nmesh/mesher/relaxation/_types.py +15 -0
  98. nmesh/mesher/relaxation/density.py +170 -0
  99. nmesh/mesher/relaxation/engine/__init__.py +18 -0
  100. nmesh/mesher/relaxation/engine/state.py +155 -0
  101. nmesh/mesher/relaxation/engine/steps.py +248 -0
  102. nmesh/mesher/relaxation/engine/topology.py +230 -0
  103. nmesh/mesher/relaxation/forces/__init__.py +96 -0
  104. nmesh/mesher/relaxation/forces/jit.py +102 -0
  105. nmesh/mesher/relaxation/forces/neighbors.py +186 -0
  106. nmesh/mesher/relaxation/forces/simplex.py +302 -0
  107. nmesh/mesher/relaxation/forces/summary.py +207 -0
  108. nmesh/mesher/relaxation/forces/types.py +92 -0
  109. nmesh/mesher/relaxation/geometry/__init__.py +6 -0
  110. nmesh/mesher/relaxation/geometry/builder.py +154 -0
  111. nmesh/mesher/relaxation/geometry/model.py +194 -0
  112. nmesh/mesher/relaxation/seeding/__init__.py +74 -0
  113. nmesh/mesher/relaxation/seeding/periodic.py +88 -0
  114. nmesh/mesher/relaxation/seeding/points.py +88 -0
  115. nmesh/mesher/relaxation/seeding/sampling.py +142 -0
  116. nmesh/mesher/relaxation/topology/__init__.py +297 -0
  117. nmesh/mesher/relaxation/topology/finalize.py +78 -0
  118. nmesh/mesher/relaxation/topology/recovery.py +310 -0
  119. nmesh/mesher/sectioned_config.py +70 -0
  120. nmesh/nmesh.py +99 -0
  121. nmesh/py.typed +1 -0
  122. nmesh/utils/__init__.py +33 -0
  123. nmesh/utils/array_list_utils.py +128 -0
  124. nmesh/utils/constants.py +22 -0
  125. nmesh/utils/timing_memory_utils.py +51 -0
  126. nmesh/utils/types.py +13 -0
  127. si/constants.py +49 -0
  128. si/physical.py +722 -0
  129. si/py.typed +1 -0
  130. simulation/__init__.py +1 -0
  131. simulation/clock.py +237 -0
  132. simulation/data_writer.py +273 -0
  133. simulation/data_writer_collection.py +267 -0
  134. simulation/hysteresis.py +74 -0
  135. simulation/hysteresis_runner.py +286 -0
  136. simulation/hysteresis_schedule.py +180 -0
  137. simulation/inference/__init__.py +3 -0
  138. simulation/inference/inference.py +95 -0
  139. simulation/py.typed +1 -0
  140. simulation/quantity.py +88 -0
  141. simulation/simulation_core.py +458 -0
  142. throttler/__init__.py +3 -0
  143. throttler/py.typed +1 -0
  144. throttler/throttler.py +55 -0
  145. when/__init__.py +3 -0
  146. when/py.typed +1 -0
  147. when/when.py +416 -0
@@ -0,0 +1,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."""