1
use std::path::PathBuf;
2

            
3
use clap::{Args, Parser, Subcommand};
4
use color_eyre::Result;
5
use tracing::Level;
6
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
7

            
8
use crate::args::{GraphArgs, MergeRule, Selection, StandardArgs};
9
use crate::subcommands::{
10
    check_for_haplotype, compare_haplotypes, coverage, mbah, mrca, mrca_scan,
11
    read_sample_names, recursive_mbah, ref_ht_comparison, rwc, sample_rwc,
12
    sample_shared_haplotype, shared_haplotype,
13
};
14

            
15
#[derive(Parser, Debug)]
16
#[command(author, version, about)]
17
pub struct Arguments {
18
    #[command(subcommand)]
19
    cmd: SubCommand,
20

            
21
    /// Verbosity level
22
    #[arg(short, long, action = clap::ArgAction::Count)]
23
    verbosity: u8,
24

            
25
    /// File path to save logging information
26
    #[arg(short, long)]
27
    log_file: Option<PathBuf>,
28
}
29

            
30
17
#[derive(clap::ValueEnum, Clone, Default, Debug)]
31
pub enum ClapSelection {
32
    /// Select all alleles
33
    #[default]
34
    All,
35
    /// Select only the alleles containing the alt variant at given a coords
36
    OnlyAlts,
37
    /// Select only the alleles containing the ref variant at given a coord
38
    OnlyRefs,
39
    /// Select only the longest alleles per sample in the majority based ancestral haplotype
40
    /// algorithm
41
    OnlyLongest,
42
    /// Use for unphased data
43
    Unphased,
44
}
45

            
46
#[derive(clap::ValueEnum, Clone, Default, Debug)]
47
pub enum ClapMergeRule {
48
    /// Merge RWC hits based on start positions
49
    #[default]
50
    Start,
51
    /// Merge RWC hits based on stop positions
52
    Stop,
53
}
54

            
55
17
#[derive(Args, Debug, Default, Clone)]
56
pub struct ClapStandardArgs {
57
    pub file: PathBuf,
58

            
59
    /// Variant coordinates, i.e. chr9:27573534
60
    #[arg(short = 'c', long)]
61
    pub coords: String,
62

            
63
    /// Output directory
64
    #[arg(short = 'o', long, default_value_os_t = PathBuf::from("./"))]
65
    pub outdir: PathBuf,
66

            
67
    /// List of samples to select from the given vcf file
68
    #[arg(short = 'S', long)]
69
    pub samples: Option<PathBuf>,
70

            
71
    /// Selection of a subset of alleles
72
    #[arg(long, value_enum, default_value_t=ClapSelection::All)]
73
    pub select: ClapSelection,
74

            
75
    /// Info limit for accepted variants
76
    #[arg(long)]
77
    pub info_limit: Option<f32>,
78

            
79
    /// Output filename prefix
80
    #[arg(long)]
81
    pub prefix: Option<String>,
82
}
83

            
84
impl From<ClapStandardArgs> for StandardArgs {
85
97
    fn from(value: ClapStandardArgs) -> Self {
86
97
        let prefix = match value.prefix {
87
            Some(v) => match v.as_str() {
88
                "" => None,
89
                v => Some(v.to_string())
90
            },
91
97
            None => None,
92
        };
93
97
        Self {
94
97
            file: value.file,
95
97
            coords: value.coords,
96
97
            output: value.outdir,
97
97
            samples: value.samples,
98
97
            selection: value.select.into(),
99
97
            info_limit: value.info_limit,
100
97
            prefix,
101
97
        }
102
97
    }
103
}
104

            
105
impl From<ClapSelection> for Selection {
106
98
    fn from(value: ClapSelection) -> Self {
107
98
        match value {
108
12
            ClapSelection::OnlyAlts => Self::OnlyAlts,
109
12
            ClapSelection::OnlyRefs => Self::OnlyRefs,
110
32
            ClapSelection::All => Self::All,
111
30
            ClapSelection::OnlyLongest => Self::OnlyLongest,
112
12
            ClapSelection::Unphased => Self::Unphased,
113
        }
114
98
    }
115
}
116

            
117
impl From<ClapMergeRule> for MergeRule {
118
7
    fn from(value: ClapMergeRule) -> Self {
119
7
        match value {
120
7
            ClapMergeRule::Start => Self::Start,
121
            ClapMergeRule::Stop => Self::Stop,
122
        }
123
7
    }
124
}
125

            
126
11
#[derive(Args, Debug, Default, Clone)]
127
pub struct ClapGraphArgs {
128
    /// Graph width in px
129
    #[arg(long)]
130
    pub width: Option<u32>,
131

            
132
    /// Graph height in px
133
    #[arg(long)]
134
    pub height: Option<u32>,
135

            
136
    /// Mark the variant coordinate
137
    #[arg(long)]
138
    pub mark_locus: bool,
139

            
140
    // Font size
141
    #[arg(long)]
142
    pub font_size: Option<usize>,
143

            
144
    // Line stroke width
145
    #[arg(long)]
146
    pub stroke_width: Option<u32>,
147

            
148
    // Font color
149
    #[arg(long)]
150
    pub color: Option<String>,
151

            
152
    // Background color
153
    #[arg(long)]
154
    pub background_color: Option<String>,
155
}
156

            
157
impl From<ClapGraphArgs> for GraphArgs {
158
13
    fn from(value: ClapGraphArgs) -> Self {
159
13
        Self {
160
13
            width: value.width.unwrap_or(2560),
161
13
            height: value.height.unwrap_or(1440),
162
13
            mark_locus: value.mark_locus,
163
13
            font_size: value.font_size.unwrap_or(20),
164
13
            stroke_width: value.stroke_width.unwrap_or(5),
165
13
            color: value.color.unwrap_or("black".to_string()),
166
13
            background_color: value.background_color.unwrap_or("white".to_string()),
167
13
        }
168
13
    }
169
}
170

            
171
#[derive(Subcommand, Debug)]
172
#[allow(clippy::upper_case_acronyms)]
173
pub enum SubCommand {
174
    /// Genome wide runs without contrahomozygosity
175
    RWC {
176
        file: PathBuf,
177

            
178
        /// Output directory
179
        #[arg(short = 'o', long, default_value_os_t = PathBuf::from("./"))]
180
        outdir: PathBuf,
181

            
182
        /// List of samples to select from the given vcf file
183
        #[arg(short = 'S', long)]
184
        samples: Option<PathBuf>,
185

            
186
        /// Info limit for accepted variants
187
        #[arg(long)]
188
        info_limit: Option<f32>,
189

            
190
        /// Sort output file
191
        #[arg(short = 's', long)]
192
        sort: bool,
193

            
194
        /// Number of threads
195
        #[arg(short = 't', long, default_value_t = 8)]
196
        threads: usize,
197

            
198
        /// Minimum run lengths for runs without contrahomozygosity
199
        #[arg(long, default_value_t = 5)]
200
        min_marker_length: usize,
201

            
202
        /// Output filename prefix
203
        #[arg(long)]
204
        prefix: Option<String>,
205
    },
206
    /// Random sampled genome wide runs without contrahomozygosity
207
    SampleRWC {
208
        file: PathBuf,
209

            
210
        /// Output directory
211
        #[arg(short = 'o', long, default_value_os_t = PathBuf::from("./"))]
212
        outdir: PathBuf,
213

            
214
        /// List of samples to select from the given vcf file
215
        #[arg(short = 'S', long)]
216
        samples: Option<PathBuf>,
217

            
218
        /// Info limit for accepted variants
219
        #[arg(long)]
220
        info_limit: Option<f32>,
221

            
222
        /// Sample size for random sampling
223
        #[arg(short = 's', long)]
224
        sample_size: usize,
225

            
226
        /// Number random sampling iterations
227
        #[arg(short = 'i', long = "iters", default_value_t = 100)]
228
        niters: i64,
229

            
230
        /// Sort output file
231
        #[arg(short = 's', long)]
232
        sort: bool,
233

            
234
        /// Number of threads
235
        #[arg(short = 't', long, default_value_t = 8)]
236
        threads: usize,
237

            
238
        /// Number of top markers per chromosome per iteration
239
        #[arg(long = "top-markers-per-chr", default_value_t = 10)]
240
        top_markers_n: usize,
241

            
242
        /// Minimum run lengths for runs without contrahomozygosity
243
        #[arg(long, default_value_t = 5)]
244
        min_marker_length: usize,
245

            
246
        /// Rule for merging variants found per iteration
247
        #[arg(long, value_enum, default_value_t=ClapMergeRule::Start)]
248
        merge_rule: ClapMergeRule,
249

            
250
        /// Output filename prefix
251
        #[arg(long)]
252
        prefix: Option<String>,
253
    },
254
    /// Random sample the runs of haplotype segments in common at a coordinate
255
    SampleSharedHaplotype {
256
        #[command(flatten)]
257
        args: ClapStandardArgs,
258

            
259
        /// Number random sampling iterations
260
        #[arg(short = 'i', long = "iters", default_value_t = 10_000)]
261
        niters: i64,
262

            
263
        /// Sample size for random sampling
264
        #[arg(short = 's', long)]
265
        sample_size: usize,
266

            
267
        /// Number of threads
268
        #[arg(short = 't', long, default_value_t = 8)]
269
        threads: usize,
270
    },
271
    /// Check samples for the existence of a given haplotype
272
    CheckForHaplotype {
273
        #[command(flatten)]
274
        args: ClapStandardArgs,
275

            
276
        /// Haplotype for checking
277
        #[arg(long)]
278
        haplotype: PathBuf,
279
    },
280
    /// Check differences between samples and a given haplotype
281
    CompareToHaplotype {
282
        #[command(flatten)]
283
        args: ClapStandardArgs,
284

            
285
        /// Haplotype for comparing to
286
        #[arg(long)]
287
        haplotype: PathBuf,
288

            
289
        /// Number of threads
290
        #[arg(short = 't', long, default_value_t = 8)]
291
        threads: usize,
292

            
293
        /// Decoy sample names for tagging
294
        #[arg(short = 'S', long)]
295
        decoy_samples: Option<PathBuf>,
296

            
297
        /// Mark shorter alleles to graph
298
        #[arg(long)]
299
        mark_shorter_alleles: bool,
300

            
301
        #[command(flatten)]
302
        graph_args: ClapGraphArgs,
303

            
304
        /// Output .png image
305
        #[arg(long)]
306
        png: bool,
307
    },
308
    /// Derive the majority based ancestral haplotype algorithm
309
    MBAH {
310
        #[command(flatten)]
311
        args: ClapStandardArgs,
312

            
313
        #[command(flatten)]
314
        graph_args: ClapGraphArgs,
315

            
316
        /// Start position of the graph
317
        #[arg(long)]
318
        start: Option<i64>,
319

            
320
        /// Stop position of the graph
321
        #[arg(long)]
322
        stop: Option<i64>,
323
    },
324
    /// Recursive majority based ancestral haplotype algorithm
325
    RecursiveMBAH {
326
        #[command(flatten)]
327
        args: ClapStandardArgs,
328

            
329
        /// Variable file
330
        #[arg(long)]
331
        variable_data: Option<PathBuf>,
332

            
333
        /// Wanted variable columns from the variable file
334
        #[arg(long = "vars")]
335
        variables: Option<Vec<String>>,
336

            
337
        /// List of decoy samples to tag
338
        #[arg(short = 'S', long)]
339
        decoy_samples: Option<PathBuf>,
340

            
341
        #[command(flatten)]
342
        graph_args: ClapGraphArgs,
343

            
344
        /// Save haplotypes of every branch, takes longer to run
345
        #[arg(long)]
346
        save_branch_haplotypes: bool,
347
    },
348
    /// Find haplotype segments in common with all the samples at a given coordinate
349
    SharedHaplotype {
350
        #[command(flatten)]
351
        args: ClapStandardArgs,
352

            
353
        /// Allow missing genotypes
354
        #[arg(long)]
355
        missing_allowed: bool,
356
    },
357
    /// Analyze the MRCA based on the Gamma method at a given coordinate
358
    Mrca {
359
        #[command(flatten)]
360
        args: ClapStandardArgs,
361

            
362
        /// Recombination rate file
363
        #[arg(short = 'r', long)]
364
        recombination_rates: PathBuf,
365
    },
366
    /// Analyze the MRCA every x markers along a given contig
367
    MrcaScan {
368
        #[command(flatten)]
369
        args: ClapStandardArgs,
370

            
371
        /// Recombination rate file
372
        #[arg(short = 'r', long)]
373
        recombination_rates: PathBuf,
374

            
375
        /// Scan start position
376
        #[arg(long)]
377
        start: Option<i64>,
378

            
379
        /// Scan stop position
380
        #[arg(long)]
381
        stop: Option<i64>,
382

            
383
        /// Run the MRCA analysis every n markers
384
        #[arg(long)]
385
        step_size: usize,
386

            
387
        /// Dont output to csv
388
        #[arg(long)]
389
        no_csv: bool,
390

            
391
        /// Draw plot
392
        #[arg(long)]
393
        plot: bool,
394

            
395
        /// Number of threads
396
        #[arg(short = 't', long, default_value_t = 8)]
397
        threads: usize,
398

            
399
        #[command(flatten)]
400
        graph_args: ClapGraphArgs,
401

            
402
        /// Only supports hg38! Mark the centromere to the graph
403
        #[arg(long)]
404
        mark_centromere: bool,
405
    },
406
    /// Show coverage levels per contig in a given vcf
407
    Coverage {
408
        file: PathBuf,
409

            
410
        /// Minimum amount of basepairs on avg per a single snp
411
        #[arg(short = 'b', long, default_value_t = 10000)]
412
        bp_per_snp: i64,
413

            
414
        /// Number of pipes
415
        #[arg(short = 'p', long, default_value_t = 100)]
416
        npipes: i64,
417

            
418
        /// Number of threads
419
        #[arg(short = 't', long, default_value_t = 8)]
420
        threads: usize,
421
    },
422
    /// Compare haplotypes to each other by alignment
423
    CompareHaplotypes {
424
        haplotypes: Vec<PathBuf>,
425

            
426
        /// Output directory
427
        #[arg(short = 'o', long, default_value_os_t = PathBuf::from("./"))]
428
        outdir: PathBuf,
429

            
430
        /// Output filename prefix
431
        #[arg(long)]
432
        prefix: Option<String>,
433

            
434
        /// Output to csv instead of stdout
435
        #[arg(long)]
436
        csv: bool,
437

            
438
        /// Hide missing (meaning yellow) variants from the stdout print
439
        #[arg(long)]
440
        hide_missing: bool,
441

            
442
        /// Tag with ok for matching, err for mismatching and mis for mismatching to null rows
443
        #[arg(long)]
444
        tag_rows: bool,
445
    },
446
    /// Output the sample names from a .fam / .vcf file
447
    Samples { file: PathBuf },
448
}
449

            
450
// csv: bool,
451

            
452
impl SubCommand {
453
38
    pub fn threads(&self) -> usize {
454
38
        match self {
455
            SubCommand::RWC { threads, .. }
456
            | SubCommand::SampleRWC { threads, .. }
457
            | SubCommand::SampleSharedHaplotype { threads, .. }
458
30
            | SubCommand::CompareToHaplotype { threads, .. }
459
1
            | SubCommand::Coverage { threads, .. }
460
37
            | SubCommand::MrcaScan { threads, .. } => *threads,
461
            SubCommand::RecursiveMBAH { .. } => 2,
462
1
            _ => 1,
463
        }
464
38
    }
465
}
466

            
467
pub fn run_args(args: Arguments) -> Result<()> {
468
    rayon::ThreadPoolBuilder::new()
469
        .num_threads(args.cmd.threads())
470
        .build_global()?;
471

            
472
    let (level, wrtr, _guard) = init_tracing(args.verbosity, &args.log_file)?;
473
    tracing_subscriber::fmt()
474
        .with_max_level(level)
475
        .with_writer(wrtr)
476
        .init();
477

            
478
    run_cmd(args.cmd)?;
479

            
480
    Ok(())
481
}
482

            
483
#[rustfmt::skip]
484
120
pub fn run_cmd(cmd: SubCommand) -> Result<()> {
485
120
    match cmd {
486
        SubCommand::RWC { file, samples, outdir, info_limit, sort, min_marker_length, prefix, .. }
487
            => rwc::run(file, samples, outdir, info_limit, sort, min_marker_length, prefix)?,
488

            
489
        SubCommand::SampleRWC { 
490
6
            file, samples, outdir, info_limit, sample_size, niters,
491
6
            sort, min_marker_length, top_markers_n, merge_rule, prefix, .. 
492
6
        } => sample_rwc::run(
493
6
            file, samples, outdir, info_limit, sample_size, niters,
494
6
            sort, min_marker_length, top_markers_n, merge_rule.into(), prefix
495
6
        )?,
496

            
497
12
        SubCommand::SampleSharedHaplotype { args, niters, sample_size, .. }
498
12
            => sample_shared_haplotype::run(args.into(), niters, sample_size)?,
499

            
500
18
        SubCommand::CheckForHaplotype { args, haplotype } 
501
18
            => check_for_haplotype::run(args.into(), haplotype)?,
502

            
503
        SubCommand::CompareToHaplotype { 
504
36
            args, haplotype, decoy_samples, mark_shorter_alleles, graph_args, png, .. 
505
36
        } => ref_ht_comparison::run(
506
36
                args.into(), haplotype, decoy_samples, mark_shorter_alleles, png,
507
36
                GraphArgs {
508
36
                    width: graph_args.width.unwrap_or(5000),
509
36
                    height: graph_args.height.unwrap_or(7500),
510
36
                    mark_locus: graph_args.mark_locus,
511
36
                    font_size: graph_args.font_size.unwrap_or(75),
512
36
                    stroke_width: graph_args.stroke_width.unwrap_or(7),
513
36
                    color: graph_args.color.unwrap_or("black".into()),
514
36
                    background_color: graph_args.background_color.unwrap_or("white".into()),
515
36
                },
516
36
            )?,
517

            
518
        SubCommand::SharedHaplotype { args, missing_allowed } 
519
            => shared_haplotype::run(args.into(), missing_allowed)?,
520

            
521
        SubCommand::MBAH { args, graph_args, start, stop }
522
            => mbah::run(args.into(), graph_args.into(), start, stop)?,
523

            
524
6
        SubCommand::Mrca { args, recombination_rates } => mrca::run(args.into(), recombination_rates)?,
525

            
526
        SubCommand::MrcaScan { 
527
12
            args, recombination_rates, start, stop, step_size, no_csv, plot, graph_args, mark_centromere, ..
528
12
        } => mrca_scan::run(
529
12
            args.into(), recombination_rates, start, stop, step_size, no_csv, plot, graph_args.into(), mark_centromere
530
12
        )?,
531

            
532
6
        SubCommand::Coverage { file, bp_per_snp, npipes, .. } => coverage::run(file, bp_per_snp, npipes)?,
533

            
534
6
        SubCommand::Samples { file } => read_sample_names::run(file)?,
535

            
536
6
        SubCommand::CompareHaplotypes { haplotypes, outdir, prefix, csv, hide_missing, tag_rows }
537
6
            => compare_haplotypes::run(haplotypes, outdir, prefix, csv, hide_missing, tag_rows)?,
538

            
539
        SubCommand::RecursiveMBAH {
540
12
            args, variable_data, variables, decoy_samples, graph_args, save_branch_haplotypes
541
12
        } => recursive_mbah::run(
542
12
            args.into(),
543
12
            GraphArgs {
544
12
                width: graph_args.width.unwrap_or(20_000),
545
12
                height: graph_args.height.unwrap_or(14_000),
546
12
                font_size: graph_args.font_size.unwrap_or(210),
547
12
                stroke_width: graph_args.stroke_width.unwrap_or(20),
548
12
                color: graph_args.color.unwrap_or("black".into()),
549
12
                background_color: graph_args.background_color.unwrap_or("white".into()),
550
12
                ..Default::default()
551
12
            },
552
12
            variable_data,
553
12
            variables,
554
12
            decoy_samples,
555
12
            save_branch_haplotypes
556
12
        )?,
557
    };
558
108
    Ok(())
559
120
}
560

            
561
6
fn init_tracing(
562
6
    verbosity: u8,
563
6
    log_file: &Option<PathBuf>,
564
6
) -> Result<(Level, NonBlocking, WorkerGuard)> {
565
6
    let level = match verbosity {
566
2
        0 => Level::ERROR,
567
1
        1 => Level::WARN,
568
1
        2 => Level::INFO,
569
1
        3 => Level::DEBUG,
570
1
        4..=std::u8::MAX => Level::TRACE,
571
    };
572

            
573
    // Write logs to stderr or file
574
6
    let (wrtr, _guard) = match log_file {
575
1
        Some(path) => {
576
1
            let file = std::fs::File::options()
577
1
                .create(true)
578
1
                .write(true)
579
1
                .truncate(true)
580
1
                .open(path)?;
581
1
            tracing_appender::non_blocking(file)
582
        }
583
5
        None => tracing_appender::non_blocking(std::io::stderr()),
584
    };
585

            
586
6
    Ok((level, wrtr, _guard))
587
6
}
588

            
589
#[cfg(test)]
590
mod tests {
591
    use super::*;
592

            
593
1
    #[test]
594
1
    fn test_init_tracing() {
595
1
        let (level, _, _) = init_tracing(0, &None).unwrap();
596
1
        assert_eq!(Level::ERROR, level);
597
1
        let (level, _, _) = init_tracing(1, &None).unwrap();
598
1
        assert_eq!(Level::WARN, level);
599
1
        let (level, _, _) = init_tracing(2, &None).unwrap();
600
1
        assert_eq!(Level::INFO, level);
601
1
        let (level, _, _) = init_tracing(3, &None).unwrap();
602
1
        assert_eq!(Level::DEBUG, level);
603
1
        let (level, _, _) = init_tracing(4, &None).unwrap();
604
1
        assert_eq!(Level::TRACE, level);
605

            
606
1
        let (_, _, _) = init_tracing(0, &Some(PathBuf::from("tests/results/logs.txt"))).unwrap();
607
1
    }
608

            
609
1
    #[test]
610
1
    fn test_from_traits() {
611
1
        let clap_args = ClapStandardArgs::default();
612
1
        let st_args1: StandardArgs = clap_args.into();
613
1

            
614
1
        let st_args2 = StandardArgs::default();
615
1

            
616
1
        assert_eq!(st_args1, st_args2);
617

            
618
1
        let clap_args = ClapGraphArgs::default();
619
1
        let gr_args1: GraphArgs = clap_args.into();
620
1
        let gr_args2 = GraphArgs::default();
621
1
        assert_eq!(gr_args1, gr_args2);
622

            
623
1
        let clap_merge_rule = ClapMergeRule::Start;
624
1
        let merge_rule: MergeRule = clap_merge_rule.into();
625
1
        assert_eq!(merge_rule, MergeRule::Start);
626

            
627
1
        let clap_selection = ClapSelection::All;
628
1
        let selection: Selection = clap_selection.into();
629
1
        assert_eq!(selection, Selection::All)
630
1
    }
631

            
632
1
    #[test]
633
1
    fn test_threads() {
634
1
        let subcommand = SubCommand::Samples {
635
1
            file: PathBuf::new(),
636
1
        };
637
1

            
638
1
        assert_eq!(1, subcommand.threads());
639

            
640
1
        let subcommand = SubCommand::Coverage {
641
1
            file: PathBuf::new(),
642
1
            bp_per_snp: 0,
643
1
            npipes: 0,
644
1
            threads: 8,
645
1
        };
646
1

            
647
1
        assert_eq!(8, subcommand.threads());
648
1
    }
649
}