2. A filtration and a basis
For persistent homology the rows need filtration values. Implement HasRowFiltration on the oracle:
/// Step 2: the filtration on rows (cells), read off the volume. Implementing it on the oracle, rather than attaching
/// a closure with `with_filtration`, leaves room to override the optional hooks later.
impl HasRowFiltration for Boundary<'_> {
type FiltrationT = NotNan<f32>;
fn filtration_value(&self, row: Cell) -> NotNan<f32> {
NotNan::new(self.0.value(row.0)).unwrap()
}
}
The filtration type must be Ord; NotNan from ordered-float is the usual choice. (For a quick experiment you can
attach a closure instead, with Boundary(&volume).with_filtration(|cell| ...), but implementing the trait on your own
type leaves room for the optional hooks in step 5.)
Rows are now ordered by (value, label), so the basis must be too:
/// As `basis_by_number`, but in filtration order: by value, then number.
fn basis(volume: &Volume, max_dim: usize) -> SplitBasis<Cell> {
let mut dims: Vec<Vec<Cell>> = vec![vec![]; max_dim + 1];
for cell in 0..volume.n_cells() {
let dim = volume.dimension(cell);
if dim <= max_dim {
dims[dim].push(Cell(cell));
}
}
for cells in &mut dims {
cells.sort_by_key(|&cell| (NotNan::new(volume.value(cell.0)).unwrap(), cell));
}
SplitBasis::from_dimensions(dims)
}
// Step 2: the lower-star filtration, by homology, with the standard algorithm.
// Every cell, cubes included, is a column.
let d = Boundary(&volume).with_basis(basis(&volume, 3));
let standard = timed("homology, standard algorithm", || {
barcode::homology(&d, Algorithm::Standard(Standard::new()), false)
});
println!(" {}", summary(&volume, &standard));
The result is a list of Bars: birth cell and value, death cell and value (None if essential), and optionally a
representative.
You can now compute persistent homology. But the standard algorithm reduces every column, cubes included, and builds every column in full. On a 40³ volume (493,039 cells) that takes 5.4 s.