holos-tda 0.9.0

Vietoris-Rips persistence and checked degree-Rips modules
Documentation
use super::*;
use crate::{
    CircularCoordinateParams, CohomologyLimits, RipsParams, SparseDistanceMatrix,
    circular_coordinate_for_class, continue_circular_coordinate,
    rips_persistence_with_classes_sparse,
};
use holos_tda_check::{CircularProofLimits, verify_circular_coordinate};

fn cycle_graph() -> SparseDistanceMatrix {
    SparseDistanceMatrix::from_triplets(
        8,
        &[
            (0, 1, 1.0),
            (1, 2, 1.0),
            (2, 3, 1.0),
            (3, 4, 1.0),
            (4, 5, 1.0),
            (5, 6, 1.0),
            (6, 7, 1.0),
            (0, 7, 1.0),
        ],
    )
    .unwrap()
}

#[test]
fn coordinate_and_identity_continuation_encode() {
    let graph = cycle_graph();
    let explained =
        rips_persistence_with_classes_sparse(&graph, &RipsParams::new(1).with_modulus(47)).unwrap();
    let coordinate = circular_coordinate_for_class(
        &graph,
        explained.classes().next().unwrap(),
        CircularCoordinateParams::default(),
    )
    .unwrap();
    let single = CircularCoordinateArtifact::from_coordinate(&graph, &coordinate).unwrap();
    let single_bytes = single.encode().unwrap();
    assert!(single_bytes.starts_with(MAGIC));
    assert_eq!(single.summary().states, 1);
    let checked =
        verify_circular_coordinate(&single_bytes, CircularProofLimits::default()).unwrap();
    assert_eq!(checked.coordinates, 1);
    assert_eq!(checked.divisibilities, vec![1]);

    let continuation = continue_circular_coordinate(
        &graph,
        &coordinate,
        &graph,
        CircularCoordinateParams::default(),
    )
    .unwrap();
    let pair = CircularCoordinateArtifact::from_continuation(
        &graph,
        &coordinate,
        &graph,
        &continuation,
        CohomologyLimits::default(),
    )
    .unwrap();
    assert_eq!(pair.summary().states, 2);
    assert!(pair.summary().continuation);
    let pair_bytes = pair.encode().unwrap();
    assert!(pair_bytes.len() > single_bytes.len());
    let checked = verify_circular_coordinate(&pair_bytes, CircularProofLimits::default()).unwrap();
    assert_eq!(
        checked.continuation,
        Some(holos_tda_check::VerifiedCircularContinuationKind::Unique)
    );

    let mut mutated = single_bytes;
    let middle = mutated.len() / 2;
    mutated[middle] ^= 1;
    assert!(verify_circular_coordinate(&mutated, CircularProofLimits::default()).is_err());
    let mut strict = CircularProofLimits::default();
    strict.max_tolerance = coordinate.tolerance / 2.0;
    assert!(verify_circular_coordinate(&pair_bytes, strict).is_err());

    let mut excessive_tolerance = single.clone();
    excessive_tolerance.tolerance = 1.0;
    let bytes = excessive_tolerance.encode().unwrap();
    let error = verify_circular_coordinate(&bytes, CircularProofLimits::default()).unwrap_err();
    assert!(error.to_string().contains("tolerance"));

    let mut zero_multiplier = single;
    zero_multiplier.states[0]
        .coordinate
        .as_mut()
        .unwrap()
        .field_multiplier = 0;
    let bytes = zero_multiplier.encode().unwrap();
    let error = verify_circular_coordinate(&bytes, CircularProofLimits::default()).unwrap_err();
    assert!(error.to_string().contains("multiplier"));
}

