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