#![allow(clippy::needless_range_loop)]
use std::collections::HashMap;
use rayon::prelude::*;
const N_SUMS: usize = 6;
const DEGREE: usize = 16;
const POINTS: usize = DEGREE + 1;
const BASE_WIDTH: f64 = 4.0;
const NOISE_ULPS: f64 = 32.0;
const MAX_DEPTH: u8 = 6;
const PANEL_TOL: f64 = 1e-14;
const ASYMPTOTE_DEPTH: f64 = 40.0;
const MIN_GROUP_CELLS: usize = 32;
const MIN_TABLED_SHARE: f64 = 0.5;
type Panel = [[f64; POINTS]; N_SUMS];
struct Table {
lo: f64,
n_base: usize,
index: Vec<(u32, u8)>,
panels: Vec<Panel>,
}
pub(crate) struct Group {
pub(crate) row: usize,
pub(crate) n: f64,
pub(crate) sum_o: f64,
sum_inv_o: f64,
sum_log_o: f64,
s_lo: f64,
s_hi: f64,
table: Table,
}
impl Group {
#[inline]
pub(crate) fn eval<const FROM: usize, const M: usize>(&self, s: f64) -> [f64; M] {
debug_assert!(FROM + M <= N_SUMS && (FROM + M <= 3 || FROM <= 2));
let mut out = [0.0; M];
if s < self.s_lo {
out.fill(s.exp() * self.sum_o);
return out;
}
if s > self.s_hi {
let r = (-s).exp() * self.sum_inv_o;
for (j, o) in out.iter_mut().enumerate() {
*o = match FROM + j {
0 => self.n * s + self.sum_log_o + r,
1 => self.n - r,
2 | 4 => r,
_ => -r,
};
}
return out;
}
let t = &self.table;
let pos = (s - t.lo) / BASE_WIDTH;
let base = (pos as usize).min(t.n_base - 1);
let (first, depth) = t.index[base];
let parts = (1usize << depth) as f64;
let within = ((pos - base as f64) * parts).clamp(0.0, parts - 1e-9);
let sub = within as usize;
let x = 2.0 * (within - sub as f64) - 1.0;
let panel = &t.panels[first as usize + sub];
let mut b_value = 0.0;
for j in 0..M {
let f = clenshaw(&panel[FROM + j], x);
out[j] = if FROM + j < 3 { f.exp() } else { f };
if FROM + j == 2 {
b_value = out[j];
}
}
for j in 0..M {
if FROM + j >= 3 {
out[j] *= b_value;
}
}
out
}
}
pub(crate) struct ZeroCells {
pub(crate) groups: Vec<Group>,
pub(crate) subject_groups: Vec<usize>,
pub(crate) loose: Vec<usize>,
pub(crate) subject_loose: Vec<usize>,
}
impl ZeroCells {
pub(crate) fn build(
design: &[f64],
log_offset: &[f64],
fid: &[usize],
n_coef: usize,
) -> Option<Self> {
let n_cells = log_offset.len();
let k = fid.len() - 1;
let mut members: Vec<Vec<usize>> = Vec::new();
let mut subject_groups = vec![0usize; k + 1];
let mut loose = Vec::new();
let mut subject_loose = vec![0usize; k + 1];
let mut tabled = 0usize;
for s in 0..k {
let mut by_row: HashMap<Vec<u64>, Vec<usize>> = HashMap::new();
for r in fid[s]..fid[s + 1] {
let key = design[r * n_coef..(r + 1) * n_coef]
.iter()
.map(|v| v.to_bits())
.collect();
by_row.entry(key).or_default().push(r);
}
let mut subject_loose_cells = Vec::new();
let mut kept: Vec<Vec<usize>> = Vec::new();
for (_, cells) in by_row {
if cells.len() >= MIN_GROUP_CELLS {
tabled += cells.len();
kept.push(cells);
} else {
subject_loose_cells.extend(cells);
}
}
kept.sort_by_key(|c| c[0]);
members.extend(kept);
subject_groups[s + 1] = members.len();
subject_loose_cells.sort_unstable();
loose.extend(subject_loose_cells);
subject_loose[s + 1] = loose.len();
}
if (tabled as f64) < MIN_TABLED_SHARE * n_cells as f64 {
return None;
}
let groups = members
.par_iter()
.map(|cells| {
let logs: Vec<f64> = cells.iter().map(|&r| log_offset[r]).collect();
build_group(cells[0], &logs)
})
.collect();
Some(Self {
groups,
subject_groups,
loose,
subject_loose,
})
}
}
fn build_group(row: usize, log_o: &[f64]) -> Group {
let mut sorted = log_o.to_vec();
sorted.sort_by(f64::total_cmp);
let mut distinct: Vec<(f64, f64)> = Vec::new();
for &l in &sorted {
match distinct.last_mut() {
Some((v, m)) if *v == l => *m += 1.0,
_ => distinct.push((l, 1.0)),
}
}
let lo_o = sorted[0];
let hi_o = sorted[sorted.len() - 1];
let s_lo = -ASYMPTOTE_DEPTH - hi_o;
let s_hi = ASYMPTOTE_DEPTH - lo_o;
let n_base = ((s_hi - s_lo) / BASE_WIDTH).ceil() as usize;
let mut index = Vec::with_capacity(n_base);
let mut panels = Vec::new();
for b in 0..n_base {
let left = s_lo + b as f64 * BASE_WIDTH;
let mut depth = 0u8;
let mut fitted = loop {
let parts = 1usize << depth;
let width = BASE_WIDTH / parts as f64;
let fitted: Vec<(Panel, bool)> = (0..parts)
.map(|p| fit_panel(&distinct, left + p as f64 * width, width))
.collect();
if depth == MAX_DEPTH || fitted.iter().all(|(_, ok)| *ok) {
break fitted;
}
depth += 1;
};
index.push((panels.len() as u32, depth));
panels.extend(fitted.drain(..).map(|(p, _)| p));
}
Group {
row,
n: log_o.len() as f64,
sum_o: compensated(log_o.iter().map(|l| l.exp())),
sum_inv_o: compensated(log_o.iter().map(|l| (-l).exp())),
sum_log_o: compensated(log_o.iter().copied()),
s_lo,
s_hi,
table: Table {
lo: s_lo,
n_base,
index,
panels,
},
}
}
fn fit_panel(distinct: &[(f64, f64)], left: f64, width: f64) -> (Panel, bool) {
let mut values = [[0.0; POINTS]; N_SUMS];
for j in 0..POINTS {
let x = (std::f64::consts::PI * j as f64 / DEGREE as f64).cos();
let sums = direct_sums(distinct, left + 0.5 * width * (x + 1.0));
values[0][j] = sums[0].ln();
values[1][j] = sums[1].ln();
values[2][j] = sums[2].ln();
for f in 3..N_SUMS {
values[f][j] = sums[f] / sums[2];
}
}
let mut panel = [[0.0; POINTS]; N_SUMS];
let mut ok = true;
for f in 0..N_SUMS {
panel[f] = chebyshev_coefficients(&values[f]);
let size = values[f].iter().fold(0.0f64, |m, v| m.max(v.abs()));
let tail = panel[f][DEGREE - 1].abs() + panel[f][DEGREE].abs();
ok &= tail <= PANEL_TOL + NOISE_ULPS * f64::EPSILON * size;
}
(panel, ok)
}
fn direct_sums(distinct: &[(f64, f64)], s: f64) -> [f64; N_SUMS] {
let mut acc = [Neumaier::default(); N_SUMS];
for &(l, m) in distinct {
let v = (s + l).exp();
let w = 1.0 / (1.0 + v);
let u = v * w;
let uw = u * w;
acc[0].add(m * v.ln_1p());
acc[1].add(m * u);
acc[2].add(m * uw);
acc[3].add(m * uw * (w - u));
acc[4].add(m * uw * (w * w - 4.0 * u * w + u * u));
acc[5].add(m * uw * (w * w * w - 11.0 * w * w * u + 11.0 * w * u * u - u * u * u));
}
acc.map(|a| a.total())
}
#[derive(Clone, Copy, Default)]
struct Neumaier {
sum: f64,
carry: f64,
}
impl Neumaier {
fn add(&mut self, x: f64) {
let t = self.sum + x;
if self.sum.abs() >= x.abs() {
self.carry += (self.sum - t) + x;
} else {
self.carry += (x - t) + self.sum;
}
self.sum = t;
}
fn total(self) -> f64 {
self.sum + self.carry
}
}
fn compensated(values: impl Iterator<Item = f64>) -> f64 {
let mut acc = Neumaier::default();
for v in values {
acc.add(v);
}
acc.total()
}
fn chebyshev_coefficients(values: &[f64; POINTS]) -> [f64; POINTS] {
let n = DEGREE as f64;
let mut c = [0.0; POINTS];
for (k, ck) in c.iter_mut().enumerate() {
let mut acc = 0.0;
for (j, &f) in values.iter().enumerate() {
let weight = if j == 0 || j == DEGREE { 0.5 } else { 1.0 };
acc += weight * f * (std::f64::consts::PI * (j * k) as f64 / n).cos();
}
*ck = 2.0 * acc / n;
}
c[0] *= 0.5;
c[DEGREE] *= 0.5;
c
}
#[inline(always)]
fn clenshaw(c: &[f64; POINTS], x: f64) -> f64 {
let two_x = 2.0 * x;
let mut b1 = 0.0;
let mut b2 = 0.0;
for k in (1..POINTS).rev() {
let b0 = c[k] + two_x * b1 - b2;
b2 = b1;
b1 = b0;
}
c[0] + x * b1 - b2
}
#[cfg(test)]
mod tests {
use super::*;
use rand::prelude::*;
use rand::rngs::SmallRng;
fn offsets(n: usize, seed: u64) -> Vec<f64> {
let mut rng = SmallRng::seed_from_u64(seed);
(0..n).map(|_| rng.random_range(6.0..10.6)).collect()
}
#[test]
fn test_group_matches_direct_sums_over_the_whole_range() {
let log_o = offsets(500, 7);
let group = build_group(0, &log_o);
let distinct: Vec<(f64, f64)> = log_o.iter().map(|&l| (l, 1.0)).collect();
let mut worst = [0.0f64; N_SUMS];
let mut s = group.s_lo - 5.0;
while s < group.s_hi + 5.0 {
let want = direct_sums(&distinct, s);
let got = group.eval::<0, 6>(s);
for f in 0..N_SUMS {
let scale = if f < 3 { want[f].abs() } else { want[2] };
worst[f] = worst[f].max((got[f] - want[f]).abs() / scale);
}
s += 0.0137;
}
for (f, w) in worst.iter().enumerate() {
assert!(*w < 1e-13, "sum {f}: worst relative error {w:e}");
}
}
#[test]
fn test_build_skips_a_continuous_design() {
let n = 400;
let log_o = offsets(n, 3);
let mut rng = SmallRng::seed_from_u64(9);
let design: Vec<f64> = (0..n)
.flat_map(|_| [1.0, rng.random_range(-1.0..1.0)])
.collect();
assert!(ZeroCells::build(&design, &log_o, &[0, 200, 400], 2).is_none());
}
#[test]
fn test_build_groups_a_categorical_design() {
let n = 400;
let log_o = offsets(n, 3);
let design: Vec<f64> = (0..n)
.flat_map(|r| [1.0, (r % 3) as f64, if r % 50 == 0 { 0.5 } else { 0.0 }])
.collect();
let zeros = ZeroCells::build(&design, &log_o, &[0, 200, 400], 3).expect("tabled");
assert_eq!(zeros.subject_groups, vec![0, 3, 6]);
assert_eq!(zeros.loose, vec![0, 50, 100, 150, 200, 250, 300, 350]);
let covered: f64 = zeros.groups.iter().map(|g| g.n).sum();
assert_eq!(covered as usize + zeros.loose.len(), n);
}
#[test]
fn test_group_is_continuous_across_the_table_ends() {
let log_o = offsets(300, 11);
let group = build_group(0, &log_o);
let distinct: Vec<(f64, f64)> = log_o.iter().map(|&l| (l, 1.0)).collect();
for edge in [group.s_lo, group.s_hi] {
for s in [edge - 1e-9, edge, edge + 1e-9] {
let want = direct_sums(&distinct, s);
let got = group.eval::<0, 6>(s);
for f in 0..N_SUMS {
let scale = if f < 3 { want[f].abs() } else { want[2] };
assert!(
(got[f] - want[f]).abs() <= 1e-12 * scale,
"sum {f} at s = {s}: {} against {}",
got[f],
want[f]
);
}
}
}
}
#[test]
fn test_group_handles_infinite_and_nan_arguments() {
let group = build_group(0, &offsets(100, 13));
assert!(
group
.eval::<0, 6>(f64::NEG_INFINITY)
.iter()
.all(|&v| v == 0.0)
);
let high = group.eval::<0, 3>(f64::INFINITY);
assert_eq!(high[0], f64::INFINITY);
assert_eq!(high[1], group.n);
assert!(group.eval::<0, 6>(f64::NAN).iter().all(|v| v.is_nan()));
}
}