#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Provenance {
HardData,
Interpolated,
Defaulted,
Assumed,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Distribution {
Deterministic,
Uniform {
lo: f64,
hi: f64,
},
Triangular {
lo: f64,
mode: f64,
hi: f64,
},
Normal {
mean: f64,
std: f64,
},
LogNormal {
mu: f64,
sigma: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Uncertain {
pub value: f64,
pub distribution: Distribution,
pub provenance: Provenance,
}
impl Uncertain {
pub fn hard(value: f64) -> Self {
Self {
value,
distribution: Distribution::Deterministic,
provenance: Provenance::HardData,
}
}
pub fn defaulted(value: f64) -> Self {
Self {
value,
distribution: Distribution::Deterministic,
provenance: Provenance::Defaulted,
}
}
pub fn assumed(value: f64) -> Self {
Self {
value,
distribution: Distribution::Deterministic,
provenance: Provenance::Assumed,
}
}
pub fn uniform(lo: f64, hi: f64) -> Self {
Self::characterised(0.5 * (lo + hi), Distribution::Uniform { lo, hi })
}
pub fn triangular(lo: f64, mode: f64, hi: f64) -> Self {
Self::characterised(mode, Distribution::Triangular { lo, mode, hi })
}
pub fn normal(mean: f64, std: f64) -> Self {
Self::characterised(mean, Distribution::Normal { mean, std })
}
pub fn lognormal(mu: f64, sigma: f64) -> Self {
Self::characterised(mu.exp(), Distribution::LogNormal { mu, sigma })
}
pub fn from_stats(stats: &crate::foundation::Stats, provenance: Provenance) -> Self {
let distribution = if stats.count < 2 || stats.std == 0.0 {
Distribution::Deterministic
} else {
Distribution::Normal {
mean: stats.mean,
std: stats.std,
}
};
Self {
value: stats.mean,
distribution,
provenance,
}
}
pub fn with_provenance(mut self, provenance: Provenance) -> Self {
self.provenance = provenance;
self
}
fn characterised(value: f64, distribution: Distribution) -> Self {
Self {
value,
distribution,
provenance: Provenance::Interpolated,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::foundation::Stats;
use approx::assert_relative_eq;
#[test]
fn deterministic_constructors_carry_provenance() {
assert_eq!(Uncertain::hard(1.0).provenance, Provenance::HardData);
assert_eq!(Uncertain::defaulted(2.0).provenance, Provenance::Defaulted);
assert_eq!(Uncertain::assumed(3.0).provenance, Provenance::Assumed);
assert_eq!(
Uncertain::hard(1.0).distribution,
Distribution::Deterministic
);
}
#[test]
fn distribution_constructors_set_point_estimate() {
assert_relative_eq!(Uncertain::uniform(2.0, 4.0).value, 3.0); assert_relative_eq!(Uncertain::triangular(1.0, 2.0, 6.0).value, 2.0); assert_relative_eq!(Uncertain::normal(5.0, 1.0).value, 5.0); assert_relative_eq!(Uncertain::lognormal(0.0, 0.5).value, 1.0); assert_eq!(
Uncertain::uniform(2.0, 4.0).provenance,
Provenance::Interpolated
);
}
#[test]
fn from_stats_fits_normal_or_collapses() {
let n = Uncertain::from_stats(&Stats::of(&[1.0, 2.0, 3.0]), Provenance::Interpolated);
assert_relative_eq!(n.value, 2.0);
assert!(matches!(n.distribution, Distribution::Normal { .. }));
let d = Uncertain::from_stats(&Stats::of(&[7.0]), Provenance::HardData);
assert_eq!(d.distribution, Distribution::Deterministic);
assert_relative_eq!(d.value, 7.0);
}
#[test]
fn with_provenance_overrides() {
let u = Uncertain::uniform(0.0, 1.0).with_provenance(Provenance::Assumed);
assert_eq!(u.provenance, Provenance::Assumed);
}
}