Skip to main content

libradicl/
lib.rs

1/*
2 * Copyright (c) 2020-2024 COMBINE-lab.
3 *
4 * This file is part of libradicl
5 * (see https://www.github.com/COMBINE-lab/libradicl).
6 *
7 * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
8 */
9
10//! `libradicl` is a crate for reading (and writing) RAD (Reduced Alignment Data) format
11//! files.  The RAD format is a binary format designed to encode alignment information
12//! about sequencing reads and how they map to a set of targets (a genome, metagenome,
13//! transcriptome, etc.). The format is "reduced" because it is allowed to contain sparser
14//! information than e.g. a [SAM](https://samtools.github.io/hts-specs/) format file.
15//!
16//!
17//! While the eventual goal of this crate is to provide a generic API to read and write RAD
18//! files that may be designed for any purpose, it is driven mostly by our (the [COMBINE-lab's](https://combine-lab.github.io/))
19//! needs within the tools we produce that use the RAD format (e.g. [`alevin-fry`](https://github.com/COMBINE-lab/alevin-fry) and
20//! [`piscem-infer`](https://github.com/COMBINE-lab/alevin-fry)).  Thus, features are generally
21//! developed and added in the order that is most urgent to the development of these tools.
22//! However, we welcome external contributions via pull requests, and are happy to discuss
23//! your potential use cases for the RAD format, and how they might be supported.
24//!
25//! This crate is broken into several components that cover the various parts of RAD files
26//! including the type tag system, the header, and the main data chunks.  The names of
27//! each module are fairly self-explanatory.
28//!
29
30// scroll now, explore nom later
31
32use crate as libradicl;
33
34use self::libradicl::rad_types::{MappedFragmentOrientation, RadIntId};
35use self::libradicl::record::AlevinFryReadRecord;
36use self::libradicl::record::AtacSeqReadRecord;
37use self::libradicl::record::{
38    CollatableMappedRecord, CollatableRecordHeader, ConvertiblePrimitiveInteger, KnownSize,
39    MappedRecord, RecordHeader,
40};
41use self::libradicl::schema::{CollateKey, TempCellInfo};
42#[allow(unused_imports)]
43use ahash::{AHasher, RandomState};
44use bio_types::strand::*;
45use scroll::Pread;
46use serde::{Deserialize, Serialize};
47
48use std::collections::HashMap;
49use std::fs::File;
50use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};
51use std::io::{BufWriter, Write};
52use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
53use std::sync::{Arc, Mutex};
54use std::vec::Vec;
55
56pub mod chunk;
57pub mod codec;
58pub mod collation;
59pub mod constants;
60pub mod exit_codes;
61pub mod header;
62pub mod io;
63pub mod rad_types;
64pub mod readers;
65pub mod record;
66pub mod schema;
67pub mod unmapped;
68pub mod utils;
69pub mod writers;
70pub use chunk::ChunkBuf;
71pub use codec::{CHUNK_CODEC_TAG, ChunkCodec};
72pub use libradicl_macros::UmiTagged;
73pub use writers::{ConcurrentChunkWriter, RadFileWriter};
74
75#[macro_use]
76mod macros;
77
78// Name of the program, to be used in diagnostic messages.
79static LIB_NAME: &str = "libradicl";
80
81pub fn lib_name() -> &'static str {
82    LIB_NAME
83}
84
85#[derive(Serialize, Deserialize, Debug)]
86#[allow(dead_code)]
87pub struct BarcodeLookupMap {
88    pub barcodes: Vec<u64>,
89    //pub counts: Vec<usize>,
90    offsets: Vec<usize>,
91    bclen: u32,
92    prefix_len: u32,
93    suffix_len: u32,
94}
95
96impl BarcodeLookupMap {
97    pub fn new(mut kv: Vec<u64>, bclen: u32) -> BarcodeLookupMap {
98        let prefix_len = bclen.div_ceil(2) as u64;
99        let suffix_len = bclen - prefix_len as u32;
100
101        let _prefix_bits = 2 * prefix_len;
102        let suffix_bits = 2 * suffix_len;
103
104        kv.sort_unstable();
105
106        let pref_mask = ((4usize.pow(prefix_len as u32) - 1) as u64) << (suffix_bits);
107        let mut offsets = vec![0; 4usize.pow(prefix_len as u32) + 1];
108        let mut prev_ind = 0xFFFF;
109
110        for (n, &v) in kv.iter().enumerate() {
111            let ind = ((v & pref_mask) >> (suffix_bits)) as usize;
112            if ind != prev_ind {
113                for item in offsets.iter_mut().take(ind).skip(prev_ind + 1) {
114                    *item = n;
115                }
116                offsets[ind] = n;
117                prev_ind = ind;
118            }
119        }
120        for item in offsets.iter_mut().skip(prev_ind + 1) {
121            *item = kv.len();
122        }
123
124        //let nbc = kv.len();
125        BarcodeLookupMap {
126            barcodes: kv,
127            //counts: vec![0usize; nbc],
128            offsets,
129            bclen,
130            prefix_len: prefix_len as u32,
131            suffix_len,
132        }
133    }
134
135    #[allow(dead_code)]
136    pub fn barcode_for_idx(&self, idx: usize) -> u64 {
137        self.barcodes[idx]
138    }
139
140    pub fn find_exact(&self, query: u64) -> Option<usize> {
141        let mut ret: Option<usize> = None;
142
143        // extract the prefix we will use to search
144        let suffix_bits = 2 * self.suffix_len;
145        let query_pref = query >> suffix_bits;
146
147        // the range of entries having query_pref as their prefix
148        let qrange = std::ops::Range {
149            start: self.offsets[query_pref as usize],
150            end: self.offsets[(query_pref + 1) as usize],
151        };
152
153        let qs = qrange.start;
154
155        // if we can, then we return the found barcode and that there was 1 best hit
156        if let Ok(res) = self.barcodes[qrange].binary_search(&query) {
157            ret = Some(qs + res);
158        }
159        ret
160    }
161
162    /// The find function searches for the barcode `query` in the
163    /// BarcodeLookupMap.  It returns a tuple `(Option<usize>, usize)` where
164    /// the first element is either Some(usize) or None.  If
165    /// Some(usize) is returned, this is the *index* of a matching/neighboring barcode
166    /// if None is returned, then no match was found.  The second element is either
167    /// 0, 1 or 2.  If 0, no match was found; if 1 a unique match was found, if 2
168    /// then 2 or more equally good matches were found.
169    ///
170    /// The parameter `try_exact` controls whether a an exact search is performed
171    /// or not.  If this parameter is true, an exact search is performed before
172    /// a neighbor search.  Otherwise, the exact search is skipped.
173    pub fn find_neighbors(&self, query: u64, try_exact: bool) -> (Option<usize>, usize) {
174        let mut ret: Option<usize> = None;
175
176        // extract the prefix we will use to search
177        let pref_bits = 2 * self.prefix_len;
178        let suffix_bits = 2 * self.suffix_len;
179        let mut query_pref = query >> suffix_bits;
180        let mut num_neighbors = 0usize;
181
182        // the range of entries having query_pref as their prefix
183        let qrange = std::ops::Range {
184            start: self.offsets[query_pref as usize],
185            end: self.offsets[(query_pref + 1) as usize],
186        };
187
188        let qs = qrange.start;
189
190        if try_exact {
191            // first, we try to find exactly.
192            // if we can, then we return the found barcode and that there was 1 best hit
193            if let Ok(res) = self.barcodes[qrange.clone()].binary_search(&query) {
194                ret = Some(qs + res);
195                num_neighbors += 1;
196                return (ret, num_neighbors);
197            }
198        }
199
200        // othwerwise, we fall back to the 1 mismatch search
201        // NOTE: We stop here as soon as we find at most 2 neighbors
202        // for the query.  Thus, we only distinguish between the
203        // the cases where the query has 1 neighbor, or 2 or more neighbors.
204
205        // if we match the prefix exactly, we will look for possible matches
206        // that are 1 mismatch off in the suffix.
207        if !(std::ops::Range::<usize>::is_empty(&qrange)) {
208            // the initial offset of suffixes for this prefix
209            let qs = qrange.start;
210
211            // for each position in the suffix
212            for i in (0..suffix_bits).step_by(2) {
213                let bit_mask = 3 << (i);
214
215                // for each nucleotide
216                for nmod in 1..4 {
217                    let nucl = 0x3 & ((query >> i) + nmod);
218                    let nquery = (query & (!bit_mask)) | (nucl << i);
219
220                    if let Ok(res) = self.barcodes[qrange.clone()].binary_search(&nquery) {
221                        ret = Some(qs + res);
222                        num_neighbors += 1;
223                        if num_neighbors >= 2 {
224                            return (ret, num_neighbors);
225                        }
226                    }
227                }
228            }
229        }
230
231        {
232            // if we get here we've had either 0 or 1 matches holding the prefix fixed
233            // so we will now hold the suffix fixed and consider possible mutations of the prefix.
234
235            // for each position in the prefix
236            for i in (suffix_bits..(suffix_bits + pref_bits)).step_by(2) {
237                let bit_mask = 3 << i;
238
239                // for each nucleotide
240                for nmod in 1..4 {
241                    let nucl = 0x3 & ((query >> i) + nmod);
242                    let nquery = (query & (!bit_mask)) | (nucl << i);
243
244                    query_pref = nquery >> suffix_bits;
245
246                    let qrange = std::ops::Range {
247                        start: self.offsets[query_pref as usize],
248                        end: self.offsets[(query_pref + 1) as usize],
249                    };
250                    let qs = qrange.start;
251                    if let Ok(res) = self.barcodes[qrange].binary_search(&nquery) {
252                        ret = Some(qs + res);
253                        num_neighbors += 1;
254                        if num_neighbors >= 2 {
255                            return (ret, num_neighbors);
256                        }
257                    }
258                }
259            }
260        }
261
262        (ret, num_neighbors)
263    }
264}
265
266#[derive(Debug, Clone)]
267pub struct GlobalEqCellList {
268    cell_ids: Vec<usize>,
269    count: u32,
270}
271
272impl GlobalEqCellList {
273    pub fn from_umi_and_count(bc_mer: usize, count: u32) -> GlobalEqCellList {
274        let mut cc = GlobalEqCellList {
275            cell_ids: Vec::new(),
276            count: 0,
277        };
278        cc.cell_ids.push(bc_mer);
279        cc.count += count;
280        cc
281    }
282    pub fn add_element(&mut self, bc_mer: usize, count: u32) {
283        self.cell_ids.push(bc_mer);
284        self.count += count;
285    }
286}
287
288/*
289#[inline]
290pub fn dump_chunk(v: &mut CorrectedCbChunk, owriter: &Mutex<BufWriter<File>>) {
291    v.data.set_position(0);
292    let nbytes = (v.data.get_ref().len()) as u32;
293    let nrec = v.nrec;
294    v.data.write_all(&nbytes.to_le_bytes()).unwrap();
295    v.data.write_all(&nrec.to_le_bytes()).unwrap();
296    owriter.lock().unwrap().write_all(v.data.get_ref()).unwrap();
297}
298*/
299
300/// Given a [BufReader]`<T>` from which to read a set of records that
301/// should reside in the same collated bucket, this function will
302/// collate the records by cell barcode, filling them into a chunk of
303/// memory exactly as they will reside on disk.  If `compress` is true
304/// the collated chunk will be compressed, and then the result will be
305/// written to the output guarded by `owriter`.
306pub fn collate_temporary_bucket_twopass_generic<
307    B: ConvertiblePrimitiveInteger,
308    T: Read + Seek,
309    U: Write,
310    R: MappedRecord + KnownSize + CollatableMappedRecord<B>,
311>(
312    reader: &mut BufReader<T>,
313    rec_context: &<R as MappedRecord>::ParsingContext,
314    nrec: u32,
315    owriter: &Mutex<U>,
316    compress: bool,
317    cb_byte_map: &mut HashMap<u64, TempCellInfo, ahash::RandomState>,
318) -> usize
319where
320    u64: From<B>,
321{
322    let mut tbuf = vec![0u8; 65536];
323    let mut total_bytes = 0usize;
324    let chunk_header_size = 2 * std::mem::size_of::<u32>() as u64;
325    let size_of_aln = R::nbytes_aln(rec_context);
326
327    let calc_record_bytes = |num_aln: usize| -> usize { R::nbytes(num_aln as u32, rec_context) };
328
329    // read each record
330    for _ in 0..(nrec as usize) {
331        // read the header of the record
332        // we don't bother reading the whole thing here
333        // because we will just copy later as need be
334        let tup =
335            <R as CollatableMappedRecord<B>>::from_bytes_collatable_header(reader, rec_context)
336                .expect("can read header");
337
338        // get the entry for this chunk, or create a new one
339        let v = cb_byte_map
340            .entry(tup.collation_group_key(rec_context))
341            .or_insert(TempCellInfo {
342                offset: chunk_header_size,
343                nbytes: chunk_header_size as u32,
344                nrec: 0_u32,
345            });
346
347        // read the alignment records from the input file
348        let na = tup.naln() as usize;
349        let req_size = size_of_aln * na;
350        if tbuf.len() < req_size {
351            tbuf.resize(req_size, 0);
352        }
353        reader.read_exact(&mut tbuf[0..(size_of_aln * na)]).unwrap();
354        // compute the total number of bytes this record requires
355        let nbytes = calc_record_bytes(na);
356        v.offset += nbytes as u64;
357        v.nbytes += nbytes as u32;
358        v.nrec += 1;
359        total_bytes += nbytes;
360    }
361
362    // each cell will have a header (8 bytes each)
363    total_bytes += cb_byte_map.len() * chunk_header_size as usize;
364    let mut output_buffer = Cursor::new(vec![0u8; total_bytes]);
365
366    // loop over all distinct cell barcodes, write their
367    // corresponding chunk header, and compute what the
368    // offset in `output_buffer` is where the corresponding
369    // records should start.
370    let mut next_offset = 0u64;
371    for (_, v) in cb_byte_map.iter_mut() {
372        // jump to the position where this chunk should start
373        // and write the header
374        output_buffer.set_position(next_offset);
375        let cell_bytes = v.nbytes;
376        let cell_rec = v.nrec;
377        output_buffer.write_all(&cell_bytes.to_le_bytes()).unwrap();
378        output_buffer.write_all(&cell_rec.to_le_bytes()).unwrap();
379        // where we will start writing records for this cell
380        v.offset = output_buffer.position();
381        // the number of bytes allocated to this chunk
382        let nbytes = v.nbytes as u64;
383        // the next record will start after this one
384        next_offset += nbytes;
385    }
386
387    // now each key points to where we should write the next record for the CB
388    // reset the input pointer
389    reader
390        .get_mut()
391        .seek(SeekFrom::Start(0))
392        .expect("could not get read pointer.");
393
394    // for each record, read it
395    for _ in 0..(nrec as usize) {
396        // read the header of the record
397        // we don't bother reading the whole thing here
398        // because we will just copy later as need be
399        let tup =
400            <R as CollatableMappedRecord<B>>::from_bytes_collatable_header(reader, rec_context)
401                .expect("can read header");
402
403        // get the entry for this chunk, or create a new one
404        if let Some(v) = cb_byte_map.get_mut(&tup.collation_group_key(rec_context)) {
405            output_buffer.set_position(v.offset);
406
407            let na = tup.naln() as usize;
408            // write the header
409            tup.write_fields(&mut output_buffer, rec_context)
410                .expect("could write header");
411
412            let bytes_for_aln_rec = R::nbytes_aln(rec_context);
413            // copy over the alignment records
414            reader
415                .read_exact(&mut tbuf[0..(bytes_for_aln_rec * na)])
416                .unwrap();
417            output_buffer
418                .write_all(&tbuf[..(bytes_for_aln_rec * na)])
419                .unwrap();
420
421            v.offset = output_buffer.position();
422        } else {
423            panic!("should not have any barcodes we can't find");
424        }
425    }
426
427    output_buffer.set_position(0);
428
429    if compress {
430        // compress the contents of output_buffer to compressed_output
431        let mut compressed_output =
432            snap::write::FrameEncoder::new(Cursor::new(Vec::<u8>::with_capacity(total_bytes)));
433        compressed_output
434            .write_all(output_buffer.get_ref())
435            .expect("could not compress the output chunk.");
436
437        output_buffer = compressed_output
438            .into_inner()
439            .expect("couldn't unwrap the FrameEncoder.");
440        output_buffer.set_position(0);
441    }
442
443    owriter
444        .lock()
445        .unwrap()
446        .write_all(output_buffer.get_ref())
447        .unwrap();
448
449    cb_byte_map.len()
450}
451
452/// Given a [BufReader]`<T>` from which to read a set of records that
453/// should reside in the same collated bucket, this function will
454/// collate the records by cell barcode, filling them into a chunk of
455/// memory exactly as they will reside on disk.  If `compress` is true
456/// the collated chunk will be compressed, and then the result will be
457/// written to the output guarded by `owriter`.
458#[deprecated(
459    since = "0.10.0",
460    note = "This function is highly-specalized and works only with the AlevinFryReadRecordT<u64> type. \
461            This function has been deprecated in favor of the more generic `collate_temporary_bucket_twopass_generic`. \
462            Please use that function instead."
463)]
464pub fn collate_temporary_bucket_twopass<T: Read + Seek, U: Write>(
465    reader: &mut BufReader<T>,
466    bct: &RadIntId,
467    umit: &RadIntId,
468    nrec: u32,
469    owriter: &Mutex<U>,
470    compress: bool,
471    cb_byte_map: &mut HashMap<u64, TempCellInfo, ahash::RandomState>,
472) -> usize {
473    let mut tbuf = vec![0u8; 65536];
474    let mut total_bytes = 0usize;
475    let chunk_header_size = 2 * std::mem::size_of::<u32>() as u64;
476    let size_of_u32 = std::mem::size_of::<u32>();
477    let size_of_bc = bct.bytes_for_type();
478    let size_of_umi = umit.bytes_for_type();
479
480    let calc_record_bytes = |num_aln: usize| -> usize {
481        size_of_u32 + size_of_bc + size_of_umi + (size_of_u32 * num_aln)
482    };
483
484    // read each record
485    for _ in 0..(nrec as usize) {
486        // read the header of the record
487        // we don't bother reading the whole thing here
488        // because we will just copy later as need be
489        let tup = AlevinFryReadRecord::from_bytes_record_header(reader, bct, umit);
490
491        // get the entry for this chunk, or create a new one
492        let v = cb_byte_map.entry(tup.0).or_insert(TempCellInfo {
493            offset: chunk_header_size,
494            nbytes: chunk_header_size as u32,
495            nrec: 0_u32,
496        });
497
498        // read the alignment records from the input file
499        let na = tup.2 as usize;
500        let req_size = size_of_u32 * na;
501        if tbuf.len() < req_size {
502            tbuf.resize(req_size, 0);
503        }
504        reader.read_exact(&mut tbuf[0..(size_of_u32 * na)]).unwrap();
505        // compute the total number of bytes this record requires
506        let nbytes = calc_record_bytes(na);
507        v.offset += nbytes as u64;
508        v.nbytes += nbytes as u32;
509        v.nrec += 1;
510        total_bytes += nbytes;
511    }
512
513    // each cell will have a header (8 bytes each)
514    total_bytes += cb_byte_map.len() * chunk_header_size as usize;
515    let mut output_buffer = Cursor::new(vec![0u8; total_bytes]);
516
517    // loop over all distinct cell barcodes, write their
518    // corresponding chunk header, and compute what the
519    // offset in `output_buffer` is where the corresponding
520    // records should start.
521    let mut next_offset = 0u64;
522    for (_, v) in cb_byte_map.iter_mut() {
523        // jump to the position where this chunk should start
524        // and write the header
525        output_buffer.set_position(next_offset);
526        let cell_bytes = v.nbytes;
527        let cell_rec = v.nrec;
528        output_buffer.write_all(&cell_bytes.to_le_bytes()).unwrap();
529        output_buffer.write_all(&cell_rec.to_le_bytes()).unwrap();
530        // where we will start writing records for this cell
531        v.offset = output_buffer.position();
532        // the number of bytes allocated to this chunk
533        let nbytes = v.nbytes as u64;
534        // the next record will start after this one
535        next_offset += nbytes;
536    }
537
538    // now each key points to where we should write the next record for the CB
539    // reset the input pointer
540    reader
541        .get_mut()
542        .seek(SeekFrom::Start(0))
543        .expect("could not get read pointer.");
544
545    // for each record, read it
546    for _ in 0..(nrec as usize) {
547        // read the header of the record
548        // we don't bother reading the whole thing here
549        // because we will just copy later as need be
550        let tup = AlevinFryReadRecord::from_bytes_record_header(reader, bct, umit);
551
552        // get the entry for this chunk, or create a new one
553        if let Some(v) = cb_byte_map.get_mut(&tup.0) {
554            output_buffer.set_position(v.offset);
555
556            // write the num align
557            let na = tup.2 as usize;
558            let nau32 = na as u32;
559            output_buffer.write_all(&nau32.to_le_bytes()).unwrap();
560
561            // write the corrected barcode
562            bct.write_to(tup.0, &mut output_buffer).unwrap();
563            umit.write_to(tup.1, &mut output_buffer).unwrap();
564
565            // read the alignment records
566            reader.read_exact(&mut tbuf[0..(size_of_u32 * na)]).unwrap();
567            // write them
568            output_buffer
569                .write_all(&tbuf[..(size_of_u32 * na)])
570                .unwrap();
571
572            v.offset = output_buffer.position();
573        } else {
574            panic!("should not have any barcodes we can't find");
575        }
576    }
577
578    output_buffer.set_position(0);
579
580    if compress {
581        // compress the contents of output_buffer to compressed_output
582        let mut compressed_output =
583            snap::write::FrameEncoder::new(Cursor::new(Vec::<u8>::with_capacity(total_bytes)));
584        compressed_output
585            .write_all(output_buffer.get_ref())
586            .expect("could not compress the output chunk.");
587
588        output_buffer = compressed_output
589            .into_inner()
590            .expect("couldn't unwrap the FrameEncoder.");
591        output_buffer.set_position(0);
592    }
593
594    owriter
595        .lock()
596        .unwrap()
597        .write_all(output_buffer.get_ref())
598        .unwrap();
599
600    cb_byte_map.len()
601}
602
603pub fn collate_temporary_bucket_twopass_atac<T: Read + Seek, U: Write>(
604    reader: &mut BufReader<T>,
605    bct: &RadIntId,
606    nrec: u32,
607    owriter: &Mutex<U>,
608    compress: bool,
609    cb_byte_map: &mut HashMap<u64, TempCellInfo, ahash::RandomState>,
610) -> usize {
611    let mut tbuf = vec![0u8; 65536];
612    let mut total_bytes = 0usize;
613    let header_size = 2 * std::mem::size_of::<u32>() as u64;
614    let size_of_bc = bct.bytes_for_type();
615    let size_of_rec = std::mem::size_of::<u32>()
616        + std::mem::size_of::<u8>()
617        + std::mem::size_of::<u32>()
618        + std::mem::size_of::<u16>();
619
620    let size_of_u32 = std::mem::size_of::<u32>();
621    let calc_record_bytes =
622        |num_aln: usize| -> usize { size_of_u32 + size_of_bc + (size_of_rec * num_aln) };
623
624    // read each record
625    for _ in 0..(nrec as usize) {
626        // read the header of the record
627        // we don't bother reading the whole thing here
628        // because we will just copy later as need be
629        let tup = AtacSeqReadRecord::from_bytes_record_header(reader, bct);
630
631        // get the entry for this chunk, or create a new one
632        let v = cb_byte_map.entry(tup.0).or_insert(TempCellInfo {
633            offset: header_size,
634            nbytes: header_size as u32,
635            nrec: 0_u32,
636        });
637
638        // read the alignment records from the input file
639        let na = tup.1 as usize;
640        let req_size = size_of_rec * na;
641        if tbuf.len() < req_size {
642            tbuf.resize(req_size, 0);
643        }
644        reader.read_exact(&mut tbuf[0..(size_of_rec * na)]).unwrap();
645        // compute the total number of bytes this record requires
646        let nbytes = calc_record_bytes(na);
647        v.offset += nbytes as u64;
648        v.nbytes += nbytes as u32;
649        v.nrec += 1;
650        total_bytes += nbytes;
651    }
652
653    // each cell will have a header (8 bytes each)
654    total_bytes += cb_byte_map.len() * header_size as usize;
655    let mut output_buffer = Cursor::new(vec![0u8; total_bytes]);
656
657    // loop over all distinct cell barcodes, write their
658    // corresponding chunk header, and compute what the
659    // offset in `output_buffer` is where the corresponding
660    // records should start.
661    let mut next_offset = 0u64;
662    for (_, v) in cb_byte_map.iter_mut() {
663        // jump to the position where this chunk should start
664        // and write the header
665        output_buffer.set_position(next_offset);
666        let cell_bytes = v.nbytes;
667        let cell_rec = v.nrec;
668        output_buffer.write_all(&cell_bytes.to_le_bytes()).unwrap();
669        output_buffer.write_all(&cell_rec.to_le_bytes()).unwrap();
670        // where we will start writing records for this cell
671        v.offset = output_buffer.position();
672        // the number of bytes allocated to this chunk
673        let nbytes = v.nbytes as u64;
674        // the next record will start after this one
675        next_offset += nbytes;
676    }
677
678    // now each key points to where we should write the next record for the CB
679    // reset the input pointer
680    reader
681        .get_mut()
682        .seek(SeekFrom::Start(0))
683        .expect("could not get read pointer.");
684
685    // for each record, read it
686    for _ in 0..(nrec as usize) {
687        // read the header of the record
688        // we don't bother reading the whole thing here
689        // because we will just copy later as need be
690        let tup = AtacSeqReadRecord::from_bytes_record_header(reader, bct);
691
692        // get the entry for this chunk, or create a new one
693        if let Some(v) = cb_byte_map.get_mut(&tup.0) {
694            output_buffer.set_position(v.offset);
695
696            // write the num align
697            let na = tup.1 as usize;
698            let nau32 = na as u32;
699            output_buffer.write_all(&nau32.to_le_bytes()).unwrap();
700
701            // write the corrected barcode
702            bct.write_to(tup.0, &mut output_buffer).unwrap();
703
704            // read the alignment records
705            reader.read_exact(&mut tbuf[0..(size_of_rec * na)]).unwrap();
706            // write them
707            output_buffer
708                .write_all(&tbuf[..(size_of_rec * na)])
709                .unwrap();
710
711            v.offset = output_buffer.position();
712        } else {
713            panic!("should not have any barcodes we can't find");
714        }
715    }
716
717    output_buffer.set_position(0);
718
719    if compress {
720        // compress the contents of output_buffer to compressed_output
721        let mut compressed_output =
722            snap::write::FrameEncoder::new(Cursor::new(Vec::<u8>::with_capacity(total_bytes)));
723        compressed_output
724            .write_all(output_buffer.get_ref())
725            .expect("could not compress the output chunk.");
726
727        output_buffer = compressed_output
728            .into_inner()
729            .expect("couldn't unwrap the FrameEncoder.");
730        output_buffer.set_position(0);
731    }
732
733    owriter
734        .lock()
735        .unwrap()
736        .write_all(output_buffer.get_ref())
737        .unwrap();
738
739    cb_byte_map.len()
740}
741
742/*
743pub fn collate_temporary_bucket<T: Read>(
744    reader: &mut T,
745    bct: &RadIntId,
746    umit: &RadIntId,
747    _nchunks: u32,
748    nrec: u32,
749    output_cache: &mut HashMap<u64, CorrectedCbChunk, ahash::RandomState>,
750) {
751    let mut tbuf = [0u8; 65536];
752    // estimated average number of records per barcode
753    // this is just for trying to pre-allocate buffers
754    // right; should not affect correctness
755    let est_num_rec = 1; //(nrec / nchunks) + 1;
756
757    // for each record, read it
758    for _ in 0..(nrec as usize) {
759        // read the header of the record
760        // we don't bother reading the whole thing here
761        // because we will just copy later as need be
762        let tup = AlevinFryReadRecord::from_bytes_record_header(reader, bct, umit);
763
764        // get the entry for this chunk, or create a new one
765        let v = output_cache
766            .entry(tup.0)
767            .or_insert_with(|| CorrectedCbChunk::from_label_and_counter(tup.0, est_num_rec));
768
769        // keep track of the number of records we're writing
770        v.nrec += 1;
771        // write the num align
772        let na = tup.2;
773        v.data.write_all(&na.to_le_bytes()).unwrap();
774        // write the corrected barcode
775        bct.write_to(tup.0, &mut v.data).unwrap();
776        umit.write_to(tup.1, &mut v.data).unwrap();
777        // read the alignment records
778        reader.read_exact(&mut tbuf[0..(4 * na as usize)]).unwrap();
779        // write them
780        v.data.write_all(&tbuf[..(4 * na as usize)]).unwrap();
781    }
782}
783
784pub fn process_corrected_cb_chunk<T: Read>(
785    reader: &mut T,
786    bct: &RadIntId,
787    umit: &RadIntId,
788    correct_map: &HashMap<u64, u64>,
789    expected_ori: &Strand,
790    output_cache: &DashMap<u64, CorrectedCbChunk>,
791    owriter: &Mutex<BufWriter<File>>,
792) {
793    let mut buf = [0u8; 8];
794    let mut tbuf = [0u8; 65536];
795
796    // get the number of bytes and records for
797    // the next chunk
798    reader.read_exact(&mut buf).unwrap();
799    let _nbytes = buf.pread::<u32>(0).unwrap();
800    let nrec = buf.pread::<u32>(4).unwrap();
801    // for each record, read it
802    for _ in 0..(nrec as usize) {
803        let tup = AlevinFryReadRecord::from_bytes_record_header(reader, bct, umit);
804        // if this record had a correct or correctable barcode
805        if let Some(corrected_id) = correct_map.get(&tup.0) {
806            let rr = AlevinFryReadRecord::from_bytes_with_header_keep_ori(
807                reader,
808                tup.0,
809                tup.1,
810                tup.2,
811                expected_ori,
812            );
813
814            if let Some(mut v) = output_cache.get_mut(corrected_id) {
815                // update the corresponding corrected chunk entry
816                v.remaining_records -= 1;
817                let last_record = v.remaining_records == 0;
818                // if there are no alignments in the record
819                // (potentially b/c of orientation filtering)
820                // then don't push info on to the vector.
821                if rr.is_empty() {
822                    if last_record {
823                        dump_chunk(&mut v, owriter);
824                    }
825                    continue;
826                }
827                v.nrec += 1;
828                let na = rr.refs.len() as u32;
829                v.data.write_all(&na.to_le_bytes()).unwrap();
830                bct.write_to(*corrected_id, &mut v.data).unwrap();
831                umit.write_to(rr.umi, &mut v.data).unwrap();
832                v.data.write_all(as_u8_slice(&rr.refs[..])).unwrap();
833                if last_record {
834                    dump_chunk(&mut v, owriter);
835                }
836            }
837        } else {
838            reader
839                .read_exact(&mut tbuf[0..(4 * (tup.2 as usize))])
840                .unwrap();
841        }
842    }
843}
844*/
845
846/// Represents a temporary bucket of barcodes whose records will
847/// be written together and then collated later in memory.
848pub struct TempBucket {
849    pub bucket_id: u32,
850    pub bucket_writer: Arc<Mutex<BufWriter<File>>>,
851    pub num_chunks: u32,
852    pub num_records: u32,
853    pub num_records_written: AtomicU32,
854    pub num_bytes_written: AtomicU64,
855}
856
857impl TempBucket {
858    pub fn from_id_and_parent(bucket_id: u32, parent: &std::path::Path) -> Self {
859        TempBucket {
860            bucket_id,
861            bucket_writer: Arc::new(Mutex::new(BufWriter::with_capacity(
862                4096_usize,
863                File::create(parent.join(format!("bucket_{}.tmp", bucket_id))).unwrap(),
864            ))),
865            num_chunks: 0u32,
866            num_records: 0u32,
867            num_records_written: AtomicU32::new(0u32),
868            num_bytes_written: AtomicU64::new(0u64),
869        }
870    }
871}
872
873/// Read an input chunk from `reader` and write the
874/// resulting records to the corresponding in-memory
875/// buffers `local_buffers`.  As soon as any buffer
876/// reaches `flush_limit`, flush the buffer by writing
877/// it to the `output_cache`.
878#[allow(clippy::too_many_arguments)]
879pub fn dump_corrected_cb_chunk_to_temp_file_generic<
880    B: ConvertiblePrimitiveInteger + std::convert::From<u64>,
881    T: Read,
882    R: MappedRecord + KnownSize + CollatableMappedRecord<B>,
883>(
884    reader: &mut BufReader<T>,
885    rec_context: &<R as MappedRecord>::ParsingContext,
886    correct_map: &HashMap<u64, u64>,
887    expected_ori: &Strand,
888    output_cache: &HashMap<u64, Arc<TempBucket>>,
889    local_buffers: &mut [Cursor<&mut [u8]>],
890    flush_limit: usize,
891) where
892    u64: From<B>,
893    <R as MappedRecord>::ParsingContext: std::fmt::Debug,
894{
895    let mut buf = [0u8; 8];
896    let mut tbuf = vec![0u8; 4096];
897    //let mut tcursor = Cursor::new(tbuf);
898    //tcursor.set_position(0);
899
900    // get the number of bytes and records for
901    // the next chunk
902    reader.read_exact(&mut buf).unwrap();
903    let _nbytes = buf.pread::<u32>(0).unwrap();
904    let nrec = buf.pread::<u32>(4).unwrap();
905
906    let expected_ori: MappedFragmentOrientation = expected_ori.into();
907
908    // for each record, read it
909    for _ in 0..(nrec as usize) {
910        let mut tup =
911            <R as CollatableMappedRecord<B>>::from_bytes_collatable_header(reader, rec_context)
912                .expect("could read header");
913
914        // if this record had a correct or correctable barcode
915        if let Some(corrected_id) = correct_map.get(&tup.collate_key().into()) {
916            let mut rr =
917                R::from_bytes_with_header_retain_ori(reader, &mut tup, rec_context, &expected_ori);
918
919            if rr.is_empty() {
920                continue;
921            }
922            if let Some(v) = output_cache.get(corrected_id) {
923                // if this is a valid barcode, then
924                // write the corresponding entry to the
925                // thread-local buffer for this bucket
926
927                let na = tup.naln() as usize;
928                // the total number of bytes this record will take
929                let nb = R::nbytes(na as u32, rec_context) as u64;
930
931                // the buffer index for this corrected barcode
932                let buffidx = v.bucket_id as usize;
933                // the current cursor for this buffer
934                let bcursor = &mut local_buffers[buffidx];
935                // the current position of the cursor
936                let len = bcursor.position() as usize;
937
938                // if writing the next record (nb bytes) will put us over
939                // the flush size for the thread-local buffer for this bucket
940                // then first flush the buffer to file.
941                if len + nb as usize >= flush_limit {
942                    let mut filebuf = v.bucket_writer.lock().unwrap();
943                    filebuf.write_all(&bcursor.get_ref()[0..len]).unwrap();
944                    // and reset the local buffer cursor
945                    bcursor.set_position(0);
946                }
947
948                // set to the corrected collate key
949                rr.set_collate_key((*corrected_id).into());
950
951                let blen = bcursor.position() as usize;
952                // now, write the record to the buffer
953                rr.write(bcursor, rec_context).expect("can write record");
954                let alen = bcursor.position() as usize;
955                let actual = alen - blen;
956                let expected = R::nbytes(na as u32, rec_context);
957                assert_eq!(
958                    expected, actual,
959                    "Expected to write {} bytes, but wrote {}.",
960                    expected, actual
961                );
962
963                // update number of written records
964                v.num_records_written.fetch_add(1, Ordering::SeqCst);
965                // update number of written bytes
966                v.num_bytes_written.fetch_add(nb, Ordering::SeqCst);
967            }
968        } else {
969            // in this branch, we don't have access to a correct barcode for
970            // what we observed, so we need to discard the remaining part of
971            // the record.
972
973            // we already read the header, so just the alignments
974            let req_len = R::nbytes_aln(rec_context) * tup.naln() as usize;
975            let do_resize = req_len > tbuf.len();
976
977            if do_resize {
978                tbuf.resize(req_len, 0);
979            }
980
981            reader.read_exact(&mut tbuf[0..req_len]).unwrap();
982
983            if do_resize {
984                tbuf.resize(4096, 0);
985                tbuf.shrink_to_fit();
986            }
987        }
988    }
989}
990
991/// Read an input chunk from `reader` and write the
992/// resulting records to the corresponding in-memory
993/// buffers `local_buffers`.  As soon as any buffer
994/// reaches `flush_limit`, flush the buffer by writing
995/// it to the `output_cache`.
996#[deprecated(
997    since = "0.10.0",
998    note = "This function is highly-specalized and works only with the AlevinFryReadRecordT<u64> type. \
999            This function has been deprecated in favor of the more generic `dump_corrected_cb_chunk_to_temp_file_generic`. \
1000            Please use that function instead."
1001)]
1002#[allow(clippy::too_many_arguments)]
1003pub fn dump_corrected_cb_chunk_to_temp_file<T: Read>(
1004    reader: &mut BufReader<T>,
1005    bct: &RadIntId,
1006    umit: &RadIntId,
1007    correct_map: &HashMap<u64, u64>,
1008    expected_ori: &Strand,
1009    output_cache: &HashMap<u64, Arc<TempBucket>>,
1010    local_buffers: &mut [Cursor<&mut [u8]>],
1011    flush_limit: usize,
1012) {
1013    let mut buf = [0u8; 8];
1014    let mut tbuf = vec![0u8; 4096];
1015    //let mut tcursor = Cursor::new(tbuf);
1016    //tcursor.set_position(0);
1017
1018    // get the number of bytes and records for
1019    // the next chunk
1020    reader.read_exact(&mut buf).unwrap();
1021    let _nbytes = buf.pread::<u32>(0).unwrap();
1022    let nrec = buf.pread::<u32>(4).unwrap();
1023
1024    let bc_bytes = bct.bytes_for_type();
1025    let umi_bytes = umit.bytes_for_type();
1026    let na_bytes = std::mem::size_of::<u32>();
1027    let target_id_bytes = std::mem::size_of::<u32>();
1028
1029    // for each record, read it
1030    for _ in 0..(nrec as usize) {
1031        let tup = AlevinFryReadRecord::from_bytes_record_header(reader, bct, umit);
1032
1033        // if this record had a correct or correctable barcode
1034        if let Some(corrected_id) = correct_map.get(&tup.0) {
1035            let rr = AlevinFryReadRecord::from_bytes_with_header_keep_ori(
1036                reader,
1037                tup.0,
1038                tup.1,
1039                tup.2,
1040                expected_ori,
1041            );
1042
1043            if rr.is_empty() {
1044                continue;
1045            }
1046            if let Some(v) = output_cache.get(corrected_id) {
1047                // if this is a valid barcode, then
1048                // write the corresponding entry to the
1049                // thread-local buffer for this bucket
1050
1051                // the total number of bytes this record will take
1052                let nb = (rr.refs.len() * target_id_bytes + na_bytes + bc_bytes + umi_bytes) as u64;
1053
1054                // the buffer index for this corrected barcode
1055                let buffidx = v.bucket_id as usize;
1056                // the current cursor for this buffer
1057                let bcursor = &mut local_buffers[buffidx];
1058                // the current position of the cursor
1059                let len = bcursor.position() as usize;
1060
1061                // if writing the next record (nb bytes) will put us over
1062                // the flush size for the thread-local buffer for this bucket
1063                // then first flush the buffer to file.
1064                if len + nb as usize >= flush_limit {
1065                    let mut filebuf = v.bucket_writer.lock().unwrap();
1066                    filebuf.write_all(&bcursor.get_ref()[0..len]).unwrap();
1067                    // and reset the local buffer cursor
1068                    bcursor.set_position(0);
1069                }
1070
1071                // now, write the record to the buffer
1072                let na = rr.refs.len() as u32;
1073                bcursor.write_all(&na.to_le_bytes()).unwrap();
1074                bct.write_to(*corrected_id, bcursor).unwrap();
1075                umit.write_to(rr.umi, bcursor).unwrap();
1076                bcursor.write_all(as_u8_slice(&rr.refs[..])).unwrap();
1077
1078                // update number of written records
1079                v.num_records_written.fetch_add(1, Ordering::SeqCst);
1080                // update number of written bytes
1081                v.num_bytes_written.fetch_add(nb, Ordering::SeqCst);
1082            }
1083        } else {
1084            // in this branch, we don't have access to a correct barcode for
1085            // what we observed, so we need to discard the remaining part of
1086            // the record.
1087            let req_len = target_id_bytes * (tup.2 as usize);
1088            let do_resize = req_len > tbuf.len();
1089
1090            if do_resize {
1091                tbuf.resize(req_len, 0);
1092            }
1093
1094            reader
1095                .read_exact(&mut tbuf[0..(target_id_bytes * (tup.2 as usize))])
1096                .unwrap();
1097
1098            if do_resize {
1099                tbuf.resize(4096, 0);
1100                tbuf.shrink_to_fit();
1101            }
1102        }
1103    }
1104}
1105
1106/// Read an input chunk from `reader` and write the
1107/// resulting records to the corresponding in-memory
1108/// buffers `local_buffers`.  As soon as any buffer
1109/// reaches `flush_limit`, flush the buffer by writing
1110/// it to the `output_cache`.
1111#[allow(clippy::too_many_arguments)]
1112pub fn dump_corrected_cb_chunk_to_temp_file_atac<T: Read>(
1113    reader: &mut BufReader<T>,
1114    bct: &RadIntId,
1115    correct_map: &HashMap<u64, u64>,
1116    output_cache: &HashMap<u64, Arc<TempBucket>>,
1117    local_buffers: &mut [Cursor<&mut [u8]>],
1118    flush_limit: usize,
1119    ck: CollateKey, // f: F
1120)
1121where
1122// F: Fn(u32)
1123{
1124    let mut buf = [0u8; 8];
1125    let mut tbuf = vec![0u8; 4096];
1126    //let mut tcursor = Cursor::new(tbuf);
1127    //tcursor.set_position(0);
1128
1129    // get the number of bytes and records for
1130    // the next chunk
1131    reader.read_exact(&mut buf).unwrap();
1132    let _nbytes = buf.pread::<u32>(0).unwrap();
1133    let nrec = buf.pread::<u32>(4).unwrap();
1134
1135    let bc_bytes = bct.bytes_for_type();
1136    let na_bytes = std::mem::size_of::<u32>();
1137    let target_id_bytes = std::mem::size_of::<u32>() + // ref id
1138                        std::mem::size_of::<u8>() + // type
1139                        std::mem::size_of::<u32>() + // position
1140                        std::mem::size_of::<u16>(); // frag_len
1141
1142    // for each record, read it
1143    for _ in 0..(nrec as usize) {
1144        let tup = AtacSeqReadRecord::from_bytes_record_header(reader, bct);
1145
1146        // if this record had a correct or correctable barcode
1147        if let Some(corrected_id) = correct_map.get(&tup.0) {
1148            // could be replaced with orientation
1149            let rr = AtacSeqReadRecord::from_bytes_with_header(reader, tup.0, tup.1);
1150
1151            if rr.is_empty() || tup.1 > 1 {
1152                continue;
1153            }
1154            let pos = rr.start_pos[0];
1155            let ref_id = rr.refs[0];
1156            let check_id = match ck {
1157                CollateKey::Barcode => *corrected_id,
1158                CollateKey::Pos(ref f) => f(pos, ref_id as usize) as u64,
1159            };
1160            if let Some(v) = output_cache.get(&check_id) {
1161                // if this is a valid barcode, then
1162                // write the corresponding entry to the
1163                // thread-local buffer for this bucket
1164
1165                // the total number of bytes this record will take
1166                let nb = (rr.refs.len() * target_id_bytes + na_bytes + bc_bytes) as u64;
1167
1168                // the buffer index for this corrected barcode
1169                let buffidx = v.bucket_id as usize;
1170                // the current cursor for this buffer
1171                let bcursor = &mut local_buffers[buffidx];
1172                // the current position of the cursor
1173                let len = bcursor.position() as usize;
1174
1175                // if writing the next record (nb bytes) will put us over
1176                // the flush size for the thread-local buffer for this bucket
1177                // then first flush the buffer to file.
1178                if len + nb as usize >= flush_limit {
1179                    let mut filebuf = v.bucket_writer.lock().unwrap();
1180                    filebuf.write_all(&bcursor.get_ref()[0..len]).unwrap();
1181                    // and reset the local buffer cursor
1182                    bcursor.set_position(0);
1183                }
1184
1185                // now, write the record to the buffer
1186                let na = rr.refs.len() as u32;
1187                bcursor.write_all(&na.to_le_bytes()).unwrap();
1188                bct.write_to(*corrected_id, bcursor).unwrap();
1189                bcursor.write_all(as_u8_slice(&rr.refs[..])).unwrap();
1190                bcursor.write_all(as_u8_slice_u8(&rr.map_type[..])).unwrap();
1191                bcursor.write_all(as_u8_slice(&rr.start_pos[..])).unwrap();
1192                bcursor
1193                    .write_all(as_u8_slice_u16(&rr.frag_lengths[..]))
1194                    .unwrap();
1195
1196                // update number of written records
1197                v.num_records_written.fetch_add(1, Ordering::SeqCst);
1198                // update number of written bytes
1199                v.num_bytes_written.fetch_add(nb, Ordering::SeqCst);
1200            }
1201        } else {
1202            // in this branch, we don't have access to a correct barcode for
1203            // what we observed, so we need to discard the remaining part of
1204            // the record.
1205            let req_len = target_id_bytes * (tup.1 as usize);
1206            let do_resize = req_len > tbuf.len();
1207
1208            if do_resize {
1209                tbuf.resize(req_len, 0);
1210            }
1211
1212            reader
1213                .read_exact(&mut tbuf[0..(target_id_bytes * (tup.1 as usize))])
1214                .unwrap();
1215
1216            if do_resize {
1217                tbuf.resize(4096, 0);
1218                tbuf.shrink_to_fit();
1219            }
1220        }
1221    }
1222}
1223
1224pub fn as_u8_slice(v: &[u32]) -> &[u8] {
1225    unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)) }
1226}
1227
1228pub fn as_u8_slice_u16(v: &[u16]) -> &[u8] {
1229    unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)) }
1230}
1231
1232pub fn as_u8_slice_u8(v: &[u8]) -> &[u8] {
1233    unsafe { std::slice::from_raw_parts(v.as_ptr(), std::mem::size_of_val(v)) }
1234}
1235
1236#[cfg(test)]
1237mod tests {
1238    use crate::BarcodeLookupMap;
1239
1240    #[test]
1241    fn test_barcode_lookup_map() {
1242        let barcode_sv_even = vec![
1243            b"AACC", b"AAGG", b"CAGT", b"CATT", b"GACC", b"GATA", b"TCAG", b"TCGT",
1244        ];
1245        let barcode_sv_odd = vec![
1246            b"AACCA", b"AAGGC", b"CAGTA", b"CATTG", b"GACCG", b"GATAC", b"TCAGA", b"TCGTG",
1247        ];
1248
1249        let mut barcode_even = Vec::with_capacity(barcode_sv_even.len());
1250        for b in barcode_sv_even.clone() {
1251            if let Some((_, km, _)) =
1252                needletail::bitkmer::BitNuclKmer::new(&b[..], b.len() as u8, false).next()
1253            {
1254                barcode_even.push(km.0);
1255            }
1256        }
1257        let mut barcode_odd = Vec::with_capacity(barcode_sv_odd.len());
1258        for b in barcode_sv_odd.clone() {
1259            if let Some((_, km, _)) =
1260                needletail::bitkmer::BitNuclKmer::new(&b[..], b.len() as u8, false).next()
1261            {
1262                barcode_odd.push(km.0);
1263            }
1264        }
1265
1266        let me = BarcodeLookupMap::new(barcode_even, 4);
1267        let mo = BarcodeLookupMap::new(barcode_odd, 5);
1268
1269        let x = b"CAGA";
1270        if let Some((_, et, _)) =
1271            needletail::bitkmer::BitNuclKmer::new(&x[..], x.len() as u8, false).next()
1272        {
1273            assert_eq!((Some(2), 1), me.find_neighbors(et.0, false));
1274        }
1275        let x = b"CAATG";
1276        if let Some((_, et, _)) =
1277            needletail::bitkmer::BitNuclKmer::new(&x[..], x.len() as u8, false).next()
1278        {
1279            assert_eq!((Some(3), 1), mo.find_neighbors(et.0, false));
1280        }
1281    }
1282}