holos-tda 0.9.0

Vietoris-Rips persistence and checked degree-Rips modules
Documentation
use std::path::{Path, PathBuf};

use crate::{
    Bigrade, BipersistenceArtifact, BipersistenceArtifactLimits, BipersistenceModule,
    BipersistenceRectangle, BipersistenceRegion, BipersistenceTerm, CircularCoordinateFamily,
    CircularCoordinateParams, CochainTerm, CohomologyClassAtlas, DegreeRipsBifiltration,
    DegreeRipsParams, Error, Result, cocycle_from_ripser_terms,
};

use super::super::read_circular_cocycle;
use super::BipersistenceCli;

pub(super) fn input_threshold(cli: &BipersistenceCli) -> Option<f64> {
    cli.threshold.or(cli.scales.last().copied())
}

pub(super) fn build_degree_rips(
    graph: &crate::SparseDistanceMatrix,
    cli: &BipersistenceCli,
    input_threshold: Option<f64>,
) -> Result<DegreeRipsBifiltration> {
    let params = DegreeRipsParams {
        max_homology_dimension: 1,
        threshold: input_threshold,
        ..DegreeRipsParams::default()
    };
    match (cli.scales.is_empty(), cli.minimum_degrees.is_empty()) {
        (true, true) => DegreeRipsBifiltration::from_graph(graph, params),
        (false, false) => DegreeRipsBifiltration::from_graph_on_grid(
            graph,
            cli.scales.clone(),
            cli.minimum_degrees.clone(),
            params,
        ),
        _ => Err(Error::InvalidInput(
            "a declared grid needs both --scale and --minimum-degree values".into(),
        )),
    }
}

pub(super) fn artifact_limits(cli: &BipersistenceCli) -> BipersistenceArtifactLimits {
    BipersistenceArtifactLimits {
        max_bytes: cli.max_artifact_bytes,
        max_coordinate_bytes: cli.max_artifact_bytes,
        ..BipersistenceArtifactLimits::default()
    }
}

pub(super) fn record_rectangles(
    artifact: &mut BipersistenceArtifact,
    module: &BipersistenceModule,
    values: &[usize],
    limits: BipersistenceArtifactLimits,
) -> Result<()> {
    for values in values.chunks_exact(4) {
        artifact.record_rectangle(
            module,
            BipersistenceRectangle::new(
                Bigrade::new(values[0], values[1]),
                Bigrade::new(values[2], values[3]),
            )?,
            limits,
        )?;
    }
    Ok(())
}

pub(super) fn record_regions(
    artifact: &mut BipersistenceArtifact,
    module: &BipersistenceModule,
    paths: &[PathBuf],
    maximum_bytes: usize,
    limits: BipersistenceArtifactLimits,
) -> Result<()> {
    for path in paths {
        artifact.record_region(module, read_region(path, maximum_bytes)?, limits)?;
    }
    Ok(())
}

pub(super) fn selected_classes(
    module: &BipersistenceModule,
    cli: &BipersistenceCli,
) -> Result<Vec<(Bigrade, Vec<BipersistenceTerm>)>> {
    let mut selections = basis_selections(&cli.classes);
    selections.extend(cocycle_selections(
        module,
        &cli.class_cocycles,
        cli.modulus,
    )?);
    selections.sort();
    selections.dedup();
    if cli.circular && selections.is_empty() {
        return Err(Error::InvalidInput(
            "--circular requires at least one --class or --class-cocycle".into(),
        ));
    }
    Ok(selections)
}

pub(super) fn record_class_claims(
    artifact: &mut BipersistenceArtifact,
    module: &BipersistenceModule,
    selections: Vec<(Bigrade, Vec<BipersistenceTerm>)>,
    circular: bool,
    circular_params: CircularCoordinateParams,
    limits: BipersistenceArtifactLimits,
) -> Result<(Vec<CohomologyClassAtlas>, Vec<CircularCoordinateFamily>)> {
    let mut atlases = Vec::with_capacity(selections.len());
    let mut families = Vec::new();
    for (grade, class) in selections {
        let atlas = module.class_atlas(grade, &class)?;
        artifact.record_class_atlas(module, &atlas, limits)?;
        if circular {
            families.push(module.circular_coordinate_family(&atlas, circular_params)?);
            artifact.record_circular_family(module, &atlas, circular_params, limits)?;
        }
        atlases.push(atlas);
    }
    Ok((atlases, families))
}

fn read_region(path: &Path, maximum_bytes: usize) -> Result<BipersistenceRegion> {
    let bytes =
        super::super::input::read_bounded_artifact(path, maximum_bytes, "bipersistence region")?;
    let text = std::str::from_utf8(&bytes)
        .map_err(|error| Error::InvalidInput(format!("invalid region file: {error}")))?;
    let mut grades = Vec::new();
    for (line_index, line) in text.lines().enumerate() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let fields = line.split_whitespace().collect::<Vec<_>>();
        if fields.len() != 2 {
            return Err(Error::InvalidInput(format!(
                "region line {} needs scale and density indices",
                line_index + 1
            )));
        }
        grades.push(Bigrade::new(
            parse_grid_index(fields[0], "region scale index")?,
            parse_grid_index(fields[1], "region density index")?,
        ));
    }
    BipersistenceRegion::new(grades)
}

fn basis_selections(values: &[usize]) -> Vec<(Bigrade, Vec<BipersistenceTerm>)> {
    values
        .chunks_exact(3)
        .map(|values| {
            (
                Bigrade::new(values[0], values[1]),
                vec![BipersistenceTerm {
                    basis_index: values[2],
                    coefficient: 1,
                }],
            )
        })
        .collect()
}

fn cocycle_selections(
    module: &BipersistenceModule,
    values: &[String],
    modulus: u32,
) -> Result<Vec<(Bigrade, Vec<BipersistenceTerm>)>> {
    values
        .chunks_exact(3)
        .map(|values| {
            let scale = parse_grid_index(&values[0], "class cocycle scale index")?;
            let density = parse_grid_index(&values[1], "class cocycle density index")?;
            let grade = Bigrade::new(scale, density);
            let rows = read_circular_cocycle(Path::new(&values[2]), modulus)?;
            let cocycle = cocycle_from_ripser_terms(module.h1_graph(grade)?, modulus, 0.0, &rows)?;
            let cochain = cocycle
                .terms
                .iter()
                .map(|term| CochainTerm {
                    simplex: vec![term.u, term.v],
                    coefficient: term.coefficient,
                })
                .collect::<Vec<_>>();
            let coordinates = module
                .cohomology_space(grade)?
                .coordinates_of_cocycle(&cochain)?;
            if coordinates.is_empty() {
                return Err(Error::InvalidInput(
                    "a selected degree-Rips cocycle is cohomologically zero".into(),
                ));
            }
            Ok((
                grade,
                coordinates
                    .into_iter()
                    .map(|(basis_index, coefficient)| BipersistenceTerm {
                        basis_index,
                        coefficient,
                    })
                    .collect(),
            ))
        })
        .collect()
}

fn parse_grid_index(value: &str, name: &str) -> Result<usize> {
    value
        .parse()
        .map_err(|error| Error::InvalidInput(format!("invalid {name} {value:?}: {error}")))
}