1use clap::{Parser, ValueEnum};
34use std::path::PathBuf;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
38pub enum Stage {
39 Curvature,
41 Symmetry,
43 Calls,
45 FinalCalls,
47}
48
49impl Stage {
50 pub fn is_calls(self) -> bool {
52 matches!(self, Stage::Calls | Stage::FinalCalls)
53 }
54}
55
56impl From<Stage> for crate::curve::scan::Stage {
57 fn from(stage: Stage) -> Self {
58 match stage {
59 Stage::Curvature => Self::Curvature,
60 Stage::Symmetry | Stage::Calls | Stage::FinalCalls => Self::Symmetry,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
68pub enum Roll {
69 Simple,
71 Active,
73}
74
75impl From<Roll> for crate::curve::matrix::RollType {
76 fn from(roll: Roll) -> Self {
77 match roll {
78 Roll::Simple => Self::Simple,
79 Roll::Active => Self::Active,
80 }
81 }
82}
83
84#[derive(Parser, Debug)]
85#[command(version = env!("CARGO_PKG_VERSION"), about = "Symmetry of DNA curvature.", long_about = None)]
86pub struct Cli {
87 pub input: PathBuf,
89
90 pub output: PathBuf,
92
93 #[arg(short, long)]
95 pub verbose: bool,
96
97 #[arg(short, long)]
99 pub matrices: Option<PathBuf>,
100
101 #[arg(long, default_value = "15", value_parser = clap::value_parser!(u16).range(1..))]
103 pub curve_step: u16,
104
105 #[arg(long, default_value = "0.33335", value_parser = parse_float_in_range)]
107 pub curve_scale: f64,
108
109 #[arg(long, default_value = "6", value_parser = clap::value_parser!(u16).range(1..))]
111 pub curve_step_one: u16,
112
113 #[arg(long, default_value = "4", value_parser = clap::value_parser!(u16).range(1..))]
115 pub curve_step_two: u16,
116
117 #[arg(long, default_value = "101", value_parser = clap::value_parser!(u16).range(1..))]
119 pub symcurve_win: u16,
120
121 #[arg(long, default_value = "1", value_parser = clap::value_parser!(u16).range(1..))]
123 pub symcurve_step: u16,
124
125 #[arg(long, default_value = "30", value_parser = clap::value_parser!(u16).range(1..))]
127 pub min_linker_size: u16,
128
129 #[arg(long, default_value = "8G")]
131 pub max_memory: crate::memory::MemoryBudget,
132
133 #[arg(long, value_enum, default_value_t = Stage::Curvature)]
135 pub stage: Stage,
136
137 #[arg(long, value_enum, default_value_t = Roll::Simple)]
139 pub roll: Roll,
140}
141
142fn parse_float_in_range(s: &str) -> Result<f64, String> {
143 let value = s
144 .parse::<f64>()
145 .map_err(|_| "Value must be a floating-point number")?;
146 if (0.0..=1.0).contains(&value) {
147 Ok(value)
148 } else {
149 Err("The value must be between 0 and 1".to_owned())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use clap::error::*;
157
158 #[test]
159 fn test_cli_args() {
160 let args = Cli::parse_from([
162 "symcurve",
163 "input.fasta",
164 "output.bw",
165 "--verbose",
166 "--matrices",
167 "matrices.yaml",
168 "--curve-step",
169 "20",
170 ]);
171 assert_eq!(args.input.to_str().unwrap(), "input.fasta");
172 assert_eq!(args.output.to_str().unwrap(), "output.bw");
173 assert!(args.verbose);
174 assert_eq!(args.matrices.unwrap().to_str().unwrap(), "matrices.yaml");
175 assert_eq!(args.curve_step, 20);
176 }
177
178 #[test]
179 fn test_missing_matrix_file() {
180 let args_result =
181 Cli::try_parse_from(["symcurve", "input.fasta", "output.bw", "--matrices"]);
182 let cmd = clap::Command::new("symcurve");
186 let mut err = clap::Error::new(ErrorKind::InvalidValue).with_cmd(&cmd);
187 err.insert(
188 ContextKind::InvalidArg,
189 ContextValue::String("--matrices <MATRICES>".to_owned()),
190 );
191 err.insert(
192 ContextKind::InvalidValue,
193 ContextValue::String("".to_owned()),
194 );
195 assert!(args_result.is_err());
196 assert_eq!(args_result.unwrap_err().to_string(), err.to_string());
197 }
198
199 #[test]
201 fn test_zero_curve_step() {
202 let args_result =
203 Cli::try_parse_from(["symcurve", "input.fasta", "output.bw", "--curve-step", "0"]);
204 assert!(args_result.is_err());
205 assert!(
206 args_result
207 .unwrap_err()
208 .to_string()
209 .starts_with("error: invalid value '0' for '--curve-step")
210 );
211 }
212
213 fn get_different_curve_scale_parsings(curve_scale_s: &str) -> Result<Cli, clap::error::Error> {
215 Cli::try_parse_from([
216 "symcurve",
217 "input.fasta",
218 "output.bw",
219 "--curve-scale",
220 curve_scale_s,
221 ])
222 }
223
224 #[test]
225 fn test_curve_scale_keeps_full_precision() {
226 let args = Cli::parse_from(["symcurve", "in.fa", "out.bw"]);
230 assert_eq!(args.curve_scale, 0.33335_f64);
231
232 let args = Cli::parse_from(["symcurve", "in.fa", "out.bw", "--curve-scale", "0.1"]);
233 assert_eq!(args.curve_scale, 0.1_f64);
234 assert_ne!(args.curve_scale, f64::from(0.1_f32));
235 }
236
237 #[test]
238 fn test_max_memory_parses_and_defaults() {
239 let args = Cli::parse_from(["symcurve", "in.fa", "out.bw"]);
240 assert_eq!(args.max_memory.to_string(), "8G");
241
242 let args = Cli::parse_from(["symcurve", "in.fa", "out.bw", "--max-memory", "512M"]);
243 assert_eq!(args.max_memory.bytes(), 512 * 1024 * 1024);
244
245 let bad = Cli::try_parse_from(["symcurve", "in.fa", "out.bw", "--max-memory", "lots"]);
246 assert!(bad.is_err());
247 assert!(
248 bad.unwrap_err().to_string().contains("8G"),
249 "the error should show the expected form"
250 );
251 }
252
253 #[test]
254 fn test_curve_scale() {
255 assert!(get_different_curve_scale_parsings("0").is_ok());
257 assert!(get_different_curve_scale_parsings("0.33").is_ok());
258 assert!(get_different_curve_scale_parsings("1").is_ok());
259 assert!(get_different_curve_scale_parsings("1.1").is_err());
260 assert!(get_different_curve_scale_parsings("-1").is_err());
261 assert!(get_different_curve_scale_parsings("abc").is_err());
262 }
263}