libradicl 0.14.3

support library for alevin-fry
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
/*
 * Copyright (c) 2020-2024 COMBINE-lab.
 *
 * This file is part of libradicl
 * (see https://www.github.com/COMBINE-lab/libradicl).
 *
 * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
 */

//! Types and functions that primarily deal with the reading and writing of
//! data [Chunk]s in the RAD file.

use crate::{self as libradicl};
use anyhow::{self, Context};
use libradicl::record::MappedRecord;
use scroll::Pread;
use std::io::{Cursor, Read};
use std::io::{Seek, SeekFrom, Write};

/// Represents a chunk of recrords in a RAD file. The record chunks constitute the
/// bulk of the RAD file, and each has an associated number of bytes and number of
/// records (encoded in the header).  This structure represents the parsed chunk and
/// it holds the associated records in its `reads` field.
#[derive(Debug, PartialEq)]
pub struct Chunk<T: MappedRecord> {
    pub nbytes: u32,
    pub nrec: u32,
    pub reads: Vec<T>,
}

/// A [CorrectedCbChunk] represents a [Chunk] of RAD records
/// that share the same underlying corrected cell barcode
/// `corrected_bc`.
#[deprecated(
    since = "0.9.1",
    note = "This type is not actually used, and its existence in the library may therefore be \
            confusing. Therefore it is being deprecated and is likelty to be removed in a future \
            release."
)]
#[derive(Debug)]
#[allow(dead_code)]
pub struct CorrectedCbChunk {
    pub(crate) remaining_records: u32,
    pub(crate) corrected_bc: u64,
    pub(crate) nrec: u32,
    pub(crate) data: Cursor<Vec<u8>>,
}

/*
impl CorrectedCbChunk {
    pub fn from_label_and_counter(corrected_bc_in: u64, num_remain: u32) -> CorrectedCbChunk {
        let mut cc = CorrectedCbChunk {
            remaining_records: num_remain,
            corrected_bc: corrected_bc_in,
            nrec: 0u32,
            data: Cursor::new(Vec::<u8>::with_capacity((num_remain * 24) as usize)),
        };
        let dummy = 0u32;
        cc.data.write_all(&dummy.to_le_bytes()).unwrap();
        cc.data.write_all(&dummy.to_le_bytes()).unwrap();
        cc
    }
}
*/

#[deprecated(
    since = "0.9.0",
    note = "This type is deprecated as it's name implies it is general, but it is specalized for the single-cell context. \
            This is replaced more generally by the ChunkContext trait and individual structures implementing this trait \
            For specific RAD file types."
)]
pub struct ChunkConfig {
    pub num_chunks: u64,
    pub bc_type: u8,
    pub umi_type: u8,
}

pub struct ChunkConfigAtac {
    pub num_chunks: u64,
    pub bc_type: u8,
}

pub trait ChunkContext {}

/// An in-memory buffer for accumulating the records of a single RAD chunk,
/// typically in a worker thread. Call [ChunkBuf::write_record] for each record,
/// then [ChunkBuf::into_bytes] to obtain a self-contained byte sequence
/// (chunk header + records) that can be appended to a RAD file via
/// [crate::writers::RadFileWriter::write_chunk_bytes] without any seeking.
pub struct ChunkBuf {
    buf: Vec<u8>,
    nrec: u32,
}

impl Default for ChunkBuf {
    fn default() -> Self {
        Self::new()
    }
}

impl ChunkBuf {
    /// Create a new empty [ChunkBuf].
    pub fn new() -> Self {
        Self {
            buf: Vec::new(),
            nrec: 0,
        }
    }

    /// Create a new [ChunkBuf] with the given initial byte capacity.
    pub fn with_capacity(cap: usize) -> Self {
        Self {
            buf: Vec::with_capacity(cap),
            nrec: 0,
        }
    }

    /// Serialize `rec` into the buffer using `ctx`. Increments the internal record count.
    pub fn write_record<R: MappedRecord>(
        &mut self,
        rec: &R,
        ctx: &R::ParsingContext,
    ) -> anyhow::Result<()> {
        rec.write(&mut self.buf, ctx)?;
        self.nrec += 1;
        Ok(())
    }

