use crate::io::config::{
ArtifactConfig, CaptureConfig, FragmentConfig, FragmentModel, MutationConfig,
RandomMutationConfig, SampleConfig, UmiConfig,
};
#[derive(Debug, Clone, Default)]
pub struct PresetOverlay {
pub coverage: Option<f64>,
pub read_length: Option<usize>,
pub chromosomes: Option<Vec<String>>,
pub fragment: Option<FragmentConfig>,
pub mutations: Option<MutationConfig>,
pub umi: Option<UmiConfig>,
pub artifacts: Option<ArtifactConfig>,
pub purity: Option<f64>,
pub capture: Option<CaptureConfig>,
}
pub fn get(name: &str) -> anyhow::Result<PresetOverlay> {
if let Some(cancer_name) = name.strip_prefix("cancer:") {
let mut overlay = crate::cli::cancer_presets::get(cancer_name)?;
if let Some(ref mut muts) = overlay.mutations {
muts.include_driver_mutations = true;
}
return Ok(overlay);
}
match name {
"small" => Ok(preset_small()),
"panel" => Ok(preset_panel()),
"wgs" => Ok(preset_wgs()),
"cfdna" => Ok(preset_cfdna()),
"ffpe" => Ok(preset_ffpe()),
"umi" => Ok(preset_umi()),
"twist" => Ok(preset_twist()),
other => anyhow::bail!(
"unknown preset '{}'; valid choices: small, panel, wgs, cfdna, ffpe, umi, twist, \
or cancer:<type> (e.g. cancer:lung_adeno)",
other
),
}
}
pub fn all_names() -> &'static [&'static str] {
&["small", "panel", "wgs", "cfdna", "ffpe", "umi", "twist"]
}
fn preset_small() -> PresetOverlay {
PresetOverlay {
coverage: Some(1.0),
chromosomes: Some(vec!["chr22".to_string()]),
mutations: Some(MutationConfig {
vcf: None,
random: Some(RandomMutationConfig {
count: 100,
vaf_min: 0.05,
vaf_max: 0.5,
snv_fraction: 0.80,
indel_fraction: 0.15,
mnv_fraction: 0.05,
signature: None,
}),
sv_count: 0,
sv_signature: None,
include_driver_mutations: false,
}),
..Default::default()
}
}
fn preset_panel() -> PresetOverlay {
PresetOverlay {
coverage: Some(500.0),
mutations: Some(MutationConfig {
vcf: None,
random: Some(RandomMutationConfig {
count: 50,
vaf_min: 0.001,
vaf_max: 0.5,
snv_fraction: 0.80,
indel_fraction: 0.15,
mnv_fraction: 0.05,
signature: None,
}),
sv_count: 0,
sv_signature: None,
include_driver_mutations: false,
}),
umi: Some(UmiConfig {
length: 8,
duplex: false,
pcr_cycles: 10,
family_size_mean: 3.0,
family_size_sd: 1.5,
inline: true,
spacer: None,
duplex_conversion_rate: None,
error_rate: None,
}),
..Default::default()
}
}
fn preset_wgs() -> PresetOverlay {
PresetOverlay {
coverage: Some(30.0),
mutations: Some(MutationConfig {
vcf: None,
random: Some(RandomMutationConfig {
count: 5000,
vaf_min: 0.001,
vaf_max: 0.5,
snv_fraction: 0.80,
indel_fraction: 0.15,
mnv_fraction: 0.05,
signature: None,
}),
sv_count: 0,
sv_signature: None,
include_driver_mutations: false,
}),
..Default::default()
}
}
fn preset_cfdna() -> PresetOverlay {
PresetOverlay {
coverage: Some(200.0),
fragment: Some(FragmentConfig {
model: FragmentModel::Cfda,
mean: 167.0,
sd: 20.0,
long_read: None,
end_motif_model: None,
ctdna_fraction: None,
mono_sd: None,
di_sd: None,
}),
purity: Some(0.02),
mutations: Some(MutationConfig {
vcf: None,
random: Some(RandomMutationConfig {
count: 200,
vaf_min: 0.001,
vaf_max: 0.05,
snv_fraction: 0.80,
indel_fraction: 0.15,
mnv_fraction: 0.05,
signature: None,
}),
sv_count: 0,
sv_signature: None,
include_driver_mutations: false,
}),
umi: Some(UmiConfig {
length: 8,
duplex: true,
pcr_cycles: 10,
family_size_mean: 3.0,
family_size_sd: 1.5,
inline: false,
spacer: None,
duplex_conversion_rate: None,
error_rate: None,
}),
..Default::default()
}
}
fn preset_ffpe() -> PresetOverlay {
PresetOverlay {
coverage: Some(30.0),
artifacts: Some(ArtifactConfig {
ffpe_damage_rate: Some(0.02),
oxog_rate: Some(0.01),
duplicate_rate: None,
pcr_error_rate: None,
}),
mutations: Some(MutationConfig {
vcf: None,
random: Some(RandomMutationConfig {
count: 500,
vaf_min: 0.001,
vaf_max: 0.5,
snv_fraction: 0.80,
indel_fraction: 0.15,
mnv_fraction: 0.05,
signature: None,
}),
sv_count: 0,
sv_signature: None,
include_driver_mutations: false,
}),
..Default::default()
}
}
fn preset_umi() -> PresetOverlay {
PresetOverlay {
coverage: Some(1000.0),
mutations: Some(MutationConfig {
vcf: None,
random: Some(RandomMutationConfig {
count: 50,
vaf_min: 0.001,
vaf_max: 0.5,
snv_fraction: 0.80,
indel_fraction: 0.15,
mnv_fraction: 0.05,
signature: None,
}),
sv_count: 0,
sv_signature: None,
include_driver_mutations: false,
}),
umi: Some(UmiConfig {
length: 9,
duplex: true,
pcr_cycles: 12,
family_size_mean: 4.0,
family_size_sd: 1.5,
inline: false,
spacer: None,
duplex_conversion_rate: None,
error_rate: None,
}),
..Default::default()
}
}
fn preset_twist() -> PresetOverlay {
PresetOverlay {
coverage: Some(2000.0),
read_length: Some(150),
fragment: Some(FragmentConfig {
model: FragmentModel::Normal,
mean: 170.0,
sd: 30.0,
long_read: None,
end_motif_model: None,
ctdna_fraction: None,
mono_sd: None,
di_sd: None,
}),
umi: Some(UmiConfig {
length: 5,
duplex: true,
pcr_cycles: 10,
family_size_mean: 3.5,
family_size_sd: 1.5,
inline: true,
spacer: Some("AT".to_string()),
duplex_conversion_rate: Some(0.90),
error_rate: Some(0.001),
}),
mutations: Some(MutationConfig {
vcf: None,
random: Some(RandomMutationConfig {
count: 50,
vaf_min: 0.0001,
vaf_max: 0.1,
snv_fraction: 0.80,
indel_fraction: 0.15,
mnv_fraction: 0.05,
signature: None,
}),
sv_count: 0,
sv_signature: None,
include_driver_mutations: false,
}),
capture: Some(CaptureConfig {
enabled: true,
targets_bed: None,
off_target_fraction: 0.03,
coverage_uniformity: 0.15,
edge_dropoff_bases: 50,
mode: "panel".to_string(),
primer_trim: 0,
coverage_cv_target: Some(0.25),
on_target_fraction_target: Some(0.95),
}),
..Default::default()
}
}
use crate::io::config::Config;
pub fn apply_preset_to_config(config: &mut Config, overlay: &PresetOverlay) {
use crate::io::config::{default_coverage, default_fragment_mean, default_fragment_sd};
if let Some(cov) = overlay.coverage {
if (config.sample.coverage - default_coverage()).abs() < 1e-9 {
config.sample.coverage = cov;
}
}
if let Some(rl) = overlay.read_length {
if config.sample.read_length == SampleConfig::default().read_length {
config.sample.read_length = rl;
}
}
if let Some(ref chroms) = overlay.chromosomes {
if config.chromosomes.is_none() {
config.chromosomes = Some(chroms.clone());
}
}
if let Some(ref frag) = overlay.fragment {
if (config.fragment.mean - default_fragment_mean()).abs() < 1e-9
&& (config.fragment.sd - default_fragment_sd()).abs() < 1e-9
{
config.fragment = frag.clone();
}
}
if let Some(ref muts) = overlay.mutations {
if config.mutations.is_none() {
config.mutations = Some(muts.clone());
}
}
if let Some(ref umi) = overlay.umi {
if config.umi.is_none() {
config.umi = Some(umi.clone());
}
}
if let Some(ref arts) = overlay.artifacts {
if config.artifacts.is_none() {
config.artifacts = Some(arts.clone());
}
}
if let Some(purity) = overlay.purity {
if config.tumour.is_none() {
config.tumour = Some(crate::io::config::TumourConfig {
purity,
ploidy: 2,
clones: Vec::new(),
msi: false,
});
}
}
if let Some(ref cap) = overlay.capture {
if config.capture.is_none() {
config.capture = Some(cap.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preset_small() {
let overlay = get("small").unwrap();
assert_eq!(overlay.coverage, Some(1.0));
assert_eq!(
overlay.chromosomes.as_deref(),
Some(["chr22".to_string()].as_slice())
);
let muts = overlay.mutations.unwrap();
let rand = muts.random.unwrap();
assert_eq!(rand.count, 100);
}
#[test]
fn test_preset_cfdna() {
let overlay = get("cfdna").unwrap();
assert_eq!(overlay.coverage, Some(200.0));
let frag = overlay.fragment.unwrap();
assert!(matches!(frag.model, FragmentModel::Cfda));
assert!((frag.mean - 167.0).abs() < 1e-9);
assert!((overlay.purity.unwrap() - 0.02).abs() < 1e-9);
assert!(overlay.umi.unwrap().duplex);
}
#[test]
fn test_all_presets_valid() {
use crate::io::config::{
Config, FragmentConfig, OutputConfig, QualityConfig, SampleConfig,
};
use std::path::PathBuf;
let base_config = || Config {
reference: PathBuf::from("/dev/null"),
output: OutputConfig {
directory: PathBuf::from("/tmp"),
fastq: true,
bam: false,
truth_vcf: false,
manifest: false,
germline_vcf: false,
single_read_bam: false,
mapq: 60,
annotate_reads: false,
},
sample: SampleConfig::default(),
fragment: FragmentConfig::default(),
quality: QualityConfig::default(),
tumour: None,
mutations: None,
umi: None,
artifacts: None,
seed: None,
threads: None,
chromosomes: None,
regions_bed: None,
copy_number: None,
gc_bias: None,
samples: None,
capture: None,
performance: Default::default(),
preset: None,
vafs: None,
germline: None,
paired: None,
};
for name in all_names() {
let overlay = get(name).expect(name);
let mut cfg = base_config();
apply_preset_to_config(&mut cfg, &overlay);
assert!(
cfg.sample.coverage > 0.0,
"preset '{name}' gave non-positive coverage"
);
}
}
#[test]
fn test_unknown_preset_errors() {
assert!(get("nonexistent").is_err());
}
#[test]
fn test_preset_precedence_yaml_wins() {
use crate::io::config::{
Config, FragmentConfig, OutputConfig, QualityConfig, SampleConfig,
};
use std::path::PathBuf;
let mut cfg = Config {
reference: PathBuf::from("/dev/null"),
output: OutputConfig {
directory: PathBuf::from("/tmp"),
fastq: true,
bam: false,
truth_vcf: false,
manifest: false,
germline_vcf: false,
single_read_bam: false,
mapq: 60,
annotate_reads: false,
},
sample: SampleConfig {
coverage: 60.0,
..SampleConfig::default()
},
fragment: FragmentConfig::default(),
quality: QualityConfig::default(),
tumour: None,
mutations: None,
umi: None,
artifacts: None,
seed: None,
threads: None,
chromosomes: None,
regions_bed: None,
copy_number: None,
gc_bias: None,
samples: None,
capture: None,
performance: Default::default(),
preset: None,
vafs: None,
germline: None,
paired: None,
};
let overlay = get("small").unwrap(); apply_preset_to_config(&mut cfg, &overlay);
assert!(
(cfg.sample.coverage - 60.0).abs() < 1e-9,
"YAML coverage should not be overwritten by preset"
);
}
}