Thomas 3 mesiacov pred
rodič
commit
89fea92bf5
5 zmenil súbory, kde vykonal 895 pridanie a 219 odobranie
  1. 91 15
      Cargo.lock
  2. 2 0
      Cargo.toml
  3. 365 0
      pandora-config.example.toml
  4. 424 200
      src/config.rs
  5. 13 4
      src/lib.rs

+ 91 - 15
Cargo.lock

@@ -843,10 +843,10 @@ version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0"
 dependencies = [
- "directories",
+ "directories 5.0.1",
  "serde",
  "thiserror 1.0.69",
- "toml",
+ "toml 0.8.23",
 ]
 
 [[package]]
@@ -1202,6 +1202,15 @@ dependencies = [
  "dirs-sys 0.4.1",
 ]
 
+[[package]]
+name = "directories"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
+dependencies = [
+ "dirs-sys 0.5.0",
+]
+
 [[package]]
 name = "dirs"
 version = "6.0.0"
@@ -1772,6 +1781,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+
 [[package]]
 name = "hashlink"
 version = "0.10.0"
@@ -2031,13 +2046,14 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
 
 [[package]]
 name = "indexmap"
-version = "2.10.0"
+version = "2.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
 dependencies = [
  "equivalent",
- "hashbrown 0.15.5",
+ "hashbrown 0.16.0",
  "serde",
+ "serde_core",
 ]
 
 [[package]]
