Skip to content

Commit 93b50e4

Browse files
committed
Py: Methods for UniformGrid, add tests for many functions
1 parent 86d79aa commit 93b50e4

File tree

4 files changed

+339
-7
lines changed

4 files changed

+339
-7
lines changed

pysplashsurf/pysplashsurf/pysplashsurf.pyi

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,26 @@ class UniformGrid:
270270
r"""
271271
Struct containing the parameters of the uniform grid used for the surface reconstruction
272272
"""
273-
...
273+
@property
274+
def aabb(self) -> Aabb3d:
275+
r"""
276+
The AABB of the grid containing all marching cubes vertices influenced by the particle kernels
277+
"""
278+
@property
279+
def cell_size(self) -> builtins.float:
280+
r"""
281+
Returns the cell size of the uniform grid (the marching cubes voxel size)
282+
"""
283+
@property
284+
def npoints_per_dim(self) -> builtins.list[builtins.int]:
285+
r"""
286+
Returns the number of points (marching cubes vertices) per dimension in the uniform grid
287+
"""
288+
@property
289+
def ncells_per_dim(self) -> builtins.list[builtins.int]:
290+
r"""
291+
Returns the number of cells (marching cubes voxels) per dimension in the uniform grid
292+
"""
274293

275294
class VertexVertexConnectivity:
276295
r"""
@@ -362,5 +381,3 @@ def reconstruction_pipeline(particles:numpy.typing.NDArray[typing.Any], *, attri
362381
363382
Note that smoothing length and cube size are given in multiples of the particle radius.
364383
"""
365-
366-
def triangulate_density_map(values:numpy.typing.NDArray[typing.Any], grid:UniformGrid, *, iso_surface_threshold:builtins.float) -> TriMesh3d: ...

pysplashsurf/src/mesh.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ where
339339
/// Enum specifying the type of mesh wrapped by a ``MeshWithData``
340340
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
341341
#[gen_stub_pyclass_enum]
342-
#[pyclass]
342+
#[pyclass(eq)]
343343
pub enum MeshType {
344344
/// 3D triangle mesh
345345
Tri3d,

pysplashsurf/src/uniform_grid.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
use crate::aabb::PyAabb3d;
2+
use crate::utils;
3+
use crate::utils::{IndexT, enum_wrapper_impl_from};
14
use pyo3::exceptions::PyTypeError;
25
use pyo3::prelude::*;
36
use pyo3_stub_gen::derive::*;
47
use splashsurf_lib::{Real, UniformGrid};
58

6-
use crate::utils;
7-
use crate::utils::{IndexT, enum_wrapper_impl_from};
8-
99
enum PyUniformGridData {
1010
F32(UniformGrid<IndexT, f32>),
1111
F64(UniformGrid<IndexT, f64>),
@@ -45,3 +45,43 @@ impl PyUniformGrid {
4545
}
4646
}
4747
}
48+
49+
#[gen_stub_pymethods]
50+
#[pymethods]
51+
impl PyUniformGrid {
52+
/// The AABB of the grid containing all marching cubes vertices influenced by the particle kernels
53+
#[getter]
54+
pub fn aabb(&self) -> PyAabb3d {
55+
match &self.inner {
56+
PyUniformGridData::F32(grid) => PyAabb3d::from(grid.aabb().clone()),
57+
PyUniformGridData::F64(grid) => PyAabb3d::from(grid.aabb().clone()),
58+
}
59+
}
60+
61+
/// Returns the cell size of the uniform grid (the marching cubes voxel size)
62+
#[getter]
63+
pub fn cell_size(&self) -> f64 {
64+
match &self.inner {
65+
PyUniformGridData::F32(grid) => grid.cell_size() as f64,
66+
PyUniformGridData::F64(grid) => grid.cell_size(),
67+
}
68+
}
69+
70+
/// Returns the number of points (marching cubes vertices) per dimension in the uniform grid
71+
#[getter]
72+
pub fn npoints_per_dim(&self) -> [IndexT; 3] {
73+
match &self.inner {
74+
PyUniformGridData::F32(grid) => grid.points_per_dim().clone(),
75+
PyUniformGridData::F64(grid) => grid.points_per_dim().clone(),
76+
}
77+
}
78+
79+
/// Returns the number of cells (marching cubes voxels) per dimension in the uniform grid
80+
#[getter]
81+
pub fn ncells_per_dim(&self) -> [IndexT; 3] {
82+
match &self.inner {
83+
PyUniformGridData::F32(grid) => grid.cells_per_dim().clone(),
84+
PyUniformGridData::F64(grid) => grid.cells_per_dim().clone(),
85+
}
86+
}
87+
}

