1use 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#[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
29pub 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
68fn 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
88fn 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
108pub 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
123pub 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
147pub fn falls(f: &Body, k: IndexId) -> bool {
149 power(f, k).is_some_and(|p| p >= 0.0)
150}
151
152#[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 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
176fn 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
198fn 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
215fn 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 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#[derive(Clone, Copy, Debug, PartialEq)]
260pub struct Rung {
261 pub offset: f64,
262 pub step: f64,
263 pub k: i64,
264}
265
266#[derive(Clone, Debug, PartialEq)]
269pub struct Lines {
270 pub taken: Vec<Line>,
271 pub dropped: Vec<Line>,
272 pub tail_db: f64,
273}
274
275pub 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
412pub 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
473fn 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
503pub fn written_out(f: &Body) -> Option<Body> {
505 let mut left = MAX_WRITTEN_TERMS;
506 within(f, &mut left)
507}
508
509fn 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}