use rayon::prelude::*;
use crate::{
MAX_POLYNOMIAL_DEGREE, Spline, prepare::Knots, time::{Grid, nanos_to_f64}, value::Value
};
pub const MAX_WINDOW: usize = MAX_POLYNOMIAL_DEGREE + 1;
pub const SCALE_FROM: f64 = 256.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Method {
pub window: usize,
pub hold: bool,
pub bounds: Option<(f64, f64)>,
}
impl Method {
pub fn new<V: Value>(spline: Spline, knots: &Knots<'_, V>) -> Self {
let window = spline.min_points().min(knots.len());
let bounds = spline.bounds_factor().map(|b| {
let (lo, hi) = knots.y_range();
let range = hi - lo;
(lo - b * range, hi + b * range)
});
Self { window, hold: matches!(spline, Spline::Cubic), bounds }
}
}
pub trait Nanos: Copy + Ord + Send + Sync + std::ops::Add<Output = Self> + std::ops::Sub<Output = Self> + TryFrom<i128> {
fn to_f64(self) -> f64;
}
impl Nanos for i128 {
#[inline]
fn to_f64(self) -> f64 {
nanos_to_f64(self)
}
}
impl Nanos for i64 {
#[inline]
fn to_f64(self) -> f64 {
#[allow(clippy::cast_precision_loss)] let v = self as f64;
v
}
}
pub const I64_SPAN: i128 = 1 << 62;
#[derive(Clone, Copy)]
pub struct Data<'k, N = i128> {
pub offsets: &'k [N],
pub y: &'k [f64],
pub inv_h: f64,
}
#[inline]
fn diff<N: Nanos>(a: N, b: N, inv_h: f64) -> f64 {
(a - b).to_f64() * inv_h
}
#[derive(Clone, Copy)]
pub struct Window {
start: usize,
len: usize,
weight: [f64; MAX_WINDOW],
}
impl Window {
pub const fn new() -> Self {
Self { start: usize::MAX, len: 0, weight: [0.0; MAX_WINDOW] }
}
fn prepare<N: Nanos>(&mut self, data: Data<'_, N>, start: usize, m: usize) {
if self.start == start && self.len == m {
return;
}
let (o, y) = (&data.offsets[start..start + m], &data.y[start..start + m]);
for (j, (weight, &yj)) in self.weight.iter_mut().zip(y).enumerate() {
let mut denominator = 1.0;
for (k, &ok) in o.iter().enumerate() {
if k != j {
denominator *= diff(o[j], ok, data.inv_h);
}
}
*weight = yj / denominator;
}
self.start = start;
self.len = m;
}
}
#[inline]
#[allow(clippy::many_single_char_names)] pub fn eval<N: Nanos>(data: Data<'_, N>, method: &Method, t: N, p: usize, window: &mut Window) -> f64 {
let Data { offsets, y, inv_h } = data;
let n = offsets.len();
let below = t < offsets[0];
let outside = below || t > offsets[n - 1];
if method.hold && outside {
return if below { y[0] } else { y[n - 1] };
}
let m = method.window;
let start = window_start(n, m, p);
let o = &offsets[start..start + m];
let value = match m {
1 => y[start],
2 => {
let alpha = diff(t, o[0], inv_h) / diff(o[1], o[0], inv_h);
y[start] + alpha * (y[start + 1] - y[start])
}
_ => {
window.prepare(data, start, m);
let mut dt = [0.0; MAX_WINDOW];
let mut s: f64 = 1.0;
for (d, &ok) in dt.iter_mut().zip(o) {
*d = diff(t, ok, inv_h);
s = s.max(d.abs());
}
let scaled = s > SCALE_FROM;
if scaled {
for d in &mut dt[..m] {
*d /= s;
}
}
let mut total = 0.0;
for (j, &weight) in window.weight[..m].iter().enumerate() {
let mut term = weight;
for (k, &d) in dt[..m].iter().enumerate() {
if k != j {
term *= d;
}
}
total += term;
}
if scaled {
for _ in 1..m {
total *= s;
}
}
total
}
};
match method.bounds {
Some((lo, hi)) if outside => value.clamp(lo, hi),
_ => value,
}
}
fn eval_chunk<N: Nanos>(data: Data<'_, N>, grid: &GridOffsets<N>, method: &Method, first: usize, out: &mut [f64]) {
let offsets = data.offsets;
let mut window = Window::new();
let mut p = None;
for (slot, t) in out.iter_mut().zip(grid.from(first)) {
let mut q = p.unwrap_or_else(|| offsets.partition_point(|&o| o <= t));
while q < offsets.len() && offsets[q] <= t {
q += 1;
}
p = Some(q);
*slot = eval(data, method, t, q, &mut window);
}
}
const PARALLEL_CHUNK: usize = 16 * 1024;
#[derive(Clone, Copy)]
struct GridOffsets<N> {
start: i128,
step: i128,
step_n: N,
}
impl<N: Nanos> GridOffsets<N> {
fn from(&self, first: usize) -> impl Iterator<Item = N> {
let step = self.step_n;
let first = N::try_from(self.start + self.step * first as i128).ok();
std::iter::successors(first, move |&o| Some(o + step))
}
}
enum Offsets<'k> {
I64(Data<'k, i64>, GridOffsets<i64>),
I128(Data<'k, i128>, GridOffsets<i128>),
}
impl<'k> Offsets<'k> {
fn new<V: Value>(knots: &'k Knots<'_, V>, grid: &Grid) -> Self {
let (start, step) = (grid.offset_nanos(knots.t0, 0), i128::from(grid.step_nanos));
let last = grid.offset_nanos(knots.t0, grid.len.saturating_sub(1));
let within = |o: i128| (-I64_SPAN..=I64_SPAN).contains(&o);
if let (Some(offsets), true, Ok(step_n)) = (knots.offsets64.as_deref(), within(start) && within(last), i64::try_from(step)) {
return Self::I64(Data { offsets, y: &knots.y, inv_h: 1.0 / knots.h }, GridOffsets { start, step, step_n });
}
Self::I128(knots.data(), GridOffsets { start, step, step_n: step })
}
}
pub fn eval_serial<V: Value>(knots: &Knots<'_, V>, grid: &Grid, method: &Method, out: &mut [f64]) {
match Offsets::new(knots, grid) {
Offsets::I64(data, g) => eval_chunk(data, &g, method, 0, out),
Offsets::I128(data, g) => eval_chunk(data, &g, method, 0, out),
}
}
pub fn eval_parallel<V: Value>(knots: &Knots<'_, V>, grid: &Grid, method: &Method, out: &mut [f64]) {
fn run<N: Nanos>(data: Data<'_, N>, g: &GridOffsets<N>, method: &Method, out: &mut [f64]) {
out.par_chunks_mut(PARALLEL_CHUNK).enumerate().for_each(|(c, chunk)| eval_chunk(data, g, method, c * PARALLEL_CHUNK, chunk));
}
match Offsets::new(knots, grid) {
Offsets::I64(data, g) => run(data, &g, method, out),
Offsets::I128(data, g) => run(data, &g, method, out),
}
}
#[inline]
const fn window_start(n: usize, m: usize, p: usize) -> usize {
let start = p.saturating_sub(m / 2);
if start > n - m { n - m } else { start }
}
const F32_GAP_RATIO: f64 = 1024.0;
pub fn f32_unsafe_windows<V: Value>(knots: &Knots<'_, V>, method: &Method) -> Option<Vec<bool>> {
let m = method.window;
let o = &knots.offsets;
if m < 2 || o.len() < 2 {
return None;
}
let (lo, hi) = (knots.h / F32_GAP_RATIO, knots.h * F32_GAP_RATIO);
let bad: Vec<bool> = o.windows(2).map(|w| !(lo..=hi).contains(&nanos_to_f64(w[1] - w[0]))).collect();
if !bad.contains(&true) {
return None;
}
let mut prefix = Vec::with_capacity(bad.len() + 1);
prefix.push(0_usize);
for &b in &bad {
prefix.push(prefix[prefix.len() - 1] + usize::from(b));
}
Some((0..=o.len() - m).map(|s| prefix[s + m - 1] > prefix[s]).collect())
}
pub fn repair<V: Value>(knots: &Knots<'_, V>, grid: &Grid, method: &Method, out: &mut [f64], unsafe_windows: Option<&[bool]>) -> usize {
let data = knots.data();
let n = data.offsets.len();
match unsafe_windows {
None => out
.par_iter_mut()
.enumerate()
.filter(|(_, v)| !v.is_finite())
.map(|(k, v)| {
let t = grid.offset_nanos(knots.t0, k);
*v = eval(data, method, t, data.offsets.partition_point(|&o| o <= t), &mut Window::new());
})
.count(),
Some(unsafe_windows) => out
.par_chunks_mut(PARALLEL_CHUNK)
.enumerate()
.map(|(c, chunk)| {
let mut window = Window::new();
let mut p = None;
let mut repaired = 0;
for (v, t) in chunk.iter_mut().zip(grid.offsets(knots.t0, c * PARALLEL_CHUNK)) {
let mut q = p.unwrap_or_else(|| data.offsets.partition_point(|&o| o <= t));
while q < n && data.offsets[q] <= t {
q += 1;
}
p = Some(q);
if !v.is_finite() || unsafe_windows[window_start(n, method.window, q)] {
*v = eval(data, method, t, q, &mut window);
repaired += 1;
}
}
repaired
})
.sum(),
}
}
#[cfg(test)]
mod tests {
use super::*;
const fn method(window: usize) -> Method {
Method { window, hold: false, bounds: None }
}
fn at(o: &[i128], y: &[f64], method: &Method, t: i128) -> f64 {
eval(Data { offsets: o, y, inv_h: 0.1 }, method, t, o.partition_point(|&x| x <= t), &mut Window::new())
}
#[test]
fn every_window_passes_through_the_knots() {
let o = [0, 10, 25, 30, 42, 60];
let y = [1.0, -2.0, 0.5, 4.0, 3.0, -1.0];
for window in 2..=o.len() {
for (i, &t) in o.iter().enumerate() {
let got = at(&o, &y, &method(window), t);
assert!((got - y[i]).abs() < 1e-12, "window {window}, knot {i}: {got} vs {}", y[i]);
}
}
}
#[test]
fn one_knot_is_constant() {
for t in [-30, 0, 75] {
assert_eq!(at(&[0], &[2.5], &method(1), t), 2.5);
}
}
#[test]
fn reproduces_polynomials_of_its_degree() {
let o: Vec<i128> = (0..8).map(|i| i * 9 + i * i / 2).collect();
for degree in 1..=5_i32 {
let f = |t: f64| (0..=degree).map(|k| f64::from(k + 1) * (t / 10.0).powi(k)).sum::<f64>() / 10.0;
#[allow(clippy::cast_precision_loss)]
let y: Vec<f64> = o.iter().map(|&t| f(t as f64)).collect();
#[allow(clippy::cast_sign_loss)]
let m = method(degree as usize + 1);
for t in [-13, 2, 17, 33, 50, 79] {
let got = at(&o, &y, &m, t);
#[allow(clippy::cast_precision_loss)]
let expected = f(t as f64);
assert!((got - expected).abs() < 1e-9 * expected.abs().max(1.0), "degree {degree} at {t}: {got} vs {expected}");
}
}
}
#[test]
fn linear_extends_the_edge_segments() {
let o = [0, 10, 20];
let y = [0.0, 1.0, 3.0];
assert!((at(&o, &y, &method(2), -10) - -1.0).abs() < 1e-15);
assert!((at(&o, &y, &method(2), 30) - 5.0).abs() < 1e-15);
assert!((at(&o, &y, &method(2), 5) - 0.5).abs() < 1e-15);
}
#[test]
fn hold_flattens_outside_only() {
let o = [0, 10, 20, 30];
let y = [0.0, 1.0, 8.0, 27.0];
let cubic = Method { window: 4, hold: true, bounds: None };
assert_eq!(at(&o, &y, &cubic, -50), 0.0);
assert_eq!(at(&o, &y, &cubic, 90), 27.0);
assert!((at(&o, &y, &cubic, 15) - 3.375).abs() < 1e-12);
}
#[test]
fn bounds_clamp_extrapolation_only() {
let o = [0, 10, 20];
let y = [0.0, 1.0, 0.0];
let bounded = Method { window: 3, hold: false, bounds: Some((-0.5, 1.5)) };
assert_eq!(at(&o, &y, &bounded, 40), -0.5);
assert!((at(&o, &y, &bounded, 10) - 1.0).abs() < 1e-15);
}
#[test]
fn quadratic_window_takes_one_knot_before_and_two_after() {
let o = [0, 10, 20, 30];
let y = [0.0, 0.0, 0.0, 1.0];
assert!((at(&o, &y, &method(3), 15) - -0.125).abs() < 1e-15);
}
#[test]
fn far_extrapolation_does_not_cancel_infinities() {
let o: Vec<i128> = (0..9).collect();
let y: Vec<f64> = (0..9).map(|i| f64::from(i) / 4.0 - 1.0 + if i == 8 { 0.1 } else { 0.0 }).collect();
let far: i128 = 10_i128.pow(38);
let data = Data { offsets: &o, y: &y, inv_h: 1e3 };
let free = Method { window: 9, hold: false, bounds: None };
let v = eval(data, &free, far, 9, &mut Window::new());
assert!(v.is_finite() && v > 1e290, "{v}");
let bounded = Method { window: 9, hold: false, bounds: Some((-2.0, 2.0)) };
assert_eq!(eval(data, &bounded, far, 9, &mut Window::new()), 2.0, "clamps to the upper bound");
assert_eq!(eval(data, &bounded, -far, 0, &mut Window::new()), 2.0, "an even-degree polynomial rises on both sides");
let unit = Data { offsets: &o, y: &y, inv_h: 1.0 };
let near = eval(unit, &free, 300, 9, &mut Window::new());
let expected: f64 = (0..9_u32).map(|j| y[j as usize] * (0..9_u32).filter(|&k| k != j).map(|k| (300.0 - f64::from(k)) / (f64::from(j) - f64::from(k))).product::<f64>()).sum();
assert!((near - expected).abs() <= 1e-9 * expected.abs(), "{near} vs {expected}");
}
#[test]
fn differences_stay_exact_far_from_the_origin() {
let n: i128 = 10_000_000;
let s: i128 = 1_000_000_000;
let o = [(n - 2) * s, (n - 1) * s];
let y = [0.0, 1.0];
let got = eval(Data { offsets: &o, y: &y, inv_h: 1e-9 }, &method(2), (n - 2) * s + s / 4, 1, &mut Window::new());
assert_eq!(got, 0.25);
let year: i128 = 365 * 86_400 * s;
let o = [0, year, year + 1, 2 * year];
let y = [0.0, 1.0, 1.0, 0.0];
let got = eval(Data { offsets: &o, y: &y, inv_h: 3.0 / 2e16 }, &method(2), year / 2, 1, &mut Window::new());
assert!((got - 0.5).abs() < 1e-15, "{got}");
}
#[test]
fn i64_and_i128_offsets_agree_bit_for_bit() {
let o: Vec<i128> = (0..40_i128).map(|i| i * i * 86_400_000_000_123 + (i * 7_919) % 1_000_003).collect();
let o64: Vec<i64> = o.iter().map(|&x| i64::try_from(x).expect("fits")).collect();
let y: Vec<f64> = (0..40).map(|i| (f64::from(i) * 0.7).sin()).collect();
let inv_h = 39.0 / 1.3e17;
for window in 1..=MAX_WINDOW {
for (hold, bounds) in [(false, None), (true, None), (false, Some((-0.5, 0.75)))] {
let method = Method { window, hold, bounds };
for k in -50..1_000_i128 {
let t = k * 157_000_000_000_017 - 3;
let p = o.partition_point(|&x| x <= t);
let wide = eval(Data { offsets: &o, y: &y, inv_h }, &method, t, p, &mut Window::new());
let narrow = eval(Data { offsets: &o64, y: &y, inv_h }, &method, i64::try_from(t).expect("fits"), p, &mut Window::new());
assert_eq!(wide.to_bits(), narrow.to_bits(), "window {window} at {t}");
}
}
}
}
}