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
simgeometry/grid.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Grid helpers for geometry, grids, and simulation domains."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cunumpy as xp
|
|
6
|
+
import numpy as np
|
|
7
|
+
from maybempi import MPI
|
|
8
|
+
from numpy import ndarray
|
|
9
|
+
|
|
10
|
+
from simgeometry._normalize import ensure_dim_tuple
|
|
11
|
+
from simgeometry._types import Array, ConfigValue, Printable
|
|
12
|
+
from simgeometry.boundingbox import BoundingBox
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Grid(BoundingBox):
|
|
16
|
+
"""Represent grid coordinates for a bounded simulation domain."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
ndim: int,
|
|
21
|
+
lower_bound: tuple,
|
|
22
|
+
upper_bound: tuple,
|
|
23
|
+
num_gridpoints: tuple,
|
|
24
|
+
num_ghost_points: int = 0,
|
|
25
|
+
comm: MPI.Comm | None = None,
|
|
26
|
+
domain_decomposition: ConfigValue = None,
|
|
27
|
+
periodic: bool | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initialize a grid.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
ndim (int): Number of dimensions (e.g., 1, 2, or 3).
|
|
34
|
+
bounds (list of tuples): List of tuples specifying min and max for each dimension.
|
|
35
|
+
num_gridpoints (list of int or int in case of ndim==1): Number of grid points in each dimension.
|
|
36
|
+
For example, [num_points_x, num_points_y, num_points_z] for 3D.
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(
|
|
39
|
+
lower_bound=lower_bound,
|
|
40
|
+
upper_bound=upper_bound,
|
|
41
|
+
comm=comm,
|
|
42
|
+
domain_decomposition=domain_decomposition,
|
|
43
|
+
periodic=periodic,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
num_gridpoints = ensure_dim_tuple(
|
|
47
|
+
num_gridpoints,
|
|
48
|
+
ndim=ndim,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.num_gridpoints = num_gridpoints # xp.array(num_gridpoints, dtype=int)
|
|
52
|
+
self._num_ghost_points = num_ghost_points
|
|
53
|
+
self.num_gridpoints_ndarray = np.array(num_gridpoints, dtype=int)
|
|
54
|
+
# On a bounded axis the first and last node sit on the boundary; on a
|
|
55
|
+
# periodic axis the last node is a neighbour of the first, not a copy
|
|
56
|
+
# of it, so there are ``n`` cells over the length rather than ``n-1``.
|
|
57
|
+
# This is the same convention as ``FieldConfig.node_spacing`` and
|
|
58
|
+
# the field solvers, so markers and the solver agree on where the
|
|
59
|
+
# nodes are.
|
|
60
|
+
periodic_axes = self.periodic or (False,) * ndim
|
|
61
|
+
self.grid_spacing = np.array(
|
|
62
|
+
[
|
|
63
|
+
(self.upper_bound[dir] - self.lower_bound[dir])
|
|
64
|
+
/ (
|
|
65
|
+
num_gridpoints[dir]
|
|
66
|
+
if periodic_axes[dir]
|
|
67
|
+
else num_gridpoints[dir] - 1
|
|
68
|
+
)
|
|
69
|
+
for dir in range(ndim)
|
|
70
|
+
],
|
|
71
|
+
dtype=float,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self._proc_index_bounds = [
|
|
75
|
+
(int(start), int(end))
|
|
76
|
+
for start, end in self.domain_decomposition_class.get_index_bounds(
|
|
77
|
+
self.num_gridpoints
|
|
78
|
+
)
|
|
79
|
+
]
|
|
80
|
+
self._num_gridpoints_local = [
|
|
81
|
+
end - start for start, end in self._proc_index_bounds
|
|
82
|
+
]
|
|
83
|
+
# Same neighbours as the decomposition: periodic wrap, PROC_NULL at walls.
|
|
84
|
+
self._neighbor_ranks = list(self.domain_decomposition_class.neighbour_ranks)
|
|
85
|
+
self._num_gridpoints_local = tuple(self._num_gridpoints_local)
|
|
86
|
+
|
|
87
|
+
# Add attribute: number of local gridpoints including ghost points
|
|
88
|
+
self._num_gridpoints_local_with_ghosts = tuple(
|
|
89
|
+
n + 2 * self._num_ghost_points for n in self._num_gridpoints_local
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def show(self, **kwargs):
|
|
93
|
+
"""Display grid lines and optional nodes, or an axis-aligned slice."""
|
|
94
|
+
from simgeometry.collection import GeometryCollection
|
|
95
|
+
|
|
96
|
+
geometry = GeometryCollection({}, bounds=(self.lower_bound, self.upper_bound))
|
|
97
|
+
return geometry.show(grid=self, labels=False, **kwargs)
|
|
98
|
+
|
|
99
|
+
def show_3d(self, **kwargs):
|
|
100
|
+
"""Display a three-dimensional grid and optional nodes."""
|
|
101
|
+
from simgeometry.collection import GeometryCollection
|
|
102
|
+
|
|
103
|
+
geometry = GeometryCollection({}, bounds=(self.lower_bound, self.upper_bound))
|
|
104
|
+
return geometry.show_3d(grid=self, labels=False, **kwargs)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def indices(self) -> list[tuple[int, int]]:
|
|
108
|
+
"""Return the indices."""
|
|
109
|
+
indices = []
|
|
110
|
+
if self.ndim == 1:
|
|
111
|
+
return list(range(self.num_gridpoints[0]))
|
|
112
|
+
elif self.ndim == 2:
|
|
113
|
+
for i in range(self.num_gridpoints[0]):
|
|
114
|
+
for j in range(self.num_gridpoints[1]):
|
|
115
|
+
indices.append((i, j))
|
|
116
|
+
return indices
|
|
117
|
+
elif self.ndim == 3:
|
|
118
|
+
for i in range(self.num_gridpoints[0]):
|
|
119
|
+
for j in range(self.num_gridpoints[1]):
|
|
120
|
+
for k in range(self.num_gridpoints[2]):
|
|
121
|
+
indices.append((i, j, k))
|
|
122
|
+
return indices
|
|
123
|
+
|
|
124
|
+
def node_points(self) -> np.ndarray:
|
|
125
|
+
"""Return the coordinates of every node as an ``(ndim, N)`` host array.
|
|
126
|
+
|
|
127
|
+
The nodes are in the order of :attr:`indices` (C order of the grid);
|
|
128
|
+
column ``n`` equals :meth:`node_coordinates` of the ``n``-th index.
|
|
129
|
+
"""
|
|
130
|
+
lower = np.asarray(self.lower_bound, dtype=float)
|
|
131
|
+
spacing = np.asarray(xp.to_numpy(self.grid_spacing), dtype=float)
|
|
132
|
+
index = np.indices(tuple(int(n) for n in self.num_gridpoints)).reshape(
|
|
133
|
+
self.ndim, -1
|
|
134
|
+
)
|
|
135
|
+
return lower[:, None] + index.astype(float) * spacing[:, None]
|
|
136
|
+
|
|
137
|
+
def indices_from_mask(self, mask: Array) -> list[int] | list[tuple[int, ...]]:
|
|
138
|
+
"""Return the :attr:`indices` of the nodes where the grid-shaped ``mask`` is set.
|
|
139
|
+
|
|
140
|
+
``mask`` is a boolean array of shape :attr:`num_gridpoints` (or flat, in
|
|
141
|
+
C order); the result keeps the order of :attr:`indices`.
|
|
142
|
+
"""
|
|
143
|
+
mask = np.asarray(xp.to_numpy(mask), dtype=bool).reshape(
|
|
144
|
+
tuple(int(n) for n in self.num_gridpoints)
|
|
145
|
+
)
|
|
146
|
+
if self.ndim == 1:
|
|
147
|
+
return np.flatnonzero(mask).tolist()
|
|
148
|
+
return [tuple(index) for index in np.argwhere(mask).tolist()]
|
|
149
|
+
|
|
150
|
+
def node_coordinates(self, index: int | tuple[int, ...]) -> np.ndarray:
|
|
151
|
+
"""Return the physical coordinates of a grid node.
|
|
152
|
+
|
|
153
|
+
``index`` is a tuple of integer node indices (a plain integer in 1D).
|
|
154
|
+
The result is ``lower_bound + index * grid_spacing`` as a float array of
|
|
155
|
+
length ``ndim``, on the host.
|
|
156
|
+
"""
|
|
157
|
+
index_array = np.atleast_1d(np.asarray(index, dtype=float))
|
|
158
|
+
if index_array.shape != (self.ndim,):
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Expected an index with {self.ndim} entries, got {index}.",
|
|
161
|
+
)
|
|
162
|
+
lower = np.asarray(self.lower_bound, dtype=float)
|
|
163
|
+
spacing = np.asarray(xp.to_numpy(self.grid_spacing), dtype=float)
|
|
164
|
+
return lower + index_array * spacing
|
|
165
|
+
|
|
166
|
+
def get_cell_indices(self, *coords: Printable) -> tuple[int, ...]:
|
|
167
|
+
"""
|
|
168
|
+
Get the cell indices for a given position, overridden to include cell size calculations.
|
|
169
|
+
|
|
170
|
+
Parameters:
|
|
171
|
+
coords (float): Coordinates for each dimension.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
tuple: Indices of the cell containing the position.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
if len(coords) != self.ndim:
|
|
178
|
+
raise ValueError(f"Expected {self.ndim} dimensions, got {len(coords)}.")
|
|
179
|
+
|
|
180
|
+
indices = []
|
|
181
|
+
|
|
182
|
+
if self.contains(coords):
|
|
183
|
+
for coord, min_bound, cell_size in zip(
|
|
184
|
+
coords,
|
|
185
|
+
self.lower_bound,
|
|
186
|
+
self.grid_spacing,
|
|
187
|
+
):
|
|
188
|
+
index = int((coord - min_bound) / cell_size)
|
|
189
|
+
indices.append(index)
|
|
190
|
+
|
|
191
|
+
return tuple(indices)
|
|
192
|
+
|
|
193
|
+
def get_grid_points(self) -> list[ndarray]:
|
|
194
|
+
"""Return grid points for one axis."""
|
|
195
|
+
grid_points_list = []
|
|
196
|
+
periodic_axes = self.periodic or (False,) * self.ndim
|
|
197
|
+
for dim in range(self.ndim):
|
|
198
|
+
min_bound = self.lower_bound[dim]
|
|
199
|
+
max_bound = self.upper_bound[dim]
|
|
200
|
+
num_points = self.num_gridpoints[dim]
|
|
201
|
+
if periodic_axes[dim]:
|
|
202
|
+
# ``num_points`` cells over the length: the last node is one
|
|
203
|
+
# spacing before the upper bound (see ``grid_spacing``).
|
|
204
|
+
grid_points = min_bound + xp.arange(num_points) * self.grid_spacing[dim]
|
|
205
|
+
else:
|
|
206
|
+
grid_points = xp.linspace(min_bound, max_bound, num_points)
|
|
207
|
+
grid_points_list.append(grid_points)
|
|
208
|
+
return grid_points_list
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def meshgrid(self) -> tuple[Array, ...]:
|
|
212
|
+
"""Return the meshgrid."""
|
|
213
|
+
return xp.meshgrid(*self.get_grid_points())
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def interpolator(self) -> ConfigValue:
|
|
217
|
+
"""Return the interpolator."""
|
|
218
|
+
return self._interpolator
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def num_gridpoints_local(self) -> tuple[int, ...]:
|
|
222
|
+
"""Return the local grid point counts."""
|
|
223
|
+
return self._num_gridpoints_local
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def num_gridpoints_local_with_ghosts(self) -> tuple[int, ...]:
|
|
227
|
+
"""Return local grid point counts including ghosts."""
|
|
228
|
+
return self._num_gridpoints_local_with_ghosts
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def proc_index_bounds(self) -> list[tuple[int, int]]:
|
|
232
|
+
"""Return this rank global index bounds."""
|
|
233
|
+
return self._proc_index_bounds
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def neighbor_ranks(self) -> list[tuple[int, int]]:
|
|
237
|
+
"""Return neighboring MPI ranks."""
|
|
238
|
+
return self._neighbor_ranks
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Compiled containment kernels for geometric objects."""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""The options every kernel folder passes to :meth:`cunumpy.kernels.Kernel.from_folder`.
|
|
2
|
+
|
|
3
|
+
The ``__init__.py`` of a kernel folder ``simgeometry/kernels/<name>/`` declares its
|
|
4
|
+
kernel with these::
|
|
5
|
+
|
|
6
|
+
kernel = xp.kernels.Kernel.from_folder(__name__, **KERNEL_OPTIONS)
|
|
7
|
+
|
|
8
|
+
which finds ``<name>_pyccel.py``, ``<name>_numpy.py`` and ``<name>_cuda.cu`` in
|
|
9
|
+
the folder.
|
|
10
|
+
Nothing is compiled here: the Pyccel version is compiled on its first host call
|
|
11
|
+
(cached on disk by :func:`~simgeometry.kernels.compile.compile_cached`), the CUDA
|
|
12
|
+
version on its first launch. Importing this module needs neither Pyccel nor CuPy.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
#: CUDA threads per block of every kernel.
|
|
20
|
+
BLOCK_SIZE = 256
|
|
21
|
+
|
|
22
|
+
#: Default dynamic shared memory per block of a CUDA device, in bytes.
|
|
23
|
+
SHARED_MEMORY_PER_BLOCK = 48 * 1024
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def shared_memory_fits(num_values: int, itemsize: int = 8) -> bool:
|
|
27
|
+
"""Whether ``num_values`` values fit the default shared memory of a block."""
|
|
28
|
+
return num_values * itemsize <= SHARED_MEMORY_PER_BLOCK
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _compile(module: Any) -> Any:
|
|
32
|
+
from simgeometry.kernels.compile import compile_cached
|
|
33
|
+
|
|
34
|
+
return compile_cached(module)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _marker_count(args: tuple[Any, ...]) -> int:
|
|
38
|
+
"""Use the last axis of the first array as the CUDA thread count."""
|
|
39
|
+
return int(args[0].shape[-1])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
#: ``<name>_pyccel.py`` compiled with Pyccel on first use; CUDA for CuPy
|
|
43
|
+
#: arguments, the host version for NumPy ones (also while CuPy is active);
|
|
44
|
+
#: without ``n_threads`` one CUDA thread per entry of the last axis of the
|
|
45
|
+
#: first array (the marker count of a component-major ``(ncomp, N)`` or
|
|
46
|
+
#: ``(N,)`` marker array). CUDA sources include cunumpy's array-view header.
|
|
47
|
+
KERNEL_OPTIONS: dict[str, Any] = {
|
|
48
|
+
"host_suffix": "_pyccel",
|
|
49
|
+
"compile_host": _compile,
|
|
50
|
+
"dispatch": "arrays",
|
|
51
|
+
"block_size": BLOCK_SIZE,
|
|
52
|
+
"n_threads_from": _marker_count,
|
|
53
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Helpers of the Metal versions (``<name>/<name>_metal.py``) of the kernels.
|
|
2
|
+
|
|
3
|
+
A Metal version is the body of a Metal Shading Language kernel run by
|
|
4
|
+
:class:`cunumpy.kernels.MetalKernel` on the GPU of an Apple-silicon Mac, wrapped in
|
|
5
|
+
a function with the arguments of the Pyccel version: NumPy arrays in, results
|
|
6
|
+
written into the output arrays. The GPU has no float64, so the kernels compute in
|
|
7
|
+
float32 (``float64="cast"``): expect a relative error of about 1e-5 against the
|
|
8
|
+
CPU versions. Import a version only where :func:`metal_available` is true.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import cunumpy as xp
|
|
14
|
+
|
|
15
|
+
#: Threads per threadgroup of every Metal kernel.
|
|
16
|
+
THREADGROUP = 256
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def metal_available() -> bool:
|
|
20
|
+
"""Whether the Metal versions can run here (MLX installed, Metal GPU present)."""
|
|
21
|
+
return bool(xp.kernels.metal_available())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_kernel(
|
|
25
|
+
name: str,
|
|
26
|
+
source: str,
|
|
27
|
+
inputs: list[str],
|
|
28
|
+
outputs: list[str],
|
|
29
|
+
header: str = "",
|
|
30
|
+
) -> xp.kernels.MetalKernel:
|
|
31
|
+
"""A :class:`~cunumpy.kernels.MetalKernel` computing in float32, outputs zeroed."""
|
|
32
|
+
return xp.kernels.MetalKernel(
|
|
33
|
+
source,
|
|
34
|
+
inputs=inputs,
|
|
35
|
+
outputs=outputs,
|
|
36
|
+
name=name,
|
|
37
|
+
header=header,
|
|
38
|
+
threadgroup=THREADGROUP,
|
|
39
|
+
float64="cast",
|
|
40
|
+
init_value=0,
|
|
41
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Compile simgeometry's Pyccel kernel modules once and reuse the builds across processes.
|
|
2
|
+
|
|
3
|
+
The cache is :class:`cunumpy.kernels.PyccelCache`: a kernel module is built with
|
|
4
|
+
``pyccel compile`` on its first use, keyed on its source, the Pyccel and Python
|
|
5
|
+
versions and the platform, and stored under ``SIMGEOMETRY_KERNEL_CACHE`` (default
|
|
6
|
+
``~/.cache/simgeometry/kernels``, or ``<tmp>/simgeometry-kernels`` if that is not writable).
|
|
7
|
+
Later processes import the stored extension directly.
|
|
8
|
+
|
|
9
|
+
Kernels are built with OpenMP, so the loops marked ``#$ omp parallel for`` run on
|
|
10
|
+
all cores (limit them with ``OMP_NUM_THREADS``; under MPI set it to the cores per
|
|
11
|
+
rank). ``SIMGEOMETRY_OPENMP=0`` builds them serial, except the kernels that call the OpenMP
|
|
12
|
+
runtime; the setting is part of the cache key.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import cunumpy as xp
|
|
18
|
+
|
|
19
|
+
#: The on-disk build cache of simgeometry's kernels.
|
|
20
|
+
KERNEL_CACHE = xp.kernels.PyccelCache("simgeometry")
|
|
21
|
+
|
|
22
|
+
#: Return the compiled form of a kernel module, building it on first use
|
|
23
|
+
#: (:meth:`cunumpy.kernels.PyccelCache.compile`).
|
|
24
|
+
compile_cached = KERNEL_CACHE.compile
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""The contains_ball kernel: Pyccel, NumPy and CUDA versions with the same arguments.
|
|
2
|
+
|
|
3
|
+
``kernel(...)`` runs the CUDA version when the arrays are CuPy arrays, and
|
|
4
|
+
otherwise the Pyccel version, or the NumPy version if Pyccel is unavailable
|
|
5
|
+
(see ``cunumpy.kernels.set_host_kernel_implementation`` to choose).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cunumpy as xp
|
|
9
|
+
|
|
10
|
+
from simgeometry.kernels._build import KERNEL_OPTIONS
|
|
11
|
+
|
|
12
|
+
kernel = xp.kernels.Kernel.from_folder(__name__, **KERNEL_OPTIONS)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The CUDA version (GPU) of the contains_ball kernel; contains_ball_pyccel.py next
|
|
2
|
+
// to this file is the Pyccel version (CPU), with the same arguments. Loaded by
|
|
3
|
+
// picnic.kernels.catalog: one thread per marker; positions are (ndim, N).
|
|
4
|
+
//
|
|
5
|
+
// mask[marker] = |position - midpoint| <= radius (negated if inverted).
|
|
6
|
+
|
|
7
|
+
#include "cunumpy/array_view.cuh"
|
|
8
|
+
|
|
9
|
+
extern "C" __global__
|
|
10
|
+
void contains_ball(
|
|
11
|
+
Array2D<double> positions,
|
|
12
|
+
Array1D<char> mask,
|
|
13
|
+
Array1D<double> midpoint,
|
|
14
|
+
double radius,
|
|
15
|
+
int inverted
|
|
16
|
+
) {
|
|
17
|
+
const long long marker = blockDim.x * (long long)blockIdx.x + threadIdx.x;
|
|
18
|
+
if (marker >= positions.shape[1]) return;
|
|
19
|
+
double distance_squared = 0.0;
|
|
20
|
+
for (long long axis = 0; axis < positions.shape[0]; ++axis) {
|
|
21
|
+
const double delta = positions(axis, marker) - midpoint(axis);
|
|
22
|
+
distance_squared += delta * delta;
|
|
23
|
+
}
|
|
24
|
+
bool inside = distance_squared <= radius * radius;
|
|
25
|
+
if (inverted == 1) inside = !inside;
|
|
26
|
+
mask(marker) = inside ? 1 : 0;
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""The Metal version (Apple-silicon GPU, float32) of the ``contains_ball`` kernel.
|
|
2
|
+
|
|
3
|
+
``contains_ball_pyccel.py`` next to this file is the CPU version with the same
|
|
4
|
+
arguments. One thread per marker (``positions`` is ``(ndim, N)``); see :mod:`simgeometry.kernels._metal`. The test is
|
|
5
|
+
done in float32, so a marker within about 1e-7 (relative) of the boundary may be
|
|
6
|
+
classified differently from the CPU version.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from simgeometry._types import Array
|
|
14
|
+
from simgeometry.kernels._metal import make_kernel
|
|
15
|
+
|
|
16
|
+
_SOURCE = """
|
|
17
|
+
uint marker = thread_position_in_grid.x;
|
|
18
|
+
uint n = positions_shape[1];
|
|
19
|
+
if (marker >= n) return;
|
|
20
|
+
uint ndim = positions_shape[0];
|
|
21
|
+
float distance_squared = 0.0f;
|
|
22
|
+
for (uint a = 0; a < ndim; ++a) {
|
|
23
|
+
float delta = positions[a * n + marker] - midpoint[a];
|
|
24
|
+
distance_squared += delta * delta;
|
|
25
|
+
}
|
|
26
|
+
bool inside = distance_squared <= params[0] * params[0];
|
|
27
|
+
if (params[1] == 1.0f) inside = !inside;
|
|
28
|
+
inside_flag[marker] = inside ? 1 : 0;
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_KERNEL = make_kernel(
|
|
32
|
+
"simgeometry_contains_ball",
|
|
33
|
+
_SOURCE,
|
|
34
|
+
inputs=["positions", "midpoint", "params"],
|
|
35
|
+
outputs=["inside_flag"],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def contains_ball(
|
|
40
|
+
positions: Array,
|
|
41
|
+
mask: Array,
|
|
42
|
+
midpoint: Array,
|
|
43
|
+
radius: float,
|
|
44
|
+
inverted: int,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Set ``mask[marker]`` to whether the marker lies within ``radius`` of ``midpoint``."""
|
|
47
|
+
if not positions.shape[1]:
|
|
48
|
+
return
|
|
49
|
+
flags = np.zeros(positions.shape[1], dtype=np.uint8)
|
|
50
|
+
_KERNEL(
|
|
51
|
+
positions, midpoint, np.array([radius, inverted], dtype=np.float32), out=flags
|
|
52
|
+
)
|
|
53
|
+
mask[:] = flags.astype(bool)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The NumPy version of the ``contains_ball`` kernel, run when Pyccel is unavailable.
|
|
2
|
+
|
|
3
|
+
Same arguments and result as ``contains_ball_pyccel.py``, vectorized over the markers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def contains_ball(
|
|
12
|
+
positions: np.ndarray,
|
|
13
|
+
mask: np.ndarray,
|
|
14
|
+
midpoint: np.ndarray,
|
|
15
|
+
radius: float,
|
|
16
|
+
inverted: int,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Set ``mask`` to whether each marker lies within ``radius`` of ``midpoint``."""
|
|
19
|
+
inside = (
|
|
20
|
+
np.sum((positions - np.asarray(midpoint)[:, None]) ** 2, axis=0)
|
|
21
|
+
<= radius * radius
|
|
22
|
+
)
|
|
23
|
+
mask[...] = ~inside if inverted == 1 else inside
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Mask of the markers inside a ball (disc in 2D, sphere in 3D, any dimension).
|
|
2
|
+
|
|
3
|
+
The Pyccel version (CPU) of the ``contains_ball`` kernel; ``contains_ball_cuda.cu``
|
|
4
|
+
next to this file is the CUDA version (GPU), with the same arguments. ``positions`` is ``(ndim, N)`` (component-major). A point
|
|
5
|
+
on the surface counts as inside. With ``inverted == 1`` the mask is negated.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def contains_ball(
|
|
10
|
+
positions: "float[:,:]",
|
|
11
|
+
mask: "bool[:]",
|
|
12
|
+
midpoint: "float[:]",
|
|
13
|
+
radius: float,
|
|
14
|
+
inverted: int,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Set ``mask[marker]`` to whether the marker lies within ``radius`` of ``midpoint``."""
|
|
17
|
+
num_markers = positions.shape[1]
|
|
18
|
+
ndim = positions.shape[0]
|
|
19
|
+
radius_squared = radius * radius
|
|
20
|
+
# fmt: off
|
|
21
|
+
#$ omp parallel for private(distance_squared, axis, delta, inside) schedule(dynamic, 4096)
|
|
22
|
+
# fmt: on
|
|
23
|
+
for marker in range(num_markers):
|
|
24
|
+
distance_squared = 0.0
|
|
25
|
+
for axis in range(ndim):
|
|
26
|
+
delta = positions[axis, marker] - midpoint[axis]
|
|
27
|
+
distance_squared += delta * delta
|
|
28
|
+
inside = distance_squared <= radius_squared
|
|
29
|
+
if inverted == 1:
|
|
30
|
+
inside = not inside
|
|
31
|
+
mask[marker] = inside
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""The contains_box kernel: Pyccel, NumPy and CUDA versions with the same arguments.
|
|
2
|
+
|
|
3
|
+
``kernel(...)`` runs the CUDA version when the arrays are CuPy arrays, and
|
|
4
|
+
otherwise the Pyccel version, or the NumPy version if Pyccel is unavailable
|
|
5
|
+
(see ``cunumpy.kernels.set_host_kernel_implementation`` to choose).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cunumpy as xp
|
|
9
|
+
|
|
10
|
+
from simgeometry.kernels._build import KERNEL_OPTIONS
|
|
11
|
+
|
|
12
|
+
kernel = xp.kernels.Kernel.from_folder(__name__, **KERNEL_OPTIONS)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The CUDA version (GPU) of the contains_box kernel; contains_box_pyccel.py next
|
|
2
|
+
// to this file is the Pyccel version (CPU), with the same arguments. Loaded by
|
|
3
|
+
// picnic.kernels.catalog: one thread per marker; positions are (ndim, N).
|
|
4
|
+
//
|
|
5
|
+
// mask[marker] = marker inside the box [lower, upper] (negated if inverted).
|
|
6
|
+
|
|
7
|
+
#include "cunumpy/array_view.cuh"
|
|
8
|
+
|
|
9
|
+
extern "C" __global__
|
|
10
|
+
void contains_box(
|
|
11
|
+
Array2D<double> positions,
|
|
12
|
+
Array1D<char> mask,
|
|
13
|
+
Array1D<double> lower,
|
|
14
|
+
Array1D<double> upper,
|
|
15
|
+
int inverted
|
|
16
|
+
) {
|
|
17
|
+
const long long marker = blockDim.x * (long long)blockIdx.x + threadIdx.x;
|
|
18
|
+
if (marker >= positions.shape[1]) return;
|
|
19
|
+
bool inside = true;
|
|
20
|
+
for (long long axis = 0; axis < positions.shape[0]; ++axis) {
|
|
21
|
+
const double x = positions(axis, marker);
|
|
22
|
+
if (x < lower(axis) || x > upper(axis)) inside = false;
|
|
23
|
+
}
|
|
24
|
+
if (inverted == 1) inside = !inside;
|
|
25
|
+
mask(marker) = inside ? 1 : 0;
|
|
26
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""The Metal version (Apple-silicon GPU, float32) of the ``contains_box`` kernel.
|
|
2
|
+
|
|
3
|
+
``contains_box_pyccel.py`` next to this file is the CPU version with the same
|
|
4
|
+
arguments. One thread per marker (``positions`` is ``(ndim, N)``); see :mod:`simgeometry.kernels._metal`. The test is
|
|
5
|
+
done in float32, so a marker within about 1e-7 (relative) of the boundary may be
|
|
6
|
+
classified differently from the CPU version.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from simgeometry._types import Array
|
|
14
|
+
from simgeometry.kernels._metal import make_kernel
|
|
15
|
+
|
|
16
|
+
_SOURCE = """
|
|
17
|
+
uint marker = thread_position_in_grid.x;
|
|
18
|
+
uint n = positions_shape[1];
|
|
19
|
+
if (marker >= n) return;
|
|
20
|
+
uint ndim = positions_shape[0];
|
|
21
|
+
bool inside = true;
|
|
22
|
+
for (uint a = 0; a < ndim; ++a) {
|
|
23
|
+
float x = positions[a * n + marker];
|
|
24
|
+
if (x < lower[a] || x > upper[a]) inside = false;
|
|
25
|
+
}
|
|
26
|
+
if (flags[0] == 1) inside = !inside;
|
|
27
|
+
inside_flag[marker] = inside ? 1 : 0;
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_KERNEL = make_kernel(
|
|
31
|
+
"simgeometry_contains_box",
|
|
32
|
+
_SOURCE,
|
|
33
|
+
inputs=["positions", "lower", "upper", "flags"],
|
|
34
|
+
outputs=["inside_flag"],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def contains_box(
|
|
39
|
+
positions: Array,
|
|
40
|
+
mask: Array,
|
|
41
|
+
lower: Array,
|
|
42
|
+
upper: Array,
|
|
43
|
+
inverted: int,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Set ``mask[marker]`` to whether the marker lies in the box ``[lower, upper]``."""
|
|
46
|
+
if not positions.shape[1]:
|
|
47
|
+
return
|
|
48
|
+
flags = np.zeros(positions.shape[1], dtype=np.uint8)
|
|
49
|
+
_KERNEL(positions, lower, upper, np.array([inverted], dtype=np.int32), out=flags)
|
|
50
|
+
mask[:] = flags.astype(bool)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""The NumPy version of the ``contains_box`` kernel, run when Pyccel is unavailable.
|
|
2
|
+
|
|
3
|
+
Same arguments and result as ``contains_box_pyccel.py``, vectorized over the markers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def contains_box(
|
|
12
|
+
positions: np.ndarray,
|
|
13
|
+
mask: np.ndarray,
|
|
14
|
+
lower: np.ndarray,
|
|
15
|
+
upper: np.ndarray,
|
|
16
|
+
inverted: int,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Set ``mask`` to whether each marker lies in the box ``[lower, upper]``."""
|
|
19
|
+
inside = np.all(
|
|
20
|
+
(positions >= np.asarray(lower)[:, None])
|
|
21
|
+
& (positions <= np.asarray(upper)[:, None]),
|
|
22
|
+
axis=0,
|
|
23
|
+
)
|
|
24
|
+
mask[...] = ~inside if inverted == 1 else inside
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Mask of the markers inside an axis-aligned box (any dimension).
|
|
2
|
+
|
|
3
|
+
The Pyccel version (CPU) of the ``contains_box`` kernel; ``contains_box_cuda.cu``
|
|
4
|
+
next to this file is the CUDA version (GPU), with the same arguments. ``positions`` is ``(ndim, N)`` (component-major). A point
|
|
5
|
+
on the box surface counts as inside. With ``inverted == 1`` the mask is
|
|
6
|
+
negated: the solid is everything outside the box.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def contains_box(
|
|
11
|
+
positions: "float[:,:]",
|
|
12
|
+
mask: "bool[:]",
|
|
13
|
+
lower: "float[:]",
|
|
14
|
+
upper: "float[:]",
|
|
15
|
+
inverted: int,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Set ``mask[marker]`` to whether the marker lies in the box ``[lower, upper]``."""
|
|
18
|
+
num_markers = positions.shape[1]
|
|
19
|
+
ndim = positions.shape[0]
|
|
20
|
+
# fmt: off
|
|
21
|
+
#$ omp parallel for private(inside, axis) schedule(dynamic, 4096)
|
|
22
|
+
# fmt: on
|
|
23
|
+
for marker in range(num_markers):
|
|
24
|
+
inside = True
|
|
25
|
+
for axis in range(ndim):
|
|
26
|
+
if positions[axis, marker] < lower[axis]:
|
|
27
|
+
inside = False
|
|
28
|
+
if positions[axis, marker] > upper[axis]:
|
|
29
|
+
inside = False
|
|
30
|
+
if inverted == 1:
|
|
31
|
+
inside = not inside
|
|
32
|
+
mask[marker] = inside
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""The contains_polygon_2d kernel: Pyccel, NumPy and CUDA versions with the same arguments.
|
|
2
|
+
|
|
3
|
+
``kernel(...)`` runs the CUDA version when the arrays are CuPy arrays, and
|
|
4
|
+
otherwise the Pyccel version, or the NumPy version if Pyccel is unavailable
|
|
5
|
+
(see ``cunumpy.kernels.set_host_kernel_implementation`` to choose).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cunumpy as xp
|
|
9
|
+
|
|
10
|
+
from simgeometry.kernels._build import KERNEL_OPTIONS
|
|
11
|
+
|
|
12
|
+
kernel = xp.kernels.Kernel.from_folder(__name__, **KERNEL_OPTIONS)
|