use ndarray::parallel::prelude::*;
use ndarray::{Array1, Array2, ArrayView1, ArrayView2, ArrayViewMut2, Axis};
use serde::{Deserialize, Serialize};
const MEMBERSHIP_SLACK_TOL: f64 = 1e-12;
const DYKSTRA_MAX_CYCLES: usize = 200;
const DYKSTRA_TOL: f64 = 1e-8;
const FACET_NORM2_FLOOR: f64 = 1e-16;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeeledHull {
pub facets: Vec<(Array1<f64>, f64)>,
pub dim: usize,
}
impl PeeledHull {
pub(crate) fn project_in_place(&self, mut points: ArrayViewMut2<'_, f64>) -> usize {
if points.nrows() == 0 {
return 0;
}
points
.axis_iter_mut(Axis(0))
.into_par_iter()
.map(|mut row| {
let view = row.view();
if self.is_inside(view) {
0usize
} else {
let proj = self.project_point(view);
row.assign(&proj);
1usize
}
})
.sum()
}
pub fn project_if_needed(&self, points: ArrayView2<f64>) -> (Array2<f64>, usize) {
let d = points.ncols();
assert_eq!(
d, self.dim,
"Dimension mismatch in PeeledHull::project_if_needed"
);
let mut out = points.to_owned();
let projected = self.project_in_place(out.view_mut());
(out, projected)
}
pub fn is_inside(&self, x: ArrayView1<f64>) -> bool {
for (a, b) in &self.facets {
let s = a.dot(&x);
if s > *b + MEMBERSHIP_SLACK_TOL {
return false;
}
}
true
}
pub fn signed_distance(&self, x: ArrayView1<f64>) -> f64 {
if self.is_inside(x) {
let mut min_slack = f64::INFINITY;
for (a, b) in &self.facets {
let slack = *b - a.dot(&x);
if slack < min_slack {
min_slack = slack;
}
}
-min_slack.max(0.0)
} else {
let z = self.project_point(x);
let diff = &z - &x;
diff.mapv(|v| v * v).sum().sqrt()
}
}
fn project_point(&self, y: ArrayView1<f64>) -> Array1<f64> {
let d = self.dim;
let m = self.facets.len();
let max_cycles = DYKSTRA_MAX_CYCLES;
let tol = DYKSTRA_TOL;
let mut x = y.to_owned();
let mut p_corr: Vec<Array1<f64>> = (0..m).map(|_| Array1::zeros(d)).collect();
for _ in 0..max_cycles {
let x_prev = x.clone();
for (i, (a, b)) in self.facets.iter().enumerate() {
let mut y_i = x.clone();
y_i += &p_corr[i];
let a_tb = a.dot(&y_i) - *b;
let a_norm2 = a.dot(a).max(FACET_NORM2_FLOOR);
if a_tb > 0.0 {
let alpha = a_tb / a_norm2;
let z = &y_i - &(a * alpha);
p_corr[i] = &y_i - &z;
x = z;
} else {
p_corr[i].fill(0.0);
x = y_i;
}
}
let diff = (&x - &x_prev).mapv(|v| v.abs()).sum();
if diff < tol {
return x;
}
}
x
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
fn unit_square_hull() -> PeeledHull {
PeeledHull {
facets: vec![
(array![1.0, 0.0], 1.0), (array![-1.0, 0.0], 0.0), (array![0.0, 1.0], 1.0), (array![0.0, -1.0], 0.0), ],
dim: 2,
}
}
#[test]
fn test_is_inside_unit_square() {
let h = unit_square_hull();
assert!(h.is_inside(array![0.5, 0.5].view())); assert!(h.is_inside(array![1.0, 0.5].view())); assert!(h.is_inside(array![0.0, 0.0].view())); assert!(!h.is_inside(array![1.1, 0.5].view())); assert!(!h.is_inside(array![-0.1, 0.5].view())); assert!(!h.is_inside(array![0.5, 1.1].view())); assert!(!h.is_inside(array![0.5, -0.1].view())); }
#[test]
fn test_project_point_unit_square() {
let h = unit_square_hull();
let p_in = array![0.5, 0.5];
let proj_in = h.project_point(p_in.view());
assert!((&proj_in - &p_in).mapv(|v| v.abs()).sum() < 1e-12);
let p_face = array![1.5, 0.5];
let proj_face = h.project_point(p_face.view());
assert!((&proj_face - &array![1.0, 0.5]).mapv(|v| v.abs()).sum() < 1e-8);
let p_corner = array![1.5, -0.5];
let proj_corner = h.project_point(p_corner.view());
assert!((&proj_corner - &array![1.0, 0.0]).mapv(|v| v.abs()).sum() < 1e-6);
}
#[test]
fn test_signed_distance_unit_square() {
let h = unit_square_hull();
let d_center = h.signed_distance(array![0.5, 0.5].view());
assert!((d_center + 0.5).abs() < 1e-12);
let d_inside = h.signed_distance(array![0.2, 0.8].view());
assert!((d_inside + 0.2).abs() < 1e-12);
let d_edge = h.signed_distance(array![1.0, 0.3].view());
assert!(d_edge.abs() < 1e-12);
let d_out_x = h.signed_distance(array![1.5, 0.5].view());
assert!((d_out_x - 0.5).abs() < 1e-8);
let d_out_corner = h.signed_distance(array![1.5, -0.5].view());
assert!((d_out_corner - (0.5f64.hypot(0.5))).abs() < 1e-6);
}
}