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,310 @@
|
|
|
1
|
+
"""Surface-recovery helpers for the relaxation meshing pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from itertools import combinations
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .._constants import (
|
|
12
|
+
BOUNDARY_FUZZ,
|
|
13
|
+
DENSITY_EPSILON,
|
|
14
|
+
STATE_BOUNDARY,
|
|
15
|
+
STATE_FIXED,
|
|
16
|
+
STATE_MOBILE,
|
|
17
|
+
STATE_SIMPLE,
|
|
18
|
+
)
|
|
19
|
+
from .._types import FloatArray
|
|
20
|
+
from ..geometry import FemGeometry
|
|
21
|
+
from . import _simplex_measures
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mirror_surface_recovery_points(
|
|
25
|
+
points: FloatArray,
|
|
26
|
+
states: np.ndarray,
|
|
27
|
+
geometry: FemGeometry,
|
|
28
|
+
a0: float,
|
|
29
|
+
simplices: np.ndarray,
|
|
30
|
+
) -> FloatArray:
|
|
31
|
+
"""Return legacy mirror/prevention points for poor boundary simplices."""
|
|
32
|
+
|
|
33
|
+
dim = geometry.dim
|
|
34
|
+
if dim not in (2, 3) or len(simplices) == 0:
|
|
35
|
+
return np.empty((0, dim), dtype=float)
|
|
36
|
+
|
|
37
|
+
simplex_measures = _simplex_measures(points, simplices, dim)
|
|
38
|
+
seen_surfaces: set[tuple[tuple[float, ...], ...]] = set()
|
|
39
|
+
additions: list[FloatArray] = []
|
|
40
|
+
|
|
41
|
+
for simplex, simplex_volume in zip(simplices, simplex_measures, strict=True):
|
|
42
|
+
simplex_points = np.asarray(points[simplex], dtype=float)
|
|
43
|
+
boundary_points, interior_points = _split_boundary_and_interior_points(
|
|
44
|
+
simplex,
|
|
45
|
+
simplex_points,
|
|
46
|
+
states,
|
|
47
|
+
)
|
|
48
|
+
if dim == 2:
|
|
49
|
+
additions.extend(
|
|
50
|
+
_mirror_points_2d(
|
|
51
|
+
boundary_points,
|
|
52
|
+
interior_points,
|
|
53
|
+
simplex_points,
|
|
54
|
+
float(simplex_volume),
|
|
55
|
+
geometry,
|
|
56
|
+
a0,
|
|
57
|
+
seen_surfaces,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
additions.extend(
|
|
62
|
+
_prevention_points_3d(
|
|
63
|
+
boundary_points,
|
|
64
|
+
simplex_points,
|
|
65
|
+
float(simplex_volume),
|
|
66
|
+
geometry,
|
|
67
|
+
a0,
|
|
68
|
+
seen_surfaces,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not additions:
|
|
73
|
+
return np.empty((0, dim), dtype=float)
|
|
74
|
+
return _dedupe_recovery_points(np.asarray(additions, dtype=float))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _split_boundary_and_interior_points(
|
|
78
|
+
simplex: np.ndarray,
|
|
79
|
+
simplex_points: FloatArray,
|
|
80
|
+
states: np.ndarray,
|
|
81
|
+
) -> tuple[list[FloatArray], list[FloatArray]]:
|
|
82
|
+
"""Split simplex vertices into boundary-like and mobile interior vertices."""
|
|
83
|
+
|
|
84
|
+
boundary_points: list[FloatArray] = []
|
|
85
|
+
interior_points: list[FloatArray] = []
|
|
86
|
+
for local_index, point_index in enumerate(simplex.tolist()):
|
|
87
|
+
state = int(states[int(point_index)])
|
|
88
|
+
point = simplex_points[local_index]
|
|
89
|
+
if state in (STATE_BOUNDARY, STATE_FIXED, STATE_SIMPLE):
|
|
90
|
+
boundary_points.append(point)
|
|
91
|
+
elif state == STATE_MOBILE:
|
|
92
|
+
interior_points.append(point)
|
|
93
|
+
return boundary_points, interior_points
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _mirror_points_2d(
|
|
97
|
+
boundary_points: list[FloatArray],
|
|
98
|
+
interior_points: list[FloatArray],
|
|
99
|
+
simplex_points: FloatArray,
|
|
100
|
+
simplex_volume: float,
|
|
101
|
+
geometry: FemGeometry,
|
|
102
|
+
a0: float,
|
|
103
|
+
seen_surfaces: set[tuple[tuple[float, ...], ...]],
|
|
104
|
+
) -> list[FloatArray]:
|
|
105
|
+
"""Return mirrored points for the legacy 2D boundary-simplex cases."""
|
|
106
|
+
|
|
107
|
+
additions: list[FloatArray] = []
|
|
108
|
+
if len(boundary_points) == 3:
|
|
109
|
+
shifted = list(boundary_points)
|
|
110
|
+
for _ in range(3):
|
|
111
|
+
vertex_point = shifted[0]
|
|
112
|
+
surface_points = np.asarray(shifted[1:], dtype=float)
|
|
113
|
+
if _register_surface(surface_points, seen_surfaces):
|
|
114
|
+
shifted = shifted[1:] + shifted[:1]
|
|
115
|
+
continue
|
|
116
|
+
additions.extend(
|
|
117
|
+
_node_action_2d(
|
|
118
|
+
surface_points, vertex_point, simplex_points, simplex_volume, geometry, a0
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
shifted = shifted[1:] + shifted[:1]
|
|
122
|
+
return additions
|
|
123
|
+
|
|
124
|
+
if len(boundary_points) == 2 and len(interior_points) == 1:
|
|
125
|
+
surface_points = np.asarray(boundary_points, dtype=float)
|
|
126
|
+
if not _register_surface(surface_points, seen_surfaces):
|
|
127
|
+
additions.extend(
|
|
128
|
+
_node_action_2d(
|
|
129
|
+
surface_points,
|
|
130
|
+
interior_points[0],
|
|
131
|
+
simplex_points,
|
|
132
|
+
simplex_volume,
|
|
133
|
+
geometry,
|
|
134
|
+
a0,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
return additions
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _node_action_2d(
|
|
141
|
+
surface_points: FloatArray,
|
|
142
|
+
vertex_point: FloatArray,
|
|
143
|
+
simplex_points: FloatArray,
|
|
144
|
+
simplex_volume: float,
|
|
145
|
+
geometry: FemGeometry,
|
|
146
|
+
a0: float,
|
|
147
|
+
) -> list[FloatArray]:
|
|
148
|
+
"""Mirror one 2D point when the surface angle and volume tests request it."""
|
|
149
|
+
|
|
150
|
+
angle = _solid_angle_2d(surface_points, vertex_point)
|
|
151
|
+
if (
|
|
152
|
+
angle > 0.55 * math.pi
|
|
153
|
+
and _volume_ratio(simplex_points, simplex_volume, geometry, a0) > 3.0e-2
|
|
154
|
+
):
|
|
155
|
+
return [_mirror_point(surface_points, vertex_point, 2)]
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _prevention_points_3d(
|
|
160
|
+
boundary_points: list[FloatArray],
|
|
161
|
+
simplex_points: FloatArray,
|
|
162
|
+
simplex_volume: float,
|
|
163
|
+
geometry: FemGeometry,
|
|
164
|
+
a0: float,
|
|
165
|
+
seen_surfaces: set[tuple[tuple[float, ...], ...]],
|
|
166
|
+
) -> list[FloatArray]:
|
|
167
|
+
"""Return midpoint prevention points for legacy 3D boundary edges."""
|
|
168
|
+
|
|
169
|
+
if len(boundary_points) < 2:
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
additions: list[FloatArray] = []
|
|
173
|
+
for left, right in combinations(boundary_points, 2):
|
|
174
|
+
surface_points = np.asarray([left, right], dtype=float)
|
|
175
|
+
already_seen = _register_surface(surface_points, seen_surfaces)
|
|
176
|
+
if already_seen:
|
|
177
|
+
continue
|
|
178
|
+
middle_point = 0.5 * (surface_points[0] + surface_points[1])
|
|
179
|
+
middle_density = geometry.density_at(middle_point)
|
|
180
|
+
middle_rod_length = a0 / (middle_density ** (1.0 / max(geometry.dim, 1)))
|
|
181
|
+
distance = float(np.linalg.norm(surface_points[0] - surface_points[1]))
|
|
182
|
+
if (
|
|
183
|
+
distance > 1.2 * middle_rod_length
|
|
184
|
+
and _volume_ratio(simplex_points, simplex_volume, geometry, a0) > 3.0e-2
|
|
185
|
+
and _is_outer_region_constrained(geometry, middle_point)
|
|
186
|
+
):
|
|
187
|
+
additions.append(middle_point)
|
|
188
|
+
return additions
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _surface_key(surface_points: FloatArray) -> tuple[tuple[float, ...], ...]:
|
|
192
|
+
"""Return a stable set-like key for a boundary surface."""
|
|
193
|
+
|
|
194
|
+
return tuple(
|
|
195
|
+
sorted(
|
|
196
|
+
tuple(
|
|
197
|
+
cast(
|
|
198
|
+
list[float],
|
|
199
|
+
np.asarray(np.round(point, decimals=10), dtype=float).tolist(),
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
for point in surface_points
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _register_surface(
|
|
208
|
+
surface_points: FloatArray,
|
|
209
|
+
seen_surfaces: set[tuple[tuple[float, ...], ...]],
|
|
210
|
+
) -> bool:
|
|
211
|
+
"""Register a surface and return whether it had already been processed."""
|
|
212
|
+
|
|
213
|
+
key = _surface_key(surface_points)
|
|
214
|
+
if key in seen_surfaces:
|
|
215
|
+
return True
|
|
216
|
+
seen_surfaces.add(key)
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _solid_angle_2d(surface_points: FloatArray, vertex_point: FloatArray) -> float:
|
|
221
|
+
"""Return the angle subtended by two surface points from a vertex."""
|
|
222
|
+
|
|
223
|
+
first = surface_points[0] - vertex_point
|
|
224
|
+
second = surface_points[1] - vertex_point
|
|
225
|
+
denom = math.sqrt(float(np.sum(first * first))) * math.sqrt(float(np.sum(second * second)))
|
|
226
|
+
if denom <= DENSITY_EPSILON:
|
|
227
|
+
return 0.0
|
|
228
|
+
cosine = min(1.0, max(-1.0, float(np.sum(first * second)) / denom))
|
|
229
|
+
return float(math.acos(cosine))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _mirror_point(surface_points: FloatArray, point: FloatArray, dim: int) -> FloatArray:
|
|
233
|
+
"""Mirror a point across a line/plane defined by boundary points."""
|
|
234
|
+
|
|
235
|
+
if len(surface_points) == dim:
|
|
236
|
+
origin = surface_points[0]
|
|
237
|
+
directions = surface_points[1:] - origin
|
|
238
|
+
_, _, vh = np.linalg.svd(directions, full_matrices=True)
|
|
239
|
+
normal = np.asarray(vh[-1], dtype=float)
|
|
240
|
+
normal_norm_sq = float(np.dot(normal, normal))
|
|
241
|
+
if normal_norm_sq <= DENSITY_EPSILON:
|
|
242
|
+
return np.asarray(point, dtype=float)
|
|
243
|
+
factor = float(np.dot(point - origin, normal)) / normal_norm_sq
|
|
244
|
+
projection = point - factor * normal
|
|
245
|
+
return np.asarray(2.0 * projection - point, dtype=float)
|
|
246
|
+
|
|
247
|
+
if len(surface_points) == dim - 1 and dim == 3:
|
|
248
|
+
start = surface_points[0]
|
|
249
|
+
segment = surface_points[1] - start
|
|
250
|
+
denom = float(np.dot(segment, segment))
|
|
251
|
+
if denom <= DENSITY_EPSILON:
|
|
252
|
+
return np.asarray(point, dtype=float)
|
|
253
|
+
fraction = float(np.dot(point - start, segment)) / denom
|
|
254
|
+
projection = start + fraction * segment
|
|
255
|
+
return np.asarray(2.0 * projection - point, dtype=float)
|
|
256
|
+
|
|
257
|
+
return np.asarray(point, dtype=float)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _volume_ratio(
|
|
261
|
+
simplex_points: FloatArray,
|
|
262
|
+
simplex_volume: float,
|
|
263
|
+
geometry: FemGeometry,
|
|
264
|
+
a0: float,
|
|
265
|
+
) -> float:
|
|
266
|
+
"""Return the legacy real-to-ideal simplex volume ratio."""
|
|
267
|
+
|
|
268
|
+
if simplex_volume <= DENSITY_EPSILON:
|
|
269
|
+
return 0.0
|
|
270
|
+
center = np.mean(simplex_points, axis=0)
|
|
271
|
+
density_here = geometry.density_at(center)
|
|
272
|
+
rod_scaled = a0 / (density_here ** (1.0 / max(geometry.dim, 1)))
|
|
273
|
+
ideal_volume = _regular_simplex_volume(rod_scaled, geometry.dim)
|
|
274
|
+
return abs(simplex_volume / max(ideal_volume, DENSITY_EPSILON))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _regular_simplex_volume(edge_length: float, dim: int) -> float:
|
|
278
|
+
"""Return the volume of a regular simplex with the supplied edge length."""
|
|
279
|
+
|
|
280
|
+
numerator = edge_length**dim * math.sqrt(dim + 1.0)
|
|
281
|
+
denominator = math.factorial(dim) * math.sqrt(2.0**dim)
|
|
282
|
+
return numerator / denominator
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _is_outer_region_constrained(geometry: FemGeometry, point: FloatArray) -> bool:
|
|
286
|
+
"""Return whether a point satisfies the legacy outer-region boundary check."""
|
|
287
|
+
|
|
288
|
+
if not geometry.points_in_bbox(np.asarray(point, dtype=float)[np.newaxis, :])[0]:
|
|
289
|
+
return False
|
|
290
|
+
for body in geometry.region_bodies:
|
|
291
|
+
if body is None:
|
|
292
|
+
continue
|
|
293
|
+
if float(body.evaluate(point)) >= BOUNDARY_FUZZ:
|
|
294
|
+
return False
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _dedupe_recovery_points(points: FloatArray) -> FloatArray:
|
|
299
|
+
"""Deduplicate generated recovery points while preserving order."""
|
|
300
|
+
|
|
301
|
+
keep_indices: list[int] = []
|
|
302
|
+
seen: set[tuple[float, ...]] = set()
|
|
303
|
+
for index, point in enumerate(points):
|
|
304
|
+
rounded = np.asarray(np.round(point, decimals=10), dtype=float)
|
|
305
|
+
key = tuple(cast(list[float], rounded.tolist()))
|
|
306
|
+
if key in seen:
|
|
307
|
+
continue
|
|
308
|
+
seen.add(key)
|
|
309
|
+
keep_indices.append(index)
|
|
310
|
+
return points[np.asarray(keep_indices, dtype=int)]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Case-preserving, sectioned INI data used by meshing parameters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import configparser
|
|
6
|
+
from collections.abc import ItemsView
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from os import PathLike
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _CasePreservingConfigParser(configparser.ConfigParser):
|
|
13
|
+
def optionxform(self, optionstr: str) -> str:
|
|
14
|
+
return optionstr
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SectionedConfig:
|
|
18
|
+
"""Small mutable INI container with typed scalar coercion."""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._data: dict[str, dict[str, Any]] = {}
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _coerce_value(value: str) -> Any:
|
|
25
|
+
text = value.strip()
|
|
26
|
+
if not text:
|
|
27
|
+
return text
|
|
28
|
+
if text.lower() in {"true", "false"}:
|
|
29
|
+
return text.lower() == "true"
|
|
30
|
+
try:
|
|
31
|
+
return int(text)
|
|
32
|
+
except ValueError:
|
|
33
|
+
pass
|
|
34
|
+
try:
|
|
35
|
+
return float(text)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return text
|
|
38
|
+
|
|
39
|
+
def _load_from_parser(self, parser: configparser.ConfigParser) -> None:
|
|
40
|
+
for section in parser.sections():
|
|
41
|
+
self.add_section(section)
|
|
42
|
+
for name, value in parser.items(section):
|
|
43
|
+
self.set(section, name, self._coerce_value(value))
|
|
44
|
+
|
|
45
|
+
def from_file(self, file_path: str | PathLike[str]) -> None:
|
|
46
|
+
parser = _CasePreservingConfigParser(delimiters=("=", ":"), interpolation=None)
|
|
47
|
+
with open(file_path, encoding="utf-8") as stream:
|
|
48
|
+
parser.read_file(stream)
|
|
49
|
+
self._load_from_parser(parser)
|
|
50
|
+
|
|
51
|
+
def from_string(self, string: str) -> None:
|
|
52
|
+
parser = _CasePreservingConfigParser(delimiters=("=", ":"), interpolation=None)
|
|
53
|
+
parser.read_file(StringIO(string))
|
|
54
|
+
self._load_from_parser(parser)
|
|
55
|
+
|
|
56
|
+
def add_section(self, section: str) -> None:
|
|
57
|
+
self._data.setdefault(section, {})
|
|
58
|
+
|
|
59
|
+
def get(self, section: str, name: str, raw: bool = False) -> Any:
|
|
60
|
+
del raw
|
|
61
|
+
return self._data.get(section, {}).get(name)
|
|
62
|
+
|
|
63
|
+
def set(self, section: str, name: str, value: Any) -> None:
|
|
64
|
+
self._data.setdefault(section, {})[name] = value
|
|
65
|
+
|
|
66
|
+
def items(self, section: str) -> ItemsView[str, Any]:
|
|
67
|
+
return self._data.get(section, {}).items()
|
|
68
|
+
|
|
69
|
+
def to_string(self) -> str:
|
|
70
|
+
return str(self._data)
|
nmesh/nmesh.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Public nmesh compatibility façade with lazy geometry and driver exports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from .mesh_generation import Mesh, get_default_meshing_parameters
|
|
8
|
+
from .mesh_io import (
|
|
9
|
+
MeshFromFile,
|
|
10
|
+
hdf5_mesh_get_permutation,
|
|
11
|
+
load,
|
|
12
|
+
mesh_from_points_and_simplices,
|
|
13
|
+
save,
|
|
14
|
+
write_mesh,
|
|
15
|
+
)
|
|
16
|
+
from .mesh_model import MeshBase, Point, Simplex
|
|
17
|
+
from .mesh_utilities import (
|
|
18
|
+
generate_1d_mesh,
|
|
19
|
+
generate_1d_mesh_components,
|
|
20
|
+
memory_report,
|
|
21
|
+
outer_corners,
|
|
22
|
+
to_lists,
|
|
23
|
+
tolists,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
_GEOMETRY_EXPORTS = {
|
|
30
|
+
"AffineTransform",
|
|
31
|
+
"Body",
|
|
32
|
+
"Box",
|
|
33
|
+
"Conic",
|
|
34
|
+
"Ellipsoid",
|
|
35
|
+
"Helix",
|
|
36
|
+
"MeshObject",
|
|
37
|
+
"bc_box",
|
|
38
|
+
"bc_ellipsoid",
|
|
39
|
+
"bc_frustum",
|
|
40
|
+
"bc_helix",
|
|
41
|
+
"difference",
|
|
42
|
+
"intersect",
|
|
43
|
+
"inverse_axis_rotation",
|
|
44
|
+
"inverse_plane_rotation",
|
|
45
|
+
"inverse_scale",
|
|
46
|
+
"inverse_shift",
|
|
47
|
+
"union",
|
|
48
|
+
}
|
|
49
|
+
_MESHER_EXPORTS = {
|
|
50
|
+
"Callback",
|
|
51
|
+
"EngineFunc",
|
|
52
|
+
"MeshEngineCommand",
|
|
53
|
+
"MeshEngineStatus",
|
|
54
|
+
"MeshingParameters",
|
|
55
|
+
"do_every_n_steps_driver",
|
|
56
|
+
"make_mg_gendriver",
|
|
57
|
+
}
|
|
58
|
+
__all__ = [
|
|
59
|
+
"Mesh",
|
|
60
|
+
"MeshBase",
|
|
61
|
+
"MeshFromFile",
|
|
62
|
+
"Point",
|
|
63
|
+
"Simplex",
|
|
64
|
+
"generate_1d_mesh",
|
|
65
|
+
"generate_1d_mesh_components",
|
|
66
|
+
"get_default_meshing_parameters",
|
|
67
|
+
"hdf5_mesh_get_permutation",
|
|
68
|
+
"load",
|
|
69
|
+
"memory_report",
|
|
70
|
+
"mesh_from_points_and_simplices",
|
|
71
|
+
"outer_corners",
|
|
72
|
+
"save",
|
|
73
|
+
"to_lists",
|
|
74
|
+
"tolists",
|
|
75
|
+
"write_mesh",
|
|
76
|
+
*_GEOMETRY_EXPORTS,
|
|
77
|
+
*_MESHER_EXPORTS,
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def __getattr__(name: str) -> Any:
|
|
82
|
+
if name in _GEOMETRY_EXPORTS:
|
|
83
|
+
from . import geometry
|
|
84
|
+
|
|
85
|
+
value = getattr(geometry, name)
|
|
86
|
+
globals()[name] = value
|
|
87
|
+
return value
|
|
88
|
+
if name == "MeshingParameters":
|
|
89
|
+
from .mesher.meshing_parameters import MeshingParameters
|
|
90
|
+
|
|
91
|
+
globals()[name] = MeshingParameters
|
|
92
|
+
return MeshingParameters
|
|
93
|
+
if name in _MESHER_EXPORTS:
|
|
94
|
+
from .mesher import driver
|
|
95
|
+
|
|
96
|
+
value = getattr(driver, name)
|
|
97
|
+
globals()[name] = value
|
|
98
|
+
return value
|
|
99
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
nmesh/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker for PEP 561 inline type information.
|
nmesh/utils/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from .array_list_utils import (
|
|
2
|
+
array_filter,
|
|
3
|
+
array_one_shorter,
|
|
4
|
+
array_position,
|
|
5
|
+
array_position_if,
|
|
6
|
+
cross_product_3d,
|
|
7
|
+
det_and_inv,
|
|
8
|
+
determinant,
|
|
9
|
+
inverse,
|
|
10
|
+
)
|
|
11
|
+
from .constants import BOUNDARY_FUZZ, MIN_ABS_SCALE_FACTOR, MIN_DIVISION_MAGNITUDE
|
|
12
|
+
from .timing_memory_utils import memstats, time_passed, time_vmem_rss
|
|
13
|
+
from .types import BoolArray, FloatArray, IntArray
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BOUNDARY_FUZZ",
|
|
17
|
+
"BoolArray",
|
|
18
|
+
"FloatArray",
|
|
19
|
+
"IntArray",
|
|
20
|
+
"MIN_ABS_SCALE_FACTOR",
|
|
21
|
+
"MIN_DIVISION_MAGNITUDE",
|
|
22
|
+
"array_filter",
|
|
23
|
+
"array_one_shorter",
|
|
24
|
+
"array_position",
|
|
25
|
+
"array_position_if",
|
|
26
|
+
"cross_product_3d",
|
|
27
|
+
"det_and_inv",
|
|
28
|
+
"determinant",
|
|
29
|
+
"inverse",
|
|
30
|
+
"memstats",
|
|
31
|
+
"time_passed",
|
|
32
|
+
"time_vmem_rss",
|
|
33
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
|
+
from typing import Any, TypeVar
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def array_filter(p: Callable[[T], bool], arr: Sequence[T]) -> NDArray[Any]:
|
|
11
|
+
"""Filter array elements using a predicate function.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
p: Predicate function returning True for elements to keep
|
|
15
|
+
arr: Input array or sequence
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
NumPy array containing only elements where p(x) is True
|
|
19
|
+
"""
|
|
20
|
+
arr_np = np.asanyarray(arr)
|
|
21
|
+
mask = np.array([p(x) for x in arr_np], dtype=bool)
|
|
22
|
+
return arr_np[mask]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def array_position(x: T, arr: Sequence[T], start: int = 0) -> int:
|
|
26
|
+
"""Find first position of element x in array starting from index start.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
x: Element to find
|
|
30
|
+
arr: Array to search
|
|
31
|
+
start: Starting index (default 0)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Index of first occurrence, or -1 if not found
|
|
35
|
+
"""
|
|
36
|
+
arr_np = np.asanyarray(arr)
|
|
37
|
+
sub_arr = arr_np[start:]
|
|
38
|
+
indices = np.where(sub_arr == x)[0]
|
|
39
|
+
if len(indices) > 0:
|
|
40
|
+
return int(indices[0] + start)
|
|
41
|
+
return -1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def array_position_if(p: Callable[[T], bool], arr: Sequence[T], start: int = 0) -> int:
|
|
45
|
+
"""Find first position where predicate p is True, starting from index start.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
p: Predicate function
|
|
49
|
+
arr: Array to search
|
|
50
|
+
start: Starting index (default 0)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Index of first match, or -1 if not found
|
|
54
|
+
"""
|
|
55
|
+
arr_np = np.asanyarray(arr)
|
|
56
|
+
sub_arr = arr_np[start:]
|
|
57
|
+
mask = np.array([p(x) for x in sub_arr], dtype=bool)
|
|
58
|
+
indices = np.where(mask)[0]
|
|
59
|
+
if len(indices) > 0:
|
|
60
|
+
return int(indices[0] + start)
|
|
61
|
+
return -1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def array_one_shorter(arr: Sequence[T], pos: int) -> NDArray[Any]:
|
|
65
|
+
"""Remove element at position pos from array.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
arr: Input array
|
|
69
|
+
pos: Index of element to remove
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
New array with element removed
|
|
73
|
+
"""
|
|
74
|
+
return np.delete(np.asanyarray(arr), pos)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def determinant(mx: Sequence[Sequence[float]]) -> float:
|
|
78
|
+
"""Compute determinant of a matrix.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
mx: Square matrix
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Determinant as float
|
|
85
|
+
"""
|
|
86
|
+
return float(np.linalg.det(np.asanyarray(mx)))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def inverse(mx: Sequence[Sequence[float]]) -> NDArray[np.float64]:
|
|
90
|
+
"""Compute inverse of a matrix.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
mx: Square invertible matrix
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Inverse matrix as NumPy array
|
|
97
|
+
"""
|
|
98
|
+
return np.linalg.inv(np.asanyarray(mx))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def det_and_inv(
|
|
102
|
+
mx: Sequence[Sequence[float]],
|
|
103
|
+
) -> tuple[float, NDArray[np.float64]]:
|
|
104
|
+
"""Compute determinant and inverse of a matrix simultaneously.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
mx: Square invertible matrix
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple of (determinant, inverse_matrix)
|
|
111
|
+
"""
|
|
112
|
+
arr = np.asanyarray(mx)
|
|
113
|
+
det = np.linalg.det(arr)
|
|
114
|
+
inv = np.linalg.inv(arr)
|
|
115
|
+
return float(det), inv
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cross_product_3d(v1: Sequence[float], v2: Sequence[float]) -> NDArray[np.float64]:
|
|
119
|
+
"""Compute 3D cross product of two vectors.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
v1: First 3D vector
|
|
123
|
+
v2: Second 3D vector
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Cross product v1 × v2 as NumPy array
|
|
127
|
+
"""
|
|
128
|
+
return np.cross(np.asanyarray(v1), np.asanyarray(v2))
|
nmesh/utils/constants.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common numerical constants used throughout the nmesh package.
|
|
3
|
+
|
|
4
|
+
These constants provide consistent numerical tolerance values across different
|
|
5
|
+
modules for operations like scale validation, division safety checks, and
|
|
6
|
+
floating-point comparisons.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BOUNDARY_FUZZ",
|
|
11
|
+
"MIN_ABS_SCALE_FACTOR",
|
|
12
|
+
"MIN_DIVISION_MAGNITUDE",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# Default tolerance used when testing whether points lie on or near boundaries.
|
|
16
|
+
BOUNDARY_FUZZ = 1e-6
|
|
17
|
+
|
|
18
|
+
# Smallest allowed absolute scale factor for affine transformations.
|
|
19
|
+
MIN_ABS_SCALE_FACTOR = 1e-12
|
|
20
|
+
|
|
21
|
+
# Smallest divisor magnitude accepted before force-law clamping kicks in.
|
|
22
|
+
MIN_DIVISION_MAGNITUDE = 1e-15
|