From 32c79597cd76a4ec0682d543c53f16fcf4cde799 Mon Sep 17 00:00:00 2001 From: Andrew Bowers Date: Mon, 27 Apr 2020 22:08:57 +1200 Subject: [PATCH 1/5] Created new Humidity and Density modules * Responded to Issue #19 - added humidity module * Created density module (required for one output from Humidity) * Updated ReadMe to include Density and Humidity * Added new modules into lib.rs * [TODO: updates to CHANGELOG, etc...] [TODO (Possibly): Add more native units (g/m3), (kg/L), (g/mL), (t/m3), (oz/cu in), (oz/US fl oz), (lb/cu yd)... ] --- README.md | 2 + src/density.rs | 239 +++++++++++++++++++++++++++++++++++++++++++++++ src/humidity.rs | 242 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 18 ++-- 4 files changed, 494 insertions(+), 7 deletions(-) create mode 100644 src/density.rs create mode 100644 src/humidity.rs diff --git a/README.md b/README.md index e66b2b2..4b329f2 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,12 @@ Conversions to and from different units are simple, and operator overrides allow - Area - Current - Data (bytes, etc) +- Density - Energy - Force - Frequency - Length +- Humidity - Mass - Power - Pressure diff --git a/src/density.rs b/src/density.rs new file mode 100644 index 0000000..9162da7 --- /dev/null +++ b/src/density.rs @@ -0,0 +1,239 @@ +//! Types and constants for handling density. + +use super::measurement::*; +use mass::Mass; +use volume::Volume; + +// Constants, metric +/// Number of pound per cubic foot in 1 kilograms per cubic meter +pub const LBCF_KGCM_FACTOR: f64 = 0.062427973725314; + +/// The `Density` struct can be used to deal with Densities in a common way, to enable mass, +/// volume and density calculations and unit conversions. +/// +/// # Example1 - calculating volume from units of mass and density +/// +/// ``` +/// extern crate measurements; +/// use measurements::{Density, Mass, Volume}; +/// +/// fn main() { +/// // Q: A 12 stone man hops into a brimming full bath, completely emersing himself. +/// // How many gallons of water spill on the floor? +/// // (Assume The human body is roughly about as dense as water - 1 gm/cm³) +/// // +/// let body_density: Density = Mass::from_grams(1.0) / Volume:: from_cubic_centimetres(1.0); +/// let mans_weight = Mass::from_stones(12.0); +/// let water_volume = mans_weight / body_density; +/// println!("{} gallons of water spilled on the floor", water_volume.as_gallons()); +///} +/// ``` +/// # Example2 - converting to ad-hoc units of density +/// +/// ``` +/// extern crate measurements; +/// use measurements::{Density, Mass, Volume}; +/// +/// fn main() { +/// // Q: what is 3 grams per litre in units of ounces per quart? +/// // +/// let density: Density = Mass::from_grams(3.0) / Volume:: from_litres(1.0); +/// let ounces = (density * Volume::from_quarts(1.0)).as_ounces(); +/// println!("Answer is {} ounces per quart", ounces); +///} +/// ``` + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug)] +pub struct Density { + kilograms_per_cubic_meter: f64, +} + +impl Density { + /// Create a new Density from a floating point value in kilograms per cubic meter + pub fn from_kilograms_per_cubic_meter(kilograms_per_cubic_meter: f64) -> Density { + Density { + kilograms_per_cubic_meter: kilograms_per_cubic_meter, + } + } + + /// Create a new Density from a floating point value in pounds per cubic feet + pub fn from_pounds_per_cubic_feet(pounds_per_cubic_foot: f64) -> Density { + Density::from_kilograms_per_cubic_meter(pounds_per_cubic_foot / LBCF_KGCM_FACTOR) + } + + /// Convert this Density to a value in kilograms per cubic meter + pub fn as_kilograms_per_cubic_meter(&self) -> f64 { + self.kilograms_per_cubic_meter + } + + /// Convert this Density to a value in pounds per cubic feet + pub fn as_pounds_per_cubic_feet(&self) -> f64 { + self.kilograms_per_cubic_meter * LBCF_KGCM_FACTOR + } +} + +// mass / volume = density +impl ::std::ops::Div for Mass { + type Output = Density; + + fn div(self, other: Volume) -> Density { + Density::from_base_units(self.as_base_units() / other.as_cubic_meters()) + } +} + +// mass / density = volume +impl ::std::ops::Div for Mass { + type Output = Volume; + + fn div(self, other: Density) -> Volume { + Volume::from_cubic_meters(self.as_base_units() / other.as_base_units()) + } +} + +// volume * density = mass +impl ::std::ops::Mul for Volume { + type Output = Mass; + + fn mul(self, other: Density) -> Mass { + Mass::from_base_units(self.as_cubic_meters() * other.as_base_units()) + } +} + +// density * volume = mass +impl ::std::ops::Mul for Density { + type Output = Mass; + + fn mul(self, other: Volume) -> Mass { + Mass::from_base_units(self.as_base_units() * other.as_cubic_meters()) + } +} + +impl Measurement for Density { + fn as_base_units(&self) -> f64 { + self.kilograms_per_cubic_meter + } + + fn from_base_units(units: f64) -> Self { + Self::from_kilograms_per_cubic_meter(units) + } + + fn get_base_units_name(&self) -> &'static str { + "kg/m\u{00B3}" + } +} + +implement_measurement! { Density } + +#[cfg(test)] +mod test { + + use super::*; + use test_utils::assert_almost_eq; + + // Metric + #[test] + fn mass_over_volume() { + let v1 = Volume::from_cubic_meters(10.0); + let m1 = Mass::from_kilograms(5.0); + let i1 = m1 / v1; + let r1 = i1.as_kilograms_per_cubic_meter(); + assert_almost_eq(r1, 0.5); + } + #[test] + fn mass_over_density() { + let m1 = Mass::from_kilograms(5.0); + let d1 = Density::from_kilograms_per_cubic_meter(10.0); + let i1 = m1 / d1; + let r1 = i1.as_cubic_meters(); + assert_almost_eq(r1, 0.5); + } + #[test] + fn volume_times_density() { + let v1 = Volume::from_cubic_meters(5.0); + let d1 = Density::from_kilograms_per_cubic_meter(10.0); + let i1 = v1 * d1; + let r1 = i1.as_kilograms(); + assert_almost_eq(r1, 50.0); + } + #[test] + fn density_times_volume() { + let v1 = Volume::from_cubic_meters(5.0); + let d1 = Density::from_kilograms_per_cubic_meter(10.0); + let i1 = v1 * d1; + let r1 = i1.as_kilograms(); + assert_almost_eq(r1, 50.0); + } + #[test] + fn cvt_pcf_to_kgcm() { + let a = Density::from_kilograms_per_cubic_meter(1.0); + let b = Density::from_pounds_per_cubic_feet(0.062428); + assert_almost_eq( + a.as_kilograms_per_cubic_meter(), + b.as_kilograms_per_cubic_meter(), + ); + assert_almost_eq(a.as_pounds_per_cubic_feet(), b.as_pounds_per_cubic_feet()); + } + + // Traits + #[test] + fn add() { + let a = Density::from_kilograms_per_cubic_meter(2.0); + let b = Density::from_kilograms_per_cubic_meter(4.0); + let c = a + b; + let d = b + a; + assert_almost_eq(c.as_kilograms_per_cubic_meter(), 6.0); + assert_eq!(c, d); + } + + #[test] + fn sub() { + let a = Density::from_kilograms_per_cubic_meter(2.0); + let b = Density::from_kilograms_per_cubic_meter(4.0); + let c = a - b; + assert_almost_eq(c.as_kilograms_per_cubic_meter(), -2.0); + } + + #[test] + fn mul() { + let a = Density::from_kilograms_per_cubic_meter(3.0); + let b = a * 2.0; + let c = 2.0 * a; + assert_almost_eq(b.as_kilograms_per_cubic_meter(), 6.0); + assert_eq!(b, c); + } + + #[test] + fn div() { + let a = Density::from_kilograms_per_cubic_meter(2.0); + let b = Density::from_kilograms_per_cubic_meter(4.0); + let c = a / b; + let d = a / 2.0; + assert_almost_eq(c, 0.5); + assert_almost_eq(d.as_kilograms_per_cubic_meter(), 1.0); + } + + #[test] + fn eq() { + let a = Density::from_kilograms_per_cubic_meter(2.0); + let b = Density::from_kilograms_per_cubic_meter(2.0); + assert_eq!(a == b, true); + } + + #[test] + fn neq() { + let a = Density::from_kilograms_per_cubic_meter(2.0); + let b = Density::from_kilograms_per_cubic_meter(4.0); + assert_eq!(a == b, false); + } + + #[test] + fn cmp() { + let a = Density::from_kilograms_per_cubic_meter(2.0); + let b = Density::from_kilograms_per_cubic_meter(4.0); + assert_eq!(a < b, true); + assert_eq!(a <= b, true); + assert_eq!(a > b, false); + assert_eq!(a >= b, false); + } +} diff --git a/src/humidity.rs b/src/humidity.rs new file mode 100644 index 0000000..2cd78df --- /dev/null +++ b/src/humidity.rs @@ -0,0 +1,242 @@ +//! Types and constants for handling humidity. + +use super::measurement::*; +use density::Density; +use pressure::Pressure; +use temperature::Temperature; + +/// The `Humidity` struct can be used to deal with relative humidity +/// in air in a common way. Relative humidity is an important metric used +/// in weather forecasts. +/// +/// Relative humidity (as a ratio and percentage) and conversions between +/// relative humidity and dewpoint are supported. It also provides calculations +/// giving vapour pressure and absolute humidity. +/// +/// Relative humidity gives the ratio of how much moisture the air is +/// holding to how much moisture it could hold at a given temperature. +/// Here we use the technical definition of humidity as ratio of the +/// actual water vapor pressure to the equilibrium vapor pressure +/// (often called the "saturation" vapor pressure). +/// +/// For dewpoint calculations, we use the algorithm commonly known as +/// the Magnus formula, with coefficients derived by Alduchov and +/// Eskridge (1996), which gives resonable accuracy (vapour pressure +/// error < 0.2%) for temperatures between 0 deg C, and 50 deg C. +/// +/// # Example: +/// +/// ``` +/// // calculate the dewpoint from the relative humidity +/// use measurements::{Humidity,Temperature}; +/// +/// let humidity = Humidity::from_percent(85.0); +/// let temp = Temperature::from_celsius(18.0); +/// let dewpoint = humidity.as_dewpoint(temp); +/// println!("At {} humidity, air at {} has a dewpoint of {}", humidity, temp, dewpoint); +/// +/// ``` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug)] +pub struct Humidity { + relative_humidity: f64, // expressed as a percentage +} + +impl Humidity { + /// Create a new Humidity from a floating point value percentage (i.e. 0.0% to 100.0%) + pub fn from_percent(percent: f64) -> Self { + Humidity { + relative_humidity: percent, + } + } + + /// Create a new Humidity from a floating point value ratio (i.e. 0.0 to 1.0) + pub fn from_ratio(relative_humidity: f64) -> Self { + Humidity { + relative_humidity: relative_humidity * 100.0, + } + } + /// Convert this relative humidity to a value expressed as a ratio (i.e. 0.0 to 1.0) + pub fn as_ratio(&self) -> f64 { + self.relative_humidity / 100.0 + } + + /// Convert this relative humidty to a value expressed as a percentage (i.e. 0.0% to 100.0%) + pub fn as_percent(&self) -> f64 { + self.relative_humidity + } + + /// Calculates Dewpoint from humidity and air temperature using the Magnus-Tetens + /// Approximation, with coefficients derived by Alduchov and Eskridge (1996). The formulas assume + // standard atmospheric pressure. + #[cfg(not(feature = "no_std"))] + pub fn as_dewpoint(&self, temp: Temperature) -> Temperature { + let humidity = self.relative_humidity / 100.0; + let celsius = temp.as_celsius(); + let dewpoint: f64 = 243.04 * (humidity.ln() + ((17.625 * celsius) / (243.04 + celsius))) + / (17.625 - humidity.ln() - ((17.625 * celsius) / (243.04 + celsius))); + Temperature::from_celsius(dewpoint) + } + /// Calculates the actual vapour pressure in the air, based onthe air temperature and humidity + /// at standard atmospheric pressure (1013.25 mb), using the Buck formula (accurate to +/- 0.02% + /// between 0 deg C and 50 deg C) + #[cfg(not(feature = "no_std"))] + pub fn as_vapor_pressure(&self, temp: Temperature) -> Pressure { + let temp = temp.as_celsius(); + let saturation_vapor_pressure = + 0.61121 * ((18.678 - (temp / 234.5)) * (temp / (257.14 + temp))).exp(); + Pressure::from_kilopascals((self.relative_humidity * saturation_vapor_pressure) / 100.0) + } + + /// Calculates the absolute humidity (i.e. the density of water vapor in the air (kg/m3)), using + /// the Ideal Gas Law equation. + #[cfg(not(feature = "no_std"))] + pub fn as_absolute_humidity(&self, temp: Temperature) -> Density { + // use the Ideal Gas Law equation (Density = Pressure / (Temperature * [gas constant + // for water vapor= 461.5 (J/kg*Kelvin)])) + let density = self.as_vapor_pressure(temp).as_pascals() / (temp.as_kelvin() * 461.5); + Density::from_kilograms_per_cubic_meter(density) + } + + /// Calculates humidity from dewpoint and air temperature using the Magnus-Tetens + /// Approximation, with coefficients derived by Alduchov and Eskridge (1996). The formulas assume + // standard atmospheric pressure. + #[cfg(not(feature = "no_std"))] + pub fn from_dewpoint(dewpoint: Temperature, temp: Temperature) -> Humidity { + let dewpoint = dewpoint.as_celsius(); + let temp = temp.as_celsius(); + let rh = 100.0 + * (((17.625 * dewpoint) / (243.04 + dewpoint)).exp() + / ((17.625 * temp) / (243.04 + temp)).exp()); + Humidity::from_percent(rh) + } +} + +impl Measurement for Humidity { + fn as_base_units(&self) -> f64 { + self.relative_humidity + } + + fn from_base_units(relative_humidity: f64) -> Self { + Self::from_percent(relative_humidity) + } + + fn get_base_units_name(&self) -> &'static str { + "%" + } +} + +impl ::std::cmp::Eq for Humidity {} +impl ::std::cmp::PartialEq for Humidity { + fn eq(&self, other: &Self) -> bool { + self.as_base_units() == other.as_base_units() + } +} + +impl ::std::cmp::PartialOrd for Humidity { + fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> { + self.as_base_units().partial_cmp(&other.as_base_units()) + } +} + +implement_display!(Humidity); + +#[cfg(test)] +mod test { + use humidity::*; + use test_utils::assert_almost_eq; + + // Humidity Units + #[test] + fn percent() { + let t = Humidity::from_percent(50.0); + let o = t.as_percent(); + + assert_almost_eq(o, 50.0); + } + + #[test] + fn ratio() { + let t = Humidity::from_ratio(0.1); + let o = t.as_ratio(); + assert_almost_eq(o, 0.1); + } + // Dewpoint calculation + #[test] + fn to_dewpoint1() { + let humidity = Humidity::from_percent(85.0); + let temp = Temperature::from_celsius(18.0); + let dewpoint = humidity.as_dewpoint(temp); + assert_almost_eq(dewpoint.as_celsius(), 15.44); + } + #[test] + fn to_dewpoint2() { + let humidity = Humidity::from_percent(40.0); + let temp = Temperature::from_celsius(5.0); + let dewpoint = humidity.as_dewpoint(temp); + assert_almost_eq(dewpoint.as_celsius(), -7.5); + } + #[test] + fn to_dewpoint3() { + let humidity = Humidity::from_percent(95.0); + let temp = Temperature::from_celsius(30.0); + let dewpoint = humidity.as_dewpoint(temp); + assert_almost_eq(dewpoint.as_celsius(), 29.11); + } + #[test] + fn from_dewpoint1() { + let temp = Temperature::from_celsius(18.0); + let dewpoint = Temperature::from_celsius(15.44); + let rh = Humidity::from_dewpoint(dewpoint, temp); + assert_almost_eq(rh.as_percent(), 85.0); + } + #[test] + fn vapour_pressure() { + let humidity = Humidity::from_percent(60.0); + let temp = Temperature::from_celsius(25.0); + let vp = humidity.as_vapor_pressure(temp); + assert_almost_eq(vp.as_hectopascals(), 19.011); + } + #[test] + // also tests as_vapor_pressure() on the fly + fn absolute_humidity() { + let humidity = Humidity::from_percent(60.0); + let temp = Temperature::from_celsius(25.0); + let density = humidity.as_absolute_humidity(temp); + assert_almost_eq(density.as_kilograms_per_cubic_meter(), 0.0138166); + } + #[test] + // round-trip test + fn from_dewpoint2() { + let humidity = Humidity::from_percent(95.0); + let temp = Temperature::from_celsius(30.0); + let dewpoint = humidity.as_dewpoint(temp); + let rh = Humidity::from_dewpoint(dewpoint, temp); + assert_almost_eq(humidity.as_percent(), rh.as_percent()); + } + + // Traits + #[test] + fn eq() { + let a = Humidity::from_percent(20.0); + let b = Humidity::from_percent(20.0); + assert_eq!(a == b, true); + } + + #[test] + fn neq() { + let a = Humidity::from_percent(20.0); + let b = Humidity::from_percent(19.0); + assert_eq!(a == b, false); + } + + #[test] + fn cmp() { + let a = Humidity::from_percent(19.0); + let b = Humidity::from_percent(20.0); + assert_eq!(a < b, true); + assert_eq!(a <= b, true); + assert_eq!(a > b, false); + assert_eq!(a >= b, false); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1703151..c6c0169 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,22 +7,21 @@ //! by an Area to get a Pressure. #![deny(warnings, missing_docs)] - -#![cfg_attr(feature="no_std", no_std)] +#![cfg_attr(feature = "no_std", no_std)] #[cfg(feature = "no_std")] use core as std; #[cfg(feature = "no_std")] -use core::time as time; +use core::time; #[cfg(not(feature = "no_std"))] -use std::time as time; +use std::time; #[cfg(feature = "serde")] #[macro_use] extern crate serde; -use std::f64::consts::PI as PI; +use std::f64::consts::PI; #[macro_use] mod measurement; @@ -34,12 +33,18 @@ pub use length::{Distance, Length}; pub mod temperature; pub use temperature::{Temperature, TemperatureDelta}; +pub mod humidity; +pub use humidity::Humidity; + pub mod mass; pub use mass::Mass; pub mod volume; pub use volume::Volume; +pub mod density; +pub use density::Density; + pub mod pressure; pub use pressure::Pressure; @@ -148,7 +153,7 @@ macro_rules! impl_maths { Self::Output::from_base_units(self.as_base_units() / rhs.as_base_units()) } } - } + }; } impl Measurement for time::Duration { @@ -218,4 +223,3 @@ impl std::ops::Div for Energy { Self::Output::from_base_units(self.as_base_units() / rhs.as_base_units()) } } - From e5087faab2207c8b126f8bee3454986adf2d9ba9 Mon Sep 17 00:00:00 2001 From: Andrew Bowers <51428462+pukeko37@users.noreply.github.com> Date: Wed, 2 Jun 2021 17:07:42 +1200 Subject: [PATCH 2/5] Update src/humidity.rs Co-authored-by: Diego Barrios Romero --- src/humidity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/humidity.rs b/src/humidity.rs index 2cd78df..4518b91 100644 --- a/src/humidity.rs +++ b/src/humidity.rs @@ -67,7 +67,7 @@ impl Humidity { } /// Calculates Dewpoint from humidity and air temperature using the Magnus-Tetens - /// Approximation, with coefficients derived by Alduchov and Eskridge (1996). The formulas assume + /// approximation, with coefficients derived by Alduchov and Eskridge (1996). The formulas assume // standard atmospheric pressure. #[cfg(not(feature = "no_std"))] pub fn as_dewpoint(&self, temp: Temperature) -> Temperature { From 8d2b517ba4cd7d07a9571dff6247548c16cd5187 Mon Sep 17 00:00:00 2001 From: Andrew Bowers <51428462+pukeko37@users.noreply.github.com> Date: Wed, 2 Jun 2021 17:08:05 +1200 Subject: [PATCH 3/5] Update src/humidity.rs Co-authored-by: Diego Barrios Romero --- src/humidity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/humidity.rs b/src/humidity.rs index 4518b91..f03b55b 100644 --- a/src/humidity.rs +++ b/src/humidity.rs @@ -77,7 +77,7 @@ impl Humidity { / (17.625 - humidity.ln() - ((17.625 * celsius) / (243.04 + celsius))); Temperature::from_celsius(dewpoint) } - /// Calculates the actual vapour pressure in the air, based onthe air temperature and humidity + /// Calculates the actual vapour pressure in the air, based on the air temperature and humidity /// at standard atmospheric pressure (1013.25 mb), using the Buck formula (accurate to +/- 0.02% /// between 0 deg C and 50 deg C) #[cfg(not(feature = "no_std"))] From 5d022081d998f3672166a62239c889aeace6b168 Mon Sep 17 00:00:00 2001 From: Andrew Bowers <51428462+pukeko37@users.noreply.github.com> Date: Wed, 2 Jun 2021 17:43:31 +1200 Subject: [PATCH 4/5] Update CHANGELOG.md Added changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e13e620..e1b79be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Added this Changelog. ### Changed +- Added: humidity, density in response to Issue #19 + - Merged in [#36](https://github.com/rust-embedded-community/rust-measurements/pull/36) to adjust bounds on `Measurements::pick_appropriate_units()`, which changes the return value for cases when the value is 1.0. ## [0.10.2] From b707a8abc0293fab17f17b6a4ba96cd55cc76703 Mon Sep 17 00:00:00 2001 From: Andrew Bowers <51428462+pukeko37@users.noreply.github.com> Date: Wed, 2 Jun 2021 22:05:49 +1200 Subject: [PATCH 5/5] Update CHANGELOG.md Co-authored-by: Diego Barrios Romero --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1b79be..8035e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. Added this Changelog. -### Changed +### Added + +- Humidity and density in [#27](https://github.com/rust-embedded-community/rust-measurements/pull/27) -- Added: humidity, density in response to Issue #19 +### Changed - Merged in [#36](https://github.com/rust-embedded-community/rust-measurements/pull/36) to adjust bounds on `Measurements::pick_appropriate_units()`, which changes the return value for cases when the value is 1.0.