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
nmesh/backend.py ADDED
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from collections.abc import Callable, Sequence
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, Any, Protocol, TypeAlias, runtime_checkable
7
+
8
+ if TYPE_CHECKING:
9
+ from .geometry import Body
10
+
11
+ Point: TypeAlias = list[float]
12
+ Simplex: TypeAlias = list[int]
13
+ Surface: TypeAlias = list[int]
14
+ BodyHandle: TypeAlias = object
15
+ MesherDriver: TypeAlias = Callable[..., object]
16
+ MesherConfig: TypeAlias = dict[str, Any]
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class RawMesh:
21
+ points: list[Point] = field(default_factory=list[Point])
22
+ simplices: list[Simplex] = field(default_factory=list[Simplex])
23
+ regions: list[int] = field(default_factory=list[int])
24
+ point_regions: list[list[int]] = field(default_factory=list[list[int]])
25
+ surfaces: list[Surface] = field(default_factory=list[Surface])
26
+ links: list[tuple[int, int]] = field(default_factory=list[tuple[int, int]])
27
+ region_volumes: list[float] = field(default_factory=list[float])
28
+ periodic_point_indices: list[list[int]] = field(default_factory=list[list[int]])
29
+ permutation: list[int] = field(default_factory=list[int])
30
+ dim: int = 3
31
+
32
+
33
+ @runtime_checkable
34
+ class MeshBackendProtocol(Protocol):
35
+ def mesh_scale_node_positions(self, raw_mesh: RawMesh, scale: float) -> None: ...
36
+ def mesh_writefile(self, path: str, raw_mesh: RawMesh) -> None: ...
37
+ def mesh_nr_simplices(self, raw_mesh: RawMesh) -> int: ...
38
+ def mesh_nr_points(self, raw_mesh: RawMesh) -> int: ...
39
+ def mesh_plotinfo(self, raw_mesh: RawMesh) -> list[object]: ...
40
+ def mesh_plotinfo_points(self, raw_mesh: RawMesh) -> list[list[float]]: ...
41
+ def mesh_plotinfo_pointsregions(self, raw_mesh: RawMesh) -> list[list[int]]: ...
42
+ def mesh_plotinfo_simplices(self, raw_mesh: RawMesh) -> list[list[int]]: ...
43
+ def mesh_plotinfo_simplicesregions(self, raw_mesh: RawMesh) -> list[int]: ...
44
+ def mesh_plotinfo_surfaces_and_surfacesregions(
45
+ self, raw_mesh: RawMesh
46
+ ) -> tuple[list[Surface], list[int]]: ...
47
+ def mesh_plotinfo_links(self, raw_mesh: RawMesh) -> list[tuple[int, int]]: ...
48
+ def mesh_dim(self, raw_mesh: RawMesh) -> int: ...
49
+ def mesh_plotinfo_regionvolumes(self, raw_mesh: RawMesh) -> list[float]: ...
50
+ def mesh_plotinfo_periodic_points_indices(self, raw_mesh: RawMesh) -> list[list[int]]: ...
51
+ def mesh_set_vertex_distribution(self, raw_mesh: RawMesh, dist: object) -> None: ...
52
+ def mesh_get_permutation(self, raw_mesh: RawMesh) -> list[int]: ...
53
+ def mesh_readfile(self, filename: str, do_reorder: bool, do_distribute: bool) -> RawMesh: ...
54
+ def copy_mesher_defaults(self, defaults: MesherConfig) -> MesherConfig: ...
55
+ def mesh_bodies_raw(
56
+ self,
57
+ driver: MesherDriver,
58
+ mesher: MesherConfig,
59
+ bb_min: Point,
60
+ bb_max: Point,
61
+ mesh_ext: int,
62
+ objects: list[Body],
63
+ a0: float,
64
+ density: str,
65
+ fixed: list[Point],
66
+ mobile: list[Point],
67
+ simply: list[Point],
68
+ periodic: list[float],
69
+ cache: str,
70
+ hints: Sequence[Sequence[object]],
71
+ ) -> RawMesh: ...
72
+ def mesh_from_points_and_simplices(
73
+ self,
74
+ dim: int,
75
+ points: list[list[float]],
76
+ simplices: list[list[int]],
77
+ regions: list[int],
78
+ periodic: list[list[int]],
79
+ reorder: bool,
80
+ distribute: bool,
81
+ ) -> RawMesh: ...
82
+ def body_union(self, objs: Sequence[BodyHandle]) -> BodyHandle: ...
83
+ def body_difference(self, obj1: BodyHandle, objs: Sequence[BodyHandle]) -> BodyHandle: ...
84
+ def body_intersection(self, objs: Sequence[BodyHandle]) -> BodyHandle: ...
85
+ def body_shifted_sc(self, obj: BodyHandle, shift: Sequence[float]) -> BodyHandle: ...
86
+ def body_shifted_bc(self, obj: BodyHandle, shift: Sequence[float]) -> BodyHandle: ...
87
+ def body_scaled(self, obj: BodyHandle, scale: Sequence[float]) -> BodyHandle: ...
88
+ def body_rotated_sc(self, obj: BodyHandle, a1: int, a2: int, ang: float) -> BodyHandle: ...
89
+ def body_rotated_bc(self, obj: BodyHandle, a1: int, a2: int, ang: float) -> BodyHandle: ...
90
+ def body_rotated_axis_sc(
91
+ self, obj: BodyHandle, axis: Sequence[float], ang: float
92
+ ) -> BodyHandle: ...
93
+ def body_rotated_axis_bc(
94
+ self, obj: BodyHandle, axis: Sequence[float], ang: float
95
+ ) -> BodyHandle: ...
96
+ def body_box(self, p1: Sequence[float], p2: Sequence[float]) -> BodyHandle: ...
97
+ def body_ellipsoid(self, length: Sequence[float]) -> BodyHandle: ...
98
+ def body_frustum(
99
+ self, c1: Sequence[float], r1: float, c2: Sequence[float], r2: float
100
+ ) -> BodyHandle: ...
101
+ def body_helix(
102
+ self, c1: Sequence[float], r1: float, c2: Sequence[float], r2: float
103
+ ) -> BodyHandle: ...
104
+
105
+ @property
106
+ def mesher_defaults(self) -> MesherConfig: ...
107
+
108
+
109
+ class StubMeshBackend:
110
+ """Lightweight in-memory backend used until the Python mesher is complete."""
111
+
112
+ def mesh_scale_node_positions(self, raw_mesh: RawMesh, scale: float) -> None:
113
+ for point in raw_mesh.points:
114
+ for index, value in enumerate(point):
115
+ point[index] = value * scale
116
+ volume_scale = abs(scale) ** raw_mesh.dim
117
+ raw_mesh.region_volumes = [volume * volume_scale for volume in raw_mesh.region_volumes]
118
+
119
+ def mesh_writefile(self, path: str, raw_mesh: RawMesh) -> None:
120
+ return None
121
+
122
+ def mesh_nr_simplices(self, raw_mesh: RawMesh) -> int:
123
+ return len(raw_mesh.simplices)
124
+
125
+ def mesh_nr_points(self, raw_mesh: RawMesh) -> int:
126
+ return len(raw_mesh.points)
127
+
128
+ def mesh_plotinfo(self, raw_mesh: RawMesh) -> list[object]:
129
+ return [
130
+ raw_mesh.points,
131
+ raw_mesh.links,
132
+ [raw_mesh.simplices, raw_mesh.point_regions, raw_mesh.regions],
133
+ ]
134
+
135
+ def mesh_plotinfo_points(self, raw_mesh: RawMesh) -> list[Point]:
136
+ return raw_mesh.points
137
+
138
+ def mesh_plotinfo_pointsregions(self, raw_mesh: RawMesh) -> list[list[int]]:
139
+ return raw_mesh.point_regions
140
+
141
+ def mesh_plotinfo_simplices(self, raw_mesh: RawMesh) -> list[Simplex]:
142
+ return raw_mesh.simplices
143
+
144
+ def mesh_plotinfo_simplicesregions(self, raw_mesh: RawMesh) -> list[int]:
145
+ return raw_mesh.regions
146
+
147
+ def mesh_plotinfo_surfaces_and_surfacesregions(
148
+ self, raw_mesh: RawMesh
149
+ ) -> tuple[list[Surface], list[int]]:
150
+ return raw_mesh.surfaces, []
151
+
152
+ def mesh_plotinfo_links(self, raw_mesh: RawMesh) -> list[tuple[int, int]]:
153
+ return raw_mesh.links
154
+
155
+ def mesh_dim(self, raw_mesh: RawMesh) -> int:
156
+ if raw_mesh.points:
157
+ return len(raw_mesh.points[0])
158
+ return raw_mesh.dim
159
+
160
+ def mesh_plotinfo_regionvolumes(self, raw_mesh: RawMesh) -> list[float]:
161
+ return raw_mesh.region_volumes
162
+
163
+ def mesh_plotinfo_periodic_points_indices(self, raw_mesh: RawMesh) -> list[list[int]]:
164
+ return raw_mesh.periodic_point_indices
165
+
166
+ def mesh_set_vertex_distribution(self, raw_mesh: RawMesh, dist: object) -> None:
167
+ raise NotImplementedError("Manual vertex distribution is not implemented.")
168
+
169
+ def mesh_get_permutation(self, raw_mesh: RawMesh) -> list[int]:
170
+ return raw_mesh.permutation
171
+
172
+ def mesh_readfile(self, filename: str, do_reorder: bool, do_distribute: bool) -> RawMesh:
173
+ return RawMesh()
174
+
175
+ def copy_mesher_defaults(self, defaults: MesherConfig) -> MesherConfig:
176
+ return copy.deepcopy(defaults)
177
+
178
+ def mesh_bodies_raw(
179
+ self,
180
+ driver: MesherDriver,
181
+ mesher: MesherConfig,
182
+ bb_min: Point,
183
+ bb_max: Point,
184
+ mesh_ext: int,
185
+ objects: list[Body],
186
+ a0: float,
187
+ density: str,
188
+ fixed: list[Point],
189
+ mobile: list[Point],
190
+ simply: list[Point],
191
+ periodic: list[float],
192
+ cache: str,
193
+ hints: Sequence[Sequence[object]],
194
+ ) -> RawMesh:
195
+ from .mesher.relaxation import mesh_bodies_raw as python_mesh_bodies_raw
196
+
197
+ return python_mesh_bodies_raw(
198
+ driver,
199
+ mesher,
200
+ bb_min,
201
+ bb_max,
202
+ mesh_ext,
203
+ objects,
204
+ a0,
205
+ density,
206
+ fixed,
207
+ mobile,
208
+ simply,
209
+ periodic,
210
+ [list(hint) for hint in hints],
211
+ )
212
+
213
+ def mesh_from_points_and_simplices(
214
+ self,
215
+ dim: int,
216
+ points: list[Point],
217
+ simplices: list[Simplex],
218
+ regions: list[int],
219
+ periodic: list[list[int]],
220
+ reorder: bool,
221
+ distribute: bool,
222
+ ) -> RawMesh:
223
+ return RawMesh(
224
+ points=points,
225
+ simplices=simplices,
226
+ regions=regions,
227
+ dim=dim,
228
+ periodic_point_indices=periodic,
229
+ )
230
+
231
+ def body_union(self, objs: Sequence[BodyHandle]) -> BodyHandle:
232
+ return ("union", objs)
233
+
234
+ def body_difference(self, obj1: BodyHandle, objs: Sequence[BodyHandle]) -> BodyHandle:
235
+ return ("difference", obj1, objs)
236
+
237
+ def body_intersection(self, objs: Sequence[BodyHandle]) -> BodyHandle:
238
+ return ("intersection", objs)
239
+
240
+ def body_shifted_sc(self, obj: BodyHandle, shift: Sequence[float]) -> BodyHandle:
241
+ return ("shifted_sc", obj, shift)
242
+
243
+ def body_shifted_bc(self, obj: BodyHandle, shift: Sequence[float]) -> BodyHandle:
244
+ return ("shifted_bc", obj, shift)
245
+
246
+ def body_scaled(self, obj: BodyHandle, scale: Sequence[float]) -> BodyHandle:
247
+ return ("scaled", obj, scale)
248
+
249
+ def body_rotated_sc(self, obj: BodyHandle, a1: int, a2: int, ang: float) -> BodyHandle:
250
+ return ("rotated_sc", obj, a1, a2, ang)
251
+
252
+ def body_rotated_bc(self, obj: BodyHandle, a1: int, a2: int, ang: float) -> BodyHandle:
253
+ return ("rotated_bc", obj, a1, a2, ang)
254
+
255
+ def body_rotated_axis_sc(
256
+ self, obj: BodyHandle, axis: Sequence[float], ang: float
257
+ ) -> BodyHandle:
258
+ return ("rotated_axis_sc", obj, axis, ang)
259
+
260
+ def body_rotated_axis_bc(
261
+ self, obj: BodyHandle, axis: Sequence[float], ang: float
262
+ ) -> BodyHandle:
263
+ return ("rotated_axis_bc", obj, axis, ang)
264
+
265
+ def body_box(self, p1: Sequence[float], p2: Sequence[float]) -> BodyHandle:
266
+ return ("box", p1, p2)
267
+
268
+ def body_ellipsoid(self, length: Sequence[float]) -> BodyHandle:
269
+ return ("ellipsoid", length)
270
+
271
+ def body_frustum(
272
+ self, c1: Sequence[float], r1: float, c2: Sequence[float], r2: float
273
+ ) -> BodyHandle:
274
+ return ("frustum", c1, r1, c2, r2)
275
+
276
+ def body_helix(
277
+ self, c1: Sequence[float], r1: float, c2: Sequence[float], r2: float
278
+ ) -> BodyHandle:
279
+ return ("helix", c1, r1, c2, r2)
280
+
281
+ @property
282
+ def mesher_defaults(self) -> MesherConfig:
283
+ return {"parameters": {}}
284
+
285
+
286
+ backend: MeshBackendProtocol = StubMeshBackend()
@@ -0,0 +1,52 @@
1
+ """
2
+ Geometry and CSG operations for the nmesh meshing library.
3
+
4
+ This package provides pure-Python geometric primitives and boolean operations.
5
+
6
+ Modules:
7
+ primitives: Basic geometric shapes (Box, Ellipsoid, Conic, Helix)
8
+ boolean_operations: CSG operations (union, difference, intersect)
9
+ transform: Affine transformations (shift, scale, rotate)
10
+ """
11
+
12
+ from .boolean_operations import difference, intersect, union
13
+ from .primitives import (
14
+ Body,
15
+ Box,
16
+ Conic,
17
+ Ellipsoid,
18
+ Helix,
19
+ MeshObject,
20
+ bc_box,
21
+ bc_ellipsoid,
22
+ bc_frustum,
23
+ bc_helix,
24
+ )
25
+ from .transform import (
26
+ AffineTransform,
27
+ inverse_axis_rotation,
28
+ inverse_plane_rotation,
29
+ inverse_scale,
30
+ inverse_shift,
31
+ )
32
+
33
+ __all__ = [
34
+ "AffineTransform",
35
+ "Body",
36
+ "Box",
37
+ "Conic",
38
+ "Ellipsoid",
39
+ "Helix",
40
+ "MeshObject",
41
+ "bc_box",
42
+ "bc_ellipsoid",
43
+ "bc_frustum",
44
+ "bc_helix",
45
+ "difference",
46
+ "intersect",
47
+ "inverse_axis_rotation",
48
+ "inverse_plane_rotation",
49
+ "inverse_scale",
50
+ "inverse_shift",
51
+ "union",
52
+ ]
@@ -0,0 +1,157 @@
1
+ """
2
+ Constructive Solid Geometry (CSG) operations for mesh objects.
3
+
4
+ This module provides boolean operations (union, difference, intersection)
5
+ for combining geometric primitives into complex shapes. Operations are
6
+ implemented using signed distance field composition with NumPy.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+ from typing import cast
13
+
14
+ import numpy as np
15
+
16
+ from ..utils.types import FloatArray
17
+ from .primitives import Body, MeshObject, _make_body
18
+
19
+
20
+ def _validate_objects(objects: object, operation: str) -> tuple[MeshObject, ...]:
21
+ if not isinstance(objects, Sequence):
22
+ raise TypeError(f"{operation} expects a sequence of MeshObject instances")
23
+ object_sequence = cast(Sequence[object], objects)
24
+ if not all(isinstance(obj, MeshObject) for obj in object_sequence):
25
+ raise TypeError(f"{operation} expects MeshObject instances")
26
+ object_tuple = tuple(obj for obj in object_sequence if isinstance(obj, MeshObject))
27
+ if len(object_tuple) < 2:
28
+ raise ValueError(f"{operation} requires at least two objects")
29
+ dims = {obj.dim for obj in object_tuple}
30
+ if len(dims) != 1:
31
+ raise ValueError(f"{operation} requires objects with the same dimension")
32
+ return object_tuple
33
+
34
+
35
+ def _collect_points(objects: Sequence[MeshObject]) -> tuple[list[list[float]], list[list[float]]]:
36
+ fixed_points: list[list[float]] = []
37
+ mobile_points: list[list[float]] = []
38
+ for obj in objects:
39
+ fixed_points.extend(obj.fixed_points)
40
+ mobile_points.extend(obj.mobile_points)
41
+ return fixed_points, mobile_points
42
+
43
+
44
+ def _body_for(obj: MeshObject) -> Body:
45
+ if obj.obj is None:
46
+ raise ValueError("Boolean operations require MeshObject instances with bodies")
47
+ return obj.obj
48
+
49
+
50
+ def _stack_evaluations(bodies: Sequence[Body], points: FloatArray) -> FloatArray:
51
+ """
52
+ Evaluate multiple bodies at the same points and stack results.
53
+
54
+ Args:
55
+ bodies: Sequence of Body instances to evaluate
56
+ points: Array of shape (N, dim) containing query points
57
+
58
+ Returns:
59
+ Array of shape (len(bodies), N) containing signed distance values
60
+
61
+ Note:
62
+ Expects each body.evaluate() to return a 1D array of length N.
63
+ No shape validation is performed for performance reasons.
64
+ """
65
+ return np.stack(
66
+ [np.asarray(body.evaluate(points), dtype=float) for body in bodies],
67
+ axis=0,
68
+ ).astype(np.float64, copy=False)
69
+
70
+
71
+ def union(objects: Sequence[MeshObject]) -> MeshObject:
72
+ """Return a geometry that is inside any supplied object.
73
+
74
+ Args:
75
+ objects: At least two objects with the same dimension.
76
+
77
+ Returns:
78
+ Combined mesh object.
79
+ """
80
+
81
+ members = _validate_objects(objects, "Union")
82
+ fixed_points, mobile_points = _collect_points(members)
83
+ dim = members[0].dim
84
+ bodies = tuple(_body_for(member) for member in members)
85
+ body = _make_body(
86
+ dim,
87
+ lambda points: np.max(_stack_evaluations(bodies, points), axis=0),
88
+ )
89
+ return MeshObject(dim, fixed_points, mobile_points, body=body)
90
+
91
+
92
+ def difference(mother: MeshObject, subtract: Sequence[MeshObject]) -> MeshObject:
93
+ """Carve each subtractor out of a mother object.
94
+
95
+ Args:
96
+ mother: Object to retain.
97
+ subtract: Same-dimensional objects to remove.
98
+
99
+ Returns:
100
+ Resulting mesh object.
101
+ """
102
+
103
+ mother_body = _body_for(mother)
104
+ subtractors = tuple(subtract)
105
+ if not subtractors:
106
+ return MeshObject(
107
+ mother.dim,
108
+ mother.fixed_points[:],
109
+ mother.mobile_points[:],
110
+ body=mother_body,
111
+ )
112
+ dims = {mother.dim, *(obj.dim for obj in subtractors)}
113
+ if len(dims) != 1:
114
+ raise ValueError("Difference requires objects with the same dimension")
115
+
116
+ fixed_points = mother.fixed_points[:]
117
+ mobile_points = mother.mobile_points[:]
118
+ for obj in subtractors:
119
+ fixed_points.extend(obj.fixed_points)
120
+ mobile_points.extend(obj.mobile_points)
121
+
122
+ dim = mother.dim
123
+ subtractor_bodies = tuple(_body_for(obj) for obj in subtractors)
124
+ body = _make_body(
125
+ dim,
126
+ lambda points: np.min(
127
+ np.vstack(
128
+ (
129
+ np.asarray(mother_body.evaluate(points), dtype=float)[None, :],
130
+ -_stack_evaluations(subtractor_bodies, points),
131
+ )
132
+ ),
133
+ axis=0,
134
+ ),
135
+ )
136
+ return MeshObject(dim, fixed_points, mobile_points, body=body)
137
+
138
+
139
+ def intersect(objects: Sequence[MeshObject]) -> MeshObject:
140
+ """Return a geometry that is inside every supplied object.
141
+
142
+ Args:
143
+ objects: At least two objects with the same dimension.
144
+
145
+ Returns:
146
+ Intersected mesh object.
147
+ """
148
+
149
+ members = _validate_objects(objects, "Intersection")
150
+ fixed_points, mobile_points = _collect_points(members)
151
+ dim = members[0].dim
152
+ bodies = tuple(_body_for(member) for member in members)
153
+ body = _make_body(
154
+ dim,
155
+ lambda points: np.min(_stack_evaluations(bodies, points), axis=0),
156
+ )
157
+ return MeshObject(dim, fixed_points, mobile_points, body=body)