@@ -2962,6 +2978,7 @@ dependencies = [
  "csv",
  "ctrlc",
  "dashmap",
+ "directories 6.0.0",
  "dirs",
  "duct",
  "env_logger",
@@ -2992,6 +3009,7 @@ dependencies = [
  "serde_json",
  "tar",
  "tempfile",
+ "toml 0.9.8",
  "tracing",
  "uuid",
 ]
@@ -3992,18 +4010,28 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.219"
+version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.219"
+version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -4042,6 +4070,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_spanned"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
+dependencies = [
+ "serde_core",
+]
+
 [[package]]
 name = "serde_v8"
 version = "0.220.0"
@@ -4561,11 +4598,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
 dependencies = [
  "serde",
- "serde_spanned",
- "toml_datetime",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
  "toml_edit",
 ]
 
+[[package]]
+name = "toml"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned 1.0.3",
+ "toml_datetime 0.7.3",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
 [[package]]
 name = "toml_datetime"
 version = "0.6.11"
@@ -4575,6 +4627,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "toml_datetime"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
+dependencies = [
+ "serde_core",
+]
+
 [[package]]
 name = "toml_edit"
 version = "0.22.27"
@@ -4583,18 +4644,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
 dependencies = [
  "indexmap",
  "serde",
- "serde_spanned",
- "toml_datetime",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
  "toml_write",
  "winnow",
 ]
 
+[[package]]
+name = "toml_parser"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
+dependencies = [
+ "winnow",
+]
+
 [[package]]
 name = "toml_write"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
 
+[[package]]
+name = "toml_writer"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
+
 [[package]]
 name = "tracing"
 version = "0.1.41"
@@ -5402,9 +5478,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
 
 [[package]]
 name = "winnow"
-version = "0.7.12"
+version = "0.7.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
+checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
 dependencies = [
  "memchr",
 ]

+ 2 - 0
Cargo.toml

@@ -47,6 +47,8 @@ bitcode = "0.6.5"
 semver = "1.0.26"
 petgraph = "0.8.1"
 regex = "1.12.2"
+toml = "0.9.8"
+directories = "6.0.0"
 
 [profile.dev]
 opt-level = 0

+ 365 - 0
pandora-config.example.toml

@@ -0,0 +1,365 @@
+# Pandora configuration template
+
+#######################################
+# General filesystem layout / I/O
+#######################################
+
+# Directory where POD / run description files are located.
+pod_dir = "/data/run_data"
+
+# Root directory where all results will be written.
+result_dir = "/data/longreads_basic_pipe"
+
+# Temporary directory used when unarchiving input data.
+unarchive_tmp_dir = "/data/unarchived"
+
+# Maximum memory available for dockerized tools, in GiB.
+docker_max_memory_go = 400
+
+# Path to the SQLite database of processed cases.
+db_cases_path = "/data/cases.sqlite"
+
+# Path to the conda activation script.
+conda_sh = "/data/miniconda3/etc/profile.d/conda.sh"
+
+
+
+
+#######################################
+# Reference genome & annotations
+#######################################
+
+# Reference FASTA used throughout the pipeline.
+reference = "/data/ref/hs1/chm13v2.0.fa"
+
+# Short reference name used in filenames.
+reference_name = "hs1"
+
+# Sequence dictionary (.dict) for the reference.
+dict_file = "/data/ref/hs1/chm13v2.0.dict"
+
+# RefSeq GFF3 annotation (sorted/indexed).
+refseq_gff = "/data/ref/hs1/chm13v2.0_RefSeq_Liftoff_v5.1_sorted.gff3.gz"
+
+# Template for mask BED file (low-quality / filtered regions).
+# {result_dir} -> global result directory
+# {id}         -> case identifier
+mask_bed = "{result_dir}/{id}/diag/mask.bed"
+
+# BED file with early-replicating regions.
+early_bed = "/data/ref/hs1/replication_early_25_hs1.bed"
+
+# BED file with late-replicating regions.
+late_bed = "/data/ref/hs1/replication_late_75_hs1.bed"
+
+# BED file with CpG coordinates.
+cpg_bed = "/data/ref/hs1/hs1/hs1_CpG.bed"
+
+# Panels of interest: [ [name, bed_path], ... ]
+panels = [
+  ["OncoT",         "/data/ref/hs1/V1_V2_V3_V4_V5_intersect_targets_hs1_uniq.bed"],
+  ["variable_chips","/data/ref/hs1/top_1500_sd_pos.bed"],
+]
+
+
+#######################################
+# Sample naming / BAM handling
+#######################################
+
+# Tumor sample label (used in paths & filenames).
+tumoral_name = "diag"
+
+# Normal sample label.
+normal_name = "mrd"
+
+# BAM tag name used for haplotagged reads.
+haplotagged_bam_tag_name = "HP"
+
+# Minimum MAPQ for reads kept during BAM filtering.
+bam_min_mapq = 40
+
+# Threads for BAM-level operations (view/sort/index…).
+bam_n_threads = 150
+
+# Number of reads sampled for BAM composition estimation.
+bam_composition_sample_size = 20000
+
+
+#######################################
+# Coverage counting / somatic-scan
+#######################################
+
+# Name of directory (under each sample dir) where counts are stored.
+count_dir_name = "counts"
+
+# Bin size (bp) for count files.
+count_bin_size = 1000
+
+# Number of chunks used to split contigs for counting.
+count_n_chunks = 1000
+
+# Force recomputation of counting even if outputs exist.
+somatic_scan_force = false
+
+
+#######################################
+# Somatic pipeline global settings
+#######################################
+
+# Force recomputation of the entire somatic pipeline.
+somatic_pipe_force = true
+
+# Default thread count for heavy tools.
+somatic_pipe_threads = 150
+
+# Template for somatic pipeline statistics directory.
+# {result_dir}, {id}
+somatic_pipe_stats = "{result_dir}/{id}/diag/somatic_pipe_stats"
+
+
+#######################################
+# Filtering / QC thresholds
+#######################################
+
+# Minimum depth in constitutional sample to consider site evaluable.
+somatic_min_constit_depth = 5
+
+# Maximum allowed ALT count in constitutional sample for a somatic call.
+somatic_max_alt_constit = 1
+
+# Window size (bp) for sequence entropy around variants.
+entropy_seq_len = 10
+
+# Minimum Shannon entropy threshold.
+min_shannon_entropy = 1.0
+
+# Max depth considered "low quality".
+max_depth_low_quality = 20
+
+# Min depth considered "high quality".
+min_high_quality_depth = 14
+
+# Minimum number of callers required to keep a variant.
+min_n_callers = 1
+
+
+#######################################
+# DeepVariant configuration
+#######################################
+
+# DeepVariant output directory template.
+# {result_dir}, {id}, {time}
+deepvariant_output_dir = "{result_dir}/{id}/{time}/DeepVariant"
+
+# Threads for DeepVariant.
+deepvariant_threads = 150
+
+# DeepVariant version / image tag.
+deepvariant_bin_version = "1.9.0"
+
+# DeepVariant model type (e.g. ONT_R104).
+deepvariant_model_type = "ONT_R104"
+
+# Force DeepVariant recomputation.
+deepvariant_force = false
+
+
+#######################################
+# DeepSomatic configuration
+#######################################
+
+# DeepSomatic output directory template.
+# {result_dir}, {id}, {time}
+deepsomatic_output_dir = "{result_dir}/{id}/{time}/DeepSomatic"
+
+# Threads for DeepSomatic.
+deepsomatic_threads = 150
+
+# DeepSomatic version / image tag.
+deepsomatic_bin_version = "1.9.0"
+
+# DeepSomatic model type.
+deepsomatic_model_type = "ONT"
+
+# Force DeepSomatic recomputation.
+deepsomatic_force = false
+
+
+#######################################
+# ClairS configuration
+#######################################
+
+# Threads for ClairS.
+clairs_threads = 155
+
+# ClairS docker tag.
+clairs_docker_tag = "latest"
+
+# Force ClairS recomputation.
+clairs_force = false
+
+# Platform preset for ClairS.
+clairs_platform = "ont_r10_dorado_sup_5khz_ssrs"
+
+# ClairS output directory template.
+# {result_dir}, {id}
+clairs_output_dir = "{result_dir}/{id}/diag/ClairS"
+
+
+#######################################
+# Savana configuration
+#######################################
+
+# Savana binary (name or full path).
+savana_bin = "savana"
+
+# Threads for Savana.
+savana_threads = 150
+
+# Savana output directory template.
+# {result_dir}, {id}
+savana_output_dir = "{result_dir}/{id}/diag/savana"
+
+# Savana copy-number output file.
+# {output_dir}, {id}, {reference_name}, {haplotagged_bam_tag_name}
+savana_copy_number = "{output_dir}/{id}_diag_{reference_name}_{haplotagged_bam_tag_name}_segmented_absolute_copy_number.tsv"
+
+# Savana raw read counts file.
+savana_read_counts = "{output_dir}/{id}_diag_{reference_name}_{haplotagged_bam_tag_name}_raw_read_counts.tsv"
+
+# Savana passed VCF.
+savana_passed_vcf = "{output_dir}/{id}_diag_savana_PASSED.vcf.gz"
+
+# Force Savana recomputation.
+savana_force = false
+
+# Constitutional phased VCF template.
+# {result_dir}, {id}
+germline_phased_vcf = "{result_dir}/{id}/diag/{id}_variants_constit_phased.vcf.gz"
+
+
+#######################################
+# Severus configuration
+#######################################
+
+# Path to Severus script.
+severus_bin = "/data/tools/MySeverus/severus.py"
+
+# Force Severus recomputation.
+severus_force = false
+
+# Threads for Severus.
+severus_threads = 32
+
+# VNTRs BED for Severus.
+vntrs_bed = "/data/ref/hs1/vntrs_chm13.bed"
+
+# Path of the Severus panel of normals.
+severus_pon = "/data/ref/hs1/PoN_1000G_chm13.tsv.gz"
+
+# Paired Severus output directory.
+# {result_dir}, {id}
+severus_output_dir = "{result_dir}/{id}/diag/severus"
+
+# Solo Severus output directory.
+# {result_dir}, {id}, {time}
+severus_solo_output_dir = "{result_dir}/{id}/{time}/severus"
+
+
+#######################################
+# Longphase configuration
+#######################################
+
+# Path to longphase binary.
+longphase_bin = "/data/tools/longphase_linux-x64"
+
+# Threads for longphase.
+longphase_threads = 150
+
+# Threads for longphase modcall step.
+longphase_modcall_threads = 8
+
+# Longphase modcall VCF template.
+# {result_dir}, {id}, {time}
+longphase_modcall_vcf = "{result_dir}/{id}/{time}/5mC_5hmC/{id}_{time}_5mC_5hmC_modcall.vcf.gz"
+
+
+#######################################
+# Modkit configuration
+#######################################
+
+# Path to modkit binary.
+modkit_bin = "modkit"
+
+# Threads for `modkit summary`.
+modkit_summary_threads = 50
+
+# Modkit summary file template.
+# {result_dir}, {id}, {time}
+modkit_summary_file = "{result_dir}/{id}/{time}/{id}_{time}_5mC_5hmC_summary.txt"
+
+
+#######################################
+# Nanomonsv configuration
+#######################################
+
+# Path to nanomonsv binary.
+nanomonsv_bin = "/home/prom/.local/bin/nanomonsv"
+
+# Paired nanomonsv output directory template.
+# {result_dir}, {id}, {time}
+nanomonsv_output_dir = "{result_dir}/{id}/{time}/nanomonsv"
+
+# Force nanomonsv recomputation.
+nanomonsv_force = false
+
+# Threads for nanomonsv.
+nanomonsv_threads = 150
+
+# Paired nanomonsv PASSED VCF template.
+# {output_dir}, {id}
+nanomonsv_passed_vcf = "{output_dir}/{id}_diag_nanomonsv_PASSED.vcf.gz"
+
+# Solo nanomonsv output directory template.
+# {result_dir}, {id}, {time}
+nanomonsv_solo_output_dir = "{result_dir}/{id}/{time}/nanomonsv-solo"
+
+# Solo nanomonsv PASSED VCF template.
+# {output_dir}, {id}, {time}
+nanomonsv_solo_passed_vcf = "{output_dir}/{id}_{time}_nanomonsv-solo_PASSED.vcf.gz"
+
+
+#######################################
+# PromethION metadata
+#######################################
+
+# Directory containing PromethION run metadata.
+promethion_runs_metadata_dir = "/data/promethion-runs-metadata"
+
+# JSON file mapping flowcell IDs / runs for Pandora.
+promethion_runs_input = "/data/pandora-flowcell-id.json"
+
+
+#######################################
+# Alignment / basecalling (Dorado)
+#######################################
+
+[align]
+# Path to Dorado binary.
+dorado_bin = "/data/tools/dorado-1.1.1-linux-x64/bin/dorado"
+
+# Dorado basecalling arguments (device, model, modifications…).
+dorado_basecall_arg = "-x 'cuda:0,1,2,3' sup,5mC_5hmC"
+
+# Reference FASTA used for alignment.
+ref_fa = "/data/ref/hs1/chm13v2.0.fa"
+
+# Minimap2 index used for alignment.
+ref_mmi = "/data/ref/chm13v2.0.mmi"
+
+# Threads for `samtools view`.
+samtools_view_threads = 20
+
+# Threads for `samtools sort`.
+samtools_sort_threads = 50
+

+ 424 - 200
src/config.rs

@@ -1,245 +1,340 @@
-#[derive(Debug, Clone)]
+use log::{info, warn};
+use serde::{Deserialize, Serialize};
+use std::fs;
+use std::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 ===
+    /// Directory where POD / run description files are located.
     pub pod_dir: String,
+
+    /// Root directory where all results will be written.
     pub result_dir: String,
+
+    /// Temporary directory used when unarchiving input data.
     pub unarchive_tmp_dir: String,
+
+    /// Maximum memory available for dockerized tools, in GiB.
+    pub docker_max_memory_go: u16,
+
+    /// Path to the SQLite database of processed cases.
+    pub db_cases_path: 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 BAM processing steps (view, sort, index…).
+    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,
+
+    /// 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 docker_max_memory_go: u16,
-    pub savana_bin: String,
-    pub savana_threads: u8,
+
+    /// BED template used to mask low-quality or filtered regions.
+    ///
+    /// Placeholders:
+    /// - `{result_dir}`: global result directory
+    /// - `{id}`: case identifier
+    pub mask_bed: String,
+
+    /// BED file with CpG coordinates in the reference.
+    pub cpg_bed: String,
+
+    /// BED file with early-replicating regions (used for replication timing–based analyses).
+    pub early_bed: String,
+
+    /// BED file with late-replicating regions.
+    pub late_bed: String,
+
+    /// Panels of interest (name, BED path).
+    pub panels: Vec<(String, 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,
+
+    /// 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 docker / binary version.
+    pub deepvariant_bin_version: 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 docker / binary version.
+    pub deepsomatic_bin_version: 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,
+
+    /// ClairS docker tag.
+    pub clairs_docker_tag: String,
+
+    /// Force ClairS recomputation.
+    pub clairs_force: 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,
+
+    // === Savana configuration ===
+    /// Savana binary name or full path.
+    pub savana_bin: String,
+
+    /// Number of threads for Savana.
+    pub savana_threads: 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,
-    pub germline_phased_vcf: String,
+
+    /// Template for Savana passed VCF output (`{output_dir}`, `{id}`).
     pub savana_passed_vcf: String,
-    pub conda_sh: String,
+
+    /// Force Savana recomputation.
     pub savana_force: bool,
-    pub deepvariant_output_dir: String,
+
+    /// Template for constitutional phased VCF (`{result_dir}`, `{id}`).
+    pub germline_phased_vcf: String,
+
+    // === 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,
+
+    // === 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,
+
+    /// 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,
-    pub longphase_modcall_threads: u8,
-    pub deepvariant_threads: u8,
-    pub deepvariant_bin_version: String,
-    pub deepvariant_model_type: String,
-    pub deepvariant_force: bool,
-    pub deepsomatic_output_dir: String,
-    pub deepsomatic_threads: u8,
-    pub deepsomatic_bin_version: String,
-    pub deepsomatic_model_type: String,
-    pub deepsomatic_force: bool,
-    pub bam_min_mapq: u8,
-    pub bam_n_threads: u8,
-    pub db_cases_path: String,
-    pub somatic_pipe_stats: String,
 
-    pub clairs_threads: u8,
-    pub clairs_docker_tag: String,
-    pub clairs_force: bool,
-    pub clairs_platform: String,
-    pub clairs_output_dir: String,
-    pub mask_bed: String,
-    pub somatic_min_constit_depth: u16,
-    pub somatic_max_alt_constit: u16,
-    pub entropy_seq_len: usize,
-    pub min_shannon_entropy: f64,
+    // === 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 somatic_pipe_force: bool,
-    pub somatic_pipe_threads: u8,
-    pub min_high_quality_depth: u32,
-    pub min_n_callers: u8,
-    pub somatic_scan_force: bool,
-    pub early_bed: String,
-    pub late_bed: String,
-    pub panels: Vec<(String, String)>,
-    pub cpg_bed: String,
-    pub max_depth_low_quality: u32,
-    pub bam_composition_sample_size: u32,
-    pub promethion_runs_metadata_dir: String,
-    pub promethion_runs_input: String,
-}
 
-// Here comes names that can't be changed from output of tools
-lazy_static! {
-    static ref DEEPVARIANT_OUTPUT_NAME: &'static str = "{id}_{time}_DeepVariant.vcf.gz";
-    static ref CLAIRS_OUTPUT_NAME: &'static str = "output.vcf.gz";
-    static ref CLAIRS_OUTPUT_INDELS_NAME: &'static str = "indel.vcf.gz";
-    static ref CLAIRS_GERMLINE_NORMAL: &'static str = "clair3_normal_germline_output.vcf.gz";
-    static ref CLAIRS_GERMLINE_TUMOR: &'static str = "clair3_tumor_germline_output.vcf.gz";
-}
+    // === PromethION runs / metadata ===
+    /// Directory containing metadata about PromethION runs.
+    pub promethion_runs_metadata_dir: String,
 
-impl Default for Config {
-    fn default() -> Self {
-        Self {
-            pod_dir: "/data/run_data".to_string(),
-            align: Default::default(),
-
-            // Reference genome
-            reference: "/data/ref/hs1/chm13v2.0.fa".to_string(),
-            reference_name: "hs1".to_string(),
-            dict_file: "/data/ref/hs1/chm13v2.0.dict".to_string(),
-            refseq_gff: "/data/ref/hs1/chm13v2.0_RefSeq_Liftoff_v5.1_sorted.gff3.gz".to_string(),
-
-            docker_max_memory_go: 400,
-
-            // File structure
-            result_dir: "/data/longreads_basic_pipe".to_string(),
-            unarchive_tmp_dir: "/data/unarchived".to_string(),
-
-            tumoral_name: "diag".to_string(),
-            normal_name: "mrd".to_string(),
-            haplotagged_bam_tag_name: "HP".to_string(),
-
-            count_dir_name: "counts".to_string(),
-            count_bin_size: 1_000,
-            count_n_chunks: 1_000,
-
-            bam_min_mapq: 40,
-            bam_n_threads: 150,
-            bam_composition_sample_size: 20_000,
-
-            promethion_runs_metadata_dir: "/data/promethion-runs-metadata".to_string(),
-            promethion_runs_input: "/data/pandora-flowcell-id.json".to_string(),
-
-            db_cases_path: "/data/cases.sqlite".to_string(),
-
-            //
-            mask_bed: "{result_dir}/{id}/diag/mask.bed".to_string(),
-
-            germline_phased_vcf: "{result_dir}/{id}/diag/{id}_variants_constit_phased.vcf.gz"
-            .to_string(),
-            conda_sh: "/data/miniconda3/etc/profile.d/conda.sh".to_string(),
-
-            somatic_pipe_stats: "{result_dir}/{id}/diag/somatic_pipe_stats"
-            .to_string(),
-
-            // DeepVariant
-            deepvariant_output_dir: "{result_dir}/{id}/{time}/DeepVariant".to_string(),
-            deepvariant_threads: 150,
-            deepvariant_bin_version: "1.9.0".to_string(),
-            deepvariant_model_type: "ONT_R104".to_string(),
-            deepvariant_force: false,
-
-            // DeepSomatic
-            deepsomatic_output_dir: "{result_dir}/{id}/{time}/DeepSomatic".to_string(),
-            deepsomatic_threads: 150,
-            deepsomatic_bin_version: "1.9.0".to_string(),
-            deepsomatic_model_type: "ONT".to_string(),
-            deepsomatic_force: false,
-
-            // ClairS
-            clairs_output_dir: "{result_dir}/{id}/diag/ClairS".to_string(),
-            clairs_docker_tag: "latest".to_string(),
-            clairs_threads: 155,
-            clairs_platform: "ont_r10_dorado_sup_5khz_ssrs".to_string(),
-            clairs_force: false,
-
-            // Savana
-            savana_bin: "savana".to_string(),
-            // savana_bin: "/home/prom/.local/bin/savana".to_string(),
-            savana_threads: 150,
-            savana_output_dir: "{result_dir}/{id}/diag/savana".to_string(),
-            savana_passed_vcf: "{output_dir}/{id}_diag_savana_PASSED.vcf.gz".to_string(),
-            savana_copy_number: "{output_dir}/{id}_diag_{reference_name}_{haplotagged_bam_tag_name}_segmented_absolute_copy_number.tsv".to_string(),
-            savana_read_counts: "{output_dir}/{id}_diag_{reference_name}_{haplotagged_bam_tag_name}_raw_read_counts.tsv".to_string(),
-            savana_force: false,
-
-            // Severus
-            severus_bin: "/data/tools/MySeverus/severus.py".to_string(),
-            severus_threads: 32,
-            vntrs_bed: "/data/ref/hs1/vntrs_chm13.bed".to_string(),
-            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: false,
-
-            // 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(),
-
-            // Nanomonsv
-            // tabix, bgzip, mafft in PATH
-            // pip install pysam, parasail;  pip install nanomonsv
-            nanomonsv_bin: "/home/prom/.local/bin/nanomonsv".to_string(),
-            nanomonsv_output_dir: "{result_dir}/{id}/{time}/nanomonsv".to_string(),
-            nanomonsv_threads: 150,
-            nanomonsv_force: false,
-            nanomonsv_passed_vcf: "{output_dir}/{id}_diag_nanomonsv_PASSED.vcf.gz".to_string(),
-
-            nanomonsv_solo_output_dir: "{result_dir}/{id}/{time}/nanomonsv-solo".to_string(),
-            nanomonsv_solo_passed_vcf: "{output_dir}/{id}_{time}_nanomonsv-solo_PASSED.vcf.gz"
-                .to_string(),
-
-            // Scan
-            somatic_scan_force: false,
-
-            // Pipe
-            somatic_pipe_force: true,
-            somatic_pipe_threads: 150,
-            somatic_min_constit_depth: 5,
-            somatic_max_alt_constit: 1,
-            entropy_seq_len: 10,
-            min_shannon_entropy: 1.0,
-
-            max_depth_low_quality: 20,
-            min_high_quality_depth: 14, 
-            min_n_callers: 1,
-            early_bed: "/data/ref/hs1/replication_early_25_hs1.bed".to_string(),
-            late_bed: "/data/ref/hs1/replication_late_75_hs1.bed".to_string(),
-            panels: vec![
-                ("OncoT".to_string(), "/data/ref/hs1/V1_V2_V3_V4_V5_intersect_targets_hs1_uniq.bed".to_string()),
-                ("variable_chips".to_string(), "/data/ref/hs1/top_1500_sd_pos.bed".to_string()),
-            ],
-            cpg_bed: "/data/ref/hs1/hs1/hs1_CpG.bed".to_string(),
-        }
-    }
+    /// JSON file describing PromethION runs and flowcell IDs.
+    pub promethion_runs_input: String,
 }
 
-#[derive(Debug, Clone)]
+#[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,
+
+    /// Reference FASTA used for alignment.
     pub ref_fa: String,
+
+    /// Minimap2 index (`.mmi`) used by Dorado or downstream tools.
     pub ref_mmi: String,
+
+    /// Number of threads given to `samtools view`.
     pub samtools_view_threads: u16,
+
+    /// Number of threads given to `samtools sort`.
     pub samtools_sort_threads: u16,
 }
 
