#![cfg_attr(docsrs, feature(doc_cfg))]
use log::info;
pub use nalgebra;
use nalgebra::Vector3;
use std::borrow::Cow;
use std::hash::Hash;
use thiserror::Error as ThisError;
#[cfg(feature = "vtk_extras")]
pub use vtkio;
pub use crate::aabb::{Aabb2d, Aabb3d, AxisAlignedBoundingBox};
pub use crate::density_map::DensityMap;
pub use crate::traits::{Index, Real, RealConvert, ThreadSafe};
pub use crate::uniform_grid::UniformGrid;
use crate::density_map::DensityMapError;
use crate::marching_cubes::MarchingCubesError;
use crate::mesh::TriMesh3d;
use crate::uniform_grid::GridConstructionError;
use crate::workspace::ReconstructionWorkspace;
#[cfg(feature = "profiling")]
#[cfg_attr(docsrs, doc(cfg(feature = "profiling")))]
pub mod profiling;
#[doc(hidden)]
pub mod profiling_macro;
mod aabb;
pub(crate) mod dense_subdomains;
pub mod density_map;
pub mod generic_tree;
pub mod halfedge_mesh;
#[cfg(feature = "io")]
#[cfg_attr(docsrs, doc(cfg(feature = "io")))]
pub mod io;
pub mod kernel;
pub mod marching_cubes;
pub mod mesh;
pub mod neighborhood_search;
pub mod postprocessing;
pub(crate) mod reconstruction;
pub mod sph_interpolation;
pub mod topology;
mod traits;
pub mod uniform_grid;
#[macro_use]
mod utils;
pub(crate) mod workspace;
pub(crate) type HashState = fxhash::FxBuildHasher;
pub(crate) type MapType<K, V> = std::collections::HashMap<K, V, HashState>;
pub(crate) type SetType<K> = std::collections::HashSet<K, HashState>;
pub(crate) fn new_map<K, V>() -> MapType<K, V> {
Default::default()
}
pub(crate) type ParallelMapType<K, V> = dashmap::DashMap<K, V, HashState>;
fn new_parallel_map<K: Eq + Hash, V>() -> ParallelMapType<K, V> {
ParallelMapType::with_hasher(HashState::default())
}
#[derive(Clone, Debug)]
pub enum SpatialDecomposition {
UniformGrid(GridDecompositionParameters),
}
impl Default for SpatialDecomposition {
fn default() -> Self {
Self::UniformGrid(GridDecompositionParameters::default())
}
}
#[derive(Clone, Debug)]
pub struct GridDecompositionParameters {
pub subdomain_num_cubes_per_dim: u32,
}
impl Default for GridDecompositionParameters {
fn default() -> Self {
Self {
subdomain_num_cubes_per_dim: 64,
}
}
}
#[derive(Clone, Debug)]
pub struct Parameters<R: Real> {
pub particle_radius: R,
pub rest_density: R,
pub compact_support_radius: R,
pub cube_size: R,
pub iso_surface_threshold: R,
pub particle_aabb: Option<Aabb3d<R>>,
pub enable_multi_threading: bool,
pub spatial_decomposition: Option<SpatialDecomposition>,
pub global_neighborhood_list: bool,
}
impl<R: Real> Parameters<R> {
pub fn try_convert<T: Real>(&self) -> Option<Parameters<T>> {
Some(Parameters {
particle_radius: self.particle_radius.try_convert()?,
rest_density: self.rest_density.try_convert()?,
compact_support_radius: self.compact_support_radius.try_convert()?,
cube_size: self.cube_size.try_convert()?,
iso_surface_threshold: self.iso_surface_threshold.try_convert()?,
particle_aabb: map_option!(&self.particle_aabb, aabb => aabb.try_convert()?),
enable_multi_threading: self.enable_multi_threading,
spatial_decomposition: self.spatial_decomposition.clone(),
global_neighborhood_list: self.global_neighborhood_list,
})
}
}
#[derive(Clone, Debug)]
pub struct SurfaceReconstruction<I: Index, R: Real> {
grid: UniformGrid<I, R>,
particle_densities: Option<Vec<R>>,
particle_inside_aabb: Option<Vec<bool>>,
particle_neighbors: Option<Vec<Vec<usize>>>,
mesh: TriMesh3d<R>,
workspace: ReconstructionWorkspace<R>,
}
impl<I: Index, R: Real> Default for SurfaceReconstruction<I, R> {
fn default() -> Self {
Self {
grid: UniformGrid::new_zero(),
particle_densities: None,
particle_neighbors: None,
particle_inside_aabb: None,
mesh: TriMesh3d::default(),
workspace: ReconstructionWorkspace::default(),
}
}
}
impl<I: Index, R: Real> SurfaceReconstruction<I, R> {
pub fn mesh(&self) -> &TriMesh3d<R> {
&self.mesh
}
pub fn particle_densities(&self) -> Option<&Vec<R>> {
self.particle_densities.as_ref()
}
pub fn particle_neighbors(&self) -> Option<&Vec<Vec<usize>>> {
self.particle_neighbors.as_ref()
}
pub fn grid(&self) -> &UniformGrid<I, R> {
&self.grid
}
}
impl<I: Index, R: Real> From<SurfaceReconstruction<I, R>> for TriMesh3d<R> {
fn from(result: SurfaceReconstruction<I, R>) -> Self {
result.mesh
}
}
#[non_exhaustive]
#[derive(Debug, ThisError)]
pub enum ReconstructionError<I: Index, R: Real> {
#[error("grid construction")]
GridConstructionError(
#[source]
#[from]
GridConstructionError<I, R>,
),
#[error("density map generation")]
DensityMapGenerationError(
#[source]
#[from]
DensityMapError<R>,
),
#[error("marching cubes")]
MarchingCubesError(
#[source]
#[from]
MarchingCubesError,
),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}
pub fn initialize_thread_pool(num_threads: usize) -> Result<(), anyhow::Error> {
rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build_global()?;
Ok(())
}
#[inline(never)]
pub fn reconstruct_surface<I: Index, R: Real>(
particle_positions: &[Vector3<R>],
parameters: &Parameters<R>,
) -> Result<SurfaceReconstruction<I, R>, ReconstructionError<I, R>> {
let mut surface = SurfaceReconstruction::default();
reconstruct_surface_inplace(particle_positions, parameters, &mut surface)?;
Ok(surface)
}
pub fn reconstruct_surface_inplace<I: Index, R: Real>(
particle_positions: &[Vector3<R>],
parameters: &Parameters<R>,
output_surface: &mut SurfaceReconstruction<I, R>,
) -> Result<(), ReconstructionError<I, R>> {
output_surface.mesh.clear();
let filtered_particle_positions = if let Some(particle_aabb) = ¶meters.particle_aabb {
profile!("filtering particles");
use rayon::prelude::*;
let mut particle_inside = output_surface
.particle_inside_aabb
.take()
.unwrap_or_default();
utils::reserve_total(&mut particle_inside, particle_positions.len());
particle_positions
.par_iter()
.map(|p| particle_aabb.contains_point(p))
.collect_into_vec(&mut particle_inside);
let particle_inside_count = particle_inside.par_iter().copied().filter(|i| *i).count();
let mut filtered_particles =
std::mem::take(output_surface.workspace.filtered_particles_mut());
filtered_particles.clear();
utils::reserve_total(&mut filtered_particles, particle_inside_count);
filtered_particles.extend(
particle_positions
.iter()
.zip(particle_inside.iter().copied())
.filter(|(_, is_inside)| *is_inside)
.map(|(p, _)| p)
.cloned(),
);
output_surface.particle_inside_aabb = Some(particle_inside);
Cow::Owned(filtered_particles)
} else {
Cow::Borrowed(particle_positions)
};
let particle_positions = filtered_particle_positions.as_ref();
output_surface.grid = grid_for_reconstruction(
particle_positions,
parameters.particle_radius,
parameters.compact_support_radius,
parameters.cube_size,
parameters.particle_aabb.as_ref(),
parameters.enable_multi_threading,
)?;
output_surface.grid.log_grid_info();
match ¶meters.spatial_decomposition {
Some(SpatialDecomposition::UniformGrid(_)) => {
reconstruction::reconstruct_surface_subdomain_grid::<I, R>(
particle_positions,
parameters,
output_surface,
)?
}
None => reconstruction::reconstruct_surface_global(
particle_positions,
parameters,
output_surface,
)?,
}
if let Cow::Owned(mut filtered_particles) = filtered_particle_positions {
filtered_particles.clear();
*output_surface.workspace.filtered_particles_mut() = filtered_particles;
}
Ok(())
}
pub fn grid_for_reconstruction<I: Index, R: Real>(
particle_positions: &[Vector3<R>],
particle_radius: R,
compact_support_radius: R,
cube_size: R,
particle_aabb: Option<&Aabb3d<R>>,
enable_multi_threading: bool,
) -> Result<UniformGrid<I, R>, ReconstructionError<I, R>> {
let mut particle_aabb = if let Some(particle_aabb) = particle_aabb {
particle_aabb.clone()
} else {
profile!("compute minimum enclosing aabb");
let particle_aabb = {
let mut aabb = if enable_multi_threading {
Aabb3d::par_from_points(particle_positions)
} else {
Aabb3d::from_points(particle_positions)
};
aabb.grow_uniformly(particle_radius);
aabb
};
info!(
"Minimal enclosing bounding box of particles was computed as: {:?}",
particle_aabb
);
particle_aabb
};
let kernel_margin =
density_map::compute_kernel_evaluation_radius::<I, R>(compact_support_radius, cube_size)
.kernel_evaluation_radius;
particle_aabb.grow_uniformly(kernel_margin);
Ok(UniformGrid::from_aabb(&particle_aabb, cube_size)?)
}