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