#![cfg_attr(docsrs, feature(doc_cfg))]
use log::{info, warn};
pub use nalgebra;
use nalgebra::Scalar;
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 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 {
None,
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,
pub auto_disable: bool,
}
impl Default for GridDecompositionParameters {
fn default() -> Self {
Self {
subdomain_num_cubes_per_dim: 64,
auto_disable: true,
}
}
}
#[derive(Clone, Debug)]
pub struct Parameters<R: Scalar> {
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 enable_simd: bool,
pub spatial_decomposition: SpatialDecomposition,
pub global_neighborhood_list: bool,
}
impl<R: Real> Parameters<R> {
pub fn new(particle_radius: R, compact_support_radius: R, cube_size: R) -> Self {
Self {
particle_radius,
rest_density: R::from_float(1000.0),
compact_support_radius,
cube_size,
iso_surface_threshold: R::from_float(0.6),
particle_aabb: None,
enable_multi_threading: true,
enable_simd: true,
spatial_decomposition: Default::default(),
global_neighborhood_list: false,
}
}
pub fn new_relative(
particle_radius: R,
relative_compact_support_radius: R,
relative_cube_size: R,
) -> Self {
Self::new(
particle_radius,
particle_radius * relative_compact_support_radius,
particle_radius * relative_cube_size,
)
}
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,
enable_simd: self.enable_simd,
spatial_decomposition: self.spatial_decomposition.clone(),
global_neighborhood_list: self.global_neighborhood_list,
})
}
}
#[derive(Clone, Debug)]
pub struct SurfaceReconstruction<I: Scalar, R: Scalar + Send + Default> {
pub grid: UniformGrid<I, R>,
pub subdomain_grid: Option<UniformGrid<I, R>>,
pub particle_densities: Option<Vec<R>>,
pub particle_inside_aabb: Option<Vec<bool>>,
pub particle_neighbors: Option<Vec<Vec<usize>>>,
pub mesh: TriMesh3d<R>,
workspace: ReconstructionWorkspace<R>,
}
impl<I: Index, R: Real> Default for SurfaceReconstruction<I, R> {
fn default() -> Self {
Self {
grid: UniformGrid::new_zero(),
subdomain_grid: None,
particle_densities: None,
particle_neighbors: None,
particle_inside_aabb: None,
mesh: TriMesh3d::default(),
workspace: ReconstructionWorkspace::default(),
}
}
}
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();
if parameters.enable_simd {
if let Some(simd) = utils::detect_simd_support() {
let simd_str = match simd {
utils::SimdFeatures::Avx2Fma => "AVX2 and FMA",
utils::SimdFeatures::Neon => "NEON",
};
info!("Vectorization enabled with support detected for {simd_str} instructions.");
} else {
warn!(
"Vectorization was enabled, but no SIMD support was detected on this CPU. Falling back to non-vectorized code."
);
}
if std::any::TypeId::of::<R>() != std::any::TypeId::of::<f32>() {
warn!(
"Vectorization is currently only supported for single-precision (f32) reconstructions. Falling back to non-vectorized code."
);
}
}
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();
particle_inside.clear();
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 {
output_surface.particle_inside_aabb = None;
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 {
SpatialDecomposition::UniformGrid(p) => {
let use_decomposition = if p.auto_disable {
let max_cubes = output_surface
.grid
.cells_per_dim()
.iter()
.copied()
.max()
.unwrap_or(I::one());
let subdomain_cubes_with_margin =
(1.2 * p.subdomain_num_cubes_per_dim as f64) as u32;
let use_decomposition =
max_cubes.to_u32().unwrap_or(u32::MAX) > subdomain_cubes_with_margin;
info!(
"Enabling decomposition: {}. Max domain extent ({}) > cubes per subdomain + 20% ({})",
use_decomposition, max_cubes, subdomain_cubes_with_margin
);
use_decomposition
} else {
true
};
if use_decomposition {
reconstruction::reconstruct_surface_subdomain_grid::<I, R>(
particle_positions,
parameters,
output_surface,
)?
} else {
reconstruction::reconstruct_surface_global(
particle_positions,
parameters,
output_surface,
)?
}
}
SpatialDecomposition::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!(
"Bounding box of particles with margin for levelset evaluation: {:?} to {:?}",
particle_aabb.min().as_slice(),
particle_aabb.max().as_slice()
);
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)?)
}