+// 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 {
@@ -254,18 +349,66 @@ impl Default for AlignConfig {
 }
 
 impl Config {
+    /// Returns the config file path, e.g.:
+    /// `~/.local/share/pandora/pandora-config.toml`.
+    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<dyn std::error::Error>> {
+        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<dyn std::error::Error>> {
+        Self::write_template_if_missing()
+    }
+
+    /// Returns `<result_dir>/<id>/<tumoral_name>`.
+    #[inline]
     pub fn tumoral_dir(&self, id: &str) -> String {
         format!("{}/{}/{}", self.result_dir, id, self.tumoral_name)
     }
 
+    /// Returns `<result_dir>/<id>/<normal_name>`.
+    #[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. `<result_dir>/<id>/<time>`.
+    #[inline]
     pub fn solo_dir(&self, id: &str, time: &str) -> String {
         format!("{}/{}/{}", self.result_dir, id, time)
     }
 
+    /// BAM for a solo run: `<solo_dir>/<id>_<time>_<reference_name>.bam`.
     pub fn solo_bam(&self, id: &str, time: &str) -> String {
         format!(
             "{}/{}_{}_{}.bam",
@@ -276,6 +419,7 @@ impl Config {
         )
     }
 
+    /// JSON sidecar for the solo BAM.
     pub fn solo_bam_info_json(&self, id: &str, time: &str) -> String {
         format!(
             "{}/{}_{}_{}_info.json",
@@ -286,6 +430,7 @@ impl Config {
         )
     }
 
+    /// Tumor BAM path: `<tumoral_dir>/<id>_<tumoral_name>_<reference_name>.bam`.
     pub fn tumoral_bam(&self, id: &str) -> String {
         format!(
             "{}/{}_{}_{}.bam",
@@ -296,6 +441,7 @@ impl Config {
         )
     }
 
+    /// Normal BAM path: `<normal_dir>/<id>_<normal_name>_<reference_name>.bam`.
     pub fn normal_bam(&self, id: &str) -> String {
         format!(
             "{}/{}_{}_{}.bam",
@@ -306,6 +452,7 @@ impl Config {
         )
     }
 
+    /// Tumor haplotagged BAM.
     pub fn tumoral_haplotagged_bam(&self, id: &str) -> String {
         format!(
             "{}/{}_{}_{}_{}.bam",
@@ -317,6 +464,7 @@ impl Config {
         )
     }
 
+    /// Normal haplotagged BAM.
     pub fn normal_haplotagged_bam(&self, id: &str) -> String {
         format!(
             "{}/{}_{}_{}_{}.bam",
@@ -328,33 +476,38 @@ impl Config {
         )
     }
 
+    /// Normal count directory: `<normal_dir>/counts`.
     pub fn normal_dir_count(&self, id: &str) -> String {
         format!("{}/{}", self.normal_dir(id), self.count_dir_name)
     }
 
+    /// Tumor count directory: `<tumoral_dir>/counts`.
     pub fn tumoral_dir_count(&self, id: &str) -> String {
         format!("{}/{}", self.tumoral_dir(id), self.count_dir_name)
     }
 
+    /// Mask BED path with `{result_dir}` and `{id}` expanded.
     pub fn mask_bed(&self, id: &str) -> String {
         self.mask_bed
             .replace("{result_dir}", &self.result_dir)
             .replace("{id}", id)
     }
 
+    /// Germline phased VCF with `{result_dir}` and `{id}` expanded.
     pub fn germline_phased_vcf(&self, id: &str) -> String {
         self.germline_phased_vcf
             .replace("{result_dir}", &self.result_dir)
             .replace("{id}", id)
     }
 
+    /// Somatic pipeline stats directory with `{result_dir}` and `{id}` expanded.
     pub fn somatic_pipe_stats(&self, id: &str) -> String {
         self.somatic_pipe_stats
             .replace("{result_dir}", &self.result_dir)
             .replace("{id}", id)
     }
 
-    // DeepVariant
+    /// DeepVariant output directory for a given run (`{result_dir}`, `{id}`, `{time}`).
     pub fn deepvariant_output_dir(&self, id: &str, time: &str) -> String {
         self.deepvariant_output_dir
             .replace("{result_dir}", &self.result_dir)
@@ -362,6 +515,7 @@ impl Config {
             .replace("{time}", time)
     }
 
+    /// DeepVariant solo VCF (raw) for `<id>, <time>`.
     pub fn deepvariant_solo_output_vcf(&self, id: &str, time: &str) -> String {
         format!(
             "{}/{}",
@@ -372,14 +526,17 @@ impl Config {
         .replace("{time}", time)
     }
 
+    /// DeepVariant output directory for the normal sample.
     pub fn deepvariant_normal_output_dir(&self, id: &str) -> String {
         self.deepvariant_output_dir(id, &self.normal_name)
     }
 
+    /// DeepVariant "tumoral output dir" (as in your original code – note: this actually returns the *PASSED VCF* path).
     pub fn deepvariant_tumoral_output_dir(&self, id: &str) -> String {
         self.deepvariant_solo_passed_vcf(id, &self.tumoral_name)
     }
 
+    /// DeepVariant solo *PASSED* VCF for `<id>, <time>`.
     pub fn deepvariant_solo_passed_vcf(&self, id: &str, time: &str) -> String {
         format!(
             "{}/{}_{}_DeepVariant_PASSED.vcf.gz",
@@ -389,15 +546,17 @@ impl Config {
         )
     }
 
+    /// DeepVariant *PASSED* VCF for the normal sample.
     pub fn deepvariant_normal_passed_vcf(&self, id: &str) -> String {
         self.deepvariant_solo_passed_vcf(id, &self.normal_name)
     }
 
+    /// DeepVariant *PASSED* VCF for the tumor sample.
     pub fn deepvariant_tumoral_passed_vcf(&self, id: &str) -> String {
         self.deepvariant_solo_passed_vcf(id, &self.tumoral_name)
     }
 
-    // DeepSomatic
+    /// DeepSomatic output directory (uses `{time} = tumoral_name`).
     pub fn deepsomatic_output_dir(&self, id: &str) -> String {
         self.deepsomatic_output_dir
             .replace("{result_dir}", &self.result_dir)
@@ -405,6 +564,7 @@ impl Config {
             .replace("{time}", &self.tumoral_name)
     }
 
+    /// DeepSomatic raw VCF.
     pub fn deepsomatic_output_vcf(&self, id: &str) -> String {
         format!(
             "{}/{}_{}_DeepSomatic.vcf.gz",
@@ -414,6 +574,7 @@ impl Config {
         )
     }
 
+    /// DeepSomatic *PASSED* VCF.
     pub fn deepsomatic_passed_vcf(&self, id: &str) -> String {
         format!(
             "{}/{}_{}_DeepSomatic_PASSED.vcf.gz",
@@ -423,13 +584,14 @@ impl Config {
         )
     }
 
-    // ClairS
+    /// ClairS output directory (`{result_dir}`, `{id}`).
     pub fn clairs_output_dir(&self, id: &str) -> String {
         self.clairs_output_dir
             .replace("{result_dir}", &self.result_dir)
             .replace("{id}", id)
     }
 
+    /// ClairS main SNP/indel VCFs (standard + indel-only).
     pub fn clairs_output_vcfs(&self, id: &str) -> (String, String) {
         let dir = self.clairs_output_dir(id);
         (
@@ -438,6 +600,7 @@ impl Config {
         )
     }
 
+    /// ClairS somatic *PASSED* VCF.
     pub fn clairs_passed_vcf(&self, id: &str) -> String {
         format!(
             "{}/{}_{}_clairs_PASSED.vcf.gz",
@@ -447,22 +610,25 @@ impl Config {
         )
     }
 
+    /// ClairS germline normal VCF.
     pub fn clairs_germline_normal_vcf(&self, id: &str) -> String {
         let dir = self.clairs_output_dir(id);
         format!("{dir}/{}", *CLAIRS_GERMLINE_NORMAL)
     }
 
+    /// ClairS germline tumor VCF.
     pub fn clairs_germline_tumor_vcf(&self, id: &str) -> String {
         let dir = self.clairs_output_dir(id);
         format!("{dir}/{}", *CLAIRS_GERMLINE_TUMOR)
     }
 
+    /// Consolidated germline *PASSED* VCF from ClairS.
     pub fn clairs_germline_passed_vcf(&self, id: &str) -> String {
         let dir = self.clairs_output_dir(id);
         format!("{dir}/{id}_diag_clair3-germline_PASSED.vcf.gz")
     }
 
-    // Nanomonsv
+    /// Paired nanomonsv output directory.
     pub fn nanomonsv_output_dir(&self, id: &str, time: &str) -> String {
         self.nanomonsv_output_dir
             .replace("{result_dir}", &self.result_dir)
@@ -470,13 +636,14 @@ impl Config {
             .replace("{time}", time)
     }
 
+    /// Paired nanomonsv *PASSED* VCF.
     pub fn nanomonsv_passed_vcf(&self, id: &str) -> String {
         self.nanomonsv_passed_vcf
             .replace("{output_dir}", &self.nanomonsv_output_dir(id, "diag"))
             .replace("{id}", id)
     }
 
-    // Nanomonsv solo
+    /// Solo nanomonsv output directory.
     pub fn nanomonsv_solo_output_dir(&self, id: &str, time: &str) -> String {
         self.nanomonsv_solo_output_dir
             .replace("{result_dir}", &self.result_dir)
@@ -484,6 +651,7 @@ impl Config {
             .replace("{time}", time)
     }
 
+    /// Solo nanomonsv *PASSED* VCF.
     pub fn nanomonsv_solo_passed_vcf(&self, id: &str, time: &str) -> String {
         self.nanomonsv_solo_passed_vcf
             .replace("{output_dir}", &self.nanomonsv_solo_output_dir(id, time))
@@ -491,13 +659,14 @@ impl Config {
             .replace("{time}", time)
     }
 
-    // Savana
+    /// Savana output directory (`{result_dir}`, `{id}`).
     pub fn savana_output_dir(&self, id: &str) -> String {
         self.savana_output_dir
             .replace("{result_dir}", &self.result_dir)
             .replace("{id}", id)
     }
 
+    /// Savana main somatic VCF (classified).
     pub fn savana_output_vcf(&self, id: &str) -> String {
         let output_dir = self.savana_output_dir(id);
 
@@ -507,13 +676,14 @@ impl Config {
         )
     }
 
+    /// Savana *PASSED* VCF.
     pub fn savana_passed_vcf(&self, id: &str) -> String {
         self.savana_passed_vcf
             .replace("{output_dir}", &self.savana_output_dir(id))
             .replace("{id}", id)
     }
 
-    // {output_dir}/{id}_diag_{reference_name}_{haplotagged_bam_tag_name}
+    /// Savana read counts file.
     pub fn savana_read_counts(&self, id: &str) -> String {
         self.savana_read_counts
             .replace("{output_dir}", &self.savana_output_dir(id))
@@ -522,6 +692,7 @@ impl Config {
             .replace("{haplotagged_bam_tag_name}", &self.haplotagged_bam_tag_name)
     }
 
+    /// Savana copy-number file.
     pub fn savana_copy_number(&self, id: &str) -> String {
         self.savana_copy_number
             .replace("{output_dir}", &self.savana_output_dir(id))
@@ -530,18 +701,20 @@ impl Config {
             .replace("{haplotagged_bam_tag_name}", &self.haplotagged_bam_tag_name)
     }
 
-    // Severus
+    /// Severus paired output directory.
     pub fn severus_output_dir(&self, id: &str) -> String {
         self.severus_output_dir
             .replace("{result_dir}", &self.result_dir)
             .replace("{id}", id)
     }
 
+    /// Severus somatic SV VCF (paired).
     pub fn severus_output_vcf(&self, id: &str) -> String {
         let output_dir = self.severus_output_dir(id);
         format!("{output_dir}/somatic_SVs/severus_somatic.vcf")
     }
 
+    /// Severus *PASSED* VCF (paired).
     pub fn severus_passed_vcf(&self, id: &str) -> String {
         format!(
             "{}/{}_diag_severus_PASSED.vcf.gz",
@@ -550,7 +723,7 @@ impl Config {
         )
     }
 
-    // Severus solo
+    /// Severus solo output directory.
     pub fn severus_solo_output_dir(&self, id: &str, time: &str) -> String {
         self.severus_solo_output_dir
             .replace("{result_dir}", &self.result_dir)
@@ -558,11 +731,13 @@ impl Config {
             .replace("{time}", time)
     }
 
+    /// Severus solo SV VCF.
     pub fn severus_solo_output_vcf(&self, id: &str, time: &str) -> String {
         let output_dir = self.severus_solo_output_dir(id, time);
         format!("{output_dir}/all_SVs/severus_all.vcf")
     }
 
+    /// Severus solo *PASSED* VCF.
     pub fn severus_solo_passed_vcf(&self, id: &str, time: &str) -> String {
         format!(
             "{}/{}_{}_severus-solo_PASSED.vcf.gz",
@@ -572,11 +747,12 @@ impl Config {
         )
     }
 
+    /// Alias for the constitutional germline VCF.
     pub fn constit_vcf(&self, id: &str) -> String {
         self.clairs_germline_passed_vcf(id)
-        // format!("{}/{}_variants_constit.vcf.gz", self.tumoral_dir(id), id)
     }
 
+    /// Constitutional phased VCF path in the tumor directory.
     pub fn constit_phased_vcf(&self, id: &str) -> String {
         format!(
             "{}/{}_variants_constit_phased.vcf.gz",
@@ -585,19 +761,22 @@ impl Config {
         )
     }
 
-    // SomaticScan
+    /// Somatic-scan output directory for a solo run (counts subdir).
     pub fn somatic_scan_solo_output_dir(&self, id: &str, time: &str) -> String {
         format!("{}/counts", self.solo_dir(id, time))
     }
 
+    /// Somatic-scan output dir for the normal sample.
     pub fn somatic_scan_normal_output_dir(&self, id: &str) -> String {
         self.somatic_scan_solo_output_dir(id, &self.normal_name)
     }
 
+    /// Somatic-scan output dir for the tumor sample.
     pub fn somatic_scan_tumoral_output_dir(&self, id: &str) -> String {
         self.somatic_scan_solo_output_dir(id, &self.tumoral_name)
     }
 
+    /// Somatic-scan count file for a given contig in a solo run.
     pub fn somatic_scan_solo_count_file(&self, id: &str, time: &str, contig: &str) -> String {
         format!(
             "{}/{}_count.tsv.gz",
@@ -606,15 +785,17 @@ impl Config {
         )
     }
 
+    /// Somatic-scan count file (normal) for a given contig.
     pub fn somatic_scan_normal_count_file(&self, id: &str, contig: &str) -> String {
         self.somatic_scan_solo_count_file(id, &self.normal_name, contig)
     }
 
+    /// Somatic-scan count file (tumor) for a given contig.
     pub fn somatic_scan_tumoral_count_file(&self, id: &str, contig: &str) -> String {
         self.somatic_scan_solo_count_file(id, &self.tumoral_name, contig)
     }
 
-    // Modkit
+    /// Modkit summary file (`{result_dir}`, `{id}`, `{time}`).
     pub fn modkit_summary_file(&self, id: &str, time: &str) -> String {
         self.modkit_summary_file
             .replace("{result_dir}", &self.result_dir)
@@ -622,6 +803,7 @@ impl Config {
             .replace("{time}", time)
     }
 
+    /// Longphase modcall VCF (`{result_dir}`, `{id}`, `{time}`).
     pub fn longphase_modcall_vcf(&self, id: &str, time: &str) -> String {
         self.longphase_modcall_vcf
             .replace("{result_dir}", &self.result_dir)
@@ -629,3 +811,45 @@ impl Config {
             .replace("{time}", time)
     }
 }
+
+impl Default for Config {
+    fn default() -> Self {
+        let path = Self::config_path();
+
+        // 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::<Config>(&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>(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>(CONFIG_TEMPLATE)
+                    .expect("embedded config template is invalid")
+            }
+        }
+    }
+}
+

+ 13 - 4
src/lib.rs

@@ -184,21 +184,30 @@ mod tests {
     // export RUST_LOG="debug"
     fn init() {
         let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
-            .is_test(true)
             .try_init();
     }
 
     #[test]
     fn it_works() {
-        let bam_path = "/data/longreads_basic_pipe/PARACHINI/diag/PARACHINI_diag_hs1.bam";
-        modkit::modkit(bam_path);
+        init();
+        info!("hello");
+        warn!("hello");
+        let v: toml::Value = toml::from_str(include_str!("../pandora-config.example.toml")).unwrap();
+        println!("kjljl");
+        info!("{:#?}", v);
+                warn!("hello");
+
+
+        // let bam_path = "/data/longreads_basic_pipe/PARACHINI/diag/PARACHINI_diag_hs1.bam";
+        // modkit::modkit(bam_path);
     }
 
     #[test]
     fn run_dorado() -> anyhow::Result<()> {
+        init();
         let case = FlowCellCase { 
             id: "CONSIGNY".to_string(), 
-            time_point: "mrd".to_string(), barcode: "07".to_string(), pod_dir: "/data/run_data/20240326-CL/CONSIGNY-MRD-NB07_RICCO-DIAG-NB08/20240326_1355_1E_PAU78333_bc25da25/pod5_pass/barcode07".into() 
+            time_point: "mrd".to_string(), barcode: "07".to_string(), pod_dir: "/mnt/beegfs01/ data/run_data/20240326-CL/CONSIGNY-MRD-NB07_RICCO-DIAG-NB08/20240326_1355_1E_PAU78333_bc25da25/pod5_pass/barcode07".into() 
         };
         dorado::Dorado::init(case, Config::default())?.run_pipe()
     }