somatic.rs 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. //! # SomaticPipe
  2. //!
  3. //! End-to-end somatic variant calling and annotation pipeline.
  4. //!
  5. //! `SomaticPipe` orchestrates the complete workflow for detecting,
  6. //! filtering, annotating, merging, and exporting high-confidence
  7. //! somatic variants from paired tumor/normal sequencing data.
  8. //!
  9. //! ## Main Features
  10. //!
  11. //! - Multi-caller somatic variant integration
  12. //! - Germline and constitutional filtering
  13. //! - BAM depth and allele support annotation
  14. //! - Sequence complexity and entropy filtering
  15. //! - External annotation (COSMIC, GnomAD, VEP)
  16. //! - Structural insertion annotation via NanomonSV
  17. //! - Export to JSON, VCF, BIT, and Pandora container formats
  18. //! - Detailed QC and filtering statistics
  19. //!
  20. //! ## Supported Variant Callers
  21. //!
  22. //! - ClairS
  23. //! - DeepSomatic
  24. //! - DeepVariant
  25. //! - NanomonSV
  26. //! - SAVANA
  27. //! - Severus
  28. //!
  29. //! ## Pipeline Overview
  30. //!
  31. //! 1. Initialize prerequisite components
  32. //! 2. Load somatic and germline variants
  33. //! 3. Remove germline/constitutional calls
  34. //! 4. Annotate constitutional BAM support
  35. //! 5. Filter low-depth and high-alt variants
  36. //! 6. Annotate sequence context and entropy
  37. //! 7. Annotate external databases (COSMIC/GnomAD)
  38. //! 8. Filter likely polymorphisms
  39. //! 9. Annotate VEP consequences
  40. //! 10. Merge and export final variants
  41. //!
  42. //! ## Outputs
  43. //!
  44. //! - `*_somatic_variants.json.gz`
  45. //! - `*_somatic_variants.bit`
  46. //! - `*_somatic_variants.vcf.gz`
  47. //! - `*.pandora`
  48. //! - QC/statistics JSON reports
  49. //!
  50. //! ## Notes
  51. //!
  52. //! The pipeline conditionally executes missing upstream analyses
  53. //! and tracks filtering statistics throughout processing.
  54. //!
  55. //! This module also defines:
  56. //!
  57. //! - [`SomaticPipeStats`]
  58. //! - [`InputStats`]
  59. //! - Pandora export helpers
  60. //! - Annotation matrix generation utilities
  61. use crate::{
  62. annotation::{
  63. dbsnp::annotate_dbsnp, is_dbsnp_and_constit_alt, is_gnomad_and_constit_alt, Caller,
  64. },
  65. create_should_run_normal_tumoral, init_solo_callers_normal_tumoral,
  66. io::bed::read_bed,
  67. pipes::{Initialize, InitializeSolo, ShouldRun},
  68. positions::{GenomeRange, GenomeRangeExt},
  69. scan::scan::SomaticScan,
  70. variant::{
  71. variants_stats::somatic_depth_quality_ranges,
  72. vcf_variant::{run_if_required, ShouldRunBox},
  73. },
  74. };
  75. use log::info;
  76. use rayon::slice::ParallelSliceMut;
  77. use serde::{Deserialize, Serialize};
  78. use std::{
  79. collections::BTreeMap,
  80. fs::{self, File},
  81. path::{Path, PathBuf},
  82. };
  83. use crate::{
  84. annotation::{Annotation, Annotations, AnnotationsStats, Sample},
  85. callers::{
  86. clairs::ClairS,
  87. deep_somatic::DeepSomatic,
  88. deep_variant::DeepVariant,
  89. nanomonsv::NanomonSV,
  90. savana::{Savana, SavanaCN},
  91. severus::Severus,
  92. },
  93. collection::bam_stats::WGSBamStats,
  94. config::Config,
  95. create_should_run, init_somatic_callers,
  96. io::somaticpipe_container::{
  97. bam_qc_section, copy_number_section, pipe_qc_section, provenance_section,
  98. variant_records_and_index_sections, variants_section, BamQcPayload, CallerOutputSummary,
  99. CompressionMetadata, ContainerHeader, EncryptionMetadata, FilterStepSummary, PipeQcPayload,
  100. ProducerMetadata, ProvenancePayload, SampleMetadata,
  101. },
  102. runners::Run,
  103. variant::{
  104. variant_collection::{ExternalAnnotation, VariantCollection, Variants},
  105. variants_stats::VariantsStats,
  106. vcf_variant::{load_variants, CallerBox},
  107. },
  108. };
  109. /// Runs the full somatic variant calling pipeline for a single sample (`id`).
  110. ///
  111. /// This function orchestrates the entire somatic variant discovery process,
  112. /// starting from raw variant caller outputs (`PASSED VCF`) and applying multiple filtering
  113. /// and annotation steps to produce high-confidence somatic variants.
  114. ///
  115. /// This function orchestrates the end-to-end somatic variant discovery process:
  116. /// - Executes and verifies upstream components if necessary
  117. /// - Loads variants from multiple callers (tumor and normal samples)
  118. /// - Applies several annotation and filtering steps (e.g. depth, population frequency, entropy)
  119. /// - Tracks filtering statistics at each step
  120. /// - Outputs high-confidence somatic variants in both `.json.gz` and `.bit` formats
  121. ///
  122. /// ## Output Overview
  123. /// The final output includes:
  124. /// - `{tumoral_dir}/{id}_somatic_variants.json.gz`: annotated somatic variants
  125. /// - `{tumoral_dir}/{id}_somatic_variants.bit`: compact binary variant representation
  126. /// - `{stats_dir}/`: multiple intermediate JSON files with annotations and statistics
  127. ///
  128. /// ## Steps
  129. ///
  130. /// This pipeline performs the following high-level steps:
  131. ///
  132. /// ### 1. Output Existence Check
  133. /// If the final JSON result already exists and [`Config::somatic_pipe_force`] is not set,
  134. /// the pipeline aborts early to avoid overwriting results.
  135. ///
  136. /// ### 2. Initialization
  137. /// - Prepares statistics and output directories.
  138. /// - Initializes variant annotations.
  139. ///
  140. /// ### 3. Pre-requisite Tool Execution
  141. /// Runs any required upstream components (e.g., alignment, basecalling, variant callers) if
  142. /// their outputs are missing, using the [`run_if_required`] logic.
  143. ///
  144. /// ### 4. Load Variant Collections
  145. /// - Initializes the configured somatic variant callers and loads their output variants from PASSED VCF.
  146. /// - Also loads germline variants (from [`ClairS::germline`]) for comparative germline filtering.
  147. ///
  148. /// ### 5. Statistics Initialization
  149. /// - Initializes a [`SomaticPipeStats`] object to track the number of variants at each step.
  150. /// - Captures initial variant counts before filtering.
  151. /// - Aggregates variant counts from all sources and stores initial annotations for quality control and
  152. /// comparison before filtering.
  153. ///
  154. /// ### 6. Filter: Germline & Solo Constitutional Variants
  155. /// - Removes variants labeled as either Germline or Solo Constitutional, assuming they are unlikely to be somatic.
  156. /// - Records count of removed variants in [`SomaticPipeStats::n_constit_germline`].
  157. ///
  158. /// ### 7. Annotation: BAM Read Depth and Alt Allele Counts
  159. /// - Uses the constitutional BAM file to annotate each variant with read depth and the number of alternate reads observed.
  160. /// - Flags variants with low depth or excessive alt reads for filtering as specified in [`Config`].
  161. ///
  162. /// ### 8. Filtering: Low Depth / High Alt Alleles
  163. /// - Removes variants with low coverage in the constitutional sample, or excessive alt allele support (suggestive of germline origin).
  164. /// - Updates stats:
  165. /// - [`SomaticPipeStats::n_low_constit`]
  166. /// - [`SomaticPipeStats::n_high_alt_constit`]
  167. ///
  168. /// ### 9. Annotation: Sequence Entropy
  169. /// Adds Shannon entropy annotation based on the reference sequence context
  170. /// around each variant (cf. [`Config::entropy_seq_len`]) to flag low-complexity regions (often repetitive).
  171. ///
  172. /// ### 10. Annotation: External Databases (COSMIC, GnomAD):
  173. /// - Uses external resources to annotate variants with:
  174. /// - COSMIC hits (somatic mutation database)
  175. /// - GnomAD allele frequencies
  176. ///
  177. /// ### 11. Filtering: GnomAD + Alt Support in Constitutional Sample
  178. /// - Removes variants that are both present in GnomAD **and** show
  179. /// alternate allele support in the constitutional BAM.
  180. /// These are highly likely to be non-somatic germline polymorphisms.
  181. /// - Updates [`SomaticPipeStats::n_high_alt_constit_gnomad`] stat.
  182. ///
  183. /// ### 12. Filtering: Low Shannon Entropy:
  184. /// - Removes variants from low-complexity regions with entropy below the configured threshold
  185. /// (cf. [`Config::min_shannon_entropy`]).
  186. /// - Updates [`SomaticPipeStats::n_low_entropies`].
  187. ///
  188. /// ### 13. Annotation: VEP (Variant Effect Predictor)
  189. /// Adds transcript-level annotations from Ensembl VEP, providing functional consequences,
  190. /// impacted genes, and regulatory features.
  191. ///
  192. /// ### 14. Merging
  193. /// Merges variant collections into a unified [`Variants`] structure,
  194. /// preserving annotations and applying deduplication logic.
  195. ///
  196. /// ### 15. Final Statistics and Saving
  197. /// - Saves final annotation stats, VEP summaries, and variant-level statistics.
  198. /// - Exports the final somatic variant set to both compressed JSON and `.bit` formats.
  199. ///
  200. /// # Returns
  201. /// - `Ok(())` if all steps completed successfully.
  202. /// - `Err` if any tool fails, if file I/O fails, or if logical conditions are violated (e.g., pre-existing output).
  203. ///
  204. /// # Errors
  205. /// - Returns early if the output file already exists and [`Config::somatic_pipe_force`] is `false`.
  206. /// - Wraps all component-specific errors using `anyhow::Result` with context.
  207. ///
  208. /// # Side Effects
  209. /// - Runs external tools conditionally (e.g., [`ClairS`], [`DeepSomatic`]).
  210. /// - Creates intermediate directories and annotation JSONs for debugging/QC.
  211. /// - May consume significant compute time depending on the number of callers, annotations, and variants.
  212. ///
  213. /// # TODOs
  214. /// - Support compressed intermediate files (`// TODO: GZ !!!`)
  215. /// - Improve filtering metrics reporting (currently not all filtered variants are tracked in final stats).
  216. ///
  217. /// # Example Output Files
  218. /// - `tumoral_dir/sample123_somatic_variants.json.gz`
  219. /// - `tumoral_dir/sample123_somatic_variants.bit`
  220. /// - `stats_dir/sample123_annotations_*.json` (intermediate annotation snapshots)
  221. ///
  222. /// # See Also
  223. /// - [`Annotations`] – core structure for managing per-variant metadata
  224. /// - [`Variants`] – the merged final variant structure
  225. /// - [`SomaticPipeStats`] – used for tracking variant counts throughout filtering
  226. ///
  227. pub struct SomaticPipe {
  228. /// Unique identifier for the sample.
  229. pub id: String,
  230. /// Configuration parameters for the pipeline.
  231. pub config: Config,
  232. }
  233. impl Initialize for SomaticPipe {
  234. /// Initializes a new `Somatic` instance with default annotations.
  235. fn initialize(id: &str, config: &crate::config::Config) -> anyhow::Result<Self> {
  236. let id = id.to_string();
  237. Ok(Self {
  238. id,
  239. config: config.clone(),
  240. })
  241. }
  242. }
  243. impl ShouldRun for SomaticPipe {
  244. fn should_run(&self) -> bool {
  245. // path to the existing “.bit” file
  246. let tumoral_dir = self.config.tumoral_dir(&self.id);
  247. let bit_path = Path::new(&tumoral_dir).join(format!("{}_somatic_variants.bit", self.id));
  248. // if we can’t read its modification time, re-run
  249. let res_meta = match fs::metadata(&bit_path).and_then(|m| m.modified()) {
  250. Ok(ts) => ts,
  251. Err(_) => return true,
  252. };
  253. let deps: [PathBuf; 5] = [
  254. Path::new(&self.config.normal_bam(&self.id)).to_path_buf(),
  255. Path::new(&self.config.tumoral_bam(&self.id)).to_path_buf(),
  256. Path::new(&self.config.reference).to_path_buf(),
  257. Path::new(&self.config.vntrs_bed).to_path_buf(),
  258. Path::new(&self.config.repeats_bed).to_path_buf(),
  259. ];
  260. deps.iter().any(|p| {
  261. fs::metadata(p)
  262. .and_then(|m| m.modified())
  263. .map_or(true, |ts| ts > res_meta)
  264. })
  265. }
  266. }
  267. impl Run for SomaticPipe {
  268. /// Executes the full somatic variant analysis pipeline.
  269. fn run(&mut self) -> anyhow::Result<()> {
  270. let config = self.config.clone();
  271. let id = self.id.clone();
  272. info!("Running somatic pipe for {id}.");
  273. // Define output paths for the final somatic variants
  274. let result_json = format!("{}/{id}_somatic_variants.json.gz", config.tumoral_dir(&id));
  275. let result_bit = format!("{}/{id}_somatic_variants.bit", config.tumoral_dir(&id));
  276. let result_vcf = format!("{}/{id}_somatic_variants.vcf.gz", config.tumoral_dir(&id));
  277. let result_pandora = format!("{}/{id}.pandora", config.tumoral_dir(&id));
  278. let outputs = [&result_json, &result_bit, &result_vcf, &result_pandora];
  279. if !config.somatic_pipe_force && outputs.iter().any(|p| Path::new(p).exists()) {
  280. return Err(anyhow::anyhow!(
  281. "Somatic Pipe output already exists for {id}."
  282. ));
  283. }
  284. let mut annotations = Annotations::default();
  285. // Create stats directory if it doesn't exist
  286. let stats_dir = config.somatic_pipe_stats(&id);
  287. if !Path::new(&stats_dir).exists() {
  288. fs::create_dir(&stats_dir)?;
  289. }
  290. // Initialize and run any pre-required input if necessary
  291. info!("Initialization prerequired pipe inputs...");
  292. let mut to_run_if_req = create_should_run!(
  293. &id,
  294. &config,
  295. SomaticScan,
  296. ClairS,
  297. NanomonSV,
  298. Severus,
  299. Savana,
  300. DeepSomatic
  301. );
  302. to_run_if_req.extend(create_should_run_normal_tumoral!(&id, &config, DeepVariant));
  303. info!("Running prerequired pipe components.");
  304. run_if_required(&mut to_run_if_req).map_err(|e| {
  305. anyhow::anyhow!("Failed to run a prerequired component of somatic pipe for {id}.\n{e}")
  306. })?;
  307. // Initialize variant callers
  308. let mut callers = init_somatic_callers!(
  309. &id,
  310. &config,
  311. ClairS,
  312. NanomonSV,
  313. Severus,
  314. Savana,
  315. DeepSomatic
  316. );
  317. callers.extend(init_solo_callers_normal_tumoral!(&id, &config, DeepVariant,));
  318. // Load variants from each caller
  319. info!("Loading variants.");
  320. let mut variants_collections = load_variants(&mut callers, &annotations)
  321. .map_err(|e| anyhow::anyhow!("Error while loading variants\n{e}"))?;
  322. // Load germline variants using ClairS
  323. info!("Loading germline variants.");
  324. let clairs_germline =
  325. ClairS::initialize(&id, &self.config.clone())?.germline(&annotations)?;
  326. variants_collections.push(clairs_germline);
  327. // Initialize statistics
  328. let mut somatic_stats = SomaticPipeStats::init(&variants_collections);
  329. info!(
  330. "Variants collections loaded from {} vcf (total of {} variants loaded)",
  331. variants_collections.len(),
  332. variants_collections
  333. .iter()
  334. .map(|v| v.variants.len())
  335. .sum::<usize>()
  336. );
  337. // Initial annotation stats (caller annotations only)
  338. let caller_cat_anns = |v: &Annotation| matches!(v, Annotation::Callers(_, _));
  339. let annot_init = annotations.callers_stat(Some(Box::new(caller_cat_anns)));
  340. somatic_stats.annot_init(
  341. &annot_init,
  342. &format!("{stats_dir}/{id}_germline_filter.json"),
  343. )?;
  344. annot_init.save_to_json(&format!("{stats_dir}/{id}_annotations_01.json"))?;
  345. // Filter out germline and solo constitutional variants
  346. info!(
  347. "Keeping somatic variants (variants neither in solo normal {} nor in germline).",
  348. config.normal_name
  349. );
  350. somatic_stats.n_constit_germline =
  351. annotations.retain_variants(&mut variants_collections, |anns| {
  352. !anns.iter().any(|ann| {
  353. matches!(
  354. ann,
  355. Annotation::Callers(_, Sample::Germline)
  356. | Annotation::Callers(_, Sample::SoloConstit)
  357. )
  358. })
  359. });
  360. annotations
  361. .callers_stat(Some(Box::new(caller_cat_anns)))
  362. .save_to_json(&format!(
  363. "{stats_dir}/{id}_annotations_02_post_germline.json"
  364. ))?;
  365. // Remove deletions stretch
  366. // info!("Removing deletions stretchs:");
  367. // variants_collections.iter_mut().for_each(|coll| {
  368. // let rm = coll.remove_strech();
  369. // info!("\t - {} {}: {}", coll.vcf.caller, coll.vcf.time, rm);
  370. // });
  371. // MASK mapq
  372. let (mut high_depth_ranges, mut low_quality_ranges) =
  373. somatic_depth_quality_ranges(&id, &config)?;
  374. high_depth_ranges.par_sort_by_key(|r| (r.contig, r.range.start));
  375. low_quality_ranges.par_sort_by_key(|r| (r.contig, r.range.start));
  376. info!(
  377. "High-depth ranges: n={} bp={}",
  378. high_depth_ranges.len(),
  379. high_depth_ranges.total_len(),
  380. );
  381. info!(
  382. "LowQ ranges: n={} bp={}",
  383. low_quality_ranges.len(),
  384. low_quality_ranges.total_len(),
  385. );
  386. variants_collections.iter_mut().for_each(|e| {
  387. let _ = e.annotate_with_ranges(&low_quality_ranges, &annotations, Annotation::LowMAPQ);
  388. });
  389. let n_masked_lowqual = annotations.retain_variants(&mut variants_collections, |anns| {
  390. !anns.contains(&Annotation::LowMAPQ)
  391. });
  392. info!("N low mapq filtered: {}", n_masked_lowqual);
  393. // Annotate with depth and number of alternate reads from constitutional BAM
  394. info!("Reading Constit BAM file for depth and pileup annotation.");
  395. variants_collections.iter().try_for_each(|c| {
  396. c.annotate_with_constit_bam(&annotations, &self.config.normal_bam(&id), &self.config)
  397. })?;
  398. annotations.somatic_constit_boundaries(
  399. self.config.somatic_max_alt_constit,
  400. self.config.somatic_min_constit_depth,
  401. );
  402. annotations
  403. .callers_stat(Some(Box::new(|v| {
  404. matches!(
  405. v,
  406. Annotation::Callers(_, _)
  407. | Annotation::ConstitAlt(_)
  408. | Annotation::ConstitDepth(_)
  409. | Annotation::HighConstitAlt
  410. | Annotation::LowConstitDepth
  411. )
  412. })))
  413. .save_to_json(&format!("{stats_dir}/{id}_annotations_03_bam.json"))?;
  414. // Filter based on low constitutional depth
  415. info!(
  416. "Removing variants when depth in constit bam < {}.",
  417. self.config.somatic_min_constit_depth
  418. );
  419. somatic_stats.n_low_constit = annotations
  420. .retain_variants(&mut variants_collections, |anns| {
  421. !anns.contains(&Annotation::LowConstitDepth)
  422. });
  423. info!(
  424. "{} variants removed when depth in constit bam < {}.",
  425. somatic_stats.n_low_constit, self.config.somatic_min_constit_depth
  426. );
  427. // Filter: remove HighConstitAlt
  428. info!(
  429. "Removing variants with SNP/indel inside the constit alignements > {}",
  430. self.config.somatic_max_alt_constit
  431. );
  432. somatic_stats.n_high_alt_constit = annotations
  433. .retain_variants(&mut variants_collections, |anns| {
  434. !anns.contains(&Annotation::HighConstitAlt)
  435. });
  436. info!(
  437. "{} variants removed with SNP/indel inside the constit alignements > {}",
  438. somatic_stats.n_high_alt_constit, self.config.somatic_max_alt_constit
  439. );
  440. annotations
  441. .callers_stat(Some(Box::new(|v| {
  442. matches!(
  443. v,
  444. Annotation::Callers(_, _)
  445. | Annotation::ConstitAlt(_)
  446. | Annotation::ConstitDepth(_)
  447. )
  448. })))
  449. .save_to_json(&format!("{stats_dir}/{id}_annotations_04_bam_filter.json"))?;
  450. // Annotate variants with sequence context (entropy + trinucleotide)
  451. info!(
  452. "Annotating variants with sequence context using reference: {}",
  453. self.config.reference
  454. );
  455. variants_collections.iter().try_for_each(|c| {
  456. c.annotate_with_sequence_context(
  457. &annotations,
  458. &self.config.reference,
  459. self.config.entropy_seq_len,
  460. )
  461. })?;
  462. // Annotate with external databases like COSMIC and GnomAD
  463. info!("Annotation with external databases like COSMIC and GnomAD.");
  464. variants_collections
  465. .iter()
  466. .try_for_each(|c| -> anyhow::Result<()> {
  467. let ext_annot = ExternalAnnotation::init(&id, &config)?;
  468. ext_annot.annotate(&c.variants, &annotations)?;
  469. Ok(())
  470. })?;
  471. annotations
  472. .callers_stat(Some(Box::new(|v| {
  473. matches!(
  474. v,
  475. Annotation::Callers(_, _) | Annotation::ConstitAlt(_) | Annotation::GnomAD(_)
  476. )
  477. })))
  478. .save_to_json(&format!("{stats_dir}/{id}_annotations_05_gnomad.json"))?;
  479. // Filter: Remove variants present in GnomAD and have alt reads in constitutional sample
  480. info!("Filtering out variants in GnomAD and in constit bam at low AF.");
  481. somatic_stats.n_high_alt_constit_gnomad = annotations
  482. .retain_variants(&mut variants_collections, |anns| {
  483. !is_gnomad_and_constit_alt(anns)
  484. });
  485. info!(
  486. "{} variants filtered, with constit alt <= max contig alt ({}) and in GnomAD.",
  487. somatic_stats.n_high_alt_constit_gnomad, self.config.somatic_max_alt_constit
  488. );
  489. // TODO: These stats doesn't capture filter metrics !!!
  490. annotations
  491. .callers_stat(Some(Box::new(|v| {
  492. matches!(
  493. v,
  494. Annotation::Callers(_, _) | Annotation::ConstitAlt(_) | Annotation::GnomAD(_)
  495. )
  496. })))
  497. .save_to_json(&format!(
  498. "{stats_dir}/{id}_annotations_06_gnomad_filter.json"
  499. ))?;
  500. // Annotate with dbSNP (skip variants already carrying a GnomAD annotation)
  501. info!("Annotating with dbSNP.");
  502. variants_collections
  503. .iter()
  504. .try_for_each(|c| annotate_dbsnp(&c.variants, &annotations, &config.db_snp))?;
  505. annotations
  506. .callers_stat(Some(Box::new(|v| {
  507. matches!(v, Annotation::Callers(_, _) | Annotation::DbSnp(_, _))
  508. })))
  509. .save_to_json(&format!("{stats_dir}/{id}_annotations_06b_dbsnp.json"))?;
  510. // Filter: remove variants in dbSNP with sufficient population freq AND constit alt
  511. info!(
  512. "Filtering out variants in dbSNP with population AF >= {} and constit alt.",
  513. config.db_snp_min_freq
  514. );
  515. somatic_stats.n_dbsnp_constit = annotations
  516. .retain_variants(&mut variants_collections, |anns| {
  517. !is_dbsnp_and_constit_alt(anns, config.db_snp_min_freq)
  518. });
  519. info!(
  520. "{} variants filtered by dbSNP + constit alt.",
  521. somatic_stats.n_dbsnp_constit
  522. );
  523. annotations
  524. .callers_stat(Some(Box::new(|v| {
  525. matches!(v, Annotation::Callers(_, _) | Annotation::DbSnp(_, _))
  526. })))
  527. .save_to_json(&format!(
  528. "{stats_dir}/{id}_annotations_06c_dbsnp_filter.json"
  529. ))?;
  530. // Filter low entropy variants
  531. annotations.low_shannon_entropy(self.config.min_shannon_entropy);
  532. info!(
  533. "Filtering out variants with low entropies ({})",
  534. config.min_shannon_entropy
  535. );
  536. annotations
  537. .callers_stat(Some(Box::new(|v| {
  538. matches!(v, Annotation::Callers(_, _) | Annotation::LowEntropy)
  539. })))
  540. .save_to_json(&format!("{stats_dir}/{id}_annotations_07_entropy.json"))?;
  541. somatic_stats.n_low_entropies = annotations
  542. .retain_variants(&mut variants_collections, |anns| {
  543. !anns.contains(&Annotation::LowEntropy)
  544. });
  545. annotations
  546. .callers_stat(Some(Box::new(|v| matches!(v, Annotation::Callers(_, _)))))
  547. .save_to_json(&format!(
  548. "{stats_dir}/{id}_annotations_08_entropy_filter.json"
  549. ))?;
  550. // Final VEP annotation
  551. info!("Annotation with VEP.");
  552. variants_collections
  553. .iter()
  554. .try_for_each(|c| -> anyhow::Result<()> {
  555. let ext_annot = ExternalAnnotation::init(&id, &config)?;
  556. ext_annot.annotate_vep(&c.variants, &annotations)?;
  557. Ok(())
  558. })?;
  559. // Insertion annotation
  560. info!("Annotation of insertions with NanomonSV (RepeatMasker and Minimap2 on reference genome).");
  561. variants_collections
  562. .iter()
  563. .filter(|c| {
  564. matches!(
  565. c.caller,
  566. Annotation::Callers(Caller::Savana, Sample::Somatic)
  567. | Annotation::Callers(Caller::Severus, Sample::Somatic)
  568. | Annotation::Callers(Caller::NanomonSV, Sample::Somatic)
  569. )
  570. })
  571. .try_for_each(|c| -> anyhow::Result<()> {
  572. c.annotate_insertions_with_nanomonsv(&annotations, &config)?;
  573. Ok(())
  574. })?;
  575. info!("Annotation of SV with CoLoRSdb.");
  576. variants_collections
  577. .iter()
  578. .filter(|c| {
  579. matches!(
  580. c.caller,
  581. Annotation::Callers(Caller::Savana, Sample::Somatic)
  582. | Annotation::Callers(Caller::Severus, Sample::Somatic)
  583. | Annotation::Callers(Caller::NanomonSV, Sample::Somatic)
  584. )
  585. })
  586. .try_for_each(|c| -> anyhow::Result<()> {
  587. annotate_dbsnp(&c.variants, &annotations, &config.db_snp)
  588. })?;
  589. annotations
  590. .callers_stat(Some(Box::new(caller_cat_anns)))
  591. .save_to_json(&format!("{stats_dir}/{id}_annotations_09_vep.json"))?;
  592. // Merge all variants into a final collection
  593. let mut variants = variants_collections.into_iter().fold(
  594. Variants::default(),
  595. |mut acc, variants_collection| {
  596. acc.merge(variants_collection, &annotations);
  597. acc
  598. },
  599. );
  600. let vep_stats = annotations.vep_stats()?;
  601. vep_stats.save_to_json(&format!("{stats_dir}/{id}_annotations_10_vep.json"))?;
  602. let variant_stats = VariantsStats::new(&mut variants, &id, &config, &high_depth_ranges)?;
  603. variant_stats.save_to_json(&format!("{stats_dir}/{id}_variants_stats_final.json.gz"))?;
  604. info!("Final unique variants: {}", variants.data.len());
  605. // Ensure sorted (should already be sorted)
  606. variants.sort();
  607. let vntrs: Vec<GenomeRange> = read_bed(&config.vntrs_bed)?
  608. .iter()
  609. .map(|r| r.range.clone())
  610. .collect();
  611. variants.annotate_with_ranges(&vntrs, Some(Annotation::VNTR), 0, Vec::new());
  612. let repeat_masker: Vec<GenomeRange> = read_bed(&config.repeats_bed)?
  613. .iter()
  614. .map(|r| r.range.clone())
  615. .collect();
  616. variants.annotate_with_ranges(&repeat_masker, Some(Annotation::Repeat), 0, Vec::new());
  617. variants.save_to_json(&result_json)?;
  618. variants.save_to_file(&result_bit)?;
  619. variants.write_vcf(&result_vcf, &config.dict_file, config.somatic_pipe_force)?;
  620. write_pandora_output(
  621. &result_pandora,
  622. &id,
  623. &config,
  624. &variants,
  625. somatic_stats,
  626. variant_stats,
  627. vep_stats,
  628. &annotations.callers_stat(None),
  629. )?;
  630. Ok(())
  631. }
  632. }
  633. fn write_pandora_output(
  634. output_path: &str,
  635. id: &str,
  636. config: &Config,
  637. variants: &Variants,
  638. somatic_pipe_stats: SomaticPipeStats,
  639. variant_stats: VariantsStats,
  640. vep_stats: crate::annotation::VepStats,
  641. final_annotation_stats: &AnnotationsStats,
  642. ) -> anyhow::Result<()> {
  643. let mut sections = Vec::new();
  644. sections.push(variants_section(variants));
  645. let (variant_records, variant_index) = variant_records_and_index_sections(variants)?;
  646. sections.push(variant_records);
  647. sections.push(variant_index);
  648. match SavanaCN::parse_file(id, config) {
  649. Ok(copy_number) => sections.push(copy_number_section(&copy_number)?.optional()),
  650. Err(err) => info!("Skipping .pandora copy_number section for {id}: {err}"),
  651. }
  652. let mut bam_qc = BamQcPayload {
  653. by_role: BTreeMap::new(),
  654. };
  655. match WGSBamStats::open(id, &config.tumoral_name, config) {
  656. Ok(stats) => {
  657. bam_qc.by_role.insert(config.tumoral_name.clone(), stats);
  658. }
  659. Err(err) => info!("Skipping .pandora tumor BAM QC for {id}: {err}"),
  660. }
  661. match WGSBamStats::open(id, &config.normal_name, config) {
  662. Ok(stats) => {
  663. bam_qc.by_role.insert(config.normal_name.clone(), stats);
  664. }
  665. Err(err) => info!("Skipping .pandora normal BAM QC for {id}: {err}"),
  666. }
  667. if !bam_qc.by_role.is_empty() {
  668. sections.push(bam_qc_section(&bam_qc)?);
  669. }
  670. let mut annotation_stats = BTreeMap::new();
  671. annotation_stats.insert("final".to_string(), final_annotation_stats.clone());
  672. let caller_outputs = caller_output_summaries(&somatic_pipe_stats);
  673. let filter_steps = filter_step_summaries(&somatic_pipe_stats);
  674. let pipe_qc = PipeQcPayload {
  675. somatic_pipe_stats,
  676. variant_stats: Some(variant_stats),
  677. annotation_stats,
  678. vep_stats: Some(vep_stats),
  679. caller_outputs,
  680. filter_steps,
  681. };
  682. sections.push(pipe_qc_section(&pipe_qc)?);
  683. let config_digest = serde_json::to_vec(config)
  684. .ok()
  685. .map(|bytes| format!("blake3:{}", blake3::hash(&bytes).to_hex()));
  686. let provenance = ProvenancePayload {
  687. config_digest,
  688. input_digests: BTreeMap::new(),
  689. tool_versions: BTreeMap::new(),
  690. command_lines: Vec::new(),
  691. };
  692. sections.push(provenance_section(&provenance)?);
  693. let header = ContainerHeader {
  694. format: "somaticpipe.output".to_string(),
  695. format_version: crate::io::somaticpipe_container::PANDORA_FORMAT_VERSION,
  696. producer: ProducerMetadata {
  697. name: env!("CARGO_PKG_NAME").to_string(),
  698. pipeline: "SomaticPipe".to_string(),
  699. pipeline_version: env!("CARGO_PKG_VERSION").to_string(),
  700. git_commit: option_env!("GIT_COMMIT").map(ToString::to_string),
  701. created_at: chrono::Utc::now(),
  702. },
  703. sample: SampleMetadata {
  704. sample_id: id.to_string(),
  705. tumor_timepoint: Some(config.tumoral_name.clone()),
  706. normal_timepoint: Some(config.normal_name.clone()),
  707. reference: Some(config.reference_name.clone()),
  708. reference_digest: None,
  709. },
  710. compression: CompressionMetadata::default(),
  711. encryption: EncryptionMetadata::default(),
  712. sections: Vec::new(),
  713. };
  714. crate::io::somaticpipe_container::write_container(output_path, header, sections)
  715. }
  716. fn caller_output_summaries(somatic_pipe_stats: &SomaticPipeStats) -> Vec<CallerOutputSummary> {
  717. somatic_pipe_stats
  718. .input
  719. .somatic
  720. .iter()
  721. .chain(&somatic_pipe_stats.input.solo_tumor)
  722. .chain(&somatic_pipe_stats.input.germline)
  723. .chain(&somatic_pipe_stats.input.solo_constit)
  724. .map(|(caller, n_input)| CallerOutputSummary {
  725. caller: caller.to_string(),
  726. n_input: *n_input,
  727. n_after_filters: None,
  728. })
  729. .collect()
  730. }
  731. fn filter_step_summaries(somatic_pipe_stats: &SomaticPipeStats) -> Vec<FilterStepSummary> {
  732. vec![
  733. FilterStepSummary {
  734. name: "germline_or_constit".to_string(),
  735. removed: somatic_pipe_stats.n_constit_germline,
  736. },
  737. FilterStepSummary {
  738. name: "low_constit_depth".to_string(),
  739. removed: somatic_pipe_stats.n_low_constit,
  740. },
  741. FilterStepSummary {
  742. name: "high_constit_alt".to_string(),
  743. removed: somatic_pipe_stats.n_high_alt_constit,
  744. },
  745. FilterStepSummary {
  746. name: "gnomad_and_constit_alt".to_string(),
  747. removed: somatic_pipe_stats.n_high_alt_constit_gnomad,
  748. },
  749. FilterStepSummary {
  750. name: "dbsnp_and_constit_alt".to_string(),
  751. removed: somatic_pipe_stats.n_dbsnp_constit,
  752. },
  753. FilterStepSummary {
  754. name: "low_entropy".to_string(),
  755. removed: somatic_pipe_stats.n_low_entropies,
  756. },
  757. ]
  758. }
  759. pub fn const_stats(id: String, config: Config) -> anyhow::Result<()> {
  760. info!("Loading Germline");
  761. let annotations = Annotations::default();
  762. let clairs_germline = ClairS::initialize(&id, &config.clone())?.germline(&annotations)?;
  763. info!("Annotation with Cosmic and GnomAD.");
  764. let ext_annot = ExternalAnnotation::init(&id, &config)?;
  765. ext_annot.annotate(&clairs_germline.variants, &annotations)?;
  766. let mut variants = Variants::default();
  767. variants.merge(clairs_germline, &annotations);
  768. info!("-- {}", variants.data.len());
  769. // let u = VariantsStats::new(&mut variants, &id, &config)?;
  770. // info!("++ {}", u.n);
  771. Ok(())
  772. }
  773. /// Holds statistical data for somatic variant pipeline processing,
  774. /// including summary counts and input categorization.
  775. #[derive(Debug, Default, Clone, Serialize, Deserialize)]
  776. pub struct SomaticPipeStats {
  777. /// Summary of input variant collections grouped by sample type.
  778. pub input: InputStats,
  779. /// Number of variants removed because they were classified as germline
  780. /// by at least one caller, or detected in the normal-only (solo constitutional)
  781. /// callset (including variants classified as both).
  782. pub n_constit_germline: usize,
  783. /// Number of variants removed due to low depth in the constitutional (normal) BAM
  784. /// (i.e. annotated with [`Annotation::LowConstitDepth`]).
  785. pub n_low_constit: usize,
  786. /// Number of variants removed due to excessive alternate-allele support in the
  787. /// constitutional (normal) BAM (i.e. annotated with [`Annotation::HighConstitAlt`]).
  788. pub n_high_alt_constit: usize,
  789. /// Number of variants removed because they are present in gnomAD and also show
  790. /// alternate-allele support in the constitutional (normal) BAM, according to
  791. /// [`is_gnomad_and_constit_alt`].
  792. pub n_high_alt_constit_gnomad: usize,
  793. /// Number of variants removed because they are present in dbSNP at or above
  794. /// `config.db_snp_min_freq` and also show alternate-allele support in the
  795. /// constitutional BAM, according to [`is_dbsnp_and_constit_alt`].
  796. pub n_dbsnp_constit: usize,
  797. /// Number of variants removed due to low sequence complexity (Shannon entropy),
  798. /// i.e. annotated with [`Annotation::LowEntropy`].
  799. pub n_low_entropies: usize,
  800. }
  801. impl SomaticPipeStats {
  802. /// Initializes a `SomaticPipeStats` object with populated `InputStats` based on the
  803. /// provided `VariantCollection`s.
  804. pub fn init(collections: &[VariantCollection]) -> Self {
  805. Self {
  806. input: InputStats::from_collections(collections),
  807. ..Default::default()
  808. }
  809. }
  810. /// Generates a tumor-vs-germline annotation matrix and writes it to a JSON file.
  811. ///
  812. /// This method analyzes co-occurrence patterns between tumor and germline/constit
  813. /// variant callers by iterating through annotation statistics from `AnnotationsStats`.
  814. /// It builds a matrix where each row corresponds to a **tumor caller** and each column
  815. /// corresponds to a **germline or constitutional caller**. Each cell contains the number
  816. /// of variant calls that were annotated by both the tumor and the germline/constit caller.
  817. ///
  818. /// In addition to writing a structured JSON file to the specified path, the function also
  819. /// prints a tab-separated human-readable matrix to stdout for convenience.
  820. ///
  821. /// # Parameters
  822. ///
  823. /// - `stats`: A reference to [`AnnotationsStats`] containing categorical annotations.
  824. /// This object holds a frequency map where keys are combinations of `Annotation`
  825. /// values (serialized as strings) and values are occurrence counts.
  826. /// - `json_path`: The path where the resulting JSON output file should be written.
  827. ///
  828. /// # Output Formats
  829. ///
  830. /// ## JSON Output (`json_path`)
  831. ///
  832. /// The output JSON is an **array of tumor caller records**, each containing:
  833. /// - `caller_name`: The name of the tumor caller (as a string).
  834. /// - `germline`: An array of JSON objects, each with one key-value pair,
  835. /// where the key is a germline/constit caller (or combination) and the value is the count.
  836. ///
  837. /// ### Example
  838. /// ```json
  839. /// [
  840. /// {
  841. /// "caller_name": "DeepVariant SoloTumor",
  842. /// "germline": {
  843. /// "ClairS Germline": 99978,
  844. /// "ClairS Germline + DeepVariant SoloConstit": 4710570
  845. /// }
  846. /// },
  847. /// {
  848. /// "caller_name": "ClairS Somatic",
  849. /// "germline": {
  850. /// "ClairS Germline": 944,
  851. /// "ClairS Germline + DeepVariant SoloConstit": 362
  852. /// }
  853. /// }
  854. /// ]
  855. /// ```
  856. ///
  857. /// ## Console Output (TSV)
  858. ///
  859. /// A tab-separated matrix is printed to stdout. Each row begins with a tumor caller,
  860. /// followed by columns showing germline/constit caller combinations and their counts.
  861. ///
  862. /// # Notes
  863. /// - Tumor callers are collected from `self.input.somatic` and `self.input.solo_tumor`.
  864. /// - Germline/constit callers are collected from `self.input.germline` and `self.input.solo_constit`.
  865. /// - Keys in the original `AnnotationsStats` map are split using `" + "` and parsed into `Annotation` enums.
  866. /// - Germline keys are sorted and joined to form canonical column labels.
  867. /// - Tumor and germline annotations are matched by exact `Annotation` equality.
  868. ///
  869. /// # Errors
  870. /// Returns an error if:
  871. /// - Any annotation key in `AnnotationsStats` fails to parse into a valid `Annotation`.
  872. /// - The JSON output file cannot be created or written to.
  873. ///
  874. /// # Dependencies
  875. /// Requires that `Annotation` implements `FromStr`, `ToString`, `PartialEq`, and `Clone`.
  876. ///
  877. /// # Example Usage
  878. /// ```rust
  879. /// let mut stats = SomaticPipeStats::init(&collections);
  880. /// stats.annot_init(&annotation_stats, "output/matrix.json")?;
  881. /// ```
  882. pub fn annot_init(&self, stats: &AnnotationsStats, json_path: &str) -> anyhow::Result<()> {
  883. #[derive(Serialize)]
  884. struct TumorRow {
  885. caller_name: String,
  886. germline: BTreeMap<String, u64>,
  887. }
  888. // Parse stats keys into Vec<Annotation> once
  889. let parsed: Vec<(Vec<Annotation>, u64)> = stats
  890. .categorical
  891. .iter()
  892. .map(|e| {
  893. let anns = e
  894. .key()
  895. .split(" + ")
  896. .map(|k| k.parse::<Annotation>())
  897. .collect::<anyhow::Result<Vec<_>>>()
  898. .map_err(|err| {
  899. anyhow::anyhow!("Error while parsing AnnotationsStats key: {err}")
  900. })?;
  901. Ok((anns, *e.value()))
  902. })
  903. .collect::<anyhow::Result<_>>()?;
  904. // Collect caller labels
  905. let tumor_callers: Vec<Annotation> = self
  906. .input
  907. .somatic
  908. .iter()
  909. .map(|(a, _)| a.clone())
  910. .chain(self.input.solo_tumor.iter().map(|(a, _)| a.clone()))
  911. .collect();
  912. let germ_callers: Vec<Annotation> = self
  913. .input
  914. .germline
  915. .iter()
  916. .map(|(a, _)| a.clone())
  917. .chain(self.input.solo_constit.iter().map(|(a, _)| a.clone()))
  918. .collect();
  919. // Matrix: tumor -> (germline label -> count), deterministic by construction
  920. let mut matrix: BTreeMap<String, BTreeMap<String, u64>> = BTreeMap::new();
  921. for (anns, v) in parsed.iter() {
  922. // only rows that include any germline/constit label
  923. if !anns.iter().any(|a| {
  924. matches!(
  925. a,
  926. Annotation::Callers(_, Sample::SoloConstit)
  927. | Annotation::Callers(_, Sample::Germline)
  928. )
  929. }) {
  930. continue;
  931. }
  932. // Which tumor callers are present in this set?
  933. let present_tumors: Vec<String> = tumor_callers
  934. .iter()
  935. .filter(|t| anns.contains(t))
  936. .map(|t| t.to_string())
  937. .collect();
  938. if present_tumors.is_empty() {
  939. continue;
  940. }
  941. // Canonical germline key: sorted labels joined by " + "
  942. let mut present_germs: Vec<String> = germ_callers
  943. .iter()
  944. .filter(|g| anns.contains(g))
  945. .map(|g| g.to_string())
  946. .collect();
  947. present_germs.sort();
  948. let germ_key = present_germs.join(" + ");
  949. for tumor in present_tumors {
  950. *matrix
  951. .entry(tumor)
  952. .or_default()
  953. .entry(germ_key.clone())
  954. .or_default() += *v;
  955. }
  956. }
  957. // Collect all germline columns (deterministic)
  958. let mut germ_cols: Vec<String> = matrix
  959. .values()
  960. .flat_map(|row| row.keys().cloned())
  961. .collect();
  962. germ_cols.sort();
  963. germ_cols.dedup();
  964. // TSV output (deterministic ordering)
  965. let mut lines: Vec<String> = Vec::with_capacity(matrix.len());
  966. for (tumor, row) in matrix.iter() {
  967. let line = format!(
  968. "{tumor}\t{}",
  969. germ_cols
  970. .iter()
  971. .map(|g| format!("{g}: {}", row.get(g).copied().unwrap_or(0)))
  972. .collect::<Vec<_>>()
  973. .join("\t")
  974. );
  975. lines.push(line);
  976. }
  977. println!("{}", lines.join("\n"));
  978. // JSON output: list of rows with all columns filled (0 if absent)
  979. let json_rows: Vec<TumorRow> = matrix
  980. .iter()
  981. .map(|(tumor, row)| {
  982. let mut full = BTreeMap::new();
  983. for g in germ_cols.iter() {
  984. full.insert(g.clone(), row.get(g).copied().unwrap_or(0));
  985. }
  986. TumorRow {
  987. caller_name: tumor.clone(),
  988. germline: full,
  989. }
  990. })
  991. .collect();
  992. let file = File::create(json_path)?;
  993. serde_json::to_writer_pretty(file, &json_rows)?;
  994. Ok(())
  995. }
  996. }
  997. /// Aggregated statistics on variant collections, grouped by sample type.
  998. ///
  999. /// The `InputStats` struct stores the number of variants observed for each
  1000. /// caller, categorized by biological sample type:
  1001. ///
  1002. /// - `solo_tumor`: Variants from tumor-only samples.
  1003. /// - `solo_constit`: Variants from normal-only (constitutional) samples.
  1004. /// - `germline`: Variants classified as germline (shared between normal and tumor).
  1005. /// - `somatic`: Variants classified as somatic (tumor-specific).
  1006. ///
  1007. /// Each vector contains tuples of the form `(Annotation, usize)`, where:
  1008. /// - `Annotation` identifies the caller and sample type.
  1009. /// - `usize` represents the number of variants for that annotation.
  1010. #[derive(Debug, Default, Clone, Serialize, Deserialize)]
  1011. pub struct InputStats {
  1012. /// Variants from tumor-only samples.
  1013. pub solo_tumor: Vec<(Annotation, usize)>,
  1014. /// Variants from normal-only (constitutional) samples.
  1015. pub solo_constit: Vec<(Annotation, usize)>,
  1016. /// Germline variants (present in both normal and tumor).
  1017. pub germline: Vec<(Annotation, usize)>,
  1018. /// Somatic variants (specific to tumor).
  1019. pub somatic: Vec<(Annotation, usize)>,
  1020. }
  1021. impl InputStats {
  1022. /// Constructs an `InputStats` instance by aggregating variant counts
  1023. /// from a list of [`VariantCollection`]s.
  1024. ///
  1025. /// Each `VariantCollection` is inspected to determine its sample type,
  1026. /// as indicated by the `Annotation::Callers(_, Sample)` variant. The
  1027. /// number of variants (`collection.variants.len()`) is recorded in the
  1028. /// appropriate field of `InputStats`, grouped by `Sample`.
  1029. ///
  1030. /// # Parameters
  1031. ///
  1032. /// - `collections`: A slice of [`VariantCollection`] objects containing
  1033. /// variants annotated with a caller and sample type.
  1034. ///
  1035. /// # Returns
  1036. ///
  1037. /// A populated `InputStats` instance with counts per (caller, sample type).
  1038. ///
  1039. /// # Examples
  1040. ///
  1041. /// ```
  1042. /// let collections: Vec<VariantCollection> = load_collections();
  1043. /// let stats = InputStats::from_collections(&collections);
  1044. /// println!("Somatic calls: {:?}", stats.somatic);
  1045. /// ```
  1046. pub fn from_collections(collections: &[VariantCollection]) -> Self {
  1047. let mut stats = Self::default();
  1048. for collection in collections.iter() {
  1049. match collection.caller {
  1050. Annotation::Callers(_, Sample::SoloTumor) => stats
  1051. .solo_tumor
  1052. .push((collection.caller.clone(), collection.variants.len())),
  1053. Annotation::Callers(_, Sample::SoloConstit) => stats
  1054. .solo_constit
  1055. .push((collection.caller.clone(), collection.variants.len())),
  1056. Annotation::Callers(_, Sample::Germline) => stats
  1057. .germline
  1058. .push((collection.caller.clone(), collection.variants.len())),
  1059. Annotation::Callers(_, Sample::Somatic) => stats
  1060. .somatic
  1061. .push((collection.caller.clone(), collection.variants.len())),
  1062. _ => (),
  1063. };
  1064. }
  1065. stats
  1066. }
  1067. }
  1068. #[cfg(test)]
  1069. mod tests {
  1070. use super::*;
  1071. use crate::helpers::test_init;
  1072. #[test]
  1073. fn somatic_pipe_run() -> anyhow::Result<()> {
  1074. test_init();
  1075. let config = Config::default();
  1076. // let pool = ThreadPoolBuilder::new()
  1077. // .num_threads(config.threads.into())
  1078. // .build()?;
  1079. //
  1080. // pool.install(|| SomaticPipe::initialize("CHALO", &config)?.run())?;
  1081. SomaticPipe::initialize("DUMCO", &config)?.run()?;
  1082. Ok(())
  1083. }
  1084. }