Jelajahi Sumber

VCF loader, new and better struct Variant, deepvariant loader

Thomas 1 tahun lalu
induk
melakukan
ba66e761f7

File diff ditekan karena terlalu besar
+ 199 - 182
Cargo.lock


+ 1 - 0
Cargo.toml

@@ -43,3 +43,4 @@ full = "0.3.0"
 rust-htslib = "0.49.0"
 podders = "0.1.4"
 arrow = "53.3.0"
+bgzip = "0.3.1"

+ 22 - 23
src/callers/clairs.rs

@@ -182,31 +182,30 @@ impl ClairS {
                 .context(format!("Error while writing logs into {log_file}"))?;
         }
 
-
-        let bam = Path::new(&self.diag_bam);
+        // let bam = Path::new(&self.diag_bam);
         // let new_fn = format!("{}_hp.bam", bam.file_stem().unwrap().to_str().unwrap());
         // let bam_hp = bam.with_file_name(new_fn);
-        LongphasePhase::new(
-            &self.id,
-            bam.to_str().unwrap(),
-            &germline_normal_tumor,
-            LongphaseConfig::default(),
-        )?
-        .run()?;
-        LongphaseHap::new(
-            &self.id,
-            &self.diag_bam,
-            &format!("{}/clair3_normal_tumoral_germline_output_PS.vcf.gz", self.output_dir),
-            LongphaseConfig::default(),
-        )
-            .run()?;
-        LongphaseHap::new(
-            &self.id,
-            &self.mrd_bam,
-            &format!("{}/clair3_normal_tumoral_germline_output_PS.vcf.gz", self.output_dir),
-            LongphaseConfig::default(),
-        )
-            .run()?;
+        // LongphasePhase::new(
+        //     &self.id,
+        //     bam.to_str().unwrap(),
+        //     &germline_normal_tumor,
+        //     LongphaseConfig::default(),
+        // )?
+        // .run()?;
+        // LongphaseHap::new(
+        //     &self.id,
+        //     &self.diag_bam,
+        //     &format!("{}/clair3_normal_tumoral_germline_output_PS.vcf.gz", self.output_dir),
+        //     LongphaseConfig::default(),
+        // )
+        //     .run()?;
+        // LongphaseHap::new(
+        //     &self.id,
+        //     &self.mrd_bam,
+        //     &format!("{}/clair3_normal_tumoral_germline_output_PS.vcf.gz", self.output_dir),
+        //     LongphaseConfig::default(),
+        // )
+        //     .run()?;
         Ok(())
     }
 }

+ 161 - 40
src/callers/deep_variant.rs

@@ -3,8 +3,13 @@ use log::info;
 use std::{fs, path::Path};
 
 use crate::{
+    collection::InitializeSolo,
     commands::bcftools::{bcftools_keep_pass, BcftoolsConfig},
-    runners::{run_wait, DockerRun},
+    config::Config,
+    helpers::{force_or_not, path_prefix},
+    io::vcf::read_vcf,
+    runners::{run_wait, DockerRun, Run},
+    variant::variant::{Annotation, Variants},
 };
 
 #[derive(Debug, Clone)]
