Skip to main content

sva_cli/args/
solve.rs

1// Concern: parses a solve's cost, its variables, stops and flags | Non-concern: running it (solve.rs), the cost's grammar (sva-core) | IO: (argv tail) -> Command or CliError
2
3use std::path::PathBuf;
4
5use super::reading::{count, hertz};
6use super::{CacheAt, Command, value};
7use sva_core::CliError;
8use sva_core::solve::{DEFAULT_COST_TOL, DEFAULT_MAX_RENDERS, DEFAULT_STEP_TOL, Stops, Vary, vary};
9
10#[derive(Debug, PartialEq)]
11pub struct SolveArgs {
12    /// The cost, or `@fits/<name>`: read once the composition is known.
13    pub cost: String,
14    pub vary: Vec<Vary>,
15    pub stops: Stops,
16    pub sets: Vec<(String, String)>,
17    pub rate: Option<u32>,
18    pub threads: Option<std::num::NonZeroUsize>,
19    pub cache: CacheAt,
20    pub write: bool,
21    pub confirm: bool,
22}
23
24fn number(raw: &str, flag: &str, fits: impl Fn(f64) -> bool, says: &str) -> Result<f64, CliError> {
25    match raw.parse::<f64>() {
26        Ok(v) if v.is_finite() && fits(v) => Ok(v),
27        _ => Err(CliError::usage(
28            "cli.bad_value",
29            format!("{flag} takes {says}, not `{raw}`"),
30        )),
31    }
32}
33
34pub(super) fn solve_args(rest: &[String]) -> Result<Command, CliError> {
35    let mut it = rest.iter();
36    let cost = it.next().filter(|a| !a.starts_with("--")).ok_or_else(|| {
37        CliError::usage(
38            "cli.missing_argument",
39            "solve needs a cost, as `sva-cli solve 'norm(repr.bands(@master) - \
40             repr.bands(sample(\"../ref.wav\")))' --vary gain=0..2`, or a cost file, as \
41             `sva-cli solve @fits/lead --vary gain=0..2`",
42        )
43    })?;
44    let mut stops = Stops {
45        max_renders: DEFAULT_MAX_RENDERS,
46        step_tol: DEFAULT_STEP_TOL,
47        cost_tol: DEFAULT_COST_TOL,
48    };
49    let (mut vary_all, mut sets) = (Vec::new(), Vec::new());
50    let (mut rate, mut threads, mut cache) = (None, None, CacheAt::Platform);
51    let (mut write, mut confirm) = (false, false);
52    while let Some(flag) = it.next() {
53        match flag.as_str() {
54            "--vary" => vary_all.push(vary(&value(&mut it, "--vary")?)?),
55            "--set" => sets.push(sva_core::set(&value(&mut it, "--set")?)?),
56            "--max-renders" => {
57                let raw = value(&mut it, "--max-renders")?;
58                stops.max_renders = count(&raw, "--max-renders", "renders")?.get();
59            }
60            "--step-tol" => {
61                let raw = value(&mut it, "--step-tol")?;
62                let says = "a fraction of each variable's first step above 0 and at most 1";
63                stops.step_tol = number(&raw, "--step-tol", |v| v > 0.0 && v <= 1.0, says)?;
64            }
65            "--cost-tol" => {
66                let raw = value(&mut it, "--cost-tol")?;
67                let says = "a cost at or above 0";
68                stops.cost_tol = number(&raw, "--cost-tol", |v| v >= 0.0, says)?;
69            }
70            "--rate" => rate = Some(hertz(&value(&mut it, "--rate")?)?),
71            "--threads" => {
72                threads = Some(count(
73                    &value(&mut it, "--threads")?,
74                    "--threads",
75                    "threads",
76                )?);
77            }
78            "--cache" => {
79                cache = match value(&mut it, "--cache")?.as_str() {
80                    "none" => CacheAt::Off,
81                    path => CacheAt::Path(PathBuf::from(path)),
82                };
83            }
84            "--write" => write = true,
85            "--confirm" => confirm = true,
86            other => {
87                return Err(CliError::usage(
88                    "cli.unknown_argument",
89                    format!("unknown argument `{other}`"),
90                ));
91            }
92        }
93    }
94    if vary_all.is_empty() {
95        return Err(CliError::usage(
96            "solve.no_vary",
97            "solve needs at least one `--vary <name>=<lo>..<hi>`, a variable under \
98             `variables/` it fits",
99        ));
100    }
101    if confirm && !write {
102        return Err(CliError::usage(
103            "cli.confirm_without_write",
104            "`--confirm` lets `--write` replace a variable's file; write both, or neither",
105        ));
106    }
107    Ok(Command::Solve(Box::new(SolveArgs {
108        cost: cost.clone(),
109        vary: vary_all,
110        stops,
111        sets,
112        rate,
113        threads,
114        cache,
115        write,
116        confirm,
117    })))
118}