use std::borrow::Borrow;
use rust_htslib::bcf::Record;
pub use crate::common::cluster_settings::ClusteringSettings;
impl ClusteringSettings {
pub fn belongs<R: Borrow<Record>>(
&self,
last: Option<R>,
candidate: R,
) -> anyhow::Result<bool> {
let Some(last) = last else {
return Ok(true);
};
Ok(candidate.borrow().pos().saturating_sub(last.borrow().end())
<= self.max_gap.try_into()?)
}
pub fn is_cluster<R, F: Fn(&R) -> &Record, G: Fn(&R) -> Option<u32>>(
&self,
cluster: &[R],
get_record: F,
get_allele: G,
) -> bool {
if cluster.is_empty() {
return false;
}
let first = get_record(cluster.first().unwrap().borrow());
let last = get_record(cluster.last().unwrap().borrow());
let l_ref = first.pos().abs_diff(last.end()) as f64;
let (mass, net, count) = cluster
.iter()
.map(|r| (get_record(r), get_allele(r)))
.filter_map(|(r, a)| self.chosen_allele_lens(r, a))
.fold((0.0, 0.0, 0), |(mass, net, count), (ref_len, alt_len)| {
(
mass + self.event_mass(ref_len, alt_len),
net + alt_len as f64 - ref_len as f64,
count + 1,
)
});
let span = self.span(l_ref, l_ref + net);
self.is_valid_cluster(mass, span, count)
}
fn chosen_allele_lens(&self, record: &Record, allele: Option<u32>) -> Option<(usize, usize)> {
let alls = record.alleles();
if alls.len() < 2 {
return None;
}
let ref_len = alls[0].len();
let alt_len = if let Some(a) = allele {
alls.get(a as usize)?.len()
} else {
alls[1..]
.iter()
.map(|a| a.len())
.max_by(|x, y| {
self.event_mass(ref_len, *x)
.partial_cmp(&self.event_mass(ref_len, *y))
.unwrap()
})
.unwrap()
};
Some((ref_len, alt_len))
}
}
#[cfg(test)]
mod tests {
use rust_htslib::bcf::{self, Header, Writer, record::GenotypeAllele};
use crate::common::cluster_settings::{ClusterStrategy, ClusteringSettings};
fn make_writer() -> Writer {
let mut header = Header::new();
header.push_record(b"##contig=<ID=chr1,length=1000000>");
header.push_record(b"##FORMAT=<ID=GT,Number=1,Type=String,Description=\"Genotype\">");
header.push_sample(b"S1");
Writer::from_path("/dev/null", &header, true, bcf::Format::Vcf).unwrap()
}
fn make_record(writer: &Writer, pos: i64, alleles: &[&[u8]]) -> bcf::Record {
let mut rec = writer.empty_record();
let rid = rec.header().name2rid(b"chr1").unwrap();
rec.set_rid(Some(rid));
rec.set_pos(pos);
rec.set_alleles(alleles).unwrap();
rec.push_genotypes(&[GenotypeAllele::Unphased(0), GenotypeAllele::Unphased(1)])
.unwrap();
rec
}
fn is_cluster(records: &[bcf::Record], settings: &ClusteringSettings) -> bool {
settings.is_cluster(records, |r| r, |_| None)
}
#[test]
fn legacy_span_is_the_reference_extent() {
let w = make_writer();
let settings = ClusteringSettings::default();
let exact = [
make_record(&w, 100, &[b"A", b"T"]),
make_record(&w, 109, &[b"A", b"T"]),
];
assert!(is_cluster(&exact, &settings));
let too_sparse = [
make_record(&w, 100, &[b"A", b"T"]),
make_record(&w, 110, &[b"A", b"T"]),
];
assert!(!is_cluster(&too_sparse, &settings));
}
#[test]
fn edit_mass_span_follows_the_query_extent() {
let w = make_writer();
let alt = vec![b'A'; 21];
let insertion = [make_record(&w, 100, &[b"A", &alt])];
assert!(is_cluster(
&insertion,
&ClusteringSettings {
strategy: ClusterStrategy::EditMass,
..Default::default()
}
));
assert!(!is_cluster(&insertion, &ClusteringSettings::default()));
}
#[test]
fn record_without_alt_allele_does_not_count_as_an_event() {
let w = make_writer();
let mut ref_only = w.empty_record();
let rid = ref_only.header().name2rid(b"chr1").unwrap();
ref_only.set_rid(Some(rid));
ref_only.set_pos(101);
ref_only.set_alleles(&[b"A"]).unwrap();
let records = [make_record(&w, 100, &[b"A", b"T"]), ref_only];
assert!(!is_cluster(&records, &ClusteringSettings::default()));
}
}