Skip to main content

sva_samples/collapse/
plan.rs

1// Concern: which row of the collapse table a form takes, what it costs and its direct sums' bounds | Non-concern: running the row | IO: (&SpectralSum, rate, Extent) -> Plan, flops, bounds
2
3use sva_formula::closed_form::{Part, map_children};
4use sva_formula::spectral_sum::atom::SpectralAtom;
5use sva_formula::{Body, ClosedForm, Lane, Line, SpectralSum, Var, normalize_closed_form};
6
7use super::active::{self, Window};
8use super::truncate::Audible;
9use super::{Extent, atoms, lines, point, truncate};
10use crate::Grid;
11use crate::error::CollapseError;
12use crate::label::Rule;
13use crate::profile::Profile;
14
15/// Both routes place a line exactly, so cost decides; the direct one hands a lone line back
16/// unchanged.
17pub fn direct_is_cheaper(lines: usize, n: usize, len: usize) -> bool {
18    (lines as f64) * (len as f64) <= (n as f64) * (n as f64).log2()
19}
20
21pub struct LinePlan {
22    pub placed: Vec<Vec<Line>>,
23    pub summed: Vec<Vec<Line>>,
24    pub dropped: Vec<Vec<Line>>,
25    pub bins: usize,
26    pub tail_db: Option<f64>,
27}
28
29/// FORMAT 9.2's window route. Outside `live` the factor zeroes a sum proven finite.
30pub struct Group {
31    pub factor: SpectralAtom,
32    pub placed: Vec<Line>,
33    pub summed: Vec<Line>,
34    pub live: Window,
35    samples: usize,
36}
37
38/// `atoms` times `samples` chose the route before a sweep skipped dead atoms, so the choice
39/// keeps its bits; `evaluated` is what it now runs.
40pub enum LanePlan {
41    Grouped {
42        groups: Vec<Group>,
43        bins: usize,
44    },
45    Sweep {
46        atoms: usize,
47        samples: usize,
48        evaluated: u128,
49    },
50}
51
52pub struct Sampled {
53    pub sum: Box<SpectralSum>,
54    pub rule: Rule,
55    pub lanes: Vec<LanePlan>,
56}
57
58/// The row of FORMAT 9.1 that runs, decided once for the collapse and the count alike.
59pub enum Plan {
60    Spectrum(Box<SpectralSum>),
61    Lines(Box<LinePlan>),
62    Sampled(Box<Sampled>),
63    /// Row 4 over the written closed form: every node the truncation leaves, at each instant
64    /// it is walked.
65    Point {
66        written: Box<ClosedForm>,
67        width: usize,
68    },
69    /// A sum whose addends name different rows, each taking its own.
70    Added(Vec<Plan>),
71}
72
73/// The rows in FORMAT 9.1's order; the first guard that holds decides.
74pub fn of(
75    sum: &SpectralSum,
76    rate: u32,
77    extent: Extent,
78    profile: &Profile,
79    len: usize,
80) -> Result<Plan, CollapseError> {
81    let ceiling = profile.ceiling(rate);
82    if sum.var == Var::F {
83        return Ok(Plan::Spectrum(Box::new(sum.clone())));
84    }
85    if let Some(found) = line_plan(sum, rate, extent, profile, len, ceiling)? {
86        return Ok(Plan::Lines(Box::new(found)));
87    }
88    let truncated = truncate::spectral_sum(sum, Audible::of(profile, rate))?;
89    let rule = match () {
90        () if atoms::band_limited(&truncated, ceiling, profile) => Rule::BandLimited,
91        () if atoms::windowed(&truncated) => Rule::CroppedPair,
92        () => Rule::PointSampled,
93    };
94    let lanes = truncated
95        .lanes
96        .iter()
97        .map(|lane| lane_plan(lane, rate, extent, len))
98        .collect();
99    Ok(Plan::Sampled(Box::new(Sampled {
100        sum: Box::new(truncated),
101        rule,
102        lanes,
103    })))
104}
105
106/// The row a closed form with no spectral sum takes: one per addend where they differ, else 4.
107pub fn of_written(
108    form: &ClosedForm,
109    rate: u32,
110    extent: Extent,
111    profile: &Profile,
112    len: usize,
113) -> Result<Plan, CollapseError> {
114    let Some(addends) = addends(form) else {
115        return point_plan(form, rate, profile);
116    };
117    let mut parts = Vec::with_capacity(addends.len());
118    for addend in &addends {
119        match of_term(addend, rate, extent, profile, len)? {
120            Plan::Added(inner) => parts.extend(inner),
121            part => parts.push(part),
122        }
123    }
124    // Addends that all name row 4 are one row 4 over the whole sum, measured once.
125    match parts.iter().all(|part| matches!(part, Plan::Point { .. })) {
126        true => point_plan(form, rate, profile),
127        false => Ok(Plan::Added(parts)),
128    }
129}
130
131/// A row the addend alone cannot take refuses nothing; the written row still stands for it.
132/// A nesting past the bound is the one exception, refused wherever it is written.
133fn of_term(
134    form: &ClosedForm,
135    rate: u32,
136    extent: Extent,
137    profile: &Profile,
138    len: usize,
139) -> Result<Plan, CollapseError> {
140    let Ok(sum) = normalize_closed_form(form) else {
141        return of_written(form, rate, extent, profile, len);
142    };
143    match of(&sum, rate, extent, profile, len) {
144        Err(nested @ CollapseError::NestedSeries { .. }) => Err(nested),
145        Err(_) => of_written(form, rate, extent, profile, len),
146        held => held,
147    }
148}
149
150pub(super) fn addends(form: &ClosedForm) -> Option<Vec<ClosedForm>> {
151    if let Body::Add(parts) = &form.body {
152        return Some(
153            parts
154                .iter()
155                .map(|part| ClosedForm {
156                    var: form.var,
157                    body: (*part.body).clone(),
158                    origin: part.origin,
159                })
160                .collect(),
161        );
162    }
163    let under = linear_over(&form.body)?;
164    let held = addends(&ClosedForm {
165        body: under,
166        ..form.clone()
167    })?;
168    Some(
169        held.into_iter()
170            .map(|addend| ClosedForm {
171                body: map_children(&form.body, |_| {
172                    Part::new(addend.origin, addend.body.clone())
173                }),
174                ..addend
175            })
176            .collect(),
177    )
178}
179
180/// Each of these is linear, so over a sum it is the sum of itself over every addend.
181fn linear_over(f: &Body) -> Option<Body> {
182    match f {
183        Body::Crop { of, .. }
184        | Body::Shift { of, .. }
185        | Body::Deriv { of, .. }
186        | Body::Channel(of, _) => Some((*of.body).clone()),
187        _ => None,
188    }
189}
190
191fn point_plan(form: &ClosedForm, rate: u32, profile: &Profile) -> Result<Plan, CollapseError> {
192    let written = ClosedForm {
193        body: truncate::written(&form.body, Audible::of(profile, rate))?,
194        ..form.clone()
195    };
196    let width = point::width_of(&written.body, &point::NoRefs).max(1);
197    Ok(Plan::Point {
198        written: Box::new(written),
199        width,
200    })
201}
202
203/// The nodes every component's evaluation walks over the samples `[from, to)` of `grid`.
204pub fn point_flops(f: &Body, grid: Grid, span: Window) -> u128 {
205    let width = point::width_of(f, &point::NoRefs).max(1);
206    (0..width).map(|c| point_work(f, c, grid, span).0).sum()
207}
208
209/// One component's `(nodes, waves)` over the samples `[from, to)` of `grid`, as
210/// `point::eval_body` walks them: a run is priced by its Horner steps and turns its lines,
211/// every other node is one, and a crop's operand counts only at the instants its window holds.
212pub fn point_work(f: &Body, component: usize, grid: Grid, span: Window) -> (u128, u128) {
213    let clock = Clock::grid(grid);
214    let Some(parts) = summed(f) else {
215        return walked(f, component, &clock, span);
216    };
217    let n = (i128::from(span.1) - i128::from(span.0)).max(0) as u128;
218    parts
219        .iter()
220        .zip(addend_windows(&parts, grid))
221        .fold((n, 0), |held, (part, live)| {
222            let (priced, waves) = walked(&part.body, component, &clock, active::meet(span, live));
223            (held.0 + priced, held.1 + waves)
224        })
225}
226
227/// A written sum's addends in order, a left-nested `a + b + c` as one list: `point::eval_body`
228/// folds either from +0 to the same bits, as no partial sum from +0 is -0.
229pub(super) fn summed(f: &Body) -> Option<Vec<&Part>> {
230    let Body::Add(parts) = f else {
231        return None;
232    };
233    let (mut head, mut tails) = (parts, Vec::new());
234    while let [first, rest @ ..] = head.as_slice()
235        && let Body::Add(inner) = &*first.body
236    {
237        tails.push(rest);
238        head = inner;
239    }
240    let mut out: Vec<&Part> = head.iter().collect();
241    for tail in tails.iter().rev() {
242        out.extend(tail.iter());
243    }
244    Some(out)
245}
246
247/// Outside its own window an addend is exact zero.
248pub(super) fn addend_windows(parts: &[&Part], grid: Grid) -> Vec<Window> {
249    parts
250        .iter()
251        .map(|part| live_window(&part.body, grid))
252        .collect()
253}
254
255pub(super) fn live_window(f: &Body, grid: Grid) -> Window {
256    live(f, &Clock::grid(grid), active::OPEN)
257}
258
259/// A crop's open window, met through products and shifts, as `point::eval_body` zeroes them.
260fn live(f: &Body, clock: &Clock, span: Window) -> Window {
261    match f {
262        Body::Crop { of, .. } => live(&of.body, clock, clock.cropped(span, f)),
263        Body::Mul(parts) => parts
264            .iter()
265            .fold(span, |held, part| live(&part.body, clock, held)),
266        Body::Shift { by, of } => live(&of.body, &clock.shifted(*by), span),
267        _ => span,
268    }
269}
270
271/// The instant a subterm is read at, from the grid's index: `None` once a warp moves it.
272#[derive(Clone)]
273struct Clock {
274    grid: Grid,
275    shifts: Option<Vec<f64>>,
276}
277
278impl Clock {
279    fn grid(grid: Grid) -> Clock {
280        Clock {
281            grid,
282            shifts: Some(Vec::new()),
283        }
284    }
285
286    fn shifted(&self, by: f64) -> Clock {
287        Clock {
288            grid: self.grid,
289            shifts: self
290                .shifts
291                .as_ref()
292                .map(|held| [held.as_slice(), &[by]].concat()),
293        }
294    }
295
296    fn warped(&self) -> Clock {
297        Clock {
298            grid: self.grid,
299            shifts: None,
300        }
301    }
302
303    /// Where a crop's gain is not zero: inside `[l, r)` less the instants a shoulder opens at.
304    fn cropped(&self, span: Window, crop: &Body) -> Window {
305        let (
306            Body::Crop {
307                l, r, rise, fall, ..
308            },
309            Some(shifts),
310        ) = (crop, &self.shifts)
311        else {
312            return span;
313        };
314        let at = |n: i64| shifts.iter().fold(self.grid.instant(n), |t, by| t - by);
315        let (l, r) = (l.value(), r.value());
316        let (mut from, mut to) = active::meet(span, active::between(l, r, at));
317        let shut = |n: i64| point::crop_gain(at(n), l, r, *rise, *fall) == 0.0;
318        while from < to && shut(from) {
319            from += 1;
320        }
321        while from < to && shut(to - 1) {
322            to -= 1;
323        }
324        (from, to)
325    }
326}
327
328fn walked(f: &Body, component: usize, clock: &Clock, span: Window) -> (u128, u128) {
329    let n = (i128::from(span.1) - i128::from(span.0)).max(0) as u128;
330    if n == 0 {
331        return (0, 0);
332    }
333    let add = |a: (u128, u128), b: (u128, u128)| (a.0 + b.0, a.1 + b.1);
334    let branch = |part: &Part, c: usize, clock: &Clock, span: Window| {
335        add((n, 0), walked(&part.body, c, clock, span))
336    };
337    match f {
338        Body::Run(run) => (super::run::steps(run) as u128 * n, run.len() as u128 * n),
339        Body::Join(parts) => {
340            let widths: Vec<usize> = parts
341                .iter()
342                .map(|p| point::width_of(&p.body, &point::NoRefs))
343                .collect();
344            match point::lane_of(&widths, component) {
345                Some((at, inner)) => branch(&parts[at], inner, clock, span),
346                None => (n, 0),
347            }
348        }
349        Body::Channel(of, k) => branch(of, usize::from(*k), clock, span),
350        Body::Crop { of, .. } => branch(of, component, clock, clock.cropped(span, f)),
351        // `point::product` walks no factor past one a shut crop zeroed.
352        Body::Mul(parts) => {
353            let mut live = span;
354            parts.iter().fold((n, 0), |held, part| {
355                let walked = walked(&part.body, component, clock, live);
356                live = clock.cropped(live, &part.body);
357                add(held, walked)
358            })
359        }
360        Body::Shift { by, of } => branch(of, component, &clock.shifted(*by), span),
361        Body::Warp { at, of } => add(
362            branch(at, component, clock, span),
363            walked(&of.body, component, &clock.warped(), span),
364        ),
365        other => sva_formula::closed_form::children(other)
366            .iter()
367            .map(|part| walked(&part.body, component, clock, span))
368            .fold((n, 0), add),
369    }
370}
371
372/// Each direct sum a row may take over this form's lines, as its rounding bound and the
373/// factor it is read under: none on a line row, the group's own on a windowed one.
374pub fn summed_bounds(
375    sum: &SpectralSum,
376    profile: &Profile,
377    rate: u32,
378) -> Result<Vec<(Option<SpectralAtom>, f64)>, CollapseError> {
379    if sum.var == Var::F {
380        return Ok(Vec::new());
381    }
382    if let Some(found) = kept_lines(sum, profile, profile.ceiling(rate))? {
383        let direct = found.kept.iter().filter_map(|kept| lines::Direct::of(kept));
384        return Ok(direct.map(|d| (None, d.bound())).collect());
385    }
386    let truncated = truncate::spectral_sum(sum, Audible::of(profile, rate))?;
387    let groups = truncated.lanes.iter().filter_map(lines::grouped).flatten();
388    Ok(groups
389        .filter_map(|(factor, held)| Some((Some(factor), lines::Direct::of(&held)?.bound())))
390        .collect())
391}
392
393/// Rows one and two: every atom a line, none of them windowed.
394fn line_plan(
395    sum: &SpectralSum,
396    rate: u32,
397    extent: Extent,
398    profile: &Profile,
399    len: usize,
400    ceiling: f64,
401) -> Result<Option<LinePlan>, CollapseError> {
402    let Some(found) = kept_lines(sum, profile, ceiling)? else {
403        return Ok(None);
404    };
405    let bins = lines::grid(&found.kept, &found.grids, rate, extent.span_secs(rate), len);
406    let split: Vec<(Vec<Line>, Vec<Line>)> = found
407        .kept
408        .iter()
409        .map(|k| match direct_is_cheaper(k.len(), bins, len) {
410            true => (Vec::new(), k.clone()),
411            false => lines::split(k, bins, rate),
412        })
413        .collect();
414    Ok(Some(LinePlan {
415        placed: split.iter().map(|(on, _)| on.clone()).collect(),
416        summed: split.into_iter().map(|(_, off)| off).collect(),
417        dropped: found.dropped,
418        bins,
419        tail_db: found.tail,
420    }))
421}
422
423/// Each lane's lines under the ceiling and over it, before any extent places them.
424pub(super) struct Kept {
425    pub(super) kept: Vec<Vec<Line>>,
426    dropped: Vec<Vec<Line>>,
427    grids: Vec<f64>,
428    tail: Option<f64>,
429}
430
431impl Kept {
432    pub(super) fn dropped(&self) -> &[Vec<Line>] {
433        &self.dropped
434    }
435
436    pub(super) fn tail(&self) -> Option<f64> {
437        self.tail
438    }
439}
440
441pub(super) fn kept_lines(
442    sum: &SpectralSum,
443    profile: &Profile,
444    ceiling: f64,
445) -> Result<Option<Kept>, CollapseError> {
446    let mut per_lane = Vec::with_capacity(sum.lanes.len());
447    let mut grids: Vec<f64> = Vec::new();
448    let mut tail: Option<f64> = None;
449    for lane in &sum.lanes {
450        match lines::of_lane(lane, ceiling, profile.floor(ceiling), profile.half_lsb()) {
451            Some(found) => {
452                if let Some(left) = found.tail_db {
453                    tail = Some(tail.map_or(left, |held: f64| held.max(left)));
454                }
455                grids.extend(found.grids);
456                per_lane.push(found.lines);
457            }
458            None => return Ok(None),
459        }
460    }
461
462    let (mut kept, mut dropped): (Vec<Vec<Line>>, Vec<Vec<Line>>) = (Vec::new(), Vec::new());
463    for found in &per_lane {
464        let (here, gone): (Vec<Line>, Vec<Line>) = found.iter().partition(|l| l.hz.abs() < ceiling);
465        kept.push(here);
466        dropped.push(gone);
467    }
468    // A form with no line at all is silence, not a band every line sat above.
469    if kept.iter().all(Vec::is_empty) && !dropped.iter().all(Vec::is_empty) {
470        let lowest = dropped
471            .iter()
472            .flatten()
473            .map(|l| l.hz.abs())
474            .fold(f64::INFINITY, f64::min);
475        return Err(CollapseError::EmptyBand { ceiling, lowest });
476    }
477    Ok(Some(Kept {
478        kept,
479        dropped,
480        grids,
481        tail,
482    }))
483}
484
485fn lane_plan(lane: &Lane, rate: u32, extent: Extent, len: usize) -> LanePlan {
486    let bins = lines::bins(extent.span_secs(rate), rate);
487    let grid = Grid::of(rate);
488    let spans = super::span::absolute(lane, extent, rate);
489    let sweep = LanePlan::Sweep {
490        atoms: lane.atoms.len(),
491        samples: spans.iter().map(|(from, to)| (to - from) as usize).sum(),
492        evaluated: active::evaluated(&active::windows(lane, grid), &spans),
493    };
494    let Some(found) = lines::grouped(lane) else {
495        return sweep;
496    };
497    let groups: Vec<Group> = found
498        .into_iter()
499        .map(|(factor, held)| {
500            let (placed, summed) = match direct_is_cheaper(held.len(), bins, len) {
501                true => (Vec::new(), held),
502                false => lines::split(&held, bins, rate),
503            };
504            let live = match bounded(&placed, &summed, bins) {
505                true => active::window(&factor, grid),
506                false => active::OPEN,
507            };
508            Group {
509                samples: active::evaluated(&[live], &[(extent.start, extent.end)]) as usize,
510                factor,
511                placed,
512                summed,
513                live,
514            }
515        })
516        .collect();
517    let grouped = LanePlan::Grouped { groups, bins };
518    match grouped.chosen_by(len) <= sweep.chosen_by(len) {
519        true => grouped,
520        false => sweep,
521    }
522}
523
524fn bounded(placed: &[Line], summed: &[Line], bins: usize) -> bool {
525    let reach: f64 = placed
526        .iter()
527        .chain(summed)
528        .map(|l| l.amp.re.abs() + l.amp.im.abs())
529        .sum();
530    reach * (bins.max(1) as f64) < 1e300
531}
532
533/// `n*log2(n)` butterflies; a length that is no power of two is Bluestein's three
534/// transforms over the next one past `2n-1`.
535pub fn transform_flops(n: usize) -> u128 {
536    let stages = |m: usize| m as u128 * (m.max(2).trailing_zeros() as u128);
537    match n.is_power_of_two() {
538        true => stages(n),
539        false => 3 * stages((2 * n - 1).next_power_of_two()),
540    }
541}
542
543impl LinePlan {
544    pub fn flops(&self, len: usize) -> u128 {
545        let transforms: u128 = self
546            .placed
547            .iter()
548            .filter(|p| !p.is_empty())
549            .map(|_| transform_flops(self.bins))
550            .sum();
551        let direct: u128 = self
552            .summed
553            .iter()
554            .map(|s| s.len() as u128 * len as u128)
555            .sum();
556        transforms + direct
557    }
558
559    pub fn rule(&self) -> Rule {
560        match (lines::distinct(&self.placed), lines::distinct(&self.summed)) {
561            (_, 0) => Rule::LineSpectrumExact,
562            (0, _) => Rule::LineSpectrumSummed,
563            _ => Rule::LineSpectrumMixed,
564        }
565    }
566}
567
568impl LanePlan {
569    pub fn flops(&self) -> u128 {
570        match self {
571            LanePlan::Grouped { groups, bins } => groups
572                .iter()
573                .map(|g| transformed(g, *bins) + (g.summed.len() as u128 + 1) * g.samples as u128)
574                .sum(),
575            LanePlan::Sweep { evaluated, .. } => *evaluated,
576        }
577    }
578
579    fn chosen_by(&self, len: usize) -> u128 {
580        match self {
581            LanePlan::Grouped { groups, bins } => groups
582                .iter()
583                .map(|g| transformed(g, *bins) + (g.summed.len() as u128 + 1) * len as u128)
584                .sum(),
585            LanePlan::Sweep { atoms, samples, .. } => *atoms as u128 * *samples as u128,
586        }
587    }
588
589    fn swept_flops(&self, len: u128) -> u128 {
590        let terms: u128 = match self {
591            LanePlan::Grouped { groups, .. } => groups
592                .iter()
593                .map(|g| (g.placed.len() + g.summed.len() + 1) as u128)
594                .sum(),
595            LanePlan::Sweep { atoms, .. } => *atoms as u128,
596        };
597        terms * len
598    }
599}
600
601fn transformed(g: &Group, bins: usize) -> u128 {
602    match g.placed.is_empty() {
603        true => 0,
604        false => transform_flops(bins),
605    }
606}
607
608impl Plan {
609    /// Outside it every sample is +0.0 and inside each is its own instant's, so a run over it
610    /// alone holds the same bits; a row reading the whole extent at once answers all of it.
611    pub fn nonzero(&self, rate: u32, extent: Extent) -> Extent {
612        let grid = Grid::of(rate);
613        let lane = |lane: &Lane, taken: &LanePlan| -> Option<Option<Window>> {
614            match taken {
615                LanePlan::Sweep { .. } if lane.modal.is_empty() => Some(active::hull(
616                    &active::windows(lane, grid),
617                    &super::span::absolute(lane, extent, rate),
618                )),
619                LanePlan::Grouped { groups, .. } if groups.iter().all(|g| g.placed.is_empty()) => {
620                    Some(active::hull(
621                        &groups.iter().map(|g| g.live).collect::<Vec<_>>(),
622                        &[(extent.start, extent.end)],
623                    ))
624                }
625                _ => None,
626            }
627        };
628        let found = match self {
629            Plan::Sampled(held) => held
630                .sum
631                .lanes
632                .iter()
633                .zip(&held.lanes)
634                .map(|(l, taken)| lane(l, taken))
635                .collect::<Option<Vec<_>>>()
636                .map(|lanes| lanes.into_iter().flatten().collect::<Vec<_>>()),
637            Plan::Added(parts) => Some(
638                parts
639                    .iter()
640                    .map(|part| part.nonzero(rate, extent))
641                    .filter(|part| !part.is_empty())
642                    .map(|part| (part.start, part.end))
643                    .collect(),
644            ),
645            _ => None,
646        };
647        match found {
648            None => extent,
649            Some(spans) => spans
650                .into_iter()
651                .map(|(a, b)| Extent::new(a, b))
652                .fold(Extent::NOWHERE, Extent::hull)
653                .intersect(extent),
654        }
655    }
656
657    /// What two extents cut to one may still differ in.
658    pub fn route(&self) -> Vec<u64> {
659        let lines = |held: &[Vec<Line>]| held.iter().map(|l| l.len() as u64).collect::<Vec<_>>();
660        match self {
661            Plan::Spectrum(_) => vec![0],
662            Plan::Lines(found) => [vec![1], lines(&found.placed), lines(&found.summed)].concat(),
663            Plan::Sampled(held) => {
664                let mut out = vec![2, held.rule as u64];
665                for lane in &held.lanes {
666                    match lane {
667                        LanePlan::Sweep { .. } => out.push(0),
668                        LanePlan::Grouped { groups, .. } => {
669                            out.push(1 + groups.len() as u64);
670                            for g in groups {
671                                out.extend([g.placed.len() as u64, g.summed.len() as u64]);
672                            }
673                        }
674                    }
675                }
676                out
677            }
678            Plan::Point { .. } => vec![3],
679            Plan::Added(parts) => {
680                let mut out = vec![4, parts.len() as u64];
681                parts.iter().for_each(|part| out.extend(part.route()));
682                out
683            }
684        }
685    }
686
687    pub fn flops(&self, rate: u32, extent: Extent) -> u128 {
688        let len = extent.len();
689        match self {
690            Plan::Spectrum(_) => transform_flops(len),
691            Plan::Lines(found) => found.flops(len),
692            Plan::Sampled(held) => held.lanes.iter().map(|lane| lane.flops()).sum(),
693            Plan::Point { written, .. } => {
694                point_flops(&written.body, Grid::of(rate), (extent.start, extent.end))
695            }
696            Plan::Added(parts) => parts.iter().map(|part| part.flops(rate, extent)).sum(),
697        }
698    }
699
700    pub fn alias_flops(&self, rate: u32, extent: Extent) -> u128 {
701        self.alias_flops_at(rate, extent, super::ALIAS_OVERSAMPLE)
702    }
703
704    /// The multiple a reading names need not be the label's `ALIAS_OVERSAMPLE`. Every component
705    /// is scored, so every component's reference is paid for.
706    pub fn alias_flops_at(&self, rate: u32, extent: Extent, oversample: usize) -> u128 {
707        let reference = (extent.len() * oversample) as u128;
708        match self {
709            Plan::Point { written, .. } => {
710                let finer = oversample as i64;
711                point_flops(
712                    &written.body,
713                    Grid::finer(rate, oversample),
714                    (extent.start * finer, extent.end * finer),
715                )
716            }
717            Plan::Sampled(held) if held.rule == Rule::PointSampled => held
718                .lanes
719                .iter()
720                .map(|lane| lane.swept_flops(reference))
721                .sum(),
722            Plan::Added(parts) => parts
723                .iter()
724                .map(|part| part.alias_flops_at(rate, extent, oversample))
725                .sum(),
726            Plan::Spectrum(_) | Plan::Lines(_) | Plan::Sampled(_) => 0,
727        }
728    }
729
730    pub fn rule(&self) -> Rule {
731        match self {
732            Plan::Spectrum(_) => Rule::InverseSpectrum,
733            Plan::Lines(found) => found.rule(),
734            Plan::Sampled(held) => held.rule,
735            Plan::Point { .. } => Rule::PointSampled,
736            Plan::Added(_) => Rule::Added,
737        }
738    }
739}