Skip to main content

feos_core/equation_of_state/
residual.rs

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