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
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)