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,230 @@
|
|
|
1
|
+
"""Topology refresh and point insertion/deletion for relaxation meshing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ...meshing_parameters import PointFate, default_handle_point_density_fun
|
|
11
|
+
from .._constants import BOUNDARY_FUZZ, STATE_BOUNDARY, STATE_MOBILE
|
|
12
|
+
from .._types import FloatArray
|
|
13
|
+
from ..forces import ForceSummary
|
|
14
|
+
from ..seeding import _classify_dynamic_states, _dedupe_points
|
|
15
|
+
from ..topology.recovery import mirror_surface_recovery_points
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RelaxationEngineTopologyMixin:
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
22
|
+
|
|
23
|
+
def _mark_topology_stale(self) -> None:
|
|
24
|
+
"""Request a fresh Delaunay triangulation before the next force step."""
|
|
25
|
+
|
|
26
|
+
self.current_simplices = None
|
|
27
|
+
self.max_rel_movement_since_last_triangulation = 0.0
|
|
28
|
+
self.points_at_last_triangulation = np.array(self.points, copy=True)
|
|
29
|
+
|
|
30
|
+
def _refresh_boundary_states(self) -> None:
|
|
31
|
+
"""Refresh dynamic points between mobile and boundary states."""
|
|
32
|
+
|
|
33
|
+
dynamic_mask = np.isin(self.states, [STATE_MOBILE, STATE_BOUNDARY])
|
|
34
|
+
if not np.any(dynamic_mask):
|
|
35
|
+
return
|
|
36
|
+
refreshed = _classify_dynamic_states(self.geometry, self.points[dynamic_mask], self.a0)
|
|
37
|
+
self.states[dynamic_mask] = refreshed
|
|
38
|
+
|
|
39
|
+
def _effective_time_step(self, force_summary: ForceSummary) -> float:
|
|
40
|
+
"""Compute the controller-like time step for the current force field."""
|
|
41
|
+
|
|
42
|
+
freedom = float(self.params.get("controller_movement_max_freedom", 3.0))
|
|
43
|
+
movement_weight = self._initial_relaxation_weight(freedom, 1.0)
|
|
44
|
+
capped_max_time_step = movement_weight * self.max_time_step
|
|
45
|
+
if force_summary.max_effective_force <= BOUNDARY_FUZZ:
|
|
46
|
+
return capped_max_time_step
|
|
47
|
+
return min(
|
|
48
|
+
capped_max_time_step,
|
|
49
|
+
(movement_weight * self.time_step_scale) / force_summary.max_effective_force,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _initial_relaxation_weight(self, init_val: float, final_val: float) -> float:
|
|
53
|
+
"""Evaluate the legacy initial-relaxation ramp for this step."""
|
|
54
|
+
|
|
55
|
+
settling_steps = int(self.params.get("controller_initial_settling_steps", 100))
|
|
56
|
+
weight_fun = self.params.get("initial_relaxation_weight_fun")
|
|
57
|
+
if callable(weight_fun):
|
|
58
|
+
result: Any = weight_fun(self.step, settling_steps, init_val, final_val)
|
|
59
|
+
return float(result)
|
|
60
|
+
fraction = min(1.0, float(self.step) / max(float(settling_steps), 1.0))
|
|
61
|
+
return init_val + (final_val - init_val) * fraction
|
|
62
|
+
|
|
63
|
+
def _density_thresholds(self) -> tuple[float, float]:
|
|
64
|
+
"""Return add/delete thresholds with the legacy initial relaxation ramp."""
|
|
65
|
+
|
|
66
|
+
freedom = float(self.params.get("controller_movement_max_freedom", 3.0))
|
|
67
|
+
thresh_add = self._initial_relaxation_weight(-0.1 * freedom, 0.0) + float(
|
|
68
|
+
self.params.get("controller_thresh_add", 1.0)
|
|
69
|
+
)
|
|
70
|
+
thresh_del = self._initial_relaxation_weight(0.1 * freedom, 0.0) + float(
|
|
71
|
+
self.params.get("controller_thresh_del", 2.0)
|
|
72
|
+
)
|
|
73
|
+
return thresh_add, thresh_del
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _is_positive_square_number(value: int) -> bool:
|
|
77
|
+
"""Return whether ``value`` is a positive perfect square."""
|
|
78
|
+
|
|
79
|
+
if value <= 0:
|
|
80
|
+
return False
|
|
81
|
+
root = int(math.sqrt(value) + 0.5)
|
|
82
|
+
return root * root == value
|
|
83
|
+
|
|
84
|
+
def _should_attempt_point_change(self) -> bool:
|
|
85
|
+
"""Return whether the legacy controller schedules point fate changes."""
|
|
86
|
+
|
|
87
|
+
return self.step < self.max_steps and self._is_positive_square_number(self.step - 10)
|
|
88
|
+
|
|
89
|
+
def _step_limit_reached(self) -> bool:
|
|
90
|
+
"""Return whether the legacy max-step and post-change settling rules are met."""
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
self.step >= self.max_steps
|
|
94
|
+
and self.step >= self.last_addition_deletion_step + self.post_change_settling_steps
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _topology_threshold(self) -> float:
|
|
98
|
+
"""Return the relaxed legacy threshold for topology refresh."""
|
|
99
|
+
|
|
100
|
+
freedom = float(self.params.get("controller_movement_max_freedom", 3.0))
|
|
101
|
+
threshold = float(self.params.get("controller_topology_threshold", 0.2))
|
|
102
|
+
return self._initial_relaxation_weight(freedom, 1.0) * threshold
|
|
103
|
+
|
|
104
|
+
def _record_triangulation(self, force_summary: ForceSummary) -> None:
|
|
105
|
+
"""Cache the current topology after a force calculation builds it."""
|
|
106
|
+
|
|
107
|
+
if self.current_simplices is None:
|
|
108
|
+
self.current_simplices = np.asarray(force_summary.simplices, dtype=int)
|
|
109
|
+
self.points_at_last_triangulation = np.array(self.points, copy=True)
|
|
110
|
+
self.max_rel_movement_since_last_triangulation = 0.0
|
|
111
|
+
|
|
112
|
+
def _update_topology_movement(self) -> None:
|
|
113
|
+
"""Track movement since the last triangulation in OCaml controller units."""
|
|
114
|
+
|
|
115
|
+
if len(self.points) != len(self.points_at_last_triangulation):
|
|
116
|
+
self._mark_topology_stale()
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if len(self.points) == 0:
|
|
120
|
+
self.max_rel_movement_since_last_triangulation = 0.0
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
displacements = np.linalg.norm(self.points - self.points_at_last_triangulation, axis=1)
|
|
124
|
+
density_scale = np.asarray(
|
|
125
|
+
[
|
|
126
|
+
self.geometry.density_at(point) ** (1.0 / max(self.geometry.dim, 1))
|
|
127
|
+
for point in self.points
|
|
128
|
+
],
|
|
129
|
+
dtype=float,
|
|
130
|
+
)
|
|
131
|
+
scaled = displacements * density_scale / max(self.a0, BOUNDARY_FUZZ)
|
|
132
|
+
self.max_rel_movement_since_last_triangulation = max(
|
|
133
|
+
self.max_rel_movement_since_last_triangulation,
|
|
134
|
+
float(np.max(scaled, initial=0.0)),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _refresh_topology_if_needed(self) -> None:
|
|
138
|
+
"""Invalidate cached topology when movement exceeds the legacy threshold."""
|
|
139
|
+
|
|
140
|
+
if self.max_rel_movement_since_last_triangulation > self._topology_threshold():
|
|
141
|
+
self._mark_topology_stale()
|
|
142
|
+
|
|
143
|
+
def _attempt_add_delete_points(self, force_summary: ForceSummary) -> None:
|
|
144
|
+
"""Apply the mesher density heuristic to add or remove mobile points."""
|
|
145
|
+
|
|
146
|
+
if len(self.points) == 0:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
additions, removals = self._evaluate_point_densities(force_summary)
|
|
150
|
+
recovery_points = mirror_surface_recovery_points(
|
|
151
|
+
self.points,
|
|
152
|
+
self.states,
|
|
153
|
+
self.geometry,
|
|
154
|
+
self.a0,
|
|
155
|
+
force_summary.simplices,
|
|
156
|
+
)
|
|
157
|
+
if len(recovery_points) > 0:
|
|
158
|
+
additions.extend(recovery_points)
|
|
159
|
+
self._apply_point_changes(additions, removals)
|
|
160
|
+
self._refresh_boundary_states()
|
|
161
|
+
|
|
162
|
+
def _evaluate_point_densities(
|
|
163
|
+
self,
|
|
164
|
+
force_summary: ForceSummary,
|
|
165
|
+
) -> tuple[list[FloatArray], list[int]]:
|
|
166
|
+
"""Decide which points to add or remove from the current cloud."""
|
|
167
|
+
|
|
168
|
+
handler = self.params.get("handle_point_density_fun", default_handle_point_density_fun)
|
|
169
|
+
thresh_add, thresh_del = self._density_thresholds()
|
|
170
|
+
additions: list[FloatArray] = []
|
|
171
|
+
removals: list[int] = []
|
|
172
|
+
|
|
173
|
+
for index, state in enumerate(self.states):
|
|
174
|
+
if state not in (STATE_MOBILE, STATE_BOUNDARY) or index in removals:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
neigh = force_summary.neighbor_map[index]
|
|
178
|
+
if not neigh:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
point = self.points[index]
|
|
182
|
+
avg_density = float(force_summary.point_density[index])
|
|
183
|
+
avg_force = float(force_summary.point_average_force[index])
|
|
184
|
+
|
|
185
|
+
fate = handler(self.rng, (avg_density, avg_force), thresh_add, thresh_del)
|
|
186
|
+
if fate == PointFate.ADD_ANOTHER:
|
|
187
|
+
additions.append(self._random_point_close_to(point))
|
|
188
|
+
elif (
|
|
189
|
+
fate == PointFate.DELETE
|
|
190
|
+
and len(self.points) - len(removals) > self.geometry.dim + 1
|
|
191
|
+
):
|
|
192
|
+
removals.append(index)
|
|
193
|
+
|
|
194
|
+
return additions, removals
|
|
195
|
+
|
|
196
|
+
def _random_point_close_to(self, point: FloatArray) -> FloatArray:
|
|
197
|
+
"""Return a Gaussian insertion candidate using the legacy rod-length scale."""
|
|
198
|
+
|
|
199
|
+
density_here = self.geometry.density_at(point)
|
|
200
|
+
effective_rod_length = self.a0 * (density_here ** (-1.0 / max(self.geometry.dim, 1)))
|
|
201
|
+
candidate = np.asarray(
|
|
202
|
+
self.rng.normal(loc=np.asarray(point, dtype=float), scale=effective_rod_length),
|
|
203
|
+
dtype=float,
|
|
204
|
+
)
|
|
205
|
+
if self.geometry.classify_points(candidate[np.newaxis, :])[0] >= 0:
|
|
206
|
+
return candidate
|
|
207
|
+
return self.geometry.project_segment_to_domain(np.asarray(point, dtype=float), candidate)
|
|
208
|
+
|
|
209
|
+
def _apply_point_changes(self, additions: list[FloatArray], removals: list[int]) -> None:
|
|
210
|
+
"""Mutate the point cloud according to the evaluated add/remove plan."""
|
|
211
|
+
|
|
212
|
+
if removals:
|
|
213
|
+
keep = np.ones(len(self.points), dtype=bool)
|
|
214
|
+
keep[np.asarray(removals, dtype=int)] = False
|
|
215
|
+
self.points = self.points[keep]
|
|
216
|
+
self.states = self.states[keep]
|
|
217
|
+
self._mark_topology_stale()
|
|
218
|
+
|
|
219
|
+
if additions:
|
|
220
|
+
additions_arr = _dedupe_points(np.asarray(additions, dtype=float))
|
|
221
|
+
if len(additions_arr) > 0:
|
|
222
|
+
self.points = np.vstack((self.points, additions_arr))
|
|
223
|
+
addition_states = _classify_dynamic_states(self.geometry, additions_arr, self.a0)
|
|
224
|
+
self.states = np.concatenate(
|
|
225
|
+
(
|
|
226
|
+
self.states,
|
|
227
|
+
addition_states,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
self._mark_topology_stale()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Compatibility facade and orchestration for relaxation meshing forces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .._types import FloatArray
|
|
10
|
+
from ..geometry import FemGeometry
|
|
11
|
+
from ..topology import _triangulate_points
|
|
12
|
+
from .neighbors import (
|
|
13
|
+
_build_neighbor_map,
|
|
14
|
+
_compute_neighbor_forces,
|
|
15
|
+
)
|
|
16
|
+
from .simplex import (
|
|
17
|
+
_classify_relevant_simplices,
|
|
18
|
+
_compute_simplex_forces,
|
|
19
|
+
_simplex_incidence_data,
|
|
20
|
+
)
|
|
21
|
+
from .summary import _finalize_force_summary
|
|
22
|
+
from .types import (
|
|
23
|
+
ForceParameters,
|
|
24
|
+
ForceSummary,
|
|
25
|
+
_extract_force_parameters,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ForceParameters",
|
|
30
|
+
"ForceSummary",
|
|
31
|
+
"_classify_relevant_simplices",
|
|
32
|
+
"_extract_force_parameters",
|
|
33
|
+
"compute_forces",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def compute_forces(
|
|
38
|
+
points: FloatArray,
|
|
39
|
+
states: np.ndarray,
|
|
40
|
+
geometry: FemGeometry,
|
|
41
|
+
a0: float,
|
|
42
|
+
params: dict[str, Any],
|
|
43
|
+
step: int,
|
|
44
|
+
simplices: np.ndarray | None = None,
|
|
45
|
+
) -> ForceSummary:
|
|
46
|
+
"""Compute neighbor, shape, volume, and irrelevant-element forces."""
|
|
47
|
+
point_count = len(points)
|
|
48
|
+
dim = geometry.dim
|
|
49
|
+
if simplices is None:
|
|
50
|
+
simplices = _triangulate_points(points, dim, states)
|
|
51
|
+
neighbor_map = _build_neighbor_map(point_count, simplices)
|
|
52
|
+
config = _extract_force_parameters(params, step)
|
|
53
|
+
point_densities = np.asarray([geometry.density_at(point) for point in points], dtype=float)
|
|
54
|
+
|
|
55
|
+
total_forces, neighbor_force_sums, neighbor_force_counts = _compute_neighbor_forces(
|
|
56
|
+
points,
|
|
57
|
+
states,
|
|
58
|
+
simplices,
|
|
59
|
+
neighbor_map,
|
|
60
|
+
point_densities,
|
|
61
|
+
dim,
|
|
62
|
+
a0,
|
|
63
|
+
config,
|
|
64
|
+
)
|
|
65
|
+
simplex_measures, incident_simplices, angle_sums = _simplex_incidence_data(
|
|
66
|
+
points, simplices, dim
|
|
67
|
+
)
|
|
68
|
+
_compute_simplex_forces(
|
|
69
|
+
total_forces,
|
|
70
|
+
points,
|
|
71
|
+
states,
|
|
72
|
+
geometry,
|
|
73
|
+
simplices,
|
|
74
|
+
point_densities,
|
|
75
|
+
simplex_measures,
|
|
76
|
+
angle_sums,
|
|
77
|
+
dim,
|
|
78
|
+
a0,
|
|
79
|
+
config,
|
|
80
|
+
)
|
|
81
|
+
return _finalize_force_summary(
|
|
82
|
+
total_forces=total_forces,
|
|
83
|
+
neighbor_map=neighbor_map,
|
|
84
|
+
simplices=simplices,
|
|
85
|
+
point_densities=point_densities,
|
|
86
|
+
neighbor_force_sums=neighbor_force_sums,
|
|
87
|
+
neighbor_force_counts=neighbor_force_counts,
|
|
88
|
+
simplex_measures=simplex_measures,
|
|
89
|
+
incident_simplices=incident_simplices,
|
|
90
|
+
angle_sums=angle_sums,
|
|
91
|
+
points=points,
|
|
92
|
+
states=states,
|
|
93
|
+
geometry=geometry,
|
|
94
|
+
a0=a0,
|
|
95
|
+
dim=dim,
|
|
96
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Numba-accelerated kernels for the relaxation meshing pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numba import njit
|
|
9
|
+
|
|
10
|
+
from ....utils.constants import MIN_DIVISION_MAGNITUDE
|
|
11
|
+
from .._constants import DENSITY_EPSILON, STATE_BOUNDARY, STATE_FIXED, STATE_MOBILE, STATE_SIMPLE
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@njit
|
|
15
|
+
def _default_relaxation_force(reduced_distance: float) -> float:
|
|
16
|
+
"""Return the legacy default mobile-mobile force law."""
|
|
17
|
+
|
|
18
|
+
if reduced_distance > 1.0:
|
|
19
|
+
return 0.0
|
|
20
|
+
return 1.0 - reduced_distance
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@njit
|
|
24
|
+
def _default_boundary_force(reduced_distance: float) -> float:
|
|
25
|
+
"""Return the legacy default boundary interaction force law."""
|
|
26
|
+
|
|
27
|
+
if reduced_distance > 1.0:
|
|
28
|
+
return 0.0
|
|
29
|
+
if reduced_distance < MIN_DIVISION_MAGNITUDE:
|
|
30
|
+
return 1.0e12
|
|
31
|
+
return 1.0 / reduced_distance - 1.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@njit
|
|
35
|
+
def accumulate_neighbor_forces_default(
|
|
36
|
+
points: np.ndarray,
|
|
37
|
+
states: np.ndarray,
|
|
38
|
+
point_densities: np.ndarray,
|
|
39
|
+
neighbor_pairs: np.ndarray,
|
|
40
|
+
a0: float,
|
|
41
|
+
neigh_force_scale: float,
|
|
42
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
43
|
+
"""Accumulate default neighbor forces and controller statistics."""
|
|
44
|
+
|
|
45
|
+
point_count, dim = points.shape
|
|
46
|
+
total_forces = np.zeros((point_count, dim), dtype=np.float64)
|
|
47
|
+
neighbor_force_sums = np.zeros(point_count, dtype=np.float64)
|
|
48
|
+
neighbor_force_counts = np.zeros(point_count, dtype=np.int64)
|
|
49
|
+
inv_dim = 1.0 / float(max(dim, 1))
|
|
50
|
+
a0_safe = max(a0, DENSITY_EPSILON)
|
|
51
|
+
|
|
52
|
+
for pair_index in range(len(neighbor_pairs)):
|
|
53
|
+
left = int(neighbor_pairs[pair_index, 0])
|
|
54
|
+
right = int(neighbor_pairs[pair_index, 1])
|
|
55
|
+
state_left = int(states[left])
|
|
56
|
+
state_right = int(states[right])
|
|
57
|
+
left_dynamic = state_left == STATE_MOBILE or state_left == STATE_BOUNDARY
|
|
58
|
+
right_dynamic = state_right == STATE_MOBILE or state_right == STATE_BOUNDARY
|
|
59
|
+
if not left_dynamic and not right_dynamic:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
true_distance_sq = 0.0
|
|
63
|
+
for axis in range(dim):
|
|
64
|
+
delta = points[right, axis] - points[left, axis]
|
|
65
|
+
true_distance_sq += delta * delta
|
|
66
|
+
true_distance = math.sqrt(true_distance_sq)
|
|
67
|
+
if true_distance <= DENSITY_EPSILON:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
avg_density = 0.5 * (point_densities[left] + point_densities[right])
|
|
71
|
+
inv_length_scale = math.pow(avg_density, inv_dim) / a0_safe
|
|
72
|
+
reduced_distance = true_distance * inv_length_scale
|
|
73
|
+
boundary_interaction = (
|
|
74
|
+
state_left == STATE_FIXED
|
|
75
|
+
or state_left == STATE_BOUNDARY
|
|
76
|
+
or state_left == STATE_SIMPLE
|
|
77
|
+
or state_right == STATE_FIXED
|
|
78
|
+
or state_right == STATE_BOUNDARY
|
|
79
|
+
or state_right == STATE_SIMPLE
|
|
80
|
+
)
|
|
81
|
+
scalar_force = (
|
|
82
|
+
_default_boundary_force(reduced_distance)
|
|
83
|
+
if boundary_interaction
|
|
84
|
+
else _default_relaxation_force(reduced_distance)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
scaled_force = neigh_force_scale * abs(scalar_force)
|
|
88
|
+
neighbor_force_sums[left] += scaled_force
|
|
89
|
+
neighbor_force_sums[right] += scaled_force
|
|
90
|
+
neighbor_force_counts[left] += 1
|
|
91
|
+
neighbor_force_counts[right] += 1
|
|
92
|
+
|
|
93
|
+
if scalar_force == 0.0:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
force_factor = neigh_force_scale * (-scalar_force)
|
|
97
|
+
for axis in range(dim):
|
|
98
|
+
contribution = force_factor * (points[right, axis] - points[left, axis])
|
|
99
|
+
total_forces[left, axis] += contribution
|
|
100
|
+
total_forces[right, axis] -= contribution
|
|
101
|
+
|
|
102
|
+
return total_forces, neighbor_force_sums, neighbor_force_counts
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Neighbor-pair force calculations for the relaxation mesher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from itertools import combinations
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ...meshing_parameters import default_boundary_node_force_fun, default_relaxation_force_fun
|
|
11
|
+
from .._constants import DENSITY_EPSILON, STATE_BOUNDARY, STATE_FIXED, STATE_MOBILE, STATE_SIMPLE
|
|
12
|
+
from .._types import FloatArray
|
|
13
|
+
from .jit import accumulate_neighbor_forces_default
|
|
14
|
+
from .types import ForceParameters
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_neighbor_pairs(simplices: np.ndarray) -> np.ndarray:
|
|
18
|
+
"""Return sorted unique point-pair edges induced by the simplices."""
|
|
19
|
+
if len(simplices) == 0:
|
|
20
|
+
return np.empty((0, 2), dtype=np.int64)
|
|
21
|
+
|
|
22
|
+
pairs: set[tuple[int, int]] = set()
|
|
23
|
+
for simplex in simplices:
|
|
24
|
+
for left, right in combinations(simplex.tolist(), 2):
|
|
25
|
+
i = int(left)
|
|
26
|
+
j = int(right)
|
|
27
|
+
pairs.add((i, j) if i <= j else (j, i))
|
|
28
|
+
return np.asarray(sorted(pairs), dtype=np.int64)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_dynamic_state(state: int) -> bool:
|
|
32
|
+
"""Return whether the state participates in relaxation movement."""
|
|
33
|
+
return state in (STATE_MOBILE, STATE_BOUNDARY)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_boundary_interaction(state_a: int, state_b: int) -> bool:
|
|
37
|
+
"""Return whether a neighbor interaction should use the boundary law."""
|
|
38
|
+
return state_a in (STATE_FIXED, STATE_BOUNDARY, STATE_SIMPLE) or state_b in (
|
|
39
|
+
STATE_FIXED,
|
|
40
|
+
STATE_BOUNDARY,
|
|
41
|
+
STATE_SIMPLE,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _neighbor_forces_python(
|
|
46
|
+
points: FloatArray,
|
|
47
|
+
states: np.ndarray,
|
|
48
|
+
point_densities: FloatArray,
|
|
49
|
+
neighbor_map: list[list[int]],
|
|
50
|
+
dim: int,
|
|
51
|
+
a0: float,
|
|
52
|
+
neigh_force_scale: float,
|
|
53
|
+
force_fun: Any,
|
|
54
|
+
boundary_force_fun: Any,
|
|
55
|
+
) -> tuple[FloatArray, FloatArray, np.ndarray]:
|
|
56
|
+
"""Fallback neighbor-force path for custom Python force callbacks."""
|
|
57
|
+
total_forces = np.zeros((len(points), dim), dtype=float)
|
|
58
|
+
neighbor_force_sums = np.zeros(len(points), dtype=float)
|
|
59
|
+
neighbor_force_counts = np.zeros(len(points), dtype=int)
|
|
60
|
+
|
|
61
|
+
for left, neighbors in enumerate(neighbor_map):
|
|
62
|
+
point_left = points[left]
|
|
63
|
+
state_left = int(states[left])
|
|
64
|
+
for right in neighbors:
|
|
65
|
+
if right <= left:
|
|
66
|
+
continue
|
|
67
|
+
contribution, scalar_force = _apply_pairwise_force(
|
|
68
|
+
point_left=point_left,
|
|
69
|
+
point_right=points[right],
|
|
70
|
+
state_left=state_left,
|
|
71
|
+
state_right=int(states[right]),
|
|
72
|
+
density_left=float(point_densities[left]),
|
|
73
|
+
density_right=float(point_densities[right]),
|
|
74
|
+
dim=dim,
|
|
75
|
+
a0=a0,
|
|
76
|
+
neigh_force_scale=neigh_force_scale,
|
|
77
|
+
force_fun=force_fun,
|
|
78
|
+
boundary_force_fun=boundary_force_fun,
|
|
79
|
+
)
|
|
80
|
+
if scalar_force is None:
|
|
81
|
+
continue
|
|
82
|
+
neighbor_force_sums[left] += abs(scalar_force)
|
|
83
|
+
neighbor_force_sums[right] += abs(scalar_force)
|
|
84
|
+
neighbor_force_counts[left] += 1
|
|
85
|
+
neighbor_force_counts[right] += 1
|
|
86
|
+
if contribution is None:
|
|
87
|
+
continue
|
|
88
|
+
total_forces[left] += contribution
|
|
89
|
+
total_forces[right] -= contribution
|
|
90
|
+
|
|
91
|
+
return total_forces, neighbor_force_sums, neighbor_force_counts
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _apply_pairwise_force(
|
|
95
|
+
*,
|
|
96
|
+
point_left: FloatArray,
|
|
97
|
+
point_right: FloatArray,
|
|
98
|
+
state_left: int,
|
|
99
|
+
state_right: int,
|
|
100
|
+
density_left: float,
|
|
101
|
+
density_right: float,
|
|
102
|
+
dim: int,
|
|
103
|
+
a0: float,
|
|
104
|
+
neigh_force_scale: float,
|
|
105
|
+
force_fun: Any,
|
|
106
|
+
boundary_force_fun: Any,
|
|
107
|
+
) -> tuple[FloatArray | None, float | None]:
|
|
108
|
+
"""Compute the pairwise neighbor contribution for one point pair."""
|
|
109
|
+
if not _is_dynamic_state(state_left) and not _is_dynamic_state(state_right):
|
|
110
|
+
return None, None
|
|
111
|
+
|
|
112
|
+
delta = point_right - point_left
|
|
113
|
+
true_distance = float(np.linalg.norm(delta))
|
|
114
|
+
if true_distance <= DENSITY_EPSILON:
|
|
115
|
+
return None, None
|
|
116
|
+
|
|
117
|
+
avg_density = 0.5 * (density_left + density_right)
|
|
118
|
+
inv_length_scale = (avg_density ** (1.0 / max(dim, 1))) / max(a0, DENSITY_EPSILON)
|
|
119
|
+
reduced_distance = true_distance * inv_length_scale
|
|
120
|
+
scalar_force = float(
|
|
121
|
+
(boundary_force_fun if _is_boundary_interaction(state_left, state_right) else force_fun)(
|
|
122
|
+
reduced_distance
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
scaled_force = neigh_force_scale * abs(scalar_force)
|
|
126
|
+
if scalar_force == 0.0:
|
|
127
|
+
return None, scaled_force
|
|
128
|
+
return neigh_force_scale * (-scalar_force) * delta, scaled_force
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _build_neighbor_map(point_count: int, simplices: np.ndarray) -> list[list[int]]:
|
|
132
|
+
"""Build undirected point adjacency from simplices."""
|
|
133
|
+
neighbors: list[set[int]] = [set() for _ in range(point_count)]
|
|
134
|
+
for simplex in simplices:
|
|
135
|
+
for left, right in combinations(simplex.tolist(), 2):
|
|
136
|
+
i = int(left)
|
|
137
|
+
j = int(right)
|
|
138
|
+
neighbors[i].add(j)
|
|
139
|
+
neighbors[j].add(i)
|
|
140
|
+
return [sorted(group) for group in neighbors]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _compute_neighbor_forces(
|
|
144
|
+
points: FloatArray,
|
|
145
|
+
states: np.ndarray,
|
|
146
|
+
simplices: np.ndarray,
|
|
147
|
+
neighbor_map: list[list[int]],
|
|
148
|
+
point_densities: FloatArray,
|
|
149
|
+
dim: int,
|
|
150
|
+
a0: float,
|
|
151
|
+
config: ForceParameters,
|
|
152
|
+
) -> tuple[FloatArray, FloatArray, np.ndarray]:
|
|
153
|
+
"""Compute neighbor-force contributions through the JIT or Python path."""
|
|
154
|
+
point_count = len(points)
|
|
155
|
+
if len(simplices) == 0:
|
|
156
|
+
return (
|
|
157
|
+
np.zeros((point_count, dim), dtype=float),
|
|
158
|
+
np.zeros(point_count, dtype=float),
|
|
159
|
+
np.zeros(point_count, dtype=int),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
config.force_fun is default_relaxation_force_fun
|
|
164
|
+
and config.boundary_force_fun is default_boundary_node_force_fun
|
|
165
|
+
):
|
|
166
|
+
neighbor_pairs = _build_neighbor_pairs(simplices)
|
|
167
|
+
return accumulate_neighbor_forces_default(
|
|
168
|
+
np.asarray(points, dtype=np.float64),
|
|
169
|
+
np.asarray(states, dtype=np.int64),
|
|
170
|
+
point_densities.astype(np.float64, copy=False),
|
|
171
|
+
neighbor_pairs,
|
|
172
|
+
float(a0),
|
|
173
|
+
config.neigh_force_scale,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return _neighbor_forces_python(
|
|
177
|
+
points,
|
|
178
|
+
states,
|
|
179
|
+
point_densities,
|
|
180
|
+
neighbor_map,
|
|
181
|
+
dim,
|
|
182
|
+
a0,
|
|
183
|
+
config.neigh_force_scale,
|
|
184
|
+
config.force_fun,
|
|
185
|
+
config.boundary_force_fun,
|
|
186
|
+
)
|