gam_terms/
isotropic_scale.rs1use ndarray::Array2;
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13#[repr(transparent)]
24#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
25#[serde(transparent)]
26pub struct OriginalUnits(f64);
27
28#[repr(transparent)]
33#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
34#[serde(transparent)]
35pub struct StandardizedUnits(f64);
36
37impl OriginalUnits {
38 pub const fn new(value: f64) -> Self {
39 Self(value)
40 }
41
42 pub const fn original_value(self) -> f64 {
45 self.0
46 }
47}
48
49impl StandardizedUnits {
50 pub const fn new(value: f64) -> Self {
51 Self(value)
52 }
53
54 pub const fn standardized_value(self) -> f64 {
56 self.0
57 }
58}
59
60impl fmt::Display for OriginalUnits {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 fmt::Display::fmt(&self.0, formatter)
63 }
64}
65
66impl fmt::Display for StandardizedUnits {
67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68 fmt::Display::fmt(&self.0, formatter)
69 }
70}
71
72#[repr(transparent)]
77#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
78#[serde(try_from = "f64", into = "f64")]
79pub struct IsotropicScale(f64);
80
81impl IsotropicScale {
82 pub const ONE: Self = Self(1.0);
83
84 pub fn new(value: f64) -> Result<Self, IsotropicScaleError> {
85 if value.is_finite() && value > 0.0 && value.recip().is_finite() {
86 Ok(Self(value))
87 } else {
88 Err(IsotropicScaleError { value })
89 }
90 }
91
92 pub fn get(self) -> f64 {
93 self.0
94 }
95
96 pub fn reciprocal(self) -> f64 {
97 self.0.recip()
98 }
99
100 pub fn to_bits(self) -> u64 {
101 self.0.to_bits()
102 }
103
104 pub fn to_standardized_units(self, value: OriginalUnits) -> StandardizedUnits {
112 StandardizedUnits(value.original_value() * self.reciprocal())
113 }
114
115 pub fn to_original_units(self, value: StandardizedUnits) -> OriginalUnits {
118 OriginalUnits(value.standardized_value() * self.0)
119 }
120
121 pub fn standardize(self, coordinates: &mut Array2<f64>) {
123 let reciprocal = self.reciprocal();
124 coordinates.mapv_inplace(|value| value * reciprocal);
125 }
126}
127
128impl TryFrom<f64> for IsotropicScale {
129 type Error = IsotropicScaleError;
130
131 fn try_from(value: f64) -> Result<Self, Self::Error> {
132 Self::new(value)
133 }
134}
135
136impl From<IsotropicScale> for f64 {
137 fn from(value: IsotropicScale) -> Self {
138 value.get()
139 }
140}
141
142#[derive(Clone, Copy, Debug, PartialEq)]
143pub struct IsotropicScaleError {
144 value: f64,
145}
146
147impl fmt::Display for IsotropicScaleError {
148 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(
150 formatter,
151 "isotropic scale must be positive and finite with a finite reciprocal, got {}",
152 self.value
153 )
154 }
155}
156
157impl std::error::Error for IsotropicScaleError {}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn construction_enforces_the_operational_invariant() {
165 assert_eq!(IsotropicScale::new(1.0), Ok(IsotropicScale::ONE));
166 assert!(IsotropicScale::new(f64::MIN_POSITIVE).is_ok());
167 assert!(IsotropicScale::new(0.0).is_err());
168 assert!(IsotropicScale::new(-1.0).is_err());
169 assert!(IsotropicScale::new(f64::NAN).is_err());
170 assert!(IsotropicScale::new(f64::INFINITY).is_err());
171 assert!(IsotropicScale::new(f64::from_bits(1)).is_err());
172 }
173
174 #[test]
175 fn the_two_frames_round_trip_through_the_only_conversion() {
176 let scale = IsotropicScale::new(4.0).unwrap();
177 let original = OriginalUnits::new(500.0);
178 let standardized = scale.to_standardized_units(original);
179 assert_eq!(standardized, StandardizedUnits::new(125.0));
180 assert_eq!(scale.to_original_units(standardized), original);
181 assert_eq!(
185 scale.to_standardized_units(OriginalUnits::new(125.0)),
186 StandardizedUnits::new(31.25)
187 );
188 }
189
190 #[test]
191 fn wire_representation_is_a_checked_scalar() {
192 let encoded = serde_json::to_string(&IsotropicScale::new(2.5).unwrap()).unwrap();
193 assert_eq!(encoded, "2.5");
194 assert_eq!(
195 serde_json::from_str::<IsotropicScale>(&encoded).unwrap(),
196 IsotropicScale::new(2.5).unwrap()
197 );
198 assert!(serde_json::from_str::<IsotropicScale>("[2.5,2.5]").is_err());
199 assert!(serde_json::from_str::<IsotropicScale>("0.0").is_err());
200 assert!(serde_json::from_str::<IsotropicScale>("-1.0").is_err());
201 }
202}