Skip to content

Commit cc75e5c

Browse files
committed
Support borrowed data for DensityMap
1 parent dba1393 commit cc75e5c

6 files changed

Lines changed: 94 additions & 27 deletions

File tree

pysplashsurf/pysplashsurf/docs/source/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Functions
1919
convert_tris_to_quads
2020
laplacian_smoothing_normals_parallel
2121
laplacian_smoothing_parallel
22+
marching_cubes
2223
marching_cubes_cleanup
2324
neighborhood_search_spatial_hashing_parallel
2425
reconstruct_surface

pysplashsurf/pysplashsurf/docs/source/functions.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ All functions infer float precision based on the input (``np.float32`` or ``np.f
1515

1616
.. autofunction:: laplacian_smoothing_parallel
1717

18+
.. autofunction:: marching_cubes
19+
1820
.. autofunction:: marching_cubes_cleanup
1921

2022
.. autofunction:: neighborhood_search_spatial_hashing_parallel

pysplashsurf/pysplashsurf/pysplashsurf.pyi

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,28 @@ def laplacian_smoothing_parallel(mesh:typing.Union[TriMesh3d, MeshWithData], ver
353353
The smoothing is performed inplace and modifies the vertices of the given mesh.
354354
"""
355355

356-
def marching_cubes(values:numpy.typing.NDArray[typing.Any], *, cube_size:builtins.float, iso_surface_threshold:builtins.float, translation:typing.Optional[typing.Sequence[builtins.float]]=None) -> tuple[TriMesh3d, UniformGrid]:
356+
def marching_cubes(values:numpy.typing.NDArray[typing.Any], *, iso_surface_threshold:builtins.float, cube_size:builtins.float, translation:typing.Optional[typing.Sequence[builtins.float]]=None, return_grid:builtins.bool=False) -> typing.Union[TriMesh3d, tuple[TriMesh3d, UniformGrid]]:
357357
r"""
358358
Performs a standard marching cubes triangulation of a 3D array of values
359+
360+
The array of values has to be a contiguous array with shape ``(nx, ny, nz)``.
361+
The iso-surface threshold defines which value is considered to be "on" the surface.
362+
The cube size and translation parameters define the scaling and translation of the resulting
363+
mesh. Without translation, the value ``values[0, 0, 0]`` is located at coordinates ``(0, 0, 0)``.
364+
365+
The values are interpreted as a "density field", meaning that values higher than the iso-surface
366+
threshold are considered to be "inside" the surface and values lower than the threshold are
367+
considered to be "outside" the surface. This is the opposite convention to an SDF (signed distance field).
368+
However, even if values of an SDF are provided as an input, the marching cubes algorithm
369+
will still work and produce a watertight surface mesh (if the surface is fully contained in the
370+
array).
371+
372+
If ``return_grid`` is set to ``True``, the function will return a tuple of the mesh and the
373+
uniform grid that was used for the triangulation. This can be used for other functions such as
374+
:py:func:`check_mesh_consistency`. Otherwise, only the mesh is returned.
375+
376+
The function is currently single-threaded. The SPH surface reconstruction functions :py:func:`reconstruction_pipeline`
377+
and :py:func:`reconstruct_surface` improve performance by processing multiple patches in parallel.
359378
"""
360379

361380
def marching_cubes_cleanup(mesh:typing.Union[TriMesh3d, MeshWithData], grid:UniformGrid, *, max_rel_snap_dist:typing.Optional[builtins.float]=None, max_iter:builtins.int=5, keep_vertices:builtins.bool=False) -> typing.Union[TriMesh3d, MeshWithData]:

pysplashsurf/src/marching_cubes.rs

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use numpy::prelude::*;
22
use numpy::{Element, PyArray3, PyUntypedArray};
3+
use pyo3::IntoPyObjectExt;
34
use pyo3::prelude::*;
45
use pyo3_stub_gen::derive::*;
56
use splashsurf_lib::nalgebra::Vector3;
@@ -53,24 +54,47 @@ pub fn check_mesh_consistency<'py>(
5354
}
5455

5556
/// Performs a standard marching cubes triangulation of a 3D array of values
57+
///
58+
/// The array of values has to be a contiguous array with shape ``(nx, ny, nz)``.
59+
/// The iso-surface threshold defines which value is considered to be "on" the surface.
60+
/// The cube size and translation parameters define the scaling and translation of the resulting
61+
/// mesh. Without translation, the value ``values[0, 0, 0]`` is located at coordinates ``(0, 0, 0)``.
62+
///
63+
/// The values are interpreted as a "density field", meaning that values higher than the iso-surface
64+
/// threshold are considered to be "inside" the surface and values lower than the threshold are
65+
/// considered to be "outside" the surface. This is the opposite convention to an SDF (signed distance field).
66+
/// However, even if values of an SDF are provided as an input, the marching cubes algorithm
67+
/// will still work and produce a watertight surface mesh (if the surface is fully contained in the
68+
/// array).
69+
///
70+
/// If ``return_grid`` is set to ``True``, the function will return a tuple of the mesh and the
71+
/// uniform grid that was used for the triangulation. This can be used for other functions such as
72+
/// :py:func:`check_mesh_consistency`. Otherwise, only the mesh is returned.
73+
///
74+
/// The function is currently single-threaded. The SPH surface reconstruction functions :py:func:`reconstruction_pipeline`
75+
/// and :py:func:`reconstruct_surface` improve performance by processing multiple patches in parallel.
5676
#[gen_stub_pyfunction]
5777
#[pyfunction]
5878
#[pyo3(name = "marching_cubes")]
59-
#[pyo3(signature = (values, *, cube_size, iso_surface_threshold, translation = None))]
79+
#[pyo3(signature = (values, *, iso_surface_threshold, cube_size, translation = None, return_grid = false))]
80+
#[gen_stub(override_return_type(type_repr="typing.Union[TriMesh3d, tuple[TriMesh3d, UniformGrid]]", imports=()))]
6081
pub fn marching_cubes<'py>(
6182
values: &Bound<'py, PyUntypedArray>,
62-
cube_size: f64,
6383
iso_surface_threshold: f64,
84+
cube_size: f64,
6485
translation: Option<[f64; 3]>,
65-
) -> PyResult<(PyTriMesh3d, PyUniformGrid)> {
86+
return_grid: bool,
87+
) -> PyResult<Py<PyAny>> {
6688
assert_eq!(values.shape().len(), 3, "values must be a 3D array");
6789

6890
fn triangulate_density_map_generic<'py, R: Real + Element>(
6991
values: &Bound<'py, PyArray3<R>>,
70-
cube_size: R,
7192
iso_surface_threshold: R,
93+
cube_size: R,
7294
translation: Option<[R; 3]>,
73-
) -> PyResult<(PyTriMesh3d, PyUniformGrid)> {
95+
return_grid: bool,
96+
) -> PyResult<Py<PyAny>> {
97+
let py = values.py();
7498
let shape = values.shape();
7599
let translation = Vector3::from(translation.unwrap_or([R::zero(); 3]));
76100
let n_cells_per_dim = [
@@ -82,31 +106,42 @@ pub fn marching_cubes<'py>(
82106
let grid = UniformGrid::new(&translation, &n_cells_per_dim, cube_size)
83107
.map_err(anyhow::Error::from)?;
84108

85-
// TODO: Replace with borrow
86-
let values = values.try_readonly()?.as_slice()?.to_vec();
87-
let density_map = DensityMap::from(values);
109+
let values = values.try_readonly()?;
110+
let density_map = DensityMap::from(values.as_slice()?);
88111

89112
let mesh = splashsurf_lib::marching_cubes::triangulate_density_map(
90113
&grid,
91114
&density_map,
92115
iso_surface_threshold,
93116
)
94117
.map_err(anyhow::Error::from)?;
95-
Ok((
96-
PyTriMesh3d::try_from_generic(mesh)?,
97-
PyUniformGrid::try_from_generic(grid)?,
98-
))
118+
119+
let mesh = PyTriMesh3d::try_from_generic(mesh)?;
120+
let grid = PyUniformGrid::try_from_generic(grid)?;
121+
122+
if return_grid {
123+
(mesh, grid).into_py_any(py)
124+
} else {
125+
mesh.into_py_any(py)
126+
}
99127
}
100128

101129
if let Ok(values) = values.downcast::<PyArray3<f32>>() {
102130
triangulate_density_map_generic(
103131
&values,
104-
cube_size as f32,
105132
iso_surface_threshold as f32,
133+
cube_size as f32,
106134
translation.map(|t| t.map(|t| t as f32)),
135+
return_grid,
107136
)
108137
} else if let Ok(values) = values.downcast::<PyArray3<f64>>() {
109-
triangulate_density_map_generic(&values, cube_size, iso_surface_threshold, translation)
138+
triangulate_density_map_generic(
139+
&values,
140+
iso_surface_threshold,
141+
cube_size,
142+
translation,
143+
return_grid,
144+
)
110145
} else {
111146
Err(utils::pyerr_unsupported_scalar())
112147
}

pysplashsurf/tests/test_sdf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def make_sdf():
2121

2222
# Note: Currently this reconstruction assumes that inside the surface values get bigger (like a density function)
2323
mesh, grid = pysplashsurf.marching_cubes(
24-
sdf, cube_size=dx, iso_surface_threshold=0.0, translation=[translation] * 3
24+
sdf, iso_surface_threshold=0.0, cube_size=dx, translation=[translation] * 3, return_grid=True
2525
)
2626

2727
assert len(mesh.vertices) > 0

splashsurf_lib/src/density_map.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//! In case of a sparse density map, the values are stored in a hashmap. The keys are so called
1313
//! "flat point indices". These are computed from the background grid point coordinates `(i,j,k)`
1414
//! analogous to multidimensional array index flattening. That means for a grid with dimensions
15-
//! `[n_x, n_y, n_z]`, the flat point index is given by the expression `i*n_x + j*n_y + k*n_z`.
15+
//! `[n_x, n_y, n_z]`, the flat point index is given by the expression `i*n_y*n_z + j*n_z + k`.
1616
//! For these point index operations, the [`UniformGrid`] is used.
1717
//!
1818
//! Note that all density mapping functions always use the global background grid for flat point
@@ -29,6 +29,7 @@ use dashmap::ReadOnlyView as ReadDashMap;
2929
use log::{info, trace, warn};
3030
use nalgebra::Vector3;
3131
use rayon::prelude::*;
32+
use std::borrow::Cow;
3233
use std::cell::RefCell;
3334
use thiserror::Error as ThisError;
3435
use thread_local::ThreadLocal;
@@ -219,37 +220,46 @@ pub fn parallel_compute_particle_densities<I: Index, R: Real>(
219220
/// The density map contains values for all points of the background grid where the density is not
220221
/// trivially zero (which is the case when a point is outside the compact support of any particles).
221222
#[derive(Clone, Debug)]
222-
pub enum DensityMap<I: Index, R: Real> {
223+
pub enum DensityMap<'a, I: Index, R: Real> {
223224
Standard(MapType<I, R>),
224225
DashMap(ReadDashMap<I, R, HashState>),
225-
Dense(Vec<R>),
226+
Dense(Cow<'a, [R]>),
226227
}
227228

228-
impl<I: Index, R: Real> Default for DensityMap<I, R> {
229+
/// Owned version of [`DensityMap`] (with static lifetime)
230+
pub type OwnedDensityMap<I, R> = DensityMap<'static, I, R>;
231+
232+
impl<I: Index, R: Real> Default for OwnedDensityMap<I, R> {
229233
fn default() -> Self {
230234
DensityMap::Standard(MapType::default())
231235
}
232236
}
233237

234-
impl<I: Index, R: Real> From<MapType<I, R>> for DensityMap<I, R> {
238+
impl<I: Index, R: Real> From<MapType<I, R>> for OwnedDensityMap<I, R> {
235239
fn from(map: MapType<I, R>) -> Self {
236240
Self::Standard(map)
237241
}
238242
}
239243

240-
impl<I: Index, R: Real> From<ParallelMapType<I, R>> for DensityMap<I, R> {
244+
impl<I: Index, R: Real> From<ParallelMapType<I, R>> for OwnedDensityMap<I, R> {
241245
fn from(map: ParallelMapType<I, R>) -> Self {
242246
Self::DashMap(map.into_read_only())
243247
}
244248
}
245249

246-
impl<I: Index, R: Real> From<Vec<R>> for DensityMap<I, R> {
250+
impl<I: Index, R: Real> From<Vec<R>> for DensityMap<'static, I, R> {
247251
fn from(values: Vec<R>) -> Self {
248-
Self::Dense(values)
252+
Self::Dense(values.into())
253+
}
254+
}
255+
256+
impl<'a, I: Index, R: Real> From<&'a [R]> for DensityMap<'a, I, R> {
257+
fn from(values: &'a [R]) -> Self {
258+
Self::Dense(values.into())
249259
}
250260
}
251261

252-
impl<I: Index, R: Real> DensityMap<I, R> {
262+
impl<'a, I: Index, R: Real> DensityMap<'a, I, R> {
253263
/// Converts the contained map into a vector of tuples of (flat_point_index, density)
254264
pub fn to_vec(&self) -> Vec<(I, R)> {
255265
match self {
@@ -357,7 +367,7 @@ pub fn sequential_generate_sparse_density_map<I: Index, R: Real>(
357367
particle_rest_mass: R,
358368
compact_support_radius: R,
359369
cube_size: R,
360-
) -> Result<DensityMap<I, R>, DensityMapError<R>> {
370+
) -> Result<OwnedDensityMap<I, R>, DensityMapError<R>> {
361371
profile!("sequential_generate_sparse_density_map");
362372

363373
let mut sparse_densities = new_map();
@@ -404,7 +414,7 @@ pub fn parallel_generate_sparse_density_map<I: Index, R: Real>(
404414
particle_rest_mass: R,
405415
compact_support_radius: R,
406416
cube_size: R,
407-
) -> Result<DensityMap<I, R>, DensityMapError<R>> {
417+
) -> Result<OwnedDensityMap<I, R>, DensityMapError<R>> {
408418
profile!("parallel_generate_sparse_density_map");
409419

410420
// Each thread will write to its own local density map

0 commit comments

Comments
 (0)