use ordered_float::NotNan;
use crate::quantization::EncodingError;
pub enum P2Quantile<const N: usize = 7> {
Linear(P2QuantileLinear<N>),
Impl(P2QuantileImpl<N>),
}
impl<const N: usize> P2Quantile<N> {
pub fn new(q: f64) -> Result<Self, EncodingError> {
const {
assert!(N >= 5, "P2Quantile requires at least 5 markers");
assert!(N % 2 == 1, "P2Quantile requires an odd number of markers");
};
if q <= 0.0 || q >= 1.0 {
return Err(EncodingError::EncodingError(
"Quantile q must be in (0, 1)".to_string(),
));
}
Ok(Self::Linear(P2QuantileLinear {
quantile: q,
observations: Default::default(),
}))
}
pub fn push(&mut self, x: f64) {
let Ok(x) = NotNan::new(x) else {
return;
};
if !x.is_finite() {
return;
}
match self {
P2Quantile::Linear(linear) => {
linear.observations.push(x);
if linear.observations.len() == N {
*self = P2Quantile::Impl(P2QuantileImpl::new_from_linear(linear));
}
}
P2Quantile::Impl(p2) => p2.push(*x),
}
}
pub fn estimate(self) -> f64 {
match self {
P2Quantile::Linear(linear) => linear.estimate(),
P2Quantile::Impl(p2) => p2.estimate(),
}
}
}
pub struct P2QuantileImpl<const N: usize> {
count: usize,
heights: [f64; N],
n_positions: [f64; N],
n_desired: [f64; N],
target_probabilities: [f64; N],
}
impl<const N: usize> P2QuantileImpl<N> {
fn new_from_linear(linear: &P2QuantileLinear<N>) -> Self {
assert_eq!(linear.observations.len(), N);
let mut buf = linear.observations.clone();
buf.sort_unstable();
let target_probabilities = Self::generate_grid_probabilities(linear.quantile);
let mut heights = [0.0f64; N];
let mut n_positions = [0.0f64; N];
let mut n_desired = [0.0f64; N];
let count_minus_one = (N - 1) as f64;
for i in 0..N {
heights[i] = *buf[i];
n_positions[i] = (i + 1) as f64;
n_desired[i] = 1.0 + target_probabilities[i] * count_minus_one;
}
P2QuantileImpl {
count: N,
heights,
n_positions,
n_desired,
target_probabilities,
}
}
fn estimate(self) -> f64 {
self.heights[N / 2]
}
fn push(&mut self, x: f64) {
self.count += 1;
let k = if x < self.heights[0] {
self.heights[0] = x;
0
} else if x > self.heights[N - 1] {
self.heights[N - 1] = x;
N - 1
} else {
self.find_marker(x)
};
for p in self.n_positions[(k + 1)..N].iter_mut() {
*p += 1.0;
}
let count_minus_one = (self.count - 1) as f64;
update_desired_positions(
&mut self.n_desired,
&self.target_probabilities,
count_minus_one,
);
for i in 1..(N - 1) {
adjust_marker(i, &mut self.heights, &mut self.n_positions, &self.n_desired);
}
}
fn find_marker(&self, x: f64) -> usize {
find_marker_simd::<N>(&self.heights, x)
}
fn generate_grid_probabilities(q: f64) -> [f64; N] {
let mut p = [0.0; N];
let additional_markers_count = (N - 5) / 2;
p[0] = 0.0;
p[1] = q * 0.5;
for i in 0..additional_markers_count {
let factor = 0.7 + 0.3 * (i + 1) as f64 / (additional_markers_count as f64 + 2.0);
p[i + 2] = q * factor;
}
p[N / 2] = q;
for i in 0..additional_markers_count {
let factor = 0.7
+ 0.3 * (additional_markers_count - i) as f64
/ (additional_markers_count as f64 + 2.0);
p[N / 2 + 1 + i] = 1.0 + (q - 1.0) * factor;
}
p[N - 2] = 1.0 + (q - 1.0) * 0.5;
p[N - 1] = 1.0;
p
}
}
#[inline(always)]
fn update_desired_positions<const N: usize>(
n_desired: &mut [f64; N],
target_probabilities: &[f64; N],
count_minus_one: f64,
) {
#[cfg(target_arch = "aarch64")]
{
unsafe { update_desired_neon(n_desired, target_probabilities, count_minus_one) };
}
#[cfg(target_arch = "x86_64")]
{
if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma")
{
unsafe { update_desired_avx2(n_desired, target_probabilities, count_minus_one) };
} else {
update_desired_scalar(n_desired, target_probabilities, count_minus_one);
}
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
update_desired_scalar(n_desired, target_probabilities, count_minus_one);
}
#[cfg(not(target_arch = "aarch64"))]
#[inline(always)]
fn update_desired_scalar<const N: usize>(
n_desired: &mut [f64; N],
target_probabilities: &[f64; N],
count_minus_one: f64,
) {
for i in 0..N {
n_desired[i] = 1.0 + target_probabilities[i] * count_minus_one;
}
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn update_desired_neon<const N: usize>(
n_desired: &mut [f64; N],
target_probabilities: &[f64; N],
count_minus_one: f64,
) {
use std::arch::aarch64::*;
unsafe {
let cm1 = vdupq_n_f64(count_minus_one);
let one = vdupq_n_f64(1.0);
let tp_ptr = target_probabilities.as_ptr();
let nd_ptr = n_desired.as_mut_ptr();
let mut i = 0;
while i + 2 <= N {
let tp = vld1q_f64(tp_ptr.add(i));
let r = vfmaq_f64(one, tp, cm1);
vst1q_f64(nd_ptr.add(i), r);
i += 2;
}
while i < N {
n_desired[i] = 1.0 + target_probabilities[i] * count_minus_one;
i += 1;
}
}
}
#[inline(always)]
fn find_marker_simd<const N: usize>(heights: &[f64; N], x: f64) -> usize {
#[cfg(target_arch = "aarch64")]
{
unsafe { find_marker_neon(heights, x) }
}
#[cfg(target_arch = "x86_64")]
{
if std::arch::is_x86_feature_detected!("avx2") {
return unsafe { find_marker_avx2(heights, x) };
}
}
#[cfg(not(target_arch = "aarch64"))]
{
find_marker_scalar(heights, x)
}
}
#[cfg(not(target_arch = "aarch64"))]
#[inline(always)]
fn find_marker_scalar<const N: usize>(heights: &[f64; N], x: f64) -> usize {
for (i, &h) in heights.iter().enumerate().take(N).skip(1) {
if x <= h {
return i - 1;
}
}
N - 1
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn find_marker_neon<const N: usize>(heights: &[f64; N], x: f64) -> usize {
use std::arch::aarch64::*;
unsafe {
let x_splat = vdupq_n_f64(x);
let h_ptr = heights.as_ptr().add(1);
let mut count: u64 = 0;
let mut i = 0;
while i + 2 < N {
let h = vld1q_f64(h_ptr.add(i));
let m = vcltq_f64(h, x_splat);
let bits = vandq_u64(m, vdupq_n_u64(1));
count += vaddvq_u64(bits);
i += 2;
}
while i < N - 1 {
if *h_ptr.add(i) < x {
count += 1;
}
i += 1;
}
count as usize
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn find_marker_avx2<const N: usize>(heights: &[f64; N], x: f64) -> usize {
use std::arch::x86_64::*;
unsafe {
let x_splat = _mm256_set1_pd(x);
let h_ptr = heights.as_ptr().add(1);
let mut count: usize = 0;
let mut i = 0;
while i + 4 < N {
let h = _mm256_loadu_pd(h_ptr.add(i));
let m = _mm256_cmp_pd::<_CMP_LT_OQ>(h, x_splat);
count += _mm256_movemask_pd(m).count_ones() as usize;
i += 4;
}
while i < N - 1 {
if *h_ptr.add(i) < x {
count += 1;
}
i += 1;
}
count
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn update_desired_avx2<const N: usize>(
n_desired: &mut [f64; N],
target_probabilities: &[f64; N],
count_minus_one: f64,
) {
use std::arch::x86_64::*;
unsafe {
let cm1 = _mm256_set1_pd(count_minus_one);
let one = _mm256_set1_pd(1.0);
let tp_ptr = target_probabilities.as_ptr();
let nd_ptr = n_desired.as_mut_ptr();
let mut i = 0;
while i + 4 <= N {
let tp = _mm256_loadu_pd(tp_ptr.add(i));
let r = _mm256_fmadd_pd(tp, cm1, one);
_mm256_storeu_pd(nd_ptr.add(i), r);
i += 4;
}
while i < N {
n_desired[i] = 1.0 + target_probabilities[i] * count_minus_one;
i += 1;
}
}
}
fn adjust_marker<const N: usize>(
i: usize,
heights: &mut [f64; N],
n_positions: &mut [f64; N],
n_desired: &[f64; N],
) {
loop {
let di = n_desired[i] - n_positions[i];
if di >= 1.0 && (n_positions[i + 1] - n_positions[i]) > 1.0 {
adjust_step(i, heights, n_positions, 1.0);
} else if di <= -1.0 && (n_positions[i - 1] - n_positions[i]) < -1.0 {
adjust_step(i, heights, n_positions, -1.0);
} else {
break;
}
}
}
fn adjust_step<const N: usize>(
i: usize,
heights: &mut [f64; N],
n_positions: &mut [f64; N],
dsign: f64,
) {
let prev_h = heights[i - 1];
let next_h = heights[i + 1];
let prev_n = n_positions[i - 1];
let next_n = n_positions[i + 1];
let cur_h = heights[i];
let cur_n = n_positions[i];
let denom = next_n - prev_n;
let mut h_par = cur_h;
if denom != 0.0 {
let a = (cur_n - prev_n + dsign) / (next_n - cur_n) * (next_h - cur_h);
let b = (next_n - cur_n - dsign) / (cur_n - prev_n) * (cur_h - prev_h);
h_par = cur_h + (a + b) * dsign / denom;
}
heights[i] = if h_par > prev_h && h_par < next_h && h_par.is_finite() {
h_par
} else if dsign > 0.0 {
cur_h + (next_h - cur_h) / (next_n - cur_n)
} else {
cur_h + (prev_h - cur_h) / (prev_n - cur_n)
};
n_positions[i] += dsign;
}
pub struct P2QuantileLinear<const N: usize> {
quantile: f64,
observations: arrayvec::ArrayVec<NotNan<f64>, N>,
}
impl<const N: usize> P2QuantileLinear<N> {
fn estimate(mut self) -> f64 {
estimate_quantile_from_slice(&mut self.observations, self.quantile)
}
}
fn estimate_quantile_from_slice(observations: &mut [NotNan<f64>], quantile: f64) -> f64 {
if observations.is_empty() {
return 0.0;
}
if observations.len() == 1 {
return *observations[0];
}
observations.sort_unstable();
let k = quantile * (observations.len() as f64 - 1.0);
let lo = k.floor() as usize;
let hi = k.ceil() as usize;
if lo == hi {
*observations[lo]
} else {
let frac = k - lo as f64;
*observations[lo] + frac * (*observations[hi] - *observations[lo])
}
}
#[cfg(test)]
mod tests {
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use rand_distr::{Poisson, StandardNormal, StudentT};
use super::*;
const N: usize = 7;
const COUNT: usize = 10_000;
#[test]
fn test_update_desired_positions_simd_parity() {
fn impl_uses_fma() -> bool {
#[cfg(target_arch = "aarch64")]
{
true
}
#[cfg(target_arch = "x86_64")]
{
std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("fma")
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
{
false
}
}
let fused = impl_uses_fma();
let mut rng = StdRng::seed_from_u64(0xC0FFEE);
for _ in 0..256 {
let target_probabilities: [f64; 7] = std::array::from_fn(|_| rng.random::<f64>());
let count_minus_one: f64 = rng.random_range(0.0..1e6);
let mut got = [0.0f64; 7];
super::update_desired_positions::<7>(&mut got, &target_probabilities, count_minus_one);
let expected: [f64; 7] = std::array::from_fn(|i| {
if fused {
f64::mul_add(target_probabilities[i], count_minus_one, 1.0)
} else {
1.0 + target_probabilities[i] * count_minus_one
}
});
assert_eq!(
got, expected,
"tp={target_probabilities:?} cm1={count_minus_one}",
);
}
}
#[test]
fn test_find_marker_simd_parity() {
let scalar_find = |heights: &[f64; 7], x: f64| -> usize {
for (i, &h) in heights.iter().enumerate().take(7).skip(1) {
if x <= h {
return i - 1;
}
}
6
};
let mut rng = StdRng::seed_from_u64(0xBADF00D);
for _ in 0..256 {
let mut h: [f64; 7] = std::array::from_fn(|_| rng.random_range(-100.0..100.0));
h.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
let mut probes: Vec<f64> = h.to_vec();
for w in h.windows(2) {
probes.push(0.5 * (w[0] + w[1]));
}
probes.push(h[0]);
probes.push(h[6]);
for x in probes {
let got = super::find_marker_simd::<7>(&h, x);
let want = scalar_find(&h, x);
assert_eq!(got, want, "h={h:?} x={x}");
}
}
}
#[test]
fn test_p_square() {
const QUANTILE: f64 = 0.99;
const THEORETICAL_VALUE: f64 = QUANTILE;
const ERROR: f64 = 1e-2;
let mut p2 = P2Quantile::<N>::new(QUANTILE).unwrap();
let mut rng = StdRng::seed_from_u64(42);
let mut data = Vec::with_capacity(COUNT);
for _ in 0..COUNT {
let value = rng.random::<f64>();
data.push(value.try_into().unwrap());
p2.push(value);
}
let p = p2.estimate();
let linear_p = estimate_quantile_from_slice(data.as_mut_slice(), QUANTILE);
assert!((p - linear_p).abs() < ERROR);
assert!((p - THEORETICAL_VALUE).abs() < ERROR);
}
#[test]
fn test_p_square_normal() {
const QUANTILE: f64 = 0.9772;
const THEORETICAL_VALUE: f64 = 2.0;
const ERROR: f64 = 0.1;
let mut p2 = P2Quantile::<N>::new(QUANTILE).unwrap();
let mut rng = StdRng::seed_from_u64(42);
let mut data = Vec::with_capacity(COUNT);
for _ in 0..COUNT {
let value: f64 = rng.sample(StandardNormal);
data.push(value.try_into().unwrap());
p2.push(value);
}
let p = p2.estimate();
let linear_p = estimate_quantile_from_slice(data.as_mut_slice(), QUANTILE);
assert!((p - linear_p).abs() < ERROR);
assert!((p - THEORETICAL_VALUE).abs() < ERROR);
}
#[test]
fn test_p_square_normal_low() {
const QUANTILE: f64 = 0.0228;
const THEORETICAL_VALUE: f64 = -2.0;
const ERROR: f64 = 0.1;
let mut p2 = P2Quantile::<N>::new(QUANTILE).unwrap();
let mut rng = StdRng::seed_from_u64(42);
let mut data = Vec::with_capacity(COUNT);
for _ in 0..COUNT {
let value: f64 = rng.sample(StandardNormal);
data.push(value.try_into().unwrap());
p2.push(value);
}
let p = p2.estimate();
let linear_p = estimate_quantile_from_slice(data.as_mut_slice(), QUANTILE);
assert!((p - linear_p).abs() < ERROR);
assert!((p - THEORETICAL_VALUE).abs() < ERROR);
}
#[test]
fn test_p_square_poisson() {
const QUANTILE: f64 = 0.99;
const THEORETICAL_VALUE: f64 = 6.0;
const ERROR: f64 = 0.3;
let mut p2 = P2Quantile::<N>::new(QUANTILE).unwrap();
let mut rng = StdRng::seed_from_u64(42);
let mut data = Vec::with_capacity(COUNT);
for _ in 0..COUNT {
let value = rng.sample(Poisson::new(2.0).unwrap());
data.push(value.try_into().unwrap());
p2.push(value);
}
let p = p2.estimate();
let linear_p = estimate_quantile_from_slice(data.as_mut_slice(), QUANTILE);
assert!((p - linear_p).abs() < ERROR);
assert!((p - THEORETICAL_VALUE).abs() < ERROR);
}
#[test]
fn test_p_square_student() {
const QUANTILE: f64 = 0.99;
const THEORETICAL_VALUE: f64 = 6.9646;
const ERROR: f64 = 0.69646;
let mut p2 = P2Quantile::<N>::new(QUANTILE).unwrap();
let mut rng = StdRng::seed_from_u64(42);
let mut data = Vec::with_capacity(COUNT);
for _ in 0..COUNT {
let value = rng.sample(StudentT::new(2.0).unwrap());
data.push(value.try_into().unwrap());
p2.push(value);
}
let p = p2.estimate();
let linear_p = estimate_quantile_from_slice(data.as_mut_slice(), QUANTILE);
assert!((p - linear_p).abs() < ERROR);
assert!((p - THEORETICAL_VALUE).abs() < ERROR);
}
#[test]
fn test_p_square_zeros() {
let mut p2 = P2Quantile::<N>::new(0.99).unwrap();
for _ in 0..COUNT {
p2.push(0.0);
}
let p = p2.estimate();
assert_eq!(p, 0.0);
}
#[test]
fn test_p_square_linear() {
let mut p2 = P2Quantile::<N>::new(0.99).unwrap();
p2.push(0.0);
p2.push(0.0);
p2.push(0.0);
let p = p2.estimate();
assert_eq!(p, 0.0);
}
#[test]
fn test_p_square_extended_grid() {
let grid = P2QuantileImpl::<7>::generate_grid_probabilities(0.99);
for i in 1..grid.len() {
assert!(grid[i] > grid[i - 1]);
}
let grid = P2QuantileImpl::<9>::generate_grid_probabilities(0.99);
for i in 1..grid.len() {
assert!(grid[i] > grid[i - 1]);
}
let grid = P2QuantileImpl::<11>::generate_grid_probabilities(0.99);
for i in 1..grid.len() {
assert!(grid[i] > grid[i - 1]);
}
}
}