1use sva_formula::spectral_sum::atom::{Singular, SpectralAtom};
4use sva_formula::{AUDIBLE_CEILING_HZ, Line, SpectralSum, Var, d_dt, envelope, line_atoms};
5use sva_samples::{
6 AliasScore, Buffer, Consumes, Peak, PitchFrame, Source, measure::bands, measure::crest,
7 measure::envelope, measure::formants, measure::loudness, measure::pitch, measure::spectrum,
8 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
17pub 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::Bindings {
35 return Ok(Answer::whole(
36 Output::Bindings(render.bindings.get(&node).cloned().unwrap_or_default()),
37 Source::Exact,
38 profile,
39 None,
40 ));
41 }
42 match representation.consumes(closed) {
43 Consumes::ClosedForm => {
44 let found;
45 let sum = match render.symbolic.get(&node) {
46 Some(held) => held,
47 None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
48 Ok(held) => {
49 found = held;
50 &found
51 }
52 Err(left) => return measured_instead(render, node, representation, left),
53 },
54 };
55 let (value, listed, source) = match exact(render, node, representation, sum) {
56 Err(left) => return measured_instead(render, node, representation, left),
57 Ok(held) => held,
58 };
59 Ok(Answer {
60 value,
61 source,
62 profile,
63 rate: None,
64 tail_db: listed.tail_db(),
65 dropped: listed.dropped,
66 })
67 }
68 _ => {
69 let buffer = render
70 .buffers
71 .get(&node)
72 .ok_or_else(|| unmaterialized(render, node, representation))?;
73 Ok(Answer::whole(
74 measured(render, node, representation, buffer)?,
75 Source::Measured,
76 profile,
77 Some(render.config.rate),
78 ))
79 }
80 }
81}
82
83fn measured_instead(
86 render: &Render,
87 node: sva_formula::NodeId,
88 representation: Representation,
89 left: EngineError,
90) -> Result<Answer, EngineError> {
91 match representation {
92 Representation::Envelope { frame_secs } if left.code() == "cast.left_algebra" => {
93 off_the_grid(render, node, frame_secs)
94 }
95 _ => Err(left),
96 }
97}
98
99fn on_the_grid(render: &Render, node: sva_formula::NodeId) -> Result<Buffer, EngineError> {
102 if let Some(held) = render.buffers.get(&node) {
103 return Ok(held.clone());
104 }
105 let (rate, horizon) = (render.config.rate, render.config.horizon);
106 let profile = &render.config.profile;
107 let written = refs::substituted_closed_form(&render.tys, node);
108 let composed;
109 let sum = match render.symbolic.get(&node) {
110 Some(held) => Some(held),
111 None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
112 Ok(held) => {
113 composed = held;
114 Some(&composed)
115 }
116 Err(left) if written.is_none() => return Err(left),
117 Err(_) => None,
118 },
119 };
120 let taken = match sum {
121 Some(sum) => sva_samples::of_spectral_sum_or_point(
122 sum,
123 written.as_ref(),
124 rate,
125 horizon,
126 profile,
127 AliasScore::NotAsked,
128 ),
129 None => sva_samples::render(
130 written
131 .as_ref()
132 .expect("a node with no sum answers off the closed form written above"),
133 rate,
134 horizon,
135 profile,
136 AliasScore::NotAsked,
137 ),
138 };
139 taken
140 .map(|(buffer, _)| buffer)
141 .map_err(|e| refused(render, node, e.code(), e.to_string()))
142}
143
144fn off_the_grid(
145 render: &Render,
146 node: sva_formula::NodeId,
147 frame_secs: Option<f64>,
148) -> Result<Answer, EngineError> {
149 let rate = render.config.rate;
150 let buffer = on_the_grid(render, node)?;
151 Ok(Answer::whole(
152 Output::Envelope(envelope::trace(
153 buffer.plane(0),
154 f64::from(rate),
155 buffer.origin_secs,
156 frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
157 )),
158 Source::Measured,
159 render.config.profile.name,
160 Some(rate),
161 ))
162}
163
164fn exact(
165 render: &Render,
166 node: sva_formula::NodeId,
167 representation: Representation,
168 sum: &SpectralSum,
169) -> Result<(Output, Listed, Source), EngineError> {
170 let source = Source::Exact;
171 let enumerated = || {
172 listed(
173 render,
174 node,
175 sum,
176 render.config.profile.floor(AUDIBLE_CEILING_HZ),
177 )
178 };
179 let (value, listed) = match representation {
180 Representation::Lines => {
181 let listed = enumerated()?;
182 let held = lines(render, node, sum.var, &listed.atoms)?;
183 (Output::Lines(held), listed)
184 }
185 Representation::Spectrum {
186 frame_secs: Some(_),
187 ..
188 } => {
189 return Err(refused(
190 render,
191 node,
192 "engine.observation_needs_samples",
193 "a closed form has no frames to take a spectrum across".to_string(),
194 ));
195 }
196 Representation::Spectrum {
198 frame_secs: None, ..
199 } => {
200 let listed = enumerated()?;
201 let mut held = lines(render, node, sum.var, &listed.atoms)?;
202 held.sort_by(|a, b| a.hz.total_cmp(&b.hz));
203 (Output::Lines(held), listed)
204 }
205 Representation::Atoms => {
206 let listed = enumerated()?;
207 let found = listed.atoms.iter().map(sketch_atom).collect();
208 (Output::Atoms(found), listed)
209 }
210 Representation::Pitch { max_notes, .. } => {
212 let listed = enumerated()?;
213 let held = lines(render, node, sum.var, &listed.atoms)?;
214 let mut found: Vec<Peak> = held
215 .iter()
216 .filter(|l| l.hz > 0.0)
217 .map(|l| Peak {
218 hz: l.hz,
219 db: 20.0 * amplitude_at(&held, l.hz).log10(),
220 })
221 .collect();
222 found.sort_by(|a, b| b.db.total_cmp(&a.db));
223 let frame = PitchFrame {
224 t_secs: render.config.horizon.start_secs,
225 notes: pitch::name_peaks(&found, max_notes),
226 };
227 (Output::Pitch(vec![frame]), listed)
228 }
229 Representation::Derivative => (Output::Symbolic(Box::new(d_dt(sum))), Listed::NONE),
230 Representation::Envelope { .. } => {
231 let held = envelope(sum).map_err(|left| {
232 EngineError::of_closed_form(
233 &left.refusal(),
234 render.tys.locate(left.origin),
235 "read the envelope off sample(...) for a measured one",
236 )
237 })?;
238 (Output::Symbolic(Box::new(held.squared)), Listed::NONE)
239 }
240 other => return Err(not_a_closed_form(render, node, other)),
241 };
242 Ok((value, listed, source))
243}
244
245struct Listed {
248 atoms: Vec<SpectralAtom>,
249 dropped: Vec<Line>,
250}
251
252impl Listed {
253 const NONE: Listed = Listed {
255 atoms: Vec::new(),
256 dropped: Vec::new(),
257 };
258
259 fn tail_db(&self) -> Option<f64> {
262 let loudest = |set: &mut dyn Iterator<Item = f64>| set.fold(0.0f64, f64::max);
263 let gone = loudest(&mut self.dropped.iter().map(|l| l.amp.abs()));
264 let kept = loudest(&mut self.atoms.iter().map(|a| a.c.abs()));
265 (gone > 0.0 && kept > 0.0).then(|| 20.0 * (gone / kept).log10())
266 }
267}
268
269fn listed(
272 render: &Render,
273 node: sva_formula::NodeId,
274 sum: &SpectralSum,
275 floor_db: f64,
276) -> Result<Listed, EngineError> {
277 let mut held = Listed::NONE;
278 for lane in &sum.lanes {
279 held.atoms.extend(lane.clone().expanded().atoms);
280 for series in &lane.series {
281 let Some(found) = line_atoms(series, AUDIBLE_CEILING_HZ, floor_db) else {
282 return Err(unenumerable(render, node));
283 };
284 held.atoms.extend(found.atoms);
285 held.dropped.extend(found.dropped);
286 }
287 }
288 Ok(held)
289}
290
291fn lines(
294 render: &Render,
295 node: sva_formula::NodeId,
296 var: Var,
297 atoms: &[SpectralAtom],
298) -> Result<Vec<Line>, EngineError> {
299 atoms
300 .iter()
301 .map(|a| line_of(var, a).ok_or_else(|| not_a_line(render, node, var, a)))
302 .collect()
303}
304
305fn line_of(var: Var, a: &SpectralAtom) -> Option<Line> {
306 match (var, a.sing) {
307 (Var::F, Singular::Delta { at, order: 0 }) => Some(Line { hz: at, amp: a.c }),
308 (Var::T, Singular::Regular) => {
309 if a.poly > 0 || a.gauss.is_some() || a.ind.is_some() || a.pole.is_some() {
310 return None;
311 }
312 match a.exp {
313 None => Some(Line { hz: 0.0, amp: a.c }),
315 Some(e) if e.sigma == 0.0 => Some(Line {
316 hz: e.omega / std::f64::consts::TAU,
317 amp: a.c,
318 }),
319 Some(_) => None,
320 }
321 }
322 _ => None,
323 }
324}
325
326fn widening(a: &SpectralAtom) -> Vec<&'static str> {
328 let mut held = Vec::new();
329 if a.poly > 0 {
330 held.push("a polynomial");
331 }
332 if a.exp.is_some_and(|e| e.sigma != 0.0) {
333 held.push("a decaying exponential");
334 }
335 if a.gauss.is_some() {
336 held.push("a Gaussian");
337 }
338 if a.pole.is_some() {
339 held.push("a pole");
340 }
341 if held.is_empty() {
342 held.push("a delta");
343 }
344 held
345}
346
347fn not_a_line(
349 render: &Render,
350 node: sva_formula::NodeId,
351 var: Var,
352 atom: &SpectralAtom,
353) -> EngineError {
354 let mut at = render.tys.locate(atom.origin);
355 if at.node.is_empty() {
356 at = Located::at(render.tys.name(node), None);
357 }
358 let message = match atom.ind {
359 _ if var == Var::F => "this term spreads over `f` rather than standing at one \
360 frequency, and a line in `f` is a delta"
361 .to_string(),
362 Some(window) => format!(
363 "this term is windowed to [{}s, {}s), and a windowed line is that line convolved \
364 with the window's transform, which has a width and is no line",
365 window.l.value(),
366 window.r.value()
367 ),
368 None => format!(
369 "this term carries {}, and a line is a bare turning exponential: each of those \
370 convolves the line with a shape of its own width",
371 widening(atom).join(", ")
372 ),
373 };
374 EngineError::refused(Diagnostic {
375 code: "read.lines_need_unwindowed_lines".to_string(),
376 message,
377 location: at,
378 help: "`--as atoms` states each term as it stands, and `--as lines` of the node under \
379 the window or envelope lists the lines it multiplies"
380 .to_string(),
381 })
382}
383
384fn amplitude_at(held: &[Line], hz: f64) -> f64 {
387 held.iter()
388 .filter(|l| (l.hz - hz).abs() <= f64::EPSILON * hz.abs() || l.hz == -hz)
389 .map(|l| l.amp.abs())
390 .sum()
391}
392
393pub fn sketch_atom(a: &SpectralAtom) -> String {
395 let factors: Vec<&'static str> = a.present().iter().map(|f| f.as_str()).collect();
396 format!("{} x {}", a.c.abs(), factors.join(" times "))
397}
398
399fn measured(
400 render: &Render,
401 node: sva_formula::NodeId,
402 representation: Representation,
403 buffer: &Buffer,
404) -> Result<Output, EngineError> {
405 Ok(match representation {
406 Representation::Alias { oversample } => {
407 let reference = oversampled(render, behind(render, node)?, oversample)?;
408 Output::Alias(Box::new(worst_alias(buffer, &reference, oversample)))
409 }
410 Representation::Ledger { depth } => Output::Ledger(attributed(render, node, depth)?),
411 other => {
412 return off_buffer(buffer, other).map_err(|fault| match fault {
413 NoReading::NeedsAGraph => not_a_reading(render, node, other),
414 NoReading::TooNarrow { need, held } => too_narrow(render, node, need, held),
415 });
416 }
417 })
418}
419
420#[derive(Clone, Copy, Debug, PartialEq, Eq)]
423pub enum NoReading {
424 NeedsAGraph,
425 TooNarrow { need: usize, held: usize },
426}
427
428pub fn off_buffer(buffer: &Buffer, representation: Representation) -> Result<Output, NoReading> {
430 let sr = f64::from(buffer.rate);
431 let start = buffer.origin_secs;
432 let plane = buffer.plane(0);
433 Ok(match representation {
434 Representation::Samples => Output::Samples(Box::new(buffer.clone())),
435 Representation::Spectrum {
436 max_peaks,
437 frame_secs,
438 } => Output::Spectrum(Box::new(spectrum::analyze(
439 plane, sr, max_peaks, frame_secs,
440 ))),
441 Representation::Envelope { frame_secs } => Output::Envelope(envelope::trace(
442 plane,
443 sr,
444 start,
445 frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
446 )),
447 Representation::Derivative => Output::Samples(Box::new(difference(buffer))),
448 Representation::Pitch {
449 max_notes,
450 frame_secs,
451 } => Output::Pitch(pitch::track(plane, sr, start, frame_secs, max_notes)),
452 Representation::Formants {
453 max_formants,
454 frame_secs,
455 } => Output::Formants(formants::track(
456 plane,
457 sr,
458 start,
459 frame_secs,
460 formants::default_order(sr),
461 max_formants,
462 )),
463 Representation::Stereo { frame_secs } => {
464 if buffer.width < 2 {
465 return Err(NoReading::TooNarrow {
466 need: 2,
467 held: buffer.width,
468 });
469 }
470 let planes: Vec<&[f64]> = (0..buffer.width).map(|c| buffer.plane(c)).collect();
471 Output::Stereo(Box::new(stereo::analyze(
472 &planes,
473 buffer.width,
474 sr,
475 start,
476 frame_secs,
477 )))
478 }
479 Representation::Bands => Output::Bands(Box::new(bands::analyze(plane, sr, start))),
480 Representation::Crest => Output::Crest(Box::new(crest::analyze(plane, sr))),
481 Representation::Loudness => {
482 let planes: Vec<&[f64]> = (0..buffer.width).map(|c| buffer.plane(c)).collect();
483 Output::Loudness(Box::new(loudness::analyze(&planes, sr, start)))
484 }
485 _ => return Err(NoReading::NeedsAGraph),
486 })
487}
488
489fn attributed(
493 render: &Render,
494 node: sva_formula::NodeId,
495 depth: usize,
496) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
497 let mut buffers = std::collections::BTreeMap::new();
498 let mut deps = std::collections::BTreeMap::new();
499 let mut kinds = std::collections::BTreeMap::new();
500 for (id, buffer) in &render.buffers {
501 let name = render.tys.name(*id).to_string();
502 let read = refs_read(render, *id)?
503 .into_iter()
504 .map(|op| render.tys.name(op).to_string())
505 .collect();
506 deps.insert(name.clone(), read);
507 kinds.insert(name.clone(), sva_samples::SignalKind::Audio);
508 buffers.insert(name, buffer.clone());
509 }
510 let mut contributed_by = std::collections::BTreeMap::new();
511 for (parent, child) in edges_under(render, node, depth)? {
512 if let Some(held) = contributed(render, parent, child)? {
513 contributed_by.insert(render.tys.name(child).to_string(), held);
514 }
515 }
516 let len = render.buffers.get(&node).map_or(0, |b| b.len());
517 Ok(sva_samples::measure::ledger::attribute(
518 &buffers,
519 &contributed_by,
520 &deps,
521 &kinds,
522 render.tys.name(node),
523 0..len,
524 depth,
525 ))
526}
527
528fn edges_under(
530 render: &Render,
531 node: sva_formula::NodeId,
532 depth: usize,
533) -> Result<Vec<(sva_formula::NodeId, sva_formula::NodeId)>, EngineError> {
534 let mut seen = std::collections::BTreeSet::from([node]);
535 let (mut level, mut out) = (vec![node], Vec::new());
536 for _ in 0..depth {
537 let mut next = Vec::new();
538 for parent in level {
539 for child in refs_read(render, parent)? {
540 if seen.insert(child) {
541 out.push((parent, child));
542 next.push(child);
543 }
544 }
545 }
546 level = next;
547 }
548 Ok(out)
549}
550
551fn refs_read(
553 render: &Render,
554 node: sva_formula::NodeId,
555) -> Result<Vec<sva_formula::NodeId>, EngineError> {
556 match render.tys.ty(node).is_closed_form() {
557 true => Ok(crate::schedule::read_operands(&render.tys, node)),
558 false => crate::render::slots::refs_read(render, node),
559 }
560}
561
562fn contributed(
566 render: &Render,
567 parent: sva_formula::NodeId,
568 child: sva_formula::NodeId,
569) -> Result<Option<Buffer>, EngineError> {
570 if !render.tys.ty(parent).is_closed_form() {
571 return crate::render::slots::contributed(render, parent, child);
572 }
573 let Value::ClosedForm(form) = render.tys.value(parent) else {
574 return Ok(None);
575 };
576 Ok(separable(&form.body, child)
577 .then(|| collapsed(render, parent, &alone(&form.body, child), form.var))
578 .flatten())
579}
580
581fn collapsed(
582 render: &Render,
583 parent: sva_formula::NodeId,
584 body: &sva_formula::Body,
585 var: Var,
586) -> Option<Buffer> {
587 let sum = refs::spectral_sum_of_body(&render.tys, parent, body, var).ok()?;
588 sva_samples::collapse::of_spectral_sum(
589 &sum,
590 render.config.rate,
591 render.config.horizon,
592 &render.config.profile,
593 AliasScore::NotAsked,
594 )
595 .ok()
596 .map(|(buffer, _)| buffer)
597}
598
599fn alone(f: &sva_formula::Body, child: sva_formula::NodeId) -> sva_formula::Body {
600 match f {
601 sva_formula::Body::Node(id) if *id != child => {
602 sva_formula::Body::Const(sva_formula::C64::ZERO)
603 }
604 other => sva_formula::closed_form::map_children(other, |p| {
605 sva_formula::Part::new(p.origin, alone(&p.body, child))
606 }),
607 }
608}
609
610fn separable(f: &sva_formula::Body, child: sva_formula::NodeId) -> bool {
612 let parts = sva_formula::closed_form::children(f);
613 match f {
614 sva_formula::Body::Add(_) => parts.iter().all(|p| separable(&p.body, child)),
615 _ => {
616 let mut holding = parts.iter().filter(|p| !refs::nodes_in(&p.body).is_empty());
617 match (holding.next(), holding.next()) {
618 (None, _) => true,
619 (Some(only), None) => separable(&only.body, child),
620 _ => !refs::nodes_in(f).contains(&child),
621 }
622 }
623 }
624}
625
626fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
629 if render.tys.ty(node).is_closed_form() {
630 return Ok(node);
631 }
632 match render.tys.value(node) {
633 crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
634 _ => Err(refused(
635 render,
636 node,
637 "engine.alias_needs_a_closed_form",
638 "an alias score is a render against the same closed form oversampled, and this node \
639 is samples with no closed form behind it"
640 .to_string(),
641 )),
642 }
643}
644
645fn worst_alias(buffer: &Buffer, reference: &Buffer, oversample: u32) -> sva_samples::Alias {
647 debug_assert_eq!(
648 buffer.width, reference.width,
649 "the reference is the same form at another rate"
650 );
651 let sr = f64::from(buffer.rate);
652 let width = buffer.width.min(reference.width);
653 sva_samples::worst_alias((0..width).map(|c| {
654 measure_alias(
655 buffer.plane(c),
656 reference.plane(c),
657 oversample as usize,
658 sr,
659 buffer.origin_secs,
660 )
661 }))
662 .expect("a buffer holds at least one component")
663}
664
665fn oversampled(
667 render: &Render,
668 node: sva_formula::NodeId,
669 oversample: u32,
670) -> Result<Buffer, EngineError> {
671 let rate = render.config.rate * oversample;
672 let taken = match refs::spectral_sum_of(&render.tys, node, Var::T) {
673 Ok(sum) => sva_samples::of_spectral_sum(
674 &sum,
675 rate,
676 render.config.horizon,
677 &render.config.profile,
678 AliasScore::NotAsked,
679 ),
680 Err(e) => match refs::substituted_closed_form(&render.tys, node) {
681 Some(form) => sva_samples::render(
682 &form,
683 rate,
684 render.config.horizon,
685 &render.config.profile,
686 AliasScore::NotAsked,
687 ),
688 None => return Err(e),
689 },
690 };
691 taken.map(|(buffer, _)| buffer).map_err(|e| {
692 EngineError::refused(Diagnostic {
693 code: e.code().to_string(),
694 message: e.to_string(),
695 location: Located::at(render.tys.name(node), None),
696 help: "an alias score needs a closed form to oversample".to_string(),
697 })
698 })
699}
700
701fn difference(buffer: &Buffer) -> Buffer {
703 let step = f64::from(buffer.rate);
704 let planes = (0..buffer.width)
705 .map(|c| {
706 let plane = buffer.plane(c);
707 plane
708 .iter()
709 .enumerate()
710 .map(|(i, x)| match i {
711 0 => 0.0,
712 _ => (x - plane[i - 1]) * step,
713 })
714 .collect()
715 })
716 .collect();
717 let mut out = Buffer::of_planes(buffer.rate, planes);
718 out.origin_secs = buffer.origin_secs;
719 out
720}
721
722fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
723 EngineError::refused(Diagnostic {
724 code: code.to_string(),
725 message,
726 location: Located::at(render.tys.name(node), None),
727 help: "ask for a reading this representation answers".to_string(),
728 })
729}
730
731fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
732 refused(
733 render,
734 node,
735 "read.series_not_enumerable",
736 "a series whose term is no line lists no lines, and an empty list would read as \
737 a node with none"
738 .to_string(),
739 )
740}
741
742fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
743 refused(
744 render,
745 node,
746 "engine.observation_needs_samples",
747 format!(
748 "`{}` reads samples, and this node is a closed form",
749 r.name()
750 ),
751 )
752}
753
754fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
755 refused(
756 render,
757 node,
758 "engine.observation_not_wired",
759 format!("`{}` takes no reading off a buffer here", r.name()),
760 )
761}
762
763fn too_narrow(render: &Render, node: sva_formula::NodeId, need: usize, held: usize) -> EngineError {
764 refused(
765 render,
766 node,
767 "type.width_mismatch",
768 format!("this reading needs {need} components and the node holds {held}"),
769 )
770}
771
772fn unmaterialized(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
773 refused(
774 render,
775 node,
776 "engine.not_materialized",
777 format!("`{}` reads samples this render never held", r.name()),
778 )
779}
780
781pub fn answer_buffer(
784 name: &str,
785 buffer: &Buffer,
786 representation: Representation,
787 profile: &'static str,
788) -> Result<Answer, EngineError> {
789 let refused = |code: &str, message: String| {
790 EngineError::refused(Diagnostic {
791 code: code.to_string(),
792 message,
793 location: Located::at(name, None),
794 help: "ask for a reading a buffer answers on its own".to_string(),
795 })
796 };
797 let value = off_buffer(buffer, representation).map_err(|fault| match fault {
798 NoReading::NeedsAGraph => refused(
799 "engine.observation_needs_a_graph",
800 format!(
801 "`{}` reads the tree a node was built from, and a file has none",
802 representation.name()
803 ),
804 ),
805 NoReading::TooNarrow { need, held } => refused(
806 "type.width_mismatch",
807 format!("this reading needs {need} components and the file holds {held}"),
808 ),
809 })?;
810 Ok(Answer::whole(
811 value,
812 Source::Measured,
813 profile,
814 Some(buffer.rate),
815 ))
816}