Skip to main content

sva_engine/render/
answer.rs

1// Concern: takes one reading off the representation a node declares, a ledger edge by edge | Non-concern: naming the observations (query.rs), the arithmetic of one | IO: (&Render, node) -> Answer
2
3use sva_formula::spectral_sum::atom::{Singular, SpectralAtom};
4use sva_formula::{Line, SpectralSum, Var, d_dt, envelope_read, line_atoms_read};
5use sva_samples::{
6    AliasScore, Buffer, Consumes, Extent, Peak, PitchFrame, Profile, Source, measure::bands,
7    measure::crest, measure::envelope, measure::formants, measure::loudness, measure::onsets,
8    measure::pitch, measure::spectrum, measure::stereo, measure_alias,
9};
10
11use crate::error::{Diagnostic, EngineError, Located};
12use crate::query::{Answer, DEFAULT_FRAME_SECS, Output, Representation};
13use crate::refs;
14use crate::render::Render;
15use crate::typing::Value;
16
17/// A closed form answers off its spectral sum and a buffer off its samples; its own `Ty`
18/// decides which, never a flag on the observation.
19pub fn answer(
20    render: &Render,
21    node: sva_formula::NodeId,
22    representation: Representation,
23) -> Result<Answer, EngineError> {
24    let closed = render.tys.ty(node).is_closed_form();
25    let profile = render.config.profile.name;
26    if representation == Representation::Flops {
27        return Ok(Answer::whole(
28            Output::Flops(Box::new(crate::flops::tree_at(render, node))),
29            Source::Exact,
30            profile,
31            None,
32        ));
33    }
34    if representation == Representation::Arguments {
35        return Ok(Answer::whole(
36            Output::Arguments(arguments_under(render, node)),
37            Source::Exact,
38            profile,
39            None,
40        ));
41    }
42    if representation == Representation::Bindings {
43        return Ok(Answer::whole(
44            Output::Bindings(render.bindings.get(&node).cloned().unwrap_or_default()),
45            Source::Exact,
46            profile,
47            None,
48        ));
49    }
50    match representation.consumes(closed) {
51        Consumes::ClosedForm => {
52            let found;
53            let sum = match render.symbolic.get(&node) {
54                Some(held) => held,
55                None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
56                    Ok(held) => {
57                        found = held;
58                        &found
59                    }
60                    Err(left) => return measured_instead(render, node, representation, left),
61                },
62            };
63            let (value, listed, source) = match exact(render, node, representation, sum) {
64                Err(left) => return measured_instead(render, node, representation, left),
65                Ok(held) => held,
66            };
67            Ok(Answer {
68                value,
69                source,
70                profile,
71                rate: None,
72                tail_db: listed.tail_db(),
73                dropped: listed.dropped,
74            })
75        }
76        _ => {
77            if render.buffer(node).is_none() {
78                return Err(unmaterialized(render, node, representation));
79            }
80            let buffer = render.output(node)?;
81            Ok(Answer::whole(
82                measured(render, node, representation, &buffer)?,
83                Source::Measured,
84                profile,
85                Some(render.config.rate),
86            ))
87        }
88    }
89}
90
91/// A closed form no atom sum reaches answers no reading symbolically. An envelope is the one
92/// FORMAT 9.3 already names off `sample(...)`, so it is measured here rather than refused.
93fn measured_instead(
94    render: &Render,
95    node: sva_formula::NodeId,
96    representation: Representation,
97    left: EngineError,
98) -> Result<Answer, EngineError> {
99    match representation {
100        Representation::Envelope { frame_secs } if left.code() == "cast.left_algebra" => {
101            off_the_grid(render, node, frame_secs)
102        }
103        _ => Err(left),
104    }
105}
106
107/// The samples this node reaches: the buffer the render holds, or the collapse it would have run,
108/// down to FORMAT 9.1's row 4 over the written closed form.
109pub(super) fn on_the_grid(
110    render: &Render,
111    node: sva_formula::NodeId,
112) -> Result<Buffer, EngineError> {
113    if render.buffer(node).is_some() {
114        return render.output(node);
115    }
116    let extent = render.range.ok_or_else(|| {
117        render.unranged.clone().unwrap_or_else(|| {
118            refused(
119                render,
120                node,
121                "engine.not_materialized",
122                "a measured envelope reads samples, and this render read none".to_string(),
123            )
124        })
125    })?;
126    collapsed_over(render, node, extent)
127}
128
129fn collapsed_over(
130    render: &Render,
131    node: sva_formula::NodeId,
132    extent: sva_samples::Extent,
133) -> Result<Buffer, EngineError> {
134    super::sampled(render, node, extent)
135}
136
137fn off_the_grid(
138    render: &Render,
139    node: sva_formula::NodeId,
140    frame_secs: Option<f64>,
141) -> Result<Answer, EngineError> {
142    let rate = render.config.rate;
143    let buffer = on_the_grid(render, node)?;
144    Ok(Answer::whole(
145        Output::Envelope(envelope::trace(
146            &buffer.slices(),
147            f64::from(rate),
148            buffer.origin_secs(),
149            frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
150        )),
151        Source::Measured,
152        render.config.profile.name,
153        Some(rate),
154    ))
155}
156
157fn exact(
158    render: &Render,
159    node: sva_formula::NodeId,
160    representation: Representation,
161    sum: &SpectralSum,
162) -> Result<(Output, Listed, Source), EngineError> {
163    let source = Source::Exact;
164    let enumerated = || listed(render, node, sum, &render.config.profile);
165    let (value, listed) = match representation {
166        Representation::Lines => {
167            let listed = enumerated()?;
168            let held = lines(render, node, sum.var, &listed.atoms)?;
169            (Output::Lines(held), listed)
170        }
171        Representation::Spectrum {
172            frame_secs: Some(_),
173            ..
174        } => {
175            return Err(EngineError::refused(Diagnostic {
176                code: "engine.observation_needs_samples".to_string(),
177                message: "a closed form's spectrum is its lines, and it has no frames to \
178                          take a spectrum across"
179                    .to_string(),
180                location: Located::at(render.tys.name(node), None),
181                help: "drop `frame` for the lines, or read `sample(...)` of it to measure \
182                       frames"
183                    .to_string(),
184            }));
185        }
186        // FORMAT 14.1: a pair's spectrum is its whole line list; no estimate, no peaks.
187        Representation::Spectrum {
188            frame_secs: None, ..
189        } => {
190            let listed = enumerated()?;
191            let mut held = lines(render, node, sum.var, &listed.atoms)?;
192            held.sort_by(|a, b| a.hz.total_cmp(&b.hz));
193            (Output::Lines(held), listed)
194        }
195        Representation::Atoms => {
196            let listed = enumerated()?;
197            let found = listed.atoms.iter().map(sketch_atom).collect();
198            (Output::Atoms(found), listed)
199        }
200        // A pair states its partials; `max_notes` is the question, not a cut answer.
201        Representation::Pitch { max_notes, .. } => {
202            let listed = enumerated()?;
203            let held = lines(render, node, sum.var, &listed.atoms)?;
204            let mut found: Vec<Peak> = held
205                .iter()
206                .filter(|l| l.hz > 0.0)
207                .map(|l| Peak {
208                    hz: l.hz,
209                    db: 20.0 * amplitude_at(&held, l.hz).log10(),
210                })
211                .collect();
212            found.sort_by(|a, b| b.db.total_cmp(&a.db));
213            let frame = PitchFrame {
214                t_secs: render
215                    .range
216                    .map_or(0.0, |range| range.start_secs(render.config.rate)),
217                notes: pitch::name_peaks(&found, max_notes),
218            };
219            (Output::Pitch(vec![frame]), listed)
220        }
221        Representation::Derivative => (Output::Symbolic(Box::new(d_dt(sum))), Listed::NONE),
222        Representation::Envelope { .. } => {
223            let held =
224                refs::read_through(&render.tys, |t| envelope_read(sum, t)).map_err(|left| {
225                    EngineError::of_closed_form(
226                        &left.refusal(),
227                        render.tys.locate(left.origin),
228                        "read the envelope off sample(...) for a measured one",
229                    )
230                })?;
231            (Output::Symbolic(Box::new(held.squared)), Listed::NONE)
232        }
233        other => return Err(not_a_closed_form(render, node, other)),
234    };
235    Ok((value, listed, source))
236}
237
238/// Every atom the spectral sum stands for, beside the terms its series truncated away or left
239/// above the ceiling: `atoms` alone holds only the terms already written out.
240struct Listed {
241    atoms: Vec<SpectralAtom>,
242    dropped: Vec<Line>,
243}
244
245impl Listed {
246    /// A reading that enumerates no series leaves nothing out.
247    const NONE: Listed = Listed {
248        atoms: Vec::new(),
249        dropped: Vec::new(),
250    };
251
252    /// FORMAT 9.3: the tail is the loudest line left out against the loudest one kept, so
253    /// it is one ratio over the whole answer, never a per-series one carried upward.
254    fn tail_db(&self) -> Option<f64> {
255        let loudest = |set: &mut dyn Iterator<Item = f64>| set.fold(0.0f64, f64::max);
256        let gone = loudest(&mut self.dropped.iter().map(|l| l.amp.abs()));
257        let kept = loudest(&mut self.atoms.iter().map(|a| a.c.abs()));
258        (gone > 0.0 && kept > 0.0).then(|| 20.0 * (gone / kept).log10())
259    }
260}
261
262/// Each lane's series enumerated under the band a rate-free reading is taken in. A series
263/// whose term no line closed form reads yields nothing, and nothing is not an exact empty answer.
264fn listed(
265    render: &Render,
266    node: sva_formula::NodeId,
267    sum: &SpectralSum,
268    profile: &Profile,
269) -> Result<Listed, EngineError> {
270    let ceiling = profile.ceiling_hz;
271    let band = (ceiling, profile.floor(ceiling), profile.half_lsb());
272    let mut held = Listed::NONE;
273    for lane in &sum.lanes {
274        held.atoms.extend(lane.clone().expanded().atoms);
275        for series in &lane.series {
276            let found = refs::read_through(&render.tys, |t| line_atoms_read(series, band, t));
277            let Some(found) = found else {
278                return Err(unenumerable(render, node));
279            };
280            held.atoms.extend(found.atoms);
281            held.dropped.extend(found.dropped);
282        }
283    }
284    Ok(held)
285}
286
287/// The exact line list of a pair: in `t` a bare turning exponential, in `f` the delta it
288/// duals to. A list every other atom was dropped from answers a spectrum this node has not.
289fn lines(
290    render: &Render,
291    node: sva_formula::NodeId,
292    var: Var,
293    atoms: &[SpectralAtom],
294) -> Result<Vec<Line>, EngineError> {
295    atoms
296        .iter()
297        .map(|a| line_of(var, a).ok_or_else(|| not_a_line(render, node, var, a)))
298        .collect()
299}
300
301fn line_of(var: Var, a: &SpectralAtom) -> Option<Line> {
302    match (var, a.sing) {
303        (Var::F, Singular::Delta { at, order: 0 }) => Some(Line::bare(at, a.c)),
304        (Var::T, Singular::Regular) => {
305            if !a.poly.is_one() || a.gauss.is_some() || a.ind.is_some() || a.pole.is_some() {
306                return None;
307            }
308            match a.exp {
309                // A constant turns at no rate, which is the line at zero hertz.
310                None => Some(Line::bare(0.0, a.c)),
311                Some(e) if e.sigma == 0.0 => Some(Line::bare(e.omega / std::f64::consts::TAU, a.c)),
312                Some(_) => None,
313            }
314        }
315        _ => None,
316    }
317}
318
319/// What gave this atom a width; a turning exponential is the line itself and never among it.
320fn widening(a: &SpectralAtom) -> Vec<&'static str> {
321    let mut held = Vec::new();
322    if !a.poly.is_one() {
323        held.push("a polynomial");
324    }
325    if a.exp.is_some_and(|e| e.sigma != 0.0) {
326        held.push("a decaying exponential");
327    }
328    if a.gauss.is_some() {
329        held.push("a Gaussian");
330    }
331    if a.pole.is_some() {
332        held.push("a pole");
333    }
334    if held.is_empty() {
335        held.push("a delta");
336    }
337    held
338}
339
340/// Convolved into a line, each shape beside the turning exponential answers a band.
341fn not_a_line(
342    render: &Render,
343    node: sva_formula::NodeId,
344    var: Var,
345    atom: &SpectralAtom,
346) -> EngineError {
347    let mut at = render.tys.locate(atom.origin);
348    if at.node.is_empty() {
349        at = Located::at(render.tys.name(node), None);
350    }
351    let message = match atom.ind {
352        _ if var == Var::F => "this term spreads over `f` rather than standing at one \
353                               frequency, and a line in `f` is a delta"
354            .to_string(),
355        Some(window) => format!(
356            "this term is windowed to [{}s, {}s), and a windowed line is that line convolved \
357             with the window's transform, which has a width and is no line",
358            window.l.value(),
359            window.r.value()
360        ),
361        None => format!(
362            "this term carries {}, and a line is a bare turning exponential: each of those \
363             convolves the line with a shape of its own width",
364            widening(atom).join(", ")
365        ),
366    };
367    EngineError::refused(Diagnostic {
368        code: "read.lines_need_unwindowed_lines".to_string(),
369        message,
370        location: at,
371        help: "`atoms` states each term as it stands, `lines` of the node under the window \
372               or envelope lists the lines it multiplies, and `pitch` or `spectrum` of \
373               `sample(...)` of it measures it frame by frame"
374            .to_string(),
375    })
376}
377
378/// The amplitude a partial sounds at: a real wave carries it in a conjugate pair, half in
379/// each, and a measured peak reads the pair's sum.
380fn amplitude_at(held: &[Line], hz: f64) -> f64 {
381    held.iter()
382        .filter(|l| (l.hz - hz).abs() <= f64::EPSILON * hz.abs() || l.hz == -hz)
383        .map(|l| l.amp.abs())
384        .sum()
385}
386
387/// One atom as the six factors it is present in, which is what `atoms` answers with.
388pub fn sketch_atom(a: &SpectralAtom) -> String {
389    let factors: Vec<&'static str> = a.present().iter().map(|f| f.as_str()).collect();
390    format!("{} x {}", a.c.abs(), factors.join(" times "))
391}
392
393fn measured(
394    render: &Render,
395    node: sva_formula::NodeId,
396    representation: Representation,
397    buffer: &Buffer,
398) -> Result<Output, EngineError> {
399    Ok(match representation {
400        Representation::Alias { oversample } => {
401            let reference = oversampled(render, behind(render, node)?, oversample)?;
402            Output::Alias(Box::new(worst_alias(buffer, &reference, oversample)))
403        }
404        Representation::Ledger { depth } => Output::Ledger(attributed(render, node, depth)?),
405        other => {
406            return off_buffer(buffer, other).map_err(|fault| match fault {
407                NoReading::NeedsAGraph => not_a_reading(render, node, other),
408                NoReading::TooNarrow { need, held } => too_narrow(render, node, need, held),
409            });
410        }
411    })
412}
413
414/// Why a buffer answered nothing: the reading needs the tree the node was built from, or
415/// the buffer is narrower than the reading needs.
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417pub enum NoReading {
418    NeedsAGraph,
419    TooNarrow { need: usize, held: usize },
420}
421
422/// Every reading a buffer answers on its own, with no graph behind it.
423pub fn off_buffer(buffer: &Buffer, representation: Representation) -> Result<Output, NoReading> {
424    let sr = f64::from(buffer.rate);
425    let start = buffer.origin_secs();
426    let plane = buffer.plane(0);
427    Ok(match representation {
428        Representation::Samples => Output::Samples(Box::new(buffer.clone())),
429        Representation::Spectrum {
430            max_peaks,
431            frame_secs,
432        } => Output::Spectrum(Box::new(spectrum::analyze(
433            plane, sr, max_peaks, frame_secs,
434        ))),
435        Representation::Envelope { frame_secs } => Output::Envelope(envelope::trace(
436            &buffer.slices(),
437            sr,
438            start,
439            frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
440        )),
441        Representation::Derivative => Output::Samples(Box::new(difference(buffer))),
442        Representation::Pitch {
443            max_notes,
444            frame_secs,
445        } => Output::Pitch(pitch::track(plane, sr, start, frame_secs, max_notes)),
446        Representation::Formants {
447            max_formants,
448            frame_secs,
449        } => Output::Formants(formants::track(
450            plane,
451            sr,
452            start,
453            frame_secs,
454            formants::default_order(sr),
455            max_formants,
456        )),
457        Representation::Stereo { frame_secs } => {
458            if buffer.width < 2 {
459                return Err(NoReading::TooNarrow {
460                    need: 2,
461                    held: buffer.width,
462                });
463            }
464            let planes: Vec<&[f64]> = (0..buffer.width).map(|c| buffer.plane(c)).collect();
465            Output::Stereo(Box::new(stereo::analyze(
466                &planes,
467                buffer.width,
468                sr,
469                start,
470                frame_secs,
471            )))
472        }
473        Representation::Bands => Output::Bands(Box::new(bands::analyze(plane, sr, start))),
474        Representation::Crest => Output::Crest(Box::new(crest::analyze(plane, sr))),
475        Representation::Onsets => Output::Onsets(Box::new(onsets::detect(plane, sr, start))),
476        Representation::Loudness => {
477            let planes: Vec<&[f64]> = (0..buffer.width).map(|c| buffer.plane(c)).collect();
478            Output::Loudness(Box::new(loudness::analyze(&planes, sr, start)))
479        }
480        _ => return Err(NoReading::NeedsAGraph),
481    })
482}
483
484/// Every held buffer under the target over the root's range, its own refs beside it, so the
485/// reading can share the target's energy down the tree it was built from. A ref the closed form
486/// adds stands there as what it contributed to the node reading it; one no addend isolates is
487/// left unattributed.
488fn attributed(
489    render: &Render,
490    node: sva_formula::NodeId,
491    depth: usize,
492) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
493    let holds = |id: sva_formula::NodeId| render.buffers.contains_key(&id);
494    let edges = edges_under(render, node, depth, &holds)?;
495    let under: std::collections::BTreeSet<sva_formula::NodeId> = std::iter::once(node)
496        .chain(edges.iter().map(|(_, child)| *child))
497        .collect();
498    let mut deps = std::collections::BTreeMap::new();
499    let mut kinds = std::collections::BTreeMap::new();
500    for id in render.buffers.keys().filter(|id| under.contains(id)) {
501        let name = render.tys.name(*id).to_string();
502        let read = refs_read(render, *id, &holds)?
503            .into_iter()
504            .map(|op| render.tys.name(op).to_string())
505            .collect();
506        deps.insert(name.clone(), read);
507        kinds.insert(name, sva_samples::SignalKind::Audio);
508    }
509    let mut contributed_by = std::collections::BTreeMap::new();
510    for (parent, child) in &edges {
511        if !crate::render::slots::reads_held(render, *parent)? {
512            return Err(unmaterialized(
513                render,
514                node,
515                Representation::Ledger { depth },
516            ));
517        }
518        if let Some(held) = contributed(render, *parent, *child)? {
519            contributed_by.insert(render.tys.name(*child).to_string(), held);
520        }
521    }
522    // A row's own samples are read where it reads rows itself or no addend isolates it.
523    let readers: std::collections::BTreeSet<_> = edges.iter().map(|(parent, _)| *parent).collect();
524    let mut buffers = std::collections::BTreeMap::new();
525    for id in render.buffers.keys().filter(|id| under.contains(id)) {
526        let name = render.tys.name(*id);
527        if *id == node || readers.contains(id) || !contributed_by.contains_key(name) {
528            buffers.insert(name.to_string(), own_samples(render, *id)?);
529        }
530    }
531    let len = render.range.map_or(0, |range| range.len());
532    Ok(sva_samples::measure::ledger::attribute(
533        &buffers,
534        &contributed_by,
535        &deps,
536        &kinds,
537        render.tys.name(node),
538        0..len,
539        depth,
540    ))
541}
542
543/// A row's own samples over the range, held or pulled.
544fn own_samples(render: &Render, id: sva_formula::NodeId) -> Result<Buffer, EngineError> {
545    let range = render.range.expect("a ledger reads a decided range");
546    match render.buffer(id) {
547        Some(_) => render.output(id),
548        None => collapsed_over(render, id, range),
549    }
550}
551
552/// What every instance under `node` was lowered with, `node` first, then breadth first. The
553/// walk is over what each node was lowered to, so it needs no buffer.
554fn arguments_under(render: &Render, node: sva_formula::NodeId) -> Vec<crate::Arguments> {
555    let under = |id: sva_formula::NodeId| -> Vec<sva_formula::NodeId> {
556        match render.tys.value(id) {
557            Value::ClosedForm(form) => refs::nodes_in(&form.body),
558            Value::Read { source, .. } | Value::Cast(_, source) => vec![*source],
559            Value::Op { args, .. } => args.clone(),
560            Value::Filter {
561                x, cutoff, q, gain, ..
562            } => vec![*x, *cutoff, *q, *gain],
563            Value::Solver { varying, .. } => varying.iter().map(|(_, a)| *a).collect(),
564            Value::SelfAt { .. } | Value::Noise(_) | Value::Stored(_) => Vec::new(),
565        }
566    };
567    let mut names: Vec<&str> = Vec::new();
568    let mut seen = std::collections::BTreeSet::from([node]);
569    let mut level = vec![node];
570    while !level.is_empty() {
571        let mut next = Vec::new();
572        for at in level {
573            let name = render.tys.name(at);
574            if !names.contains(&name) {
575                names.push(name);
576            }
577            next.extend(under(at).into_iter().filter(|c| seen.insert(*c)));
578        }
579        level = next;
580    }
581    names
582        .into_iter()
583        .filter_map(|name| render.tys.arguments(name).cloned())
584        .collect()
585}
586
587/// The tree a ledger walks; a node several read is attributed to the first to reach it.
588fn edges_under(
589    render: &Render,
590    node: sva_formula::NodeId,
591    depth: usize,
592    holds: &dyn Fn(sva_formula::NodeId) -> bool,
593) -> Result<Vec<(sva_formula::NodeId, sva_formula::NodeId)>, EngineError> {
594    let mut seen = std::collections::BTreeSet::from([node]);
595    let (mut level, mut out) = (vec![node], Vec::new());
596    for _ in 0..depth {
597        let mut next = Vec::new();
598        for parent in level {
599            for child in refs_read(render, parent, holds)? {
600                if seen.insert(child) {
601                    out.push((parent, child));
602                    next.push(child);
603                }
604            }
605        }
606        level = next;
607    }
608    Ok(out)
609}
610
611/// Every ref one node's program reads, one row per name.
612fn refs_read(
613    render: &Render,
614    node: sva_formula::NodeId,
615    holds: &dyn Fn(sva_formula::NodeId) -> bool,
616) -> Result<Vec<sva_formula::NodeId>, EngineError> {
617    crate::render::slots::refs_read(render, node, holds)
618}
619
620/// What one ref contributed to the node reading it, at that node's own offset and window: its
621/// program with every other slot silenced. Two under one product have no addend apiece and no
622/// share either, so that edge is left unattributed.
623fn contributed(
624    render: &Render,
625    parent: sva_formula::NodeId,
626    child: sva_formula::NodeId,
627) -> Result<Option<Buffer>, EngineError> {
628    let range = render.range.expect("a ledger reads a decided range");
629    Ok(crate::render::slots::contributed(render, parent, child)?
630        .map(|held| held.over(range, held.extent())))
631}
632
633/// The closed form an alias score oversamples: the node itself where it is one, and the operand of
634/// the `sample(...)` that collapsed it where it is not.
635fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
636    if render.tys.ty(node).is_closed_form() {
637        return Ok(node);
638    }
639    match render.tys.value(node) {
640        crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
641        _ => Err(refused(
642            render,
643            node,
644            "engine.alias_needs_a_closed_form",
645            "an alias score is a render against the same closed form oversampled, and this node \
646             is samples with no closed form behind it"
647                .to_string(),
648        )),
649    }
650}
651
652/// The score a point sampling's label carries where a reading asks for one: the node against
653/// its own closed form oversampled.
654pub(super) fn alias_db(
655    render: &Render,
656    node: sva_formula::NodeId,
657    buffer: &Buffer,
658) -> Result<f64, EngineError> {
659    let oversample = sva_samples::ALIAS_OVERSAMPLE as u32;
660    let reference = oversampled(render, behind(render, node)?, oversample)?;
661    Ok(worst_alias(buffer, &reference, oversample).asr_db)
662}
663
664/// Each component scored against its own reference, and `worst` says which one answers.
665fn worst_alias(buffer: &Buffer, reference: &Buffer, oversample: u32) -> sva_samples::Alias {
666    debug_assert_eq!(
667        buffer.width, reference.width,
668        "the reference is the same form at another rate"
669    );
670    let sr = f64::from(buffer.rate);
671    let width = buffer.width.min(reference.width);
672    sva_samples::worst_alias((0..width).map(|c| {
673        measure_alias(
674            buffer.plane(c),
675            reference.plane(c),
676            oversample as usize,
677            sr,
678            buffer.origin_secs(),
679        )
680    }))
681    .expect("a buffer holds at least one component")
682}
683
684/// The same closed form read at a multiple of the rate, which is what an alias score is against.
685fn oversampled(
686    render: &Render,
687    node: sva_formula::NodeId,
688    oversample: u32,
689) -> Result<Buffer, EngineError> {
690    let rate = render.config.rate * oversample;
691    let range = render.range.expect("an alias score reads a decided range");
692    let k = i64::from(oversample);
693    let extent = Extent::new(range.start * k, range.end * k);
694    let taken = match refs::spectral_sum_of(&render.tys, node, Var::T) {
695        Ok(sum) => refs::read_through(&render.tys, |t| {
696            sva_samples::of_spectral_sum_read(
697                &sum,
698                (rate, extent),
699                &render.config.profile,
700                AliasScore::NotAsked,
701                t,
702            )
703        }),
704        Err(_) => return super::finer(render, node, oversample, extent),
705    };
706    taken.map(|(buffer, _)| buffer).map_err(|e| {
707        EngineError::refused(Diagnostic {
708            code: e.code().to_string(),
709            message: e.to_string(),
710            location: Located::at(render.tys.name(node), None),
711            help: "an alias score needs a closed form to oversample".to_string(),
712        })
713    })
714}
715
716/// A first difference on the grid, which is what a derivative is once the closed form is gone.
717fn difference(buffer: &Buffer) -> Buffer {
718    let step = f64::from(buffer.rate);
719    let planes = (0..buffer.width)
720        .map(|c| {
721            let plane = buffer.plane(c);
722            plane
723                .iter()
724                .enumerate()
725                .map(|(i, x)| match i {
726                    0 => 0.0,
727                    _ => (x - plane[i - 1]) * step,
728                })
729                .collect()
730        })
731        .collect();
732    let mut out = Buffer::of_planes(buffer.rate, planes);
733    out.start = buffer.start;
734    out
735}
736
737fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
738    EngineError::refused(Diagnostic {
739        code: code.to_string(),
740        message,
741        location: Located::at(render.tys.name(node), None),
742        help: "ask for a reading this representation answers".to_string(),
743    })
744}
745
746fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
747    refused(
748        render,
749        node,
750        "read.series_not_enumerable",
751        "a series whose term is no line lists no lines, and an empty list would read as \
752         a node with none"
753            .to_string(),
754    )
755}
756
757fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
758    refused(
759        render,
760        node,
761        "engine.observation_needs_samples",
762        format!(
763            "`{}` reads samples, and this node is a closed form",
764            r.name()
765        ),
766    )
767}
768
769fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
770    refused(
771        render,
772        node,
773        "engine.observation_not_wired",
774        format!("`{}` takes no reading off a buffer here", r.name()),
775    )
776}
777
778fn too_narrow(render: &Render, node: sva_formula::NodeId, need: usize, held: usize) -> EngineError {
779    refused(
780        render,
781        node,
782        "type.width_mismatch",
783        format!("this reading needs {need} components and the node holds {held}"),
784    )
785}
786
787pub(super) fn unheld(render: &Render, node: sva_formula::NodeId) -> EngineError {
788    refused(
789        render,
790        node,
791        "engine.not_materialized",
792        "this render held no samples of it".to_string(),
793    )
794}
795
796fn unmaterialized(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
797    refused(
798        render,
799        node,
800        "engine.not_materialized",
801        format!("`{}` reads samples this render never held", r.name()),
802    )
803}
804
805/// One reading off a buffer nothing rendered — an external file, say — under the profile the
806/// caller names. Every representation needing the graph behind it refuses here.
807pub fn answer_buffer(
808    name: &str,
809    buffer: &Buffer,
810    representation: Representation,
811    profile: &'static str,
812) -> Result<Answer, EngineError> {
813    let refused = |code: &str, message: String| {
814        EngineError::refused(Diagnostic {
815            code: code.to_string(),
816            message,
817            location: Located::at(name, None),
818            help: "ask for a reading a buffer answers on its own".to_string(),
819        })
820    };
821    let value = off_buffer(buffer, representation).map_err(|fault| match fault {
822        NoReading::NeedsAGraph => refused(
823            "engine.observation_needs_a_graph",
824            format!(
825                "`{}` reads the tree a node was built from, and a file has none",
826                representation.name()
827            ),
828        ),
829        NoReading::TooNarrow { need, held } => refused(
830            "type.width_mismatch",
831            format!("this reading needs {need} components and the file holds {held}"),
832        ),
833    })?;
834    Ok(Answer::whole(
835        value,
836        Source::Measured,
837        profile,
838        Some(buffer.rate),
839    ))
840}