1mod cost;
4mod load;
5mod residual;
6mod search;
7mod vary;
8
9pub use cost::{
10 Cost, From, Held, LOAD_JSON, Leaf, Load, OPERATORS, Op, REDUCERS, REPR, SUBTRACTS, Target,
11 Tree, built, cost, takes,
12};
13pub use load::{NoFiles, Texts};
14pub use residual::{FLOOR_DB, MISSED_NOTE_CENTS, MISSED_ONSET_SECS, RULES};
15pub use search::{
16 DEFAULT_COST_TOL, DEFAULT_MAX_RENDERS, DEFAULT_STEP_TOL, FIRST_STEP_OF_SPAN, Stopped, Stops,
17};
18pub use vary::{Precision, Vary, vary};
19
20use std::borrow::Cow;
21use std::num::NonZeroUsize;
22use std::sync::Arc;
23
24use sva_ast::{Listing, Recording, Source};
25use sva_engine::{Abandon, Backend, EngineError, Output, Resampled, Session, Tier};
26
27use crate::cli_error::CliError;
28use crate::json::{escape, list, num};
29use crate::output::{Diagnostic, Severity};
30use crate::query::{Asked, asked, call};
31use crate::{Job, execute_over, overlaid};
32use cost::Node;
33use search::{Next, Search};
34
35pub struct Problem<'a> {
36 pub source: &'a dyn Source,
37 pub texts: &'a dyn Texts,
39 pub cost: Cost,
40 pub vary: Vec<Vary>,
41 pub sets: Vec<(String, String)>,
43 pub rate: Option<u32>,
44 pub threads: Option<NonZeroUsize>,
45 pub stops: Stops,
46}
47
48#[derive(Clone, Debug, PartialEq)]
50pub struct Point {
51 pub render: usize,
52 pub values: Vec<(String, f64, String)>,
53 pub total: f64,
54 pub best: f64,
56 pub terms: Vec<f64>,
57 pub computed: Vec<(String, u64)>,
60}
61
62pub struct Solved {
63 pub cost: String,
64 pub terms: Vec<String>,
65 pub start: Vec<String>,
66 pub best: Point,
67 pub on_bound: Vec<bool>,
68 pub renders: usize,
69 pub stopped_by: Stopped,
70 pub trace: Vec<Point>,
71 pub resampled: Vec<Resampled>,
72}
73
74pub const SOLVE_ONLY: &str = "`repr.<reading>(...)` and `load_json(...)` are leaves of a \
76 `sva-cli solve` cost, and only a cost takes them: write the cost on solve's command line \
77 or in `fits/<name>`, as `sva-cli solve @fits/<name> --vary ...`; a node renders, and \
78 `render --representation` reads it";
79
80fn loads_at(text: &str, byte: usize) -> bool {
83 let before = text[..byte.min(text.len())].trim_end();
84 let before = before.strip_suffix('(').map(str::trim_end);
85 before.is_some_and(|b| b.ends_with(LOAD_JSON))
86}
87
88pub(crate) fn hinted(refused: CliError, source: &dyn Source, target: Option<&str>) -> CliError {
91 let solve_only = match &refused {
92 CliError::Refusals(rs) => rs.iter().any(|r| {
93 r.code == sva_ast::DiagCode::MisplacedPath
94 && source
95 .get(&r.at.path)
96 .ok()
97 .flatten()
98 .is_some_and(|text| loads_at(&text, r.at.span.start))
99 }),
100 CliError::BadProbe(_) => {
101 target.is_some_and(|t| t.match_indices('"').any(|(at, _)| loads_at(t, at)))
102 }
103 _ => false,
104 };
105 match solve_only {
106 true => CliError::Hinted {
107 error: Box::new(refused),
108 help: SOLVE_ONLY,
109 },
110 false => refused,
111 }
112}
113
114pub const FITS: &str = "fits";
116
117pub fn cost_of(source: &dyn Source, written: &str) -> Result<Cost, CliError> {
119 let named = written
120 .trim()
121 .strip_prefix('@')
122 .filter(|path| path.starts_with(&format!("{FITS}/")) && sva_ast::whole_ref_path(path));
123 let Some(path) = named else {
124 return cost::cost(written);
125 };
126 let text = source.get(path).map_err(CliError::Io)?.ok_or_else(|| {
127 CliError::NotFound(format!(
128 "`{written}` names no file `{path}` in {}; write the cost there, under an \
129 annotation line, as a node is written",
130 source.name()
131 ))
132 })?;
133 cost::cost_file(path, &text)
134}
135
136struct Recordings<'a> {
138 under: &'a dyn Source,
139 held: Vec<(String, Arc<Recording>)>,
140}
141
142impl Source for Recordings<'_> {
143 fn paths(&self) -> Result<Listing, String> {
144 self.under.paths()
145 }
146
147 fn get(&self, path: &str) -> Result<Option<Cow<'_, str>>, String> {
148 self.under.get(path)
149 }
150
151 fn generation(&self, path: &str) -> Option<u64> {
152 self.under.generation(path)
153 }
154
155 fn name(&self) -> String {
156 self.under.name()
157 }
158
159 fn recording(&self, path: &str) -> Result<Option<Arc<Recording>>, String> {
160 match self.held.iter().find(|(key, _)| key == path) {
161 Some((_, held)) => Ok(Some(Arc::clone(held))),
162 None => self.under.recording(path),
163 }
164 }
165}
166
167fn recorded<'a>(cost: &Cost, under: &'a dyn Source) -> Recordings<'a> {
168 let held = cost
169 .leaves
170 .iter()
171 .filter_map(|leaf| leaf.recording.clone())
172 .map(|held| (held.path.clone(), held))
173 .collect();
174 Recordings { under, held }
175}
176
177pub fn checked(problem: &Problem) -> Result<(Vec<f64>, Vec<Held>), CliError> {
180 let base = recorded(&problem.cost, problem.source);
181 checked_over(problem, &base)
182}
183
184fn checked_over(problem: &Problem, base: &dyn Source) -> Result<(Vec<f64>, Vec<Held>), CliError> {
185 for one in &problem.vary {
186 if let Some((name, _)) = problem.sets.iter().find(|(n, _)| *n == one.name) {
187 return Err(CliError::usage(
188 "solve.set_and_vary",
189 format!(
190 "`{name}` is both held by `--set` and varied by `--vary`; a variable is one \
191 or the other"
192 ),
193 ));
194 }
195 if problem.vary.iter().filter(|v| v.name == one.name).count() > 1 {
196 return Err(CliError::usage(
197 "solve.vary_twice",
198 format!("`{}` is varied twice; vary each variable once", one.name),
199 ));
200 }
201 }
202 let held = overlaid(base, &problem.sets)?;
203 let start = problem
204 .vary
205 .iter()
206 .map(|one| one.start(&held))
207 .collect::<Result<Vec<_>, _>>()?;
208 for one in &problem.vary {
209 reaches(problem, base, one)?;
210 }
211 let mut sets = problem.sets.clone();
212 sets.extend(
213 problem
214 .vary
215 .iter()
216 .zip(&start)
217 .map(|(one, v)| (one.name.clone(), one.text(*v, Precision::Shortest))),
218 );
219 let at_start = overlaid(base, &sets)?;
220 let side = load::Side {
221 source: &at_start,
222 rate: problem.rate,
223 };
224 let loads = load::loaded(&problem.cost, problem.texts, &side)?;
225 problem.cost.typed(Some(&loads))?;
226 Ok((start, loads))
227}
228
229fn reaches(problem: &Problem, base: &dyn Source, one: &Vary) -> Result<(), CliError> {
231 let planned = |value: f64| -> Result<Vec<String>, CliError> {
232 let mut sets = problem.sets.clone();
233 sets.push((one.name.clone(), one.text(value, Precision::Shortest)));
234 let source = overlaid(base, &sets)?;
235 targets(&problem.cost)
236 .iter()
237 .map(|target| {
238 let job = Job {
239 rate: problem.rate,
240 ..Job::over(&source, target)
241 };
242 let (render, _) = crate::plan(&job)?;
243 let identity = sva_engine::identity(&render.tys, render.root)?;
244 Ok(format!("{identity:?} {:?}", render.config.range))
245 })
246 .collect()
247 };
248 if planned(one.lo)? == planned(one.hi)? {
249 return Err(CliError::usage(
250 "solve.reaches_nothing",
251 format!(
252 "`{}` reaches no target of the cost: every target renders the same at `{}` as \
253 at `{}`; vary a variable a target reads, as `@variables/{}` in its node, or add \
254 a target that reads it",
255 one.name,
256 one.text(one.lo, Precision::Shortest),
257 one.text(one.hi, Precision::Shortest),
258 one.name
259 ),
260 ));
261 }
262 Ok(())
263}
264
265fn targets(cost: &Cost) -> Vec<String> {
266 let mut out: Vec<String> = Vec::new();
267 for leaf in &cost.leaves {
268 if !out.contains(&leaf.target) {
269 out.push(leaf.target.clone());
270 }
271 }
272 out
273}
274
275pub async fn solve<B: Backend>(
278 problem: &Problem<'_>,
279 tier: &Tier<B>,
280 heard: &mut dyn FnMut(&Point),
281 abandon: &dyn Abandon,
282) -> Result<Solved, CliError> {
283 let base = recorded(&problem.cost, problem.source);
284 let (start, loads) = checked_over(problem, &base)?;
285 let mut search = Search::new(&problem.vary, &start, problem.stops);
286 let targets = targets(&problem.cost);
287 let mut sessions: Vec<Session> = targets.iter().map(|_| Session::default()).collect();
288 let samples = asked(&call("samples")?)?;
289 let mut trace: Vec<Point> = Vec::new();
290 let mut resampled: Vec<Resampled> = Vec::new();
291 let stopped_by = loop {
292 let values = match search.next() {
293 Next::Done(stopped) => break stopped,
294 Next::Render(values) => values,
295 };
296 if abandon.abandoned().await {
297 search.cancel();
298 continue;
299 }
300 let digits = search.digits();
301 let written: Vec<(String, f64, String)> = problem
302 .vary
303 .iter()
304 .zip(values.iter().zip(digits))
305 .map(|(one, (v, d))| {
306 let text = one.text(*v, d);
307 (one.name.clone(), one.number(&text), text)
308 })
309 .collect();
310 let point = Rendering {
311 problem,
312 base: &base,
313 loads: &loads,
314 targets: &targets,
315 samples: &samples,
316 abandon,
317 };
318 let evaluated = point
319 .evaluated(&written, tier, &mut sessions, &mut resampled)
320 .await;
321 let (total, terms, computed) = match evaluated {
322 Err(CliError::Engine(EngineError::Abandoned)) => {
323 search.cancel();
324 continue;
325 }
326 other => other?,
327 };
328 let point = Point {
329 render: trace.len() + 1,
330 values: written,
331 total,
332 best: trace
333 .last()
334 .map_or(total, |last: &Point| last.best.min(total)),
335 terms,
336 computed,
337 };
338 heard(&point);
339 trace.push(point);
340 search.tell(total);
341 };
342 let best = trace
343 .iter()
344 .reduce(|held, p| if p.total < held.total { p } else { held })
345 .cloned()
346 .ok_or_else(|| {
347 CliError::usage(
348 "solve.no_render",
349 "the solve was stopped before its first render finished, so it holds no point",
350 )
351 })?;
352 Ok(Solved {
353 cost: problem.cost.text.clone(),
354 terms: problem.cost.terms.iter().map(|t| t.text.clone()).collect(),
355 start: problem
356 .vary
357 .iter()
358 .zip(&start)
359 .map(|(one, v)| one.text(*v, Precision::Shortest))
360 .collect(),
361 on_bound: best
362 .values
363 .iter()
364 .zip(&problem.vary)
365 .map(|((_, v, _), one)| *v <= one.lo || *v >= one.hi)
366 .collect(),
367 best,
368 renders: trace.len(),
369 stopped_by,
370 trace,
371 resampled,
372 })
373}
374
375struct Rendering<'p, 'a> {
376 problem: &'p Problem<'a>,
377 base: &'p dyn Source,
378 loads: &'p [Held],
379 targets: &'p [String],
380 samples: &'p Asked,
381 abandon: &'p dyn Abandon,
382}
383
384type Evaluated = (f64, Vec<f64>, Vec<(String, u64)>);
385
386impl Rendering<'_, '_> {
387 async fn evaluated<B: Backend>(
389 &self,
390 written: &[(String, f64, String)],
391 tier: &Tier<B>,
392 sessions: &mut [Session],
393 resampled: &mut Vec<Resampled>,
394 ) -> Result<Evaluated, CliError> {
395 let problem = self.problem;
396 let mut sets = problem.sets.clone();
397 sets.extend(written.iter().map(|(n, _, t)| (n.clone(), t.clone())));
398 let source = overlaid(self.base, &sets)?;
399 let mut readings: Vec<Option<Output>> = vec![None; problem.cost.leaves.len()];
400 let mut computed = Vec::new();
401 for (target, session) in self.targets.iter().zip(sessions.iter_mut()) {
402 let job = Job {
403 rate: problem.rate,
404 threads: problem.threads,
405 asked: std::slice::from_ref(self.samples),
406 abandon: self.abandon,
407 ..Job::over(&source, target)
408 };
409 let rendered = execute_over(job, tier, session).await?;
410 computed.push((target.clone(), rendered.render.work().computed_samples));
411 if let Some(label) = rendered.label() {
412 for one in &label.resampled {
413 if !resampled.contains(one) {
414 resampled.push(*one);
415 }
416 }
417 }
418 let Output::Samples(buffer) = rendered.answered(self.samples)?.value else {
419 unreachable!("`samples` answers samples");
420 };
421 let per_bar = rendered.graph.tempo().map(|tempo| tempo.seconds_per_bar());
422 let profile = rendered.config.profile.name;
423 for (at, leaf) in problem.cost.leaves.iter().enumerate() {
424 if leaf.target != *target {
425 continue;
426 }
427 let representation = leaf.asked.representation(rendered.config.rate, per_bar)?;
428 let answer =
429 sva_engine::answer_buffer(&leaf.written, &buffer, representation, profile)?;
430 readings[at] = Some(answer.value);
431 }
432 }
433 let readings: Vec<Output> = readings
434 .into_iter()
435 .map(|r| r.expect("every leaf's target rendered"))
436 .collect();
437 let mut terms = Vec::new();
438 for term in &problem.cost.terms {
439 terms.push(term.sign * scalar(&value(&term.node, &readings, self.loads)?));
440 }
441 Ok((terms.iter().sum(), terms, computed))
442 }
443}
444
445#[derive(Clone, Debug)]
446enum Value {
447 Scalar(f64),
448 Vector(Vec<f64>),
449}
450
451fn scalar(v: &Value) -> f64 {
452 match v {
453 Value::Scalar(x) => *x,
454 Value::Vector(_) => unreachable!("the cost typed every term to a number"),
455 }
456}
457
458fn lengths(a: usize, b: usize, what: &str) -> CliError {
459 CliError::usage(
460 "solve.length_mismatch",
461 format!(
462 "{what} meets a vector of {a} values with one of {b}; give a weight vector one \
463 weight per value of the residual it weighs"
464 ),
465 )
466}
467
468fn side<'r>(node: &Node, readings: &'r [Output], loads: &'r [Held]) -> Option<&'r Output> {
470 match node {
471 Node::Leaf(at) => Some(&readings[*at]),
472 Node::Loaded(at) => match &loads[*at] {
473 Held::Reading(reading) => Some(reading),
474 Held::Number(_) | Held::Vector(_) => None,
475 },
476 _ => None,
477 }
478}
479
480fn value(node: &Node, readings: &[Output], loads: &[Held]) -> Result<Value, CliError> {
481 let value = |node: &Node| value(node, readings, loads);
482 Ok(match node {
483 Node::Num(n) => Value::Scalar(*n),
484 Node::Vector(v) => Value::Vector(v.clone()),
485 Node::Loaded(at) => match &loads[*at] {
486 Held::Number(n) => Value::Scalar(*n),
487 Held::Vector(v) => Value::Vector(v.clone()),
488 Held::Reading(_) => {
489 unreachable!("the cost typed every held reading into a subtraction")
490 }
491 },
492 Node::Leaf(_) => unreachable!("the cost typed every leaf into a subtraction"),
493 Node::Neg(x) => match value(x)? {
494 Value::Scalar(s) => Value::Scalar(-s),
495 Value::Vector(v) => Value::Vector(v.iter().map(|x| -x).collect()),
496 },
497 Node::Bin(Op::Sub, a, b)
498 if side(a, readings, loads).is_some() && side(b, readings, loads).is_some() =>
499 {
500 let (Some(x), Some(y)) = (side(a, readings, loads), side(b, readings, loads)) else {
501 unreachable!("matched above");
502 };
503 let vector =
504 residual::residual(x, y).expect("the cost typed both sides to one reading");
505 Value::Vector(vector)
506 }
507 Node::Bin(op, a, b) => {
508 let (a, b) = (value(a)?, value(b)?);
509 let apply = |x: f64, y: f64| match op {
510 Op::Add => x + y,
511 Op::Sub => x - y,
512 Op::Mul => x * y,
513 Op::Div => x / y,
514 };
515 match (a, b) {
516 (Value::Scalar(x), Value::Scalar(y)) => Value::Scalar(apply(x, y)),
517 (Value::Vector(v), Value::Scalar(y)) => {
518 Value::Vector(v.iter().map(|x| apply(*x, y)).collect())
519 }
520 (Value::Scalar(x), Value::Vector(v)) => {
521 Value::Vector(v.iter().map(|y| apply(x, *y)).collect())
522 }
523 (Value::Vector(v), Value::Vector(w)) if v.len() == w.len() => {
524 Value::Vector(v.iter().zip(&w).map(|(x, y)| apply(*x, *y)).collect())
525 }
526 (Value::Vector(v), Value::Vector(w)) => {
527 return Err(lengths(v.len(), w.len(), "a sum of two vectors"));
528 }
529 }
530 }
531 Node::Reduce(name, args) => {
532 let args = args.iter().map(value).collect::<Result<Vec<_>, _>>()?;
533 match (*name, args.as_slice()) {
534 ("abs", [Value::Scalar(x)]) => Value::Scalar(x.abs()),
535 ("abs", [Value::Vector(v)]) => Value::Vector(v.iter().map(|x| x.abs()).collect()),
536 ("norm", [Value::Vector(v)]) => {
537 Value::Scalar(v.iter().map(|x| x * x).sum::<f64>().sqrt())
538 }
539 ("sum", [Value::Vector(v)]) => Value::Scalar(v.iter().sum()),
540 ("max", [Value::Vector(v)]) => {
541 Value::Scalar(v.iter().copied().reduce(f64::max).unwrap_or(0.0))
542 }
543 ("dot", [Value::Vector(w), Value::Vector(v)]) if w.len() == v.len() => {
544 Value::Scalar(w.iter().zip(v).map(|(a, b)| a * b).sum())
545 }
546 ("dot", [Value::Vector(w), Value::Vector(v)]) => {
547 return Err(lengths(w.len(), v.len(), "`dot`"));
548 }
549 _ => unreachable!("the cost typed every reducer's arguments"),
550 }
551 }
552 })
553}
554
555pub fn point_json(point: &Point) -> String {
557 let values: Vec<String> = point
558 .values
559 .iter()
560 .map(|(name, _, text)| format!("\"{}\": \"{}\"", escape(name), escape(text)))
561 .collect();
562 format!(
563 "{{\"render\": {}, \"values\": {{{}}}, \"cost\": {}, \"best\": {}}}",
564 point.render,
565 values.join(", "),
566 num(point.total),
567 num(point.best)
568 )
569}
570
571pub fn solved_data(solved: &Solved, vary: &[Vary], extra: &str) -> String {
573 let point_values = |p: &Point| {
574 let fields: Vec<String> = p
575 .values
576 .iter()
577 .map(|(n, v, t)| {
578 format!(
579 "\"{}\": {{ \"value\": {}, \"text\": \"{}\" }}",
580 escape(n),
581 num(*v),
582 escape(t)
583 )
584 })
585 .collect();
586 format!("{{ {} }}", fields.join(", "))
587 };
588 let values: Vec<String> = solved
589 .best
590 .values
591 .iter()
592 .zip(vary)
593 .zip(solved.start.iter().zip(&solved.on_bound))
594 .map(|(((n, v, t), one), (start, bound))| {
595 format!(
596 "\"{}\": {{ \"value\": {}, \"text\": \"{}\", \"unit\": \"{}\", \"start\": \"{}\", \
597 \"on_bound\": {bound} }}",
598 escape(n),
599 num(*v),
600 escape(t),
601 one.unit,
602 escape(start)
603 )
604 })
605 .collect();
606 let terms: Vec<(String, f64)> = solved
607 .terms
608 .iter()
609 .cloned()
610 .zip(solved.best.terms.iter().copied())
611 .collect();
612 let terms = list(&terms, |(text, v)| {
613 format!(
614 "{{ \"term\": \"{}\", \"value\": {} }}",
615 escape(text),
616 num(*v)
617 )
618 });
619 let trace = list(&solved.trace, |p| {
620 let computed: Vec<String> = p
621 .computed
622 .iter()
623 .map(|(t, n)| format!("\"{}\": {n}", escape(t)))
624 .collect();
625 format!(
626 "{{ \"render\": {}, \"values\": {}, \"cost\": {}, \"computed_samples\": {{ {} }} }}",
627 p.render,
628 point_values(p),
629 num(p.total),
630 computed.join(", ")
631 )
632 });
633 let resampled = list(&solved.resampled, crate::answer::resampled_json);
634 format!(
635 "{{\"cost_expression\": \"{}\", \"values\": {{ {} }}, \"cost\": {{ \"total\": {}, \
636 \"terms\": {terms} }}, \"renders\": {}, \"stopped_by\": \"{}\", \"resampled\": \
637 {resampled}{extra}, \"trace\": {trace}}}",
638 escape(&solved.cost),
639 values.join(", "),
640 num(solved.best.total),
641 solved.renders,
642 solved.stopped_by.name()
643 )
644}
645
646pub fn findings(solved: &Solved, vary: &[Vary]) -> Vec<Diagnostic> {
648 let mut out = Vec::new();
649 for ((name, _, text), (bound, one)) in solved
650 .best
651 .values
652 .iter()
653 .zip(solved.on_bound.iter().zip(vary))
654 {
655 if *bound {
656 out.push(
657 Diagnostic::new(
658 "solve.on_bound",
659 format!(
660 "`{name}` ended on a bound of `{}..{}`, at `{text}`: the fit wants past it",
661 one.text(one.lo, Precision::Shortest),
662 one.text(one.hi, Precision::Shortest)
663 ),
664 )
665 .with_severity(Severity::Warning)
666 .helped("widen the bound, or the model lacks what the reference holds"),
667 );
668 }
669 }
670 if solved.stopped_by == Stopped::Budget {
671 out.push(
672 Diagnostic::new(
673 "solve.budget",
674 format!(
675 "the solve spent `--max-renders` ({}) before its steps or cost converged",
676 solved.renders
677 ),
678 )
679 .with_severity(Severity::Warning)
680 .helped("raise `--max-renders`, narrow the bounds, or vary fewer variables at once"),
681 );
682 }
683 for r in &solved.resampled {
684 out.push(
685 Diagnostic::new(
686 "solve.resampled",
687 format!(
688 "a recording at {} Hz was read at {} Hz through a windowed sinc: within \
689 {:.1e} dB up to {} Hz, at least {:.1} dB down from {} Hz",
690 r.from_hz, r.to_hz, r.ripple_db, r.passband_hz, r.stopband_db, r.stopband_hz
691 ),
692 )
693 .with_severity(Severity::Warning)
694 .helped("pass `--rate` at the recording's own rate to read its samples unchanged"),
695 );
696 }
697 out
698}