Skip to main content

sva_samples/collapse/
truncate.rs

1// Concern: truncates every series to the terms the profile leaves, once per collapse | Non-concern: evaluating what is left (point.rs) | IO: (&SpectralSum or &Body) -> the same, series-free
2
3use std::f64::consts::TAU;
4
5use sva_formula::affine::exact_constant;
6use sva_formula::closed_form::{Bound, Series, children, map_children};
7use sva_formula::series::{mentions_line, substitute};
8use sva_formula::spectral_sum::atom::{Exp, Factors, Singular, SpectralAtom};
9use sva_formula::table::series::{Shape, read};
10use sva_formula::{Body, C64, Lane, Part, SpectralSum, Var, lines};
11
12use crate::error::CollapseError;
13use crate::profile::Profile;
14
15/// What a series is truncated against: the observation's own ceiling and audibility floor.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Audible {
18    ceiling: f64,
19    floor_db: f64,
20}
21
22impl Audible {
23    pub fn of(profile: &Profile, rate: u32) -> Audible {
24        let ceiling = profile.ceiling(rate);
25        Audible {
26            ceiling,
27            floor_db: profile.floor(ceiling),
28        }
29    }
30}
31
32/// A line series is placed analytically under `sva_formula`'s own bound; an expanded one
33/// becomes a formula the sample loop walks, which is what this caps.
34const MAX_EXPANDED_TERMS: usize = 1 << 13;
35
36/// FORMAT 6.2 truncates a series once, at collapse: every term the ceiling and the floor
37/// leave becomes an ordinary atom before the first sample is read.
38pub fn spectral_sum(n: &SpectralSum, band: Audible) -> Result<SpectralSum, CollapseError> {
39    if n.lanes.iter().all(|l| l.series.is_empty()) {
40        return Ok(n.clone());
41    }
42    let mut lanes = Vec::with_capacity(n.lanes.len());
43    for lane in &n.lanes {
44        let mut held = Lane {
45            series: Vec::new(),
46            ..lane.clone()
47        };
48        for s in &lane.series {
49            held.atoms.extend(atoms(s, band)?);
50        }
51        lanes.push(held);
52    }
53    Ok(SpectralSum::of(n.var, lanes))
54}
55
56/// The same truncation over a written closed form, whose series a spectral sum never reached.
57pub fn written(f: &Body, band: Audible) -> Result<Body, CollapseError> {
58    if let Body::Series(s) = f {
59        return expanded(s, band);
60    }
61    let mut found = None;
62    let out = map_children(f, |p| match written(&p.body, band) {
63        Ok(body) => Part::new(p.origin, body),
64        Err(e) => {
65            found = Some(e);
66            p.clone()
67        }
68    });
69    match found {
70        Some(e) => Err(e),
71        None => Ok(out),
72    }
73}
74
75fn atoms(s: &Series, band: Audible) -> Result<Vec<SpectralAtom>, CollapseError> {
76    let (body, window) = sva_formula::crop_peeled(&s.term.body);
77    let bare = Series {
78        term: Part::new(s.term.origin, body),
79        ..s.clone()
80    };
81    let taken = enumerated(&bare, band);
82    if !taken.is_empty() {
83        return Ok(taken
84            .into_iter()
85            .map(|l| {
86                SpectralAtom::new(
87                    l.amp,
88                    Factors {
89                        exp: Some(Exp::at(0.0, TAU * l.hz)),
90                        ind: window,
91                        ..Factors::NONE
92                    },
93                    Singular::Regular,
94                    s.term.origin,
95                )
96            })
97            .collect());
98    }
99    let body = expanded(s, band)?;
100    let held = sva_formula::normalize(&body, Var::T)
101        .map_err(|_| CollapseError::NotEvaluable("a series term"))?;
102    Ok(held.lanes.into_iter().flat_map(|l| l.atoms).collect())
103}
104
105/// A line series places its terms analytically; anything else is summed term by term.
106fn expanded(s: &Series, band: Audible) -> Result<Body, CollapseError> {
107    let taken = enumerated(s, band);
108    if !taken.is_empty() {
109        return Ok(sum(taken.iter().map(wave).collect()));
110    }
111    let count = terms(s, band)?;
112    let mut parts = Vec::with_capacity(count);
113    for i in 0..count {
114        let form = substitute(&s.term.body, s.index, (s.lo + i as i64) as f64);
115        parts.push(written(&form, band)?);
116    }
117    Ok(sum(parts))
118}
119
120fn enumerated(s: &Series, band: Audible) -> Vec<sva_formula::Line> {
121    match read(&s.term.body) {
122        Some(Shape::Lines(_)) => lines(s, band.ceiling, band.floor_db).taken,
123        _ => Vec::new(),
124    }
125}
126
127fn wave(l: &sva_formula::Line) -> Body {
128    Body::Mul(vec![
129        Part::bare(Body::Const(l.amp)),
130        Part::bare(Body::Apply(
131            sva_formula::Unary::Exp,
132            Part::bare(Body::Mul(vec![
133                Part::bare(Body::Const(C64::new(0.0, TAU * l.hz))),
134                Part::bare(Body::Line),
135            ])),
136        )),
137    ])
138}
139
140fn sum(parts: Vec<Body>) -> Body {
141    match parts.len() {
142        0 => Body::Const(C64::ZERO),
143        1 => parts.into_iter().next().expect("one part"),
144        _ => Body::Add(parts.into_iter().map(Part::bare).collect()),
145    }
146}
147
148/// A nesting is priced whole before it expands: a written bound counts like the floor's.
149fn terms(s: &Series, band: Audible) -> Result<usize, CollapseError> {
150    let cost = cost(s, band).ok_or(CollapseError::NotEvaluable(
151        "a series whose term count no coefficient bounds",
152    ))?;
153    if cost.depth == 1 && cost.own > MAX_EXPANDED_TERMS {
154        return Err(CollapseError::NotEvaluable(
155            "a series of more terms than one instant expands",
156        ));
157    }
158    match cost.terms > MAX_EXPANDED_TERMS {
159        true => Err(CollapseError::NestedSeries {
160            depth: cost.depth,
161            terms: cost.terms,
162            bound: MAX_EXPANDED_TERMS,
163        }),
164        false => Ok(cost.own),
165    }
166}
167
168/// Series deep, terms the whole nesting takes, terms this level alone takes.
169struct Cost {
170    depth: usize,
171    terms: usize,
172    own: usize,
173}
174
175fn cost(s: &Series, band: Audible) -> Option<Cost> {
176    let own = counted(s, band)?;
177    let inside = substitute(&s.term.body, s.index, s.lo as f64);
178    let (depth, inner) = price(&inside, band)?;
179    Some(Cost {
180        depth: depth + 1,
181        terms: own.saturating_mul(inner),
182        own,
183    })
184}
185
186/// A sum of series costs their counts added; every other spelling multiplies.
187fn price(f: &Body, band: Audible) -> Option<(usize, usize)> {
188    if let Body::Series(s) = f {
189        let cost = cost(s, band)?;
190        return Some((cost.depth, cost.terms));
191    }
192    let summed = matches!(f, Body::Add(_));
193    let mut depth = 0;
194    let mut terms: usize = if summed { 0 } else { 1 };
195    for p in children(f) {
196        let (d, n) = price(&p.body, band)?;
197        depth = depth.max(d);
198        terms = match summed {
199            true => terms.saturating_add(n),
200            false => terms.saturating_mul(n),
201        };
202    }
203    Some((depth, terms.max(1)))
204}
205
206fn counted(s: &Series, band: Audible) -> Option<usize> {
207    let hi = match s.hi {
208        Bound::Finite(n) => return usize::try_from((n - s.lo + 1).max(0)).ok(),
209        Bound::Infinite => MAX_EXPANDED_TERMS,
210    };
211    let coefficient = coefficient(&s.term.body)?;
212    let floor = 10f64.powf(band.floor_db / 20.0);
213    let mut peak = 0.0f64;
214    for i in 0..hi {
215        let at = substitute(&coefficient, s.index, (s.lo + i as i64) as f64);
216        let held = exact_constant(&at)?.abs();
217        peak = peak.max(held);
218        if peak > 0.0 && held < peak * floor {
219            return Some(i.max(1));
220        }
221    }
222    None
223}
224
225/// A turning factor stands for the bound it turns inside; one with no bound leaves the
226/// count undecided. A node and a series turn too: the index reaches either only as a shift.
227fn coefficient(f: &Body) -> Option<Body> {
228    let one = || Some(Body::Const(C64::ONE));
229    match f {
230        Body::Line | Body::Node(_) | Body::Series(_) => one(),
231        Body::Mul(parts) | Body::Add(parts) => {
232            let mut kept = Vec::with_capacity(parts.len());
233            for p in parts {
234                kept.push(Part::new(p.origin, coefficient(&p.body)?));
235            }
236            Some(match f {
237                Body::Mul(_) => Body::Mul(kept),
238                _ => Body::Add(kept),
239            })
240        }
241        Body::Div(a, b) => Some(Body::Div(
242            Part::new(a.origin, coefficient(&a.body)?),
243            Part::new(b.origin, coefficient(&b.body)?),
244        )),
245        Body::Apply(
246            sva_formula::Unary::Sin
247            | sva_formula::Unary::Cos
248            | sva_formula::Unary::Tanh
249            | sva_formula::Unary::Sat,
250            _,
251        ) => one(),
252        Body::Crop { of, .. } | Body::Shift { of, .. } => coefficient(&of.body),
253        Body::Pow(base, n) => Some(Body::Pow(
254            Part::new(base.origin, coefficient(&base.body)?),
255            *n,
256        )),
257        other if !mentions_line(other) => Some(other.clone()),
258        _ => None,
259    }
260}