    /// Return the number of records accumulated so far.
    pub fn nrec(&self) -> u32 {
        self.nrec
    }

    /// Return the number of record bytes accumulated so far (excluding the chunk header).
    pub fn byte_len(&self) -> usize {
        self.buf.len()
    }

    /// Reset the buffer for reuse without reallocating.
    pub fn clear(&mut self) {
        self.buf.clear();
        self.nrec = 0;
    }

    /// Finalise the chunk: prepend the 4-byte `nbytes` and 4-byte `nrec` header and
    /// return the complete chunk byte sequence.  `nbytes` includes the header itself,
    /// matching the value produced by [Chunk::write].
    pub fn into_bytes(self) -> Vec<u8> {
        let nrec = self.nrec;
        let body = self.buf;
        // nbytes covers the 4-byte nbytes field, the 4-byte nrec field, and all records.
        let nbytes: u32 = (body.len() as u32) + 8;
        let mut result = Vec::with_capacity(body.len() + 8);
        result.extend_from_slice(&nbytes.to_le_bytes());
        result.extend_from_slice(&nrec.to_le_bytes());
        result.extend_from_slice(&body);
        result
    }

    /// Like [`Self::into_bytes`], but the payload is compressed with `codec`.
    /// The returned chunk keeps the `[u32 nbytes][u32 nrec]` header, with
    /// `nbytes` set to the *compressed* framing size (header + compressed
    /// payload). A reader restores it via [`crate::codec::decompress_payload`].
    /// [`ChunkCodec::None`] is identical to [`Self::into_bytes`].
    pub fn into_bytes_with_codec(self, codec: crate::codec::ChunkCodec) -> anyhow::Result<Vec<u8>> {
        use crate::codec::ChunkCodec;
        if codec == ChunkCodec::None {
            return Ok(self.into_bytes());
        }
        let nrec = self.nrec;
        let comp = crate::codec::compress_payload(codec, &self.buf)?;
        let nbytes: u32 = (comp.len() as u32) + 8;
        let mut result = Vec::with_capacity(comp.len() + 8);
        result.extend_from_slice(&nbytes.to_le_bytes());
        result.extend_from_slice(&nrec.to_le_bytes());
        result.extend_from_slice(&comp);
        Ok(result)
    }
}

pub struct AlevinFryChunkContext {
    pub num_chunks: u64,
    pub bc_type: u8,
    pub umi_type: u8,
}

impl ChunkContext for AlevinFryChunkContext {}

impl<R: MappedRecord> Chunk<R> {
    /// Read the header of the next [Chunk] from the provided `reader`. This
    /// function returns a tuple representing the number of bytes and number of
    /// records, respectively, in the chunk.
    #[inline]
    pub fn read_header<T: Read>(reader: &mut T) -> (u32, u32) {
        let mut buf = [0u8; 8];
        reader.read_exact(&mut buf).unwrap();
        let nbytes = buf.pread::<u32>(0).unwrap();
        let nrec = buf.pread::<u32>(4).unwrap();
        (nbytes, nrec)
    }

    /// Write this chunk to the provided `writer`, which must implement [std::io::Seek].
    /// This is because in order to write the number of bytes in the chunk, we need to know
    /// the size of the final encoding.  Returns Ok(()) on sucess, or propagates any errors
    /// otherwise.
    pub fn write<W: Write + Seek>(
        &self,
        writer: &mut W,
        ctx: &R::ParsingContext,
    ) -> anyhow::Result<()> {
        let dummy_num_bytes = 0_u32;
        let start_pos = writer
            .stream_position()
            .context("couldn't get stream position at start")?;

        writer
            .write_all(&dummy_num_bytes.to_le_bytes())
            .context("couldn't write dummy bytes for chunk")?;

        let nrec = self.reads.len() as u32;
        writer
            .write_all(&nrec.to_le_bytes())
            .context("couldn't write num records for chunk")?;

        for r in &self.reads {
            r.write(writer, ctx).context("couldn't write record")?;
        }
        let end_pos = writer
            .stream_position()
            .context("couldn't get stream position at end")?;
        let nbytes: u32 = (end_pos - start_pos) as u32;
        writer
            .seek(SeekFrom::Current(-(nbytes as i64)))
            .context("couldn't seek to start of chunk")?;
        writer
            .write_all(&nbytes.to_le_bytes())
            .context("couldn't write bytes for chunk")?;

        let seek_fwd = (nbytes as usize) - std::mem::size_of_val(&nbytes);
        writer
            .seek(SeekFrom::Current(seek_fwd as i64))
            .context("couldn't seek to end of chunk")?;
        Ok(())
    }

