1use std::cell::RefCell;
4use std::collections::HashMap;
5use std::f64::consts::TAU;
6
7use sva_formula::affine::{Axis, axis_read, exact_constant_at, exact_constant_read};
8use sva_formula::closed_form::{Bound, Series, children, map_children};
9use sva_formula::fourier_dual::series::{Shape, read_with};
10use sva_formula::series::{falls, mentions, mentions_line_read, ratio, substitute};
11use sva_formula::spectral_sum::atom::{Exp, Factors, Singular, SpectralAtom};
12use sva_formula::spectral_sum::merge::simplify;
13use sva_formula::spectral_sum::sup::magnitude_upper_bound_from_instant;
14use sva_formula::through::{Read, looked};
15use sva_formula::{
16 Banded, Body, C64, Codomain, Env, IndexId, Lane, NodeId, Opaque, ParamId, Part, Reads, Run,
17 SpectralSum, Ty, Unary, Var, d_dt, lines_read, normalize_read,
18};
19
20use crate::error::CollapseError;
21use crate::profile::Profile;
22
23#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct Audible {
27 ceiling: f64,
28 precision: f64,
29 floor_db: f64,
30}
31
32impl Audible {
33 pub fn of(profile: &Profile, rate: u32) -> Audible {
34 Audible::on(profile, crate::grid::Grid::of(rate))
35 }
36
37 pub fn on(profile: &Profile, grid: crate::grid::Grid) -> Audible {
38 let ceiling = profile.ceiling_hz.min(grid.sr() / 2.0);
39 Audible {
40 ceiling,
41 precision: profile.half_lsb(),
42 floor_db: profile.floor(ceiling),
43 }
44 }
45
46 pub(crate) fn ceiling(self) -> f64 {
47 self.ceiling
48 }
49
50 pub fn key(self) -> [u64; 3] {
51 [self.ceiling, self.precision, self.floor_db].map(f64::to_bits)
52 }
53
54 fn read_at(self, at: &Body, reads: &dyn Reads) -> Audible {
56 match sva_formula::calculus::steepest_read(at, reads) {
57 Some(rate) if rate > 1.0 => Audible {
58 ceiling: self.ceiling / rate,
59 ..self
60 },
61 _ => self,
62 }
63 }
64}
65
66const MAX_EXPANDED_TERMS: usize = 1 << 13;
69
70const MOST_PER_INSTANT: i64 = 1 << 10;
74
75pub fn spectral_sum(n: &SpectralSum, band: Audible) -> Result<SpectralSum, CollapseError> {
78 spectral_sum_read(n, band, &Opaque)
79}
80
81pub fn spectral_sum_read(
82 n: &SpectralSum,
83 band: Audible,
84 reads: &dyn Reads,
85) -> Result<SpectralSum, CollapseError> {
86 Terms::new(reads).spectral_sum(n, band)
87}
88
89pub fn written(f: &Body, band: Audible) -> Result<Body, CollapseError> {
91 written_with(f, band, &Opaque, &mut |id, _| Ok(id))
92}
93
94pub fn written_with(
97 f: &Body,
98 band: Audible,
99 reads: &dyn Reads,
100 named: &mut dyn FnMut(NodeId, Audible) -> Result<NodeId, CollapseError>,
101) -> Result<Body, CollapseError> {
102 Terms::new(reads).written(f, band, named)
103}
104
105pub fn dropped_db(f: &Body) -> Option<f64> {
107 let own = match f {
108 Body::Banded(b) => Some(b.dropped_db),
109 _ => None,
110 };
111 let inner = children(f).into_iter().filter_map(|p| dropped_db(&p.body));
112 own.into_iter().chain(inner).reduce(f64::max)
113}
114
115pub(super) fn windowed_lines(
116 s: &Series,
117 band: Audible,
118 reads: &dyn Reads,
119) -> Option<(Vec<sva_formula::Line>, Option<sva_formula::Indicator>)> {
120 Terms::new(reads).windowed_lines(s, band)
121}
122
123struct Terms<'a> {
125 reads: &'a dyn Reads,
126 bounds: PerBand<Option<Bounded>>,
127 prices: PerBand<Option<Price>>,
128 series: RefCell<HashMap<NodeId, bool>>,
129 summed: PerBand<NodeId>,
130}
131
132type PerBand<T> = RefCell<HashMap<(NodeId, [u64; 3]), T>>;
133
134type Price = (usize, Option<usize>);
136
137struct Cost {
139 depth: usize,
140 terms: Option<usize>,
141 own: usize,
142}
143
144#[derive(Clone)]
147struct Bounded {
148 body: Body,
149 exact: bool,
150}
151
152impl<'a> Terms<'a> {
153 fn new(reads: &'a dyn Reads) -> Terms<'a> {
154 Terms {
155 reads,
156 bounds: RefCell::default(),
157 prices: RefCell::default(),
158 series: RefCell::default(),
159 summed: RefCell::default(),
160 }
161 }
162
163 fn holds_series(&self, id: NodeId) -> bool {
164 if let Some(held) = self.series.borrow().get(&id) {
165 return *held;
166 }
167 let form = self.reads.view(id, &Read::As);
168 let found = form.is_some_and(|form| self.reaches_series(&form));
169 self.series.borrow_mut().insert(id, found);
170 found
171 }
172
173 fn reaches_series(&self, f: &Body) -> bool {
174 match f {
175 Body::Series(_) => true,
176 Body::Node(id) => self.holds_series(*id),
177 other => children(other).iter().any(|p| self.reaches_series(&p.body)),
178 }
179 }
180
181 fn summed(&self, id: NodeId, band: Audible) -> Result<NodeId, CollapseError> {
183 if !self.holds_series(id) {
184 return Ok(id);
185 }
186 let key = (id, band.key());
187 if let Some(held) = self.summed.borrow().get(&key) {
188 return Ok(*held);
189 }
190 let form = self
191 .reads
192 .view(id, &Read::As)
193 .expect("a ref holding a series names a form");
194 let body = self.written(&form, band, &mut |n, b| self.summed(n, b))?;
195 let stand = self
196 .reads
197 .stand_for(body)
198 .expect("a reader handing out forms stands one in");
199 self.summed.borrow_mut().insert(key, stand);
200 Ok(stand)
201 }
202
203 fn spectral_sum(&self, n: &SpectralSum, band: Audible) -> Result<SpectralSum, CollapseError> {
204 if n.lanes.iter().all(|l| l.series.is_empty()) {
205 return Ok(n.clone());
206 }
207 let mut lanes = Vec::with_capacity(n.lanes.len());
208 for lane in &n.lanes {
209 let mut held = Lane {
210 series: Vec::new(),
211 ..lane.clone()
212 };
213 for s in &lane.series {
214 held.atoms.extend(self.atoms(s, band)?);
215 }
216 lanes.push(held);
217 }
218 Ok(SpectralSum::of(n.var, lanes))
219 }
220
221 fn written(
222 &self,
223 f: &Body,
224 band: Audible,
225 named: &mut dyn FnMut(NodeId, Audible) -> Result<NodeId, CollapseError>,
226 ) -> Result<Body, CollapseError> {
227 match f {
228 Body::Node(id) => return Ok(Body::Node(named(*id, band)?)),
229 Body::Series(s) => return self.expanded(s, band, named),
230 Body::Warp { at, of } => {
231 let moved = band.read_at(&at.body, self.reads);
232 return Ok(Body::Warp {
233 at: Part::new(at.origin, self.written(&at.body, band, named)?),
234 of: Part::new(of.origin, self.written(&of.body, moved, named)?),
235 });
236 }
237 _ => {}
238 }
239 let mut found = None;
240 let out = map_children(f, |p| match self.written(&p.body, band, named) {
241 Ok(body) => Part::new(p.origin, body),
242 Err(e) => {
243 found = Some(e);
244 p.clone()
245 }
246 });
247 match found {
248 Some(e) => Err(e),
249 None => Ok(out),
250 }
251 }
252
253 fn windowed_lines(
254 &self,
255 s: &Series,
256 band: Audible,
257 ) -> Option<(Vec<sva_formula::Line>, Option<sva_formula::Indicator>)> {
258 let (body, window) = sva_formula::crop_peeled(&looked(&s.term.body, self.reads));
259 let bare = Series {
260 term: Part::new(s.term.origin, body),
261 ..s.clone()
262 };
263 let taken = self.enumerated(&bare, band).ok()?;
264 (!taken.is_empty()).then_some((taken, window))
265 }
266
267 fn atoms(&self, s: &Series, band: Audible) -> Result<Vec<SpectralAtom>, CollapseError> {
268 if let Some((taken, window)) = self.windowed_lines(s, band) {
269 return Ok(taken
270 .into_iter()
271 .map(|l| {
272 SpectralAtom::new(
273 l.amp,
274 Factors {
275 exp: Some(Exp::at(0.0, TAU * l.hz)),
276 ind: window,
277 ..Factors::NONE
278 },
279 Singular::Regular,
280 s.term.origin,
281 )
282 })
283 .collect());
284 }
285 if self.carried(s).is_some() && self.precise(s, band).is_none() {
286 return Err(CollapseError::NotEvaluable(
287 "a series summed instant by instant",
288 ));
289 }
290 let body = self.expanded(s, band, &mut |id, b| self.summed(id, b))?;
291 let held = normalize_read(&body, Var::T, self.reads)
292 .map_err(|_| CollapseError::NotEvaluable("a series term"))?;
293 let mut atoms = Vec::new();
294 for lane in held.lanes {
295 let mut summed = Lane::of(lane.atoms);
296 for inner in &lane.series {
297 summed.atoms.extend(self.atoms(inner, band)?);
298 }
299 if !lane.series.is_empty() {
300 simplify(&mut summed);
301 }
302 atoms.extend(summed.atoms);
303 }
304 Ok(atoms)
305 }
306
307 fn expanded(
309 &self,
310 s: &Series,
311 band: Audible,
312 named: &mut dyn FnMut(NodeId, Audible) -> Result<NodeId, CollapseError>,
313 ) -> Result<Body, CollapseError> {
314 let taken = self.enumerated(s, band)?;
315 if !taken.is_empty() {
316 let runs = Run::of(&taken).into_iter();
317 return Ok(sum(runs.map(|r| Body::Run(Box::new(r))).collect()));
318 }
319 if self.precise(s, band).is_none()
320 && let Some(banded) = self.banded(s, band, named)?
321 {
322 return Ok(Body::Banded(Box::new(banded)));
323 }
324 let count = self.terms(s, band)?;
325 let mut parts = Vec::with_capacity(count);
326 for i in 0..count {
327 let form = self.term(&s.term.body, s.index, (s.lo + i as i64) as f64);
328 parts.push(self.written(&form, band, named)?);
329 }
330 Ok(sum(parts))
331 }
332
333 fn term(&self, f: &Body, k: IndexId, value: f64) -> Body {
336 match f {
337 Body::Index(i) if *i == k => Body::Const(C64::real(value)),
338 Body::Warp { at, of } if mentions(&at.body, k) => {
339 let time = self.term(&at.body, k, value);
340 let moved = match &*of.body {
341 Body::Node(id) => self.reads.moved(*id, &time),
342 _ => None,
343 };
344 match moved {
345 Some(stand) => Body::Node(stand),
346 None => Body::Warp {
347 at: Part::new(at.origin, time),
348 of: Part::new(of.origin, self.term(&of.body, k, value)),
349 },
350 }
351 }
352 other => map_children(other, |p| Part::new(p.origin, self.term(&p.body, k, value))),
353 }
354 }
355
356 fn enumerated(
358 &self,
359 s: &Series,
360 band: Audible,
361 ) -> Result<Vec<sva_formula::Line>, CollapseError> {
362 match read_with(&s.term.body, self.reads) {
363 Some(Shape::Lines(_)) => {
364 let held = (band.ceiling, band.floor_db, band.precision);
365 lines_read(s, held, self.reads)
366 .map(|l| l.taken)
367 .ok_or(CollapseError::NotEvaluable(
368 "a series whose tail no bound sums",
369 ))
370 }
371 _ => Ok(Vec::new()),
372 }
373 }
374
375 fn terms(&self, s: &Series, band: Audible) -> Result<usize, CollapseError> {
377 let cost = self.cost(s, band).ok_or(CollapseError::NotEvaluable(
378 "a series whose term count no coefficient bounds",
379 ))?;
380 if cost.depth == 1 && cost.own > MAX_EXPANDED_TERMS {
381 return Err(CollapseError::NotEvaluable(
382 "a series of more terms than one instant expands",
383 ));
384 }
385 match cost.terms {
386 Some(terms) if terms <= MAX_EXPANDED_TERMS => Ok(cost.own),
387 terms => Err(CollapseError::NestedSeries {
388 depth: cost.depth,
389 terms,
390 bound: MAX_EXPANDED_TERMS,
391 }),
392 }
393 }
394
395 fn cost(&self, s: &Series, band: Audible) -> Option<Cost> {
396 let own = self.counted(s, band)?;
397 let inside = substitute(&s.term.body, s.index, s.lo as f64);
398 let (depth, inner) = self.price(&inside, band)?;
399 Some(Cost {
400 depth: depth + 1,
401 terms: inner.and_then(|inner| own.checked_mul(inner)),
402 own,
403 })
404 }
405
406 fn price(&self, f: &Body, band: Audible) -> Option<Price> {
408 match f {
409 Body::Series(s) => {
410 let cost = self.cost(s, band)?;
411 return Some((cost.depth, cost.terms));
412 }
413 Body::Node(id) => return self.node_price(*id, band),
414 _ => {}
415 }
416 let summed = matches!(f, Body::Add(_));
417 let mut depth = 0;
418 let mut terms: Option<usize> = Some(if summed { 0 } else { 1 });
419 for p in children(f) {
420 let (d, n) = self.price(&p.body, band)?;
421 if d == 0 {
422 continue;
423 }
424 depth = depth.max(d);
425 terms = terms.zip(n).and_then(|(terms, n)| match summed {
426 true => terms.checked_add(n),
427 false => terms.checked_mul(n),
428 });
429 }
430 Some((depth, terms.map(|n| n.max(1))))
431 }
432
433 fn node_price(&self, id: NodeId, band: Audible) -> Option<Price> {
434 let key = (id, band.key());
435 if let Some(held) = self.prices.borrow().get(&key) {
436 return *held;
437 }
438 let found = match self.reads.view(id, &Read::As) {
439 Some(form) => self.price(&form, band),
440 None => Some((0, Some(1))),
441 };
442 self.prices.borrow_mut().insert(key, found);
443 found
444 }
445
446 fn counted(&self, s: &Series, band: Audible) -> Option<usize> {
449 if let Bound::Finite(n) = s.hi {
450 return usize::try_from((n - s.lo + 1).max(0)).ok();
451 }
452 let precise = self.precise(s, band);
453 precise.or_else(|| self.carried(s).map(|_| MOST_PER_INSTANT as usize))
454 }
455
456 fn precise(&self, s: &Series, band: Audible) -> Option<usize> {
458 let bound = self.bound(&s.term.body, band)?;
459 let ratio = ratio(&bound.body, s.index).filter(|r| *r < 1.0 && bound.exact)?;
460 for i in 0..MAX_EXPANDED_TERMS {
461 let at = (s.lo + i as i64) as f64;
462 let held = exact_constant_at(&bound.body, s.index, at, self.reads)?.abs();
463 if held / (1.0 - ratio) <= band.precision {
464 return Some(i.max(1));
465 }
466 }
467 None
468 }
469
470 fn carried<'s>(&self, s: &'s Series) -> Option<&'s Body> {
473 if s.hi != Bound::Infinite {
474 return None;
475 }
476 let angle = carrier(&s.term.body, s.index, self.reads)?;
477 (degree(angle, s.index)? == 1 && self.real(angle)).then_some(angle)
478 }
479
480 fn banded(
483 &self,
484 s: &Series,
485 band: Audible,
486 named: &mut dyn FnMut(NodeId, Audible) -> Result<NodeId, CollapseError>,
487 ) -> Result<Option<Banded>, CollapseError> {
488 let Some(angle) = self.carried(s) else {
489 return Ok(None);
490 };
491 let at = |k: f64| substitute(angle, s.index, k);
492 let (still, once) = (at(0.0), at(1.0));
493 let per = Body::Add(vec![
494 Part::bare(once),
495 Part::bare(Body::Mul(vec![
496 Part::bare(Body::Const(C64::real(-1.0))),
497 Part::bare(still.clone()),
498 ])),
499 ]);
500 let turning = |f: &Body| {
501 let sum = normalize_read(f, Var::T, self.reads).ok()?;
502 let [lane] = sum.lanes.as_slice() else {
503 return None;
504 };
505 let plain = lane.series.is_empty() && lane.modal.is_empty();
506 plain.then(|| d_dt(&sum).ok()).flatten()
507 };
508 let (Some(slope), Some(offset)) = (turning(&per), turning(&still)) else {
509 return Ok(None);
510 };
511 let term = self.written(&s.term.body, band, named)?;
512 let weight = steady(&s.term.body, angle);
513 let mut banded = Banded {
514 series: Series {
515 term: Part::new(s.term.origin, term),
516 ..s.clone()
517 },
518 slope,
519 offset,
520 omega: TAU * band.ceiling,
521 most: MOST_PER_INSTANT,
522 widest: MOST_PER_INSTANT,
523 reach: f64::INFINITY,
524 dropped_db: f64::INFINITY,
525 };
526 self.bounded(&mut banded, &weight, band);
527 match banded.dropped_db.is_finite() {
528 true => Ok(Some(banded)),
529 false => Err(CollapseError::NotEvaluable(
530 "a series whose dropped terms no bound holds",
531 )),
532 }
533 }
534
535 fn bounded(&self, b: &mut Banded, weight: &Body, band: Audible) {
537 let (lo, k) = (b.series.lo, b.series.index);
538 let (fastest, drift) = (sup(&b.slope), sup(&b.offset));
539 let (slowest, still) = (least(&b.slope), least(&b.offset).max(0.0));
540 let count = |last: f64| (last.ceil().clamp(-9e18, 9e18) as i64 - lo).clamp(0, b.most);
541 let kept = match fastest > 0.0 {
542 true => count((b.omega - drift) / fastest),
543 false if drift < b.omega => b.most,
544 false => 0,
545 };
546 b.widest = match slowest > 0.0 {
547 true => count((b.omega + drift.max(still)) / slowest),
548 false => b.most,
549 };
550 let Some(bound) = self.bound(weight, band).filter(|w| w.exact) else {
551 return;
552 };
553 let at = |n: i64| Some(exact_constant_at(&bound.body, k, n as f64, self.reads)?.abs());
554 b.reach = (lo..lo + b.widest)
555 .try_fold(0.0, |held, n| Some(held + at(n)?))
556 .unwrap_or(f64::INFINITY);
557 if lo >= 1
558 && falls(&bound.body, k)
559 && let (Some(top), Some(gone)) = (at(lo), at(lo + kept))
560 {
561 b.dropped_db = 20.0 * (gone / top).log10();
562 }
563 }
564
565 fn bound(&self, f: &Body, band: Audible) -> Option<Bounded> {
566 let held = |body: Body, exact: bool| Some(Bounded { body, exact });
567 let under = |p: &Part| {
568 self.bound(&p.body, band)
569 .map(|b| (Part::new(p.origin, b.body), b.exact))
570 };
571 let all = |parts: &[Part], wrap: &dyn Fn(Part) -> Part| {
572 let mut exact = true;
573 let mut kept = Vec::with_capacity(parts.len());
574 for p in parts {
575 let (part, known) = under(p)?;
576 exact &= known;
577 kept.push(wrap(part));
578 }
579 Some((kept, exact))
580 };
581 let mentions_line = |f: &Body| mentions_line_read(f, self.reads);
582 match f {
583 Body::Series(s) => match self.total(s, band) {
584 Some(sum) => held(Body::Const(C64::real(sum)), true),
585 None => held(Body::Const(C64::ONE), false),
586 },
587 Body::Node(id) => self.node_bound(*id, band),
588 Body::Line => held(Body::Const(C64::ONE), false),
589 Body::Mul(parts) => {
590 let (kept, exact) = all(parts, &|p| p)?;
591 held(Body::Mul(kept), exact)
592 }
593 Body::Add(parts) => {
594 let (kept, exact) = all(parts, &|p| Part::bare(Body::Apply(Unary::Abs, p)))?;
595 held(Body::Add(kept), exact)
596 }
597 Body::Div(a, b) if !mentions_line(&b.body) => {
598 let (num, exact) = under(a)?;
599 held(Body::Div(num, b.clone()), exact)
600 }
601 Body::Div(a, b) => {
602 let ((num, _), (den, _)) = (under(a)?, under(b)?);
603 held(Body::Div(num, den), false)
604 }
605 Body::Apply(Unary::Sin | Unary::Cos | Unary::Tanh | Unary::Sat | Unary::Step, arg) => {
606 held(Body::Const(C64::ONE), self.real(&arg.body))
607 }
608 Body::Crop { of, .. } | Body::Shift { of, .. } | Body::Warp { of, .. } => {
609 self.bound(&of.body, band)
610 }
611 Body::Pow(base, n) => {
612 let (part, exact) = under(base)?;
613 held(Body::Pow(part, *n), exact && *n >= 0)
614 }
615 other if !mentions_line(other) => held(other.clone(), true),
616 _ => None,
617 }
618 }
619
620 fn node_bound(&self, id: NodeId, band: Audible) -> Option<Bounded> {
622 let key = (id, band.key());
623 if let Some(held) = self.bounds.borrow().get(&key) {
624 return held.clone();
625 }
626 let found = match self.reads.view(id, &Read::As) {
627 Some(form) => self.bound(&form, band).and_then(|b| {
628 let value = exact_constant_read(&b.body, self.reads)?;
629 Some(Bounded {
630 body: Body::Const(value),
631 exact: b.exact,
632 })
633 }),
634 None => Some(Bounded {
635 body: Body::Const(C64::ONE),
636 exact: false,
637 }),
638 };
639 self.bounds.borrow_mut().insert(key, found.clone());
640 found
641 }
642
643 fn total(&self, s: &Series, band: Audible) -> Option<f64> {
645 let bound = self.bound(&s.term.body, band).filter(|b| b.exact)?.body;
646 let at = |i: i64| Some(exact_constant_at(&bound, s.index, i as f64, self.reads)?.abs());
647 if let (Bound::Infinite, Some(r)) = (s.hi, ratio(&bound, s.index).filter(|r| *r < 1.0)) {
648 return Some(at(s.lo)? / (1.0 - r));
649 }
650 let kept =
651 i64::try_from(self.counted(s, band).filter(|n| *n <= MAX_EXPANDED_TERMS)?).ok()?;
652 (s.lo..s.lo + kept).try_fold(0.0, |held, i| Some(held + at(i)?))
653 }
654
655 fn real(&self, f: &Body) -> bool {
656 axis_read(f, &Unread, self.reads) == Axis::Real
657 }
658}
659
660fn sum(parts: Vec<Body>) -> Body {
661 match parts.len() {
662 0 => Body::Const(C64::ZERO),
663 1 => parts.into_iter().next().expect("one part"),
664 _ => Body::Add(parts.into_iter().map(Part::bare).collect()),
665 }
666}
667
668fn carrier<'f>(f: &'f Body, k: IndexId, reads: &dyn Reads) -> Option<&'f Body> {
671 let (mut found, mut clean) = (Vec::new(), true);
672 moving(f, k, reads, (&mut found, &mut clean));
673 match found.as_slice() {
674 [angle] if clean => Some(*angle),
675 _ => None,
676 }
677}
678
679fn moving<'f>(
680 f: &'f Body,
681 k: IndexId,
682 reads: &dyn Reads,
683 (found, clean): (&mut Vec<&'f Body>, &mut bool),
684) {
685 if !mentions(f, k) {
686 return;
687 }
688 match f {
689 Body::Apply(Unary::Sin | Unary::Cos, arg) => found.push(&arg.body),
690 Body::Apply(_, arg) if mentions_line_read(&arg.body, reads) => *clean = false,
691 Body::Warp { .. }
692 | Body::Series(_)
693 | Body::Banded(_)
694 | Body::Keyed { .. }
695 | Body::Delta { .. }
696 | Body::Pv(_)
697 | Body::Deriv { .. } => *clean = false,
698 other => {
699 for p in children(other) {
700 moving(&p.body, k, reads, (&mut *found, &mut *clean));
701 }
702 }
703 }
704}
705
706fn degree(f: &Body, k: IndexId) -> Option<u32> {
708 if !mentions(f, k) {
709 return Some(0);
710 }
711 match f {
712 Body::Index(i) if *i == k => Some(1),
713 Body::Add(parts) => parts
714 .iter()
715 .try_fold(0, |held, p| Some(held.max(degree(&p.body, k)?))),
716 Body::Mul(parts) => parts
717 .iter()
718 .try_fold(0, |held, p| Some(held + degree(&p.body, k)?)),
719 Body::Div(num, den) if !mentions(&den.body, k) => degree(&num.body, k),
720 Body::Pow(base, n) if *n >= 0 => Some(degree(&base.body, k)? * n.unsigned_abs()),
721 _ => None,
722 }
723}
724
725fn steady(term: &Body, angle: &Body) -> Body {
727 match term {
728 Body::Apply(Unary::Sin | Unary::Cos, arg) if std::ptr::eq(&*arg.body, angle) => {
729 Body::Const(C64::ONE)
730 }
731 other => map_children(other, |p| Part::new(p.origin, steady(&p.body, angle))),
732 }
733}
734
735fn sup(f: &SpectralSum) -> f64 {
737 let atoms = f.lanes.iter().flat_map(|lane| lane.atoms.iter());
738 atoms
739 .map(|a| magnitude_upper_bound_from_instant(a, f64::NEG_INFINITY).unwrap_or(f64::INFINITY))
740 .sum()
741}
742
743fn least(f: &SpectralSum) -> f64 {
745 let atoms = f.lanes.iter().flat_map(|lane| lane.atoms.iter());
746 let (still, moving): (Vec<_>, Vec<_>) = atoms.partition(|a| a.is_bare());
747 let held = still
748 .iter()
749 .fold(C64::ZERO, |held, a: &&SpectralAtom| held + a.c)
750 .abs();
751 let moves: f64 = moving
752 .iter()
753 .map(|a| magnitude_upper_bound_from_instant(a, f64::NEG_INFINITY).unwrap_or(f64::INFINITY))
754 .sum();
755 (held - moves).max(0.0)
756}
757
758struct Unread;
759
760impl Env for Unread {
761 fn node(&self, _: NodeId) -> Ty {
762 Ty::form(Var::T, false, Codomain::Complex)
763 }
764
765 fn param(&self, _: ParamId) -> Ty {
766 Ty::form(Var::T, false, Codomain::Complex)
767 }
768}