11use 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+
268327fn 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" ]
443480mod pileup;
481+ #[ path = "bam_backend/query.rs" ]
482+ mod query;
444483
445484use 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} ;
0 commit comments