    /// Read the next [Chunk] from the provided reader and return it.
    #[inline]
    pub fn from_bytes_with_tags<T: Read>(_reader: &mut T, _ctx: &R::ParsingContext) -> Self {
        // think about how best to implement this, and where to store the tags
        // (a) should the tags be part of the record, or stored externally (e.g. in a parallel
        // Vec)?
        // (b) should the tags be read into an "unparsed" structure (e.g. a binary blob) and
        // then parsed on demand, or parsed as they are read here?
        // (c) What's the best mechanism to allow the user to access the tags?
        todo!("Should read and store the optional tags associated with each record.");
    }

    /// Read the next [Chunk] from the provided reader and return it.
    #[inline]
    pub fn from_bytes<T: Read>(reader: &mut T, ctx: &R::ParsingContext) -> Self {
        let (nbytes, nrec) = Self::read_header(reader);
        //println!("parsed chunk header :: nbytes {} {}", nbytes, nrec);
        let mut c = Self {
            nbytes,
            nrec,
            reads: Vec::<R>::with_capacity(nrec as usize),
        };

        for _i in 0..(nrec as usize) {
            c.reads.push(R::from_bytes_with_context(reader, ctx));
        }
        c
    }

    /// Peeks to the first [libradicl::record::AlevinFryReadRecord] in the buffer `buf`, and returns
    /// the barcode and umi associated with this record.  It is assumed
    /// that there is at least one [libradicl::record::AlevinFryReadRecord] present in the buffer.
    #[inline]
    pub fn peek_record(buf: &[u8], ctx: &R::ParsingContext) -> R::PeekResult {
        R::peek_record(buf, ctx)
    }
}

#[cfg(test)]
mod tests {
    use crate::chunk::Chunk;
    use crate::header::{RadHeader, RadPrelude};
    use crate::rad_types::{RadIntId, TagMap, TagSection, TagSectionLabel, TagValue};
    use crate::rad_types::{RadType, TagDesc};
    use crate::record::{AlevinFryReadRecord, AlevinFryRecordContext, RecordContext};
    use std::io::Cursor;

    #[test]
    fn can_write_af_chunk() {
        let rec = AlevinFryReadRecord {
            bc: 12345_u64,
            umi: 6789_u64,
            dirs: vec![true, true, true, false],
            refs: vec![123, 456, 78, 910],
        };

        let ft = TagSection::new_with_label(TagSectionLabel::FileTags);
        let mut rt = TagSection::new_with_label(TagSectionLabel::ReadTags);
        rt.add_tag_desc(TagDesc {
            name: "b".to_string(),
            typeid: RadType::Int(RadIntId::U32),
        });
        rt.add_tag_desc(TagDesc {
            name: "u".to_string(),
            typeid: RadType::Int(RadIntId::U32),
        });
        let at = TagSection::new_with_label(TagSectionLabel::AlignmentTags);

        let ctx = AlevinFryRecordContext::get_context_from_tag_section(&ft, &rt, &at).unwrap();

        let chunk = Chunk::<AlevinFryReadRecord> {
            nbytes: 148_u32,
            nrec: 5_u32,
            reads: vec![rec; 5],
        };

        let buf: Vec<u8> = Vec::new();
        let mut cursor = Cursor::new(buf);
        chunk
            .write(&mut cursor, &ctx)
            .expect("couldn't write chunk");
        chunk
            .write(&mut cursor, &ctx)
            .expect("couldn't write chunk");

        cursor.set_position(0);
        let new_chunk = Chunk::<AlevinFryReadRecord>::from_bytes(&mut cursor, &ctx);
        let new_chunk2 = Chunk::<AlevinFryReadRecord>::from_bytes(&mut cursor, &ctx);

        assert_eq!(chunk, new_chunk);
        assert_eq!(chunk, new_chunk2);
    }