#[test]
fn checker_rejects_each_coordinate_claim_family() {
    let graph = cycle_graph();
    let explained =
        rips_persistence_with_classes_sparse(&graph, &RipsParams::new(1).with_modulus(47)).unwrap();
    let coordinate = circular_coordinate_for_class(
        &graph,
        explained.classes().next().unwrap(),
        CircularCoordinateParams::default(),
    )
    .unwrap();
    let artifact = CircularCoordinateArtifact::from_coordinate(&graph, &coordinate).unwrap();
    let rejects = |candidate: CircularCoordinateArtifact| {
        assert!(
            verify_circular_coordinate(
                &candidate.encode().unwrap(),
                CircularProofLimits::default(),
            )
            .is_err()
        );
    };

    let mut candidate = artifact.clone();
    candidate.modulus = 4;
    rejects(candidate);
    let mut candidate = artifact.clone();
    candidate.scale = f64::NAN;
    rejects(candidate);
    let mut candidate = artifact.clone();
    candidate.tolerance = 1.0;
    rejects(candidate);
    let mut candidate = artifact.clone();
    candidate.states[0].vertex_count += 1;
    rejects(candidate);
    let mut candidate = artifact.clone();
    candidate.states[0].edges.pop();
    rejects(candidate);

    fn coordinate_of(candidate: &mut CircularCoordinateArtifact) -> &mut CircularCoordinate {
        candidate.states[0].coordinate.as_mut().unwrap()
    }
    let mut candidate = artifact.clone();
    let coordinate = coordinate_of(&mut candidate);
    let mut bytes = *coordinate.space.as_bytes();
    bytes[0] ^= 1;
    coordinate.space = crate::CohomologySpaceId::from_bytes(bytes);
    rejects(candidate);
    let mut candidate = artifact.clone();
    coordinate_of(&mut candidate).field_multiplier = 2;
    rejects(candidate);
    let mut candidate = artifact.clone();
    coordinate_of(&mut candidate).divisibility += 1;
    rejects(candidate);
    let mut candidate = artifact.clone();
    coordinate_of(&mut candidate).source[0].coefficient = 2;
    rejects(candidate);
    let mut candidate = artifact.clone();
    coordinate_of(&mut candidate).integral[0].coefficient += 1;
    rejects(candidate);
    let mut candidate = artifact.clone();
    coordinate_of(&mut candidate).class[0].coefficient = 2;
    rejects(candidate);
    let mut candidate = artifact;
    coordinate_of(&mut candidate).potential[0] = 0.25;
    rejects(candidate);
}

#[test]
fn checker_enforces_each_circular_collection_limit() {
    let graph = SparseDistanceMatrix::from_triplets(
        8,
        &[
            (0, 1, 1.0),
            (1, 2, 1.0),
            (2, 3, 1.0),
            (0, 3, 1.0),
            (4, 5, 1.0),
            (5, 6, 1.0),
            (4, 6, 1.0),
        ],
    )
    .unwrap();
    let explained =
        rips_persistence_with_classes_sparse(&graph, &RipsParams::new(1).with_modulus(47)).unwrap();
    let coordinate = circular_coordinate_for_class(
        &graph,
        explained.classes().next().unwrap(),
        CircularCoordinateParams::default(),
    )
    .unwrap();
    let bytes = CircularCoordinateArtifact::from_coordinate(&graph, &coordinate)
        .unwrap()
        .encode()
        .unwrap();
    let rejects = |limits: CircularProofLimits| {
        assert!(verify_circular_coordinate(&bytes, limits).is_err());
    };

    let mut limits = CircularProofLimits::default();
    limits.proof.max_bytes = bytes.len() - 1;
    rejects(limits);
    let mut limits = CircularProofLimits::default();
    limits.proof.max_snapshots = 0;
    rejects(limits);
    let mut limits = CircularProofLimits::default();
    limits.proof.max_vertices = graph.len() - 1;
    rejects(limits);
    let mut limits = CircularProofLimits::default();
    limits.proof.max_edges = graph.edges().count() - 1;
    rejects(limits);
    let mut limits = CircularProofLimits::default();
    limits.proof.max_triangles = 0;
    rejects(limits);
    let mut limits = CircularProofLimits::default();
    limits.proof.max_terms = 0;
    rejects(limits);
    let mut limits = CircularProofLimits::default();
    limits.max_tolerance = coordinate.tolerance / 2.0;
    rejects(limits);
}

#[test]
fn checker_accepts_cycle_artifacts_across_small_sizes() {
    for vertices in 4..=32 {
        let mut edges = (0..vertices - 1)
            .map(|u| (u, u + 1, 1.0))
            .collect::<Vec<_>>();
        edges.push((0, vertices - 1, 1.0));
        let graph = SparseDistanceMatrix::from_triplets(vertices, &edges).unwrap();
        let explained =
            rips_persistence_with_classes_sparse(&graph, &RipsParams::new(1).with_modulus(47))
                .unwrap();
        let coordinate = circular_coordinate_for_class(
            &graph,
            explained.classes().next().unwrap(),
            CircularCoordinateParams::default(),
        )
        .unwrap();
        let bytes = CircularCoordinateArtifact::from_coordinate(&graph, &coordinate)
            .unwrap()
            .encode()
            .unwrap();
        verify_circular_coordinate(&bytes, CircularProofLimits::default()).unwrap();
    }
}

