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