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,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
+ )
@@ -0,0 +1,6 @@
1
+ """Public compatibility exports for relaxation geometry."""
2
+
3
+ from .builder import fem_geometry_from_bodies
4
+ from .model import FemGeometry
5
+
6
+ __all__ = ["FemGeometry", "fem_geometry_from_bodies"]