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, ConstantAt, Reading, affine_in, affine_read, axis_read};
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(crate::exp(slope.exact()?.re))
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 terms: Vec<(ConstantAt, ConstantAt)> = voices
318        .iter()
319        .map(|(place, weight)| {
320            let at = |f: &Body| ConstantAt::of(f, s.index, reads);
321            (at(place), at(weight))
322        })
323        .collect();
324    let unread = |f: &ConstantAt| f.at(s.lo as f64).is_none();
325    if truncates
326        && terms
327            .iter()
328            .any(|(place, weight)| unread(place) || unread(weight))
329    {
330        return Some(Lines {
331            taken: Vec::new(),
332            dropped: Vec::new(),
333            tail_db: f64::NEG_INFINITY,
334        });
335    }
336    if truncates && ratio.is_none() && decays.is_none() {
337        return None;
338    }
339    let floor = libm::pow(10f64, floor_db / 20.0);
340    let ladders: Vec<Option<(f64, f64)>> = voices
341        .iter()
342        .map(|(place, _)| ladder(place, s.index, reads))
343        .collect();
344
345    let mut taken = Vec::new();
346    let mut dropped = Vec::new();
347    let mut peak = 0.0f64;
348    let mut left = None;
349    for k in s.lo..=hi {
350        let mut here = Vec::new();
351        for ((place, weight), ladder) in terms.iter().zip(&ladders) {
352            let (Some(hz), Some(amp)) = (place.at(k as f64).map(|c| c.re), weight.at(k as f64))
353            else {
354                continue;
355            };
356            let rung = ladder.map(|(offset, step)| Rung { offset, step, k });
357            here.push(Line { hz, amp, rung });
358        }
359        let whole = here.len() == voices.len();
360        let tail = match (ratio, &decays) {
361            (Some(r), _) => {
362                whole.then(|| here.iter().map(|l| l.amp.abs()).sum::<f64>() / (1.0 - r))
363            }
364            (None, Some(decays)) if whole => here
365                .iter()
366                .zip(decays)
367                .try_fold(0.0, |held, (l, d)| Some(held + d.tail(l.amp.abs(), k)?)),
368            (None, _) => None,
369        };
370        let gone = tail.filter(|tail| match ratio {
371            Some(_) => *tail <= precision,
372            None => peak > 0.0 && *tail < peak * floor,
373        });
374        if truncates && k > s.lo && gone.is_some() {
375            dropped.extend(here);
376            left = gone;
377            break;
378        }
379        for line in here {
380            if line.hz.abs() < ceiling {
381                peak = peak.max(line.amp.abs());
382                taken.push(line);
383            } else {
384                dropped.push(line);
385            }
386        }
387    }
388    if truncates && left.is_none() {
389        return None;
390    }
391    let loudest = |set: &[Line]| set.iter().map(|l| l.amp.abs()).fold(0.0f64, f64::max);
392    let gone = loudest(&dropped).max(left.unwrap_or(0.0));
393    Some(Lines {
394        tail_db: if gone > 0.0 && peak > 0.0 {
395            20.0 * libm::log10(gone / peak)
396        } else {
397            f64::NEG_INFINITY
398        },
399        taken,
400        dropped,
401    })
402}
403
404fn ladder(place: &Body, k: IndexId, reads: &dyn Reads) -> Option<(f64, f64)> {
405    let (slope, offset) = affine_read(place, Reading::Index(k), reads)?;
406    let (slope, offset) = (slope.exact()?, offset.exact()?);
407    let real = slope.im == 0.0 && offset.im == 0.0;
408    (real && slope.re.is_finite() && offset.re.is_finite()).then_some((offset.re, slope.re))
409}
410
411#[derive(Clone, Debug, PartialEq)]
412pub struct Enumerated {
413    pub atoms: Vec<SpectralAtom>,
414    pub dropped: Vec<Line>,
415}
416
417/// A crop of a series is the series of cropped terms: the window lifts off, goes back on
418/// each. `None` where no line closed form reads under it. A delta's `hz` is an instant, not a pitch.
419pub fn line_atoms(s: &Series, ceiling: f64, floor_db: f64, precision: f64) -> Option<Enumerated> {
420    line_atoms_read(s, (ceiling, floor_db, precision), &Opaque)
421}
422
423pub fn line_atoms_read(s: &Series, band: (f64, f64, f64), reads: &dyn Reads) -> Option<Enumerated> {
424    let (body, window) =
425        crate::spectral_sum::image::crop_peeled(&crate::through::looked(&s.term.body, reads));
426    let bare = Series {
427        term: Part::new(s.term.origin, body),
428        ..s.clone()
429    };
430    let singular = match read_with(&bare.term.body, reads)? {
431        Shape::Deltas(_) => true,
432        Shape::Lines(_) => false,
433    };
434    let found = lines_read(&bare, band, reads)?;
435    let atoms = found
436        .taken
437        .into_iter()
438        .filter_map(|l| match singular {
439            true => window.is_none_or(|w| w.contains(l.hz)).then(|| {
440                SpectralAtom::new(
441                    l.amp,
442                    Factors::NONE,
443                    Singular::Delta { at: l.hz, order: 0 },
444                    s.term.origin,
445                )
446            }),
447            false => Some(SpectralAtom::new(
448                l.amp,
449                Factors {
450                    exp: Some(Exp::at(0.0, TAU * l.hz)),
451                    ind: window,
452                    ..Factors::NONE
453                },
454                Singular::Regular,
455                s.term.origin,
456            )),
457        })
458        .collect();
459    Some(Enumerated {
460        atoms,
461        dropped: found.dropped,
462    })
463}
464
465fn places(shape: &Shape) -> Vec<(Body, Body)> {
466    match shape {
467        Shape::Lines(lines) => lines
468            .iter()
469            .map(|l| (l.freq.clone(), l.amp.clone()))
470            .collect(),
471        Shape::Deltas(deltas) => deltas
472            .iter()
473            .map(|d| (d.at.clone(), d.weight.clone()))
474            .collect(),
475    }
476}
477
478/// The last index the walk reaches, solving `|slope*k + offset| <= ceiling`
479/// at both signs: an offset opposing the slope carries the line back in before it leaves.
480fn leaves_band(place: &Body, k: IndexId, ceiling: f64, reads: &dyn Reads) -> Option<i64> {
481    let (slope, offset) = affine_read(place, Reading::Index(k), reads)?;
482    let (slope, offset) = (slope.exact()?.re, offset.exact()?.re);
483    if slope == 0.0 {
484        return None;
485    }
486    let ends = [(ceiling - offset) / slope, (-ceiling - offset) / slope];
487    let last = ends[0].max(ends[1]).floor();
488    Some(last.clamp(0.0, MAX_TERMS as f64) as i64 + 1)
489}
490
491const MAX_TERMS: i64 = 1 << 20;
492
493pub fn substitute(f: &Body, k: IndexId, value: f64) -> Body {
494    match f {
495        Body::Index(i) if *i == k => Body::Const(C64::real(value)),
496        other => map_children(other, |p| {
497            Part::new(p.origin, substitute(&p.body, k, value))
498        }),
499    }
500}
501
502const MAX_WRITTEN_TERMS: i64 = 1 << 13;
503
504/// `f`, each finite series summed term by term; `None` where one is infinite or too long.
505pub fn written_out(f: &Body) -> Option<Body> {
506    let mut left = MAX_WRITTEN_TERMS;
507    within(f, &mut left)
508}
509
510/// `left` counts the terms every series written out so far may still add, nesting included.
511fn within(f: &Body, left: &mut i64) -> Option<Body> {
512    if let Body::Series(s) = f {
513        let Bound::Finite(hi) = s.hi else {
514            return None;
515        };
516        *left = left.checked_sub(hi.checked_sub(s.lo)?.checked_add(1)?.max(0))?;
517        if *left < 0 {
518            return None;
519        }
520        let terms = (s.lo..=hi)
521            .map(|k| {
522                Some(Part::new(
523                    s.term.origin,
524                    within(&substitute(&s.term.body, s.index, k as f64), left)?,
525                ))
526            })
527            .collect::<Option<Vec<_>>>()?;
528        return Some(match terms.len() {
529            0 => Body::Const(C64::ZERO),
530            _ => Body::Add(terms),
531        });
532    }
533    let mut whole = true;
534    let out = map_children(f, |p| match within(&p.body, left) {
535        Some(body) => Part::new(p.origin, body),
536        None => {
537            whole = false;
538            p.clone()
539        }
540    });
541    whole.then_some(out)
542}