holos-tda 0.9.0

Vietoris-Rips persistence and checked degree-Rips modules
Documentation
use std::fmt::Write as _;

use super::*;

const ESSENTIAL_GRAPH: &str = "0 1 1\n0 3 1\n1 2 1\n2 3 1\n";
const FINITE_GRAPH: &str = "0 1 1\n0 2 2\n0 3 1\n1 2 1\n1 3 2\n2 3 1\n";
const LOWER_DISTANCE_BASE: &str = "1 2 1 1 2 1\n";
const LOWER_DISTANCE_CHANGED: &str = "1 2 1 1 2.5 1\n";

fn essential_graph(name: &str) -> TempFile {
    TempFile::new(name, ESSENTIAL_GRAPH)
}

fn finite_graph(name: &str) -> TempFile {
    TempFile::new(name, FINITE_GRAPH)
}

#[test]
fn persistent_class_cli_hands_a_finite_witness_to_the_checker() {
    let graph = finite_graph("persistent_class_finite.spr");
    let artifact = TempFile::new("persistent_class_finite.hspc", "");
    let output = run(&[
        "persistent-class",
        graph.path().to_str().unwrap(),
        artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--threshold",
        "2",
        "--modulus",
        "5",
        "--space",
        "0",
        "--basis",
        "0",
    ]);
    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let bytes = std::fs::read(artifact.path()).unwrap();
    assert!(is_persistent_class(&bytes));
    let checked = verify_persistent_class(&bytes, ProofLimits::default()).unwrap();
    assert_eq!(checked.interval().birth, 1.0);
    assert_eq!(checked.interval().death, 2.0);
    assert!(!checked.cycle().is_empty());
    assert!(!checked.bounding_chain().is_empty());
}

#[test]
fn persistent_class_cli_hands_an_essential_witness_to_the_checker() {
    let graph = essential_graph("persistent_class_essential.spr");
    let artifact = TempFile::new("persistent_class_essential.hspc", "");
    let output = run(&[
        "persistent-class",
        graph.path().to_str().unwrap(),
        artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--threshold",
        "1",
        "--modulus",
        "5",
        "--space",
        "0",
        "--basis",
        "0",
    ]);
    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let bytes = std::fs::read(artifact.path()).unwrap();
    assert!(is_persistent_class(&bytes));
    let checked = verify_persistent_class(&bytes, ProofLimits::default()).unwrap();
    assert!(checked.interval().death.is_infinite());
    assert!(checked.bounding_chain().is_empty());
}

#[test]
fn persistent_class_cli_keeps_all_lower_distance_edges_in_the_source_binding() {
    let base = TempFile::new("persistent_lower_base.lower", LOWER_DISTANCE_BASE);
    let changed = TempFile::new("persistent_lower_changed.lower", LOWER_DISTANCE_CHANGED);
    let base_artifact = TempFile::new("persistent_lower_base.hspc", "");
    let changed_artifact = TempFile::new("persistent_lower_changed.hspc", "");
    for (input, output) in [
        (base.path(), base_artifact.path()),
        (changed.path(), changed_artifact.path()),
    ] {
        let result = run(&[
            "persistent-class",
            input.to_str().unwrap(),
            output.to_str().unwrap(),
            "--format",
            "lower-distance",
            "--threshold",
            "1",
            "--modulus",
            "5",
            "--space",
            "0",
            "--basis",
            "0",
        ]);
        assert!(result.status.success(), "stderr: {}", stderr(&result));
    }
    let base_bytes = std::fs::read(base_artifact.path()).unwrap();
    let changed_bytes = std::fs::read(changed_artifact.path()).unwrap();
    assert_ne!(base_bytes, changed_bytes);
    let base_checked = verify_persistent_class(&base_bytes, ProofLimits::default()).unwrap();
    let changed_checked = verify_persistent_class(&changed_bytes, ProofLimits::default()).unwrap();
    assert_eq!(base_checked.interval(), changed_checked.interval());
    assert_eq!(base_checked.source().len(), 6);
    assert_eq!(changed_checked.source().len(), 6);
    assert_eq!(base_checked.source()[4].value(), 2.0);
    assert_eq!(changed_checked.source()[4].value(), 2.5);
}

