Skip to main content

gwseq_io/bbi/
section.rs

1//! The wig section buffer and its encoding heuristic.
2//!
3//! A bigWig stores values in sections, and each section is written in the
4//! narrowest of three encodings that holds it: **fixedStep** while every value
5//! shares one span and one step, four bytes a value; **variableStep** once the
6//! starts turn irregular, eight; **bedGraph** once the spans differ too,
7//! twelve. A section opens fixedStep and only ever widens.
8//!
9//! So every value that breaks the shape of the section it is being added to
10//! poses the same question, and it is the only interesting question in this
11//! file: **widen, or close the section here and open a narrower one?**
12//!
13//! - Widening costs the extra item width on everything already buffered *and*
14//!   on everything the section goes on to hold, because it can never narrow
15//!   again.
16//! - Closing costs a section header, an R-tree leaf and a cold zlib stream —
17//!   about 64 bytes — and leaves the next value to start a fresh section that
18//!   may hit exactly the same problem.
19//!
20//! # What the answer is, and how it is known
21//!
22//! `examples/section_policy.rs` writes the same data through every rule in
23//! [`SectionPolicy`] and reports the size of the file that comes out. That is
24//! the only way to answer this: every rule produces a *valid* bigWig, so no
25//! correctness test can distinguish them, and the difference between the best
26//! and the worst is a factor of five.
27//!
28//! The measurement says two things.
29//!
30//! **The two obvious rules both fail catastrophically, in opposite
31//! directions.** Always splitting is 356% worse on data that is irregular
32//! throughout, because it writes one section per value. Always widening is 86%
33//! worse on data that is regular with occasional breaks, because one odd value
34//! makes the next thousand three times as wide. No fixed rule can do well on
35//! both, and no arithmetic at the moment of the break can tell them apart:
36//! the two cases look identical there.
37//!
38//! **What tells them apart is what happened last time.** A split that was
39//! right is followed by a section that goes on to hold real work; a split that
40//! was wrong is followed by one that breaks again immediately. So
41//! [`SectionPolicy::Adaptive`] splits by default and stops as soon as two
42//! sections in a row come out too short to be worth writing — and starts
43//! again the moment one does not. Three details make it work, and each was
44//! added because the measurement showed the file getting bigger without it:
45//!
46//! 1. **The tail is costed over the room left in the section**, not over what
47//!    the current call happens to have left. A caller adding values one at a
48//!    time has nothing left in the call, and the section still goes on filling
49//!    in the wider encoding.
50//! 2. **The streak only overrules the arithmetic for a section that is itself
51//!    short.** A section holding a thousand uniform values is always worth
52//!    closing, whatever the burst before it did.
53//! 3. **A long run that cannot extend the open section flushes it first.**
54//!    Nothing else in the writer is in a position to notice: after the first
55//!    value widens a section, no *further* widening is needed, so the run
56//!    pours in at bedGraph width and no decision is ever taken.
57//!
58//! Over six shapes of synthetic input that is **17% smaller** than the rule
59//! this library shipped with, and best or within 0.05% of best on every one.
60//! Over four real tracks it is best or tied on all four, and 0.8% smaller in
61//! total. The two constants it adds are at the optimum and on a plateau: see
62//! `--sweep`.
63//!
64//! # Reading a change to this file
65//!
66//! `section_counts` on the writer is the observable, and it is where to look
67//! first: it reports how many sections went out in each encoding. A change
68//! that makes files bigger shows up there before it shows up in a byte count.
69//! `examples/section_policy.rs` is the check, and it takes seconds.
70
71use crate::bbi::block::WigEncoding;
72use crate::bbi::header::to_bbi_u32;
73use crate::error::Result;
74
75/// Hard ceiling on a section: its item count is a `u16` (Supp. Table 13).
76pub const MAX_ITEMS_PER_SECTION: usize = 65535;
77
78/// What closing a section early costs on disk: a fresh 24-byte section header,
79/// a fresh 32-byte R-tree leaf, and the framing of a fresh zlib stream, since
80/// Supp. Table 13 has every section compressed separately and so starting from
81/// a cold window.
82pub const SECTION_SPLIT_COST: i64 = 64;
83
84/// How much of a widening survives deflate, as a percentage. The columns
85/// varStep and bedGraph add are near-constant-stride coordinates, of which
86/// deflate keeps very little, so widening a section costs far less on disk than
87/// the difference in item size suggests.
88pub const DOWNGRADE_COMPRESSION_PERCENT: i64 = 25;
89
90/// Fewest items a section must hold to be worth writing on its own. Below
91/// this, the 64 bytes of header, R-tree leaf and zlib framing outweigh what
92/// the narrower encoding saves.
93pub const MIN_SPLIT_ITEMS: i64 = 32;
94
95/// How many too-short sections in a row [`SectionPolicy::Adaptive`] tolerates
96/// before it stops splitting. Two: one runt can be a coincidence.
97pub const RUNT_PATIENCE: u32 = 2;
98
99/// How a section decides between widening its encoding and closing where it
100/// stands.
101///
102/// Every variant produces a valid bigWig; they differ only in how big it is.
103/// [`SectionPolicy::Adaptive`] is the default and the only one a caller should
104/// normally set — the rest exist so that "is this rule any good?" is a
105/// question with a measured answer rather than an opinion. See
106/// `examples/section_policy.rs`, which writes the same data through each and
107/// reports the bytes; the answer, over six shapes of input, is that `Adaptive`
108/// writes files **17% smaller** than `Cost` and is the smallest or within half
109/// a percent of it on every one.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub enum SectionPolicy {
112    /// Weigh the bytes a widening costs against the bytes a split costs,
113    /// counting the widening over what is buffered plus what the current call
114    /// still has to give.
115    ///
116    /// The rule this library shipped with, and measurably not a good one: a
117    /// caller adding values one at a time has nothing left in the call, so the
118    /// second term vanishes and the widening is costed over the prefix alone
119    /// — while the section goes on filling in the wider encoding and paying
120    /// for every value of it. Kept as the baseline the others are measured
121    /// against.
122    Cost,
123    /// Never widen: close the section and start a new one in the narrower
124    /// encoding. Most sections, smallest items.
125    Split,
126    /// Never split for encoding: widen and keep going until `items_per_slot`.
127    /// Fewest sections, widest items.
128    Widen,
129    /// Weigh only what is already buffered, ignoring what the call still has
130    /// to hand over. The obvious rule, and what the cost model would be
131    /// without its second term.
132    PrefixOnly,
133    /// Weigh the widening against the **room left in the section**, rather
134    /// than against what the current call happens to have left.
135    ///
136    /// [`SectionPolicy::Cost`] costs the tail at `min(batch_remaining, room)`,
137    /// and a caller adding values one at a time has `batch_remaining == 0` —
138    /// so the tail term vanishes and the widening is costed over the prefix
139    /// alone. But the section does not stop there: it goes on filling to
140    /// `items_per_slot` in the wider encoding, paying the extra width for
141    /// every one of them. This costs what will actually be paid.
142    Room,
143    /// [`SectionPolicy::Room`], and never split a section too short to be
144    /// worth writing.
145    ///
146    /// The failure `Room` has on its own is that data which is irregular
147    /// *throughout* breaks the section immediately, every time: split, reopen,
148    /// break at item two, split again — sections of one item, and a file
149    /// several times larger than it should be. Widening a section that has
150    /// barely started costs almost nothing, so below the floor it always
151    /// widens.
152    Runt,
153    /// [`SectionPolicy::Room`], and split unless the data has just shown that
154    /// splitting does not work.
155    ///
156    /// The two failure modes pull opposite ways and no arithmetic at the
157    /// break can tell them apart, because both look identical at that instant:
158    ///
159    /// - Data that is **regular with rare breaks** wants a split. Widening
160    ///   makes the whole rest of the section wider to absorb one odd value.
161    /// - Data that is **irregular throughout** wants a widening. Splitting
162    ///   emits one section per value, an R-tree leaf and a zlib stream each.
163    ///
164    /// What does tell them apart is what happened last time. A split that was
165    /// right is followed by a section that goes on to hold many values; a split
166    /// that was wrong is followed by one that breaks again immediately. So this
167    /// splits by default and stops as soon as two sections in a row have come
168    /// out too short to be worth writing — and starts again the moment one
169    /// does not.
170    ///
171    /// The cost is bounded: at most `runt_patience` short sections are written
172    /// before it adapts, once per stretch of irregular data.
173    ///
174    /// Two things beyond the streak make this the default rather than
175    /// [`SectionPolicy::Runt`]:
176    ///
177    /// - The streak only overrules the arithmetic for a section that is
178    ///   *itself* short. A section holding a thousand uniform values is always
179    ///   worth closing, whatever the burst before it did.
180    /// - A long run that cannot extend the open section flushes it first — see
181    ///   `WigSection::should_flush_for_run`. Without that, one odd value
182    ///   makes the next thousand cost three times as much, and nothing else in
183    ///   the writer is in a position to notice.
184    #[default]
185    Adaptive,
186}
187
188/// The two constants the [`SectionPolicy::Cost`] model is built from, exposed
189/// so they can be swept rather than argued about.
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub struct CostModel {
192    /// Bytes a split costs. 64 by default: a fresh section header, a fresh
193    /// R-tree leaf and the framing of a fresh zlib stream.
194    pub split_cost: i64,
195    /// Percentage of a widening that survives deflate. 25 by default: the
196    /// columns a wider encoding adds are near-constant-stride coordinates, of
197    /// which deflate keeps very little.
198    pub compression_percent: i64,
199    /// Fewest items a section must hold to count as worth writing. Below this
200    /// [`SectionPolicy::Runt`] will not close a section, and
201    /// [`SectionPolicy::Adaptive`] counts one against its patience.
202    pub min_split_items: i64,
203    /// How many too-short sections in a row [`SectionPolicy::Adaptive`] writes
204    /// before it concludes that splitting is not working here.
205    pub runt_patience: u32,
206}
207
208impl Default for CostModel {
209    fn default() -> Self {
210        Self {
211            split_cost: SECTION_SPLIT_COST,
212            compression_percent: DOWNGRADE_COMPRESSION_PERCENT,
213            min_split_items: MIN_SPLIT_ITEMS,
214            runt_patience: RUNT_PATIENCE,
215        }
216    }
217}
218
219/// The 24-byte section header (Supp. Table 13).
220pub const WIG_HEADER_SIZE: usize = 24;
221
222/// Values buffered for the section being built, and what shape they have taken.
223///
224/// `starts` and `ends` are only materialised once the encoding needs them:
225/// while the section is still fixedStep every start is `first_start + i * step`
226/// and every end that start plus `span`, so a contiguous run of values goes in
227/// as one extend rather than as three pushes per item.
228#[derive(Debug, Default)]
229pub struct WigSection {
230    pub chr_ix: u32,
231    pub encoding: Option<WigEncoding>,
232    pub first_start: i64,
233    /// Start delta shared by every adjacent pair, `None` while fewer than two
234    /// items are buffered.
235    pub uniform_step: Option<i64>,
236    /// Width shared by every item, meaningless once the encoding has reached
237    /// bedGraph.
238    pub uniform_span: i64,
239    pub last_start: i64,
240    pub last_end: i64,
241    pub starts: Vec<i64>,
242    pub ends: Vec<i64>,
243    pub values: Vec<f32>,
244    items_per_slot: usize,
245    policy: SectionPolicy,
246    cost: CostModel,
247    /// Sections closed in a row holding fewer than `min_split_items`, which is
248    /// what [`SectionPolicy::Adaptive`] reads the shape of the data from.
249    /// Counted at [`WigSection::clear`], the one place a section ends.
250    runt_streak: u32,
251}
252
253impl WigSection {
254    /// `items_per_slot`, and the rule the section decides widening with.
255    /// See [`SectionPolicy`].
256    pub fn with_policy(items_per_slot: usize, policy: SectionPolicy, cost: CostModel) -> Self {
257        Self {
258            items_per_slot,
259            policy,
260            cost,
261            ..Default::default()
262        }
263    }
264
265    pub fn len(&self) -> usize {
266        self.values.len()
267    }
268
269    pub fn is_empty(&self) -> bool {
270        self.values.is_empty()
271    }
272
273    pub fn is_full(&self) -> bool {
274        self.values.len() >= self.items_per_slot
275    }
276
277    /// The encoding of a section that has items in it. A fresh section reports
278    /// fixedStep, which is what it opens as.
279    pub fn encoding(&self) -> WigEncoding {
280        self.encoding.unwrap_or(WigEncoding::FixedStep)
281    }
282
283    pub fn clear(&mut self) {
284        // The one place a section ends, so the one place to notice how it
285        // went. A run of short ones is `Adaptive`'s signal that splitting is
286        // not working on this data.
287        //
288        // A *long* section only clears that signal if it stayed fixedStep.
289        // One that had to widen and then filled up is evidence the other way
290        // — the data here is irregular and widening was the right call — so
291        // clearing the streak on it would make the next section pay the same
292        // couple of runt sections to learn the same thing, once per slot, for
293        // as long as the irregularity lasts.
294        if !self.is_empty() {
295            if (self.len() as i64) < self.cost.min_split_items {
296                self.runt_streak = self.runt_streak.saturating_add(1);
297            } else if self.encoding() == WigEncoding::FixedStep {
298                self.runt_streak = 0;
299            }
300        }
301        // Not `*self = Self::new(..)`: the policy, the cost model and that
302        // streak outlive a flush, as `items_per_slot` does.
303        self.chr_ix = 0;
304        self.encoding = None;
305        self.first_start = -1;
306        self.uniform_step = None;
307        self.uniform_span = -1;
308        self.last_start = -1;
309        self.last_end = -1;
310        // Cleared rather than replaced, so the capacity survives the flush and
311        // a steady run of sections allocates nothing at all.
312        self.starts.clear();
313        self.ends.clear();
314        self.values.clear();
315    }
316
317    /// Whether a section that has to widen from `old` to `new` should keep
318    /// accumulating rather than close where it stands.
319    ///
320    /// Closing costs a section header, an R-tree leaf and a fresh zlib stream;
321    /// widening costs the difference in item size over everything buffered, and
322    /// over whatever is still to come in the call that broke the uniformity.
323    /// Both are counted in bytes on disk, which is why the widening is
324    /// discounted.
325    fn should_widen(&self, old: WigEncoding, new: WigEncoding, batch_remaining: usize) -> bool {
326        match self.policy {
327            SectionPolicy::Split => return false,
328            SectionPolicy::Widen => return true,
329            SectionPolicy::Cost
330            | SectionPolicy::PrefixOnly
331            | SectionPolicy::Room
332            | SectionPolicy::Runt
333            | SectionPolicy::Adaptive => {}
334        }
335        let count = self.len() as i64;
336        // A section too short to be worth writing is never worth closing: the
337        // one it opens will meet the same value and close just as short.
338        if self.policy == SectionPolicy::Runt && count < self.cost.min_split_items {
339            return true;
340        }
341        // Splitting has already produced this many runts in a row, so it is not
342        // going to work on this value either — *if* this section is itself
343        // short. A section that has already collected real work is always
344        // worth closing whatever the recent history: the streak is there to
345        // stop a cascade of one-item sections, not to condemn a thousand
346        // uniform values to bedGraph because the burst before them did not
347        // split well.
348        if self.policy == SectionPolicy::Adaptive
349            && count < self.cost.min_split_items
350            && self.runt_streak >= self.cost.runt_patience
351        {
352            return true;
353        }
354        let (old_size, new_size) = (old.item_size() as i64, new.item_size() as i64);
355        let prefix_cost = (new_size - old_size) * count;
356        // Against fixedStep, not against `old`: what the tail costs is the
357        // whole widening from the narrowest form, since that is what those
358        // items would otherwise have been written as in a section of their own.
359        let room = self.items_per_slot as i64 - count;
360        let widened_items = match self.policy {
361            SectionPolicy::PrefixOnly => 0,
362            SectionPolicy::Cost => (batch_remaining as i64).min(room),
363            _ => room,
364        };
365        let tail_cost = (new_size - WigEncoding::FixedStep.item_size() as i64) * widened_items;
366        (prefix_cost + tail_cost) * self.cost.compression_percent / 100 < self.cost.split_cost
367    }
368
369    /// Materialise the coordinate columns a wider encoding needs.
370    fn widen(&mut self, target: WigEncoding) {
371        let count = self.len();
372        if self.encoding() == WigEncoding::FixedStep && target != WigEncoding::FixedStep {
373            // With one item buffered there is no step yet, and the span stands
374            // in for it — which is right, since the one start is `first_start`
375            // either way.
376            let step = if count >= 2 {
377                self.uniform_step.unwrap_or(self.uniform_span)
378            } else {
379                self.uniform_span
380            };
381            self.starts.clear();
382            self.starts
383                .extend((0..count).map(|i| self.first_start + i as i64 * step));
384            self.encoding = Some(WigEncoding::VarStep);
385        }
386        if self.encoding() == WigEncoding::VarStep && target == WigEncoding::BedGraph {
387            self.ends.clear();
388            let span = self.uniform_span;
389            self.ends.extend(self.starts.iter().map(|s| s + span));
390            self.encoding = Some(WigEncoding::BedGraph);
391        }
392    }
393
394    fn push(&mut self, start: i64, end: i64, value: f32) {
395        self.values.push(value);
396        if self.encoding() != WigEncoding::FixedStep {
397            self.starts.push(start);
398        }
399        if self.encoding() == WigEncoding::BedGraph {
400            self.ends.push(end);
401        }
402        self.last_start = start;
403        self.last_end = end;
404    }
405
406    /// Open an empty section on `chr_ix` at `start`, with `span`.
407    fn open(&mut self, chr_ix: u32, start: i64, span: i64) {
408        self.chr_ix = chr_ix;
409        self.first_start = start;
410        self.uniform_span = span;
411        self.uniform_step = None;
412        self.encoding = Some(WigEncoding::FixedStep);
413    }
414
415    /// Offer one item to the open section.
416    ///
417    /// `batch_remaining` is how many items the caller's current call still has
418    /// to hand over after this one: the heuristic costs a widening over what is
419    /// buffered *and* over what is still coming, which is what tells a one-off
420    /// break of uniformity from the start of a long run of a different shape,
421    /// and why a run handed over in one call encodes better than the same
422    /// values one at a time.
423    ///
424    /// Returns [`Accept::Flush`] when the section has to be written before this
425    /// item can go anywhere — the item is *not* buffered, and the caller offers
426    /// it again once the section is empty.
427    pub fn offer(
428        &mut self,
429        chr_ix: u32,
430        start: i64,
431        span: i64,
432        value: f32,
433        batch_remaining: usize,
434    ) -> Accept {
435        if !self.is_empty() && chr_ix != self.chr_ix {
436            return Accept::Flush;
437        }
438        if self.is_empty() {
439            self.open(chr_ix, start, span);
440            self.push(start, start + span, value);
441            return Accept::Buffered;
442        }
443
444        let count = self.len();
445        let mut needed = WigEncoding::FixedStep;
446        if span != self.uniform_span {
447            needed = WigEncoding::BedGraph;
448        } else if count >= 2 && Some(start - self.last_start) != self.uniform_step {
449            needed = WigEncoding::VarStep;
450        }
451        // With a single item buffered the step is not defined yet, so any
452        // second item of the same span still leaves the section fixedStep.
453        if needed.item_size() > self.encoding().item_size() {
454            if !self.should_widen(self.encoding(), needed, batch_remaining) {
455                return Accept::Flush;
456            }
457            self.widen(needed);
458        }
459        if count == 1 && self.encoding() == WigEncoding::FixedStep {
460            self.uniform_step = Some(start - self.first_start);
461        }
462        self.push(start, start + span, value);
463        Accept::Buffered
464    }
465
466    /// Whether a long contiguous run should be given a section of its own
467    /// rather than poured into the one that is open.
468    ///
469    /// The case this catches has nothing to do with the run and everything to
470    /// do with what is already there. A run that **cannot extend** the open
471    /// section — because the section has widened, or holds a different span,
472    /// or stopped somewhere else — is handed over one value at a time, and
473    /// every one of those values forces the section wider. So a burst of
474    /// irregular values, or a single value of an odd width, makes the *next*
475    /// thousand uniform values cost twelve bytes each instead of four. Nothing
476    /// else in the writer can notice, because after the first of them no
477    /// further widening is needed: bedGraph accepts anything.
478    ///
479    /// The arithmetic is [`Self::should_widen`]'s read the other way round.
480    /// The run saves the width difference on every value it holds and costs
481    /// one split.
482    pub fn should_flush_for_run(&self, chr_ix: u32, start: i64, span: i64, run_len: usize) -> bool {
483        if !matches!(self.policy, SectionPolicy::Adaptive) {
484            return false;
485        }
486        // Nothing to flush, or the run extends what is open and costs nothing.
487        if self.is_empty() || self.extends_run(chr_ix, start, span) {
488            return false;
489        }
490        // A change of chromosome flushes anyway, further up.
491        if self.chr_ix != chr_ix {
492            return false;
493        }
494        // Poured in one at a time, the run would end up at bedGraph width: a
495        // second span always forces it, whatever the section holds now.
496        let poured = WigEncoding::BedGraph.item_size() as i64;
497        let fresh = WigEncoding::FixedStep.item_size() as i64;
498        let room = (self.items_per_slot - self.len()) as i64;
499        let saved = (poured - fresh) * (run_len as i64).min(room);
500        saved * self.cost.compression_percent / 100 > self.cost.split_cost
501    }
502
503    /// Whether a contiguous fixedStep run of `span` starting at `start` extends
504    /// the open section rather than opening one.
505    ///
506    /// The fast path's precondition, ported from `write_values`: the section
507    /// has to be fixedStep, on this chromosome, of this span, either holding a
508    /// single item (whose step is not fixed yet) or already stepping by `span`,
509    /// and ending exactly where the run begins.
510    pub fn extends_run(&self, chr_ix: u32, start: i64, span: i64) -> bool {
511        self.encoding() == WigEncoding::FixedStep
512            && !self.is_empty()
513            && self.chr_ix == chr_ix
514            && self.uniform_span == span
515            && (self.len() == 1 || self.uniform_step == Some(span))
516            && self.last_end == start
517    }
518
519    /// Take a contiguous fixedStep run whole: no starts materialised, one
520    /// extend. The fast path the API documentation points callers at.
521    ///
522    /// The caller must have checked [`Self::extends_run`], or the section must
523    /// be empty. `start` is the first item's start and the run is `values.len()`
524    /// items of `span` each.
525    pub fn extend_run(&mut self, chr_ix: u32, start: i64, span: i64, values: &[f32]) {
526        debug_assert!(!values.is_empty());
527        if self.is_empty() {
528            self.open(chr_ix, start, span);
529        }
530        // A run laid down this way is contiguous, so its step is its span; a
531        // section holding one item has none yet, and this is what gives it one.
532        self.uniform_step = Some(span);
533        self.values.extend_from_slice(values);
534        self.last_start = start + span * (values.len() as i64 - 1);
535        self.last_end = start + span * values.len() as i64;
536    }
537
538    /// Encode to the section's chosen form, ready to compress.
539    pub fn encode(&self) -> Result<Vec<u8>> {
540        let count = self.len();
541        let encoding = self.encoding();
542        let mut out = Vec::with_capacity(WIG_HEADER_SIZE + count * encoding.item_size());
543
544        out.extend_from_slice(&to_bbi_u32(self.chr_ix as i64, "chromId")?.to_le_bytes());
545        out.extend_from_slice(&to_bbi_u32(self.first_start, "chromStart")?.to_le_bytes());
546        // The end of the last item, which is not the end of the last step: a
547        // section of 10 bp items spaced 100 bp apart stops 90 bp short of it.
548        out.extend_from_slice(&to_bbi_u32(self.last_end, "chromEnd")?.to_le_bytes());
549        let step = if encoding == WigEncoding::FixedStep {
550            if count >= 2 {
551                self.uniform_step.unwrap_or(self.uniform_span)
552            } else {
553                self.uniform_span
554            }
555        } else {
556            0
557        };
558        out.extend_from_slice(&to_bbi_u32(step, "itemStep")?.to_le_bytes());
559        let span = if encoding == WigEncoding::BedGraph {
560            0
561        } else {
562            self.uniform_span
563        };
564        out.extend_from_slice(&to_bbi_u32(span, "itemSpan")?.to_le_bytes());
565        out.push(encoding as u8);
566        out.push(0); // reserved
567        if count > MAX_ITEMS_PER_SECTION {
568            return Err(crate::error::Error::invalid(format!(
569                "wig section of {count} items exceeds the {MAX_ITEMS_PER_SECTION} its item \
570                 count is stored on"
571            )));
572        }
573        out.extend_from_slice(&(count as u16).to_le_bytes());
574        debug_assert_eq!(out.len(), WIG_HEADER_SIZE);
575
576        match encoding {
577            WigEncoding::FixedStep => {
578                for value in &self.values {
579                    out.extend_from_slice(&value.to_le_bytes());
580                }
581            }
582            WigEncoding::VarStep => {
583                for i in 0..count {
584                    out.extend_from_slice(&to_bbi_u32(self.starts[i], "chromStart")?.to_le_bytes());
585                    out.extend_from_slice(&self.values[i].to_le_bytes());
586                }
587            }
588            WigEncoding::BedGraph => {
589                for i in 0..count {
590                    out.extend_from_slice(&to_bbi_u32(self.starts[i], "chromStart")?.to_le_bytes());
591                    out.extend_from_slice(&to_bbi_u32(self.ends[i], "chromEnd")?.to_le_bytes());
592                    out.extend_from_slice(&self.values[i].to_le_bytes());
593                }
594            }
595        }
596        Ok(out)
597    }
598}
599
600/// What the buffer decided to do with an item.
601#[derive(Debug, Clone, Copy, PartialEq, Eq)]
602pub enum Accept {
603    /// Buffered.
604    Buffered,
605    /// The section must be flushed first; the item is *not* buffered.
606    Flush,
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612
613    /// The default policy, which is what a writer gets.
614    fn section() -> WigSection {
615        WigSection::with_policy(1024, SectionPolicy::default(), CostModel::default())
616    }
617
618    /// A section pinned to the cost rule, for the tests that are about *that*
619    /// rule's arithmetic rather than about what the writer does.
620    fn cost_section() -> WigSection {
621        WigSection::with_policy(1024, SectionPolicy::Cost, CostModel::default())
622    }
623
624    /// A section that always widens, for the tests about what an encoding
625    /// *writes* rather than about when it is chosen. Pinning the policy keeps
626    /// them from breaking every time the heuristic is measured again.
627    fn widening_section() -> WigSection {
628        WigSection::with_policy(1024, SectionPolicy::Widen, CostModel::default())
629    }
630
631    /// Offer an item, flushing and retrying once if the section says to — which
632    /// is what the writer does, and what makes the encoding the observable.
633    fn offer(s: &mut WigSection, start: i64, span: i64, value: f32, remaining: usize) -> bool {
634        match s.offer(0, start, span, value, remaining) {
635            Accept::Buffered => false,
636            Accept::Flush => {
637                s.clear();
638                assert_eq!(s.offer(0, start, span, value, remaining), Accept::Buffered);
639                true
640            }
641        }
642    }
643
644    #[test]
645    fn a_regular_run_stays_fixed_step() {
646        let mut s = section();
647        for i in 0..10 {
648            assert!(!offer(&mut s, i * 10, 10, i as f32, 0));
649        }
650        assert_eq!(s.encoding(), WigEncoding::FixedStep);
651        assert_eq!(s.len(), 10);
652        // No coordinate column was ever materialised.
653        assert!(s.starts.is_empty() && s.ends.is_empty());
654        assert_eq!(s.uniform_step, Some(10));
655    }
656
657    #[test]
658    fn a_gap_widens_to_var_step_when_the_widening_is_cheap() {
659        let mut s = cost_section();
660        for i in 0..4 {
661            offer(&mut s, i * 10, 10, 1.0, 0);
662        }
663        // One item out of step, nothing else coming: 4 items * 4 extra bytes
664        // is 16, a quarter of which is 4, well under the 64 a split costs.
665        assert!(!offer(&mut s, 100, 10, 1.0, 0));
666        assert_eq!(s.encoding(), WigEncoding::VarStep);
667        assert_eq!(s.starts, [0, 10, 20, 30, 100]);
668    }
669
670    #[test]
671    fn a_different_span_widens_all_the_way_to_bedgraph() {
672        let mut s = cost_section();
673        for i in 0..3 {
674            offer(&mut s, i * 10, 10, 1.0, 0);
675        }
676        assert!(!offer(&mut s, 30, 25, 1.0, 0));
677        assert_eq!(s.encoding(), WigEncoding::BedGraph);
678        assert_eq!(s.starts, [0, 10, 20, 30]);
679        assert_eq!(s.ends, [10, 20, 30, 55]);
680    }
681
682    #[test]
683    fn a_long_tail_of_the_new_shape_splits_the_section_instead() {
684        // The whole point of `batch_remaining`: the same irregularity that is
685        // absorbed above is a split when a long run of it is still coming.
686        let mut s = cost_section();
687        for i in 0..4 {
688            offer(&mut s, i * 10, 10, 1.0, 0);
689        }
690        // Widening to bedGraph costs 8 more bytes on each of 4 buffered items
691        // (32) plus 8 on each of the 500 still to come (4000); a quarter of
692        // 4032 is far past 64.
693        assert!(offer(&mut s, 30, 25, 1.0, 500));
694        // Flushed, and the item opened a fresh fixedStep section of its own.
695        assert_eq!(s.encoding(), WigEncoding::FixedStep);
696        assert_eq!(s.len(), 1);
697        assert_eq!(s.uniform_span, 25);
698    }
699
700    #[test]
701    fn the_tail_is_costed_against_fixed_step_not_against_the_current_encoding() {
702        // A varStep section widening to bedGraph. Costed against varStep the
703        // tail would be 4 bytes an item; against fixedStep it is 8, and only
704        // the second answer splits here. This is the term that is easiest to
705        // get wrong and the one nothing else would catch.
706        let mut s = cost_section();
707        offer(&mut s, 0, 10, 1.0, 0);
708        offer(&mut s, 10, 10, 1.0, 0);
709        offer(&mut s, 40, 10, 1.0, 0); // irregular start -> varStep
710        assert_eq!(s.encoding(), WigEncoding::VarStep);
711
712        // 3 buffered * 4 extra = 12; tail of 30 items * 8 = 240; (12+240)/4 =
713        // 63, which is under 64 — so it widens, by one byte of margin.
714        let mut narrow = cost_section();
715        for (start, span) in [(0, 10), (10, 10), (40, 10)] {
716            offer(&mut narrow, start, span, 1.0, 0);
717        }
718        assert!(!offer(&mut narrow, 50, 25, 1.0, 30));
719        assert_eq!(narrow.encoding(), WigEncoding::BedGraph);
720
721        // One more item in the tail and it does not: (12 + 248) / 4 = 65.
722        let mut wide = cost_section();
723        for (start, span) in [(0, 10), (10, 10), (40, 10)] {
724            offer(&mut wide, start, span, 1.0, 0);
725        }
726        assert!(offer(&mut wide, 50, 25, 1.0, 31));
727    }
728
729    // ---- the default policy ------------------------------------------
730    //
731    // Three behaviours, and each is the fix for a case the measured comparison
732    // in `examples/section_policy.rs` showed the old cost model losing.
733
734    /// Data that breaks every other value must not produce one section per
735    /// value. After `runt_patience` short sections the policy stops splitting.
736    #[test]
737    fn a_cascade_of_short_sections_stops_itself() {
738        let mut s = section();
739        let mut flushes = 0;
740        let mut at = 0i64;
741        for i in 0..200 {
742            let span = if i % 2 == 0 { 10 } else { 11 };
743            if offer(&mut s, at, span, 1.0, 0) {
744                flushes += 1;
745            }
746            at += span;
747        }
748        // Two runts to learn it, and then nothing: the section widened and
749        // took the rest.
750        assert!(
751            flushes <= RUNT_PATIENCE as usize + 1,
752            "{flushes} sections for 200 values"
753        );
754        assert_eq!(s.encoding(), WigEncoding::BedGraph);
755        assert!(s.len() > 190, "{} values buffered", s.len());
756    }
757
758    /// ...but the streak must not condemn a section that has real work in it.
759    /// A burst of irregular values followed by a long uniform run has to split
760    /// at the end of the run, not widen it.
761    #[test]
762    fn a_long_section_splits_however_bad_the_recent_history() {
763        let mut s = section();
764        // A burst, which teaches the policy that splitting is not working.
765        let mut at = 0i64;
766        for i in 0..12 {
767            let span = 7 + (i % 5);
768            offer(&mut s, at, span, 1.0, 0);
769            at += span + 1;
770        }
771        assert!(s.encoding() != WigEncoding::FixedStep);
772        s.clear();
773
774        // Now a long uniform run, and then one value that breaks it.
775        let mut at = 10_000i64;
776        for _ in 0..500 {
777            offer(&mut s, at, 10, 1.0, 0);
778            at += 10;
779        }
780        assert_eq!(s.encoding(), WigEncoding::FixedStep);
781        assert_eq!(s.len(), 500);
782        // 500 buffered values are worth far more than one split, so this
783        // closes the section rather than making all 500 twelve bytes wide.
784        assert!(offer(&mut s, at, 25, 1.0, 0), "a 500-value section widened");
785        assert_eq!(s.len(), 1);
786    }
787
788    /// A long run that cannot extend the open section gets one of its own.
789    ///
790    /// Nothing else in the writer can notice this: once a section is bedGraph
791    /// no further widening is needed, so the run pours in at twelve bytes a
792    /// value and no decision is ever taken.
793    #[test]
794    fn a_long_run_flushes_a_section_it_cannot_extend() {
795        let mut s = section();
796        // A widened section holding a handful of values.
797        let mut at = 0i64;
798        for i in 0..6 {
799            let span = 7 + (i % 3);
800            offer(&mut s, at, span, 1.0, 0);
801            at += span + 2;
802        }
803        assert_eq!(s.encoding(), WigEncoding::BedGraph);
804        // A thousand uniform values are worth a split many times over.
805        assert!(s.should_flush_for_run(0, at, 10, 1000));
806        // A handful are not.
807        assert!(!s.should_flush_for_run(0, at, 10, 4));
808        // Nor is a run on another chromosome, which flushes for its own reason.
809        assert!(!s.should_flush_for_run(1, at, 10, 1000));
810
811        // And a run that simply extends what is open costs nothing to keep.
812        let mut fixed = section();
813        for i in 0..50 {
814            offer(&mut fixed, i * 10, 10, 1.0, 0);
815        }
816        assert!(!fixed.should_flush_for_run(0, 500, 10, 1000));
817    }
818
819    /// The floor is what "short" means, and both users of it agree.
820    #[test]
821    fn the_runt_floor_is_read_from_the_cost_model() {
822        let cost = CostModel {
823            min_split_items: 4,
824            ..Default::default()
825        };
826        let mut s = WigSection::with_policy(1024, SectionPolicy::Adaptive, cost);
827        // Three values, then flush: short, so the streak rises.
828        for i in 0..3 {
829            offer(&mut s, i * 10, 10, 1.0, 0);
830        }
831        s.clear();
832        for i in 0..3 {
833            offer(&mut s, i * 10, 10, 1.0, 0);
834        }
835        s.clear();
836        // Two runts in a row at a floor of 4: the policy has stopped splitting.
837        let mut at = 0i64;
838        offer(&mut s, at, 10, 1.0, 0);
839        at += 10;
840        assert!(
841            !offer(&mut s, at, 25, 1.0, 0),
842            "still splitting after 2 runts"
843        );
844        assert_eq!(s.encoding(), WigEncoding::BedGraph);
845    }
846
847    #[test]
848    fn the_tail_is_capped_by_the_room_left_in_the_section() {
849        // A batch of a million does not cost a million: only what still fits.
850        let mut s = WigSection::with_policy(8, SectionPolicy::Cost, CostModel::default());
851        for i in 0..4 {
852            offer(&mut s, i * 10, 10, 1.0, 0);
853        }
854        // Room for 4 more, so the tail is 4 * 8 = 32, plus 4 * 8 = 32
855        // buffered; a quarter of 64 is 16, under the split cost.
856        assert!(!offer(&mut s, 30, 25, 1.0, 1_000_000));
857        assert_eq!(s.encoding(), WigEncoding::BedGraph);
858    }
859
860    #[test]
861    fn a_change_of_chromosome_always_flushes() {
862        let mut s = section();
863        s.offer(0, 0, 10, 1.0, 0);
864        assert_eq!(s.offer(1, 0, 10, 1.0, 0), Accept::Flush);
865    }
866
867    #[test]
868    fn a_second_item_of_the_same_span_stays_fixed_step_whatever_its_start() {
869        // With one item buffered there is no step to break.
870        let mut s = section();
871        offer(&mut s, 0, 10, 1.0, 0);
872        assert!(!offer(&mut s, 900, 10, 1.0, 1_000_000));
873        assert_eq!(s.encoding(), WigEncoding::FixedStep);
874        assert_eq!(s.uniform_step, Some(900));
875    }
876
877    #[test]
878    fn widening_a_single_item_section_uses_its_span_as_the_step() {
879        // There is no step yet, and the one start is `first_start` either way.
880        let mut s = widening_section();
881        offer(&mut s, 40, 10, 1.0, 0);
882        offer(&mut s, 60, 25, 1.0, 0); // different span -> bedGraph
883        assert_eq!(s.encoding(), WigEncoding::BedGraph);
884        assert_eq!(s.starts, [40, 60]);
885        assert_eq!(s.ends, [50, 85]);
886    }
887
888    #[test]
889    fn a_run_extends_the_open_section_only_when_it_lines_up() {
890        let mut s = section();
891        s.extend_run(0, 0, 10, &[1.0, 2.0, 3.0]);
892        assert_eq!(s.len(), 3);
893        assert_eq!(s.last_end, 30);
894        assert!(s.extends_run(0, 30, 10));
895        assert!(!s.extends_run(0, 40, 10), "a gap does not extend");
896        assert!(!s.extends_run(0, 30, 20), "a different span does not");
897        assert!(!s.extends_run(1, 30, 10), "another chromosome does not");
898        s.extend_run(0, 30, 10, &[4.0]);
899        assert_eq!(s.len(), 4);
900        assert_eq!(s.encoding(), WigEncoding::FixedStep);
901        assert!(s.starts.is_empty(), "the fast path materialises no starts");
902    }
903
904    #[test]
905    fn a_single_item_section_is_extended_by_a_run_that_meets_it() {
906        // `len() == 1` has no step yet, so a run of the same span extends it.
907        let mut s = section();
908        offer(&mut s, 0, 10, 1.0, 0);
909        assert!(s.extends_run(0, 10, 10));
910        s.extend_run(0, 10, 10, &[2.0, 3.0]);
911        assert_eq!(s.uniform_step, Some(10));
912        assert_eq!(s.len(), 3);
913    }
914
915    // ---- encoding --------------------------------------------------------
916
917    fn header_of(bytes: &[u8]) -> (u32, u32, u32, u32, u32, u8, u16) {
918        let u32_at = |o: usize| u32::from_le_bytes(bytes[o..o + 4].try_into().unwrap());
919        (
920            u32_at(0),
921            u32_at(4),
922            u32_at(8),
923            u32_at(12),
924            u32_at(16),
925            bytes[20],
926            u16::from_le_bytes(bytes[22..24].try_into().unwrap()),
927        )
928    }
929
930    #[test]
931    fn a_fixed_step_section_encodes_to_a_header_and_a_column_of_values() {
932        let mut s = section();
933        for i in 0..3 {
934            offer(&mut s, 100 + i * 10, 10, i as f32, 0);
935        }
936        let bytes = s.encode().unwrap();
937        assert_eq!(bytes.len(), WIG_HEADER_SIZE + 3 * 4);
938        let (chr, start, end, step, span, kind, count) = header_of(&bytes);
939        assert_eq!((chr, start, end), (0, 100, 130));
940        assert_eq!((step, span), (10, 10));
941        assert_eq!(kind, WigEncoding::FixedStep as u8);
942        assert_eq!(count, 3);
943    }
944
945    #[test]
946    fn a_sparse_fixed_step_section_ends_at_its_last_item_not_its_last_step() {
947        // 10 bp items spaced 100 apart: chromEnd is 210, not 300.
948        let mut s = section();
949        for i in 0..3 {
950            offer(&mut s, i * 100, 10, 1.0, 0);
951        }
952        let (_, start, end, step, span, _, _) = header_of(&s.encode().unwrap());
953        assert_eq!((start, end), (0, 210));
954        assert_eq!((step, span), (100, 10));
955    }
956
957    #[test]
958    fn a_var_step_section_writes_starts_and_a_span_but_no_step() {
959        let mut s = widening_section();
960        offer(&mut s, 0, 10, 1.0, 0);
961        offer(&mut s, 10, 10, 2.0, 0);
962        offer(&mut s, 40, 10, 3.0, 0);
963        let bytes = s.encode().unwrap();
964        assert_eq!(bytes.len(), WIG_HEADER_SIZE + 3 * 8);
965        let (_, _, end, step, span, kind, count) = header_of(&bytes);
966        assert_eq!(kind, WigEncoding::VarStep as u8);
967        assert_eq!((step, span, end, count), (0, 10, 50, 3));
968        assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), 0);
969        assert_eq!(u32::from_le_bytes(bytes[32..36].try_into().unwrap()), 10);
970        assert_eq!(u32::from_le_bytes(bytes[40..44].try_into().unwrap()), 40);
971    }
972
973    #[test]
974    fn a_bedgraph_section_writes_both_coordinates_and_neither_step_nor_span() {
975        let mut s = widening_section();
976        offer(&mut s, 0, 10, 1.0, 0);
977        offer(&mut s, 10, 25, 2.0, 0);
978        let bytes = s.encode().unwrap();
979        assert_eq!(bytes.len(), WIG_HEADER_SIZE + 2 * 12);
980        let (_, _, end, step, span, kind, count) = header_of(&bytes);
981        assert_eq!(kind, WigEncoding::BedGraph as u8);
982        assert_eq!((step, span, end, count), (0, 0, 35, 2));
983    }
984
985    #[test]
986    fn a_single_item_section_writes_its_span_as_its_step() {
987        // There is no step, and a reader multiplies the step by the index —
988        // which for the one item is zero either way.
989        let mut s = section();
990        offer(&mut s, 60, 15, 1.0, 0);
991        let (_, start, end, step, span, _, count) = header_of(&s.encode().unwrap());
992        assert_eq!((start, end, step, span, count), (60, 75, 15, 15, 1));
993    }
994
995    #[test]
996    fn an_encoded_section_reads_back_through_the_reader() {
997        // The round trip that matters: the writer's own reader has to decode
998        // what it wrote, item for item.
999        for shape in 0..3 {
1000            let mut s = section();
1001            match shape {
1002                0 => {
1003                    for i in 0..5 {
1004                        offer(&mut s, i * 10, 10, i as f32 * 1.5, 0);
1005                    }
1006                }
1007                1 => {
1008                    offer(&mut s, 0, 10, 1.0, 0);
1009                    offer(&mut s, 10, 10, 2.0, 0);
1010                    offer(&mut s, 45, 10, 3.0, 0);
1011                }
1012                _ => {
1013                    offer(&mut s, 0, 10, 1.0, 0);
1014                    offer(&mut s, 10, 25, 2.0, 0);
1015                    offer(&mut s, 60, 5, 3.0, 0);
1016                }
1017            }
1018            let want: Vec<(i64, i64, f32)> = (0..s.len())
1019                .map(|i| {
1020                    let start = if s.encoding() == WigEncoding::FixedStep {
1021                        s.first_start + i as i64 * s.uniform_step.unwrap_or(s.uniform_span)
1022                    } else {
1023                        s.starts[i]
1024                    };
1025                    let end = if s.encoding() == WigEncoding::BedGraph {
1026                        s.ends[i]
1027                    } else {
1028                        start + s.uniform_span
1029                    };
1030                    (start, end, s.values[i])
1031                })
1032                .collect();
1033
1034            let bytes = bytes::Bytes::from(s.encode().unwrap());
1035            let header = crate::bbi::block::read_wig_header(&bytes, "test.bigwig").unwrap();
1036            assert_eq!(header.item_count as usize, s.len(), "shape {shape}");
1037            let got: Vec<(i64, i64, f32)> = (0..header.item_count as usize)
1038                .map(|i| {
1039                    let item = crate::bbi::block::read_wig_item(&bytes, &header, i, "test.bigwig")
1040                        .unwrap();
1041                    (item.start, item.end, item.value)
1042                })
1043                .collect();
1044            assert_eq!(got, want, "shape {shape}");
1045        }
1046    }
1047
1048    #[test]
1049    fn a_coordinate_past_32_bits_is_refused_rather_than_truncated() {
1050        let mut s = section();
1051        offer(&mut s, 5_000_000_000, 10, 1.0, 0);
1052        let err = s.encode().unwrap_err().to_string();
1053        assert!(err.contains("chromStart 5000000000"), "{err}");
1054    }
1055}