feos_core/equation_of_state/
residual.rs

1use crate::{FeosError, FeosResult, ReferenceSystem, state::StateHD};
2use nalgebra::{DVector, DefaultAllocator, Dim, Dyn, OMatrix, OVector, U1, allocator::Allocator};
3use num_dual::{DualNum, Gradients, partial, partial2, second_derivative, third_derivative};
4use quantity::ad::first_derivative;
5use quantity::*;
6use std::ops::Deref;
7use std::sync::Arc;
8use typenum::Quot;
9
10/// Molar weight of all components.
11///
12/// Enables calculation of (mass) specific properties.
13pub trait Molarweight<N: Dim = Dyn, D: DualNum<f64> + Copy = f64>
14where
15    DefaultAllocator: Allocator<N>,
16{
17    fn molar_weight(&self) -> MolarWeight<OVector<D, N>>;
18}
19
20impl<C: Deref<Target = T>, T: Molarweight<N, D>, N: Dim, D: DualNum<f64> + Copy> Molarweight<N, D>
21    for C
22where
23    DefaultAllocator: Allocator<N>,
24{
25    fn molar_weight(&self) -> MolarWeight<OVector<D, N>> {
26        T::molar_weight(self)
27    }
28}
29
30/// A model from which models for subsets of its components can be extracted.
31pub trait Subset {
32    /// Return a model consisting of the components
33    /// contained in component_list.
34    fn subset(&self, component_list: &[usize]) -> Self;
35}
36
37impl<T: Subset> Subset for Arc<T> {
38    fn subset(&self, component_list: &[usize]) -> Self {
39        Arc::new(T::subset(self, component_list))
40    }
41}
42
43/// A simple residual Helmholtz energy model for arbitrary many components
44/// and no automatic differentiation of model parameters.
45///
46/// This is a shortcut to implementing `Residual<Dyn, f64>`. To avoid unnecessary
47/// cloning, `Residual<Dyn, f64>` is automatically implemented for all pointer
48/// types that deref to the struct implementing `ResidualDyn` and are `Clone`
49/// (i.e., `Rc<T>`, `Arc<T>`, `&T`, ...).
50pub trait ResidualDyn {
51    /// Return the number of components in the system.
52    fn components(&self) -> usize;
53
54    /// Return the maximum density in Angstrom^-3.
55    ///
56    /// This value is used as an estimate for a liquid phase for phase
57    /// equilibria and other iterations. It is not explicitly meant to
58    /// be a mathematical limit for the density (if those exist in the
59    /// equation of state anyways).
60    fn compute_max_density<D: DualNum<f64> + Copy>(&self, molefracs: &DVector<D>) -> D;
61
62    /// Evaluate the reduced Helmholtz energy density of each individual contribution
63    /// and return them together with a string representation of the contribution.
64    fn reduced_helmholtz_energy_density_contributions<D: DualNum<f64> + Copy>(
65        &self,
66        state: &StateHD<D>,
67    ) -> Vec<(&'static str, D)>;
68}
69
70impl<C: Deref<Target = T> + Clone, T: ResidualDyn, D: DualNum<f64> + Copy> Residual<Dyn, D> for C {
71    type Real = Self;
72    type Lifted<D2: DualNum<f64, Inner = D> + Copy> = Self;
73    fn re(&self) -> Self::Real {
74        self.clone()
75    }
76    fn lift<D2: DualNum<f64, Inner = D> + Copy>(&self) -> Self::Lifted<D2> {
77        self.clone()
78    }
79    fn components(&self) -> usize {
80        ResidualDyn::components(self.deref())
81    }
82    fn compute_max_density(&self, molefracs: &DVector<D>) -> D {
83        ResidualDyn::compute_max_density(self.deref(), molefracs)
84    }
85    fn reduced_helmholtz_energy_density_contributions(
86        &self,
87        state: &StateHD<D, Dyn>,
88    ) -> Vec<(&'static str, D)> {
89        ResidualDyn::reduced_helmholtz_energy_density_contributions(self.deref(), state)
90    }
91}
92
93/// A residual Helmholtz energy model.
94pub trait Residual<N: Dim = Dyn, D: DualNum<f64> + Copy = f64>: Clone
95where
96    DefaultAllocator: Allocator<N>,
97{
98    /// Return the number of components in the system.
99    fn components(&self) -> usize;
100
101    /// Return a generic composition vector for a pure component.
102    ///
103    /// Panics if N is not Dyn(1) or Const<1>.
104    fn pure_molefracs() -> OVector<D, N> {
105        OVector::from_element_generic(N::from_usize(1), U1, D::one())
106    }
107
108    /// The residual model with only the real parts of the model parameters.
109    type Real: Residual<N>;
110
111    /// The residual model with the model parameters lifted to a higher dual number.
112    type Lifted<D2: DualNum<f64, Inner = D> + Copy>: Residual<N, D2>;
113
114    /// Return the real part of the residual model.
115    fn re(&self) -> Self::Real;
116
117    /// Return the lifted residual model.
118    fn lift<D2: DualNum<f64, Inner = D> + Copy>(&self) -> Self::Lifted<D2>;
119
120    /// Return the maximum density in Angstrom^-3.
121    ///
122    /// This value is used as an estimate for a liquid phase for phase
123    /// equilibria and other iterations. It is not explicitly meant to
124    /// be a mathematical limit for the density (if those exist in the
125    /// equation of state anyways).
126    fn compute_max_density(&self, molefracs: &OVector<D, N>) -> D;
127
128    /// Evaluate the reduced Helmholtz energy density of each individual contribution
129    /// and return them together with a string representation of the contribution.
130    fn reduced_helmholtz_energy_density_contributions(
131        &self,
132        state: &StateHD<D, N>,
133    ) -> Vec<(&'static str, D)>;
134
135    /// Evaluate the residual reduced Helmholtz energy density $\beta f^\mathrm{res}$.
136    fn reduced_residual_helmholtz_energy_density(&self, state: &StateHD<D, N>) -> D {
137        self.reduced_helmholtz_energy_density_contributions(state)
138            .iter()
139            .fold(D::zero(), |acc, (_, a)| acc + a)
140    }
141
142    /// Evaluate the molar Helmholtz energy of each individual contribution
143    /// and return them together with a string representation of the contribution.
144    fn molar_helmholtz_energy_contributions(
145        &self,
146        temperature: D,
147        molar_volume: D,
148        molefracs: &OVector<D, N>,
149    ) -> Vec<(&'static str, D)> {
150        let state = StateHD::new(temperature, molar_volume, molefracs);
151        self.reduced_helmholtz_energy_density_contributions(&state)
152            .into_iter()
153            .map(|(n, f)| (n, f * temperature * molar_volume))
154            .collect()
155    }
156
157    /// Evaluate the residual molar Helmholtz energy $a^\mathrm{res}$.
158    fn residual_molar_helmholtz_energy(
159        &self,
160        temperature: D,
161        molar_volume: D,
162        molefracs: &OVector<D, N>,
163    ) -> D {
164        let state = StateHD::new(temperature, molar_volume, molefracs);
165        self.reduced_residual_helmholtz_energy_density(&state) * temperature * molar_volume
166    }
167
168    /// Evaluate the residual Helmholtz energy $A^\mathrm{res}$.
169    fn residual_helmholtz_energy(&self, temperature: D, volume: D, moles: &OVector<D, N>) -> D {
170        let state = StateHD::new_density(temperature, &(moles / volume));
171        self.reduced_residual_helmholtz_energy_density(&state) * temperature * volume
172    }
173
174    /// Evaluate the residual Helmholtz energy $A^\mathrm{res}$.
175    fn residual_helmholtz_energy_unit(
176        &self,
177        temperature: Temperature<D>,
178        volume: Volume<D>,
179        moles: &Moles<OVector<D, N>>,
180    ) -> Energy<D> {
181        let temperature = temperature.into_reduced();
182        let total_moles = moles.sum();
183        let molar_volume = (volume / total_moles).into_reduced();
184        let molefracs = moles / total_moles;
185        let state = StateHD::new(temperature, molar_volume, &molefracs);
186        Pressure::from_reduced(self.reduced_residual_helmholtz_energy_density(&state) * temperature)
187            * volume
188    }
189
190    /// Check if the provided optional molar concentration is consistent with the
191    /// equation of state.
192    ///
193    /// In general, the number of elements in `molefracs` needs to match the number
194    /// of components of the equation of state. For a pure component, however,
195    /// no molefracs need to be provided.
196    fn validate_molefracs(&self, molefracs: &Option<OVector<D, N>>) -> FeosResult<OVector<D, N>> {
197        let l = molefracs.as_ref().map_or(1, |m| m.len());
198        if self.components() == l {
199            match molefracs {
200                Some(m) => Ok(m.clone()),
201                None => Ok(OVector::from_element_generic(
202                    N::from_usize(1),
203                    U1,
204                    D::one(),
205                )),
206            }
207        } else {
208            Err(FeosError::IncompatibleComponents(self.components(), l))
209        }
210    }
211
212    /// Calculate the maximum density.
213    ///
214    /// This value is used as an estimate for a liquid phase for phase
215    /// equilibria and other iterations. It is not explicitly meant to
216    /// be a mathematical limit for the density (if those exist in the
217    /// equation of state anyways).
218    fn max_density(&self, molefracs: &Option<OVector<D, N>>) -> FeosResult<Density<D>> {
219        let x = self.validate_molefracs(molefracs)?;
220        Ok(Density::from_reduced(self.compute_max_density(&x)))
221    }
222
223    /// Calculate the second virial coefficient $B(T)$
224    fn second_virial_coefficient(
225        &self,
226        temperature: Temperature<D>,
227        molefracs: &Option<OVector<D, N>>,
228    ) -> MolarVolume<D> {
229        let x = self.validate_molefracs(molefracs).unwrap();
230        let (_, _, d2f) = second_derivative(
231            partial2(
232                |rho, &t, x| {
233                    let state = StateHD::new_virial(t, rho, x);
234                    self.lift()
235                        .reduced_residual_helmholtz_energy_density(&state)
236                },
237                &temperature.into_reduced(),
238                &x,
239            ),
240            D::from(0.0),
241        );
242
243        Quantity::from_reduced(d2f * 0.5)
244    }
245
246    /// Calculate the third virial coefficient $C(T)$
247    fn third_virial_coefficient(
248        &self,
249        temperature: Temperature<D>,
250        molefracs: &Option<OVector<D, N>>,
251    ) -> Quot<MolarVolume<D>, Density<D>> {
252        let x = self.validate_molefracs(molefracs).unwrap();
253        let (_, _, _, d3f) = third_derivative(
254            partial2(
255                |rho, &t, x| {
256                    let state = StateHD::new_virial(t, rho, x);
257                    self.lift()
258                        .reduced_residual_helmholtz_energy_density(&state)
259                },
260                &temperature.into_reduced(),
261                &x,
262            ),
263            D::from(0.0),
264        );
265
266        Quantity::from_reduced(d3f / 3.0)
267    }
268
269    /// Calculate the temperature derivative of the second virial coefficient $B'(T)$
270    fn second_virial_coefficient_temperature_derivative(
271        &self,
272        temperature: Temperature<D>,
273        molefracs: &Option<OVector<D, N>>,
274    ) -> Quot<MolarVolume<D>, Temperature<D>> {
275        let (_, db_dt) = first_derivative(
276            partial(
277                |t, x| self.lift().second_virial_coefficient(t, x),
278                molefracs,
279            ),
280            temperature,
281        );
282        db_dt
283    }
284
285    /// Calculate the temperature derivative of the third virial coefficient $C'(T)$
286    fn third_virial_coefficient_temperature_derivative(
287        &self,
288        temperature: Temperature<D>,
289        molefracs: &Option<OVector<D, N>>,
290    ) -> Quot<Quot<MolarVolume<D>, Density<D>>, Temperature<D>> {
291        let (_, dc_dt) = first_derivative(
292            partial(|t, x| self.lift().third_virial_coefficient(t, x), molefracs),
293            temperature,
294        );
295        dc_dt
296    }
297
298    // The following methods are used in phase equilibrium algorithms
299
300    /// calculates a_res, p, dp_drho
301    fn p_dpdrho(&self, temperature: D, density: D, molefracs: &OVector<D, N>) -> (D, D, D) {
302        let molar_volume = density.recip();
303        let (a, da, d2a) = second_derivative(
304            partial2(
305                |molar_volume, &t, x| {
306                    self.lift()
307                        .residual_molar_helmholtz_energy(t, molar_volume, x)
308                },
309                &temperature,
310                molefracs,
311            ),
312            molar_volume,
313        );
314        (
315            a * density,
316            -da + temperature * density,
317            molar_volume * molar_volume * d2a + temperature,
318        )
319    }
320
321    /// calculates p, dp_drho, d2p_drho2
322    fn p_dpdrho_d2pdrho2(
323        &self,
324        temperature: D,
325        density: D,
326        molefracs: &OVector<D, N>,
327    ) -> (D, D, D) {
328        let molar_volume = density.recip();
329        let (_, da, d2a, d3a) = third_derivative(
330            partial2(
331                |molar_volume, &t, x| {
332                    self.lift()
333                        .residual_molar_helmholtz_energy(t, molar_volume, x)
334                },
335                &temperature,
336                molefracs,
337            ),
338            molar_volume,
339        );
340        (
341            -da + temperature * density,
342            molar_volume * molar_volume * d2a + temperature,
343            -molar_volume * molar_volume * molar_volume * (d2a * 2.0 + molar_volume * d3a),
344        )
345    }
346
347    /// calculates p, mu_res, dp_drho, dmu_drho
348    #[expect(clippy::type_complexity)]
349    fn dmu_drho(
350        &self,
351        temperature: D,
352        partial_density: &OVector<D, N>,
353    ) -> (D, OVector<D, N>, OVector<D, N>, OMatrix<D, N, N>)
354    where
355        N: Gradients,
356        DefaultAllocator: Allocator<N, N>,
357    {
358        let (f_res, mu_res, dmu_res) = N::hessian(
359            |rho, &t| {
360                let state = StateHD::new_density(t, &rho);
361                self.lift()
362                    .reduced_residual_helmholtz_energy_density(&state)
363                    * t
364            },
365            partial_density,
366            &temperature,
367        );
368        let p = mu_res.dot(partial_density) - f_res + temperature * partial_density.sum();
369        let dmu = dmu_res + OMatrix::from_diagonal(&partial_density.map(|d| temperature / d));
370        let dp = &dmu * partial_density;
371        (p, mu_res, dp, dmu)
372    }
373
374    /// calculates p, mu_res, dp_dv, dmu_dv
375    fn dmu_dv(
376        &self,
377        temperature: D,
378        molar_volume: D,
379        molefracs: &OVector<D, N>,
380    ) -> (D, OVector<D, N>, D, OVector<D, N>)
381    where
382        N: Gradients,
383    {
384        let (_, mu_res, a_res_v, mu_res_v) = N::partial_hessian(
385            |x, v, &t| self.lift().residual_helmholtz_energy(t, v, &x),
386            molefracs,
387            molar_volume,
388            &temperature,
389        );
390        let p = -a_res_v + temperature / molar_volume;
391        let mu_v = mu_res_v.map(|m| m - temperature / molar_volume);
392        let p_v = mu_v.dot(molefracs) / molar_volume;
393        (p, mu_res, p_v, mu_v)
394    }
395
396    /// calculates dp_dt, dmu_res_dt
397    fn dmu_dt(&self, temperature: D, partial_density: &OVector<D, N>) -> (D, OVector<D, N>)
398    where
399        N: Gradients,
400    {
401        let (_, _, f_res_t, mu_res_t) = N::partial_hessian(
402            |rho, t, _: &()| {
403                let state = StateHD::new_density(t, &rho);
404                self.lift()
405                    .reduced_residual_helmholtz_energy_density(&state)
406                    * t
407            },
408            partial_density,
409            temperature,
410            &(),
411        );
412        let p_t = -f_res_t + partial_density.dot(&mu_res_t) + partial_density.sum();
413        (p_t, mu_res_t)
414    }
415}
416
417/// Reference values and residual entropy correlations for entropy scaling.
418pub trait EntropyScaling<N: Dim = Dyn, D: DualNum<f64> + Copy = f64>
419where
420    DefaultAllocator: Allocator<N>,
421{
422    fn viscosity_reference(
423        &self,
424        temperature: Temperature<D>,
425        volume: Volume<D>,
426        moles: &Moles<OVector<D, N>>,
427    ) -> Viscosity<D>;
428    fn viscosity_correlation(&self, s_res: D, x: &OVector<D, N>) -> D;
429    fn diffusion_reference(
430        &self,
431        temperature: Temperature<D>,
432        volume: Volume<D>,
433        moles: &Moles<OVector<D, N>>,
434    ) -> Diffusivity<D>;
435    fn diffusion_correlation(&self, s_res: D, x: &OVector<D, N>) -> D;
436    fn thermal_conductivity_reference(
437        &self,
438        temperature: Temperature<D>,
439        volume: Volume<D>,
440        moles: &Moles<OVector<D, N>>,
441    ) -> ThermalConductivity<D>;
442    fn thermal_conductivity_correlation(&self, s_res: D, x: &OVector<D, N>) -> D;
443}
444
445impl<C: Deref<Target = T>, T: EntropyScaling<N, D>, N: Dim, D: DualNum<f64> + Copy>
446    EntropyScaling<N, D> for C
447where
448    DefaultAllocator: Allocator<N>,
449{
450    fn viscosity_reference(
451        &self,
452        temperature: Temperature<D>,
453        volume: Volume<D>,
454        moles: &Moles<OVector<D, N>>,
455    ) -> Viscosity<D> {
456        self.deref().viscosity_reference(temperature, volume, moles)
457    }
458    fn viscosity_correlation(&self, s_res: D, x: &OVector<D, N>) -> D {
459        self.deref().viscosity_correlation(s_res, x)
460    }
461    fn diffusion_reference(
462        &self,
463        temperature: Temperature<D>,
464        volume: Volume<D>,
465        moles: &Moles<OVector<D, N>>,
466    ) -> Diffusivity<D> {
467        self.deref().diffusion_reference(temperature, volume, moles)
468    }
469    fn diffusion_correlation(&self, s_res: D, x: &OVector<D, N>) -> D {
470        self.deref().diffusion_correlation(s_res, x)
471    }
472    fn thermal_conductivity_reference(
473        &self,
474        temperature: Temperature<D>,
475        volume: Volume<D>,
476        moles: &Moles<OVector<D, N>>,
477    ) -> ThermalConductivity<D> {
478        self.deref()
479            .thermal_conductivity_reference(temperature, volume, moles)
480    }
481    fn thermal_conductivity_correlation(&self, s_res: D, x: &OVector<D, N>) -> D {
482        self.deref().thermal_conductivity_correlation(s_res, x)
483    }
484}
485
486/// Dummy implementation for [EquationOfState](super::EquationOfState)s that only contain an ideal gas contribution.
487pub struct NoResidual(pub usize);
488
489impl Subset for NoResidual {
490    fn subset(&self, component_list: &[usize]) -> Self {
491        Self(component_list.len())
492    }
493}
494
495impl ResidualDyn for NoResidual {
496    fn components(&self) -> usize {
497        self.0
498    }
499
500    fn compute_max_density<D: DualNum<f64> + Copy>(&self, _: &DVector<D>) -> D {
501        D::one()
502    }
503
504    fn reduced_helmholtz_energy_density_contributions<D: DualNum<f64> + Copy>(
505        &self,
506        _: &StateHD<D>,
507    ) -> Vec<(&'static str, D)> {
508        vec![]
509    }
510}