pysplashsurf/tests/test_basic.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import pysplashsurf
22
import numpy as np
33
import meshio
4+
import os.path
5+
import pathlib
6+
import tempfile
7+
8+
DIR = pathlib.Path(__file__).parent.resolve()
9+
VTK_PATH = DIR.joinpath("ParticleData_Random_1000.vtk")
410

511

612
def test_aabb_class():
@@ -30,3 +36,272 @@ def test_aabb_class():
3036
assert aabb.contains_point([0.0, 0.0, 0.0])
3137
assert not aabb.contains_point([2.0, 1.0, 4.2])
3238
assert not aabb.contains_point([1.0, -1.0, 5.0])
39+
40+
41+
def impl_basic_test(dtype):
42+
particles = np.array(meshio.read(VTK_PATH).points, dtype=dtype)
43+
44+
mesh_with_data, reconstruction = pysplashsurf.reconstruction_pipeline(
45+
particles,
46+
particle_radius=0.025,
47+
rest_density=1000.0,
48+
smoothing_length=2.0,
49+
cube_size=1.0,
50+
iso_surface_threshold=0.6,
51+
mesh_smoothing_iters=5,
52+
output_mesh_smoothing_weights=True,
53+
)
54+
55+
assert type(mesh_with_data) is pysplashsurf.MeshWithData
56+
assert type(reconstruction) is pysplashsurf.SurfaceReconstruction
57+
assert type(mesh_with_data.mesh) is pysplashsurf.TriMesh3d
58+
59+
mesh = mesh_with_data.mesh
60+
61+
assert mesh_with_data.dtype == mesh.dtype
62+
assert mesh_with_data.dtype == dtype
63+
64+
assert type(mesh_with_data.mesh_type) is pysplashsurf.MeshType
65+
assert mesh_with_data.mesh_type == pysplashsurf.MeshType.Tri3d
66+
67+
assert mesh.vertices.dtype == dtype
68+
assert mesh.triangles.dtype in [np.uint32, np.uint64]
69+
70+
assert mesh_with_data.nvertices == len(mesh.vertices)
71+
assert mesh_with_data.ncells == len(mesh.triangles)
72+
73+
assert mesh_with_data.nvertices in range(21000, 25000)
74+
assert mesh_with_data.ncells in range(45000, 49000)
75+
76+
assert mesh.vertices.shape == (mesh_with_data.nvertices, 3)
77+
assert mesh.triangles.shape == (mesh_with_data.ncells, 3)
78+
79+
assert len(mesh_with_data.point_attributes) == 2
80+
assert len(mesh_with_data.cell_attributes) == 0
81+
82+
assert "sw" in mesh_with_data.point_attributes
83+
assert "wnn" in mesh_with_data.point_attributes
84+
85+
sw = mesh_with_data.point_attributes["sw"]
86+
wnn = mesh_with_data.point_attributes["wnn"]
87+
88+
assert len(sw) == mesh_with_data.nvertices
89+
assert len(wnn) == mesh_with_data.nvertices
90+
91+
assert sw.dtype == dtype
92+
assert wnn.dtype == dtype
93+
94+
assert sw.shape == (mesh_with_data.nvertices,)
95+
assert wnn.shape == (mesh_with_data.nvertices,)
96+
97+
assert sw.min() >= 0.0
98+
assert sw.max() <= 1.0
99+
100+
assert wnn.min() >= 0.0
101+
102+
103+
def test_pipeline_f32():
104+
impl_basic_test(np.float32)
105+
106+
107+
def test_pipeline_f64():
108+
impl_basic_test(np.float64)
109+
110+
111+
def test_reconstruct():
112+
particles = np.array(meshio.read(VTK_PATH).points, dtype=np.float32)
113+
114+
reconstruction = pysplashsurf.reconstruct_surface(
115+
particles,
116+
particle_radius=0.025,
117+
rest_density=1000.0,
118+
smoothing_length=2.0 * 0.025,
119+
cube_size=1.0 * 0.025,
120+
iso_surface_threshold=0.6,
121+
global_neighborhood_list=True,
122+
)
123+
124+
assert type(reconstruction) is pysplashsurf.SurfaceReconstruction
125+
assert type(reconstruction.mesh) is pysplashsurf.TriMesh3d
126+
assert type(reconstruction.grid) is pysplashsurf.UniformGrid
127+
assert type(reconstruction.particle_densities) is np.ndarray
128+
assert type(reconstruction.particle_inside_aabb) is type(None)
129+
assert type(reconstruction.particle_neighbors) is pysplashsurf.NeighborhoodLists
130+
131+
mesh = reconstruction.mesh
132+
133+
assert mesh.dtype == np.float32
134+
135+
assert reconstruction.particle_densities.dtype == np.float32
136+
assert len(reconstruction.particle_densities) == len(particles)
137+
138+
assert len(mesh.vertices) in range(25000, 30000)
139+
assert len(mesh.triangles) in range(49000, 53000)
140+
141+
142+
def test_neighborhood_search():
143+
particles = np.array(meshio.read(VTK_PATH).points, dtype=np.float32)
144+
145+
reconstruction = pysplashsurf.reconstruct_surface(
146+
particles,
147+
particle_radius=0.025,
148+
rest_density=1000.0,
149+
smoothing_length=2.0 * 0.025,
150+
cube_size=1.0 * 0.025,
151+
iso_surface_threshold=0.6,
152+
global_neighborhood_list=True,
153+
)
154+
155+
neighbors_reconstruct = reconstruction.particle_neighbors.get_neighborhood_lists()
156+
157+
assert type(neighbors_reconstruct) is list
158+
assert len(neighbors_reconstruct) == len(particles)
159+
160+
aabb = reconstruction.grid.aabb
161+
162+
neighbor_lists = pysplashsurf.neighborhood_search_spatial_hashing_parallel(
163+
particles, domain=aabb, search_radius=4.0 * 0.025
164+
)
165+
166+
assert type(neighbor_lists) is pysplashsurf.NeighborhoodLists
167+
168+
neighbors = neighbor_lists.get_neighborhood_lists()
169+
170+
assert type(neighbors) is list
171+
assert len(neighbors) == len(particles)
172+
assert len(neighbors) == len(neighbors_reconstruct)
173+
174+
# TODO: Compare with naive neighbor search
175+
176+
177+
def test_check_consistency():
178+
particles = np.array(meshio.read(VTK_PATH).points, dtype=np.float32)
179+
180+
reconstruction = pysplashsurf.reconstruct_surface(
181+
particles,
182+
particle_radius=0.025,
183+
rest_density=1000.0,
184+
smoothing_length=2.0 * 0.025,
185+
cube_size=1.0 * 0.025,
186+
iso_surface_threshold=0.6,
187+
global_neighborhood_list=True,
188+
)
189+
mesh = reconstruction.mesh
190+
191+
assert pysplashsurf.check_mesh_consistency(mesh, reconstruction.grid) is None
192+
193+
mesh_with_data, reconstruction = pysplashsurf.reconstruction_pipeline(
194+
particles,
195+
particle_radius=0.025,
196+
rest_density=1000.0,
197+
smoothing_length=2.0,
198+
cube_size=1.0,
199+
iso_surface_threshold=0.6,
200+
mesh_smoothing_iters=5,
201+
output_mesh_smoothing_weights=True,
202+
)
203+
204+
assert (
205+
pysplashsurf.check_mesh_consistency(mesh_with_data, reconstruction.grid) is None
206+
)
207+
208+
209+
def test_tris_to_quads():
210+
particles = np.array(meshio.read(VTK_PATH).points, dtype=np.float32)
211+
212+
mesh_with_data, reconstruction = pysplashsurf.reconstruction_pipeline(
213+
particles,
214+
particle_radius=0.025,
215+
rest_density=1000.0,
216+
smoothing_length=2.0,
217+
cube_size=1.0,
218+
iso_surface_threshold=0.6,
219+
mesh_smoothing_iters=5,
220+
output_mesh_smoothing_weights=True,
221+
)
222+
223+
mesh_with_data_quads = pysplashsurf.convert_tris_to_quads(mesh_with_data)
224+
225+
assert type(mesh_with_data_quads.mesh) is pysplashsurf.MixedTriQuadMesh3d
226+
assert mesh_with_data_quads.mesh_type == pysplashsurf.MeshType.MixedTriQuad3d
227+
228+
assert mesh_with_data_quads.nvertices == mesh_with_data.nvertices
229+
assert mesh_with_data_quads.ncells < mesh_with_data.ncells
230+
231+
tris = mesh_with_data_quads.mesh.get_triangles()
232+
quads = mesh_with_data_quads.mesh.get_quads()
233+
234+
assert tris.dtype in [np.uint32, np.uint64]
235+
assert quads.dtype in [np.uint32, np.uint64]
236+
237+
assert len(tris) + len(quads) == mesh_with_data_quads.ncells
238+
239+
assert tris.shape == (len(tris), 3)
240+
assert quads.shape == (len(quads), 4)
241+
242+
assert len(tris) in range(35000, 39000)
243+
assert len(quads) in range(4600, 5000)
244+
245+
assert len(mesh_with_data.point_attributes) == 2
246+
assert len(mesh_with_data.cell_attributes) == 0
247+
248+
assert "sw" in mesh_with_data.point_attributes
249+
assert "wnn" in mesh_with_data.point_attributes
250+
251+
252+
def test_interpolator():
253+
particles = np.array(meshio.read(VTK_PATH).points, dtype=np.float32)
254+
255+
mesh_with_data, reconstruction = pysplashsurf.reconstruction_pipeline(
256+
particles,
257+
particle_radius=0.025,
258+
rest_density=1000.0,
259+
smoothing_length=2.0,
260+
cube_size=1.0,
261+
iso_surface_threshold=0.6,
262+
mesh_smoothing_iters=5,
263+
output_mesh_smoothing_weights=True,
264+
)
265+
266+
compact_support = 4.0 * 0.025
267+
rest_mass = 1000.0 * 0.025**3
268+
269+
interpolator = pysplashsurf.SphInterpolator(
270+
particles, reconstruction.particle_densities, rest_mass, compact_support
271+
)
272+
273+
assert type(interpolator) is pysplashsurf.SphInterpolator
274+
275+
mesh = mesh_with_data.mesh
276+
mesh_densities = interpolator.interpolate_quantity(
277+
reconstruction.particle_densities, mesh.vertices
278+
)
279+
280+
assert type(mesh_densities) is np.ndarray
281+
assert mesh_densities.dtype == np.float32
282+
assert mesh_densities.shape == (len(mesh.vertices),)
283+
assert mesh_densities.min() >= 0.0
284+
285+
mesh_particles = interpolator.interpolate_quantity(particles, mesh.vertices)
286+
287+
assert type(mesh_particles) is np.ndarray
288+
assert mesh_particles.dtype == np.float32
289+
assert mesh_particles.shape == (len(mesh.vertices), 3)
290+
291+
mesh_sph_normals = interpolator.interpolate_normals(mesh.vertices)
292+
293+
assert type(mesh_sph_normals) is np.ndarray
294+
assert mesh_sph_normals.dtype == np.float32
295+
assert mesh_sph_normals.shape == (len(mesh.vertices), 3)
296+
297+
mesh_with_data.add_point_attribute("density", mesh_densities)
298+
mesh_with_data.add_point_attribute("position", mesh_particles)
299+
mesh_with_data.add_point_attribute("normal", mesh_sph_normals)
300+
301+
assert "density" in mesh_with_data.point_attributes
302+
assert "position" in mesh_with_data.point_attributes
303+
assert "normal" in mesh_with_data.point_attributes
304+
305+
assert np.array_equal(mesh_with_data.point_attributes["density"], mesh_densities)
306+
assert np.array_equal(mesh_with_data.point_attributes["position"], mesh_particles)
307+
assert np.array_equal(mesh_with_data.point_attributes["normal"], mesh_sph_normals)

0 commit comments

Comments
 (0)