Skip to main content

gwseq_io/bbi/
extract.rs

1//! The extraction kernels.
2//!
3//! Where the read time goes. They share one shape: walk the R-tree for the leaves the batch's loci touch,
4//! inflate each leaf, walk its items forward, and fold every overlap into the
5//! output.
6//!
7//! # The loc cursor
8//!
9//! A block hands out its items in increasing (chromosome, position) order, so a
10//! locus the cursor has moved past cannot come back into range. Without the
11//! cursor, every item rescans its block's loci from the first, which makes a
12//! block of n items against m loci cost n·m. It is not an optimisation to leave
13//! for later — it is the difference between linear and quadratic on a request
14//! with many loci.
15//!
16//! # Where each worker writes
17//!
18//! A batch is a contiguous run of loci **in file order**, but a locus's output
19//! slice follows the order the request asked in — so a batch's outputs are
20//! scattered through the result, not contiguous in it. Each worker therefore
21//! fills a compact buffer covering only its own loci, and the caller scatters
22//! those into the result once every worker has finished. No two workers ever
23//! touch the same bin, so no bin's accumulation order depends on scheduling.
24
25use bytes::Bytes;
26
27use crate::bbi::block::{DataInterval, DataIntervals};
28use crate::bbi::rtree::LeafWalk;
29use crate::error::{Error, Result};
30use crate::genomic::{BinMode, BinStats, IndexedLoc, IndexedLocs, LocBatch, ValueStats};
31use crate::progress::ProgressTracker;
32use crate::source::ByteSource;
33
34/// Everything a kernel needs that is the same for every batch.
35pub(crate) struct Extraction<'a> {
36    pub source: &'a dyn ByteSource,
37    pub locs: &'a IndexedLocs,
38    pub batches: &'a [LocBatch],
39    /// Where the R-tree's root node begins — past the 48-byte index header.
40    pub tree_root: u64,
41    /// True when reading a zoom level rather than the full data.
42    pub zoom: bool,
43    pub uncompress_buffer_size: u32,
44    pub tracker: &'a ProgressTracker,
45}
46
47impl Extraction<'_> {
48    fn read_leaf(&self, offset: u64, size: u64) -> Result<Bytes> {
49        let raw = self.source.read_exact_at(offset, size as usize)?;
50        crate::bbi::block::decompress(raw, self.uncompress_buffer_size, self.source.path())
51    }
52
53    /// Walk every (leaf, loci) pair of one batch, handing each interval to
54    /// `visit` together with the locus it overlaps and their overlap.
55    ///
56    /// The three value kernels differ only in what they do with that, so the
57    /// walk — the R-tree descent, the block inflation, the cursor, the four
58    /// overlap tests — lives here once.
59    fn walk_batch(
60        &self,
61        batch: LocBatch,
62        mut visit: impl FnMut(&DataInterval, usize, &IndexedLoc, i64, i64) -> Result<()>,
63    ) -> Result<()> {
64        let locs = &self.locs.locs;
65        let leaves = LeafWalk::new(self.source, self.tree_root, locs, batch, self.tracker)?;
66        for leaf in leaves {
67            let (leaf, loc_range) = leaf?;
68            let block = self.read_leaf(leaf.offset, leaf.size)?;
69            let intervals = DataIntervals::new(
70                block,
71                self.zoom,
72                locs,
73                loc_range.clone(),
74                self.source.path(),
75            )?;
76            let mut cursor = loc_range.start;
77            // Indexed rather than iterated: `visit` is handed the locus's index
78            // in the whole request, which is what tells a kernel where in its
79            // output the locus belongs. Enumerating a subslice would report a
80            // different number.
81            #[allow(clippy::needless_range_loop)]
82            for interval in intervals {
83                let interval = interval?;
84                cursor = advance_cursor(
85                    locs,
86                    cursor,
87                    loc_range.end,
88                    interval.chr_index,
89                    interval.start,
90                );
91                for index in cursor..loc_range.end {
92                    let loc = &locs[index];
93                    // A tree item may straddle a chromosome boundary, so its
94                    // loci are not all on the interval's chromosome. The cursor
95                    // has already passed the ones behind it, so the first one
96                    // beyond ends the scan.
97                    if interval.chr_index != loc.chr_index as u32 {
98                        break;
99                    }
100                    if interval.end <= loc.binned_start {
101                        break;
102                    }
103                    // The grid left this locus narrower than a bin: it reads
104                    // nothing, and its bin size is 0.
105                    if loc.binned_end <= loc.binned_start {
106                        continue;
107                    }
108                    if interval.start >= loc.binned_end {
109                        continue;
110                    }
111                    let overlap_start = interval.start.max(loc.binned_start);
112                    let overlap_end = interval.end.min(loc.binned_end);
113                    visit(&interval, index, loc, overlap_start, overlap_end)?;
114                }
115            }
116        }
117        Ok(())
118    }
119}
120
121/// The first locus at or after `cursor` that a value starting at
122/// (`chr`, `start`), or any value after it, could still overlap.
123#[inline]
124fn advance_cursor(
125    locs: &[IndexedLoc],
126    mut cursor: usize,
127    end: usize,
128    chr: u32,
129    start: i64,
130) -> usize {
131    while cursor < end {
132        let loc = &locs[cursor];
133        let loc_chr = loc.chr_index as u32;
134        if loc_chr > chr {
135            break;
136        }
137        if loc_chr == chr && loc.binned_end > start {
138            break;
139        }
140        cursor += 1;
141    }
142    cursor
143}
144
145/// Values into a flat `(loci × bins)` buffer. The `read_values` kernel.
146pub(crate) fn values(
147    ex: &Extraction<'_>,
148    executor: &crate::parallel::Executor,
149    bin_mode: BinMode,
150    def_value: f32,
151) -> Result<Vec<f32>> {
152    let bin_count = ex.locs.bin_count;
153    let per_batch = executor.map_batches(ex.batches, |_, batch| {
154        let mut stats = vec![BinStats::default(); batch.len() * bin_count];
155        ex.walk_batch(*batch, |interval, index, loc, from, to| {
156            let base = (index - batch.start) * bin_count;
157            let bin_start = loc.bin_at(from);
158            let bin_end = loc.bin_after(to);
159            // Weighted by the bases of the bin the interval covers, not by the
160            // interval itself: a record stands for a range, and at a zoom level
161            // that range is a whole window. Counting it once per bin makes
162            // `sum` and `count` track how the file cut its records. The two
163            // agree where a record covers one base.
164            for b in bin_start..bin_end {
165                if b as usize >= bin_count {
166                    break;
167                }
168                let covered = loc.bin_coverage(b, from, to);
169                if covered <= 0.0 {
170                    continue;
171                }
172                stats[base + b as usize].add(interval.value, covered);
173            }
174            Ok(())
175        })?;
176        Ok(stats)
177    })?;
178
179    let mut output = vec![def_value; ex.locs.output_len];
180    for (batch, stats) in ex.batches.iter().zip(&per_batch) {
181        for (offset, index) in (batch.start..batch.end).enumerate() {
182            let loc = &ex.locs.locs[index];
183            for b in 0..bin_count {
184                let s = &stats[offset * bin_count + b];
185                if s.count <= 0.0 {
186                    continue;
187                }
188                output[loc.output_start + b] = s.apply(bin_mode);
189            }
190        }
191    }
192    ex.locs.reverse_output_rows(&mut output);
193    Ok(output)
194}
195
196/// One [`ValueStats`] per locus, for `quantify`.
197pub(crate) fn values_stats(
198    ex: &Extraction<'_>,
199    executor: &crate::parallel::Executor,
200) -> Result<Vec<ValueStats>> {
201    let bin_count = ex.locs.bin_count;
202    let per_batch = executor.map_batches(ex.batches, |_, batch| {
203        let mut stats = vec![ValueStats::default(); batch.len()];
204        ex.walk_batch(*batch, |interval, index, _loc, from, to| {
205            let overlap = to - from;
206            // Weighted by the bases the interval has data for, not by the bases
207            // it spans: a zoom record summarises a fixed window that may be
208            // mostly empty, so weighting by the span would grow every sum and
209            // count with the zoom level. The identity at full resolution.
210            let span = interval.end - interval.start;
211            let covered = if span > 0 && interval.valid_count != span {
212                (interval.valid_count as f64 * overlap as f64 / span as f64).round() as i64
213            } else {
214                overlap
215            };
216            // The record's own sum of squares, prorated over the part this
217            // window takes. Squaring the mean would keep only the variance
218            // *between* windows and drop the variance inside each, collapsing
219            // `sd` as the zoom level rises.
220            let fraction = if interval.valid_count > 0 {
221                covered as f64 / interval.valid_count as f64
222            } else {
223                0.0
224            };
225            stats[index - batch.start].add_aggregate(
226                interval.min_value,
227                interval.max_value,
228                interval.value as f64 * covered as f64,
229                interval.sum_squared * fraction,
230                covered,
231            );
232            Ok(())
233        })?;
234        Ok(stats)
235    })?;
236
237    let mut output = vec![ValueStats::default(); ex.locs.locs.len()];
238    for (batch, stats) in ex.batches.iter().zip(&per_batch) {
239        for (offset, index) in (batch.start..batch.end).enumerate() {
240            output[ex.locs.locs[index].row(bin_count)] = stats[offset];
241        }
242    }
243    Ok(output)
244}
245
246/// The bin of a locus a profile walk is currently filling.
247#[derive(Debug, Clone, Copy)]
248struct OpenBin {
249    /// -1 before the first value.
250    bin: i64,
251    stats: BinStats,
252    /// The locus is read from its end, so the bin lands mirrored in the
253    /// profile. Carried here rather than looked up per value: the walk holds
254    /// one of these per locus of its batch for as long as it lasts.
255    reverse: bool,
256}
257
258/// One [`ValueStats`] per bin, folded across loci, for `profile`.
259///
260/// The one kernel whose workers all contribute to every bin, so each keeps a
261/// private `bin_count` row and they are merged at the end **in batch order**.
262/// That merge order is behaviour: it fixes the `f32` accumulation and so the
263/// last bits of the result.
264///
265/// A bin is folded into its column as soon as the walk leaves it, so a locus
266/// never holds more than the bin it is filling. The walk only moves forward, so
267/// bins fill left to right and none is ever reopened — holding them all would
268/// be the whole (locus × bin) matrix.
269pub(crate) fn values_profile(
270    ex: &Extraction<'_>,
271    executor: &crate::parallel::Executor,
272    bin_mode: BinMode,
273) -> Result<Vec<ValueStats>> {
274    let bin_count = ex.locs.bin_count;
275    let per_batch = executor.map_batches(ex.batches, |_, batch| {
276        let mut column = vec![ValueStats::default(); bin_count];
277        let mut open: Vec<OpenBin> = (batch.start..batch.end)
278            .map(|i| OpenBin {
279                bin: -1,
280                stats: BinStats::default(),
281                reverse: ex.locs.locs[i].reverse,
282            })
283            .collect();
284
285        let close = |open: &mut OpenBin, column: &mut Vec<ValueStats>| {
286            if open.stats.count <= 0.0 {
287                return;
288            }
289            let value = open.stats.apply(bin_mode);
290            // A locus read from its end has its bins mirrored into the profile.
291            // Only here, and not in `open.bin` itself, which the walk compares
292            // against the bin it is filling.
293            let col = if open.reverse {
294                bin_count as i64 - 1 - open.bin
295            } else {
296                open.bin
297            };
298            if col >= 0 && (col as usize) < column.len() {
299                // One bin's value, folded in at full width. Squaring in `f32`
300                // and widening afterwards throws away half the mantissa of the
301                // square before the sum ever sees it, and gives a profile of
302                // constant data a standard deviation of 1.13 where the answer
303                // is 0.
304                column[col as usize].add(value);
305            }
306            open.stats = BinStats::default();
307        };
308
309        ex.walk_batch(*batch, |interval, index, loc, from, to| {
310            let bin_start = loc.bin_at(from);
311            let bin_end = loc.bin_after(to);
312            let slot = &mut open[index - batch.start];
313            // Weighted by covered bases, as `values` weights its own — a
314            // profile is read_values by column, so the two have to bin alike.
315            for b in bin_start..bin_end {
316                if b as usize >= bin_count {
317                    break;
318                }
319                let covered = loc.bin_coverage(b, from, to);
320                if covered <= 0.0 {
321                    continue;
322                }
323                if b != slot.bin {
324                    close(slot, &mut column);
325                    slot.bin = b;
326                }
327                slot.stats.add(interval.value, covered);
328            }
329            Ok(())
330        })?;
331        for slot in &mut open {
332            close(slot, &mut column);
333        }
334        Ok(column)
335    })?;
336
337    let mut output = vec![ValueStats::default(); bin_count];
338    for column in &per_batch {
339        for (col, batch_stats) in column.iter().enumerate() {
340            if batch_stats.count == 0 {
341                continue;
342            }
343            output[col].merge(batch_stats);
344        }
345    }
346    Ok(output)
347}
348
349/// Walk every (leaf, loci) pair of one batch's bed blocks.
350///
351/// The bed twin of [`Extraction::walk_batch`]: same R-tree descent, same
352/// cursor, same four overlap tests, but the block decodes into entries rather
353/// than intervals. `visit` is handed the entry and the index of every locus it
354/// overlaps.
355impl Extraction<'_> {
356    pub(crate) fn walk_bed_batch(
357        &self,
358        batch: LocBatch,
359        auto_sql: &indexmap::IndexMap<String, String>,
360        col_count: usize,
361        mut visit: impl FnMut(&mut BedRecord, &[usize]) -> Result<()>,
362    ) -> Result<()> {
363        let locs = &self.locs.locs;
364        let leaves = LeafWalk::new(self.source, self.tree_root, locs, batch, self.tracker)?;
365        let mut matched: Vec<usize> = Vec::new();
366        for leaf in leaves {
367            let (leaf, loc_range) = leaf?;
368            let block = self.read_leaf(leaf.offset, leaf.size)?;
369            let records = super::block::BedRecords::new(
370                block,
371                auto_sql,
372                col_count,
373                locs,
374                loc_range.clone(),
375                self.source.path(),
376            )?;
377            let mut cursor = loc_range.start;
378            for record in records {
379                let (chr_index, start, end, fields) = record?;
380                cursor = advance_cursor(locs, cursor, loc_range.end, chr_index, start);
381                matched.clear();
382                // A zero-length entry occupies the single base it names. See
383                // `BedRecords::next`.
384                let reach = end.max(start + 1);
385                // Indexed rather than enumerated: `visit` is handed the locus's
386                // index in the whole request, which is what tells a kernel where
387                // in its output the locus belongs.
388                #[allow(clippy::needless_range_loop)]
389                for index in cursor..loc_range.end {
390                    let loc = &locs[index];
391                    if chr_index != loc.chr_index as u32 {
392                        break;
393                    }
394                    if reach <= loc.binned_start {
395                        break;
396                    }
397                    if start >= loc.binned_end {
398                        continue;
399                    }
400                    matched.push(index);
401                }
402                if matched.is_empty() {
403                    continue;
404                }
405                let mut entry = BedRecord {
406                    chr_index,
407                    start,
408                    end,
409                    fields,
410                };
411                visit(&mut entry, &matched)?;
412            }
413        }
414        Ok(())
415    }
416}
417
418/// A bed record as the walk hands it over: coordinates plus whatever columns
419/// were asked for, before the chromosome index becomes a name.
420pub(crate) struct BedRecord {
421    pub chr_index: u32,
422    pub start: i64,
423    pub end: i64,
424    pub fields: Vec<(String, String)>,
425}
426
427/// bigBed entries per locus.
428///
429/// An entry belongs to every locus it overlaps, so two overlapping loci both
430/// report the entries they share. The fields are moved into the **last** locus
431/// that claims the entry and cloned into the rest, which is what keeps the
432/// common case — an entry in exactly one locus — free of a copy.
433pub(crate) fn entries(
434    ex: &Extraction<'_>,
435    executor: &crate::parallel::Executor,
436    auto_sql: &indexmap::IndexMap<String, String>,
437    chr_names: &[String],
438    col_count: usize,
439) -> Result<Vec<Vec<super::BedEntry>>> {
440    let bin_count = ex.locs.bin_count;
441    let per_batch = executor.map_batches(ex.batches, |_, batch| {
442        let mut out: Vec<Vec<super::BedEntry>> = vec![Vec::new(); batch.len()];
443        ex.walk_bed_batch(*batch, auto_sql, col_count, |entry, matched| {
444            let chr = chr_names
445                .get(entry.chr_index as usize)
446                .cloned()
447                .unwrap_or_default();
448            for (n, index) in matched.iter().enumerate() {
449                let fields = if n + 1 == matched.len() {
450                    std::mem::take(&mut entry.fields)
451                } else {
452                    entry.fields.clone()
453                };
454                out[index - batch.start].push(super::BedEntry {
455                    chr: chr.clone(),
456                    start: entry.start,
457                    end: entry.end,
458                    fields,
459                });
460            }
461            Ok(())
462        })?;
463        Ok(out)
464    })?;
465
466    let mut output: Vec<Vec<super::BedEntry>> = vec![Vec::new(); ex.locs.locs.len()];
467    for (batch, lists) in ex.batches.iter().zip(per_batch) {
468        for (offset, list) in lists.into_iter().enumerate() {
469            output[ex.locs.locs[batch.start + offset].row(bin_count)] = list;
470        }
471    }
472    // A locus reads its entries in block order, and a block straddling two loci
473    // hands them over per block rather than per position, so the list has to be
474    // put back in coordinate order.
475    for entries in &mut output {
476        entries.sort_by(|a, b| (&a.chr, a.start, a.end).cmp(&(&b.chr, b.start, b.end)));
477    }
478    Ok(output)
479}
480
481/// Depth of coverage of a bigBed's entries, binned — what a bigBed means by a
482/// "value".
483///
484/// The depth over the bin, not the number of entries touching it: an entry
485/// covering a third of a bin raises its depth by a third, so a bin holds the
486/// mean depth over the bases it spans whatever its width. The same at the
487/// default bin size of one.
488pub(crate) fn entries_pileup(
489    ex: &Extraction<'_>,
490    executor: &crate::parallel::Executor,
491    auto_sql: &indexmap::IndexMap<String, String>,
492    def_value: f32,
493) -> Result<Vec<f32>> {
494    let bin_count = ex.locs.bin_count;
495    let per_batch = executor.map_batches(ex.batches, |_, batch| {
496        let mut depth = vec![0.0f32; batch.len() * bin_count];
497        // A pileup counts coverage, so it asks for the 3 coordinate columns
498        // alone and never pays for the fields it would drop.
499        ex.walk_bed_batch(*batch, auto_sql, 3, |entry, matched| {
500            for index in matched {
501                let loc = &ex.locs.locs[*index];
502                if loc.binned_end <= loc.binned_start {
503                    continue;
504                }
505                let from = entry.start.max(loc.binned_start);
506                let to = entry.end.min(loc.binned_end);
507                let base = (index - batch.start) * bin_count;
508                for b in loc.bin_at(from)..loc.bin_after(to) {
509                    if b as usize >= bin_count {
510                        break;
511                    }
512                    let fraction = loc.bin_fraction(b, from, to);
513                    if fraction <= 0.0 {
514                        continue;
515                    }
516                    depth[base + b as usize] += fraction as f32;
517                }
518            }
519            Ok(())
520        })?;
521        Ok(depth)
522    })?;
523
524    let mut output = vec![0.0f32; ex.locs.output_len];
525    for (batch, depth) in ex.batches.iter().zip(&per_batch) {
526        for (offset, index) in (batch.start..batch.end).enumerate() {
527            let loc = &ex.locs.locs[index];
528            output[loc.output_start..loc.output_end]
529                .copy_from_slice(&depth[offset * bin_count..(offset + 1) * bin_count]);
530        }
531    }
532    // A pileup of 0 is exactly a bin no entry reached, which is what def_value
533    // stands for.
534    if def_value != 0.0 {
535        for value in &mut output {
536            if *value == 0.0 {
537                *value = def_value;
538            }
539        }
540    }
541    // Mirrored here rather than at each of the three callers: the profile reads
542    // this by column and the quantification by row, and both are right once the
543    // rows themselves are the way they were asked for.
544    ex.locs.reverse_output_rows(&mut output);
545    Ok(output)
546}
547
548/// The pileup reduced to one [`ValueStats`] per locus, for `quantify`.
549///
550/// Over the bins the request asked for, not one per locus. Pinning `bin_count`
551/// to 1 would pile every entry of a locus into a single bin and leave the
552/// reduction nothing to run over — mean, sum, min and max would all come back
553/// as the number of overlapping entries.
554///
555/// A bin no entry reached holds `def_value`, put there by the pileup, so every
556/// bin counts and the reduction sees the numbers `read_values` would show. A
557/// NaN `def_value` is how a caller asks for the uncovered bins to be left out,
558/// and they are skipped rather than poisoning every statistic of the locus.
559pub(crate) fn pileup_stats(locs: &IndexedLocs, pileup: &[f32]) -> Vec<ValueStats> {
560    let mut output = vec![ValueStats::default(); locs.locs.len()];
561    for loc in &locs.locs {
562        let stats = &mut output[loc.row(locs.bin_count)];
563        for value in &pileup[loc.output_start..loc.output_end] {
564            if value.is_nan() {
565                continue;
566            }
567            stats.add(*value);
568        }
569    }
570    output
571}
572
573/// The pileup reduced to one [`ValueStats`] per bin, for `profile`.
574///
575/// Every locus holds a value in every bin — one no entry reached piles up to
576/// `def_value` — so every one of them counts, and dividing the sum by anything
577/// less would inflate it. A NaN `def_value` is again how a caller asks for the
578/// loci whose bin held nothing to be left out.
579pub(crate) fn pileup_profile(locs: &IndexedLocs, pileup: &[f32]) -> Vec<ValueStats> {
580    let mut output = vec![ValueStats::default(); locs.bin_count];
581    for (col, stats) in output.iter_mut().enumerate() {
582        for loc in &locs.locs {
583            let value = pileup[loc.output_start + col];
584            if value.is_nan() {
585                continue;
586            }
587            stats.add(value);
588        }
589    }
590    output
591}
592
593// ---------------------------------------------------------------------------
594// Whole-file iterators
595// ---------------------------------------------------------------------------
596//
597// Plain `Iterator`s, exhausted after one pass, exposing their windows' regions
598// up front. A window never spans two chromosomes and no bin straddles a window
599// boundary, so concatenating a chromosome's windows gives exactly what a
600// whole-chromosome read gives at the same bin size.
601
602/// Region of one window: chromosome, start, end.
603pub type WindowLoc = (String, i64, i64);
604
605/// Bytes a piece has to stand to read before splitting a window is worth it.
606///
607/// A piece costs a thread and an index descent, which one holding next to
608/// nothing never earns back: on a sparse file, where a megabase window is a few
609/// hundred bytes, a walk split 24 ways measures several times slower than one
610/// read window by window.
611const MIN_PIECE_DATA_SIZE: f64 = 16384.0;
612
613/// What both whole-file walks share: the windows, where the walk has got to,
614/// and how finely a window is worth splitting.
615///
616/// `locs` is behind an [`Arc`](std::sync::Arc) so that a walk can be restarted
617/// without rebuilding it — see [`Walk::restarted`]. It is the only field with
618/// a heap allocation, and for a whole-genome walk it is a few thousand
619/// entries, so sharing it makes a restart cost a refcount bump instead of a
620/// copy.
621#[derive(Debug)]
622struct Walk {
623    locs: std::sync::Arc<Vec<WindowLoc>>,
624    next: usize,
625    parallel: usize,
626    /// Bytes of the file's data one base pair of the genome holds on average,
627    /// which is what a window's share of the file is estimated from.
628    bytes_per_bp: f64,
629    total_coverage: u64,
630    done_coverage: u64,
631}
632
633impl Walk {
634    fn new(locs: Vec<WindowLoc>, parallel: usize, data_size: u64, genome_size: i64) -> Self {
635        let total_coverage = locs.iter().map(|(_, s, e)| (e - s).max(0) as u64).sum();
636        let bytes_per_bp = if genome_size < 1 || data_size < 1 {
637            0.0
638        } else {
639            data_size as f64 / genome_size as f64
640        };
641        Self {
642            locs: std::sync::Arc::new(locs),
643            next: 0,
644            parallel: parallel.max(1),
645            bytes_per_bp,
646            total_coverage,
647            done_coverage: 0,
648        }
649    }
650
651    /// The same plan, back at the first window.
652    ///
653    /// Shares `locs` rather than copying it, so this is a refcount bump and
654    /// two integers. What resets is the cursor and the progress tally: a
655    /// second pass reports its own progress from zero, which is what a caller
656    /// watching it expects.
657    fn restarted(&self) -> Self {
658        Self {
659            locs: self.locs.clone(),
660            next: 0,
661            parallel: self.parallel,
662            bytes_per_bp: self.bytes_per_bp,
663            total_coverage: self.total_coverage,
664            done_coverage: 0,
665        }
666    }
667
668    /// Split a window into the pieces the threads share out.
669    ///
670    /// `units` is what the window is measured in and what the pieces divide:
671    /// bins for a values walk, base pairs for an entries walk. Returns how many
672    /// units a piece holds and how many pieces that takes.
673    ///
674    /// A piece is never empty of units, so a window with fewer units than
675    /// pieces comes back as fewer pieces rather than as empty ones. The last
676    /// piece is the one that may reach past the window — the units rarely
677    /// divide evenly — and the caller drops what it read out there.
678    fn split(&self, units: i64, coverage: i64) -> (i64, i64) {
679        let window_data = self.bytes_per_bp * coverage as f64;
680        let worth = (window_data / MIN_PIECE_DATA_SIZE) as i64;
681        let pieces = worth.min(self.parallel as i64).max(1);
682        let piece_units = ((units + pieces - 1) / pieces).max(1);
683        (piece_units, (units + piece_units - 1) / piece_units)
684    }
685
686    /// Report the window the walk stands on and move past it.
687    ///
688    /// Reported as a window is handed over rather than as one is read, so that
689    /// progress counts what the caller has seen.
690    fn take(&mut self, progress: Option<&crate::progress::ProgressFn>) -> usize {
691        let index = self.next;
692        self.next += 1;
693        let (_, start, end) = &self.locs[index];
694        self.done_coverage += (end - start).max(0) as u64;
695        if let Some(report) = progress {
696            report(self.done_coverage, self.total_coverage);
697        }
698        index
699    }
700
701    /// The final report, once the last window has been handed over — or
702    /// straight away for a walk over no window at all.
703    fn finish(&mut self, progress: Option<&crate::progress::ProgressFn>) {
704        if let Some(report) = progress {
705            if self.done_coverage < self.total_coverage {
706                self.done_coverage = self.total_coverage;
707                report(self.total_coverage, self.total_coverage);
708            }
709        }
710    }
711}
712
713/// Successive windows of values over whole chromosomes.
714///
715/// Exhausted after one pass. `locs()` gives the region of each window up
716/// front, so `iter.locs().to_vec()` before the walk is how both are had at
717/// once.
718pub struct ValuesWalk {
719    walk: Walk,
720    bin_size: i64,
721    /// Bins each window hands back, which for the last window of a chromosome
722    /// is fewer than a full window holds. Shared with any restart of this
723    /// walk, as `Walk::locs` is.
724    bins: std::sync::Arc<Vec<i64>>,
725    bin_mode: BinMode,
726    def_value: f32,
727    zoom: super::Zoom,
728    progress: Option<crate::progress::ProgressFn>,
729}
730
731/// Hand-written: a progress callback is a boxed closure and cannot be `Debug`.
732impl std::fmt::Debug for ValuesWalk {
733    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734        f.debug_struct("ValuesWalk")
735            .field("windows", &self.walk.locs.len())
736            .field("next", &self.walk.next)
737            .field("bin_size", &self.bin_size)
738            .finish()
739    }
740}
741
742impl ValuesWalk {
743    /// The same walk, back at its first window.
744    ///
745    /// What makes the Python iterator re-iterable: `__iter__` hands back a
746    /// fresh cursor over the plan that is already built, so a second `for`
747    /// loop walks the file again instead of yielding nothing. Costs two
748    /// refcount bumps — the windows and the per-window bin counts are shared,
749    /// not copied — and re-reads the file, which is what a second pass is.
750    pub fn restarted(&self) -> Self {
751        Self {
752            walk: self.walk.restarted(),
753            bin_size: self.bin_size,
754            bins: self.bins.clone(),
755            bin_mode: self.bin_mode,
756            def_value: self.def_value,
757            zoom: self.zoom,
758            progress: self.progress.clone(),
759        }
760    }
761
762    /// Resolve the whole walk up front: the windows, the zoom level, and every
763    /// argument that could be refused.
764    ///
765    /// A request that cannot be read says so when it is made rather than part
766    /// way through the iteration.
767    pub fn plan(reader: &super::BbiReader, req: &super::ValuesRequest, span: i64) -> Result<Self> {
768        if span < 1 {
769            return Err(Error::invalid(format!(
770                "span must be positive (got {span})"
771            )));
772        }
773        // A walk lays its windows out on the grid the whole genome shares, so
774        // the bin size has to be a whole number of base pairs — which
775        // `BinPlan::new` now requires of every request, not only of this one.
776        let bin_size =
777            crate::genomic::BinPlan::new(req.common.bin_size, None, req.common.full_bin)?
778                .whole_bin_size();
779        if reader.kind().is_bigbed() && !matches!(req.common.zoom, super::Zoom::Full) {
780            return Err(Error::invalid("zoom is only supported for bigwig files"));
781        }
782        // Resolved here rather than at the first step, so a request that cannot
783        // be read says so when it is made instead of part way through the walk.
784        let level = reader.select_zoom(req.common.bin_size, req.common.zoom)?;
785
786        // Counted in bins rather than base pairs, so no bin straddles a window
787        // boundary; `span` is rounded up to a whole number of them.
788        let bins_per_window = ((span + bin_size - 1) / bin_size).max(1);
789        let mut locs = Vec::new();
790        let mut bins = Vec::new();
791        for chr in reader.chr_sizes().select(&req.common.locs.chr_ids)? {
792            // The bins of the chromosome, which the windows share out. Its last
793            // is partial, and only `full_bin` keeps it — as it is only
794            // `full_bin` that keeps it in a read of the whole chromosome.
795            let chr_bins = if req.common.full_bin {
796                (chr.size + bin_size - 1) / bin_size
797            } else {
798                chr.size / bin_size
799            };
800            let mut bin = 0;
801            while bin < chr_bins {
802                let start = bin * bin_size;
803                let window_bins = bins_per_window.min(chr_bins - bin);
804                locs.push((
805                    chr.id.clone(),
806                    start,
807                    (start + window_bins * bin_size).min(chr.size),
808                ));
809                bins.push(window_bins);
810                bin += bins_per_window;
811            }
812        }
813
814        let walk = Walk::new(
815            locs,
816            reader.parallel(),
817            reader.data_size(level),
818            reader.genome_size(),
819        );
820        Ok(Self {
821            walk,
822            bin_size,
823            bins: std::sync::Arc::new(bins),
824            bin_mode: req.bin_mode,
825            def_value: req.common.def_value,
826            zoom: req.common.zoom,
827            progress: req.common.progress.clone(),
828        })
829    }
830
831    /// Number of windows, so `len(iterator)` works on the Python side.
832    pub fn len(&self) -> usize {
833        self.walk.locs.len()
834    }
835
836    /// The whole number of base pairs each bin covers.
837    ///
838    /// Resolved at `plan` from the request's `f64`, so a caller laying window
839    /// values out on coordinates — as the exporters do — reads the grid the
840    /// walk actually used rather than rounding the request a second time.
841    pub fn bin_size(&self) -> i64 {
842        self.bin_size
843    }
844
845    pub fn is_empty(&self) -> bool {
846        self.walk.locs.is_empty()
847    }
848
849    /// The region of each window: the nth array covers `locs()[n]`.
850    pub fn locs(&self) -> &[WindowLoc] {
851        &self.walk.locs
852    }
853
854    /// Read one window as pieces of itself, laid end to end.
855    fn read(&self, reader: &super::BbiReader, index: usize) -> Result<Vec<f32>> {
856        let (chr, start, end) = &self.walk.locs[index];
857        let (piece_bins, piece_count) = self.walk.split(self.bins[index], end - start);
858        let piece_span = piece_bins * self.bin_size;
859
860        let chr_ids = vec![chr.clone(); piece_count as usize];
861        let starts: Vec<i64> = (0..piece_count).map(|i| start + i * piece_span).collect();
862        let ends: Vec<i64> = starts.iter().map(|s| s + piece_span).collect();
863
864        // Every piece is read at the same width, which is what settles the read
865        // on `piece_bins` bins for all of them and lays the window out as one
866        // buffer. `full_bin` is false whatever the walk was asked for: the
867        // pieces are on the grid already — it is the windows themselves that a
868        // full_bin walk lays out one bin further.
869        //
870        // Progress is not passed down: it is reported per window handed over, so
871        // a step never reports a window the caller has not seen.
872        let request =
873            super::ValuesRequest::new(crate::genomic::Locs::spans(&chr_ids, &starts, &ends)?)
874                .bin_size(self.bin_size as f64)
875                .bin_count(piece_bins as usize)
876                .bin_mode(self.bin_mode)
877                .def_value(self.def_value)
878                .zoom(self.zoom);
879        let values = reader.read_values(&request)?;
880        Ok(values.into_raw_vec_and_offset().0)
881    }
882
883    /// The next window, or `None` once the walk is spent.
884    ///
885    /// Takes the reader on every step rather than holding it: a `#[pyclass]`
886    /// cannot carry a borrow, and the Python iterator drives exactly this.
887    pub fn next_window(
888        &mut self,
889        reader: &super::BbiReader,
890    ) -> Option<Result<ndarray::Array1<f32>>> {
891        if self.walk.next >= self.walk.locs.len() {
892            self.walk.finish(self.progress.as_ref());
893            return None;
894        }
895        // Read before the window is taken; `take` is what moves the walk on.
896        let index = self.walk.next;
897        let mut values = match self.read(reader, index) {
898            Ok(v) => v,
899            // A failed step is not a step at all: the walk stays where it was,
900            // so the call after it comes back here and raises the same error
901            // rather than reporting the walk spent. That is what makes closing
902            // a reader under a walk raise every time instead of once and then
903            // stopping, as `LocusWalk` does and as the "Non-obvious
904            // constraints" section of ARCHITECTURE requires.
905            Err(e) => return Some(Err(e)),
906        };
907        self.walk.take(self.progress.as_ref());
908        // The pieces rarely divide the window evenly, so the last one may reach
909        // past it; an array standing for more than the window it names is not
910        // one to hand over.
911        values.truncate(self.bins[index] as usize);
912        Some(Ok(ndarray::Array1::from_vec(values)))
913    }
914}
915
916/// [`ValuesWalk`] as a plain [`Iterator`], for Rust callers.
917///
918/// Exhausted after one pass.
919#[derive(Debug)]
920pub struct ValuesWindows<'a> {
921    reader: &'a super::BbiReader,
922    walk: ValuesWalk,
923}
924
925impl<'a> ValuesWindows<'a> {
926    pub(crate) fn plan(
927        reader: &'a super::BbiReader,
928        req: &super::ValuesRequest,
929        span: i64,
930    ) -> Result<Self> {
931        Ok(Self {
932            reader,
933            walk: ValuesWalk::plan(reader, req, span)?,
934        })
935    }
936
937    pub fn len(&self) -> usize {
938        self.walk.len()
939    }
940    pub fn is_empty(&self) -> bool {
941        self.walk.is_empty()
942    }
943    /// The region of each window: the nth array covers `locs()[n]`.
944    pub fn locs(&self) -> &[WindowLoc] {
945        self.walk.locs()
946    }
947    /// The whole number of base pairs each bin covers. See
948    /// [`ValuesWalk::bin_size`].
949    pub fn bin_size(&self) -> i64 {
950        self.walk.bin_size()
951    }
952}
953
954impl Iterator for ValuesWindows<'_> {
955    type Item = Result<ndarray::Array1<f32>>;
956    fn next(&mut self) -> Option<Self::Item> {
957        self.walk.next_window(self.reader)
958    }
959}
960
961/// Successive windows of bed entries over whole chromosomes.
962pub struct EntryWalk {
963    walk: Walk,
964    col_count: usize,
965    progress: Option<crate::progress::ProgressFn>,
966}
967
968/// Hand-written: a progress callback is a boxed closure and cannot be `Debug`.
969impl std::fmt::Debug for EntryWalk {
970    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
971        f.debug_struct("EntryWalk")
972            .field("windows", &self.walk.locs.len())
973            .field("next", &self.walk.next)
974            .field("col_count", &self.col_count)
975            .finish()
976    }
977}
978
979impl EntryWalk {
980    /// The same walk, back at its first window. See
981    /// [`ValuesWalk::restarted`].
982    pub fn restarted(&self) -> Self {
983        Self {
984            walk: self.walk.restarted(),
985            col_count: self.col_count,
986            progress: self.progress.clone(),
987        }
988    }
989
990    pub fn plan(reader: &super::BbiReader, req: &super::EntriesRequest, span: i64) -> Result<Self> {
991        if !reader.kind().is_bigbed() {
992            return Err(Error::invalid("iter_all_entries only for bigbed"));
993        }
994        if span < 1 {
995            return Err(Error::invalid(format!(
996                "span must be positive (got {span})"
997            )));
998        }
999        reader.check_col_count(req.col_count, 3)?;
1000
1001        let mut locs = Vec::new();
1002        for chr in reader.chr_sizes().select(&req.common.locs.chr_ids)? {
1003            let mut start = 0;
1004            while start < chr.size {
1005                locs.push((chr.id.clone(), start, (start + span).min(chr.size)));
1006                start += span;
1007            }
1008        }
1009        // No reordering: the windows are built in chromosome then coordinate
1010        // order already, which is the order to read them in.
1011        let walk = Walk::new(
1012            locs,
1013            reader.parallel(),
1014            reader.data_size(None),
1015            reader.genome_size(),
1016        );
1017        Ok(Self {
1018            walk,
1019            col_count: req.col_count,
1020            progress: req.common.progress.clone(),
1021        })
1022    }
1023
1024    pub fn len(&self) -> usize {
1025        self.walk.locs.len()
1026    }
1027
1028    pub fn is_empty(&self) -> bool {
1029        self.walk.locs.is_empty()
1030    }
1031
1032    pub fn locs(&self) -> &[WindowLoc] {
1033        &self.walk.locs
1034    }
1035
1036    fn read(&self, reader: &super::BbiReader, index: usize) -> Result<Vec<super::BedEntry>> {
1037        let (chr, start, end) = &self.walk.locs[index];
1038        let coverage = end - start;
1039        let (piece_span, piece_count) = self.walk.split(coverage, coverage);
1040
1041        let chr_ids = vec![chr.clone(); piece_count as usize];
1042        let mut starts = Vec::with_capacity(piece_count as usize);
1043        let mut ends = Vec::with_capacity(piece_count as usize);
1044        let mut piece_min_starts = Vec::with_capacity(piece_count as usize);
1045        for i in 0..piece_count {
1046            let piece_start = start + i * piece_span;
1047            piece_min_starts.push(piece_start);
1048            // Read from a base before the piece, so an entry covering no base —
1049            // a zero-length one at the piece's very first base — is not missed
1050            // by a test that asks it to overlap. The piece still *reports* from
1051            // its own start, which is what keeps an entry reaching over a
1052            // boundary to the one piece it starts in.
1053            starts.push((piece_start - 1).max(0));
1054            ends.push((piece_start + piece_span).min(*end));
1055        }
1056
1057        let request =
1058            super::EntriesRequest::new(crate::genomic::Locs::spans(&chr_ids, &starts, &ends)?)
1059                .col_count(self.col_count);
1060        let pieces = reader.read_entries(&request)?;
1061
1062        let mut out = Vec::new();
1063        for (piece, min_start) in pieces.into_iter().zip(piece_min_starts) {
1064            out.extend(piece.into_iter().filter(|e| e.start >= min_start));
1065        }
1066        Ok(out)
1067    }
1068
1069    /// The next window, or `None` once the walk is spent.
1070    pub fn next_window(
1071        &mut self,
1072        reader: &super::BbiReader,
1073    ) -> Option<Result<Vec<super::BedEntry>>> {
1074        if self.walk.next >= self.walk.locs.len() {
1075            self.walk.finish(self.progress.as_ref());
1076            return None;
1077        }
1078        let index = self.walk.next;
1079        let entries = match self.read(reader, index) {
1080            Ok(e) => e,
1081            // Left where it was on failure. See `ValuesWalk::next_window`.
1082            Err(e) => return Some(Err(e)),
1083        };
1084        self.walk.take(self.progress.as_ref());
1085        Some(Ok(entries))
1086    }
1087}
1088
1089/// [`EntryWalk`] as a plain [`Iterator`], for Rust callers.
1090#[derive(Debug)]
1091pub struct EntryWindows<'a> {
1092    reader: &'a super::BbiReader,
1093    walk: EntryWalk,
1094}
1095
1096impl<'a> EntryWindows<'a> {
1097    pub(crate) fn plan(
1098        reader: &'a super::BbiReader,
1099        req: &super::EntriesRequest,
1100        span: i64,
1101    ) -> Result<Self> {
1102        Ok(Self {
1103            reader,
1104            walk: EntryWalk::plan(reader, req, span)?,
1105        })
1106    }
1107
1108    pub fn len(&self) -> usize {
1109        self.walk.len()
1110    }
1111    pub fn is_empty(&self) -> bool {
1112        self.walk.is_empty()
1113    }
1114    pub fn locs(&self) -> &[WindowLoc] {
1115        self.walk.locs()
1116    }
1117}
1118
1119impl Iterator for EntryWindows<'_> {
1120    type Item = Result<Vec<super::BedEntry>>;
1121    fn next(&mut self) -> Option<Self::Item> {
1122        self.walk.next_window(self.reader)
1123    }
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128    use super::*;
1129
1130    fn loc(chr: usize, start: i64, end: i64) -> IndexedLoc {
1131        IndexedLoc {
1132            chr_index: chr,
1133            start,
1134            end,
1135            binned_start: start,
1136            binned_end: end,
1137            bin_size: 1.0,
1138            reverse: false,
1139            output_start: 0,
1140            output_end: 1,
1141        }
1142    }
1143
1144    #[test]
1145    fn the_cursor_skips_loci_the_block_has_passed() {
1146        let locs = [loc(0, 0, 10), loc(0, 20, 30), loc(0, 40, 50)];
1147        // A value at 25 cannot reach the first locus.
1148        assert_eq!(advance_cursor(&locs, 0, 3, 0, 25), 1);
1149        // One at 45 cannot reach the first two.
1150        assert_eq!(advance_cursor(&locs, 0, 3, 0, 45), 2);
1151        // One at 5 reaches everything from the start.
1152        assert_eq!(advance_cursor(&locs, 0, 3, 0, 5), 0);
1153    }
1154
1155    #[test]
1156    fn the_cursor_stops_at_a_higher_chromosome() {
1157        let locs = [loc(0, 0, 10), loc(1, 0, 10), loc(2, 0, 10)];
1158        // A value on chromosome 1 passes the chromosome-0 locus and stops.
1159        assert_eq!(advance_cursor(&locs, 0, 3, 1, 5), 1);
1160        // A value on chromosome 0 never advances past its own.
1161        assert_eq!(advance_cursor(&locs, 0, 3, 0, 5), 0);
1162    }
1163
1164    #[test]
1165    fn the_cursor_never_goes_backwards() {
1166        let locs = [loc(0, 0, 10), loc(0, 20, 30)];
1167        assert_eq!(advance_cursor(&locs, 1, 2, 0, 0), 1);
1168    }
1169}