use chrono::{DateTime, Utc};
use rayon::prelude::*;
use crate::{Error, Result, time::posix_nanos, value::Value};
pub struct Knots<'a, V> {
pub t0: i128,
pub h: f64,
pub y: Vec<f64>,
pub offsets: Vec<i128>,
pub offsets64: Option<Vec<i64>>,
pub originals: Vec<&'a V>,
pub centre: f64,
pub scale: f64,
pub min: f64,
pub max: f64,
}
const PARALLEL_FROM: usize = 1 << 14;
impl<'a, V: Value> Knots<'a, V> {
pub fn new(len: usize, timestamp: impl Fn(usize) -> DateTime<Utc> + Sync, value: impl Fn(usize) -> &'a V + Sync, parallel: bool) -> Result<Self> {
if len == 0 {
return Err(Error::NoPoints);
}
let sample = |i: usize| {
let v = value(i);
v.to_finite_f64().map(|y| (posix_nanos(timestamp(i)), y, v)).ok_or(i)
};
let out_of_range = |i: usize| Error::ValueOutOfRange { timestamp: timestamp(i) };
let parallel = parallel && len >= PARALLEL_FROM;
let mut samples = if parallel {
let mut samples = Vec::new();
(0..len).into_par_iter().map(|i| sample(i).unwrap_or_else(|_| (0, f64::NAN, value(i)))).collect_into_vec(&mut samples);
if let Some(i) = samples.par_iter().position_first(|&(_, y, _)| y.is_nan()) {
return Err(out_of_range(i));
}
samples
} else {
let mut samples = Vec::with_capacity(len);
for i in 0..len {
samples.push(sample(i).map_err(out_of_range)?);
}
samples
};
if !samples.is_sorted_by_key(|&(n, _, _)| n) {
if parallel {
samples.par_sort_by_key(|&(n, _, _)| n);
} else {
samples.sort_by_key(|&(n, _, _)| n);
}
}
samples.dedup_by(|later, kept| {
let same = later.0 == kept.0;
if same {
std::mem::swap(later, kept);
}
same
});
let distinct = samples;
let raw_values: Vec<f64> = distinct.iter().map(|&(_, y, _)| y).collect();
let (min, max) = raw_values.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| (lo.min(v), hi.max(v)));
let half_width = max / 2.0 - min / 2.0;
let scale = if half_width > 0.0 { half_width } else { 1.0 };
let centre = min / 2.0 + max / 2.0;
let t0 = distinct[0].0;
let offsets: Vec<i128> = distinct.iter().map(|&(n, _, _)| n - t0).collect();
let n = offsets.len();
let offsets64 = if offsets[n - 1] <= crate::kernel::I64_SPAN { offsets.iter().map(|&o| i64::try_from(o).ok()).collect() } else { None };
#[allow(clippy::cast_precision_loss)] let h = if n > 1 { offsets[n - 1] as f64 / (n - 1) as f64 } else { 1.0 };
let y = raw_values.iter().map(|&v| (v - centre) / scale).collect();
let originals = distinct.into_iter().map(|(_, _, v)| v).collect();
Ok(Self { t0, h, y, offsets, offsets64, originals, centre, scale, min, max })
}
pub const fn len(&self) -> usize {
self.offsets.len()
}
pub fn data(&self) -> crate::kernel::Data<'_> {
crate::kernel::Data { offsets: &self.offsets, y: &self.y, inv_h: 1.0 / self.h }
}
pub fn y_range(&self) -> (f64, f64) {
self.y.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| (lo.min(v), hi.max(v)))
}
}
#[cfg(test)]
mod tests {
use bigdecimal::BigDecimal;
use chrono::TimeZone;
use super::*;
fn at(secs: i64) -> DateTime<Utc> {
Utc.timestamp_opt(secs, 0).single().expect("valid")
}
fn from_slices<'a, V: Value>(ts: &[DateTime<Utc>], vs: &'a [V], parallel: bool) -> Result<Knots<'a, V>> {
assert_eq!(ts.len(), vs.len());
Knots::new(ts.len(), |i| ts[i], |i| &vs[i], parallel)
}
#[test]
fn sorts_and_keeps_the_last_duplicate() {
let ts = [at(20), at(0), at(10), at(10)];
let vs = [2.0, 0.0, 1.0, 9.0];
let knots = from_slices(&ts, &vs, false).expect("knots");
assert_eq!(knots.len(), 3);
assert_eq!(knots.offsets, vec![0, 10_000_000_000, 20_000_000_000]);
assert_eq!(knots.h, 10_000_000_000.0);
assert_eq!(knots.originals.iter().map(|v| **v).collect::<Vec<_>>(), vec![0.0, 9.0, 2.0]);
let restored: Vec<f64> = knots.y.iter().map(|y| y * knots.scale + knots.centre).collect();
assert_eq!(restored, vec![0.0, 9.0, 2.0]);
}
#[test]
fn constant_and_single_inputs_normalise() {
let knots = from_slices(&[at(0), at(5)], &[7.0, 7.0], false).expect("knots");
assert_eq!(knots.y, vec![0.0, 0.0]);
assert_eq!(knots.centre, 7.0);
let knots = from_slices(&[at(0)], &[3.0], false).expect("knots");
assert_eq!(knots.offsets, vec![0]);
}
#[test]
fn rejects_empty_and_unrepresentable() {
let none: [f64; 0] = [];
assert!(matches!(from_slices(&[], &none, false), Err(Error::NoPoints)));
let huge: BigDecimal = "1e999".parse().expect("parse");
assert!(matches!(from_slices(&[at(0)], &[huge], false), Err(Error::ValueOutOfRange { .. })));
assert!(matches!(from_slices(&[at(0)], &[f64::NAN], false), Err(Error::ValueOutOfRange { .. })));
}
#[test]
fn extreme_magnitudes_do_not_overflow_the_scale() {
let knots = from_slices(&[at(0), at(1)], &[f64::MAX, -f64::MAX], false).expect("knots");
assert!(knots.scale.is_finite() && knots.centre.is_finite());
assert_eq!(knots.y, vec![1.0, -1.0]);
}
#[test]
fn parallel_preparation_matches_serial() {
let n = 3 * PARALLEL_FROM;
let shuffled = |i: usize| i64::try_from((i * 7_919) % n / 4 * 4).expect("small");
let ts: Vec<_> = (0..n).map(|i| at(shuffled(i))).collect();
let vs: Vec<f64> = (0..n).map(|i| (f64::from(u32::try_from(i).expect("small")) * 0.37).sin()).collect();
let serial = from_slices(&ts, &vs, false).expect("knots");
let parallel = from_slices(&ts, &vs, true).expect("knots");
assert_eq!(serial.len(), n / 4);
assert_eq!(parallel.offsets, serial.offsets);
assert_eq!(parallel.y, serial.y);
assert!(parallel.originals.iter().zip(&serial.originals).all(|(a, b)| std::ptr::eq(*a, *b)), "the same input wins every instant");
let (first, later) = (1, 7);
assert!(ts[first] > ts[later], "the later input must be the earlier instant");
let mut bad = vs.clone();
bad[first] = f64::NAN;
bad[later] = f64::INFINITY;
let expected = Err(Error::ValueOutOfRange { timestamp: ts[first] });
assert_eq!(from_slices(&ts, &bad, true).map(|k| k.len()), expected);
assert_eq!(from_slices(&ts, &bad, false).map(|k| k.len()), expected);
}
}