use crate::iteration::comprehension::cardinality::{Interval, MeasureName, ProductMeasure};
use crate::numeric::special::{
inv_regularized_beta, inv_regularized_gamma_p, normal_cdf, probit, regularized_beta,
regularized_gamma_p,
};
#[derive(Debug, Clone, PartialEq)]
pub enum AxisMeasure {
Uniform,
Named {
name: MeasureName,
params: Vec<f64>,
},
}
impl AxisMeasure {
pub fn from_product(measure: &ProductMeasure, axis: usize) -> Result<Self, String> {
match measure {
ProductMeasure::Uniform => Ok(AxisMeasure::Uniform),
ProductMeasure::Named(name) => Self::named(*name, &[]),
ProductMeasure::Product(children) => match children.get(axis) {
Some(child) => Self::from_product(child, 0),
None => Err(format!(
"product measure has {} axes; axis {axis} requested",
children.len()
)),
},
}
}
pub fn named(name: MeasureName, params: &[f64]) -> Result<Self, String> {
Ok(AxisMeasure::Named {
name,
params: name.resolve_params(params)?,
})
}
pub fn cdf(&self, x: f64) -> Option<f64> {
let AxisMeasure::Named { name, params } = self else {
return None;
};
if x.is_nan() {
return Some(f64::NAN);
}
Some(match name {
MeasureName::Normal => normal_cdf((x - params[0]) / params[1]),
MeasureName::Exponential => {
if x <= 0.0 {
0.0
} else {
1.0 - (-params[0] * x).exp()
}
}
MeasureName::Pareto => {
let (scale, shape) = (params[0], params[1]);
if x <= scale {
0.0
} else {
1.0 - (scale / x).powf(shape)
}
}
MeasureName::Beta => regularized_beta(x, params[0], params[1]),
MeasureName::LogNormal => {
if x <= 0.0 {
0.0
} else {
normal_cdf((x.ln() - params[0]) / params[1])
}
}
MeasureName::Gamma => {
if x <= 0.0 {
0.0
} else {
regularized_gamma_p(params[0], x / params[1])
}
}
MeasureName::Uniform01 => x.clamp(0.0, 1.0),
})
}
pub fn quantile(&self, p: f64) -> Option<f64> {
let AxisMeasure::Named { name, params } = self else {
return None;
};
Some(match name {
MeasureName::Normal => params[0] + params[1] * probit(p),
MeasureName::Exponential => -(1.0 - p).ln() / params[0],
MeasureName::Pareto => params[0] / (1.0 - p).powf(1.0 / params[1]),
MeasureName::Beta => inv_regularized_beta(p, params[0], params[1]),
MeasureName::LogNormal => (params[0] + params[1] * probit(p)).exp(),
MeasureName::Gamma => params[1] * inv_regularized_gamma_p(p, params[0]),
MeasureName::Uniform01 => p.clamp(0.0, 1.0),
})
}
pub fn map_unit(&self, u: f64, interval: &Interval) -> f64 {
let x = match self.cdf(interval.lo) {
None => interval.lo + u * (interval.hi - interval.lo),
Some(f_lo) => {
let f_hi = self.cdf(interval.hi).unwrap_or(1.0);
let p = f_lo + u * (f_hi - f_lo);
let q = self.quantile(p).unwrap_or(interval.lo);
if q.is_nan() { interval.lo } else { q }
}
};
inside(x.clamp(interval.lo, interval.hi), interval)
}
pub fn endpoint(&self, interval: &Interval, upper: bool) -> f64 {
let x = if upper { interval.hi } else { interval.lo };
inside(x, interval)
}
}
fn inside(x: f64, interval: &Interval) -> f64 {
if interval.lo_open && x == interval.lo && x.is_finite() {
x.next_up().min(interval.hi)
} else if interval.hi_open && x == interval.hi && x.is_finite() {
x.next_down().max(interval.lo)
} else {
x
}
}
#[cfg(test)]
mod tests {
use super::*;
fn named(name: MeasureName, params: &[f64]) -> AxisMeasure {
AxisMeasure::named(name, params).unwrap()
}
#[test]
fn uniform_is_affine() {
let iv = Interval::closed(2.0, 4.0);
assert_eq!(AxisMeasure::Uniform.map_unit(0.0, &iv), 2.0);
assert_eq!(AxisMeasure::Uniform.map_unit(0.5, &iv), 3.0);
assert_eq!(AxisMeasure::Uniform.map_unit(0.25, &iv), 2.5);
}
#[test]
fn every_named_measure_inverts_its_own_cdf() {
let all = [
named(MeasureName::Normal, &[10.0, 2.0]),
named(MeasureName::Exponential, &[0.5]),
named(MeasureName::Pareto, &[2.0, 3.0]),
named(MeasureName::Beta, &[2.0, 5.0]),
named(MeasureName::LogNormal, &[0.0, 0.5]),
named(MeasureName::Gamma, &[2.5, 2.0]),
named(MeasureName::Uniform01, &[]),
];
for m in &all {
for p in [0.05, 0.3, 0.5, 0.8, 0.95] {
let x = m.quantile(p).unwrap();
let back = m.cdf(x).unwrap();
assert!((back - p).abs() < 1e-3, "{m:?} p={p} x={x} back={back}");
}
}
}
#[test]
fn a_named_measure_on_its_full_support_is_its_quantile() {
let m = named(MeasureName::Normal, &[0.0, 1.0]);
let iv = Interval::open(f64::NEG_INFINITY, f64::INFINITY);
assert_eq!(m.map_unit(0.5, &iv), m.quantile(0.5).unwrap());
assert_eq!(m.map_unit(0.975, &iv), m.quantile(0.975).unwrap());
}
#[test]
fn a_named_measure_on_a_narrower_interval_is_restricted_to_it() {
let m = named(MeasureName::Exponential, &[1.0]);
let iv = Interval::closed(0.0, 1.0);
let f1 = 1.0 - (-1.0f64).exp();
for u in [0.0, 0.25, 0.5, 0.9] {
let expected = -(1.0 - u * f1).ln();
let got = m.map_unit(u, &iv);
assert!((got - expected).abs() < 1e-12, "u={u} got={got}");
assert!((0.0..=1.0).contains(&got));
}
}
#[test]
fn open_ends_are_never_returned() {
let iv = Interval::open(0.0, 1.0);
let u = AxisMeasure::Uniform;
assert!(u.map_unit(0.0, &iv) > 0.0);
assert!(u.endpoint(&iv, false) > 0.0);
assert!(u.endpoint(&iv, true) < 1.0);
assert_eq!(u.endpoint(&Interval::closed(0.0, 1.0), true), 1.0);
}
#[test]
fn a_product_measure_is_indexed_per_axis() {
let pm = ProductMeasure::Product(vec![
ProductMeasure::Uniform,
ProductMeasure::Named(MeasureName::Exponential),
]);
assert_eq!(
AxisMeasure::from_product(&pm, 0).unwrap(),
AxisMeasure::Uniform
);
assert_eq!(
AxisMeasure::from_product(&pm, 1).unwrap(),
named(MeasureName::Exponential, &[1.0])
);
assert!(AxisMeasure::from_product(&pm, 2).is_err());
}
#[test]
fn parameters_are_checked_against_the_table() {
assert!(AxisMeasure::named(MeasureName::Normal, &[1.0]).is_err());
assert!(AxisMeasure::named(MeasureName::Normal, &[1.0, f64::NAN]).is_err());
assert_eq!(
AxisMeasure::named(MeasureName::Gamma, &[]).unwrap(),
named(MeasureName::Gamma, &[1.0, 1.0])
);
}
}