holos-tda 0.9.0

Vietoris-Rips persistence and checked degree-Rips modules
Documentation
use super::codec::diagram_bits_equal;
use super::*;
use crate::{
    CertificateLimits, ContinuationKind, ProgramEventKind, ProgramUpdateMode, RipsParams,
    SparseDistanceMatrix,
};
use proptest::prelude::*;

fn graph(first: f64, second: f64) -> SparseDistanceMatrix {
    SparseDistanceMatrix::from_triplets(
        7,
        &[
            (0, 1, 1.0),
            (1, 2, 2.0),
            (2, 3, 3.0),
            (0, 3, first),
            (3, 4, 1.5),
            (4, 5, 2.5),
            (5, 6, 3.5),
            (3, 6, second),
        ],
    )
    .unwrap()
}

#[test]
fn trace_round_trips_reuse_repair_and_continuation() {
    let initial = graph(4.0, 4.0);
    let updates = [graph(4.01, 4.01), graph(5.0, 4.01), graph(4.0, 4.0)];
    let trace = ProgramTraceArtifact::build(
        &initial,
        &updates,
        &RipsParams::new(1).with_modulus(3),
        CertificateLimits::default(),
    )
    .unwrap();
    assert!(trace.steps.iter().any(|step| {
        step.continuation
            .iter()
            .any(|record| record.kind == ContinuationKind::Split)
    }));
    let bytes = trace.encode().unwrap();
    let decoded = ProgramTraceArtifact::decode(
        &bytes,
        ProgramTraceDecodeLimits::default(),
        CertificateLimits::default(),
    )
    .unwrap();
    assert_eq!(decoded.encode().unwrap(), bytes);
    let verified = decoded.verify(CertificateLimits::default()).unwrap();
    assert_eq!(verified.steps.len(), updates.len());
    assert!(diagram_bits_equal(
        &verified.steps.last().unwrap().diagram,
        &verified.final_program.result().diagram
    ));
}

#[test]
fn trace_replays_a_retained_reduction_suffix() {
    let initial = SparseDistanceMatrix::from_triplets(
        4,
        &[
            (0, 1, 1.0),
            (0, 2, 2.0),
            (0, 3, 3.0),
            (1, 2, 4.0),
            (1, 3, 5.0),
            (2, 3, 6.0),
        ],
    )
    .unwrap();
    let updated = SparseDistanceMatrix::from_triplets(
        4,
        &[
            (0, 1, 1.0),
            (0, 2, 2.0),
            (0, 3, 3.0),
            (1, 2, 4.0),
            (1, 3, 6.5),
            (2, 3, 6.0),
        ],
    )
    .unwrap();
    let trace = ProgramTraceArtifact::build(
        &initial,
        &[updated],
        &RipsParams::new(1).with_modulus(3),
        CertificateLimits::default(),
    )
    .unwrap();
    assert_eq!(trace.steps[0].mode, ProgramUpdateMode::Repaired);
    assert_eq!(trace.steps[0].work.atoms_repaired, 1);
    assert!(trace.steps[0].work.reduction_columns_reused > 0);
    trace.verify(CertificateLimits::default()).unwrap();
}

#[test]
fn class_space_rebuild_retains_the_reduction() {
    let edges = [
        (0, 1, 2.0),
        (0, 2, 1.0),
        (0, 3, 1.0),
        (0, 4, 1.0),
        (1, 2, 1.0),
        (1, 3, 1.0),
        (1, 4, 1.0),
    ];
    let initial = SparseDistanceMatrix::from_triplets(5, &edges).unwrap();
    let mut updated_edges = edges;
    updated_edges[1].2 = 1.1;
    let updated = SparseDistanceMatrix::from_triplets(5, &updated_edges).unwrap();
    let trace = ProgramTraceArtifact::build(
        &initial,
        &[updated],
        &RipsParams::new(1).with_modulus(3),
        CertificateLimits::default(),
    )
    .unwrap();
    assert_eq!(trace.initial_program().atoms().len(), 1);
    assert_eq!(trace.initial_program().atoms()[0].atlas().spaces().len(), 1);
    assert_eq!(
        trace.initial_program().atoms()[0].atlas().spaces()[0]
            .basis
            .len(),
        2
    );
    let step = &trace.steps()[0];
    assert_eq!(step.mode(), ProgramUpdateMode::Repaired);
    assert_eq!(step.work().atoms_repaired, 0);
    assert_eq!(step.work().atoms_rebuilt, 1);
    assert_eq!(step.work().reduction_columns_reduced, 0);
    assert!(step.work().reduction_columns_reused > 0);
    assert_eq!(step.events().len(), 1);
    assert_eq!(step.events()[0].kind, ProgramEventKind::AtomRebuilt);
    trace.verify(CertificateLimits::default()).unwrap();
}

#[test]
fn changed_work_truncation_and_trailing_bytes_are_rejected() {
    let initial = graph(4.0, 4.0);
    let updates = [graph(4.01, 4.01), graph(5.0, 4.01)];
    let trace = ProgramTraceArtifact::build(
        &initial,
        &updates,
        &RipsParams::new(1),
        CertificateLimits::default(),
    )
    .unwrap();
    let bytes = trace.encode().unwrap();
    assert!((0..bytes.len()).all(|end| {
        ProgramTraceArtifact::decode(
            &bytes[..end],
            ProgramTraceDecodeLimits::default(),
            CertificateLimits::default(),
        )
        .is_err()
    }));
    let mut trailing = bytes.clone();
    trailing.push(0);
    assert!(
        ProgramTraceArtifact::decode(
            &trailing,
            ProgramTraceDecodeLimits::default(),
            CertificateLimits::default()
        )
        .is_err()
    );
    let mut changed = trace.clone();
    changed.steps[0].work.edges_checked += 1;
    assert!(changed.verify(CertificateLimits::default()).is_err());
}

#[test]
fn trace_step_count_checks_remaining_bytes_before_allocation() {
    let initial = graph(4.0, 4.0);
    let trace = ProgramTraceArtifact::build(
        &initial,
        &[],
        &RipsParams::new(1),
        CertificateLimits::default(),
    )
    .unwrap();
    let mut bytes = trace.encode().unwrap();
    bytes[11..19].copy_from_slice(&10_000_u64.to_be_bytes());
    let limits = ProgramTraceDecodeLimits {
        max_steps: 10_000,
        ..ProgramTraceDecodeLimits::default()
    };
    let error =
        ProgramTraceArtifact::decode(&bytes, limits, CertificateLimits::default()).unwrap_err();
    assert!(error.message().contains("trace step records"));
}

proptest! {
    #[test]
    fn arbitrary_short_envelopes_never_panic(bytes in proptest::collection::vec(any::<u8>(), 0..4096)) {
        let _ = ProgramTraceArtifact::decode(
            &bytes,
            ProgramTraceDecodeLimits::default(),
            CertificateLimits::default(),
        );
    }
}