code wiki / (root) / nx_fastq.nx

nx_fastq.nx source

↩ module page · 147 lines · 6125 B

1// nx_fastq.nx -- FASTQ single-record stream parser. 2// 3// license_tier: INDEPENDENT_REDERIVE 4// genealogy_id: international-research-sources/cock-2010-fastq-format 5// 6// G2.0b of NISHI_GENOMICS_SUBSTRATE_ROADMAP.md. The bridge between 7// raw sequencer-emitted byte streams and the substrate's typed DNA 8// + Phred primitives. Parses ONE record per call (4-line format) 9// and emits offset metadata; the caller composes downstream with: 10// 11// dna_pack_ambig(buf + seq_start, seq_len, out_bases, out_nbits) 12// nx_phred33_decode_string(buf + qual_start, qual_len, out_qs) 13// 14// FASTQ canonical 4-line format (Cock 2010): 15// line 1: '@' + identifier [+ optional description] 16// line 2: nucleotide sequence (single line in G2.0b -- see deferred) 17// line 3: '+' [+ optional repeated identifier] 18// line 4: quality string, len == seq len, Phred+33 encoded 19// 20// Validation enforced: 21// - record starts with '@' (offset position 0) 22// - separator line starts with '+' 23// - seq_len == qual_len 24// 25// What G2.0b does NOT do (deferred): 26// - Multi-line sequence / quality (rare in modern data; complicates 27// parsing because qual chars include '@' and '+' so line-by-line 28// scanning ambiguates without lookahead) -- G2.0b.2 29// - gzip / bgzip decompression -- callers pre-decompress; nx_gzip 30// wrapper exists, integration is G2.0b.3 31// - Header field parsing (Illumina-style "INSTRUMENT:RUN:FLOWCELL:..." 32// metadata) -- G2.0b.4 33// - Phred encoding auto-detection (+33 vs +64) -- G2.0a.1 heuristic 34// - Paired-end record pairing across R1/R2 files -- G2.0b.5 35// 36// API: 37// nx_find_lf(buf, start, end) -> i64 offset of next LF or -1 38// fastq_parse_one_record(buf, buf_len, offset, out_meta) -> i64 39// 40// out_meta layout (7 i64s): 41// [0] header_start (offset after the '@', start of identifier) 42// [1] header_len (bytes from header_start to end of line 1) 43// [2] seq_start 44// [3] seq_len 45// [4] qual_start 46// [5] qual_len (== seq_len, validated) 47// [6] next_offset (where the next record begins; == buf_len if last) 48// 49// Return codes: 50// 0 = success 51// 1 = EOF / no record at this offset 52// -1 = malformed record (bad markers, length mismatch, truncated) 53// 54// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml) 55// intended_use: "Stream-parse FASTQ records from a byte 56// buffer; the entry point for real-world 57// sequencer data into the alignment pipeline" 58// sil_target: SIL2 59// asil_target: QM 60// dal_target: DAL C 61// iec_62304_class: B 62// evidence: [no_floating_point, deterministic, 63// bit_equal_reproducible, 64// cock_2010_4_line_canonical_format, 65// composes_nx_const_ASCII_markers, 66// composes_nx_sequence_ambig_and_nx_phred, 67// seq_qual_length_invariant_enforced, 68// license_tier_INDEPENDENT_REDERIVE] 69// hazard_register: [bug-tape-fastq-multi-line-qual-confused-with-record-start, 70// bug-tape-fastq-seq-qual-length-mismatch-silent, 71// bug-tape-fastq-cr-lf-windows-line-ending-off-by-one, 72// bug-tape-fastq-truncated-trailing-record-not-flagged, 73// bug-tape-fastq-empty-record-returned-as-zero-len-not-skipped] 74// residual_risk: "G2.0b assumes single-line seq + qual. 75// Modern Illumina / ONT / PacBio FASTQ 76// always single-line; legacy multi-line 77// from old NCBI dumps will fail with -1 78// and require G2.0b.2. CRLF line endings 79// not stripped; caller should normalise." 80// verdict: NOT_YET_EVALUATED 81 82import "nx_syscalls.nx" 83import "nx_const.nx" 84 85// Find the next LF (0x0A) byte in [start, end). Returns the offset 86// of the LF, or -1 if none found. 87func nx_find_lf(buf: *u8, start: i64, end: i64) -> i64 { 88 var i: i64 = start 89 while i < end { 90 if (buf[i] & 0xff) == NX_ASCII_LF { return i } 91 i = i + 1 92 } 93 return -1 94} 95 96// Parse one FASTQ record beginning at `offset`. See header for 97// metadata layout and return codes. 98func fastq_parse_one_record(buf: *u8, buf_len: i64, offset: i64, 99 out_meta: *i64) -> i64 { 100 if buf_len < 0 { return -1 } 101 if offset < 0 { return -1 } 102 if offset >= buf_len { return 1 } // EOF 103 104 // ---- Line 1: '@' header ---- 105 if (buf[offset] & 0xff) != NX_ASCII_AT { return -1 } 106 let header_start: i64 = offset + 1 107 let lf1: i64 = nx_find_lf(buf, header_start, buf_len) 108 if lf1 < 0 { return -1 } 109 let header_len: i64 = lf1 - header_start 110 111 // ---- Line 2: sequence ---- 112 let seq_start: i64 = lf1 + 1 113 if seq_start >= buf_len { return -1 } 114 let lf2: i64 = nx_find_lf(buf, seq_start, buf_len) 115 if lf2 < 0 { return -1 } 116 let seq_len: i64 = lf2 - seq_start 117 118 // ---- Line 3: '+' separator ---- 119 let sep_start: i64 = lf2 + 1 120 if sep_start >= buf_len { return -1 } 121 if (buf[sep_start] & 0xff) != NX_ASCII_PLUS { return -1 } 122 let lf3: i64 = nx_find_lf(buf, sep_start, buf_len) 123 if lf3 < 0 { return -1 } 124 125 // ---- Line 4: quality ---- 126 let qual_start: i64 = lf3 + 1 127 if qual_start > buf_len { return -1 } 128 // qual_len must equal seq_len; we read exactly that many bytes 129 // and the next byte must be either LF or EOF. 130 let qual_end: i64 = qual_start + seq_len 131 if qual_end > buf_len { return -1 } 132 133 var next_offset: i64 = qual_end 134 if next_offset < buf_len { 135 if (buf[next_offset] & 0xff) != NX_ASCII_LF { return -1 } 136 next_offset = next_offset + 1 137 } 138 139 out_meta[0] = header_start 140 out_meta[1] = header_len 141 out_meta[2] = seq_start 142 out_meta[3] = seq_len 143 out_meta[4] = qual_start 144 out_meta[5] = seq_len // qual_len enforced equal to seq_len 145 out_meta[6] = next_offset 146 return 0 147}