#![cfg(feature = "std")]
use linear_isomorphic::*;
use rstar::primitives::GeomWithData;
use rstar::{Point, RTree};
pub const KNN_K: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SamplePoint<const D: usize>
{
coords: [f64; D],
}
impl<const D: usize> SamplePoint<D>
{
pub fn from_vertex<Vert, S>(vertex: &Vert) -> Self
where
Vert: VectorSpace<Scalar = S>,
S: linear_isomorphic::RealField,
{
let mut coords = [0.0; D];
for (i, c) in coords.iter_mut().enumerate()
{
*c = vertex[i].to_f64().unwrap_or(0.0);
}
Self { coords }
}
}
impl<const D: usize> Point for SamplePoint<D>
{
type Scalar = f64;
const DIMENSIONS: usize = D;
fn generate(mut generator: impl FnMut(usize) -> f64) -> Self
{
let mut coords = [0.0; D];
for (i, c) in coords.iter_mut().enumerate()
{
*c = generator(i);
}
Self { coords }
}
fn nth(&self, index: usize) -> f64 { self.coords[index] }
fn nth_mut(&mut self, index: usize) -> &mut f64 { &mut self.coords[index] }
}
pub type IndexedSample<const D: usize> = GeomWithData<SamplePoint<D>, usize>;
pub type KnnQuery<const D: usize> = RTree<IndexedSample<D>>;
pub fn index_samples<const D: usize, Vert, S>(samples: &[(Vert, usize)]) -> KnnQuery<D>
where
Vert: VectorSpace<Scalar = S>,
S: linear_isomorphic::RealField,
{
RTree::bulk_load(
samples
.iter()
.map(|(v, vid)| IndexedSample::new(SamplePoint::from_vertex(v), *vid))
.collect(),
)
}
pub fn neighbourhood_radius<const D: usize>(
query: &KnnQuery<D>,
point: SamplePoint<D>,
index: usize,
k: usize,
) -> Option<f64>
{
query
.nearest_neighbor_iter_with_distance_2(point)
.filter(|(sample, _)| sample.data != index)
.take(k)
.last()
.map(|(_, distance_squared)| distance_squared.sqrt())
}
pub fn knn_area_element<const D: usize, Vert, S>(
query: &KnnQuery<D>,
vertex: &Vert,
index: usize,
k: usize,
) -> S
where
Vert: VectorSpace<Scalar = S>,
S: linear_isomorphic::RealField,
{
let point = SamplePoint::from_vertex(vertex);
let radius = neighbourhood_radius(query, point, index, k).unwrap_or(0.0);
S::from(core::f64::consts::PI * radius * radius / k as f64).unwrap_or_else(S::default)
}
pub fn integrate<Vert, Aarea, Function, S>(
samples: impl Iterator<Item = (Vert, usize)>,
area_element: Aarea,
function: Function,
) -> S
where
Aarea: Fn(&Vert, usize) -> S,
S: linear_isomorphic::RealField,
Function: Fn(&Vert, usize) -> S,
{
let mut res = S::default();
for (v, vid) in samples
{
let area = area_element(&v, vid);
let val = function(&v, vid);
res += val * area;
}
res
}
pub fn integrate_points<const D: usize, Vert, Function, S>(
samples: impl Iterator<Item = (Vert, usize)>,
function: Function,
) -> S
where
Vert: VectorSpace<Scalar = S>,
S: linear_isomorphic::RealField,
Function: Fn(&Vert, usize) -> S,
{
let samples: Vec<(Vert, usize)> = samples.collect();
if samples.is_empty()
{
return S::default();
}
let query = index_samples::<D, _, _>(&samples);
let k = KNN_K.min(samples.len() - 1).max(1);
integrate(
samples.into_iter(),
|v, vid| knn_area_element(&query, v, vid, k),
function,
)
}
#[cfg(test)]
mod tests
{
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use super::*;
type Vec3 = nalgebra::Vector3<f64>;
fn standard_normal(rng: &mut StdRng) -> f64
{
let u1 = 1.0 - rng.random::<f64>();
let u2 = rng.random::<f64>();
(-2.0 * u1.ln()).sqrt() * (core::f64::consts::TAU * u2).cos()
}
fn unit_sphere_samples(count: usize) -> Vec<(Vec3, usize)>
{
let mut rng = StdRng::seed_from_u64(0x5eed_5b1e_0f77_3a91);
(0..count)
.map(|i| {
let v = Vec3::new(
standard_normal(&mut rng),
standard_normal(&mut rng),
standard_normal(&mut rng),
);
(v.normalize(), i)
})
.collect()
}
#[test]
fn integrates_the_area_of_the_sphere()
{
let samples = unit_sphere_samples(20_000);
let area = integrate_points::<3, _, _, _>(samples.into_iter(), |_, _| 1.0);
let expected = 4.0 * core::f64::consts::PI;
assert!(
(area - expected).abs() < 0.001 * expected,
"{area} vs {expected}"
);
}
#[test]
fn integrates_the_volume_of_the_sphere()
{
let samples = unit_sphere_samples(20_000);
let volume =
integrate_points::<3, _, _, _>(samples.into_iter(), |v, _| v.dot(v) / 3.);
let expected = 4.0 / 3. * core::f64::consts::PI;
assert!(
(volume - expected).abs() < 0.001 * expected,
"{volume} vs {expected}"
);
}
#[test]
fn integrates_a_varying_function_over_the_sphere()
{
let samples = unit_sphere_samples(20_000);
let integral =
integrate_points::<3, _, _, _>(samples.into_iter(), |v, _| v.z * v.z);
let expected = 4.0 * core::f64::consts::PI / 3.0;
assert!(
(integral - expected).abs() < 0.01 * expected,
"{integral} vs {expected}"
);
}
}