Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6. Leaving cells out

This is where a lazy oracle pulls ahead of a stored matrix. A cell in a zero-length apparent pair needs no reduction and gives no bar, and the reduction can recognise it again as an owner when it needs it (through first_column_in_row). So it needn’t be in the basis at all:

/// Step 6: whether a cell is in a zero-length apparent pair, as either its birth or its death. Such cells needn't be
/// in the basis at all: the reduction finds them again as owners (through `first_column_in_row`) when it needs them.
fn in_zero_apparent_pair(volume: &Volume, cell: u64) -> bool {
    let value = volume.value(cell);
    // A birth: its oldest coface has the same value, and the cell is that coface's youngest face
    let birth = oldest_coface(volume, cell)
        .is_some_and(|coface| volume.value(coface) == value && youngest_face(volume, coface) == Some(cell));
    // A death: its youngest face has the same value, and the cell is that face's oldest coface
    let death = youngest_face(volume, cell)
        .is_some_and(|face| volume.value(face) == value && oldest_coface(volume, face) == Some(cell));
    birth || death
}

/// The columns to reduce, up to dimension `max_dim`, without the cells of zero-length apparent pairs. Cells are
/// streamed (never all held at once), so memory goes on the cells that are kept.
fn reduction_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 && !in_zero_apparent_pair(volume, cell) {
            dims[dim].push(Cell(cell));
        }
    }
    for cells in &mut dims {
        cells.sort_by_key(|&cell| key(volume, cell.0));
    }
    SplitBasis::from_dimensions(dims)
}

A cell is in a zero-length apparent pair as a birth if its oldest coface has the same value and has it as youngest face, or as a death if its youngest face has the same value and has it as oldest coface. Both checks are local: a few voxel lookups. Cells are streamed, so only those kept are ever stored.

    // Step 6: leave the cells of zero-length apparent pairs out of the basis altogether
    let reduced = timed("build the reduction basis", || reduction_basis(&volume, 2));
    let reduced_size = basis_size(&reduced);
    let delta = Coboundary(&volume).with_basis(reduced);
    let lazy = timed("cohomology, clearing, reduction basis", || {
        barcode::cohomology(&delta, Algorithm::Clearing(algorithm.clone()))
    });
    println!(
        "    basis: {reduced_size} cells instead of {full_size} ({:.1}%)",
        100.0 * reduced_size as f64 / full_size as f64
    );
    if let (Some(full), Some(lazy)) = (full_peak, peak_memory_mb()) {
        println!("    peak memory: {lazy:.0} MB instead of {full:.0} MB");
    }

On a 96³ volume (7 million cells, 6.1 million of them up to dimension 2):

build every cell up to dimension 2                    2965.6 ms
cohomology, clearing, hooks                           3017.3 ms
build the reduction basis                             2211.4 ms
cohomology, clearing, reduction basis                 2020.2 ms
    basis: 338183 cells instead of 6110496 (5.5%)
    peak memory: 78 MB instead of 326 MB

The basis is 18× smaller, the reduction 1.5× faster, and peak memory a quarter. Building the basis is now the largest cost, and it is embarrassingly parallel (phlite_rips builds its basis on all cores).

For comparison, storing this complex explicitly, as PHAT or the phlite Python package do, takes about 36 bytes per cell and 8 per boundary entry, roughly 400 MB here, before any reduction.

Trade-offs

  • Only the clearing algorithms can leave cells out: the standard algorithm needs every column.
  • The test must be exactly right: leave out a cell that isn’t in a zero-length apparent pair and its bar is lost.
  • It pays when most pairs are zero-length apparent pairs (lower-star filtrations of smooth data, Vietoris–Rips). On noise, where few pairs have length zero, the test costs more than it saves.