PyRXMesh 0.2.1rc0__cp311-cp311-win_amd64.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.
pyrxmesh/__init__.py ADDED
@@ -0,0 +1,382 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ _dll_handles = []
8
+
9
+
10
+ def _add_windows_dll_dir(path: Path) -> None:
11
+ if os.name != "nt" or not path.exists():
12
+ return
13
+ try:
14
+ handle = os.add_dll_directory(str(path))
15
+ except (FileNotFoundError, OSError):
16
+ return
17
+ _dll_handles.append(handle)
18
+
19
+
20
+ def _prepare_dll_search_path() -> None:
21
+ if os.name != "nt":
22
+ return
23
+
24
+ package_dir = Path(__file__).resolve().parent
25
+ _add_windows_dll_dir(package_dir)
26
+ _add_windows_dll_dir(package_dir / "bin")
27
+ _add_windows_dll_dir(package_dir / "lib")
28
+
29
+ for env_name in ("CUDA_PATH", "CUDA_HOME"):
30
+ cuda_root = os.environ.get(env_name)
31
+ if cuda_root:
32
+ _add_windows_dll_dir(Path(cuda_root) / "bin")
33
+
34
+ for env_name, cuda_root in os.environ.items():
35
+ if env_name.startswith("CUDA_PATH_V") and cuda_root:
36
+ _add_windows_dll_dir(Path(cuda_root) / "bin")
37
+
38
+ default_cuda_root = Path("C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA")
39
+ if default_cuda_root.exists():
40
+ for cuda_bin in sorted(default_cuda_root.glob("v*/bin"), reverse=True):
41
+ _add_windows_dll_dir(cuda_bin)
42
+
43
+
44
+ _prepare_dll_search_path()
45
+
46
+ _extension_import_error: ModuleNotFoundError | None = None
47
+
48
+ try:
49
+ from ._rxmesh import (
50
+ Attribute,
51
+ CGSolver,
52
+ CholeskySolver,
53
+ DEdgeHandle,
54
+ DType,
55
+ DenseMatrix,
56
+ EdgeAttributeFloat32,
57
+ EdgeAttributeFloat64,
58
+ EdgeAttributeInt32,
59
+ EdgeAttributeInt8,
60
+ EdgeHandle,
61
+ ElementKind,
62
+ FaceAttributeFloat32,
63
+ FaceAttributeFloat64,
64
+ FaceAttributeInt32,
65
+ FaceAttributeInt8,
66
+ FaceHandle,
67
+ HessianSparseMatrix,
68
+ JacobianSparseMatrix,
69
+ Layout,
70
+ Location,
71
+ LogLevel,
72
+ Op,
73
+ PCGSolver,
74
+ QRSolver,
75
+ RXMeshStatic,
76
+ LUSolver,
77
+ SparseMatrix,
78
+ VertexAttributeFloat32,
79
+ VertexAttributeFloat64,
80
+ VertexAttributeInt32,
81
+ VertexAttributeInt8,
82
+ VertexHandle,
83
+ abi_version,
84
+ build_config_tag,
85
+ create_plane,
86
+ cuda_stream_synchronize,
87
+ cuDSSCholeskySolver,
88
+ has_cudss,
89
+ init,
90
+ show,
91
+ )
92
+ except ModuleNotFoundError as exc:
93
+ if exc.name != f"{__name__}._rxmesh":
94
+ raise
95
+ _extension_import_error = exc
96
+ __all__ = []
97
+ else:
98
+ __all__ = [
99
+ "Attribute",
100
+ "CGSolver",
101
+ "CholeskySolver",
102
+ "DEdgeHandle",
103
+ "DType",
104
+ "DenseMatrix",
105
+ "EdgeAttributeFloat32",
106
+ "EdgeAttributeFloat64",
107
+ "EdgeAttributeInt32",
108
+ "EdgeAttributeInt8",
109
+ "EdgeHandle",
110
+ "ElementKind",
111
+ "FaceAttributeFloat32",
112
+ "FaceAttributeFloat64",
113
+ "FaceAttributeInt32",
114
+ "FaceAttributeInt8",
115
+ "FaceHandle",
116
+ "HessianSparseMatrix",
117
+ "JacobianSparseMatrix",
118
+ "Layout",
119
+ "Location",
120
+ "LogLevel",
121
+ "Op",
122
+ "PCGSolver",
123
+ "QRSolver",
124
+ "RXMeshStatic",
125
+ "LUSolver",
126
+ "SparseMatrix",
127
+ "VertexAttributeFloat32",
128
+ "VertexAttributeFloat64",
129
+ "VertexAttributeInt32",
130
+ "VertexAttributeInt8",
131
+ "VertexHandle",
132
+ "abi_version",
133
+ "build_config_tag",
134
+ "create_plane",
135
+ "cuda_stream_synchronize",
136
+ "cuDSSCholeskySolver",
137
+ "has_cudss",
138
+ "init",
139
+ "show",
140
+ ]
141
+
142
+ def _require_torch():
143
+ try:
144
+ import torch
145
+ except ImportError as exc:
146
+ raise ImportError(
147
+ "PyTorch is required for PyRXMesh torch interop helpers. "
148
+ "Install torch or use the DLPack/NumPy APIs directly."
149
+ ) from exc
150
+ return torch
151
+
152
+ class _DenseMatrixDlpackView:
153
+ def __init__(self, matrix, location):
154
+ self._matrix = matrix
155
+ self._location = location
156
+ self._is_host = location == Location.HOST
157
+ self._is_device = location == Location.DEVICE
158
+
159
+ def __dlpack__(self, stream=None):
160
+ return self._matrix.to_dlpack(self._location, stream=stream)
161
+
162
+ def __dlpack_device__(self):
163
+ if self._is_host:
164
+ return (1, 0)
165
+ if self._is_device:
166
+ torch = _require_torch()
167
+ return (2, torch.cuda.current_device())
168
+ raise ValueError(
169
+ "DenseMatrix.to_torch() location must be Location.HOST or "
170
+ "Location.DEVICE."
171
+ )
172
+
173
+ class _SparseMatrixDlpackView:
174
+ def __init__(self, matrix, location, component):
175
+ self._matrix = matrix
176
+ self._location = location
177
+ self._component = component
178
+ self._is_host = location == Location.HOST
179
+ self._is_device = location == Location.DEVICE
180
+
181
+ def __dlpack__(self, stream=None):
182
+ if self._component == "row_ptr":
183
+ return self._matrix._row_ptr_dlpack(
184
+ self._location,
185
+ stream=stream,
186
+ )
187
+ if self._component == "col_idx":
188
+ return self._matrix._col_indices_dlpack(
189
+ self._location,
190
+ stream=stream,
191
+ )
192
+ if self._component == "values":
193
+ return self._matrix._values_dlpack(
194
+ self._location,
195
+ stream=stream,
196
+ )
197
+ raise ValueError("Unsupported SparseMatrix DLPack component.")
198
+
199
+ def __dlpack_device__(self):
200
+ if self._is_host:
201
+ return (1, 0)
202
+ if self._is_device:
203
+ torch = _require_torch()
204
+ return (2, torch.cuda.current_device())
205
+ raise ValueError(
206
+ "SparseMatrix.to_torch() location must be Location.HOST or "
207
+ "Location.DEVICE."
208
+ )
209
+
210
+ def _dense_matrix_to_torch(self, location=Location.DEVICE):
211
+ torch = _require_torch()
212
+ return torch.utils.dlpack.from_dlpack(
213
+ _DenseMatrixDlpackView(self, location)
214
+ )
215
+
216
+ def _dense_matrix_from_torch_copy(source):
217
+ tensor = source.detach() if hasattr(source, "detach") else source
218
+ return DenseMatrix.from_dlpack_copy(tensor)
219
+
220
+ def _sparse_matrix_to_torch(self, location=Location.DEVICE):
221
+ torch = _require_torch()
222
+ crow = torch.utils.dlpack.from_dlpack(
223
+ _SparseMatrixDlpackView(self, location, "row_ptr")
224
+ )
225
+ col = torch.utils.dlpack.from_dlpack(
226
+ _SparseMatrixDlpackView(self, location, "col_idx")
227
+ )
228
+ val = torch.utils.dlpack.from_dlpack(
229
+ _SparseMatrixDlpackView(self, location, "values")
230
+ )
231
+ return torch.sparse_csr_tensor(
232
+ crow,
233
+ col,
234
+ val,
235
+ size=self.shape,
236
+ device=val.device,
237
+ )
238
+
239
+ def _sparse_matrix_from_torch_values_copy(
240
+ self,
241
+ values,
242
+ target=Location.ALL,
243
+ stream=None,
244
+ ):
245
+ tensor = values.detach() if hasattr(values, "detach") else values
246
+ self.from_dlpack_values_copy(tensor, target=target, stream=stream)
247
+ return self
248
+
249
+ def _torch_dtype_name(dtype):
250
+ torch = _require_torch()
251
+ if dtype == torch.float32:
252
+ return "float32"
253
+ if dtype == torch.float64:
254
+ return "float64"
255
+ if dtype == torch.int32:
256
+ return "int32"
257
+ raise TypeError(f"Unsupported torch dtype for PyRXMesh copy: {dtype}")
258
+
259
+ def _sparse_matrix_from_torch_copy(source, dtype=None, stream=None):
260
+ torch = _require_torch()
261
+ if source.layout != torch.sparse_csr:
262
+ raise TypeError(
263
+ "SparseMatrix.from_torch_copy() expects a torch sparse CSR "
264
+ "tensor."
265
+ )
266
+ value_dtype = dtype or _torch_dtype_name(source.values().dtype)
267
+ return SparseMatrix.from_dlpack_copy(
268
+ source.crow_indices().detach(),
269
+ source.col_indices().detach(),
270
+ source.values().detach(),
271
+ tuple(source.shape),
272
+ dtype=value_dtype,
273
+ stream=stream,
274
+ )
275
+
276
+ def _attribute_to_torch(self, location=Location.DEVICE):
277
+ torch = _require_torch()
278
+ return torch.utils.dlpack.from_dlpack(self.to_dlpack(location))
279
+
280
+ def _attribute_from_torch_copy(self, values, target=Location.ALL):
281
+ tensor = values.detach() if hasattr(values, "detach") else values
282
+ self.from_dlpack_copy(tensor, target=target)
283
+ return self
284
+
285
+ def _sparse_matrix_to_scipy_csr(self):
286
+ try:
287
+ import scipy.sparse as scipy_sparse
288
+ except ImportError as exc:
289
+ raise ImportError(
290
+ "SciPy is required for SparseMatrix.to_scipy_csr(). "
291
+ "Install scipy or use to_numpy()."
292
+ ) from exc
293
+
294
+ row_ptr, col_idx, values = self.to_numpy(Location.HOST)
295
+ return scipy_sparse.csr_matrix((values, col_idx, row_ptr), shape=self.shape)
296
+
297
+ def _sparse_matrix_to_scipy_csr_copy(self, source=Location.HOST, stream=None):
298
+ try:
299
+ import scipy.sparse as scipy_sparse
300
+ except ImportError as exc:
301
+ raise ImportError(
302
+ "SciPy is required for SparseMatrix.to_scipy_csr_copy(). "
303
+ "Install scipy or use to_numpy_copy()."
304
+ ) from exc
305
+
306
+ row_ptr, col_idx, values = self.to_numpy_copy(source=source, stream=stream)
307
+ return scipy_sparse.csr_matrix((values, col_idx, row_ptr), shape=self.shape)
308
+
309
+ _native_sparse_matrix_multiply_vector = SparseMatrix.multiply_vector
310
+
311
+ def _sparse_matrix_multiply_vector(
312
+ self,
313
+ vector,
314
+ stream=None,
315
+ ):
316
+ if isinstance(vector, DenseMatrix):
317
+ return _native_sparse_matrix_multiply_vector(
318
+ self,
319
+ vector,
320
+ stream,
321
+ )
322
+
323
+ import numpy as np
324
+
325
+ values = np.asarray(vector)
326
+ if values.ndim == 1:
327
+ values = values.reshape(-1, 1)
328
+ if values.ndim != 2 or values.shape[1] != 1:
329
+ raise ValueError(
330
+ "SparseMatrix.multiply_vector() expects a 1D array or a "
331
+ "DenseMatrix/array with shape (cols, 1)."
332
+ )
333
+ if values.shape[0] != self.cols:
334
+ raise ValueError(
335
+ "SparseMatrix.multiply_vector() vector length must match matrix.cols."
336
+ )
337
+
338
+ dense = DenseMatrix(
339
+ self.cols,
340
+ 1,
341
+ dtype=self.dtype,
342
+ location=Location.ALL,
343
+ )
344
+ dense.from_numpy_copy(values, target=Location.ALL, stream=stream)
345
+ return _native_sparse_matrix_multiply_vector(self, dense, stream)
346
+
347
+ DenseMatrix.to_torch = _dense_matrix_to_torch
348
+ DenseMatrix.from_torch_copy = staticmethod(_dense_matrix_from_torch_copy)
349
+ SparseMatrix.to_torch = _sparse_matrix_to_torch
350
+ SparseMatrix.from_torch_copy = staticmethod(_sparse_matrix_from_torch_copy)
351
+ SparseMatrix.from_torch_values_copy = _sparse_matrix_from_torch_values_copy
352
+ SparseMatrix.to_scipy_csr = _sparse_matrix_to_scipy_csr
353
+ SparseMatrix.to_scipy_csr_copy = _sparse_matrix_to_scipy_csr_copy
354
+ SparseMatrix.multiply_vector = _sparse_matrix_multiply_vector
355
+ _attribute_types = (
356
+ Attribute,
357
+ VertexAttributeFloat32,
358
+ VertexAttributeFloat64,
359
+ VertexAttributeInt32,
360
+ VertexAttributeInt8,
361
+ EdgeAttributeFloat32,
362
+ EdgeAttributeFloat64,
363
+ EdgeAttributeInt32,
364
+ EdgeAttributeInt8,
365
+ FaceAttributeFloat32,
366
+ FaceAttributeFloat64,
367
+ FaceAttributeInt32,
368
+ FaceAttributeInt8,
369
+ )
370
+ for _attribute_type in _attribute_types:
371
+ _attribute_type.to_torch = _attribute_to_torch
372
+ _attribute_type.from_torch_copy = _attribute_from_torch_copy
373
+ del _attribute_type, _attribute_types
374
+
375
+
376
+ def __getattr__(name: str):
377
+ if _extension_import_error is not None:
378
+ raise ModuleNotFoundError(
379
+ "pyrxmesh._rxmesh is not installed yet. Build/install PyRXMesh "
380
+ "before using runtime mesh bindings."
381
+ ) from _extension_import_error
382
+ raise AttributeError(name)
Binary file
@@ -0,0 +1,13 @@
1
+ {
2
+ "abi_version": "2",
3
+ "pyrxmesh_version": "0.2.1rc0",
4
+ "build_config_tag": "abi=2,pyrxmesh=0.2.1rc0,rxmesh_repo=https://github.com/owensgroup/RXMesh.git,rxmesh_tag=main,rxmesh_source=,polyscope=ON,double=OFF,cudss=OFF,suitesparse=OFF,cuda_arch=89",
5
+ "rxmesh_git_repository": "https://github.com/owensgroup/RXMesh.git",
6
+ "rxmesh_git_tag": "main",
7
+ "rxmesh_source_dir": "",
8
+ "use_polyscope": "ON",
9
+ "use_double": "OFF",
10
+ "use_cudss": "OFF",
11
+ "use_suitesparse": "OFF",
12
+ "cuda_architectures": "89"
13
+ }
pyrxmesh/cmake_dir.py ADDED
@@ -0,0 +1,17 @@
1
+ """Print the installed PyRXMesh CMake package directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def get_cmake_dir() -> Path:
9
+ return Path(__file__).resolve().parent / "cmake"
10
+
11
+
12
+ def main() -> None:
13
+ print(get_cmake_dir())
14
+
15
+
16
+ if __name__ == "__main__":
17
+ main()
pyrxmesh/diff.py ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from . import HessianSparseMatrix, JacobianSparseMatrix
4
+
5
+ __all__ = [
6
+ "HessianSparseMatrix",
7
+ "JacobianSparseMatrix",
8
+ ]
pyrxmesh/geometry.py ADDED
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from . import create_plane
4
+
5
+ __all__ = ["create_plane"]
pyrxmesh/plugin.py ADDED
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import re
5
+ from pathlib import Path
6
+
7
+
8
+ _MODULE_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
9
+
10
+
11
+ def _write(path: Path, content: str, force: bool) -> None:
12
+ if path.exists() and not force:
13
+ raise FileExistsError(f"{path} already exists; pass --force to overwrite it")
14
+ path.parent.mkdir(parents=True, exist_ok=True)
15
+ path.write_text(content, encoding="utf-8", newline="\n")
16
+
17
+
18
+ def _pyproject(module: str) -> str:
19
+ dist_name = module.replace("_", "-")
20
+ return f"""[build-system]
21
+ requires = [
22
+ "scikit-build-core>=0.12",
23
+ "pybind11>=2.13",
24
+ ]
25
+ build-backend = "scikit_build_core.build"
26
+
27
+ [project]
28
+ name = "{dist_name}"
29
+ version = "0.1.0"
30
+ description = "Custom PyRXMesh CUDA kernels"
31
+ requires-python = ">=3.10"
32
+ dependencies = [
33
+ "PyRXMesh",
34
+ ]
35
+
36
+ [tool.scikit-build]
37
+ minimum-version = "0.12"
38
+ cmake.version = ">=3.24,<4"
39
+ cmake.build-type = "Release"
40
+ build-dir = "build/{{wheel_tag}}"
41
+ wheel.packages = ["src/{module}"]
42
+ build.targets = ["_{module}"]
43
+ install.components = ["python"]
44
+ """
45
+
46
+
47
+ def _cmake(module: str) -> str:
48
+ return f"""cmake_minimum_required(VERSION 3.24 FATAL_ERROR)
49
+
50
+ if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
51
+ set(CMAKE_CUDA_ARCHITECTURES native)
52
+ endif()
53
+
54
+ project({module} LANGUAGES C CXX CUDA)
55
+
56
+ set(CMAKE_CXX_STANDARD 17)
57
+ set(CMAKE_CUDA_STANDARD 17)
58
+ set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
59
+ set(CMAKE_CUDA_STANDARD_REQUIRED TRUE)
60
+ set(CMAKE_CXX_EXTENSIONS OFF)
61
+
62
+ find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module)
63
+
64
+ execute_process(
65
+ COMMAND "${{Python3_EXECUTABLE}}" -m pyrxmesh.cmake_dir
66
+ OUTPUT_VARIABLE pyrxmesh_DIR
67
+ OUTPUT_STRIP_TRAILING_WHITESPACE
68
+ COMMAND_ERROR_IS_FATAL ANY
69
+ )
70
+
71
+ find_package(pyrxmesh CONFIG REQUIRED PATHS "${{pyrxmesh_DIR}}" NO_DEFAULT_PATH)
72
+
73
+ pyrxmesh_add_plugin(_{module} src/{module}.cu)
74
+
75
+ install(TARGETS _{module}
76
+ LIBRARY DESTINATION {module} COMPONENT python
77
+ RUNTIME DESTINATION {module} COMPONENT python
78
+ )
79
+ """
80
+
81
+
82
+ def _source(module: str) -> str:
83
+ return f"""#include <pybind11/pybind11.h>
84
+
85
+ #include "pyrxmesh/plugin_api.h"
86
+
87
+ namespace py = pybind11;
88
+ using namespace rxmesh;
89
+
90
+ void compute_edge_lengths(py::object mesh_obj,
91
+ py::object coords_obj,
92
+ py::object out_obj)
93
+ {{
94
+ auto coords = pyrxmesh::vertex_attribute<float>(coords_obj);
95
+ auto out = pyrxmesh::edge_attribute<float>(out_obj);
96
+
97
+ if (coords.get_num_attributes() < 3) {{
98
+ throw std::runtime_error("coords must have at least 3 values per vertex.");
99
+ }}
100
+ if (out.get_num_attributes() != 1) {{
101
+ throw std::runtime_error("out must have exactly 1 value per edge.");
102
+ }}
103
+
104
+ pyrxmesh::for_each<Op::EV, 256>(
105
+ mesh_obj,
106
+ [coords, out] __device__(const EdgeHandle& eh,
107
+ const VertexIterator& iter) mutable {{
108
+ const Eigen::Vector3f a = coords.to_eigen<3>(iter[0]);
109
+ const Eigen::Vector3f b = coords.to_eigen<3>(iter[1]);
110
+ out(eh) = (a - b).norm();
111
+ }});
112
+ }}
113
+
114
+ PYBIND11_MODULE(_{module}, m)
115
+ {{
116
+ pyrxmesh::require_compatible_runtime(m);
117
+
118
+ m.def("compute_edge_lengths",
119
+ &compute_edge_lengths,
120
+ py::arg("mesh"),
121
+ py::arg("coords"),
122
+ py::arg("out"));
123
+ }}
124
+ """
125
+
126
+
127
+ def _init(module: str) -> str:
128
+ return f"""from __future__ import annotations
129
+
130
+ from ._{module} import compute_edge_lengths
131
+
132
+ __all__ = ["compute_edge_lengths"]
133
+ """
134
+
135
+
136
+ def _readme(module: str) -> str:
137
+ return f"""# {module}
138
+
139
+ Custom CUDA kernels for PyRXMesh.
140
+
141
+ ## Layout
142
+
143
+ ```text
144
+ {module}/
145
+ pyproject.toml
146
+ CMakeLists.txt
147
+ src/
148
+ {module}.cu # CUDA source for the plugin
149
+ {module}/__init__.py # Python package source
150
+ my_script.py # your own scripts can live here
151
+ ```
152
+
153
+ Python sources live under `src/`, so the plugin root never shadows the
154
+ installed package. Run scripts can sit at the plugin root and `import {module}`
155
+ directly with no `sys.path` setup.
156
+
157
+ ## Build
158
+
159
+ Build inside the same conda environment where `pyrxmesh` is installed:
160
+
161
+ ```bash
162
+ python -m pip install -v --no-build-isolation .
163
+ ```
164
+
165
+ After installation, `{module}` is available from any directory in the env,
166
+ just like `pyrxmesh`.
167
+
168
+ ## Use from Python
169
+
170
+ ```python
171
+ import pyrxmesh as rx
172
+ import {module}
173
+
174
+ mesh = rx.RXMeshStatic("mesh.obj")
175
+ coords = mesh.input_vertex_coordinates()
176
+ edge_lengths = mesh.add_edge_attribute("edge_lengths", dtype="float32", dim=1)
177
+
178
+ {module}.compute_edge_lengths(mesh, coords, edge_lengths)
179
+ print(edge_lengths.to_numpy_copy(source=rx.Location.DEVICE))
180
+ ```
181
+ """
182
+
183
+
184
+ def init_plugin(module: str, output_dir: Path, force: bool = False) -> Path:
185
+ if not _MODULE_RE.match(module):
186
+ raise ValueError(
187
+ "Plugin module name must be a valid Python identifier, for example "
188
+ "'my_kernels'."
189
+ )
190
+
191
+ root = output_dir / module
192
+ root.mkdir(parents=True, exist_ok=True)
193
+
194
+ _write(root / "pyproject.toml", _pyproject(module), force)
195
+ _write(root / "CMakeLists.txt", _cmake(module), force)
196
+ _write(root / "README.md", _readme(module), force)
197
+ _write(root / "src" / f"{module}.cu", _source(module), force)
198
+ _write(root / "src" / module / "__init__.py", _init(module), force)
199
+
200
+ return root
201
+
202
+
203
+ def main(argv: list[str] | None = None) -> None:
204
+ parser = argparse.ArgumentParser(description=__doc__)
205
+ subparsers = parser.add_subparsers(dest="command", required=True)
206
+
207
+ init = subparsers.add_parser("init", help="Create a custom kernel plugin")
208
+ init.add_argument("module", help="Python module name, for example my_kernels")
209
+ init.add_argument(
210
+ "--output-dir",
211
+ type=Path,
212
+ default=Path.cwd(),
213
+ help="Directory where the plugin package directory will be created",
214
+ )
215
+ init.add_argument(
216
+ "--force",
217
+ action="store_true",
218
+ help="Overwrite existing scaffold files",
219
+ )
220
+
221
+ args = parser.parse_args(argv)
222
+
223
+ if args.command == "init":
224
+ root = init_plugin(args.module, args.output_dir, args.force)
225
+ print(root)
226
+
227
+
228
+ if __name__ == "__main__":
229
+ main()
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyRXMesh
3
+ Version: 0.2.1rc0
4
+ Summary: Python bindings for RXMesh, a CUDA/C++ library for mesh processing on the GPU.
5
+ Author: Ahmed Mahmoud
6
+ License-Expression: BSD-2-Clause
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: C++
10
+ Classifier: Programming Language :: Python :: 3
11
+ Project-URL: RXMesh, https://github.com/owensgroup/RXMesh
12
+ Project-URL: Documentation, https://ahdhn.github.io/RXMeshDocs/
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: numpy>=1.26
15
+ Description-Content-Type: text/markdown
16
+
17
+ # PyRXMesh
18
+
19
+ Python bindings for [RXMesh](https://github.com/owensgroup/RXMesh), a CUDA/C++ library for GPU mesh processing.
20
+
21
+ PyRXMesh allows you to keep mesh workflows in Python while using RXMesh data structures and GPU kernels underneath. It supports exchanging arrays and attributes with NumPy and PyTorch, building sparse matrices, and solving linear systems. We use a Python plugin to allow CUDA kernels to be written, compiled, and used directly in Python. Check out [CUSTOM_CUDA_PLUGINS.md](CUSTOM_CUDA_PLUGINS.md) for more details.
22
+
23
+
24
+ ## Installation
25
+
26
+ Install PyRXMesh with:
27
+
28
+ ```bash
29
+ python -m pip install PyRXMesh
30
+ ```
31
+
32
+ PyRXMesh is a CUDA package. You need an NVIDIA driver compatible with the CUDA version used by the wheel. For source builds and development setup, see [DEVELOPING.md](DEVELOPING.md).
33
+
34
+ ## Simple Example
35
+
36
+ ```python
37
+ import pyrxmesh as rx
38
+
39
+ #Use GPU 0
40
+ rx.init(0)
41
+
42
+ mesh = rx.RXMeshStatic("mesh.obj")
43
+ print(mesh.num_vertices, mesh.num_edges, mesh.num_faces)
44
+
45
+ rx.show()
46
+ ```
47
+
48
+ The repository includes a small viewer example:
49
+
50
+ ```bash
51
+ python examples/load_and_show.py --input mesh.obj
52
+ ```
53
+
54
+ ## Mesh Attributes
55
+
56
+ Attributes are data attached to vertices, edges, or faces
57
+
58
+ ```python
59
+ import pyrxmesh as rx
60
+
61
+ mesh = rx.RXMeshStatic("mesh.obj")
62
+
63
+ coords = mesh.input_vertex_coordinates()
64
+ velocity = mesh.add_vertex_attribute("velocity", dtype="float32",
65
+ dim=3, location=rx.Location.ALL)
66
+
67
+ velocity.reset(0.0, rx.Location.DEVICE)
68
+
69
+ host_coords = coords.to_numpy_copy(source=rx.Location.HOST)
70
+ print(host_coords.shape)
71
+ ```
72
+
73
+ ## NumPy And Torch Interop
74
+
75
+ Unsuffixed `to_*` methods return zero-copy views when possible. Methods ending
76
+ in `_copy` make an explicit copy.
77
+
78
+ ```python
79
+ import pyrxmesh as rx
80
+
81
+ mesh = rx.RXMeshStatic("mesh.obj")
82
+ attr = mesh.add_vertex_attribute("temperature", dtype="float32",
83
+ dim=1, location=rx.Location.ALL)
84
+
85
+ attr.reset(1.0, rx.Location.ALL)
86
+
87
+ view = attr.to_numpy(rx.Location.HOST)
88
+ view[0, 0] = 42.0
89
+
90
+ owned = attr.to_numpy_copy(source=rx.Location.HOST)
91
+ print(owned[0, 0])
92
+ ```
93
+
94
+ Torch views use DLPack:
95
+
96
+ ```python
97
+ tensor = attr.to_torch(rx.Location.DEVICE)
98
+ tensor += 2.0
99
+ ```
100
+
101
+ ## Dense And Sparse Matrices
102
+
103
+ PyRXMesh exposes RXMesh dense matrices and CSR sparse matrices.
104
+
105
+ ```python
106
+ import numpy as np
107
+ import pyrxmesh as rx
108
+
109
+ matrix = rx.DenseMatrix(4, 3, dtype="float32", location=rx.Location.ALL)
110
+ values = np.arange(12, dtype=np.float32).reshape(4, 3)
111
+
112
+ matrix.from_numpy_copy(values, target=rx.Location.ALL)
113
+ print(matrix.norm2())
114
+ ```
115
+
116
+ Sparse matrices are CSR-only:
117
+
118
+ ```python
119
+ mesh = rx.RXMeshStatic("mesh.obj")
120
+ laplace_like = mesh.sparse_matrix(rx.Op.VV, dtype="float32")
121
+
122
+ row_ptr, col_idx, values = laplace_like.to_numpy_copy()
123
+ print(laplace_like.shape, laplace_like.nnz)
124
+ ```
125
+
126
+ You can multiply sparse matrices by dense vectors:
127
+
128
+ ```python
129
+ x = np.ones((laplace_like.cols, 1), dtype=np.float32)
130
+ y = laplace_like.multiply_vector(x)
131
+ print(y.to_numpy_copy(source=rx.Location.HOST))
132
+ ```
133
+
134
+ ## Solvers
135
+
136
+ RXMesh solver bindings work with `SparseMatrix` and `DenseMatrix` objects:
137
+
138
+ ```python
139
+ import numpy as np
140
+ import pyrxmesh as rx
141
+
142
+ row_ptr = np.array([0, 2, 5, 7], dtype=np.int32)
143
+ col_idx = np.array([0, 1, 0, 1, 2, 1, 2], dtype=np.int32)
144
+ values = np.array([4, 1, 1, 3, 1, 1, 2], dtype=np.float32)
145
+
146
+ A = rx.SparseMatrix.from_numpy_copy(row_ptr, col_idx, values,
147
+ shape=(3, 3), dtype="float32")
148
+
149
+ b = rx.DenseMatrix(3, 1, dtype="float32", location=rx.Location.ALL)
150
+ b.from_numpy_copy(np.array([[1], [2], [3]], dtype=np.float32))
151
+
152
+ solver = rx.CGSolver(A, unknown_dim=1)
153
+ x = solver.solve(b)
154
+
155
+ print(x.to_numpy_copy(source=rx.Location.HOST))
156
+ ```
157
+
158
+ ## Documentation
159
+ - [RXMeshDocs](https://ahdhn.github.io/RXMeshDocs/): RXMesh CUDA/C++ documentation website
160
+ - [DEVELOPING.md](DEVELOPING.md): build PyRXMesh from source and work against RXMesh forks, tags, or local checkouts.
161
+ - [CUSTOM_CUDA_PLUGINS.md](CUSTOM_CUDA_PLUGINS.md): write small compiled CUDA plugins that operate on PyRXMesh meshes and attributes.
@@ -0,0 +1,12 @@
1
+ pyrxmesh/__init__.py,sha256=5dZNYxEZ4cv80RpOhX6tC8GnWmu06wthtYabQZ3VMu4,12658
2
+ pyrxmesh/_rxmesh.cp311-win_amd64.pyd,sha256=pGNLpMtM2LxxLa60_mVfL_k-9otBUc0hsMbgV9yMfcY,67914752
3
+ pyrxmesh/build_config.json,sha256=Ym6uhIZbalIT0GaNbbMHcchhn-LBIMKPrrO2xk7_yB4,522
4
+ pyrxmesh/cmake_dir.py,sha256=ylPlG-hpZA2VJ-GlnicP2K7dGmxl_1ur-3zeaZVnTaY,312
5
+ pyrxmesh/diff.py,sha256=CbMedL95HUA7q8Lz-er2WqwH4D3SLdYbMHBwb_jTKGg,174
6
+ pyrxmesh/geometry.py,sha256=eX4y8mieixPWohWK3rFkG8DaY1ej46XmJmSf-KLihc8,96
7
+ pyrxmesh/plugin.py,sha256=CbDeNnAKGSeB12VmEdeHy48cgpEpymXYrge3RbGnXGg,6298
8
+ pyrxmesh-0.2.1rc0.dist-info/METADATA,sha256=e4aDfe1h3z18JsN-dfQP5-sumLPwDD-BjHCi-vvypiw,4587
9
+ pyrxmesh-0.2.1rc0.dist-info/WHEEL,sha256=NHmw5Qi_104FW4Xq2pbA3Gs3F6EqwqCa-xRmRnEiDI8,106
10
+ pyrxmesh-0.2.1rc0.dist-info/entry_points.txt,sha256=ZM8Ckpy6TpMGdTRL05cC3l2Vs9FmBu2_jZ6u9DwDujc,58
11
+ pyrxmesh-0.2.1rc0.dist-info/licenses/LICENSE,sha256=Mq7J1MpxYoaE1t_I7XkUNyOJqwslkMSXkMtnt8gtGkk,1372
12
+ pyrxmesh-0.2.1rc0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 0.12.2
3
+ Root-Is-Purelib: false
4
+ Tag: cp311-cp311-win_amd64
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pyrxmesh-plugin = pyrxmesh.plugin:main
3
+
@@ -0,0 +1,25 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Ahmed Mahmoud and PyRXMesh contributors
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.