#![allow(clippy::indexing_slicing)]
use super::position_as_f64;
#[derive(Debug, Clone)]
pub struct P2Quantile {
count: u64,
heights: [f64; 5],
positions: [i64; 5],
desired: [f64; 5],
increments: [f64; 5],
}
impl P2Quantile {
#[must_use]
pub fn new(p: f64) -> Self {
Self {
count: 0,
heights: [0.0; 5],
positions: [1, 2, 3, 4, 5],
desired: [
1.0,
2.0_f64.mul_add(p, 1.0),
4.0_f64.mul_add(p, 1.0),
2.0_f64.mul_add(p, 3.0),
5.0,
],
increments: [0.0, p / 2.0, p, f64::midpoint(1.0, p), 1.0],
}
}
pub fn update(&mut self, x: f64) {
if self.count < 5 {
self.bootstrap(x);
return;
}
let k = self.locate_cell(x);
for marker in (k + 1)..5 {
self.positions[marker] += 1;
}
for marker in 0..5 {
self.desired[marker] += self.increments[marker];
}
self.adjust_interior();
self.count += 1;
}
#[must_use]
pub fn value(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
if self.count < 5 {
let last = usize::try_from(self.count).unwrap_or(0).saturating_sub(1);
return self.heights.get(last).copied().unwrap_or(0.0);
}
self.heights[2]
}
#[must_use]
pub const fn count(&self) -> u64 {
self.count
}
fn bootstrap(&mut self, x: f64) {
let slot = usize::try_from(self.count).unwrap_or(0);
if let Some(h) = self.heights.get_mut(slot) {
*h = x;
}
self.count += 1;
if self.count == 5 {
self.heights
.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
}
}
fn locate_cell(&mut self, x: f64) -> usize {
if x < self.heights[0] {
self.heights[0] = x;
return 0;
}
for k in 0..4 {
if x < self.heights[k + 1] {
return k;
}
}
self.heights[4] = x;
3
}
fn adjust_interior(&mut self) {
for i in 1..4 {
let d = self.desired[i] - position_as_f64(self.positions[i]);
let gap_hi = self.positions[i + 1] - self.positions[i];
let gap_lo = self.positions[i] - self.positions[i - 1];
if (d >= 1.0 && gap_hi > 1) || (d <= -1.0 && gap_lo > 1) {
let dir = d.signum();
let candidate = self.parabolic(i, dir);
self.heights[i] =
if self.heights[i - 1] < candidate && candidate < self.heights[i + 1] {
candidate
} else {
self.linear(i, dir)
};
self.positions[i] += i64::from(dir >= 0.0) * 2 - 1;
}
}
}
fn parabolic(&self, i: usize, dir: f64) -> f64 {
let pos_lo = position_as_f64(self.positions[i - 1]);
let pos_mid = position_as_f64(self.positions[i]);
let pos_hi = position_as_f64(self.positions[i + 1]);
let upper =
(pos_mid - pos_lo + dir) * (self.heights[i + 1] - self.heights[i]) / (pos_hi - pos_mid);
let lower =
(pos_hi - pos_mid - dir) * (self.heights[i] - self.heights[i - 1]) / (pos_mid - pos_lo);
(dir / (pos_hi - pos_lo)).mul_add(upper + lower, self.heights[i])
}
fn linear(&self, i: usize, dir: f64) -> f64 {
let neighbor = if dir >= 0.0 { i + 1 } else { i - 1 };
let pos_i = position_as_f64(self.positions[i]);
let pos_neighbor = position_as_f64(self.positions[neighbor]);
self.heights[i] + dir * (self.heights[neighbor] - self.heights[i]) / (pos_neighbor - pos_i)
}
}