@@ -21,7 +26,7 @@ pub struct DeepVariantConfig {
 impl Default for DeepVariantConfig {
     fn default() -> Self {
         Self {
-            bin_version: "1.6.1".to_string(),
+            bin_version: "1.8.0".to_string(),
             threads: 155,
             model_type: "ONT_R104".to_string(),
             result_dir: "/data/longreads_basic_pipe".to_string(),
@@ -35,50 +40,57 @@ impl Default for DeepVariantConfig {
 #[derive(Debug)]
 pub struct DeepVariant {
     pub id: String,
-    pub time_point: String,
+    pub time: String,
     pub bam: String,
     pub output_dir: String,
     pub output_vcf: String,
     pub vcf_passed: String,
-    pub log: String,
     pub log_dir: String,
-    pub config: DeepVariantConfig,
+    pub config: Config,
 }
 
-impl DeepVariant {
-    pub fn new(id: &str, time_point: &str, bam: &str, config: DeepVariantConfig) -> Self {
-        let output_dir = format!("{}/{}/{}/DeepVariant", config.result_dir, id, time_point);
-        let output_vcf = format!("{output_dir}/{}_{}_DeepVariant.vcf.gz", id, time_point);
-        let vcf_passed = format!(
-            "{}/{}_{}_DeepVariant_PASSED.vcf.gz",
-            output_dir, id, time_point
-        );
-        let log_dir = format!("{}/{}/log/DeepVariant", config.result_dir, id);
+impl InitializeSolo for DeepVariant {
+    fn initialize(id: &str, time: &str, config: Config) -> anyhow::Result<Self> {
+        let id = id.to_string();
+        let time = time.to_string();
 
-        Self {
-            id: id.to_string(),
-            time_point: time_point.to_string(),
-            bam: bam.to_string(),
-            config,
-            log: String::default(),
+        let log_dir = format!("{}/{}/log/deepvariant", config.result_dir, &id);
+        if !Path::new(&log_dir).exists() {
+            fs::create_dir_all(&log_dir)
+                .context(format!("Failed  to create {log_dir} directory"))?;
+        }
+
+        let bam = config.solo_bam(&id, &time);
+        if !Path::new(&bam).exists() {
+            anyhow::bail!("Bam files doesn't exists: {bam}")
+        }
+
+        let output_vcf = config.deepvariant_output_vcf(&id, &time);
+        let output_dir = config.deepvariant_output_dir(&id, &time);
+        let vcf_passed = format!("{}_PASSED.vcf.gz", path_prefix(&output_vcf)?);
+        fs::create_dir_all(&output_dir).context(format!("Can't create dir: {output_dir}"))?;
+
+        Ok(Self {
+            id,
+            time,
+            bam,
             output_dir,
             output_vcf,
             vcf_passed,
             log_dir,
-        }
+            config,
+        })
     }
+}
 
-    pub fn run(&self) -> anyhow::Result<()> {
-        if self.config.force && Path::new(&self.output_vcf).exists() {
-            fs::remove_dir_all(&self.output_dir)?;
-        }
-        // Create out dir
+impl Run for DeepVariant {
+    fn run(&mut self) -> anyhow::Result<()> {
+        force_or_not(&self.vcf_passed, self.config.deepvariant_force)?;
         if !Path::new(&self.output_dir).exists() {
-            fs::create_dir_all(&self.output_dir).expect("Failed to create output directory");
-        }
-
-        if !Path::new(&self.log_dir).exists() {
-            fs::create_dir_all(&self.log_dir).expect("Failed to create log directory");
+            fs::create_dir_all(&self.output_dir).context(format!(
+                "Failed to create output directory: {}",
+                self.output_dir
+            ))?;
         }
 
         // Run Docker command if output VCF doesn't exist
@@ -87,33 +99,34 @@ impl DeepVariant {
                 "run",
                 "-d",
                 "-v",
-                "/data:/data", // <---
+                "/data:/data",
                 "-v",
                 &format!("{}:/output", self.output_dir),
-                &format!("google/deepvariant:{}", self.config.bin_version),
+                &format!("google/deepvariant:{}", self.config.deepvariant_bin_version),
                 "/opt/deepvariant/bin/run_deepvariant",
-                &format!("--model_type={}", self.config.model_type),
+                &format!("--model_type={}", self.config.deepvariant_model_type),
                 "--ref",
                 &self.config.reference,
                 "--reads",
                 &self.bam,
                 "--output_vcf",
-                &format!("/output/{}_{}_DeepVariant.vcf.gz", self.id, self.time_point),
+                &format!("/output/{}_{}_DeepVariant.vcf.gz", self.id, self.time),
                 "--output_gvcf",
                 &format!(
                     "/output/{}_{}_DeepVariant.g.vcf.gz",
-                    self.id, self.time_point
+                    self.id, self.time
                 ),
-                &format!("--num_shards={}", self.config.threads),
+                &format!("--num_shards={}", self.config.deepvariant_threads),
                 "--logging_dir",
-                &format!("/output/{}_{}_DeepVariant_logs", self.id, self.time_point),
+                "--vcf_stats_report=true",
+                &format!("/output/{}_{}_DeepVariant_logs", self.id, self.time),
                 "--dry_run=false",
                 "--sample_name",
-                &format!("{}_{}", self.id, self.time_point),
+                &format!("{}_{}", self.id, self.time),
             ]);
             let report = run_wait(&mut docker_run).context(format!(
                 "Erreur while running DeepVariant for {} {}",
-                self.id, self.time_point
+                self.id, self.time
             ))?;
             report
                 .save_to_file(&format!("{}/deepvariant_", self.log_dir))
@@ -133,6 +146,114 @@ impl DeepVariant {
                 .save_to_file(&format!("{}/bcftools_pass_", self.log_dir))
                 .unwrap();
         }
+
         Ok(())
     }
 }
+
+// impl DeepVariant {
+//     pub fn new(id: &str, time_point: &str, bam: &str, config: DeepVariantConfig) -> Self {
+//         let output_dir = format!("{}/{}/{}/DeepVariant", config.result_dir, id, time_point);
+//         let output_vcf = format!("{output_dir}/{}_{}_DeepVariant.vcf.gz", id, time_point);
+//         let vcf_passed = format!(
+//             "{}/{}_{}_DeepVariant_PASSED.vcf.gz",
+//             output_dir, id, time_point
+//         );
+//         let log_dir = format!("{}/{}/log/DeepVariant", config.result_dir, id);
+//
+//         Self {
+//             id: id.to_string(),
+//             time_point: time_point.to_string(),
+//             bam: bam.to_string(),
+//             config,
+//             log: String::default(),
+//             output_dir,
+//             output_vcf,
+//             vcf_passed,
+//             log_dir,
+//         }
+//     }
+//
+//     pub fn run(&self) -> anyhow::Result<()> {
+//         if self.config.force && Path::new(&self.output_vcf).exists() {
+//             fs::remove_dir_all(&self.output_dir)?;
+//         }
+//         // Create out dir
+//         if !Path::new(&self.output_dir).exists() {
+//             fs::create_dir_all(&self.output_dir).expect("Failed to create output directory");
+//         }
+//
+//         if !Path::new(&self.log_dir).exists() {
+//             fs::create_dir_all(&self.log_dir).expect("Failed to create log directory");
+//         }
+//
+//         // Run Docker command if output VCF doesn't exist
+//         if !Path::new(&self.output_vcf).exists() {
+//             let mut docker_run = DockerRun::new(&[
+//                 "run",
+//                 "-d",
+//                 "-v",
+//                 "/data:/data",
+//                 "-v",
+//                 &format!("{}:/output", self.output_dir),
+//                 &format!("google/deepvariant:{}", self.config.bin_version),
+//                 "/opt/deepvariant/bin/run_deepvariant",
+//                 &format!("--model_type={}", self.config.model_type),
+//                 "--ref",
+//                 &self.config.reference,
+//                 "--reads",
+//                 &self.bam,
+//                 "--output_vcf",
+//                 &format!("/output/{}_{}_DeepVariant.vcf.gz", self.id, self.time_point),
+//                 "--output_gvcf",
+//                 &format!(
+//                     "/output/{}_{}_DeepVariant.g.vcf.gz",
+//                     self.id, self.time_point
+//                 ),
+//                 &format!("--num_shards={}", self.config.threads),
+//                 "--logging_dir",
+//                 "--vcf_stats_report=true",
+//                 &format!("/output/{}_{}_DeepVariant_logs", self.id, self.time_point),
+//                 "--dry_run=false",
+//                 "--sample_name",
+//                 &format!("{}_{}", self.id, self.time_point),
+//             ]);
+//             let report = run_wait(&mut docker_run).context(format!(
+//                 "Erreur while running DeepVariant for {} {}",
+//                 self.id, self.time_point
+//             ))?;
+//             report
+//                 .save_to_file(&format!("{}/deepvariant_", self.log_dir))
+//                 .context("Can't save DeepVariant logs")?;
+//         }
+//
+//         // Keep PASS
+//         if !Path::new(&self.vcf_passed).exists() {
+//             info!("Filtering PASS variants");
+//             let report = bcftools_keep_pass(
+//                 &self.output_vcf,
+//                 &self.vcf_passed,
+//                 BcftoolsConfig::default(),
+//             )
+//             .unwrap();
+//             report
+//                 .save_to_file(&format!("{}/bcftools_pass_", self.log_dir))
+//                 .unwrap();
+//         }
+//         Ok(())
+//     }
+// }
+
+// VCF
+impl Variants for DeepVariant {
+    fn variants(&self) -> anyhow::Result<Vec<crate::variant::variant::Variant>> {
+        let mut annotations = vec![Annotation::Source("DeepVariant".to_string())];
+        match self.time.as_str() {
+            "diag" => annotations.push(Annotation::Diag),
+            "mrd" => annotations.push(Annotation::Constit),
+            _ => anyhow::bail!("Unrecognised time point {}", self.time),
+        }
+
+        read_vcf(&self.vcf_passed, &annotations)
+    }
+}

+ 10 - 20
src/callers/savana.rs

@@ -1,8 +1,5 @@
 use crate::{
-    collection::{HasOutputs, Initialize, Version},
-    commands::bcftools::{bcftools_keep_pass, BcftoolsConfig},
-    config::Config,
-    runners::{run_wait, CommandRun, Run},
+    collection::{HasOutputs, Initialize, Version}, commands::bcftools::{bcftools_keep_pass, BcftoolsConfig}, config::Config, helpers::force_or_not, runners::{run_wait, CommandRun, Run}
 };
 use anyhow::Context;
 use std::{fs, path::Path};
@@ -16,16 +13,6 @@ pub struct Savana {
 
 impl Initialize for Savana {
     fn initialize(id: &str, config: Config) -> anyhow::Result<Self> {
-        let mut output_vcf_exists = Path::new(&config.savana_output_vcf(id)).exists();
-        if config.savana_force && output_vcf_exists {
-            fs::remove_dir_all(config.savana_output_dir(id))?;
-            output_vcf_exists = false;
-        }
-
-        if output_vcf_exists {
-            anyhow::bail!("{} already exists.", config.savana_output_vcf(id))
-        }
-
         let log_dir = format!("{}/{}/log/savana", config.result_dir, id);
         if !Path::new(&log_dir).exists() {
             fs::create_dir_all(&log_dir)
@@ -42,6 +29,8 @@ impl Initialize for Savana {
 
 impl Run for Savana {
     fn run(&mut self) -> anyhow::Result<()> {
+        force_or_not(&self.config.savana_output_vcf(&self.id), self.config.savana_force)?;
+
         let id = &self.id;
         let savana_args = [
             // "run",
@@ -57,7 +46,7 @@ impl Run for Savana {
             &self.config.germline_phased_vcf(id),
             "--no_blacklist",
             "--threads",
-            &self.config.savana_threads.to_string()
+            &self.config.savana_threads.to_string(),
         ];
         let args = [
             "-c",
@@ -117,10 +106,8 @@ impl Version for Savana {
             ),
         ];
         let mut cmd_run = CommandRun::new("bash", &args);
-        let report = run_wait(&mut cmd_run).context(format!(
-            "Error while running `savana {}`",
-            args.join(" ")
-        ))?;
+        let report = run_wait(&mut cmd_run)
+            .context(format!("Error while running `savana {}`", args.join(" ")))?;
         let log = report.log;
         let start = log
             .find("Version ")
@@ -129,7 +116,10 @@ impl Version for Savana {
         let end = log[start_index..]
             .find('\n')
             .context("Failed to find newline after 'Version '")?;
-        Ok(log[start_index..start_index + end].to_string().trim().to_string())
+        Ok(log[start_index..start_index + end]
+            .to_string()
+            .trim()
+            .to_string())
     }
 }
 

+ 3 - 4
src/collection/mod.rs

@@ -32,7 +32,7 @@ use crate::{
         assembler::{Assembler, AssemblerConfig},
         variants::{RunVariantsAgg, VariantsConfig},
         whole_scan::{WholeScan, WholeScanConfig},
-    },
+    }, runners::Run,
 };
 
 pub mod bam;
@@ -785,9 +785,8 @@ impl CollectionsTasks {
             CollectionsTasks::DeepVariant {
                 id,
                 time_point,
-                bam,
-                config,
-            } => DeepVariant::new(&id, &time_point, &bam, config).run(),
+                ..
+            } => DeepVariant::initialize(&id, &time_point, Config::default())?.run(),
             CollectionsTasks::ClairS {
                 id,
                 diag_bam,

+ 4 - 1
src/commands/bcftools.rs

@@ -1,5 +1,5 @@
 use anyhow::Context;
-use std::fs;
+use std::{fs, path::Path};
 use uuid::Uuid;
 
 use crate::runners::{run_wait, CommandRun, RunReport};
@@ -24,6 +24,9 @@ pub fn bcftools_keep_pass(
     output: &str,
     config: BcftoolsConfig,
 ) -> anyhow::Result<RunReport> {
+    if !Path::new(input).exists() {
+        anyhow::bail!("File doesnt exist {input}")
+    }
     // First sort
     let tmp_file = format!("/tmp/{}", Uuid::new_v4());
     let mut cmd_run = CommandRun::new(&config.bin, &["sort", input, "-o", &tmp_file]);

+ 156 - 73
src/commands/longphase.rs

@@ -1,4 +1,6 @@
-use crate::runners::{run_wait, CommandRun};
+use crate::{
+    collection::{Initialize, InitializeSolo}, config::Config, helpers::path_prefix, runners::{run_wait, CommandRun, Run}
+};
 use anyhow::Context;
 use duct::cmd;
 use std::{
@@ -7,7 +9,10 @@ use std::{
 };
 use tracing::info;
 
-use super::bcftools::{bcftools_keep_pass, BcftoolsConfig};
+use super::{
+    bcftools::{bcftools_keep_pass, BcftoolsConfig},
+    modkit::ModkitSummary,
+};
 
 #[derive(Debug, Clone)]
 pub struct LongphaseConfig {
@@ -41,11 +46,11 @@ pub struct LongphaseHap {
 }
 
 impl LongphaseHap {
-    pub fn new(id: &str, bam: &str, vcf: &str, config: LongphaseConfig) -> Self {
+    pub fn new(id: &str, bam: &str, phased_vcf: &str, config: LongphaseConfig) -> Self {
         let log_dir = format!("{}/{}/log/longphase", config.result_dir, id);
 
         let bam = Path::new(bam);
-        let new_fn = format!("{}_hp", bam.file_stem().unwrap().to_str().unwrap());
+        let new_fn = format!("{}_HP", bam.file_stem().unwrap().to_str().unwrap());
         let bam_hp = bam.with_file_name(new_fn);
 
         Self {
@@ -53,7 +58,7 @@ impl LongphaseHap {
             bam: bam.to_path_buf(),
             config,
             log_dir,
-            vcf: vcf.to_string(),
+            vcf: phased_vcf.to_string(),
             bam_hp: bam_hp.to_path_buf(),
         }
     }
@@ -79,6 +84,7 @@ impl LongphaseHap {
                 &self.config.reference,
                 "-t",
                 &self.config.threads.to_string(),
+                "--tagSupplementary",
                 "-o",
                 self.bam_hp.to_str().unwrap(),
             ];
@@ -113,93 +119,170 @@ impl LongphaseHap {
 // /data/tools/longphase_linux-x64 phase -s ClairS/clair3_normal_tumoral_germline_output.vcf.gz -b CUNY_diag_hs1_hp.bam -r /data/ref/hs1/chm13v2.0.fa -t 155 --ont -o ClairS/clair3_normal_tumoral_germline_output_PS
 #[derive(Debug)]
 pub struct LongphasePhase {
-    pub vcf: PathBuf,
-    pub out_prefix: PathBuf,
-    pub bam_hp: String,
-    pub config: LongphaseConfig,
+    pub vcf: String,
+    pub out_prefix: String,
+    pub bam: String,
+    pub config: Config,
     pub log_dir: String,
+    pub modcall_vcf: String,
 }
 
-impl LongphasePhase {
-    pub fn new(id: &str, bam_hp: &str, vcf: &str, config: LongphaseConfig) -> anyhow::Result<Self> {
-        let log_dir = format!("{}/{}/log/longphase", config.result_dir, id);
-
-        let vcf = Path::new(vcf);
-        let mut stem = vcf
-            .file_stem()
-            .and_then(|s| s.to_str())
-            .map(String::from)
-            .context(format!("Can't parse stem for {}", vcf.display()))?;
-        if stem.ends_with(".vcf") {
-            stem = stem.replace(".vcf", "")
+impl Initialize for LongphasePhase {
+    fn initialize(id: &str, config: crate::config::Config) -> anyhow::Result<Self> {
+        let log_dir = format!("{}/{}/log/longphase_phase", config.result_dir, id);
+        if !Path::new(&log_dir).exists() {
+            fs::create_dir_all(&log_dir)
+                .context(format!("Failed  to create {log_dir} directory"))?;
         }
+        let vcf = config.constit_vcf(id);
+        let bam = config.tumoral_bam(id);
+        let out_prefix = path_prefix(&config.constit_phased_vcf(id))?;
+        let modcall_vcf = config.longphase_modcall_vcf(id, "diag");
 
-        let new_file_name = format!("{stem}_PS");
-        let out_prefix = vcf.with_file_name(new_file_name);
-
-        Ok(Self {
-            bam_hp: bam_hp.to_string(),
+        Ok(LongphasePhase {
             config,
             log_dir,
-            vcf: vcf.to_path_buf(),
+            vcf,
             out_prefix,
+            bam,
+            modcall_vcf,
         })
     }
+}
 
-    pub fn run(&mut self) -> anyhow::Result<()> {
-        let uncompressed_vcf = format!("{}.vcf", self.out_prefix.display());
-        let final_vcf = format!("{}.gz", uncompressed_vcf);
+impl Run for LongphasePhase {
+    fn run(&mut self) -> anyhow::Result<()> {
+        let args = [
+            "phase",
+            "-s",
+            &self.vcf,
+            "-b",
+            &self.bam,
+            "-r",
+            &self.config.reference,
+            "--mod-file",
+            &self.modcall_vcf,
+            "-t",
+            &self.config.longphase_threads.to_string(),
+            "--ont",
+            "-o",
+            &self.out_prefix,
+        ];
+        let mut cmd_run = CommandRun::new(&self.config.longphase_bin, &args);
+        let report = run_wait(&mut cmd_run).context(format!(
+            "Error while running `{} {}`",
+            self.config.longphase_bin,
+            args.join(" ")
+        ))?;
 
-        if self.config.force && Path::new(&final_vcf).exists() {
-            fs::remove_file(&final_vcf)?;
+        let log_file = format!("{}/longphase_phase_", self.log_dir);
+        report
+            .save_to_file(&log_file)
+            .context(format!("Error while writing logs into {log_file}"))?;
+        Ok(())
+    }
+}
+
+#[derive(Debug)]
+pub struct LongphaseModcallSolo {
+    pub id: String,
+    pub time: String,
+    pub bam: String,
+    pub prefix: String,
+    pub reference: String,
+    pub threads: u8,
+    pub log_dir: String,
+    pub mod_threshold: f64,
+    pub config: Config,
+}
+
+impl InitializeSolo for LongphaseModcallSolo {
+    fn initialize(id: &str, time: &str, config: Config) -> anyhow::Result<Self> {
+        let id = id.to_string();
+        let time = time.to_string();
+
+        let log_dir = format!("{}/{}/log/longphase_modcall_solo", config.result_dir, &id);
+        if !Path::new(&log_dir).exists() {
+            fs::create_dir_all(&log_dir)
+                .context(format!("Failed  to create {log_dir} directory"))?;
         }
 
-        if !Path::new(&self.log_dir).exists() {
-            fs::create_dir_all(&self.log_dir).expect("Failed to create output directory");
+        let bam = config.solo_bam(&id, &time);
+        if !Path::new(&bam).exists() {
+            anyhow::bail!("Bam files doesn't exists: {bam}")
         }
 
-        // Run command if output VCF doesn't exist
-        if !Path::new(&final_vcf).exists() {
-            let args = [
-                "phase",
-                "-s",
-                self.vcf.to_str().unwrap(),
-                "-b",
-                &self.bam_hp,
-                "-r",
-                &self.config.reference,
-                "-t",
-                &self.config.threads.to_string(),
-                "--ont",
-                "-o",
-                self.out_prefix.to_str().unwrap(),
-            ];
-            let mut cmd_run = CommandRun::new(&self.config.bin, &args);
-            let report = run_wait(&mut cmd_run).context(format!(
-                "Error while running `{} {}`",
-                self.config.bin,
-                args.join(" ")
-            ))?;
+        let mut modkit_summary = ModkitSummary::initialize(&id, &time, config.clone())?;
+        modkit_summary.load()?;
+        let mod_threshold = modkit_summary
+            .result
+            .ok_or_else(|| anyhow::anyhow!("Error no ModkitSummary for {id} {time}"))?
+            .pass_threshold;
 
-            let log_file = format!("{}/longphase_phase_", self.log_dir);
-            report
-                .save_to_file(&log_file)
-                .context(format!("Error while writing logs into {log_file}"))?;
+        let out_vcf = config.longphase_modcall_vcf(&id, &time);
+        let out_dir = Path::new(&out_vcf)
+            .parent()
+            .ok_or_else(|| anyhow::anyhow!("Can't get dir of {out_vcf}"))?;
+        fs::create_dir_all(out_dir)?;
+        let prefix = path_prefix(&out_vcf)?;
 
-            let report =
-                bcftools_keep_pass(&uncompressed_vcf, &final_vcf, BcftoolsConfig::default())
-                    .context(format!(
-                        "Error while running bcftools keep PASS for {}",
-                        &final_vcf
-                    ))?;
-            let log_file = format!("{}/bcftools_pass", self.log_dir);
-            report
-                .save_to_file(&log_file)
-                .context(format!("Error while writing logs into {log_file}"))?;
-        } else {
-            info!("Longphase output vcf already exists");
-        }
+        Ok(Self {
+            id,
+            time,
+            bam,
+            reference: config.reference.to_string(),
+            threads: config.longphase_modcall_threads,
+            config,
+            log_dir,
+            mod_threshold,
+            prefix,
+        })
+    }
+}
+
+impl Run for LongphaseModcallSolo {
+    fn run(&mut self) -> anyhow::Result<()> {
+        let args = [
+            "modcall",
+            "-b",
+            &self.bam,
+            "-t",
+            &self.threads.to_string(),
+            "-r",
+            &self.reference,
+            "-m",
+            &self.mod_threshold.to_string(),
+            "-o",
+            &self.prefix,
+        ];
+        let mut cmd_run = CommandRun::new(&self.config.longphase_bin, &args);
+        run_wait(&mut cmd_run)
+            .context(format!(
+                "Error while running `longphase modcall {}`",
+                args.join(" ")
+            ))?
+            .save_to_file(&format!("{}/longphase_modcall_", self.log_dir))
+            .context(format!(
+                "Error while writing logs into {}/longphase_modcall",
+                self.log_dir
+            ))?;
 
+        let vcf = format!("{}.vcf", self.prefix);
+        bcftools_keep_pass(
+            &vcf,
+            &format!("{}.vcf.gz", self.prefix),
+            BcftoolsConfig::default(),
+        )
+        .context(format!(
+            "Can't run BCFtools PASS for LongphaseModcallSolo: {} {}",
+            self.id, self.time
+        ))?
+        .save_to_file(&format!("{}/longphase_modcall_pass_", self.log_dir))
+        .context(format!(
+            "Error while writing logs into {}/longphase_modcall_pass",
+            self.log_dir
+        ))?;
+        fs::remove_file(&vcf).context(format!("Can't remove file: {vcf}"))?;
         Ok(())
     }
 }

+ 169 - 2
src/commands/modkit.rs

@@ -1,8 +1,13 @@
-use std::{fs, path::PathBuf};
+use std::{
+    fs::{self, File}, io::{BufRead, BufReader}, path::{Path, PathBuf}, str::FromStr
+};
 
 use anyhow::Context;
 
-use crate::runners::{run_wait, CommandRun};
+use crate::{
+    collection::InitializeSolo,
+    runners::{run_wait, CommandRun, Run},
+};
 
 #[derive(Debug, Clone)]
 pub struct ModkitConfig {
@@ -130,3 +135,165 @@ pub fn dmr_c_mrd_diag(id: &str, config: &ModkitConfig) -> anyhow::Result<()> {
 
     modkit_dmr(&mrd, &diag, &out, "C", config)
 }
+
+pub struct ModkitSummary {
+    pub id: String,
+    pub time: String,
+    pub bam: String,
+    pub threads: u8,
+    pub log_dir: String,
+    pub summary_file: String,
+    pub result: Option<ModkitSummaryResult>,
+}
+
+impl InitializeSolo for ModkitSummary {
+    fn initialize(id: &str, time: &str, config: crate::config::Config) -> anyhow::Result<Self> {
+        let id = id.to_string();
+        let time = time.to_string();
+        let log_dir = format!("{}/{}/log/modkit_summary", config.result_dir, id);
+        if !Path::new(&log_dir).exists() {
+            fs::create_dir_all(&log_dir)
+                .context(format!("Failed  to create {log_dir} directory"))?;
+        }
+
+        let bam = config.solo_bam(&id, &time);
+        if !Path::new(&bam).exists() {
+            anyhow::bail!("Required bam file doesn't exist: {bam}");
+        }
+
+        let summary_file = config.modkit_summary_file(&id, &time);
+        Ok(ModkitSummary {
+            id,
+            time,
+            bam,
+            log_dir,
+            result: None,
+            summary_file,
+            threads: config.modkit_summary_threads,
+        })
+    }
+}
+
+impl Run for ModkitSummary {
+    fn run(&mut self) -> anyhow::Result<()> {
+        let modkit_args = [
+            "summary",
+            "-t",
+            &self.threads.to_string(),
+            &self.bam,
+            ">",
+            &self.summary_file
+        ];
+        let args = [
+            "-c",
+            &modkit_args.join(" ")
+        ];
+        let mut cmd_run = CommandRun::new("bash", &args);
+        let report = run_wait(&mut cmd_run).context(format!(
+            "Error while running `modkit summary {}`",
+            args.join(" ")
+        ))?;
+
+        let log_file = format!("{}/modkit_summary_", self.log_dir);
+        report
+            .save_to_file(&log_file)
+            .context(format!("Error while writing logs into {log_file}"))?;
+        Ok(())
+    }
+}
+
+impl ModkitSummary {
+    pub fn load(&mut self) -> anyhow::Result<()> {
+        if !Path::new(&self.summary_file).exists() {
+            self.run()?;
+        }
+        self.result = Some(ModkitSummaryResult::parse_file(&self.summary_file)?);
+        Ok(())
+    }
+}
+
+#[derive(Debug)]
+pub struct ModkitSummaryResult {
+    pub base: char,
+    pub total_reads_used: u64,
+    pub count_reads: u64,
+    pub pass_threshold: f64,
+    pub base_data: Vec<ModkitSummaryBase>,
+}
+
+#[derive(Debug)]
+pub struct ModkitSummaryBase {
+    pub base: char,
+    pub code: char,
+    pub pass_count: u64,
+    pub pass_frac: f64,
+    pub all_count: u64,
+    pub all_frac: f64,
+}
+
+impl ModkitSummaryResult {
+    fn parse_file(filename: &str) -> anyhow::Result<Self> {
+        let file = File::open(filename).context("Failed to open file")?;
+        let reader = BufReader::new(file);
+        let mut lines = reader.lines();
+
+        let base = lines
+            .next()
+            .context("Missing base line")?
+            .context("Failed to read base line")?
+            .split_whitespace()
+            .last()
+            .context("Invalid base line")?
+            .chars()
+            .next()
+            .context("Invalid base character")?;
+
+        let total_reads_used = parse_value(&mut lines, "total_reads_used")?;
+        let count_reads = parse_value(&mut lines, "count_reads_C")?;
+        let pass_threshold = parse_value(&mut lines, "pass_threshold_C")?;
+
+        lines.next(); // Skip header line
+
+        let mut base_data = Vec::new();
+        for line in lines {
+            let line = line.context("Failed to read line")?;
+            let parts: Vec<&str> = line.split_whitespace().collect();
+            if parts.len() == 6 {
+                base_data.push(ModkitSummaryBase {
+                    base: parts[0].chars().next().context("Invalid base character")?,
+                    code: parts[1].chars().next().context("Invalid code character")?,
+                    pass_count: u64::from_str(parts[2]).context("Invalid pass_count")?,
+                    pass_frac: f64::from_str(parts[3]).context("Invalid pass_frac")?,
+                    all_count: u64::from_str(parts[4]).context("Invalid all_count")?,
+                    all_frac: f64::from_str(parts[5]).context("Invalid all_frac")?,
+                });
+            }
+        }
+
+        Ok(ModkitSummaryResult {
+            base,
+            total_reads_used,
+            count_reads,
+            pass_threshold,
+            base_data,
+        })
+    }
+}
+
+fn parse_value<T: FromStr>(
+    lines: &mut std::io::Lines<BufReader<File>>,
+    key: &str,
+) -> anyhow::Result<T>
+where
+    T::Err: std::error::Error + Send + Sync + 'static,
+{
+    let line = lines
+        .next()
+        .context(format!("Missing {} line", key))?
+        .context(format!("Failed to read {} line", key))?;
+    let value = line
+        .split_whitespace()
+        .last()
+        .context(format!("Invalid {} line", key))?;
+    T::from_str(value).context(format!("Failed to parse {} value", key))
+}

+ 61 - 9
src/config.rs

@@ -3,7 +3,6 @@ pub struct Config {
     pub pod_dir: String,
     pub result_dir: String,
     pub align: AlignConfig,
-
     pub reference: String,
     pub reference_name: String,
     pub savana_bin: String,
@@ -24,6 +23,17 @@ pub struct Config {
     pub severus_pon: String,
     pub severus_output_dir: String,
     pub severus_solo_output_dir: String,
+    pub longphase_bin: String,
+    pub longphase_threads: u8,
+    pub longphase_modcall_vcf: String,
+    pub modkit_bin: String,
+    pub modkit_summary_threads: u8,
+    pub modkit_summary_file: String,
+    pub longphase_modcall_threads: u8,
+    pub deepvariant_threads: u8,
+    pub deepvariant_bin_version: String,
+    pub deepvariant_model_type: String,
+    pub deepvariant_force: bool,
 }
 
 lazy_static! {
@@ -53,7 +63,11 @@ impl Default for Config {
 
             // DeepVariant
             deepvariant_output_dir: "{result_dir}/{id}/{time}/DeepVariant".to_string(),
-            
+            deepvariant_threads: 155,
+            deepvariant_bin_version: "1.8.0".to_string(),
+            deepvariant_model_type: "ONT_R104".to_string(),
+            deepvariant_force: false,
+
             // Savana
             savana_bin: "savana".to_string(),
             savana_threads: 150,
@@ -68,8 +82,20 @@ impl Default for Config {
             severus_pon: "/data/ref/hs1/PoN_1000G_chm13.tsv.gz".to_string(),
             severus_output_dir: "{result_dir}/{id}/diag/severus".to_string(),
             severus_solo_output_dir: "{result_dir}/{id}/{time}/severus".to_string(),
-
             severus_force: true,
+
+            // Longphase
+            longphase_bin: "/data/tools/longphase_linux-x64".to_string(),
+            longphase_threads: 150,
+            longphase_modcall_threads: 8, // ! out of memory
+            longphase_modcall_vcf:
+                "{result_dir}/{id}/{time}/5mC_5hmC/{id}_{time}_5mC_5hmC_modcall.vcf.gz".to_string(),
+
+            // modkit
+            modkit_bin: "modkit".to_string(),
+            modkit_summary_threads: 50,
+            modkit_summary_file: "{result_dir}/{id}/{time}/{id}_{time}_5mC_5hmC_summary.txt"
+                .to_string(),
         }
     }
 }
@@ -87,7 +113,7 @@ pub struct AlignConfig {
 impl Default for AlignConfig {
     fn default() -> Self {
         Self {
-            dorado_bin: "/data/tools/dorado-0.8.3-linux-x64/bin/dorado".to_string(),
+            dorado_bin: "/data/tools/dorado-0.9.0-linux-x64/bin/dorado".to_string(),
             dorado_basecall_arg: "-x 'cuda:0,1,2,3' sup,5mC_5hmC".to_string(), // since v0.8.0 need
             // to specify cuda devices (exclude the T1000)
             ref_fa: "/data/ref/hs1/chm13v2.0.fa".to_string(),
@@ -141,7 +167,6 @@ impl Config {
         )
     }
 
-
     pub fn tumoral_haplotagged_bam(&self, id: &str) -> String {
         format!(
             "{}/{}_{}_{}_{}.bam",
@@ -179,9 +204,13 @@ impl Config {
     }
 
     pub fn deepvariant_output_vcf(&self, id: &str, time: &str) -> String {
-        format!("{}/{}", self.deepvariant_output_dir(id, time), *DEEPVARIANT_OUTPUT_NAME)
-            .replace("{id}", id)
-            .replace("{time}", time)
+        format!(
+            "{}/{}",
+            self.deepvariant_output_dir(id, time),
+            *DEEPVARIANT_OUTPUT_NAME
+        )
+        .replace("{id}", id)
+        .replace("{time}", time)
     }
 
     // Savana
@@ -240,7 +269,30 @@ impl Config {
         format!(
             "{}/{}_{}_severus-solo_PASSED.vcf.gz",
             &self.severus_solo_output_dir(id, time),
-            id, time
+            id,
+            time
         )
     }
+
+    pub fn constit_vcf(&self, id: &str) -> String {
+        format!("{}/{}_variants_constit.vcf.gz", self.tumoral_dir(id), id)
+    }
+
+    pub fn constit_phased_vcf(&self, id: &str) -> String {
+        format!("{}/{}_variants_constit_PS.vcf.gz", self.tumoral_dir(id), id)
+    }
+
+    pub fn modkit_summary_file(&self, id: &str, time: &str) -> String {
+        self.modkit_summary_file
+            .replace("{result_dir}", &self.result_dir)
+            .replace("{id}", id)
+            .replace("{time}", time)
+    }
+
+    pub fn longphase_modcall_vcf(&self, id: &str, time: &str) -> String {
+        self.longphase_modcall_vcf
+            .replace("{result_dir}", &self.result_dir)
+            .replace("{id}", id)
+            .replace("{time}", time)
+    }
 }

+ 53 - 10
src/helpers.rs

@@ -1,19 +1,21 @@
-use std::fs;
 use anyhow::Context;
+use std::{fs, path::Path};
 
 pub fn find_unique_file(dir_path: &str, suffix: &str) -> anyhow::Result<String> {
     let mut matching_files = Vec::new();
 
-    for entry in fs::read_dir(dir_path)
-        .with_context(|| format!("Failed to read directory: {}", dir_path))?
+    for entry in
+        fs::read_dir(dir_path).with_context(|| format!("Failed to read directory: {}", dir_path))?
     {
         let entry = entry.with_context(|| "Failed to read directory entry")?;
         let path = entry.path();
-        
-        if path.is_file() && path.file_name()
-            .and_then(|name| name.to_str())
-            .map(|name| name.ends_with(suffix))
-            .unwrap_or(false)
+
+        if path.is_file()
+            && path
+                .file_name()
+                .and_then(|name| name.to_str())
+                .map(|name| name.ends_with(suffix))
+                .unwrap_or(false)
         {
             matching_files.push(path);
         }
@@ -23,7 +25,48 @@ pub fn find_unique_file(dir_path: &str, suffix: &str) -> anyhow::Result<String>
         0 => Err(anyhow::anyhow!("No file found ending with '{}'", suffix))
             .with_context(|| format!("In directory: {}", dir_path)),
         1 => Ok(matching_files[0].to_string_lossy().into_owned()),
-        _ => Err(anyhow::anyhow!("Multiple files found ending with '{}'", suffix))
-            .with_context(|| format!("In directory: {}", dir_path)),
+        _ => Err(anyhow::anyhow!(
+            "Multiple files found ending with '{}'",
+            suffix
+        ))
+        .with_context(|| format!("In directory: {}", dir_path)),
+    }
+}
+
+pub fn path_prefix(out: &str) -> anyhow::Result<String> {
+    let out_path = Path::new(&out);
+
+    let out_dir = out_path
+        .parent()
+        .ok_or_else(|| anyhow::anyhow!("Can't parse the dir of {}", out_path.display()))?;
+
+    let name = out_path
+        .file_name()
+        .and_then(|name| name.to_str())
+        .ok_or_else(|| anyhow::anyhow!("Can't parse the file name of {}", out_path.display()))?;
+
+    let stem = name
+        .split_once('.')
+        .map(|(stem, _)| stem)
+        .ok_or_else(|| anyhow::anyhow!("Can't parse the file stem of {}", name))?;
+
+    Ok(format!("{}/{stem}", out_dir.display()))
+}
+
+pub fn force_or_not(path: &str, force: bool) -> anyhow::Result<()> {
+    let path = Path::new(path);
+    let mut output_exists = path.exists();
+
+    if force && output_exists {
+        fs::remove_dir_all(
+            path.parent()
+                .context(format!("Can't parse the parent dir of {}", path.display()))?,
+        )?;
+        output_exists = false;
+    }
+
+    if output_exists {
+        anyhow::bail!("{} already exists.", path.display())
     }
+    Ok(())
 }

+ 2 - 2
src/io/mod.rs

@@ -1,3 +1,3 @@
-
 pub mod pod5_infos;
-
+pub mod readers;
+pub mod vcf;

+ 28 - 0
src/io/readers.rs

@@ -0,0 +1,28 @@
+use std::{fs::File, io::BufReader};
+
+use anyhow::Context;
+use bgzip::BGZFReader;
+use log::info;
+
+pub fn get_reader(path: &str) -> anyhow::Result<Box<dyn std::io::Read>> {
+    info!("Reading: {path}");
+    let file_type = *path
+        .split(".")
+        .collect::<Vec<&str>>()
+        .last()
+        .context(format!("Can't parse {path}"))?;
+    assert!(file_type == "gz" || file_type == "vcf");
+
+    let raw_reader: Box<dyn std::io::Read> = Box::new(File::open(path)?);
+
+    match file_type {
+        "gz" => {
+            let reader = Box::new(BGZFReader::new(raw_reader)?);
+            Ok(Box::new(BufReader::new(reader)))
+        }
+        "vcf" => Ok(Box::new(BufReader::new(raw_reader))),
+        t => {
+            panic!("unknown file type: {}", t)
+        }
+    }
+}

+ 31 - 0
src/io/vcf.rs

@@ -0,0 +1,31 @@
+use std::io::{BufRead, BufReader};
+
+use log::warn;
+
+use crate::variant::variant::{Annotation, Variant};
+
+use super::readers::get_reader;
+
+pub fn read_vcf(
+    path: &str,
+    annotations: &[Annotation],
+) -> anyhow::Result<Vec<Variant>> {
+    let reader = BufReader::new(get_reader(path)?);
+
+    let mut res = Vec::new();
+    for (i, line) in reader.lines().enumerate() {
+        match line {
+            Ok(line) => {
+                if line.starts_with("#") {
+                    continue;
+                }
+                let mut v: Variant = line.parse()?;
+                v.annotations = annotations.to_vec();
+                res.push(v);
+            },
+            Err(e) => warn!("Can't read line {i}: {e}"),
+        }
+    }
+
+    Ok(res)
+}

+ 40 - 31
src/lib.rs

@@ -26,12 +26,13 @@ mod tests {
 
     use callers::{nanomonsv::nanomonsv_create_pon, savana::Savana, severus::{Severus, SeverusSolo}};
     use collection::{Initialize, InitializeSolo, Version};
-    use commands::{longphase::{LongphaseConfig, LongphaseHap, LongphasePhase}, modkit::{bed_methyl, ModkitConfig}};
+    use commands::{longphase::{LongphaseConfig, LongphaseHap, LongphaseModcallSolo, LongphasePhase}, modkit::{bed_methyl, ModkitConfig}};
     use functions::assembler::{Assembler, AssemblerConfig};
     use log::info;
     // use pandora_lib_assembler::assembler::AssembleConfig;
     use rayon::prelude::*;
     use runners::Run;
+    use variant::variant::{Variant, Variants};
 
     use self::{callers::deep_variant::DeepVariantConfig, collection::pod5::{FlowCellCase, Pod5Collection}, commands::dorado, config::Config};
     use super::*;
@@ -119,20 +120,6 @@ mod tests {
         Dorado::from_mux(cases, config)
     }
 
-    #[test_log::test]
-    fn deep_variant() -> anyhow::Result<()> {
-        // let config = DeepVariantConfig {
-        //     result_dir: "/data/test".to_string(),
-        //     ..DeepVariantConfig::default()
-        // };
-        // DeepVariant::new("test_a", "diag", "/data/test_data/subset.bam", config).run()
-        let config = DeepVariantConfig {
-            result_dir: "/data/test".to_string(),
-            ..DeepVariantConfig::default()
-        };
-        DeepVariant::new("LEVASSEUR", "mrd", "/data/longreads_basic_pipe/LEVASSEUR/mrd/LEVASSEUR_mrd_hs1.bam", config).run()
-    }
-
     #[test_log::test]
     fn clairs() -> anyhow::Result<()> {
         let config = ClairSConfig {
@@ -395,10 +382,7 @@ mod tests {
     #[test]
     fn run_deepvariant() -> anyhow::Result<()> {
         init();
-        let id = "HAMROUNE";
-        let diag_bam = format!("/data/longreads_basic_pipe/{id}/diag/{id}_diag_hs1.bam");
-        // let mrd_bam = format!("/data/longreads_basic_pipe/{id}/mrd/{id}_mrd_hs1.bam");
-        DeepVariant::new(id, "diag", &diag_bam, DeepVariantConfig::default()).run()
+        DeepVariant::initialize("HAMROUNE", "diag", Config::default())?.run()
     }
 
 
@@ -415,20 +399,45 @@ mod tests {
     }
 
     #[test]
-    fn run_longphase_phase() -> anyhow::Result<()> {
+    fn run_longphase_modcall() -> anyhow::Result<()> {   
         init();
-        let id = "BECERRA";
-        // let config = Config::default();
-        
-        let mrd_bam = format!("/data/longreads_basic_pipe/{id}/mrd/{id}_mrd_hs1.bam");
-        let diag_bam = format!("/data/longreads_basic_pipe/{id}/diag/{id}_diag_hs1.bam");
-        let mrd_bam_hp = format!("/data/longreads_basic_pipe/{id}/mrd/{id}_mrd_hs1_hp.bam");
-        let vcf = format!("/data/longreads_basic_pipe/{id}/mrd/DeepVariant/BECERRA_mrd_DeepVariant_PASSED.vcf.gz");
-        let hp_vcf = format!("/data/longreads_basic_pipe/{id}/mrd/DeepVariant/BECERRA_mrd_DeepVariant_PASSED_PS.vcf.gz");
+        let id = "ADJAGBA";
+        let time = "diag";
+        LongphaseModcallSolo::initialize(id, time, Config::default())?.run()
+    }
+
+    #[test]
+    fn run_longphase_phase() -> anyhow::Result<()> {   
+        init();
+        let id = "ADJAGBA";
+        LongphasePhase::initialize(id, Config::default())?.run()
+    }
+
+    #[test]
+    fn variant_parse() -> anyhow::Result<()> {
+        let row = "chr1\t1366\t.\tC\tCCCT\t8.2\tPASS\t.\tGT:GQ:DP:AD:VAF:PL\t1/1:4:6:1,4:0.666667:6,4,0";
+        let variant: Variant = row.parse()?;
+        let var_string = variant.into_vcf_row();
+        assert_eq!(row, &var_string);
+
+        let row = "chr1\t1366\t.\tC\tCCCT\t8.2\tPASS\t.";
+        let variant: Variant = row.parse()?;
+        let var_string = variant.into_vcf_row();
+        assert_eq!(row, &var_string);
 
 
-        // LongphasePhase::new(id, &mrd_bam_hp, &vcf, LongphaseConfig::default())?.run();
-        // LongphaseHap::new(id, &mrd_bam, &hp_vcf, LongphaseConfig::default()).run();
-        LongphaseHap::new(id, &diag_bam, &hp_vcf, LongphaseConfig::default()).run()
+        Ok(())
+    }
+
+    #[test]
+    fn variant_load_deepvariant() -> anyhow::Result<()> {   
+        init();
+        let id = "ADJAGBA";
+        let time = "diag";
+        let mut dv = DeepVariant::initialize(id, time, Config::default())?;
+        dv.run()?;
+        let variants = dv.variants()?;
+        println!("Deepvariant for {id} {time}: variants {}", variants.len());
+        Ok(())
     }
 }

+ 48 - 40
src/runners.rs

@@ -2,10 +2,14 @@ use std::{
     fs::File,
     io::{BufRead, BufReader, Write},
     process::{Child, Command, Stdio},
-    sync::{mpsc::{self, TryRecvError}, Arc, Mutex},
+    sync::{
+        mpsc::{self, TryRecvError},
+        Arc, Mutex,
+    },
     thread,
 };
 
+use bgzip::{BGZFWriter, Compression};
 use chrono::{DateTime, Utc};
 use log::{info, warn};
 use serde::{Deserialize, Serialize};
@@ -44,13 +48,24 @@ pub struct RunReport {
 }
 
 impl RunReport {
-    /// Serialize the RunReport to a JSON string
+    // pub fn save_to_file(&self, file_prefix: &str) -> std::io::Result<()> {
+    //     let json_data = serde_json::to_string_pretty(self).expect("Failed to serialize RunReport");
+    //     let uuid = Uuid::new_v4().to_string()[..5].to_string();
+    //     let file_path = format!("{}{}.log", file_prefix, uuid);
+    //     let mut file = File::create(&file_path)?;
+    //     file.write_all(json_data.as_bytes())?;
+    //     Ok(())
+    // }
+
+    /// Serialize the RunReport to a JSON string and save to file_prefix.log.gz
     pub fn save_to_file(&self, file_prefix: &str) -> std::io::Result<()> {
         let json_data = serde_json::to_string_pretty(self).expect("Failed to serialize RunReport");
         let uuid = Uuid::new_v4().to_string()[..5].to_string();
-        let file_path = format!("{}{}.log", file_prefix, uuid);
-        let mut file = File::create(&file_path)?;
-        file.write_all(json_data.as_bytes())?;
+        let file_path = format!("{}{}.log.gz", file_prefix, uuid);
+        let file = File::create(&file_path)?;
+        let mut writer = BGZFWriter::new(file, Compression::default());
+        writer.write_all(json_data.as_bytes())?;
+        writer.close()?;
         Ok(())
     }
 }
@@ -98,6 +113,13 @@ impl Run for DockerRun {
             .output()
             .expect("Failed to run Docker command");
 
+        let info = format!("Running with docker: {}", &self.args.join(" "));
+        info!("{info}");
+        {
+            let mut logs_lock = self.logs.lock().unwrap();
+            logs_lock.push_str(&format!("{info}\n"));
+        }
+
         // add id to Arc
         let id = String::from_utf8_lossy(&output.stdout).trim().to_string();
         {
@@ -149,7 +171,12 @@ impl Wait for DockerRun {
             .status()
             .expect("Failed to wait on Docker container");
         if !status.success() {
-            warn!("Docker command failed with status: {}", status);
+            let warn = format!("Docker command failed with status: {status}");
+            warn!("{warn}");
+            {
+                let mut logs_lock = self.logs.lock().unwrap();
+                logs_lock.push_str(&format!("\n{warn}\n"));
+            }
         }
 
         let mut container_id_lock = DOCKER_ID.lock().unwrap();
@@ -192,31 +219,16 @@ impl CommandRun {
 
 impl Run for CommandRun {
     fn run(&mut self) -> anyhow::Result<()> {
-        // info!("Running command: {} {}", &self.bin, &self.args.join(" "));
-        // let mut child = Command::new(&self.bin)
-        //     .args(&self.args)
-        //     .stdout(Stdio::inherit())
-        //     .stderr(Stdio::piped())
-        //     .spawn()
-        //     .expect("Failed to spawn");
-        //
-        // if let Some(stderr) = child.stderr.take() {
-        //     let reader = BufReader::new(stderr);
-        //     for line in reader.lines().map_while(Result::ok) {
-        //         info!("[{}] {line}", self.bin);
-        //         self.log.push_str(&line);
-        //         self.log.push('\n');
-        //     }
-        // }
-        // self.child = Some(child);
-        // Ok(())
-        info!("Running command: {} {}", &self.bin, &self.args.join(" "));
+        let info = format!("Running command: {} {}", &self.bin, &self.args.join(" "));
+        info!("{info}");
         let mut child = Command::new(&self.bin)
             .args(&self.args)
             .stdout(Stdio::piped())
             .stderr(Stdio::piped())
             .spawn()?;
 
+        self.log.push_str(&format!("{info}\n"));
+
         let stdout = child.stdout.take().expect("Failed to capture stdout");
         let stderr = child.stderr.take().expect("Failed to capture stderr");
 
@@ -228,8 +240,7 @@ impl Run for CommandRun {
             s.spawn(|| {
                 for line in stdout_reader.lines().map_while(Result::ok) {
                     info!("[{}:stdout] {}", self.bin, line);
-                    tx
-                        .send(("stdout".to_string(), line.to_string()))
+                    tx.send(("stdout".to_string(), line.to_string()))
                         .expect("Channel send failed");
                 }
             });
@@ -237,8 +248,7 @@ impl Run for CommandRun {
             s.spawn(|| {
                 for line in stderr_reader.lines().map_while(Result::ok) {
                     info!("[{}:stderr] {}", self.bin, line);
-                    tx
-                        .send(("stderr".to_string(), line.to_string()))
+                    tx.send(("stderr".to_string(), line.to_string()))
                         .expect("Channel send failed");
                 }
             });
@@ -249,25 +259,23 @@ impl Run for CommandRun {
     }
 }
 
-
 impl Wait for CommandRun {
     fn wait(&mut self) -> anyhow::Result<()> {
         if let Some(child) = &mut self.child {
             loop {
                 match self.rx.try_recv() {
                     Ok((stream, line)) => {
-                        self.log.push_str(&format!("[{}] {}: {}\n", self.bin, stream, line));
+                        self.log
+                            .push_str(&format!("[{}] {}: {}\n", self.bin, stream, line));
                     }
-                    Err(TryRecvError::Empty) => {
-                        match child.try_wait()? {
-                            Some(_) => {
-                                break;
-                            }
-                            None => {
-                                std::thread::sleep(std::time::Duration::from_millis(100));
-                            }
+                    Err(TryRecvError::Empty) => match child.try_wait()? {
+                        Some(_) => {
+                            break;
                         }
-                    }
+                        None => {
+                            std::thread::sleep(std::time::Duration::from_millis(100));
+                        }
+                    },
                     Err(TryRecvError::Disconnected) => {
                         break;
                     }

+ 1 - 1
src/variant/mod.rs

@@ -1,2 +1,2 @@
-// pub mod variant;
+pub mod variant;
 

+ 226 - 279
src/variant/variant.rs

@@ -1,276 +1,260 @@
+use anyhow::{anyhow, Context, Ok};
 use serde::{Deserialize, Serialize};
+use std::{fmt, str::FromStr};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Variant {
+    pub contig: String,
+    pub position: u32,
+    pub id: String,
+    pub reference: ReferenceAlternative,
+    pub alternative: ReferenceAlternative,
+    pub quality: Option<f32>,
+    pub filter: Filter,
+    pub info: String,
+    pub formats: Formats,
+    pub annotations: Vec<Annotation>,
+}
 
-#[derive(Debug, Clone, Serialize, Eq, PartialEq, Deserialize)]
-pub enum VariantType {
-    Somatic,
-    Constitutionnal,
+impl PartialEq for Variant {
+    fn eq(&self, other: &Self) -> bool {
+        // Nota bene: id, filter, info, format and quality is intentionally not compared
+        self.contig == other.contig
+            && self.position == other.position
+            && self.reference == other.reference
+            && self.alternative == other.alternative
+    }
 }
 
+impl Eq for Variant {}
+impl FromStr for Variant {
+    type Err = anyhow::Error;
 
+    fn from_str(s: &str) -> anyhow::Result<Self> {
+        let v: Vec<&str> = s.split('\t').collect();
 
-impl Variant {
-    pub fn from_vcfrow(row: &VCFRow, source: VCFSource, variant_type: VariantType) -> anyhow::Result<Self> {
-        let callers_data = vec![CallerData {
-            qual: row.qual.parse::<f32>().ok(),
-            info: parse_info(&row.info, &source).context(anyhow!(
-                "Can't parse {:?} info for {}",
-                source,
-                row.info
-            ))?,
-            format: parse_format(&source, &row.value).context(anyhow!(
-                "Can't parse {:?} format for {}",
-                source,
-                row.value
-            ))?,
-        }];
-
-        Ok(Variant {
-            contig: row.chr.to_string(),
-            position: row.pos,
-            reference: row
-                .reference
+        let formats = if v.len() == 10 {
+            (
+                *v.get(8).ok_or(anyhow!("Can't parse formats from: {s}"))?,
+                *v.get(9).ok_or(anyhow!("Can't parse formats from: {s}"))?,
+            )
+                .try_into()
+                .context(format!("Can't parse formats from: {s}"))?
+        } else {
+            Formats::default()
+        };
+
+        Ok(Self {
+            contig: v
+                .first()
+                .ok_or(anyhow!("Can't parse contig from: {s}"))?
+                .to_string(),
+            position: v
+                .get(1)
+                .ok_or(anyhow!("Can't parse contig from: {s}"))?
+                .parse()
+                .context(format!("Can't parse position from: {s}"))?,
+            id: v
+                .get(2)
+                .ok_or(anyhow!("Can't parse id from: {s}"))?
+                .to_string(),
+            reference: v
+                .get(3)
+                .ok_or(anyhow!("Can't parse reference from: {s}"))?
+                .parse()
+                .context(format!("Can't parse reference from: {s}"))?,
+            alternative: v
+                .get(4)
+                .ok_or(anyhow!("Can't parse alternative from: {s}"))?
                 .parse()
-                .context(anyhow!("Error while parsing {}", row.reference))?,
-            alternative: row
-                .alt
+                .context(format!("Can't parse alternative from: {s}"))?,
+            quality: v
+                .get(5)
+                .map(|s| s.parse::<f32>().ok()) // Try to parse as f64; returns Option<f64>
+                .unwrap_or(None),
+            filter: v
+                .get(6)
+                .ok_or(anyhow!("Can't parse filter from: {s}"))?
                 .parse()
-                .context(anyhow!("Error while parsing {}", row.alt))?,
-            n_ref: None,
-            n_alt: None,
-            vaf: None,
-            depth: None,
-            callers_data,
-            source: vec![source],
-            variant_type,
+                .context(format!("Can't parse filter from: {s}"))?,
+            info: v
+                .get(7)
+                .ok_or(anyhow!("Can't parse id from: {s}"))?
+                .to_string(),
+            formats,
             annotations: Vec::new(),
         })
     }
+}
 
-    pub fn get_depth(&mut self) -> u32 {
-        if let Some(depth) = self.depth {
-            depth
-        } else {
-            let depth = self
-                .callers_data
-                .iter_mut()
-                .map(|v| v.get_depth())
-                .max()
-                .unwrap();
-            self.depth = Some(depth);
-            depth
+// #CHROM  POS     ID      REF     ALT     QUAL    FILTER  INFO    FORMAT  ADJAGBA_diag
+impl Variant {
+    pub fn into_vcf_row(&self) -> String {
+        let mut columns = vec![
+            self.contig.to_string(),
+            self.position.to_string(),
+            self.id.to_string(),
+            self.reference.to_string(),
+            self.alternative.to_string(),
+            self.quality
+                .map(|v| v.to_string())
+                .unwrap_or(".".to_string()),
+            self.filter.to_string(),
+            self.info.to_string(),
+        ];
+
+        if !self.formats.0.is_empty() {
+            let (format, values) = self.formats.clone().into();
+            columns.push(format);
+            columns.push(values);
         }
-    }
 
-    pub fn get_n_alt(&mut self) -> u32 {
-        if let Some(n_alt) = self.n_alt {
-            n_alt
-        } else {
-            let n_alt = self
-                .callers_data
-                .iter_mut()
-                .map(|v| v.get_n_alt())
-                .max()
-                .unwrap();
-            self.n_alt = Some(n_alt);
-            n_alt
-        }
+        columns.join("\t")
     }
+}
 
-    pub fn vaf(&mut self) -> f32 {
-        let n_alt = self.get_n_alt() as f32;
-        let depth = self.get_depth() as f32;
-        self.vaf = Some(n_alt / depth);
-        self.vaf.unwrap()
-    }
+// Tag
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+pub enum Annotation {
+    Source(String),
+    Diag,
+    Constit,
+    Other((String, String)), // (key, value)
+}
 
-    pub fn is_ins(&self) -> bool {
-        matches!(
-            (&self.reference, &self.alternative),
-            (
-                ReferenceAlternative::Nucleotide(_),
-                ReferenceAlternative::Nucleotides(_)
-            )
-        )
-    }
+/// Format
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub enum Format {
+    GT(String),
+    GQ(u32),
+    DP(u32),
+    AD(Vec<u32>),
+    VAF(f32),
+    PL(Vec<u32>),
+    Other((String, String)), // (key, value)
+}
 
-    pub fn alteration_category(&self) -> AlterationCategory {
-        match (&self.reference, &self.alternative) {
-            (ReferenceAlternative::Nucleotide(_), ReferenceAlternative::Nucleotide(_)) => {
-                AlterationCategory::Snv
-            }
-            (ReferenceAlternative::Nucleotide(_), ReferenceAlternative::Nucleotides(_)) => {
-                AlterationCategory::Ins
-            }
-            (ReferenceAlternative::Nucleotide(_), ReferenceAlternative::Unstructured(_)) => {
-                AlterationCategory::Other
-            }
-            (ReferenceAlternative::Nucleotides(_), ReferenceAlternative::Nucleotide(_)) => {
-                AlterationCategory::Del
-            }
-            (ReferenceAlternative::Nucleotides(a), ReferenceAlternative::Nucleotides(b))
-                if a.len() < b.len() =>
-            {
-                AlterationCategory::Ins
-            }
-            (ReferenceAlternative::Nucleotides(a), ReferenceAlternative::Nucleotides(b))
-                if a.len() > b.len() =>
-            {
-                AlterationCategory::Del
-            }
-            (ReferenceAlternative::Nucleotides(_), ReferenceAlternative::Nucleotides(_)) => {
-                AlterationCategory::Rep
-            }
-            (ReferenceAlternative::Nucleotides(_), ReferenceAlternative::Unstructured(_)) => {
-                AlterationCategory::Other
-            }
-            (ReferenceAlternative::Unstructured(_), ReferenceAlternative::Nucleotide(_)) => {
-                AlterationCategory::Other
-            }
-            (ReferenceAlternative::Unstructured(_), ReferenceAlternative::Nucleotides(_)) => {
-                AlterationCategory::Other
-            }
-            (ReferenceAlternative::Unstructured(_), ReferenceAlternative::Unstructured(_)) => {
-                AlterationCategory::Other
-            }
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
+pub struct Formats(Vec<Format>);
+
+impl TryFrom<(&str, &str)> for Formats {
+    type Error = anyhow::Error;
+
+    fn try_from((k, v): (&str, &str)) -> anyhow::Result<Self> {
+        let keys: Vec<&str> = k.split(':').collect();
+        let values: Vec<&str> = v.split(':').collect();
+
+        if keys.len() != values.len() {
+            anyhow::bail!("Mismatch between keys and values count for {k} {v}");
         }
+
+        Ok(Self(
+            keys.into_iter()
+                .zip(values)
+                .map(|(key, value)| Format::try_from((key, value)))
+                .collect::<Result<Vec<Format>, _>>()
+                .map_err(|e| anyhow::anyhow!("Failed to parse format: {e}"))?,
+        ))
     }
+}
 
-    pub fn to_min_string(&mut self) -> String {
-        let depth = self.get_depth();
-        let n_alt = self.get_n_alt();
+impl From<Formats> for (String, String) {
+    fn from(formats: Formats) -> Self {
+        let mut keys = Vec::new();
+        let mut values = Vec::new();
 
-        format!(
-            "DP:AD\t{}:{}",
-            depth,
-            [(depth - n_alt).to_string(), n_alt.to_string()].join(",")
-        )
-    }
+        for format in formats.0 {
+            let (key, value): (String, String) = format.into();
+            keys.push(key);
+            values.push(value);
+        }
 
-    pub fn get_veps(&self) -> Vec<VEP> {
-        self.annotations
-            .iter()
-            .flat_map(|e| {
-                if let AnnotationType::VEP(e) = e {
-                    e.clone()
-                } else {
-                    vec![]
-                }
-            })
-            .collect()
+        (keys.join(":"), values.join(":"))
     }
-    pub fn get_best_vep(&self) -> Result<VEP> {
-        get_best_vep(&self.get_veps())
+}
+
+impl TryFrom<(&str, &str)> for Format {
+    type Error = anyhow::Error;
+
+    fn try_from((key, value): (&str, &str)) -> anyhow::Result<Self> {
+        Ok(match key {
+            "GT" => Format::GT(value.to_string()),
+            "GQ" => Format::GQ(value.parse().context(format!("Can't parse GQ: {value}"))?),
+            "DP" => Format::DP(value.parse().context(format!("Can't parse DP: {value}"))?),
+            "AD" => Format::AD(
+                value
+                    .split(',')
+                    .map(|e| e.parse().context("Failed to parse AD"))
+                    .collect::<anyhow::Result<Vec<_>>>()?,
+            ),
+            "VAF" => Format::VAF(value.parse().context(format!("Can't parse VAF: {value}"))?),
+            "PL" => Format::PL(
+                value
+                    .split(',')
+                    .map(|e| e.parse().context("Failed to parse AD"))
+                    .collect::<anyhow::Result<Vec<_>>>()?,
+            ),
+            _ => Format::Other((key.to_string(), value.to_string())),
+        })
     }
+}
 
-    pub fn is_from_category(&self, and_categories: &[Category]) -> bool {
-        let mut vec_bools = Vec::new();
-        for category in and_categories.iter() {
-            match category {
-                Category::VariantCategory(vc) => {
-                    for annotations in self.annotations.iter() {
-                        if let AnnotationType::VariantCategory(vvc) = annotations {
-                            if vc == vvc {
-                                vec_bools.push(true);
-                                break;
-                            }
-                        }
-                    }
-                }
-                Category::PositionRange { contig, from, to } => {
-                    if self.contig == *contig {
-                        match (from, to) {
-                            (None, None) => vec_bools.push(true),
-                            (None, Some(to)) => vec_bools.push(self.position <= *to),
-                            (Some(from), None) => vec_bools.push(self.position >= *from),
-                            (Some(from), Some(to)) => {
-                                vec_bools.push(self.position >= *from && self.position <= *to)
-                            }
-                        }
-                    } else {
-                        vec_bools.push(false);
-                    }
-                }
-                Category::VCFSource(_) => (),
-                Category::NCosmic(n) => {
-                    let mut bools = Vec::new();
-                    for annotations in self.annotations.iter() {
-                        if let AnnotationType::Cosmic(c) = annotations {
-                            bools.push(c.cosmic_cnt >= *n);
-                            break;
-                        }
-                    }
-                    vec_bools.push(bools.iter().any(|&b| b));
-                }
-                Category::NCBIFeature(ncbi_feature) => {
-                    let mut bools = Vec::new();
-                    for annotations in self.annotations.iter() {
-                        if let AnnotationType::NCBIGFF(v) = annotations {
-                            bools.push(v.feature == *ncbi_feature);
-                        }
-                    }
-                    vec_bools.push(bools.iter().any(|&b| b));
-                }
-                Category::VAF { min, max } => {
-                    let v = if self.vaf.is_none() {
-                        let mut s = self.clone();
-                        s.vaf()
-                    } else {
-                        self.vaf.unwrap()
-                    };
-                    vec_bools.push(v >= *min && v <= *max);
-                }
-                Category::Pangolin => {
-                    vec_bools.push(
-                        self.annotations
-                            .iter()
-                            .filter(|a| matches!(a, AnnotationType::Pangolin(_)))
-                            .count()
-                            > 0,
-                    );
-                }
+impl From<Format> for (String, String) {
+    fn from(format: Format) -> Self {
+        match format {
+            Format::GT(value) => ("GT".to_string(), value),
+            Format::GQ(value) => ("GQ".to_string(), value.to_string()),
+            Format::DP(value) => ("DP".to_string(), value.to_string()),
+            Format::AD(values) => {
+                let value_str = values
+                    .iter()
+                    .map(|v| v.to_string())
+                    .collect::<Vec<_>>()
+                    .join(",");
+                ("AD".to_string(), value_str)
+            }
+            Format::VAF(value) => ("VAF".to_string(), value.to_string()),
+            Format::PL(values) => {
+                let value_str = values
+                    .iter()
+                    .map(|v| v.to_string())
+                    .collect::<Vec<_>>()
+                    .join(",");
+                ("PL".to_string(), value_str)
             }
+            Format::Other((key, value)) => (key, value),
         }
-        vec_bools.iter().all(|&x| x)
-    }
-
-    pub fn callers(&self) -> Vec<String> {
-        self.source
-            .iter()
-            .map(|source| source.to_string())
-            .collect()
     }
 }
+
+/// Filter
 #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
-pub enum AlterationCategory {
-    Snv,
-    Ins,
-    Del,
-    Rep,
-    Other,
+pub enum Filter {
+    PASS,
+    Other(String),
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
-pub enum AnnotationType {
-    VariantCategory(VariantCategory),
-    VEP(Vec<VEP>),
-    Cluster(i32),
-    Cosmic(Cosmic),
-    GnomAD(GnomAD),
-    NCBIGFF(NCBIGFF),
-    Pangolin(Pangolin),
-    Phase(PhaseAnnotation),
+impl FromStr for Filter {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> anyhow::Result<Self> {
+        match s {
+            "PASS" => Ok(Filter::PASS),
+            _ => Ok(Filter::Other(s.to_string())),
+        }
+    }
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
-pub enum VariantCategory {
-    Somatic,
-    LowMRDDepth,
-    LOH,
-    Constit,
-    LowDiversity,
+impl fmt::Display for Filter {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Filter::PASS => write!(f, "PASS"),
+            Filter::Other(ref s) => write!(f, "{}", s),
+        }
+    }
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, ToSchema)]
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
 pub enum ReferenceAlternative {
     Nucleotide(Base),
     Nucleotides(Vec<Base>),
@@ -280,7 +264,7 @@ pub enum ReferenceAlternative {
 impl FromStr for ReferenceAlternative {
     type Err = anyhow::Error;
 
-    fn from_str(s: &str) -> Result<Self> {
+    fn from_str(s: &str) -> anyhow::Result<Self> {
         let possible_bases = s.as_bytes().iter();
         let mut res: Vec<Base> = Vec::new();
         for &base in possible_bases {
@@ -313,7 +297,7 @@ impl fmt::Display for ReferenceAlternative {
     }
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, ToSchema)]
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
 pub enum Base {
     A,
     T,
@@ -324,14 +308,14 @@ pub enum Base {
 
 impl TryFrom<u8> for Base {
     type Error = anyhow::Error;
-    fn try_from(base: u8) -> Result<Self> {
+    fn try_from(base: u8) -> anyhow::Result<Self> {
         match base {
             b'A' => Ok(Base::A),
             b'T' => Ok(Base::T),
             b'C' => Ok(Base::C),
             b'G' => Ok(Base::G),
             b'N' => Ok(Base::N),
-            _ => Err(anyhow!(
+            _ => Err(anyhow::anyhow!(
                 "Unknown base: {}",
                 String::from_utf8_lossy(&[base])
             )),
@@ -365,43 +349,6 @@ impl fmt::Display for Base {
     }
 }
 
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
-pub enum Format {
-    DeepVariant(DeepVariantFormat),
-    ClairS(ClairSFormat),
-    Sniffles(SnifflesFormat),
-    Nanomonsv(NanomonsvFormat),
-}
-
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
-pub enum Info {
-    #[schema(value_type=String)]
-    DeepVariant(DeepVariantInfo),
-    #[schema(value_type=String)]
-    ClairS(ClairSInfo),
-    #[schema(value_type=String)]
-    Sniffles(SnifflesInfo),
-    #[schema(value_type=String)]
-    Nanomonsv(NanomonsvInfo),
-}
-
-fn parse_info(s: &str, source: &VCFSource) -> Result<Info> {
-    match source {
-        VCFSource::DeepVariant => Ok(Info::DeepVariant(s.parse()?)),
-        VCFSource::ClairS => Ok(Info::ClairS(s.parse()?)),
-        VCFSource::Sniffles => Ok(Info::Sniffles(s.parse()?)),
-        VCFSource::Nanomonsv => Ok(Info::Nanomonsv(s.parse()?)),
-    }
-}
-
-fn parse_format(vcf_source: &VCFSource, data: &str) -> Result<Format> {
-    let res = match vcf_source {
-        VCFSource::DeepVariant => Format::DeepVariant(data.parse()?),
-        VCFSource::ClairS => Format::ClairS(data.parse()?),
-        VCFSource::Sniffles => Format::Sniffles(data.parse()?),
-        VCFSource::Nanomonsv => Format::Nanomonsv(data.parse()?),
-    };
-    Ok(res)
+pub trait Variants {
+    fn variants(&self) -> anyhow::Result<Vec<Variant>>;
 }
-
-

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini