aga8/
composition.rs

1//! Gas composition
2
3/// A complete gas composition made up of gas components.
4///
5/// A gas composition contains 21 gas components named by the field names in the struct.
6/// The unit for each component is *mole fraction*, so the sum of all components should be `1.0`.
7/// If the initial sum of components is not `1.0`, you can use the normalize function to scale it to `1.0`.
8///
9/// # Example
10/// ```
11/// let air = aga8::composition::Composition {
12///     nitrogen: 0.78,
13///     oxygen: 0.21,
14///     argon: 0.009,
15///     carbon_dioxide: 0.000_4,
16///     water: 0.000_6,
17///     ..Default::default()
18///     };
19///
20/// assert!((air.sum() - 1.0).abs() < 1.0e-10);
21/// ```
22#[repr(C)]
23#[derive(Default)]
24pub struct Composition {
25    /// Methane CH<sub>4</sub>
26    pub methane: f64,
27    /// Nitrogen N
28    pub nitrogen: f64,
29    /// Carbon Dioxide CO<sub>2</sub>
30    pub carbon_dioxide: f64,
31    /// Ethane C<sub>2</sub>H<sub>6</sub>
32    pub ethane: f64,
33    /// Propane C<sub>3</sub>H<sub>8</sub>
34    pub propane: f64,
35    /// Isobutane C<sub>4</sub>H<sub>10</sub>
36    pub isobutane: f64,
37    /// Butane C<sub>4</sub>H<sub>10</sub>
38    pub n_butane: f64,
39    /// Isopentane C<sub>5</sub>H<sub>12</sub>
40    pub isopentane: f64,
41    /// Pentane C<sub>5</sub>H<sub>12</sub>
42    pub n_pentane: f64,
43    /// Isopentane C<sub>6</sub>H<sub>14</sub>
44    pub hexane: f64,
45    /// Heptane C<sub>7</sub>H<sub>16</sub>
46    pub heptane: f64,
47    /// Octane C<sub>8</sub>H<sub>18</sub>
48    pub octane: f64,
49    /// Nonane C<sub>9</sub>H<sub>20</sub>
50    pub nonane: f64,
51    /// Decane C<sub>10</sub>H<sub>22</sub>
52    pub decane: f64,
53    /// Hydrogen H
54    pub hydrogen: f64,
55    /// Oxygen O
56    pub oxygen: f64,
57    /// Carbon monoxide CO
58    pub carbon_monoxide: f64,
59    /// Water H<sub>2</sub>O
60    pub water: f64,
61    /// Hydrogen sulfide H<sub>2</sub>S
62    pub hydrogen_sulfide: f64,
63    /// Helium He
64    pub helium: f64,
65    /// Argon Ar
66    pub argon: f64,
67}
68
69impl Composition {
70    /// Compute the sum of all components.
71    ///
72    /// # Example
73    /// ```
74    /// let comp = aga8::composition::Composition {
75    ///     methane: 50.0,
76    ///     ethane: 25.0,
77    ///     carbon_dioxide: 25.0,
78    ///     ..Default::default()
79    /// };
80    ///
81    /// assert!((comp.sum() - 100.0).abs() < 1.0e-10);
82    /// ```
83    pub fn sum(&self) -> f64 {
84        self.methane
85            + self.nitrogen
86            + self.carbon_dioxide
87            + self.ethane
88            + self.propane
89            + self.isobutane
90            + self.n_butane
91            + self.isopentane
92            + self.n_pentane
93            + self.hexane
94            + self.heptane
95            + self.octane
96            + self.nonane
97            + self.decane
98            + self.hydrogen
99            + self.oxygen
100            + self.carbon_monoxide
101            + self.water
102            + self.hydrogen_sulfide
103            + self.helium
104            + self.argon
105    }
106
107    /// Normalizes the composition sum to 1.0.
108    ///
109    /// # Example
110    /// ```
111    /// let mut comp = aga8::composition::Composition {
112    ///     methane: 50.0,
113    ///     ethane: 50.0,
114    ///     ..Default::default()
115    /// };
116    ///
117    /// comp.normalize();
118    ///
119    /// assert!((comp.ethane - 0.5).abs() < 1.0e-10);
120    /// assert!((comp.methane - 0.5).abs() < 1.0e-10);
121    /// ```
122    pub fn normalize(&mut self) -> Result<(), CompositionError> {
123        let sum = self.sum();
124        if sum > 0.0 {
125            let factor = 1.0 / sum;
126
127            self.methane *= factor;
128            self.nitrogen *= factor;
129            self.carbon_dioxide *= factor;
130            self.ethane *= factor;
131            self.propane *= factor;
132            self.isobutane *= factor;
133            self.n_butane *= factor;
134            self.isopentane *= factor;
135            self.n_pentane *= factor;
136            self.hexane *= factor;
137            self.heptane *= factor;
138            self.octane *= factor;
139            self.nonane *= factor;
140            self.decane *= factor;
141            self.hydrogen *= factor;
142            self.oxygen *= factor;
143            self.carbon_monoxide *= factor;
144            self.water *= factor;
145            self.hydrogen_sulfide *= factor;
146            self.helium *= factor;
147            self.argon *= factor;
148        } else {
149            return Err(CompositionError::Empty);
150        }
151        Ok(())
152    }
153
154    /// Checks that the composition is valid.
155    ///
156    /// # Example
157    /// ```
158    /// let mut comp = aga8::composition::Composition {
159    ///     methane: 0.5,
160    ///     ethane: 0.5,
161    ///     ..Default::default()
162    /// };
163    ///
164    /// assert_eq!(comp.check(), Ok(()));
165    /// ```
166    pub fn check(&self) -> Result<(), CompositionError> {
167        if (self.sum() - 0.0).abs() < 1.0e-10 {
168            return Err(CompositionError::Empty);
169        }
170        if (self.sum() - 1.0).abs() > 1.0e-2 {
171            return Err(CompositionError::BadSum);
172        }
173        Ok(())
174    }
175}
176
177/// Error conditions for composition
178#[repr(C)]
179#[derive(Debug, PartialEq, Eq)]
180pub enum CompositionError {
181    /// Composition is valid
182    Ok = 0,
183    /// Composition is empty, i.e. all component values are zero.
184    Empty,
185    /// The sum of the components is not 1.0000
186    BadSum,
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn valid_is_ok() {
195        let comp = Composition {
196            methane: 0.5,
197            ethane: 0.5,
198            ..Default::default()
199        };
200
201        assert_eq!(comp.check(), Ok(()));
202    }
203
204    #[test]
205    fn empty_is_error() {
206        let comp = Composition {
207            ..Default::default()
208        };
209
210        assert_eq!(comp.check(), Err(CompositionError::Empty));
211    }
212
213    #[test]
214    fn too_big_is_error() {
215        let comp = Composition {
216            ethane: 0.5,
217            methane: 0.6,
218            propane: 0.4,
219            ..Default::default()
220        };
221
222        assert_eq!(comp.check(), Err(CompositionError::BadSum));
223    }
224
225    #[test]
226    fn normalized_is_1() {
227        let mut comp = Composition {
228            methane: 83.0,
229            ethane: 17.0,
230            ..Default::default()
231        };
232
233        comp.normalize().unwrap();
234
235        assert_eq!(comp.sum(), 1.0);
236    }
237
238    #[test]
239    fn normalize_empty_is_error() {
240        let mut comp = Composition {
241            ..Default::default()
242        };
243
244        assert_eq!(comp.normalize(), Err(CompositionError::Empty));
245    }
246}