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 band = Audible::of(profile, rate);
387    let groups = sum.lanes.iter().map(|lane| lines::line_groups(lane, band));
388    let Some(groups) = groups.collect::<Option<Vec<_>>>() else {
389        return Ok(Vec::new());
390    };
391    Ok(groups
392        .into_iter()
393        .flatten()
394        .filter_map(|(factor, held)| Some((Some(factor), lines::Direct::of(&held)?.bound())))
395        .collect())
396}
397
398/// Rows one and two: every atom a line, none of them windowed.
399fn line_plan(
400    sum: &SpectralSum,
401    rate: u32,
402    extent: Extent,
403    profile: &Profile,
404    len: usize,
405    ceiling: f64,
406) -> Result<Option<LinePlan>, CollapseError> {
407    let Some(found) = kept_lines(sum, profile, ceiling)? else {
408        return Ok(None);
409    };
410    let bins = lines::grid(&found.kept, &found.grids, rate, extent.span_secs(rate), len);
411    let split: Vec<(Vec<Line>, Vec<Line>)> = found
412        .kept
413        .iter()
414        .map(|k| match direct_is_cheaper(k.len(), bins, len) {
415            true => (Vec::new(), k.clone()),
416            false => lines::split(k, bins, rate),
417        })
418        .collect();
419    Ok(Some(LinePlan {
420        placed: split.iter().map(|(on, _)| on.clone()).collect(),
421        summed: split.into_iter().map(|(_, off)| off).collect(),
422        dropped: found.dropped,
423        bins,
424        tail_db: found.tail,
425    }))
426}
427
428/// Each lane's lines under the ceiling and over it, before any extent places them.
429pub(super) struct Kept {
430    pub(super) kept: Vec<Vec<Line>>,
431    dropped: Vec<Vec<Line>>,
432    grids: Vec<f64>,
433    tail: Option<f64>,
434}
435
436impl Kept {
437    pub(super) fn dropped(&self) -> &[Vec<Line>] {
438        &self.dropped
439    }
440
441    pub(super) fn tail(&self) -> Option<f64> {
442        self.tail
443    }
444}
445
446pub(super) fn kept_lines(
447    sum: &SpectralSum,
448    profile: &Profile,
449    ceiling: f64,
450) -> Result<Option<Kept>, CollapseError> {
451    let mut per_lane = Vec::with_capacity(sum.lanes.len());
452    let mut grids: Vec<f64> = Vec::new();
453    let mut tail: Option<f64> = None;
454    for lane in &sum.lanes {
455        match lines::of_lane(lane, ceiling, profile.floor(ceiling), profile.half_lsb()) {
456            Some(found) => {
457                if let Some(left) = found.tail_db {
458                    tail = Some(tail.map_or(left, |held: f64| held.max(left)));
459                }
460                grids.extend(found.grids);
461                per_lane.push(found.lines);
462            }
463            None => return Ok(None),
464        }
465    }
466
467    let (mut kept, mut dropped): (Vec<Vec<Line>>, Vec<Vec<Line>>) = (Vec::new(), Vec::new());
468    for found in &per_lane {
469        let (here, gone): (Vec<Line>, Vec<Line>) = found.iter().partition(|l| l.hz.abs() < ceiling);
470        kept.push(here);
471        dropped.push(gone);
472    }
473    // A form with no line at all is silence, not a band every line sat above.
474    if kept.iter().all(Vec::is_empty) && !dropped.iter().all(Vec::is_empty) {
475        let lowest = dropped
476            .iter()
477            .flatten()
478            .map(|l| l.hz.abs())
479            .fold(f64::INFINITY, f64::min);
480        return Err(CollapseError::EmptyBand { ceiling, lowest });
481    }
482    Ok(Some(Kept {
483        kept,
484        dropped,
485        grids,
486        tail,
487    }))
488}
489
490fn lane_plan(lane: &Lane, rate: u32, extent: Extent, len: usize) -> LanePlan {
491    let bins = lines::bins(extent.span_secs(rate), rate);
492    let grid = Grid::of(rate);
493    let spans = super::span::absolute(lane, extent, rate);
494    let sweep = LanePlan::Sweep {
495        atoms: lane.atoms.len(),
496        samples: spans.iter().map(|(from, to)| (to - from) as usize).sum(),
497        evaluated: active::evaluated(&active::windows(lane, grid), &spans),
498    };
499    let Some(found) = lines::grouped(lane) else {
500        return sweep;
501    };
502    let groups: Vec<Group> = found
503        .into_iter()
504        .map(|(factor, held)| {
505            let (placed, summed) = match direct_is_cheaper(held.len(), bins, len) {
506                true => (Vec::new(), held),
507                false => lines::split(&held, bins, rate),
508            };
509            let live = group_window(&factor, &[placed.as_slice(), &summed].concat(), bins, grid);
510            Group {
511                samples: active::evaluated(&[live], &[(extent.start, extent.end)]) as usize,
512                factor,
513                placed,
514                summed,
515                live,
516            }
517        })
518        .collect();
519    let grouped = LanePlan::Grouped { groups, bins };
520    match grouped.chosen_by(len) <= sweep.chosen_by(len) {
521        true => grouped,
522        false => sweep,
523    }
524}
525
526/// Outside it the factor zeroes its lines' sum, where that sum, `bins` times over, is finite.
527pub(super) fn group_window(
528    factor: &SpectralAtom,
529    held: &[Line],
530    bins: usize,
531    grid: Grid,
532) -> Window {
533    let reach: f64 = held.iter().map(|l| l.amp.re.abs() + l.amp.im.abs()).sum();
534    match reach * (bins.max(1) as f64) < 1e300 {
535        true => active::window(factor, grid),
536        false => active::OPEN,
537    }
538}
539
540/// `n*log2(n)` butterflies; a length that is no power of two is Bluestein's three
541/// transforms over the next one past `2n-1`.
542pub fn transform_flops(n: usize) -> u128 {
543    let stages = |m: usize| m as u128 * (m.max(2).trailing_zeros() as u128);
544    match n.is_power_of_two() {
545        true => stages(n),
546        false => 3 * stages((2 * n - 1).next_power_of_two()),
547    }
548}
549
550impl LinePlan {
551    pub fn flops(&self, len: usize) -> u128 {
552        let transforms: u128 = self
553            .placed
554            .iter()
555            .filter(|p| !p.is_empty())
556            .map(|_| transform_flops(self.bins))
557            .sum();
558        let direct: u128 = self
559            .summed
560            .iter()
561            .map(|s| s.len() as u128 * len as u128)
562            .sum();
563        transforms + direct
564    }
565
566    pub fn rule(&self) -> Rule {
567        match (lines::distinct(&self.placed), lines::distinct(&self.summed)) {
568            (_, 0) => Rule::LineSpectrumExact,
569            (0, _) => Rule::LineSpectrumSummed,
570            _ => Rule::LineSpectrumMixed,
571        }
572    }
573}
574
575impl LanePlan {
576    pub fn flops(&self) -> u128 {
577        match self {
578            LanePlan::Grouped { groups, bins } => groups
579                .iter()
580                .map(|g| transformed(g, *bins) + (g.summed.len() as u128 + 1) * g.samples as u128)
581                .sum(),
582            LanePlan::Sweep { evaluated, .. } => *evaluated,
583        }
584    }
585
586    fn chosen_by(&self, len: usize) -> u128 {
587        match self {
588            LanePlan::Grouped { groups, bins } => groups
589                .iter()
590                .map(|g| transformed(g, *bins) + (g.summed.len() as u128 + 1) * len as u128)
591                .sum(),
592            LanePlan::Sweep { atoms, samples, .. } => *atoms as u128 * *samples as u128,
593        }
594    }
595
596    fn swept_flops(&self, len: u128) -> u128 {
597        let terms: u128 = match self {
598            LanePlan::Grouped { groups, .. } => groups
599                .iter()
600                .map(|g| (g.placed.len() + g.summed.len() + 1) as u128)
601                .sum(),
602            LanePlan::Sweep { atoms, .. } => *atoms as u128,
603        };
604        terms * len
605    }
606}
607
608fn transformed(g: &Group, bins: usize) -> u128 {
609    match g.placed.is_empty() {
610        true => 0,
611        false => transform_flops(bins),
612    }
613}
614
615impl Plan {
616    /// Outside it every sample is +0.0 and inside each is its own instant's, so a run over it
617    /// alone holds the same bits; a row reading the whole extent at once answers all of it.
618    pub fn nonzero(&self, rate: u32, extent: Extent) -> Extent {
619        let grid = Grid::of(rate);
620        let lane = |lane: &Lane, taken: &LanePlan| -> Option<Option<Window>> {
621            match taken {
622                LanePlan::Sweep { .. } if lane.modal.is_empty() => Some(active::hull(
623                    &active::windows(lane, grid),
624                    &super::span::absolute(lane, extent, rate),
625                )),
626                LanePlan::Grouped { groups, .. } if groups.iter().all(|g| g.placed.is_empty()) => {
627                    Some(active::hull(
628                        &groups.iter().map(|g| g.live).collect::<Vec<_>>(),
629                        &[(extent.start, extent.end)],
630                    ))
631                }
632                _ => None,
633            }
634        };
635        let found = match self {
636            Plan::Sampled(held) => held
637                .sum
638                .lanes
639                .iter()
640                .zip(&held.lanes)
641                .map(|(l, taken)| lane(l, taken))
642                .collect::<Option<Vec<_>>>()
643                .map(|lanes| lanes.into_iter().flatten().collect::<Vec<_>>()),
644            Plan::Added(parts) => Some(
645                parts
646                    .iter()
647                    .map(|part| part.nonzero(rate, extent))
648                    .filter(|part| !part.is_empty())
649                    .map(|part| (part.start, part.end))
650                    .collect(),
651            ),
652            _ => None,
653        };
654        match found {
655            None => extent,
656            Some(spans) => spans
657                .into_iter()
658                .map(|(a, b)| Extent::new(a, b))
659                .fold(Extent::NOWHERE, Extent::hull)
660                .intersect(extent),
661        }
662    }
663
664    /// What two extents cut to one may still differ in.
665    pub fn route(&self) -> Vec<u64> {
666        let lines = |held: &[Vec<Line>]| held.iter().map(|l| l.len() as u64).collect::<Vec<_>>();
667        match self {
668            Plan::Spectrum(_) => vec![0],
669            Plan::Lines(found) => [vec![1], lines(&found.placed), lines(&found.summed)].concat(),
670            Plan::Sampled(held) => {
671                let mut out = vec![2, held.rule as u64];
672                for lane in &held.lanes {
673                    match lane {
674                        LanePlan::Sweep { .. } => out.push(0),
675                        LanePlan::Grouped { groups, .. } => {
676                            out.push(1 + groups.len() as u64);
677                            for g in groups {
678                                out.extend([g.placed.len() as u64, g.summed.len() as u64]);
679                            }
680                        }
681                    }
682                }
683                out
684            }
685            Plan::Point { .. } => vec![3],
686            Plan::Added(parts) => {
687                let mut out = vec![4, parts.len() as u64];
688                parts.iter().for_each(|part| out.extend(part.route()));
689                out
690            }
691        }
692    }
693
694    pub fn flops(&self, rate: u32, extent: Extent) -> u128 {
695        let len = extent.len();
696        match self {
697            Plan::Spectrum(_) => transform_flops(len),
698            Plan::Lines(found) => found.flops(len),
699            Plan::Sampled(held) => held.lanes.iter().map(|lane| lane.flops()).sum(),
700            Plan::Point { written, .. } => {
701                point_flops(&written.body, Grid::of(rate), (extent.start, extent.end))
702            }
703            Plan::Added(parts) => parts.iter().map(|part| part.flops(rate, extent)).sum(),
704        }
705    }
706
707    pub fn alias_flops(&self, rate: u32, extent: Extent) -> u128 {
708        self.alias_flops_at(rate, extent, super::ALIAS_OVERSAMPLE)
709    }
710
711    /// The multiple a reading names need not be the label's `ALIAS_OVERSAMPLE`. Every component
712    /// is scored, so every component's reference is paid for.
713    pub fn alias_flops_at(&self, rate: u32, extent: Extent, oversample: usize) -> u128 {
714        let reference = (extent.len() * oversample) as u128;
715        match self {
716            Plan::Point { written, .. } => {
717                let finer = oversample as i64;
718                point_flops(
719                    &written.body,
720                    Grid::finer(rate, oversample),
721                    (extent.start * finer, extent.end * finer),
722                )
723            }
724            Plan::Sampled(held) if held.rule == Rule::PointSampled => held
725                .lanes
726                .iter()
727                .map(|lane| lane.swept_flops(reference))
728                .sum(),
729            Plan::Added(parts) => parts
730                .iter()
731                .map(|part| part.alias_flops_at(rate, extent, oversample))
732                .sum(),
733            Plan::Spectrum(_) | Plan::Lines(_) | Plan::Sampled(_) => 0,
734        }
735    }
736
737    pub fn rule(&self) -> Rule {
738        match self {
739            Plan::Spectrum(_) => Rule::InverseSpectrum,
740            Plan::Lines(found) => found.rule(),
741            Plan::Sampled(held) => held.rule,
742            Plan::Point { .. } => Rule::PointSampled,
743            Plan::Added(_) => Rule::Added,
744        }
745    }
746}