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