#[test]
fn checker_reconstructs_each_continuation_status() {
    let old_graph = SparseDistanceMatrix::from_triplets(
        8,
        &[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (0, 3, 1.0)],
    )
    .unwrap();
    let explained =
        rips_persistence_with_classes_sparse(&old_graph, &RipsParams::new(1).with_modulus(5))
            .unwrap();
    let old_coordinate = circular_coordinate_for_class(
        &old_graph,
        explained.classes().next().unwrap(),
        CircularCoordinateParams::default(),
    )
    .unwrap();
    let cases = [
        (
            SparseDistanceMatrix::from_triplets(8, &[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0)])
                .unwrap(),
            holos_tda_check::VerifiedCircularContinuationKind::NoNonzeroContinuation,
        ),
        (
            SparseDistanceMatrix::from_triplets(
                8,
                &[
                    (0, 1, 1.0),
                    (1, 2, 1.0),
                    (2, 3, 1.0),
                    (0, 3, 1.0),
                    (0, 2, 1.0),
                ],
            )
            .unwrap(),
            holos_tda_check::VerifiedCircularContinuationKind::NoExtension,
        ),
        (
            SparseDistanceMatrix::from_triplets(
                8,
                &[(4, 5, 1.0), (5, 6, 1.0), (6, 7, 1.0), (4, 7, 1.0)],
            )
            .unwrap(),
            holos_tda_check::VerifiedCircularContinuationKind::Ambiguous,
        ),
    ];
    for (new_graph, expected) in cases {
        let continuation = continue_circular_coordinate(
            &old_graph,
            &old_coordinate,
            &new_graph,
            CircularCoordinateParams::default(),
        )
        .unwrap();
        let artifact = CircularCoordinateArtifact::from_continuation(
            &old_graph,
            &old_coordinate,
            &new_graph,
            &continuation,
            CohomologyLimits::default(),
        )
        .unwrap();
        let checked =
            verify_circular_coordinate(&artifact.encode().unwrap(), CircularProofLimits::default())
                .unwrap();
        assert_eq!(checked.continuation, Some(expected));
        assert_eq!(checked.coordinates, 1);
        assert_eq!(
            checked.ambiguity_rank > 0,
            expected == holos_tda_check::VerifiedCircularContinuationKind::Ambiguous
        );
        let mut changed = artifact.clone();
        changed.continuation.as_mut().unwrap().kind = CohomologyContinuationKind::Unique;
        assert!(
            verify_circular_coordinate(&changed.encode().unwrap(), CircularProofLimits::default(),)
                .is_err()
        );
        if expected == holos_tda_check::VerifiedCircularContinuationKind::Ambiguous {
            let mut changed = artifact;
            changed.continuation.as_mut().unwrap().ambiguity.clear();
            assert!(
                verify_circular_coordinate(
                    &changed.encode().unwrap(),
                    CircularProofLimits::default(),
                )
                .is_err()
            );
        }
    }
}

#[test]
fn checker_accepts_a_nonunit_continued_class_vector() {
    let graph = cycle_graph();
    let explained =
        rips_persistence_with_classes_sparse(&graph, &RipsParams::new(1).with_modulus(47)).unwrap();
    let mut coordinate = circular_coordinate_for_class(
        &graph,
        explained.classes().next().unwrap(),
        CircularCoordinateParams::default(),
    )
    .unwrap();
    for term in &mut coordinate.source {
        term.coefficient = term.coefficient * 2 % 47;
    }
    for term in &mut coordinate.integral {
        term.coefficient *= 2;
    }
    for term in &mut coordinate.class {
        term.coefficient = term.coefficient * 2 % 47;
    }
    for value in &mut coordinate.potential {
        *value *= 2.0;
    }
    coordinate.divisibility *= 2;
    let continuation = continue_circular_coordinate(
        &graph,
        &coordinate,
        &graph,
        CircularCoordinateParams::default(),
    )
    .unwrap();
    let artifact = CircularCoordinateArtifact::from_continuation(
        &graph,
        &coordinate,
        &graph,
        &continuation,
        CohomologyLimits::default(),
    )
    .unwrap();
    let checked =
        verify_circular_coordinate(&artifact.encode().unwrap(), CircularProofLimits::default())
            .unwrap();
    assert_eq!(checked.coordinates, 2);
    assert_eq!(checked.divisibilities, vec![2, 2]);
    assert_eq!(
        checked.continuation,
        Some(holos_tda_check::VerifiedCircularContinuationKind::Unique)
    );
}