simgeometry 0.1.0__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.
- simgeometry/__init__.py +38 -0
- simgeometry/_bounds.py +16 -0
- simgeometry/_normalize.py +23 -0
- simgeometry/_particle_boundaries.py +37 -0
- simgeometry/_types.py +22 -0
- simgeometry/api.py +247 -0
- simgeometry/boundingbox.py +231 -0
- simgeometry/collection.py +65 -0
- simgeometry/crossings.py +319 -0
- simgeometry/cut_edges.py +309 -0
- simgeometry/cylindrical.py +43 -0
- simgeometry/domain_decomposition.py +496 -0
- simgeometry/evaluation.py +105 -0
- simgeometry/fractions.py +68 -0
- simgeometry/grid.py +238 -0
- simgeometry/kernels/__init__.py +1 -0
- simgeometry/kernels/_build.py +53 -0
- simgeometry/kernels/_metal.py +41 -0
- simgeometry/kernels/compile.py +24 -0
- simgeometry/kernels/contains_ball/__init__.py +12 -0
- simgeometry/kernels/contains_ball/contains_ball_cuda.cu +27 -0
- simgeometry/kernels/contains_ball/contains_ball_metal.py +53 -0
- simgeometry/kernels/contains_ball/contains_ball_numpy.py +23 -0
- simgeometry/kernels/contains_ball/contains_ball_pyccel.py +31 -0
- simgeometry/kernels/contains_box/__init__.py +12 -0
- simgeometry/kernels/contains_box/contains_box_cuda.cu +26 -0
- simgeometry/kernels/contains_box/contains_box_metal.py +50 -0
- simgeometry/kernels/contains_box/contains_box_numpy.py +24 -0
- simgeometry/kernels/contains_box/contains_box_pyccel.py +32 -0
- simgeometry/kernels/contains_polygon_2d/__init__.py +12 -0
- simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_cuda.cu +35 -0
- simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_metal.py +58 -0
- simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_numpy.py +28 -0
- simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_pyccel.py +40 -0
- simgeometry/mesh.py +147 -0
- simgeometry/objects/__init__.py +35 -0
- simgeometry/objects/aperture2d.py +183 -0
- simgeometry/objects/aperture3d.py +361 -0
- simgeometry/objects/circle.py +138 -0
- simgeometry/objects/cuboid.py +173 -0
- simgeometry/objects/function_object.py +214 -0
- simgeometry/objects/line.py +127 -0
- simgeometry/objects/polygon.py +182 -0
- simgeometry/objects/polyline.py +67 -0
- simgeometry/objects/rectangle.py +130 -0
- simgeometry/objects/sphere.py +181 -0
- simgeometry/objects/triangle.py +148 -0
- simgeometry/operations.py +386 -0
- simgeometry/panels.py +80 -0
- simgeometry/plotting.py +179 -0
- simgeometry/py.typed +0 -0
- simgeometry/queries.py +445 -0
- simgeometry/sampling.py +75 -0
- simgeometry/shapes.py +402 -0
- simgeometry/simulation_domain.py +277 -0
- simgeometry/utils.py +24 -0
- simgeometry/visualization.py +381 -0
- simgeometry-0.1.0.dist-info/METADATA +128 -0
- simgeometry-0.1.0.dist-info/RECORD +62 -0
- simgeometry-0.1.0.dist-info/WHEEL +5 -0
- simgeometry-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
- simgeometry-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Rectangle helpers for geometric object definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from operator import add, sub
|
|
6
|
+
|
|
7
|
+
import cunumpy as xp
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from simgeometry._types import Array, ConfigValue
|
|
11
|
+
from simgeometry.shapes import Shape2D, _component_major
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Rectangle(Shape2D):
|
|
15
|
+
"""Represent a rectangular geometry object."""
|
|
16
|
+
|
|
17
|
+
exact_outline = True
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
midpoint: tuple | list,
|
|
22
|
+
side_lengths: tuple,
|
|
23
|
+
bc: ConfigValue = None,
|
|
24
|
+
inverted: bool = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Initialize a rectangle in 2D space.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
midpoint (list or tuple): Coordinates of the midpoint of the rectangle.
|
|
31
|
+
side_lengths (list or tuple): Lengths of the sides of the rectangle in each dimension.
|
|
32
|
+
bc (optional): Boundary condition information.
|
|
33
|
+
inverted (bool, optional): Cover everything except the rectangle.
|
|
34
|
+
"""
|
|
35
|
+
assert len(midpoint) == 2, (
|
|
36
|
+
f"Length of midpoint must match ndim (2). {len(midpoint) =}"
|
|
37
|
+
)
|
|
38
|
+
assert len(side_lengths) == 2, "Length of side_lengths must match ndim."
|
|
39
|
+
|
|
40
|
+
self.midpoint = list(midpoint)
|
|
41
|
+
self.side_lengths = side_lengths
|
|
42
|
+
self._bc = bc
|
|
43
|
+
self._inverted = bool(inverted)
|
|
44
|
+
|
|
45
|
+
# Calculate bounds based on the midpoint and side lengths
|
|
46
|
+
half_lengths = tuple(len / 2 for len in self.side_lengths)
|
|
47
|
+
lower_bound = tuple(map(sub, self.midpoint, half_lengths))
|
|
48
|
+
upper_bound = tuple(map(add, self.midpoint, half_lengths))
|
|
49
|
+
|
|
50
|
+
super().__init__(
|
|
51
|
+
lower_bound=lower_bound,
|
|
52
|
+
upper_bound=upper_bound,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def mirror(self, axis: int, position: float) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Mirror the rectangle across a specified axis at a given position.
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
axis (int): Axis index (0 for x-axis, 1 for y-axis).
|
|
61
|
+
position (float): Position along the axis to mirror.
|
|
62
|
+
"""
|
|
63
|
+
if axis < 0 or axis > 1:
|
|
64
|
+
raise ValueError("Axis must be 0 (x-axis) or 1 (y-axis).")
|
|
65
|
+
|
|
66
|
+
# Mirror the midpoint coordinate along the specified axis
|
|
67
|
+
self.midpoint[axis] = 2 * position - self.midpoint[axis]
|
|
68
|
+
|
|
69
|
+
half_lengths = tuple(len / 2 for len in self.side_lengths)
|
|
70
|
+
lower_bound = tuple(map(sub, self.midpoint, half_lengths))
|
|
71
|
+
upper_bound = tuple(map(add, self.midpoint, half_lengths))
|
|
72
|
+
|
|
73
|
+
self.set_bounding_box(lower_bound, upper_bound)
|
|
74
|
+
|
|
75
|
+
def copy(self) -> Rectangle:
|
|
76
|
+
"""
|
|
77
|
+
Create a copy of the rectangle.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Rectangle: A new instance of Rectangle with the same midpoint, side lengths, and boundary conditions.
|
|
81
|
+
"""
|
|
82
|
+
return Rectangle(
|
|
83
|
+
midpoint=self.midpoint.copy(),
|
|
84
|
+
side_lengths=self.side_lengths,
|
|
85
|
+
bc=self._bc,
|
|
86
|
+
inverted=self._inverted,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def translate(self, offset: tuple[float, float]) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Translate the xyz by the given offset.
|
|
92
|
+
|
|
93
|
+
Parameters:
|
|
94
|
+
offset (tuple): (dx, dy) translation vector.
|
|
95
|
+
"""
|
|
96
|
+
offset_vec = xp.asarray(offset)
|
|
97
|
+
|
|
98
|
+
# Apply translation
|
|
99
|
+
self.midpoint = list(xp.asarray(self.midpoint) + offset_vec)
|
|
100
|
+
|
|
101
|
+
half_lengths = tuple(len / 2 for len in self.side_lengths)
|
|
102
|
+
lower_bound = tuple(map(sub, self.midpoint, half_lengths))
|
|
103
|
+
upper_bound = tuple(map(add, self.midpoint, half_lengths))
|
|
104
|
+
|
|
105
|
+
self.set_bounding_box(lower_bound, upper_bound)
|
|
106
|
+
|
|
107
|
+
def _corner_loops(self) -> list[np.ndarray]:
|
|
108
|
+
"""The four corners as one counter-clockwise ``(2, 4)`` loop."""
|
|
109
|
+
(xmin, ymin), (xmax, ymax) = self.lower_bound, self.upper_bound
|
|
110
|
+
return [np.array([[xmin, xmax, xmax, xmin], [ymin, ymin, ymax, ymax]], float)]
|
|
111
|
+
|
|
112
|
+
def contains(self, coords: Array) -> bool:
|
|
113
|
+
"""
|
|
114
|
+
Check if the given coordinates are inside the rectangle.
|
|
115
|
+
|
|
116
|
+
Parameters:
|
|
117
|
+
coords (list or tuple): Coordinates for each dimension.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
bool: True if the coordinates are inside the rectangle, False otherwise.
|
|
121
|
+
"""
|
|
122
|
+
return self._apply_inversion(self.bounding_box.contains(coords))
|
|
123
|
+
|
|
124
|
+
def contains_points(self, points: Array) -> Array:
|
|
125
|
+
"""Vectorised :meth:`contains`: a mask of the ``(2, n)`` points inside."""
|
|
126
|
+
points = _component_major(points, 2)
|
|
127
|
+
lower = xp.asarray(self.bounding_box.lower_bound, dtype=float)[:, None]
|
|
128
|
+
upper = xp.asarray(self.bounding_box.upper_bound, dtype=float)[:, None]
|
|
129
|
+
inside = xp.all((points >= lower) & (points < upper), axis=0)
|
|
130
|
+
return ~inside if self.inverted else inside
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Sphere helpers for geometric object definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cunumpy as xp
|
|
6
|
+
|
|
7
|
+
from simgeometry._types import Array, ConfigValue
|
|
8
|
+
from simgeometry.shapes import Shape3D, _component_major
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Sphere(Shape3D):
|
|
12
|
+
"""Represent a spherical geometry object."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
midpoint: list | tuple,
|
|
17
|
+
radius: float,
|
|
18
|
+
bc: ConfigValue = None,
|
|
19
|
+
inverted: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Initialize a sphere in 3D.
|
|
23
|
+
|
|
24
|
+
Parameters:
|
|
25
|
+
midpoint (list or tuple): Coordinates of the midpoint of the sphere.
|
|
26
|
+
radius (float): Radius of the sphere.
|
|
27
|
+
bc (optional): Boundary condition information.
|
|
28
|
+
"""
|
|
29
|
+
assert len(midpoint) == 3, "Length of midpoint must be 3 for 3D Sphere."
|
|
30
|
+
assert isinstance(radius, (int, float)) and radius > 0, (
|
|
31
|
+
"Radius must be a positive number."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self.midpoint = list(midpoint)
|
|
35
|
+
self.radius = radius
|
|
36
|
+
self._bc = bc
|
|
37
|
+
self._inverted = bool(inverted)
|
|
38
|
+
|
|
39
|
+
# Calculate bounds based on the midpoint and radius
|
|
40
|
+
lower_bound = tuple(m - self.radius for m in self.midpoint)
|
|
41
|
+
upper_bound = tuple(m + self.radius for m in self.midpoint)
|
|
42
|
+
|
|
43
|
+
super().__init__(
|
|
44
|
+
lower_bound=lower_bound,
|
|
45
|
+
upper_bound=upper_bound,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def mirror(self, axis: int, position: float) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Mirror the sphere across a specified axis at a given position.
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
axis (int): Axis index (0 for x-axis, 1 for y-axis, 2 for z-axis).
|
|
54
|
+
position (float): Position along the axis to mirror.
|
|
55
|
+
"""
|
|
56
|
+
if axis < 0 or axis > 2:
|
|
57
|
+
raise ValueError("Axis must be 0 (x-axis), 1 (y-axis), or 2 (z-axis).")
|
|
58
|
+
|
|
59
|
+
# Mirror the midpoint coordinate along the specified axis
|
|
60
|
+
self.midpoint[axis] = 2 * position - self.midpoint[axis]
|
|
61
|
+
|
|
62
|
+
lower_bound = tuple(m - self.radius for m in self.midpoint)
|
|
63
|
+
upper_bound = tuple(m + self.radius for m in self.midpoint)
|
|
64
|
+
|
|
65
|
+
self.set_bounding_box(lower_bound, upper_bound)
|
|
66
|
+
|
|
67
|
+
def copy(self) -> Sphere:
|
|
68
|
+
"""
|
|
69
|
+
Create a copy of the sphere.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Sphere: A new instance of Sphere with the same midpoint, radius, and boundary conditions.
|
|
73
|
+
"""
|
|
74
|
+
return Sphere(
|
|
75
|
+
midpoint=self.midpoint.copy(),
|
|
76
|
+
radius=self.radius,
|
|
77
|
+
bc=self._bc,
|
|
78
|
+
inverted=self._inverted,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def translate(self, offset: tuple[float, float]) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Translate the xyz by the given offset.
|
|
84
|
+
|
|
85
|
+
Parameters:
|
|
86
|
+
offset (tuple): (dx, dy) translation vector.
|
|
87
|
+
"""
|
|
88
|
+
offset_vec = xp.asarray(offset)
|
|
89
|
+
|
|
90
|
+
# Apply translation
|
|
91
|
+
self.midpoint = list(xp.asarray(self.midpoint) + offset_vec)
|
|
92
|
+
|
|
93
|
+
lower_bound = tuple(m - self.radius for m in self.midpoint)
|
|
94
|
+
upper_bound = tuple(m + self.radius for m in self.midpoint)
|
|
95
|
+
|
|
96
|
+
self.set_bounding_box(lower_bound, upper_bound)
|
|
97
|
+
|
|
98
|
+
def get_vertices(self, slice_axis: int, slice_position: float) -> ConfigValue:
|
|
99
|
+
"""
|
|
100
|
+
Get the vertices of a slice of the sphere along a given axis.
|
|
101
|
+
|
|
102
|
+
Parameters:
|
|
103
|
+
slice_axis (int): Axis along which to slice (0 for x, 1 for y, 2 for z).
|
|
104
|
+
slice_position (float): Position along the slice_axis where the slice is taken.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
vertices (ndarray): Array of vertices defining the slice circle.
|
|
108
|
+
If the slice does not intersect the sphere, returns an empty array.
|
|
109
|
+
"""
|
|
110
|
+
assert 0 <= slice_axis < 3, "slice_axis must be 0, 1, or 2."
|
|
111
|
+
|
|
112
|
+
inverted = self.inverted
|
|
113
|
+
|
|
114
|
+
# Calculate the distance from the sphere center to the slicing plane
|
|
115
|
+
distance = abs(slice_position - self.midpoint[slice_axis])
|
|
116
|
+
|
|
117
|
+
# Check if the plane intersects the sphere
|
|
118
|
+
if distance > self.radius:
|
|
119
|
+
# The plane does not intersect the sphere
|
|
120
|
+
return xp.array([]), inverted
|
|
121
|
+
|
|
122
|
+
elif distance == self.radius:
|
|
123
|
+
# The plane touches the sphere at a single point
|
|
124
|
+
axis1 = (slice_axis + 1) % 3
|
|
125
|
+
axis2 = (slice_axis + 2) % 3
|
|
126
|
+
point = xp.array([self.midpoint[axis1], self.midpoint[axis2]])
|
|
127
|
+
return xp.array([point]), inverted
|
|
128
|
+
|
|
129
|
+
else:
|
|
130
|
+
# The plane intersects the sphere in a circle
|
|
131
|
+
# Compute the radius of the circle
|
|
132
|
+
r = xp.sqrt(self.radius**2 - distance**2)
|
|
133
|
+
|
|
134
|
+
# Get the center of the circle in the plane
|
|
135
|
+
axis1 = (slice_axis + 1) % 3
|
|
136
|
+
axis2 = (slice_axis + 2) % 3
|
|
137
|
+
center1 = self.midpoint[axis1]
|
|
138
|
+
center2 = self.midpoint[axis2]
|
|
139
|
+
|
|
140
|
+
# Generate points along the circle
|
|
141
|
+
N = 100 # Number of points to approximate the circle
|
|
142
|
+
theta = xp.linspace(0, 2 * xp.pi, N)
|
|
143
|
+
x = center1 + r * xp.cos(theta)
|
|
144
|
+
y = center2 + r * xp.sin(theta)
|
|
145
|
+
|
|
146
|
+
# Combine x and y into an array of vertices
|
|
147
|
+
vertices = xp.column_stack((x, y))
|
|
148
|
+
|
|
149
|
+
return xp.array(vertices), inverted
|
|
150
|
+
|
|
151
|
+
def contains(self, coords: Array) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
Check if the given coordinates are inside the sphere.
|
|
154
|
+
|
|
155
|
+
Parameters:
|
|
156
|
+
coords (list or tuple): Coordinates for each dimension.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
bool: True if the coordinates are inside the sphere, False otherwise.
|
|
160
|
+
"""
|
|
161
|
+
coords = xp.asarray(coords, dtype=float)
|
|
162
|
+
midpoint = xp.asarray(self.midpoint, dtype=float)
|
|
163
|
+
distance_squared = xp.sum((coords - midpoint) ** 2)
|
|
164
|
+
inside = bool(distance_squared <= self.radius**2)
|
|
165
|
+
return (not inside) if self.inverted else inside
|
|
166
|
+
|
|
167
|
+
def contains_points(self, points: Array) -> Array:
|
|
168
|
+
"""Vectorised :meth:`contains`: a mask of the ``(3, n)`` points inside."""
|
|
169
|
+
points = _component_major(points, 3)
|
|
170
|
+
midpoint = xp.asarray(self.midpoint, dtype=float)[:, None]
|
|
171
|
+
distance_squared = xp.sum((points - midpoint) ** 2, axis=0)
|
|
172
|
+
inside = distance_squared <= self.radius**2
|
|
173
|
+
return ~inside if self.inverted else inside
|
|
174
|
+
|
|
175
|
+
def volume(self) -> float:
|
|
176
|
+
"""Calculate the volume of the sphere."""
|
|
177
|
+
return (4 / 3) * xp.pi * self.radius**3
|
|
178
|
+
|
|
179
|
+
def surface_area(self) -> float:
|
|
180
|
+
"""Calculate the surface area of the sphere."""
|
|
181
|
+
return 4 * xp.pi * self.radius**2
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Triangle helpers for geometric object definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cunumpy as xp
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from simgeometry._types import Array, ConfigValue, Vertices2DLike
|
|
9
|
+
from simgeometry.shapes import Shape2D, _component_major
|
|
10
|
+
from simgeometry.utils import vertices_to_bounds
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Triangle(Shape2D):
|
|
14
|
+
"""Represent a triangular geometry object."""
|
|
15
|
+
|
|
16
|
+
exact_outline = True
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
vertices: Vertices2DLike,
|
|
21
|
+
bc: ConfigValue = None,
|
|
22
|
+
inverted: bool = False,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Initialize a triangle in 2D space.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
vertices (list or tuple): List of three tuples, each representing a vertex (x, y).
|
|
29
|
+
bc (optional): Boundary condition information.
|
|
30
|
+
inverted (bool, optional): Cover everything except the triangle.
|
|
31
|
+
"""
|
|
32
|
+
assert len(vertices) == 3, "Triangle must have exactly three vertices."
|
|
33
|
+
assert all(len(vertex) == 2 for vertex in vertices), (
|
|
34
|
+
"Each vertex must have two coordinates."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self._vertices = [xp.array(vertex) for vertex in vertices]
|
|
38
|
+
self._bc = bc
|
|
39
|
+
self._inverted = bool(inverted)
|
|
40
|
+
|
|
41
|
+
# Calculate bounds
|
|
42
|
+
xs = [vertex[0] for vertex in self._vertices]
|
|
43
|
+
ys = [vertex[1] for vertex in self._vertices]
|
|
44
|
+
|
|
45
|
+
lower_bound = (min(xs), min(ys))
|
|
46
|
+
upper_bound = (max(xs), max(ys))
|
|
47
|
+
|
|
48
|
+
super().__init__(
|
|
49
|
+
lower_bound=lower_bound,
|
|
50
|
+
upper_bound=upper_bound,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def mirror(self, axis: int, position: float) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Mirror the triangle across a specified axis at a given position.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
axis (int): Axis index (0 for x-axis, 1 for y-axis).
|
|
59
|
+
position (float): Position along the axis to mirror.
|
|
60
|
+
"""
|
|
61
|
+
for i in range(3):
|
|
62
|
+
if axis == 0: # Mirror across vertical line x = position
|
|
63
|
+
self._vertices[i][0] = 2 * position - self._vertices[i][0]
|
|
64
|
+
elif axis == 1: # Mirror across horizontal line y = position
|
|
65
|
+
self._vertices[i][1] = 2 * position - self._vertices[i][1]
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError("Axis must be 0 (x-axis) or 1 (y-axis).")
|
|
68
|
+
|
|
69
|
+
lower_bound, upper_bound = vertices_to_bounds(xp.array(self._vertices))
|
|
70
|
+
|
|
71
|
+
self.set_bounding_box(lower_bound, upper_bound)
|
|
72
|
+
|
|
73
|
+
def copy(self) -> Triangle:
|
|
74
|
+
"""
|
|
75
|
+
Create a copy of the triangle.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Triangle: A new instance of Triangle with the same vertices and boundary conditions.
|
|
79
|
+
"""
|
|
80
|
+
vertices_copy = [vertex.copy() for vertex in self._vertices]
|
|
81
|
+
return Triangle(vertices=vertices_copy, bc=self._bc, inverted=self._inverted)
|
|
82
|
+
|
|
83
|
+
def translate(self, offset: tuple[float, float]) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Translate the xyz by the given offset.
|
|
86
|
+
|
|
87
|
+
Parameters:
|
|
88
|
+
offset (tuple): (dx, dy) translation vector.
|
|
89
|
+
"""
|
|
90
|
+
offset_vec = xp.asarray(offset)
|
|
91
|
+
|
|
92
|
+
# Apply translation to each vertex
|
|
93
|
+
for i in range(3):
|
|
94
|
+
self._vertices[i] += offset_vec
|
|
95
|
+
|
|
96
|
+
lower_bound, upper_bound = vertices_to_bounds(xp.array(self._vertices))
|
|
97
|
+
|
|
98
|
+
self.set_bounding_box(lower_bound, upper_bound)
|
|
99
|
+
|
|
100
|
+
def contains(self, coords: Array) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Check if the given coordinates are inside the (possibly inverted) triangle.
|
|
103
|
+
|
|
104
|
+
Parameters:
|
|
105
|
+
coords (list or tuple): Coordinates (x, y).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
bool: True if the point is inside the object, False otherwise.
|
|
109
|
+
"""
|
|
110
|
+
# Use the same arithmetic as the batch path. Separate scalar dot
|
|
111
|
+
# products can round differently at an edge and disagree with node_mask.
|
|
112
|
+
point = xp.asarray(coords, dtype=float).reshape(2, 1)
|
|
113
|
+
return bool(xp.to_numpy(self.contains_points(point))[0])
|
|
114
|
+
|
|
115
|
+
def contains_points(self, points: Array) -> Array:
|
|
116
|
+
"""Vectorised :meth:`contains`: a mask of the ``(2, n)`` points inside."""
|
|
117
|
+
points = _component_major(points, 2)
|
|
118
|
+
vertices = [xp.asarray(vertex, dtype=float) for vertex in self._vertices]
|
|
119
|
+
lower = xp.asarray(self.bounding_box.lower_bound, dtype=float)[:, None]
|
|
120
|
+
upper = xp.asarray(self.bounding_box.upper_bound, dtype=float)[:, None]
|
|
121
|
+
in_box = xp.all((points >= lower) & (points < upper), axis=0)
|
|
122
|
+
# Barycentric coordinates for every point, including the edges.
|
|
123
|
+
v0 = vertices[2] - vertices[0]
|
|
124
|
+
v1 = vertices[1] - vertices[0]
|
|
125
|
+
v2x = points[0] - vertices[0][0]
|
|
126
|
+
v2y = points[1] - vertices[0][1]
|
|
127
|
+
dot00 = v0[0] * v0[0] + v0[1] * v0[1]
|
|
128
|
+
dot01 = v0[0] * v1[0] + v0[1] * v1[1]
|
|
129
|
+
dot02 = v0[0] * v2x + v0[1] * v2y
|
|
130
|
+
dot11 = v1[0] * v1[0] + v1[1] * v1[1]
|
|
131
|
+
dot12 = v1[0] * v2x + v1[1] * v2y
|
|
132
|
+
denom = dot00 * dot11 - dot01 * dot01
|
|
133
|
+
if denom == 0:
|
|
134
|
+
inside = xp.zeros(points.shape[1], dtype=bool) # degenerate triangle
|
|
135
|
+
else:
|
|
136
|
+
inv_denom = 1 / denom
|
|
137
|
+
u = (dot11 * dot02 - dot01 * dot12) * inv_denom
|
|
138
|
+
v = (dot00 * dot12 - dot01 * dot02) * inv_denom
|
|
139
|
+
inside = in_box & (u >= 0) & (v >= 0) & (u + v <= 1)
|
|
140
|
+
return ~inside if self.inverted else inside
|
|
141
|
+
|
|
142
|
+
def _corner_loops(self) -> list[np.ndarray]:
|
|
143
|
+
"""The three vertices as one ``(2, 3)`` loop."""
|
|
144
|
+
return [
|
|
145
|
+
np.array(
|
|
146
|
+
[[float(xp.to_numpy(v)[i]) for v in self._vertices] for i in (0, 1)]
|
|
147
|
+
)
|
|
148
|
+
]
|