use crate::error::{Result, TransformError};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct AlphaConfig {
pub max_alpha: f64,
}
impl Default for AlphaConfig {
fn default() -> Self {
Self {
max_alpha: f64::INFINITY,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Simplex {
pub vertices: Vec<usize>,
pub filtration_value: f64,
}
impl Simplex {
pub fn dimension(&self) -> usize {
self.vertices.len().saturating_sub(1)
}
pub fn boundary_faces(&self) -> Vec<Vec<usize>> {
if self.vertices.len() <= 1 {
return Vec::new();
}
(0..self.vertices.len())
.map(|i| {
self.vertices
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, &v)| v)
.collect::<Vec<usize>>()
})
.collect()
}
}
pub fn circumradius_2d(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> f64 {
let ax = a[0] - c[0];
let ay = a[1] - c[1];
let bx = b[0] - c[0];
let by = b[1] - c[1];
let d = 2.0 * (ax * by - ay * bx);
if d.abs() < 1e-14 {
return f64::INFINITY; }
let ux = (by * (ax * ax + ay * ay) - ay * (bx * bx + by * by)) / d;
let uy = (ax * (bx * bx + by * by) - bx * (ax * ax + ay * ay)) / d;
(ux * ux + uy * uy).sqrt()
}
#[inline]
fn dist2d(a: [f64; 2], b: [f64; 2]) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
(dx * dx + dy * dy).sqrt()
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Triangle {
v: [usize; 3],
}
impl Triangle {
fn new(a: usize, b: usize, c: usize) -> Self {
let mut v = [a, b, c];
v.sort_unstable();
Self { v }
}
}
fn in_circumcircle(p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
let ax = a[0] - p[0];
let ay = a[1] - p[1];
let bx = b[0] - p[0];
let by = b[1] - p[1];
let cx = c[0] - p[0];
let cy = c[1] - p[1];
let det = ax * (by * (cx * cx + cy * cy) - cy * (bx * bx + by * by))
- ay * (bx * (cx * cx + cy * cy) - cx * (bx * bx + by * by))
+ (ax * ax + ay * ay) * (bx * cy - by * cx);
let orientation = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
if orientation > 0.0 {
det > 0.0
} else {
det < 0.0
}
}
fn bowyer_watson(points: &[[f64; 2]]) -> Vec<Triangle> {
let n = points.len();
if n < 3 {
return Vec::new();
}
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for p in points {
min_x = min_x.min(p[0]);
min_y = min_y.min(p[1]);
max_x = max_x.max(p[0]);
max_y = max_y.max(p[1]);
}
let dx = max_x - min_x;
let dy = max_y - min_y;
let delta_max = dx.max(dy).max(1.0);
let mid_x = (min_x + max_x) / 2.0;
let mid_y = (min_y + max_y) / 2.0;
let st0 = [mid_x - 20.0 * delta_max, mid_y - delta_max];
let st1 = [mid_x, mid_y + 20.0 * delta_max];
let st2 = [mid_x + 20.0 * delta_max, mid_y - delta_max];
let super_indices = [n, n + 1, n + 2];
let mut pts: Vec<[f64; 2]> = points.to_vec();
pts.push(st0);
pts.push(st1);
pts.push(st2);
let mut triangles: HashSet<Triangle> = HashSet::from([Triangle::new(
super_indices[0],
super_indices[1],
super_indices[2],
)]);
for (idx, &p) in points.iter().enumerate() {
let bad: Vec<Triangle> = triangles
.iter()
.filter(|t| in_circumcircle(p, pts[t.v[0]], pts[t.v[1]], pts[t.v[2]]))
.cloned()
.collect();
let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
for t in &bad {
let edges = [(t.v[0], t.v[1]), (t.v[0], t.v[2]), (t.v[1], t.v[2])];
for (a, b) in edges {
let key = (a.min(b), a.max(b));
*edge_count.entry(key).or_insert(0) += 1;
}
}
let boundary: Vec<(usize, usize)> = edge_count
.into_iter()
.filter(|(_, cnt)| *cnt == 1)
.map(|(e, _)| e)
.collect();
for t in &bad {
triangles.remove(t);
}
for (a, b) in boundary {
triangles.insert(Triangle::new(idx, a, b));
}
}
triangles
.into_iter()
.filter(|t| !t.v.iter().any(|&v| v >= n))
.collect()
}
#[derive(Debug, Clone)]
pub struct AlphaComplex {
pub points: Vec<[f64; 2]>,
pub simplices: Vec<Simplex>,
}
impl AlphaComplex {
pub fn new(points: &[[f64; 2]]) -> Self {
let n = points.len();
let mut simplex_map: HashMap<Vec<usize>, f64> = HashMap::new();
for i in 0..n {
simplex_map.insert(vec![i], 0.0);
}
if n >= 2 {
let triangles = bowyer_watson(points);
let mut edges: HashSet<(usize, usize)> = HashSet::new();
for t in &triangles {
edges.insert((t.v[0].min(t.v[1]), t.v[0].max(t.v[1])));
edges.insert((t.v[0].min(t.v[2]), t.v[0].max(t.v[2])));
edges.insert((t.v[1].min(t.v[2]), t.v[1].max(t.v[2])));
}
for (a, b) in &edges {
let d = dist2d(points[*a], points[*b]) / 2.0;
let key = vec![*a, *b];
let entry = simplex_map.entry(key).or_insert(f64::INFINITY);
if d < *entry {
*entry = d;
}
}
for t in &triangles {
let a = t.v[0];
let b = t.v[1];
let c = t.v[2];
let r = circumradius_2d(points[a], points[b], points[c]);
let e_ab = simplex_map
.get(&vec![a.min(b), a.max(b)])
.copied()
.unwrap_or(0.0);
let e_ac = simplex_map
.get(&vec![a.min(c), a.max(c)])
.copied()
.unwrap_or(0.0);
let e_bc = simplex_map
.get(&vec![b.min(c), b.max(c)])
.copied()
.unwrap_or(0.0);
let fv = r.max(e_ab).max(e_ac).max(e_bc);
simplex_map.insert(vec![a, b, c], fv);
}
}
let mut simplices: Vec<Simplex> = simplex_map
.into_iter()
.map(|(vertices, fv)| Simplex {
vertices,
filtration_value: fv,
})
.collect();
simplices.sort_by(|a, b| {
a.filtration_value
.partial_cmp(&b.filtration_value)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.vertices.len().cmp(&b.vertices.len()))
.then(a.vertices.cmp(&b.vertices))
});
Self {
points: points.to_vec(),
simplices,
}
}
pub fn filtration(&self, alpha: f64) -> Vec<Simplex> {
self.simplices
.iter()
.filter(|s| s.filtration_value <= alpha)
.cloned()
.collect()
}
pub fn persistence_pairs(&self) -> Vec<(f64, f64, usize)> {
compute_persistence_from_simplices(&self.simplices)
}
}
pub fn compute_persistence_from_simplices(simplices: &[Simplex]) -> Vec<(f64, f64, usize)> {
let n = simplices.len();
let mut simplex_index: HashMap<Vec<usize>, usize> = HashMap::new();
for (idx, s) in simplices.iter().enumerate() {
simplex_index.insert(s.vertices.clone(), idx);
}
let mut columns: Vec<Vec<usize>> = simplices
.iter()
.map(|s| {
let mut col: Vec<usize> = s
.boundary_faces()
.iter()
.filter_map(|face| simplex_index.get(face).copied())
.collect();
col.sort_unstable();
col
})
.collect();
let mut pivot_col: HashMap<usize, usize> = HashMap::new(); let mut pairs: Vec<(f64, f64, usize)> = Vec::new();
let mut paired: HashSet<usize> = HashSet::new();
for j in 0..n {
while let Some(&pivot) = columns[j].last() {
if let Some(&k) = pivot_col.get(&pivot) {
let col_k = columns[k].clone();
sym_diff_sorted(&mut columns[j], &col_k);
} else {
break;
}
}
if let Some(&pivot) = columns[j].last() {
pivot_col.insert(pivot, j);
let birth_idx = pivot;
let death_idx = j;
let dim = simplices[birth_idx].dimension();
pairs.push((
simplices[birth_idx].filtration_value,
simplices[death_idx].filtration_value,
dim,
));
paired.insert(birth_idx);
paired.insert(death_idx);
}
}
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
pairs
}
pub fn sym_diff_sorted(a: &mut Vec<usize>, b: &[usize]) {
let mut result: Vec<usize> = Vec::new();
let mut ai = 0;
let mut bi = 0;
while ai < a.len() && bi < b.len() {
match a[ai].cmp(&b[bi]) {
std::cmp::Ordering::Less => {
result.push(a[ai]);
ai += 1;
}
std::cmp::Ordering::Greater => {
result.push(b[bi]);
bi += 1;
}
std::cmp::Ordering::Equal => {
ai += 1;
bi += 1;
}
}
}
result.extend_from_slice(&a[ai..]);
result.extend_from_slice(&b[bi..]);
*a = result;
}
#[cfg(test)]
mod tests {
use super::*;
fn equilateral_pts() -> Vec<[f64; 2]> {
vec![[0.0, 0.0], [1.0, 0.0], [0.5, 3_f64.sqrt() / 2.0]]
}
#[test]
fn test_circumradius_equilateral() {
let pts = equilateral_pts();
let r = circumradius_2d(pts[0], pts[1], pts[2]);
let expected = 1.0 / 3_f64.sqrt();
assert!(
(r - expected).abs() < 1e-10,
"circumradius={r}, expected={expected}"
);
}
#[test]
fn test_circumradius_right_triangle() {
let a = [0.0, 0.0];
let b = [3.0, 0.0];
let c = [0.0, 4.0];
let r = circumradius_2d(a, b, c);
assert!((r - 2.5).abs() < 1e-10, "circumradius={r}");
}
#[test]
fn test_filtration_threshold() {
let pts: Vec<[f64; 2]> = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 0.5]];
let ac = AlphaComplex::new(&pts);
let f0 = ac.filtration(0.0);
assert!(f0.iter().all(|s| s.dimension() == 0));
assert_eq!(f0.len(), 5);
let f_inf = ac.filtration(f64::INFINITY);
assert!(!f_inf.is_empty());
assert!(f_inf.iter().filter(|s| s.dimension() == 0).count() >= 5);
}
#[test]
fn test_persistence_pairs_triangle() {
let pts = equilateral_pts();
let ac = AlphaComplex::new(&pts);
let pairs = ac.persistence_pairs();
for (birth, death, _dim) in &pairs {
assert!(birth <= death, "Invalid pair: birth={birth}, death={death}");
}
}
#[test]
fn test_persistence_pairs_not_empty_for_triangle() {
let pts = equilateral_pts();
let ac = AlphaComplex::new(&pts);
let pairs = ac.persistence_pairs();
assert!(!pairs.is_empty(), "Expected at least one persistence pair");
}
#[test]
fn test_five_point_cloud() {
let pts: Vec<[f64; 2]> = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0], [1.0, 1.0]];
let ac = AlphaComplex::new(&pts);
assert_eq!(
ac.simplices.iter().filter(|s| s.dimension() == 0).count(),
5
);
assert!(
ac.simplices.iter().any(|s| s.dimension() == 1),
"Expected edges"
);
}
}