holos-tda 0.9.0

Vietoris-Rips persistence and checked degree-Rips modules
Documentation
use super::common::Rng;

use super::super::matrix::DistanceMatrix;
use super::super::point_cloud::{PointCloudGraph, PointCloudParams, PointCloudStrategy};
use super::super::sparse::SparseDistanceMatrix;

fn edge_bits(matrix: &SparseDistanceMatrix) -> Vec<(usize, usize, u64)> {
    matrix
        .edges()
        .map(|(u, v, distance)| (u, v, distance.to_bits()))
        .collect()
}

#[test]
fn threshold_point_kernels_match_the_dense_constructor() {
    let mut rng = Rng::new(0x8d47_2016_7f31);
    for dimensions in [0usize, 1, 2, 3, 7, 13] {
        let mut points = Vec::new();
        for i in 0..47 {
            let point = (0..dimensions)
                .map(|axis| {
                    let raw = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
                    if i % 11 == 0 {
                        axis as f64 * 0.125
                    } else {
                        raw.mul_add(4.0, -2.0)
                    }
                })
                .collect();
            points.push(point);
        }
        let dense = DistanceMatrix::from_points(&points).unwrap();
        for threshold in [0.0, 0.25, 1.0, 4.0, f64::INFINITY] {
            let expected = edge_bits(&dense.to_sparse_at(threshold).unwrap());
            for strategy in [PointCloudStrategy::KdTree, PointCloudStrategy::Exhaustive] {
                let serial = PointCloudGraph::build(
                    &points,
                    PointCloudParams::new(threshold).with_strategy(strategy),
                )
                .unwrap();
                let parallel = PointCloudGraph::build(
                    &points,
                    PointCloudParams::new(threshold)
                        .with_strategy(strategy)
                        .with_threads(3),
                )
                .unwrap();
                assert_eq!(
                    edge_bits(serial.matrix()),
                    expected,
                    "dimensions {dimensions}, threshold {threshold}, strategy {strategy:?}"
                );
                assert_eq!(edge_bits(parallel.matrix()), expected);
                assert_eq!(serial.stats(), parallel.stats());
            }
        }
    }
}

#[test]
fn threshold_point_constructor_preserves_extreme_distance_bits() {
    let points = vec![
        vec![0.0, 0.0],
        vec![1e-200, 0.0],
        vec![0.0, 1e200],
        vec![1e308, 0.0],
        vec![-1e308, 0.0],
    ];
    let dense = DistanceMatrix::from_points(&points).unwrap();
    for threshold in [0.0, 1e-200, 1e200, f64::MAX, f64::INFINITY] {
        let expected = edge_bits(&dense.to_sparse_at(threshold).unwrap());
        let graph = PointCloudGraph::build(&points, PointCloudParams::new(threshold)).unwrap();
        assert_eq!(edge_bits(graph.matrix()), expected, "threshold {threshold}");
    }
}

#[test]
fn automatic_point_route_is_frozen() {
    let low = vec![vec![0.0; 12], vec![1.0; 12]];
    let high = vec![vec![0.0; 13], vec![1.0; 13]];
    assert_eq!(
        PointCloudGraph::build(&low, PointCloudParams::new(1.0))
            .unwrap()
            .stats()
            .strategy,
        PointCloudStrategy::KdTree
    );
    assert_eq!(
        PointCloudGraph::build(&high, PointCloudParams::new(1.0))
            .unwrap()
            .stats()
            .strategy,
        PointCloudStrategy::Exhaustive
    );
    assert_eq!(
        PointCloudGraph::build(&low, PointCloudParams::new(f64::INFINITY))
            .unwrap()
            .stats()
            .strategy,
        PointCloudStrategy::Exhaustive
    );
}

#[test]
fn overflowing_difference_is_an_absent_edge() {
    let d = DistanceMatrix::from_points(&[vec![1e308], vec![-1e308]]).unwrap();
    assert_eq!(d.get(0, 1), f64::INFINITY);
}

#[test]
fn non_finite_coordinates_are_rejected() {
    assert!(DistanceMatrix::from_points(&[vec![f64::INFINITY], vec![0.0]]).is_err());
    assert!(DistanceMatrix::from_points(&[vec![f64::NAN], vec![0.0]]).is_err());
}

#[test]
fn negative_zero_entries_are_normalized() {
    let d = DistanceMatrix::from_condensed(vec![-0.0]).unwrap();
    assert!(d.get(0, 1).is_sign_positive());
}

#[test]
fn validation_errors_carry_the_condensed_index() {
    let err = DistanceMatrix::from_condensed(vec![1.0, f64::NAN, 1.0]).unwrap_err();
    assert!(err.to_string().contains("index 1"), "{err}");
    let err = DistanceMatrix::from_condensed(vec![1.0, 1.0, -2.0]).unwrap_err();
    assert!(err.to_string().contains("index 2"), "{err}");
}

#[test]
fn empty_condensed_means_one_point() {
    assert_eq!(DistanceMatrix::from_condensed(vec![]).unwrap().len(), 1);
}