Skip to main content

gwseq_io/bbi/
writer.rs

1//! The bigWig / bigBed writer.
2//!
3//! Three passes over one file:
4//!
5//! 1. **Data.** Values or entries accumulate into sections
6//!    ([`super::section`]); a full section is compressed on the pool and
7//!    appended, and its bounds recorded as an R-tree leaf item.
8//! 2. **Zoom.** Coarser levels are built by reading the data section back —
9//!    through the same handle, since [`LocalSink`] is open both ways.
10//! 3. **Index and headers.** The R-tree is written, then the reserved header
11//!    fields are patched with the offsets and counts now known.
12//!
13//! The file goes out in one forward pass with no temporary files. Every part of
14//! a bbi file is reached through an explicit offset in its 64-byte header, so
15//! the parts need not sit in the order Supp. Table 4 lists them in:
16//!
17//! ```text
18//! 0     bbiHeader          64 bytes, patched at close, magic stamped last
19//! 64    zoom header slots  24 * MAX_ZOOM_LEVELS, patched at close
20//! 304   autoSql            bigBed only, known at open
21//!       totalSummary       40 bytes, patched at close
22//!       dataCount          8 bytes, patched at close
23//!       data blocks        streamed as they fill
24//!       data R-tree
25//!       zoom levels        zoomCount, records, R-tree, per level
26//!       chromosome B+tree
27//!       trailing magic
28//! ```
29//!
30//! The chromosome tree comes last because it is the last thing known: sizes and
31//! ids are inferred from what was written. Until the magic is stamped the
32//! header reads as zeroes and every reader refuses the file, so an abandoned
33//! write cannot be mistaken for a short but valid one.
34//!
35//! Zoom levels are built by reading the data section back out of the file just
36//! written, each level after the first out of the level below it. That costs
37//! one inflate of what was already deflated and keeps the levels contiguous,
38//! with a reduction ladder chosen from the finished data rather than guessed
39//! from its first few thousand items.
40//!
41//! [`LocalSink`]: crate::source::LocalSink
42
43use std::collections::BinaryHeap;
44
45use indexmap::IndexMap;
46
47use crate::bbi::block::{read_wig_header, read_wig_item, WigEncoding};
48use crate::bbi::chr_tree::WriteEntry;
49use crate::bbi::header::{
50    build_auto_sql, to_bbi_f32, to_bbi_u32, write_header, write_total_summary, write_zoom_header,
51    BbiHeader, BbiKind, TotalSummary, ZoomHeader, BBI_HEADER_SIZE, BBI_OUTPUT_VERSION,
52    TOTAL_SUMMARY_SIZE, ZOOM_HEADER_SIZE,
53};
54use crate::bbi::rtree::{LeafItem, TREE_BLOCK_SIZE};
55use crate::bbi::section::{Accept, CostModel, SectionPolicy, WigSection, MAX_ITEMS_PER_SECTION};
56use crate::error::{Error, Result};
57use crate::genomic::ChrMap;
58use crate::parallel::{resolve_parallel, Executor, Promise};
59use crate::source::{ByteSink, ByteSource, LocalSink};
60
61/// Items a section or a block holds by default, matching UCSC's writers. Also
62/// the unit the R-tree indexes, since each of them is compressed and fetched on
63/// its own. A bed entry is bigger and more variable than a wig value, which is
64/// why fewer of them go in a block.
65pub const WIG_ITEMS_PER_SLOT: usize = 1024;
66pub const BED_ITEMS_PER_SLOT: usize = 512;
67
68/// zlib level for data and zoom blocks. A bbi file is written once and read
69/// many times, and 6 is what UCSC's writers use.
70pub const COMPRESSION_LEVEL: u32 = 6;
71
72/// Items whose spans are averaged to anchor the zoom reduction ladder. The
73/// ladder has to be fixed before the candidate levels can be counted, which is
74/// why it comes from a sample rather than from the whole file.
75const RESOLUTION_SAMPLE_ITEMS: u64 = 4096;
76
77/// The eight bytes reserved after the summary for the data count.
78const DATA_COUNT_SIZE: u64 = 8;
79
80/// Slots the header reserves for zoom levels.
81const MAX_ZOOM_LEVELS: usize = 10;
82/// The first level summarises about ten items, and each level after it four
83/// times as much.
84const INITIAL_ZOOM_FACTOR: i64 = 10;
85const ZOOM_INCREMENT: i64 = 4;
86/// Bytes one zoom record takes (Supp. Table 19).
87const ZOOM_RECORD_SIZE: u64 = 32;
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum FieldType {
91    String,
92    Int,
93    Uint,
94    Float,
95}
96
97impl FieldType {
98    pub fn as_str(self) -> &'static str {
99        match self {
100            FieldType::String => "string",
101            FieldType::Int => "int",
102            FieldType::Uint => "uint",
103            FieldType::Float => "float",
104        }
105    }
106}
107
108pub struct BbiWriterOptions {
109    pub kind: BbiKind,
110    /// Declared sizes. Every written coordinate is checked against them, and a
111    /// written id resolves against their keys the way a read one does. `None`
112    /// infers each chromosome's size from what is written.
113    pub chr_sizes: Option<ChrMap>,
114    /// bigBed only. The first three must be the coordinates.
115    pub fields: IndexMap<String, String>,
116    pub items_per_slot: Option<usize>,
117    pub block_size: u32,
118    pub compression_level: u32,
119    pub parallel: i64,
120    /// How a wig section decides between widening its encoding and closing.
121    /// Every setting writes a valid file; they differ in how big it is. See
122    /// [`SectionPolicy`] and `examples/section_policy.rs`.
123    pub section_policy: SectionPolicy,
124    /// The constants behind [`SectionPolicy::Cost`]. Exposed for the same
125    /// reason: so the model can be measured rather than argued about.
126    pub cost_model: CostModel,
127}
128
129impl Default for BbiWriterOptions {
130    fn default() -> Self {
131        Self {
132            kind: BbiKind::BigWig,
133            chr_sizes: None,
134            fields: IndexMap::new(),
135            items_per_slot: None,
136            block_size: TREE_BLOCK_SIZE,
137            compression_level: COMPRESSION_LEVEL,
138            parallel: -1,
139            section_policy: SectionPolicy::default(),
140            cost_model: CostModel::default(),
141        }
142    }
143}
144
145/// How many sections were written in each encoding. What to compare first
146/// when two writes of the same values differ in size — see `bbi/section.rs`,
147/// which is private and so not linked from here.
148#[derive(Debug, Clone, Copy, Default)]
149pub struct SectionCounts {
150    pub bedgraph: u64,
151    pub varstep: u64,
152    pub fixedstep: u64,
153}
154
155impl SectionCounts {
156    pub fn total(&self) -> u64 {
157        self.bedgraph + self.varstep + self.fixedstep
158    }
159
160    fn bump(&mut self, encoding: WigEncoding) {
161        match encoding {
162            WigEncoding::BedGraph => self.bedgraph += 1,
163            WigEncoding::VarStep => self.varstep += 1,
164            WigEncoding::FixedStep => self.fixedstep += 1,
165        }
166    }
167}
168
169/// A chromosome as the writer has seen it.
170#[derive(Debug, Clone, Copy)]
171struct ChrState {
172    index: u32,
173    size: i64,
174    declared: bool,
175}
176
177/// A block whose deflate is still running, and the index entry waiting on it.
178///
179/// Everything about the entry but the offset and the size is known before the
180/// block is compressed, so those two are all the commit has left to fill in.
181struct PendingBlock {
182    leaf: LeafItem,
183    /// A one-shot: the worker sends, the commit receives. `Ok` on the calling
184    /// thread when there is no pool.
185    result: PendingResult,
186    /// Section type of a wig block, `None` for a bed or zoom one.
187    encoding: Option<WigEncoding>,
188}
189
190enum PendingResult {
191    InFlight(Promise<Result<Vec<u8>>>),
192}
193
194impl PendingResult {
195    fn take(self) -> Result<Vec<u8>> {
196        match self {
197            // A worker that panicked drops its half without filling it; the
198            // panic itself has already been reported by rayon, and this turns
199            // the silence into an error of the call that is running.
200            PendingResult::InFlight(promise) => promise
201                .wait()
202                .unwrap_or_else(|| Err(Error::invalid("a block failed to compress"))),
203        }
204    }
205}
206
207pub struct BbiWriter {
208    path: String,
209    kind: BbiKind,
210    sink: Option<LocalSink>,
211    executor: Option<Executor>,
212    block_size: u32,
213    items_per_slot: usize,
214    compression_level: u32,
215
216    // Offsets of the reserved prefix, fixed once at open.
217    auto_sql_offset: u64,
218    total_summary_offset: u64,
219    full_data_offset: u64,
220    full_index_offset: u64,
221    chr_tree_offset: u64,
222
223    // bigBed only: the columns of an entry, and what the header says of them.
224    bed_fields: IndexMap<String, String>,
225    field_count: u16,
226    defined_field_count: u16,
227
228    declared_sizes: Option<ChrMap>,
229    chrs: IndexMap<String, ChrState>,
230    current_chr: Option<String>,
231    last_chr_id: String,
232    last_chr_index: Option<u32>,
233    last_chr_end: i64,
234    last_entry_start: i64,
235
236    section: WigSection,
237
238    // bigBed only: the block being filled, its bounds, and the sweep turning
239    // the entries into the coverage the summary and the zoom levels hold.
240    bed_block: Vec<u8>,
241    bed_block_count: usize,
242    bed_bounds: Option<(u32, u32, u32, u32)>,
243    coverage: CoverageSweep,
244
245    data_items: Vec<LeafItem>,
246    /// Where a placed block is indexed, and whether it counts as a section.
247    ///
248    /// `None` during the data pass: a block goes into `data_items` and bumps
249    /// the encoding counters. `Some` during a zoom pass: it goes into that
250    /// level's own items and counts towards nothing, a zoom block being an
251    /// index entry rather than a section. One slot is enough because the two
252    /// never overlap — the zoom pass runs inside `close()`, after the data
253    /// pipeline has drained.
254    zoom_items: Option<Vec<LeafItem>>,
255    pending: std::collections::VecDeque<PendingBlock>,
256    pending_limit: usize,
257    section_count: u64,
258    uncompress_buffer_size: u64,
259    data_body_size: u64,
260    section_counts: SectionCounts,
261
262    summary: TotalSummary,
263    item_count: u64,
264    entry_count: u64,
265    /// Values dropped for not being finite. A NaN or infinity leaves a gap,
266    /// which is what a bigWig means by a base carrying no data.
267    skipped_count: u64,
268    /// Values cut back to the end of a declared chromosome they hung over. A
269    /// value that *starts* at or past the end is an error instead — a span that
270    /// does not divide a chromosome is ordinary, a value off the end is not.
271    clipped_count: u64,
272
273    ladder_frozen: bool,
274    ladder_start_item_count: u64,
275    zoom_reductions: [i64; MAX_ZOOM_LEVELS],
276    zoom_res_sizes: [u64; MAX_ZOOM_LEVELS],
277    zoom_res_ends: [i64; MAX_ZOOM_LEVELS],
278    zoom_res_chr_index: Option<u32>,
279    zoom_headers: Vec<ZoomHeader>,
280
281    closed: bool,
282    /// Set while a call is part-way through, so a `Drop` after a failure leaves
283    /// the file unfinished rather than turning the caller's error into a file
284    /// that looks complete.
285    failed: bool,
286}
287
288impl BbiWriter {
289    pub fn create(path: &str, options: BbiWriterOptions) -> Result<Self> {
290        if crate::source::is_url(path) {
291            return Err(Error::invalid(format!(
292                "{path} is a url, which cannot be written to"
293            )));
294        }
295        if options.kind == BbiKind::BigWig && !options.fields.is_empty() {
296            return Err(Error::invalid("fields is only supported for bigbed files"));
297        }
298        let items_per_slot = options.items_per_slot.unwrap_or(match options.kind {
299            BbiKind::BigWig => WIG_ITEMS_PER_SLOT,
300            BbiKind::BigBed => BED_ITEMS_PER_SLOT,
301        });
302        // A wig section states its item count on 16 bits, which is the cap; a
303        // bed block has no count of its own, but one rule for both is easier to
304        // explain.
305        if !(1..=MAX_ITEMS_PER_SECTION).contains(&items_per_slot) {
306            return Err(Error::invalid(format!(
307                "items_per_slot {items_per_slot} invalid (1 to {MAX_ITEMS_PER_SECTION}, \
308                 or -1 for the default)"
309            )));
310        }
311        if options.block_size < 2 {
312            return Err(Error::invalid(format!(
313                "block_size {} invalid (>= 2)",
314                options.block_size
315            )));
316        }
317        if options.compression_level > 9 {
318            return Err(Error::invalid(format!(
319                "compression_level {} invalid (0 to 9)",
320                options.compression_level
321            )));
322        }
323        if let Some(sizes) = &options.chr_sizes {
324            for entry in sizes.iter() {
325                if entry.size <= 0 {
326                    return Err(Error::invalid(format!(
327                        "size {} of chromosome {} must be positive",
328                        entry.size, entry.id
329                    )));
330                }
331                to_bbi_u32(entry.size, "chromSize")?;
332            }
333        }
334
335        // Deflate is nearly the whole cost of writing a block, and blocks are
336        // independent, so this is the one part of the write path worth more
337        // than one thread. A single worker starts none: the block is then
338        // compressed in place and the pipeline stays empty.
339        let parallel = resolve_parallel(options.parallel);
340        let (executor, pending_limit) = if parallel > 1 {
341            // Blocks in flight, enough to keep every worker fed while the
342            // oldest is waited on. A wig block runs to a few kilobytes, so the
343            // whole queue is well under a megabyte, and it is what bounds the
344            // memory a producer faster than the workers can run the writer into.
345            (Some(Executor::new(options.parallel)?), parallel * 4)
346        } else {
347            (None, 0)
348        };
349
350        // The columns are known now and never change, so a bigBed's autoSql is
351        // the one part of the prefix that can be written rather than reserved.
352        let mut bed_fields = options.fields;
353        let mut auto_sql_text = String::new();
354        let (mut field_count, mut defined_field_count) = (0u16, 0u16);
355        if options.kind == BbiKind::BigBed {
356            if bed_fields.is_empty() {
357                bed_fields = [
358                    ("chr", "string"),
359                    ("start", "uint"),
360                    ("end", "uint"),
361                    ("name", "string"),
362                ]
363                .into_iter()
364                .map(|(a, b)| (a.to_string(), b.to_string()))
365                .collect();
366            }
367            let described = build_auto_sql(&bed_fields)?;
368            auto_sql_text = described.text;
369            field_count = described.field_count;
370            defined_field_count = described.defined_field_count;
371        }
372
373        let mut sink = LocalSink::create(path)?;
374        // The rest of the prefix goes out as zeroes, so an abandoned file
375        // carries no magic and reads as "not a bigwig or bigbed file" rather
376        // than as a valid header pointing at data that was never written.
377        let prefix_size = BBI_HEADER_SIZE + MAX_ZOOM_LEVELS as u64 * ZOOM_HEADER_SIZE;
378        let mut prefix = vec![0u8; prefix_size as usize];
379        let mut auto_sql_offset = 0;
380        if !auto_sql_text.is_empty() {
381            auto_sql_offset = prefix_size;
382            prefix.extend_from_slice(auto_sql_text.as_bytes());
383            prefix.push(0);
384        }
385        let total_summary_offset = prefix.len() as u64;
386        let full_data_offset = total_summary_offset + TOTAL_SUMMARY_SIZE;
387        prefix.resize(
388            prefix.len() + (TOTAL_SUMMARY_SIZE + DATA_COUNT_SIZE) as usize,
389            0,
390        );
391        sink.append(&prefix)?;
392
393        Ok(Self {
394            path: path.to_string(),
395            kind: options.kind,
396            sink: Some(sink),
397            executor,
398            block_size: options.block_size,
399            items_per_slot,
400            compression_level: options.compression_level,
401            auto_sql_offset,
402            total_summary_offset,
403            full_data_offset,
404            full_index_offset: 0,
405            chr_tree_offset: 0,
406            bed_fields,
407            field_count,
408            defined_field_count,
409            declared_sizes: options.chr_sizes,
410            chrs: IndexMap::new(),
411            current_chr: None,
412            last_chr_id: String::new(),
413            last_chr_index: None,
414            last_chr_end: 0,
415            last_entry_start: -1,
416            section: WigSection::with_policy(
417                items_per_slot,
418                options.section_policy,
419                options.cost_model,
420            ),
421            bed_block: Vec::new(),
422            bed_block_count: 0,
423            bed_bounds: None,
424            coverage: CoverageSweep::default(),
425            data_items: Vec::new(),
426            zoom_items: None,
427            pending: std::collections::VecDeque::new(),
428            pending_limit,
429            section_count: 0,
430            uncompress_buffer_size: 0,
431            data_body_size: 0,
432            section_counts: SectionCounts::default(),
433            summary: TotalSummary::default(),
434            item_count: 0,
435            entry_count: 0,
436            skipped_count: 0,
437            clipped_count: 0,
438            ladder_frozen: false,
439            ladder_start_item_count: 0,
440            zoom_reductions: [0; MAX_ZOOM_LEVELS],
441            zoom_res_sizes: [0; MAX_ZOOM_LEVELS],
442            zoom_res_ends: [0; MAX_ZOOM_LEVELS],
443            zoom_res_chr_index: None,
444            zoom_headers: Vec::new(),
445            closed: false,
446            failed: false,
447        })
448    }
449
450    // -- byte plumbing -----------------------------------------------------
451
452    fn sink(&mut self) -> Result<&mut LocalSink> {
453        self.sink.as_mut().ok_or_else(|| Error::Closed {
454            path: self.path.clone(),
455        })
456    }
457
458    fn emit(&mut self, bytes: &[u8]) -> Result<()> {
459        let path = self.path.clone();
460        self.sink
461            .as_mut()
462            .ok_or(Error::Closed { path })?
463            .append(bytes)?;
464        Ok(())
465    }
466
467    /// Overwrite bytes already placed, for a field reserved and filled later.
468    fn patch(&mut self, offset: u64, bytes: &[u8]) -> Result<()> {
469        self.drain()?;
470        let path = self.path.clone();
471        self.sink
472            .as_mut()
473            .ok_or(Error::Closed { path })?
474            .write_all_at(offset, bytes)
475    }
476
477    /// Offset the next emitted byte will land at, once nothing is in flight.
478    ///
479    /// Only meaningful with the pipeline empty: a block still compressing has
480    /// room reserved for it here that it has not taken yet.
481    fn sync_cursor(&mut self) -> Result<u64> {
482        self.drain()?;
483        Ok(self.sink()?.position())
484    }
485
486    fn is_bigwig(&self) -> bool {
487        self.kind == BbiKind::BigWig
488    }
489
490    // -- block pipeline ----------------------------------------------------
491
492    /// Compress a block, or hand it back unchanged on an uncompressed file.
493    fn pack_block(body: Vec<u8>, level: u32, path: &str) -> Result<Vec<u8>> {
494        if level == 0 {
495            return Ok(body);
496        }
497        use std::io::Write as _;
498        let mut encoder =
499            flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(level));
500        encoder
501            .write_all(&body)
502            .and_then(|_| encoder.finish())
503            .map_err(|e| Error::io(path, e))
504    }
505
506    /// Place a compressed block at the running offset and index it.
507    ///
508    /// The one place an offset is handed out, and so the one place the order of
509    /// the file is decided.
510    fn place_block(
511        &mut self,
512        mut leaf: LeafItem,
513        block: &[u8],
514        encoding: Option<WigEncoding>,
515    ) -> Result<()> {
516        leaf.offset = self.sink()?.position();
517        leaf.size = block.len() as u64;
518        let zooming = match &mut self.zoom_items {
519            Some(items) => {
520                items.push(leaf);
521                true
522            }
523            None => {
524                self.data_items.push(leaf);
525                false
526            }
527        };
528        self.emit(block)?;
529        // A zoom block is neither a section nor an encoding of one, so the
530        // counters a caller reads as `section_count` must not see it.
531        if !zooming {
532            if let Some(encoding) = encoding {
533                self.section_counts.bump(encoding);
534            }
535            self.section_count += 1;
536        }
537        Ok(())
538    }
539
540    /// Place the oldest block in flight, waiting on its deflate if it is still
541    /// running.
542    ///
543    /// Where the pipeline is put back in order. Blocks are compressed in
544    /// whatever order the workers finish them and placed in the order they were
545    /// submitted, so the offsets, the leaves of the R-tree and the bytes of the
546    /// file all agree and the result is the file the serial path would have
547    /// written, byte for byte.
548    fn commit_one(&mut self) -> Result<()> {
549        let Some(item) = self.pending.pop_front() else {
550            return Ok(());
551        };
552        let block = item.result.take()?;
553        self.place_block(item.leaf, &block, item.encoding)
554    }
555
556    fn drain(&mut self) -> Result<()> {
557        while !self.pending.is_empty() {
558            self.commit_one()?;
559        }
560        Ok(())
561    }
562
563    /// Compress a block and index it, on a worker thread if there is one.
564    ///
565    /// Submission blocks once enough blocks are in flight, which is what bounds
566    /// the memory the pipeline holds. A producer faster than the workers then
567    /// runs at their pace, which is the pace the serial path ran at anyway.
568    fn submit_block(
569        &mut self,
570        leaf: LeafItem,
571        body: Vec<u8>,
572        encoding: Option<WigEncoding>,
573    ) -> Result<()> {
574        self.uncompress_buffer_size = self.uncompress_buffer_size.max(body.len() as u64);
575        let level = self.compression_level;
576        let Some(executor) = &self.executor else {
577            let block = Self::pack_block(body, level, &self.path)?;
578            return self.place_block(leaf, &block, encoding);
579        };
580        let promise: Promise<Result<Vec<u8>>> = Promise::new();
581        let path = self.path.clone();
582        let worker = promise.clone();
583        executor.spawn(move || worker.set(Self::pack_block(body, level, &path)));
584        self.pending.push_back(PendingBlock {
585            leaf,
586            result: PendingResult::InFlight(promise),
587            encoding,
588        });
589        while self.pending.len() >= self.pending_limit {
590            self.commit_one()?;
591        }
592        Ok(())
593    }
594
595    // -- chromosomes and validation ----------------------------------------
596
597    /// The index of `chr_id`, assigning it one on first sight.
598    fn resolve_chr(&mut self, chr_id: &str) -> Result<u32> {
599        if let Some(index) = self.last_chr_index {
600            if chr_id == self.last_chr_id {
601                return Ok(index);
602            }
603        }
604        let mut name = chr_id.to_string();
605        let mut declared = None;
606        if let Some(sizes) = &self.declared_sizes {
607            let entry = sizes.resolve(chr_id)?;
608            name = entry.id.clone();
609            declared = Some(entry.size);
610        }
611        if let Some(state) = self.chrs.get(&name) {
612            if Some(state.index) != self.last_chr_index {
613                return Err(Error::invalid(format!(
614                    "chromosome {name} was already written, values must be pooled by chromosome"
615                )));
616            }
617            self.last_chr_id = chr_id.to_string();
618            return Ok(state.index);
619        }
620        // A wig section carries one chromosome id in its header, so it has to
621        // end here. A bed block does not: every record of it carries its own,
622        // which is what lets a genome of many small scaffolds pack them into
623        // one block rather than spend a block and an index leaf on each.
624        if self.is_bigwig() {
625            self.flush_section()?;
626        }
627        let index = to_bbi_u32(self.chrs.len() as i64, "chromId")?;
628        self.chrs.insert(
629            name.clone(),
630            ChrState {
631                index,
632                size: declared.unwrap_or(0),
633                declared: declared.is_some(),
634            },
635        );
636        self.current_chr = Some(name);
637        self.last_chr_id = chr_id.to_string();
638        self.last_chr_index = Some(index);
639        self.last_chr_end = 0;
640        self.last_entry_start = -1;
641        Ok(index)
642    }
643
644    fn chr_state(&self) -> ChrState {
645        let name = self.current_chr.as_deref().unwrap_or_default();
646        self.chrs[name]
647    }
648
649    fn grow_chr(&mut self, end: i64) {
650        if let Some(name) = &self.current_chr {
651            let state = self.chrs.get_mut(name).expect("current chromosome exists");
652            if !state.declared {
653                state.size = state.size.max(end);
654            }
655        }
656    }
657
658    /// The end a value may actually claim on the chromosome it sits on: its
659    /// own, or the end of the chromosome when it hangs over it.
660    ///
661    /// Values wider than one base come off a grid, and a chromosome is rarely a
662    /// whole number of bins long, so the last one of a chromosome read at a bin
663    /// size of ten overshoots by up to nine bases. That is arithmetic rather
664    /// than an error, and it is cut back here. Starting at or past the end is a
665    /// different thing — the value belongs to no base of it — and raises.
666    ///
667    /// A value cut back here leaves the chromosome at its end, so nothing can
668    /// follow it there: anything starting inside overlaps it and anything
669    /// starting past it raises. A cut value is therefore always the last of its
670    /// section, which is what lets a section carry one that no longer matches
671    /// its uniform span.
672    fn clip_to_chr(&mut self, start: i64, end: i64) -> Result<i64> {
673        let state = self.chr_state();
674        if !state.declared || end <= state.size {
675            return Ok(end);
676        }
677        if start >= state.size {
678            return Err(Error::invalid(format!(
679                "{}:{start}-{end} starts past the end of {}, which is {} bases long",
680                self.last_chr_id, self.last_chr_id, state.size
681            )));
682        }
683        self.clipped_count += 1;
684        Ok(state.size)
685    }
686
687    /// Check a range against the ordering contract and the chromosome it sits
688    /// on, and take it as the new high-water mark.
689    ///
690    /// `last_start` is the start of the last value the range covers, which is
691    /// `start` for a single one. Only that value can be cut back by the end of
692    /// a chromosome, so it is the one the chromosome is read against: a run
693    /// reaching whole values past the end starts one of them past it and
694    /// raises, where a run merely hanging over the end does not.
695    fn validate_range(&mut self, start: i64, end: i64, last_start: i64) -> Result<i64> {
696        if start < 0 {
697            return Err(Error::invalid(format!(
698                "start {start} must not be negative"
699            )));
700        }
701        // `end == start` is allowed: BED 1.0 permits it and an insertion is
702        // written that way, so refusing it made `convert_to_bigbed` fail on
703        // files the format calls valid. It covers no base, which the coverage
704        // sweep already handles — the run it would open closes at the position
705        // it opened at — and `EntryWalk::read` already goes out of its way to
706        // find such an entry when reading one back. bigWig values are a
707        // different matter and stay strictly positive-width.
708        if end < start {
709            return Err(Error::invalid(format!(
710                "{}:{start}-{end} ends before it starts",
711                self.last_chr_id
712            )));
713        }
714        if start < self.last_chr_end {
715            return Err(Error::invalid(format!(
716                "{}:{start}-{end} starts before the end {} of the previous value, values \
717                 must be added in order and without overlap",
718                self.last_chr_id, self.last_chr_end
719            )));
720        }
721        let end = self.clip_to_chr(last_start, end)?;
722        to_bbi_u32(end, "coordinate")?;
723        self.last_chr_end = end;
724        Ok(end)
725    }
726
727    /// Check a bed entry against the ordering contract and its chromosome.
728    ///
729    /// Only the starts have to be in order, unlike the values of a bigWig. A
730    /// bed of anything real — genes, repeats, peaks called twice over — has
731    /// entries that overlap and nest, and the format asks only that they be
732    /// sorted by (chromosome, start).
733    fn validate_entry(&mut self, start: i64, end: i64) -> Result<()> {
734        if start < 0 {
735            return Err(Error::invalid(format!(
736                "start {start} must not be negative"
737            )));
738        }
739        // `end == start` is allowed: BED 1.0 permits it and an insertion is
740        // written that way, so refusing it made `convert_to_bigbed` fail on
741        // files the format calls valid. It covers no base, which the coverage
742        // sweep already handles — the run it would open closes at the position
743        // it opened at — and `EntryWalk::read` already goes out of its way to
744        // find such an entry when reading one back. bigWig values are a
745        // different matter and stay strictly positive-width.
746        if end < start {
747            return Err(Error::invalid(format!(
748                "{}:{start}-{end} ends before it starts",
749                self.last_chr_id
750            )));
751        }
752        if start < self.last_entry_start {
753            return Err(Error::invalid(format!(
754                "{}:{start}-{end} starts before the previous entry at {}, entries must be \
755                 added in order of their start",
756                self.last_chr_id, self.last_entry_start
757            )));
758        }
759        let state = self.chr_state();
760        if state.declared && end > state.size {
761            return Err(Error::invalid(format!(
762                "{}:{start}-{end} runs past the end of {}, which is {} bases long",
763                self.last_chr_id, self.last_chr_id, state.size
764            )));
765        }
766        to_bbi_u32(end, "coordinate")?;
767        self.last_entry_start = start;
768        Ok(())
769    }
770
771    // -- statistics --------------------------------------------------------
772
773    /// Add one value covering `span` bases to the whole-file summary.
774    ///
775    /// The extremes are seeded from the first value rather than compared
776    /// against the NaN the struct defaults them to, since a comparison against
777    /// NaN is false either way and would leave both of them NaN for ever.
778    fn account_value(&mut self, value: f32, span: i64) {
779        if self.summary.bases_covered == 0 {
780            self.summary.min_value = value as f64;
781            self.summary.max_value = value as f64;
782        } else {
783            if (value as f64) < self.summary.min_value {
784                self.summary.min_value = value as f64;
785            }
786            if (value as f64) > self.summary.max_value {
787                self.summary.max_value = value as f64;
788            }
789        }
790        self.summary.bases_covered += span as u64;
791        self.summary.sum_data += value as f64 * span as f64;
792        self.summary.sum_squared += value as f64 * value as f64 * span as f64;
793        self.item_count += 1;
794        if !self.ladder_frozen && self.item_count >= RESOLUTION_SAMPLE_ITEMS {
795            self.freeze_zoom_ladder();
796        }
797    }
798
799    /// Fix the reduction ladder from the mean item span seen so far.
800    ///
801    /// UCSC anchors the first level at ten times the mean span, then
802    /// quadruples, taking the mean from the whole file it wrote to a temporary
803    /// one. Here the candidates are counted as the data goes by, so the mean
804    /// comes from the first few thousand items. The ladder only decides how
805    /// coarse the summaries are, so a poor estimate makes the file bigger or
806    /// its zoomed queries wider, never wrong.
807    fn freeze_zoom_ladder(&mut self) {
808        let mean_span = self
809            .summary
810            .bases_covered
811            .checked_div(self.item_count)
812            .unwrap_or(1)
813            .max(1) as i64;
814        let mut reduction = (mean_span * INITIAL_ZOOM_FACTOR).max(1);
815        for level in 0..MAX_ZOOM_LEVELS {
816            self.zoom_reductions[level] = reduction;
817            self.zoom_res_ends[level] = 0;
818            // Capped rather than wrapped: a reduction level is a u32, and past
819            // the widest chromosome a coarser one summarises nothing new.
820            if reduction > 0xFFFF_FFFF / ZOOM_INCREMENT {
821                continue;
822            }
823            reduction *= ZOOM_INCREMENT;
824        }
825        self.zoom_res_chr_index = None;
826        self.ladder_start_item_count = self.item_count;
827        self.ladder_frozen = true;
828    }
829
830    /// Count the windows each candidate reduction would open over
831    /// `[start, end)`.
832    ///
833    /// Windows are anchored to the data rather than aligned to a grid, the way
834    /// the zoom pass itself anchors them, so the counts are what that pass will
835    /// actually produce. Contiguous items give the same answer whether they are
836    /// counted one at a time or as a single range, which is what lets a run of
837    /// values be counted in one call.
838    fn count_resolutions(&mut self, chr_index: u32, start: i64, end: i64) {
839        if !self.ladder_frozen {
840            return;
841        }
842        if self.zoom_res_chr_index != Some(chr_index) {
843            self.zoom_res_ends = [0; MAX_ZOOM_LEVELS];
844            self.zoom_res_chr_index = Some(chr_index);
845        }
846        for level in 0..MAX_ZOOM_LEVELS {
847            let reduction = self.zoom_reductions[level];
848            if start >= self.zoom_res_ends[level] {
849                self.zoom_res_sizes[level] += 1;
850                self.zoom_res_ends[level] = start + reduction;
851            }
852            if end > self.zoom_res_ends[level] {
853                // Closed form rather than a loop: one bedGraph interval can be
854                // megabases wide against a reduction of a few dozen bases.
855                let extra = (end - self.zoom_res_ends[level] + reduction - 1) / reduction;
856                self.zoom_res_sizes[level] += extra as u64;
857                self.zoom_res_ends[level] += extra * reduction;
858            }
859        }
860    }
861
862    // -- section accumulation ----------------------------------------------
863
864    /// Put one validated value over `[start, end)` into the open section, and
865    /// into everything the file has to say about it afterwards.
866    fn place_value(
867        &mut self,
868        chr_index: u32,
869        start: i64,
870        end: i64,
871        value: f32,
872        batch_remaining: usize,
873    ) -> Result<()> {
874        if !value.is_finite() {
875            self.skipped_count += 1;
876            return Ok(());
877        }
878        self.grow_chr(end);
879        self.account_value(value, end - start);
880        self.count_resolutions(chr_index, start, end);
881        self.add_item(chr_index, start, end - start, value, batch_remaining)
882    }
883
884    /// Add one item to the open section, opening, widening or closing it as the
885    /// item requires.
886    fn add_item(
887        &mut self,
888        chr_index: u32,
889        start: i64,
890        span: i64,
891        value: f32,
892        batch_remaining: usize,
893    ) -> Result<()> {
894        if self
895            .section
896            .offer(chr_index, start, span, value, batch_remaining)
897            == Accept::Flush
898        {
899            self.flush_section()?;
900            // Offered again, to the empty section — which cannot refuse it. The
901            // retry is the whole point of `Flush`, so it is a statement and not
902            // an assertion: inside a `debug_assert` it would be compiled out of
903            // a release build and every item that split a section would be
904            // silently dropped.
905            let retried = self
906                .section
907                .offer(chr_index, start, span, value, batch_remaining);
908            debug_assert_eq!(retried, Accept::Buffered);
909        }
910        if self.section.is_full() {
911            self.flush_section()?;
912        }
913        Ok(())
914    }
915
916    /// Encode, compress and write the open section, and index the block it
917    /// became.
918    fn flush_section(&mut self) -> Result<()> {
919        if self.section.is_empty() {
920            return Ok(());
921        }
922        let body = self.section.encode()?;
923        self.data_body_size += body.len() as u64;
924        let leaf = LeafItem {
925            start_chr: self.section.chr_ix,
926            start_base: to_bbi_u32(self.section.first_start, "chromStart")?,
927            end_chr: self.section.chr_ix,
928            end_base: to_bbi_u32(self.section.last_end, "chromEnd")?,
929            offset: 0,
930            size: 0,
931        };
932        let encoding = self.section.encoding();
933        self.section.clear();
934        self.submit_block(leaf, body, Some(encoding))
935    }
936
937    // -- bed records -------------------------------------------------------
938
939    /// Append one record to the block being filled (Supp. Table 12).
940    ///
941    /// A declared field the caller leaves out is written empty rather than
942    /// dropped. Every record of a bed carries every column, so a missing one
943    /// would make the record unreadable rather than merely incomplete.
944    fn append_bed_record(
945        &mut self,
946        chr_index: u32,
947        start: i64,
948        end: i64,
949        values: &IndexMap<String, String>,
950    ) -> Result<()> {
951        for name in values.keys() {
952            if !self.bed_fields.contains_key(name) {
953                return Err(Error::invalid(format!(
954                    "field {name} is not one this file declares"
955                )));
956            }
957            // The coordinates are the record's own three fields and are given
958            // as arguments, so naming one here is a mistake worth reporting
959            // rather than a value that would silently go nowhere.
960            if self.bed_fields.get_index_of(name).is_some_and(|i| i < 3) {
961                return Err(Error::invalid(format!(
962                    "field {name} is a coordinate, which is written from start and end"
963                )));
964            }
965        }
966        let (start_u32, end_u32) = (
967            to_bbi_u32(start, "chromStart")?,
968            to_bbi_u32(end, "chromEnd")?,
969        );
970        self.bed_bounds = Some(match self.bed_bounds {
971            None => (chr_index, start_u32, chr_index, end_u32),
972            Some((sc, sb, ec, _)) if chr_index != ec => (sc, sb, chr_index, end_u32),
973            // Entries may nest, so the furthest one reaches is not the last one
974            // to start.
975            Some((sc, sb, ec, eb)) => (sc, sb, ec, eb.max(end_u32)),
976        });
977
978        self.bed_block.extend_from_slice(&chr_index.to_le_bytes());
979        self.bed_block.extend_from_slice(&start_u32.to_le_bytes());
980        self.bed_block.extend_from_slice(&end_u32.to_le_bytes());
981        for (index, (name, kind)) in self.bed_fields.iter().enumerate() {
982            if index < 3 {
983                continue;
984            }
985            if index > 3 {
986                self.bed_block.push(b'\t');
987            }
988            match values.get(name) {
989                Some(text) => {
990                    if text.contains('\t') || text.contains('\0') {
991                        return Err(Error::invalid(format!(
992                            "field {name} value {text} contains a tab or a null byte"
993                        )));
994                    }
995                    self.bed_block.extend_from_slice(text.as_bytes());
996                }
997                // A numeric column left out is written as a zero: a bed reader
998                // parses it, where an empty one is not a number at all.
999                None if kind != "string" => self.bed_block.push(b'0'),
1000                None => {}
1001            }
1002        }
1003        self.bed_block.push(0);
1004        self.bed_block_count += 1;
1005        self.entry_count += 1;
1006        Ok(())
1007    }
1008
1009    /// Compress and write the block of bed entries being filled.
1010    fn flush_bed_block(&mut self) -> Result<()> {
1011        if self.bed_block_count == 0 {
1012            return Ok(());
1013        }
1014        let body = std::mem::take(&mut self.bed_block);
1015        // The next block starts from a fresh buffer reserved to what the last
1016        // one came to, rather than grown into over a hundred entries.
1017        self.bed_block = Vec::with_capacity(body.len());
1018        self.data_body_size += body.len() as u64;
1019        let (sc, sb, ec, eb) = self.bed_bounds.take().expect("a filled block has bounds");
1020        let leaf = LeafItem {
1021            start_chr: sc,
1022            start_base: sb,
1023            end_chr: ec,
1024            end_base: eb,
1025            offset: 0,
1026            size: 0,
1027        };
1028        self.bed_block_count = 0;
1029        self.submit_block(leaf, body, None)
1030    }
1031
1032    /// End whatever is being filled, whichever kind of file this is.
1033    fn flush_pending(&mut self) -> Result<()> {
1034        if self.is_bigwig() {
1035            self.flush_section()?;
1036        } else {
1037            self.flush_bed_block()?;
1038        }
1039        self.drain()
1040    }
1041
1042    // -- the public write API ----------------------------------------------
1043
1044    /// Write one value over `[start, end)`.
1045    ///
1046    /// A value that overruns the end of a declared chromosome having started
1047    /// inside it is written up to that end rather than refused, since a
1048    /// chromosome is rarely a whole number of bins long; `clipped_count` counts
1049    /// them. A value that is not finite is not written at all — it leaves a
1050    /// gap, which is what a bigWig means by a base carrying no data.
1051    pub fn write_value(&mut self, chr: &str, start: i64, end: i64, value: f32) -> Result<()> {
1052        self.check_open()?;
1053        if !self.is_bigwig() {
1054            return Err(Error::invalid(
1055                "write_value is only for bigwig files, use write_entry",
1056            ));
1057        }
1058        self.failed = true;
1059        let result = (|| {
1060            let chr_index = self.resolve_chr(chr)?;
1061            let end = self.validate_range(start, end, start)?;
1062            self.place_value(chr_index, start, end, value, 0)
1063        })();
1064        self.failed = result.is_err();
1065        result
1066    }
1067
1068    /// Write `values.len()` values of `span` bases each, the first at `start`
1069    /// and each one starting where the one before it ended.
1070    ///
1071    /// The fast path. Values that come in this way are a fixedStep section by
1072    /// construction — four bytes an item against twelve — so a run handed over
1073    /// in one call costs a third of what the same values cost one at a time. A
1074    /// run with no gaps also goes in as a single extend: the starts are implied
1075    /// by the step and never materialised.
1076    ///
1077    /// Only the last value of a run can overrun the end of a declared
1078    /// chromosome having started inside it, since the ones before it are a
1079    /// whole span short of it. It is cut back to that end and, no longer being
1080    /// a step of `span`, laid down on its own rather than in the extend.
1081    pub fn write_values(&mut self, chr: &str, start: i64, span: i64, values: &[f32]) -> Result<()> {
1082        self.check_open()?;
1083        if !self.is_bigwig() {
1084            return Err(Error::invalid(
1085                "write_values is only for bigwig files, use write_entry",
1086            ));
1087        }
1088        if values.is_empty() {
1089            return Ok(());
1090        }
1091        self.failed = true;
1092        let result = self.write_values_inner(chr, start, span, values);
1093        self.failed = result.is_err();
1094        result
1095    }
1096
1097    fn write_values_inner(
1098        &mut self,
1099        chr: &str,
1100        start: i64,
1101        span: i64,
1102        values: &[f32],
1103    ) -> Result<()> {
1104        if span <= 0 {
1105            return Err(Error::invalid(format!("span {span} must be positive")));
1106        }
1107        let count = values.len() as i64;
1108        let chr_index = self.resolve_chr(chr)?;
1109        let last_start = start + span * (count - 1);
1110        let last_end = self.validate_range(start, last_start + span, last_start)?;
1111        // A value cut short is not a step of span, so it cannot ride the run.
1112        let run_count = if last_end - last_start == span {
1113            count
1114        } else {
1115            count - 1
1116        };
1117
1118        let mut index = 0i64;
1119        while index < run_count {
1120            let item_start = start + span * index;
1121            // A section that has widened cannot narrow again, so a long
1122            // uniform run fed into one is written at the wider item size for
1123            // no reason. Close it first and let the run have a fixedStep
1124            // section of its own — see `WigSection::should_flush_for_run`.
1125            if self.section.should_flush_for_run(
1126                chr_index,
1127                item_start,
1128                span,
1129                (run_count - index) as usize,
1130            ) {
1131                self.flush_section()?;
1132            }
1133            let extends = self.section.extends_run(chr_index, item_start, span);
1134            if extends || self.section.is_empty() {
1135                // Only as far as the section can take, not as far as the run
1136                // goes: scanning the whole run and keeping a slotful rescans
1137                // the remainder on the next pass, which is quadratic in the
1138                // size of the call.
1139                let limit =
1140                    (run_count - index).min(self.items_per_slot as i64 - self.section.len() as i64);
1141                let mut run = 0i64;
1142                while run < limit && values[(index + run) as usize].is_finite() {
1143                    run += 1;
1144                }
1145                if run > 0 {
1146                    let slice = &values[index as usize..(index + run) as usize];
1147                    self.section.extend_run(chr_index, item_start, span, slice);
1148                    let section_end = start + span * (index + run);
1149                    // Once for the run, not once per value: `grow_chr` takes
1150                    // the maximum, so the run's own end is the only call that
1151                    // can change anything — and each one is a hash lookup on
1152                    // the chromosome name.
1153                    self.grow_chr(section_end);
1154                    for i in 0..run {
1155                        self.account_value(values[(index + i) as usize], span);
1156                    }
1157                    self.count_resolutions(chr_index, item_start, section_end);
1158                    index += run;
1159                    if self.section.is_full() {
1160                        self.flush_section()?;
1161                    }
1162                    continue;
1163                }
1164            }
1165            self.place_value(
1166                chr_index,
1167                item_start,
1168                item_start + span,
1169                values[index as usize],
1170                (count - index - 1) as usize,
1171            )?;
1172            index += 1;
1173        }
1174        if run_count != count {
1175            self.place_value(
1176                chr_index,
1177                last_start,
1178                last_end,
1179                values[(count - 1) as usize],
1180                0,
1181            )?;
1182        }
1183        Ok(())
1184    }
1185
1186    /// Write one bed entry over `[start, end)`.
1187    ///
1188    /// `values` are the fields past the coordinates, already formatted as the
1189    /// text a bed stores, under the names the file's own fields declare. A
1190    /// declared field left out is written empty.
1191    ///
1192    /// Entries may overlap and nest, unlike the values of a bigWig. Only their
1193    /// starts have to be in order.
1194    pub fn write_entry(
1195        &mut self,
1196        chr: &str,
1197        start: i64,
1198        end: i64,
1199        values: &IndexMap<String, String>,
1200    ) -> Result<()> {
1201        self.check_open()?;
1202        if self.is_bigwig() {
1203            return Err(Error::invalid(
1204                "write_entry is only for bigbed files, use write_value",
1205            ));
1206        }
1207        self.failed = true;
1208        let result = (|| {
1209            let chr_index = self.resolve_chr(chr)?;
1210            self.validate_entry(start, end)?;
1211            self.grow_chr(end);
1212            self.append_bed_record(chr_index, start, end, values)?;
1213            // The sweep's runs are what the summary and the zoom levels of a
1214            // bigBed hold, so they are accumulated as the entries arrive.
1215            let runs = self.coverage.add(chr_index, start, end);
1216            for (chr, s, e, depth) in runs {
1217                self.account_value(depth, e - s);
1218                self.count_resolutions(chr, s, e);
1219            }
1220            if self.bed_block_count >= self.items_per_slot {
1221                self.flush_bed_block()?;
1222            }
1223            Ok(())
1224        })();
1225        self.failed = result.is_err();
1226        result
1227    }
1228
1229    fn check_open(&self) -> Result<()> {
1230        if self.closed {
1231            return Err(Error::invalid(format!(
1232                "error writing to {} (file is closed)",
1233                self.path
1234            )));
1235        }
1236        Ok(())
1237    }
1238
1239    // -- finalisation ------------------------------------------------------
1240
1241    /// Finish the file: the index, the zoom levels, the chromosome tree and the
1242    /// headers, in that order.
1243    ///
1244    /// Calling it twice is harmless. Until it returns, nothing on disk is a
1245    /// bigWig or bigBed.
1246    pub fn close(&mut self) -> Result<()> {
1247        if self.closed {
1248            return Ok(());
1249        }
1250        self.failed = true;
1251        let result = self.finish();
1252        // The deflate threads stop here rather than at drop, so a closed file
1253        // holds none of this library's — what a caller closing one, or leaving
1254        // its with-block, expects. Also on the way out of a failure.
1255        self.executor = None;
1256        self.pending.clear();
1257        result?;
1258        if let Some(sink) = &mut self.sink {
1259            sink.close()?;
1260        }
1261        self.sink = None;
1262        self.closed = true;
1263        self.failed = false;
1264        Ok(())
1265    }
1266
1267    fn finish(&mut self) -> Result<()> {
1268        // The sweep holds the runs its last entries left open, and they belong
1269        // to the summary, so it has to be finished before the block is.
1270        if !self.is_bigwig() {
1271            for (chr, s, e, depth) in self.coverage.finish() {
1272                self.account_value(depth, e - s);
1273                self.count_resolutions(chr, s, e);
1274            }
1275        }
1276        self.flush_pending()?;
1277
1278        self.full_index_offset = self.sync_cursor()?;
1279        self.write_data_tree()?;
1280        self.write_zoom_levels()?;
1281        self.write_chromosome_tree()?;
1282
1283        // The file's own magic repeated at the end (Supp. Table 5), as UCSC's
1284        // writers close a file, marking it untruncated. It has to match the
1285        // type: no reader here checks it, but one that does would call every
1286        // bigBed corrupt.
1287        let magic = self.magic();
1288        self.emit(&magic.to_le_bytes())?;
1289        self.sink()?.flush()?;
1290        self.write_headers()
1291    }
1292
1293    fn magic(&self) -> u32 {
1294        match self.kind {
1295            BbiKind::BigWig => super::BIGWIG_MAGIC,
1296            BbiKind::BigBed => super::BIGBED_MAGIC,
1297        }
1298    }
1299
1300    fn write_data_tree(&mut self) -> Result<()> {
1301        let items = std::mem::take(&mut self.data_items);
1302        let mut bytes = Vec::new();
1303        super::rtree::write_tree(
1304            &items,
1305            self.full_index_offset,
1306            self.block_size,
1307            self.items_per_slot as u32,
1308            self.full_index_offset,
1309            &mut |b| bytes.extend_from_slice(b),
1310        )?;
1311        self.data_items = items;
1312        self.emit(&bytes)
1313    }
1314
1315    fn write_chromosome_tree(&mut self) -> Result<()> {
1316        let mut entries: Vec<WriteEntry> = self
1317            .chrs
1318            .iter()
1319            .map(|(id, state)| {
1320                Ok(WriteEntry {
1321                    id: id.clone(),
1322                    size: to_bbi_u32(state.size.max(1), "chromSize")?,
1323                    index: state.index,
1324                })
1325            })
1326            .collect::<Result<_>>()?;
1327        // The tree is searched by name, so its leaves are ordered by name. The
1328        // ids they carry follow the order the chromosomes were written in,
1329        // which is what keeps the data sorted by (chromosome, start), and the
1330        // format has never required the two orders to agree.
1331        entries.sort_by(|a, b| a.id.cmp(&b.id));
1332        self.chr_tree_offset = self.sync_cursor()?;
1333        let mut bytes = Vec::new();
1334        super::chr_tree::write_tree(&entries, self.chr_tree_offset, self.block_size, &mut |b| {
1335            bytes.extend_from_slice(b)
1336        })?;
1337        self.emit(&bytes)
1338    }
1339
1340    fn write_headers(&mut self) -> Result<()> {
1341        let mut zoom_bytes = Vec::new();
1342        for header in &self.zoom_headers {
1343            zoom_bytes.extend_from_slice(&write_zoom_header(header));
1344        }
1345        if !zoom_bytes.is_empty() {
1346            self.patch(BBI_HEADER_SIZE, &zoom_bytes)?;
1347        }
1348
1349        let summary = write_total_summary(&self.summary);
1350        self.patch(self.total_summary_offset, &summary)?;
1351
1352        // A bigWig counts the sections it holds, a bigBed the entries.
1353        let count = if self.is_bigwig() {
1354            self.section_count
1355        } else {
1356            self.entry_count
1357        };
1358        self.patch(self.full_data_offset, &count.to_le_bytes())?;
1359
1360        let header = BbiHeader {
1361            kind: self.kind,
1362            version: BBI_OUTPUT_VERSION,
1363            zoom_levels: self.zoom_headers.len() as u16,
1364            chr_tree_offset: self.chr_tree_offset,
1365            full_data_offset: self.full_data_offset,
1366            full_index_offset: self.full_index_offset,
1367            field_count: self.field_count,
1368            defined_field_count: self.defined_field_count,
1369            auto_sql_offset: self.auto_sql_offset,
1370            total_summary_offset: self.total_summary_offset,
1371            uncompress_buffer_size: if self.compression_level > 0 {
1372                to_bbi_u32(self.uncompress_buffer_size as i64, "uncompressBufSize")?
1373            } else {
1374                0
1375            },
1376        };
1377        let bytes = write_header(&header)?;
1378
1379        // Everything but the magic, then the magic alone. The file is not a bbi
1380        // file until those four bytes land, so a write dying between the two
1381        // leaves something no reader accepts rather than a header pointing at
1382        // nothing.
1383        self.patch(4, &bytes[4..])?;
1384        let magic = self.magic();
1385        self.patch(0, &magic.to_le_bytes())
1386    }
1387
1388    // -- accessors ---------------------------------------------------------
1389
1390    pub fn path(&self) -> &str {
1391        &self.path
1392    }
1393    pub fn kind(&self) -> BbiKind {
1394        self.kind
1395    }
1396    pub fn fields(&self) -> &IndexMap<String, String> {
1397        &self.bed_fields
1398    }
1399    pub fn section_counts(&self) -> SectionCounts {
1400        self.section_counts
1401    }
1402    pub fn section_count(&self) -> u64 {
1403        self.section_count
1404    }
1405    pub fn entry_count(&self) -> u64 {
1406        self.entry_count
1407    }
1408    pub fn skipped_count(&self) -> u64 {
1409        self.skipped_count
1410    }
1411    pub fn clipped_count(&self) -> u64 {
1412        self.clipped_count
1413    }
1414    pub fn is_closed(&self) -> bool {
1415        self.closed
1416    }
1417    /// Chromosome sizes as written, in the order the chromosomes were written.
1418    pub fn chr_sizes(&self) -> Vec<(String, i64)> {
1419        self.chrs
1420            .iter()
1421            .map(|(id, state)| (id.clone(), state.size))
1422            .collect()
1423    }
1424
1425    /// Give up on the file, stopping the workers and **removing what was
1426    /// written**.
1427    ///
1428    /// What a caller that will never close this writer says, so `Drop` leaves
1429    /// the file alone rather than finishing it. Removing it rather than leaving
1430    /// it is what the converters promise: `LocalSink::create` truncated the
1431    /// path at open, so what would otherwise stay behind is a zero-magic stub —
1432    /// no reader accepts it, which is right, but the caller still has to clean
1433    /// it up, and an interrupted conversion that says it left no file must not
1434    /// leave one. Harmless on a writer already closed, which owns a finished
1435    /// file rather than an abandoned one.
1436    pub fn abandon(&mut self) {
1437        if self.closed {
1438            return;
1439        }
1440        self.failed = true;
1441        self.pending.clear();
1442        self.executor = None;
1443        if let Some(mut sink) = self.sink.take() {
1444            sink.discard();
1445        }
1446    }
1447}
1448
1449impl Drop for BbiWriter {
1450    fn drop(&mut self) {
1451        // A writer that already threw is left alone: finishing it would turn
1452        // the caller's error into a file that looks complete and is not.
1453        if self.failed || self.closed {
1454            return;
1455        }
1456        let _ = self.close();
1457    }
1458}
1459
1460impl std::fmt::Debug for BbiWriter {
1461    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1462        f.debug_struct("BbiWriter")
1463            .field("path", &self.path)
1464            .field("type", &self.kind.as_str())
1465            .field("closed", &self.closed)
1466            .finish()
1467    }
1468}
1469
1470/// Rolling coverage depth over sorted, overlapping entries — what a bigBed's
1471/// summary and zoom levels describe.
1472///
1473/// Supp. Table 5 puts it as "the values correspond to those of a BigWig
1474/// constructed by the depth of coverage of bases", so a base under three
1475/// entries counts once towards the bases covered and three towards the sum, and
1476/// a base under none counts towards neither.
1477///
1478/// Entries arrive sorted by start but may overlap and nest freely, which is the
1479/// ordinary shape of a bed — genes overlap. A heap of the ends still open is
1480/// therefore what says how deep the current base is, and it holds one entry per
1481/// unit of depth rather than one per entry of the file.
1482#[derive(Debug, Default)]
1483pub(crate) struct CoverageSweep {
1484    chr_index: Option<u32>,
1485    position: i64,
1486    /// A min-heap of the ends still open, which `Reverse` is what makes of
1487    /// Rust's max-heap.
1488    ends: BinaryHeap<std::cmp::Reverse<i64>>,
1489}
1490
1491/// One run of constant depth: chromosome, start, end, depth.
1492type Run = (u32, i64, i64, f32);
1493
1494impl CoverageSweep {
1495    /// Add one entry, returning every run of constant depth it closes.
1496    ///
1497    /// A run is only emitted once nothing can change it, which is why the entry
1498    /// that opens it is not the entry that reports it, and why `finish` has to
1499    /// be called before the numbers mean anything.
1500    fn add(&mut self, chr: u32, start: i64, end: i64) -> Vec<Run> {
1501        let mut out = Vec::new();
1502        if self.chr_index != Some(chr) {
1503            out.extend(self.finish());
1504            self.chr_index = Some(chr);
1505            self.position = start;
1506        }
1507        while self.ends.peek().is_some_and(|e| e.0 <= start) {
1508            let expiry = self.ends.peek().expect("just peeked").0;
1509            if expiry > self.position {
1510                out.push((
1511                    self.chr_index.expect("a run has a chromosome"),
1512                    self.position,
1513                    expiry,
1514                    self.ends.len() as f32,
1515                ));
1516                self.position = expiry;
1517            }
1518            while self.ends.peek().is_some_and(|e| e.0 == expiry) {
1519                self.ends.pop();
1520            }
1521        }
1522        // Whatever is still open reaches past this entry's start, so the depth
1523        // over what is left before it is constant.
1524        if !self.ends.is_empty() && start > self.position {
1525            out.push((
1526                self.chr_index.expect("a run has a chromosome"),
1527                self.position,
1528                start,
1529                self.ends.len() as f32,
1530            ));
1531        }
1532        if start > self.position {
1533            self.position = start;
1534        }
1535        self.ends.push(std::cmp::Reverse(end));
1536        out
1537    }
1538
1539    /// Close every run still open, at the end of the chromosome or of the file.
1540    fn finish(&mut self) -> Vec<Run> {
1541        let mut out = Vec::new();
1542        while let Some(std::cmp::Reverse(expiry)) = self.ends.peek().copied() {
1543            if expiry > self.position {
1544                out.push((
1545                    self.chr_index.expect("a run has a chromosome"),
1546                    self.position,
1547                    expiry,
1548                    self.ends.len() as f32,
1549                ));
1550                self.position = expiry;
1551            }
1552            while self.ends.peek().is_some_and(|e| e.0 == expiry) {
1553                self.ends.pop();
1554            }
1555        }
1556        self.chr_index = None;
1557        self.position = 0;
1558        out
1559    }
1560}
1561
1562// -- the zoom pass ---------------------------------------------------------
1563
1564/// The zoom window being accumulated, holding the statistics of Supp. Table 19
1565/// with the two sums widened.
1566///
1567/// The sums are `f64` even though the record stores them as `f32`, because a
1568/// window of a few million bases passes the point where an `f32` accumulator
1569/// stops making progress long before it is closed.
1570#[derive(Debug, Default)]
1571struct ZoomWindow {
1572    open: bool,
1573    chr_index: u32,
1574    start: i64,
1575    end: i64,
1576    limit: i64,
1577    valid_count: u64,
1578    min_value: f32,
1579    max_value: f32,
1580    sum_data: f64,
1581    sum_squared: f64,
1582}
1583
1584/// One zoom record (Supp. Table 19).
1585#[derive(Debug, Clone, Copy)]
1586struct ZoomRecord {
1587    chr_index: u32,
1588    chr_start: u32,
1589    chr_end: u32,
1590    valid_count: u32,
1591    min_value: f32,
1592    max_value: f32,
1593    sum_data: f32,
1594    sum_squared: f32,
1595}
1596
1597impl ZoomRecord {
1598    fn write(&self, out: &mut Vec<u8>) {
1599        out.extend_from_slice(&self.chr_index.to_le_bytes());
1600        out.extend_from_slice(&self.chr_start.to_le_bytes());
1601        out.extend_from_slice(&self.chr_end.to_le_bytes());
1602        out.extend_from_slice(&self.valid_count.to_le_bytes());
1603        out.extend_from_slice(&self.min_value.to_le_bytes());
1604        out.extend_from_slice(&self.max_value.to_le_bytes());
1605        out.extend_from_slice(&self.sum_data.to_le_bytes());
1606        out.extend_from_slice(&self.sum_squared.to_le_bytes());
1607    }
1608
1609    fn read(block: &[u8], offset: usize) -> Self {
1610        let u32_at = |o: usize| u32::from_le_bytes(block[o..o + 4].try_into().expect("4 bytes"));
1611        let f32_at = |o: usize| f32::from_le_bytes(block[o..o + 4].try_into().expect("4 bytes"));
1612        Self {
1613            chr_index: u32_at(offset),
1614            chr_start: u32_at(offset + 4),
1615            chr_end: u32_at(offset + 8),
1616            valid_count: u32_at(offset + 12),
1617            min_value: f32_at(offset + 16),
1618            max_value: f32_at(offset + 20),
1619            sum_data: f32_at(offset + 24),
1620            sum_squared: f32_at(offset + 28),
1621        }
1622    }
1623}
1624
1625/// Accumulates zoom records at one reduction, closing a window whenever the
1626/// data leaves it, and grouping the closed records into blocks.
1627///
1628/// The blocks a level produces go out through the writer's own pipeline — the
1629/// same compression on the same pool, placed in submission order — but they are
1630/// indexed by the level rather than by the data section, and count towards
1631/// neither `section_count` nor the encoding counters. `BbiWriter::zoom_items`
1632/// is the switch.
1633struct ZoomLevelBuilder {
1634    reduction: i64,
1635    items_per_slot: usize,
1636    window: ZoomWindow,
1637    records: Vec<ZoomRecord>,
1638    /// Encoded, uncompressed blocks and their leaves, in submission order.
1639    ///
1640    /// A staging queue, not an accumulator: the writer drains it into its own
1641    /// pipeline after every call that can close a block, so what sits here is
1642    /// what one source block just produced rather than a whole level. Holding
1643    /// a level was the difference between a bounded `close()` and one resident
1644    /// gigabyte per gigabyte of data body.
1645    blocks: Vec<(LeafItem, Vec<u8>)>,
1646    record_count: u64,
1647}
1648
1649impl ZoomLevelBuilder {
1650    fn new(reduction: i64, items_per_slot: usize) -> Self {
1651        Self {
1652            reduction,
1653            items_per_slot,
1654            window: ZoomWindow::default(),
1655            records: Vec::new(),
1656            blocks: Vec::new(),
1657            record_count: 0,
1658        }
1659    }
1660
1661    fn open_window(&mut self, chr_index: u32, start: i64, min_value: f32, max_value: f32) {
1662        self.window = ZoomWindow {
1663            open: true,
1664            chr_index,
1665            start,
1666            end: start,
1667            limit: start + self.reduction,
1668            valid_count: 0,
1669            min_value,
1670            max_value,
1671            sum_data: 0.0,
1672            sum_squared: 0.0,
1673        };
1674    }
1675
1676    fn close_window(&mut self) -> Result<()> {
1677        if !self.window.open {
1678            return Ok(());
1679        }
1680        self.window.open = false;
1681        if self.window.valid_count == 0 {
1682            return Ok(());
1683        }
1684        self.records.push(ZoomRecord {
1685            chr_index: self.window.chr_index,
1686            chr_start: to_bbi_u32(self.window.start, "chromStart")?,
1687            chr_end: to_bbi_u32(self.window.end, "chromEnd")?,
1688            valid_count: to_bbi_u32(self.window.valid_count as i64, "validCount")?,
1689            min_value: self.window.min_value,
1690            max_value: self.window.max_value,
1691            sum_data: to_bbi_f32(self.window.sum_data),
1692            sum_squared: to_bbi_f32(self.window.sum_squared),
1693        });
1694        self.record_count += 1;
1695        if self.records.len() >= self.items_per_slot {
1696            self.flush_block()?;
1697        }
1698        Ok(())
1699    }
1700
1701    /// Add a full-resolution interval, splitting it across as many windows as
1702    /// it spans.
1703    ///
1704    /// An interval of a bedGraph can be megabases wide, so at the first level
1705    /// it has to be cut rather than summarised whole. Each piece contributes
1706    /// the bases it actually covers, which is what makes `validCount` the count
1707    /// of bases carrying data and not the width of the window — the distinction
1708    /// every summed or counted query depends on.
1709    fn add_interval(&mut self, chr_index: u32, mut start: i64, end: i64, value: f32) -> Result<()> {
1710        while start < end {
1711            if !self.window.open || self.window.chr_index != chr_index || start >= self.window.limit
1712            {
1713                self.close_window()?;
1714                self.open_window(chr_index, start, value, value);
1715            }
1716            let part_end = end.min(self.window.limit);
1717            let overlap = part_end - start;
1718            self.window.valid_count += overlap as u64;
1719            self.window.sum_data += value as f64 * overlap as f64;
1720            self.window.sum_squared += value as f64 * value as f64 * overlap as f64;
1721            if value < self.window.min_value {
1722                self.window.min_value = value;
1723            }
1724            if value > self.window.max_value {
1725                self.window.max_value = value;
1726            }
1727            self.window.end = part_end;
1728            start = part_end;
1729            if start >= self.window.limit {
1730                self.close_window()?;
1731            }
1732        }
1733        Ok(())
1734    }
1735
1736    /// Merge a record of the level below, whole.
1737    ///
1738    /// Unlike an interval, an already summarised record is never cut. Splitting
1739    /// one would mean guessing how its mass sits inside it, so the coarse
1740    /// window is allowed to overrun instead, which is what UCSC does and what
1741    /// keeps the sums exactly conserved across levels.
1742    fn add_record(&mut self, record: &ZoomRecord) -> Result<()> {
1743        if record.valid_count == 0 {
1744            return Ok(());
1745        }
1746        if !self.window.open
1747            || self.window.chr_index != record.chr_index
1748            || record.chr_start as i64 >= self.window.limit
1749        {
1750            self.close_window()?;
1751            self.open_window(
1752                record.chr_index,
1753                record.chr_start as i64,
1754                record.min_value,
1755                record.max_value,
1756            );
1757        }
1758        self.window.valid_count += record.valid_count as u64;
1759        self.window.sum_data += record.sum_data as f64;
1760        self.window.sum_squared += record.sum_squared as f64;
1761        if record.min_value < self.window.min_value {
1762            self.window.min_value = record.min_value;
1763        }
1764        if record.max_value > self.window.max_value {
1765            self.window.max_value = record.max_value;
1766        }
1767        if record.chr_end as i64 > self.window.end {
1768            self.window.end = record.chr_end as i64;
1769        }
1770        Ok(())
1771    }
1772
1773    /// Close the buffered records into one block.
1774    ///
1775    /// A block may straddle chromosomes, and should. A zoom record carries its
1776    /// own chromosome id, unlike a wig section, and a genome of thousands of
1777    /// scaffolds would otherwise end up with a 32-byte block per scaffold and
1778    /// an index larger than the data it indexes.
1779    fn flush_block(&mut self) -> Result<()> {
1780        if self.records.is_empty() {
1781            return Ok(());
1782        }
1783        let mut body = Vec::with_capacity(self.records.len() * ZOOM_RECORD_SIZE as usize);
1784        for record in &self.records {
1785            record.write(&mut body);
1786        }
1787        let first = self.records.first().expect("not empty");
1788        let last = self.records.last().expect("not empty");
1789        let leaf = LeafItem {
1790            start_chr: first.chr_index,
1791            start_base: first.chr_start,
1792            // Windows never overlap and go out in order, so the last record of
1793            // the block is also the furthest it reaches.
1794            end_chr: last.chr_index,
1795            end_base: last.chr_end,
1796            offset: 0,
1797            size: 0,
1798        };
1799        self.records.clear();
1800        self.blocks.push((leaf, body));
1801        Ok(())
1802    }
1803
1804    fn finish(&mut self) -> Result<()> {
1805        self.close_window()?;
1806        self.flush_block()
1807    }
1808}
1809
1810impl BbiWriter {
1811    /// Hand every block the builder has closed to the writer's own pipeline.
1812    ///
1813    /// Called as the level is produced rather than once at the end. Two things
1814    /// come of that: what the pass holds is bounded by `pending_limit` — which
1815    /// is what bounds the data pass too — instead of by the size of the level,
1816    /// and the deflate runs on the pool rather than inline on this thread,
1817    /// which had the workers idle through the longest part of `close()`.
1818    ///
1819    /// Appending while the pass reads is safe: it only ever reads offsets
1820    /// written before it started — the data section, or the level below — and
1821    /// only ever appends past the end of them.
1822    fn drain_zoom_blocks(&mut self, builder: &mut ZoomLevelBuilder) -> Result<()> {
1823        // Taken rather than iterated in place: `submit_block` borrows the
1824        // writer, and the builder is not part of it.
1825        for (leaf, body) in std::mem::take(&mut builder.blocks) {
1826            self.submit_block(leaf, body, None)?;
1827        }
1828        Ok(())
1829    }
1830
1831    /// Build every zoom level, from the finished data section outwards.
1832    fn write_zoom_levels(&mut self) -> Result<()> {
1833        if !self.ladder_frozen {
1834            self.freeze_zoom_ladder();
1835        }
1836        if self.data_items.is_empty() {
1837            return Ok(());
1838        }
1839
1840        // The finest level worth keeping is the first whose records take at
1841        // most half the room the data does: a summary no smaller than what it
1842        // summarises is never worth reading in its place.
1843        //
1844        // Both sides are measured uncompressed. The two compress nothing like
1845        // as well as each other — a zoom record is eight numbers, four of them
1846        // floats, where a wig section is a column of coordinates deflate eats
1847        // almost entirely — so assuming they do picks a level about one rung
1848        // too fine, and a rung is a factor of four.
1849        //
1850        // The counters only saw items that arrived after the ladder was fixed,
1851        // so what they hold is scaled up to the whole file.
1852        let counted = (self.item_count - self.ladder_start_item_count).max(1);
1853        let sample_scale = self.item_count as f64 / counted as f64;
1854        let mut first_level = MAX_ZOOM_LEVELS - 1;
1855        for level in 0..MAX_ZOOM_LEVELS {
1856            let estimate =
1857                self.zoom_res_sizes[level] as f64 * sample_scale * ZOOM_RECORD_SIZE as f64;
1858            if estimate <= self.data_body_size as f64 / 2.0 {
1859                first_level = level;
1860                break;
1861            }
1862        }
1863
1864        // Taken, not cloned: nothing reads `data_items` after this, and on a
1865        // whole-genome 1 bp file the list is a leaf per block — tens of
1866        // megabytes that were being held twice for no reason.
1867        let mut source = std::mem::take(&mut self.data_items);
1868        let mut from_data = true;
1869        let mut previous_count: Option<u64> = None;
1870        for level in first_level..MAX_ZOOM_LEVELS {
1871            let reduction = self.zoom_reductions[level];
1872            if reduction > 0xFFFF_FFFF {
1873                break;
1874            }
1875            let (count, header, items) = self.write_zoom_level(reduction, &source, from_data)?;
1876            if count == 0 {
1877                break;
1878            }
1879            self.zoom_headers.push(header);
1880            source = items;
1881            from_data = false;
1882            // A level indexed by a single node answers any query in one index
1883            // read and one block read, and so does every coarser level. UCSC
1884            // keeps going down to a handful of records; on a 3 Mb test file
1885            // each extra level cost 8 kB — an index node is padded to
1886            // block_size slots — to hold a hundred bytes, and changed no query.
1887            if count <= self.block_size as u64 {
1888                break;
1889            }
1890            // Data sparse enough that every item is its own window at every
1891            // resolution, so widening the windows changes nothing.
1892            if previous_count.is_some_and(|p| count >= p) {
1893                break;
1894            }
1895            previous_count = Some(count);
1896        }
1897        Ok(())
1898    }
1899
1900    /// Build one zoom level out of `source`, which is the data section for the
1901    /// first level and the level below for every one after it.
1902    fn write_zoom_level(
1903        &mut self,
1904        reduction: i64,
1905        source: &[LeafItem],
1906        from_data: bool,
1907    ) -> Result<(u64, ZoomHeader, Vec<LeafItem>)> {
1908        let data_offset = self.sync_cursor()?;
1909        self.emit(&0u32.to_le_bytes())?; // record count, patched below
1910
1911        let mut builder = ZoomLevelBuilder::new(reduction, self.items_per_slot);
1912        // A bigBed summarises the depth of coverage its entries make, so the
1913        // first level runs the same sweep over them the summary did, this time
1914        // reporting into the reducer.
1915        let mut sweep = CoverageSweep::default();
1916
1917        // Every block placed from here until the level is finished belongs to
1918        // this level, not to the data section. Set before the first
1919        // `drain_zoom_blocks`, cleared once the pipeline has drained.
1920        self.zoom_items = Some(Vec::new());
1921
1922        // Blocks are consumed in strict source order, which the reducer
1923        // requires: add_interval, add_record and the sweep all walk a position
1924        // that only ever moves forward.
1925        //
1926        // The result is threaded through `run` so that a failure anywhere in it
1927        // still clears `zoom_items` on the way out: leaving the writer in zoom
1928        // mode would send the *next* file's data blocks into a level's index.
1929        let run = (|| -> Result<()> {
1930            for item in source {
1931                let raw = {
1932                    let source = self.sink()?.as_source()?;
1933                    source.read_exact_at(item.offset, item.size as usize)?
1934                };
1935                let block = if self.compression_level > 0 {
1936                    super::block::decompress(raw, self.uncompress_buffer_size as u32, &self.path)?
1937                } else {
1938                    raw
1939                };
1940                if from_data && self.is_bigwig() {
1941                    let header = read_wig_header(&block, &self.path)?;
1942                    for i in 0..header.item_count as usize {
1943                        let item = read_wig_item(&block, &header, i, &self.path)?;
1944                        builder.add_interval(item.chr_index, item.start, item.end, item.value)?;
1945                        // Inside the loop, not only after it: one bedGraph
1946                        // interval can be megabases wide, and at the finest
1947                        // reduction that is a block of records on its own.
1948                        self.drain_zoom_blocks(&mut builder)?;
1949                    }
1950                } else if from_data {
1951                    // Collected rather than reduced inside the visitor: the
1952                    // sweep and the builder both need `&mut`, and the visitor
1953                    // already borrows the block.
1954                    let mut records = Vec::new();
1955                    super::block::visit_bed_records(&block, &self.path, |chr, start, end| {
1956                        records.push((chr, start, end))
1957                    })?;
1958                    for (chr, start, end) in records {
1959                        for (chr, s, e, depth) in sweep.add(chr, start, end) {
1960                            builder.add_interval(chr, s, e, depth)?;
1961                        }
1962                        self.drain_zoom_blocks(&mut builder)?;
1963                    }
1964                } else {
1965                    let count = block.len() / ZOOM_RECORD_SIZE as usize;
1966                    for i in 0..count {
1967                        let record = ZoomRecord::read(&block, i * ZOOM_RECORD_SIZE as usize);
1968                        builder.add_record(&record)?;
1969                    }
1970                    self.drain_zoom_blocks(&mut builder)?;
1971                }
1972            }
1973            if from_data && !self.is_bigwig() {
1974                for (chr, s, e, depth) in sweep.finish() {
1975                    builder.add_interval(chr, s, e, depth)?;
1976                }
1977            }
1978            builder.finish()?;
1979            self.drain_zoom_blocks(&mut builder)?;
1980            // Every block of this level is placed once the pipeline is empty,
1981            // which is what makes the items below complete and in file order.
1982            self.drain()
1983        })();
1984        let items = self.zoom_items.take().unwrap_or_default();
1985        run?;
1986
1987        if builder.record_count == 0 {
1988            return Ok((
1989                0,
1990                ZoomHeader {
1991                    reduction_level: 0,
1992                    data_offset: 0,
1993                    index_offset: 0,
1994                },
1995                Vec::new(),
1996            ));
1997        }
1998
1999        let count = to_bbi_u32(builder.record_count as i64, "zoomCount")?;
2000        self.patch(data_offset, &count.to_le_bytes())?;
2001
2002        let index_offset = self.sync_cursor()?;
2003        let mut bytes = Vec::new();
2004        super::rtree::write_tree(
2005            &items,
2006            index_offset,
2007            self.block_size,
2008            self.items_per_slot as u32,
2009            index_offset,
2010            &mut |b| bytes.extend_from_slice(b),
2011        )?;
2012        self.emit(&bytes)?;
2013
2014        Ok((
2015            builder.record_count,
2016            ZoomHeader {
2017                reduction_level: to_bbi_u32(reduction, "reductionLevel")?,
2018                data_offset,
2019                index_offset,
2020            },
2021            items,
2022        ))
2023    }
2024}
2025
2026#[cfg(test)]
2027mod tests {
2028    use super::*;
2029    use crate::bbi::{BbiReader, Zoom};
2030
2031    fn temp(name: &str) -> std::path::PathBuf {
2032        let dir = std::env::temp_dir().join("gwseq_writer_tests");
2033        std::fs::create_dir_all(&dir).unwrap();
2034        dir.join(name)
2035    }
2036
2037    fn sizes(pairs: &[(&str, i64)]) -> ChrMap {
2038        ChrMap::from_entries(pairs.iter().map(|(a, b)| ((*a).to_string(), *b)))
2039    }
2040
2041    fn wig_options(chr_sizes: Option<ChrMap>, parallel: i64) -> BbiWriterOptions {
2042        BbiWriterOptions {
2043            kind: BbiKind::BigWig,
2044            chr_sizes,
2045            parallel,
2046            ..Default::default()
2047        }
2048    }
2049
2050    /// Every read path a caller has, over the whole of one chromosome.
2051    fn read_back(path: &std::path::Path, chr: &str, end: i64) -> Vec<f32> {
2052        let reader = BbiReader::open(path.to_str().unwrap(), 1, 1.0 / 3.0, None, None).unwrap();
2053        let request = crate::bbi::ValuesRequest::new(
2054            crate::genomic::Locs::spans(&[chr.to_string()], &[0], &[end]).unwrap(),
2055        )
2056        .bin_size(1.0)
2057        .def_value(f32::NAN);
2058        reader
2059            .read_values(&request)
2060            .unwrap()
2061            .into_raw_vec_and_offset()
2062            .0
2063    }
2064
2065    #[test]
2066    fn a_written_bigwig_reads_back_value_for_value() {
2067        let path = temp("values.bigwig");
2068        let values: Vec<f32> = (0..5000).map(|i| (i as f32) * 0.25).collect();
2069        {
2070            let mut w = BbiWriter::create(
2071                path.to_str().unwrap(),
2072                wig_options(Some(sizes(&[("chr1", 5000)])), 1),
2073            )
2074            .unwrap();
2075            w.write_values("chr1", 0, 1, &values).unwrap();
2076            w.close().unwrap();
2077        }
2078        let got = read_back(&path, "chr1", 5000);
2079        assert_eq!(got.len(), 5000);
2080        assert_eq!(got, values);
2081        std::fs::remove_file(&path).ok();
2082    }
2083
2084    #[test]
2085    fn the_reader_sees_the_header_the_writer_wrote() {
2086        let path = temp("header.bigwig");
2087        {
2088            let mut w = BbiWriter::create(
2089                path.to_str().unwrap(),
2090                wig_options(Some(sizes(&[("chr1", 1000), ("chr2", 500)])), 1),
2091            )
2092            .unwrap();
2093            for i in 0..1000 {
2094                w.write_value("chr1", i, i + 1, i as f32).unwrap();
2095            }
2096            for i in 0..500 {
2097                w.write_value("chr2", i, i + 1, 1.0).unwrap();
2098            }
2099            w.close().unwrap();
2100        }
2101        let reader = BbiReader::open(path.to_str().unwrap(), 1, 1.0 / 3.0, None, None).unwrap();
2102        assert_eq!(reader.kind(), BbiKind::BigWig);
2103        assert_eq!(reader.header().version, BBI_OUTPUT_VERSION);
2104        assert_eq!(reader.chr_sizes().len(), 2);
2105        assert_eq!(reader.chr_sizes().resolve("chr2").unwrap().size, 500);
2106        let summary = reader.total_summary();
2107        assert_eq!(summary.bases_covered, 1500);
2108        assert_eq!(summary.min_value, 0.0);
2109        assert_eq!(summary.max_value, 999.0);
2110        // sum over 0..1000 is 499500, plus 500 ones.
2111        assert_eq!(summary.sum_data, 500_000.0);
2112        std::fs::remove_file(&path).ok();
2113    }
2114
2115    #[test]
2116    fn a_written_file_carries_zoom_levels_that_read_back() {
2117        let path = temp("zoom.bigwig");
2118        {
2119            let mut w = BbiWriter::create(
2120                path.to_str().unwrap(),
2121                wig_options(Some(sizes(&[("chr1", 100_000)])), 1),
2122            )
2123            .unwrap();
2124            let values: Vec<f32> = (0..100_000).map(|i| (i % 97) as f32).collect();
2125            w.write_values("chr1", 0, 1, &values).unwrap();
2126            w.close().unwrap();
2127        }
2128        let reader = BbiReader::open(path.to_str().unwrap(), 1, 1.0 / 3.0, None, None).unwrap();
2129        assert!(
2130            !reader.zoom_headers().is_empty(),
2131            "no zoom levels were written"
2132        );
2133        // Reading through a zoom level has to agree with reading through the
2134        // data, which is what a summary is for.
2135        let request = |zoom| {
2136            crate::bbi::ValuesRequest::new(
2137                crate::genomic::Locs::spans(&["chr1".into()], &[0], &[100_000]).unwrap(),
2138            )
2139            .bin_size(10_000.0)
2140            .zoom(zoom)
2141        };
2142        let full = reader.read_values(&request(Zoom::Full)).unwrap();
2143        let zoomed = reader.read_values(&request(Zoom::Auto)).unwrap();
2144        for (a, b) in full.iter().zip(zoomed.iter()) {
2145            assert!((a - b).abs() < 0.5, "{a} vs {b}");
2146        }
2147        std::fs::remove_file(&path).ok();
2148    }
2149
2150    #[test]
2151    fn a_written_bigbed_reads_back_entry_for_entry() {
2152        let path = temp("entries.bigbed");
2153        let fields: IndexMap<String, String> = [
2154            ("chr", "string"),
2155            ("start", "uint"),
2156            ("end", "uint"),
2157            ("name", "string"),
2158            ("score", "uint"),
2159        ]
2160        .into_iter()
2161        .map(|(a, b)| (a.to_string(), b.to_string()))
2162        .collect();
2163        {
2164            let mut w = BbiWriter::create(
2165                path.to_str().unwrap(),
2166                BbiWriterOptions {
2167                    kind: BbiKind::BigBed,
2168                    chr_sizes: Some(sizes(&[("chr1", 10_000)])),
2169                    fields: fields.clone(),
2170                    parallel: 1,
2171                    ..Default::default()
2172                },
2173            )
2174            .unwrap();
2175            for i in 0..1000i64 {
2176                let values: IndexMap<String, String> = [
2177                    ("name".to_string(), format!("item{i}")),
2178                    ("score".to_string(), (i % 1000).to_string()),
2179                ]
2180                .into_iter()
2181                .collect();
2182                w.write_entry("chr1", i * 5, i * 5 + 8, &values).unwrap();
2183            }
2184            w.close().unwrap();
2185        }
2186        let reader = BbiReader::open(path.to_str().unwrap(), 1, 1.0 / 3.0, None, None).unwrap();
2187        assert_eq!(reader.kind(), BbiKind::BigBed);
2188        assert_eq!(
2189            reader
2190                .auto_sql()
2191                .keys()
2192                .map(String::as_str)
2193                .collect::<Vec<_>>(),
2194            ["chrom", "chromStart", "chromEnd", "name", "score"]
2195        );
2196        let request = crate::bbi::EntriesRequest::new(
2197            crate::genomic::Locs::spans(&["chr1".into()], &[0], &[10_000]).unwrap(),
2198        );
2199        let per_locus = reader.read_entries(&request).unwrap();
2200        assert_eq!(per_locus[0].len(), 1000);
2201        assert_eq!(per_locus[0][0].start, 0);
2202        assert_eq!(per_locus[0][0].end, 8);
2203        assert_eq!(per_locus[0][7].fields[0].1, "item7");
2204        assert_eq!(per_locus[0][999].start, 4995);
2205        std::fs::remove_file(&path).ok();
2206    }
2207
2208    #[test]
2209    fn the_deflate_pipeline_writes_the_same_bytes_as_the_serial_path() {
2210        // The pipeline places blocks in submission order, so the file is the
2211        // one a single thread would have written — byte for byte, which is the
2212        // whole promise.
2213        let values: Vec<f32> = (0..40_000).map(|i| ((i * 7) % 251) as f32).collect();
2214        let mut written: Vec<Vec<u8>> = Vec::new();
2215        for parallel in [1i64, 4] {
2216            let path = temp(&format!("parallel{parallel}.bigwig"));
2217            let mut w = BbiWriter::create(
2218                path.to_str().unwrap(),
2219                wig_options(Some(sizes(&[("chr1", 40_000)])), parallel),
2220            )
2221            .unwrap();
2222            w.write_values("chr1", 0, 1, &values).unwrap();
2223            w.close().unwrap();
2224            written.push(std::fs::read(&path).unwrap());
2225            std::fs::remove_file(&path).ok();
2226        }
2227        assert_eq!(written[0].len(), written[1].len());
2228        assert!(written[0] == written[1], "the two files differ");
2229    }
2230
2231    #[test]
2232    fn a_value_that_splits_a_section_is_still_written() {
2233        // The section refuses to widen once it is large enough, and the value
2234        // that made it refuse has to land in the *next* section rather than be
2235        // dropped. Worth its own test because the retry was once inside a
2236        // `debug_assert`, which compiles the call away in a release build: this
2237        // passed in debug and lost one value per split in the wheel. Run the
2238        // suite with `--release` as well as without.
2239        let path = temp("split.bigwig");
2240        {
2241            let mut w = BbiWriter::create(
2242                path.to_str().unwrap(),
2243                wig_options(Some(sizes(&[("chr1", 100_000)])), 1),
2244            )
2245            .unwrap();
2246            // A thousand 10 bp values, then one of a different span: too big a
2247            // section to widen, so it splits.
2248            w.write_values("chr1", 0, 10, &[1.0; 1000]).unwrap();
2249            w.write_value("chr1", 20_000, 20_005, 2.0).unwrap();
2250            w.write_value("chr1", 20_005, 20_010, 2.0).unwrap();
2251            w.close().unwrap();
2252        }
2253        let reader = BbiReader::open(path.to_str().unwrap(), 1, 1.0 / 3.0, None, None).unwrap();
2254        // 10 000 bases of the run plus 10 of the two values after it.
2255        assert_eq!(reader.total_summary().bases_covered, 10_010);
2256        let request = crate::bbi::ValuesRequest::new(
2257            crate::genomic::Locs::spans(&["chr1".into()], &[20_000], &[20_010]).unwrap(),
2258        )
2259        .bin_size(1.0)
2260        .def_value(-9.0);
2261        let got = reader.read_values(&request).unwrap();
2262        assert!(
2263            got.iter().all(|v| *v == 2.0),
2264            "the value that split the section was dropped: {got:?}"
2265        );
2266        std::fs::remove_file(&path).ok();
2267    }
2268
2269    /// What the converters promise an interrupted run leaves behind: nothing.
2270    /// A zero-magic stub is no reader's idea of a bigWig, which is right, but
2271    /// it is still a file the caller has to clean up.
2272    #[test]
2273    fn an_abandoned_file_is_removed() {
2274        let path = temp("abandoned.bigwig");
2275        {
2276            let mut w = BbiWriter::create(
2277                path.to_str().unwrap(),
2278                wig_options(Some(sizes(&[("chr1", 100)])), 1),
2279            )
2280            .unwrap();
2281            w.write_value("chr1", 0, 10, 1.0).unwrap();
2282            w.abandon();
2283            // Twice, and after the sink has already gone.
2284            w.abandon();
2285        }
2286        assert!(!path.exists(), "abandon left {} behind", path.display());
2287    }
2288
2289    #[test]
2290    fn out_of_order_and_overlapping_values_are_refused() {
2291        let path = temp("order.bigwig");
2292        let mut w = BbiWriter::create(
2293            path.to_str().unwrap(),
2294            wig_options(Some(sizes(&[("chr1", 1000), ("chr2", 1000)])), 1),
2295        )
2296        .unwrap();
2297        w.write_value("chr1", 100, 200, 1.0).unwrap();
2298        let err = w
2299            .write_value("chr1", 150, 250, 1.0)
2300            .unwrap_err()
2301            .to_string();
2302        assert!(err.contains("starts before the end 200"), "{err}");
2303        // And a chromosome cannot be returned to once left.
2304        let mut w2 = BbiWriter::create(
2305            path.to_str().unwrap(),
2306            wig_options(Some(sizes(&[("chr1", 1000), ("chr2", 1000)])), 1),
2307        )
2308        .unwrap();
2309        w2.write_value("chr1", 0, 10, 1.0).unwrap();
2310        w2.write_value("chr2", 0, 10, 1.0).unwrap();
2311        let err = w2.write_value("chr1", 20, 30, 1.0).unwrap_err().to_string();
2312        assert!(err.contains("was already written"), "{err}");
2313        std::fs::remove_file(&path).ok();
2314    }
2315
2316    #[test]
2317    fn a_value_hanging_over_a_chromosome_is_clipped_and_one_past_it_is_refused() {
2318        let path = temp("clip.bigwig");
2319        let mut w = BbiWriter::create(
2320            path.to_str().unwrap(),
2321            wig_options(Some(sizes(&[("chr1", 95)])), 1),
2322        )
2323        .unwrap();
2324        // A 10 bp grid over a 95 bp chromosome: the last bin overshoots by 5.
2325        w.write_values("chr1", 0, 10, &[1.0; 10]).unwrap();
2326        assert_eq!(w.clipped_count(), 1);
2327        let err = w.write_value("chr1", 95, 105, 1.0).unwrap_err().to_string();
2328        assert!(err.contains("starts past the end"), "{err}");
2329        std::fs::remove_file(&path).ok();
2330    }
2331
2332    #[test]
2333    fn a_non_finite_value_is_skipped_rather_than_written() {
2334        let path = temp("skip.bigwig");
2335        {
2336            let mut w = BbiWriter::create(
2337                path.to_str().unwrap(),
2338                wig_options(Some(sizes(&[("chr1", 5)])), 1),
2339            )
2340            .unwrap();
2341            w.write_values("chr1", 0, 1, &[1.0, f32::NAN, 3.0, f32::INFINITY, 5.0])
2342                .unwrap();
2343            assert_eq!(w.skipped_count(), 2);
2344            w.close().unwrap();
2345        }
2346        let got = read_back(&path, "chr1", 5);
2347        assert_eq!(got[0], 1.0);
2348        assert!(got[1].is_nan(), "the gap is a gap");
2349        assert_eq!(got[2], 3.0);
2350        assert!(got[3].is_nan());
2351        assert_eq!(got[4], 5.0);
2352        std::fs::remove_file(&path).ok();
2353    }
2354
2355    #[test]
2356    fn an_undeclared_chromosome_grows_to_what_is_written_to_it() {
2357        let path = temp("infer.bigwig");
2358        {
2359            let mut w = BbiWriter::create(path.to_str().unwrap(), wig_options(None, 1)).unwrap();
2360            w.write_value("chrZ", 100, 250, 1.0).unwrap();
2361            w.close().unwrap();
2362        }
2363        let reader = BbiReader::open(path.to_str().unwrap(), 1, 1.0 / 3.0, None, None).unwrap();
2364        assert_eq!(reader.chr_sizes().resolve("chrZ").unwrap().size, 250);
2365        std::fs::remove_file(&path).ok();
2366    }
2367
2368    #[test]
2369    fn writing_through_a_closed_writer_is_refused() {
2370        let path = temp("closed.bigwig");
2371        let mut w = BbiWriter::create(
2372            path.to_str().unwrap(),
2373            wig_options(Some(sizes(&[("chr1", 10)])), 1),
2374        )
2375        .unwrap();
2376        w.write_value("chr1", 0, 5, 1.0).unwrap();
2377        w.close().unwrap();
2378        w.close().unwrap(); // idempotent
2379        let err = w.write_value("chr1", 5, 10, 1.0).unwrap_err().to_string();
2380        assert!(err.contains("file is closed"), "{err}");
2381        std::fs::remove_file(&path).ok();
2382    }
2383
2384    #[test]
2385    fn the_open_time_checks_refuse_what_the_format_cannot_hold() {
2386        let path = temp("bad.bigwig");
2387        let p = path.to_str().unwrap();
2388        let bad = |o: BbiWriterOptions| BbiWriter::create(p, o).unwrap_err().to_string();
2389        assert!(bad(BbiWriterOptions {
2390            items_per_slot: Some(0),
2391            ..Default::default()
2392        })
2393        .contains("items_per_slot 0 invalid"));
2394        assert!(bad(BbiWriterOptions {
2395            items_per_slot: Some(70_000),
2396            ..Default::default()
2397        })
2398        .contains("items_per_slot 70000 invalid"));
2399        assert!(bad(BbiWriterOptions {
2400            block_size: 1,
2401            ..Default::default()
2402        })
2403        .contains("block_size 1 invalid"));
2404        assert!(bad(BbiWriterOptions {
2405            compression_level: 10,
2406            ..Default::default()
2407        })
2408        .contains("compression_level 10 invalid"));
2409        assert!(bad(BbiWriterOptions {
2410            chr_sizes: Some(sizes(&[("chr1", 0)])),
2411            ..Default::default()
2412        })
2413        .contains("must be positive"));
2414        assert!(
2415            BbiWriter::create("https://example.org/x.bigwig", BbiWriterOptions::default())
2416                .unwrap_err()
2417                .to_string()
2418                .contains("cannot be written to")
2419        );
2420        std::fs::remove_file(&path).ok();
2421    }
2422
2423    #[test]
2424    fn the_coverage_sweep_reports_the_depth_of_nested_entries() {
2425        let mut sweep = CoverageSweep::default();
2426        let mut runs = Vec::new();
2427        // [0,10) and [5,20) overlap on [5,10), where the depth is two.
2428        runs.extend(sweep.add(0, 0, 10));
2429        runs.extend(sweep.add(0, 5, 20));
2430        runs.extend(sweep.finish());
2431        assert_eq!(runs, [(0, 0, 5, 1.0), (0, 5, 10, 2.0), (0, 10, 20, 1.0)]);
2432    }
2433
2434    #[test]
2435    fn the_coverage_sweep_closes_a_chromosome_before_starting_the_next() {
2436        let mut sweep = CoverageSweep::default();
2437        let mut runs = Vec::new();
2438        runs.extend(sweep.add(0, 0, 10));
2439        runs.extend(sweep.add(1, 0, 10));
2440        runs.extend(sweep.finish());
2441        assert_eq!(runs, [(0, 0, 10, 1.0), (1, 0, 10, 1.0)]);
2442    }
2443}