Rust¶
The Python package is a thin layer over two Rust crates, which you can use directly:
| Crate | What it is |
|---|---|
runlength |
The core. Rle<V> holds values of any Rust type, RleBatch<K, V> keys of any hashable type. It depends only on rayon and rustc-hash. |
runlength-arrow |
The same over Apache Arrow arrays: values of any Arrow type, and batch keys as Arrow tables. The Python package uses this one. |
cargo add runlength
The API reference for both is on docs.rs. The examples below come from
crates/runlength/examples/tour.rs,
which CI runs; each assert_eq! shows what the line before it gives.
Sequences¶
Rle<V> owns a sequence and RleRef<'_, V> borrows one. Their shared
methods are on the RleLike trait, which the prelude brings in.
// Runs of length 3 and 2, holding 1 and 2: the sequence 1 1 1 2 2.
let a = Rle::from_runs(&[3, 2], vec![1, 2])?;
assert_eq!(format!("{a:?}"), "Rle { runs: [3, 2], values: [1, 2] }");
assert_eq!(a.to_dense(), vec![1, 1, 1, 2, 2]);
assert_eq!((a.len(), a.num_runs(), a.ends()), (5, 2, &[3, 5][..]));
assert_eq!(a.get(3), Some(&2));
// Values can be any type; neighbours merge when they are identical.
let labels = Rle::from_dense(["x", "x", "y"]);
assert_eq!(
format!("{labels:?}"),
r#"Rle { runs: [2, 1], values: ["x", "y"] }"#
);
Combining sequences¶
Operators work on numbers. plan_with and zip_with work on any types, as
described in Combining sequences.
let b = Rle::from_runs(&[1, 4], vec![10, 20])?;
// Operators work position by position and merge the result.
let sum = &a + &b;
assert_eq!(
format!("{sum:?}"),
"Rle { runs: [1, 2, 2], values: [11, 21, 22] }"
);
let less = a.elem_lt(&b);
assert_eq!(format!("{less:?}"), "Rle { runs: [5], values: [true] }");
// The plan: where the output segments end, and which run of each input
// covers each segment. It never looks at values.
let plan = a.plan_with(&b);
assert_eq!(plan.ends, vec![1, 3, 5]);
assert_eq!((plan.idx_a, plan.idx_b), (vec![0, 0, 1], vec![0, 1, 1]));
// So any function works, on any value types. No fill values: the lengths
// must match.
let pairs = a.zip_with(&b, None, None, |x, y| format!("{x}/{y}"))?;
assert_eq!(
format!("{pairs:?}"),
r#"Rle { runs: [1, 2, 2], values: ["1/10", "1/20", "2/20"] }"#
);
The _raw variants (zip_with_raw, ...) skip the final merge.
Coverage¶
// How many of the intervals [0, 3), [2, 4) and [8, 10) cover each position.
let starts = [0, 2, 8];
let ends = [3, 4, 10];
let cov = coverage_count(&starts, &ends, None)?;
assert_eq!(
format!("{cov:?}"),
"Rle { runs: [2, 1, 1, 4, 2], values: [1, 2, 1, 0, 1] }"
);
// Or the sum of a value per interval.
let scores = coverage_sum(&starts, &ends, &[0.5, 2.0, 1.0], None)?;
assert_eq!(scores.max(), Some(&2.5));
// Mean coverage over many slices at once, without copying them.
let means = cov.slice_reduce(&[0, 4], &[4, 10], |s| s.mean())?;
assert_eq!(means, vec![Some(1.25), Some(1.0 / 3.0)]);
// Which intervals cover each segment, for any other aggregation.
let plan = overlap_plan(&starts, &ends, None)?;
assert_eq!(plan.ends, vec![2, 3, 4, 8, 10]);
assert_eq!(plan.ids(1), &[0, 1]);
coverage_max and coverage_min take the largest or smallest value per
position instead of the sum, and coverage_sum_opt leaves uncovered
positions as None.
Batches¶
// One sequence per key; keys can be any hashable type.
let keys = ["chr1", "chr1", "chr2"];
let batch = coverage_count_by_key(&keys, &[0, 2, 5], &[3, 4, 7])?;
assert_eq!(
format!("{batch:?}"),
r#"{"chr1": RleRef { runs: [2, 1, 1], values: [1, 2, 1] }, "chr2": RleRef { runs: [5, 2], values: [0, 1] }}"#
);
assert_eq!(batch.get("chr2").map(|r| r.len()), Some(7));
// Operators join two batches on their keys (an outer join) in parallel.
let doubled = &batch + &batch;
assert_eq!(doubled.get("chr1").unwrap().to_dense(), vec![2, 2, 4, 2]);
// Or pick the join and the function yourself.
let other = coverage_count_by_key(&["chr2"], &[0], &[7])?;
let both = batch.zip_with(&other, How::Inner, Some(&0), Some(&0), |x, y| x * y)?;
assert_eq!(both.keys(), &["chr2"]);
collapse merges keys, getitems and slice_reduce slice many sequences
by key, and map_rles and reduce apply a function to every sequence, all in
parallel.
Conventions¶
- Positions and lengths are
i64, every sequence starts at 0, and intervals are half-open,[start, end). - Neighbouring runs merge when their values are
Identical:NaNmerges withNaN, and-0.0stays apart from0.0. ImplementIdenticalfor your own value types (it's implemented for numbers,bool,char, strings,Option, tuples,Vecand more). - Functions that can fail return
runlength::Result. Input is checked once, where it enters (run lengths must not be negative,runsandvaluesmust have the same length); after that, operations can't fail on bad data.