use crate::error::StaticError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WeightedSample {
pub length: f64,
pub value: f64,
}
impl WeightedSample {
#[must_use]
pub fn new(length: f64, value: f64) -> Self {
Self { length, value }
}
}
pub fn power_law_mean(samples: &[WeightedSample], p: f64) -> Result<f64, StaticError> {
if samples.is_empty() {
return Err(StaticError::InvalidInput("no samples to upscale".into()));
}
let total: f64 = samples.iter().map(|s| s.length).sum();
if !(total.is_finite() && total > 0.0) {
return Err(StaticError::InvalidInput(format!(
"total sample length must be finite and > 0, got {total}"
)));
}
for s in samples {
if !s.length.is_finite() || s.length < 0.0 {
return Err(StaticError::InvalidInput(format!(
"sample length must be finite and >= 0, got {}",
s.length
)));
}
if p <= 0.0 && !(s.value.is_finite() && s.value > 0.0) {
return Err(StaticError::InvalidInput(format!(
"geometric/harmonic mean needs values > 0, got {}",
s.value
)));
}
}
if p == 0.0 {
let acc: f64 = samples.iter().map(|s| s.length * s.value.ln()).sum();
Ok((acc / total).exp())
} else {
let acc: f64 = samples.iter().map(|s| s.length * s.value.powf(p)).sum();
Ok((acc / total).powf(1.0 / p))
}
}
pub fn arithmetic_mean(samples: &[WeightedSample]) -> Result<f64, StaticError> {
power_law_mean(samples, 1.0)
}
pub fn harmonic_mean(samples: &[WeightedSample]) -> Result<f64, StaticError> {
power_law_mean(samples, -1.0)
}
pub fn geometric_mean(samples: &[WeightedSample]) -> Result<f64, StaticError> {
power_law_mean(samples, 0.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn s(len: f64, v: f64) -> WeightedSample {
WeightedSample::new(len, v)
}
#[test]
fn equal_weight_arithmetic() {
let x = arithmetic_mean(&[s(1.0, 0.2), s(1.0, 0.3)]).unwrap();
assert!((x - 0.25).abs() < 1e-12);
}
#[test]
fn length_weighting() {
let x = arithmetic_mean(&[s(3.0, 0.1), s(1.0, 0.3)]).unwrap();
assert!((x - 0.15).abs() < 1e-12);
}
#[test]
fn harmonic_le_geometric_le_arithmetic() {
let d = [s(1.0, 10.0), s(1.0, 1000.0)];
let h = harmonic_mean(&d).unwrap();
let g = geometric_mean(&d).unwrap();
let a = arithmetic_mean(&d).unwrap();
assert!(h < g && g < a, "h={h} g={g} a={a}");
assert!((g - 100.0).abs() < 1e-9);
assert!((h - 2.0 / (0.1 + 0.001)).abs() < 1e-9);
}
#[test]
fn nonpositive_value_rejected_for_geometric() {
assert!(geometric_mean(&[s(1.0, 0.0)]).is_err());
assert!(harmonic_mean(&[s(1.0, -2.0)]).is_err());
assert!(arithmetic_mean(&[s(1.0, 0.0)]).is_ok());
}
#[test]
fn empty_rejected() {
assert!(arithmetic_mean(&[]).is_err());
}
}