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}