1use ndarray::parallel::prelude::*;
2use ndarray::{Array1, Array2, ArrayView1, ArrayView2, ArrayViewMut2, Axis};
3use serde::{Deserialize, Serialize};
4
5const MEMBERSHIP_SLACK_TOL: f64 = 1e-12;
10
11const DYKSTRA_MAX_CYCLES: usize = 200;
14
15const DYKSTRA_TOL: f64 = 1e-8;
18
19const FACET_NORM2_FLOOR: f64 = 1e-16;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PeeledHull {
28 pub facets: Vec<(Array1<f64>, f64)>,
30 pub dim: usize,
32}
33
34impl PeeledHull {
35 pub(crate) fn project_in_place(&self, mut points: ArrayViewMut2<'_, f64>) -> usize {
37 if points.nrows() == 0 {
38 return 0;
39 }
40
41 points
42 .axis_iter_mut(Axis(0))
43 .into_par_iter()
44 .map(|mut row| {
45 let view = row.view();
46 if self.is_inside(view) {
47 0usize
48 } else {
49 let proj = self.project_point(view);
50 row.assign(&proj);
51 1usize
52 }
53 })
54 .sum()
55 }
56
57 pub fn project_if_needed(&self, points: ArrayView2<f64>) -> (Array2<f64>, usize) {
59 let d = points.ncols();
60 assert_eq!(
61 d, self.dim,
62 "Dimension mismatch in PeeledHull::project_if_needed"
63 );
64
65 let mut out = points.to_owned();
66 let projected = self.project_in_place(out.view_mut());
67 (out, projected)
68 }
69
70 pub fn is_inside(&self, x: ArrayView1<f64>) -> bool {
72 for (a, b) in &self.facets {
73 let s = a.dot(&x);
74 if s > *b + MEMBERSHIP_SLACK_TOL {
75 return false;
76 }
77 }
78 true
79 }
80
81 pub fn signed_distance(&self, x: ArrayView1<f64>) -> f64 {
88 if self.is_inside(x) {
89 let mut min_slack = f64::INFINITY;
92 for (a, b) in &self.facets {
93 let slack = *b - a.dot(&x);
94 if slack < min_slack {
95 min_slack = slack;
96 }
97 }
98 -min_slack.max(0.0)
100 } else {
101 let z = self.project_point(x);
103 let diff = &z - &x;
104
105 diff.mapv(|v| v * v).sum().sqrt()
106 }
107 }
108
109 fn project_point(&self, y: ArrayView1<f64>) -> Array1<f64> {
111 let d = self.dim;
112 let m = self.facets.len();
113 let max_cycles = DYKSTRA_MAX_CYCLES;
114 let tol = DYKSTRA_TOL;
115
116 let mut x = y.to_owned();
118 let mut p_corr: Vec<Array1<f64>> = (0..m).map(|_| Array1::zeros(d)).collect();
119
120 for _ in 0..max_cycles {
121 let x_prev = x.clone();
122 for (i, (a, b)) in self.facets.iter().enumerate() {
123 let mut y_i = x.clone();
125 y_i += &p_corr[i];
126
127 let a_tb = a.dot(&y_i) - *b;
129 let a_norm2 = a.dot(a).max(FACET_NORM2_FLOOR);
130 if a_tb > 0.0 {
131 let alpha = a_tb / a_norm2;
133 let z = &y_i - &(a * alpha);
134 p_corr[i] = &y_i - &z;
136 x = z;
137 } else {
138 p_corr[i].fill(0.0);
140 x = y_i;
141 }
142 }
143
144 let diff = (&x - &x_prev).mapv(|v| v.abs()).sum();
146 if diff < tol {
147 return x;
148 }
149 }
150
151 x
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use ndarray::array;
160
161 fn unit_square_hull() -> PeeledHull {
162 PeeledHull {
164 facets: vec![
165 (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), ],
170 dim: 2,
171 }
172 }
173
174 #[test]
175 fn test_is_inside_unit_square() {
176 let h = unit_square_hull();
177 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())); }
185
186 #[test]
187 fn test_project_point_unit_square() {
188 let h = unit_square_hull();
189 let p_in = array![0.5, 0.5];
191 let proj_in = h.project_point(p_in.view());
192 assert!((&proj_in - &p_in).mapv(|v| v.abs()).sum() < 1e-12);
193
194 let p_face = array![1.5, 0.5];
196 let proj_face = h.project_point(p_face.view());
197 assert!((&proj_face - &array![1.0, 0.5]).mapv(|v| v.abs()).sum() < 1e-8);
198
199 let p_corner = array![1.5, -0.5];
201 let proj_corner = h.project_point(p_corner.view());
202 assert!((&proj_corner - &array![1.0, 0.0]).mapv(|v| v.abs()).sum() < 1e-6);
203 }
204
205 #[test]
206 fn test_signed_distance_unit_square() {
207 let h = unit_square_hull();
208 let d_center = h.signed_distance(array![0.5, 0.5].view());
210 assert!((d_center + 0.5).abs() < 1e-12);
211
212 let d_inside = h.signed_distance(array![0.2, 0.8].view());
214 assert!((d_inside + 0.2).abs() < 1e-12);
215
216 let d_edge = h.signed_distance(array![1.0, 0.3].view());
218 assert!(d_edge.abs() < 1e-12);
219
220 let d_out_x = h.signed_distance(array![1.5, 0.5].view());
222 assert!((d_out_x - 0.5).abs() < 1e-8);
223
224 let d_out_corner = h.signed_distance(array![1.5, -0.5].view());
226 assert!((d_out_corner - (0.5f64.hypot(0.5))).abs() < 1e-6);
227 }
228}