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,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]