use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; const CONFIG_TEMPLATE: &str = include_str!("../pandora-config.example.toml"); #[derive(Debug, Clone, Serialize, Deserialize)] /// Global configuration for the Pandora somatic pipeline. /// /// Loaded from `~/.local/share/pandora/pandora-config.toml` (see [`Config::config_path`]). /// Most fields are path templates that can contain placeholders such as: /// `{result_dir}`, `{id}`, `{time}`, `{reference_name}`, `{haplotagged_bam_tag_name}`, `{output_dir}`. pub struct Config { // === General filesystem layout / I/O === /// Root directory where all results will be written. pub result_dir: String, /// Temporary directory. pub tmp_dir: String, /// Run cache directory. pub run_cache_dir: String, /// Runner can slurm pub slurm_runner: bool, /// Slurm max parallel jobs pub slurm_max_par: u8, /// Software threads pub threads: u8, /// Singularity/Apptainer bin pub singularity_bin: String, /// Path to the `conda.sh` activation script (used to activate envs before running tools). pub conda_sh: String, // === Alignment / BAM handling === /// Configuration for Dorado + samtools alignment pipeline. pub align: AlignConfig, /// Minimum MAPQ for reads to be kept during BAM filtering. pub bam_min_mapq: u8, /// Number of threads for hts BAM reader pub bam_n_threads: u8, /// Number of reads sampled when estimating BAM composition (e.g. tumor contamination). pub bam_composition_sample_size: u32, // === Reference genome and annotations === /// Path to the reference FASTA used throughout the pipeline. pub reference: String, /// Short name for the reference (e.g. "hs1"), used in filenames. pub reference_name: String, /// Pseudoautosomal regions (PARs) BED file. pub pseudoautosomal_regions_bed: String, /// Path to the sequence dictionary (`.dict`) for the reference. pub dict_file: String, /// Path to the RefSeq GFF3 annotation, sorted and indexed. pub refseq_gff: String, pub refseq_gtf: String, /// dbSNP vcf.gz file (should be indexed) pub db_snp: String, /// CoLoRSdb jasmine (should be indexed, tbi) pub colors_db: String, pub colors_db_smallvar: String, /// BED with genes on the 4th column pub genes_bed: String, /// Cytobands BED file pub cytobands_bed: String, /// Chromosome alias file pub chromosomes_alias: String, /// BED template used to mask low-quality or filtered regions. /// /// Placeholders: /// - `{result_dir}`: global result directory /// - `{id}`: case identifier pub mask_bed: String, /// Panels of interest (name, BED path). pub panels: Vec<(String, String)>, /// Repeats bed file pub repeats_bed: String, // === Sample naming conventions === /// Label used for the tumor sample in directory and file names (e.g. "diag"). pub tumoral_name: String, /// Label used for the normal sample (e.g. "mrd"). pub normal_name: String, /// BAM tag name used for haplotagged reads (e.g. "HP"). pub haplotagged_bam_tag_name: String, // === Coverage counting (somatic-scan) === /// Name of the subdirectory (under each sample dir) where count files are stored. pub count_dir_name: String, /// Bin size (bp) for count files. pub count_bin_size: u32, /// Number of chunks used to split chromosomes for counting. pub count_n_chunks: u32, /// Whether to force recomputation of coverage / counting even if outputs already exist. pub somatic_scan_force: bool, // === Somatic pipeline global options === /// Whether to force recomputation of the whole somatic pipeline. pub somatic_pipe_force: bool, /// Default number of threads for most heavy tools (DeepVariant, Savana, etc.). pub somatic_pipe_threads: u8, /// Path template to the per-case somatic pipeline statistics directory. /// /// Placeholders: `{result_dir}`, `{id}`. pub somatic_pipe_stats: String, // === Basic somatic filtering / QC thresholds === /// Minimum depth in the constitutional sample to consider a site evaluable. pub somatic_min_constit_depth: u16, /// Maximum allowed ALT count in the constitutional sample for a somatic call. pub somatic_max_alt_constit: u16, /// Window size (bp) used when computing sequence entropy around variants. pub entropy_seq_len: usize, /// Minimum Shannon entropy threshold for keeping a variant. pub min_shannon_entropy: f64, /// Minimum dbSNP population frequency (max across studies) to consider a variant a known /// polymorphism for the dbSNP + constitutional-alt filter. pub db_snp_min_freq: f32, /// Maximum depth considered "low quality" for certain filters. pub max_depth_low_quality: u32, /// Minimum depth considered "high quality" for certain filters. pub min_high_quality_depth: u32, /// Minimum number of callers supporting a variant for it to be kept. pub min_n_callers: u8, // === DeepVariant configuration === /// Template for the DeepVariant output directory (solo and normal/tumor runs). /// /// Placeholders: `{result_dir}`, `{id}`, `{time}`. pub deepvariant_output_dir: String, /// Number of threads to use for DeepVariant. pub deepvariant_threads: u8, /// DeepVariant singularity image path pub deepvariant_image: String, /// DeepVariant model type (e.g. "ONT_R104"). pub deepvariant_model_type: String, /// Force DeepVariant recomputation even if outputs already exist. pub deepvariant_force: bool, // === DeepSomatic configuration === /// Template for the DeepSomatic output directory. /// /// Placeholders: `{result_dir}`, `{id}`, `{time}`. pub deepsomatic_output_dir: String, /// Number of threads for DeepSomatic. pub deepsomatic_threads: u8, /// DeepSomatic singularity image path pub deepsomatic_image: String, /// DeepSomatic model type (e.g. "ONT"). pub deepsomatic_model_type: String, /// Force DeepSomatic recomputation. pub deepsomatic_force: bool, // === ClairS configuration === /// Number of threads for ClairS. pub clairs_threads: u8, /// Path to ClairS singularity image. pub clairs_image: String, /// Force ClairS recomputation. pub clairs_force: bool, /// Keep per-part directories after chunked ClairS merging (default: false). pub clairs_keep_parts: bool, /// Platform preset for ClairS (e.g. "ont_r10_dorado_sup_5khz_ssrs"). pub clairs_platform: String, /// Template for ClairS output directory (`{result_dir}`, `{id}`). pub clairs_output_dir: String, // === GATK configuration === /// Path to the GATK container image (Singularity/Apptainer .sif, or a docker:// URI /// if you pull at runtime). /// /// Examples: /// - "/containers/gatk_4.6.0.0.sif" /// - "docker://broadinstitute/gatk:latest" pub gatk_image: String, /// Path to a BED file restricting analysis to target regions (0-based, half-open). /// Must match contig naming of the reference/BAMs (e.g. "chr9" vs "9"). /// /// Used for targeted calling (e.g. Mutect2 `-L` or region chunking). pub gatk_bed_path: String, /// Local single-run CPU threads (non-Slurm execution). /// Used for full-run Mutect2 or other GATK tools. /// Typically forwarded to: /// - `--native-pair-hmm-threads` /// - `--reader-threads` /// Should match available cores on the node. pub gatk_threads: usize, // e.g. 32 /// Local single-run memory limit in GB. /// Used to size Java heap: /// `--java-options "-Xmx{mem}g"` /// Should leave headroom for native memory (PairHMM, buffers). pub gatk_mem_gb: u32, // e.g. 120 /// Per-chunk CPU threads when running chunked under Slurm. /// Applies to each parallel job independently. pub gatk_slurm_threads: usize, // e.g. 8 /// Per-chunk memory (GB) when running under Slurm. /// Used both for scheduler request and Java heap sizing per chunk. /// Must be sufficient for interval-restricted Mutect2. pub gatk_slurm_mem_gb: u32, // e.g. 32 /// If true, force re-run of GATK steps by removing or ignoring existing outputs. pub gatk_force: bool, /// Template for GATK output directory (`{result_dir}`, `{id}`). pub gatk_output_dir: String, /// GATK passed VCF pub gatk_passed_vcf: String, // === Savana configuration === /// Savana binary name or full path. pub savana_bin: String, /// Number of threads for Savana. pub savana_threads: u8, /// RAM capacity used for running Savana with slurm. pub savana_mem: u8, /// Template for Savana output directory (`{result_dir}`, `{id}`). pub savana_output_dir: String, /// Template for Savana copy number file. /// /// Placeholders: `{output_dir}`, `{id}`, `{reference_name}`, `{haplotagged_bam_tag_name}`. pub savana_copy_number: String, /// Template for Savana raw read counts file. /// /// Same placeholders as [`Config::savana_copy_number`]. pub savana_read_counts: String, /// Template for Savana passed VCF output (`{output_dir}`, `{id}`). pub savana_passed_vcf: String, /// Force Savana recomputation. pub savana_force: bool, /// Template for constitutional phased VCF (`{result_dir}`, `{id}`). pub germline_phased_vcf: String, // === CoRAL === /// Number of CPU threads for the CoRAL reconstruction job. /// /// CoRAL is CPU-bound during breakpoint graph construction and quadratic /// programming cycle extraction. 8–16 threads is sufficient for most /// focal amplification cases; increase for highly complex ecDNA with /// many amplicons. /// /// # TOML /// ```toml /// coral_threads = 16 /// ``` pub coral_threads: u8, pub coral_dir: String, /// Memory allocation for the CoRAL SLURM job (e.g. `"32G"`). /// /// Memory usage scales with amplicon complexity and BAM depth. /// 32G is sufficient for typical WGS at 30–60×; increase to 64G /// for highly rearranged genomes (chromothripsis, high ecDNA copy number). /// /// # TOML /// ```toml /// coral_slurm_mem = "32G" /// ``` pub coral_slurm_mem: String, /// SLURM partition to use for CoRAL jobs. /// /// CoRAL requires only CPU — do not submit to a GPU partition. /// Use the same partition as other CPU-bound callers (e.g. Severus, NanomonSV). /// /// # TOML /// ```toml /// coral_slurm_partition = "cpuq" /// ``` pub coral_slurm_partition: String, /// Minimum copy number gain threshold for a segment to be considered /// a focal amplification seed (CoRAL `--gain`). /// /// CoRAL applies this threshold to the raw absolute CN values from the /// cn_segs BED — do NOT pre-correct for purity or ploidy, as this may /// cause entire chromosome arms to exceed the threshold in aneuploid tumours. /// /// Default in CoRAL is 6.0 (diploid assumption). For hyperdiploid tumours /// (e.g. hyperploid ALL, CML blast crisis) consider lowering to 4.0–5.0. /// /// # TOML /// ```toml /// coral_seed_gain = 6.0 /// ``` pub coral_seed_gain: f64, /// Minimum size in base pairs for a CN segment to qualify as a seed /// (CoRAL `--min-seed-size`). /// /// Segments below this size are discarded even if they exceed `coral_seed_gain`. /// Two merged proximal segments (see `coral_max_seg_gap`) are evaluated /// against this threshold as a single combined interval. /// /// Default in CoRAL is 100000 (100 kb). Reducing this risks including /// artefactual short high-copy segments; increasing it misses small focal /// amplifications (e.g. narrow EGFR or MYC peaks). /// /// # TOML /// ```toml /// coral_min_seed_size = 100000 /// ``` pub coral_min_seed_size: u32, /// Maximum gap in base pairs between two proximal CN segments to allow /// merging into a single seed candidate (CoRAL `--max-seg-gap`). /// /// If two amplified segments are separated by a gap smaller than this value, /// they are merged before the `coral_min_seed_size` filter is applied. /// This handles cases where a single focal amplicon is split by a low-coverage /// or diploid bin. /// /// Default in CoRAL is 300000 (300 kb). For haematological cancers with /// compact focal amplifications (e.g. NUP214::ABL1, ABL1 amplification in /// CML blast crisis) a tighter value such as 100000 reduces spurious merging /// of adjacent independent amplicons. /// /// # TOML /// ```toml /// coral_max_seg_gap = 100000 /// ``` pub coral_max_seg_gap: u32, // === Severus configuration === /// Path to Severus main script (`severus.py`). pub severus_bin: String, /// Force Severus recomputation. pub severus_force: bool, /// Number of threads for Severus. pub severus_threads: u8, /// VNTRs BED file for Severus. pub vntrs_bed: String, /// Path to Severus PoN file (TSV or VCF). pub severus_pon: String, /// Template for Severus tumor/normal (paired) output directory. /// /// Placeholders: `{result_dir}`, `{id}`. pub severus_output_dir: String, /// Template for Severus solo output directory. /// /// Placeholders: `{result_dir}`, `{id}`, `{time}`. pub severus_solo_output_dir: String, // === MARLIN === pub marlin_bed: String, // === Echtvar === pub echtvar_bin: String, pub echtvar_sources: Vec, // === Bcftools configuration === /// Path to Bcftools binary. pub bcftools_bin: String, /// Number of threads for Bcftools. pub bcftools_threads: u8, // === Longphase configuration === /// Path to longphase binary. pub longphase_bin: String, /// Number of threads for longphase. pub longphase_threads: u8, /// Number of threads for longphase modcall step. pub longphase_modcall_threads: u8, /// Force longphase recomputation (haplotagging/phasing). pub longphase_force: bool, /// Template for longphase modcall VCF. /// /// Placeholders: `{result_dir}`, `{id}`, `{time}`. pub longphase_modcall_vcf: String, // === Modkit configuration === /// Path to modkit binary. pub modkit_bin: String, /// Number of threads for `modkit summary`. pub modkit_summary_threads: u8, /// Template for modkit summary output file. /// /// Placeholders: `{result_dir}`, `{id}`, `{time}`. pub modkit_summary_file: String, // === Nanomonsv configuration === /// Path to nanomonsv binary. pub nanomonsv_bin: String, /// Template for paired nanomonsv output directory (`{result_dir}`, `{id}`, `{time}`). pub nanomonsv_output_dir: String, /// Force nanomonsv recomputation. pub nanomonsv_force: bool, /// Number of threads for nanomonsv. pub nanomonsv_threads: u8, /// Template for paired nanomonsv passed VCF (`{output_dir}`, `{id}`). pub nanomonsv_passed_vcf: String, /// Template for solo nanomonsv output directory. /// /// Placeholders: `{result_dir}`, `{id}`, `{time}`. pub nanomonsv_solo_output_dir: String, /// Template for solo nanomonsv passed VCF (`{output_dir}`, `{id}`, `{time}`). pub nanomonsv_solo_passed_vcf: String, pub nanomonsv_simple_repeat_bed: String, pub nanomonsv_line1_bed: String, // === Straglr configuration === /// Path to Straglr executable. pub straglr_bin: String, /// Path to STR loci BED file for Straglr. pub straglr_loci_bed: String, /// Size of reported difference between normal and tumoral pub straglr_min_size_diff: u32, /// Minimum CN of reported difference between normal and tumoral pub straglr_min_support_diff: u32, /// Minimum read support for STR genotyping. pub straglr_min_support: u32, /// Minimum cluster size for STR detection. pub straglr_min_cluster_size: u32, /// Whether to genotype in size mode. pub straglr_genotype_in_size: bool, /// Template for paired Straglr output directory. /// /// Placeholders: `{result_dir}`, `{id}`. pub straglr_output_dir: String, /// Template for solo Straglr output directory. /// /// Placeholders: `{result_dir}`, `{id}`, `{time}`. pub straglr_solo_output_dir: String, /// Force Straglr recomputation. pub straglr_force: bool, // === PromethION runs / metadata === /// Directory containing metadata about PromethION runs. pub promethion_runs_metadata_dir: String, /// JSON file describing PromethION runs and flowcell IDs. pub promethion_runs_input: String, // === VEP === /// Path to VEP singularity image pub vep_image: String, /// Path to the VEP cache directory pub vep_cache_dir: String, /// Path to VEP sorted GFF pub vep_gff: String, // === Flye === /// "python_bin_path flye_bin_path" ex.: "/usr/bin/python /home/t_steimle/somatic_pipe_tools/Flye/bin/flye" pub flye_bin: String, /// Flye threads pub flye_threads: u8, /// Slurm max mem pub flye_slurm_mem: String, pub minimap2_bin: String, pub minimap2_threads: u8, pub minimap2_slurm_mem: String, pub medaka_env: String, pub medaka_consensus_bin: String, pub medaka_threads: u8, pub medaka_slurm_mem: String, pub medaka_model: String, pub wtdbg2_bin: String, pub wtdbg2_threads: u8, pub wtdbg2_slurm_mem: String, // === longcallD configuration === /// Template for the longcallD output directory (solo and normal/tumor runs). /// /// Placeholders: `{result_dir}`, `{id}`, `{time}`. pub longcalld_output_dir: String, pub longcalld_bin: String, pub longcalld_threads: u8, pub longcalld_slurm_mem: String, } #[derive(Debug, Clone, Serialize, Deserialize)] /// Configuration for basecalling and alignment using Dorado and samtools. pub struct AlignConfig { /// Path to Dorado binary. pub dorado_bin: String, /// Arguments passed to `dorado basecaller` (e.g. devices and model name). pub dorado_basecall_arg: String, /// Should dorado re-align after demux ? pub dorado_should_realign: bool, /// Dorado aligner threads number pub dorado_aligner_threads: u8, /// Reference FASTA used for alignment. pub ref_fa: String, /// Minimap2 index (`.mmi`) used by Dorado or downstream tools. pub ref_mmi: String, /// Path to Samtools binary. pub samtools_bin: String, /// Number of threads given to `samtools view`. pub samtools_view_threads: u8, /// Number of threads given to `samtools sort`. pub samtools_sort_threads: u8, /// Number of threads given to `samtools merge`. pub samtools_merge_threads: u8, /// Number of threads given to `samtools split`. pub samtools_split_threads: u8, } // Here comes names that can't be changed from output of tools lazy_static! { /// Template name for DeepVariant VCF outputs. static ref DEEPVARIANT_OUTPUT_NAME: &'static str = "{id}_{time}_DeepVariant.vcf.gz"; /// ClairS main SNP/indel VCF name. static ref CLAIRS_OUTPUT_NAME: &'static str = "output.vcf.gz"; /// ClairS indel-only VCF name. static ref CLAIRS_OUTPUT_INDELS_NAME: &'static str = "indel.vcf.gz"; /// ClairS germline normal VCF name. static ref CLAIRS_GERMLINE_NORMAL: &'static str = "clair3_normal_germline_output.vcf.gz"; /// ClairS germline tumor VCF name. static ref CLAIRS_GERMLINE_TUMOR: &'static str = "clair3_tumor_germline_output.vcf.gz"; } // impl Default for AlignConfig { // fn default() -> Self { // Self { // dorado_bin: "/data/tools/dorado-1.1.1-linux-x64/bin/dorado".to_string(), // dorado_basecall_arg: "-x 'cuda:0,1,2,3' sup,5mC_5hmC".to_string(), // ref_fa: "/data/ref/hs1/chm13v2.0.fa".to_string(), // ref_mmi: "/data/ref/chm13v2.0.mmi".to_string(), // samtools_view_threads: 20, // samtools_sort_threads: 50, // } // } // } // impl Config { /// Returns the config file path, e.g.: /// `~/.local/share/pandora/pandora-config.toml`. pub fn config_path() -> PathBuf { let mut path = directories::ProjectDirs::from("", "", "pandora") .expect("Could not determine project directory") .config_dir() .to_path_buf(); path.push("pandora-config.toml"); path } /// Install the commented template config on disk **if it does not exist yet**. /// /// This writes `CONFIG_TEMPLATE` verbatim so comments are preserved. fn write_template_if_missing() -> Result<(), Box> { let path = Self::config_path(); if path.exists() { // Do not touch an existing user config. return Ok(()); } if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } fs::write(&path, CONFIG_TEMPLATE)?; info!("Config template written to: {}", path.display()); Ok(()) } /// “Save” configuration. /// /// In this model, we do **not** overwrite the user config (to preserve comments). /// `save()` only ensures the template exists on disk on first run. pub fn save(&self) -> Result<(), Box> { Self::write_template_if_missing() } pub fn from_path(path: impl AsRef) -> Self { let path = path.as_ref().to_path_buf(); // First, ensure there is at least a file on disk (template on first run). if let Err(e) = Self::write_template_if_missing() { warn!( "Warning: failed to ensure config template at {}: {}", path.display(), e ); } // Try to load and parse the user config file. match fs::read_to_string(&path) { Ok(content) => match toml::from_str::(&content) { Ok(cfg) => cfg, Err(e) => { warn!( "Warning: failed to parse user config {}: {}. Falling back to embedded template.", path.display(), e ); // Fallback: parse the embedded template. toml::from_str::(CONFIG_TEMPLATE) .expect("embedded config template is invalid") } }, Err(e) => { warn!( "Warning: failed to read user config {}: {}. Falling back to embedded template.", path.display(), e ); toml::from_str::(CONFIG_TEMPLATE) .expect("embedded config template is invalid") } } } /// Returns `//`. #[inline] pub fn tumoral_dir(&self, id: &str) -> String { format!("{}/{}/{}", self.result_dir, id, self.tumoral_name) } /// Returns `//`. #[inline] pub fn normal_dir(&self, id: &str) -> String { format!("{}/{}/{}", self.result_dir, id, self.normal_name) } /// Returns the directory for a "solo" run (timepoint or tag), i.e. `//