#[test]
fn persistent_circular_cli_accepts_a_supplied_mod_two_lift() {
    let graph = essential_graph("persistent_circular_mod_two.spr");
    let class_artifact = TempFile::new("persistent_circular_mod_two.hspc", "");
    let class_output = run(&[
        "persistent-class",
        graph.path().to_str().unwrap(),
        class_artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--threshold",
        "1",
        "--modulus",
        "2",
        "--space",
        "0",
        "--basis",
        "0",
    ]);
    assert!(
        class_output.status.success(),
        "stderr: {}",
        stderr(&class_output)
    );
    let checked_class = verify_persistent_class(
        &std::fs::read(class_artifact.path()).unwrap(),
        ProofLimits::default(),
    )
    .unwrap();
    let mut lift_text = String::new();
    for term in checked_class.cocycle() {
        writeln!(
            lift_text,
            "{} {} {}",
            term.u(),
            term.v(),
            term.coefficient()
        )
        .unwrap();
    }
    let lift = TempFile::new("persistent_circular_mod_two.integral", &lift_text);
    let phases = TempFile::new("persistent_circular_mod_two.phase", "");
    let artifact = TempFile::new("persistent_circular_mod_two.hsph", "");
    let output = run(&[
        "persistent-circular",
        graph.path().to_str().unwrap(),
        artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--threshold",
        "1",
        "--modulus",
        "2",
        "--space",
        "0",
        "--basis",
        "0",
        "--integral-lift",
        lift.path().to_str().unwrap(),
        "--phases",
        phases.path().to_str().unwrap(),
    ]);
    assert!(output.status.success(), "stderr: {}", stderr(&output));
    let bytes = std::fs::read(artifact.path()).unwrap();
    assert!(is_persistent_coordinate(&bytes));
    let checked = verify_persistent_coordinate(&bytes, CircularProofLimits::default()).unwrap();
    assert_eq!(checked.modulus(), 2);
    assert_eq!(checked.class().interval().death, f64::INFINITY);
    assert!(checked.class().bounding_chain().is_empty());
    assert_eq!(
        std::fs::read_to_string(phases.path())
            .unwrap()
            .lines()
            .count(),
        4
    );
}

#[test]
fn persistent_circular_cli_keeps_old_output_when_lift_parsing_fails() {
    let graph = essential_graph("persistent_circular_bad_lift.spr");
    let lift = TempFile::new("persistent_circular_bad_lift.integral", "1 0 1\n");
    let artifact = TempFile::new("persistent_circular_bad_lift.hsph", "previous output");
    let output = run(&[
        "persistent-circular",
        graph.path().to_str().unwrap(),
        artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--threshold",
        "1",
        "--modulus",
        "2",
        "--space",
        "0",
        "--basis",
        "0",
        "--integral-lift",
        lift.path().to_str().unwrap(),
    ]);
    assert!(!output.status.success());
    assert_eq!(
        std::fs::read_to_string(artifact.path()).unwrap(),
        "previous output"
    );
}

#[test]
fn persistent_class_cli_rejects_lexical_output_aliases_before_writing() {
    let graph = finite_graph("persistent_class_output_alias.spr");
    let artifact = TempFile::new("persistent_class_output_alias.hspc", "previous output");
    let record = artifact
        .path()
        .parent()
        .unwrap()
        .join(".")
        .join(artifact.path().file_name().unwrap());
    let output = run(&[
        "persistent-class",
        graph.path().to_str().unwrap(),
        artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--space",
        "0",
        "--basis",
        "0",
        "--record",
        record.to_str().unwrap(),
    ]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("refer to the same file"));
    assert_eq!(
        std::fs::read_to_string(artifact.path()).unwrap(),
        "previous output"
    );
}

#[test]
fn persistent_circular_cli_rejects_aliasing_artifact_and_phase_paths() {
    let graph = essential_graph("persistent_circular_output_alias.spr");
    let artifact = TempFile::new("persistent_circular_output_alias.hsph", "previous output");
    let phases = artifact
        .path()
        .parent()
        .unwrap()
        .join(".")
        .join(artifact.path().file_name().unwrap());
    let output = run(&[
        "persistent-circular",
        graph.path().to_str().unwrap(),
        artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--threshold",
        "1",
        "--modulus",
        "2",
        "--space",
        "0",
        "--basis",
        "0",
        "--phases",
        phases.to_str().unwrap(),
    ]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("refer to the same file"));
    assert_eq!(
        std::fs::read_to_string(artifact.path()).unwrap(),
        "previous output"
    );
}

#[test]
fn persistent_circular_cli_rejects_aliasing_record_and_phase_paths() {
    let graph = essential_graph("persistent_circular_record_phase_alias.spr");
    let artifact = TempFile::new("persistent_circular_record_phase.hsph", "");
    let record = TempFile::new("persistent_circular_record_phase.record", "previous record");
    let output = run(&[
        "persistent-circular",
        graph.path().to_str().unwrap(),
        artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--threshold",
        "1",
        "--modulus",
        "2",
        "--space",
        "0",
        "--basis",
        "0",
        "--record",
        record.path().to_str().unwrap(),
        "--phases",
        record.path().to_str().unwrap(),
    ]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("refer to the same file"));
    assert_eq!(
        std::fs::read_to_string(record.path()).unwrap(),
        "previous record"
    );
    assert_eq!(std::fs::read_to_string(artifact.path()).unwrap(), "");
}

#[cfg(unix)]
#[test]
fn persistent_class_cli_rejects_record_symlink_to_artifact() {
    use std::os::unix::fs::symlink;

    let graph = finite_graph("persistent_class_output_symlink.spr");
    let artifact = TempFile::new("persistent_class_output_symlink.hspc", "previous output");
    let record = TempFile::new("persistent_class_output_symlink.record", "");
    std::fs::remove_file(record.path()).unwrap();
    symlink(artifact.path(), record.path()).unwrap();
    let output = run(&[
        "persistent-class",
        graph.path().to_str().unwrap(),
        artifact.path().to_str().unwrap(),
        "--format",
        "sparse",
        "--space",
        "0",
        "--basis",
        "0",
        "--record",
        record.path().to_str().unwrap(),
    ]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("refer to the same file"));
    assert_eq!(
        std::fs::read_to_string(artifact.path()).unwrap(),
        "previous output"
    );
}