Skip to main content

diploid_contam_estimator/
lib.rs

1pub mod bedreader;
2pub mod cli;
3pub mod contamination_estimator;
4pub mod model;
5pub mod vcfreader;
6
7use bedreader::read_bed;
8use contamination_estimator::calculate_contam_hypothesis;
9use log::info;
10use model::{ContamProbResult, VariantPosition};
11use std::fs::File;
12use std::io::Write;
13use std::option::Option;
14use std::string::String;
15use std::vec::Vec;
16use vcfreader::build_variant_list;
17
18const MAX_CONTAM: usize = 400; // should be 0.399 because we divide 1000
19const DECIMAL_PLACE: f64 = 0.001; // how precise we want for the contamination level
20
21/// write string to file
22///
23/// # Arguments:
24/// * `filename`: the file name of the new file to be written to
25/// * `json_string`: String to be written to the file
26pub fn write_json(filename: &str, json_string: String) -> Result<(), String> {
27    let mut output_file = File::create(filename).map_err(|e| e.to_string())?;
28    write!(output_file, "{}", json_string).unwrap();
29    info!("Written debug file at: {}", filename);
30    Ok(())
31}
32
33/// the actual workflow to takes in a variant vcf file and calcualte the
34/// contamination level
35///
36/// # Arguments:
37///
38/// * `vcf_file`: the file path to the input vcf file for the analysis
39/// * `snv_only_flag`: boolean flag indicating whether we should only look at SNV instead of both SNV and indel
40/// * `depth_threshold`: removing all variants with read depth below this threshold
41/// * `prob_json`: for debug, a json file name for writing the contam level and the
42///              respecitive log likelihoos into ("_no_file" will turn off writing a file)
43/// * `prob_json`: for debug, a json file name for writing the list of variants that are being
44///              used for the contam level compuatation ("_no_file" will turn off writing a file)
45///
46/// # Return:
47/// * the contamination level with the highest log likelihood
48///
49/// # Examples:
50///
51/// ```
52/// use diploid_contam_estimator::run;
53/// let best_guess_contam = run("data/test.vcf", None, true, 100, Some("prob.json"), Some("variant.json")).unwrap();
54/// assert_eq!(best_guess_contam, 0.046);
55/// ```
56pub fn run(
57    vcf_file: &str,
58    loci_bed: Option<&str>,
59    snv_only_flag: bool,
60    depth_threshold: usize,
61    prob_json: Option<&str>,
62    variant_json: Option<&str>,
63) -> Result<f64, String> {
64    // collect varaints
65    let regions: Vec<String> = match loci_bed {
66        Some(bed) => read_bed(bed)?,
67        _ => vec![],
68    };
69    let mut variant_vector: Vec<VariantPosition> =
70        build_variant_list(vcf_file, snv_only_flag, depth_threshold, regions)?;
71
72    // using variants as input to estimate contamination
73    let mut result_vector: Vec<ContamProbResult> = Vec::with_capacity(MAX_CONTAM); // initialize a result array to store all result
74    let mut best_guess: Option<ContamProbResult> = None;
75    let contamination_range_to_evaluate = (1..MAX_CONTAM).map(|x| x as f64 * DECIMAL_PLACE);
76    for hypothetical_contamination_level in contamination_range_to_evaluate {
77        // loop over the hypothetical contamination level
78        // and calculate the log likelihood
79        let log_prob: f64 =
80            calculate_contam_hypothesis(&mut variant_vector, hypothetical_contamination_level)?;
81
82        // store them into a result object
83        let output: ContamProbResult = ContamProbResult {
84            contamination_level: hypothetical_contamination_level,
85            log_likelihood: log_prob,
86        };
87        // and put them in to a result array
88        result_vector.push(output);
89
90        // evaluate whether the newly computed result
91        // is better than the previous best one?
92        // We will always keep the better guess
93        match best_guess {
94            None => {
95                best_guess = Some(output);
96            }
97            Some(bg) => {
98                if output.log_likelihood > bg.log_likelihood {
99                    best_guess = Some(output);
100                }
101            }
102        }
103    }
104    let best_guess_contam_level = best_guess
105        .ok_or("No best guess contam object")?
106        .contamination_level;
107
108    // just writing out the result/intermediate files
109    if prob_json.is_some() {
110        // write result json file
111        let json_string =
112            serde_json::to_string_pretty(&result_vector).map_err(|e| e.to_string())?;
113        write_json(prob_json.ok_or("No prob json name found")?, json_string)?
114    }
115
116    if variant_json.is_some() {
117        // recalculate loglik
118        info!("Adding labels to the variant data json");
119        calculate_contam_hypothesis(&mut variant_vector, best_guess_contam_level)?;
120        // write variant json file
121        let json_string =
122            serde_json::to_string_pretty(&variant_vector).map_err(|e| e.to_string())?;
123        write_json(
124            variant_json.ok_or("No variant json name found")?,
125            json_string,
126        )?
127    }
128
129    Ok(best_guess_contam_level)
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use assert_approx_eq::assert_approx_eq;
136    use rstest::*;
137    use serde_json::Value;
138    use std::io::Read;
139
140    #[rstest]
141    #[case(
142        false,
143        true,
144        1000,
145        Some("prob.json"),
146        Some("variants.json"),
147        0.046,
148        None
149    )]
150    #[case(false, true, 1000, None, None, 0.046, None)]
151    #[case(false, true, 10, None, None, 0.046, None)]
152    #[case(false, true, 10, None, None, 0.046, None)]
153    #[case(false, false, 1100, None, None, 0.399, None)]
154    #[case(false, true, 1100, None, None, 0.043, None)]
155    #[case(true, true, 200, None, None, 0.001, Some("data/test.bed"))] // fetch region from bed
156    #[case(true, true, 200, None, None, 0.046, None)] // fetch region from bed
157    fn test_run(
158        #[case] gz_input: bool,
159        #[case] snv_only_flag: bool,
160        #[case] depth_threshold: usize,
161        #[case] prob_json: Option<&str>,
162        #[case] variant_json: Option<&str>,
163        #[case] expected_out: f64,
164        #[case] bed_file: Option<&str>,
165    ) {
166        // this is an end to end testing to test everything in
167        // the workflow
168        let vcf_file = match gz_input {
169            false => "data/test.vcf",
170            true => "data/test.vcf.gz",
171        };
172        let best_guess_contam_level: f64 = run(
173            vcf_file,
174            bed_file,
175            snv_only_flag,
176            depth_threshold,
177            prob_json,
178            variant_json,
179        )
180        .unwrap();
181        assert_approx_eq!(best_guess_contam_level, expected_out);
182    }
183
184    #[test]
185    #[should_panic(expected = "Fetching bed loci from non bgzipped")]
186    fn test_workflow_exception() {
187        run(
188            "data/test.vcf",
189            Some("data/test.bed"),
190            true,
191            100,
192            None,
193            None,
194        )
195        .unwrap();
196    }
197
198    #[test]
199    fn test_write_json() {
200        let json_string = "{\"data/test.vcf\":0.046 }";
201        write_json("test.json", json_string.to_string()).unwrap();
202
203        let mut file = File::open("test.json").unwrap();
204        let mut data = String::new();
205        file.read_to_string(&mut data).unwrap();
206
207        let json_data: Value = serde_json::from_str(&data).expect("Bad json data?");
208        assert_eq!(json_data["data/test.vcf"], 0.046);
209    }
210}