Skip to main content

sva_formula/
series.rs

1// Concern: decides a series' convergence in A and enumerates the lines it yields | Non-concern: placing them on a grid (sva-samples) | IO: (&Series, ceiling) -> bool, Lines, Enumerated
2
3use std::f64::consts::TAU;
4
5use crate::affine::{Axis, Reading, affine_in, affine_read, axis_read, exact_constant_at};
6use crate::closed_form::{
7    Body, Bound, IndexId, Part, Series, Unary, children, map_children, read_at,
8};
9use crate::complex::C64;
10use crate::env::Env;
11use crate::fourier_dual::series::{Shape, read_with};
12use crate::spectral_sum::atom::{Exp, Factors, Singular, SpectralAtom};
13use crate::through::{Opaque, Reads};
14
15/// A tempered limit needs the coefficient polynomially bounded: `1/k` is, `2^k` is not.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum IndexGrowth {
18    Polynomial,
19    Unbounded,
20}
21
22pub fn summable(s: &Series, env: &dyn Env) -> bool {
23    match s.hi {
24        Bound::Finite(_) => true,
25        Bound::Infinite => growth(&s.term.body, s.index, env) == IndexGrowth::Polynomial,
26    }
27}
28
29/// Decided from where the index sits, never by evaluating a term; a ref read at a time the
30/// index moves is its form read there.
31pub fn growth(f: &Body, k: IndexId, env: &dyn Env) -> IndexGrowth {
32    if !mentions(f, k) {
33        return IndexGrowth::Polynomial;
34    }
35    match f {
36        Body::Index(_) | Body::Line | Body::Const(_) => IndexGrowth::Polynomial,
37        Body::Add(parts) | Body::Mul(parts) | Body::Join(parts) => join(parts, k, env),
38        Body::Div(a, b) => {
39            join(std::slice::from_ref(a), k, env).and(join(std::slice::from_ref(b), k, env))
40        }
41        Body::Pow(base, _) => growth(&base.body, k, env),
42        Body::Keyed { .. } => IndexGrowth::Polynomial,
43        Body::Apply(Unary::Sin | Unary::Cos, arg) => bounded_along(arg, k, Axis::Real, env),
44        Body::Apply(Unary::Exp, arg) if logarithmic(&arg.body, k) => IndexGrowth::Polynomial,
45        Body::Apply(Unary::Exp, arg) => exponential_in(arg, k, env),
46        Body::Channel(of, _) | Body::Crop { of, .. } => growth(&of.body, k, env),
47        Body::Shift { of, .. } | Body::Deriv { of, .. } => growth(&of.body, k, env),
48        Body::Warp { at, of } => match &*of.body {
49            Body::Crop { of: inner, .. } => growth(&read_at(&inner.body, &at.body), k, env),
50            Body::Node(id) => env
51                .reads()
52                .growth(*id, &at.body, k)
53                .unwrap_or(IndexGrowth::Polynomial),
54            other => growth(&read_at(other, &at.body), k, env),
55        },
56        Body::Pv(at) | Body::Delta { at, .. } => growth(&at.body, k, env),
57        Body::Series(inner) => growth(&inner.term.body, k, env),
58        _ => IndexGrowth::Unbounded,
59    }
60}
61
62fn join(parts: &[Part], k: IndexId, env: &dyn Env) -> IndexGrowth {
63    parts.iter().fold(IndexGrowth::Polynomial, |acc, p| {
64        acc.and(growth(&p.body, k, env))
65    })
66}
67
68/// A turning exponent is bounded and a falling one is a geometric decay; only a rising real
69/// part outgrows every polynomial.
70fn exponential_in(arg: &Part, k: IndexId, env: &dyn Env) -> IndexGrowth {
71    if let bounded @ IndexGrowth::Polynomial = bounded_along(arg, k, Axis::Imaginary, env) {
72        return bounded;
73    }
74    match affine_in(&arg.body, Reading::Index(k)).and_then(|(slope, _)| slope.exact()) {
75        Some(slope) if slope.re < 0.0 => IndexGrowth::Polynomial,
76        _ => IndexGrowth::Unbounded,
77    }
78}
79
80fn bounded_along(arg: &Part, k: IndexId, wanted: Axis, env: &dyn Env) -> IndexGrowth {
81    if !mentions(&arg.body, k) || axis_read(&arg.body, env, env.reads()) == wanted {
82        IndexGrowth::Polynomial
83    } else {
84        IndexGrowth::Unbounded
85    }
86}
87
88/// An exponent reaching the index only through a logarithm is a power of it.
89fn logarithmic(f: &Body, k: IndexId) -> bool {
90    match f {
91        _ if !mentions(f, k) => true,
92        Body::Apply(Unary::Log, _) => true,
93        Body::Add(parts) | Body::Mul(parts) => parts.iter().all(|p| logarithmic(&p.body, k)),
94        Body::Div(a, b) => logarithmic(&a.body, k) && logarithmic(&b.body, k),
95        _ => false,
96    }
97}
98
99impl IndexGrowth {
100    fn and(self, other: IndexGrowth) -> IndexGrowth {
101        match (self, other) {
102            (IndexGrowth::Polynomial, IndexGrowth::Polynomial) => IndexGrowth::Polynomial,
103            _ => IndexGrowth::Unbounded,
104        }
105    }
106}
107
108/// What separates a series term's coefficient from its wave.
109pub fn mentions_line(f: &Body) -> bool {
110    mentions_line_read(f, &Opaque)
111}
112
113pub fn mentions_line_read(f: &Body, reads: &dyn Reads) -> bool {
114    match f {
115        Body::Line => true,
116        Body::Node(id) => reads.mentions_line(*id),
117        other => children(other)
118            .iter()
119            .any(|p| mentions_line_read(&p.body, reads)),
120    }
121}
122
123/// A bound on `|c(k+1)| / |c(k)|` holding at every index.
124pub fn ratio(f: &Body, k: IndexId) -> Option<f64> {
125    if !mentions(f, k) {
126        return Some(1.0);
127    }
128    match f {
129        Body::Mul(parts) => parts
130            .iter()
131            .try_fold(1.0, |held, p| Some(held * ratio(&p.body, k)?)),
132        Body::Add(parts) => parts.iter().try_fold(0.0f64, |held, p| match &*p.body {
133            Body::Apply(Unary::Abs, _) => Some(held.max(ratio(&p.body, k)?)),
134            _ => None,
135        }),
136        Body::Div(num, den) if !mentions(&den.body, k) => ratio(&num.body, k),
137        Body::Apply(Unary::Abs, arg) => ratio(&arg.body, k),
138        Body::Apply(Unary::Exp, arg) => {
139            let (slope, _) = affine_in(&arg.body, Reading::Index(k))?;
140            Some(slope.exact()?.re.exp())
141        }
142        Body::Pow(base, n) if *n >= 0 => Some(ratio(&base.body, k)?.powi(*n)),
143        _ => None,
144    }
145}
146
147/// Whether `|f(j)| <= |f(i)|` for every `j >= i >= 1`.
148pub fn falls(f: &Body, k: IndexId) -> bool {
149    power(f, k).is_some_and(|p| p >= 0.0)
150}
151
152/// Bounds every term from an index on by that term's magnitude alone.
153#[derive(Clone, Copy, Debug, PartialEq)]
154enum Decay {
155    Geometric(f64),
156    Power(f64),
157}
158
159fn decay(f: &Body, k: IndexId) -> Option<Decay> {
160    if let Some(r) = ratio(f, k).filter(|r| *r < 1.0) {
161        return Some(Decay::Geometric(r));
162    }
163    power(f, k).filter(|p| *p > 1.0).map(Decay::Power)
164}
165
166impl Decay {
167    /// `sum_{j>=k} |c(j)|` from `|c(k)|`; a power law by `sum_{j>k} (k/j)^p <= k/(p-1)`.
168    fn tail(self, here: f64, k: i64) -> Option<f64> {
169        match self {
170            Decay::Geometric(r) => Some(here / (1.0 - r)),
171            Decay::Power(p) => (k >= 1).then(|| here * (1.0 + k as f64 / (p - 1.0))),
172        }
173    }
174}
175
176/// A `p` with `|f(j)| <= |f(i)|*(i/j)^p` for every `j >= i >= 1`.
177fn power(f: &Body, k: IndexId) -> Option<f64> {
178    if let Some(e) = exponent(f, k) {
179        return Some(-e);
180    }
181    match f {
182        Body::Mul(parts) => parts
183            .iter()
184            .try_fold(0.0, |held, p| Some(held + power(&p.body, k)?)),
185        Body::Add(parts) => parts
186            .iter()
187            .try_fold(f64::INFINITY, |held, p| match &*p.body {
188                Body::Apply(Unary::Abs, _) => Some(held.min(power(&p.body, k)?)),
189                _ => None,
190            }),
191        Body::Div(num, den) => Some(power(&num.body, k)? + rises(&den.body, k)?),
192        Body::Apply(Unary::Abs, arg) => power(&arg.body, k),
193        Body::Pow(base, n) if *n >= 0 => Some(power(&base.body, k)? * f64::from(*n)),
194        _ => ratio(f, k).filter(|r| *r <= 1.0).map(|_| 0.0),
195    }
196}
197
198/// An `e` with `|f(j)| >= |f(i)|*(j/i)^e` for every `j >= i >= 1`: `a*k + b`, `a > 0`,
199/// `b <= 0 < a + b`, rises at least as `k` does.
200fn rises(f: &Body, k: IndexId) -> Option<f64> {
201    if let Some(e) = exponent(f, k) {
202        return Some(e);
203    }
204    match f {
205        Body::Pow(base, n) if *n >= 0 => Some(rises(&base.body, k)? * f64::from(*n)),
206        _ => {
207            let (slope, offset) = affine_in(f, Reading::Index(k))?;
208            let (a, b) = (slope.exact()?, offset.exact()?);
209            let real = a.im == 0.0 && b.im == 0.0;
210            (real && a.re > 0.0 && b.re <= 0.0 && a.re + b.re > 0.0).then_some(1.0)
211        }
212    }
213}
214
215/// The `e` with `|f(j)| = |f(i)|*(j/i)^e` exactly for every `i, j >= 1`.
216fn exponent(f: &Body, k: IndexId) -> Option<f64> {
217    if !mentions(f, k) {
218        return Some(0.0);
219    }
220    match f {
221        Body::Index(i) if *i == k => Some(1.0),
222        Body::Mul(parts) => parts
223            .iter()
224            .try_fold(0.0, |held, p| Some(held + exponent(&p.body, k)?)),
225        Body::Div(num, den) => Some(exponent(&num.body, k)? - exponent(&den.body, k)?),
226        Body::Apply(Unary::Abs, arg) => exponent(&arg.body, k),
227        Body::Pow(base, n) => Some(exponent(&base.body, k)? * f64::from(*n)),
228        _ => None,
229    }
230}
231
232pub fn mentions(f: &Body, k: IndexId) -> bool {
233    reaches(f, &|x| matches!(x, Body::Index(i) if *i == k))
234}
235
236fn reaches(f: &Body, leaf: &dyn Fn(&Body) -> bool) -> bool {
237    leaf(f) || children(f).iter().any(|p| reaches(&p.body, leaf))
238}
239
240#[derive(Clone, Copy, Debug, PartialEq)]
241pub struct Line {
242    pub hz: f64,
243    pub amp: C64,
244    /// Where the term's own frequency places it, which `hz` rounds.
245    pub rung: Option<Rung>,
246}
247
248impl Line {
249    pub fn bare(hz: f64, amp: C64) -> Line {
250        Line {
251            hz,
252            amp,
253            rung: None,
254        }
255    }
256}
257
258/// Exactly `offset + step*k` Hz: the frequency a series term writes at index `k`.
259#[derive(Clone, Copy, Debug, PartialEq)]
260pub struct Rung {
261    pub offset: f64,
262    pub step: f64,
263    pub k: i64,
264}
265
266/// `tail_db` is what was dropped against the loudest taken line: the loudest dropped line, or
267/// where the walk stopped short, the bound on the summed magnitude of every term it left.
268#[derive(Clone, Debug, PartialEq)]
269pub struct Lines {
270    pub taken: Vec<Line>,
271    pub dropped: Vec<Line>,
272    pub tail_db: f64,
273}
274
275/// Stops at the ceiling where the frequency closed form leaves the band. Where it never does, the
276/// walk stops only where a decay bounds the whole tail: a geometric one at `precision`, a power
277/// law under the floor against the loudest line taken. `None` where the term is no line, or an
278/// infinite tail has no such bound.
279pub fn lines(s: &Series, ceiling: f64, floor_db: f64, precision: f64) -> Option<Lines> {
280    lines_read(s, (ceiling, floor_db, precision), &Opaque)
281}
282
283pub fn lines_read(s: &Series, band: (f64, f64, f64), reads: &dyn Reads) -> Option<Lines> {
284    walk(s, band, reads)
285}
286
287fn walk(
288    s: &Series,
289    (ceiling, floor_db, precision): (f64, f64, f64),
290    reads: &dyn Reads,
291) -> Option<Lines> {
292    let shape = read_with(&s.term.body, reads)?;
293    let voices = places(&shape);
294    let band = voices
295        .iter()
296        .filter_map(|(place, _)| leaves_band(place, s.index, ceiling, reads))
297        .fold(None, |held: Option<i64>, next| {
298            Some(held.map_or(next, |held| held.max(next)))
299        });
300    let hi = match (s.hi, band) {
301        (Bound::Finite(n), Some(last)) => n.min(last),
302        (Bound::Finite(n), None) => n,
303        (Bound::Infinite, Some(last)) => last,
304        (Bound::Infinite, None) => s.lo.saturating_add(MAX_TERMS),
305    };
306    let ratio = voices
307        .iter()
308        .try_fold(0.0f64, |held, (_, weight)| {
309            Some(held.max(ratio(weight, s.index)?))
310        })
311        .filter(|r| *r < 1.0);
312    let decays: Option<Vec<Decay>> = voices
313        .iter()
314        .map(|(_, weight)| decay(weight, s.index))
315        .collect();
316    let truncates = band.is_none() && s.hi == Bound::Infinite;
317    let unread = |f: &Body| at_index(f, s.index, s.lo, reads).is_none();
318    if truncates
319        && voices
320            .iter()
321            .any(|(place, weight)| unread(place) || unread(weight))
322    {
323        return Some(Lines {
324            taken: Vec::new(),
325            dropped: Vec::new(),
326            tail_db: f64::NEG_INFINITY,
327        });
328    }
329    if truncates && ratio.is_none() && decays.is_none() {
330        return None;
331    }
332    let floor = 10f64.powf(floor_db / 20.0);
333    let ladders: Vec<Option<(f64, f64)>> = voices
334        .iter()
335        .map(|(place, _)| ladder(place, s.index, reads))
336        .collect();
337
338    let mut taken = Vec::new();
339    let mut dropped = Vec::new();
340    let mut peak = 0.0f64;
341    let mut left = None;
342    for k in s.lo..=hi {
343        let mut here = Vec::new();
344        for ((place, weight), ladder) in voices.iter().zip(&ladders) {
345            let (Some(hz), Some(amp)) = (
346                at_index(place, s.index, k, reads).map(|c| c.re),
347                at_index(weight, s.index, k, reads),
348            ) else {
349                continue;
350            };
351            let rung = ladder.map(|(offset, step)| Rung { offset, step, k });
352            here.push(Line { hz, amp, rung });
353        }
354        let whole = here.len() == voices.len();
355        let tail = match (ratio, &decays) {
356            (Some(r), _) => {
357                whole.then(|| here.iter().map(|l| l.amp.abs()).sum::<f64>() / (1.0 - r))
358            }
359            (None, Some(decays)) if whole => here
360                .iter()
361                .zip(decays)
362                .try_fold(0.0, |held, (l, d)| Some(held + d.tail(l.amp.abs(), k)?)),
363            (None, _) => None,
364        };
365        let gone = tail.filter(|tail| match ratio {
366            Some(_) => *tail <= precision,
367            None => peak > 0.0 && *tail < peak * floor,
368        });
369        if truncates && k > s.lo && gone.is_some() {
370            dropped.extend(here);
371            left = gone;
372            break;
373        }
374        for line in here {
375            if line.hz.abs() < ceiling {
376                peak = peak.max(line.amp.abs());
377                taken.push(line);
378            } else {
379                dropped.push(line);
380            }
381        }
382    }
383    if truncates && left.is_none() {
384        return None;
385    }
386    let loudest = |set: &[Line]| set.iter().map(|l| l.amp.abs()).fold(0.0f64, f64::max);
387    let gone = loudest(&dropped).max(left.unwrap_or(0.0));
388    Some(Lines {
389        tail_db: if gone > 0.0 && peak > 0.0 {
390            20.0 * (gone / peak).log10()
391        } else {
392            f64::NEG_INFINITY
393        },
394        taken,
395        dropped,
396    })
397}
398
399fn ladder(place: &Body, k: IndexId, reads: &dyn Reads) -> Option<(f64, f64)> {
400    let (slope, offset) = affine_read(place, Reading::Index(k), reads)?;
401    let (slope, offset) = (slope.exact()?, offset.exact()?);
402    let real = slope.im == 0.0 && offset.im == 0.0;
403    (real && slope.re.is_finite() && offset.re.is_finite()).then_some((offset.re, slope.re))
404}
405
406#[derive(Clone, Debug, PartialEq)]
407pub struct Enumerated {
408    pub atoms: Vec<SpectralAtom>,
409    pub dropped: Vec<Line>,
410}
411
412/// A crop of a series is the series of cropped terms: the window lifts off, goes back on
413/// each. `None` where no line closed form reads under it. A delta's `hz` is an instant, not a pitch.
414pub fn line_atoms(s: &Series, ceiling: f64, floor_db: f64, precision: f64) -> Option<Enumerated> {
415    line_atoms_read(s, (ceiling, floor_db, precision), &Opaque)
416}
417
418pub fn line_atoms_read(s: &Series, band: (f64, f64, f64), reads: &dyn Reads) -> Option<Enumerated> {
419    let (body, window) =
420        crate::spectral_sum::image::crop_peeled(&crate::through::looked(&s.term.body, reads));
421    let bare = Series {
422        term: Part::new(s.term.origin, body),
423        ..s.clone()
424    };
425    let singular = match read_with(&bare.term.body, reads)? {
426        Shape::Deltas(_) => true,
427        Shape::Lines(_) => false,
428    };
429    let found = lines_read(&bare, band, reads)?;
430    let atoms = found
431        .taken
432        .into_iter()
433        .filter_map(|l| match singular {
434            true => window.is_none_or(|w| w.contains(l.hz)).then(|| {
435                SpectralAtom::new(
436                    l.amp,
437                    Factors::NONE,
438                    Singular::Delta { at: l.hz, order: 0 },
439                    s.term.origin,
440                )
441            }),
442            false => Some(SpectralAtom::new(
443                l.amp,
444                Factors {
445                    exp: Some(Exp::at(0.0, TAU * l.hz)),
446                    ind: window,
447                    ..Factors::NONE
448                },
449                Singular::Regular,
450                s.term.origin,
451            )),
452        })
453        .collect();
454    Some(Enumerated {
455        atoms,
456        dropped: found.dropped,
457    })
458}
459
460fn places(shape: &Shape) -> Vec<(Body, Body)> {
461    match shape {
462        Shape::Lines(lines) => lines
463            .iter()
464            .map(|l| (l.freq.clone(), l.amp.clone()))
465            .collect(),
466        Shape::Deltas(deltas) => deltas
467            .iter()
468            .map(|d| (d.at.clone(), d.weight.clone()))
469            .collect(),
470    }
471}
472
473/// The last index the walk reaches, solving `|slope*k + offset| <= ceiling`
474/// at both signs: an offset opposing the slope carries the line back in before it leaves.
475fn leaves_band(place: &Body, k: IndexId, ceiling: f64, reads: &dyn Reads) -> Option<i64> {
476    let (slope, offset) = affine_read(place, Reading::Index(k), reads)?;
477    let (slope, offset) = (slope.exact()?.re, offset.exact()?.re);
478    if slope == 0.0 {
479        return None;
480    }
481    let ends = [(ceiling - offset) / slope, (-ceiling - offset) / slope];
482    let last = ends[0].max(ends[1]).floor();
483    Some(last.clamp(0.0, MAX_TERMS as f64) as i64 + 1)
484}
485
486const MAX_TERMS: i64 = 1 << 20;
487
488fn at_index(f: &Body, k: IndexId, value: i64, reads: &dyn Reads) -> Option<C64> {
489    exact_constant_at(f, k, value as f64, reads)
490}
491
492pub fn substitute(f: &Body, k: IndexId, value: f64) -> Body {
493    match f {
494        Body::Index(i) if *i == k => Body::Const(C64::real(value)),
495        other => map_children(other, |p| {
496            Part::new(p.origin, substitute(&p.body, k, value))
497        }),
498    }
499}
500
501const MAX_WRITTEN_TERMS: i64 = 1 << 13;
502
503/// `f`, each finite series summed term by term; `None` where one is infinite or too long.
504pub fn written_out(f: &Body) -> Option<Body> {
505    let mut left = MAX_WRITTEN_TERMS;
506    within(f, &mut left)
507}
508
509/// `left` counts the terms every series written out so far may still add, nesting included.
510fn within(f: &Body, left: &mut i64) -> Option<Body> {
511    if let Body::Series(s) = f {
512        let Bound::Finite(hi) = s.hi else {
513            return None;
514        };
515        *left = left.checked_sub(hi.checked_sub(s.lo)?.checked_add(1)?.max(0))?;
516        if *left < 0 {
517            return None;
518        }
519        let terms = (s.lo..=hi)
520            .map(|k| {
521                Some(Part::new(
522                    s.term.origin,
523                    within(&substitute(&s.term.body, s.index, k as f64), left)?,
524                ))
525            })
526            .collect::<Option<Vec<_>>>()?;
527        return Some(match terms.len() {
528            0 => Body::Const(C64::ZERO),
529            _ => Body::Add(terms),
530        });
531    }
532    let mut whole = true;
533    let out = map_children(f, |p| match within(&p.body, left) {
534        Some(body) => Part::new(p.origin, body),
535        None => {
536            whole = false;
537            p.clone()
538        }
539    });
540    whole.then_some(out)
541}