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::spectral_sum::atom::{Exp, Factors, Singular, SpectralAtom};
12use crate::table::series::{Shape, read_with};
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, false)
285}
286
287pub fn leading_read(s: &Series, band: (f64, f64, f64), reads: &dyn Reads) -> Option<Vec<Line>> {
289 let first = walk(s, band, reads, true)?.taken;
290 (!first.is_empty()).then_some(first)
291}
292
293fn walk(
294 s: &Series,
295 (ceiling, floor_db, precision): (f64, f64, f64),
296 reads: &dyn Reads,
297 leading: bool,
298) -> Option<Lines> {
299 let shape = read_with(&s.term.body, reads)?;
300 let voices = places(&shape);
301 let band = voices
302 .iter()
303 .filter_map(|(place, _)| leaves_band(place, s.index, ceiling, reads))
304 .fold(None, |held: Option<i64>, next| {
305 Some(held.map_or(next, |held| held.max(next)))
306 });
307 let hi = match (s.hi, band) {
308 (Bound::Finite(n), Some(last)) => n.min(last),
309 (Bound::Finite(n), None) => n,
310 (Bound::Infinite, Some(last)) => last,
311 (Bound::Infinite, None) => s.lo.saturating_add(MAX_TERMS),
312 };
313 let ratio = voices
314 .iter()
315 .try_fold(0.0f64, |held, (_, weight)| {
316 Some(held.max(ratio(weight, s.index)?))
317 })
318 .filter(|r| *r < 1.0);
319 let decays: Option<Vec<Decay>> = voices
320 .iter()
321 .map(|(_, weight)| decay(weight, s.index))
322 .collect();
323 let truncates = band.is_none() && s.hi == Bound::Infinite;
324 if leading && truncates {
325 return None;
326 }
327 let unread = |f: &Body| at_index(f, s.index, s.lo, reads).is_none();
328 if truncates
329 && voices
330 .iter()
331 .any(|(place, weight)| unread(place) || unread(weight))
332 {
333 return Some(Lines {
334 taken: Vec::new(),
335 dropped: Vec::new(),
336 tail_db: f64::NEG_INFINITY,
337 });
338 }
339 if truncates && ratio.is_none() && decays.is_none() {
340 return None;
341 }
342 let floor = 10f64.powf(floor_db / 20.0);
343 let ladders: Vec<Option<(f64, f64)>> = voices
344 .iter()
345 .map(|(place, _)| ladder(place, s.index, reads))
346 .collect();
347
348 let mut taken = Vec::new();
349 let mut dropped = Vec::new();
350 let mut peak = 0.0f64;
351 let mut left = None;
352 let last = if leading { hi.min(s.lo) } else { hi };
353 for k in s.lo..=last {
354 let mut here = Vec::new();
355 for ((place, weight), ladder) in voices.iter().zip(&ladders) {
356 let (Some(hz), Some(amp)) = (
357 at_index(place, s.index, k, reads).map(|c| c.re),
358 at_index(weight, s.index, k, reads),
359 ) else {
360 continue;
361 };
362 let rung = ladder.map(|(offset, step)| Rung { offset, step, k });
363 here.push(Line { hz, amp, rung });
364 }
365 let whole = here.len() == voices.len();
366 let tail = match (ratio, &decays) {
367 (Some(r), _) => {
368 whole.then(|| here.iter().map(|l| l.amp.abs()).sum::<f64>() / (1.0 - r))
369 }
370 (None, Some(decays)) if whole => here
371 .iter()
372 .zip(decays)
373 .try_fold(0.0, |held, (l, d)| Some(held + d.tail(l.amp.abs(), k)?)),
374 (None, _) => None,
375 };
376 let gone = tail.filter(|tail| match ratio {
377 Some(_) => *tail <= precision,
378 None => peak > 0.0 && *tail < peak * floor,
379 });
380 if truncates && k > s.lo && gone.is_some() {
381 dropped.extend(here);
382 left = gone;
383 break;
384 }
385 for line in here {
386 if line.hz.abs() < ceiling {
387 peak = peak.max(line.amp.abs());
388 taken.push(line);
389 } else {
390 dropped.push(line);
391 }
392 }
393 }
394 if truncates && left.is_none() {
395 return None;
396 }
397 let loudest = |set: &[Line]| set.iter().map(|l| l.amp.abs()).fold(0.0f64, f64::max);
398 let gone = loudest(&dropped).max(left.unwrap_or(0.0));
399 Some(Lines {
400 tail_db: if gone > 0.0 && peak > 0.0 {
401 20.0 * (gone / peak).log10()
402 } else {
403 f64::NEG_INFINITY
404 },
405 taken,
406 dropped,
407 })
408}
409
410fn ladder(place: &Body, k: IndexId, reads: &dyn Reads) -> Option<(f64, f64)> {
411 let (slope, offset) = affine_read(place, Reading::Index(k), reads)?;
412 let (slope, offset) = (slope.exact()?, offset.exact()?);
413 let real = slope.im == 0.0 && offset.im == 0.0;
414 (real && slope.re.is_finite() && offset.re.is_finite()).then_some((offset.re, slope.re))
415}
416
417pub fn spacing(s: &Series) -> Option<f64> {
420 spacing_read(s, &Opaque)
421}
422
423pub fn spacing_read(s: &Series, reads: &dyn Reads) -> Option<f64> {
424 let Some(shape @ Shape::Lines(_)) = read_with(&s.term.body, reads) else {
425 return None;
426 };
427 let mut held: Option<f64> = None;
428 for (place, _) in places(&shape) {
429 let (slope, offset) = affine_read(&place, Reading::Index(s.index), reads)?;
430 let (slope, offset) = (slope.exact()?.re, offset.exact()?.re);
431 if slope == 0.0 || !slope.is_finite() || !offset.is_finite() {
432 return None;
433 }
434 let steps = offset / slope;
435 if (steps.round() - steps).abs() > TURN_EPSILON * steps.abs().max(1.0) {
436 return None;
437 }
438 match held {
439 Some(step) if step != slope.abs() => return None,
440 _ => held = Some(slope.abs()),
441 }
442 }
443 held
444}
445
446#[derive(Clone, Debug, PartialEq)]
447pub struct Enumerated {
448 pub atoms: Vec<SpectralAtom>,
449 pub dropped: Vec<Line>,
450}
451
452pub fn line_atoms(s: &Series, ceiling: f64, floor_db: f64, precision: f64) -> Option<Enumerated> {
455 line_atoms_read(s, (ceiling, floor_db, precision), &Opaque)
456}
457
458pub fn line_atoms_read(s: &Series, band: (f64, f64, f64), reads: &dyn Reads) -> Option<Enumerated> {
459 let (body, window) =
460 crate::spectral_sum::image::crop_peeled(&crate::through::looked(&s.term.body, reads));
461 let bare = Series {
462 term: Part::new(s.term.origin, body),
463 ..s.clone()
464 };
465 let singular = match read_with(&bare.term.body, reads)? {
466 Shape::Deltas(_) => true,
467 Shape::Lines(_) => false,
468 };
469 let found = lines_read(&bare, band, reads)?;
470 let atoms = found
471 .taken
472 .into_iter()
473 .filter_map(|l| match singular {
474 true => window.is_none_or(|w| w.contains(l.hz)).then(|| {
475 SpectralAtom::new(
476 l.amp,
477 Factors::NONE,
478 Singular::Delta { at: l.hz, order: 0 },
479 s.term.origin,
480 )
481 }),
482 false => Some(SpectralAtom::new(
483 l.amp,
484 Factors {
485 exp: Some(Exp::at(0.0, TAU * l.hz)),
486 ind: window,
487 ..Factors::NONE
488 },
489 Singular::Regular,
490 s.term.origin,
491 )),
492 })
493 .collect();
494 Some(Enumerated {
495 atoms,
496 dropped: found.dropped,
497 })
498}
499
500pub fn commensurate(hz: f64, horizon: f64) -> bool {
503 let turns = hz * horizon;
504 (turns.round() - turns).abs() <= TURN_EPSILON * turns.abs().max(1.0)
505}
506
507const TURN_EPSILON: f64 = 1e-9;
508
509fn places(shape: &Shape) -> Vec<(Body, Body)> {
510 match shape {
511 Shape::Lines(lines) => lines
512 .iter()
513 .map(|l| (l.freq.clone(), l.amp.clone()))
514 .collect(),
515 Shape::Deltas(deltas) => deltas
516 .iter()
517 .map(|d| (d.at.clone(), d.weight.clone()))
518 .collect(),
519 }
520}
521
522fn leaves_band(place: &Body, k: IndexId, ceiling: f64, reads: &dyn Reads) -> Option<i64> {
525 let (slope, offset) = affine_read(place, Reading::Index(k), reads)?;
526 let (slope, offset) = (slope.exact()?.re, offset.exact()?.re);
527 if slope == 0.0 {
528 return None;
529 }
530 let ends = [(ceiling - offset) / slope, (-ceiling - offset) / slope];
531 let last = ends[0].max(ends[1]).floor();
532 Some(last.clamp(0.0, MAX_TERMS as f64) as i64 + 1)
533}
534
535const MAX_TERMS: i64 = 1 << 20;
536
537fn at_index(f: &Body, k: IndexId, value: i64, reads: &dyn Reads) -> Option<C64> {
538 exact_constant_at(f, k, value as f64, reads)
539}
540
541pub fn substitute(f: &Body, k: IndexId, value: f64) -> Body {
542 match f {
543 Body::Index(i) if *i == k => Body::Const(C64::real(value)),
544 other => map_children(other, |p| {
545 Part::new(p.origin, substitute(&p.body, k, value))
546 }),
547 }
548}
549
550const MAX_WRITTEN_TERMS: i64 = 1 << 13;
551
552pub fn written_out(f: &Body) -> Option<Body> {
554 let mut left = MAX_WRITTEN_TERMS;
555 within(f, &mut left)
556}
557
558fn within(f: &Body, left: &mut i64) -> Option<Body> {
560 if let Body::Series(s) = f {
561 let Bound::Finite(hi) = s.hi else {
562 return None;
563 };
564 *left = left.checked_sub(hi.checked_sub(s.lo)?.checked_add(1)?.max(0))?;
565 if *left < 0 {
566 return None;
567 }
568 let terms = (s.lo..=hi)
569 .map(|k| {
570 Some(Part::new(
571 s.term.origin,
572 within(&substitute(&s.term.body, s.index, k as f64), left)?,
573 ))
574 })
575 .collect::<Option<Vec<_>>>()?;
576 return Some(match terms.len() {
577 0 => Body::Const(C64::ZERO),
578 _ => Body::Add(terms),
579 });
580 }
581 let mut whole = true;
582 let out = map_children(f, |p| match within(&p.body, left) {
583 Some(body) => Part::new(p.origin, body),
584 None => {
585 whole = false;
586 p.clone()
587 }
588 });
589 whole.then_some(out)
590}