Skip to main content

feos_core/
lib.rs

1#![warn(clippy::all)]
2#![allow(clippy::reversed_empty_ranges)]
3#![warn(clippy::allow_attributes)]
4use quantity::{Quantity, SIUnit};
5use std::ops::{Div, Mul};
6
7/// Print messages with level `Verbosity::Iter` or higher.
8#[macro_export]
9macro_rules! log_iter {
10    ($verbosity:expr, $($arg:tt)*) => {
11        if $verbosity >= Verbosity::Iter {
12            println!($($arg)*);
13        }
14    }
15}
16
17/// Print messages with level `Verbosity::Result` or higher.
18#[macro_export]
19macro_rules! log_result {
20    ($verbosity:expr, $($arg:tt)*) => {
21        if $verbosity >= Verbosity::Result {
22            println!($($arg)*);
23        }
24    }
25}
26
27pub mod ad;
28pub mod cubic;
29mod density_iteration;
30mod equation_of_state;
31mod errors;
32pub mod parameter;
33mod phase_equilibria;
34mod state;
35pub use equation_of_state::{
36    EntropyScaling, EquationOfState, IdealGas, IdealGasAD, Molarweight, NoResidual, Residual,
37    ResidualDyn, Subset, Total,
38};
39pub use errors::{FeosError, FeosResult};
40#[cfg(feature = "ndarray")]
41pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero};
42pub use phase_equilibria::{PhaseEquilibrium, TemperatureOrPressure};
43pub use state::{Composition, Contributions, DensityInitialization, State, StateHD, StateVec};
44
45/// Level of detail in the iteration output.
46#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Default)]
47pub enum Verbosity {
48    /// Do not print output.
49    #[default]
50    None,
51    /// Print information about the success of failure of the iteration.
52    Result,
53    /// Print a detailed outpur for every iteration.
54    Iter,
55}
56
57/// Options for the various phase equilibria solvers.
58///
59/// If the values are [None], solver specific default
60/// values are used.
61#[derive(Copy, Clone, Default)]
62pub struct SolverOptions {
63    /// Maximum number of iterations.
64    pub max_iter: Option<usize>,
65    /// Tolerance.
66    pub tol: Option<f64>,
67    /// Iteration outpput indicated by the [Verbosity] enum.
68    pub verbosity: Verbosity,
69}
70
71impl From<(Option<usize>, Option<f64>, Option<Verbosity>)> for SolverOptions {
72    fn from(options: (Option<usize>, Option<f64>, Option<Verbosity>)) -> Self {
73        Self {
74            max_iter: options.0,
75            tol: options.1,
76            verbosity: options.2.unwrap_or(Verbosity::None),
77        }
78    }
79}
80
81impl SolverOptions {
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    pub fn max_iter(mut self, max_iter: usize) -> Self {
87        self.max_iter = Some(max_iter);
88        self
89    }
90
91    pub fn tol(mut self, tol: f64) -> Self {
92        self.tol = Some(tol);
93        self
94    }
95
96    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
97        self.verbosity = verbosity;
98        self
99    }
100
101    pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) {
102        (
103            self.max_iter.unwrap_or(max_iter),
104            self.tol.unwrap_or(tol),
105            self.verbosity,
106        )
107    }
108}
109
110/// Reference values used for reduced properties in feos
111const REFERENCE_VALUES: [f64; 7] = [
112    1e-12,               // 1 ps
113    1e-10,               // 1 Å
114    1.380649e-27,        // Fixed through k_B
115    1.0,                 // 1 A
116    1.0,                 // 1 K
117    1.0 / 6.02214076e23, // 1/N_AV
118    1.0,                 // 1 Cd
119];
120
121const fn powi(x: f64, n: i32) -> f64 {
122    match n {
123        ..=-1 => powi(1.0 / x, -n),
124        0 => 1.0,
125        n if n % 2 == 0 => powi(x * x, n / 2),
126        n => x * powi(x * x, (n - 1) / 2),
127    }
128}
129
130/// Conversion between reduced units and SI units.
131pub trait ReferenceSystem {
132    type Inner;
133    const T: i8;
134    const L: i8;
135    const M: i8;
136    const I: i8;
137    const THETA: i8;
138    const N: i8;
139    const J: i8;
140    const FACTOR: f64 = powi(REFERENCE_VALUES[0], Self::T as i32)
141        * powi(REFERENCE_VALUES[1], Self::L as i32)
142        * powi(REFERENCE_VALUES[2], Self::M as i32)
143        * powi(REFERENCE_VALUES[3], Self::I as i32)
144        * powi(REFERENCE_VALUES[4], Self::THETA as i32)
145        * powi(REFERENCE_VALUES[5], Self::N as i32)
146        * powi(REFERENCE_VALUES[6], Self::J as i32);
147
148    fn from_reduced(value: Self::Inner) -> Self
149    where
150        Self::Inner: Mul<f64, Output = Self::Inner>;
151
152    fn to_reduced(&self) -> Self::Inner
153    where
154        for<'a> &'a Self::Inner: Div<f64, Output = Self::Inner>;
155
156    fn into_reduced(self) -> Self::Inner
157    where
158        Self::Inner: Div<f64, Output = Self::Inner>;
159}
160
161/// Conversion to and from reduced units
162impl<
163    Inner,
164    const T: i8,
165    const L: i8,
166    const M: i8,
167    const I: i8,
168    const THETA: i8,
169    const N: i8,
170    const J: i8,
171> ReferenceSystem for Quantity<Inner, SIUnit<T, L, M, I, THETA, N, J>>
172{
173    type Inner = Inner;
174    const T: i8 = T;
175    const L: i8 = L;
176    const M: i8 = M;
177    const I: i8 = I;
178    const THETA: i8 = THETA;
179    const N: i8 = N;
180    const J: i8 = J;
181    fn from_reduced(value: Inner) -> Self
182    where
183        Inner: Mul<f64, Output = Inner>,
184    {
185        Self::new(value * Self::FACTOR)
186    }
187
188    fn to_reduced(&self) -> Inner
189    where
190        for<'a> &'a Inner: Div<f64, Output = Inner>,
191    {
192        self.convert_to(Quantity::new(Self::FACTOR))
193    }
194
195    fn into_reduced(self) -> Inner
196    where
197        Inner: Div<f64, Output = Inner>,
198    {
199        self.convert_into(Quantity::new(Self::FACTOR))
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use crate::Contributions;
206    use crate::FeosResult;
207    use crate::State;
208    use crate::cubic::*;
209    use crate::equation_of_state::{EquationOfState, IdealGas};
210    use crate::parameter::*;
211    use approx::*;
212    use num_dual::DualNum;
213    use quantity::{BAR, KELVIN, MOL, RGAS};
214
215    // Only to be able to instantiate an `EquationOfState`
216    #[derive(Clone, Copy)]
217    struct NoIdealGas;
218
219    impl IdealGas for NoIdealGas {
220        fn ideal_gas_model(&self) -> &'static str {
221            "NoIdealGas"
222        }
223
224        fn ln_lambda3<D: DualNum<f64> + Copy>(&self, _: D) -> D {
225            unreachable!()
226        }
227    }
228
229    fn pure_record_vec() -> Vec<PureRecord<PengRobinsonRecord, ()>> {
230        let records = r#"[
231            {
232                "identifier": {
233                    "cas": "74-98-6",
234                    "name": "propane",
235                    "iupac_name": "propane",
236                    "smiles": "CCC",
237                    "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3",
238                    "formula": "C3H8"
239                },
240                "tc": 369.96,
241                "pc": 4250000.0,
242                "acentric_factor": 0.153,
243                "molarweight": 44.0962
244            },
245            {
246                "identifier": {
247                    "cas": "106-97-8",
248                    "name": "butane",
249                    "iupac_name": "butane",
250                    "smiles": "CCCC",
251                    "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3",
252                    "formula": "C4H10"
253                },
254                "tc": 425.2,
255                "pc": 3800000.0,
256                "acentric_factor": 0.199,
257                "molarweight": 58.123
258            }
259        ]"#;
260        serde_json::from_str(records).expect("Unable to parse json.")
261    }
262
263    #[test]
264    fn validate_residual_properties() -> FeosResult<()> {
265        let mixture = pure_record_vec();
266        let propane = &mixture[0];
267        let parameters = PengRobinsonParameters::new_pure(propane.clone())?;
268        let residual = PengRobinson::new(parameters);
269
270        let sr = State::new_npt(&&residual, 300.0 * KELVIN, 1.0 * BAR, 2.0 * MOL, None)?;
271
272        let parameters = PengRobinsonParameters::new_pure(propane.clone())?;
273        let residual = PengRobinson::new(parameters);
274        let eos = EquationOfState::new(vec![NoIdealGas], residual);
275        let s = State::new_npt(&&eos, 300.0 * KELVIN, 1.0 * BAR, 2.0 * MOL, None)?;
276
277        // pressure
278        assert_relative_eq!(
279            s.pressure(Contributions::Total),
280            sr.pressure(Contributions::Total),
281            max_relative = 1e-15
282        );
283        assert_relative_eq!(
284            s.pressure(Contributions::Residual),
285            sr.pressure(Contributions::Residual),
286            max_relative = 1e-15
287        );
288        assert_relative_eq!(
289            s.compressibility(Contributions::Total),
290            sr.compressibility(Contributions::Total),
291            max_relative = 1e-15
292        );
293        assert_relative_eq!(
294            s.compressibility(Contributions::Residual),
295            sr.compressibility(Contributions::Residual),
296            max_relative = 1e-15
297        );
298
299        // residual properties
300        assert_relative_eq!(
301            s.helmholtz_energy(Contributions::Residual)?,
302            sr.residual_helmholtz_energy()?,
303            max_relative = 1e-15
304        );
305        assert_relative_eq!(
306            s.molar_helmholtz_energy(Contributions::Residual),
307            sr.residual_molar_helmholtz_energy(),
308            max_relative = 1e-15
309        );
310        assert_relative_eq!(
311            s.entropy(Contributions::Residual)?,
312            sr.residual_entropy()?,
313            max_relative = 1e-15
314        );
315        assert_relative_eq!(
316            s.molar_entropy(Contributions::Residual),
317            sr.residual_molar_entropy(),
318            max_relative = 1e-15
319        );
320        assert_relative_eq!(
321            s.enthalpy(Contributions::Residual)?,
322            sr.residual_enthalpy()?,
323            max_relative = 1e-15
324        );
325        assert_relative_eq!(
326            s.molar_enthalpy(Contributions::Residual),
327            sr.residual_molar_enthalpy(),
328            max_relative = 1e-15
329        );
330        assert_relative_eq!(
331            s.internal_energy(Contributions::Residual)?,
332            sr.residual_internal_energy()?,
333            max_relative = 1e-15
334        );
335        assert_relative_eq!(
336            s.molar_internal_energy(Contributions::Residual),
337            sr.residual_molar_internal_energy(),
338            max_relative = 1e-15
339        );
340        assert_relative_eq!(
341            s.gibbs_energy(Contributions::Residual)?
342                - s.total_moles()?
343                    * RGAS
344                    * s.temperature
345                    * s.compressibility(Contributions::Total).ln(),
346            sr.residual_gibbs_energy()?,
347            max_relative = 1e-15
348        );
349        assert_relative_eq!(
350            s.molar_gibbs_energy(Contributions::Residual)
351                - RGAS * s.temperature * s.compressibility(Contributions::Total).ln(),
352            sr.residual_molar_gibbs_energy(),
353            max_relative = 1e-15
354        );
355        assert_relative_eq!(
356            s.chemical_potential(Contributions::Residual),
357            sr.residual_chemical_potential(),
358            max_relative = 1e-15
359        );
360
361        // pressure derivatives
362        assert_relative_eq!(
363            s.structure_factor(),
364            sr.structure_factor(),
365            max_relative = 1e-15
366        );
367        assert_relative_eq!(
368            s.dp_dt(Contributions::Total),
369            sr.dp_dt(Contributions::Total),
370            max_relative = 1e-15
371        );
372        assert_relative_eq!(
373            s.dp_dt(Contributions::Residual),
374            sr.dp_dt(Contributions::Residual),
375            max_relative = 1e-15
376        );
377        assert_relative_eq!(
378            s.dp_dv(Contributions::Total),
379            sr.dp_dv(Contributions::Total),
380            max_relative = 1e-15
381        );
382        assert_relative_eq!(
383            s.dp_dv(Contributions::Residual),
384            sr.dp_dv(Contributions::Residual),
385            max_relative = 1e-15
386        );
387        assert_relative_eq!(
388            s.dp_drho(Contributions::Total),
389            sr.dp_drho(Contributions::Total),
390            max_relative = 1e-15
391        );
392        assert_relative_eq!(
393            s.dp_drho(Contributions::Residual),
394            sr.dp_drho(Contributions::Residual),
395            max_relative = 1e-15
396        );
397        assert_relative_eq!(
398            s.d2p_dv2(Contributions::Total),
399            sr.d2p_dv2(Contributions::Total),
400            max_relative = 1e-15
401        );
402        assert_relative_eq!(
403            s.d2p_dv2(Contributions::Residual),
404            sr.d2p_dv2(Contributions::Residual),
405            max_relative = 1e-15
406        );
407        assert_relative_eq!(
408            s.d2p_drho2(Contributions::Total),
409            sr.d2p_drho2(Contributions::Total),
410            max_relative = 1e-15
411        );
412        assert_relative_eq!(
413            s.d2p_drho2(Contributions::Residual),
414            sr.d2p_drho2(Contributions::Residual),
415            max_relative = 1e-15
416        );
417        assert_relative_eq!(
418            s.n_dp_dni(Contributions::Total),
419            sr.n_dp_dni(Contributions::Total),
420            max_relative = 1e-15
421        );
422        assert_relative_eq!(
423            s.n_dp_dni(Contributions::Residual),
424            sr.n_dp_dni(Contributions::Residual),
425            max_relative = 1e-15
426        );
427
428        // entropy
429        assert_relative_eq!(
430            s.ds_dt(Contributions::Residual),
431            sr.ds_res_dt(),
432            max_relative = 1e-15
433        );
434
435        // chemical potential
436        assert_relative_eq!(
437            s.dmu_dt(Contributions::Residual),
438            sr.dmu_res_dt(),
439            max_relative = 1e-15
440        );
441        assert_relative_eq!(
442            s.n_dmu_dni(Contributions::Residual),
443            sr.n_dmu_dni(Contributions::Residual),
444            max_relative = 1e-15
445        );
446        assert_relative_eq!(
447            s.dmu_dt(Contributions::Residual),
448            sr.dmu_res_dt(),
449            max_relative = 1e-15
450        );
451
452        // fugacity
453        assert_relative_eq!(s.ln_phi(), sr.ln_phi(), max_relative = 1e-15);
454        assert_relative_eq!(s.dln_phi_dt(), sr.dln_phi_dt(), max_relative = 1e-15);
455        assert_relative_eq!(s.dln_phi_dp(), sr.dln_phi_dp(), max_relative = 1e-15);
456        assert_relative_eq!(s.n_dln_phi_dnj(), sr.n_dln_phi_dnj(), max_relative = 1e-15);
457        assert_relative_eq!(
458            s.thermodynamic_factor(),
459            sr.thermodynamic_factor(),
460            max_relative = 1e-15
461        );
462
463        // residual properties using multiple derivatives
464        assert_relative_eq!(
465            s.molar_isochoric_heat_capacity(Contributions::Residual),
466            sr.residual_molar_isochoric_heat_capacity(),
467            max_relative = 1e-15
468        );
469        assert_relative_eq!(
470            s.dc_v_dt(Contributions::Residual),
471            sr.dc_v_res_dt(),
472            max_relative = 1e-15
473        );
474        assert_relative_eq!(
475            s.molar_isobaric_heat_capacity(Contributions::Residual),
476            sr.residual_molar_isobaric_heat_capacity(),
477            max_relative = 1e-15
478        );
479        Ok(())
480    }
481}