use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
enum DispersionSource {
Known,
Estimated,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
struct DispersionWire {
source: DispersionSource,
phi: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "DispersionWire", into = "DispersionWire")]
pub struct Dispersion {
source: DispersionSource,
phi: f64,
}
#[derive(Clone, Copy, Debug, Error, PartialEq)]
pub enum DispersionError {
#[error("dispersion phi must be finite, got {phi}")]
NonFinite { phi: f64 },
#[error("a known dispersion must be strictly positive, got {phi}")]
NonPositiveKnown { phi: f64 },
#[error("an estimated dispersion must be non-negative, got {phi}")]
NegativeEstimate { phi: f64 },
#[error("zero estimated dispersion has no finite reciprocal")]
ZeroHasNoReciprocal,
#[error("the reciprocal of dispersion phi={phi} is not representable as a finite f64")]
ReciprocalNotRepresentable { phi: f64 },
#[error("dispersion multiplier must be finite and strictly positive, got {multiplier}")]
InvalidMultiplier { multiplier: f64 },
#[error(
"rescaling dispersion phi={phi} by multiplier={multiplier} is not representable as a finite f64"
)]
RescaleNotRepresentable { phi: f64, multiplier: f64 },
}
impl Dispersion {
pub const UNIT: Self = Self {
source: DispersionSource::Known,
phi: 1.0,
};
pub const ZERO_ESTIMATE: Self = Self {
source: DispersionSource::Estimated,
phi: 0.0,
};
#[inline]
pub fn known(phi: f64) -> Result<Self, DispersionError> {
if !phi.is_finite() {
return Err(DispersionError::NonFinite { phi });
}
if phi <= 0.0 {
return Err(DispersionError::NonPositiveKnown { phi });
}
Ok(Self {
source: DispersionSource::Known,
phi,
})
}
#[inline]
pub fn estimated(phi: f64) -> Result<Self, DispersionError> {
if !phi.is_finite() {
return Err(DispersionError::NonFinite { phi });
}
if phi < 0.0 {
return Err(DispersionError::NegativeEstimate { phi });
}
let phi = if phi == 0.0 { 0.0 } else { phi };
Ok(Self {
source: DispersionSource::Estimated,
phi,
})
}
#[inline]
pub fn from_reciprocal(value: f64, estimated: bool) -> Result<Self, DispersionError> {
if !value.is_finite() {
return Err(DispersionError::NonFinite { phi: value });
}
if value <= 0.0 {
return Err(DispersionError::NonPositiveKnown { phi: value });
}
let phi = 1.0 / value;
if !phi.is_finite() || phi == 0.0 {
return Err(DispersionError::ReciprocalNotRepresentable { phi: value });
}
if estimated {
Self::estimated(phi)
} else {
Self::known(phi)
}
}
#[inline]
pub const fn phi(self) -> f64 {
self.phi
}
#[inline]
pub const fn is_estimated(self) -> bool {
matches!(self.source, DispersionSource::Estimated)
}
#[inline]
pub const fn is_zero_estimate(self) -> bool {
self.is_estimated() && self.phi == 0.0
}
#[inline]
pub fn reciprocal(self) -> Result<f64, DispersionError> {
if self.phi == 0.0 {
return Err(DispersionError::ZeroHasNoReciprocal);
}
let reciprocal = 1.0 / self.phi;
if !reciprocal.is_finite() {
return Err(DispersionError::ReciprocalNotRepresentable { phi: self.phi });
}
Ok(reciprocal)
}
#[inline]
pub fn sqrt(self) -> f64 {
self.phi.sqrt()
}
}
impl From<Dispersion> for DispersionWire {
fn from(value: Dispersion) -> Self {
Self {
source: value.source,
phi: value.phi,
}
}
}
impl TryFrom<DispersionWire> for Dispersion {
type Error = DispersionError;
fn try_from(value: DispersionWire) -> Result<Self, Self::Error> {
match value.source {
DispersionSource::Known => Self::known(value.phi),
DispersionSource::Estimated => Self::estimated(value.phi),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructors_enforce_distinct_domains() {
assert_eq!(Dispersion::known(2.5).unwrap().phi(), 2.5);
assert_eq!(
Dispersion::estimated(0.0).unwrap(),
Dispersion::ZERO_ESTIMATE
);
assert!(Dispersion::known(0.0).is_err());
assert_eq!(
Dispersion::estimated(-0.0).unwrap().phi().to_bits(),
0.0_f64.to_bits()
);
assert!(Dispersion::estimated(-1.0).is_err());
assert!(Dispersion::known(f64::NAN).is_err());
}
#[test]
fn reciprocal_is_exactly_fallible_at_the_boundary() {
assert_eq!(Dispersion::known(4.0).unwrap().reciprocal(), Ok(0.25));
assert_eq!(
Dispersion::ZERO_ESTIMATE.reciprocal(),
Err(DispersionError::ZeroHasNoReciprocal)
);
}
#[test]
fn sqrt_preserves_zero_boundary() {
assert_eq!(Dispersion::ZERO_ESTIMATE.sqrt(), 0.0);
assert_eq!(Dispersion::estimated(9.0).unwrap().sqrt(), 3.0);
}
#[test]
fn source_is_part_of_identity() {
assert_ne!(
Dispersion::known(1.0).unwrap(),
Dispersion::estimated(1.0).unwrap()
);
assert!(!Dispersion::UNIT.is_estimated());
}
}