Skip to main content

gwseq_io/bbi/
convert.rs

1//! bedGraph / WIG → bigWig, BED → bigBed.
2//!
3//! Streaming throughout: nothing is sorted or spooled,
4//! so a conversion of any size holds a megabyte of input and one open section.
5//! Input that is not already pooled by chromosome and ordered raises, naming
6//! the line — which is what `sort -k1,1 -k2,2n` is for.
7
8use std::io::{BufRead, Read as _};
9use std::path::Path;
10
11use indexmap::IndexMap;
12
13use crate::bbi::header::{BbiKind, BED_FIELD_NAMES};
14use crate::bbi::writer::{BbiWriter, BbiWriterOptions};
15use crate::error::{Error, Result};
16use crate::genomic::ChrMap;
17use crate::progress::{CancelFlag, ProgressFn, ProgressTracker};
18
19/// Values gathered before they are handed to the writer in one call.
20const VALUE_BATCH: usize = 65536;
21/// Lines between two progress reports.
22const PROGRESS_INTERVAL: u64 = 65536;
23/// A line longer than this is a binary file being read as text, not a record.
24const MAX_LINE_SIZE: usize = 16 << 20;
25
26/// What an ordering complaint is asking for. Appended to anything the *writer*
27/// refuses, which is where an out-of-order or overlapping input surfaces — the
28/// converter itself cannot tell that from a malformed line, and the caller's
29/// next move is the same either way.
30const ORDER_HINT: &str = "input must be pooled by chromosome and sorted by start, \
31                          eg with `sort -k1,1 -k2,2n`";
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum TextFormat {
35    BedGraph,
36    Wig,
37    Bed,
38}
39
40impl TextFormat {
41    pub fn as_str(self) -> &'static str {
42        match self {
43            TextFormat::BedGraph => "bedgraph",
44            TextFormat::Wig => "wig",
45            TextFormat::Bed => "bed",
46        }
47    }
48}
49
50#[derive(Debug, Clone)]
51pub struct ConvertResult {
52    pub format: TextFormat,
53    pub line_count: u64,
54    pub item_count: u64,
55    pub skipped_count: u64,
56    pub clipped_count: u64,
57    pub chr_sizes: Vec<(String, i64)>,
58}
59
60/// Reads lines and says which one it is on, so a failure names it.
61struct LineReader {
62    input: crate::source::TextInput,
63    path: String,
64    size: u64,
65    consumed: u64,
66    line_number: u64,
67    buffer: Vec<u8>,
68}
69
70impl LineReader {
71    fn open(path: &Path) -> Result<Self> {
72        let (input, size) = crate::source::open_text(path)?;
73        Ok(Self {
74            input,
75            path: path.to_string_lossy().into_owned(),
76            size,
77            consumed: 0,
78            line_number: 0,
79            buffer: Vec::with_capacity(4096),
80        })
81    }
82
83    /// The next line, with its newline and any carriage return before it
84    /// stripped. `None` at the end of the file; a final line with no newline of
85    /// its own is still returned.
86    /// The next line into `out`, `false` at the end of the file.
87    ///
88    /// Into the caller's buffer rather than back as a `&str`, so the loop can
89    /// hold the line and still call `fail`/`guard` — which borrow the reader
90    /// for the path and the line number. Returning a borrow meant a
91    /// `to_string()` per line to break that conflict, which on a multi-GB
92    /// bedGraph is an allocation per record; one buffer reused for the file is
93    /// a `memcpy` instead.
94    fn read_line_into(&mut self, out: &mut String) -> Result<bool> {
95        let Some(line) = self.next_line()? else {
96            return Ok(false);
97        };
98        // Two moves of the same bytes would be one too many, but the borrow
99        // checker cannot see that `line` comes out of `self` — and this is the
100        // copy the `to_string` was making anyway, minus the allocation.
101        out.clear();
102        out.push_str(line);
103        Ok(true)
104    }
105
106    fn next_line(&mut self) -> Result<Option<&str>> {
107        self.buffer.clear();
108        // Capped as it is read, not after: `read_until` on a binary file with
109        // no newline in it grows the buffer to the whole file before anything
110        // gets to complain about the length. One byte over the limit is enough
111        // to tell that it was exceeded.
112        let read = self
113            .input
114            .by_ref()
115            .take(MAX_LINE_SIZE as u64 + 1)
116            .read_until(b'\n', &mut self.buffer)
117            .map_err(|e| Error::io(&self.path, e))?;
118        if read == 0 {
119            return Ok(None);
120        }
121        // Counted before the trim, since this is progress through the file.
122        self.consumed += read as u64;
123        self.line_number += 1;
124        if self.buffer.len() > MAX_LINE_SIZE {
125            return Err(Error::format(
126                &self.path,
127                format!(
128                    "line {} is longer than {MAX_LINE_SIZE} bytes",
129                    self.line_number
130                ),
131            ));
132        }
133        while matches!(self.buffer.last(), Some(b'\n' | b'\r')) {
134            self.buffer.pop();
135        }
136        // Lossy rather than strict, and lossy *per byte*: a bed's name column
137        // carries whatever the annotation had in it, and refusing a file for
138        // one bad byte in a comment would be worse than passing the replacement
139        // character on. Replacing the whole line with the one replacement
140        // character — which is what `unwrap_or` did here — is worse still: a
141        // `# café` in Latin-1 became a line that is neither a record nor a
142        // declaration, and a bed with one such byte in a name column became a
143        // line with one column.
144        //
145        // The fix-up rewrites the buffer rather than staging the string
146        // elsewhere, so this still hands back a borrow of one place. It costs a
147        // second validation pass on every line, which beside the allocation and
148        // the split this line is about to go through is nothing.
149        if std::str::from_utf8(&self.buffer).is_err() {
150            self.buffer = String::from_utf8_lossy(&self.buffer)
151                .into_owned()
152                .into_bytes();
153        }
154        Ok(Some(
155            std::str::from_utf8(&self.buffer).expect("the buffer above is valid utf-8 or replaced"),
156        ))
157    }
158
159    /// Wrap an error with the line it happened on, which is what makes a
160    /// conversion failure actionable.
161    ///
162    /// `Format`, not `InvalidArgument`: everything that reaches here is a
163    /// complaint about the *input file* — a column that will not parse, a
164    /// record out of order, a coordinate past the end of its chromosome — and
165    /// the hierarchy's own definition puts that under `InvalidFile`, which is
166    /// what a caller catching "the file is bad" reaches for. `InvalidArgument`
167    /// is for what the caller asked for, and Python surfaces it as a
168    /// `ValueError`, which a malformed bedGraph is not. The rendered message is
169    /// unchanged: `Format` prints as `{path}: {what}`.
170    ///
171    /// Every helper that parses a field is wrapped through here, so their own
172    /// variants never reach a caller.
173    fn fail(&self, message: impl std::fmt::Display) -> Error {
174        Error::format(&self.path, format!("line {}: {message}", self.line_number))
175    }
176
177    /// As `fail`, and says what an ordering complaint is asking for. Used
178    /// around the calls into the writer, which is where one surfaces.
179    fn guard(&self, message: impl std::fmt::Display) -> Error {
180        Error::format(
181            &self.path,
182            format!("line {}: {message}\n{ORDER_HINT}", self.line_number),
183        )
184    }
185}
186
187// -- line shapes -----------------------------------------------------------
188
189fn is_blank(c: char) -> bool {
190    c == ' ' || c == '\t'
191}
192
193/// Split on runs of spaces and tabs, leading and trailing ones dropped.
194///
195/// What bedGraph and WIG are read with. UCSC's readers split them this way, and
196/// neither format has a column that can hold a space, so a run of whitespace is
197/// always a separator and never data.
198fn split_blanks(line: &str) -> Vec<&str> {
199    line.split(is_blank).filter(|f| !f.is_empty()).collect()
200}
201
202/// Split on single tabs, so an empty column stays an empty field.
203///
204/// What BED is read with. A BED is tab-delimited and its name column is allowed
205/// to hold spaces — UCSC's own tables are full of them — so splitting a BED on
206/// whitespace would cut a name in half.
207fn split_tabs(line: &str) -> Vec<&str> {
208    line.split('\t').collect()
209}
210
211/// Drop a trailing empty column that is a formatting artefact rather than a
212/// field.
213///
214/// A line ending on a tab and a line whose last column is genuinely empty look
215/// identical on their own, so this decides by the shape the file has already
216/// shown. `expected` is the column count the first record fixed — or the one
217/// `fields=` declared, when the caller said — and a line carrying exactly one
218/// more than that, empty, ended on a tab. A line already the right width keeps
219/// its empty last column, which is what it is.
220///
221/// Dropping unconditionally, as this once did, made a BED whose `name` column
222/// is empty on some records and not on others fail with "has N columns and the
223/// first one had N+1" — the pop applied to some lines and not to others.
224fn trim_trailing_tab(fields: &mut Vec<&str>, expected: Option<usize>) {
225    if fields.len() < 2 || !fields.last().is_some_and(|f| f.is_empty()) {
226        return;
227    }
228    match expected {
229        // Nothing to compare against yet: the first record of a file with no
230        // declared fields, where a trailing tab is the likelier reading.
231        None => {
232            fields.pop();
233        }
234        Some(width) if fields.len() == width + 1 => {
235            fields.pop();
236        }
237        Some(_) => {}
238    }
239}
240
241/// True when the line begins with `token`, case-insensitively, followed by a
242/// space, a tab or the end.
243fn starts_with_token(line: &str, token: &str) -> bool {
244    let bytes = line.as_bytes();
245    let token = token.as_bytes();
246    if bytes.len() < token.len() {
247        return false;
248    }
249    if !bytes[..token.len()].eq_ignore_ascii_case(token) {
250        return false;
251    }
252    bytes.len() == token.len() || bytes[token.len()] == b' ' || bytes[token.len()] == b'\t'
253}
254
255/// True when a line carries no data: blank, a comment, or one of the `track`
256/// and `browser` declarations a text track is wrapped in.
257fn is_skipped_line(line: &str) -> bool {
258    let trimmed = line.trim_start_matches(is_blank);
259    trimmed.is_empty()
260        || trimmed.starts_with('#')
261        || starts_with_token(trimmed, "track")
262        || starts_with_token(trimmed, "browser")
263}
264
265fn is_declaration(field: &str) -> bool {
266    starts_with_token(field, "fixedstep") || starts_with_token(field, "variablestep")
267}
268
269/// True when the four fields have the shape of a bedGraph record: two whole
270/// numbers and a number after the chromosome.
271///
272/// Only ever asked of the first line carrying data, to settle the format. Once
273/// that is settled the fields are parsed for real, and a later record that does
274/// not parse is an error rather than a change of mind.
275fn is_bedgraph_record(fields: &[&str]) -> bool {
276    fields[1].parse::<i64>().is_ok()
277        && fields[2].parse::<i64>().is_ok()
278        && fields[3].parse::<f64>().is_ok()
279}
280
281/// True when the fields are the body of a WIG section: one number for a
282/// fixedStep, or a position and a number for a variableStep.
283///
284/// Only ever asked of a line that has settled neither format, to tell a WIG
285/// missing its declaration from a file that is not a WIG at all.
286fn is_orphan_wig_data(fields: &[&str]) -> bool {
287    !fields.is_empty() && fields.len() <= 2 && fields.iter().all(|f| f.parse::<f64>().is_ok())
288}
289
290/// Which format the input is comes from its **content**, not its name: the
291/// first line that is neither blank, a comment, nor a `track` or `browser`
292/// declaration decides. A `fixedStep` or `variableStep` line makes it a WIG,
293/// four columns a bedGraph, anything else is refused. The decision is made
294/// once, so a file holding both is refused too.
295pub fn sniff_format(first_data_line: &str) -> Result<TextFormat> {
296    let fields = split_blanks(first_data_line);
297    if fields.first().is_some_and(|f| is_declaration(f)) {
298        return Ok(TextFormat::Wig);
299    }
300    if fields.len() == 4 && is_bedgraph_record(&fields) {
301        return Ok(TextFormat::BedGraph);
302    }
303    if is_orphan_wig_data(&fields) {
304        return Err(Error::invalid(
305            "wig data before any fixedStep or variableStep declaration",
306        ));
307    }
308    Err(Error::invalid(format!(
309        "\"{first_data_line}\" is neither a bedgraph record (chr, start, end, value) \
310         nor a wig declaration (fixedStep or variableStep), so the format of the input \
311         cannot be told"
312    )))
313}
314
315// -- the wig declaration ---------------------------------------------------
316
317/// The declaration a WIG section is written under (the UCSC wiggle format).
318///
319/// WIG coordinates are 1-based, bedGraph 0-based half-open. `step` and `span`
320/// both default to 1; a declaration with no `chrom`, or a `fixedStep` with no
321/// `start`, is an error rather than a guess — a WIG with no start has no
322/// coordinates at all.
323#[derive(Debug, Clone, Default)]
324pub struct WigDeclaration {
325    pub fixed_step: bool,
326    pub chr: String,
327    /// 0-based, converted from the 1-based coordinate the file carries.
328    /// Meaningless for a variableStep.
329    pub start: i64,
330    pub step: i64,
331    pub span: i64,
332}
333
334pub fn parse_wig_declaration(line: &str) -> Result<WigDeclaration> {
335    let fields = split_blanks(line);
336    let mut declaration = WigDeclaration {
337        fixed_step: fields
338            .first()
339            .is_some_and(|f| starts_with_token(f, "fixedstep")),
340        step: 1,
341        span: 1,
342        ..Default::default()
343    };
344    let (mut has_chr, mut has_start) = (false, false);
345    for field in &fields[1..] {
346        let Some((key, value)) = field.split_once('=') else {
347            return Err(Error::invalid(format!(
348                "\"{field}\" is not a key=value of a wig declaration"
349            )));
350        };
351        let number = |what: &str| -> Result<i64> {
352            value
353                .parse::<i64>()
354                .map_err(|_| Error::invalid(format!("could not read \"{value}\" as a {what}")))
355        };
356        match key.to_ascii_lowercase().as_str() {
357            "chrom" => {
358                declaration.chr = value.to_string();
359                has_chr = true;
360            }
361            "start" => {
362                let start = number("start")?;
363                if start < 1 {
364                    return Err(Error::invalid(format!(
365                        "start {start} is not a 1-based coordinate"
366                    )));
367                }
368                declaration.start = start - 1;
369                has_start = true;
370            }
371            "step" => declaration.step = number("step")?,
372            "span" => declaration.span = number("span")?,
373            other => {
374                return Err(Error::invalid(format!(
375                    "{other} is not a wig declaration key (chrom, start, step, span)"
376                )))
377            }
378        }
379    }
380    if !has_chr {
381        return Err(Error::invalid("wig declaration has no chrom"));
382    }
383    if declaration.fixed_step && !has_start {
384        return Err(Error::invalid("fixedStep declaration has no start"));
385    }
386    if declaration.step <= 0 {
387        return Err(Error::invalid(format!(
388            "step {} must be positive",
389            declaration.step
390        )));
391    }
392    if declaration.span <= 0 {
393        return Err(Error::invalid(format!(
394            "span {} must be positive",
395            declaration.span
396        )));
397    }
398    Ok(declaration)
399}
400
401// -- the binning sink ------------------------------------------------------
402
403/// Takes values from the reader and hands them to the writer, binning them
404/// first when a bin size was asked for.
405///
406/// With a bin size the writer is handed bins rather than values, so this
407/// carries the ordering and clipping contracts itself. Otherwise overlapping
408/// input averages into a bin and comes out as a number nothing in the file
409/// says, and a value hanging over a chromosome opens a bin past it and fails
410/// only at the flush, naming a position rather than its line. The wording is
411/// the writer's, so the same input reports the same way either way.
412struct ValueSink {
413    bin_size: i64,
414    declared: Option<ChrMap>,
415    chr: String,
416    /// The open bin, and the run of closed ones waiting to go out.
417    bin: Option<i64>,
418    bin_sum: f64,
419    bin_covered: i64,
420    run_start_bin: Option<i64>,
421    run_values: Vec<f32>,
422    item_count: u64,
423    clipped_count: u64,
424    /// End of the last value taken on this chromosome, and the size the
425    /// declaration gives it. Both reset by `set_chr`.
426    last_end: i64,
427    chr_size: Option<i64>,
428}
429
430impl ValueSink {
431    fn new(bin_size: i64, declared: Option<ChrMap>) -> Self {
432        Self {
433            bin_size,
434            declared,
435            chr: String::new(),
436            bin: None,
437            bin_sum: 0.0,
438            bin_covered: 0,
439            run_start_bin: None,
440            run_values: Vec::new(),
441            item_count: 0,
442            clipped_count: 0,
443            last_end: 0,
444            chr_size: None,
445        }
446    }
447
448    fn binning(&self) -> bool {
449        self.bin_size > 0
450    }
451
452    /// Point the sink at a chromosome, closing what the last one left open.
453    ///
454    /// The declared size is resolved here for its *error*, which the writer
455    /// would raise the same but later: with a bin size it sees no value until a
456    /// bin closes and a run flushes, by which point the line carrying the name
457    /// is thousands behind.
458    fn set_chr(&mut self, writer: &mut BbiWriter, chr: &str) -> Result<()> {
459        if self.chr == chr {
460            return Ok(());
461        }
462        self.finish(writer)?;
463        self.chr.clear();
464        self.chr.push_str(chr);
465        self.last_end = 0;
466        self.chr_size = None;
467        if let Some(declared) = &self.declared {
468            self.chr_size = Some(declared.resolve(chr)?.size);
469        }
470        Ok(())
471    }
472
473    fn add(
474        &mut self,
475        writer: &mut BbiWriter,
476        chr: &str,
477        start: i64,
478        end: i64,
479        value: f32,
480    ) -> Result<()> {
481        self.set_chr(writer, chr)?;
482        self.item_count += 1;
483        if !self.binning() {
484            let chr = std::mem::take(&mut self.chr);
485            let result = writer.write_value(&chr, start, end, value);
486            self.chr = chr;
487            return result;
488        }
489        self.accumulate(writer, start, end, value)
490    }
491
492    fn add_run(
493        &mut self,
494        writer: &mut BbiWriter,
495        chr: &str,
496        start: i64,
497        span: i64,
498        values: &[f32],
499    ) -> Result<()> {
500        if values.is_empty() {
501            return Ok(());
502        }
503        self.set_chr(writer, chr)?;
504        self.item_count += values.len() as u64;
505        if !self.binning() {
506            let chr = std::mem::take(&mut self.chr);
507            let result = writer.write_values(&chr, start, span, values);
508            self.chr = chr;
509            return result;
510        }
511        for (i, value) in values.iter().enumerate() {
512            let i = i as i64;
513            self.accumulate(writer, start + span * i, start + span * (i + 1), *value)?;
514        }
515        Ok(())
516    }
517
518    /// Spread one value over the bins it overlaps.
519    fn accumulate(
520        &mut self,
521        writer: &mut BbiWriter,
522        start: i64,
523        mut end: i64,
524        value: f32,
525    ) -> Result<()> {
526        if start < 0 {
527            return Err(Error::invalid(format!("start {start} is negative")));
528        }
529        if end <= start {
530            return Err(Error::invalid(format!(
531                "end {end} is not past the start {start}"
532            )));
533        }
534        if start < self.last_end {
535            return Err(Error::invalid(format!(
536                "{}:{start}-{end} starts before the end {} of the previous value, values \
537                 must be added in order and without overlap",
538                self.chr, self.last_end
539            )));
540        }
541        if let Some(size) = self.chr_size {
542            if end > size {
543                if start >= size {
544                    return Err(Error::invalid(format!(
545                        "{}:{start}-{end} starts past the end of {}, which is {size} bases long",
546                        self.chr, self.chr
547                    )));
548                }
549                self.clipped_count += 1;
550                end = size;
551            }
552        }
553        self.last_end = end;
554        let mut index = start / self.bin_size;
555        while index * self.bin_size < end {
556            if Some(index) != self.bin {
557                self.close_bin(writer)?;
558                self.bin = Some(index);
559                self.bin_sum = 0.0;
560                self.bin_covered = 0;
561            }
562            let overlap = end.min((index + 1) * self.bin_size) - start.max(index * self.bin_size);
563            self.bin_sum += value as f64 * overlap as f64;
564            self.bin_covered += overlap;
565            index += 1;
566        }
567        Ok(())
568    }
569
570    /// Emit the open bin, into the run when it can extend it.
571    fn close_bin(&mut self, writer: &mut BbiWriter) -> Result<()> {
572        let Some(index) = self.bin.take() else {
573            return Ok(());
574        };
575        let (covered, sum) = (self.bin_covered, self.bin_sum);
576        self.bin_sum = 0.0;
577        self.bin_covered = 0;
578        if covered <= 0 {
579            return Ok(());
580        }
581        let value = (sum / covered as f64) as f32;
582        if self
583            .run_start_bin
584            .is_some_and(|first| index != first + self.run_values.len() as i64)
585        {
586            self.flush_run(writer)?;
587        }
588        if self.run_start_bin.is_none() {
589            self.run_start_bin = Some(index);
590        }
591        self.run_values.push(value);
592        if self.run_values.len() >= VALUE_BATCH {
593            self.flush_run(writer)?;
594        }
595        Ok(())
596    }
597
598    fn flush_run(&mut self, writer: &mut BbiWriter) -> Result<()> {
599        let Some(first) = self.run_start_bin.take() else {
600            return Ok(());
601        };
602        if self.run_values.is_empty() {
603            return Ok(());
604        }
605        let start = first * self.bin_size;
606        let values = std::mem::take(&mut self.run_values);
607        let chr = std::mem::take(&mut self.chr);
608        let result = writer.write_values(&chr, start, self.bin_size, &values);
609        self.chr = chr;
610        self.run_values = values;
611        self.run_values.clear();
612        result
613    }
614
615    /// Close the open bin and the open run, at the end of the input.
616    fn finish(&mut self, writer: &mut BbiWriter) -> Result<()> {
617        self.close_bin(writer)?;
618        self.flush_run(writer)
619    }
620}
621
622// -- convert_to_bigwig -----------------------------------------------------
623
624/// Convert a bedGraph or WIG file into a bigWig.
625///
626/// Which of the two the input is comes from its content, not its name. A
627/// `bin_size` of `None` writes the values as they stand; a positive one
628/// averages them into bins of that width, weighting each value by the bases it
629/// covers.
630pub fn convert_to_bigwig(
631    input: &Path,
632    output: &Path,
633    bin_size: Option<i64>,
634    mut options: BbiWriterOptions,
635    progress: Option<ProgressFn>,
636    cancel: Option<CancelFlag>,
637) -> Result<ConvertResult> {
638    let bin_size = bin_size.unwrap_or(0);
639    if bin_size < 0 {
640        return Err(Error::invalid(format!(
641            "bin_size {bin_size} must not be negative"
642        )));
643    }
644    options.kind = BbiKind::BigWig;
645    let declared = options.chr_sizes.clone();
646
647    let mut reader = LineReader::open(input)?;
648    let mut writer = BbiWriter::create(&output.to_string_lossy(), options)?;
649    let mut sink = ValueSink::new(bin_size, declared);
650    let tracker = ProgressTracker::with_callback(reader.size, progress);
651
652    let mut format: Option<TextFormat> = None;
653    let mut line_count = 0u64;
654    let mut declaration = WigDeclaration::default();
655    let mut declared_yet = false;
656    // The run of contiguous fixedStep values being gathered, and where it
657    // started. Only a fixedStep whose step is its span can build one: anything
658    // else leaves gaps or overlaps, which a run has neither of.
659    let mut run: Vec<f32> = Vec::new();
660    let mut run_start = 0i64;
661    let mut reported = 0u64;
662
663    // The body is a closure so the writer can be abandoned on any failure: a
664    // conversion promises its caller either a whole file or none, and a
665    // `BbiWriter` left to drop would close itself and leave a valid bigWig
666    // holding however much had been read.
667    let mut line = String::new();
668    let result = (|| -> Result<()> {
669        while reader.read_line_into(&mut line)? {
670            line_count += 1;
671            if line_count % PROGRESS_INTERVAL == 0 {
672                tracker.add(reader.consumed - reported);
673                reported = reader.consumed;
674                // Checked here rather than per line: the callback is the only
675                // place a cancellation can come from, so nothing can have
676                // changed in between.
677                if cancel.as_ref().is_some_and(CancelFlag::is_cancelled) {
678                    return Err(Error::invalid("conversion cancelled"));
679                }
680            }
681            if is_skipped_line(&line) {
682                continue;
683            }
684            let fields = split_blanks(&line);
685            if fields.is_empty() {
686                continue;
687            }
688            let declaration_line = is_declaration(fields[0]);
689
690            if format.is_none() {
691                format = Some(sniff_format(&line).map_err(|e| reader.fail(e))?);
692            }
693
694            if format == Some(TextFormat::Wig) {
695                if declaration_line {
696                    flush_run(&mut sink, &mut writer, &declaration, run_start, &mut run)
697                        .map_err(|e| reader.guard(e))?;
698                    declaration = parse_wig_declaration(&line).map_err(|e| reader.fail(e))?;
699                    declared_yet = true;
700                    continue;
701                }
702                if !declared_yet {
703                    return Err(
704                        reader.fail("wig data before any fixedStep or variableStep declaration")
705                    );
706                }
707                if declaration.fixed_step {
708                    if fields.len() != 1 {
709                        return Err(reader.fail(format!(
710                            "fixedStep data has {} columns, not 1",
711                            fields.len()
712                        )));
713                    }
714                    let value = parse_f32(fields[0]).map_err(|e| reader.fail(e))?;
715                    // A contiguous run is what write_values takes in one
716                    // extend; anything else is handed over a value at a time,
717                    // which the writer encodes just as tightly but has to walk.
718                    if declaration.step == declaration.span {
719                        if run.is_empty() {
720                            run_start = declaration.start;
721                        }
722                        run.push(value);
723                        if run.len() >= VALUE_BATCH {
724                            flush_run(&mut sink, &mut writer, &declaration, run_start, &mut run)
725                                .map_err(|e| reader.guard(e))?;
726                        }
727                    } else {
728                        sink.add(
729                            &mut writer,
730                            &declaration.chr,
731                            declaration.start,
732                            declaration.start + declaration.span,
733                            value,
734                        )
735                        .map_err(|e| reader.guard(e))?;
736                    }
737                    declaration.start += declaration.step;
738                } else {
739                    if fields.len() != 2 {
740                        return Err(reader.fail(format!(
741                            "variableStep data has {} columns, not 2",
742                            fields.len()
743                        )));
744                    }
745                    let start = parse_i64(fields[0], "position").map_err(|e| reader.fail(e))?;
746                    let value = parse_f32(fields[1]).map_err(|e| reader.fail(e))?;
747                    if start < 1 {
748                        return Err(reader.fail(format!("position {start} is not 1-based")));
749                    }
750                    sink.add(
751                        &mut writer,
752                        &declaration.chr,
753                        start - 1,
754                        start - 1 + declaration.span,
755                        value,
756                    )
757                    .map_err(|e| reader.guard(e))?;
758                }
759                continue;
760            }
761
762            if declaration_line {
763                return Err(
764                    reader.fail("wig declaration in what has been read as a bedgraph so far")
765                );
766            }
767            if fields.len() != 4 {
768                return Err(reader.fail(format!(
769                    "bedgraph record has {} columns, not 4",
770                    fields.len()
771                )));
772            }
773            let start = parse_i64(fields[1], "start").map_err(|e| reader.fail(e))?;
774            let end = parse_i64(fields[2], "end").map_err(|e| reader.fail(e))?;
775            let value = parse_f32(fields[3]).map_err(|e| reader.fail(e))?;
776            sink.add(&mut writer, fields[0], start, end, value)
777                .map_err(|e| reader.guard(e))?;
778        }
779
780        flush_run(&mut sink, &mut writer, &declaration, run_start, &mut run)
781            .map_err(|e| reader.guard(e))?;
782        sink.finish(&mut writer).map_err(|e| reader.guard(e))?;
783        writer.close()
784    })();
785
786    if let Err(error) = result {
787        writer.abandon();
788        return Err(error);
789    }
790    tracker.done_report();
791
792    Ok(ConvertResult {
793        // An input holding no data at all still produces a valid, empty bigWig;
794        // it simply never settled on a format.
795        format: format.unwrap_or(TextFormat::BedGraph),
796        line_count,
797        item_count: sink.item_count,
798        skipped_count: writer.skipped_count(),
799        // Only one of the two sees any given value — with a bin size the writer
800        // is handed bins and never the values they came from — so the report
801        // adds them.
802        clipped_count: writer.clipped_count() + sink.clipped_count,
803        chr_sizes: writer.chr_sizes(),
804    })
805}
806
807fn flush_run(
808    sink: &mut ValueSink,
809    writer: &mut BbiWriter,
810    declaration: &WigDeclaration,
811    run_start: i64,
812    run: &mut Vec<f32>,
813) -> Result<()> {
814    if run.is_empty() {
815        return Ok(());
816    }
817    let result = sink.add_run(writer, &declaration.chr, run_start, declaration.span, run);
818    run.clear();
819    result
820}
821
822fn parse_i64(text: &str, what: &str) -> Result<i64> {
823    text.parse()
824        .map_err(|_| Error::invalid(format!("could not read \"{text}\" as a {what}")))
825}
826
827fn parse_f32(text: &str) -> Result<f32> {
828    text.parse::<f64>()
829        .map(|v| v as f32)
830        .map_err(|_| Error::invalid(format!("could not read \"{text}\" as a number")))
831}
832
833// -- convert_to_bigbed -----------------------------------------------------
834
835/// The types the standard BED columns carry, parallel to `BED_FIELD_NAMES`.
836///
837/// itemRgb, blockSizes and blockStarts hold comma-separated lists — autoSql
838/// calls them `uint[3]` and `int[]` — and this library writes only the four
839/// scalar types, so they go out as strings. A reader gets the text either way.
840const BED_FIELD_STANDARD_TYPES: &[&str] = &[
841    "string", "uint", "uint", "string", "uint", "string", "uint", "uint", "string", "uint",
842    "string", "string",
843];
844
845/// Names and types for a BED of `col_count` columns that says nothing about
846/// itself.
847///
848/// The standard BED columns as far as the file has them, then `field13` and up
849/// — the same naming a bigBed carrying no autoSql is read with, so a BED
850/// converted here and a bigBed read there describe their columns alike.
851pub fn default_bed_fields(col_count: usize) -> IndexMap<String, String> {
852    (0..col_count)
853        .map(|index| {
854            if index < BED_FIELD_NAMES.len() {
855                (
856                    BED_FIELD_NAMES[index].to_string(),
857                    BED_FIELD_STANDARD_TYPES[index].to_string(),
858                )
859            } else {
860                (format!("field{}", index + 1), "string".to_string())
861            }
862        })
863        .collect()
864}
865
866/// Convert a BED file into a bigBed.
867///
868/// Entries may overlap and nest, which is the ordinary shape of a BED, so only
869/// their starts have to be in order. Chromosomes still have to be pooled.
870pub fn convert_to_bigbed(
871    input: &Path,
872    output: &Path,
873    mut options: BbiWriterOptions,
874    progress: Option<ProgressFn>,
875    cancel: Option<CancelFlag>,
876) -> Result<ConvertResult> {
877    options.kind = BbiKind::BigBed;
878    let mut reader = LineReader::open(input)?;
879    let tracker = ProgressTracker::with_callback(reader.size, progress);
880
881    // The writer needs the columns at construction, since a bigBed's autoSql is
882    // written before any record is. When they were not given they come from the
883    // first record, so the writer is opened lazily — which also means a BED that
884    // turns out to be malformed leaves no file behind at all.
885    let mut writer: Option<BbiWriter> = None;
886    let mut declared_fields = std::mem::take(&mut options.fields);
887    let mut col_count = 0usize;
888    let mut line_count = 0u64;
889    let mut reported = 0u64;
890    let mut values: IndexMap<String, String> = IndexMap::new();
891
892    let mut line = String::new();
893    let result = (|| -> Result<()> {
894        while reader.read_line_into(&mut line)? {
895            line_count += 1;
896            if line_count % PROGRESS_INTERVAL == 0 {
897                tracker.add(reader.consumed - reported);
898                reported = reader.consumed;
899                // Checked here rather than per line: the callback is the only
900                // place a cancellation can come from, so nothing can have
901                // changed in between.
902                if cancel.as_ref().is_some_and(CancelFlag::is_cancelled) {
903                    return Err(Error::invalid("conversion cancelled"));
904                }
905            }
906            if is_skipped_line(&line) {
907                continue;
908            }
909            let mut fields = split_tabs(&line);
910            trim_trailing_tab(
911                &mut fields,
912                if writer.is_none() {
913                    (!declared_fields.is_empty()).then(|| declared_fields.len())
914                } else {
915                    Some(col_count)
916                },
917            );
918            if fields.len() < 3 {
919                return Err(reader.fail(format!(
920                    "bed record has {} tab-separated columns, and needs at least 3 \
921                     (chrom, chromStart, chromEnd)",
922                    fields.len()
923                )));
924            }
925
926            if writer.is_none() {
927                col_count = fields.len();
928                if declared_fields.is_empty() {
929                    declared_fields = default_bed_fields(col_count);
930                } else if declared_fields.len() != col_count {
931                    return Err(reader.fail(format!(
932                        "fields declares {} columns and the first record has {col_count}",
933                        declared_fields.len()
934                    )));
935                }
936                let mut opened = BbiWriterOptions {
937                    fields: declared_fields.clone(),
938                    ..clone_options(&options)
939                };
940                opened.kind = BbiKind::BigBed;
941                writer = Some(
942                    BbiWriter::create(&output.to_string_lossy(), opened)
943                        .map_err(|e| reader.fail(e))?,
944                );
945                for name in declared_fields.keys().skip(3) {
946                    values.insert(name.clone(), String::new());
947                }
948            } else if fields.len() != col_count {
949                return Err(reader.fail(format!(
950                    "bed record has {} columns and the first one had {col_count}; a bigbed \
951                     stores one shape of record",
952                    fields.len()
953                )));
954            }
955
956            let start = parse_i64(fields[1], "chromStart").map_err(|e| reader.fail(e))?;
957            let end = parse_i64(fields[2], "chromEnd").map_err(|e| reader.fail(e))?;
958            // Overwritten in place rather than rebuilt: an `IndexMap` keeps an
959            // entry where it was first inserted, so a record past the first
960            // costs no allocation and no rehash.
961            for (index, slot) in values.values_mut().enumerate() {
962                slot.clear();
963                slot.push_str(fields[index + 3]);
964            }
965            writer
966                .as_mut()
967                .expect("opened above")
968                .write_entry(fields[0], start, end, &values)
969                .map_err(|e| reader.guard(e))?;
970        }
971        Ok(())
972    })();
973
974    if let Err(error) = result {
975        if let Some(writer) = &mut writer {
976            writer.abandon();
977        }
978        return Err(error);
979    }
980
981    // A BED holding no record at all still produces a valid, empty bigBed, with
982    // whatever columns were asked for or the standard bed3.
983    let mut writer = match writer {
984        Some(writer) => writer,
985        None => {
986            if declared_fields.is_empty() {
987                declared_fields = default_bed_fields(3);
988            }
989            let mut opened = BbiWriterOptions {
990                fields: declared_fields,
991                ..clone_options(&options)
992            };
993            opened.kind = BbiKind::BigBed;
994            BbiWriter::create(&output.to_string_lossy(), opened)?
995        }
996    };
997    writer.close()?;
998    tracker.done_report();
999
1000    Ok(ConvertResult {
1001        format: TextFormat::Bed,
1002        line_count,
1003        item_count: writer.entry_count(),
1004        skipped_count: writer.skipped_count(),
1005        clipped_count: writer.clipped_count(),
1006        chr_sizes: writer.chr_sizes(),
1007    })
1008}
1009
1010/// `BbiWriterOptions` is not `Clone` — a `ChrMap` in it is, but the struct is
1011/// built once per writer — and the bigBed path opens its writer lazily, so it
1012/// needs the options twice.
1013fn clone_options(options: &BbiWriterOptions) -> BbiWriterOptions {
1014    BbiWriterOptions {
1015        kind: options.kind,
1016        chr_sizes: options.chr_sizes.clone(),
1017        fields: options.fields.clone(),
1018        items_per_slot: options.items_per_slot,
1019        block_size: options.block_size,
1020        compression_level: options.compression_level,
1021        parallel: options.parallel,
1022        section_policy: options.section_policy,
1023        cost_model: options.cost_model,
1024    }
1025}