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::cell::RefCell;
4use std::collections::HashMap;
5use std::f64::consts::TAU;
6
7use sva_formula::affine::{Axis, axis_read, exact_constant_at, exact_constant_read};
8use sva_formula::closed_form::{Bound, Series, children, map_children};
9use sva_formula::series::{falls, mentions, mentions_line_read, ratio, substitute};
10use sva_formula::spectral_sum::atom::{Exp, Factors, Singular, SpectralAtom};
11use sva_formula::spectral_sum::merge::simplify;
12use sva_formula::spectral_sum::sup::{Fate, fate, sup_from};
13use sva_formula::table::series::{Shape, read_with};
14use sva_formula::through::{Read, looked};
15use sva_formula::{
16    Banded, Body, C64, Codomain, Env, IndexId, Lane, NodeId, Opaque, ParamId, Part, Reads, Run,
17    SpectralSum, Ty, Unary, Var, d_dt, leading_read, lines_read, normalize_read,
18};
19
20use crate::error::CollapseError;
21use crate::profile::Profile;
22
23/// What a series is truncated against: the observation's own ceiling, the amplitude a
24/// dropped tail provably stays within, and the floor a tail no bound sums falls under.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct Audible {
27    ceiling: f64,
28    precision: f64,
29    floor_db: f64,
30}
31
32impl Audible {
33    pub fn of(profile: &Profile, rate: u32) -> Audible {
34        Audible::on(profile, crate::Grid::of(rate))
35    }
36
37    pub fn on(profile: &Profile, grid: crate::Grid) -> Audible {
38        let ceiling = profile.ceiling_hz.min(grid.sr() / 2.0);
39        Audible {
40            ceiling,
41            precision: profile.half_lsb(),
42            floor_db: profile.floor(ceiling),
43        }
44    }
45
46    pub fn key(self) -> [u64; 3] {
47        [self.ceiling, self.precision, self.floor_db].map(f64::to_bits)
48    }
49
50    /// A line read at `at(t)` sounds at `hz*at'(t)`; an unbounded `at'` keeps the band, unclaimed.
51    fn read_at(self, at: &Body, reads: &dyn Reads) -> Audible {
52        match sva_formula::calculus::steepest_read(at, reads) {
53            Some(rate) if rate > 1.0 => Audible {
54                ceiling: self.ceiling / rate,
55                ..self
56            },
57            _ => self,
58        }
59    }
60}
61
62/// A line series is placed analytically under `sva_formula`'s own bound; an expanded one
63/// becomes a formula the sample loop walks, which is what this caps.
64const MAX_EXPANDED_TERMS: usize = 1 << 13;
65
66/// The most terms of a carrier series one instant sums: every term under a 20 kHz ceiling
67/// down to a 19.5 Hz fundamental, the bottom of the audible band; below it what an instant
68/// leaves out is bounded and reported.
69const MOST_PER_INSTANT: i64 = 1 << 10;
70
71/// FORMAT 6.2 truncates a series once, at collapse: every term the ceiling and the precision
72/// leave becomes an ordinary atom before the first sample is read.
73pub fn spectral_sum(n: &SpectralSum, band: Audible) -> Result<SpectralSum, CollapseError> {
74    spectral_sum_read(n, band, &Opaque)
75}
76
77pub fn spectral_sum_read(
78    n: &SpectralSum,
79    band: Audible,
80    reads: &dyn Reads,
81) -> Result<SpectralSum, CollapseError> {
82    Terms::new(reads).spectral_sum(n, band)
83}
84
85/// A steady atom's magnitude `n`'s truncation keeps, found listing no series; `None` where
86/// truncating may refuse.
87pub fn steady_read(n: &SpectralSum, band: Audible, reads: &dyn Reads) -> Option<f64> {
88    Terms::new(reads).steady(n, band)
89}
90
91/// The same truncation over a written closed form, whose series a spectral sum never reached.
92pub fn written(f: &Body, band: Audible) -> Result<Body, CollapseError> {
93    written_with(f, band, &Opaque, &mut |id, _| Ok(id))
94}
95
96/// Each ref truncated as the form `reads` names, under the band it is read in, and renamed to
97/// what `named` calls that truncation.
98pub fn written_with(
99    f: &Body,
100    band: Audible,
101    reads: &dyn Reads,
102    named: &mut dyn FnMut(NodeId, Audible) -> Result<NodeId, CollapseError>,
103) -> Result<Body, CollapseError> {
104    Terms::new(reads).written(f, band, named)
105}
106
107/// The loudest term any banded series in `f` may drop, in dB against its loudest.
108pub fn dropped_db(f: &Body) -> Option<f64> {
109    let own = match f {
110        Body::Banded(b) => Some(b.dropped_db),
111        _ => None,
112    };
113    let inner = children(f).into_iter().filter_map(|p| dropped_db(&p.body));
114    own.into_iter().chain(inner).reduce(f64::max)
115}
116
117pub(super) fn windowed_lines(
118    s: &Series,
119    band: Audible,
120    reads: &dyn Reads,
121) -> Option<(Vec<sva_formula::Line>, Option<sva_formula::Indicator>)> {
122    Terms::new(reads).windowed_lines(s, band)
123}
124
125/// One truncation's reading of the refs its terms hold, once per node and band.
126struct Terms<'a> {
127    reads: &'a dyn Reads,
128    bounds: PerBand<Option<Bounded>>,
129    prices: PerBand<Option<Price>>,
130    series: RefCell<HashMap<NodeId, bool>>,
131    summed: PerBand<NodeId>,
132}
133
134type PerBand<T> = RefCell<HashMap<(NodeId, [u64; 3]), T>>;
135
136/// Series deep, and terms the whole nesting takes.
137type Price = (usize, Option<usize>);
138
139/// Series deep, terms the whole nesting takes, terms this level takes.
140struct Cost {
141    depth: usize,
142    terms: Option<usize>,
143    own: usize,
144}
145
146/// A term's coefficient in the index. `exact` where it bounds the term's magnitude; elsewhere
147/// it only stands in, a turning factor or a name read as one.
148#[derive(Clone)]
149struct Bounded {
150    body: Body,
151    exact: bool,
152}
153
154impl<'a> Terms<'a> {
155    fn new(reads: &'a dyn Reads) -> Terms<'a> {
156        Terms {
157            reads,
158            bounds: RefCell::default(),
159            prices: RefCell::default(),
160            series: RefCell::default(),
161            summed: RefCell::default(),
162        }
163    }
164
165    fn holds_series(&self, id: NodeId) -> bool {
166        if let Some(held) = self.series.borrow().get(&id) {
167            return *held;
168        }
169        let form = self.reads.view(id, &Read::As);
170        let found = form.is_some_and(|form| self.reaches_series(&form));
171        self.series.borrow_mut().insert(id, found);
172        found
173    }
174
175    fn reaches_series(&self, f: &Body) -> bool {
176        match f {
177            Body::Series(_) => true,
178            Body::Node(id) => self.holds_series(*id),
179            other => children(other).iter().any(|p| self.reaches_series(&p.body)),
180        }
181    }
182
183    /// `id`, or a node standing for its form with each series it holds summed term by term.
184    fn summed(&self, id: NodeId, band: Audible) -> Result<NodeId, CollapseError> {
185        if !self.holds_series(id) {
186            return Ok(id);
187        }
188        let key = (id, band.key());
189        if let Some(held) = self.summed.borrow().get(&key) {
190            return Ok(*held);
191        }
192        let form = self
193            .reads
194            .view(id, &Read::As)
195            .expect("a ref holding a series names a form");
196        let body = self.written(&form, band, &mut |n, b| self.summed(n, b))?;
197        let stand = self
198            .reads
199            .stand_for(body)
200            .expect("a reader handing out forms stands one in");
201        self.summed.borrow_mut().insert(key, stand);
202        Ok(stand)
203    }
204
205    fn spectral_sum(&self, n: &SpectralSum, band: Audible) -> Result<SpectralSum, CollapseError> {
206        if n.lanes.iter().all(|l| l.series.is_empty()) {
207            return Ok(n.clone());
208        }
209        let mut lanes = Vec::with_capacity(n.lanes.len());
210        for lane in &n.lanes {
211            let mut held = Lane {
212                series: Vec::new(),
213                ..lane.clone()
214            };
215            for s in &lane.series {
216                held.atoms.extend(self.atoms(s, band)?);
217            }
218            lanes.push(held);
219        }
220        Ok(SpectralSum::of(n.var, lanes))
221    }
222
223    fn written(
224        &self,
225        f: &Body,
226        band: Audible,
227        named: &mut dyn FnMut(NodeId, Audible) -> Result<NodeId, CollapseError>,
228    ) -> Result<Body, CollapseError> {
229        match f {
230            Body::Node(id) => return Ok(Body::Node(named(*id, band)?)),
231            Body::Series(s) => return self.expanded(s, band, named),
232            Body::Warp { at, of } => {
233                let moved = band.read_at(&at.body, self.reads);
234                return Ok(Body::Warp {
235                    at: Part::new(at.origin, self.written(&at.body, band, named)?),
236                    of: Part::new(of.origin, self.written(&of.body, moved, named)?),
237                });
238            }
239            _ => {}
240        }
241        let mut found = None;
242        let out = map_children(f, |p| match self.written(&p.body, band, named) {
243            Ok(body) => Part::new(p.origin, body),
244            Err(e) => {
245                found = Some(e);
246                p.clone()
247            }
248        });
249        match found {
250            Some(e) => Err(e),
251            None => Ok(out),
252        }
253    }
254
255    /// Only a line under the ceiling counts, so a direct sum over the kept lines has one.
256    fn steady(&self, n: &SpectralSum, band: Audible) -> Option<f64> {
257        let in_band =
258            |a: &SpectralAtom| (a.exp.map_or(0.0, |e| e.omega) / TAU).abs() < band.ceiling;
259        let mut most = 0.0f64;
260        for lane in &n.lanes {
261            let steady = lane
262                .atoms
263                .iter()
264                .filter(|a| fate(a) == Fate::Steady && in_band(a));
265            most = steady.map(|a| a.c.abs()).fold(most, f64::max);
266            for s in &lane.series {
267                let (body, window) = sva_formula::crop_peeled(&looked(&s.term.body, self.reads));
268                let bare = Series {
269                    term: Part::new(s.term.origin, body),
270                    ..s.clone()
271                };
272                let Some(Shape::Lines(_)) = read_with(&bare.term.body, self.reads) else {
273                    return None;
274                };
275                let held = (band.ceiling, band.floor_db, band.precision);
276                let first = leading_read(&bare, held, self.reads)?;
277                if window.is_none() {
278                    most = first.iter().map(|l| l.amp.abs()).fold(most, f64::max);
279                }
280            }
281        }
282        (most > 0.0).then_some(most)
283    }
284
285    fn windowed_lines(
286        &self,
287        s: &Series,
288        band: Audible,
289    ) -> Option<(Vec<sva_formula::Line>, Option<sva_formula::Indicator>)> {
290        let (body, window) = sva_formula::crop_peeled(&looked(&s.term.body, self.reads));
291        let bare = Series {
292            term: Part::new(s.term.origin, body),
293            ..s.clone()
294        };
295        let taken = self.enumerated(&bare, band).ok()?;
296        (!taken.is_empty()).then_some((taken, window))
297    }
298
299    fn atoms(&self, s: &Series, band: Audible) -> Result<Vec<SpectralAtom>, CollapseError> {
300        if let Some((taken, window)) = self.windowed_lines(s, band) {
301            return Ok(taken
302                .into_iter()
303                .map(|l| {
304                    SpectralAtom::new(
305                        l.amp,
306                        Factors {
307                            exp: Some(Exp::at(0.0, TAU * l.hz)),
308                            ind: window,
309                            ..Factors::NONE
310                        },
311                        Singular::Regular,
312                        s.term.origin,
313                    )
314                })
315                .collect());
316        }
317        if self.carried(s).is_some() && self.precise(s, band).is_none() {
318            return Err(CollapseError::NotEvaluable(
319                "a series summed instant by instant",
320            ));
321        }
322        let body = self.expanded(s, band, &mut |id, b| self.summed(id, b))?;
323        let held = normalize_read(&body, Var::T, self.reads)
324            .map_err(|_| CollapseError::NotEvaluable("a series term"))?;
325        let mut atoms = Vec::new();
326        for lane in held.lanes {
327            let mut summed = Lane::of(lane.atoms);
328            for inner in &lane.series {
329                summed.atoms.extend(self.atoms(inner, band)?);
330            }
331            if !lane.series.is_empty() {
332                simplify(&mut summed);
333            }
334            atoms.extend(summed.atoms);
335        }
336        Ok(atoms)
337    }
338
339    /// A line series keeps its terms as runs; anything else is summed term by term.
340    fn expanded(
341        &self,
342        s: &Series,
343        band: Audible,
344        named: &mut dyn FnMut(NodeId, Audible) -> Result<NodeId, CollapseError>,
345    ) -> Result<Body, CollapseError> {
346        let taken = self.enumerated(s, band)?;
347        if !taken.is_empty() {
348            let runs = Run::of(&taken).into_iter();
349            return Ok(sum(runs.map(|r| Body::Run(Box::new(r))).collect()));
350        }
351        if self.precise(s, band).is_none()
352            && let Some(banded) = self.banded(s, band, named)?
353        {
354            return Ok(Body::Banded(Box::new(banded)));
355        }
356        let count = self.terms(s, band)?;
357        let mut parts = Vec::with_capacity(count);
358        for i in 0..count {
359            let form = self.term(&s.term.body, s.index, (s.lo + i as i64) as f64);
360            parts.push(self.written(&form, band, named)?);
361        }
362        Ok(sum(parts))
363    }
364
365    /// One term at index `k`, a ref read at a time the index moves standing for its form read
366    /// there.
367    fn term(&self, f: &Body, k: IndexId, value: f64) -> Body {
368        match f {
369            Body::Index(i) if *i == k => Body::Const(C64::real(value)),
370            Body::Warp { at, of } if mentions(&at.body, k) => {
371                let time = self.term(&at.body, k, value);
372                let moved = match &*of.body {
373                    Body::Node(id) => self.reads.moved(*id, &time),
374                    _ => None,
375                };
376                match moved {
377                    Some(stand) => Body::Node(stand),
378                    None => Body::Warp {
379                        at: Part::new(at.origin, time),
380                        of: Part::new(of.origin, self.term(&of.body, k, value)),
381                    },
382                }
383            }
384            other => map_children(other, |p| Part::new(p.origin, self.term(&p.body, k, value))),
385        }
386    }
387
388    /// A line series whose tail no decay bounds refuses rather than be summed term by term.
389    fn enumerated(
390        &self,
391        s: &Series,
392        band: Audible,
393    ) -> Result<Vec<sva_formula::Line>, CollapseError> {
394        match read_with(&s.term.body, self.reads) {
395            Some(Shape::Lines(_)) => {
396                let held = (band.ceiling, band.floor_db, band.precision);
397                lines_read(s, held, self.reads)
398                    .map(|l| l.taken)
399                    .ok_or(CollapseError::NotEvaluable(
400                        "a series whose tail no bound sums",
401                    ))
402            }
403            _ => Ok(Vec::new()),
404        }
405    }
406
407    /// A nesting is priced whole before it expands: a written bound counts like the floor's.
408    fn terms(&self, s: &Series, band: Audible) -> Result<usize, CollapseError> {
409        let cost = self.cost(s, band).ok_or(CollapseError::NotEvaluable(
410            "a series whose term count no coefficient bounds",
411        ))?;
412        if cost.depth == 1 && cost.own > MAX_EXPANDED_TERMS {
413            return Err(CollapseError::NotEvaluable(
414                "a series of more terms than one instant expands",
415            ));
416        }
417        match cost.terms {
418            Some(terms) if terms <= MAX_EXPANDED_TERMS => Ok(cost.own),
419            terms => Err(CollapseError::NestedSeries {
420                depth: cost.depth,
421                terms,
422                bound: MAX_EXPANDED_TERMS,
423            }),
424        }
425    }
426
427    fn cost(&self, s: &Series, band: Audible) -> Option<Cost> {
428        let own = self.counted(s, band)?;
429        let inside = substitute(&s.term.body, s.index, s.lo as f64);
430        let (depth, inner) = self.price(&inside, band)?;
431        Some(Cost {
432            depth: depth + 1,
433            terms: inner.and_then(|inner| own.checked_mul(inner)),
434            own,
435        })
436    }
437
438    /// Series counts add under a sum, multiply elsewhere; a part with no series prices nothing.
439    fn price(&self, f: &Body, band: Audible) -> Option<Price> {
440        match f {
441            Body::Series(s) => {
442                let cost = self.cost(s, band)?;
443                return Some((cost.depth, cost.terms));
444            }
445            Body::Node(id) => return self.node_price(*id, band),
446            _ => {}
447        }
448        let summed = matches!(f, Body::Add(_));
449        let mut depth = 0;
450        let mut terms: Option<usize> = Some(if summed { 0 } else { 1 });
451        for p in children(f) {
452            let (d, n) = self.price(&p.body, band)?;
453            if d == 0 {
454                continue;
455            }
456            depth = depth.max(d);
457            terms = terms.zip(n).and_then(|(terms, n)| match summed {
458                true => terms.checked_add(n),
459                false => terms.checked_mul(n),
460            });
461        }
462        Some((depth, terms.map(|n| n.max(1))))
463    }
464
465    fn node_price(&self, id: NodeId, band: Audible) -> Option<Price> {
466        let key = (id, band.key());
467        if let Some(held) = self.prices.borrow().get(&key) {
468            return *held;
469        }
470        let found = match self.reads.view(id, &Read::As) {
471            Some(form) => self.price(&form, band),
472            None => Some((0, Some(1))),
473        };
474        self.prices.borrow_mut().insert(key, found);
475        found
476    }
477
478    /// A geometric magnitude bound stops where its whole tail rounds away; a carrier summed
479    /// instant by instant expands at most `MOST_PER_INSTANT` terms an instant.
480    fn counted(&self, s: &Series, band: Audible) -> Option<usize> {
481        if let Bound::Finite(n) = s.hi {
482            return usize::try_from((n - s.lo + 1).max(0)).ok();
483        }
484        let precise = self.precise(s, band);
485        precise.or_else(|| self.carried(s).map(|_| MOST_PER_INSTANT as usize))
486    }
487
488    /// The terms before the whole tail of a geometric magnitude bound rounds away.
489    fn precise(&self, s: &Series, band: Audible) -> Option<usize> {
490        let bound = self.bound(&s.term.body, band)?;
491        let ratio = ratio(&bound.body, s.index).filter(|r| *r < 1.0 && bound.exact)?;
492        for i in 0..MAX_EXPANDED_TERMS {
493            let at = (s.lo + i as i64) as f64;
494            let held = exact_constant_at(&bound.body, s.index, at, self.reads)?.abs();
495            if held / (1.0 - ratio) <= band.precision {
496                return Some(i.max(1));
497            }
498        }
499        None
500    }
501
502    /// The angle of a term's one carrier, where it turns at a rate affine in the index and
503    /// nothing else in the term moves with both the index and time.
504    fn carried<'s>(&self, s: &'s Series) -> Option<&'s Body> {
505        if s.hi != Bound::Infinite {
506            return None;
507        }
508        let angle = carrier(&s.term.body, s.index, self.reads)?;
509        (degree(angle, s.index)? == 1 && self.real(angle)).then_some(angle)
510    }
511
512    /// `s` summed at each instant over only the terms whose carrier turns under the ceiling
513    /// there, each turning at the time derivative of its angle read off a spectral sum.
514    fn banded(
515        &self,
516        s: &Series,
517        band: Audible,
518        named: &mut dyn FnMut(NodeId, Audible) -> Result<NodeId, CollapseError>,
519    ) -> Result<Option<Banded>, CollapseError> {
520        let Some(angle) = self.carried(s) else {
521            return Ok(None);
522        };
523        let at = |k: f64| substitute(angle, s.index, k);
524        let (still, once) = (at(0.0), at(1.0));
525        let per = Body::Add(vec![
526            Part::bare(once),
527            Part::bare(Body::Mul(vec![
528                Part::bare(Body::Const(C64::real(-1.0))),
529                Part::bare(still.clone()),
530            ])),
531        ]);
532        let turning = |f: &Body| {
533            let sum = normalize_read(f, Var::T, self.reads).ok()?;
534            let [lane] = sum.lanes.as_slice() else {
535                return None;
536            };
537            (lane.series.is_empty() && lane.modal.is_empty()).then(|| d_dt(&sum))
538        };
539        let (Some(slope), Some(offset)) = (turning(&per), turning(&still)) else {
540            return Ok(None);
541        };
542        let term = self.written(&s.term.body, band, named)?;
543        let weight = steady(&s.term.body, angle);
544        let mut banded = Banded {
545            series: Series {
546                term: Part::new(s.term.origin, term),
547                ..s.clone()
548            },
549            slope,
550            offset,
551            omega: TAU * band.ceiling,
552            most: MOST_PER_INSTANT,
553            widest: MOST_PER_INSTANT,
554            reach: f64::INFINITY,
555            dropped_db: f64::INFINITY,
556        };
557        self.bounded(&mut banded, &weight, band);
558        match banded.dropped_db.is_finite() {
559            true => Ok(Some(banded)),
560            false => Err(CollapseError::NotEvaluable(
561                "a series whose dropped terms no bound holds",
562            )),
563        }
564    }
565
566    /// The most terms an instant sums, a bound on the sum, and the loudest term it may drop.
567    fn bounded(&self, b: &mut Banded, weight: &Body, band: Audible) {
568        let (lo, k) = (b.series.lo, b.series.index);
569        let (fastest, drift) = (sup(&b.slope), sup(&b.offset));
570        let (slowest, still) = (least(&b.slope), least(&b.offset).max(0.0));
571        let count = |last: f64| (last.ceil().clamp(-9e18, 9e18) as i64 - lo).clamp(0, b.most);
572        let kept = match fastest > 0.0 {
573            true => count((b.omega - drift) / fastest),
574            false if drift < b.omega => b.most,
575            false => 0,
576        };
577        b.widest = match slowest > 0.0 {
578            true => count((b.omega + drift.max(still)) / slowest),
579            false => b.most,
580        };
581        let Some(bound) = self.bound(weight, band).filter(|w| w.exact) else {
582            return;
583        };
584        let at = |n: i64| Some(exact_constant_at(&bound.body, k, n as f64, self.reads)?.abs());
585        b.reach = (lo..lo + b.widest)
586            .try_fold(0.0, |held, n| Some(held + at(n)?))
587            .unwrap_or(f64::INFINITY);
588        if lo >= 1
589            && falls(&bound.body, k)
590            && let (Some(top), Some(gone)) = (at(lo), at(lo + kept))
591        {
592            b.dropped_db = 20.0 * (gone / top).log10();
593        }
594    }
595
596    fn bound(&self, f: &Body, band: Audible) -> Option<Bounded> {
597        let held = |body: Body, exact: bool| Some(Bounded { body, exact });
598        let under = |p: &Part| {
599            self.bound(&p.body, band)
600                .map(|b| (Part::new(p.origin, b.body), b.exact))
601        };
602        let all = |parts: &[Part], wrap: &dyn Fn(Part) -> Part| {
603            let mut exact = true;
604            let mut kept = Vec::with_capacity(parts.len());
605            for p in parts {
606                let (part, known) = under(p)?;
607                exact &= known;
608                kept.push(wrap(part));
609            }
610            Some((kept, exact))
611        };
612        let mentions_line = |f: &Body| mentions_line_read(f, self.reads);
613        match f {
614            Body::Series(s) => match self.total(s, band) {
615                Some(sum) => held(Body::Const(C64::real(sum)), true),
616                None => held(Body::Const(C64::ONE), false),
617            },
618            Body::Node(id) => self.node_bound(*id, band),
619            Body::Line => held(Body::Const(C64::ONE), false),
620            Body::Mul(parts) => {
621                let (kept, exact) = all(parts, &|p| p)?;
622                held(Body::Mul(kept), exact)
623            }
624            Body::Add(parts) => {
625                let (kept, exact) = all(parts, &|p| Part::bare(Body::Apply(Unary::Abs, p)))?;
626                held(Body::Add(kept), exact)
627            }
628            Body::Div(a, b) if !mentions_line(&b.body) => {
629                let (num, exact) = under(a)?;
630                held(Body::Div(num, b.clone()), exact)
631            }
632            Body::Div(a, b) => {
633                let ((num, _), (den, _)) = (under(a)?, under(b)?);
634                held(Body::Div(num, den), false)
635            }
636            Body::Apply(Unary::Sin | Unary::Cos | Unary::Tanh | Unary::Sat | Unary::Step, arg) => {
637                held(Body::Const(C64::ONE), self.real(&arg.body))
638            }
639            Body::Crop { of, .. } | Body::Shift { of, .. } | Body::Warp { of, .. } => {
640                self.bound(&of.body, band)
641            }
642            Body::Pow(base, n) => {
643                let (part, exact) = under(base)?;
644                held(Body::Pow(part, *n), exact && *n >= 0)
645            }
646            other if !mentions_line(other) => held(other.clone(), true),
647            _ => None,
648        }
649    }
650
651    /// A ref holds no index, so its bound is one number; one no number holds bounds nothing.
652    fn node_bound(&self, id: NodeId, band: Audible) -> Option<Bounded> {
653        let key = (id, band.key());
654        if let Some(held) = self.bounds.borrow().get(&key) {
655            return held.clone();
656        }
657        let found = match self.reads.view(id, &Read::As) {
658            Some(form) => self.bound(&form, band).and_then(|b| {
659                let value = exact_constant_read(&b.body, self.reads)?;
660                Some(Bounded {
661                    body: Body::Const(value),
662                    exact: b.exact,
663                })
664            }),
665            None => Some(Bounded {
666                body: Body::Const(C64::ONE),
667                exact: false,
668            }),
669        };
670        self.bounds.borrow_mut().insert(key, found.clone());
671        found
672    }
673
674    /// A geometric series sums to its first bound over `1 - ratio`; any other to what it keeps.
675    fn total(&self, s: &Series, band: Audible) -> Option<f64> {
676        let bound = self.bound(&s.term.body, band).filter(|b| b.exact)?.body;
677        let at = |i: i64| Some(exact_constant_at(&bound, s.index, i as f64, self.reads)?.abs());
678        if let (Bound::Infinite, Some(r)) = (s.hi, ratio(&bound, s.index).filter(|r| *r < 1.0)) {
679            return Some(at(s.lo)? / (1.0 - r));
680        }
681        let kept =
682            i64::try_from(self.counted(s, band).filter(|n| *n <= MAX_EXPANDED_TERMS)?).ok()?;
683        (s.lo..s.lo + kept).try_fold(0.0, |held, i| Some(held + at(i)?))
684    }
685
686    fn real(&self, f: &Body) -> bool {
687        axis_read(f, &Unread, self.reads) == Axis::Real
688    }
689}
690
691fn sum(parts: Vec<Body>) -> Body {
692    match parts.len() {
693        0 => Body::Const(C64::ZERO),
694        1 => parts.into_iter().next().expect("one part"),
695        _ => Body::Add(parts.into_iter().map(Part::bare).collect()),
696    }
697}
698
699/// The one `sin` or `cos` whose argument moves with the index, where nothing else moving with
700/// both the index and time is in the term.
701fn carrier<'f>(f: &'f Body, k: IndexId, reads: &dyn Reads) -> Option<&'f Body> {
702    let (mut found, mut clean) = (Vec::new(), true);
703    moving(f, k, reads, (&mut found, &mut clean));
704    match found.as_slice() {
705        [angle] if clean => Some(*angle),
706        _ => None,
707    }
708}
709
710fn moving<'f>(
711    f: &'f Body,
712    k: IndexId,
713    reads: &dyn Reads,
714    (found, clean): (&mut Vec<&'f Body>, &mut bool),
715) {
716    if !mentions(f, k) {
717        return;
718    }
719    match f {
720        Body::Apply(Unary::Sin | Unary::Cos, arg) => found.push(&arg.body),
721        Body::Apply(_, arg) if mentions_line_read(&arg.body, reads) => *clean = false,
722        Body::Warp { .. }
723        | Body::Series(_)
724        | Body::Banded(_)
725        | Body::Keyed { .. }
726        | Body::Delta { .. }
727        | Body::Pv(_)
728        | Body::Deriv { .. } => *clean = false,
729        other => {
730            for p in children(other) {
731                moving(&p.body, k, reads, (&mut *found, &mut *clean));
732            }
733        }
734    }
735}
736
737/// How many times the index multiplies into `f`, where it is a polynomial in it.
738fn degree(f: &Body, k: IndexId) -> Option<u32> {
739    if !mentions(f, k) {
740        return Some(0);
741    }
742    match f {
743        Body::Index(i) if *i == k => Some(1),
744        Body::Add(parts) => parts
745            .iter()
746            .try_fold(0, |held, p| Some(held.max(degree(&p.body, k)?))),
747        Body::Mul(parts) => parts
748            .iter()
749            .try_fold(0, |held, p| Some(held + degree(&p.body, k)?)),
750        Body::Div(num, den) if !mentions(&den.body, k) => degree(&num.body, k),
751        Body::Pow(base, n) if *n >= 0 => Some(degree(&base.body, k)? * n.unsigned_abs()),
752        _ => None,
753    }
754}
755
756/// `term` with its carrier at its loudest: the weight bounding every term's magnitude.
757fn steady(term: &Body, angle: &Body) -> Body {
758    match term {
759        Body::Apply(Unary::Sin | Unary::Cos, arg) if std::ptr::eq(&*arg.body, angle) => {
760            Body::Const(C64::ONE)
761        }
762        other => map_children(other, |p| Part::new(p.origin, steady(&p.body, angle))),
763    }
764}
765
766/// At least `sup |f(t)|` over every instant; infinite where an atom grows without bound.
767fn sup(f: &SpectralSum) -> f64 {
768    let atoms = f.lanes.iter().flat_map(|lane| lane.atoms.iter());
769    atoms
770        .map(|a| sup_from(a, f64::NEG_INFINITY).unwrap_or(f64::INFINITY))
771        .sum()
772}
773
774/// At most `inf |f(t)|` over every instant: its constant less all that moves it.
775fn least(f: &SpectralSum) -> f64 {
776    let atoms = f.lanes.iter().flat_map(|lane| lane.atoms.iter());
777    let (still, moving): (Vec<_>, Vec<_>) = atoms.partition(|a| a.is_bare());
778    let held = still
779        .iter()
780        .fold(C64::ZERO, |held, a: &&SpectralAtom| held + a.c)
781        .abs();
782    let moves: f64 = moving
783        .iter()
784        .map(|a| sup_from(a, f64::NEG_INFINITY).unwrap_or(f64::INFINITY))
785        .sum();
786    (held - moves).max(0.0)
787}
788
789struct Unread;
790
791impl Env for Unread {
792    fn node(&self, _: NodeId) -> Ty {
793        Ty::form(Var::T, false, Codomain::Complex)
794    }
795
796    fn param(&self, _: ParamId) -> Ty {
797        Ty::form(Var::T, false, Codomain::Complex)
798    }
799}