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,302 @@
|
|
|
1
|
+
"""Simplex shape, volume, and relevance forces for the relaxation mesher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .._constants import BOUNDARY_FUZZ, DENSITY_EPSILON, STATE_MOBILE
|
|
10
|
+
from .._types import FloatArray
|
|
11
|
+
from ..geometry import FemGeometry
|
|
12
|
+
from ..topology import (
|
|
13
|
+
_boundary_state_mask,
|
|
14
|
+
_classify_simplices_with_probes,
|
|
15
|
+
_simplex_measures,
|
|
16
|
+
_simplex_volume_order_ratio,
|
|
17
|
+
)
|
|
18
|
+
from .types import ForceParameters, _corner_force_threshold, _regular_simplex_volume
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _vertex_angle(points: FloatArray, simplex: np.ndarray, local_index: int, dim: int) -> float:
|
|
22
|
+
"""Return the angle or solid angle covered by a simplex at one vertex."""
|
|
23
|
+
vertex = points[int(simplex[local_index])]
|
|
24
|
+
others: list[FloatArray] = [
|
|
25
|
+
np.asarray(points[int(simplex[index])] - vertex, dtype=float)
|
|
26
|
+
for index in range(len(simplex))
|
|
27
|
+
if index != local_index
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
if dim == 1:
|
|
31
|
+
return math.pi
|
|
32
|
+
|
|
33
|
+
if dim == 2:
|
|
34
|
+
first = others[0]
|
|
35
|
+
second = others[1]
|
|
36
|
+
denom = math.sqrt(float(np.sum(first * first))) * math.sqrt(float(np.sum(second * second)))
|
|
37
|
+
if denom <= DENSITY_EPSILON:
|
|
38
|
+
return 0.0
|
|
39
|
+
cosine = min(1.0, max(-1.0, float(np.sum(first * second)) / denom))
|
|
40
|
+
return float(math.acos(cosine))
|
|
41
|
+
|
|
42
|
+
if dim == 3:
|
|
43
|
+
a, b, c = others
|
|
44
|
+
numer = abs(float(np.sum(a * np.cross(b, c))))
|
|
45
|
+
norm_a = math.sqrt(float(np.sum(a * a)))
|
|
46
|
+
norm_b = math.sqrt(float(np.sum(b * b)))
|
|
47
|
+
norm_c = math.sqrt(float(np.sum(c * c)))
|
|
48
|
+
denom = (
|
|
49
|
+
norm_a * norm_b * norm_c
|
|
50
|
+
+ float(np.sum(a * b)) * norm_c
|
|
51
|
+
+ float(np.sum(a * c)) * norm_b
|
|
52
|
+
+ float(np.sum(b * c)) * norm_a
|
|
53
|
+
)
|
|
54
|
+
if numer <= DENSITY_EPSILON and denom <= DENSITY_EPSILON:
|
|
55
|
+
return 0.0
|
|
56
|
+
return float(2.0 * math.atan2(numer, max(denom, DENSITY_EPSILON)))
|
|
57
|
+
|
|
58
|
+
return 1.0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _shape_force_matrix(vertices: FloatArray, dim: int) -> FloatArray:
|
|
62
|
+
"""Build the covariance-derived shape force matrix for one simplex."""
|
|
63
|
+
covariance = np.asarray(np.transpose(vertices) @ vertices, dtype=float)
|
|
64
|
+
covariance /= max(float(len(vertices) - 1), 1.0)
|
|
65
|
+
|
|
66
|
+
eigenvalues, eigenvectors = np.linalg.eigh(covariance)
|
|
67
|
+
eigenvectors = np.asarray(eigenvectors, dtype=float)
|
|
68
|
+
safe_eigenvalues = np.maximum(
|
|
69
|
+
np.asarray(eigenvalues, dtype=float),
|
|
70
|
+
DENSITY_EPSILON,
|
|
71
|
+
)
|
|
72
|
+
product = float(np.prod(safe_eigenvalues))
|
|
73
|
+
if product <= DENSITY_EPSILON:
|
|
74
|
+
return np.zeros((dim, dim), dtype=float)
|
|
75
|
+
|
|
76
|
+
scaled = safe_eigenvalues * ((1.0 / product) ** (1.0 / max(dim, 1)))
|
|
77
|
+
diagonal = -np.log(np.maximum(scaled, DENSITY_EPSILON))
|
|
78
|
+
transformed = np.zeros((dim, dim), dtype=float)
|
|
79
|
+
transformed[np.arange(dim), np.arange(dim)] = diagonal
|
|
80
|
+
return np.asarray(eigenvectors @ transformed @ np.transpose(eigenvectors), dtype=float)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _project_force_to_tangent(
|
|
84
|
+
geometry: FemGeometry,
|
|
85
|
+
point: FloatArray,
|
|
86
|
+
force: FloatArray,
|
|
87
|
+
) -> FloatArray:
|
|
88
|
+
"""Remove the normal component of a force at a boundary point."""
|
|
89
|
+
normal = geometry.boundary_normal(point)
|
|
90
|
+
normal_norm = float(np.linalg.norm(normal))
|
|
91
|
+
if normal_norm <= BOUNDARY_FUZZ:
|
|
92
|
+
return np.asarray(force, dtype=float)
|
|
93
|
+
unit_normal = normal / normal_norm
|
|
94
|
+
return np.asarray(force, dtype=float) - float(np.dot(force, unit_normal)) * unit_normal
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _simplex_force_contribution(
|
|
98
|
+
simplex_points: FloatArray,
|
|
99
|
+
dim: int,
|
|
100
|
+
density_here: float,
|
|
101
|
+
a0: float,
|
|
102
|
+
relaxation_weight: float,
|
|
103
|
+
shape_force_scale: float,
|
|
104
|
+
volume_force_scale: float,
|
|
105
|
+
sliver_correction: float,
|
|
106
|
+
volume: float,
|
|
107
|
+
) -> FloatArray:
|
|
108
|
+
"""Compute the shape and volume force contribution for one simplex."""
|
|
109
|
+
center = np.mean(simplex_points, axis=0)
|
|
110
|
+
vertices = simplex_points - center
|
|
111
|
+
ideal_edge_length = a0 / (density_here ** (1.0 / max(dim, 1)))
|
|
112
|
+
ideal_volume = _regular_simplex_volume(ideal_edge_length, dim)
|
|
113
|
+
return _compute_volume_forces(
|
|
114
|
+
vertices,
|
|
115
|
+
relaxation_weight,
|
|
116
|
+
volume_force_scale,
|
|
117
|
+
volume,
|
|
118
|
+
ideal_volume,
|
|
119
|
+
) + _compute_shape_forces(
|
|
120
|
+
vertices=vertices,
|
|
121
|
+
dim=dim,
|
|
122
|
+
relaxation_weight=relaxation_weight,
|
|
123
|
+
shape_force_scale=shape_force_scale,
|
|
124
|
+
sliver_correction=sliver_correction,
|
|
125
|
+
volume=volume,
|
|
126
|
+
ideal_volume=ideal_volume,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _compute_volume_forces(
|
|
131
|
+
vertices: FloatArray,
|
|
132
|
+
relaxation_weight: float,
|
|
133
|
+
volume_force_scale: float,
|
|
134
|
+
volume: float,
|
|
135
|
+
ideal_volume: float,
|
|
136
|
+
) -> FloatArray:
|
|
137
|
+
"""Return the isotropic volume-restoring forces for one simplex."""
|
|
138
|
+
forces = np.zeros_like(vertices)
|
|
139
|
+
if volume_force_scale <= 0.0 or volume <= DENSITY_EPSILON or ideal_volume <= DENSITY_EPSILON:
|
|
140
|
+
return forces
|
|
141
|
+
volume_factor = volume_force_scale * math.log(ideal_volume / volume)
|
|
142
|
+
return forces + relaxation_weight * volume_factor * vertices
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _compute_shape_forces(
|
|
146
|
+
*,
|
|
147
|
+
vertices: FloatArray,
|
|
148
|
+
dim: int,
|
|
149
|
+
relaxation_weight: float,
|
|
150
|
+
shape_force_scale: float,
|
|
151
|
+
sliver_correction: float,
|
|
152
|
+
volume: float,
|
|
153
|
+
ideal_volume: float,
|
|
154
|
+
) -> FloatArray:
|
|
155
|
+
"""Return the covariance-derived shape-correction forces for one simplex."""
|
|
156
|
+
forces = np.zeros_like(vertices)
|
|
157
|
+
if shape_force_scale <= 0.0:
|
|
158
|
+
return forces
|
|
159
|
+
|
|
160
|
+
shape_matrix = _shape_force_matrix(vertices, dim)
|
|
161
|
+
if not np.any(shape_matrix):
|
|
162
|
+
return forces
|
|
163
|
+
|
|
164
|
+
vol_correction = _shape_volume_correction(volume, ideal_volume, dim)
|
|
165
|
+
for index, vertex in enumerate(vertices):
|
|
166
|
+
norm = float(np.linalg.norm(vertex))
|
|
167
|
+
if norm <= DENSITY_EPSILON:
|
|
168
|
+
continue
|
|
169
|
+
raw_force = shape_matrix @ vertex
|
|
170
|
+
normal = vertex / norm
|
|
171
|
+
projection = float(np.dot(raw_force, normal))
|
|
172
|
+
angular = raw_force - projection * normal
|
|
173
|
+
forces[index] += relaxation_weight * vol_correction * shape_force_scale * angular
|
|
174
|
+
if projection > 0.0 and sliver_correction > 0.0:
|
|
175
|
+
longitudinal = raw_force - angular
|
|
176
|
+
forces[index] += (
|
|
177
|
+
shape_force_scale
|
|
178
|
+
* sliver_correction
|
|
179
|
+
* relaxation_weight
|
|
180
|
+
* max(vol_correction - 1.0, 0.0)
|
|
181
|
+
* longitudinal
|
|
182
|
+
)
|
|
183
|
+
return forces
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _shape_volume_correction(volume: float, ideal_volume: float, dim: int) -> float:
|
|
187
|
+
"""Return the volume-dependent multiplier applied to shape forces."""
|
|
188
|
+
if volume <= DENSITY_EPSILON or ideal_volume <= DENSITY_EPSILON:
|
|
189
|
+
return 0.0
|
|
190
|
+
offset = 0.0 if dim <= 2 else 1.0
|
|
191
|
+
return max(1.0, offset + math.log(ideal_volume / volume))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _simplex_incidence_data(
|
|
195
|
+
points: FloatArray,
|
|
196
|
+
simplices: np.ndarray,
|
|
197
|
+
dim: int,
|
|
198
|
+
) -> tuple[FloatArray, list[list[int]], FloatArray]:
|
|
199
|
+
"""Collect simplex measures plus per-point incident simplex and angle data."""
|
|
200
|
+
simplex_measures = _simplex_measures(points, simplices, dim)
|
|
201
|
+
incident_simplices: list[list[int]] = [[] for _ in range(len(points))]
|
|
202
|
+
angle_sums = np.zeros(len(points), dtype=float)
|
|
203
|
+
for simplex_index, simplex in enumerate(simplices):
|
|
204
|
+
for local_index, point_index in enumerate(simplex.tolist()):
|
|
205
|
+
point_id = int(point_index)
|
|
206
|
+
incident_simplices[point_id].append(simplex_index)
|
|
207
|
+
angle_sums[point_id] += _vertex_angle(points, simplex, local_index, dim)
|
|
208
|
+
return simplex_measures, incident_simplices, angle_sums
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _compute_simplex_forces(
|
|
212
|
+
total_forces: FloatArray,
|
|
213
|
+
points: FloatArray,
|
|
214
|
+
states: np.ndarray,
|
|
215
|
+
geometry: FemGeometry,
|
|
216
|
+
simplices: np.ndarray,
|
|
217
|
+
point_densities: FloatArray,
|
|
218
|
+
simplex_measures: FloatArray,
|
|
219
|
+
angle_sums: FloatArray,
|
|
220
|
+
dim: int,
|
|
221
|
+
a0: float,
|
|
222
|
+
config: ForceParameters,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Accumulate simplex shape, volume, and irrelevant-element forces."""
|
|
225
|
+
del point_densities
|
|
226
|
+
if len(simplices) == 0:
|
|
227
|
+
return
|
|
228
|
+
if (
|
|
229
|
+
config.shape_force_scale <= 0.0
|
|
230
|
+
and config.volume_force_scale <= 0.0
|
|
231
|
+
and config.irrel_force_scale <= 0.0
|
|
232
|
+
):
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
relevant_simplices = _classify_relevant_simplices(
|
|
236
|
+
points,
|
|
237
|
+
states,
|
|
238
|
+
geometry,
|
|
239
|
+
simplices,
|
|
240
|
+
simplex_measures,
|
|
241
|
+
dim,
|
|
242
|
+
config,
|
|
243
|
+
)
|
|
244
|
+
suppress_corner_forces = angle_sums < _corner_force_threshold(dim)
|
|
245
|
+
for simplex_index, simplex in enumerate(simplices):
|
|
246
|
+
simplex_points = points[simplex]
|
|
247
|
+
simplex_force = np.zeros_like(simplex_points)
|
|
248
|
+
volume = float(simplex_measures[simplex_index])
|
|
249
|
+
if relevant_simplices[simplex_index]:
|
|
250
|
+
density_here = geometry.density_at(np.mean(simplex_points, axis=0))
|
|
251
|
+
simplex_force = _simplex_force_contribution(
|
|
252
|
+
simplex_points=simplex_points,
|
|
253
|
+
dim=dim,
|
|
254
|
+
density_here=density_here,
|
|
255
|
+
a0=a0,
|
|
256
|
+
relaxation_weight=config.relaxation_weight,
|
|
257
|
+
shape_force_scale=config.shape_force_scale,
|
|
258
|
+
volume_force_scale=config.volume_force_scale,
|
|
259
|
+
sliver_correction=config.sliver_correction,
|
|
260
|
+
volume=volume,
|
|
261
|
+
)
|
|
262
|
+
elif config.irrel_force_scale > 0.0:
|
|
263
|
+
center = np.mean(simplex_points, axis=0)
|
|
264
|
+
for local_index, point_index in enumerate(simplex.tolist()):
|
|
265
|
+
if int(states[point_index]) != STATE_MOBILE:
|
|
266
|
+
continue
|
|
267
|
+
simplex_force[local_index] = config.irrel_force_scale * (
|
|
268
|
+
center - simplex_points[local_index]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
for local_index, point_index in enumerate(simplex.tolist()):
|
|
272
|
+
point_id = int(point_index)
|
|
273
|
+
if suppress_corner_forces[point_id]:
|
|
274
|
+
continue
|
|
275
|
+
total_forces[point_id] += simplex_force[local_index]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _classify_relevant_simplices(
|
|
279
|
+
points: FloatArray,
|
|
280
|
+
states: np.ndarray,
|
|
281
|
+
geometry: FemGeometry,
|
|
282
|
+
simplices: np.ndarray,
|
|
283
|
+
simplex_measures: FloatArray,
|
|
284
|
+
dim: int,
|
|
285
|
+
config: ForceParameters,
|
|
286
|
+
) -> np.ndarray:
|
|
287
|
+
"""Return the legacy relevant-simplex mask for force calculation."""
|
|
288
|
+
if len(simplices) == 0:
|
|
289
|
+
return np.empty(0, dtype=bool)
|
|
290
|
+
|
|
291
|
+
region_ids, probe_consistent = _classify_simplices_with_probes(
|
|
292
|
+
points,
|
|
293
|
+
simplices,
|
|
294
|
+
geometry,
|
|
295
|
+
)
|
|
296
|
+
boundary_mask = _boundary_state_mask(points, states, geometry)
|
|
297
|
+
all_boundary = np.all(boundary_mask[simplices], axis=1)
|
|
298
|
+
boundary_ratio = _simplex_volume_order_ratio(points, simplices, dim)
|
|
299
|
+
flat_boundary = all_boundary & (boundary_ratio < config.smallest_allowed_volume_ratio)
|
|
300
|
+
return (
|
|
301
|
+
(region_ids >= 0) & probe_consistent & (simplex_measures > BOUNDARY_FUZZ) & ~flat_boundary
|
|
302
|
+
)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Per-point force metrics for the relaxation mesher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .._constants import BOUNDARY_FUZZ, DENSITY_EPSILON, STATE_BOUNDARY, STATE_FIXED, STATE_SIMPLE
|
|
10
|
+
from .._types import FloatArray
|
|
11
|
+
from ..geometry import FemGeometry
|
|
12
|
+
from .simplex import _project_force_to_tangent
|
|
13
|
+
from .types import ForceSummary, _sphere_volume
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _finalize_force_summary(
|
|
17
|
+
total_forces: FloatArray,
|
|
18
|
+
neighbor_map: list[list[int]],
|
|
19
|
+
simplices: np.ndarray,
|
|
20
|
+
point_densities: FloatArray,
|
|
21
|
+
neighbor_force_sums: FloatArray,
|
|
22
|
+
neighbor_force_counts: np.ndarray,
|
|
23
|
+
simplex_measures: FloatArray,
|
|
24
|
+
incident_simplices: list[list[int]],
|
|
25
|
+
angle_sums: FloatArray,
|
|
26
|
+
points: FloatArray,
|
|
27
|
+
states: np.ndarray,
|
|
28
|
+
geometry: FemGeometry,
|
|
29
|
+
a0: float,
|
|
30
|
+
dim: int,
|
|
31
|
+
) -> ForceSummary:
|
|
32
|
+
"""Assemble the per-point density and effective-force metrics."""
|
|
33
|
+
point_count = len(points)
|
|
34
|
+
point_density, point_average_force, point_effective_force = _empty_force_arrays(point_count)
|
|
35
|
+
|
|
36
|
+
if point_count == 0:
|
|
37
|
+
return _make_force_summary(
|
|
38
|
+
total_forces,
|
|
39
|
+
neighbor_map,
|
|
40
|
+
simplices,
|
|
41
|
+
point_density,
|
|
42
|
+
point_average_force,
|
|
43
|
+
point_effective_force,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
boundary_mask = geometry.boundary_mask(points, tolerance=max(0.05 * a0, BOUNDARY_FUZZ))
|
|
47
|
+
|
|
48
|
+
for point_index, point in enumerate(points):
|
|
49
|
+
point_state = int(states[point_index])
|
|
50
|
+
density_here = float(point_densities[point_index])
|
|
51
|
+
effective_rod_length = a0 / (density_here ** (1.0 / max(dim, 1)))
|
|
52
|
+
ideal_local_volume = _sphere_volume(0.5 * effective_rod_length, dim)
|
|
53
|
+
angle = float(angle_sums[point_index])
|
|
54
|
+
point_average_force[point_index] = _average_neighbor_force(
|
|
55
|
+
neighbor_force_sums[point_index],
|
|
56
|
+
int(neighbor_force_counts[point_index]),
|
|
57
|
+
angle,
|
|
58
|
+
)
|
|
59
|
+
corrected_volume = _corrected_point_volume(
|
|
60
|
+
simplex_measures=simplex_measures,
|
|
61
|
+
incident=incident_simplices[point_index],
|
|
62
|
+
angle=angle,
|
|
63
|
+
dim=dim,
|
|
64
|
+
state=point_state,
|
|
65
|
+
has_multiple_immobile_neighbors=_has_multiple_immobile_neighbors(
|
|
66
|
+
point_index,
|
|
67
|
+
states,
|
|
68
|
+
neighbor_map,
|
|
69
|
+
),
|
|
70
|
+
ideal_local_volume=ideal_local_volume,
|
|
71
|
+
)
|
|
72
|
+
point_density[point_index] = ideal_local_volume / max(corrected_volume, DENSITY_EPSILON)
|
|
73
|
+
point_effective_force[point_index] = _point_effective_force(
|
|
74
|
+
total_force=total_forces[point_index],
|
|
75
|
+
state=point_state,
|
|
76
|
+
geometry=geometry,
|
|
77
|
+
point=point,
|
|
78
|
+
is_boundary=bool(boundary_mask[point_index]),
|
|
79
|
+
density_here=density_here,
|
|
80
|
+
a0=a0,
|
|
81
|
+
dim=dim,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return _make_force_summary(
|
|
85
|
+
total_forces,
|
|
86
|
+
neighbor_map,
|
|
87
|
+
simplices,
|
|
88
|
+
point_density,
|
|
89
|
+
point_average_force,
|
|
90
|
+
point_effective_force,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _empty_force_arrays(point_count: int) -> tuple[FloatArray, FloatArray, FloatArray]:
|
|
95
|
+
"""Allocate the per-point summary arrays used by ``_finalize_force_summary``."""
|
|
96
|
+
return (
|
|
97
|
+
np.ones(point_count, dtype=float),
|
|
98
|
+
np.zeros(point_count, dtype=float),
|
|
99
|
+
np.zeros(point_count, dtype=float),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _make_force_summary(
|
|
104
|
+
total_forces: FloatArray,
|
|
105
|
+
neighbor_map: list[list[int]],
|
|
106
|
+
simplices: np.ndarray,
|
|
107
|
+
point_density: FloatArray,
|
|
108
|
+
point_average_force: FloatArray,
|
|
109
|
+
point_effective_force: FloatArray,
|
|
110
|
+
) -> ForceSummary:
|
|
111
|
+
"""Construct a ``ForceSummary`` from the completed per-point arrays."""
|
|
112
|
+
return ForceSummary(
|
|
113
|
+
total_forces=total_forces,
|
|
114
|
+
neighbor_map=neighbor_map,
|
|
115
|
+
simplices=simplices,
|
|
116
|
+
point_density=point_density,
|
|
117
|
+
point_average_force=point_average_force,
|
|
118
|
+
point_effective_force=point_effective_force,
|
|
119
|
+
max_effective_force=float(np.max(point_effective_force, initial=0.0)),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _average_neighbor_force(force_sum: float, force_count: int, angle: float) -> float:
|
|
124
|
+
"""Return the mean absolute neighbor-force magnitude for a point."""
|
|
125
|
+
if angle <= DENSITY_EPSILON:
|
|
126
|
+
return 1.0e4
|
|
127
|
+
if force_count <= 0:
|
|
128
|
+
return 0.0
|
|
129
|
+
return force_sum / float(force_count)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _has_multiple_immobile_neighbors(
|
|
133
|
+
point_index: int,
|
|
134
|
+
states: np.ndarray,
|
|
135
|
+
neighbor_map: list[list[int]],
|
|
136
|
+
) -> bool:
|
|
137
|
+
"""Return whether a boundary point is trapped between multiple immobile nodes."""
|
|
138
|
+
if int(states[point_index]) != STATE_BOUNDARY:
|
|
139
|
+
return False
|
|
140
|
+
immobile_neighbors = sum(
|
|
141
|
+
1
|
|
142
|
+
for neighbor_index in neighbor_map[point_index]
|
|
143
|
+
if int(states[neighbor_index]) in (STATE_FIXED, STATE_SIMPLE)
|
|
144
|
+
)
|
|
145
|
+
return immobile_neighbors > 1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _corrected_point_volume(
|
|
149
|
+
*,
|
|
150
|
+
simplex_measures: FloatArray,
|
|
151
|
+
incident: list[int],
|
|
152
|
+
angle: float,
|
|
153
|
+
dim: int,
|
|
154
|
+
state: int,
|
|
155
|
+
has_multiple_immobile_neighbors: bool,
|
|
156
|
+
ideal_local_volume: float,
|
|
157
|
+
) -> float:
|
|
158
|
+
"""Return the local corrected control volume for one point."""
|
|
159
|
+
if has_multiple_immobile_neighbors or angle <= DENSITY_EPSILON:
|
|
160
|
+
return 1.0e-4 * ideal_local_volume
|
|
161
|
+
if not incident:
|
|
162
|
+
return 1.0e-4 * ideal_local_volume
|
|
163
|
+
|
|
164
|
+
incident_measures = simplex_measures[np.asarray(incident, dtype=int)]
|
|
165
|
+
if len(incident) == 1:
|
|
166
|
+
local_volume = float(incident_measures[0]) / float(max(dim, 1))
|
|
167
|
+
else:
|
|
168
|
+
local_volume = float(np.sum(incident_measures)) / float(dim + 1)
|
|
169
|
+
|
|
170
|
+
correction = _voronoi_angle_correction(dim, angle, len(incident))
|
|
171
|
+
corrected_volume = local_volume * correction
|
|
172
|
+
if state == STATE_BOUNDARY:
|
|
173
|
+
corrected_volume *= 1.2
|
|
174
|
+
return corrected_volume
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _voronoi_angle_correction(dim: int, angle: float, incident_count: int) -> float:
|
|
178
|
+
"""Return the legacy angular correction for Voronoi control volumes."""
|
|
179
|
+
if dim == 1:
|
|
180
|
+
return 1.5
|
|
181
|
+
if dim == 2:
|
|
182
|
+
return 2.0 * math.pi / angle
|
|
183
|
+
if dim == 3:
|
|
184
|
+
return 4.0 * math.pi / angle
|
|
185
|
+
return math.factorial(dim + 1) / float(max(incident_count, 1))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _point_effective_force(
|
|
189
|
+
*,
|
|
190
|
+
total_force: FloatArray,
|
|
191
|
+
state: int,
|
|
192
|
+
geometry: FemGeometry,
|
|
193
|
+
point: FloatArray,
|
|
194
|
+
is_boundary: bool,
|
|
195
|
+
density_here: float,
|
|
196
|
+
a0: float,
|
|
197
|
+
dim: int,
|
|
198
|
+
) -> float:
|
|
199
|
+
"""Return the normalized effective force magnitude for one point."""
|
|
200
|
+
effective_force = np.asarray(total_force, dtype=float)
|
|
201
|
+
if state == STATE_BOUNDARY or is_boundary:
|
|
202
|
+
effective_force = _project_force_to_tangent(geometry, point, effective_force)
|
|
203
|
+
return (
|
|
204
|
+
float(np.linalg.norm(effective_force))
|
|
205
|
+
* (density_here ** (1.0 / max(dim, 1)))
|
|
206
|
+
/ max(a0, DENSITY_EPSILON)
|
|
207
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Shared types, configuration, and scalar helpers for meshing forces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ...meshing_parameters import (
|
|
12
|
+
default_boundary_node_force_fun,
|
|
13
|
+
default_initial_relaxation_weight,
|
|
14
|
+
default_relaxation_force_fun,
|
|
15
|
+
)
|
|
16
|
+
from .._types import FloatArray
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class ForceSummary:
|
|
21
|
+
"""Container for the per-step force calculation results."""
|
|
22
|
+
|
|
23
|
+
total_forces: FloatArray
|
|
24
|
+
neighbor_map: list[list[int]]
|
|
25
|
+
simplices: np.ndarray
|
|
26
|
+
point_density: FloatArray
|
|
27
|
+
point_average_force: FloatArray
|
|
28
|
+
point_effective_force: FloatArray
|
|
29
|
+
max_effective_force: float
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class ForceParameters:
|
|
34
|
+
"""Resolved controller parameters for one relaxation-force evaluation."""
|
|
35
|
+
|
|
36
|
+
shape_force_scale: float
|
|
37
|
+
volume_force_scale: float
|
|
38
|
+
neigh_force_scale: float
|
|
39
|
+
irrel_force_scale: float
|
|
40
|
+
sliver_correction: float
|
|
41
|
+
smallest_allowed_volume_ratio: float
|
|
42
|
+
relaxation_weight: float
|
|
43
|
+
force_fun: Any
|
|
44
|
+
boundary_force_fun: Any
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _corner_force_threshold(dim: int) -> float:
|
|
48
|
+
"""Return the solid-angle threshold below which corner suppression applies."""
|
|
49
|
+
if dim == 2:
|
|
50
|
+
return 0.75 * math.pi
|
|
51
|
+
if dim == 3:
|
|
52
|
+
return 0.85 * math.pi
|
|
53
|
+
return -math.inf
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _regular_simplex_volume(edge_length: float, dim: int) -> float:
|
|
57
|
+
"""Return the volume of a regular simplex with the supplied edge length."""
|
|
58
|
+
if dim <= 0:
|
|
59
|
+
return edge_length
|
|
60
|
+
numerator = edge_length**dim * math.sqrt(dim + 1.0)
|
|
61
|
+
denominator = math.factorial(dim) * math.sqrt(2.0**dim)
|
|
62
|
+
return numerator / denominator
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sphere_volume(radius: float, dim: int) -> float:
|
|
66
|
+
"""Return the d-dimensional volume of a sphere."""
|
|
67
|
+
return (math.pi ** (0.5 * dim) / math.gamma(0.5 * dim + 1.0)) * (radius**dim)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _extract_force_parameters(params: dict[str, Any], step: int) -> ForceParameters:
|
|
71
|
+
"""Resolve the mesher controller parameters used by ``compute_forces``."""
|
|
72
|
+
settling_steps = int(params.get("controller_initial_settling_steps", 100))
|
|
73
|
+
relaxation_weight_fun = params.get(
|
|
74
|
+
"initial_relaxation_weight_fun",
|
|
75
|
+
default_initial_relaxation_weight,
|
|
76
|
+
)
|
|
77
|
+
return ForceParameters(
|
|
78
|
+
shape_force_scale=float(params.get("controller_shape_force_scale", 0.1)),
|
|
79
|
+
volume_force_scale=float(params.get("controller_volume_force_scale", 0.0)),
|
|
80
|
+
neigh_force_scale=float(params.get("controller_neigh_force_scale", 1.0)),
|
|
81
|
+
irrel_force_scale=float(params.get("controller_irrel_elem_force_scale", 1.0)),
|
|
82
|
+
sliver_correction=float(params.get("controller_sliver_correction", 1.0)),
|
|
83
|
+
smallest_allowed_volume_ratio=float(
|
|
84
|
+
params.get("controller_smallest_allowed_volume_ratio", 1.0)
|
|
85
|
+
),
|
|
86
|
+
relaxation_weight=float(relaxation_weight_fun(step, settling_steps, 0.0, 1.0)),
|
|
87
|
+
force_fun=params.get("relaxation_force_fun", default_relaxation_force_fun),
|
|
88
|
+
boundary_force_fun=params.get(
|
|
89
|
+
"boundary_node_force_fun",
|
|
90
|
+
default_boundary_node_force_fun,
|
|
91
|
+
),
|
|
92
|
+
)
|