use crate::trace::event_structure::TracePoset;
use crate::trace::gf2::{BoundaryMatrix, PersistencePairs};
#[derive(Debug, Clone)]
pub struct SquareComplex {
pub num_vertices: usize,
pub edges: Vec<(usize, usize)>,
pub squares: Vec<(usize, usize, usize, usize)>,
}
impl SquareComplex {
#[must_use]
pub fn from_edges(num_vertices: usize, mut edges: Vec<(usize, usize)>) -> Self {
edges.sort_unstable();
edges.dedup();
let mut succs: Vec<Vec<usize>> = vec![Vec::new(); num_vertices];
for &(s, t) in &edges {
succs[s].push(t);
}
for s in &mut succs {
s.sort_unstable();
s.dedup();
}
let mut squares = Vec::new();
for a in 0..num_vertices {
let sa = &succs[a];
for (ib, &b) in sa.iter().enumerate() {
for &c in &sa[ib + 1..] {
let sb = &succs[b];
let sc = &succs[c];
let mut jb = 0;
let mut jc = 0;
while jb < sb.len() && jc < sc.len() {
match sb[jb].cmp(&sc[jc]) {
std::cmp::Ordering::Less => jb += 1,
std::cmp::Ordering::Greater => jc += 1,
std::cmp::Ordering::Equal => {
let d = sb[jb];
squares.push((a, b, c, d));
jb += 1;
jc += 1;
}
}
}
}
}
}
squares.sort_unstable();
Self {
num_vertices,
edges,
squares,
}
}
#[must_use]
pub fn from_trace_poset(poset: &TracePoset) -> Self {
let n = poset.len();
let mut edges = Vec::new();
for i in 0..n {
for &j in poset.succs(i) {
edges.push((i, j));
}
}
Self::from_edges(n, edges)
}
fn edge_index(&self, s: usize, t: usize) -> usize {
self.edges
.binary_search(&(s, t))
.unwrap_or_else(|_| panic!("edge ({s}, {t}) not in complex"))
}
#[must_use]
pub fn boundary_1(&self) -> BoundaryMatrix {
let mut d1 = BoundaryMatrix::zeros(self.num_vertices, self.edges.len());
for (col, &(s, t)) in self.edges.iter().enumerate() {
d1.set(s, col);
d1.set(t, col);
}
d1
}
#[must_use]
pub fn boundary_2(&self) -> BoundaryMatrix {
let mut d2 = BoundaryMatrix::zeros(self.edges.len(), self.squares.len());
for (col, &(a, b, c, d)) in self.squares.iter().enumerate() {
d2.set(self.edge_index(a, b), col);
d2.set(self.edge_index(a, c), col);
d2.set(self.edge_index(b, d), col);
d2.set(self.edge_index(c, d), col);
}
d2
}
#[must_use]
pub fn combined_boundary_matrix(&self) -> BoundaryMatrix {
let edge_offset = self.num_vertices;
let square_offset = edge_offset + self.edges.len();
let total_cells = square_offset + self.squares.len();
let mut matrix = BoundaryMatrix::zeros(total_cells, total_cells);
for (edge_idx, &(s, t)) in self.edges.iter().enumerate() {
let col = edge_offset + edge_idx;
matrix.set(s, col);
matrix.set(t, col);
}
for (square_idx, &(a, b, c, d)) in self.squares.iter().enumerate() {
let col = square_offset + square_idx;
matrix.set(edge_offset + self.edge_index(a, b), col);
matrix.set(edge_offset + self.edge_index(a, c), col);
matrix.set(edge_offset + self.edge_index(b, d), col);
matrix.set(edge_offset + self.edge_index(c, d), col);
}
matrix
}
#[must_use]
pub fn h1_persistence_pairs(&self) -> PersistencePairs {
let edge_start = self.num_vertices;
let edge_end = edge_start + self.edges.len();
let square_start = edge_end;
let square_end = square_start + self.squares.len();
let reduced = self.combined_boundary_matrix().reduce();
let pairs = reduced.persistence_pairs();
PersistencePairs {
pairs: pairs
.pairs
.into_iter()
.filter(|&(birth, death)| {
(edge_start..edge_end).contains(&birth)
&& (square_start..square_end).contains(&death)
})
.collect(),
unpaired: pairs
.unpaired
.into_iter()
.filter(|birth| (edge_start..edge_end).contains(birth))
.collect(),
}
}
}
#[must_use]
pub fn matmul_gf2(a: &BoundaryMatrix, b: &BoundaryMatrix) -> BoundaryMatrix {
assert_eq!(
a.cols(),
b.rows(),
"matmul dimension mismatch: A is {}x{}, B is {}x{}",
a.rows(),
a.cols(),
b.rows(),
b.cols()
);
let mut result = BoundaryMatrix::zeros(a.rows(), b.cols());
for j in 0..b.cols() {
for row_of_b in b.column(j).ones() {
let a_col = a.column(row_of_b).clone();
result.column_mut(j).xor_assign(&a_col);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::trace::event_structure::TracePoset;
use crate::trace::{TraceData, TraceEvent, TraceEventKind};
use crate::types::{RegionId, TaskId, Time};
#[test]
fn single_edge() {
let cx = SquareComplex::from_edges(2, vec![(0, 1)]);
assert_eq!(cx.edges.len(), 1);
assert_eq!(cx.squares.len(), 0);
let d1 = cx.boundary_1();
assert_eq!(d1.rows(), 2);
assert_eq!(d1.cols(), 1);
assert!(d1.get(0, 0));
assert!(d1.get(1, 0));
}
#[test]
fn triangle_no_squares() {
let cx = SquareComplex::from_edges(3, vec![(0, 1), (0, 2), (1, 2)]);
assert_eq!(cx.edges.len(), 3);
assert_eq!(cx.squares.len(), 0); }
#[test]
fn single_square() {
let cx = SquareComplex::from_edges(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]);
assert_eq!(cx.edges.len(), 4);
assert_eq!(cx.squares.len(), 1);
assert_eq!(cx.squares[0], (0, 1, 2, 3));
}
#[test]
fn boundary_composition_single_square() {
let cx = SquareComplex::from_edges(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]);
let d1 = cx.boundary_1();
let d2 = cx.boundary_2();
let product = matmul_gf2(&d1, &d2);
for j in 0..product.cols() {
assert!(product.column(j).is_zero(), "∂₁∂₂ column {j} is non-zero");
}
}
#[test]
fn boundary_composition_grid() {
let edges = vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (3, 5), (4, 5)];
let cx = SquareComplex::from_edges(6, edges);
assert_eq!(cx.squares.len(), 2);
let d1 = cx.boundary_1();
let d2 = cx.boundary_2();
let product = matmul_gf2(&d1, &d2);
for j in 0..product.cols() {
assert!(product.column(j).is_zero(), "∂₁∂₂ column {j} is non-zero");
}
}
#[test]
fn boundary_composition_large_lattice() {
let n = 4;
let idx = |i: usize, j: usize| i * n + j;
let mut edges = Vec::new();
for i in 0..n {
for j in 0..n {
if j + 1 < n {
edges.push((idx(i, j), idx(i, j + 1)));
}
if i + 1 < n {
edges.push((idx(i, j), idx(i + 1, j)));
}
}
}
let cx = SquareComplex::from_edges(n * n, edges);
assert_eq!(cx.squares.len(), 9);
let d1 = cx.boundary_1();
let d2 = cx.boundary_2();
let product = matmul_gf2(&d1, &d2);
for j in 0..product.cols() {
assert!(
product.column(j).is_zero(),
"∂₁∂₂ column {j} is non-zero in {n}x{n} grid"
);
}
}
#[test]
fn deterministic_ordering() {
let e1 = vec![(2, 3), (0, 1), (1, 3), (0, 2)];
let e2 = vec![(0, 2), (1, 3), (0, 1), (2, 3)];
let cx1 = SquareComplex::from_edges(4, e1);
let cx2 = SquareComplex::from_edges(4, e2);
assert_eq!(cx1.edges, cx2.edges);
assert_eq!(cx1.squares, cx2.squares);
}
#[test]
fn empty_complex() {
let cx = SquareComplex::from_edges(0, vec![]);
assert_eq!(cx.edges.len(), 0);
assert_eq!(cx.squares.len(), 0);
let d1 = cx.boundary_1();
let d2 = cx.boundary_2();
assert_eq!(d1.rows(), 0);
assert_eq!(d1.cols(), 0);
assert_eq!(d2.rows(), 0);
assert_eq!(d2.cols(), 0);
}
#[test]
fn vertices_only() {
let cx = SquareComplex::from_edges(5, vec![]);
assert_eq!(cx.edges.len(), 0);
assert_eq!(cx.squares.len(), 0);
}
#[test]
fn toy_trace_independent_tasks() {
let r1 = RegionId::new_for_test(1, 0);
let r2 = RegionId::new_for_test(2, 0);
let t1 = TaskId::new_for_test(1, 0);
let t2 = TaskId::new_for_test(2, 0);
let trace = vec![
TraceEvent::spawn(1, Time::from_nanos(10), t1, r1),
TraceEvent::spawn(2, Time::from_nanos(20), t2, r2),
];
let poset = TracePoset::from_trace(&trace);
let cx = SquareComplex::from_trace_poset(&poset);
assert_eq!(cx.num_vertices, 2);
assert_eq!(cx.edges.len(), 0);
assert_eq!(cx.squares.len(), 0);
}
#[test]
fn toy_trace_dependent_tasks() {
let r = RegionId::new_for_test(1, 0);
let t = TaskId::new_for_test(7, 0);
let trace = vec![
TraceEvent::spawn(1, Time::from_nanos(10), t, r),
TraceEvent::schedule(2, Time::from_nanos(20), t, r),
];
let poset = TracePoset::from_trace(&trace);
let cx = SquareComplex::from_trace_poset(&poset);
assert_eq!(cx.num_vertices, 2);
assert_eq!(cx.edges.len(), 1);
assert_eq!(cx.edges[0], (0, 1));
assert_eq!(cx.squares.len(), 0);
}
#[test]
fn toy_trace_diamond() {
let r1 = RegionId::new_for_test(1, 0);
let r2 = RegionId::new_for_test(2, 0);
let t1 = TaskId::new_for_test(1, 0);
let t2 = TaskId::new_for_test(2, 0);
let trace = vec![
TraceEvent::spawn(1, Time::from_nanos(10), t1, r1),
TraceEvent::spawn(2, Time::from_nanos(20), t2, r2),
TraceEvent::schedule(3, Time::from_nanos(30), t1, r1),
TraceEvent::schedule(4, Time::from_nanos(40), t2, r2),
];
let poset = TracePoset::from_trace(&trace);
let cx = SquareComplex::from_trace_poset(&poset);
assert_eq!(cx.num_vertices, 4);
assert_eq!(cx.edges.len(), 2);
assert_eq!(cx.squares.len(), 0);
let d1 = cx.boundary_1();
let d2 = cx.boundary_2();
let product = matmul_gf2(&d1, &d2);
assert_eq!(product.cols(), 0);
}
#[test]
fn toy_trace_commutation_square() {
let trace = vec![
TraceEvent::new(
1,
Time::from_nanos(10),
TraceEventKind::ChaosInjection,
TraceData::Chaos {
kind: "chaos-a".to_string(),
task: None,
detail: "write global state".to_string(),
},
),
TraceEvent::new(
2,
Time::from_nanos(20),
TraceEventKind::Checkpoint,
TraceData::Checkpoint {
sequence: 1,
active_tasks: 1,
active_regions: 1,
},
),
TraceEvent::new(
3,
Time::from_nanos(30),
TraceEventKind::Checkpoint,
TraceData::Checkpoint {
sequence: 2,
active_tasks: 1,
active_regions: 1,
},
),
TraceEvent::new(
4,
Time::from_nanos(40),
TraceEventKind::ChaosInjection,
TraceData::Chaos {
kind: "chaos-b".to_string(),
task: None,
detail: "write global state".to_string(),
},
),
];
let poset = TracePoset::from_trace(&trace);
let cx = SquareComplex::from_trace_poset(&poset);
assert_eq!(cx.squares.len(), 1);
assert_eq!(cx.squares[0], (0, 1, 2, 3));
}
#[test]
fn toy_trace_dependent_actions_no_square() {
let trace = vec![
TraceEvent::new(
1,
Time::from_nanos(10),
TraceEventKind::ChaosInjection,
TraceData::Chaos {
kind: "write-a".to_string(),
task: None,
detail: "write global state".to_string(),
},
),
TraceEvent::new(
2,
Time::from_nanos(20),
TraceEventKind::ChaosInjection,
TraceData::Chaos {
kind: "write-b".to_string(),
task: None,
detail: "write global state".to_string(),
},
),
TraceEvent::new(
3,
Time::from_nanos(30),
TraceEventKind::Checkpoint,
TraceData::Checkpoint {
sequence: 1,
active_tasks: 1,
active_regions: 1,
},
),
TraceEvent::new(
4,
Time::from_nanos(40),
TraceEventKind::Checkpoint,
TraceData::Checkpoint {
sequence: 2,
active_tasks: 1,
active_regions: 1,
},
),
];
let poset = TracePoset::from_trace(&trace);
let cx = SquareComplex::from_trace_poset(&poset);
assert_eq!(cx.squares.len(), 0);
}
#[test]
fn triangle_cycle_survives_as_unpaired_h1_class() {
let cx = SquareComplex::from_edges(3, vec![(0, 1), (0, 2), (1, 2)]);
let pairs = cx.h1_persistence_pairs();
assert!(pairs.pairs.is_empty(), "unfilled triangle has no H1 deaths");
assert_eq!(
pairs.unpaired.len(),
1,
"triangle should contribute one H1 class"
);
assert!(
(3..6).contains(&pairs.unpaired[0]),
"triangle H1 birth should come from an edge column, got {}",
pairs.unpaired[0]
);
}
#[test]
fn h1_cycle_born_and_killed() {
let cx = SquareComplex::from_edges(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]);
let pairs = cx.h1_persistence_pairs();
let square_pair = pairs.pairs.iter().find(|&&(_, death)| death == 10);
assert!(
square_pair.is_some(),
"expected 1-cycle killed by square at column 10, pairs: {pairs:?}"
);
let (birth, death) = square_pair.unwrap();
assert!(
(5..=8).contains(birth),
"birth {birth} should be an edge in the cycle (cols 5..=8), paired with {death}"
);
assert_eq!(
pairs.unpaired,
Vec::<usize>::new(),
"the square should kill the only H1 class in this complex. birth was {birth}"
);
}
#[test]
fn from_trace_poset_matches_from_edges() {
let r = RegionId::new_for_test(1, 0);
let t1 = TaskId::new_for_test(1, 0);
let t2 = TaskId::new_for_test(2, 0);
let trace = vec![
TraceEvent::spawn(1, Time::from_nanos(10), t1, r),
TraceEvent::spawn(2, Time::from_nanos(20), t2, r),
TraceEvent::schedule(3, Time::from_nanos(30), t1, r),
];
let poset = TracePoset::from_trace(&trace);
let mut manual_edges = Vec::new();
for i in 0..poset.len() {
for &j in poset.succs(i) {
manual_edges.push((i, j));
}
}
let cx_manual = SquareComplex::from_edges(poset.len(), manual_edges);
let cx_poset = SquareComplex::from_trace_poset(&poset);
assert_eq!(cx_manual.edges, cx_poset.edges);
assert_eq!(cx_manual.squares, cx_poset.squares);
}
}