measurements/
humidity.rs

1//! Types and constants for handling humidity.
2
3use super::measurement::*;
4use crate::{density::Density, pressure::Pressure, temperature::Temperature};
5
6/// The `Humidity` struct can be used to deal with relative humidity
7/// in air in a common way. Relative humidity is an important metric used
8/// in weather forecasts.
9///
10/// Relative humidity (as a ratio and percentage) and conversions between
11/// relative humidity and dewpoint are supported. It also provides calculations
12/// giving vapour pressure and absolute humidity.
13///
14/// Relative humidity gives the ratio of how much moisture the air is
15/// holding to how much moisture it could hold at a given temperature.
16/// Here we use the technical definition of humidity as ratio of the
17/// actual water vapor pressure to the equilibrium vapor pressure
18/// (often called the "saturation" vapor pressure).
19///
20/// For dewpoint calculations, we use the algorithm commonly known as
21/// the Magnus formula, with coefficients derived by Alduchov and
22/// Eskridge (1996), which gives resonable accuracy (vapour pressure
23/// error < 0.2%) for temperatures between 0 deg C, and 50 deg C.
24///
25/// # Example:
26///
27/// ```
28///     //  calculate the dewpoint from the relative humidity
29///     use measurements::{Humidity,Temperature};
30///
31///     let humidity = Humidity::from_percent(85.0);
32///     let temp = Temperature::from_celsius(18.0);
33///     let dewpoint = humidity.as_dewpoint(temp);
34///     println!("At {} humidity, air at {} has a dewpoint of {}", humidity, temp, dewpoint);
35///
36/// ```
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38#[derive(Copy, Clone, Debug, Default)]
39pub struct Humidity {
40    relative_humidity: f64, // expressed as a percentage
41}
42
43impl Humidity {
44    /// Create a new Humidity from a floating point value percentage (i.e. 0.0% to 100.0%)
45    pub fn from_percent(percent: f64) -> Self {
46        Humidity {
47            relative_humidity: percent,
48        }
49    }
50
51    /// Create a new Humidity from a floating point value ratio (i.e. 0.0 to 1.0)
52    pub fn from_ratio(relative_humidity: f64) -> Self {
53        Humidity {
54            relative_humidity: relative_humidity * 100.0,
55        }
56    }
57    /// Convert this relative humidity to a value expressed as a ratio (i.e. 0.0 to 1.0)
58    pub fn as_ratio(&self) -> f64 {
59        self.relative_humidity / 100.0
60    }
61
62    /// Convert this relative humidty to a value expressed as a percentage (i.e. 0.0% to 100.0%)
63    pub fn as_percent(&self) -> f64 {
64        self.relative_humidity
65    }
66
67    /// Calculates Dewpoint from humidity and air temperature using the Magnus-Tetens
68    /// approximation, with coefficients derived by Alduchov and Eskridge (1996). The formulas assume
69    //  standard atmospheric pressure.
70    #[cfg(feature = "std")]
71    pub fn as_dewpoint(&self, temp: Temperature) -> Temperature {
72        let humidity = self.relative_humidity / 100.0;
73        let celsius = temp.as_celsius();
74        let dewpoint: f64 = 243.04 * (humidity.ln() + ((17.625 * celsius) / (243.04 + celsius)))
75            / (17.625 - humidity.ln() - ((17.625 * celsius) / (243.04 + celsius)));
76        Temperature::from_celsius(dewpoint)
77    }
78
79    /// Calculates Dewpoint from humidity and air temperature using the Magnus-Tetens
80    /// approximation, with coefficients derived by Alduchov and Eskridge (1996). The formulas assume
81    //  standard atmospheric pressure.
82    #[cfg(not(feature = "std"))]
83    pub fn as_dewpoint(&self, temp: Temperature) -> Temperature {
84        let humidity = self.relative_humidity / 100.0;
85        let celsius = temp.as_celsius();
86        let humidity_ln = libm::log(humidity);
87        let dewpoint: f64 = 243.04 * (humidity_ln + ((17.625 * celsius) / (243.04 + celsius)))
88            / (17.625 - humidity_ln - ((17.625 * celsius) / (243.04 + celsius)));
89        Temperature::from_celsius(dewpoint)
90    }
91
92    /// Calculates the actual vapour pressure in the air, based on the air temperature and humidity
93    /// at standard atmospheric pressure (1013.25 mb), using the Buck formula (accurate to +/- 0.02%
94    /// between 0 deg C and 50 deg C)
95    #[cfg(feature = "std")]
96    pub fn as_vapor_pressure(&self, temp: Temperature) -> Pressure {
97        let temp = temp.as_celsius();
98        let saturation_vapor_pressure =
99            0.61121 * ((18.678 - (temp / 234.5)) * (temp / (257.14 + temp))).exp();
100        Pressure::from_kilopascals((self.relative_humidity * saturation_vapor_pressure) / 100.0)
101    }
102
103    /// Calculates the actual vapour pressure in the air, based on the air temperature and humidity
104    /// at standard atmospheric pressure (1013.25 mb), using the Buck formula (accurate to +/- 0.02%
105    /// between 0 deg C and 50 deg C)
106    #[cfg(not(feature = "std"))]
107    pub fn as_vapor_pressure(&self, temp: Temperature) -> Pressure {
108        let temp = temp.as_celsius();
109        let saturation_vapor_pressure =
110            0.61121 * libm::exp((18.678 - (temp / 234.5)) * (temp / (257.14 + temp)));
111        Pressure::from_kilopascals((self.relative_humidity * saturation_vapor_pressure) / 100.0)
112    }
113
114    /// Calculates the absolute humidity (i.e. the density of water vapor in the air (kg/m3)), using
115    /// the Ideal Gas Law equation.
116    pub fn as_absolute_humidity(&self, temp: Temperature) -> Density {
117        // use the Ideal Gas Law equation (Density = Pressure / (Temperature * [gas constant
118        // for water vapor= 461.5 (J/kg*Kelvin)]))
119        let density = self.as_vapor_pressure(temp).as_pascals() / (temp.as_kelvin() * 461.5);
120        Density::from_kilograms_per_cubic_meter(density)
121    }
122
123    /// Calculates humidity from dewpoint and air temperature using the Magnus-Tetens
124    /// Approximation, with coefficients derived by Alduchov and Eskridge (1996). The formulas assume
125    //  standard atmospheric pressure.
126    #[cfg(feature = "std")]
127    pub fn from_dewpoint(dewpoint: Temperature, temp: Temperature) -> Humidity {
128        let dewpoint = dewpoint.as_celsius();
129        let temp = temp.as_celsius();
130        let rh = 100.0
131            * (((17.625 * dewpoint) / (243.04 + dewpoint)).exp()
132                / ((17.625 * temp) / (243.04 + temp)).exp());
133        Humidity::from_percent(rh)
134    }
135
136    /// Calculates humidity from dewpoint and air temperature using the Magnus-Tetens
137    /// Approximation, with coefficients derived by Alduchov and Eskridge (1996). The formulas assume
138    //  standard atmospheric pressure.
139    #[cfg(not(feature = "std"))]
140    pub fn from_dewpoint(dewpoint: Temperature, temp: Temperature) -> Humidity {
141        let dewpoint = dewpoint.as_celsius();
142        let temp = temp.as_celsius();
143        let rh = 100.0
144            * (libm::exp((17.625 * dewpoint) / (243.04 + dewpoint))
145                / libm::exp((17.625 * temp) / (243.04 + temp)));
146        Humidity::from_percent(rh)
147    }
148}
149
150impl Measurement for Humidity {
151    fn get_base_units_name(&self) -> &'static str {
152        "%"
153    }
154
155    fn as_base_units(&self) -> f64 {
156        self.relative_humidity
157    }
158
159    fn from_base_units(relative_humidity: f64) -> Self {
160        Self::from_percent(relative_humidity)
161    }
162}
163
164impl ::core::cmp::Eq for Humidity {}
165impl ::core::cmp::PartialEq for Humidity {
166    fn eq(&self, other: &Self) -> bool {
167        self.as_base_units() == other.as_base_units()
168    }
169}
170
171impl ::core::cmp::PartialOrd for Humidity {
172    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
173        self.as_base_units().partial_cmp(&other.as_base_units())
174    }
175}
176
177implement_display!(Humidity);
178
179#[cfg(test)]
180mod test {
181    use crate::{humidity::*, test_utils::assert_almost_eq};
182
183    // Humidity Units
184    #[test]
185    fn percent() {
186        let t = Humidity::from_percent(50.0);
187        let o = t.as_percent();
188
189        assert_almost_eq(o, 50.0);
190    }
191
192    #[test]
193    fn ratio() {
194        let t = Humidity::from_ratio(0.1);
195        let o = t.as_ratio();
196        assert_almost_eq(o, 0.1);
197    }
198    // Dewpoint calculation
199    #[test]
200    fn to_dewpoint1() {
201        let humidity = Humidity::from_percent(85.0);
202        let temp = Temperature::from_celsius(18.0);
203        let dewpoint = humidity.as_dewpoint(temp);
204        assert_almost_eq(dewpoint.as_celsius(), 15.44);
205    }
206    #[test]
207    fn to_dewpoint2() {
208        let humidity = Humidity::from_percent(40.0);
209        let temp = Temperature::from_celsius(5.0);
210        let dewpoint = humidity.as_dewpoint(temp);
211        assert_almost_eq(dewpoint.as_celsius(), -7.5);
212    }
213    #[test]
214    fn to_dewpoint3() {
215        let humidity = Humidity::from_percent(95.0);
216        let temp = Temperature::from_celsius(30.0);
217        let dewpoint = humidity.as_dewpoint(temp);
218        assert_almost_eq(dewpoint.as_celsius(), 29.11);
219    }
220    #[test]
221    fn from_dewpoint1() {
222        let temp = Temperature::from_celsius(18.0);
223        let dewpoint = Temperature::from_celsius(15.44);
224        let rh = Humidity::from_dewpoint(dewpoint, temp);
225        assert_almost_eq(rh.as_percent(), 85.0);
226    }
227    #[test]
228    fn vapour_pressure() {
229        let humidity = Humidity::from_percent(60.0);
230        let temp = Temperature::from_celsius(25.0);
231        let vp = humidity.as_vapor_pressure(temp);
232        assert_almost_eq(vp.as_hectopascals(), 19.011);
233    }
234    #[test]
235    // also tests as_vapor_pressure() on the fly
236    fn absolute_humidity() {
237        let humidity = Humidity::from_percent(60.0);
238        let temp = Temperature::from_celsius(25.0);
239        let density = humidity.as_absolute_humidity(temp);
240        assert_almost_eq(density.as_kilograms_per_cubic_meter(), 0.0138166);
241    }
242    #[test]
243    // round-trip test
244    fn from_dewpoint2() {
245        let humidity = Humidity::from_percent(95.0);
246        let temp = Temperature::from_celsius(30.0);
247        let dewpoint = humidity.as_dewpoint(temp);
248        let rh = Humidity::from_dewpoint(dewpoint, temp);
249        assert_almost_eq(humidity.as_percent(), rh.as_percent());
250    }
251
252    // Traits
253    #[test]
254    fn eq() {
255        let a = Humidity::from_percent(20.0);
256        let b = Humidity::from_percent(20.0);
257        assert_eq!(a == b, true);
258    }
259
260    #[test]
261    fn neq() {
262        let a = Humidity::from_percent(20.0);
263        let b = Humidity::from_percent(19.0);
264        assert_eq!(a == b, false);
265    }
266
267    #[test]
268    fn cmp() {
269        let a = Humidity::from_percent(19.0);
270        let b = Humidity::from_percent(20.0);
271        assert_eq!(a < b, true);
272        assert_eq!(a <= b, true);
273        assert_eq!(a > b, false);
274        assert_eq!(a >= b, false);
275    }
276}