Skip to main content

symcurve/curve/
iters.rs

1//! This module provides data structures and Iterator implementations for the calculation of DNA
2//! curvature.
3//!
4//! It includes the necessary data structures for representing the DNA data and the traits and
5//! implementations for iterating over this data. The iterators provided allow for efficient and
6//! convenient traversal and manipulation of the DNA data for the purpose of curvature calculation.
7use crate::curve::matrix;
8use std::collections::VecDeque;
9use std::f64::consts::{PI, TAU};
10use std::iter::{FusedIterator, Iterator};
11
12/// The step a triplet contributes to the traced path.
13///
14/// The roll displaces along the accumulated twist and the tilt along the perpendicular:
15///
16/// ```text
17/// dx = roll * sin(T) + tilt * sin(T - pi/2)
18/// dy = roll * cos(T) + tilt * cos(T - pi/2)
19/// ```
20///
21/// The tilt term is `T - pi/2`, matching both the reference implementation and the
22/// published equations. `T + pi/2` is the opposite perpendicular, which is the same as
23/// negating the tilt, so the sign here is not free to choose. It has no effect while the
24/// supplied tilt matrix is uniformly zero, which is why it is pinned by a test on this
25/// function rather than by one on the iterator.
26fn step(roll: f64, tilt: f64, twist_sum: f64) -> TripletData {
27    let perpendicular = twist_sum - PI / 2.0;
28    TripletData {
29        dx: (roll * twist_sum.sin()) + (tilt * perpendicular.sin()),
30        dy: (roll * twist_sum.cos()) + (tilt * perpendicular.cos()),
31    }
32}
33
34/// How many items a sliding window will still yield.
35///
36/// A window of `window` items over `remaining` inputs yields `remaining - window + 1`, and
37/// `buffered` items are already held. Reported so that collecting into a `Vec` can
38/// allocate once rather than growing repeatedly, which matters at genome scale.
39fn window_size_hint(
40    inner: (usize, Option<usize>),
41    buffered: usize,
42    window: usize,
43) -> (usize, Option<usize>) {
44    let available = |n: usize| (n + buffered + 1).saturating_sub(window);
45    (available(inner.0), inner.1.map(available))
46}
47
48/// The step a triplet of nucleotides contributes to the traced path.
49///
50/// The twist, roll and tilt looked up for the triplet are combined into this step as it is
51/// produced; only the step itself is carried forward, since nothing downstream reads the
52/// individual parameters.
53///
54/// # Fields
55///
56/// * `dx`: The delta x value, calculated from the roll, tilt and accumulated twist.
57/// * `dy`: The delta y value, calculated from the roll, tilt and accumulated twist.
58#[derive(Clone, Copy, Debug)]
59struct TripletData {
60    dx: f64,
61    dy: f64,
62}
63
64/// An iterator-wrapping struct that yields TripletData from an inner `u8` iterator.
65///
66/// `TripletWindowsIter` wraps around another iterator that yields `u8` (representing nucleotides),
67/// and looks up the roll/tilt/twist values for each triplet of nucleotides in the inner iterator,
68/// then populates a `TripletData` struct with these values.  While iterating, it also keeps track
69/// of the sum of the twist values for the current window of triplets.
70///
71/// # Type Parameters
72///
73/// * `I`: The type of the inner iterator. Must be an iterator over `u8`.
74///
75/// # Fields
76///
77/// * `base_buffer`: A buffer that stores the current triplet of nucleotides.
78/// * `inner`: The inner iterator that yields `u8`.
79/// * `twist_sum`: The sum of the twist values for the current triplet.
80/// * `roll_type`: The current roll type.
81struct TripletWindowsIter<I> {
82    base_buffer: VecDeque<u8>,
83    inner: I,
84    twist_sum: f64,
85    roll_type: matrix::RollType,
86    /// Set once the inner iterator has returned None, so it is not polled again.
87    /// Iterator only guarantees anything about repeated calls after exhaustion for
88    /// iterators that are Fused, and the inner one need not be.
89    inner_done: bool,
90}
91
92/// Implementation of the `Iterator` trait for `TripletWindowsIter` struct.
93///
94/// This iterator yields `TripletData` items, which are calculated based on the next three bases
95/// as a sliding window from the inner iterator, as well as the current roll type.
96///
97/// # Type Parameters
98///
99/// * `I`: The type of the inner iterator. Must be an iterator over `u8`.
100///
101/// # Returns
102///
103/// The `next` method returns `Some(TripletData)` if there are enough items left in the inner
104/// iterator, or `None` if there are not.
105impl<I> Iterator for TripletWindowsIter<I>
106where
107    I: Iterator<Item = u8>,
108{
109    type Item = TripletData;
110
111    fn next(&mut self) -> Option<Self::Item> {
112        // Fill the buffer with the next three items from the inner iterator.
113        while !self.inner_done && self.base_buffer.len() < matrix::TRIPLET_SIZE {
114            match self.inner.next() {
115                Some(item) => self.base_buffer.push_back(item),
116                None => self.inner_done = true,
117            }
118        }
119        // When the buffer is full, calculate the twist, roll, and tilt values.
120        if self.base_buffer.len() >= matrix::TRIPLET_SIZE {
121            // Fixed-size, so no allocation: this runs once per base.
122            let triplet: [u8; matrix::TRIPLET_SIZE] = [
123                self.base_buffer[0],
124                self.base_buffer[1],
125                self.base_buffer[2],
126            ];
127            // Decode the ASCII bases once, then index each matrix with the result.
128            let ixs = matrix::triplet_indices(&triplet).unwrap();
129            let twist = matrix::lookup_by_index(&ixs, &matrix::TWIST);
130            let roll = match self.roll_type {
131                matrix::RollType::Simple => matrix::lookup_by_index(&ixs, &matrix::ROLL_SIMPLE),
132                matrix::RollType::Active => matrix::lookup_by_index(&ixs, &matrix::ROLL_ACTIVE),
133            };
134            let tilt = matrix::lookup_by_index(&ixs, &matrix::TILT);
135            self.twist_sum += twist;
136            // Only sin and cos of this are ever used, so keeping it in [0, TAU) changes
137            // nothing mathematically while stopping it from growing without bound. Left
138            // to accumulate it reaches ~1.5e8 radians over a chromosome, where an ulp is
139            // 3e-8 radians and the per-step rounding has compounded far past that.
140            if !(0.0..TAU).contains(&self.twist_sum) {
141                self.twist_sum = self.twist_sum.rem_euclid(TAU);
142            }
143            // Create a TripletData instance and return it.
144            let window = step(roll, tilt, self.twist_sum);
145            self.base_buffer.pop_front();
146            Some(window)
147        } else {
148            None
149        }
150    }
151
152    fn size_hint(&self) -> (usize, Option<usize>) {
153        window_size_hint(
154            self.inner.size_hint(),
155            self.base_buffer.len(),
156            matrix::TRIPLET_SIZE,
157        )
158    }
159}
160
161impl<I> FusedIterator for TripletWindowsIter<I> where I: Iterator<Item = u8> {}
162
163/// A trait for `u8` Iterators to yield `TripletData`.
164///
165/// `TripletWindowsIterator` is a trait for iterators over `u8` that provides a method for
166/// transforming the iterator into a `TripletWindowsIter`. This allows for convenient conversion
167/// of any iterator over `u8` into an iterator that yields triplets of nucleotides. This is
168/// **layer 1** of the iterator stack.
169///
170/// # Type Parameters
171///
172/// * `Self`: The type implementing this trait. Must be an iterator over `u8`.
173///
174/// # Methods
175///
176/// * `triplet_windows_iter`: Takes a `RollType` and returns a `TripletWindowsIter` that yields
177///   triplets of nucleotides from the original iterator.
178trait TripletWindowsIterator: Iterator<Item = u8> + Sized {
179    fn triplet_windows_iter(self, roll_type: matrix::RollType) -> TripletWindowsIter<Self> {
180        TripletWindowsIter {
181            base_buffer: VecDeque::new(),
182            inner: self,
183            twist_sum: 0.0,
184            roll_type,
185            inner_done: false,
186        }
187    }
188}
189
190impl<I: Iterator<Item = u8>> TripletWindowsIterator for I {}
191
192/// A point on the path traced by the accumulated steps.
193///
194/// # Fields
195///
196/// * `x`: The x coordinate.
197/// * `y`: The y coordinate.
198#[derive(Clone, Copy, Debug)]
199struct CoordsData {
200    x: f64,
201    y: f64,
202}
203
204impl CoordsData {
205    /// Constructor for `CoordsData`.
206    fn new(x: f64, y: f64) -> Self {
207        CoordsData { x, y }
208    }
209}
210
211/// An iterator-wrapping struct that yields `CoordsData` from another iterator.
212///
213/// `CoordsIter` wraps around another iterator that yields `TripletData`, and yields `CoordsData`
214/// calculated from the `TripletData` and the previous coordinates and deltas. It also keeps track
215/// of whether it has yielded the tail coordinates yet.
216///
217/// # Type Parameters
218///
219/// * `I`: The type of the inner iterator. Must be an iterator over `TripletData`.
220///
221/// # Fields
222///
223/// * `inner`: The inner iterator that yields `TripletData`.
224/// * `head`: A boolean that indicates whether the first `CoordsData` has been yielded yet.
225/// * `tail`: A boolean that indicates whether the end of the iterator has been reached,
226///   at which point one more `CoordsData` is yielded with no associated `TripletData`.
227/// * `prev_x_coord`: The x coordinate from the previous `CoordsData`.
228/// * `prev_y_coord`: The y coordinate from the previous `CoordsData`.
229/// * `prev_dx`: The delta x from the previous `TripletData`.
230/// * `prev_dy`: The delta y from the previous `TripletData`.
231struct CoordsIter<I: Iterator> {
232    inner: I,
233    head: bool,
234    tail: bool,
235    prev_x_coord: f64,
236    prev_y_coord: f64,
237    prev_dx: f64,
238    prev_dy: f64,
239}
240
241impl<I> Iterator for CoordsIter<I>
242where
243    I: Iterator<Item = TripletData>,
244{
245    type Item = CoordsData;
246
247    /// Advance the traced path by one step.
248    ///
249    /// The first point the path would yield is the origin before any step has been
250    /// applied, which carries no information, so it is skipped. Once the inner iterator
251    /// runs out one final point is emitted, applying the last step.
252    fn next(&mut self) -> Option<Self::Item> {
253        loop {
254            match self.inner.next() {
255                Some(triplet_data) => {
256                    let point = self.step();
257                    self.prev_dx = triplet_data.dx;
258                    self.prev_dy = triplet_data.dy;
259                    if self.head {
260                        return Some(point);
261                    }
262                    // Discard the origin and go round again rather than recursing.
263                    self.head = true;
264                }
265                None if !self.tail => {
266                    self.tail = true;
267                    return Some(self.step());
268                }
269                None => return None,
270            }
271        }
272    }
273
274    fn size_hint(&self) -> (usize, Option<usize>) {
275        // One point is dropped from the front and one added at the end, so the count
276        // matches the inner iterator's, give or take what has already been consumed.
277        let (lower, upper) = self.inner.size_hint();
278        let pending = usize::from(!self.tail);
279        (
280            lower.saturating_add(pending).saturating_sub(1),
281            upper.and_then(|u| u.checked_add(pending)),
282        )
283    }
284}
285
286impl<I> FusedIterator for CoordsIter<I> where I: Iterator<Item = TripletData> {}
287
288impl<I> CoordsIter<I>
289where
290    I: Iterator<Item = TripletData>,
291{
292    /// Apply the previous step to the current position and return the point reached.
293    fn step(&mut self) -> CoordsData {
294        self.prev_x_coord += self.prev_dx;
295        self.prev_y_coord += self.prev_dy;
296        CoordsData::new(self.prev_x_coord, self.prev_y_coord)
297    }
298}
299
300/// A trait for `TripletData` Iterators to yield `CoordsData`.
301///
302/// `CoordsIterator` is a trait for iterators over `TripletData` that provides a method for
303/// transforming the iterator into a `CoordsIter`. This allows for convenient conversion
304/// of any iterator over `TripletData` into an iterator that yields `CoordsData`. This
305/// is **layer 2** of the iterator stack.
306///
307/// # Type Parameters
308///
309/// * `Self`: The type implementing this trait. Must be an iterator over `TripletData`.
310///
311/// # Methods
312///
313/// * `coords_iter`: Returns a `CoordsIter` that yields `CoordsData` calculated from the
314///   `TripletData` yielded by the original iterator.
315trait CoordsIterator: Iterator<Item = TripletData> + Sized {
316    fn coords_iter(self) -> CoordsIter<Self> {
317        CoordsIter {
318            inner: self,
319            head: false,
320            tail: false,
321            prev_x_coord: 0.0,
322            prev_y_coord: 0.0,
323            prev_dx: 0.0,
324            prev_dy: 0.0,
325        }
326    }
327}
328
329impl<I: Iterator<Item = TripletData>> CoordsIterator for I {}
330
331/// Represents the data for a rolling mean of the x and y coordinates.
332///
333/// # Fields
334///
335/// * `x_bar`: The weighted mean of the x coordinates.
336/// * `y_bar`: The weighted mean of the y coordinates.
337struct RollMeanData {
338    x_bar: f64,
339    y_bar: f64,
340}
341
342/// How many items may pass before the rolling sums are rebuilt from the buffer.
343///
344/// A running sum that is added to and subtracted from never sheds the rounding of the
345/// values that have left it, so its error ratchets upward. Rebuilding costs one pass over
346/// a window of about a hundred items, so amortised over this interval it is a fraction of
347/// a percent of the work.
348const ROLL_SUM_REBUILD_INTERVAL: usize = 1 << 16;
349
350/// Represents the data for a rolling mean of the x and y coordinates.
351///
352/// The `RollMeanData` struct contains the weighted x and y means for a window of coordinates
353/// that is 2 * `step_size` + 1 in length.
354///
355/// # Fields
356///
357/// * `inner`: The inner iterator that yields `CoordsData`.
358/// * `buffer`: A buffer that stores the current window of coordinates.
359/// * `step_size`: Half the size of the window minus one.  In other words,
360///   2 * `step_size` + 1 is the size of the window.
361/// * `x_roll_sum`: The sum of the x coordinates in the current window.
362/// * `y_roll_sum`: The sum of the y coordinates in the current window.
363/// * `since_rebuild`: Items processed since the rolling sums were last rebuilt.
364struct RollMeanIter<I> {
365    inner: I,
366    buffer: VecDeque<CoordsData>,
367    step_size: usize,
368    x_roll_sum: f64,
369    y_roll_sum: f64,
370    since_rebuild: usize,
371    /// Set once the inner iterator has returned None, so it is not polled again.
372    inner_done: bool,
373}
374
375/// Implementation of the `Iterator` trait for `RollMeanIter`.
376///
377/// This iterator wraps another iterator of items of type `CoordsData` and computes
378/// a rolling mean of the `x` and `y` values of the items.
379impl<I> Iterator for RollMeanIter<I>
380where
381    I: Iterator<Item = CoordsData>,
382{
383    type Item = RollMeanData;
384
385    /// Computes the next item of the rolling mean iterator.
386    ///
387    /// This method computes the rolling mean of the `x` and `y` values of the next
388    /// `window_size` items from the inner iterator, where `window_size` is `step_size * 2 + 1`.
389    ///
390    /// The method returns `Some(RollMeanData)` if there are enough items in the inner iterator,
391    /// and `None` otherwise.
392    fn next(&mut self) -> Option<Self::Item> {
393        // Fill the buffer with the next three items from the inner iterator.
394        let window_size = self.step_size * 2 + 1;
395        while !self.inner_done && self.buffer.len() < window_size {
396            match self.inner.next() {
397                Some(item) => {
398                    self.x_roll_sum += item.x;
399                    self.y_roll_sum += item.y;
400                    self.buffer.push_back(item);
401                }
402                None => self.inner_done = true,
403            }
404        }
405        if self.buffer.len() >= window_size {
406            self.since_rebuild += 1;
407            if self.since_rebuild >= ROLL_SUM_REBUILD_INTERVAL {
408                self.x_roll_sum = self.buffer.iter().map(|item| item.x).sum();
409                self.y_roll_sum = self.buffer.iter().map(|item| item.y).sum();
410                self.since_rebuild = 0;
411            }
412            // get the fron/back items without removing them and adjust the roll sum
413            let adj_x_roll_sum = self.x_roll_sum
414                - (0.5 * self.buffer.front().unwrap().x)
415                - (0.5 * self.buffer.back().unwrap().x);
416            let adj_y_roll_sum = self.y_roll_sum
417                - (0.5 * self.buffer.front().unwrap().y)
418                - (0.5 * self.buffer.back().unwrap().y);
419            let x_bar = adj_x_roll_sum / (window_size as f64 - 1_f64);
420            let y_bar = adj_y_roll_sum / (window_size as f64 - 1_f64);
421            let result = Some(RollMeanData { x_bar, y_bar });
422            let item = self.buffer.pop_front().unwrap();
423            self.x_roll_sum -= item.x;
424            self.y_roll_sum -= item.y;
425            result
426        } else {
427            None
428        }
429    }
430
431    fn size_hint(&self) -> (usize, Option<usize>) {
432        window_size_hint(
433            self.inner.size_hint(),
434            self.buffer.len(),
435            self.step_size * 2 + 1,
436        )
437    }
438}
439
440impl<I> FusedIterator for RollMeanIter<I> where I: Iterator<Item = CoordsData> {}
441
442/// A trait for iterators that can compute a rolling mean of `CoordsData`.
443///
444/// This trait extends the `Iterator` trait, adding a `roll_mean_iter` method that
445/// wraps the iterator in a `RollMeanIter`. The `RollMeanIter` computes a rolling mean
446/// of the `x` and `y` values of the items from the original iterator.
447trait RollMeanIterator: Iterator<Item = CoordsData> + Sized {
448    /// Wraps the iterator in a `RollMeanIter`.
449    ///
450    /// This method takes ownership of the iterator and returns a `RollMeanIter` that
451    /// computes a rolling mean of the `x` and `y` values of the items from the original iterator.
452    ///
453    /// # Parameters
454    ///
455    /// * `step_size`: half of the window size minus one. In other words, 2 * `step_size` + 1 is
456    ///   the size of the window.
457    ///
458    /// # Returns
459    ///
460    /// A `RollMeanIter` that computes a rolling mean of the `x` and `y` values of the items.
461    fn roll_mean_iter(self, step_size: usize) -> RollMeanIter<Self> {
462        RollMeanIter {
463            inner: self,
464            buffer: VecDeque::new(),
465            step_size,
466            x_roll_sum: 0.0,
467            y_roll_sum: 0.0,
468            since_rebuild: 0,
469            inner_done: false,
470        }
471    }
472}
473
474impl<I: Iterator<Item = CoordsData>> RollMeanIterator for I {}
475
476/// An iterator that computes the Euclidean distance between pairs of items from an inner iterator.
477///
478/// `EucDistIter` wraps another iterator that yields `RollMeanData`. It computes the Euclidean
479/// distance between each pair of items from the inner iterator.
480///
481/// # Fields
482///
483/// * `inner`: The inner iterator that yields `RollMeanData`.
484///
485/// * `buffer`: A buffer that stores 2 * `curve_step_size` + 1 items from the inner iterator.
486///
487/// * `curve_step_size`: The distance from the midpoint base in the window.  
488struct EucDistIter<I> {
489    inner: I,
490    buffer: VecDeque<RollMeanData>,
491    curve_step_size: usize,
492    /// Set once the inner iterator has returned None, so it is not polled again.
493    inner_done: bool,
494}
495
496impl<I> Iterator for EucDistIter<I>
497where
498    I: Iterator<Item = RollMeanData>,
499{
500    type Item = f64;
501
502    /// Computes the next item of the Euclidean distance iterator.
503    ///
504    /// This method computes the Euclidean distance between each pair of consecutive items
505    /// from the inner iterator. The Euclidean distance is computed as the square root of
506    /// the sum of the squares of the differences of the `x_bar` and `y_bar` values of the items.
507    ///
508    /// The method returns `Some(f64)` if there are enough items in the inner iterator,
509    /// and `None` otherwise.
510    fn next(&mut self) -> Option<Self::Item> {
511        // Fill the buffer with the next three items from the inner iterator.
512        let window_size = self.curve_step_size * 2 + 1;
513        while !self.inner_done && self.buffer.len() < window_size {
514            match self.inner.next() {
515                Some(item) => self.buffer.push_back(item),
516                None => self.inner_done = true,
517            }
518        }
519        if self.buffer.len() >= window_size {
520            let left = self.buffer.front().unwrap();
521            let right = self.buffer.back().unwrap();
522            let curve =
523                ((right.y_bar - left.y_bar).powi(2) + (right.x_bar - left.x_bar).powi(2)).sqrt();
524            self.buffer.pop_front();
525            Some(curve)
526        } else {
527            None
528        }
529    }
530
531    fn size_hint(&self) -> (usize, Option<usize>) {
532        window_size_hint(
533            self.inner.size_hint(),
534            self.buffer.len(),
535            self.curve_step_size * 2 + 1,
536        )
537    }
538}
539
540impl<I> FusedIterator for EucDistIter<I> where I: Iterator<Item = RollMeanData> {}
541
542trait EucDistIterator: Iterator<Item = RollMeanData> + Sized {
543    fn euc_dist_iter(self, curve_step_size: usize) -> EucDistIter<Self> {
544        EucDistIter {
545            inner: self,
546            buffer: VecDeque::new(),
547            curve_step_size,
548            inner_done: false,
549        }
550    }
551}
552
553impl<I: Iterator<Item = RollMeanData>> EucDistIterator for I {}
554
555/// The value assigned when a dyad's symmetry component comes out exactly zero.
556///
557/// A zero sum means every mirrored pair around the dyad was exactly equal, so the
558/// reciprocal is undefined. The reference implementation substitutes 100 and carries on,
559/// and callers downstream treat that as a saturated score rather than a real one.
560pub const DEGENERATE_SYMMETRY: f64 = 100.0;
561
562/// An iterator that computes symmetry of curvature around each candidate dyad.
563///
564/// This is the last stage: it consumes curvature values and yields one symmetry score per
565/// dyad. A score is non-zero only where the curvature has a strict local minimum, which is
566/// what a nucleosome dyad is expected to look like, and rises the more symmetric the
567/// curvature is on either side of it.
568///
569/// # Fields
570///
571/// * `inner`: The inner iterator that yields curvature values.
572/// * `buffer`: A buffer holding 2 * `win` + 1 curvature values, the dyad at its centre.
573/// * `win`: The margin kept on each side of the dyad.
574/// * `step`: The stride, both between dyads and between the mirrored pairs summed at each.
575/// * `inner_done`: Set once the inner iterator has returned None, so it is not polled again.
576pub struct SymCurveIter<I> {
577    inner: I,
578    buffer: VecDeque<f64>,
579    win: usize,
580    step: usize,
581    inner_done: bool,
582}
583
584impl<I> Iterator for SymCurveIter<I>
585where
586    I: Iterator<Item = f64>,
587{
588    type Item = f64;
589
590    /// Computes the symmetry score for the next dyad.
591    ///
592    /// Following the reference implementation, for a dyad \(d\):
593    ///
594    /// ```text
595    /// sum    = SUM over m of |curv[d + m] - curv[d - m]|,  m = 0, step, 2*step, ... <= win/2
596    /// slope  = (curv[d-1] - curv[d]) + (curv[d+1] - curv[d])
597    /// weight = 1/slope   if curv[d] is a strict local minimum and slope >= 0.01
598    ///          0         otherwise
599    /// score  = weight / sum
600    /// ```
601    ///
602    /// The mirrored sum runs out to `win / 2`, but a dyad is only considered once `win`
603    /// values are available on each side. That wider margin is the reference's, and it
604    /// means the first and last `win` curvature values yield no score even though only
605    /// `win / 2` are read. Reproduced here so the output positions match.
606    fn next(&mut self) -> Option<Self::Item> {
607        let span = 2 * self.win + 1;
608        while !self.inner_done && self.buffer.len() < span {
609            match self.inner.next() {
610                Some(value) => self.buffer.push_back(value),
611                None => self.inner_done = true,
612            }
613        }
614        if self.buffer.len() < span {
615            return None;
616        }
617
618        let dyad = self.win;
619        let half = self.win / 2;
620        let mut sum = 0.0;
621        let mut offset = 0;
622        while offset <= half {
623            sum += (self.buffer[dyad + offset] - self.buffer[dyad - offset]).abs();
624            offset += self.step;
625        }
626
627        let current = self.buffer[dyad];
628        let before = self.buffer[dyad - 1];
629        let after = self.buffer[dyad + 1];
630        let slope = (before - current) + (after - current);
631        let weight = if current < before && current < after && slope >= 0.01 {
632            1.0 / slope
633        } else {
634            0.0
635        };
636
637        // Compared against zero exactly, as the reference does: the substitution is for a
638        // sum that is precisely zero, not one that is merely small.
639        let score = if sum != 0.0 {
640            weight / sum
641        } else {
642            DEGENERATE_SYMMETRY
643        };
644
645        // Advance by `step`: drop what the buffer holds and skip the rest at the source,
646        // so a stride longer than the span still lands on the reference's next dyad.
647        let held = self.buffer.len().min(self.step);
648        self.buffer.drain(..held);
649        for _ in held..self.step {
650            if self.inner.next().is_none() {
651                self.inner_done = true;
652                break;
653            }
654        }
655        Some(score)
656    }
657
658    fn size_hint(&self) -> (usize, Option<usize>) {
659        let (lower, upper) =
660            window_size_hint(self.inner.size_hint(), self.buffer.len(), span_of(self.win));
661        let strided = |n: usize| n.div_ceil(self.step);
662        (strided(lower), upper.map(strided))
663    }
664}
665
666impl<I> FusedIterator for SymCurveIter<I> where I: Iterator<Item = f64> {}
667
668/// The number of curvature values a dyad needs in view: `win` on each side, plus itself.
669fn span_of(win: usize) -> usize {
670    2 * win + 1
671}
672
673/// A trait for curvature iterators to yield symmetry scores.
674///
675/// This is **layer 5** of the iterator stack, sitting on the curvature values that
676/// [`CurveIter`] produces.
677pub trait SymCurveIterator: Iterator<Item = f64> + Sized {
678    /// Wraps the iterator in a [`SymCurveIter`].
679    ///
680    /// # Parameters
681    ///
682    /// * `win`: The margin kept on each side of a dyad. The reference uses 101.
683    /// * `step`: The stride between dyads and between mirrored pairs. Values below 1 are
684    ///   treated as 1, since a stride of zero would never advance.
685    fn sym_curve_iter(self, win: usize, step: usize) -> SymCurveIter<Self> {
686        SymCurveIter {
687            inner: self,
688            buffer: VecDeque::new(),
689            win,
690            step: step.max(1),
691            inner_done: false,
692        }
693    }
694}
695
696impl<I: Iterator<Item = f64>> SymCurveIterator for I {}
697
698/// An iterator that computes the curvature of a DNA sequence.
699///
700/// `CurveIter` wraps an iterator that yields `u8` and computes the curvature of the DNA sequence
701/// represented by the nucleotides.
702///
703/// # Fields
704///
705/// * `inner`: The inner iterator that yields `u8`.
706pub struct CurveIter<I: Iterator<Item = u8>> {
707    inner: EucDistIter<RollMeanIter<CoordsIter<TripletWindowsIter<I>>>>,
708    curve_scale: f64,
709}
710
711impl<I: Iterator<Item = u8>> Iterator for CurveIter<I> {
712    type Item = f64;
713
714    /// Computes the next item of the curvature iterator.
715    fn next(&mut self) -> Option<Self::Item> {
716        self.inner.next().map(|x| x * self.curve_scale)
717    }
718
719    fn size_hint(&self) -> (usize, Option<usize>) {
720        // Scaling is one-to-one, so the stack below reports the count unchanged.
721        self.inner.size_hint()
722    }
723}
724
725impl<I: Iterator<Item = u8>> FusedIterator for CurveIter<I> {}
726
727/// Construct a `CurveIter` from an iterator that yields `u8`.
728///
729/// This function constructs a `CurveIter` from an iterator that yields `u8`. The `CurveIter`
730/// computes the curvature of the DNA sequence represented by the nucleotides.
731///
732/// # Parameters
733///
734/// * `seq_iter`: An iterator that yields `u8`.
735/// * `roll_type`: The type of roll (either simple or activated).
736/// * `step_b`: Half of the window size minus one. In other words, 2 * `step_size` + 1 is
737///   the size of the window.
738/// * `step_c`: The distance from the midpoint base to the sides in the curve window.
739impl<I: Iterator<Item = u8>> CurveIter<I> {
740    /// Build a curvature iterator over a stream of bases.
741    ///
742    /// Bases must be A, C, G or T in either case; anything else will panic, so split a
743    /// record with [`crate::fasta::split_seq_by_gaps`] first. For scoring whole records
744    /// use [`crate::curve::scan`], which handles splitting, positions and threading.
745    ///
746    /// The first `step_b + step_c + 1` bases and the last of the same produce no value,
747    /// because the windows need context on both sides.
748    ///
749    /// ```
750    /// use symcurve::curve::iters::CurveIter;
751    /// use symcurve::curve::matrix::RollType;
752    ///
753    /// let seq = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
754    /// let curves: Vec<f64> =
755    ///     CurveIter::new(seq.iter().copied(), RollType::Simple, 5, 15, 0.33335).collect();
756    /// assert_eq!(curves.len(), seq.len() - 2 * (5 + 15 + 1));
757    /// ```
758    pub fn new(
759        seq_iter: I,
760        roll_type: matrix::RollType,
761        step_b: usize,
762        step_c: usize,
763        curve_scale: f64,
764    ) -> Self {
765        Self {
766            inner: seq_iter
767                .triplet_windows_iter(roll_type)
768                .coords_iter()
769                .roll_mean_iter(step_b)
770                .euc_dist_iter(step_c),
771            curve_scale,
772        }
773    }
774}
775
776#[cfg(test)]
777mod tests {
778    use super::*;
779    use approx::assert_relative_eq;
780
781    /// Below is a table of some of the expected values for the triplet iterator over the DNA
782    ///
783    /// | pos|nuc|trip | ixs |  twist |  roll_s |   tilt |twist_sum| dx_simp | dy_simp |
784    /// | --:| -:| --: | --: | -----: | ------: | -----: | ------: | ------: | ------: |
785    /// |  0 | C | CCA | 330 | 0.5986 |  0.7000 | 0.0000 |  0.5986 |  0.3945 |  0.5783 |
786    /// |  1 | C | CAA | 300 | 0.5986 |  6.2000 | 0.0000 |  1.1973 |  5.7725 |  2.2622 |
787    /// |  2 | A | AAC | 003 | 0.5986 |  1.6000 | 0.0000 |  1.7959 |  1.5596 | -0.3572 |
788    /// |  3 | A | ACA | 030 | 0.5986 |  5.8000 | 0.0000 |  2.3946 |  3.9408 | -4.2556 |
789    /// |  4 | C | CAT | 301 | 0.5986 |  8.7000 | 0.0000 |  2.9932 |  1.2860 | -8.6044 |
790    /// |  5 | A | ATT | 011 | 0.5986 |  0.0000 | 0.0000 |  3.5919 |  0.0000 |  0.0000 |
791    /// |  6 | T | TTT | 111 | 0.5986 |  0.1000 | 0.0000 |  4.1905 | -0.0867 | -0.0498 |
792    /// |  7 | T | TTT | 111 | 0.5986 |  0.1000 | 0.0000 |  4.7892 | -0.0997 |  0.0077 |
793    /// |  8 | T | TTG | 112 | 0.5986 |  6.2000 | 0.0000 |  5.3878 | -4.8387 |  3.8765 |
794    /// |  9 | T | TGA | 120 | 0.5986 | 10.0000 | 0.0000 |  5.9865 | -2.9238 |  9.5630 |
795    /// | 10 | G | GAC | 203 | 0.5986 |  5.6000 | 0.0000 |  6.5851 |  1.6653 |  5.3467 |
796    /// | 11 | A | ACT | 031 | 0.5986 |  2.0000 | 0.0000 |  7.1838 |  1.5674 |  1.2423 |
797    /// | 12 | C | CTT | 311 | 0.5986 |  4.2000 | 0.0000 |  7.7824 |  4.1892 |  0.3003 |
798    /// | 13 | T | TTT | 111 | 0.5986 |  0.1000 | 0.0000 |  8.3811 |  0.0864 | -0.0503 |
799    /// | 14 | T | TTT | 111 | 0.5986 |  0.1000 | 0.0000 |  8.9797 |  0.0431 | -0.0903 |
800    /// | 15 | T | TTT | 111 | 0.5986 |  0.1000 | 0.0000 |  9.5784 | -0.0153 | -0.0988 |
801    /// | 16 | T | TTG | 112 | 0.5986 |  6.2000 | 0.0000 | 10.1770 | -4.2363 | -4.5270 |
802    /// | 17 | T | TGG | 122 | 0.5986 |  0.7000 | 0.0000 | 10.7757 | -0.6831 | -0.1527 |
803    /// | 18 | G | GGG | 222 | 0.5986 |  5.7000 | 0.0000 | 11.3743 | -5.2961 |  2.1075 |
804    /// | 19 | G | GGA | 220 | 0.5986 |  6.2000 | 0.0000 | 11.9729 | -3.4670 |  5.1400 |
805    /// | 20 | G | GAG | 202 | 0.5986 |  6.6000 | 0.0000 | 12.5716 |  0.0345 |  6.5999 |
806    /// | 21 | A | AGG | 022 | 0.5986 |  4.7000 | 0.0000 | 13.1702 |  2.6688 |  3.8688 |
807    /// | 22 | G | GGG | 222 | 0.5986 |  5.7000 | 0.0000 | 13.7689 |  5.3178 |  2.0520 |
808    /// | 23 | G | GGC | 223 | 0.5986 |  8.2000 | 0.0000 | 14.3675 |  7.9834 | -1.8724 |
809    /// | 24 | G | GCA | 230 | 0.5986 |  7.5000 | 0.0000 | 14.9662 |  5.0670 | -5.5295 |
810    /// | 25 | C | CAC | 303 | 0.5986 |  6.8000 | 0.0000 | 15.5648 |  0.9700 | -6.7305 |
811    /// | 26 | A | ACT | 031 | 0.5986 |  2.0000 | 0.0000 | 16.1635 | -0.8799 | -1.7961 |
812    /// | 27 | C | CTA | 310 | 0.5986 |  7.8000 | 0.0000 | 16.7621 | -6.7820 | -3.8528 |
813    /// | 28 | T | TAG | 102 | 0.5986 |  7.8000 | 0.0000 | 17.3608 | -7.7738 |  0.6390 |
814    /// | 29 | A | AGC | 023 | 0.5986 |  6.3000 | 0.0000 | 17.9594 | -4.8961 |  3.9646 |
815    /// | 30 | G | GCA | 230 | 0.5986 |  7.5000 | 0.0000 | 18.5581 | -2.1553 |  7.1836 |
816    /// | 31 | C | CAC | 303 | 0.5986 |  6.8000 | 0.0000 | 19.1567 |  2.0560 |  6.4817 |
817    /// | 32 | A | ACC | 033 | 0.5986 |  5.2000 | 0.0000 | 19.7554 |  4.0920 |  3.2087 |
818    /// | 33 | C | CCT | 331 | 0.5986 |  4.7000 | 0.0000 | 20.3540 |  4.6897 |  0.3116 |
819    /// | 34 | C | CTA | 310 | 0.5986 |  7.8000 | 0.0000 | 20.9527 |  6.7208 | -3.9587 |
820    /// | 35 | T | TAT | 101 | 0.5986 |  9.7000 | 0.0000 | 21.5513 |  4.1302 | -8.7767 |
821    /// | 36 | A | ATC | 013 | 0.5986 |  3.6000 | 0.0000 | 22.1500 | -0.5693 | -3.5547 |
822    /// | 37 | T | TCT | 131 | 0.5986 |  6.5000 | 0.0000 | 22.7486 | -4.4660 | -4.7228 |
823    /// | 38 | C | CTA | 310 | 0.5986 |  7.8000 | 0.0000 | 23.3472 | -7.6209 | -1.6618 |
824    /// | 39 | T | TAC | 103 | 0.5986 |  6.4000 | 0.0000 | 23.9459 | -5.9340 |  2.3974 |
825    /// | 40 | A | ACC | 033 | 0.5986 |  5.2000 | 0.0000 | 24.5445 | -2.8853 |  4.3261 |
826    /// | 41 | C | CCC | 333 | 0.5986 |  5.7000 | 0.0000 | 25.1432 |  0.0596 |  5.6997 |
827    /// | 42 | C | CCT | 331 | 0.5986 |  4.7000 | 0.0000 | 25.7418 |  2.6890 |  3.8548 |
828    /// | 43 | C | CTG | 312 | 0.5986 |  9.6000 | 0.0000 | 26.3405 |  8.9743 |  3.4092 |
829    /// | 44 | T | TGA | 120 | 0.5986 | 10.0000 | 0.0000 | 26.9391 |  9.7238 | -2.3342 |
830    /// | 45 | G | GAA | 200 | 0.5986 |  5.1000 | 0.0000 | 27.5378 |  3.4259 | -3.7780 |
831    /// | 46 | A | AAT | 001 | 0.5986 |  0.0000 | 0.0000 | 28.1364 |  0.0000 |  0.0000 |
832    /// | 47 | A | ATC | 013 | 0.5986 |  3.6000 | 0.0000 | 28.7351 | -1.6006 | -3.2246 |
833    /// | 48 | T |     |     |         |        |        |         |         |         |
834    /// | 49 | C |     |     |         |        |        |         |         |         |
835    #[test]
836    fn test_triplet_iter_long() {
837        let dna = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
838        let windows: Vec<TripletData> = dna
839            .iter()
840            .cloned()
841            .triplet_windows_iter(matrix::RollType::Simple)
842            .collect();
843        assert_eq!(windows.len(), dna.len() - 2);
844        // check first two
845        assert_relative_eq!(windows[0].dx, 0.3945, epsilon = 1e-4);
846        assert_relative_eq!(windows[0].dy, 0.5783, epsilon = 1e-4);
847        assert_relative_eq!(windows[1].dx, 5.7725, epsilon = 1e-4);
848        assert_relative_eq!(windows[1].dy, 2.2622, epsilon = 1e-4);
849        // check last two
850        assert_relative_eq!(windows[46].dx, 0.0000, epsilon = 1e-4);
851        assert_relative_eq!(windows[46].dy, 0.0000, epsilon = 1e-4);
852        assert_relative_eq!(windows[47].dx, -1.6006, epsilon = 1e-4);
853        assert_relative_eq!(windows[47].dy, -3.2246, epsilon = 1e-4);
854    }
855
856    #[test]
857    fn test_triplet_iter_too_short() {
858        let dna = b"AC";
859        let windows: Vec<TripletData> = dna
860            .iter()
861            .cloned()
862            .triplet_windows_iter(matrix::RollType::Simple)
863            .collect();
864        assert_eq!(windows.len(), 0);
865    }
866
867    /// Below is a table of some of the expected values for the coords iterator over the DNA
868    ///
869    /// | pos|nuc|trip | dx_simp | dy_simp |  x_coord |  y_coord |
870    /// | --:| -:| --: | ------: | ------: | -------: | -------: |
871    /// |  0 | C | CCA |  0.3945 |  0.5783 |          |          |
872    /// |  1 | C | CAA |  5.7725 |  2.2622 |   0.3945 |   0.5783 |
873    /// |  2 | A | AAC |  1.5596 | -0.3572 |   6.1670 |   2.8405 |
874    /// |  3 | A | ACA |  3.9408 | -4.2556 |   7.7266 |   2.4833 |
875    /// |  4 | C | CAT |  1.2860 | -8.6044 |  11.6674 |  -1.7723 |
876    /// |  5 | A | ATT |  0.0000 |  0.0000 |  12.9534 | -10.3767 |
877    /// |  6 | T | TTT | -0.0867 | -0.0498 |  12.9534 | -10.3767 |
878    /// |  7 | T | TTT | -0.0997 |  0.0077 |  12.8667 | -10.4266 |
879    /// |  8 | T | TTG | -4.8387 |  3.8765 |  12.7670 | -10.4189 |
880    /// |  9 | T | TGA | -2.9238 |  9.5630 |   7.9283 |  -6.5424 |
881    /// | 10 | G | GAC |  1.6653 |  5.3467 |   5.0045 |   3.0206 |
882    /// | 11 | A | ACT |  1.5674 |  1.2423 |   6.6698 |   8.3673 |
883    /// | 12 | C | CTT |  4.1892 |  0.3003 |   8.2372 |   9.6096 |
884    /// | 13 | T | TTT |  0.0864 | -0.0503 |  12.4264 |   9.9099 |
885    /// | 14 | T | TTT |  0.0431 | -0.0903 |  12.5128 |   9.8596 |
886    /// | 15 | T | TTT | -0.0153 | -0.0988 |  12.5559 |   9.7693 |
887    /// | 16 | T | TTG | -4.2363 | -4.5270 |  12.5406 |   9.6705 |
888    /// | 17 | T | TGG | -0.6831 | -0.1527 |   8.3043 |   5.1435 |
889    /// | 18 | G | GGG | -5.2961 |  2.1075 |   7.6212 |   4.9908 |
890    /// | 19 | G | GGA | -3.4670 |  5.1400 |   2.3251 |   7.0983 |
891    /// | 20 | G | GAG |  0.0345 |  6.5999 |  -1.1419 |  12.2383 |
892    /// | 21 | A | AGG |  2.6688 |  3.8688 |  -1.1074 |  18.8382 |
893    /// | 22 | G | GGG |  5.3178 |  2.0520 |   1.5614 |  22.7069 |
894    /// | 23 | G | GGC |  7.9834 | -1.8724 |   6.8792 |  24.7590 |
895    /// | 24 | G | GCA |  5.0670 | -5.5295 |  14.8626 |  22.8866 |
896    /// | 25 | C | CAC |  0.9700 | -6.7305 |  19.9296 |  17.3571 |
897    /// | 26 | A | ACT | -0.8799 | -1.7961 |  20.8995 |  10.6266 |
898    /// | 27 | C | CTA | -6.7820 | -3.8528 |  20.0197 |   8.8305 |
899    /// | 28 | T | TAG | -7.7738 |  0.6390 |  13.2377 |   4.9777 |
900    /// | 29 | A | AGC | -4.8961 |  3.9646 |   5.4639 |   5.6167 |
901    /// | 30 | G | GCA | -2.1553 |  7.1836 |   0.5678 |   9.5814 |
902    /// | 31 | C | CAC |  2.0560 |  6.4817 |  -1.5875 |  16.7650 |
903    /// | 32 | A | ACC |  4.0920 |  3.2087 |   0.4685 |  23.2467 |
904    /// | 33 | C | CCT |  4.6897 |  0.3116 |   4.5605 |  26.4554 |
905    /// | 34 | C | CTA |  6.7208 | -3.9587 |   9.2502 |  26.7669 |
906    /// | 35 | T | TAT |  4.1302 | -8.7767 |  15.9709 |  22.8083 |
907    /// | 36 | A | ATC | -0.5693 | -3.5547 |  20.1012 |  14.0315 |
908    /// | 37 | T | TCT | -4.4660 | -4.7228 |  19.5319 |  10.4768 |
909    /// | 38 | C | CTA | -7.6209 | -1.6618 |  15.0659 |   5.7540 |
910    /// | 39 | T | TAC | -5.9340 |  2.3974 |   7.4450 |   4.0922 |
911    /// | 40 | A | ACC | -2.8853 |  4.3261 |   1.5109 |   6.4896 |
912    /// | 41 | C | CCC |  0.0596 |  5.6997 |  -1.3743 |  10.8157 |
913    /// | 42 | C | CCT |  2.6890 |  3.8548 |  -1.3148 |  16.5154 |
914    /// | 43 | C | CTG |  8.9743 |  3.4092 |   1.3742 |  20.3701 |
915    /// | 44 | T | TGA |  9.7238 | -2.3342 |  10.3485 |  23.7794 |
916    /// | 45 | G | GAA |  3.4259 | -3.7780 |  20.0722 |  21.4451 |
917    /// | 46 | A | AAT |  0.0000 |  0.0000 |  23.4981 |  17.6671 |
918    /// | 47 | A | ATC | -1.6006 | -3.2246 |  23.4981 |  17.6671 |
919    /// | 48 | T |     |         |         |  21.8975 |  14.4425 |
920    /// | 49 | C |     |         |         |          |          |
921    #[test]
922    fn test_coords_iter() {
923        let dna = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
924        let coords: Vec<CoordsData> = dna
925            .iter()
926            .cloned()
927            .triplet_windows_iter(matrix::RollType::Simple)
928            .coords_iter()
929            .collect();
930        let coords_len = coords.len();
931        assert_eq!(coords_len, dna.len() - 2);
932        // check first two
933        assert_relative_eq!(coords[0].x, 0.3945, epsilon = 1e-4);
934        assert_relative_eq!(coords[0].y, 0.5783, epsilon = 1e-4);
935        assert_relative_eq!(coords[1].x, 6.1670, epsilon = 1e-4);
936        assert_relative_eq!(coords[1].y, 2.8405, epsilon = 1e-4);
937        // check last two
938        assert_relative_eq!(coords[coords_len - 2].x, 23.4981, epsilon = 1e-4);
939        assert_relative_eq!(coords[coords_len - 2].y, 17.6671, epsilon = 1e-4);
940        assert_relative_eq!(coords[coords_len - 1].x, 21.8975, epsilon = 1e-4);
941        assert_relative_eq!(coords[coords_len - 1].y, 14.4425, epsilon = 1e-4);
942    }
943
944    /// Helper for test_rollmean_iter()
945    fn get_some_coords() -> Vec<CoordsData> {
946        let x_values = vec![
947            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
948        ];
949        let y_values = vec![
950            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0,
951        ];
952
953        x_values
954            .into_iter()
955            .zip(y_values)
956            .map(|(x, y)| CoordsData::new(x, y))
957            .collect()
958    }
959
960    #[test]
961    fn test_rollmean_iter() {
962        let rolls: Vec<_> = get_some_coords().into_iter().roll_mean_iter(2).collect();
963        assert_eq!(rolls.len(), 8);
964        // x̄₃ = (½x₁ + x₂ + x₃ + x₄ + ½x₅)/4
965        // x̄₃ = (0.5 + 2 + 3 + 4 + 2.5)/4 = 3
966        assert_relative_eq!(rolls[0].x_bar, 3.0, epsilon = 1e-4);
967        assert_relative_eq!(rolls[0].y_bar, 0.0, epsilon = 1e-4);
968        // x̄₃ = (½x₂ + x₃ + x₄ + x₅ + ½x₆)/4
969        // x̄₃ = (1 + 3 + 4 + 5 + 3)/4 = 16 / 4 = 4
970        assert_relative_eq!(rolls[1].x_bar, 4.0, epsilon = 1e-4);
971        assert_relative_eq!(rolls[2].x_bar, 5.0, epsilon = 1e-4);
972        assert_relative_eq!(rolls[7].y_bar, 10.0, epsilon = 1e-4);
973        let rolls: Vec<_> = get_some_coords().into_iter().roll_mean_iter(3).collect();
974        // x̄₃ = (½x₁ + x₂ + x₃ + x₄ + x₅ + x₆+ ½x₇)/6
975        // x̄₃ = (0.5 + 2 + 3 + 4 + 5 + 6 + 3.5)/6 = 24 / 6 = 4
976        assert_relative_eq!(rolls[0].x_bar, 4.0, epsilon = 1e-4);
977        assert_eq!(rolls.len(), 6);
978    }
979
980    /// | pos|nuc|trip |  x_coord |  y_coord |    x_bar |    y_bar |
981    /// | --:| -:| --: | -------: | -------: | -------: | -------: |
982    /// |  0 | C | CCA |          |          |          |          |
983    /// |  1 | C | CAA |   0.3945 |   0.5783 |          |          |
984    /// |  2 | A | AAC |   6.1670 |   2.8405 |          |          |
985    /// |  3 | A | ACA |   7.7266 |   2.4833 |          |          |
986    /// |  4 | C | CAT |  11.6674 |  -1.7723 |          |          |
987    /// |  5 | A | ATT |  12.9534 | -10.3767 |          |          |
988    /// |  6 | T | TTT |  12.9534 | -10.3767 |   9.3566 |  -3.7097 |
989    /// |  7 | T | TTT |  12.8667 | -10.4266 |   9.7739 |  -2.9818 |
990    /// |  8 | T | TTG |  12.7670 | -10.4189 |  10.1124 |  -2.2720 |
991    /// |  9 | T | TGA |   7.9283 |  -6.5424 |  10.3897 |  -1.3191 |
992    /// | 10 | G | GAC |   5.0045 |   3.0206 |  10.4121 |   0.2698 |
993    /// | 11 | A | ACT |   6.6698 |   8.3673 |  10.3716 |   2.2795 |
994    /// | 12 | C | CTT |   8.2372 |   9.6096 |  10.1228 |   4.0604 |
995    /// | 13 | T | TTT |  12.4264 |   9.9099 |   9.6374 |   5.6094 |
996    /// | 14 | T | TTT |  12.5128 |   9.8596 |   9.0999 |   7.0619 |
997    /// | 15 | T | TTT |  12.5559 |   9.7693 |   8.5125 |   8.2048 |
998    /// | 16 | T | TTG |  12.5406 |   9.6705 |   7.8163 |   9.1892 |
999    /// | 17 | T | TGG |   8.3043 |   5.1435 |   7.0936 |  10.3676 |
1000    /// | 18 | G | GGG |   7.6212 |   4.9908 |   6.4825 |  11.7650 |
1001    /// | 19 | G | GGA |   2.3251 |   7.0983 |   6.3226 |  13.1588 |
1002    /// | 20 | G | GAG |  -1.1419 |  12.2383 |   6.8088 |  14.1895 |
1003    /// | 21 | A | AGG |  -1.1074 |  18.8382 |   7.5954 |  14.6167 |
1004    /// | 22 | G | GGG |   1.5614 |  22.7069 |   8.5991 |  14.8489 |
1005    /// | 23 | G | GGC |   6.8792 |  24.7590 |   9.4657 |  15.0326 |
1006    /// | 24 | G | GCA |  14.8626 |  22.8866 |   9.9035 |  14.9578 |
1007    /// | 25 | C | CAC |  19.9296 |  17.3571 |  10.1459 |  14.7509 |
1008    /// | 26 | A | ACT |  20.8995 |  10.6266 |  10.2074 |  14.5144 |
1009    /// | 27 | C | CTA |  20.0197 |   8.8305 |  10.1287 |  14.4377 |
1010    /// | 28 | T | TAG |  13.2377 |   4.9777 |   9.9582 |  14.5496 |
1011    /// | 29 | A | AGC |   5.4639 |   5.6167 |   9.5616 |  14.8284 |
1012    /// | 30 | G | GCA |   0.5678 |   9.5814 |   9.0830 |  15.2950 |
1013    /// | 31 | C | CAC |  -1.5875 |  16.7650 |   8.8452 |  15.7378 |
1014    /// | 32 | A | ACC |   0.4685 |  23.2467 |   8.7809 |  15.9903 |
1015    /// | 33 | C | CCT |   4.5605 |  26.4554 |   8.8479 |  16.1115 |
1016    /// | 34 | C | CTA |   9.2502 |  26.7669 |   9.0384 |  16.0740 |
1017    /// | 35 | T | TAT |  15.9709 |  22.8083 |   9.1846 |  15.8432 |
1018    /// | 36 | A | ATC |  20.1012 |  14.0315 |   9.2424 |  15.3912 |
1019    /// | 37 | T | TCT |  19.5319 |  10.4768 |   9.1639 |  14.7571 |
1020    /// | 38 | C | CTA |  15.0659 |   5.7540 |   8.9154 |  14.1163 |
1021    /// | 39 | T | TAC |   7.4450 |   4.0922 |   8.8110 |  13.6627 |
1022    /// | 40 | A | ACC |   1.5109 |   6.4896 |   9.0710 |  13.4451 |
1023    /// | 41 | C | CCC |  -1.3743 |  10.8157 |   9.4459 |  13.5588 |
1024    /// | 42 | C | CCT |  -1.3148 |  16.5154 |   9.8141 |  14.1000 |
1025    /// | 43 | C | CTG |   1.3742 |  20.3701 |  10.3540 |  14.8940 |
1026    /// | 44 | T | TGA |  10.3485 |  23.7794 |          |          |
1027    /// | 45 | G | GAA |  20.0722 |  21.4451 |          |          |
1028    /// | 46 | A | AAT |  23.4981 |  17.6671 |          |          |
1029    /// | 47 | A | ATC |  23.4981 |  17.6671 |          |          |
1030    /// | 48 | T |     |  21.8975 |  14.4425 |          |          |
1031    /// | 49 | C |     |          |          |          |          |
1032    #[test]
1033    fn test_rollmeans_from_seq() {
1034        let dna = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
1035        let step_size = 5;
1036        let means: Vec<RollMeanData> = dna
1037            .iter()
1038            .cloned()
1039            .triplet_windows_iter(matrix::RollType::Simple)
1040            .coords_iter()
1041            .roll_mean_iter(step_size)
1042            .collect();
1043        let means_len = means.len();
1044        assert_eq!(means_len, dna.len() - 2 - 2 * step_size);
1045        // check first two
1046        assert_relative_eq!(means[0].x_bar, 9.3566, epsilon = 1e-4);
1047        assert_relative_eq!(means[0].y_bar, -3.7097, epsilon = 1e-4);
1048        assert_relative_eq!(means[1].x_bar, 9.7739, epsilon = 1e-4);
1049        assert_relative_eq!(means[1].y_bar, -2.9818, epsilon = 1e-4);
1050        // check last two
1051        assert_relative_eq!(means[means_len - 2].x_bar, 9.8141, epsilon = 1e-4);
1052        assert_relative_eq!(means[means_len - 2].y_bar, 14.1000, epsilon = 1e-4);
1053        assert_relative_eq!(means[means_len - 1].x_bar, 10.3540, epsilon = 1e-4);
1054        assert_relative_eq!(means[means_len - 1].y_bar, 14.8940, epsilon = 1e-4);
1055    }
1056
1057    /// Helper for test_eucdist_iter()
1058    fn get_some_means() -> Vec<RollMeanData> {
1059        let x_values = vec![3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 8.0, 5.0, 17.0];
1060        let y_values = vec![0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0, 10.0, 10.0];
1061
1062        x_values
1063            .into_iter()
1064            .zip(y_values)
1065            .map(|(x_bar, y_bar)| RollMeanData { x_bar, y_bar })
1066            .collect()
1067    }
1068
1069    #[test]
1070    fn test_eucdist_iter() {
1071        let mean_rolls: Vec<_> = get_some_means();
1072        let vec_size = mean_rolls.len();
1073        let curve_step_size = 2;
1074        let euc_dists: Vec<_> = mean_rolls
1075            .into_iter()
1076            .euc_dist_iter(curve_step_size)
1077            .collect();
1078        // check curve_step_size number of items on both flanks are discarded
1079        assert_eq!(euc_dists.len(), vec_size - 2 * (curve_step_size));
1080        // √((7.0-3.0)² + (10.0-0.0)²) = √116 = 10.770329614269007
1081        assert_relative_eq!(euc_dists[0], 10.7703, epsilon = 1e-4);
1082        // √((8.0-4.0)² + (10.0-0.0)²) = √116 = 10.770329614269007
1083        assert_relative_eq!(euc_dists[1], 10.7703, epsilon = 1e-4);
1084        // √((8.0-5.0)² + (10.0-0.0)²) = √109 = 10.44031
1085        assert_relative_eq!(euc_dists[2], 10.44031, epsilon = 1e-4);
1086        // √((5.0-6.0)² + (10.0-0.0)²) = √101 = 10.04988
1087        assert_relative_eq!(euc_dists[3], 10.04988, epsilon = 1e-4);
1088        // √((17.0-7.0)² + (10.0-10.0)²) = √100 = 10.0
1089        assert_relative_eq!(euc_dists[4], 10.0, epsilon = 1e-4);
1090    }
1091    /// | pos|nuc|trip |    x_bar |    y_bar |   curve |
1092    /// | --:| -:| --: | -------: | -------: | ------: |
1093    /// |  0 | C | CCA |          |          |         |
1094    /// |  1 | C | CAA |          |          |         |
1095    /// |  2 | A | AAC |          |          |         |
1096    /// |  3 | A | ACA |          |          |         |
1097    /// |  4 | C | CAT |          |          |         |
1098    /// |  5 | A | ATT |          |          |         |
1099    /// |  6 | T | TTT |   9.3566 |  -3.7097 |         |
1100    /// |  7 | T | TTT |   9.7739 |  -2.9818 |         |
1101    /// |  8 | T | TTG |  10.1124 |  -2.2720 |         |
1102    /// |  9 | T | TGA |  10.3897 |  -1.3191 |         |
1103    /// | 10 | G | GAC |  10.4121 |   0.2698 |         |
1104    /// | 11 | A | ACT |  10.3716 |   2.2795 |         |
1105    /// | 12 | C | CTT |  10.1228 |   4.0604 |         |
1106    /// | 13 | T | TTT |   9.6374 |   5.6094 |         |
1107    /// | 14 | T | TTT |   9.0999 |   7.0619 |         |
1108    /// | 15 | T | TTT |   8.5125 |   8.2048 |         |
1109    /// | 16 | T | TTG |   7.8163 |   9.1892 |         |
1110    /// | 17 | T | TGG |   7.0936 |  10.3676 |         |
1111    /// | 18 | G | GGG |   6.4825 |  11.7650 |         |
1112    /// | 19 | G | GGA |   6.3226 |  13.1588 |         |
1113    /// | 20 | G | GAG |   6.8088 |  14.1895 |         |
1114    /// | 21 | A | AGG |   7.5954 |  14.6167 | 19.1012 |
1115    /// | 22 | G | GGG |   8.5991 |  14.8489 | 17.7494 |
1116    /// | 23 | G | GGC |   9.4657 |  15.0326 | 16.4319 |
1117    /// | 24 | G | GCA |   9.9035 |  14.9578 | 15.0647 |
1118    /// | 25 | C | CAC |  10.1459 |  14.7509 | 13.2434 |
1119    /// | 26 | A | ACT |  10.2074 |  14.5144 | 11.3172 |
1120    /// | 27 | C | CTA |  10.1287 |  14.4377 | 10.0444 |
1121    /// | 28 | T | TAG |   9.9582 |  14.5496 |  9.3122 |
1122    /// | 29 | A | AGC |   9.5616 |  14.8284 |         |
1123    /// | 30 | G | GCA |   9.0830 |  15.2950 |         |
1124    /// | 31 | C | CAC |   8.8452 |  15.7378 |         |
1125    /// | 32 | A | ACC |   8.7809 |  15.9903 |         |
1126    /// | 33 | C | CCT |   8.8479 |  16.1115 |         |
1127    /// | 34 | C | CTA |   9.0384 |  16.0740 |         |
1128    /// | 35 | T | TAT |   9.1846 |  15.8432 |         |
1129    /// | 36 | A | ATC |   9.2424 |  15.3912 |         |
1130    /// | 37 | T | TCT |   9.1639 |  14.7571 |         |
1131    /// | 38 | C | CTA |   8.9154 |  14.1163 |         |
1132    /// | 39 | T | TAC |   8.8110 |  13.6627 |         |
1133    /// | 40 | A | ACC |   9.0710 |  13.4451 |         |
1134    /// | 41 | C | CCC |   9.4459 |  13.5588 |         |
1135    /// | 42 | C | CCT |   9.8141 |  14.1000 |         |
1136    /// | 43 | C | CTG |  10.3540 |  14.8940 |         |
1137    /// | 44 | T | TGA |          |          |         |
1138    /// | 45 | G | GAA |          |          |         |
1139    /// | 46 | A | AAT |          |          |         |
1140    /// | 47 | A | ATC |          |          |         |
1141    /// | 48 | T |     |          |          |         |
1142    /// | 49 | C |     |          |          |         |
1143    #[test]
1144    fn test_eucdist_iter_from_seq() {
1145        let dna = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
1146        let step_size = 5;
1147        let curve_step = 15;
1148        let curves: Vec<_> = dna
1149            .iter()
1150            .cloned()
1151            .triplet_windows_iter(matrix::RollType::Simple)
1152            .coords_iter()
1153            .roll_mean_iter(step_size)
1154            .euc_dist_iter(curve_step)
1155            .collect();
1156        let curves_len = curves.len();
1157        assert_eq!(
1158            curves_len,
1159            dna.len() - 2 - (2 * step_size) - (2 * curve_step)
1160        );
1161        // check all
1162        assert_relative_eq!(curves[0], 19.1012, epsilon = 1e-4);
1163        assert_relative_eq!(curves[1], 17.7494, epsilon = 1e-4);
1164        assert_relative_eq!(curves[2], 16.4319, epsilon = 1e-4);
1165        assert_relative_eq!(curves[3], 15.0647, epsilon = 1e-4);
1166        assert_relative_eq!(curves[4], 13.2434, epsilon = 1e-4);
1167        assert_relative_eq!(curves[5], 11.3172, epsilon = 1e-4);
1168        assert_relative_eq!(curves[6], 10.0444, epsilon = 1e-4);
1169        assert_relative_eq!(curves[7], 9.3122, epsilon = 1e-4);
1170    }
1171
1172    #[test]
1173    fn test_curve_iter() {
1174        let seq = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
1175        let seq_len = seq.len();
1176        let curves: Vec<_> = CurveIter::new(
1177            seq.iter().cloned(),
1178            matrix::RollType::Simple,
1179            5,
1180            15,
1181            0.33335,
1182        )
1183        .collect();
1184        assert_eq!(curves.len(), seq_len - (21 * 2));
1185        assert_relative_eq!(curves[0], 6.3674, epsilon = 1e-4);
1186        assert_relative_eq!(curves[1], 5.9168, epsilon = 1e-4);
1187        assert_relative_eq!(curves[2], 5.4776, epsilon = 1e-4);
1188        assert_relative_eq!(curves[3], 5.0218, epsilon = 1e-4);
1189        assert_relative_eq!(curves[4], 4.4147, epsilon = 1e-4);
1190        assert_relative_eq!(curves[5], 3.7726, epsilon = 1e-4);
1191        assert_relative_eq!(curves[6], 3.3483, epsilon = 1e-4);
1192        assert_relative_eq!(curves[7], 3.1042, epsilon = 1e-4);
1193    }
1194
1195    /// A direct transcription of the reference implementation's SYMCURV subroutine,
1196    /// kept deliberately unidiomatic so it reads against the Perl line by line and can
1197    /// serve as an oracle for the iterator.
1198    ///
1199    /// ```perl
1200    /// for (my $dyad = $win ; $dyad < scalar(@Curv) - $win ; $dyad += $step) {
1201    ///     my $weight = 0; my $sum = 0;
1202    ///     for (my $j = $dyad, my $k = $dyad ;
1203    ///          $j < $dyad + int($win/2) + 1, $k > $dyad - int($win/2) - 1 ;
1204    ///          $j += $step, $k -= $step) {
1205    ///         $sum += abs($Curv[$j] - $Curv[$k]);
1206    ///     }
1207    ///     if (($Curv[$dyad] < $Curv[$dyad-1]) and ($Curv[$dyad] < $Curv[$dyad+1])
1208    ///         and ((($Curv[$dyad-1]-$Curv[$dyad]) + ($Curv[$dyad+1]-$Curv[$dyad])) >= 0.01)) {
1209    ///         $weight = 1/(($Curv[$dyad-1]-$Curv[$dyad]) + ($Curv[$dyad+1]-$Curv[$dyad]))
1210    ///     } else { $weight = 0; }
1211    ///     if ($sum != 0) { $symcurv[$dyad] = (1/$sum) * $weight; }
1212    ///     else           { $symcurv[$dyad] = 100; }
1213    /// }
1214    /// ```
1215    ///
1216    /// Note the inner loop's comma operator: Perl evaluates only the last condition, so
1217    /// the `$j` bound is dead and `$k` alone terminates the loop. Transcribed as written.
1218    fn perl_symcurv(curv: &[f64], win: usize, step: usize) -> Vec<(usize, f64)> {
1219        let mut out = Vec::new();
1220        if curv.len() < 2 * win + 1 {
1221            return out;
1222        }
1223        let half = win / 2;
1224        let mut dyad = win;
1225        while dyad < curv.len() - win {
1226            let mut sum = 0.0;
1227            let (mut j, mut k) = (dyad, dyad);
1228            // `$k > $dyad - int($win/2) - 1`
1229            while k + half + 1 > dyad {
1230                sum += (curv[j] - curv[k]).abs();
1231                j += step;
1232                if k < step {
1233                    break;
1234                }
1235                k -= step;
1236            }
1237            let weight = if curv[dyad] < curv[dyad - 1]
1238                && curv[dyad] < curv[dyad + 1]
1239                && ((curv[dyad - 1] - curv[dyad]) + (curv[dyad + 1] - curv[dyad])) >= 0.01
1240            {
1241                1.0 / ((curv[dyad - 1] - curv[dyad]) + (curv[dyad + 1] - curv[dyad]))
1242            } else {
1243                0.0
1244            };
1245            let value = if sum != 0.0 {
1246                (1.0 / sum) * weight
1247            } else {
1248                100.0
1249            };
1250            out.push((dyad, value));
1251            dyad += step;
1252        }
1253        out
1254    }
1255
1256    fn synthetic_curvature(n: usize, seed: u64) -> Vec<f64> {
1257        // Smooth-ish with genuine local minima, so the minimum test is actually exercised.
1258        let mut x = seed;
1259        (0..n)
1260            .map(|i| {
1261                x ^= x << 13;
1262                x ^= x >> 7;
1263                x ^= x << 17;
1264                let noise = (x >> 11) as f64 / (1u64 << 53) as f64;
1265                2.0 + (i as f64 / 7.0).sin() + 0.5 * (i as f64 / 3.0).cos() + 0.05 * noise
1266            })
1267            .collect()
1268    }
1269
1270    #[test]
1271    fn test_sym_curve_matches_the_reference_transcription() {
1272        for (n, win, step) in [
1273            (600usize, 101usize, 1usize),
1274            (600, 101, 3),
1275            (400, 51, 1),
1276            (300, 20, 1),
1277            (300, 20, 7),
1278            (250, 101, 1), // too short: no dyads at all
1279        ] {
1280            let curv = synthetic_curvature(n, 0x2545F4914F6CDD1D ^ n as u64);
1281            let expected = perl_symcurv(&curv, win, step);
1282            let got: Vec<f64> = curv.iter().copied().sym_curve_iter(win, step).collect();
1283            assert_eq!(
1284                got.len(),
1285                expected.len(),
1286                "count differs for n={n} win={win} step={step}"
1287            );
1288            for (i, (&value, &(dyad, want))) in got.iter().zip(&expected).enumerate() {
1289                assert_relative_eq!(value, want, epsilon = 1e-12, max_relative = 1e-12);
1290                // The first score belongs to curvature index `win`, then every `step`.
1291                assert_eq!(dyad, win + i * step);
1292            }
1293        }
1294    }
1295
1296    #[test]
1297    fn test_sym_curve_is_zero_away_from_local_minima() {
1298        // A strictly increasing curve has no local minimum, so every dyad scores zero.
1299        let curv: Vec<f64> = (0..500).map(|i| i as f64 * 0.01).collect();
1300        let got: Vec<f64> = curv.iter().copied().sym_curve_iter(101, 1).collect();
1301        assert!(!got.is_empty());
1302        assert!(
1303            got.iter().all(|&v| v == 0.0),
1304            "expected all zero, got {got:?}"
1305        );
1306    }
1307
1308    #[test]
1309    fn test_sym_curve_saturates_when_perfectly_symmetric() {
1310        // Constant curvature makes every mirrored pair equal, so the sum is exactly zero
1311        // and the reference substitutes 100.
1312        let curv = vec![1.25f64; 500];
1313        let got: Vec<f64> = curv.iter().copied().sym_curve_iter(101, 1).collect();
1314        assert!(!got.is_empty());
1315        assert!(
1316            got.iter().all(|&v| v == DEGENERATE_SYMMETRY),
1317            "expected the saturated value"
1318        );
1319    }
1320
1321    #[test]
1322    fn test_sym_curve_scores_a_symmetric_minimum() {
1323        // A V shape centred in the window: a genuine local minimum with perfectly
1324        // symmetric sides, so weight is positive and the score is finite and positive.
1325        let win = 20usize;
1326        let n = 2 * win + 1;
1327        let centre = win as f64;
1328        let curv: Vec<f64> = (0..n)
1329            .map(|i| 1.0 + (i as f64 - centre).abs() * 0.1)
1330            .collect();
1331        let got: Vec<f64> = curv.iter().copied().sym_curve_iter(win, 1).collect();
1332        assert_eq!(got.len(), 1);
1333        // Mirrored pairs are equal by construction, so the sum is zero and it saturates.
1334        assert_eq!(got[0], DEGENERATE_SYMMETRY);
1335
1336        // Break the symmetry slightly: now the sum is non-zero and the score is finite.
1337        let mut skewed = curv.clone();
1338        skewed[win + 3] += 0.4;
1339        let got: Vec<f64> = skewed.iter().copied().sym_curve_iter(win, 1).collect();
1340        assert_eq!(got.len(), 1);
1341        assert!(got[0] > 0.0 && got[0].is_finite(), "got {}", got[0]);
1342        assert!(got[0] < DEGENERATE_SYMMETRY);
1343    }
1344
1345    #[test]
1346    fn test_sym_curve_needs_win_on_both_sides() {
1347        for len in [0usize, 1, 100, 202, 203, 204] {
1348            let curv = synthetic_curvature(len, 7);
1349            let got: Vec<f64> = curv.iter().copied().sym_curve_iter(101, 1).collect();
1350            let expected = len.saturating_sub(2 * 101);
1351            assert_eq!(got.len(), expected, "len {len}");
1352        }
1353    }
1354
1355    #[test]
1356    fn test_sym_curve_stacks_onto_the_curve_iterator() {
1357        // The whole pipeline, bases through to symmetry.
1358        let unit = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
1359        let mut seq = Vec::new();
1360        for _ in 0..20 {
1361            seq.extend_from_slice(unit);
1362        }
1363        let (step_b, step_c, win) = (5usize, 15usize, 101usize);
1364        let curves: Vec<f64> = CurveIter::new(
1365            seq.iter().copied(),
1366            matrix::RollType::Simple,
1367            step_b,
1368            step_c,
1369            0.33335,
1370        )
1371        .collect();
1372        let direct: Vec<f64> = curves.iter().copied().sym_curve_iter(win, 1).collect();
1373        let stacked: Vec<f64> = CurveIter::new(
1374            seq.iter().copied(),
1375            matrix::RollType::Simple,
1376            step_b,
1377            step_c,
1378            0.33335,
1379        )
1380        .sym_curve_iter(win, 1)
1381        .collect();
1382        assert_eq!(direct.len(), curves.len() - 2 * win);
1383        assert_eq!(direct, stacked);
1384        assert!(direct.iter().any(|&v| v > 0.0), "no dyad scored at all");
1385    }
1386
1387    #[test]
1388    fn test_step_places_tilt_on_the_reference_side() {
1389        // The supplied tilt matrix is uniformly zero, so no test driving the iterator can
1390        // tell T - pi/2 from T + pi/2. Exercise the formula directly with a non-zero tilt.
1391        //
1392        // Expected values come from the reference implementation's expression,
1393        //   dx = roll*sin(T) + tilt*sin(T - pi/2)
1394        //   dy = roll*cos(T) + tilt*cos(T - pi/2)
1395        // written out here as its trigonometric identity so the test does not simply
1396        // restate the code: sin(T - pi/2) = -cos(T) and cos(T - pi/2) = sin(T).
1397        for &(roll, tilt, twist) in &[
1398            (5.0, 2.0, 0.0),
1399            (0.7, 1.5, 0.598647428),
1400            (3.05865, -0.25, 1.7),
1401            (0.0, 1.0, 3.0),
1402            (6.2, 0.0, 2.5),
1403        ] {
1404            let got = step(roll, tilt, twist);
1405            let expected_dx = roll * twist.sin() - tilt * twist.cos();
1406            let expected_dy = roll * twist.cos() + tilt * twist.sin();
1407            assert_relative_eq!(got.dx, expected_dx, epsilon = 1e-12);
1408            assert_relative_eq!(got.dy, expected_dy, epsilon = 1e-12);
1409        }
1410    }
1411
1412    #[test]
1413    fn test_step_tilt_sign_is_not_the_opposite_perpendicular() {
1414        // T + pi/2 is the exact negation of T - pi/2, so a sign slip is invisible unless
1415        // something checks it. With a non-zero tilt the two differ by twice the tilt term.
1416        let (roll, tilt, twist) = (2.0, 1.0, 0.9);
1417        let got = step(roll, tilt, twist);
1418        let wrong_dx = roll * twist.sin() + tilt * (twist + PI / 2.0).sin();
1419        assert!(
1420            (got.dx - wrong_dx).abs() > 1.0,
1421            "dx {} matches the opposite perpendicular {}",
1422            got.dx,
1423            wrong_dx
1424        );
1425        // Tilt contributes nothing when it is zero, whichever convention is used.
1426        assert_relative_eq!(
1427            step(roll, 0.0, twist).dx,
1428            roll * twist.sin(),
1429            epsilon = 1e-12
1430        );
1431    }
1432
1433    #[test]
1434    fn test_step_matches_the_pipeline() {
1435        // The iterator must actually be using this function.
1436        let seq = b"CCAACATTTT";
1437        let twist = matrix::matrix_lookup(b"CCA", &matrix::TWIST).unwrap();
1438        let roll = matrix::matrix_lookup(b"CCA", &matrix::ROLL_SIMPLE).unwrap();
1439        let tilt = matrix::matrix_lookup(b"CCA", &matrix::TILT).unwrap();
1440        let first = seq
1441            .iter()
1442            .copied()
1443            .triplet_windows_iter(matrix::RollType::Simple)
1444            .next()
1445            .unwrap();
1446        let expected = step(roll, tilt, twist);
1447        assert_relative_eq!(first.dx, expected.dx, epsilon = 1e-12);
1448        assert_relative_eq!(first.dy, expected.dy, epsilon = 1e-12);
1449    }
1450
1451    #[test]
1452    fn test_roll_type_selects_the_matching_matrix() {
1453        // Guards the pairing that the ROLL_SIMPLE / ROLL_ACTIVE doc comments describe.
1454        // The two matrices disagree at CCA (0.7 simple, 3.05865 active), so this fails if
1455        // the constants are ever swapped to "fix" a mismatch with their docs.
1456        //
1457        // Checked through dx rather than a stored roll value: after one triplet the
1458        // accumulated twist is a single TWIST entry, so dx is roll * sin(twist), which
1459        // pins the routing and the step formula together.
1460        let cca = b"CCA";
1461        let twist = matrix::matrix_lookup(cca, &matrix::TWIST).unwrap();
1462        let dx_for = |roll_type: matrix::RollType| -> f64 {
1463            cca.iter()
1464                .copied()
1465                .triplet_windows_iter(roll_type)
1466                .next()
1467                .unwrap()
1468                .dx
1469        };
1470        assert_relative_eq!(
1471            dx_for(matrix::RollType::Simple),
1472            0.7 * twist.sin(),
1473            epsilon = 1e-12
1474        );
1475        assert_relative_eq!(
1476            dx_for(matrix::RollType::Active),
1477            3.05865 * twist.sin(),
1478            epsilon = 1e-12
1479        );
1480        assert_relative_eq!(
1481            matrix::matrix_lookup(cca, &matrix::ROLL_SIMPLE).unwrap(),
1482            0.7,
1483            epsilon = 1e-9
1484        );
1485        assert_relative_eq!(
1486            matrix::matrix_lookup(cca, &matrix::ROLL_ACTIVE).unwrap(),
1487            3.05865,
1488            epsilon = 1e-9
1489        );
1490    }
1491
1492    #[test]
1493    fn test_long_runs_agree_with_locally_computed_scores() {
1494        // A score depends only on the bases within lead_in of it, so computing one at the
1495        // far end of a long sequence must match computing it from a short slice around
1496        // that position. Accumulated state is what breaks this: before twist was kept
1497        // bounded and the rolling sums rebuilt, the same comparison drifted to 1.6e-10
1498        // over this input, so the threshold here fails against that behaviour.
1499        let bases = *b"ACGT";
1500        let mut x: u64 = 0x5555AAAA33337777;
1501        let n = 300_000usize;
1502        let (step_b, step_c) = (5usize, 15usize);
1503        let lead = step_b + step_c + 1;
1504        let seq: Vec<u8> = (0..n)
1505            .map(|_| {
1506                x ^= x << 13;
1507                x ^= x >> 7;
1508                x ^= x << 17;
1509                bases[(x % 4) as usize]
1510            })
1511            .collect();
1512
1513        let score = |s: &[u8]| -> Vec<f64> {
1514            CurveIter::new(
1515                s.iter().copied(),
1516                matrix::RollType::Simple,
1517                step_b,
1518                step_c,
1519                0.33335,
1520            )
1521            .collect()
1522        };
1523
1524        let long = score(&seq);
1525        for &p in &[long.len() - 1, long.len() / 2, long.len() - 1000] {
1526            let local = score(&seq[p..p + 2 * lead + 1]);
1527            let rel = (long[p] - local[0]).abs() / long[p].abs().max(1e-12);
1528            assert!(
1529                rel < 1e-11,
1530                "score {p} drifted: long {} vs local {} (rel {rel:.3e})",
1531                long[p],
1532                local[0]
1533            );
1534        }
1535    }
1536
1537    #[test]
1538    fn test_rolling_sums_stay_equal_to_a_fresh_sum() {
1539        // Run past the rebuild interval so the rebuild path is exercised, and check the
1540        // rolling means still match ones computed directly over each window.
1541        let bases = *b"ACGT";
1542        let mut x: u64 = 0x0F1E2D3C4B5A6978;
1543        let n = ROLL_SUM_REBUILD_INTERVAL + 5_000;
1544        let seq: Vec<u8> = (0..n)
1545            .map(|_| {
1546                x ^= x << 13;
1547                x ^= x >> 7;
1548                x ^= x << 17;
1549                bases[(x % 4) as usize]
1550            })
1551            .collect();
1552
1553        let step_size = 5usize;
1554        let window = 2 * step_size + 1;
1555        let coords: Vec<CoordsData> = seq
1556            .iter()
1557            .copied()
1558            .triplet_windows_iter(matrix::RollType::Simple)
1559            .coords_iter()
1560            .collect();
1561        let means: Vec<RollMeanData> = seq
1562            .iter()
1563            .copied()
1564            .triplet_windows_iter(matrix::RollType::Simple)
1565            .coords_iter()
1566            .roll_mean_iter(step_size)
1567            .collect();
1568
1569        assert!(means.len() > ROLL_SUM_REBUILD_INTERVAL);
1570        for &i in &[
1571            0usize,
1572            1,
1573            ROLL_SUM_REBUILD_INTERVAL - 1,
1574            ROLL_SUM_REBUILD_INTERVAL + 1,
1575            means.len() - 1,
1576        ] {
1577            // The trapezoidal mean: interior at full weight, the two ends at half.
1578            let slice = &coords[i..i + window];
1579            let x_sum: f64 = slice.iter().map(|c| c.x).sum::<f64>()
1580                - 0.5 * slice[0].x
1581                - 0.5 * slice[window - 1].x;
1582            let expected = x_sum / (window as f64 - 1.0);
1583            assert_relative_eq!(
1584                means[i].x_bar,
1585                expected,
1586                epsilon = 1e-9,
1587                max_relative = 1e-12
1588            );
1589        }
1590    }
1591
1592    #[test]
1593    fn test_size_hint_is_accurate_at_every_layer() {
1594        // A size_hint that lies is worse than none, since callers preallocate from it.
1595        // Check each layer against the count it actually produces, before and partway
1596        // through iteration.
1597        let seq = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
1598        let (step_b, step_c) = (5usize, 15usize);
1599
1600        let triplets = seq
1601            .iter()
1602            .copied()
1603            .triplet_windows_iter(matrix::RollType::Simple);
1604        let (lo, hi) = triplets.size_hint();
1605        let actual = triplets.count();
1606        assert_eq!(actual, seq.len() - 2);
1607        assert!(
1608            lo <= actual && hi == Some(actual),
1609            "triplets: {lo}..{hi:?} vs {actual}"
1610        );
1611
1612        let coords = seq
1613            .iter()
1614            .copied()
1615            .triplet_windows_iter(matrix::RollType::Simple)
1616            .coords_iter();
1617        let (lo, hi) = coords.size_hint();
1618        let actual = coords.count();
1619        assert!(
1620            lo <= actual && hi.is_some_and(|h| h >= actual),
1621            "coords: {lo}..{hi:?} vs {actual}"
1622        );
1623
1624        let means = seq
1625            .iter()
1626            .copied()
1627            .triplet_windows_iter(matrix::RollType::Simple)
1628            .coords_iter()
1629            .roll_mean_iter(step_b);
1630        let (lo, hi) = means.size_hint();
1631        let actual = means.count();
1632        assert!(
1633            lo <= actual && hi.is_some_and(|h| h >= actual),
1634            "means: {lo}..{hi:?} vs {actual}"
1635        );
1636
1637        let curves = CurveIter::new(
1638            seq.iter().copied(),
1639            matrix::RollType::Simple,
1640            step_b,
1641            step_c,
1642            0.33335,
1643        );
1644        let (lo, hi) = curves.size_hint();
1645        let collected: Vec<f64> = curves.collect();
1646        assert_eq!(collected.len(), seq.len() - 2 * (step_b + step_c + 1));
1647        assert!(
1648            lo <= collected.len() && hi.is_some_and(|h| h >= collected.len()),
1649            "curves: {lo}..{hi:?} vs {}",
1650            collected.len()
1651        );
1652
1653        // Partway through, the hint must still bound what is left.
1654        let mut it = CurveIter::new(
1655            seq.iter().copied(),
1656            matrix::RollType::Simple,
1657            step_b,
1658            step_c,
1659            0.33335,
1660        );
1661        it.next();
1662        it.next();
1663        let (lo, hi) = it.size_hint();
1664        let left = it.count();
1665        assert!(
1666            lo <= left && hi.is_some_and(|h| h >= left),
1667            "partway: {lo}..{hi:?} vs {left}"
1668        );
1669    }
1670
1671    #[test]
1672    fn test_iterators_keep_returning_none_after_exhaustion() {
1673        // FusedIterator is a promise, and the layers previously kept polling an inner
1674        // iterator that had already finished, which Iterator does not define for a
1675        // non-fused source.
1676        let seq = b"CCAACATTTTGACTTTTTGGGAGGG";
1677        let mut curves =
1678            CurveIter::new(seq.iter().copied(), matrix::RollType::Simple, 2, 2, 0.33335);
1679        while curves.next().is_some() {}
1680        for _ in 0..5 {
1681            assert!(curves.next().is_none(), "yielded again after finishing");
1682        }
1683
1684        // Too short to produce anything at all: still None, repeatedly.
1685        let mut empty = CurveIter::new(
1686            b"ACG".iter().copied(),
1687            matrix::RollType::Simple,
1688            5,
1689            15,
1690            0.33335,
1691        );
1692        for _ in 0..5 {
1693            assert!(empty.next().is_none());
1694        }
1695    }
1696
1697    #[test]
1698    fn test_curve_iter_is_case_insensitive() {
1699        // Soft-masked sequence must score identically to the same sequence unmasked.
1700        // Before non-ACGT handling was added, the lowercase run panicked in the
1701        // matrix lookup rather than producing a value at all.
1702        let upper = b"CCAACATTTTGACTTTTTGGGAGGGCACTAGCACCTATCTACCCTGAATC";
1703        let mixed = b"CCAACATTTTgacttttTGGGAGGGCACTagcacctatcTACCCTGAATC";
1704        assert_eq!(upper.len(), mixed.len());
1705
1706        let curve = |seq: &[u8]| -> Vec<f64> {
1707            CurveIter::new(
1708                seq.iter().copied(),
1709                matrix::RollType::Simple,
1710                5,
1711                15,
1712                0.33335,
1713            )
1714            .collect()
1715        };
1716
1717        let from_upper = curve(upper);
1718        let from_mixed = curve(mixed);
1719        assert_eq!(from_upper.len(), from_mixed.len());
1720        assert!(!from_upper.is_empty());
1721        for (u, m) in from_upper.iter().zip(&from_mixed) {
1722            assert_relative_eq!(u, m, epsilon = 1e-12);
1723        }
1724    }
1725
1726    #[test]
1727    fn test_sym_curve_step_larger_than_the_span_matches_the_reference() {
1728        // A stride longer than the buffered span drains the buffer entirely, so the
1729        // remainder of the stride has to be skipped in the source as well, or the dyads
1730        // drift away from the reference's `win + i * step`.
1731        let curv = synthetic_curvature(80, 0x9E3779B97F4A7C15);
1732        for (win, step) in [(2usize, 9usize), (3, 7), (5, 30)] {
1733            let expected = perl_symcurv(&curv, win, step);
1734            let got: Vec<f64> = curv.iter().copied().sym_curve_iter(win, step).collect();
1735            assert_eq!(got.len(), expected.len(), "count for win={win} step={step}");
1736            for (i, (&value, &(dyad, want))) in got.iter().zip(&expected).enumerate() {
1737                assert_eq!(dyad, win + i * step);
1738                assert_relative_eq!(value, want, epsilon = 1e-12, max_relative = 1e-12);
1739            }
1740        }
1741    }
1742}