PyRXMesh 0.2.1rc0__cp312-cp312-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 +382 -0
- pyrxmesh/_rxmesh.cp312-win_amd64.pyd +0 -0
- pyrxmesh/build_config.json +13 -0
- pyrxmesh/cmake_dir.py +17 -0
- pyrxmesh/diff.py +8 -0
- pyrxmesh/geometry.py +5 -0
- pyrxmesh/plugin.py +229 -0
- pyrxmesh-0.2.1rc0.dist-info/METADATA +161 -0
- pyrxmesh-0.2.1rc0.dist-info/RECORD +12 -0
- pyrxmesh-0.2.1rc0.dist-info/WHEEL +5 -0
- pyrxmesh-0.2.1rc0.dist-info/entry_points.txt +3 -0
- pyrxmesh-0.2.1rc0.dist-info/licenses/LICENSE +25 -0
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
pyrxmesh/geometry.py
ADDED
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.cp312-win_amd64.pyd,sha256=yUi4KG2OXS59Ah3sVTnl-Qb6dYdmXX6PT6IPly7iXDs,67932672
|
|
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=iRw_rFuoPx3ccZ--hmGPhTmXnY5HgQPGh1UyQokugHM,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,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.
|