Skip to main content

holodeck_lib/
read.rs

1//! Simulated read pair generation.
2//!
3//! Combines fragment extraction, error model application, and read naming
4//! into complete [`ReadPair`] objects ready for FASTQ output. Computes
5//! proper CIGARs from the haplotype-to-reference coordinate mapping for
6//! golden BAM output.
7
8use noodles::sam::alignment::record::cigar::op::{Kind, Op};
9use noodles::sam::alignment::record_buf::Cigar;
10use rand::Rng;
11
12use crate::error_model::{self, ErrorModel, ReadEnd};
13use crate::fragment::{Fragment, extract_read_bases, lowercase_fraction, uppercase_in_place};
14use crate::read_naming::{TruthAlignment, encoded_pe_name, encoded_se_name, simple_name};
15
16/// A single simulated read with bases, quality scores, and metadata.
17#[derive(Debug, Clone)]
18pub struct SimulatedRead {
19    /// Read name (shared between R1 and R2 of a pair).
20    pub name: String,
21    /// Base sequence (possibly with errors applied).
22    pub bases: Vec<u8>,
23    /// Quality scores (Phred+33 encoded).
24    pub qualities: Vec<u8>,
25}
26
27/// A simulated read pair (or single read for SE mode).
28#[derive(Debug)]
29pub struct ReadPair {
30    /// First read (always present).
31    pub read1: SimulatedRead,
32    /// Second read (present only for paired-end mode).
33    pub read2: Option<SimulatedRead>,
34    /// Truth alignment for R1.
35    pub r1_truth: TruthAlignment,
36    /// Truth alignment for R2 (present only for paired-end mode).
37    pub r2_truth: Option<TruthAlignment>,
38    /// Truth CIGAR for R1, reflecting haplotype variants and adapter
39    /// soft-clipping.
40    pub r1_cigar: Cigar,
41    /// Truth CIGAR for R2. `None` for single-end reads.
42    pub r2_cigar: Option<Cigar>,
43}
44
45/// Generate a simulated read pair from a fragment.
46///
47/// Extracts R1 and optionally R2 bases from the fragment, applies the error
48/// model, constructs read names with truth information, and computes CIGARs
49/// from the fragment's reference coordinate mapping.
50///
51/// Returns `None` if either R1 or R2 has a lowercase-base fraction greater
52/// than `max_n_frac` (i.e. too many bases came from ambiguity-resolved
53/// reference positions); the caller should resample. See the [`fragment`]
54/// module documentation for the lowercase-marker convention.
55///
56/// [`fragment`]: crate::fragment
57///
58/// # Arguments
59/// * `fragment` — The source fragment with bases and reference positions.
60/// * `contig_name` — Contig name for read naming.
61/// * `read_num` — 1-based read pair number.
62/// * `read_length` — Desired read length.
63/// * `paired` — Whether to generate both R1 and R2.
64/// * `adapter_r1` — Adapter sequence for R1.
65/// * `adapter_r2` — Adapter sequence for R2.
66/// * `max_n_frac` — Reject the pair if R1 or R2 has a lowercase fraction
67///   exceeding this threshold. Use `1.0` to disable.
68/// * `error_model` — Error model to apply.
69/// * `simple_names` — Use simple names instead of encoded truth names.
70/// * `rng` — Random number generator.
71#[allow(clippy::too_many_arguments, clippy::too_many_lines)] // Orchestrator for the read-pair pipeline
72pub fn generate_read_pair(
73    fragment: &Fragment,
74    contig_name: &str,
75    read_num: u64,
76    read_length: usize,
77    paired: bool,
78    adapter_r1: &[u8],
79    adapter_r2: &[u8],
80    max_n_frac: f64,
81    model: &impl ErrorModel,
82    simple_names: bool,
83    rng: &mut impl Rng,
84) -> Option<ReadPair> {
85    let frag_len = fragment.bases.len();
86    let genomic = frag_len.min(read_length);
87    let adapter_bases = read_length.saturating_sub(genomic);
88    let right_start = frag_len.saturating_sub(genomic);
89
90    // For a forward (top-strand) fragment, R1 reads from the left end
91    // (forward strand) and R2 reads from the right end (reverse strand).
92    // For a reverse (bottom-strand) fragment, R1 reads from the right end
93    // (reverse strand) and R2 reads from the left end (forward strand).
94    // The left read is always forward-strand and the right read is always
95    // reverse-strand; what changes is which becomes R1 vs R2.
96    let r1_negative_strand = !fragment.is_forward;
97
98    // Extract both mates' bases and run the ambiguity-fraction check on
99    // each before applying errors. Errors mutate bases and advance `rng`, so
100    // checking first means rejection costs nothing and the RNG stream is
101    // only consumed for pairs that will actually be emitted.
102    let mut r1_bases =
103        extract_read_bases(&fragment.bases, read_length, adapter_r1, r1_negative_strand);
104    if lowercase_fraction(&r1_bases) > max_n_frac {
105        return None;
106    }
107
108    // R2 is on the negative strand whenever the fragment is forward-strand.
109    let r2_negative_strand = fragment.is_forward;
110    let r2_bases_pre = if paired {
111        let bases =
112            extract_read_bases(&fragment.bases, read_length, adapter_r2, r2_negative_strand);
113        if lowercase_fraction(&bases) > max_n_frac {
114            return None;
115        }
116        Some(bases)
117    } else {
118        None
119    };
120
121    // Both mates passed the filter (or we're SE) — now apply errors.
122    uppercase_in_place(&mut r1_bases);
123    let (r1_errors, r1_quals) =
124        error_model::apply_errors(model, &mut r1_bases, ReadEnd::Read1, rng);
125
126    // R1 ref_positions and CIGAR.  Fragment ref_positions are always in
127    // ascending (forward) reference order; negative-strand reads take from
128    // the right end of the fragment but the positions are still ascending.
129    let r1_positions = if r1_negative_strand {
130        &fragment.ref_positions[right_start..frag_len]
131    } else {
132        &fragment.ref_positions[..genomic]
133    };
134    let r1_cigar = cigar_from_ref_positions(r1_positions, adapter_bases, r1_negative_strand);
135
136    // R1 truth position: leftmost reference coordinate (1-based).
137    let r1_ref_pos = if fragment.ref_positions.is_empty() {
138        0
139    } else if r1_negative_strand {
140        fragment.ref_positions[right_start] + 1
141    } else {
142        fragment.ref_positions[0] + 1
143    };
144
145    // Fragment length in bases. When it is shorter than the read length,
146    // bases `[fragment_length..read_length)` of each emitted read are adapter
147    // (optionally N-padded).
148    #[expect(clippy::cast_possible_truncation, reason = "fragment length fits in u32")]
149    let fragment_length = frag_len as u32;
150
151    let r1_truth = TruthAlignment {
152        contig: contig_name.to_string(),
153        position: r1_ref_pos,
154        is_forward: fragment.is_forward,
155        haplotype: fragment.haplotype_index,
156        fragment_length,
157        n_errors: r1_errors,
158    };
159
160    // Finish R2 (or emit an SE pair). Matching on the pre-extracted R2
161    // bases avoids re-checking `paired` and keeps the panic-free path clean.
162    match r2_bases_pre {
163        None => {
164            let name = if simple_names {
165                simple_name(read_num)
166            } else {
167                encoded_se_name(read_num, &r1_truth)
168            };
169
170            Some(ReadPair {
171                read1: SimulatedRead { name, bases: r1_bases, qualities: r1_quals },
172                read2: None,
173                r1_truth,
174                r2_truth: None,
175                r1_cigar,
176                r2_cigar: None,
177            })
178        }
179        Some(mut r2_bases) => {
180            uppercase_in_place(&mut r2_bases);
181            let (r2_errors, r2_quals) =
182                error_model::apply_errors(model, &mut r2_bases, ReadEnd::Read2, rng);
183
184            // R2 ref_positions and CIGAR.
185            let r2_positions = if r2_negative_strand {
186                &fragment.ref_positions[right_start..frag_len]
187            } else {
188                &fragment.ref_positions[..genomic]
189            };
190            let r2_cigar =
191                cigar_from_ref_positions(r2_positions, adapter_bases, r2_negative_strand);
192
193            // R2 truth position: leftmost reference coordinate (1-based).
194            let r2_ref_pos = if fragment.ref_positions.is_empty() {
195                0
196            } else if r2_negative_strand {
197                fragment.ref_positions[right_start] + 1
198            } else {
199                fragment.ref_positions[0] + 1
200            };
201
202            let r2_truth = TruthAlignment {
203                contig: contig_name.to_string(),
204                position: r2_ref_pos,
205                is_forward: !fragment.is_forward,
206                haplotype: fragment.haplotype_index,
207                fragment_length,
208                n_errors: r2_errors,
209            };
210
211            let name = if simple_names {
212                simple_name(read_num)
213            } else {
214                encoded_pe_name(read_num, &r1_truth, &r2_truth)
215            };
216
217            Some(ReadPair {
218                read1: SimulatedRead { name: name.clone(), bases: r1_bases, qualities: r1_quals },
219                read2: Some(SimulatedRead { name, bases: r2_bases, qualities: r2_quals }),
220                r1_truth,
221                r2_truth: Some(r2_truth),
222                r1_cigar,
223                r2_cigar: Some(r2_cigar),
224            })
225        }
226    }
227}
228
229/// Compute a CIGAR from a slice of ascending reference positions, plus
230/// optional adapter soft-clipping.
231///
232/// Positions must be in ascending order (forward strand). Consecutive
233/// positions incrementing by 1 produce M ops, same position produces I ops,
234/// and gaps produce D ops.
235///
236/// This works for both R1 and R2: BAM CIGARs are always expressed in forward
237/// reference order from the leftmost aligned position, so even negative-strand
238/// reads use ascending positions.
239///
240/// Adapter bases are appended as a soft-clip (S) operation. For
241/// negative-strand reads (`negative_strand = true`) the adapter is sequenced
242/// at the 3' end of the read but sits at the left (5') end of the stored BAM
243/// record after reverse-complementing, so the S op is placed at the *start*
244/// of the CIGAR. For forward-strand reads the S op is placed at the *end*.
245#[must_use]
246pub fn cigar_from_ref_positions(
247    positions: &[u32],
248    adapter_bases: usize,
249    negative_strand: bool,
250) -> Cigar {
251    let mut ops: Vec<Op> = Vec::new();
252
253    if positions.is_empty() {
254        if adapter_bases > 0 {
255            ops.push(Op::new(Kind::SoftClip, adapter_bases));
256        }
257        return Cigar::from(ops);
258    }
259
260    let mut match_run: usize = 1; // First base is always a match.
261    let mut ins_run: usize = 0;
262
263    for i in 1..positions.len() {
264        let prev = positions[i - 1];
265        let curr = positions[i];
266
267        if curr == prev + 1 {
268            // Sequential ascending position: flush any insertion, extend match.
269            if ins_run > 0 {
270                ops.push(Op::new(Kind::Insertion, ins_run));
271                ins_run = 0;
272            }
273            match_run += 1;
274        } else if curr == prev {
275            // Same position: insertion base. Flush match run, extend insertion.
276            if match_run > 0 {
277                ops.push(Op::new(Kind::Match, match_run));
278                match_run = 0;
279            }
280            ins_run += 1;
281        } else {
282            // Gap in positions: deletion. Flush current runs.
283            if ins_run > 0 {
284                ops.push(Op::new(Kind::Insertion, ins_run));
285                ins_run = 0;
286            }
287            if match_run > 0 {
288                ops.push(Op::new(Kind::Match, match_run));
289            }
290            let gap = curr.saturating_sub(prev).saturating_sub(1);
291            if gap > 0 {
292                ops.push(Op::new(Kind::Deletion, gap as usize));
293            }
294            match_run = 1; // Current base starts a new match.
295        }
296    }
297
298    // Flush remaining runs.
299    if ins_run > 0 {
300        ops.push(Op::new(Kind::Insertion, ins_run));
301    }
302    if match_run > 0 {
303        ops.push(Op::new(Kind::Match, match_run));
304    }
305
306    // Adapter bases as soft-clip. Negative-strand reads are stored
307    // reverse-complemented in BAM, so the adapter (at the 3' end in read
308    // order) moves to the left (5') end of the record — a leading S op.
309    if adapter_bases > 0 {
310        if negative_strand {
311            ops.insert(0, Op::new(Kind::SoftClip, adapter_bases));
312        } else {
313            ops.push(Op::new(Kind::SoftClip, adapter_bases));
314        }
315    }
316
317    Cigar::from(ops)
318}
319
320/// Format a CIGAR as a human-readable string (e.g. "50M3I20M2S").
321#[must_use]
322pub fn cigar_to_string(cigar: &Cigar) -> String {
323    use std::fmt::Write;
324    cigar.as_ref().iter().fold(String::new(), |mut s, op| {
325        let kind_char = match op.kind() {
326            Kind::Match => 'M',
327            Kind::Insertion => 'I',
328            Kind::Deletion => 'D',
329            Kind::SoftClip => 'S',
330            Kind::HardClip => 'H',
331            Kind::Skip => 'N',
332            Kind::Pad => 'P',
333            Kind::SequenceMatch => '=',
334            Kind::SequenceMismatch => 'X',
335        };
336        let _ = write!(s, "{}{kind_char}", op.len());
337        s
338    })
339}
340
341#[cfg(test)]
342mod tests {
343    use rand::SeedableRng;
344    use rand::rngs::SmallRng;
345
346    use super::*;
347    use crate::error_model::illumina::IlluminaErrorModel;
348
349    /// Build a simple fragment for testing.
350    fn test_fragment(bases: &[u8], ref_start: u32) -> Fragment {
351        #[expect(clippy::cast_possible_truncation, reason = "test data is small")]
352        let ref_positions: Vec<u32> = (ref_start..ref_start + bases.len() as u32).collect();
353        Fragment {
354            bases: bases.to_vec(),
355            ref_positions,
356            ref_start,
357            is_forward: true,
358            haplotype_index: 0,
359        }
360    }
361
362    // --- CIGAR generation tests ---
363
364    #[test]
365    fn test_cigar_all_match() {
366        let cigar = cigar_from_ref_positions(&[0, 1, 2, 3, 4], 0, false);
367        assert_eq!(cigar_to_string(&cigar), "5M");
368    }
369
370    #[test]
371    fn test_cigar_with_insertion() {
372        // Positions 0,1,2,2,2,3,4: two inserted bases at ref pos 2.
373        let cigar = cigar_from_ref_positions(&[0, 1, 2, 2, 2, 3, 4], 0, false);
374        assert_eq!(cigar_to_string(&cigar), "3M2I2M");
375    }
376
377    #[test]
378    fn test_cigar_with_deletion() {
379        // Gap from 2 to 5: 2 deleted ref bases.
380        let cigar = cigar_from_ref_positions(&[0, 1, 2, 5, 6], 0, false);
381        assert_eq!(cigar_to_string(&cigar), "3M2D2M");
382    }
383
384    #[test]
385    fn test_cigar_with_adapter_softclip_forward() {
386        // Forward-strand: adapter is a trailing soft-clip.
387        let cigar = cigar_from_ref_positions(&[0, 1, 2], 2, false);
388        assert_eq!(cigar_to_string(&cigar), "3M2S");
389    }
390
391    #[test]
392    fn test_cigar_with_adapter_softclip_negative_strand() {
393        // Negative-strand: adapter moves to a leading soft-clip after RC.
394        let cigar = cigar_from_ref_positions(&[0, 1, 2], 2, true);
395        assert_eq!(cigar_to_string(&cigar), "2S3M");
396    }
397
398    #[test]
399    fn test_cigar_all_adapter() {
400        // All-adapter read: placement doesn't matter, but it still round-trips.
401        let cigar = cigar_from_ref_positions(&[], 5, false);
402        assert_eq!(cigar_to_string(&cigar), "5S");
403    }
404
405    #[test]
406    fn test_cigar_with_insertion_and_deletion() {
407        // Insertion at pos 2 (two extra bases), then deletion of 2 ref bases.
408        let cigar = cigar_from_ref_positions(&[0, 1, 2, 2, 5, 6], 0, false);
409        assert_eq!(cigar_to_string(&cigar), "3M1I2D2M");
410    }
411
412    #[test]
413    fn test_cigar_with_adapter_and_deletion() {
414        let cigar = cigar_from_ref_positions(&[0, 1, 4, 5], 3, false);
415        assert_eq!(cigar_to_string(&cigar), "2M2D2M3S");
416    }
417
418    #[test]
419    fn test_cigar_single_base() {
420        let cigar = cigar_from_ref_positions(&[42], 0, false);
421        assert_eq!(cigar_to_string(&cigar), "1M");
422    }
423
424    #[test]
425    fn test_cigar_high_positions() {
426        // Negative-strand R2: positions start from a high offset (ascending),
427        // no adapter — CIGAR is identical to forward strand.
428        let cigar = cigar_from_ref_positions(&[100, 101, 102, 103, 104], 0, true);
429        assert_eq!(cigar_to_string(&cigar), "5M");
430    }
431
432    // --- Read pair generation tests ---
433
434    #[test]
435    fn test_generate_pe_read_pair() {
436        let fragment = test_fragment(b"ACGTACGTACGTACGTACGT", 100);
437        let model = IlluminaErrorModel::new(10, 0.0, 0.0);
438        let mut rng = SmallRng::seed_from_u64(42);
439
440        let pair = generate_read_pair(
441            &fragment, "chr1", 1, 10, true, b"ADAPTER", b"ADAPTER", 1.0, &model, false, &mut rng,
442        )
443        .expect("no ambiguous bases — should not reject");
444
445        assert_eq!(pair.read1.bases, b"ACGTACGTAC");
446        assert!(pair.read2.is_some());
447        assert_eq!(pair.r1_truth.position, 101);
448        assert!(pair.r2_truth.is_some());
449        assert_eq!(cigar_to_string(&pair.r1_cigar), "10M");
450        assert_eq!(cigar_to_string(pair.r2_cigar.as_ref().unwrap()), "10M");
451    }
452
453    #[test]
454    fn test_generate_se_read() {
455        let fragment = test_fragment(b"ACGTACGTAC", 100);
456        let model = IlluminaErrorModel::new(10, 0.0, 0.0);
457        let mut rng = SmallRng::seed_from_u64(42);
458
459        let pair = generate_read_pair(
460            &fragment, "chr1", 5, 10, false, b"ADAPTER", b"ADAPTER", 1.0, &model, false, &mut rng,
461        )
462        .unwrap();
463
464        assert!(pair.read2.is_none());
465        assert!(pair.r2_cigar.is_none());
466        assert_eq!(cigar_to_string(&pair.r1_cigar), "10M");
467    }
468
469    #[test]
470    fn test_adapter_cigar_softclip() {
471        // Forward-strand fragment: R1 is positive-strand (trailing S), R2 is
472        // negative-strand (leading S after BAM reverse-complement).
473        let fragment = test_fragment(b"AC", 0);
474        let model = IlluminaErrorModel::new(5, 0.0, 0.0);
475        let mut rng = SmallRng::seed_from_u64(42);
476
477        let pair = generate_read_pair(
478            &fragment, "chr1", 1, 5, true, b"TTTTT", b"GGGGG", 1.0, &model, false, &mut rng,
479        )
480        .unwrap();
481
482        assert_eq!(cigar_to_string(&pair.r1_cigar), "2M3S");
483        assert_eq!(cigar_to_string(pair.r2_cigar.as_ref().unwrap()), "3S2M");
484    }
485
486    #[test]
487    fn test_simple_name_mode() {
488        let fragment = test_fragment(b"ACGT", 0);
489        let model = IlluminaErrorModel::new(4, 0.0, 0.0);
490        let mut rng = SmallRng::seed_from_u64(42);
491
492        let pair = generate_read_pair(
493            &fragment, "chr1", 42, 4, true, b"A", b"A", 1.0, &model, true, &mut rng,
494        )
495        .unwrap();
496
497        assert_eq!(pair.read1.name, "holodeck::42");
498    }
499
500    #[test]
501    fn test_quality_scores_correct_length() {
502        let fragment = test_fragment(b"ACGTACGTAC", 0);
503        let model = IlluminaErrorModel::new(10, 0.001, 0.01);
504        let mut rng = SmallRng::seed_from_u64(42);
505
506        let pair = generate_read_pair(
507            &fragment, "chr1", 1, 10, true, b"A", b"A", 1.0, &model, false, &mut rng,
508        )
509        .unwrap();
510
511        assert_eq!(pair.read1.qualities.len(), 10);
512        assert_eq!(pair.read2.as_ref().unwrap().qualities.len(), 10);
513    }
514
515    #[test]
516    fn test_rejects_when_r1_exceeds_max_n_frac() {
517        // Fragment entirely lowercase: every base on R1 (forward) is
518        // flagged, lowercase_fraction == 1.0, which exceeds any threshold < 1.
519        let mut fragment = test_fragment(b"acgtacgtac", 100);
520        // Mark as forward so R1 is the positive-strand, genomic-bases read.
521        fragment.is_forward = true;
522        let model = IlluminaErrorModel::new(10, 0.0, 0.0);
523        let mut rng = SmallRng::seed_from_u64(42);
524
525        let pair = generate_read_pair(
526            &fragment, "chr1", 1, 10, true, b"ADAPTER", b"ADAPTER", 0.5, &model, false, &mut rng,
527        );
528
529        assert!(pair.is_none(), "all-lowercase fragment should be rejected at threshold 0.5");
530    }
531
532    #[test]
533    fn test_accepts_when_lowercase_below_threshold() {
534        // 3 lowercase out of 10 = 0.3, below threshold 0.5 — should accept
535        // and also emit uppercase bases.
536        let fragment = test_fragment(b"ACaGcTAtCA", 0);
537        let model = IlluminaErrorModel::new(10, 0.0, 0.0);
538        let mut rng = SmallRng::seed_from_u64(42);
539
540        let pair = generate_read_pair(
541            &fragment, "chr1", 1, 10, true, b"ADAPTER", b"ADAPTER", 0.5, &model, false, &mut rng,
542        )
543        .expect("0.3 < 0.5 — should accept");
544
545        // Emitted bases must be uppercase ACGT only.
546        for &b in &pair.read1.bases {
547            assert!(matches!(b, b'A' | b'C' | b'G' | b'T' | b'N'), "r1 got {b:?}");
548        }
549        for &b in &pair.read2.as_ref().unwrap().bases {
550            assert!(matches!(b, b'A' | b'C' | b'G' | b'T' | b'N'), "r2 got {b:?}");
551        }
552    }
553
554    #[test]
555    fn test_rejects_before_applying_errors_to_r1() {
556        // Clean forward half (becomes R1), all-lowercase reverse half (becomes
557        // R2 via reverse-complement). R1 passes the filter, but the pair
558        // must be rejected *before* R1's errors advance the RNG — so a
559        // subsequent call on a clean fragment produces identical output to
560        // skipping the rejected call entirely.
561        let mut fragment_half_bad = test_fragment(&[b'A'; 20], 0);
562        // Second half lowercase: becomes R2 when is_forward=true.
563        for b in &mut fragment_half_bad.bases[10..] {
564            *b = b'a';
565        }
566        fragment_half_bad.is_forward = true;
567
568        let clean_fragment = test_fragment(&[b'A'; 20], 0);
569        let model = IlluminaErrorModel::new(10, 0.5, 0.5); // High rate so errors are observable.
570
571        // Run A: rejected pair, then clean pair.
572        let mut rng_a = SmallRng::seed_from_u64(123);
573        let rejected = generate_read_pair(
574            &fragment_half_bad,
575            "chr1",
576            1,
577            10,
578            true,
579            b"TTTTTTTTTT",
580            b"TTTTTTTTTT",
581            0.5,
582            &model,
583            false,
584            &mut rng_a,
585        );
586        assert!(rejected.is_none(), "expected rejection when R2 is all-lowercase");
587        let after_reject = generate_read_pair(
588            &clean_fragment,
589            "chr1",
590            2,
591            10,
592            true,
593            b"TTTTTTTTTT",
594            b"TTTTTTTTTT",
595            0.5,
596            &model,
597            false,
598            &mut rng_a,
599        )
600        .unwrap();
601
602        // Run B: skip the rejected call entirely on an identical RNG seed.
603        let mut rng_b = SmallRng::seed_from_u64(123);
604        let direct = generate_read_pair(
605            &clean_fragment,
606            "chr1",
607            2,
608            10,
609            true,
610            b"TTTTTTTTTT",
611            b"TTTTTTTTTT",
612            0.5,
613            &model,
614            false,
615            &mut rng_b,
616        )
617        .unwrap();
618
619        // If rejection had consumed any RNG draws (for R1's apply_errors or
620        // qualities), the clean pair's bases/qualities would diverge.
621        assert_eq!(after_reject.read1.bases, direct.read1.bases);
622        assert_eq!(after_reject.read1.qualities, direct.read1.qualities);
623        assert_eq!(
624            after_reject.read2.as_ref().unwrap().bases,
625            direct.read2.as_ref().unwrap().bases
626        );
627    }
628}