extern crate alloc;
use alloc::boxed::Box;
use alice_zip::law::SignalLaw;
use crate::math::u64_f64;
use crate::sketch::DDSketch2048;
pub const DEFAULT_RELATIVE_ACCURACY: f64 = 0.01;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ResidualError {
InvalidAccuracy,
InvalidScale,
}
impl core::fmt::Display for ResidualError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
Self::InvalidAccuracy => "relative accuracy must be finite and in (0, 1)",
Self::InvalidScale => "scale must be finite, positive and normal",
})
}
}
#[cfg(feature = "std")]
impl std::error::Error for ResidualError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PointClass {
Summarised {
residual: f64,
},
OutOfRange,
NonFinite,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResidualDistribution {
pub mean: f64,
pub min: f64,
pub max: f64,
pub max_abs: f64,
pub p50: f64,
pub p90: f64,
pub p99: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct ResidualSummary {
pub law_id: [u8; 32],
pub count: u64,
pub out_of_range: u64,
pub non_finite: u64,
pub relative_accuracy: f64,
pub accurate_range: (f64, f64),
pub outside_accuracy: u64,
pub distribution: Option<ResidualDistribution>,
}
#[derive(Debug, Clone)]
pub struct ResidualSketch<'a> {
law: &'a SignalLaw,
sketch: Box<DDSketch2048>,
scale: f64,
range: (f64, f64),
out_of_range: u64,
non_finite: u64,
outside_accuracy: u64,
}
fn power_of_two_floor(v: f64) -> Option<f64> {
if !v.is_normal() || v <= 0.0 {
return None;
}
Some(f64::from_bits(v.to_bits() & 0x7ff0_0000_0000_0000))
}
fn default_scale(law: &SignalLaw) -> f64 {
power_of_two_floor(law.residual().rms).unwrap_or(1.0)
}
impl<'a> ResidualSketch<'a> {
pub fn new(law: &'a SignalLaw, alpha: f64) -> Result<Self, ResidualError> {
Self::build(law, alpha, default_scale(law))
}
pub fn with_scale(law: &'a SignalLaw, alpha: f64, scale: f64) -> Result<Self, ResidualError> {
let scale = power_of_two_floor(scale).ok_or(ResidualError::InvalidScale)?;
Self::build(law, alpha, scale)
}
fn build(law: &'a SignalLaw, alpha: f64, scale: f64) -> Result<Self, ResidualError> {
if !(alpha > 0.0 && alpha < 1.0) {
return Err(ResidualError::InvalidAccuracy);
}
Ok(Self::unchecked(law, alpha, scale))
}
fn unchecked(law: &'a SignalLaw, alpha: f64, scale: f64) -> Self {
let sketch = Box::new(DDSketch2048::new(alpha));
let range = sketch.accurate_range();
Self {
law,
sketch,
scale,
range,
out_of_range: 0,
non_finite: 0,
outside_accuracy: 0,
}
}
pub fn push(&mut self, x: f64, y: f64) -> PointClass {
let Ok(fx) = self.law.evaluate(x) else {
self.out_of_range += 1;
return PointClass::OutOfRange;
};
let residual = y - fx;
if !residual.is_finite() {
self.non_finite += 1;
return PointClass::NonFinite;
}
let scaled = residual / self.scale;
let m = scaled.abs();
if m != 0.0 && (m < self.range.0 || m > self.range.1) {
self.outside_accuracy += 1;
}
self.sketch.insert(scaled);
PointClass::Summarised { residual }
}
pub fn extend(&mut self, points: &[(f64, f64)]) {
for &(x, y) in points {
self.push(x, y);
}
}
#[must_use]
pub fn quantile(&self, q: f64) -> Option<f64> {
if self.sketch.count() == 0 || !(0.0..=1.0).contains(&q) {
return None;
}
Some(self.sketch.quantile(q) * self.scale)
}
#[must_use]
pub const fn scale(&self) -> f64 {
self.scale
}
#[must_use]
pub fn accurate_range(&self) -> (f64, f64) {
(self.range.0 * self.scale, self.range.1 * self.scale)
}
#[must_use]
pub fn law_id(&self) -> [u8; 32] {
self.law.law_id(&crate::SEMANTICS_ID)
}
#[must_use]
pub fn summary(&self) -> ResidualSummary {
let count = self.sketch.count();
let distribution = (count > 0).then(|| {
let min = self.sketch.min() * self.scale;
let max = self.sketch.max() * self.scale;
let q = |p: f64| self.sketch.quantile(p) * self.scale;
ResidualDistribution {
mean: self.sketch.sum() * self.scale / u64_f64(count),
min,
max,
max_abs: min.abs().max(max.abs()),
p50: q(0.50),
p90: q(0.90),
p99: q(0.99),
}
});
ResidualSummary {
law_id: self.law_id(),
count,
out_of_range: self.out_of_range,
non_finite: self.non_finite,
relative_accuracy: self.sketch.alpha(),
accurate_range: self.accurate_range(),
outside_accuracy: self.outside_accuracy,
distribution,
}
}
}
#[must_use]
pub fn residual_summary(law: &SignalLaw, points: &[(f64, f64)]) -> ResidualSummary {
let mut sketch = ResidualSketch::unchecked(law, DEFAULT_RELATIVE_ACCURACY, default_scale(law));
sketch.extend(points);
sketch.summary()
}