use serde::{Deserialize, Serialize};
use crate::cover::OpenCover;
use crate::section::SectionFamily;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CohomologyGroup {
pub degree: usize,
pub dimension: usize,
pub generators: Vec<Vec<f64>>,
}
fn gaussian_elimination(mat: &mut Vec<Vec<f64>>, tol: f64) -> usize {
if mat.is_empty() || mat[0].is_empty() {
return 0;
}
let rows = mat.len();
let cols = mat[0].len();
let mut pivot_row = 0;
for col in 0..cols {
let mut best = None;
let mut best_val = 0.0_f64;
for r in pivot_row..rows {
let v = mat[r][col].abs();
if v > best_val && v > tol {
best = Some(r);
best_val = v;
}
}
let Some(pr) = best else { continue };
mat.swap(pivot_row, pr);
let pivot_val = mat[pivot_row][col];
for r in (pivot_row + 1)..rows {
let factor = mat[r][col] / pivot_val;
for c in col..cols {
mat[r][c] -= factor * mat[pivot_row][c];
if mat[r][c].abs() < tol {
mat[r][c] = 0.0;
}
}
}
pivot_row += 1;
if pivot_row == rows {
break;
}
}
pivot_row
}
fn nullspace(mat: &[Vec<f64>], tol: f64) -> Vec<Vec<f64>> {
let ncols = if mat.is_empty() {
0
} else {
mat[0].len()
};
if mat.is_empty() && ncols == 0 {
return vec![];
}
if mat.is_empty() {
return (0..ncols)
.map(|i| {
let mut v = vec![0.0; ncols];
v[i] = 1.0;
v
})
.collect();
}
let rows = mat.len();
let cols = mat[0].len();
if cols == 0 {
return vec![];
}
let mut m = mat.to_vec();
let mut pivot_cols: Vec<usize> = Vec::new();
let mut pivot_row = 0;
for col in 0..cols {
let mut best = None;
let mut best_val = 0.0;
for r in pivot_row..rows {
let v = m[r][col].abs();
if v > best_val && v > tol {
best = Some(r);
best_val = v;
}
}
let Some(pr) = best else { continue };
m.swap(pivot_row, pr);
let pv = m[pivot_row][col];
for c in 0..cols {
m[pivot_row][c] /= pv;
}
for r in 0..rows {
if r == pivot_row {
continue;
}
let factor = m[r][col];
if factor.abs() > tol {
for c in 0..cols {
m[r][c] -= factor * m[pivot_row][c];
if m[r][c].abs() < tol {
m[r][c] = 0.0;
}
}
}
}
pivot_cols.push(col);
pivot_row += 1;
if pivot_row == rows {
break;
}
}
let free_cols: Vec<usize> = (0..cols).filter(|c| !pivot_cols.contains(c)).collect();
free_cols
.iter()
.map(|&fc| {
let mut v = vec![0.0; cols];
v[fc] = 1.0;
for (i, &pc) in pivot_cols.iter().enumerate() {
v[pc] = -m[i][fc];
}
for x in &mut v {
if x.abs() < tol {
*x = 0.0;
}
}
v
})
.collect()
}
fn build_d0_matrix(cover: &OpenCover) -> (Vec<Vec<f64>>, Vec<(usize, usize, usize)>) {
let intersections = cover.nonempty_intersections();
let mut row_labels: Vec<(usize, usize, usize)> = Vec::new(); let ncols: usize = cover.sets.iter().map(|s| s.len()).sum();
let mut mat: Vec<Vec<f64>> = Vec::new();
let mut col_offset = 0;
let col_offsets: Vec<usize> = cover
.sets
.iter()
.scan(0, |acc, s| {
let off = *acc;
*acc += s.len();
Some(off)
})
.collect();
for (i, j, inter) in &intersections {
for (k, &global_idx) in inter.iter().enumerate() {
let mut row = vec![0.0; ncols];
let pos_i = cover.sets[*i]
.iter()
.position(|&x| x == global_idx)
.unwrap();
row[col_offsets[*i] + pos_i] = 1.0;
let pos_j = cover.sets[*j]
.iter()
.position(|&x| x == global_idx)
.unwrap();
row[col_offsets[*j] + pos_j] = -1.0;
mat.push(row);
row_labels.push((*i, *j, k));
}
}
(mat, row_labels)
}
fn build_d1_matrix(cover: &OpenCover) -> Vec<Vec<f64>> {
let triples = cover.triple_intersections();
let intersections = cover.nonempty_intersections();
let ncols: usize = intersections.iter().map(|(_, _, v)| v.len()).sum();
if triples.is_empty() || ncols == 0 {
return vec![];
}
let inter_col_offsets: Vec<usize> = intersections
.iter()
.scan(0, |acc, (_, _, v)| {
let off = *acc;
*acc += v.len();
Some(off)
})
.collect();
let mut pair_idx = std::collections::HashMap::new();
for (idx, (i, j, _)) in intersections.iter().enumerate() {
pair_idx.insert((*i, *j), idx);
}
let mut mat: Vec<Vec<f64>> = Vec::new();
for (i, j, k, tri_inter) in &triples {
for (t, &global_idx) in tri_inter.iter().enumerate() {
let mut row = vec![0.0; ncols];
if let Some(&pij) = pair_idx.get(&(*i, *j)) {
let local_pos = intersections[pij]
.2
.iter()
.position(|&x| x == global_idx)
.unwrap();
row[inter_col_offsets[pij] + local_pos] += 1.0;
}
if let Some(&pik) = pair_idx.get(&(*i, *k)) {
let local_pos = intersections[pik]
.2
.iter()
.position(|&x| x == global_idx)
.unwrap();
row[inter_col_offsets[pik] + local_pos] -= 1.0;
}
if let Some(&pjk) = pair_idx.get(&(*j, *k)) {
let local_pos = intersections[pjk]
.2
.iter()
.position(|&x| x == global_idx)
.unwrap();
row[inter_col_offsets[pjk] + local_pos] += 1.0;
}
mat.push(row);
}
}
mat
}
pub fn compute_cohomology(cover: &OpenCover, tol: f64) -> (CohomologyGroup, CohomologyGroup) {
let (d0, _) = build_d0_matrix(cover);
let d1 = build_d1_matrix(cover);
let c0_dim: usize = cover.sets.iter().map(|s| s.len()).sum();
let c1_dim: usize = cover
.nonempty_intersections()
.iter()
.map(|(_, _, v)| v.len())
.sum();
let h0_gens = if d0.is_empty() && c0_dim > 0 {
(0..c0_dim)
.map(|i| {
let mut v = vec![0.0; c0_dim];
v[i] = 1.0;
v
})
.collect()
} else {
nullspace(&d0, tol)
};
let h0 = CohomologyGroup {
degree: 0,
dimension: h0_gens.len(),
generators: h0_gens,
};
let d1_rank = {
let mut d1_copy = d1.clone();
gaussian_elimination(&mut d1_copy, tol)
};
let ker_d1_dim = c1_dim.saturating_sub(d1_rank);
let im_d0_dim = {
let mut d0_copy = d0.clone();
gaussian_elimination(&mut d0_copy, tol)
};
let h1_dim = ker_d1_dim.saturating_sub(im_d0_dim);
let ker_d1_gens = if d1.is_empty() && c1_dim > 0 {
(0..c1_dim)
.map(|i| {
let mut v = vec![0.0; c1_dim];
v[i] = 1.0;
v
})
.collect()
} else {
nullspace(&d1, tol)
};
let d0_gens = nullspace_with_complement(&d0, c1_dim, tol);
let _ = d0_gens;
let mut h1_gens = Vec::new();
let im_d0_basis = compute_image_basis(&d0, tol);
for g in &ker_d1_gens {
if !is_in_span(g, &im_d0_basis, tol) {
h1_gens.push(g.clone());
}
}
let h1 = CohomologyGroup {
degree: 1,
dimension: h1_dim,
generators: h1_gens,
};
(h0, h1)
}
fn compute_image_basis(mat: &[Vec<f64>], tol: f64) -> Vec<Vec<f64>> {
if mat.is_empty() || mat[0].is_empty() {
return vec![];
}
let rows = mat.len();
let cols = mat[0].len();
let mut m = mat.to_vec();
let rank = gaussian_elimination(&mut m, tol);
m[..rank].to_vec()
}
fn is_in_span(v: &[f64], basis: &[Vec<f64>], tol: f64) -> bool {
if basis.is_empty() {
return v.iter().all(|x| x.abs() < tol);
}
let n = v.len();
let mut aug: Vec<Vec<f64>> = basis
.iter()
.filter(|b| b.len() == n)
.cloned()
.collect();
if aug.is_empty() {
return v.iter().all(|x| x.abs() < tol);
}
let cols = aug[0].len();
aug.push(v.to_vec());
let rank_without = {
let mut m = aug[..aug.len() - 1].to_vec();
gaussian_elimination(&mut m, tol)
};
let rank_with = {
let mut m = aug.clone();
gaussian_elimination(&mut m, tol)
};
rank_with == rank_without
}
fn nullspace_with_complement(
mat: &[Vec<f64>],
_total_dim: usize,
tol: f64,
) -> Vec<Vec<f64>> {
nullspace(mat, tol)
}
pub fn compute_h0_from_sections(
cover: &OpenCover,
sections: &SectionFamily,
tol: f64,
) -> CohomologyGroup {
let (h0, _) = compute_cohomology(cover, tol);
h0
}
pub fn compute_h1_from_sections(
cover: &OpenCover,
sections: &SectionFamily,
tol: f64,
) -> CohomologyGroup {
let (_, h1) = compute_cohomology(cover, tol);
h1
}
#[cfg(test)]
mod gauss_tests {
use super::*;
#[test]
fn test_gauss_identity() {
let mut m = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let rank = gaussian_elimination(&mut m, 1e-10);
assert_eq!(rank, 2);
}
#[test]
fn test_gauss_singular() {
let mut m = vec![vec![1.0, 2.0], vec![2.0, 4.0]];
let rank = gaussian_elimination(&mut m, 1e-10);
assert_eq!(rank, 1);
}
#[test]
fn test_nullspace_identity() {
let m = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let ns = nullspace(&m, 1e-10);
assert!(ns.is_empty());
}
#[test]
fn test_nullspace_zero_row() {
let m = vec![vec![1.0, 1.0]];
let ns = nullspace(&m, 1e-10);
assert_eq!(ns.len(), 1);
assert!((ns[0][0] - 1.0).abs() < 1e-6 || (ns[0][0] + 1.0).abs() < 1e-6);
}
#[test]
fn test_nullspace_empty_matrix() {
let m: Vec<Vec<f64>> = vec![vec![0.0, 0.0], vec![0.0, 0.0]];
let ns = nullspace(&m, 1e-10);
assert_eq!(ns.len(), 2);
}
}