use std::ops::Range;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HaloWindow {
pub index: usize,
pub valid_global: Range<u64>,
pub read_global: Range<u64>,
pub valid_local: Range<usize>,
}
impl HaloWindow {
#[inline]
pub fn read_len(&self) -> usize {
(self.read_global.end - self.read_global.start) as usize
}
#[inline]
pub fn valid_len(&self) -> usize {
self.valid_local.len()
}
#[inline]
pub fn is_interior_local(&self, local_sample: usize) -> bool {
self.valid_local.contains(&local_sample)
}
#[inline]
pub fn to_global_sample(&self, local_sample: usize) -> u64 {
self.read_global.start + local_sample as u64
}
#[inline]
pub fn remap_event(&self, local_sample: usize) -> Option<u64> {
self.is_interior_local(local_sample).then(|| self.to_global_sample(local_sample))
}
pub fn around(index: usize, valid: Range<u64>, left_halo: u64, right_halo: u64, total_samples: u64) -> Self {
let end = valid.end.min(total_samples);
let start = valid.start.min(end);
let read_start = start.saturating_sub(left_halo);
let read_end = (end + right_halo).min(total_samples);
let left_pad = (start - read_start) as usize;
Self {
index,
valid_global: start..end,
read_global: read_start..read_end,
valid_local: left_pad..left_pad + (end - start) as usize,
}
}
}
#[derive(Debug, Clone)]
pub struct ChunkSchedule {
windows: Vec<HaloWindow>,
max_read_samples: usize,
}
impl ChunkSchedule {
pub fn new(
target_range: Range<u64>,
batch_samples: u64,
left_halo: u64,
right_halo: u64,
total_samples: u64,
) -> Self {
let start = target_range.start.min(total_samples);
let end = target_range.end.min(total_samples);
let step = batch_samples.max(1);
let mut windows = Vec::new();
let mut max_read_samples = 0usize;
let mut s0 = start;
let mut index = 0usize;
while s0 < end {
let s1 = (s0 + step).min(end);
let win = HaloWindow::around(index, s0..s1, left_halo, right_halo, total_samples);
max_read_samples = max_read_samples.max(win.read_len());
windows.push(win);
s0 = s1;
index += 1;
}
Self {
windows,
max_read_samples,
}
}
pub fn full_recording(
total_samples: u64,
batch_samples: u64,
left_halo: u64,
right_halo: u64,
) -> Self {
Self::new(
0..total_samples,
batch_samples,
left_halo,
right_halo,
total_samples,
)
}
pub fn windows(&self) -> &[HaloWindow] {
&self.windows
}
pub fn into_windows(self) -> Vec<HaloWindow> {
self.windows
}
pub fn len(&self) -> usize {
self.windows.len()
}
pub fn is_empty(&self) -> bool {
self.windows.is_empty()
}
pub fn max_read_samples(&self) -> usize {
self.max_read_samples
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chunk_schedule_partitions_without_gaps_or_overlap() {
let total = 10_000u64;
let sched = ChunkSchedule::full_recording(total, 3_000, 500, 200);
assert_eq!(sched.len(), 4);
assert_eq!(sched.max_read_samples(), 3_700);
let w = sched.windows();
assert_eq!(w[0].valid_global, 0..3_000);
assert_eq!(w[0].read_global, 0..3_200);
assert_eq!(w[0].valid_local, 0..3_000);
assert_eq!(w[0].to_global_sample(w[0].valid_local.start), 0);
assert_eq!(w[1].valid_global, 3_000..6_000);
assert_eq!(w[1].read_global, 2_500..6_200);
assert_eq!(w[1].valid_local, 500..3_500);
assert!(!w[1].is_interior_local(499));
assert!(w[1].is_interior_local(500));
assert_eq!(w[1].to_global_sample(500), 3_000);
assert_eq!(w[1].to_global_sample(3_499), 5_999);
assert_eq!(w[3].valid_global, 9_000..10_000);
assert_eq!(w[3].read_global, 8_500..10_000);
assert_eq!(w[3].valid_local, 500..1_500);
assert_eq!(w[3].to_global_sample(1_499), 9_999);
}
#[test]
fn around_clamps_and_remaps_events() {
let w = HaloWindow::around(7, 30..60, 5, 5, 62);
assert_eq!((w.index, w.read_global.clone(), w.valid_local.clone()), (7, 25..62, 5..35));
assert_eq!(w.remap_event(4), None);
assert_eq!(w.remap_event(5), Some(30));
assert_eq!(w.remap_event(34), Some(59));
assert_eq!(w.remap_event(35), None);
assert_eq!(HaloWindow::around(0, 90..120, 5, 5, 100).valid_global, 90..100);
}
}