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.
Files changed (62) hide show
  1. simgeometry/__init__.py +38 -0
  2. simgeometry/_bounds.py +16 -0
  3. simgeometry/_normalize.py +23 -0
  4. simgeometry/_particle_boundaries.py +37 -0
  5. simgeometry/_types.py +22 -0
  6. simgeometry/api.py +247 -0
  7. simgeometry/boundingbox.py +231 -0
  8. simgeometry/collection.py +65 -0
  9. simgeometry/crossings.py +319 -0
  10. simgeometry/cut_edges.py +309 -0
  11. simgeometry/cylindrical.py +43 -0
  12. simgeometry/domain_decomposition.py +496 -0
  13. simgeometry/evaluation.py +105 -0
  14. simgeometry/fractions.py +68 -0
  15. simgeometry/grid.py +238 -0
  16. simgeometry/kernels/__init__.py +1 -0
  17. simgeometry/kernels/_build.py +53 -0
  18. simgeometry/kernels/_metal.py +41 -0
  19. simgeometry/kernels/compile.py +24 -0
  20. simgeometry/kernels/contains_ball/__init__.py +12 -0
  21. simgeometry/kernels/contains_ball/contains_ball_cuda.cu +27 -0
  22. simgeometry/kernels/contains_ball/contains_ball_metal.py +53 -0
  23. simgeometry/kernels/contains_ball/contains_ball_numpy.py +23 -0
  24. simgeometry/kernels/contains_ball/contains_ball_pyccel.py +31 -0
  25. simgeometry/kernels/contains_box/__init__.py +12 -0
  26. simgeometry/kernels/contains_box/contains_box_cuda.cu +26 -0
  27. simgeometry/kernels/contains_box/contains_box_metal.py +50 -0
  28. simgeometry/kernels/contains_box/contains_box_numpy.py +24 -0
  29. simgeometry/kernels/contains_box/contains_box_pyccel.py +32 -0
  30. simgeometry/kernels/contains_polygon_2d/__init__.py +12 -0
  31. simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_cuda.cu +35 -0
  32. simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_metal.py +58 -0
  33. simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_numpy.py +28 -0
  34. simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_pyccel.py +40 -0
  35. simgeometry/mesh.py +147 -0
  36. simgeometry/objects/__init__.py +35 -0
  37. simgeometry/objects/aperture2d.py +183 -0
  38. simgeometry/objects/aperture3d.py +361 -0
  39. simgeometry/objects/circle.py +138 -0
  40. simgeometry/objects/cuboid.py +173 -0
  41. simgeometry/objects/function_object.py +214 -0
  42. simgeometry/objects/line.py +127 -0
  43. simgeometry/objects/polygon.py +182 -0
  44. simgeometry/objects/polyline.py +67 -0
  45. simgeometry/objects/rectangle.py +130 -0
  46. simgeometry/objects/sphere.py +181 -0
  47. simgeometry/objects/triangle.py +148 -0
  48. simgeometry/operations.py +386 -0
  49. simgeometry/panels.py +80 -0
  50. simgeometry/plotting.py +179 -0
  51. simgeometry/py.typed +0 -0
  52. simgeometry/queries.py +445 -0
  53. simgeometry/sampling.py +75 -0
  54. simgeometry/shapes.py +402 -0
  55. simgeometry/simulation_domain.py +277 -0
  56. simgeometry/utils.py +24 -0
  57. simgeometry/visualization.py +381 -0
  58. simgeometry-0.1.0.dist-info/METADATA +128 -0
  59. simgeometry-0.1.0.dist-info/RECORD +62 -0
  60. simgeometry-0.1.0.dist-info/WHEEL +5 -0
  61. simgeometry-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
  62. 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
+ ]