1use 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
61pub const WIG_ITEMS_PER_SLOT: usize = 1024;
66pub const BED_ITEMS_PER_SLOT: usize = 512;
67
68pub const COMPRESSION_LEVEL: u32 = 6;
71
72const RESOLUTION_SAMPLE_ITEMS: u64 = 4096;
76
77const DATA_COUNT_SIZE: u64 = 8;
79
80const MAX_ZOOM_LEVELS: usize = 10;
82const INITIAL_ZOOM_FACTOR: i64 = 10;
85const ZOOM_INCREMENT: i64 = 4;
86const 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 pub chr_sizes: Option<ChrMap>,
114 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 pub section_policy: SectionPolicy,
124 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#[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#[derive(Debug, Clone, Copy)]
171struct ChrState {
172 index: u32,
173 size: i64,
174 declared: bool,
175}
176
177struct PendingBlock {
182 leaf: LeafItem,
183 result: PendingResult,
186 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 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 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 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 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 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 skipped_count: u64,
268 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 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 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 let parallel = resolve_parallel(options.parallel);
340 let (executor, pending_limit) = if parallel > 1 {
341 (Some(Executor::new(options.parallel)?), parallel * 4)
346 } else {
347 (None, 0)
348 };
349
350 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn close(&mut self) -> Result<()> {
1247 if self.closed {
1248 return Ok(());
1249 }
1250 self.failed = true;
1251 let result = self.finish();
1252 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 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 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 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 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 self.patch(4, &bytes[4..])?;
1384 let magic = self.magic();
1385 self.patch(0, &magic.to_le_bytes())
1386 }
1387
1388 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 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 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 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#[derive(Debug, Default)]
1483pub(crate) struct CoverageSweep {
1484 chr_index: Option<u32>,
1485 position: i64,
1486 ends: BinaryHeap<std::cmp::Reverse<i64>>,
1489}
1490
1491type Run = (u32, i64, i64, f32);
1493
1494impl CoverageSweep {
1495 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 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 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#[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#[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
1625struct ZoomLevelBuilder {
1634 reduction: i64,
1635 items_per_slot: usize,
1636 window: ZoomWindow,
1637 records: Vec<ZoomRecord>,
1638 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 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 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 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 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 fn drain_zoom_blocks(&mut self, builder: &mut ZoomLevelBuilder) -> Result<()> {
1823 for (leaf, body) in std::mem::take(&mut builder.blocks) {
1826 self.submit_block(leaf, body, None)?;
1827 }
1828 Ok(())
1829 }
1830
1831 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 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 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 if count <= self.block_size as u64 {
1888 break;
1889 }
1890 if previous_count.is_some_and(|p| count >= p) {
1893 break;
1894 }
1895 previous_count = Some(count);
1896 }
1897 Ok(())
1898 }
1899
1900 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())?; let mut builder = ZoomLevelBuilder::new(reduction, self.items_per_slot);
1912 let mut sweep = CoverageSweep::default();
1916
1917 self.zoom_items = Some(Vec::new());
1921
1922 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 self.drain_zoom_blocks(&mut builder)?;
1949 }
1950 } else if from_data {
1951 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 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 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 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 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 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 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 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 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 #[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 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 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 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(); 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 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}