Skip to main content

sva_samples/collapse/
run.rs

1// Concern: sums one run of lines at an instant and bounds that sum's rounding | Non-concern: which lines form a run (sva-formula) | IO: (&Run, t) -> C64; (&Run) -> f64
2
3use std::f64::consts::TAU;
4
5use sva_formula::{C64, Mirror, Run};
6
7/// Horner's rule in the rotor, whose turns are reduced from the exact product.
8pub fn at(run: &Run, t: f64) -> C64 {
9    let mut out = [C64::ZERO];
10    each(run, &[t], &mut out);
11    out[0]
12}
13
14/// `at` at each of `t`: every instant's sum takes the operations `at` does, in its order.
15pub fn each(run: &Run, t: &[f64], out: &mut [C64]) {
16    each_at(Width::widest(), run, t, out);
17}
18
19pub fn each_at(width: Width, run: &Run, t: &[f64], out: &mut [C64]) {
20    for (t, out) in t.chunks(LANES).zip(out.chunks_mut(LANES)) {
21        side_by_side(width, run, t, out);
22    }
23}
24
25/// Instants a ladder instruction steps: an instant's IEEE operations and their order are the
26/// same at every width, none fused.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct Width(Kind);
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31enum Kind {
32    /// SSE2 on x86-64, SIMD128 on wasm32.
33    Built,
34    #[cfg(target_arch = "x86_64")]
35    Avx2,
36}
37
38impl Width {
39    pub const BUILT: Width = Width(Kind::Built);
40
41    pub fn widest() -> Width {
42        #[cfg(target_arch = "x86_64")]
43        if std::arch::is_x86_feature_detected!("avx2") {
44            return Width(Kind::Avx2);
45        }
46        Width::BUILT
47    }
48
49    pub fn all() -> Vec<Width> {
50        let mut all = vec![Width::BUILT];
51        if Width::widest() != Width::BUILT {
52            all.push(Width::widest());
53        }
54        all
55    }
56}
57
58/// Instants summed side by side: one instant's sum waits on its own last product alone.
59const LANES: usize = 64;
60
61fn side_by_side(width: Width, run: &Run, t: &[f64], out: &mut [C64]) {
62    let n = t.len();
63    let (mut rotor, mut z) = ([C64::ZERO; LANES], [C64::ZERO; LANES]);
64    for ((rotor, z), t) in rotor.iter_mut().zip(&mut z).zip(t) {
65        let step = turns(run.step, *t);
66        let start = wrap(turns(run.offset, *t) + wrap(run.first as f64 * step));
67        *rotor = cis(start);
68        *z = match (run.amps.len(), start.to_bits() == step.to_bits()) {
69            (1, _) => C64::ONE,
70            (_, true) => *rotor,
71            (_, false) => cis(step),
72        };
73    }
74    let mut up = [C64::ZERO; LANES];
75    horner(width, &run.amps, &z[..n], &mut up[..n]);
76    let mut down = [C64::ZERO; LANES];
77    if let Mirror::Held(amps) = &run.mirror {
78        z.iter_mut().for_each(|z| *z = z.conj());
79        horner(width, amps, &z[..n], &mut down[..n]);
80    }
81    for (i, out) in out.iter_mut().enumerate() {
82        let up = rotor[i] * up[i];
83        *out = match &run.mirror {
84            Mirror::None => up,
85            Mirror::Conjugate => up + up.conj(),
86            Mirror::Held(_) => up + rotor[i].conj() * down[i],
87        };
88    }
89}
90
91/// A conjugate mirror takes none of its own.
92pub fn steps(run: &Run) -> usize {
93    match &run.mirror {
94        Mirror::Held(amps) => run.amps.len() + amps.len(),
95        Mirror::None | Mirror::Conjugate => run.amps.len(),
96    }
97}
98
99/// `|at(run, t) - S(t)|` at every finite `t`, given `sin` and `cos` within `2^-52`.
100pub fn bound(run: &Run) -> f64 {
101    let rotor = (32.0 + 12.0 * (run.first as f64).abs()) * U;
102    let term =
103        |j: usize| (rotor + j as f64 * STEP_ROTOR + (j + 1) as f64 * PRODUCT_AND_SUM).exp_m1();
104    let ladder = |amps: &[C64]| -> (f64, f64) {
105        amps.iter()
106            .enumerate()
107            .fold((0.0, 0.0), |(err, sum), (j, a)| {
108                (err + a.abs() * term(j), sum + a.abs())
109            })
110    };
111    let (up, up_sum) = ladder(&run.amps);
112    let (down, down_sum) = match &run.mirror {
113        Mirror::None => (0.0, 0.0),
114        Mirror::Conjugate => (up, up_sum),
115        Mirror::Held(amps) => ladder(amps),
116    };
117    let err = (1.0 + U) * (up + down) + U * (up_sum + down_sum);
118    err * (1.0 + 4.0 * (run.len() + 2) as f64 * U)
119}
120
121/// The largest magnitude the exact sum reaches, rounded up.
122pub fn reach(run: &Run) -> f64 {
123    let sum = |amps: &[C64]| amps.iter().map(|a| a.abs()).sum::<f64>();
124    let mirrored = match &run.mirror {
125        Mirror::None => 0.0,
126        Mirror::Conjugate => sum(&run.amps),
127        Mirror::Held(amps) => sum(amps),
128    };
129    (sum(&run.amps) + mirrored) * (1.0 + 2.0 * (run.len() + 1) as f64 * U)
130}
131
132const U: f64 = f64::EPSILON / 2.0;
133
134const STEP_ROTOR: f64 = 24.0 * U;
135
136const PRODUCT_AND_SUM: f64 = (2.236_067_977_499_8 + 1.0) * U;
137
138fn horner(width: Width, amps: &[C64], z: &[C64], out: &mut [C64]) {
139    match width.0 {
140        #[cfg(target_arch = "x86_64")]
141        // SAFETY: `widest` builds this kind once the CPU reports AVX2.
142        Kind::Avx2 => unsafe { ladder_avx2(amps, z, out) },
143        Kind::Built => ladder(amps, z, out),
144    }
145}
146
147/// FMA stays off, so no product and sum can fuse.
148#[cfg(target_arch = "x86_64")]
149#[target_feature(enable = "avx2")]
150fn ladder_avx2(amps: &[C64], z: &[C64], out: &mut [C64]) {
151    ladder(amps, z, out);
152}
153
154#[inline(always)]
155fn ladder(amps: &[C64], z: &[C64], out: &mut [C64]) {
156    let n = z.len();
157    let mut rest = amps.iter().rev();
158    let last = rest.next().copied().unwrap_or(C64::ZERO);
159    let (mut zr, mut zi) = ([0.0; LANES], [0.0; LANES]);
160    for ((zr, zi), z) in zr.iter_mut().zip(&mut zi).zip(z) {
161        (*zr, *zi) = (z.re, z.im);
162    }
163    let (mut re, mut im) = ([last.re; LANES], [last.im; LANES]);
164    for a in rest {
165        let lanes = re[..n].iter_mut().zip(&mut im[..n]);
166        for ((re, im), (zr, zi)) in lanes.zip(zr[..n].iter().zip(&zi[..n])) {
167            let (r, i) = (*re, *im);
168            *re = (r * zr - i * zi) + a.re;
169            *im = (r * zi + i * zr) + a.im;
170        }
171    }
172    for ((out, re), im) in out.iter_mut().zip(re).zip(im) {
173        *out = C64::new(re, im);
174    }
175}
176
177fn turns(x: f64, t: f64) -> f64 {
178    let p = x * t;
179    let e = x.mul_add(t, -p);
180    wrap(wrap(p) + wrap(e))
181}
182
183fn wrap(x: f64) -> f64 {
184    x - x.round_ties_even()
185}
186
187fn cis(turns: f64) -> C64 {
188    let (sin, cos) = (TAU * turns).sin_cos();
189    C64::new(cos, sin)
190}