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,154 @@
|
|
|
1
|
+
"""Construction helpers for relaxation geometry bundles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ....backend import RawMesh
|
|
11
|
+
from ....geometry.primitives import Body
|
|
12
|
+
from .._constants import BOUNDARY_FUZZ
|
|
13
|
+
from .._types import DensityFunction, FloatArray, RegionFunction
|
|
14
|
+
from ..density import _compile_density_function
|
|
15
|
+
from .model import FemGeometry
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _raw_mesh_points(raw_mesh: RawMesh, dim: int) -> FloatArray:
|
|
19
|
+
"""Return mesh points as a validated floating-point array."""
|
|
20
|
+
|
|
21
|
+
coords: FloatArray = np.asarray(raw_mesh.points, dtype=np.float64)
|
|
22
|
+
if coords.size == 0:
|
|
23
|
+
return np.empty((0, dim), dtype=float)
|
|
24
|
+
if coords.ndim == 1:
|
|
25
|
+
coords = coords[np.newaxis, :]
|
|
26
|
+
if coords.shape[1] != dim:
|
|
27
|
+
raise ValueError(f"Expected hint mesh points with dimension {dim}, got {coords.shape[1]}")
|
|
28
|
+
return coords
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _dedupe_hint_points(points: FloatArray) -> FloatArray:
|
|
32
|
+
if len(points) == 0:
|
|
33
|
+
return points
|
|
34
|
+
keep_indices: list[int] = []
|
|
35
|
+
seen: set[tuple[float, ...]] = set()
|
|
36
|
+
for index, point in enumerate(points):
|
|
37
|
+
key = tuple(cast(list[float], np.round(np.asarray(point), decimals=10).tolist()))
|
|
38
|
+
if key not in seen:
|
|
39
|
+
seen.add(key)
|
|
40
|
+
keep_indices.append(index)
|
|
41
|
+
return points[np.asarray(keep_indices, dtype=int)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _process_bodies_and_hints(
|
|
45
|
+
bodies: list[Body], hints: Sequence[Sequence[object]], dim: int
|
|
46
|
+
) -> tuple[list[RegionFunction], list[FloatArray], list[int], list[Body | None]]:
|
|
47
|
+
"""Convert bodies and hint meshes into region predicates and hint blocks."""
|
|
48
|
+
|
|
49
|
+
body_functions: list[RegionFunction] = []
|
|
50
|
+
piece_hints: list[FloatArray] = []
|
|
51
|
+
region_ids: list[int] = []
|
|
52
|
+
region_bodies: list[Body | None] = []
|
|
53
|
+
for region_id, body in enumerate(bodies, start=1):
|
|
54
|
+
body_functions.append(
|
|
55
|
+
lambda points, member=body: (
|
|
56
|
+
np.asarray(member.evaluate(points), dtype=float) >= -BOUNDARY_FUZZ
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
piece_hints.append(np.empty((0, dim), dtype=float))
|
|
60
|
+
region_ids.append(region_id)
|
|
61
|
+
region_bodies.append(body)
|
|
62
|
+
next_region_id = len(region_ids) + 1
|
|
63
|
+
for hint in hints:
|
|
64
|
+
if len(hint) != 2:
|
|
65
|
+
raise ValueError("Each mesh hint must contain a mesh and a body.")
|
|
66
|
+
hint_mesh, hint_body = hint
|
|
67
|
+
if not isinstance(hint_mesh, RawMesh) or not isinstance(hint_body, Body):
|
|
68
|
+
raise TypeError("Mesh hints must contain a RawMesh and Body.")
|
|
69
|
+
hint_points = _raw_mesh_points(hint_mesh, dim)
|
|
70
|
+
if len(hint_points) > 0:
|
|
71
|
+
mask = np.asarray(hint_body.evaluate(hint_points), dtype=float) >= -BOUNDARY_FUZZ
|
|
72
|
+
hint_points = _dedupe_hint_points(hint_points[mask])
|
|
73
|
+
body_functions.append(
|
|
74
|
+
lambda points, member=hint_body: (
|
|
75
|
+
np.asarray(member.evaluate(points), dtype=float) >= -BOUNDARY_FUZZ
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
piece_hints.append(hint_points)
|
|
79
|
+
region_ids.append(next_region_id)
|
|
80
|
+
region_bodies.append(hint_body)
|
|
81
|
+
next_region_id += 1
|
|
82
|
+
return body_functions, piece_hints, region_ids, region_bodies
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _build_region_lists(
|
|
86
|
+
bbox_min: FloatArray,
|
|
87
|
+
bbox_max: FloatArray,
|
|
88
|
+
body_functions: list[RegionFunction],
|
|
89
|
+
region_ids: list[int],
|
|
90
|
+
region_bodies: list[Body | None],
|
|
91
|
+
*,
|
|
92
|
+
mesh_exterior: bool,
|
|
93
|
+
) -> tuple[list[RegionFunction], list[int], list[Body | None]]:
|
|
94
|
+
"""Build ordered region lists, including the exterior region when requested."""
|
|
95
|
+
|
|
96
|
+
region_functions: list[RegionFunction] = []
|
|
97
|
+
ordered_region_ids: list[int] = []
|
|
98
|
+
ordered_region_bodies: list[Body | None] = []
|
|
99
|
+
if mesh_exterior:
|
|
100
|
+
|
|
101
|
+
def exterior(points: FloatArray) -> np.ndarray:
|
|
102
|
+
bbox_mask = np.all(
|
|
103
|
+
(points >= bbox_min - BOUNDARY_FUZZ) & (points <= bbox_max + BOUNDARY_FUZZ),
|
|
104
|
+
axis=1,
|
|
105
|
+
)
|
|
106
|
+
inside_any = np.zeros(len(points), dtype=bool)
|
|
107
|
+
for body_fun in body_functions:
|
|
108
|
+
inside_any |= body_fun(points)
|
|
109
|
+
return bbox_mask & ~inside_any
|
|
110
|
+
|
|
111
|
+
region_functions.append(exterior)
|
|
112
|
+
ordered_region_ids.append(0)
|
|
113
|
+
ordered_region_bodies.append(None)
|
|
114
|
+
region_functions.extend(body_functions)
|
|
115
|
+
ordered_region_ids.extend(region_ids)
|
|
116
|
+
ordered_region_bodies.extend(region_bodies)
|
|
117
|
+
return region_functions, ordered_region_ids, ordered_region_bodies
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def fem_geometry_from_bodies(
|
|
121
|
+
bounding_box: tuple[FloatArray, FloatArray],
|
|
122
|
+
bodies: list[Body],
|
|
123
|
+
hints: Sequence[Sequence[object]],
|
|
124
|
+
*,
|
|
125
|
+
density: str | DensityFunction | None = None,
|
|
126
|
+
mesh_exterior: bool = False,
|
|
127
|
+
) -> FemGeometry:
|
|
128
|
+
"""Construct the geometry bundle consumed by the Python meshing engine."""
|
|
129
|
+
|
|
130
|
+
bbox_min: FloatArray = np.asarray(bounding_box[0], dtype=np.float64)
|
|
131
|
+
bbox_max: FloatArray = np.asarray(bounding_box[1], dtype=np.float64)
|
|
132
|
+
dim = int(len(bbox_min))
|
|
133
|
+
body_functions, piece_hints, region_ids, region_bodies = _process_bodies_and_hints(
|
|
134
|
+
bodies, hints, dim
|
|
135
|
+
)
|
|
136
|
+
region_functions, ordered_region_ids, ordered_region_bodies = _build_region_lists(
|
|
137
|
+
bbox_min,
|
|
138
|
+
bbox_max,
|
|
139
|
+
body_functions,
|
|
140
|
+
region_ids,
|
|
141
|
+
region_bodies,
|
|
142
|
+
mesh_exterior=mesh_exterior,
|
|
143
|
+
)
|
|
144
|
+
return FemGeometry(
|
|
145
|
+
dim=dim,
|
|
146
|
+
bbox_min=bbox_min,
|
|
147
|
+
bbox_max=bbox_max,
|
|
148
|
+
density_fun=_compile_density_function(density),
|
|
149
|
+
region_ids=tuple(ordered_region_ids),
|
|
150
|
+
region_functions=tuple(region_functions),
|
|
151
|
+
region_bodies=tuple(ordered_region_bodies),
|
|
152
|
+
piece_hints=tuple(piece_hints),
|
|
153
|
+
mesh_exterior=bool(mesh_exterior),
|
|
154
|
+
)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Runtime geometry queries for the relaxation meshing pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ....geometry.primitives import Body
|
|
11
|
+
from .._constants import BOUNDARY_FUZZ, DENSITY_EPSILON
|
|
12
|
+
from .._types import DensityFunction, FloatArray, RegionFunction
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class FemGeometry:
|
|
17
|
+
"""Geometry bundle used by the Python meshing engine."""
|
|
18
|
+
|
|
19
|
+
dim: int
|
|
20
|
+
bbox_min: FloatArray
|
|
21
|
+
bbox_max: FloatArray
|
|
22
|
+
density_fun: DensityFunction
|
|
23
|
+
region_ids: tuple[int, ...]
|
|
24
|
+
region_functions: tuple[RegionFunction, ...]
|
|
25
|
+
region_bodies: tuple[Body | None, ...]
|
|
26
|
+
piece_hints: tuple[FloatArray, ...]
|
|
27
|
+
mesh_exterior: bool
|
|
28
|
+
|
|
29
|
+
def points_in_bbox(self, points: FloatArray) -> np.ndarray:
|
|
30
|
+
"""Return a mask showing which points lie inside the bounding box."""
|
|
31
|
+
|
|
32
|
+
return np.all(
|
|
33
|
+
(points >= self.bbox_min - BOUNDARY_FUZZ) & (points <= self.bbox_max + BOUNDARY_FUZZ),
|
|
34
|
+
axis=1,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def classify_points(self, points: FloatArray) -> np.ndarray:
|
|
38
|
+
"""Return the first matching region id for each point, or -1 outside."""
|
|
39
|
+
|
|
40
|
+
if points.size == 0:
|
|
41
|
+
return np.empty(0, dtype=int)
|
|
42
|
+
coords = np.asarray(points, dtype=float)
|
|
43
|
+
assigned = np.full(len(coords), -1, dtype=int)
|
|
44
|
+
bbox_mask = self.points_in_bbox(coords)
|
|
45
|
+
for region_id, region_fun in zip(self.region_ids, self.region_functions, strict=True):
|
|
46
|
+
mask = bbox_mask & region_fun(coords) & (assigned < 0)
|
|
47
|
+
assigned[mask] = region_id
|
|
48
|
+
return assigned
|
|
49
|
+
|
|
50
|
+
def density_at(self, point: FloatArray) -> float:
|
|
51
|
+
"""Evaluate the density function with a small positive lower bound."""
|
|
52
|
+
|
|
53
|
+
return max(float(self.density_fun(point)), DENSITY_EPSILON)
|
|
54
|
+
|
|
55
|
+
def boundary_mask(self, points: FloatArray, tolerance: float) -> np.ndarray:
|
|
56
|
+
"""Return a mask for points that lie close to a region or box boundary."""
|
|
57
|
+
|
|
58
|
+
if points.size == 0:
|
|
59
|
+
return np.empty(0, dtype=bool)
|
|
60
|
+
coords = np.asarray(points, dtype=float)
|
|
61
|
+
mask = np.zeros(len(coords), dtype=bool)
|
|
62
|
+
for body in self.region_bodies:
|
|
63
|
+
if body is not None:
|
|
64
|
+
mask |= np.abs(np.asarray(body.evaluate(coords), dtype=float)) <= tolerance
|
|
65
|
+
if self.mesh_exterior:
|
|
66
|
+
mask |= np.any(
|
|
67
|
+
np.isclose(coords, self.bbox_min, atol=tolerance, rtol=0.0)
|
|
68
|
+
| np.isclose(coords, self.bbox_max, atol=tolerance, rtol=0.0),
|
|
69
|
+
axis=1,
|
|
70
|
+
)
|
|
71
|
+
return mask & self.points_in_bbox(coords)
|
|
72
|
+
|
|
73
|
+
def boundary_distance(self, point: FloatArray) -> float:
|
|
74
|
+
"""Return the smallest available distance-like scalar to a boundary."""
|
|
75
|
+
|
|
76
|
+
coord = np.asarray(point, dtype=float)
|
|
77
|
+
distances = [
|
|
78
|
+
abs(float(body.evaluate(coord))) for body in self.region_bodies if body is not None
|
|
79
|
+
]
|
|
80
|
+
if self.mesh_exterior:
|
|
81
|
+
distances.extend(
|
|
82
|
+
min(
|
|
83
|
+
abs(float(coord[axis] - self.bbox_min[axis])),
|
|
84
|
+
abs(float(coord[axis] - self.bbox_max[axis])),
|
|
85
|
+
)
|
|
86
|
+
for axis in range(self.dim)
|
|
87
|
+
)
|
|
88
|
+
return min(distances) if distances else math.inf
|
|
89
|
+
|
|
90
|
+
def boundary_normal(self, point: FloatArray) -> FloatArray:
|
|
91
|
+
"""Estimate a unit normal for the closest relevant boundary."""
|
|
92
|
+
|
|
93
|
+
coord = np.asarray(point, dtype=float)
|
|
94
|
+
best_distance = math.inf
|
|
95
|
+
best_normal = np.zeros(self.dim, dtype=float)
|
|
96
|
+
for body in self.region_bodies:
|
|
97
|
+
if body is None:
|
|
98
|
+
continue
|
|
99
|
+
distance = abs(float(body.evaluate(coord)))
|
|
100
|
+
if distance > best_distance:
|
|
101
|
+
continue
|
|
102
|
+
gradient = self.boundary_gradient(coord, body)
|
|
103
|
+
norm = float(np.linalg.norm(gradient))
|
|
104
|
+
if norm > DENSITY_EPSILON:
|
|
105
|
+
best_distance = distance
|
|
106
|
+
best_normal = gradient / norm
|
|
107
|
+
if self.mesh_exterior:
|
|
108
|
+
for axis in range(self.dim):
|
|
109
|
+
dist_min = abs(float(coord[axis] - self.bbox_min[axis]))
|
|
110
|
+
if dist_min < best_distance:
|
|
111
|
+
best_distance = dist_min
|
|
112
|
+
best_normal = np.zeros(self.dim, dtype=float)
|
|
113
|
+
best_normal[axis] = 1.0
|
|
114
|
+
dist_max = abs(float(coord[axis] - self.bbox_max[axis]))
|
|
115
|
+
if dist_max < best_distance:
|
|
116
|
+
best_distance = dist_max
|
|
117
|
+
best_normal = np.zeros(self.dim, dtype=float)
|
|
118
|
+
best_normal[axis] = -1.0
|
|
119
|
+
return best_normal
|
|
120
|
+
|
|
121
|
+
def boundary_gradient(self, point: FloatArray, body: Body) -> FloatArray:
|
|
122
|
+
"""Estimate the gradient of one implicit body at a point."""
|
|
123
|
+
|
|
124
|
+
coord = np.asarray(point, dtype=float)
|
|
125
|
+
gradient = np.zeros(self.dim, dtype=float)
|
|
126
|
+
extent = max(float(np.max(self.bbox_max - self.bbox_min)), 1.0)
|
|
127
|
+
epsilon = max(BOUNDARY_FUZZ * 10.0, extent * 1.0e-6)
|
|
128
|
+
for axis in range(self.dim):
|
|
129
|
+
offset = np.zeros(self.dim, dtype=float)
|
|
130
|
+
offset[axis] = epsilon
|
|
131
|
+
gradient[axis] = (
|
|
132
|
+
float(body.evaluate(coord + offset)) - float(body.evaluate(coord - offset))
|
|
133
|
+
) / (2.0 * epsilon)
|
|
134
|
+
return gradient
|
|
135
|
+
|
|
136
|
+
def project_point_to_boundary_from_inside(
|
|
137
|
+
self, point: FloatArray, *, acceptable_fuzz: float, max_steps: int
|
|
138
|
+
) -> FloatArray:
|
|
139
|
+
"""Project an interior point onto implicit boundaries using the legacy correction."""
|
|
140
|
+
|
|
141
|
+
coords: FloatArray = np.array(point, dtype=np.float64, copy=True)
|
|
142
|
+
bodies = [body for body in self.region_bodies if body is not None]
|
|
143
|
+
if not bodies:
|
|
144
|
+
return self._project_point_to_box_boundary(coords)
|
|
145
|
+
for _ in range(max(max_steps, 0)):
|
|
146
|
+
violated_body: Body | None = None
|
|
147
|
+
violated_value = 0.0
|
|
148
|
+
for body in bodies:
|
|
149
|
+
value = float(body.evaluate(coords))
|
|
150
|
+
if value > acceptable_fuzz:
|
|
151
|
+
violated_body, violated_value = body, value
|
|
152
|
+
break
|
|
153
|
+
if violated_body is None:
|
|
154
|
+
return coords
|
|
155
|
+
gradient = self.boundary_gradient(coords, violated_body)
|
|
156
|
+
gradient_norm_sq = float(np.dot(gradient, gradient))
|
|
157
|
+
scale = (
|
|
158
|
+
1.0e-6
|
|
159
|
+
if gradient_norm_sq <= DENSITY_EPSILON
|
|
160
|
+
else -violated_value / gradient_norm_sq
|
|
161
|
+
)
|
|
162
|
+
coords = coords + scale * gradient
|
|
163
|
+
return coords
|
|
164
|
+
|
|
165
|
+
def _project_point_to_box_boundary(self, point: FloatArray) -> FloatArray:
|
|
166
|
+
coords: FloatArray = np.array(point, dtype=np.float64, copy=True)
|
|
167
|
+
distances_min = np.abs(coords - self.bbox_min)
|
|
168
|
+
distances_max = np.abs(coords - self.bbox_max)
|
|
169
|
+
min_axis = int(np.argmin(distances_min))
|
|
170
|
+
max_axis = int(np.argmin(distances_max))
|
|
171
|
+
if distances_min[min_axis] <= distances_max[max_axis]:
|
|
172
|
+
coords[min_axis] = self.bbox_min[min_axis]
|
|
173
|
+
else:
|
|
174
|
+
coords[max_axis] = self.bbox_max[max_axis]
|
|
175
|
+
return coords
|
|
176
|
+
|
|
177
|
+
def project_segment_to_domain(
|
|
178
|
+
self, start: FloatArray, end: FloatArray, *, iterations: int = 18
|
|
179
|
+
) -> FloatArray:
|
|
180
|
+
"""Project an outside point back into the domain along a segment."""
|
|
181
|
+
|
|
182
|
+
lower: FloatArray = np.array(start, dtype=np.float64, copy=True)
|
|
183
|
+
upper: FloatArray = np.array(end, dtype=np.float64, copy=True)
|
|
184
|
+
if self.classify_points(lower[np.newaxis, :])[0] < 0:
|
|
185
|
+
return lower
|
|
186
|
+
if self.classify_points(upper[np.newaxis, :])[0] >= 0:
|
|
187
|
+
return upper
|
|
188
|
+
for _ in range(iterations):
|
|
189
|
+
middle = 0.5 * (lower + upper)
|
|
190
|
+
if self.classify_points(middle[np.newaxis, :])[0] >= 0:
|
|
191
|
+
lower = middle
|
|
192
|
+
else:
|
|
193
|
+
upper = middle
|
|
194
|
+
return lower
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Compatibility exports and seed-point assembly for relaxation meshing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .._constants import STATE_FIXED, STATE_SIMPLE
|
|
10
|
+
from .._types import FloatArray
|
|
11
|
+
from ..geometry import FemGeometry
|
|
12
|
+
from .periodic import _apply_periodic_fixed_states, _periodic_outer_box_points
|
|
13
|
+
from .points import _as_float_array as _as_float_array # noqa: F401
|
|
14
|
+
from .points import _classify_dynamic_states as _classify_dynamic_states # noqa: F401
|
|
15
|
+
from .points import _dedupe_fixed_mobile, _filter_relevant_points # noqa: F401
|
|
16
|
+
from .points import _dedupe_points as _dedupe_points
|
|
17
|
+
from .sampling import _collect_hint_points, _select_generated_points
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _prepare_initial_points(
|
|
21
|
+
geometry: FemGeometry,
|
|
22
|
+
a0: float,
|
|
23
|
+
fixed_points: FloatArray,
|
|
24
|
+
mobile_points: FloatArray,
|
|
25
|
+
simply_points: FloatArray,
|
|
26
|
+
periodic: list[float] | list[bool],
|
|
27
|
+
rng: np.random.Generator,
|
|
28
|
+
params: dict[str, Any] | None = None,
|
|
29
|
+
) -> tuple[FloatArray, np.ndarray]:
|
|
30
|
+
"""Prepare the initial point cloud and point-state array for relaxation."""
|
|
31
|
+
|
|
32
|
+
params = {} if params is None else params
|
|
33
|
+
fixed_points, mobile_points, simply_points = _dedupe_fixed_mobile(
|
|
34
|
+
fixed_points, mobile_points, simply_points
|
|
35
|
+
)
|
|
36
|
+
fixed_points = _filter_relevant_points(geometry, fixed_points)
|
|
37
|
+
mobile_points = _filter_relevant_points(geometry, mobile_points)
|
|
38
|
+
simply_points = _filter_relevant_points(geometry, simply_points)
|
|
39
|
+
|
|
40
|
+
if len(simply_points) > 0:
|
|
41
|
+
states = np.full(len(simply_points), STATE_SIMPLE, dtype=int)
|
|
42
|
+
_apply_periodic_fixed_states(states, simply_points, geometry, periodic, a0)
|
|
43
|
+
return simply_points, states
|
|
44
|
+
|
|
45
|
+
generated_points = (
|
|
46
|
+
_select_generated_points(
|
|
47
|
+
geometry,
|
|
48
|
+
a0,
|
|
49
|
+
fixed_points,
|
|
50
|
+
mobile_points,
|
|
51
|
+
simply_points,
|
|
52
|
+
rng,
|
|
53
|
+
params,
|
|
54
|
+
)
|
|
55
|
+
if len(mobile_points) == 0
|
|
56
|
+
else np.empty((0, geometry.dim), dtype=float)
|
|
57
|
+
)
|
|
58
|
+
hint_block = _collect_hint_points(geometry)
|
|
59
|
+
periodic_block = _periodic_outer_box_points(geometry, a0, periodic)
|
|
60
|
+
all_points = np.vstack(
|
|
61
|
+
(fixed_points, simply_points, mobile_points, hint_block, periodic_block, generated_points)
|
|
62
|
+
)
|
|
63
|
+
states = np.concatenate(
|
|
64
|
+
(
|
|
65
|
+
np.full(len(fixed_points), STATE_FIXED, dtype=int),
|
|
66
|
+
np.full(len(simply_points), STATE_SIMPLE, dtype=int),
|
|
67
|
+
_classify_dynamic_states(geometry, mobile_points, a0),
|
|
68
|
+
np.full(len(hint_block), STATE_FIXED, dtype=int),
|
|
69
|
+
np.full(len(periodic_block), STATE_FIXED, dtype=int),
|
|
70
|
+
_classify_dynamic_states(geometry, generated_points, a0),
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
_apply_periodic_fixed_states(states, all_points, geometry, periodic, a0)
|
|
74
|
+
return all_points, states
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Periodic-boundary seed-point and state helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from itertools import product
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .._constants import BOUNDARY_FUZZ, DENSITY_EPSILON, STATE_FIXED
|
|
11
|
+
from .._types import FloatArray
|
|
12
|
+
from ..geometry import FemGeometry
|
|
13
|
+
from .points import _dedupe_points, _filter_relevant_points
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _periodic_outer_box_points(
|
|
17
|
+
geometry: FemGeometry, a0: float, periodic: list[float] | list[bool]
|
|
18
|
+
) -> FloatArray:
|
|
19
|
+
"""Create paired fixed seed points on periodic outer-box faces."""
|
|
20
|
+
|
|
21
|
+
periodic_flags = [bool(value) for value in periodic]
|
|
22
|
+
if not any(periodic_flags):
|
|
23
|
+
return np.empty((0, geometry.dim), dtype=float)
|
|
24
|
+
points: list[FloatArray] = []
|
|
25
|
+
spacing = max(a0, DENSITY_EPSILON)
|
|
26
|
+
for periodic_axis, enabled in enumerate(periodic_flags):
|
|
27
|
+
if not enabled:
|
|
28
|
+
continue
|
|
29
|
+
points.extend(_periodic_face_points(geometry, periodic_axis, spacing))
|
|
30
|
+
if not points:
|
|
31
|
+
return np.empty((0, geometry.dim), dtype=float)
|
|
32
|
+
return _filter_relevant_points(geometry, _dedupe_points(np.asarray(points, dtype=float)))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _periodic_face_points(
|
|
36
|
+
geometry: FemGeometry,
|
|
37
|
+
periodic_axis: int,
|
|
38
|
+
spacing: float,
|
|
39
|
+
) -> list[FloatArray]:
|
|
40
|
+
"""Return matching points on the two periodic faces of one axis."""
|
|
41
|
+
|
|
42
|
+
other_axes = [axis for axis in range(geometry.dim) if axis != periodic_axis]
|
|
43
|
+
face_axes: list[FloatArray] = []
|
|
44
|
+
for axis in other_axes:
|
|
45
|
+
extent = float(geometry.bbox_max[axis] - geometry.bbox_min[axis])
|
|
46
|
+
count = max(2, int(math.floor(extent / spacing)) + 1)
|
|
47
|
+
face_axes.append(
|
|
48
|
+
np.linspace(
|
|
49
|
+
float(geometry.bbox_min[axis]),
|
|
50
|
+
float(geometry.bbox_max[axis]),
|
|
51
|
+
count,
|
|
52
|
+
dtype=float,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
points: list[FloatArray] = []
|
|
57
|
+
for coordinates in product(*(axis.tolist() for axis in face_axes)):
|
|
58
|
+
for side in (geometry.bbox_min[periodic_axis], geometry.bbox_max[periodic_axis]):
|
|
59
|
+
point = np.zeros(geometry.dim, dtype=float)
|
|
60
|
+
point[periodic_axis] = side
|
|
61
|
+
for axis, value in zip(other_axes, coordinates, strict=True):
|
|
62
|
+
point[axis] = value
|
|
63
|
+
points.append(point)
|
|
64
|
+
return points
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _apply_periodic_fixed_states(
|
|
68
|
+
states: np.ndarray,
|
|
69
|
+
all_points: FloatArray,
|
|
70
|
+
geometry: FemGeometry,
|
|
71
|
+
periodic: list[float] | list[bool],
|
|
72
|
+
a0: float,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Mark points on periodic boundaries as fixed."""
|
|
75
|
+
|
|
76
|
+
_ = a0
|
|
77
|
+
if len(all_points) == 0:
|
|
78
|
+
return
|
|
79
|
+
periodic_mask = np.zeros(len(all_points), dtype=bool)
|
|
80
|
+
for axis, enabled in enumerate(map(bool, periodic)):
|
|
81
|
+
if enabled:
|
|
82
|
+
periodic_mask |= np.isclose(
|
|
83
|
+
all_points[:, axis], geometry.bbox_min[axis], atol=BOUNDARY_FUZZ, rtol=0.0
|
|
84
|
+
)
|
|
85
|
+
periodic_mask |= np.isclose(
|
|
86
|
+
all_points[:, axis], geometry.bbox_max[axis], atol=BOUNDARY_FUZZ, rtol=0.0
|
|
87
|
+
)
|
|
88
|
+
states[periodic_mask] = STATE_FIXED
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Point normalization and state classification for relaxation seeding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .._constants import BOUNDARY_FUZZ, STATE_BOUNDARY, STATE_MOBILE
|
|
10
|
+
from .._types import FloatArray
|
|
11
|
+
from ..geometry import FemGeometry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _as_float_array(points: Any, dim: int | None = None) -> FloatArray:
|
|
15
|
+
"""Convert point-like input into a 2D float array with optional dimension validation."""
|
|
16
|
+
|
|
17
|
+
if points is None:
|
|
18
|
+
return np.empty((0, 0 if dim is None else dim), dtype=float)
|
|
19
|
+
coords = np.asarray(points, dtype=float)
|
|
20
|
+
if coords.size == 0:
|
|
21
|
+
return np.empty((0, 0 if dim is None else dim), dtype=float)
|
|
22
|
+
if coords.ndim == 1:
|
|
23
|
+
dim = int(coords.shape[0]) if dim is None else dim
|
|
24
|
+
coords = coords[np.newaxis, :]
|
|
25
|
+
if dim is not None and coords.shape[1] != dim:
|
|
26
|
+
raise ValueError(f"Expected points with dimension {dim}, got {coords.shape[1]}")
|
|
27
|
+
return coords.astype(float, copy=False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _point_key(point: FloatArray, decimals: int = 10) -> tuple[float, ...]:
|
|
31
|
+
"""Return a rounded tuple key for deduplication of point coordinates."""
|
|
32
|
+
|
|
33
|
+
return tuple(np.round(np.asarray(point, dtype=float), decimals=decimals).tolist())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _dedupe_points(points: FloatArray) -> FloatArray:
|
|
37
|
+
"""Drop duplicate points while preserving first-seen order."""
|
|
38
|
+
|
|
39
|
+
if len(points) == 0:
|
|
40
|
+
return points
|
|
41
|
+
keep_indices: list[int] = []
|
|
42
|
+
seen: set[tuple[float, ...]] = set()
|
|
43
|
+
for index, point in enumerate(points):
|
|
44
|
+
key = _point_key(point)
|
|
45
|
+
if key not in seen:
|
|
46
|
+
seen.add(key)
|
|
47
|
+
keep_indices.append(index)
|
|
48
|
+
return points[np.asarray(keep_indices, dtype=int)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _dedupe_fixed_mobile(
|
|
52
|
+
fixed_points: FloatArray,
|
|
53
|
+
mobile_points: FloatArray,
|
|
54
|
+
simply_points: FloatArray,
|
|
55
|
+
) -> tuple[FloatArray, FloatArray, FloatArray]:
|
|
56
|
+
"""Deduplicate seed points across fixed, mobile, and simply categories."""
|
|
57
|
+
|
|
58
|
+
seen: set[tuple[float, ...]] = set()
|
|
59
|
+
|
|
60
|
+
def filter_points(points: FloatArray) -> FloatArray:
|
|
61
|
+
keep_indices: list[int] = []
|
|
62
|
+
for index, point in enumerate(points):
|
|
63
|
+
key = _point_key(point)
|
|
64
|
+
if key not in seen:
|
|
65
|
+
seen.add(key)
|
|
66
|
+
keep_indices.append(index)
|
|
67
|
+
if not keep_indices:
|
|
68
|
+
return np.empty((0, points.shape[1]), dtype=float)
|
|
69
|
+
return points[np.asarray(keep_indices, dtype=int)]
|
|
70
|
+
|
|
71
|
+
return tuple(map(filter_points, (fixed_points, mobile_points, simply_points))) # type: ignore[return-value]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _classify_dynamic_states(geometry: FemGeometry, points: FloatArray, a0: float) -> np.ndarray:
|
|
75
|
+
"""Return mobile or boundary states for the supplied movable points."""
|
|
76
|
+
|
|
77
|
+
_ = a0
|
|
78
|
+
if len(points) == 0:
|
|
79
|
+
return np.empty(0, dtype=int)
|
|
80
|
+
states = np.full(len(points), STATE_MOBILE, dtype=int)
|
|
81
|
+
states[geometry.boundary_mask(points, tolerance=BOUNDARY_FUZZ)] = STATE_BOUNDARY
|
|
82
|
+
return states
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _filter_relevant_points(geometry: FemGeometry, points: FloatArray) -> FloatArray:
|
|
86
|
+
"""Keep only points that belong to one of the meshed regions."""
|
|
87
|
+
|
|
88
|
+
return points if len(points) == 0 else points[geometry.classify_points(points) >= 0]
|