1use std::f64::consts::TAU;
4
5use crate::affine::{Axis, Reading, affine_in, axis, exact_constant};
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};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum IndexGrowth {
17 Polynomial,
18 Unbounded,
19}
20
21pub fn summable(s: &Series, env: &dyn Env) -> bool {
22 match s.hi {
23 Bound::Finite(_) => true,
24 Bound::Infinite => growth(&s.term.body, s.index, env) == IndexGrowth::Polynomial,
25 }
26}
27
28pub fn growth(f: &Body, k: IndexId, env: &dyn Env) -> IndexGrowth {
30 if !mentions(f, k) {
31 return IndexGrowth::Polynomial;
32 }
33 match f {
34 Body::Index(_) | Body::Line | Body::Const(_) => IndexGrowth::Polynomial,
35 Body::Add(parts) | Body::Mul(parts) | Body::Join(parts) => join(parts, k, env),
36 Body::Div(a, b) => {
37 join(std::slice::from_ref(a), k, env).and(join(std::slice::from_ref(b), k, env))
38 }
39 Body::Pow(base, _) => growth(&base.body, k, env),
40 Body::Keyed { .. } => IndexGrowth::Polynomial,
41 Body::Apply(Unary::Sin | Unary::Cos, arg) => bounded_along(arg, k, Axis::Real, env),
42 Body::Apply(Unary::Exp, arg) if logarithmic(&arg.body, k) => IndexGrowth::Polynomial,
43 Body::Apply(Unary::Exp, arg) => exponential_in(arg, k, env),
44 Body::Channel(of, _) | Body::Crop { of, .. } => growth(&of.body, k, env),
45 Body::Shift { of, .. } | Body::Deriv { of, .. } => growth(&of.body, k, env),
46 Body::Warp { at, of } => match &*of.body {
47 Body::Crop { of: inner, .. } => growth(&read_at(&inner.body, &at.body), k, env),
48 other => growth(&read_at(other, &at.body), k, env),
49 },
50 Body::Pv(at) | Body::Delta { at, .. } => growth(&at.body, k, env),
51 Body::Series(inner) => growth(&inner.term.body, k, env),
52 _ => IndexGrowth::Unbounded,
53 }
54}
55
56fn exponential_in(arg: &Part, k: IndexId, env: &dyn Env) -> IndexGrowth {
59 if let bounded @ IndexGrowth::Polynomial = bounded_along(arg, k, Axis::Imaginary, env) {
60 return bounded;
61 }
62 match affine_in(&arg.body, Reading::Index(k)).and_then(|(slope, _)| slope.exact()) {
63 Some(slope) if slope.re < 0.0 => IndexGrowth::Polynomial,
64 _ => IndexGrowth::Unbounded,
65 }
66}
67
68fn bounded_along(arg: &Part, k: IndexId, wanted: Axis, env: &dyn Env) -> IndexGrowth {
69 if !mentions(&arg.body, k) || axis(&arg.body, env) == wanted {
70 IndexGrowth::Polynomial
71 } else {
72 IndexGrowth::Unbounded
73 }
74}
75
76fn logarithmic(f: &Body, k: IndexId) -> bool {
78 match f {
79 _ if !mentions(f, k) => true,
80 Body::Apply(Unary::Log, _) => true,
81 Body::Add(parts) | Body::Mul(parts) => parts.iter().all(|p| logarithmic(&p.body, k)),
82 Body::Div(a, b) => logarithmic(&a.body, k) && logarithmic(&b.body, k),
83 _ => false,
84 }
85}
86
87fn join(parts: &[Part], k: IndexId, env: &dyn Env) -> IndexGrowth {
88 parts.iter().fold(IndexGrowth::Polynomial, |acc, p| {
89 acc.and(growth(&p.body, k, env))
90 })
91}
92
93impl IndexGrowth {
94 fn and(self, other: IndexGrowth) -> IndexGrowth {
95 match (self, other) {
96 (IndexGrowth::Polynomial, IndexGrowth::Polynomial) => IndexGrowth::Polynomial,
97 _ => IndexGrowth::Unbounded,
98 }
99 }
100}
101
102pub fn mentions_line(f: &Body) -> bool {
104 reaches(f, &|x| matches!(x, Body::Line))
105}
106
107pub fn ratio(f: &Body, k: IndexId) -> Option<f64> {
109 if !mentions(f, k) {
110 return Some(1.0);
111 }
112 match f {
113 Body::Mul(parts) => parts
114 .iter()
115 .try_fold(1.0, |held, p| Some(held * ratio(&p.body, k)?)),
116 Body::Add(parts) => parts.iter().try_fold(0.0f64, |held, p| match &*p.body {
117 Body::Apply(Unary::Abs, _) => Some(held.max(ratio(&p.body, k)?)),
118 _ => None,
119 }),
120 Body::Div(num, den) if !mentions(&den.body, k) => ratio(&num.body, k),
121 Body::Apply(Unary::Abs, arg) => ratio(&arg.body, k),
122 Body::Apply(Unary::Exp, arg) => {
123 let (slope, _) = affine_in(&arg.body, Reading::Index(k))?;
124 Some(slope.exact()?.re.exp())
125 }
126 Body::Pow(base, n) if *n >= 0 => Some(ratio(&base.body, k)?.powi(*n)),
127 _ => None,
128 }
129}
130
131pub fn mentions(f: &Body, k: IndexId) -> bool {
132 reaches(f, &|x| matches!(x, Body::Index(i) if *i == k))
133}
134
135fn reaches(f: &Body, leaf: &dyn Fn(&Body) -> bool) -> bool {
136 leaf(f) || children(f).iter().any(|p| reaches(&p.body, leaf))
137}
138
139#[derive(Clone, Copy, Debug, PartialEq)]
140pub struct Line {
141 pub hz: f64,
142 pub amp: C64,
143 pub rung: Option<Rung>,
145}
146
147impl Line {
148 pub fn bare(hz: f64, amp: C64) -> Line {
149 Line {
150 hz,
151 amp,
152 rung: None,
153 }
154 }
155}
156
157#[derive(Clone, Copy, Debug, PartialEq)]
159pub struct Rung {
160 pub offset: f64,
161 pub step: f64,
162 pub k: i64,
163}
164
165#[derive(Clone, Debug, PartialEq)]
167pub struct Lines {
168 pub taken: Vec<Line>,
169 pub dropped: Vec<Line>,
170 pub tail_db: f64,
171}
172
173pub const AUDIBLE_CEILING_HZ: f64 = 20_000.0;
174
175pub fn lines(s: &Series, ceiling: f64, floor_db: f64, precision: f64) -> Lines {
179 let ceiling = ceiling.min(AUDIBLE_CEILING_HZ);
180 let Some(shape) = read(&s.term.body) else {
181 return Lines {
182 taken: Vec::new(),
183 dropped: Vec::new(),
184 tail_db: f64::NEG_INFINITY,
185 };
186 };
187 let voices = places(&shape);
188 let band = voices
189 .iter()
190 .filter_map(|(place, _)| leaves_band(place, s.index, ceiling))
191 .fold(None, |held: Option<i64>, next| {
192 Some(held.map_or(next, |held| held.max(next)))
193 });
194 let hi = match (s.hi, band) {
195 (Bound::Finite(n), Some(last)) => n.min(last),
196 (Bound::Finite(n), None) => n,
197 (Bound::Infinite, Some(last)) => last,
198 (Bound::Infinite, None) => s.lo.saturating_add(MAX_TERMS),
199 };
200 let ratio = voices
201 .iter()
202 .try_fold(0.0f64, |held, (_, weight)| {
203 Some(held.max(ratio(weight, s.index)?))
204 })
205 .filter(|r| *r < 1.0);
206 let floor = 10f64.powf(floor_db / 20.0);
207 let ladders: Vec<Option<(f64, f64)>> = voices
208 .iter()
209 .map(|(place, _)| ladder(place, s.index))
210 .collect();
211
212 let mut taken = Vec::new();
213 let mut dropped = Vec::new();
214 let mut first = 0.0f64;
215 for k in s.lo..=hi {
216 let mut here = Vec::new();
217 for ((place, weight), ladder) in voices.iter().zip(&ladders) {
218 let (Some(hz), Some(amp)) = (
219 at_index(place, s.index, k).map(|c| c.re),
220 at_index(weight, s.index, k),
221 ) else {
222 continue;
223 };
224 let rung = ladder.map(|(offset, step)| Rung { offset, step, k });
225 here.push(Line { hz, amp, rung });
226 }
227 let loudest = here.iter().map(|l| l.amp.abs()).fold(0.0f64, f64::max);
228 if k == s.lo {
229 first = loudest;
230 }
231 let bound: f64 = here.iter().map(|l| l.amp.abs()).sum();
232 let gone = match ratio {
233 Some(r) => here.len() == voices.len() && bound / (1.0 - r) <= precision,
234 None => first > 0.0 && loudest < first * floor,
235 };
236 if band.is_none() && k > s.lo && gone {
237 dropped.extend(here);
238 break;
239 }
240 for line in here {
241 if line.hz.abs() <= ceiling {
242 taken.push(line);
243 } else {
244 dropped.push(line);
245 }
246 }
247 }
248 let loudest = |set: &[Line]| set.iter().map(|l| l.amp.abs()).fold(0.0f64, f64::max);
249 let (kept, gone) = (loudest(&taken), loudest(&dropped));
250 Lines {
251 tail_db: if gone > 0.0 && kept > 0.0 {
252 20.0 * (gone / kept).log10()
253 } else {
254 f64::NEG_INFINITY
255 },
256 taken,
257 dropped,
258 }
259}
260
261fn ladder(place: &Body, k: IndexId) -> Option<(f64, f64)> {
262 let (slope, offset) = affine_in(place, Reading::Index(k))?;
263 let (slope, offset) = (slope.exact()?, offset.exact()?);
264 let real = slope.im == 0.0 && offset.im == 0.0;
265 (real && slope.re.is_finite() && offset.re.is_finite()).then_some((offset.re, slope.re))
266}
267
268pub fn spacing(s: &Series) -> Option<f64> {
271 let Some(shape @ Shape::Lines(_)) = read(&s.term.body) else {
272 return None;
273 };
274 let mut held: Option<f64> = None;
275 for (place, _) in places(&shape) {
276 let (slope, offset) = affine_in(&place, Reading::Index(s.index))?;
277 let (slope, offset) = (slope.exact()?.re, offset.exact()?.re);
278 if slope == 0.0 || !slope.is_finite() || !offset.is_finite() {
279 return None;
280 }
281 let steps = offset / slope;
282 if (steps.round() - steps).abs() > TURN_EPSILON * steps.abs().max(1.0) {
283 return None;
284 }
285 match held {
286 Some(step) if step != slope.abs() => return None,
287 _ => held = Some(slope.abs()),
288 }
289 }
290 held
291}
292
293#[derive(Clone, Debug, PartialEq)]
294pub struct Enumerated {
295 pub atoms: Vec<SpectralAtom>,
296 pub dropped: Vec<Line>,
297}
298
299pub fn line_atoms(s: &Series, ceiling: f64, floor_db: f64, precision: f64) -> Option<Enumerated> {
302 let (body, window) = crate::spectral_sum::image::crop_peeled(&s.term.body);
303 let bare = Series {
304 term: Part::new(s.term.origin, body),
305 ..s.clone()
306 };
307 let singular = match read(&bare.term.body)? {
308 Shape::Deltas(_) => true,
309 Shape::Lines(_) => false,
310 };
311 let found = lines(&bare, ceiling, floor_db, precision);
312 let atoms = found
313 .taken
314 .into_iter()
315 .filter_map(|l| match singular {
316 true => window.is_none_or(|w| w.contains(l.hz)).then(|| {
317 SpectralAtom::new(
318 l.amp,
319 Factors::NONE,
320 Singular::Delta { at: l.hz, order: 0 },
321 s.term.origin,
322 )
323 }),
324 false => Some(SpectralAtom::new(
325 l.amp,
326 Factors {
327 exp: Some(Exp::at(0.0, TAU * l.hz)),
328 ind: window,
329 ..Factors::NONE
330 },
331 Singular::Regular,
332 s.term.origin,
333 )),
334 })
335 .collect();
336 Some(Enumerated {
337 atoms,
338 dropped: found.dropped,
339 })
340}
341
342pub fn commensurate(hz: f64, horizon: f64) -> bool {
345 let turns = hz * horizon;
346 (turns.round() - turns).abs() <= TURN_EPSILON * turns.abs().max(1.0)
347}
348
349const TURN_EPSILON: f64 = 1e-9;
350
351fn places(shape: &Shape) -> Vec<(Body, Body)> {
352 match shape {
353 Shape::Lines(lines) => lines
354 .iter()
355 .map(|l| (l.freq.clone(), l.amp.clone()))
356 .collect(),
357 Shape::Deltas(deltas) => deltas
358 .iter()
359 .map(|d| (d.at.clone(), d.weight.clone()))
360 .collect(),
361 }
362}
363
364fn leaves_band(place: &Body, k: IndexId, ceiling: f64) -> Option<i64> {
367 let (slope, offset) = affine_in(place, Reading::Index(k))?;
368 let (slope, offset) = (slope.exact()?.re, offset.exact()?.re);
369 if slope == 0.0 {
370 return None;
371 }
372 let ends = [(ceiling - offset) / slope, (-ceiling - offset) / slope];
373 let last = ends[0].max(ends[1]).floor();
374 Some(last.clamp(0.0, MAX_TERMS as f64) as i64 + 1)
375}
376
377const MAX_TERMS: i64 = 1 << 20;
378
379fn at_index(f: &Body, k: IndexId, value: i64) -> Option<C64> {
380 exact_constant(&substitute(f, k, value as f64))
381}
382
383pub fn substitute(f: &Body, k: IndexId, value: f64) -> Body {
384 match f {
385 Body::Index(i) if *i == k => Body::Const(C64::real(value)),
386 other => map_children(other, |p| {
387 Part::new(p.origin, substitute(&p.body, k, value))
388 }),
389 }
390}
391
392const MAX_WRITTEN_TERMS: i64 = 1 << 13;
393
394pub fn written_out(f: &Body) -> Option<Body> {
396 let mut left = MAX_WRITTEN_TERMS;
397 within(f, &mut left)
398}
399
400fn within(f: &Body, left: &mut i64) -> Option<Body> {
402 if let Body::Series(s) = f {
403 let Bound::Finite(hi) = s.hi else {
404 return None;
405 };
406 *left = left.checked_sub(hi.checked_sub(s.lo)?.checked_add(1)?.max(0))?;
407 if *left < 0 {
408 return None;
409 }
410 let terms = (s.lo..=hi)
411 .map(|k| {
412 Some(Part::new(
413 s.term.origin,
414 within(&substitute(&s.term.body, s.index, k as f64), left)?,
415 ))
416 })
417 .collect::<Option<Vec<_>>>()?;
418 return Some(match terms.len() {
419 0 => Body::Const(C64::ZERO),
420 _ => Body::Add(terms),
421 });
422 }
423 let mut whole = true;
424 let out = map_children(f, |p| match within(&p.body, left) {
425 Some(body) => Part::new(p.origin, body),
426 None => {
427 whole = false;
428 p.clone()
429 }
430 });
431 whole.then_some(out)
432}