use std::path::{Path, PathBuf}; use anyhow::{Ok, Result}; use uuid::Uuid; use crate::{ commands::{ CapturedOutput, Command as JobCommand, LocalBatchRunner, LocalRunner, SbatchRunner, SlurmParams, SlurmRunner, }, config::Config, run, }; use super::{cosmic::Cosmic, gnomad::GnomAD}; pub struct EchtvarJob { pub input_vcf: PathBuf, pub output_vcf: PathBuf, pub config: Config, } impl JobCommand for EchtvarJob { fn cmd(&self) -> String { let sources = self .config .echtvar_sources .iter() .map(|e| format!("-e {e}")) .collect::>() .join(" "); format!( "{echtvar_bin} anno {sources} {input_vcf} {output_vcf}", echtvar_bin = self.config.echtvar_bin, sources = sources, input_vcf = self.input_vcf.display(), output_vcf = self.output_vcf.display() ) } } impl LocalRunner for EchtvarJob {} impl LocalBatchRunner for EchtvarJob {} impl SlurmRunner for EchtvarJob { fn slurm_args(&self) -> Vec { SlurmParams { job_name: Some(format!("echtvar_{}", Uuid::new_v4())), cpus_per_task: Some(1), mem: Some("20G".into()), partition: Some("shortq".into()), gres: None, } .to_args() } } impl SbatchRunner for EchtvarJob { fn slurm_params(&self) -> SlurmParams { SlurmParams { job_name: Some(format!("echtvar_{}", Uuid::new_v4())), cpus_per_task: Some(1), mem: Some("20G".into()), partition: Some("shortq".into()), gres: None, } } } pub fn run_echtvar( in_path: impl AsRef, output_vcf: impl AsRef, config: &Config, ) -> Result { let mut job = EchtvarJob { input_vcf: in_path.as_ref().into(), output_vcf: output_vcf.as_ref().into(), config: config.clone(), }; run!(config, &mut job) } pub fn parse_echtvar_val(s: &str) -> Result<(Option, Option)> { let mut cosmic_parts = Vec::new(); let mut gnomad_parts = Vec::new(); for part in s.split(';').map(str::trim).filter(|p| !p.is_empty()) { if part.starts_with("gnomad_") { gnomad_parts.push(part); } else { cosmic_parts.push(part); } } // COSMIC: missing if CNT is absent or CNT is -1 or MISSING let cosmic = { let cnt = cosmic_parts.iter().find(|p| p.starts_with("CNT=")).copied(); match cnt { Some("CNT=-1") | Some("CNT=MISSING") | None => None, Some(_) => Some(cosmic_parts.join(";").parse::()?), } }; // gnomAD: your choice; this keeps your "all -1 => None" behavior let gnomad = if gnomad_parts.is_empty() || gnomad_parts.iter().all(|p| p.ends_with("=-1")) { None } else { Some(gnomad_parts.join(";").parse::()?) }; Ok((cosmic, gnomad)) } #[cfg(test)] mod tests { use super::*; use crate::{ annotation::{Annotation, Annotations, Caller, Sample}, helpers::test_init, variant::{variant_collection::ExternalAnnotation, vcf_variant::VcfVariant}, }; #[test] fn echtvar_parse() -> anyhow::Result<()> { test_init(); let s = "gnomad_ac=1;gnomad_an=-1;gnomad_af=-1;gnomad_af_oth=-1;gnomad_af_ami=-1;gnomad_af_sas=-1;gnomad_af_fin=-1;gnomad_af_eas=-1;gnomad_af_amr=-1;gnomad_af_afr=-1;gnomad_af_mid=-1;gnomad_af_asj=-1;gnomad_af_nfe=-1;CNT=188"; let (cosmic, _gnomad) = parse_echtvar_val(s)?; println!("{cosmic:#?}"); Ok(()) } #[test] fn echtvar_run() -> anyhow::Result<()> { test_init(); let config = Config::default(); let variants: Vec = vec![ "chr12\t25116560\t.\tC\tG\t18.2\tPASS\t.\tGT:GQ:DP:AD:VAF:PL\t1/1:18:45:37,7:0.155556:18,25,0".parse()?, "chr1\t5619\trs1470452993\tA\tC \t.\tPASS\t.\t.\t.".parse()? ]; let annotations = Annotations::default(); let caller = Annotation::Callers(Caller::ClairS, Sample::Somatic); variants.iter().for_each(|v| { annotations.insert_update(v.hash(), std::slice::from_ref(&caller)); }); let ext_annot = ExternalAnnotation::init("TEST", &config)?; ext_annot.annotate(&variants, &annotations)?; println!("{annotations:#?}"); Ok(()) } }