    #[test]
    fn can_write_af_file() {
        // mock the header
        let hdr = RadHeader {
            is_paired: 0,
            ref_count: 3,
            ref_names: vec!["tgt1".to_string(), "tgt2".to_string(), "tgt3".to_string()],
            num_chunks: 2,
        };

        // describe the barcode and UMI length tags
        let bc_desc = TagDesc {
            name: "bclen".to_string(),
            typeid: RadType::Int(RadIntId::U16),
        };
        let umi_desc = TagDesc {
            name: "umilen".to_string(),
            typeid: RadType::Int(RadIntId::U16),
        };
        let mut file_tags = TagSection::new_with_label(TagSectionLabel::FileTags);
        file_tags.add_tag_desc(bc_desc);
        file_tags.add_tag_desc(umi_desc);

        // per-read barcode and umi encoding
        let rd_bc = TagDesc {
            name: "b".to_string(),
            typeid: RadType::Int(RadIntId::U32),
        };
        let rd_umi = TagDesc {
            name: "u".to_string(),
            typeid: RadType::Int(RadIntId::U32),
        };
        let mut read_tags = TagSection::new_with_label(TagSectionLabel::ReadTags);
        read_tags.add_tag_desc(rd_bc);
        read_tags.add_tag_desc(rd_umi);

        // per alignment information
        let aln_ent = TagDesc {
            name: "compressed_ori_refid".to_string(),
            typeid: RadType::Int(RadIntId::U32),
        };
        let mut aln_tags = TagSection::new_with_label(TagSectionLabel::AlignmentTags);
        aln_tags.add_tag_desc(aln_ent);

        // create the whole prelude
        let prelude = RadPrelude {
            hdr,
            file_tags,
            read_tags,
            aln_tags,
        };

        // create the buffer we will write into
        let buf: Vec<u8> = Vec::new();
        let mut cursor = Cursor::new(buf);

        // write the prelude
        prelude
            .write(&mut cursor)
            .expect("cannot write prelude to buffer");

        // create and write the file tag map
        // barcode length 16, umi length 12
        let mut file_tag_map = TagMap::with_keyset(&prelude.file_tags.tags);
        file_tag_map.add(TagValue::U16(16));
        file_tag_map.add(TagValue::U16(12));
        file_tag_map
            .write_values(&mut cursor)
            .expect("cannot write file tag map");

        // the record that will comprise our chunks
        let rec = AlevinFryReadRecord {
            bc: 12345_u64,
            umi: 6789_u64,
            dirs: vec![true, true, true, false],
            refs: vec![123, 456, 78, 910],
        };

        let ctx = AlevinFryRecordContext::get_context_from_tag_section(
            &prelude.file_tags,
            &prelude.read_tags,
            &prelude.aln_tags,
        )
        .unwrap();
        let chunk = Chunk::<AlevinFryReadRecord> {
            nbytes: 148_u32,
            nrec: 5_u32,
            reads: vec![rec; 5],
        };

        // write the same chunk twice to ensure we're computing the
        // chunk number of bytes and offsets correctly
        chunk
            .write(&mut cursor, &ctx)
            .expect("couldn't write chunk");
        chunk
            .write(&mut cursor, &ctx)
            .expect("couldn't write chunk");

        // set to the start of the buffer
        cursor.set_position(0);

        // read in the prelude, the tag map and the chunks
        let new_prelude =
            RadPrelude::from_bytes(&mut cursor).expect("cannot read prelude from buffer");

        let new_file_tag_map = &prelude
            .file_tags
            .try_parse_tags_from_bytes(&mut cursor)
            .expect("cannot read file TagMap");

        println!("new_prelude = {}", new_prelude.summary(None).unwrap());
        println!("new_file_tag_map = {:?}", new_file_tag_map);

        assert_eq!(prelude, new_prelude);
        assert_eq!(&file_tag_map, new_file_tag_map);

        let new_chunk = Chunk::<AlevinFryReadRecord>::from_bytes(&mut cursor, &ctx);
        let new_chunk2 = Chunk::<AlevinFryReadRecord>::from_bytes(&mut cursor, &ctx);

        assert_eq!(chunk, new_chunk);
        assert_eq!(chunk, new_chunk2);
    }
}