Skip to main content

feos_core/state/
residual_properties.rs

1use super::{Contributions, State};
2use crate::equation_of_state::{EntropyScaling, Molarweight, Residual, Subset};
3use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem};
4use nalgebra::allocator::Allocator;
5use nalgebra::{DMatrix, DVector, DefaultAllocator, OMatrix, OVector, dvector};
6use num_dual::{Dual, DualNum, Gradients, partial, partial2};
7use quantity::*;
8use std::ops::{Add, Div, Neg, Sub};
9
10type InvT<T> = Quantity<T, <_Temperature as Neg>::Output>;
11type InvP<T> = Quantity<T, <_Pressure as Neg>::Output>;
12type POverT<T> = Quantity<T, <_Pressure as Sub<_Temperature>>::Output>;
13
14/// # State properties
15impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
16where
17    DefaultAllocator: Allocator<N>,
18{
19    pub(super) fn contributions<
20        T: Add<T, Output = T>,
21        U,
22        I: FnOnce() -> Quantity<T, U>,
23        R: FnOnce() -> Quantity<T, U>,
24    >(
25        ideal_gas: I,
26        residual: R,
27        contributions: Contributions,
28    ) -> Quantity<T, U> {
29        match contributions {
30            Contributions::IdealGas => ideal_gas(),
31            Contributions::Total => ideal_gas() + residual(),
32            Contributions::Residual => residual(),
33        }
34    }
35
36    /// Residual Helmholtz energy $A^\text{res}$
37    pub fn residual_helmholtz_energy(&self) -> FeosResult<Energy<D>> {
38        Ok(self.residual_molar_helmholtz_energy() * self.total_moles()?)
39    }
40
41    /// Residual molar Helmholtz energy $a^\text{res}$
42    pub fn residual_molar_helmholtz_energy(&self) -> MolarEnergy<D> {
43        *self.cache.a.get_or_init(|| {
44            self.eos.residual_molar_helmholtz_energy(
45                self.temperature,
46                self.molar_volume,
47                &self.molefracs,
48            )
49        })
50    }
51
52    /// Residual entropy $S^\text{res}=\left(\frac{\partial A^\text{res}}{\partial T}\right)_{V,N_i}$
53    pub fn residual_entropy(&self) -> FeosResult<Entropy<D>> {
54        Ok(self.residual_molar_entropy() * self.total_moles()?)
55    }
56
57    /// Residual molar entropy $s^\text{res}=\left(\frac{\partial a^\text{res}}{\partial T}\right)_{V,N_i}$
58    pub fn residual_molar_entropy(&self) -> MolarEntropy<D> {
59        -*self.cache.da_dt.get_or_init(|| {
60            let (a, da_dt) = quantity::ad::first_derivative(
61                partial2(
62                    |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
63                    &self.molar_volume,
64                    &self.molefracs,
65                ),
66                self.temperature,
67            );
68            let _ = self.cache.a.set(a);
69            da_dt
70        })
71    }
72
73    /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$
74    pub fn pressure(&self, contributions: Contributions) -> Pressure<D> {
75        let ideal_gas = || self.density * RGAS * self.temperature;
76        let residual = || {
77            -*self.cache.da_dv.get_or_init(|| {
78                let (a, da_dv) = quantity::ad::first_derivative(
79                    partial2(
80                        |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
81                        &self.temperature,
82                        &self.molefracs,
83                    ),
84                    self.molar_volume,
85                );
86                let _ = self.cache.a.set(a);
87                da_dv
88            })
89        };
90        Self::contributions(ideal_gas, residual, contributions)
91    }
92
93    /// Residual chemical potential: $\mu_i^\text{res}=\left(\frac{\partial A^\text{res}}{\partial N_i}\right)_{T,V,N_j}$
94    pub fn residual_chemical_potential(&self) -> MolarEnergy<OVector<D, N>> {
95        self.cache
96            .da_dn
97            .get_or_init(|| {
98                let (a, mu) = quantity::ad::gradient_copy(
99                    partial2(
100                        |n: Dimensionless<_>, &t, &v| {
101                            self.eos.lift().residual_molar_helmholtz_energy(t, v, &n)
102                        },
103                        &self.temperature,
104                        &self.molar_volume,
105                    ),
106                    &Dimensionless::new(self.molefracs.clone()),
107                );
108                let _ = self.cache.a.set(a);
109                mu
110            })
111            .clone()
112    }
113
114    /// Compressibility factor: $Z=\frac{pV}{NRT}$
115    pub fn compressibility(&self, contributions: Contributions) -> D {
116        (self.pressure(contributions) / (self.density * self.temperature * RGAS)).into_value()
117    }
118
119    // pressure derivatives
120
121    /// Partial derivative of pressure w.r.t. molar volume: $\left(\frac{\partial p}{\partial v}\right)_{T,N_i}$
122    pub fn dp_dv(
123        &self,
124        contributions: Contributions,
125    ) -> <Pressure<D> as Div<MolarVolume<D>>>::Output {
126        let ideal_gas = || -self.density * RGAS * self.temperature / self.molar_volume;
127        let residual = || {
128            -*self.cache.d2a_dv2.get_or_init(|| {
129                let (a, da_dv, d2a_dv2) = quantity::ad::second_derivative(
130                    partial2(
131                        |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
132                        &self.temperature,
133                        &self.molefracs,
134                    ),
135                    self.molar_volume,
136                );
137                let _ = self.cache.a.set(a);
138                let _ = self.cache.da_dv.set(da_dv);
139                d2a_dv2
140            })
141        };
142        Self::contributions(ideal_gas, residual, contributions)
143    }
144
145    /// Partial derivative of pressure w.r.t. density: $\left(\frac{\partial p}{\partial \rho}\right)_{T,N_i}$
146    pub fn dp_drho(
147        &self,
148        contributions: Contributions,
149    ) -> <Pressure<D> as Div<Density<D>>>::Output {
150        -self.molar_volume / self.density * self.dp_dv(contributions)
151    }
152
153    /// Partial derivative of pressure w.r.t. temperature: $\left(\frac{\partial p}{\partial T}\right)_{V,N_i}$
154    pub fn dp_dt(&self, contributions: Contributions) -> POverT<D> {
155        let ideal_gas = || self.density * RGAS;
156        let residual = || {
157            -*self.cache.d2a_dtdv.get_or_init(|| {
158                let (a, da_dt, da_dv, d2a_dtdv) = quantity::ad::second_partial_derivative(
159                    partial(
160                        |(t, v), n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
161                        &self.molefracs,
162                    ),
163                    (self.temperature, self.molar_volume),
164                );
165                let _ = self.cache.a.set(a);
166                let _ = self.cache.da_dt.set(da_dt);
167                let _ = self.cache.da_dv.set(da_dv);
168                d2a_dtdv
169            })
170        };
171        Self::contributions(ideal_gas, residual, contributions)
172    }
173
174    /// Partial derivative of pressure w.r.t. moles: $N\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$
175    pub fn n_dp_dni(&self, contributions: Contributions) -> Pressure<OVector<D, N>> {
176        let residual = -self
177            .cache
178            .d2a_dndv
179            .get_or_init(|| {
180                let (a, da_dn, da_dv, dmu_dv) = quantity::ad::partial_hessian_copy(
181                    partial(
182                        |(n, v): (Dimensionless<_>, _), &t| {
183                            self.eos.lift().residual_molar_helmholtz_energy(t, v, &n)
184                        },
185                        &self.temperature,
186                    ),
187                    (
188                        &Dimensionless::new(self.molefracs.clone()),
189                        self.molar_volume,
190                    ),
191                );
192                let _ = self.cache.a.set(a);
193                let _ = self.cache.da_dn.set(da_dn);
194                let _ = self.cache.da_dv.set(da_dv);
195                dmu_dv
196            })
197            .clone();
198        let (r, c) = residual.shape_generic();
199        let ideal_gas = || self.temperature * self.density * RGAS;
200        Quantity::from_fn_generic(r, c, |i, _| {
201            Self::contributions(ideal_gas, || residual.get(i), contributions)
202        })
203    }
204
205    /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$
206    pub fn d2p_dv2(
207        &self,
208        contributions: Contributions,
209    ) -> <<Pressure<D> as Div<MolarVolume<D>>>::Output as Div<MolarVolume<D>>>::Output {
210        let ideal_gas = || {
211            self.density * RGAS * self.temperature / (self.molar_volume * self.molar_volume) * 2.0
212        };
213        let residual = || {
214            -*self.cache.d3a_dv3.get_or_init(|| {
215                let (a, da_dv, d2a_dv2, d3a_dv3) = quantity::ad::third_derivative(
216                    partial2(
217                        |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
218                        &self.temperature,
219                        &self.molefracs,
220                    ),
221                    self.molar_volume,
222                );
223                let _ = self.cache.a.set(a);
224                let _ = self.cache.da_dv.set(da_dv);
225                let _ = self.cache.d2a_dv2.set(d2a_dv2);
226                d3a_dv3
227            })
228        };
229        Self::contributions(ideal_gas, residual, contributions)
230    }
231
232    /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$
233    pub fn d2p_drho2(
234        &self,
235        contributions: Contributions,
236    ) -> <<Pressure<D> as Div<Density<D>>>::Output as Div<Density<D>>>::Output {
237        self.molar_volume.powi::<3>()
238            * (self.molar_volume * self.d2p_dv2(contributions) + self.dp_dv(contributions) * 2.0)
239    }
240
241    /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$
242    pub fn structure_factor(&self) -> D {
243        -(self.temperature * self.density * RGAS
244            / (self.molar_volume * self.dp_dv(Contributions::Total)))
245        .into_value()
246    }
247
248    /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$
249    pub fn partial_molar_volume(&self) -> MolarVolume<OVector<D, N>> {
250        -self.n_dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total)
251    }
252
253    /// Partial derivative of chemical potential w.r.t. moles: $N\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$
254    pub fn n_dmu_dni(&self, contributions: Contributions) -> MolarEnergy<OMatrix<D, N, N>>
255    where
256        DefaultAllocator: Allocator<N, N>,
257    {
258        let (a, da_dn, d2a_dn2) = quantity::ad::hessian_copy(
259            partial2(
260                |n: Dimensionless<_>, &t, &v| {
261                    self.eos.lift().residual_molar_helmholtz_energy(t, v, &n)
262                },
263                &self.temperature,
264                &self.molar_volume,
265            ),
266            &Dimensionless::new(self.molefracs.clone()),
267        );
268        let _ = self.cache.a.set(a);
269        let _ = self.cache.da_dn.set(da_dn);
270        let residual = || d2a_dn2;
271        let ideal_gas = || {
272            Dimensionless::new(OMatrix::from_diagonal(&self.molefracs.map(|x| x.recip())))
273                * (self.temperature * RGAS)
274        };
275        Self::contributions(ideal_gas, residual, contributions)
276    }
277
278    /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$
279    pub fn isothermal_compressibility(&self) -> InvP<D> {
280        -(self.dp_dv(Contributions::Total) * self.molar_volume).inv()
281    }
282
283    // entropy derivatives
284
285    /// Partial derivative of the residual molar entropy w.r.t. temperature: $\left(\frac{\partial s^\text{res}}{\partial T}\right)_{V,N_i}$
286    pub fn ds_res_dt(&self) -> <MolarEntropy<D> as Div<Temperature<D>>>::Output {
287        -*self.cache.d2a_dt2.get_or_init(|| {
288            let (a, da_dt, d2a_dt2) = quantity::ad::second_derivative(
289                partial2(
290                    |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
291                    &self.molar_volume,
292                    &self.molefracs,
293                ),
294                self.temperature,
295            );
296            let _ = self.cache.a.set(a);
297            let _ = self.cache.da_dt.set(da_dt);
298            d2a_dt2
299        })
300    }
301
302    /// Second partial derivative of the residual molar entropy w.r.t. temperature: $\left(\frac{\partial^2s^\text{res}}{\partial T^2}\right)_{V,N_i}$
303    pub fn d2s_res_dt2(
304        &self,
305    ) -> <<MolarEntropy<D> as Div<Temperature<D>>>::Output as Div<Temperature<D>>>::Output {
306        -*self.cache.d3a_dt3.get_or_init(|| {
307            let (a, da_dt, d2a_dt2, d3a_dt3) = quantity::ad::third_derivative(
308                partial2(
309                    |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
310                    &self.molar_volume,
311                    &self.molefracs,
312                ),
313                self.temperature,
314            );
315            let _ = self.cache.a.set(a);
316            let _ = self.cache.da_dt.set(da_dt);
317            let _ = self.cache.d2a_dt2.set(d2a_dt2);
318            d3a_dt3
319        })
320    }
321
322    /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$
323    pub fn dmu_res_dt(&self) -> MolarEntropy<OVector<D, N>> {
324        self.cache
325            .d2a_dndt
326            .get_or_init(|| {
327                let (a, da_dn, da_dt, d2a_dndt) = quantity::ad::partial_hessian_copy(
328                    partial(
329                        |(n, t): (Dimensionless<_>, _), &v| {
330                            self.eos.lift().residual_molar_helmholtz_energy(t, v, &n)
331                        },
332                        &self.molar_volume,
333                    ),
334                    (
335                        &Dimensionless::new(self.molefracs.clone()),
336                        self.temperature,
337                    ),
338                );
339                let _ = self.cache.a.set(a);
340                let _ = self.cache.da_dn.set(da_dn);
341                let _ = self.cache.da_dt.set(da_dt);
342                d2a_dndt
343            })
344            .clone()
345    }
346
347    /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$
348    pub fn ln_phi(&self) -> OVector<D, N> {
349        let mu_res = self.residual_chemical_potential();
350        let ln_z = self.compressibility(Contributions::Total).ln();
351        (mu_res / (self.temperature * RGAS))
352            .into_value()
353            .map(|mu| mu - ln_z)
354    }
355
356    /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. temperature: $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,N_i}$
357    pub fn dln_phi_dt(&self) -> InvT<OVector<D, N>> {
358        let vi = self.partial_molar_volume();
359        ((self.dmu_res_dt()
360            - self.residual_chemical_potential() / self.temperature
361            - vi * self.dp_dt(Contributions::Total))
362            / (self.temperature * RGAS))
363            .add_scalar(self.temperature.inv())
364    }
365
366    /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. pressure: $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,N_i}$
367    pub fn dln_phi_dp(&self) -> InvP<OVector<D, N>> {
368        (self.partial_molar_volume() / (self.temperature * RGAS))
369            .add_scalar(-self.pressure(Contributions::Total).inv())
370    }
371
372    /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. moles: $N\left(\frac{\partial\ln\varphi_i}{\partial N_j}\right)_{T,p,N_k}$
373    pub fn n_dln_phi_dnj(&self) -> OMatrix<D, N, N>
374    where
375        DefaultAllocator: Allocator<N, N>,
376    {
377        let dmu_dni = self.n_dmu_dni(Contributions::Residual);
378        let dp_dni = self.n_dp_dni(Contributions::Total);
379        let dp_dv = self.dp_dv(Contributions::Total);
380        let (r, c) = dmu_dni.shape_generic();
381        let dp_dn_2 = Quantity::from_fn_generic(r, c, |i, j| dp_dni.get(i) * dp_dni.get(j));
382        ((dmu_dni + dp_dn_2 / dp_dv) / (self.temperature * RGAS))
383            .into_value()
384            .add_scalar(D::from(1.0))
385    }
386}
387
388impl<E: Residual + Subset> State<E> {
389    /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure.
390    pub fn ln_phi_pure_liquid(&self) -> FeosResult<DVector<f64>> {
391        let pressure = self.pressure(Contributions::Total);
392        (0..self.eos.components())
393            .map(|i| {
394                let eos = self.eos.subset(&[i]);
395                let state = State::new_npt(
396                    &eos,
397                    self.temperature,
398                    pressure,
399                    dvector![1.0],
400                    Some(crate::DensityInitialization::Liquid),
401                )?;
402                Ok(state.ln_phi()[0])
403            })
404            .collect::<FeosResult<Vec<_>>>()
405            .map(DVector::from)
406    }
407
408    /// Activity coefficient $\ln \gamma_i = \ln \varphi_i(T, p, \mathbf{N}) - \ln \varphi_i^\mathrm{pure}(T, p)$
409    pub fn ln_symmetric_activity_coefficient(&self) -> FeosResult<DVector<f64>> {
410        Ok(match self.eos.components() {
411            1 => dvector![0.0],
412            _ => self.ln_phi() - &self.ln_phi_pure_liquid()?,
413        })
414    }
415
416    /// Henry's law constant $H_{i,s}=\lim_{x_i\to 0}\frac{y_ip}{x_i}=p_s^\mathrm{sat}\frac{\varphi_i^{\infty,\mathrm{L}}}{\varphi_i^{\infty,\mathrm{V}}}$
417    ///
418    /// The composition of the (possibly mixed) solvent is determined by the molefracs. All components for which the composition is 0 are treated as solutes.
419    ///
420    /// For some reason the compiler is overwhelmed if returning a quantity array, therefore it is returned as list.
421    pub fn henrys_law_constant(
422        eos: &E,
423        temperature: Temperature,
424        molefracs: &DVector<f64>,
425    ) -> FeosResult<Vec<Pressure>> {
426        // Calculate the phase equilibrium (bubble point) of the solvent only
427        let (solvent_comps, solvent_molefracs): (Vec<_>, Vec<_>) = molefracs
428            .iter()
429            .enumerate()
430            .filter_map(|(i, &x)| (x != 0.0).then_some((i, x)))
431            .unzip();
432        let solvent_molefracs = DVector::from_vec(solvent_molefracs);
433        let solvent = eos.subset(&solvent_comps);
434        let vle = PhaseEquilibrium::bubble_point(
435            &solvent,
436            temperature,
437            &solvent_molefracs,
438            None,
439            None,
440            Default::default(),
441        )?;
442
443        // Calculate the liquid state including the Henry components
444        let liquid = State::new(eos, temperature, vle.liquid().density, molefracs.clone())?;
445
446        // Calculate the vapor state including the Henry components
447        let mut molefracs_vapor = molefracs.clone();
448        solvent_comps
449            .into_iter()
450            .zip(&vle.vapor().molefracs)
451            .for_each(|(i, &y)| molefracs_vapor[i] = y);
452        let vapor = State::new(eos, temperature, vle.vapor().density, molefracs.clone())?;
453
454        // Determine the Henry's law coefficients and return only those of the Henry components
455        let p = vle.vapor().pressure(Contributions::Total).into_reduced();
456        let h = (liquid.ln_phi() - vapor.ln_phi()).map(f64::exp) * p;
457        Ok(h.into_iter()
458            .zip(molefracs)
459            .filter_map(|(h, &x)| (x == 0.0).then_some(h))
460            .map(|&h| Pressure::from_reduced(h))
461            .collect())
462    }
463
464    /// Henry's law constant $H_{i,s}=\lim_{x_i\to 0}\frac{y_ip}{x_i}=p_s^\mathrm{sat}\frac{\varphi_i^{\infty,\mathrm{L}}}{\varphi_i^{\infty,\mathrm{V}}}$ for a binary system
465    ///
466    /// The solute (i) is the first component and the solvent (s) the second component.
467    pub fn henrys_law_constant_binary(eos: &E, temperature: Temperature) -> FeosResult<Pressure> {
468        Ok(Self::henrys_law_constant(eos, temperature, &dvector![0.0, 1.0])?[0])
469    }
470}
471
472impl<E: Residual> State<E> {
473    /// Thermodynamic factor: $\Gamma_{ij}=\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$
474    pub fn thermodynamic_factor(&self) -> DMatrix<f64> {
475        let dln_phi_dnj = self.n_dln_phi_dnj();
476        let n = self.eos.components() - 1;
477        DMatrix::from_fn(n, n, |i, j| {
478            dln_phi_dnj[(i, j)] - dln_phi_dnj[(i, n)] + if i == j { 1.0 } else { 0.0 }
479        })
480    }
481}
482
483impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
484where
485    DefaultAllocator: Allocator<N>,
486{
487    /// Residual molar isochoric heat capacity: $c_v^\text{res}=\left(\frac{\partial u^\text{res}}{\partial T}\right)_{V,N_i}$
488    pub fn residual_molar_isochoric_heat_capacity(&self) -> MolarEntropy<D> {
489        self.ds_res_dt() * self.temperature
490    }
491
492    /// Partial derivative of the residual molar isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V^\text{res}}{\partial T}\right)_{V,N_i}$
493    pub fn dc_v_res_dt(&self) -> <MolarEntropy<D> as Div<Temperature<D>>>::Output {
494        self.temperature * self.d2s_res_dt2() + self.ds_res_dt()
495    }
496
497    /// Residual molar isobaric heat capacity: $c_p^\text{res}=\left(\frac{\partial h^\text{res}}{\partial T}\right)_{p,N_i}$
498    pub fn residual_molar_isobaric_heat_capacity(&self) -> MolarEntropy<D> {
499        let dp_dt = self.dp_dt(Contributions::Total);
500        self.temperature * (self.ds_res_dt() - dp_dt * dp_dt / self.dp_dv(Contributions::Total))
501            - RGAS
502    }
503
504    /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+p^\text{res}V$
505    pub fn residual_enthalpy(&self) -> FeosResult<Energy<D>> {
506        Ok(self.residual_molar_enthalpy() * self.total_moles()?)
507    }
508
509    /// Residual molar enthalpy: $h^\text{res}(T,p,\mathbf{n})=a^\text{res}+Ts^\text{res}+p^\text{res}v$
510    pub fn residual_molar_enthalpy(&self) -> MolarEnergy<D> {
511        self.temperature * self.residual_molar_entropy()
512            + self.residual_molar_helmholtz_energy()
513            + self.pressure(Contributions::Residual) * self.molar_volume
514    }
515
516    /// Residual internal energy: $U^\text{res}(T,V,\mathbf{n})=A^\text{res}+TS^\text{res}$
517    pub fn residual_internal_energy(&self) -> FeosResult<Energy<D>> {
518        Ok(self.residual_molar_internal_energy() * self.total_moles()?)
519    }
520
521    /// Residual molar internal energy: $u^\text{res}(T,V,\mathbf{n})=a^\text{res}+Ts^\text{res}$
522    pub fn residual_molar_internal_energy(&self) -> MolarEnergy<D> {
523        self.temperature * self.residual_molar_entropy() + self.residual_molar_helmholtz_energy()
524    }
525
526    /// Residual Gibbs energy: $G^\text{res}(T,p,\mathbf{n})=A^\text{res}+p^\text{res}V-NRT \ln Z$
527    pub fn residual_gibbs_energy(&self) -> FeosResult<Energy<D>> {
528        Ok(self.residual_molar_gibbs_energy() * self.total_moles()?)
529    }
530
531    /// Residual Gibbs energy: $g^\text{res}(T,p,\mathbf{n})=a^\text{res}+p^\text{res}v-RT \ln Z$
532    pub fn residual_molar_gibbs_energy(&self) -> MolarEnergy<D> {
533        self.pressure(Contributions::Residual) * self.molar_volume
534            + self.residual_molar_helmholtz_energy()
535            - self.temperature
536                * RGAS
537                * Dimensionless::new(self.compressibility(Contributions::Total).ln())
538    }
539
540    /// Molar Helmholtz energy $a^\text{res}$ evaluated for each residual contribution of the equation of state.
541    pub fn residual_molar_helmholtz_energy_contributions(
542        &self,
543    ) -> Vec<(&'static str, MolarEnergy<D>)> {
544        let residual_contributions = self.eos.helmholtz_energy_contributions(
545            self.temperature.into_reduced(),
546            self.density.into_reduced().recip(),
547            &self.molefracs,
548        );
549        let mut res = Vec::with_capacity(residual_contributions.len());
550        for (s, v) in residual_contributions {
551            res.push((s, MolarEnergy::from_reduced(v)));
552        }
553        res
554    }
555
556    /// Chemical potential $\mu_i^\text{res}$ evaluated for each residual contribution of the equation of state.
557    pub fn residual_chemical_potential_contributions(
558        &self,
559        component: usize,
560    ) -> Vec<(&'static str, MolarEnergy<D>)> {
561        let t = Dual::from_re(self.temperature.into_reduced());
562        let v = Dual::from_re(self.temperature.into_reduced());
563        let mut x = self.molefracs.map(Dual::from_re);
564        x[component].eps = D::one();
565        let contributions = self.eos.lift().helmholtz_energy_contributions(t, v, &x);
566        let mut res = Vec::with_capacity(contributions.len());
567        for (s, v) in contributions {
568            res.push((s, MolarEnergy::from_reduced(v.eps)));
569        }
570        res
571    }
572
573    /// Pressure $p$ evaluated for each contribution of the equation of state.
574    pub fn pressure_contributions(&self) -> Vec<(&'static str, Pressure<D>)> {
575        let t = Dual::from_re(self.temperature.into_reduced());
576        let v = Dual::from_re(self.density.into_reduced().recip()).derivative();
577        let x = self.molefracs.map(Dual::from_re);
578        let contributions = self.eos.lift().helmholtz_energy_contributions(t, v, &x);
579        let mut res = Vec::with_capacity(contributions.len() + 1);
580        res.push(("Ideal gas", self.density * RGAS * self.temperature));
581        for (s, v) in contributions {
582            res.push((s, Pressure::from_reduced(-v.eps)));
583        }
584        res
585    }
586}
587
588impl<E: Residual<N, D> + Molarweight<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
589where
590    DefaultAllocator: Allocator<N>,
591{
592    /// Total molar weight: $MW=\sum_ix_iMW_i$
593    pub fn total_molar_weight(&self) -> MolarWeight<D> {
594        self.eos
595            .molar_weight()
596            .dot(&Dimensionless::new(self.molefracs.clone()))
597    }
598
599    /// Mass of each component: $m_i=n_iMW_i$
600    pub fn mass(&self) -> FeosResult<Mass<OVector<D, N>>> {
601        Ok(self
602            .eos
603            .molar_weight()
604            .component_mul(&Dimensionless::new(self.molefracs.clone()))
605            * self.total_moles()?)
606    }
607
608    /// Total mass: $m=\sum_im_i=nMW$
609    pub fn total_mass(&self) -> FeosResult<Mass<D>> {
610        Ok(self.total_molar_weight() * self.total_moles()?)
611    }
612
613    /// Mass density: $\rho^{(m)}=\frac{m}{V}$
614    pub fn mass_density(&self) -> MassDensity<D> {
615        self.density * self.total_molar_weight()
616    }
617
618    /// Mass fractions: $w_i=\frac{m_i}{m}$
619    pub fn massfracs(&self) -> OVector<D, N> {
620        self.eos
621            .molar_weight()
622            .convert_into(self.total_molar_weight())
623            .component_mul(&self.molefracs)
624    }
625}
626
627/// # Transport properties
628///
629/// These properties are available for equations of state
630/// that implement the [EntropyScaling] trait.
631impl<E: Residual<N, D> + EntropyScaling<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
632where
633    DefaultAllocator: Allocator<N>,
634{
635    /// Return the viscosity via entropy scaling.
636    pub fn viscosity(&self) -> Viscosity<D> {
637        let s = self.residual_molar_entropy().into_reduced();
638        self.eos
639            .viscosity_reference(self.temperature, self.molar_volume, &self.molefracs)
640            * Dimensionless::new(self.eos.viscosity_correlation(s, &self.molefracs).exp())
641    }
642
643    /// Return the logarithm of the reduced viscosity.
644    ///
645    /// This term equals the viscosity correlation function
646    /// that is used for entropy scaling.
647    pub fn ln_viscosity_reduced(&self) -> D {
648        let s = self.residual_molar_entropy().into_reduced();
649        self.eos.viscosity_correlation(s, &self.molefracs)
650    }
651
652    /// Return the viscosity reference as used in entropy scaling.
653    pub fn viscosity_reference(&self) -> Viscosity<D> {
654        self.eos
655            .viscosity_reference(self.temperature, self.molar_volume, &self.molefracs)
656    }
657
658    /// Return the diffusion via entropy scaling.
659    pub fn diffusion(&self) -> Diffusivity<D> {
660        let s = self.residual_molar_entropy().into_reduced();
661        self.eos
662            .diffusion_reference(self.temperature, self.molar_volume, &self.molefracs)
663            * Dimensionless::new(self.eos.diffusion_correlation(s, &self.molefracs).exp())
664    }
665
666    /// Return the logarithm of the reduced diffusion.
667    ///
668    /// This term equals the diffusion correlation function
669    /// that is used for entropy scaling.
670    pub fn ln_diffusion_reduced(&self) -> D {
671        let s = self.residual_molar_entropy().into_reduced();
672        self.eos.diffusion_correlation(s, &self.molefracs)
673    }
674
675    /// Return the diffusion reference as used in entropy scaling.
676    pub fn diffusion_reference(&self) -> Diffusivity<D> {
677        self.eos
678            .diffusion_reference(self.temperature, self.molar_volume, &self.molefracs)
679    }
680
681    /// Return the thermal conductivity via entropy scaling.
682    pub fn thermal_conductivity(&self) -> ThermalConductivity<D> {
683        let s = self.residual_molar_entropy().into_reduced();
684        self.eos.thermal_conductivity_reference(
685            self.temperature,
686            self.molar_volume,
687            &self.molefracs,
688        ) * Dimensionless::new(
689            self.eos
690                .thermal_conductivity_correlation(s, &self.molefracs)
691                .exp(),
692        )
693    }
694
695    /// Return the logarithm of the reduced thermal conductivity.
696    ///
697    /// This term equals the thermal conductivity correlation function
698    /// that is used for entropy scaling.
699    pub fn ln_thermal_conductivity_reduced(&self) -> D {
700        let s = self.residual_molar_entropy().into_reduced();
701        self.eos
702            .thermal_conductivity_correlation(s, &self.molefracs)
703    }
704
705    /// Return the thermal conductivity reference as used in entropy scaling.
706    pub fn thermal_conductivity_reference(&self) -> ThermalConductivity<D> {
707        self.eos.thermal_conductivity_reference(
708            self.temperature,
709            self.molar_volume,
710            &self.molefracs,
711        )
712    }
713}