gam_terms/
isotropic_scale.rs1use ndarray::Array2;
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13#[repr(transparent)]
18#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
19#[serde(try_from = "f64", into = "f64")]
20pub struct IsotropicScale(f64);
21
22impl IsotropicScale {
23 pub const ONE: Self = Self(1.0);
24
25 pub fn new(value: f64) -> Result<Self, IsotropicScaleError> {
26 if value.is_finite() && value > 0.0 && value.recip().is_finite() {
27 Ok(Self(value))
28 } else {
29 Err(IsotropicScaleError { value })
30 }
31 }
32
33 pub fn get(self) -> f64 {
34 self.0
35 }
36
37 pub fn reciprocal(self) -> f64 {
38 self.0.recip()
39 }
40
41 pub fn to_bits(self) -> u64 {
42 self.0.to_bits()
43 }
44
45 pub fn to_standardized_units(self, value: f64) -> f64 {
47 value * self.reciprocal()
48 }
49
50 pub fn standardize(self, coordinates: &mut Array2<f64>) {
52 let reciprocal = self.reciprocal();
53 coordinates.mapv_inplace(|value| value * reciprocal);
54 }
55}
56
57impl TryFrom<f64> for IsotropicScale {
58 type Error = IsotropicScaleError;
59
60 fn try_from(value: f64) -> Result<Self, Self::Error> {
61 Self::new(value)
62 }
63}
64
65impl From<IsotropicScale> for f64 {
66 fn from(value: IsotropicScale) -> Self {
67 value.get()
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq)]
72pub struct IsotropicScaleError {
73 value: f64,
74}
75
76impl fmt::Display for IsotropicScaleError {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 write!(
79 formatter,
80 "isotropic scale must be positive and finite with a finite reciprocal, got {}",
81 self.value
82 )
83 }
84}
85
86impl std::error::Error for IsotropicScaleError {}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn construction_enforces_the_operational_invariant() {
94 assert_eq!(IsotropicScale::new(1.0), Ok(IsotropicScale::ONE));
95 assert!(IsotropicScale::new(f64::MIN_POSITIVE).is_ok());
96 assert!(IsotropicScale::new(0.0).is_err());
97 assert!(IsotropicScale::new(-1.0).is_err());
98 assert!(IsotropicScale::new(f64::NAN).is_err());
99 assert!(IsotropicScale::new(f64::INFINITY).is_err());
100 assert!(IsotropicScale::new(f64::from_bits(1)).is_err());
101 }
102
103 #[test]
104 fn wire_representation_is_a_checked_scalar() {
105 let encoded = serde_json::to_string(&IsotropicScale::new(2.5).unwrap()).unwrap();
106 assert_eq!(encoded, "2.5");
107 assert_eq!(
108 serde_json::from_str::<IsotropicScale>(&encoded).unwrap(),
109 IsotropicScale::new(2.5).unwrap()
110 );
111 assert!(serde_json::from_str::<IsotropicScale>("[2.5,2.5]").is_err());
112 assert!(serde_json::from_str::<IsotropicScale>("0.0").is_err());
113 assert!(serde_json::from_str::<IsotropicScale>("-1.0").is_err());
114 }
115}