runlength 0.1.0

Generic run-length encoded sequences with plan-based arithmetic.
Documentation
//! Property tests for the v2 operations, checked against dense vectors.

use proptest::prelude::*;
use runlength::prelude::*;
use runlength::{
    concat, coverage_max, coverage_max_by_key, coverage_min, overlap_plan, plan_many, zip_many,
};

fn rle() -> impl Strategy<Value = Rle<i64>> {
    prop::collection::vec((0i64..4, 0i64..3), 0..8).prop_map(|pairs| {
        let (runs, values): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();
        Rle::from_runs(&runs, values).unwrap()
    })
}

fn intervals() -> impl Strategy<Value = Vec<(i64, i64, i64)>> {
    prop::collection::vec((0i64..12, 0i64..5, -2i64..4), 0..8)
        .prop_map(|v| v.into_iter().map(|(s, len, w)| (s, s + len, w)).collect())
}

fn covering(iv: &[(i64, i64, i64)], pos: i64) -> Vec<usize> {
    (0..iv.len())
        .filter(|&i| iv[i].0 <= pos && pos < iv[i].1)
        .collect()
}

proptest! {
    #[test]
    fn shift_matches_dense(a in rle(), distance in -12i64..12) {
        let dense = a.to_dense();
        let n = dense.len() as i64;
        let expected: Vec<i64> = (0..n)
            .map(|i| {
                let source = i - distance;
                if (0..n).contains(&source) { dense[source as usize] } else { 9 }
            })
            .collect();
        let shifted = a.shift(distance, 9);
        prop_assert_eq!(shifted.to_dense(), expected);
        prop_assert!(shifted.is_merged());
    }

    #[test]
    fn reversed_matches_dense(a in rle()) {
        let mut dense = a.to_dense();
        dense.reverse();
        prop_assert_eq!(a.reversed().to_dense(), dense);
        prop_assert_eq!(a.reversed().reversed(), a);
    }

    #[test]
    fn concat_matches_dense(parts in prop::collection::vec(rle(), 0..4)) {
        let joined = concat(parts.iter().map(|p| p.view()));
        let dense: Vec<i64> = parts.iter().flat_map(|p| p.to_dense()).collect();
        prop_assert_eq!(joined.to_dense(), dense);
        prop_assert!(joined.is_merged());
    }

    #[test]
    fn ranges_where_matches_dense(a in rle()) {
        let (starts, ends) = a.ranges_where(|&v| v > 0);
        let dense = a.to_dense();
        let mut expected = vec![false; dense.len()];
        for (&s, &e) in starts.iter().zip(&ends) {
            for x in &mut expected[s as usize..e as usize] { *x = true; }
        }
        prop_assert_eq!(expected, dense.iter().map(|&v| v > 0).collect::<Vec<_>>());
        // Ranges never touch: touching ones are joined.
        prop_assert!(starts.iter().skip(1).zip(&ends).all(|(s, e)| s > e));
    }

    #[test]
    fn zip_many_matches_dense(parts in prop::collection::vec(rle(), 1..5)) {
        let views: Vec<_> = parts.iter().map(|p| p.view()).collect();
        let total = zip_many(&views, Some(&0), |v| v.iter().copied().sum::<i64>()).unwrap();
        let n = parts.iter().map(|p| p.len()).max().unwrap_or(0) as usize;
        let mut expected = vec![0; n];
        for p in &parts {
            for (i, v) in p.to_dense().into_iter().enumerate() { expected[i] += v; }
        }
        prop_assert_eq!(total.to_dense(), expected);

        let ends: Vec<&[i64]> = parts.iter().map(|p| p.ends()).collect();
        let plan = plan_many(&ends);
        let mut union: Vec<i64> = ends.iter().flat_map(|e| e.iter().copied()).collect();
        union.sort();
        union.dedup();
        prop_assert_eq!(plan.ends, union);
        prop_assert_eq!(plan.idx.len(), parts.len());

        let strict = zip_many(&views, None, |v| v.len());
        prop_assert_eq!(strict.is_ok(), parts.iter().all(|p| p.len() == parts[0].len()));
    }

    #[test]
    fn coverage_extremes_match_dense(iv in intervals(), length in 0i64..25) {
        let starts: Vec<i64> = iv.iter().map(|x| x.0).collect();
        let ends: Vec<i64> = iv.iter().map(|x| x.1).collect();
        let weights: Vec<i64> = iv.iter().map(|x| x.2).collect();
        let n = ends.iter().copied().max().unwrap_or(0);
        let max = coverage_max(&starts, &ends, &weights, None).unwrap();
        let min = coverage_min(&starts, &ends, &weights, None).unwrap();
        prop_assert!(max.is_merged() && min.is_merged());
        let expected_max: Vec<Option<i64>> = (0..n).map(|p| covering(&iv, p).iter().map(|&i| iv[i].2).max()).collect();
        let expected_min: Vec<Option<i64>> = (0..n).map(|p| covering(&iv, p).iter().map(|&i| iv[i].2).min()).collect();
        prop_assert_eq!(max.to_dense(), expected_max);
        prop_assert_eq!(min.to_dense(), expected_min);

        let padded = coverage_max(&starts, &ends, &weights, Some(length));
        prop_assert_eq!(padded.is_ok(), length >= n);
    }

    #[test]
    fn overlap_plan_matches_dense(iv in intervals()) {
        let starts: Vec<i64> = iv.iter().map(|x| x.0).collect();
        let ends: Vec<i64> = iv.iter().map(|x| x.1).collect();
        let plan = overlap_plan(&starts, &ends, None).unwrap();
        let n = ends.iter().copied().max().unwrap_or(0);
        prop_assert_eq!(plan.ends.last().copied().unwrap_or(0), n);
        let mut start = 0;
        for s in 0..plan.num_segments() {
            for pos in start..plan.ends[s] {
                prop_assert_eq!(plan.ids(s), &covering(&iv, pos)[..]);
            }
            start = plan.ends[s];
        }
    }

    #[test]
    fn collapse_matches_per_key_sums(entries in prop::collection::vec(((0u8..3, any::<bool>()), rle()), 0..6)) {
        let mut seen = vec![];
        let entries: Vec<_> = entries.into_iter().filter(|(k, _)| if seen.contains(k) { false } else { seen.push(*k); true }).collect();
        let batch = RleBatch::from_rles(entries.clone()).unwrap();
        let collapsed = batch.collapse(|(chrom, _)| *chrom, Some(&0), |a, b| a + b).unwrap();
        for (chrom, rle) in collapsed.iter() {
            let members: Vec<&Rle<i64>> = entries.iter().filter(|((c, _), _)| c == chrom).map(|(_, r)| r).collect();
            let expected = members.iter().skip(1).fold(members[0].clone(), |acc, r| &acc + *r);
            prop_assert!(rle.equals(&expected));
        }
    }
}

#[test]
fn coverage_max_works_for_strings_and_nan() {
    let names = ["b", "a", "c"].map(String::from);
    let max = coverage_max(&[0, 1, 3], &[2, 4, 5], &names, None).unwrap();
    let dense: Vec<Option<String>> = max.to_dense();
    let expected = ["b", "b", "a", "c", "c"].map(|s| Some(s.to_string()));
    assert_eq!(dense, expected);

    let floats = coverage_max(&[0, 0, 2], &[2, 1, 3], &[f64::NAN, 1.0, f64::NAN], None).unwrap();
    let dense = floats.to_dense();
    assert_eq!(dense[0], Some(1.0));
    assert!(dense[1].unwrap().is_nan() && dense[2].unwrap().is_nan());
}

#[test]
fn coverage_max_by_key_groups() {
    let batch = coverage_max_by_key(&["x", "y", "x"], &[0, 0, 1], &[2, 1, 3], &[1, 5, 3]).unwrap();
    assert_eq!(batch.keys(), &["x", "y"]);
    assert_eq!(
        batch.get(&"x").unwrap().to_dense(),
        vec![Some(1), Some(3), Some(3)]
    );
}