use bytes::Bytes;
use crate::bbi::block::{DataInterval, DataIntervals};
use crate::bbi::rtree::LeafWalk;
use crate::error::{Error, Result};
use crate::genomic::{BinMode, BinStats, IndexedLoc, IndexedLocs, LocBatch, ValueStats};
use crate::progress::ProgressTracker;
use crate::source::ByteSource;
pub(crate) struct Extraction<'a> {
pub source: &'a dyn ByteSource,
pub locs: &'a IndexedLocs,
pub batches: &'a [LocBatch],
pub tree_root: u64,
pub zoom: bool,
pub uncompress_buffer_size: u32,
pub tracker: &'a ProgressTracker,
}
impl Extraction<'_> {
fn read_leaf(&self, offset: u64, size: u64) -> Result<Bytes> {
let raw = self.source.read_exact_at(offset, size as usize)?;
crate::bbi::block::decompress(raw, self.uncompress_buffer_size, self.source.path())
}
fn walk_batch(
&self,
batch: LocBatch,
mut visit: impl FnMut(&DataInterval, usize, &IndexedLoc, i64, i64) -> Result<()>,
) -> Result<()> {
let locs = &self.locs.locs;
let leaves = LeafWalk::new(self.source, self.tree_root, locs, batch, self.tracker)?;
for leaf in leaves {
let (leaf, loc_range) = leaf?;
let block = self.read_leaf(leaf.offset, leaf.size)?;
let intervals = DataIntervals::new(
block,
self.zoom,
locs,
loc_range.clone(),
self.source.path(),
)?;
let mut cursor = loc_range.start;
#[allow(clippy::needless_range_loop)]
for interval in intervals {
let interval = interval?;
cursor = advance_cursor(
locs,
cursor,
loc_range.end,
interval.chr_index,
interval.start,
);
for index in cursor..loc_range.end {
let loc = &locs[index];
if interval.chr_index != loc.chr_index as u32 {
break;
}
if interval.end <= loc.binned_start {
break;
}
if loc.binned_end <= loc.binned_start {
continue;
}
if interval.start >= loc.binned_end {
continue;
}
let overlap_start = interval.start.max(loc.binned_start);
let overlap_end = interval.end.min(loc.binned_end);
visit(&interval, index, loc, overlap_start, overlap_end)?;
}
}
}
Ok(())
}
}
#[inline]
fn advance_cursor(
locs: &[IndexedLoc],
mut cursor: usize,
end: usize,
chr: u32,
start: i64,
) -> usize {
while cursor < end {
let loc = &locs[cursor];
let loc_chr = loc.chr_index as u32;
if loc_chr > chr {
break;
}
if loc_chr == chr && loc.binned_end > start {
break;
}
cursor += 1;
}
cursor
}
pub(crate) fn values(
ex: &Extraction<'_>,
executor: &crate::parallel::Executor,
bin_mode: BinMode,
def_value: f32,
) -> Result<Vec<f32>> {
let bin_count = ex.locs.bin_count;
let per_batch = executor.map_batches(ex.batches, |_, batch| {
let mut stats = vec![BinStats::default(); batch.len() * bin_count];
ex.walk_batch(*batch, |interval, index, loc, from, to| {
let base = (index - batch.start) * bin_count;
let bin_start = loc.bin_at(from);
let bin_end = loc.bin_after(to);
for b in bin_start..bin_end {
if b as usize >= bin_count {
break;
}
let covered = loc.bin_coverage(b, from, to);
if covered <= 0.0 {
continue;
}
stats[base + b as usize].add(interval.value, covered);
}
Ok(())
})?;
Ok(stats)
})?;
let mut output = vec![def_value; ex.locs.output_len];
for (batch, stats) in ex.batches.iter().zip(&per_batch) {
for (offset, index) in (batch.start..batch.end).enumerate() {
let loc = &ex.locs.locs[index];
for b in 0..bin_count {
let s = &stats[offset * bin_count + b];
if s.count <= 0.0 {
continue;
}
output[loc.output_start + b] = s.apply(bin_mode);
}
}
}
ex.locs.reverse_output_rows(&mut output);
Ok(output)
}
pub(crate) fn values_stats(
ex: &Extraction<'_>,
executor: &crate::parallel::Executor,
) -> Result<Vec<ValueStats>> {
let bin_count = ex.locs.bin_count;
let per_batch = executor.map_batches(ex.batches, |_, batch| {
let mut stats = vec![ValueStats::default(); batch.len()];
ex.walk_batch(*batch, |interval, index, _loc, from, to| {
let overlap = to - from;
let span = interval.end - interval.start;
let covered = if span > 0 && interval.valid_count != span {
(interval.valid_count as f64 * overlap as f64 / span as f64).round() as i64
} else {
overlap
};
let fraction = if interval.valid_count > 0 {
covered as f64 / interval.valid_count as f64
} else {
0.0
};
stats[index - batch.start].add_aggregate(
interval.min_value,
interval.max_value,
interval.value as f64 * covered as f64,
interval.sum_squared * fraction,
covered,
);
Ok(())
})?;
Ok(stats)
})?;
let mut output = vec![ValueStats::default(); ex.locs.locs.len()];
for (batch, stats) in ex.batches.iter().zip(&per_batch) {
for (offset, index) in (batch.start..batch.end).enumerate() {
output[ex.locs.locs[index].row(bin_count)] = stats[offset];
}
}
Ok(output)
}
#[derive(Debug, Clone, Copy)]
struct OpenBin {
bin: i64,
stats: BinStats,
reverse: bool,
}
pub(crate) fn values_profile(
ex: &Extraction<'_>,
executor: &crate::parallel::Executor,
bin_mode: BinMode,
) -> Result<Vec<ValueStats>> {
let bin_count = ex.locs.bin_count;
let per_batch = executor.map_batches(ex.batches, |_, batch| {
let mut column = vec![ValueStats::default(); bin_count];
let mut open: Vec<OpenBin> = (batch.start..batch.end)
.map(|i| OpenBin {
bin: -1,
stats: BinStats::default(),
reverse: ex.locs.locs[i].reverse,
})
.collect();
let close = |open: &mut OpenBin, column: &mut Vec<ValueStats>| {
if open.stats.count <= 0.0 {
return;
}
let value = open.stats.apply(bin_mode);
let col = if open.reverse {
bin_count as i64 - 1 - open.bin
} else {
open.bin
};
if col >= 0 && (col as usize) < column.len() {
column[col as usize].add(value);
}
open.stats = BinStats::default();
};
ex.walk_batch(*batch, |interval, index, loc, from, to| {
let bin_start = loc.bin_at(from);
let bin_end = loc.bin_after(to);
let slot = &mut open[index - batch.start];
for b in bin_start..bin_end {
if b as usize >= bin_count {
break;
}
let covered = loc.bin_coverage(b, from, to);
if covered <= 0.0 {
continue;
}
if b != slot.bin {
close(slot, &mut column);
slot.bin = b;
}
slot.stats.add(interval.value, covered);
}
Ok(())
})?;
for slot in &mut open {
close(slot, &mut column);
}
Ok(column)
})?;
let mut output = vec![ValueStats::default(); bin_count];
for column in &per_batch {
for (col, batch_stats) in column.iter().enumerate() {
if batch_stats.count == 0 {
continue;
}
output[col].merge(batch_stats);
}
}
Ok(output)
}
impl Extraction<'_> {
pub(crate) fn walk_bed_batch(
&self,
batch: LocBatch,
auto_sql: &indexmap::IndexMap<String, String>,
col_count: usize,
mut visit: impl FnMut(&mut BedRecord, &[usize]) -> Result<()>,
) -> Result<()> {
let locs = &self.locs.locs;
let leaves = LeafWalk::new(self.source, self.tree_root, locs, batch, self.tracker)?;
let mut matched: Vec<usize> = Vec::new();
for leaf in leaves {
let (leaf, loc_range) = leaf?;
let block = self.read_leaf(leaf.offset, leaf.size)?;
let records = super::block::BedRecords::new(
block,
auto_sql,
col_count,
locs,
loc_range.clone(),
self.source.path(),
)?;
let mut cursor = loc_range.start;
for record in records {
let (chr_index, start, end, fields) = record?;
cursor = advance_cursor(locs, cursor, loc_range.end, chr_index, start);
matched.clear();
let reach = end.max(start + 1);
#[allow(clippy::needless_range_loop)]
for index in cursor..loc_range.end {
let loc = &locs[index];
if chr_index != loc.chr_index as u32 {
break;
}
if reach <= loc.binned_start {
break;
}
if start >= loc.binned_end {
continue;
}
matched.push(index);
}
if matched.is_empty() {
continue;
}
let mut entry = BedRecord {
chr_index,
start,
end,
fields,
};
visit(&mut entry, &matched)?;
}
}
Ok(())
}
}
pub(crate) struct BedRecord {
pub chr_index: u32,
pub start: i64,
pub end: i64,
pub fields: Vec<(String, String)>,
}
pub(crate) fn entries(
ex: &Extraction<'_>,
executor: &crate::parallel::Executor,
auto_sql: &indexmap::IndexMap<String, String>,
chr_names: &[String],
col_count: usize,
) -> Result<Vec<Vec<super::BedEntry>>> {
let bin_count = ex.locs.bin_count;
let per_batch = executor.map_batches(ex.batches, |_, batch| {
let mut out: Vec<Vec<super::BedEntry>> = vec![Vec::new(); batch.len()];
ex.walk_bed_batch(*batch, auto_sql, col_count, |entry, matched| {
let chr = chr_names
.get(entry.chr_index as usize)
.cloned()
.unwrap_or_default();
for (n, index) in matched.iter().enumerate() {
let fields = if n + 1 == matched.len() {
std::mem::take(&mut entry.fields)
} else {
entry.fields.clone()
};
out[index - batch.start].push(super::BedEntry {
chr: chr.clone(),
start: entry.start,
end: entry.end,
fields,
});
}
Ok(())
})?;
Ok(out)
})?;
let mut output: Vec<Vec<super::BedEntry>> = vec![Vec::new(); ex.locs.locs.len()];
for (batch, lists) in ex.batches.iter().zip(per_batch) {
for (offset, list) in lists.into_iter().enumerate() {
output[ex.locs.locs[batch.start + offset].row(bin_count)] = list;
}
}
for entries in &mut output {
entries.sort_by(|a, b| (&a.chr, a.start, a.end).cmp(&(&b.chr, b.start, b.end)));
}
Ok(output)
}
pub(crate) fn entries_pileup(
ex: &Extraction<'_>,
executor: &crate::parallel::Executor,
auto_sql: &indexmap::IndexMap<String, String>,
def_value: f32,
) -> Result<Vec<f32>> {
let bin_count = ex.locs.bin_count;
let per_batch = executor.map_batches(ex.batches, |_, batch| {
let mut depth = vec![0.0f32; batch.len() * bin_count];
ex.walk_bed_batch(*batch, auto_sql, 3, |entry, matched| {
for index in matched {
let loc = &ex.locs.locs[*index];
if loc.binned_end <= loc.binned_start {
continue;
}
let from = entry.start.max(loc.binned_start);
let to = entry.end.min(loc.binned_end);
let base = (index - batch.start) * bin_count;
for b in loc.bin_at(from)..loc.bin_after(to) {
if b as usize >= bin_count {
break;
}
let fraction = loc.bin_fraction(b, from, to);
if fraction <= 0.0 {
continue;
}
depth[base + b as usize] += fraction as f32;
}
}
Ok(())
})?;
Ok(depth)
})?;
let mut output = vec![0.0f32; ex.locs.output_len];
for (batch, depth) in ex.batches.iter().zip(&per_batch) {
for (offset, index) in (batch.start..batch.end).enumerate() {
let loc = &ex.locs.locs[index];
output[loc.output_start..loc.output_end]
.copy_from_slice(&depth[offset * bin_count..(offset + 1) * bin_count]);
}
}
if def_value != 0.0 {
for value in &mut output {
if *value == 0.0 {
*value = def_value;
}
}
}
ex.locs.reverse_output_rows(&mut output);
Ok(output)
}
pub(crate) fn pileup_stats(locs: &IndexedLocs, pileup: &[f32]) -> Vec<ValueStats> {
let mut output = vec![ValueStats::default(); locs.locs.len()];
for loc in &locs.locs {
let stats = &mut output[loc.row(locs.bin_count)];
for value in &pileup[loc.output_start..loc.output_end] {
if value.is_nan() {
continue;
}
stats.add(*value);
}
}
output
}
pub(crate) fn pileup_profile(locs: &IndexedLocs, pileup: &[f32]) -> Vec<ValueStats> {
let mut output = vec![ValueStats::default(); locs.bin_count];
for (col, stats) in output.iter_mut().enumerate() {
for loc in &locs.locs {
let value = pileup[loc.output_start + col];
if value.is_nan() {
continue;
}
stats.add(value);
}
}
output
}
pub type WindowLoc = (String, i64, i64);
const MIN_PIECE_DATA_SIZE: f64 = 16384.0;
#[derive(Debug)]
struct Walk {
locs: std::sync::Arc<Vec<WindowLoc>>,
next: usize,
parallel: usize,
bytes_per_bp: f64,
total_coverage: u64,
done_coverage: u64,
}
impl Walk {
fn new(locs: Vec<WindowLoc>, parallel: usize, data_size: u64, genome_size: i64) -> Self {
let total_coverage = locs.iter().map(|(_, s, e)| (e - s).max(0) as u64).sum();
let bytes_per_bp = if genome_size < 1 || data_size < 1 {
0.0
} else {
data_size as f64 / genome_size as f64
};
Self {
locs: std::sync::Arc::new(locs),
next: 0,
parallel: parallel.max(1),
bytes_per_bp,
total_coverage,
done_coverage: 0,
}
}
fn restarted(&self) -> Self {
Self {
locs: self.locs.clone(),
next: 0,
parallel: self.parallel,
bytes_per_bp: self.bytes_per_bp,
total_coverage: self.total_coverage,
done_coverage: 0,
}
}
fn split(&self, units: i64, coverage: i64) -> (i64, i64) {
let window_data = self.bytes_per_bp * coverage as f64;
let worth = (window_data / MIN_PIECE_DATA_SIZE) as i64;
let pieces = worth.min(self.parallel as i64).max(1);
let piece_units = ((units + pieces - 1) / pieces).max(1);
(piece_units, (units + piece_units - 1) / piece_units)
}
fn take(&mut self, progress: Option<&crate::progress::ProgressFn>) -> usize {
let index = self.next;
self.next += 1;
let (_, start, end) = &self.locs[index];
self.done_coverage += (end - start).max(0) as u64;
if let Some(report) = progress {
report(self.done_coverage, self.total_coverage);
}
index
}
fn finish(&mut self, progress: Option<&crate::progress::ProgressFn>) {
if let Some(report) = progress {
if self.done_coverage < self.total_coverage {
self.done_coverage = self.total_coverage;
report(self.total_coverage, self.total_coverage);
}
}
}
}
pub struct ValuesWalk {
walk: Walk,
bin_size: i64,
bins: std::sync::Arc<Vec<i64>>,
bin_mode: BinMode,
def_value: f32,
zoom: super::Zoom,
progress: Option<crate::progress::ProgressFn>,
}
impl std::fmt::Debug for ValuesWalk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ValuesWalk")
.field("windows", &self.walk.locs.len())
.field("next", &self.walk.next)
.field("bin_size", &self.bin_size)
.finish()
}
}
impl ValuesWalk {
pub fn restarted(&self) -> Self {
Self {
walk: self.walk.restarted(),
bin_size: self.bin_size,
bins: self.bins.clone(),
bin_mode: self.bin_mode,
def_value: self.def_value,
zoom: self.zoom,
progress: self.progress.clone(),
}
}
pub fn plan(reader: &super::BbiReader, req: &super::ValuesRequest, span: i64) -> Result<Self> {
if span < 1 {
return Err(Error::invalid(format!(
"span must be positive (got {span})"
)));
}
let bin_size =
crate::genomic::BinPlan::new(req.common.bin_size, None, req.common.full_bin)?
.whole_bin_size();
if reader.kind().is_bigbed() && !matches!(req.common.zoom, super::Zoom::Full) {
return Err(Error::invalid("zoom is only supported for bigwig files"));
}
let level = reader.select_zoom(req.common.bin_size, req.common.zoom)?;
let bins_per_window = ((span + bin_size - 1) / bin_size).max(1);
let mut locs = Vec::new();
let mut bins = Vec::new();
for chr in reader.chr_sizes().select(&req.common.locs.chr_ids)? {
let chr_bins = if req.common.full_bin {
(chr.size + bin_size - 1) / bin_size
} else {
chr.size / bin_size
};
let mut bin = 0;
while bin < chr_bins {
let start = bin * bin_size;
let window_bins = bins_per_window.min(chr_bins - bin);
locs.push((
chr.id.clone(),
start,
(start + window_bins * bin_size).min(chr.size),
));
bins.push(window_bins);
bin += bins_per_window;
}
}
let walk = Walk::new(
locs,
reader.parallel(),
reader.data_size(level),
reader.genome_size(),
);
Ok(Self {
walk,
bin_size,
bins: std::sync::Arc::new(bins),
bin_mode: req.bin_mode,
def_value: req.common.def_value,
zoom: req.common.zoom,
progress: req.common.progress.clone(),
})
}
pub fn len(&self) -> usize {
self.walk.locs.len()
}
pub fn bin_size(&self) -> i64 {
self.bin_size
}
pub fn is_empty(&self) -> bool {
self.walk.locs.is_empty()
}
pub fn locs(&self) -> &[WindowLoc] {
&self.walk.locs
}
fn read(&self, reader: &super::BbiReader, index: usize) -> Result<Vec<f32>> {
let (chr, start, end) = &self.walk.locs[index];
let (piece_bins, piece_count) = self.walk.split(self.bins[index], end - start);
let piece_span = piece_bins * self.bin_size;
let chr_ids = vec![chr.clone(); piece_count as usize];
let starts: Vec<i64> = (0..piece_count).map(|i| start + i * piece_span).collect();
let ends: Vec<i64> = starts.iter().map(|s| s + piece_span).collect();
let request =
super::ValuesRequest::new(crate::genomic::Locs::spans(&chr_ids, &starts, &ends)?)
.bin_size(self.bin_size as f64)
.bin_count(piece_bins as usize)
.bin_mode(self.bin_mode)
.def_value(self.def_value)
.zoom(self.zoom);
let values = reader.read_values(&request)?;
Ok(values.into_raw_vec_and_offset().0)
}
pub fn next_window(
&mut self,
reader: &super::BbiReader,
) -> Option<Result<ndarray::Array1<f32>>> {
if self.walk.next >= self.walk.locs.len() {
self.walk.finish(self.progress.as_ref());
return None;
}
let index = self.walk.next;
let mut values = match self.read(reader, index) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
self.walk.take(self.progress.as_ref());
values.truncate(self.bins[index] as usize);
Some(Ok(ndarray::Array1::from_vec(values)))
}
}
#[derive(Debug)]
pub struct ValuesWindows<'a> {
reader: &'a super::BbiReader,
walk: ValuesWalk,
}
impl<'a> ValuesWindows<'a> {
pub(crate) fn plan(
reader: &'a super::BbiReader,
req: &super::ValuesRequest,
span: i64,
) -> Result<Self> {
Ok(Self {
reader,
walk: ValuesWalk::plan(reader, req, span)?,
})
}
pub fn len(&self) -> usize {
self.walk.len()
}
pub fn is_empty(&self) -> bool {
self.walk.is_empty()
}
pub fn locs(&self) -> &[WindowLoc] {
self.walk.locs()
}
pub fn bin_size(&self) -> i64 {
self.walk.bin_size()
}
}
impl Iterator for ValuesWindows<'_> {
type Item = Result<ndarray::Array1<f32>>;
fn next(&mut self) -> Option<Self::Item> {
self.walk.next_window(self.reader)
}
}
pub struct EntryWalk {
walk: Walk,
col_count: usize,
progress: Option<crate::progress::ProgressFn>,
}
impl std::fmt::Debug for EntryWalk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntryWalk")
.field("windows", &self.walk.locs.len())
.field("next", &self.walk.next)
.field("col_count", &self.col_count)
.finish()
}
}
impl EntryWalk {
pub fn restarted(&self) -> Self {
Self {
walk: self.walk.restarted(),
col_count: self.col_count,
progress: self.progress.clone(),
}
}
pub fn plan(reader: &super::BbiReader, req: &super::EntriesRequest, span: i64) -> Result<Self> {
if !reader.kind().is_bigbed() {
return Err(Error::invalid("iter_all_entries only for bigbed"));
}
if span < 1 {
return Err(Error::invalid(format!(
"span must be positive (got {span})"
)));
}
reader.check_col_count(req.col_count, 3)?;
let mut locs = Vec::new();
for chr in reader.chr_sizes().select(&req.common.locs.chr_ids)? {
let mut start = 0;
while start < chr.size {
locs.push((chr.id.clone(), start, (start + span).min(chr.size)));
start += span;
}
}
let walk = Walk::new(
locs,
reader.parallel(),
reader.data_size(None),
reader.genome_size(),
);
Ok(Self {
walk,
col_count: req.col_count,
progress: req.common.progress.clone(),
})
}
pub fn len(&self) -> usize {
self.walk.locs.len()
}
pub fn is_empty(&self) -> bool {
self.walk.locs.is_empty()
}
pub fn locs(&self) -> &[WindowLoc] {
&self.walk.locs
}
fn read(&self, reader: &super::BbiReader, index: usize) -> Result<Vec<super::BedEntry>> {
let (chr, start, end) = &self.walk.locs[index];
let coverage = end - start;
let (piece_span, piece_count) = self.walk.split(coverage, coverage);
let chr_ids = vec![chr.clone(); piece_count as usize];
let mut starts = Vec::with_capacity(piece_count as usize);
let mut ends = Vec::with_capacity(piece_count as usize);
let mut piece_min_starts = Vec::with_capacity(piece_count as usize);
for i in 0..piece_count {
let piece_start = start + i * piece_span;
piece_min_starts.push(piece_start);
starts.push((piece_start - 1).max(0));
ends.push((piece_start + piece_span).min(*end));
}
let request =
super::EntriesRequest::new(crate::genomic::Locs::spans(&chr_ids, &starts, &ends)?)
.col_count(self.col_count);
let pieces = reader.read_entries(&request)?;
let mut out = Vec::new();
for (piece, min_start) in pieces.into_iter().zip(piece_min_starts) {
out.extend(piece.into_iter().filter(|e| e.start >= min_start));
}
Ok(out)
}
pub fn next_window(
&mut self,
reader: &super::BbiReader,
) -> Option<Result<Vec<super::BedEntry>>> {
if self.walk.next >= self.walk.locs.len() {
self.walk.finish(self.progress.as_ref());
return None;
}
let index = self.walk.next;
let entries = match self.read(reader, index) {
Ok(e) => e,
Err(e) => return Some(Err(e)),
};
self.walk.take(self.progress.as_ref());
Some(Ok(entries))
}
}
#[derive(Debug)]
pub struct EntryWindows<'a> {
reader: &'a super::BbiReader,
walk: EntryWalk,
}
impl<'a> EntryWindows<'a> {
pub(crate) fn plan(
reader: &'a super::BbiReader,
req: &super::EntriesRequest,
span: i64,
) -> Result<Self> {
Ok(Self {
reader,
walk: EntryWalk::plan(reader, req, span)?,
})
}
pub fn len(&self) -> usize {
self.walk.len()
}
pub fn is_empty(&self) -> bool {
self.walk.is_empty()
}
pub fn locs(&self) -> &[WindowLoc] {
self.walk.locs()
}
}
impl Iterator for EntryWindows<'_> {
type Item = Result<Vec<super::BedEntry>>;
fn next(&mut self) -> Option<Self::Item> {
self.walk.next_window(self.reader)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn loc(chr: usize, start: i64, end: i64) -> IndexedLoc {
IndexedLoc {
chr_index: chr,
start,
end,
binned_start: start,
binned_end: end,
bin_size: 1.0,
reverse: false,
output_start: 0,
output_end: 1,
}
}
#[test]
fn the_cursor_skips_loci_the_block_has_passed() {
let locs = [loc(0, 0, 10), loc(0, 20, 30), loc(0, 40, 50)];
assert_eq!(advance_cursor(&locs, 0, 3, 0, 25), 1);
assert_eq!(advance_cursor(&locs, 0, 3, 0, 45), 2);
assert_eq!(advance_cursor(&locs, 0, 3, 0, 5), 0);
}
#[test]
fn the_cursor_stops_at_a_higher_chromosome() {
let locs = [loc(0, 0, 10), loc(1, 0, 10), loc(2, 0, 10)];
assert_eq!(advance_cursor(&locs, 0, 3, 1, 5), 1);
assert_eq!(advance_cursor(&locs, 0, 3, 0, 5), 0);
}
#[test]
fn the_cursor_never_goes_backwards() {
let locs = [loc(0, 0, 10), loc(0, 20, 30)];
assert_eq!(advance_cursor(&locs, 1, 2, 0, 0), 1);
}
}