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}