use crate::error::{Result, TransformError};
use crate::tda::alpha_complex::sym_diff_sorted;
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct CubicalConfig {
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CubicalCell {
pub coordinates: Vec<usize>,
pub dimension: usize,
}
impl CubicalCell {
pub fn new(coordinates: Vec<usize>) -> Self {
let dimension = coordinates.iter().filter(|&&c| c % 2 == 1).count();
Self {
coordinates,
dimension,
}
}
pub fn sort_key(&self) -> (usize, &[usize]) {
(self.dimension, &self.coordinates)
}
}
#[derive(Debug, Clone)]
pub struct CubicalComplex {
pub cells: Vec<(CubicalCell, f64)>,
pub spatial_dim: usize,
}
impl CubicalComplex {
pub fn from_image_2d(image: &[Vec<f64>]) -> Result<Self> {
if image.is_empty() || image[0].is_empty() {
return Err(TransformError::InvalidInput(
"Image must be non-empty".to_string(),
));
}
let rows = image.len();
let cols = image[0].len();
for row in image.iter() {
if row.len() != cols {
return Err(TransformError::InvalidInput(
"All image rows must have the same length".to_string(),
));
}
}
let vertex_value = |r: usize, c: usize| -> f64 {
let pr = r.min(rows - 1);
let pc = c.min(cols - 1);
image[pr][pc]
};
let mut cells: Vec<(CubicalCell, f64)> = Vec::new();
for r in 0..=rows {
for c in 0..=cols {
let fv = vertex_value(r, c);
cells.push((CubicalCell::new(vec![2 * r, 2 * c]), fv));
}
}
for r in 0..=rows {
for c in 0..cols {
let fv = vertex_value(r, c).max(vertex_value(r, c + 1));
cells.push((CubicalCell::new(vec![2 * r, 2 * c + 1]), fv));
}
}
for r in 0..rows {
for c in 0..=cols {
let fv = vertex_value(r, c).max(vertex_value(r + 1, c));
cells.push((CubicalCell::new(vec![2 * r + 1, 2 * c]), fv));
}
}
for r in 0..rows {
for c in 0..cols {
let fv = vertex_value(r, c)
.max(vertex_value(r, c + 1))
.max(vertex_value(r + 1, c))
.max(vertex_value(r + 1, c + 1));
cells.push((CubicalCell::new(vec![2 * r + 1, 2 * c + 1]), fv));
}
}
cells.sort_by(|(ca, fa), (cb, fb)| {
fa.partial_cmp(fb)
.unwrap_or(std::cmp::Ordering::Equal)
.then(ca.dimension.cmp(&cb.dimension))
.then(ca.coordinates.cmp(&cb.coordinates))
});
Ok(Self {
cells,
spatial_dim: 2,
})
}
pub fn from_image_3d(image: &[Vec<Vec<f64>>]) -> Result<Self> {
if image.is_empty() || image[0].is_empty() || image[0][0].is_empty() {
return Err(TransformError::InvalidInput(
"3D image must be non-empty".to_string(),
));
}
let slices = image.len();
let rows = image[0].len();
let cols = image[0][0].len();
let vertex_value = |s: usize, r: usize, c: usize| -> f64 {
let ps = s.min(slices - 1);
let pr = r.min(rows - 1);
let pc = c.min(cols - 1);
image[ps][pr][pc]
};
let mut cells: Vec<(CubicalCell, f64)> = Vec::new();
for s in 0..=slices {
for r in 0..=rows {
for c in 0..=cols {
let fv = vertex_value(s, r, c);
cells.push((CubicalCell::new(vec![2 * s, 2 * r, 2 * c]), fv));
}
}
}
for s in 0..slices {
for r in 0..=rows {
for c in 0..=cols {
let fv = vertex_value(s, r, c).max(vertex_value(s + 1, r, c));
cells.push((CubicalCell::new(vec![2 * s + 1, 2 * r, 2 * c]), fv));
}
}
}
for s in 0..=slices {
for r in 0..rows {
for c in 0..=cols {
let fv = vertex_value(s, r, c).max(vertex_value(s, r + 1, c));
cells.push((CubicalCell::new(vec![2 * s, 2 * r + 1, 2 * c]), fv));
}
}
}
for s in 0..=slices {
for r in 0..=rows {
for c in 0..cols {
let fv = vertex_value(s, r, c).max(vertex_value(s, r, c + 1));
cells.push((CubicalCell::new(vec![2 * s, 2 * r, 2 * c + 1]), fv));
}
}
}
for s in 0..slices {
for r in 0..rows {
for c in 0..=cols {
let fv = vertex_value(s, r, c)
.max(vertex_value(s + 1, r, c))
.max(vertex_value(s, r + 1, c))
.max(vertex_value(s + 1, r + 1, c));
cells.push((CubicalCell::new(vec![2 * s + 1, 2 * r + 1, 2 * c]), fv));
}
}
}
for s in 0..slices {
for r in 0..=rows {
for c in 0..cols {
let fv = vertex_value(s, r, c)
.max(vertex_value(s + 1, r, c))
.max(vertex_value(s, r, c + 1))
.max(vertex_value(s + 1, r, c + 1));
cells.push((CubicalCell::new(vec![2 * s + 1, 2 * r, 2 * c + 1]), fv));
}
}
}
for s in 0..=slices {
for r in 0..rows {
for c in 0..cols {
let fv = vertex_value(s, r, c)
.max(vertex_value(s, r + 1, c))
.max(vertex_value(s, r, c + 1))
.max(vertex_value(s, r + 1, c + 1));
cells.push((CubicalCell::new(vec![2 * s, 2 * r + 1, 2 * c + 1]), fv));
}
}
}
for s in 0..slices {
for r in 0..rows {
for c in 0..cols {
let fv = vertex_value(s, r, c)
.max(vertex_value(s + 1, r, c))
.max(vertex_value(s, r + 1, c))
.max(vertex_value(s, r, c + 1))
.max(vertex_value(s + 1, r + 1, c))
.max(vertex_value(s + 1, r, c + 1))
.max(vertex_value(s, r + 1, c + 1))
.max(vertex_value(s + 1, r + 1, c + 1));
cells.push((CubicalCell::new(vec![2 * s + 1, 2 * r + 1, 2 * c + 1]), fv));
}
}
}
cells.sort_by(|(ca, fa), (cb, fb)| {
fa.partial_cmp(fb)
.unwrap_or(std::cmp::Ordering::Equal)
.then(ca.dimension.cmp(&cb.dimension))
.then(ca.coordinates.cmp(&cb.coordinates))
});
Ok(Self {
cells,
spatial_dim: 3,
})
}
pub fn boundary(&self, cell: &CubicalCell) -> Vec<CubicalCell> {
if cell.dimension == 0 {
return Vec::new();
}
let mut faces = Vec::new();
for (i, &coord) in cell.coordinates.iter().enumerate() {
if coord % 2 == 1 {
let mut coords_low = cell.coordinates.clone();
coords_low[i] = coord - 1;
faces.push(CubicalCell::new(coords_low));
let mut coords_high = cell.coordinates.clone();
coords_high[i] = coord + 1;
faces.push(CubicalCell::new(coords_high));
}
}
faces
}
pub fn persistence_diagram(&self) -> Vec<(f64, f64, usize)> {
let n = self.cells.len();
let cell_index: HashMap<&CubicalCell, usize> = self
.cells
.iter()
.enumerate()
.map(|(i, (c, _))| (c, i))
.collect();
let mut columns: Vec<Vec<usize>> = self
.cells
.iter()
.map(|(cell, _)| {
let mut col: Vec<usize> = self
.boundary(cell)
.iter()
.filter_map(|face| cell_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();
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 (birth_cell, birth_fv) = &self.cells[birth_idx];
let (_, death_fv) = &self.cells[death_idx];
let dim = birth_cell.dimension;
pairs.push((*birth_fv, *death_fv, dim));
}
}
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
pairs
}
pub fn cell_count(&self, dim: usize) -> usize {
self.cells
.iter()
.filter(|(c, _)| c.dimension == dim)
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_3x3() -> Vec<Vec<f64>> {
vec![
vec![0.0, 1.0, 0.0],
vec![1.0, 1.0, 1.0],
vec![0.0, 1.0, 0.0],
]
}
#[test]
fn test_cell_counts_3x3() {
let img = simple_3x3();
let cc = CubicalComplex::from_image_2d(&img).expect("should build");
assert_eq!(cc.cell_count(0), 16, "Expected 16 vertices");
assert_eq!(cc.cell_count(1), 24, "Expected 24 edges");
assert_eq!(cc.cell_count(2), 9, "Expected 9 faces");
assert_eq!(cc.cells.len(), 49, "Expected 49 total cells");
}
#[test]
fn test_boundary_of_edge() {
let edge = CubicalCell::new(vec![0, 1]);
assert_eq!(edge.dimension, 1);
let img = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
let cc = CubicalComplex::from_image_2d(&img).expect("should build");
let faces = cc.boundary(&edge);
assert_eq!(faces.len(), 2);
assert!(faces.iter().all(|f| f.dimension == 0));
}
#[test]
fn test_boundary_of_vertex_empty() {
let vtx = CubicalCell::new(vec![0, 0]);
let img = vec![vec![1.0]];
let cc = CubicalComplex::from_image_2d(&img).expect("should build");
let faces = cc.boundary(&vtx);
assert!(faces.is_empty());
}
#[test]
fn test_persistence_diagram_non_trivial() {
let img = simple_3x3();
let cc = CubicalComplex::from_image_2d(&img).expect("should build");
let diag = cc.persistence_diagram();
assert!(!diag.is_empty(), "Expected non-empty persistence diagram");
for (birth, death, _) in &diag {
assert!(birth <= death, "birth={birth} > death={death}");
}
}
#[test]
fn test_from_image_3d_basic() {
let vol = vec![
vec![vec![0.0, 1.0], vec![1.0, 0.0]],
vec![vec![1.0, 0.0], vec![0.0, 1.0]],
];
let cc = CubicalComplex::from_image_3d(&vol).expect("3D build should succeed");
assert_eq!(cc.cell_count(0), 27, "Expected 27 vertices");
assert!(cc.cell_count(3) > 0, "Expected 3-cells");
}
#[test]
fn test_invalid_empty_image() {
let result = CubicalComplex::from_image_2d(&[]);
assert!(result.is_err());
}
}