1#[cfg(all(feature = "simd", target_arch = "x86_64"))]
9use std::arch::x86_64::{
10 __m128i, __m256i, _mm_cvtsi128_si32, _mm_max_epu8, _mm_min_epu8, _mm_srli_si128,
11 _mm256_add_epi64, _mm256_castsi256_si128, _mm256_cmpgt_epi8, _mm256_extracti128_si256,
12 _mm256_loadu_si256, _mm256_max_epu8, _mm256_min_epu8, _mm256_movemask_epi8, _mm256_or_si256,
13 _mm256_sad_epu8, _mm256_set1_epi8, _mm256_setzero_si256, _mm256_storeu_si256, _mm256_sub_epi8,
14};
15use std::fmt;
16use std::io::Read;
17
18use crate::fastq_frame::{self, Line, RecordLines, RecordValidation};
19use crate::{FastqConfig, FastqError, Result as FastqResult};
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23pub struct BaseSummary {
24 pub len: usize,
26 pub a: usize,
28 pub c: usize,
30 pub g: usize,
32 pub t: usize,
34 pub n: usize,
36}
37
38impl BaseSummary {
39 pub fn gc_bases(self) -> usize {
41 self.c + self.g
42 }
43
44 pub fn canonical_bases(self) -> usize {
46 self.a + self.c + self.g + self.t
47 }
48
49 pub fn is_empty(self) -> bool {
51 self.len == 0
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct PackedSequence {
62 pub bases: Vec<u8>,
64 pub n_mask: Vec<u8>,
66 pub summary: BaseSummary,
68}
69
70impl PackedSequence {
71 pub fn len(&self) -> usize {
73 self.summary.len
74 }
75
76 pub fn is_empty(&self) -> bool {
78 self.summary.is_empty()
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum PackedBase {
85 A,
87 C,
89 G,
91 T,
93 N,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PackBuffer {
100 Bases,
102 NMask,
104 QualityBins,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum PackError {
111 OutputTooSmall {
113 buffer: PackBuffer,
115 needed: usize,
117 provided: usize,
119 },
120 InvalidQuality {
122 offset: usize,
124 byte: u8,
126 },
127 UnsortedQualityThresholds {
129 index: usize,
131 },
132 TooManyQualityThresholds {
134 count: usize,
136 },
137}
138
139#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
141pub struct QualitySummary {
142 pub len: usize,
144 pub min_phred: Option<u8>,
146 pub max_phred: Option<u8>,
148 pub sum_phred: u64,
150 pub q20_bases: usize,
152 pub q30_bases: usize,
154}
155
156impl QualitySummary {
157 pub fn mean_phred(self) -> Option<f64> {
159 if self.len == 0 {
160 None
161 } else {
162 Some(self.sum_phred as f64 / self.len as f64)
163 }
164 }
165
166 pub fn is_empty(self) -> bool {
168 self.len == 0
169 }
170
171 fn observe(&mut self, phred: u8) {
172 self.len += 1;
173 self.min_phred = Some(self.min_phred.map_or(phred, |min| min.min(phred)));
174 self.max_phred = Some(self.max_phred.map_or(phred, |max| max.max(phred)));
175 self.sum_phred += u64::from(phred);
176 self.q20_bases += usize::from(phred >= 20);
177 self.q30_bases += usize::from(phred >= 30);
178 }
179}
180
181#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
183pub struct PackedRecordSummary {
184 pub bases: BaseSummary,
186 pub qualities: QualitySummary,
188}
189
190#[derive(Debug, Clone, Copy)]
195pub struct TrustedPackedRecord<'a> {
196 pub name: &'a [u8],
198 pub seq: &'a [u8],
200 pub qual: &'a [u8],
202 pub bases: &'a [u8],
204 pub n_mask: &'a [u8],
206 pub summary: PackedRecordSummary,
208}
209
210#[derive(Debug, Clone, Copy)]
212pub struct TrustedPackedPair<'a> {
213 pub first: TrustedPackedRecord<'a>,
215 pub second: TrustedPackedRecord<'a>,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum PackKernel {
222 Scalar,
224 PortableSimd,
229 Avx2,
231}
232
233pub fn selected_pack_kernel() -> PackKernel {
235 select_pack_kernel()
236}
237
238#[cfg(all(feature = "simd", target_arch = "x86_64"))]
239fn select_pack_kernel() -> PackKernel {
240 if std::is_x86_feature_detected!("avx2") {
241 return PackKernel::Avx2;
242 }
243 PackKernel::Scalar
244}
245
246#[cfg(not(all(feature = "simd", target_arch = "x86_64")))]
247fn select_pack_kernel() -> PackKernel {
248 PackKernel::Scalar
249}
250
251#[derive(Debug, Clone, Copy)]
253pub struct TrustedPackSlab {
254 pub records: u64,
256}
257
258pub trait TrustedPackSink {
260 fn record(&mut self, record: TrustedPackedRecord<'_>) -> FastqResult<()>;
262
263 fn slab(&mut self, _slab: TrustedPackSlab) -> FastqResult<()> {
265 Ok(())
266 }
267}
268
269impl<F> TrustedPackSink for F
270where
271 F: FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
272{
273 fn record(&mut self, record: TrustedPackedRecord<'_>) -> FastqResult<()> {
274 self(record)
275 }
276}
277
278impl fmt::Display for PackError {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 match self {
281 Self::OutputTooSmall {
282 buffer,
283 needed,
284 provided,
285 } => write!(
286 f,
287 "{buffer:?} output too small: need {needed} bytes, got {provided}"
288 ),
289 Self::InvalidQuality { offset, byte } => {
290 write!(f, "invalid Phred+33 quality byte {byte} at offset {offset}")
291 }
292 Self::UnsortedQualityThresholds { index } => {
293 write!(f, "quality threshold at index {index} is not sorted")
294 }
295 Self::TooManyQualityThresholds { count } => {
296 write!(f, "too many quality thresholds: {count}")
297 }
298 }
299 }
300}
301
302impl std::error::Error for PackError {}
303
304pub fn pack_trusted_fastq(
309 input: &[u8],
310 on_record: impl FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
311) -> FastqResult<()> {
312 pack_trusted_fastq_sink(input, on_record)
313}
314
315pub fn pack_trusted_fastq_sink(input: &[u8], sink: impl TrustedPackSink) -> FastqResult<()> {
317 pack_trusted_fastq_direct_sink(input, sink)
318}
319
320pub fn pack_trusted_fastq_read<R: Read>(
322 mut reader: R,
323 config: FastqConfig,
324 on_record: impl FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
325) -> FastqResult<()> {
326 pack_trusted_fastq_read_sink(&mut reader, config, on_record)
327}
328
329pub fn pack_trusted_fastq_read_sink<R: Read>(
331 mut reader: R,
332 config: FastqConfig,
333 mut sink: impl TrustedPackSink,
334) -> FastqResult<()> {
335 pack_trusted_fastq_read_direct_sink_impl(&mut reader, config, &mut sink)
336}
337
338pub fn pack_trusted_fastq_direct(
340 input: &[u8],
341 on_record: impl FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
342) -> FastqResult<()> {
343 pack_trusted_fastq_direct_sink(input, on_record)
344}
345
346pub fn pack_trusted_fastq_direct_sink(
348 input: &[u8],
349 mut sink: impl TrustedPackSink,
350) -> FastqResult<()> {
351 let mut bases = Vec::new();
352 let mut n_mask = Vec::new();
353 let slab = pack_trusted_fastq_direct_slab(
354 input,
355 SlabContext {
356 base_offset: 0,
357 first_record_index: 0,
358 eof: true,
359 },
360 &mut bases,
361 &mut n_mask,
362 &mut sink,
363 )?;
364 debug_assert_eq!(slab.next_start, input.len());
365 Ok(())
366}
367
368pub fn pack_trusted_fastq_read_direct<R: Read>(
370 mut reader: R,
371 config: FastqConfig,
372 on_record: impl FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
373) -> FastqResult<()> {
374 pack_trusted_fastq_read_direct_sink(&mut reader, config, on_record)
375}
376
377pub fn pack_trusted_fastq_read_direct_sink<R: Read>(
379 reader: R,
380 config: FastqConfig,
381 mut sink: impl TrustedPackSink,
382) -> FastqResult<()> {
383 pack_trusted_fastq_read_direct_sink_impl(reader, config, &mut sink)
384}
385
386fn pack_trusted_fastq_read_direct_sink_impl<R: Read>(
387 reader: R,
388 config: FastqConfig,
389 sink: &mut impl TrustedPackSink,
390) -> FastqResult<()> {
391 let mut reader = TrustedFastqPackReader::new(reader, config);
392 while reader.next_slab(sink)? {}
393 Ok(())
394}
395
396struct TrustedFastqPackReader<R> {
397 reader: R,
398 slab_size: usize,
399 buf: Vec<u8>,
400 start: usize,
401 len: usize,
402 eof: bool,
403 base_offset: u64,
404 record_index: u64,
405 bases: Vec<u8>,
406 n_mask: Vec<u8>,
407}
408
409impl<R: Read> TrustedFastqPackReader<R> {
410 fn new(reader: R, config: FastqConfig) -> Self {
411 let slab_size = config.slab_size.max(1024);
412 Self {
413 reader,
414 slab_size,
415 buf: vec![0_u8; slab_size],
416 start: 0,
417 len: 0,
418 eof: false,
419 base_offset: 0,
420 record_index: 0,
421 bases: Vec::new(),
422 n_mask: Vec::new(),
423 }
424 }
425
426 fn next_slab(&mut self, sink: &mut impl TrustedPackSink) -> FastqResult<bool> {
427 if self.eof && self.start == self.len {
428 return Ok(false);
429 }
430
431 self.compact_incomplete_tail()?;
432 while !self.eof && self.len < self.slab_size {
433 let n = self.reader.read(&mut self.buf[self.len..self.slab_size])?;
434 if n == 0 {
435 self.eof = true;
436 break;
437 }
438 self.len += n;
439 }
440
441 let context = SlabContext {
442 base_offset: self.base_offset,
443 first_record_index: self.record_index,
444 eof: self.eof,
445 };
446 let slab = pack_trusted_fastq_direct_slab(
447 &self.buf[self.start..self.len],
448 context,
449 &mut self.bases,
450 &mut self.n_mask,
451 sink,
452 )?;
453 if slab.records != 0 {
454 sink.slab(TrustedPackSlab {
455 records: slab.records,
456 })?;
457 }
458 self.record_index += slab.records;
459
460 let next_start = self.start + slab.next_start;
461 if next_start == self.len {
462 self.base_offset += self.len.saturating_sub(self.start) as u64;
463 self.start = 0;
464 self.len = 0;
465 } else {
466 let carry = self.len - next_start;
467 if next_start == self.start && carry == self.slab_size && !self.eof {
468 return Err(FastqError::RecordTooLarge {
469 slab_size: self.slab_size,
470 });
471 }
472 self.base_offset += slab.next_start as u64;
473 self.start = next_start;
474 }
475
476 if self.eof {
477 if self.start == self.len {
478 return Ok(slab.records != 0);
479 }
480 return Err(FastqError::RecordTooLarge {
481 slab_size: self.slab_size,
482 });
483 }
484
485 Ok(true)
486 }
487
488 fn compact_incomplete_tail(&mut self) -> FastqResult<()> {
489 if self.start == 0 {
490 if self.len == self.slab_size && !self.eof {
491 return Err(FastqError::RecordTooLarge {
492 slab_size: self.slab_size,
493 });
494 }
495 return Ok(());
496 }
497 if self.start == self.len {
498 self.start = 0;
499 self.len = 0;
500 return Ok(());
501 }
502
503 let carry = self.len - self.start;
504 self.buf.copy_within(self.start..self.len, 0);
505 self.start = 0;
506 self.len = carry;
507 if self.len == self.slab_size && !self.eof {
508 return Err(FastqError::RecordTooLarge {
509 slab_size: self.slab_size,
510 });
511 }
512 Ok(())
513 }
514}
515
516pub fn pack_trusted_paired_fastq_read<R1: Read, R2: Read>(
522 first: R1,
523 second: R2,
524 config: FastqConfig,
525 pair_validation: crate::PairValidation,
526 mut on_pair: impl FnMut(TrustedPackedPair<'_>) -> FastqResult<()>,
527) -> FastqResult<()> {
528 let mut first_reader = TrustedFastqLineReader::new(first, config.clone());
529 let mut second_reader = TrustedFastqLineReader::new(second, config);
530 let mut first_bases = Vec::new();
531 let mut first_n_mask = Vec::new();
532 let mut second_bases = Vec::new();
533 let mut second_n_mask = Vec::new();
534 let mut pair_index = 0_u64;
535
536 loop {
537 let first_count = first_reader.available_records()?;
538 let second_count = second_reader.available_records()?;
539
540 if first_count == 0 || second_count == 0 {
541 if first_count == 0
542 && second_count == 0
543 && first_reader.is_done()
544 && second_reader.is_done()
545 {
546 return Ok(());
547 }
548 if (first_count == 0 && first_reader.is_done())
549 || (second_count == 0 && second_reader.is_done())
550 {
551 return Err(FastqError::Format(
552 "paired FASTQ inputs have different record counts".into(),
553 ));
554 }
555 continue;
556 }
557
558 let pairs = first_count.min(second_count);
559 for index in 0..pairs {
560 let first = first_reader.record_lines(index);
561 let second = second_reader.record_lines(index);
562 let first_record_index = first_reader.record_index + index as u64;
563 let second_record_index = second_reader.record_index + index as u64;
564
565 fastq_frame::validate_record(
566 first,
567 first_reader.base_offset,
568 first_record_index,
569 RecordValidation::TRUSTED_PACK,
570 )?;
571 fastq_frame::validate_record(
572 second,
573 second_reader.base_offset,
574 second_record_index,
575 RecordValidation::TRUSTED_PACK,
576 )?;
577
578 if pair_validation != crate::PairValidation::None
579 && !trusted_pair_ids_match(first.name.bytes, second.name.bytes, pair_validation)
580 {
581 return Err(fastq_frame::format_at(
582 "paired FASTQ record identifiers do not match",
583 0,
584 0,
585 pair_index,
586 0,
587 ));
588 }
589
590 let first_summary = pack_bases_and_summarize_qualities_into(
591 first.seq.bytes,
592 first.qual.bytes,
593 &mut first_bases,
594 &mut first_n_mask,
595 )
596 .map_err(|err| {
597 fastq_frame::format_at(
598 err.to_string(),
599 first_reader.base_offset,
600 first.qual.start,
601 first_record_index,
602 3,
603 )
604 })?;
605 let second_summary = pack_bases_and_summarize_qualities_into(
606 second.seq.bytes,
607 second.qual.bytes,
608 &mut second_bases,
609 &mut second_n_mask,
610 )
611 .map_err(|err| {
612 fastq_frame::format_at(
613 err.to_string(),
614 second_reader.base_offset,
615 second.qual.start,
616 second_record_index,
617 3,
618 )
619 })?;
620
621 on_pair(TrustedPackedPair {
622 first: TrustedPackedRecord {
623 name: first.name.bytes,
624 seq: first.seq.bytes,
625 qual: first.qual.bytes,
626 bases: &first_bases,
627 n_mask: &first_n_mask,
628 summary: first_summary,
629 },
630 second: TrustedPackedRecord {
631 name: second.name.bytes,
632 seq: second.seq.bytes,
633 qual: second.qual.bytes,
634 bases: &second_bases,
635 n_mask: &second_n_mask,
636 summary: second_summary,
637 },
638 })?;
639 pair_index += 1;
640 }
641
642 first_reader.consume_records(pairs)?;
643 second_reader.consume_records(pairs)?;
644 }
645}
646
647struct TrustedFastqLineReader<R> {
648 reader: R,
649 slab_size: usize,
650 buf: Vec<u8>,
651 len: usize,
652 eof: bool,
653 base_offset: u64,
654 record_index: u64,
655 scan_cursor: usize,
656 records: Vec<RecordSpan>,
657 consumed_records: usize,
658}
659
660impl<R: Read> TrustedFastqLineReader<R> {
661 fn new(reader: R, config: FastqConfig) -> Self {
662 let slab_size = config.slab_size.max(1024);
663 Self {
664 reader,
665 slab_size,
666 buf: vec![0_u8; slab_size],
667 len: 0,
668 eof: false,
669 base_offset: 0,
670 record_index: 0,
671 scan_cursor: 0,
672 records: Vec::with_capacity(slab_size / 96),
673 consumed_records: 0,
674 }
675 }
676
677 fn available_records(&mut self) -> FastqResult<usize> {
678 if self.consumed_records < self.records.len() {
679 return Ok(self.records.len() - self.consumed_records);
680 }
681 if self.eof && self.scan_cursor == self.len {
682 self.reset_consumed();
683 return Ok(0);
684 }
685
686 self.compact_consumed_or_tail()?;
687 while !self.eof && self.len < self.slab_size {
688 let n = self.reader.read(&mut self.buf[self.len..self.slab_size])?;
689 if n == 0 {
690 self.eof = true;
691 break;
692 }
693 self.len += n;
694 }
695
696 self.scan_records()?;
697 let available_records = self.records.len() - self.consumed_records;
698 if available_records == 0 && self.len == self.slab_size && !self.eof {
699 return Err(FastqError::RecordTooLarge {
700 slab_size: self.slab_size,
701 });
702 }
703 Ok(available_records)
704 }
705
706 fn is_done(&self) -> bool {
707 self.eof && self.consumed_records == self.records.len() && self.scan_cursor == self.len
708 }
709
710 fn record_lines(&self, index: usize) -> RecordLines<'_> {
711 self.records[self.consumed_records + index].to_lines(&self.buf[..self.len])
712 }
713
714 fn consume_records(&mut self, records: usize) -> FastqResult<()> {
715 if records == 0 {
716 return Ok(());
717 }
718 self.consumed_records += records;
719 if self.consumed_records > self.records.len() {
720 return Err(FastqError::Format(
721 "internal trusted FASTQ record cursor advanced past available records".into(),
722 ));
723 }
724 self.record_index += records as u64;
725 Ok(())
726 }
727
728 fn scan_records(&mut self) -> FastqResult<()> {
729 let mut cursor = self.scan_cursor;
730 let scan_base = cursor;
731 let mut newlines = memchr::memchr_iter(b'\n', &self.buf[scan_base..self.len])
732 .map(|offset| scan_base + offset);
733 while cursor < self.len {
734 let record_start = cursor;
735 let Some(name) =
736 trusted_direct_line(&self.buf[..self.len], &mut cursor, &mut newlines, self.eof)
737 else {
738 self.scan_cursor = record_start;
739 return Ok(());
740 };
741 let Some(seq) =
742 trusted_direct_line(&self.buf[..self.len], &mut cursor, &mut newlines, self.eof)
743 else {
744 return self.incomplete_or_truncated(record_start, 1);
745 };
746 let Some(plus) =
747 trusted_direct_line(&self.buf[..self.len], &mut cursor, &mut newlines, self.eof)
748 else {
749 return self.incomplete_or_truncated(record_start, 2);
750 };
751 let Some(qual) =
752 trusted_direct_line(&self.buf[..self.len], &mut cursor, &mut newlines, self.eof)
753 else {
754 return self.incomplete_or_truncated(record_start, 3);
755 };
756
757 self.records.push(RecordSpan {
758 name: LineSpan::from_line(name),
759 seq: LineSpan::from_line(seq),
760 plus: LineSpan::from_line(plus),
761 qual: LineSpan::from_line(qual),
762 });
763 self.scan_cursor = cursor;
764 }
765 Ok(())
766 }
767
768 fn incomplete_or_truncated(&mut self, record_start: usize, line_index: u8) -> FastqResult<()> {
769 if self.eof {
770 let record_index =
771 self.record_index + (self.records.len() - self.consumed_records) as u64;
772 Err(fastq_frame::format_at(
773 "truncated FASTQ record",
774 self.base_offset,
775 record_start,
776 record_index,
777 line_index,
778 ))
779 } else {
780 self.scan_cursor = record_start;
781 Ok(())
782 }
783 }
784
785 fn compact_consumed_or_tail(&mut self) -> FastqResult<()> {
786 if self.consumed_records < self.records.len() {
787 return Ok(());
788 }
789
790 let retain_start = self.scan_cursor;
791 if retain_start == self.len {
792 self.base_offset += self.len as u64;
793 self.len = 0;
794 self.scan_cursor = 0;
795 self.reset_consumed();
796 return Ok(());
797 }
798 if retain_start > 0 {
799 let carry = self.len - retain_start;
800 self.buf.copy_within(retain_start..self.len, 0);
801 self.base_offset += retain_start as u64;
802 self.len = carry;
803 self.scan_cursor = 0;
804 self.reset_consumed();
805 }
806 if self.len == self.slab_size && !self.eof {
807 return Err(FastqError::RecordTooLarge {
808 slab_size: self.slab_size,
809 });
810 }
811 Ok(())
812 }
813
814 fn reset_consumed(&mut self) {
815 self.records.clear();
816 self.consumed_records = 0;
817 }
818}
819
820#[derive(Clone, Copy)]
821struct LineSpan {
822 start: usize,
823 end: usize,
824}
825
826impl LineSpan {
827 fn from_line(line: Line<'_>) -> Self {
828 Self {
829 start: line.start,
830 end: line.start + line.bytes.len(),
831 }
832 }
833
834 fn to_line<'a>(self, bytes: &'a [u8]) -> Line<'a> {
835 Line {
836 bytes: &bytes[self.start..self.end],
837 start: self.start,
838 }
839 }
840}
841
842#[derive(Clone, Copy)]
843struct RecordSpan {
844 name: LineSpan,
845 seq: LineSpan,
846 plus: LineSpan,
847 qual: LineSpan,
848}
849
850impl RecordSpan {
851 fn to_lines<'a>(self, bytes: &'a [u8]) -> RecordLines<'a> {
852 RecordLines {
853 name: self.name.to_line(bytes),
854 seq: self.seq.to_line(bytes),
855 plus: self.plus.to_line(bytes),
856 qual: self.qual.to_line(bytes),
857 }
858 }
859}
860
861pub const fn packed_base_len(base_count: usize) -> usize {
863 base_count / 4 + if base_count.is_multiple_of(4) { 0 } else { 1 }
864}
865
866pub const fn bit_mask_len(bit_count: usize) -> usize {
868 bit_count / 8 + if bit_count.is_multiple_of(8) { 0 } else { 1 }
869}
870
871const BASE_N: u8 = 4;
872const BASE_LUT: [u8; 256] = base_lut();
873const BASE_QUAD_STATES: usize = 5 * 5 * 5 * 5;
874const BASE_QUAD_LUT: [u32; BASE_QUAD_STATES] = base_quad_lut();
875
876const fn base_lut() -> [u8; 256] {
877 let mut table = [BASE_N; 256];
878 table[b'A' as usize] = 0;
879 table[b'a' as usize] = 0;
880 table[b'C' as usize] = 1;
881 table[b'c' as usize] = 1;
882 table[b'G' as usize] = 2;
883 table[b'g' as usize] = 2;
884 table[b'T' as usize] = 3;
885 table[b't' as usize] = 3;
886 table
887}
888
889const fn base_quad_lut() -> [u32; BASE_QUAD_STATES] {
890 let mut table = [0_u32; BASE_QUAD_STATES];
891 let mut c0 = 0_u8;
892 while c0 <= BASE_N {
893 let mut c1 = 0_u8;
894 while c1 <= BASE_N {
895 let mut c2 = 0_u8;
896 while c2 <= BASE_N {
897 let mut c3 = 0_u8;
898 while c3 <= BASE_N {
899 let key = quad_key(c0, c1, c2, c3);
900 table[key] = quad_entry(c0, c1, c2, c3);
901 c3 += 1;
902 }
903 c2 += 1;
904 }
905 c1 += 1;
906 }
907 c0 += 1;
908 }
909 table
910}
911
912const fn quad_key(c0: u8, c1: u8, c2: u8, c3: u8) -> usize {
913 c0 as usize + (c1 as usize * 5) + (c2 as usize * 25) + (c3 as usize * 125)
914}
915
916const fn quad_entry(c0: u8, c1: u8, c2: u8, c3: u8) -> u32 {
917 let codes = [c0, c1, c2, c3];
918 let mut packed = 0_u32;
919 let mut mask = 0_u32;
920 let mut counts = [0_u32; 5];
921 let mut i = 0;
922 while i < 4 {
923 let code = codes[i];
924 if code < BASE_N {
925 packed |= (code as u32) << (i * 2);
926 counts[code as usize] += 1;
927 } else {
928 mask |= 1 << i;
929 counts[BASE_N as usize] += 1;
930 }
931 i += 1;
932 }
933
934 packed
935 | (mask << 8)
936 | (counts[0] << 12)
937 | (counts[1] << 15)
938 | (counts[2] << 18)
939 | (counts[3] << 21)
940 | (counts[4] << 24)
941}
942
943pub fn pack_bases(seq: &[u8]) -> PackedSequence {
945 let mut bases = vec![0; packed_base_len(seq.len())];
946 let mut n_mask = vec![0; bit_mask_len(seq.len())];
947 let summary = pack_bases_exact_zeroed(seq, &mut bases, &mut n_mask);
948 PackedSequence {
949 bases,
950 n_mask,
951 summary,
952 }
953}
954
955pub fn pack_bases_into(seq: &[u8], bases: &mut Vec<u8>, n_mask: &mut Vec<u8>) -> BaseSummary {
957 bases.clear();
958 n_mask.clear();
959 bases.resize(packed_base_len(seq.len()), 0);
960 n_mask.resize(bit_mask_len(seq.len()), 0);
961 pack_bases_exact_zeroed(seq, bases, n_mask)
962}
963
964#[inline]
969pub fn pack_bases_and_summarize_qualities_into(
970 seq: &[u8],
971 qualities: &[u8],
972 bases: &mut Vec<u8>,
973 n_mask: &mut Vec<u8>,
974) -> Result<PackedRecordSummary, PackError> {
975 bases.clear();
976 n_mask.clear();
977 bases.resize(packed_base_len(seq.len()), 0);
978 n_mask.resize(bit_mask_len(seq.len()), 0);
979
980 if seq.len() == qualities.len() {
981 pack_bases_and_qualities_exact_zeroed(seq, qualities, bases, n_mask)
982 } else {
983 Ok(PackedRecordSummary {
984 bases: pack_bases_exact_zeroed(seq, bases, n_mask),
985 qualities: summarize_qualities(qualities)?,
986 })
987 }
988}
989
990pub fn pack_bases_into_slices(
994 seq: &[u8],
995 bases: &mut [u8],
996 n_mask: &mut [u8],
997) -> Result<BaseSummary, PackError> {
998 let bases_needed = packed_base_len(seq.len());
999 let mask_needed = bit_mask_len(seq.len());
1000 if bases.len() < bases_needed {
1001 return Err(PackError::OutputTooSmall {
1002 buffer: PackBuffer::Bases,
1003 needed: bases_needed,
1004 provided: bases.len(),
1005 });
1006 }
1007 if n_mask.len() < mask_needed {
1008 return Err(PackError::OutputTooSmall {
1009 buffer: PackBuffer::NMask,
1010 needed: mask_needed,
1011 provided: n_mask.len(),
1012 });
1013 }
1014
1015 Ok(pack_bases_exact(
1016 seq,
1017 &mut bases[..bases_needed],
1018 &mut n_mask[..mask_needed],
1019 ))
1020}
1021
1022pub fn packed_base_at(bases: &[u8], n_mask: &[u8], index: usize) -> Option<PackedBase> {
1024 let base_byte = *bases.get(index / 4)?;
1025 if is_masked(n_mask, index)? {
1026 return Some(PackedBase::N);
1027 }
1028
1029 match (base_byte >> ((index % 4) * 2)) & 0b11 {
1030 0 => Some(PackedBase::A),
1031 1 => Some(PackedBase::C),
1032 2 => Some(PackedBase::G),
1033 3 => Some(PackedBase::T),
1034 _ => None,
1035 }
1036}
1037
1038pub fn is_masked(n_mask: &[u8], index: usize) -> Option<bool> {
1040 let mask_byte = *n_mask.get(index / 8)?;
1041 Some(((mask_byte >> (index % 8)) & 1) != 0)
1042}
1043
1044pub fn summarize_qualities(qualities: &[u8]) -> Result<QualitySummary, PackError> {
1046 #[cfg(all(feature = "simd", target_arch = "x86_64"))]
1047 if qualities.len() >= 32 && std::is_x86_feature_detected!("avx2") {
1048 return unsafe { summarize_qualities_avx2(qualities) };
1049 }
1050 let mut summary = QualityAccumulator::default();
1051 for (offset, &byte) in qualities.iter().enumerate() {
1052 summary.observe(byte, offset)?;
1053 }
1054 Ok(summary.finish())
1055}
1056
1057#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1058#[target_feature(enable = "avx2")]
1059unsafe fn summarize_qualities_avx2(qualities: &[u8]) -> Result<QualitySummary, PackError> {
1060 let low = _mm256_set1_epi8(33);
1061 let high = _mm256_set1_epi8(126);
1062 let offset = _mm256_set1_epi8(33);
1063 let q20 = _mm256_set1_epi8(52);
1064 let q30 = _mm256_set1_epi8(62);
1065 let zero = _mm256_setzero_si256();
1066 let mut min_phred = _mm256_set1_epi8(93);
1067 let mut max_phred = _mm256_setzero_si256();
1068 let mut sum_phred = _mm256_setzero_si256();
1069
1070 let mut summary = QualityAccumulator::default();
1071 let mut i = 0;
1072 while i + 32 <= qualities.len() {
1073 let bytes = unsafe { _mm256_loadu_si256(qualities.as_ptr().add(i).cast::<__m256i>()) };
1074 let too_low = _mm256_cmpgt_epi8(low, bytes);
1075 let too_high = _mm256_cmpgt_epi8(bytes, high);
1076 let invalid = _mm256_movemask_epi8(_mm256_or_si256(too_low, too_high));
1077 if invalid != 0 {
1078 let offset = invalid.trailing_zeros() as usize;
1079 return Err(PackError::InvalidQuality {
1080 offset: i + offset,
1081 byte: qualities[i + offset],
1082 });
1083 }
1084
1085 summary.q20_bases +=
1086 _mm256_movemask_epi8(_mm256_cmpgt_epi8(bytes, q20)).count_ones() as usize;
1087 summary.q30_bases +=
1088 _mm256_movemask_epi8(_mm256_cmpgt_epi8(bytes, q30)).count_ones() as usize;
1089 let phreds = _mm256_sub_epi8(bytes, offset);
1090 min_phred = _mm256_min_epu8(min_phred, phreds);
1091 max_phred = _mm256_max_epu8(max_phred, phreds);
1092 sum_phred = _mm256_add_epi64(sum_phred, _mm256_sad_epu8(phreds, zero));
1093 summary.len += 32;
1094 i += 32;
1095 }
1096
1097 unsafe { finish_avx2_quality_vectors(&mut summary, min_phred, max_phred, sum_phred) };
1098
1099 while i < qualities.len() {
1100 summary.observe(qualities[i], i)?;
1101 i += 1;
1102 }
1103
1104 Ok(summary.finish())
1105}
1106
1107#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1108#[target_feature(enable = "avx2")]
1109unsafe fn finish_avx2_quality_vectors(
1110 summary: &mut QualityAccumulator,
1111 min_phred: __m256i,
1112 max_phred: __m256i,
1113 sum_phred: __m256i,
1114) {
1115 if summary.len == 0 {
1116 return;
1117 }
1118
1119 let mut sum_lanes = [0_u64; 4];
1120 unsafe {
1121 _mm256_storeu_si256(sum_lanes.as_mut_ptr().cast::<__m256i>(), sum_phred);
1122 }
1123 summary.min_phred = summary
1124 .min_phred
1125 .min(unsafe { horizontal_min_u8_256(min_phred) });
1126 summary.max_phred = summary
1127 .max_phred
1128 .max(unsafe { horizontal_max_u8_256(max_phred) });
1129 summary.sum_phred = sum_lanes.iter().copied().sum();
1130}
1131
1132#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1133#[target_feature(enable = "avx2")]
1134unsafe fn horizontal_min_u8_256(value: __m256i) -> u8 {
1135 let low = _mm256_castsi256_si128(value);
1136 let high = _mm256_extracti128_si256(value, 1);
1137 unsafe { horizontal_min_u8_128(_mm_min_epu8(low, high)) }
1138}
1139
1140#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1141#[target_feature(enable = "avx2")]
1142unsafe fn horizontal_max_u8_256(value: __m256i) -> u8 {
1143 let low = _mm256_castsi256_si128(value);
1144 let high = _mm256_extracti128_si256(value, 1);
1145 unsafe { horizontal_max_u8_128(_mm_max_epu8(low, high)) }
1146}
1147
1148#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1149#[target_feature(enable = "avx2")]
1150unsafe fn horizontal_min_u8_128(mut value: __m128i) -> u8 {
1151 value = _mm_min_epu8(value, _mm_srli_si128(value, 8));
1152 value = _mm_min_epu8(value, _mm_srli_si128(value, 4));
1153 value = _mm_min_epu8(value, _mm_srli_si128(value, 2));
1154 value = _mm_min_epu8(value, _mm_srli_si128(value, 1));
1155 _mm_cvtsi128_si32(value) as u8
1156}
1157
1158#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1159#[target_feature(enable = "avx2")]
1160unsafe fn horizontal_max_u8_128(mut value: __m128i) -> u8 {
1161 value = _mm_max_epu8(value, _mm_srli_si128(value, 8));
1162 value = _mm_max_epu8(value, _mm_srli_si128(value, 4));
1163 value = _mm_max_epu8(value, _mm_srli_si128(value, 2));
1164 value = _mm_max_epu8(value, _mm_srli_si128(value, 1));
1165 _mm_cvtsi128_si32(value) as u8
1166}
1167
1168pub fn bin_qualities_into(
1173 qualities: &[u8],
1174 thresholds: &[u8],
1175 out: &mut Vec<u8>,
1176) -> Result<QualitySummary, PackError> {
1177 validate_thresholds(thresholds)?;
1178 out.clear();
1179 out.reserve(qualities.len());
1180
1181 let mut summary = QualitySummary::default();
1182 for (offset, &byte) in qualities.iter().enumerate() {
1183 let phred = phred33(byte, offset)?;
1184 summary.observe(phred);
1185 out.push(quality_bin(phred, thresholds));
1186 }
1187 Ok(summary)
1188}
1189
1190pub fn bin_qualities_into_slice(
1194 qualities: &[u8],
1195 thresholds: &[u8],
1196 out: &mut [u8],
1197) -> Result<QualitySummary, PackError> {
1198 validate_thresholds(thresholds)?;
1199 if out.len() < qualities.len() {
1200 return Err(PackError::OutputTooSmall {
1201 buffer: PackBuffer::QualityBins,
1202 needed: qualities.len(),
1203 provided: out.len(),
1204 });
1205 }
1206
1207 let mut summary = QualitySummary::default();
1208 for (offset, &byte) in qualities.iter().enumerate() {
1209 let phred = phred33(byte, offset)?;
1210 summary.observe(phred);
1211 out[offset] = quality_bin(phred, thresholds);
1212 }
1213 Ok(summary)
1214}
1215
1216fn pack_trusted_fastq_direct_slab(
1217 input: &[u8],
1218 context: SlabContext,
1219 bases: &mut Vec<u8>,
1220 n_mask: &mut Vec<u8>,
1221 sink: &mut impl TrustedPackSink,
1222) -> FastqResult<SlabResult> {
1223 let mut cursor = 0;
1224 let mut records = 0_u64;
1225 let mut newlines = memchr::memchr_iter(b'\n', input);
1226
1227 while cursor < input.len() {
1228 let record_start = cursor;
1229 let Some(name) = trusted_direct_line(input, &mut cursor, &mut newlines, context.eof) else {
1230 return Ok(SlabResult {
1231 next_start: record_start,
1232 records,
1233 });
1234 };
1235 let Some(seq) = trusted_direct_line(input, &mut cursor, &mut newlines, context.eof) else {
1236 return incomplete_or_truncated_direct(input, context, record_start, records, 1);
1237 };
1238 let Some(plus) = trusted_direct_line(input, &mut cursor, &mut newlines, context.eof) else {
1239 return incomplete_or_truncated_direct(input, context, record_start, records, 2);
1240 };
1241 let Some(qual) = trusted_direct_line(input, &mut cursor, &mut newlines, context.eof) else {
1242 return incomplete_or_truncated_direct(input, context, record_start, records, 3);
1243 };
1244 let record = RecordLines {
1245 name,
1246 seq,
1247 plus,
1248 qual,
1249 };
1250
1251 observe_trusted_packed_record(
1252 record,
1253 context.base_offset,
1254 context.first_record_index + records,
1255 bases,
1256 n_mask,
1257 sink,
1258 )?;
1259 records += 1;
1260 }
1261
1262 Ok(SlabResult {
1263 next_start: input.len(),
1264 records,
1265 })
1266}
1267
1268fn trusted_direct_line<'a>(
1269 input: &'a [u8],
1270 cursor: &mut usize,
1271 newlines: &mut impl Iterator<Item = usize>,
1272 eof: bool,
1273) -> Option<Line<'a>> {
1274 let start = *cursor;
1275 if start >= input.len() {
1276 return None;
1277 }
1278
1279 let end = match newlines.next() {
1280 Some(end) => {
1281 *cursor = end + 1;
1282 end
1283 }
1284 None if eof => {
1285 *cursor = input.len();
1286 input.len()
1287 }
1288 None => return None,
1289 };
1290 let end = fastq_frame::trim_cr_end(input, start, end);
1291 Some(Line {
1292 bytes: &input[start..end],
1293 start,
1294 })
1295}
1296
1297fn incomplete_or_truncated_direct(
1298 _input: &[u8],
1299 context: SlabContext,
1300 record_start: usize,
1301 records: u64,
1302 line_index: u8,
1303) -> FastqResult<SlabResult> {
1304 if context.eof {
1305 Err(fastq_frame::format_at(
1306 "truncated FASTQ record",
1307 context.base_offset,
1308 record_start,
1309 context.first_record_index + records,
1310 line_index,
1311 ))
1312 } else {
1313 Ok(SlabResult {
1314 next_start: record_start,
1315 records,
1316 })
1317 }
1318}
1319
1320#[derive(Clone, Copy)]
1321struct SlabContext {
1322 base_offset: u64,
1323 first_record_index: u64,
1324 eof: bool,
1325}
1326
1327#[derive(Clone, Copy)]
1328struct SlabResult {
1329 next_start: usize,
1330 records: u64,
1331}
1332
1333fn observe_trusted_packed_record(
1334 record: RecordLines<'_>,
1335 base_offset: u64,
1336 record_index: u64,
1337 bases: &mut Vec<u8>,
1338 n_mask: &mut Vec<u8>,
1339 sink: &mut impl TrustedPackSink,
1340) -> FastqResult<()> {
1341 fastq_frame::validate_record(
1342 record,
1343 base_offset,
1344 record_index,
1345 RecordValidation::TRUSTED_PACK,
1346 )?;
1347
1348 let summary =
1349 pack_bases_and_summarize_qualities_into(record.seq.bytes, record.qual.bytes, bases, n_mask)
1350 .map_err(|err| {
1351 fastq_frame::format_at(
1352 err.to_string(),
1353 base_offset,
1354 record.qual.start,
1355 record_index,
1356 3,
1357 )
1358 })?;
1359 sink.record(TrustedPackedRecord {
1360 name: record.name.bytes,
1361 seq: record.seq.bytes,
1362 qual: record.qual.bytes,
1363 bases: &bases[..],
1364 n_mask: &n_mask[..],
1365 summary,
1366 })
1367}
1368
1369fn trusted_pair_ids_match(
1370 first_name: &[u8],
1371 second_name: &[u8],
1372 mode: crate::PairValidation,
1373) -> bool {
1374 match mode {
1375 crate::PairValidation::None => true,
1376 crate::PairValidation::FastSlash => {
1377 fastq_frame::fast_slash_pair_ids_match(first_name, second_name).unwrap_or_else(|| {
1378 fastq_frame::normalized_pair_id(first_name)
1379 == fastq_frame::normalized_pair_id(second_name)
1380 })
1381 }
1382 crate::PairValidation::Full => {
1383 fastq_frame::normalized_pair_id(first_name)
1384 == fastq_frame::normalized_pair_id(second_name)
1385 }
1386 }
1387}
1388
1389fn pack_bases_exact(seq: &[u8], bases: &mut [u8], n_mask: &mut [u8]) -> BaseSummary {
1390 let bases_needed = packed_base_len(seq.len());
1391 if bases_needed == 0 {
1392 return BaseSummary::default();
1393 }
1394
1395 bases[..bases_needed].fill(0);
1396 let mask_needed = bit_mask_len(seq.len());
1397 if mask_needed != 0 {
1398 n_mask[..mask_needed].fill(0);
1399 }
1400
1401 pack_bases_exact_zeroed(seq, bases, n_mask)
1402}
1403
1404fn pack_bases_exact_zeroed(seq: &[u8], bases: &mut [u8], n_mask: &mut [u8]) -> BaseSummary {
1405 let bases_needed = packed_base_len(seq.len());
1406 if bases_needed == 0 {
1407 return BaseSummary::default();
1408 }
1409
1410 let mut summary = BaseSummary {
1411 len: seq.len(),
1412 ..BaseSummary::default()
1413 };
1414
1415 let full_chunks = seq.len() / 4;
1416 let mut chunk_index = 0;
1417 let mut base_index = 0;
1418 #[cfg(feature = "simd")]
1419 while chunk_index + 4 <= full_chunks {
1420 pack_seq_16(
1421 &seq[base_index..base_index + 16],
1422 base_index,
1423 &mut bases[chunk_index..chunk_index + 4],
1424 &mut summary,
1425 n_mask,
1426 );
1427 chunk_index += 4;
1428 base_index += 16;
1429 }
1430 while chunk_index < full_chunks {
1431 let c0 = BASE_LUT[usize::from(seq[base_index])];
1432 let c1 = BASE_LUT[usize::from(seq[base_index + 1])];
1433 let c2 = BASE_LUT[usize::from(seq[base_index + 2])];
1434 let c3 = BASE_LUT[usize::from(seq[base_index + 3])];
1435 pack_quad_from_codes(
1436 c0,
1437 c1,
1438 c2,
1439 c3,
1440 base_index,
1441 &mut bases[chunk_index],
1442 &mut summary,
1443 n_mask,
1444 );
1445 chunk_index += 1;
1446 base_index += 4;
1447 }
1448
1449 let tail_start = full_chunks * 4;
1450 let mut index = tail_start;
1451 while index < seq.len() {
1452 let offset = index - tail_start;
1453 let code = BASE_LUT[usize::from(seq[index])];
1454 if code < BASE_N {
1455 add_base_count(&mut summary, code);
1456 bases[full_chunks] |= code << (offset * 2);
1457 } else {
1458 summary.n += 1;
1459 n_mask[index / 8] |= 1 << (index % 8);
1460 }
1461 index += 1;
1462 }
1463
1464 summary
1465}
1466
1467fn pack_bases_and_qualities_exact_zeroed(
1468 seq: &[u8],
1469 qualities: &[u8],
1470 bases: &mut [u8],
1471 n_mask: &mut [u8],
1472) -> Result<PackedRecordSummary, PackError> {
1473 let bases_needed = packed_base_len(seq.len());
1474 if bases_needed == 0 {
1475 return Ok(PackedRecordSummary::default());
1476 }
1477
1478 #[cfg(all(feature = "simd", target_arch = "x86_64"))]
1479 if seq.len() >= 32 && std::is_x86_feature_detected!("avx2") {
1480 return unsafe { pack_bases_and_qualities_exact_avx2(seq, qualities, bases, n_mask) };
1481 }
1482
1483 let mut bases_summary = BaseSummary {
1484 len: seq.len(),
1485 ..BaseSummary::default()
1486 };
1487 let mut quality_summary = QualityAccumulator::default();
1488
1489 let full_chunks = seq.len() / 4;
1490 let mut chunk_index = 0;
1491 let mut base_index = 0;
1492 #[cfg(feature = "simd")]
1493 while chunk_index + 4 <= full_chunks {
1494 pack_seq_16(
1495 &seq[base_index..base_index + 16],
1496 base_index,
1497 &mut bases[chunk_index..chunk_index + 4],
1498 &mut bases_summary,
1499 n_mask,
1500 );
1501 let end = base_index + 16;
1502 while base_index < end {
1503 quality_summary.observe(qualities[base_index], base_index)?;
1504 base_index += 1;
1505 }
1506 chunk_index += 4;
1507 }
1508 while chunk_index < full_chunks {
1509 let c0 = BASE_LUT[usize::from(seq[base_index])];
1510 let c1 = BASE_LUT[usize::from(seq[base_index + 1])];
1511 let c2 = BASE_LUT[usize::from(seq[base_index + 2])];
1512 let c3 = BASE_LUT[usize::from(seq[base_index + 3])];
1513 pack_quad_from_codes(
1514 c0,
1515 c1,
1516 c2,
1517 c3,
1518 base_index,
1519 &mut bases[chunk_index],
1520 &mut bases_summary,
1521 n_mask,
1522 );
1523 quality_summary.observe(qualities[base_index], base_index)?;
1524 quality_summary.observe(qualities[base_index + 1], base_index + 1)?;
1525 quality_summary.observe(qualities[base_index + 2], base_index + 2)?;
1526 quality_summary.observe(qualities[base_index + 3], base_index + 3)?;
1527 chunk_index += 1;
1528 base_index += 4;
1529 }
1530
1531 let tail_start = full_chunks * 4;
1532 let mut index = tail_start;
1533 while index < seq.len() {
1534 let offset = index - tail_start;
1535 let code = BASE_LUT[usize::from(seq[index])];
1536 if code < BASE_N {
1537 add_base_count(&mut bases_summary, code);
1538 bases[full_chunks] |= code << (offset * 2);
1539 } else {
1540 bases_summary.n += 1;
1541 n_mask[index / 8] |= 1 << (index % 8);
1542 }
1543 quality_summary.observe(qualities[index], index)?;
1544 index += 1;
1545 }
1546
1547 Ok(PackedRecordSummary {
1548 bases: bases_summary,
1549 qualities: quality_summary.finish(),
1550 })
1551}
1552
1553#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1554#[target_feature(enable = "avx2")]
1555unsafe fn pack_bases_and_qualities_exact_avx2(
1556 seq: &[u8],
1557 qualities: &[u8],
1558 bases: &mut [u8],
1559 n_mask: &mut [u8],
1560) -> Result<PackedRecordSummary, PackError> {
1561 let low = _mm256_set1_epi8(33);
1562 let high = _mm256_set1_epi8(126);
1563 let offset = _mm256_set1_epi8(33);
1564 let q20 = _mm256_set1_epi8(52);
1565 let q30 = _mm256_set1_epi8(62);
1566 let zero = _mm256_setzero_si256();
1567 let mut min_phred = _mm256_set1_epi8(93);
1568 let mut max_phred = _mm256_setzero_si256();
1569 let mut sum_phred = _mm256_setzero_si256();
1570
1571 let mut bases_summary = BaseSummary {
1572 len: seq.len(),
1573 ..BaseSummary::default()
1574 };
1575 let mut quality_summary = QualityAccumulator::default();
1576 let mut base_index = 0;
1577 let mut chunk_index = 0;
1578
1579 while base_index + 32 <= seq.len() {
1580 let block_start = base_index;
1581 pack_seq_16(
1582 &seq[base_index..base_index + 16],
1583 base_index,
1584 &mut bases[chunk_index..chunk_index + 4],
1585 &mut bases_summary,
1586 n_mask,
1587 );
1588 base_index += 16;
1589 chunk_index += 4;
1590
1591 pack_seq_16(
1592 &seq[base_index..base_index + 16],
1593 base_index,
1594 &mut bases[chunk_index..chunk_index + 4],
1595 &mut bases_summary,
1596 n_mask,
1597 );
1598 base_index += 16;
1599 chunk_index += 4;
1600
1601 let bytes =
1602 unsafe { _mm256_loadu_si256(qualities.as_ptr().add(block_start).cast::<__m256i>()) };
1603 let too_low = _mm256_cmpgt_epi8(low, bytes);
1604 let too_high = _mm256_cmpgt_epi8(bytes, high);
1605 let invalid = _mm256_movemask_epi8(_mm256_or_si256(too_low, too_high));
1606 if invalid != 0 {
1607 let offset = invalid.trailing_zeros() as usize;
1608 return Err(PackError::InvalidQuality {
1609 offset: block_start + offset,
1610 byte: qualities[block_start + offset],
1611 });
1612 }
1613
1614 quality_summary.q20_bases +=
1615 _mm256_movemask_epi8(_mm256_cmpgt_epi8(bytes, q20)).count_ones() as usize;
1616 quality_summary.q30_bases +=
1617 _mm256_movemask_epi8(_mm256_cmpgt_epi8(bytes, q30)).count_ones() as usize;
1618 let phreds = _mm256_sub_epi8(bytes, offset);
1619 min_phred = _mm256_min_epu8(min_phred, phreds);
1620 max_phred = _mm256_max_epu8(max_phred, phreds);
1621 sum_phred = _mm256_add_epi64(sum_phred, _mm256_sad_epu8(phreds, zero));
1622 quality_summary.len += 32;
1623 }
1624
1625 unsafe { finish_avx2_quality_vectors(&mut quality_summary, min_phred, max_phred, sum_phred) };
1626
1627 let full_chunks = seq.len() / 4;
1628 while chunk_index < full_chunks {
1629 let c0 = BASE_LUT[usize::from(seq[base_index])];
1630 let c1 = BASE_LUT[usize::from(seq[base_index + 1])];
1631 let c2 = BASE_LUT[usize::from(seq[base_index + 2])];
1632 let c3 = BASE_LUT[usize::from(seq[base_index + 3])];
1633 pack_quad_from_codes(
1634 c0,
1635 c1,
1636 c2,
1637 c3,
1638 base_index,
1639 &mut bases[chunk_index],
1640 &mut bases_summary,
1641 n_mask,
1642 );
1643 quality_summary.observe(qualities[base_index], base_index)?;
1644 quality_summary.observe(qualities[base_index + 1], base_index + 1)?;
1645 quality_summary.observe(qualities[base_index + 2], base_index + 2)?;
1646 quality_summary.observe(qualities[base_index + 3], base_index + 3)?;
1647 chunk_index += 1;
1648 base_index += 4;
1649 }
1650
1651 while base_index < seq.len() {
1652 let offset = base_index - (full_chunks * 4);
1653 let code = BASE_LUT[usize::from(seq[base_index])];
1654 if code < BASE_N {
1655 add_base_count(&mut bases_summary, code);
1656 bases[full_chunks] |= code << (offset * 2);
1657 } else {
1658 bases_summary.n += 1;
1659 n_mask[base_index / 8] |= 1 << (base_index % 8);
1660 }
1661 quality_summary.observe(qualities[base_index], base_index)?;
1662 base_index += 1;
1663 }
1664
1665 Ok(PackedRecordSummary {
1666 bases: bases_summary,
1667 qualities: quality_summary.finish(),
1668 })
1669}
1670
1671#[cfg(feature = "simd")]
1672#[inline(always)]
1673fn pack_seq_16(
1674 seq: &[u8],
1675 base_index: usize,
1676 bases: &mut [u8],
1677 summary: &mut BaseSummary,
1678 n_mask: &mut [u8],
1679) {
1680 debug_assert!(seq.len() >= 16);
1681 debug_assert!(bases.len() >= 4);
1682 debug_assert_eq!(base_index % 8, 0);
1683
1684 let e0 = quad_entry_from_bases(seq[0], seq[1], seq[2], seq[3]);
1685 let e1 = quad_entry_from_bases(seq[4], seq[5], seq[6], seq[7]);
1686 let e2 = quad_entry_from_bases(seq[8], seq[9], seq[10], seq[11]);
1687 let e3 = quad_entry_from_bases(seq[12], seq[13], seq[14], seq[15]);
1688
1689 bases[0] = e0 as u8;
1690 bases[1] = e1 as u8;
1691 bases[2] = e2 as u8;
1692 bases[3] = e3 as u8;
1693
1694 let mask01 = (((e0 >> 8) & 0x0f) | (((e1 >> 8) & 0x0f) << 4)) as u8;
1695 if mask01 != 0 {
1696 n_mask[base_index / 8] |= mask01;
1697 }
1698 let mask23 = (((e2 >> 8) & 0x0f) | (((e3 >> 8) & 0x0f) << 4)) as u8;
1699 if mask23 != 0 {
1700 n_mask[base_index / 8 + 1] |= mask23;
1701 }
1702
1703 summary.a += entry_count_4(e0, e1, e2, e3, 12);
1704 summary.c += entry_count_4(e0, e1, e2, e3, 15);
1705 summary.g += entry_count_4(e0, e1, e2, e3, 18);
1706 summary.t += entry_count_4(e0, e1, e2, e3, 21);
1707 summary.n += entry_count_4(e0, e1, e2, e3, 24);
1708}
1709
1710#[cfg(feature = "simd")]
1711#[inline(always)]
1712fn quad_entry_from_bases(b0: u8, b1: u8, b2: u8, b3: u8) -> u32 {
1713 quad_entry_from_codes(
1714 BASE_LUT[usize::from(b0)],
1715 BASE_LUT[usize::from(b1)],
1716 BASE_LUT[usize::from(b2)],
1717 BASE_LUT[usize::from(b3)],
1718 )
1719}
1720
1721#[cfg(feature = "simd")]
1722#[inline(always)]
1723fn entry_count_4(e0: u32, e1: u32, e2: u32, e3: u32, shift: u32) -> usize {
1724 (((e0 >> shift) & 0x07)
1725 + ((e1 >> shift) & 0x07)
1726 + ((e2 >> shift) & 0x07)
1727 + ((e3 >> shift) & 0x07)) as usize
1728}
1729
1730#[inline(always)]
1731#[allow(clippy::too_many_arguments)]
1732fn pack_quad_from_codes(
1733 c0: u8,
1734 c1: u8,
1735 c2: u8,
1736 c3: u8,
1737 base_index: usize,
1738 base_out: &mut u8,
1739 summary: &mut BaseSummary,
1740 n_mask: &mut [u8],
1741) {
1742 apply_quad_entry(
1743 quad_entry_from_codes(c0, c1, c2, c3),
1744 base_index,
1745 base_out,
1746 summary,
1747 n_mask,
1748 );
1749}
1750
1751#[inline(always)]
1752fn quad_entry_from_codes(c0: u8, c1: u8, c2: u8, c3: u8) -> u32 {
1753 debug_assert!(c0 <= BASE_N);
1754 debug_assert!(c1 <= BASE_N);
1755 debug_assert!(c2 <= BASE_N);
1756 debug_assert!(c3 <= BASE_N);
1757 let key = quad_key(c0, c1, c2, c3);
1758 debug_assert!(key < BASE_QUAD_LUT.len());
1759 unsafe { *BASE_QUAD_LUT.get_unchecked(key) }
1762}
1763
1764#[inline(always)]
1765fn apply_quad_entry(
1766 entry: u32,
1767 base_index: usize,
1768 base_out: &mut u8,
1769 summary: &mut BaseSummary,
1770 n_mask: &mut [u8],
1771) {
1772 *base_out = entry as u8;
1773 let mask = ((entry >> 8) & 0x0f) as u8;
1774 if mask != 0 {
1775 n_mask[base_index / 8] |= mask << (base_index % 8);
1776 }
1777 summary.a += ((entry >> 12) & 0x07) as usize;
1778 summary.c += ((entry >> 15) & 0x07) as usize;
1779 summary.g += ((entry >> 18) & 0x07) as usize;
1780 summary.t += ((entry >> 21) & 0x07) as usize;
1781 summary.n += ((entry >> 24) & 0x07) as usize;
1782}
1783
1784struct QualityAccumulator {
1785 len: usize,
1786 min_phred: u8,
1787 max_phred: u8,
1788 sum_phred: u64,
1789 q20_bases: usize,
1790 q30_bases: usize,
1791}
1792
1793impl Default for QualityAccumulator {
1794 fn default() -> Self {
1795 Self {
1796 len: 0,
1797 min_phred: u8::MAX,
1798 max_phred: 0,
1799 sum_phred: 0,
1800 q20_bases: 0,
1801 q30_bases: 0,
1802 }
1803 }
1804}
1805
1806impl QualityAccumulator {
1807 #[inline(always)]
1808 fn observe(&mut self, byte: u8, offset: usize) -> Result<(), PackError> {
1809 let phred = phred33(byte, offset)?;
1810 self.min_phred = self.min_phred.min(phred);
1811 self.max_phred = self.max_phred.max(phred);
1812 self.len += 1;
1813 self.sum_phred += u64::from(phred);
1814 self.q20_bases += usize::from(phred >= 20);
1815 self.q30_bases += usize::from(phred >= 30);
1816 Ok(())
1817 }
1818
1819 #[inline(always)]
1820 fn finish(self) -> QualitySummary {
1821 if self.len == 0 {
1822 return QualitySummary::default();
1823 }
1824 QualitySummary {
1825 len: self.len,
1826 min_phred: Some(self.min_phred),
1827 max_phred: Some(self.max_phred),
1828 sum_phred: self.sum_phred,
1829 q20_bases: self.q20_bases,
1830 q30_bases: self.q30_bases,
1831 }
1832 }
1833}
1834
1835#[inline(always)]
1836fn add_base_count(summary: &mut BaseSummary, code: u8) {
1837 match code {
1838 0 => summary.a += 1,
1839 1 => summary.c += 1,
1840 2 => summary.g += 1,
1841 3 => summary.t += 1,
1842 _ => unreachable!(),
1843 }
1844}
1845
1846#[inline(always)]
1847fn phred33(byte: u8, offset: usize) -> Result<u8, PackError> {
1848 if (33..=126).contains(&byte) {
1849 Ok(byte - 33)
1850 } else {
1851 Err(PackError::InvalidQuality { offset, byte })
1852 }
1853}
1854
1855fn validate_thresholds(thresholds: &[u8]) -> Result<(), PackError> {
1856 if thresholds.len() > usize::from(u8::MAX) {
1857 return Err(PackError::TooManyQualityThresholds {
1858 count: thresholds.len(),
1859 });
1860 }
1861
1862 for (index, pair) in thresholds.windows(2).enumerate() {
1863 if pair[0] > pair[1] {
1864 return Err(PackError::UnsortedQualityThresholds { index: index + 1 });
1865 }
1866 }
1867
1868 Ok(())
1869}
1870
1871fn quality_bin(phred: u8, thresholds: &[u8]) -> u8 {
1872 let mut bin = 0;
1873 for &threshold in thresholds {
1874 if phred < threshold {
1875 break;
1876 }
1877 bin += 1;
1878 }
1879 bin
1880}
1881
1882#[cfg(test)]
1883mod tests;