Skip to content

Commit af963ed

Browse files
authored
Merge pull request #71 from OpenMined/madhava/reporting-fixes2
Madhava/reporting fixes2
2 parents ae2be9e + 9639b89 commit af963ed

32 files changed

Lines changed: 1367 additions & 145 deletions

File tree

rust/bioscript-core/src/variant.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub struct VariantSpec {
3131
pub grch38_assembly_ref: Option<String>,
3232
pub reference: Option<String>,
3333
pub alternate: Option<String>,
34+
pub observed_alternates: Vec<String>,
3435
pub kind: Option<VariantKind>,
3536
pub deletion_length: Option<usize>,
3637
pub motifs: Vec<String>,

rust/bioscript-formats/src/alignment.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ pub fn for_each_raw_cram_record_with_reader<R, F>(
140140
) -> Result<(), RuntimeError>
141141
where
142142
R: Read + Seek,
143-
F: FnMut(cram::Record<'_>) -> Result<bool, RuntimeError>,
143+
F: FnMut(&cram::Record<'_>) -> Result<bool, RuntimeError>,
144144
{
145145
for_each_raw_cram_record_with_reader_inner(reader, label, locus, true, on_record)
146146
}

rust/bioscript-formats/src/alignment/cram_stream.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub(crate) fn for_each_raw_cram_record_with_reader_inner<R, F>(
7474
) -> Result<(), RuntimeError>
7575
where
7676
R: Read + Seek,
77-
F: FnMut(cram::Record<'_>) -> Result<bool, RuntimeError>,
77+
F: FnMut(&cram::Record<'_>) -> Result<bool, RuntimeError>,
7878
{
7979
// Re-seeks to position 0 before reading the header so this helper is
8080
// idempotent across repeated calls on the same indexed reader (e.g. a
@@ -178,7 +178,7 @@ where
178178
selected_containers,
179179
allow_reference_md5_mismatch,
180180
&mut |record| {
181-
let alignment_record = build_alignment_record_from_cram(label, &record)?;
181+
let alignment_record = build_alignment_record_from_cram(label, record)?;
182182
on_record(alignment_record)
183183
},
184184
)
@@ -196,7 +196,7 @@ fn stream_selected_cram_records<R, F>(
196196
) -> Result<(), RuntimeError>
197197
where
198198
R: Read + Seek,
199-
F: FnMut(cram::Record<'_>) -> Result<bool, RuntimeError>,
199+
F: FnMut(&cram::Record<'_>) -> Result<bool, RuntimeError>,
200200
{
201201
let interval = region.interval();
202202

@@ -325,7 +325,7 @@ fn handle_decoded_cram_record<F>(
325325
on_record: &mut F,
326326
) -> bool
327327
where
328-
F: FnMut(cram::Record<'_>) -> Result<bool, RuntimeError>,
328+
F: FnMut(&cram::Record<'_>) -> Result<bool, RuntimeError>,
329329
{
330330
let alignment_record = match build_alignment_record_from_cram(label, record) {
331331
Ok(record) => record,
@@ -344,7 +344,7 @@ where
344344
return true;
345345
}
346346

347-
match on_record(record.clone()) {
347+
match on_record(record) {
348348
Ok(true) => true,
349349
Ok(false) => {
350350
*stop = true;

rust/bioscript-formats/src/genotype.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ mod tests {
396396
grch38_assembly_ref: None,
397397
reference: Some("A".to_owned()),
398398
alternate: Some("G".to_owned()),
399+
observed_alternates: vec!["G".to_owned()],
399400
kind: Some(VariantKind::Snp),
400401
deletion_length: None,
401402
motifs: Vec::new(),
@@ -1530,6 +1531,7 @@ mod tests {
15301531
&locus("chr_test", 1000, 1000),
15311532
"A",
15321533
"AT",
1534+
&[2],
15331535
Some("mini_indel".to_owned()),
15341536
Some(Assembly::Grch38),
15351537
)

rust/bioscript-formats/src/genotype/alignment_bytes.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,14 @@ impl AlignmentBytesBackend {
138138
VariantKind::Insertion | VariantKind::Indel => {
139139
let reference = variant.reference.clone().unwrap_or_else(|| "I".to_owned());
140140
let alternate = variant.alternate.clone().unwrap_or_else(|| "D".to_owned());
141+
let alternate_lengths = indel_alternate_lengths(variant, &alternate);
141142
observe_cram_indel_with_reader(
142143
reader,
143144
LABEL,
144145
&locus,
145146
&reference,
146147
&alternate,
148+
&alternate_lengths,
147149
matched_rsid,
148150
Some(assembly),
149151
)
@@ -185,6 +187,21 @@ impl AlignmentBytesBackend {
185187
}
186188
}
187189

190+
fn indel_alternate_lengths(variant: &VariantSpec, fallback_alternate: &str) -> Vec<usize> {
191+
let mut lengths = variant
192+
.observed_alternates
193+
.iter()
194+
.map(String::len)
195+
.filter(|len| *len > 0)
196+
.collect::<Vec<_>>();
197+
if lengths.is_empty() {
198+
lengths.push(fallback_alternate.len());
199+
}
200+
lengths.sort_unstable();
201+
lengths.dedup();
202+
lengths
203+
}
204+
188205
fn first_base(value: Option<&str>) -> Option<char> {
189206
value.and_then(|s| s.chars().next())
190207
}

rust/bioscript-formats/src/genotype/bam_backend.rs

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
collections::BTreeMap,
2+
collections::{BTreeMap, BTreeSet},
33
fs::File,
44
io::{Read, Seek},
55
};
@@ -113,6 +113,7 @@ pub fn observe_bam_variant<R: Read + Seek>(
113113
&locus,
114114
ref_char,
115115
alt_char,
116+
&variant.observed_alternates,
116117
variant.rsids.first().cloned(),
117118
assembly,
118119
)
@@ -133,12 +134,14 @@ pub fn observe_bam_variant<R: Read + Seek>(
133134
variant.rsids.first().map_or("variant", String::as_str)
134135
))
135136
})?;
137+
let alternate_lengths = indel_alternate_lengths(variant, alternate);
136138
observe_bam_indel_with_reader(
137139
reader,
138140
label,
139141
&locus,
140142
reference,
141143
alternate,
144+
&alternate_lengths,
142145
variant.rsids.first().cloned(),
143146
assembly,
144147
)
@@ -156,7 +159,8 @@ fn observe_bam_snp_with_reader<R: Read + Seek>(
156159
label: &str,
157160
locus: &GenomicLocus,
158161
reference: char,
159-
alternate: char,
162+
mut alternate: char,
163+
observed_alternates: &[String],
160164
matched_rsid: Option<String>,
161165
assembly: Option<Assembly>,
162166
) -> Result<VariantObservation, RuntimeError> {
@@ -242,6 +246,14 @@ fn observe_bam_snp_with_reader<R: Read + Seek>(
242246
}
243247
}
244248

249+
alternate = select_observed_snp_alternate(
250+
reference,
251+
alternate,
252+
observed_alternates,
253+
&counts.filtered_base_counts,
254+
&counts.raw_base_counts,
255+
);
256+
recount_bam_snp_counts(&mut counts, reference, alternate);
245257
let ref_count = counts.filtered_ref_count;
246258
let alt_count = counts.filtered_alt_count;
247259
let depth = counts.filtered_depth;
@@ -265,6 +277,53 @@ fn observe_bam_snp_with_reader<R: Read + Seek>(
265277
})
266278
}
267279

280+
fn select_observed_snp_alternate(
281+
reference: char,
282+
preferred_alternate: char,
283+
observed_alternates: &[String],
284+
filtered_base_counts: &BTreeMap<String, u32>,
285+
raw_base_counts: &BTreeMap<String, u32>,
286+
) -> char {
287+
let preferred_alternate = preferred_alternate.to_ascii_uppercase();
288+
let reference = reference.to_ascii_uppercase();
289+
let mut candidates = BTreeSet::from([preferred_alternate]);
290+
candidates.extend(
291+
observed_alternates
292+
.iter()
293+
.filter_map(|alt| alt.trim().chars().next())
294+
.map(|alt| alt.to_ascii_uppercase())
295+
.filter(|alt| *alt != reference),
296+
);
297+
candidates
298+
.into_iter()
299+
.max_by_key(|candidate| {
300+
let key = candidate.to_string();
301+
(
302+
filtered_base_counts.get(&key).copied().unwrap_or(0),
303+
raw_base_counts.get(&key).copied().unwrap_or(0),
304+
u8::from(*candidate == preferred_alternate),
305+
)
306+
})
307+
.unwrap_or(preferred_alternate)
308+
}
309+
310+
fn recount_bam_snp_counts(counts: &mut BamSnpPileupCounts, reference: char, alternate: char) {
311+
let reference = reference.to_ascii_uppercase().to_string();
312+
let alternate = alternate.to_ascii_uppercase().to_string();
313+
counts.filtered_ref_count = counts
314+
.filtered_base_counts
315+
.get(&reference)
316+
.copied()
317+
.unwrap_or(0);
318+
counts.filtered_alt_count = counts
319+
.filtered_base_counts
320+
.get(&alternate)
321+
.copied()
322+
.unwrap_or(0);
323+
counts.raw_ref_count = counts.raw_base_counts.get(&reference).copied().unwrap_or(0);
324+
counts.raw_alt_count = counts.raw_base_counts.get(&alternate).copied().unwrap_or(0);
325+
}
326+
268327
fn observe_bam_deletion_with_reader<R: Read + Seek>(
269328
reader: &mut noodles::bam::io::indexed_reader::IndexedReader<noodles::bgzf::io::Reader<R>>,
270329
label: &str,
@@ -334,6 +393,7 @@ fn observe_bam_indel_with_reader<R: Read + Seek>(
334393
locus: &GenomicLocus,
335394
reference: &str,
336395
alternate: &str,
396+
alternate_lengths: &[usize],
337397
matched_rsid: Option<String>,
338398
assembly: Option<Assembly>,
339399
) -> Result<VariantObservation, RuntimeError> {
@@ -354,8 +414,12 @@ fn observe_bam_indel_with_reader<R: Read + Seek>(
354414
if alignment_record.is_unmapped || !record_overlaps_locus(&alignment_record, locus) {
355415
continue;
356416
}
357-
let classification =
358-
classify_expected_indel(&alignment_record, locus, reference.len(), alternate)?;
417+
let classification = classify_expected_indel_lengths(
418+
&alignment_record,
419+
locus,
420+
reference.len(),
421+
alternate_lengths,
422+
)?;
359423
if !classification.covering {
360424
continue;
361425
}
@@ -397,54 +461,30 @@ fn observe_bam_indel_with_reader<R: Read + Seek>(
397461
})
398462
}
399463

400-
fn read_bam_header<R: Read + Seek>(
401-
reader: &mut noodles::bam::io::indexed_reader::IndexedReader<noodles::bgzf::io::Reader<R>>,
402-
label: &str,
403-
) -> Result<noodles::sam::Header, RuntimeError> {
404-
reader
405-
.get_mut()
406-
.seek(noodles::bgzf::VirtualPosition::MIN)
407-
.map_err(|err| RuntimeError::Io(format!("failed to rewind BAM {label}: {err}")))?;
408-
reader
409-
.read_header()
410-
.map_err(|err| RuntimeError::Io(format!("failed to read BAM header {label}: {err}")))
411-
}
412-
413-
fn bam_region(
414-
header: &noodles::sam::Header,
415-
locus: &GenomicLocus,
416-
) -> Result<noodles::core::Region, RuntimeError> {
417-
let chrom = resolve_bam_reference_name(header, &locus.chrom).ok_or_else(|| {
418-
RuntimeError::Unsupported(format!(
419-
"indexed BAM does not contain contig {} for {}:{}-{}",
420-
locus.chrom, locus.chrom, locus.start, locus.end
421-
))
422-
})?;
423-
format!("{chrom}:{}-{}", locus.start, locus.end)
424-
.parse()
425-
.map_err(|err| RuntimeError::Io(format!("invalid BAM query region: {err}")))
426-
}
427-
428-
fn resolve_bam_reference_name(header: &noodles::sam::Header, chrom: &str) -> Option<String> {
429-
let candidates = [
430-
chrom.to_owned(),
431-
format!("chr{chrom}"),
432-
chrom.trim_start_matches("chr").to_owned(),
433-
];
434-
candidates.into_iter().find(|candidate| {
435-
header.reference_sequences().iter().any(|(name, _)| {
436-
let name_bytes: &[u8] = name.as_ref();
437-
name_bytes == candidate.as_bytes()
438-
})
439-
})
464+
fn indel_alternate_lengths(variant: &VariantSpec, fallback_alternate: &str) -> Vec<usize> {
465+
let mut lengths = variant
466+
.observed_alternates
467+
.iter()
468+
.map(String::len)
469+
.filter(|len| *len > 0)
470+
.collect::<Vec<_>>();
471+
if lengths.is_empty() {
472+
lengths.push(fallback_alternate.len());
473+
}
474+
lengths.sort_unstable();
475+
lengths.dedup();
476+
lengths
440477
}
441478

442479
#[path = "bam_backend/pileup.rs"]
443480
mod pileup;
481+
#[path = "bam_backend/query.rs"]
482+
mod query;
444483

445484
use pileup::{
446485
BamSnpPileupCounts, bam_alignment_record, bam_base_quality_at_reference_position,
447-
classify_expected_indel, describe_copy_number_decision_rule, describe_snp_decision_rule,
448-
indel_at_anchor, infer_copy_number_genotype, infer_snp_genotype, normalize_pileup_base,
449-
record_overlaps_locus, spans_position,
486+
classify_expected_indel_lengths, describe_copy_number_decision_rule,
487+
describe_snp_decision_rule, indel_at_anchor, infer_copy_number_genotype, infer_snp_genotype,
488+
normalize_pileup_base, record_overlaps_locus, spans_position,
450489
};
490+
use query::{bam_region, read_bam_header};

rust/bioscript-formats/src/genotype/bam_backend/pileup.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,22 @@ pub(super) fn indel_at_anchor(
193193
None
194194
}
195195

196+
#[allow(dead_code)]
196197
pub(super) fn classify_expected_indel(
197198
record: &AlignmentRecord,
198199
locus: &GenomicLocus,
199200
reference_len: usize,
200201
alternate: &str,
201202
) -> Result<IndelClassification, RuntimeError> {
202-
let alt_len = alternate.len();
203+
classify_expected_indel_lengths(record, locus, reference_len, &[alternate.len()])
204+
}
205+
206+
pub(super) fn classify_expected_indel_lengths(
207+
record: &AlignmentRecord,
208+
locus: &GenomicLocus,
209+
reference_len: usize,
210+
alternate_lengths: &[usize],
211+
) -> Result<IndelClassification, RuntimeError> {
203212
let anchor_start = locus.start.saturating_sub(1);
204213
let anchor_end = locus.end;
205214

@@ -226,7 +235,7 @@ pub(super) fn classify_expected_indel(
226235
return Ok(IndelClassification {
227236
covering: true,
228237
reference_like: false,
229-
matches_alt: observed_len == alt_len,
238+
matches_alt: alternate_lengths.contains(&observed_len),
230239
observed_len,
231240
});
232241
}

0 commit comments

Comments
 (0)