feos_core/state/
residual_properties.rs

1use super::{Contributions, Derivative::*, PartialDerivative, State};
2use crate::equation_of_state::{EntropyScaling, Molarweight, Residual};
3use crate::errors::EosResult;
4use crate::phase_equilibria::PhaseEquilibrium;
5use crate::ReferenceSystem;
6use ndarray::{arr1, Array1, Array2};
7use quantity::*;
8use std::ops::{Add, Div};
9use std::sync::Arc;
10use typenum::P2;
11
12/// # State properties
13impl<E: Residual> State<E> {
14    pub(super) fn get_or_compute_derivative_residual(&self, derivative: PartialDerivative) -> f64 {
15        let mut cache = self.cache.lock().unwrap();
16
17        match derivative {
18            PartialDerivative::Zeroth => {
19                let new_state = self.derive0();
20                let computation =
21                    || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
22                cache.get_or_insert_with_f64(computation)
23            }
24            PartialDerivative::First(v) => {
25                let new_state = self.derive1(v);
26                let computation =
27                    || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
28                cache.get_or_insert_with_d64(v, computation)
29            }
30            PartialDerivative::Second(v) => {
31                let new_state = self.derive2(v);
32                let computation =
33                    || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
34                cache.get_or_insert_with_d2_64(v, computation)
35            }
36            PartialDerivative::SecondMixed(v1, v2) => {
37                let new_state = self.derive2_mixed(v1, v2);
38                let computation =
39                    || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
40                cache.get_or_insert_with_hd64(v1, v2, computation)
41            }
42            PartialDerivative::Third(v) => {
43                let new_state = self.derive3(v);
44                let computation =
45                    || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
46                cache.get_or_insert_with_hd364(v, computation)
47            }
48        }
49    }
50}
51
52impl<E: Residual> State<E> {
53    fn contributions<T: Add<T, Output = T>, U>(
54        ideal_gas: Quantity<T, U>,
55        residual: Quantity<T, U>,
56        contributions: Contributions,
57    ) -> Quantity<T, U> {
58        match contributions {
59            Contributions::IdealGas => ideal_gas,
60            Contributions::Total => ideal_gas + residual,
61            Contributions::Residual => residual,
62        }
63    }
64
65    /// Residual Helmholtz energy $A^\text{res}$
66    pub fn residual_helmholtz_energy(&self) -> Energy {
67        Energy::from_reduced(self.get_or_compute_derivative_residual(PartialDerivative::Zeroth))
68    }
69
70    /// Residual molar Helmholtz energy $a^\text{res}$
71    pub fn residual_molar_helmholtz_energy(&self) -> MolarEnergy {
72        self.residual_helmholtz_energy() / self.total_moles
73    }
74
75    /// Residual Helmholtz energy $A^\text{res}$ evaluated for each contribution of the equation of state.
76    pub fn residual_helmholtz_energy_contributions(&self) -> Vec<(String, Energy)> {
77        let new_state = self.derive0();
78        let residual_contributions = self.eos.residual_helmholtz_energy_contributions(&new_state);
79        let mut res = Vec::with_capacity(residual_contributions.len());
80        for (s, v) in residual_contributions {
81            res.push((s, Energy::from_reduced(v * new_state.temperature)));
82        }
83        res
84    }
85
86    /// Residual entropy $S^\text{res}=\left(\frac{\partial A^\text{res}}{\partial T}\right)_{V,N_i}$
87    pub fn residual_entropy(&self) -> Entropy {
88        Entropy::from_reduced(
89            -self.get_or_compute_derivative_residual(PartialDerivative::First(DT)),
90        )
91    }
92
93    /// Residual entropy $s^\text{res}=\left(\frac{\partial a^\text{res}}{\partial T}\right)_{V,N_i}$
94    pub fn residual_molar_entropy(&self) -> MolarEntropy {
95        self.residual_entropy() / self.total_moles
96    }
97
98    /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$
99    pub fn pressure(&self, contributions: Contributions) -> Pressure {
100        let ideal_gas = self.density * RGAS * self.temperature;
101        let residual = Pressure::from_reduced(
102            -self.get_or_compute_derivative_residual(PartialDerivative::First(DV)),
103        );
104        Self::contributions(ideal_gas, residual, contributions)
105    }
106
107    /// Residual chemical potential: $\mu_i^\text{res}=\left(\frac{\partial A^\text{res}}{\partial N_i}\right)_{T,V,N_j}$
108    pub fn residual_chemical_potential(&self) -> MolarEnergy<Array1<f64>> {
109        MolarEnergy::from_reduced(Array1::from_shape_fn(self.eos.components(), |i| {
110            self.get_or_compute_derivative_residual(PartialDerivative::First(DN(i)))
111        }))
112    }
113
114    /// Chemical potential $\mu_i^\text{res}$ evaluated for each contribution of the equation of state.
115    pub fn residual_chemical_potential_contributions(
116        &self,
117        component: usize,
118    ) -> Vec<(String, MolarEnergy)> {
119        let new_state = self.derive1(DN(component));
120        let contributions = self.eos.residual_helmholtz_energy_contributions(&new_state);
121        let mut res = Vec::with_capacity(contributions.len());
122        for (s, v) in contributions {
123            res.push((
124                s,
125                MolarEnergy::from_reduced((v * new_state.temperature).eps),
126            ));
127        }
128        res
129    }
130
131    /// Compressibility factor: $Z=\frac{pV}{NRT}$
132    pub fn compressibility(&self, contributions: Contributions) -> f64 {
133        (self.pressure(contributions) / (self.density * self.temperature * RGAS)).into_value()
134    }
135
136    // pressure derivatives
137
138    /// Partial derivative of pressure w.r.t. volume: $\left(\frac{\partial p}{\partial V}\right)_{T,N_i}$
139    pub fn dp_dv(&self, contributions: Contributions) -> <Pressure as Div<Volume>>::Output {
140        let ideal_gas = -self.density * RGAS * self.temperature / self.volume;
141        let residual = Quantity::from_reduced(
142            -self.get_or_compute_derivative_residual(PartialDerivative::Second(DV)),
143        );
144        Self::contributions(ideal_gas, residual, contributions)
145    }
146
147    /// Partial derivative of pressure w.r.t. density: $\left(\frac{\partial p}{\partial \rho}\right)_{T,N_i}$
148    pub fn dp_drho(&self, contributions: Contributions) -> <Pressure as Div<Density>>::Output {
149        -self.volume / self.density * self.dp_dv(contributions)
150    }
151
152    /// Partial derivative of pressure w.r.t. temperature: $\left(\frac{\partial p}{\partial T}\right)_{V,N_i}$
153    pub fn dp_dt(&self, contributions: Contributions) -> <Pressure as Div<Temperature>>::Output {
154        let ideal_gas = self.density * RGAS;
155        let residual = Quantity::from_reduced(
156            -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DT)),
157        );
158        Self::contributions(ideal_gas, residual, contributions)
159    }
160
161    /// Partial derivative of pressure w.r.t. moles: $\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$
162    pub fn dp_dni(
163        &self,
164        contributions: Contributions,
165    ) -> <Pressure as Div<Moles<Array1<f64>>>>::Output {
166        let ideal_gas = Quantity::from_vec(vec![
167            RGAS * self.temperature / self.volume;
168            self.eos.components()
169        ]);
170        let residual = Quantity::from_reduced(Array1::from_shape_fn(self.eos.components(), |i| {
171            -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DN(i)))
172        }));
173        Self::contributions(ideal_gas, residual, contributions)
174    }
175
176    /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$
177    pub fn d2p_dv2(
178        &self,
179        contributions: Contributions,
180    ) -> <<Pressure as Div<Volume>>::Output as Div<Volume>>::Output {
181        let ideal_gas = 2.0 * self.density * RGAS * self.temperature / (self.volume * self.volume);
182        let residual = Quantity::from_reduced(
183            -self.get_or_compute_derivative_residual(PartialDerivative::Third(DV)),
184        );
185        Self::contributions(ideal_gas, residual, contributions)
186    }
187
188    /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$
189    pub fn d2p_drho2(
190        &self,
191        contributions: Contributions,
192    ) -> <<Pressure as Div<Density>>::Output as Div<Density>>::Output {
193        self.volume / (self.density * self.density)
194            * (self.volume * self.d2p_dv2(contributions) + 2.0 * self.dp_dv(contributions))
195    }
196
197    /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$
198    pub fn structure_factor(&self) -> f64 {
199        -(RGAS * self.temperature * self.density / (self.volume * self.dp_dv(Contributions::Total)))
200            .into_value()
201    }
202
203    // This function is designed specifically for use in density iterations
204    pub(crate) fn p_dpdrho(&self) -> (Pressure, <Pressure as Div<Density>>::Output) {
205        let dp_dv = self.dp_dv(Contributions::Total);
206        (
207            self.pressure(Contributions::Total),
208            (-self.volume * dp_dv / self.density),
209        )
210    }
211
212    /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$
213    pub fn partial_molar_volume(&self) -> MolarVolume<Array1<f64>> {
214        -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total)
215    }
216
217    /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$
218    pub fn dmu_dni(
219        &self,
220        contributions: Contributions,
221    ) -> <MolarEnergy<Array2<f64>> as Div<Moles>>::Output {
222        let n = self.eos.components();
223        Quantity::from_shape_fn((n, n), |(i, j)| {
224            let ideal_gas = if i == j {
225                RGAS * self.temperature / self.moles.get(i)
226            } else {
227                Quantity::from_reduced(0.0)
228            };
229            let residual =
230                Quantity::from_reduced(self.get_or_compute_derivative_residual(
231                    PartialDerivative::SecondMixed(DN(i), DN(j)),
232                ));
233            Self::contributions(ideal_gas, residual, contributions)
234        })
235    }
236
237    // This function is designed specifically for use in spinodal iterations
238    #[allow(clippy::type_complexity)]
239    pub(crate) fn d2pdrho2(
240        &self,
241    ) -> (
242        Pressure,
243        <Pressure as Div<Density>>::Output,
244        <<Pressure as Div<Density>>::Output as Div<Density>>::Output,
245    ) {
246        let d2p_dv2 = self.d2p_dv2(Contributions::Total);
247        let dp_dv = self.dp_dv(Contributions::Total);
248        (
249            self.pressure(Contributions::Total),
250            (-self.volume * dp_dv / self.density),
251            (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)),
252        )
253    }
254
255    /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$
256    pub fn isothermal_compressibility(&self) -> <f64 as Div<Pressure>>::Output {
257        -1.0 / (self.dp_dv(Contributions::Total) * self.volume)
258    }
259
260    /// Pressure $p$ evaluated for each contribution of the equation of state.
261    pub fn pressure_contributions(&self) -> Vec<(String, Pressure)> {
262        let new_state = self.derive1(DV);
263        let contributions = self.eos.residual_helmholtz_energy_contributions(&new_state);
264        let mut res = Vec::with_capacity(contributions.len() + 1);
265        res.push(("Ideal gas".into(), self.density * RGAS * self.temperature));
266        for (s, v) in contributions {
267            res.push((s, Pressure::from_reduced(-(v * new_state.temperature).eps)));
268        }
269        res
270    }
271
272    // entropy derivatives
273
274    /// Partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial S^\text{res}}{\partial T}\right)_{V,N_i}$
275    pub fn ds_res_dt(&self) -> <Entropy as Div<Temperature>>::Output {
276        Quantity::from_reduced(
277            -self.get_or_compute_derivative_residual(PartialDerivative::Second(DT)),
278        )
279    }
280
281    /// Second partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial^2S^\text{res}}{\partial T^2}\right)_{V,N_i}$
282    pub fn d2s_res_dt2(
283        &self,
284    ) -> <<Entropy as Div<Temperature>>::Output as Div<Temperature>>::Output {
285        Quantity::from_reduced(
286            -self.get_or_compute_derivative_residual(PartialDerivative::Third(DT)),
287        )
288    }
289
290    /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$
291    pub fn dmu_res_dt(&self) -> <MolarEnergy<Array1<f64>> as Div<Temperature>>::Output {
292        Quantity::from_reduced(Array1::from_shape_fn(self.eos.components(), |i| {
293            self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DT, DN(i)))
294        }))
295    }
296
297    /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$
298    pub fn ln_phi(&self) -> Array1<f64> {
299        (self.residual_chemical_potential() / (RGAS * self.temperature)).into_value()
300            - self.compressibility(Contributions::Total).ln()
301    }
302
303    /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure.
304    pub fn ln_phi_pure_liquid(&self) -> EosResult<Array1<f64>> {
305        let pressure = self.pressure(Contributions::Total);
306        (0..self.eos.components())
307            .map(|i| {
308                let eos = Arc::new(self.eos.subset(&[i]));
309                let state = Self::new_npt(
310                    &eos,
311                    self.temperature,
312                    pressure,
313                    &Moles::from_reduced(arr1(&[1.0])),
314                    crate::DensityInitialization::Liquid,
315                )?;
316                Ok(state.ln_phi()[0])
317            })
318            .collect()
319    }
320
321    /// Activity coefficient $\ln \gamma_i = \ln \varphi_i(T, p, \mathbf{N}) - \ln \varphi_i^\mathrm{pure}(T, p)$
322    pub fn ln_symmetric_activity_coefficient(&self) -> EosResult<Array1<f64>> {
323        match self.eos.components() {
324            1 => Ok(arr1(&[0.0])),
325            _ => Ok(self.ln_phi() - &self.ln_phi_pure_liquid()?),
326        }
327    }
328
329    /// 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}}}$
330    ///
331    /// The composition of the (possibly mixed) solvent is determined by the molefracs. All components for which the composition is 0 are treated as solutes.
332    pub fn henrys_law_constant(
333        eos: &Arc<E>,
334        temperature: Temperature,
335        molefracs: &Array1<f64>,
336    ) -> EosResult<Pressure<Array1<f64>>> {
337        // Calculate the phase equilibrium (bubble point) of the solvent only
338        let (solvent_comps, solvent_molefracs): (Vec<_>, Vec<_>) = molefracs
339            .iter()
340            .enumerate()
341            .filter_map(|(i, &x)| (x != 0.0).then_some((i, x)))
342            .unzip();
343        let solvent_molefracs = Array1::from_vec(solvent_molefracs);
344        let solvent = Arc::new(eos.subset(&solvent_comps));
345        let vle = if solvent_comps.len() == 1 {
346            PhaseEquilibrium::pure(&solvent, temperature, None, Default::default())
347        } else {
348            PhaseEquilibrium::bubble_point(
349                &solvent,
350                temperature,
351                &solvent_molefracs,
352                None,
353                None,
354                Default::default(),
355            )
356        }?;
357
358        // Calculate the liquid state including the Henry components
359        let liquid = State::new_nvt(
360            eos,
361            temperature,
362            vle.liquid().volume,
363            &(molefracs * vle.liquid().total_moles),
364        )?;
365
366        // Calculate the vapor state including the Henry components
367        let mut molefracs_vapor = molefracs.clone();
368        solvent_comps
369            .into_iter()
370            .zip(&vle.vapor().molefracs)
371            .for_each(|(i, &y)| molefracs_vapor[i] = y);
372        let vapor = State::new_nvt(
373            eos,
374            temperature,
375            vle.vapor().volume,
376            &(molefracs_vapor * vle.vapor().total_moles),
377        )?;
378
379        // Determine the Henry's law coefficients and return only those of the Henry components
380        let p = vle.vapor().pressure(Contributions::Total);
381        let h = (liquid.ln_phi() - vapor.ln_phi()).mapv(f64::exp) * p;
382        Ok(h.into_iter()
383            .zip(molefracs)
384            .filter_map(|(h, &x)| (x == 0.0).then_some(h))
385            .collect())
386    }
387
388    /// 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
389    ///
390    /// The solute (i) is the first component and the solvent (s) the second component.
391    pub fn henrys_law_constant_binary(
392        eos: &Arc<E>,
393        temperature: Temperature,
394    ) -> EosResult<Pressure> {
395        Ok(Self::henrys_law_constant(eos, temperature, &arr1(&[0.0, 1.0]))?.get(0))
396    }
397
398    /// 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}$
399    pub fn dln_phi_dt(&self) -> <f64 as Div<Temperature<Array1<f64>>>>::Output {
400        let vi = self.partial_molar_volume();
401        (self.dmu_res_dt()
402            - self.residual_chemical_potential() / self.temperature
403            - vi * self.dp_dt(Contributions::Total))
404            / (RGAS * self.temperature)
405            + 1.0 / self.temperature
406    }
407
408    /// 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}$
409    pub fn dln_phi_dp(&self) -> <f64 as Div<Pressure<Array1<f64>>>>::Output {
410        self.partial_molar_volume() / (RGAS * self.temperature)
411            - 1.0 / self.pressure(Contributions::Total)
412    }
413
414    /// 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}$
415    pub fn dln_phi_dnj(&self) -> <f64 as Div<Moles<Array2<f64>>>>::Output {
416        let n = self.eos.components();
417        let dmu_dni = self.dmu_dni(Contributions::Residual);
418        let dp_dni = self.dp_dni(Contributions::Total);
419        let dp_dv = self.dp_dv(Contributions::Total);
420        let dp_dn_2 = Quantity::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j));
421        (dmu_dni + dp_dn_2 / dp_dv) / (RGAS * self.temperature) + 1.0 / self.total_moles
422    }
423
424    /// Thermodynamic factor: $\Gamma_{ij}=\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$
425    pub fn thermodynamic_factor(&self) -> Array2<f64> {
426        let dln_phi_dnj = (self.dln_phi_dnj() * Moles::from_reduced(1.0)).into_value();
427        let moles = self.moles.to_reduced();
428        let n = self.eos.components() - 1;
429        Array2::from_shape_fn((n, n), |(i, j)| {
430            moles[i] * (dln_phi_dnj[[i, j]] - dln_phi_dnj[[i, n]]) + if i == j { 1.0 } else { 0.0 }
431        })
432    }
433
434    /// Residual molar isochoric heat capacity: $c_v^\text{res}=\left(\frac{\partial u^\text{res}}{\partial T}\right)_{V,N_i}$
435    pub fn residual_molar_isochoric_heat_capacity(&self) -> MolarEntropy {
436        self.temperature * self.ds_res_dt() / self.total_moles
437    }
438
439    /// 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}$
440    pub fn dc_v_res_dt(&self) -> <MolarEntropy as Div<Temperature>>::Output {
441        (self.temperature * self.d2s_res_dt2() + self.ds_res_dt()) / self.total_moles
442    }
443
444    /// Residual molar isobaric heat capacity: $c_p^\text{res}=\left(\frac{\partial h^\text{res}}{\partial T}\right)_{p,N_i}$
445    pub fn residual_molar_isobaric_heat_capacity(&self) -> MolarEntropy {
446        self.temperature / self.total_moles
447            * (self.ds_res_dt()
448                - self.dp_dt(Contributions::Total).powi::<P2>() / self.dp_dv(Contributions::Total))
449            - RGAS
450    }
451
452    /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+p^\text{res}V$
453    pub fn residual_enthalpy(&self) -> Energy {
454        self.temperature * self.residual_entropy()
455            + self.residual_helmholtz_energy()
456            + self.pressure(Contributions::Residual) * self.volume
457    }
458
459    /// Residual molar enthalpy: $h^\text{res}(T,p,\mathbf{n})=a^\text{res}+Ts^\text{res}+p^\text{res}v$
460    pub fn residual_molar_enthalpy(&self) -> MolarEnergy {
461        self.residual_enthalpy() / self.total_moles
462    }
463
464    /// Residual internal energy: $U^\text{res}(T,V,\mathbf{n})=A^\text{res}+TS^\text{res}$
465    pub fn residual_internal_energy(&self) -> Energy {
466        self.temperature * self.residual_entropy() + self.residual_helmholtz_energy()
467    }
468
469    /// Residual molar internal energy: $u^\text{res}(T,V,\mathbf{n})=a^\text{res}+Ts^\text{res}$
470    pub fn residual_molar_internal_energy(&self) -> MolarEnergy {
471        self.residual_internal_energy() / self.total_moles
472    }
473
474    /// Residual Gibbs energy: $G^\text{res}(T,p,\mathbf{n})=A^\text{res}+p^\text{res}V-NRT \ln Z$
475    pub fn residual_gibbs_energy(&self) -> Energy {
476        self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy()
477            - self.total_moles
478                * RGAS
479                * self.temperature
480                * self.compressibility(Contributions::Total).ln()
481    }
482
483    /// Residual Gibbs energy: $g^\text{res}(T,p,\mathbf{n})=a^\text{res}+p^\text{res}v-RT \ln Z$
484    pub fn residual_molar_gibbs_energy(&self) -> MolarEnergy {
485        self.residual_gibbs_energy() / self.total_moles
486    }
487}
488
489impl<E: Residual + Molarweight> State<E> {
490    /// Total molar weight: $MW=\sum_ix_iMW_i$
491    pub fn total_molar_weight(&self) -> MolarWeight {
492        (self.eos.molar_weight() * Dimensionless::new(&self.molefracs)).sum()
493    }
494
495    /// Mass of each component: $m_i=n_iMW_i$
496    pub fn mass(&self) -> Mass<Array1<f64>> {
497        self.moles.clone() * self.eos.molar_weight()
498    }
499
500    /// Total mass: $m=\sum_im_i=nMW$
501    pub fn total_mass(&self) -> Mass {
502        self.total_moles * self.total_molar_weight()
503    }
504
505    /// Mass density: $\rho^{(m)}=\frac{m}{V}$
506    pub fn mass_density(&self) -> MassDensity {
507        self.density * self.total_molar_weight()
508    }
509
510    /// Mass fractions: $w_i=\frac{m_i}{m}$
511    pub fn massfracs(&self) -> Array1<f64> {
512        (self.mass() / self.total_mass()).into_value()
513    }
514}
515
516/// # Transport properties
517///
518/// These properties are available for equations of state
519/// that implement the [EntropyScaling] trait.
520impl<E: Residual + EntropyScaling> State<E> {
521    /// Return the viscosity via entropy scaling.
522    pub fn viscosity(&self) -> EosResult<Viscosity> {
523        let s = self.residual_molar_entropy().to_reduced();
524        Ok(self
525            .eos
526            .viscosity_reference(self.temperature, self.volume, &self.moles)?
527            * self.eos.viscosity_correlation(s, &self.molefracs)?.exp())
528    }
529
530    /// Return the logarithm of the reduced viscosity.
531    ///
532    /// This term equals the viscosity correlation function
533    /// that is used for entropy scaling.
534    pub fn ln_viscosity_reduced(&self) -> EosResult<f64> {
535        let s = self.residual_molar_entropy().to_reduced();
536        self.eos.viscosity_correlation(s, &self.molefracs)
537    }
538
539    /// Return the viscosity reference as used in entropy scaling.
540    pub fn viscosity_reference(&self) -> EosResult<Viscosity> {
541        self.eos
542            .viscosity_reference(self.temperature, self.volume, &self.moles)
543    }
544
545    /// Return the diffusion via entropy scaling.
546    pub fn diffusion(&self) -> EosResult<Diffusivity> {
547        let s = self.residual_molar_entropy().to_reduced();
548        Ok(self
549            .eos
550            .diffusion_reference(self.temperature, self.volume, &self.moles)?
551            * self.eos.diffusion_correlation(s, &self.molefracs)?.exp())
552    }
553
554    /// Return the logarithm of the reduced diffusion.
555    ///
556    /// This term equals the diffusion correlation function
557    /// that is used for entropy scaling.
558    pub fn ln_diffusion_reduced(&self) -> EosResult<f64> {
559        let s = self.residual_molar_entropy().to_reduced();
560        self.eos.diffusion_correlation(s, &self.molefracs)
561    }
562
563    /// Return the diffusion reference as used in entropy scaling.
564    pub fn diffusion_reference(&self) -> EosResult<Diffusivity> {
565        self.eos
566            .diffusion_reference(self.temperature, self.volume, &self.moles)
567    }
568
569    /// Return the thermal conductivity via entropy scaling.
570    pub fn thermal_conductivity(&self) -> EosResult<ThermalConductivity> {
571        let s = self.residual_molar_entropy().to_reduced();
572        Ok(self
573            .eos
574            .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)?
575            * self
576                .eos
577                .thermal_conductivity_correlation(s, &self.molefracs)?
578                .exp())
579    }
580
581    /// Return the logarithm of the reduced thermal conductivity.
582    ///
583    /// This term equals the thermal conductivity correlation function
584    /// that is used for entropy scaling.
585    pub fn ln_thermal_conductivity_reduced(&self) -> EosResult<f64> {
586        let s = self.residual_molar_entropy().to_reduced();
587        self.eos
588            .thermal_conductivity_correlation(s, &self.molefracs)
589    }
590
591    /// Return the thermal conductivity reference as used in entropy scaling.
592    pub fn thermal_conductivity_reference(&self) -> EosResult<ThermalConductivity> {
593        self.eos
594            .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)
595    }
596}