1use 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#[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(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
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 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
417pub 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
478fn 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
504pub fn written_out(f: &Body) -> Option<Body> {
506 let mut left = MAX_WRITTEN_TERMS;
507 within(f, &mut left)
508}
509
510fn 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}