diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index e4f97f9d9..b616301d8 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -4,7 +4,7 @@ //! of state - with a single contribution to the Helmholtz energy - can be implemented. //! The implementation closely follows the form of the equations given in //! [this wikipedia article](https://en.wikipedia.org/wiki/Cubic_equations_of_state#Peng%E2%80%93Robinson_equation_of_state). -use crate::equation_of_state::{Components, Residual}; +use crate::equation_of_state::{Components, Molarweight, Residual}; use crate::parameter::{Identifier, Parameter, ParameterError, PureRecord}; use crate::state::StateHD; use ndarray::{Array1, Array2, ScalarOperand}; @@ -214,7 +214,9 @@ impl Residual for PengRobinson { self.residual_helmholtz_energy(state), )] } +} +impl Molarweight for PengRobinson { fn molar_weight(&self) -> MolarWeight> { &self.parameters.molarweight * (GRAM / MOL) } diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 745bf6ccd..6f612f6c5 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -9,7 +9,7 @@ mod ideal_gas; mod residual; pub use ideal_gas::IdealGas; -pub use residual::{EntropyScaling, NoResidual, Residual}; +pub use residual::{EntropyScaling, Molarweight, NoResidual, Residual}; /// The number of components that the model is initialized for. pub trait Components { @@ -91,7 +91,9 @@ impl Residual for EquationOfState { ) -> Vec<(String, D)> { self.residual.residual_helmholtz_energy_contributions(state) } +} +impl Molarweight for EquationOfState { fn molar_weight(&self) -> MolarWeight> { self.residual.molar_weight() } diff --git a/feos-core/src/equation_of_state/residual.rs b/feos-core/src/equation_of_state/residual.rs index a1f8d90fb..9bcb0961a 100644 --- a/feos-core/src/equation_of_state/residual.rs +++ b/feos-core/src/equation_of_state/residual.rs @@ -8,7 +8,14 @@ use quantity::*; use std::ops::Div; use typenum::Quot; -/// A reisdual Helmholtz energy model. +/// Molar weight of all components. +/// +/// Enables calculation of (mass) specific properties. +pub trait Molarweight { + fn molar_weight(&self) -> MolarWeight>; +} + +/// A residual Helmholtz energy model. pub trait Residual: Components + Send + Sync { /// Return the maximum density in Angstrom^-3. /// @@ -18,11 +25,6 @@ pub trait Residual: Components + Send + Sync { /// equation of state anyways). fn compute_max_density(&self, moles: &Array1) -> f64; - /// Molar weight of all components. - /// - /// Enables calculation of (mass) specific properties. - fn molar_weight(&self) -> MolarWeight>; - /// Evaluate the reduced Helmholtz energy of each individual contribution /// and return them together with a string representation of the contribution. fn residual_helmholtz_energy_contributions + Copy + ScalarOperand>( @@ -193,8 +195,4 @@ impl Residual for NoResidual { ) -> Vec<(String, D)> { vec![] } - - fn molar_weight(&self) -> MolarWeight> { - panic!("No mass specific properties are available for this model!") - } } diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 9467ff7bc..202626391 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -33,7 +33,7 @@ pub mod parameter; mod phase_equilibria; mod state; pub use equation_of_state::{ - Components, EntropyScaling, EquationOfState, IdealGas, NoResidual, Residual, + Components, EntropyScaling, EquationOfState, IdealGas, Molarweight, NoResidual, Residual, }; pub use errors::{EosError, EosResult}; pub use phase_equilibria::{ diff --git a/feos-core/src/python/phase_equilibria.rs b/feos-core/src/python/phase_equilibria.rs index 42e20e763..e9b5e00f2 100644 --- a/feos-core/src/python/phase_equilibria.rs +++ b/feos-core/src/python/phase_equilibria.rs @@ -943,21 +943,23 @@ macro_rules! impl_phase_equilibrium { if different_pressures { dict.insert(String::from("pressure vapor"), p_v); dict.insert(String::from("pressure liquid"), p_l); - }else { + } else { dict.insert(String::from("pressure"), p_v); } dict.insert(String::from("density liquid"), self.0.liquid().density().convert_to(MOL / METER.powi::()).into_raw_vec_and_offset().0); dict.insert(String::from("density vapor"), self.0.vapor().density().convert_to(MOL / METER.powi::()).into_raw_vec_and_offset().0); - dict.insert(String::from("mass density liquid"), self.0.liquid().mass_density().convert_to(KILOGRAM / METER.powi::()).into_raw_vec_and_offset().0); - dict.insert(String::from("mass density vapor"), self.0.vapor().mass_density().convert_to(KILOGRAM / METER.powi::()).into_raw_vec_and_offset().0); dict.insert(String::from("molar enthalpy liquid"), self.0.liquid().molar_enthalpy(contributions).convert_to(KILO * JOULE / MOL).into_raw_vec_and_offset().0); dict.insert(String::from("molar enthalpy vapor"), self.0.vapor().molar_enthalpy(contributions).convert_to(KILO * JOULE / MOL).into_raw_vec_and_offset().0); dict.insert(String::from("molar entropy liquid"), self.0.liquid().molar_entropy(contributions).convert_to(KILO * JOULE / KELVIN / MOL).into_raw_vec_and_offset().0); dict.insert(String::from("molar entropy vapor"), self.0.vapor().molar_entropy(contributions).convert_to(KILO * JOULE / KELVIN / MOL).into_raw_vec_and_offset().0); - dict.insert(String::from("specific enthalpy liquid"), self.0.liquid().specific_enthalpy(contributions).convert_to(KILO * JOULE / KILOGRAM).into_raw_vec_and_offset().0); - dict.insert(String::from("specific enthalpy vapor"), self.0.vapor().specific_enthalpy(contributions).convert_to(KILO * JOULE / KILOGRAM).into_raw_vec_and_offset().0); - dict.insert(String::from("specific entropy liquid"), self.0.liquid().specific_entropy(contributions).convert_to(KILO * JOULE / KELVIN / KILOGRAM).into_raw_vec_and_offset().0); - dict.insert(String::from("specific entropy vapor"), self.0.vapor().specific_entropy(contributions).convert_to(KILO * JOULE / KELVIN / KILOGRAM).into_raw_vec_and_offset().0); + if self.0.states[0].liquid().eos.residual.has_molar_weight() { + dict.insert(String::from("mass density liquid"), self.0.liquid().mass_density().convert_to(KILOGRAM / METER.powi::()).into_raw_vec_and_offset().0); + dict.insert(String::from("mass density vapor"), self.0.vapor().mass_density().convert_to(KILOGRAM / METER.powi::()).into_raw_vec_and_offset().0); + dict.insert(String::from("specific enthalpy liquid"), self.0.liquid().specific_enthalpy(contributions).convert_to(KILO * JOULE / KILOGRAM).into_raw_vec_and_offset().0); + dict.insert(String::from("specific enthalpy vapor"), self.0.vapor().specific_enthalpy(contributions).convert_to(KILO * JOULE / KILOGRAM).into_raw_vec_and_offset().0); + dict.insert(String::from("specific entropy liquid"), self.0.liquid().specific_entropy(contributions).convert_to(KILO * JOULE / KELVIN / KILOGRAM).into_raw_vec_and_offset().0); + dict.insert(String::from("specific entropy vapor"), self.0.vapor().specific_entropy(contributions).convert_to(KILO * JOULE / KELVIN / KILOGRAM).into_raw_vec_and_offset().0); + } dict } diff --git a/feos-core/src/python/state.rs b/feos-core/src/python/state.rs index 4beea59f3..35181df19 100644 --- a/feos-core/src/python/state.rs +++ b/feos-core/src/python/state.rs @@ -1254,7 +1254,7 @@ macro_rules! impl_state { /// SIArray1 #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] fn molar_entropy(&self, contributions: Contributions) -> MolarEntropy> { - StateVec::from(self).molar_entropy(contributions).into() + StateVec::from(self).molar_entropy(contributions) } /// Return mass specific entropy. @@ -1270,7 +1270,7 @@ macro_rules! impl_state { /// SIArray1 #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] fn specific_entropy(&self, contributions: Contributions) -> SpecificEntropy> { - StateVec::from(self).specific_entropy(contributions).into() + StateVec::from(self).specific_entropy(contributions) } /// Return molar enthalpy. @@ -1286,7 +1286,7 @@ macro_rules! impl_state { /// SIArray1 #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] fn molar_enthalpy(&self, contributions: Contributions) -> MolarEnergy> { - StateVec::from(self).molar_enthalpy(contributions).into() + StateVec::from(self).molar_enthalpy(contributions) } /// Return mass specific enthalpy. @@ -1302,18 +1302,18 @@ macro_rules! impl_state { /// SIArray1 #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] fn specific_enthalpy(&self, contributions: Contributions) -> SpecificEnergy> { - StateVec::from(self).specific_enthalpy(contributions).into() + StateVec::from(self).specific_enthalpy(contributions) } #[getter] fn get_temperature(&self) -> Temperature> { - StateVec::from(self).temperature().into() + StateVec::from(self).temperature() } #[getter] fn get_pressure(&self) -> Pressure> { - StateVec::from(self).pressure().into() + StateVec::from(self).pressure() } #[getter] @@ -1323,12 +1323,12 @@ macro_rules! impl_state { #[getter] fn get_density(&self) -> Density> { - StateVec::from(self).density().into() + StateVec::from(self).density() } #[getter] fn get_moles<'py>(&self, py: Python<'py>) -> Moles> { - StateVec::from(self).moles().into() + StateVec::from(self).moles() } #[getter] @@ -1337,13 +1337,13 @@ macro_rules! impl_state { } #[getter] - fn get_mass_density(&self) -> MassDensity> { - StateVec::from(self).mass_density().into() + fn get_mass_density(&self) -> Option>> { + self.0[0].eos.residual.has_molar_weight().then(|| StateVec::from(self).mass_density()) } #[getter] - fn get_massfracs<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { - StateVec::from(self).massfracs().into_pyarray_bound(py) + fn get_massfracs<'py>(&self, py: Python<'py>) -> Option>> { + self.0[0].eos.residual.has_molar_weight().then(|| StateVec::from(self).massfracs().into_pyarray_bound(py)) } /// Returns selected properties of a StateVec as dictionary. @@ -1385,11 +1385,13 @@ macro_rules! impl_state { dict.insert(String::from("temperature"), states.temperature().convert_to(KELVIN).into_raw_vec_and_offset().0); dict.insert(String::from("pressure"), states.pressure().convert_to(PASCAL).into_raw_vec_and_offset().0); dict.insert(String::from("density"), states.density().convert_to(MOL / METER.powi::()).into_raw_vec_and_offset().0); - dict.insert(String::from("mass density"), states.mass_density().convert_to(KILOGRAM / METER.powi::()).into_raw_vec_and_offset().0); dict.insert(String::from("molar enthalpy"), states.molar_enthalpy(contributions).convert_to(KILO * JOULE / MOL).into_raw_vec_and_offset().0); dict.insert(String::from("molar entropy"), states.molar_entropy(contributions).convert_to(KILO * JOULE / KELVIN / MOL).into_raw_vec_and_offset().0); - dict.insert(String::from("specific enthalpy"), states.specific_enthalpy(contributions).convert_to(KILO * JOULE / KILOGRAM).into_raw_vec_and_offset().0); - dict.insert(String::from("specific entropy"), states.specific_entropy(contributions).convert_to(KILO * JOULE / KELVIN / KILOGRAM).into_raw_vec_and_offset().0); + if states.0[0].eos.residual.has_molar_weight() { + dict.insert(String::from("mass density"), states.mass_density().convert_to(KILOGRAM / METER.powi::()).into_raw_vec_and_offset().0); + dict.insert(String::from("specific enthalpy"), states.specific_enthalpy(contributions).convert_to(KILO * JOULE / KILOGRAM).into_raw_vec_and_offset().0); + dict.insert(String::from("specific entropy"), states.specific_entropy(contributions).convert_to(KILO * JOULE / KELVIN / KILOGRAM).into_raw_vec_and_offset().0); + } dict } } diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index a88ad3695..414f41fa3 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -1,5 +1,5 @@ #![allow(non_snake_case)] -use crate::{Components, IdealGas, Residual, StateHD}; +use crate::{Components, IdealGas, Molarweight, Residual, StateHD}; use ndarray::{Array1, ScalarOperand}; use num_dual::*; use numpy::convert::IntoPyArray; @@ -203,7 +203,9 @@ macro_rules! impl_residual { ) -> Vec<(String, D)> { vec![("Python".to_string(), self.residual_helmholtz_energy(state))] } + } + impl Molarweight for PyResidual { fn molar_weight(&self) -> MolarWeight> { Python::with_gil(|py| { let py_result = self.0.bind(py).call_method0("molar_weight").unwrap(); diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index bb9510c4c..25567aed7 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -1,5 +1,5 @@ use super::{Contributions, Derivative::*, PartialDerivative, State}; -use crate::equation_of_state::{IdealGas, Residual}; +use crate::equation_of_state::{IdealGas, Molarweight, Residual}; use crate::ReferenceSystem; use ndarray::Array1; use quantity::*; @@ -74,14 +74,6 @@ impl State { self.temperature * self.ds_dt(contributions) / self.total_moles } - /// Specific isochoric heat capacity: $c_v^{(m)}=\frac{C_v}{m}$ - pub fn specific_isochoric_heat_capacity( - &self, - contributions: Contributions, - ) -> SpecificEntropy { - self.molar_isochoric_heat_capacity(contributions) / self.total_molar_weight() - } - /// Partial derivative of the molar isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V}{\partial T}\right)_{V,N_i}$ pub fn dc_v_dt( &self, @@ -103,11 +95,6 @@ impl State { } } - /// Specific isobaric heat capacity: $c_p^{(m)}=\frac{C_p}{m}$ - pub fn specific_isobaric_heat_capacity(&self, contributions: Contributions) -> SpecificEntropy { - self.molar_isobaric_heat_capacity(contributions) / self.total_molar_weight() - } - /// Entropy: $S=-\left(\frac{\partial A}{\partial T}\right)_{V,N_i}$ pub fn entropy(&self, contributions: Contributions) -> Entropy { Entropy::from_reduced( @@ -120,11 +107,6 @@ impl State { self.entropy(contributions) / self.total_moles } - /// Specific entropy: $s^{(m)}=\frac{S}{m}$ - pub fn specific_entropy(&self, contributions: Contributions) -> SpecificEntropy { - self.molar_entropy(contributions) / self.total_molar_weight() - } - /// Partial molar entropy: $s_i=\left(\frac{\partial S}{\partial N_i}\right)_{T,p,N_j}$ pub fn partial_molar_entropy(&self) -> MolarEntropy> { let c = Contributions::Total; @@ -160,11 +142,6 @@ impl State { self.enthalpy(contributions) / self.total_moles } - /// Specific enthalpy: $h^{(m)}=\frac{H}{m}$ - pub fn specific_enthalpy(&self, contributions: Contributions) -> SpecificEnergy { - self.molar_enthalpy(contributions) / self.total_molar_weight() - } - /// Partial molar enthalpy: $h_i=\left(\frac{\partial H}{\partial N_i}\right)_{T,p,N_j}$ pub fn partial_molar_enthalpy(&self) -> MolarEnergy> { let s = self.partial_molar_entropy(); @@ -184,11 +161,6 @@ impl State { self.helmholtz_energy(contributions) / self.total_moles } - /// Specific Helmholtz energy: $a^{(m)}=\frac{A}{m}$ - pub fn specific_helmholtz_energy(&self, contributions: Contributions) -> SpecificEnergy { - self.molar_helmholtz_energy(contributions) / self.total_molar_weight() - } - /// Internal energy: $U=A+TS$ pub fn internal_energy(&self, contributions: Contributions) -> Energy { self.temperature * self.entropy(contributions) + self.helmholtz_energy(contributions) @@ -199,11 +171,6 @@ impl State { self.internal_energy(contributions) / self.total_moles } - /// Specific internal energy: $u^{(m)}=\frac{U}{m}$ - pub fn specific_internal_energy(&self, contributions: Contributions) -> SpecificEnergy { - self.molar_internal_energy(contributions) / self.total_molar_weight() - } - /// Gibbs energy: $G=A+pV$ pub fn gibbs_energy(&self, contributions: Contributions) -> Energy { self.pressure(contributions) * self.volume + self.helmholtz_energy(contributions) @@ -214,11 +181,6 @@ impl State { self.gibbs_energy(contributions) / self.total_moles } - /// Specific Gibbs energy: $g^{(m)}=\frac{G}{m}$ - pub fn specific_gibbs_energy(&self, contributions: Contributions) -> SpecificEnergy { - self.molar_gibbs_energy(contributions) / self.total_molar_weight() - } - /// Joule Thomson coefficient: $\mu_{JT}=\left(\frac{\partial T}{\partial p}\right)_{H,N_i}$ pub fn joule_thomson(&self) -> >::Output { let c = Contributions::Total; @@ -278,6 +240,46 @@ impl State { } res } +} + +impl State { + /// Specific isochoric heat capacity: $c_v^{(m)}=\frac{C_v}{m}$ + pub fn specific_isochoric_heat_capacity( + &self, + contributions: Contributions, + ) -> SpecificEntropy { + self.molar_isochoric_heat_capacity(contributions) / self.total_molar_weight() + } + + /// Specific isobaric heat capacity: $c_p^{(m)}=\frac{C_p}{m}$ + pub fn specific_isobaric_heat_capacity(&self, contributions: Contributions) -> SpecificEntropy { + self.molar_isobaric_heat_capacity(contributions) / self.total_molar_weight() + } + + /// Specific entropy: $s^{(m)}=\frac{S}{m}$ + pub fn specific_entropy(&self, contributions: Contributions) -> SpecificEntropy { + self.molar_entropy(contributions) / self.total_molar_weight() + } + + /// Specific enthalpy: $h^{(m)}=\frac{H}{m}$ + pub fn specific_enthalpy(&self, contributions: Contributions) -> SpecificEnergy { + self.molar_enthalpy(contributions) / self.total_molar_weight() + } + + /// Specific Helmholtz energy: $a^{(m)}=\frac{A}{m}$ + pub fn specific_helmholtz_energy(&self, contributions: Contributions) -> SpecificEnergy { + self.molar_helmholtz_energy(contributions) / self.total_molar_weight() + } + + /// Specific internal energy: $u^{(m)}=\frac{U}{m}$ + pub fn specific_internal_energy(&self, contributions: Contributions) -> SpecificEnergy { + self.molar_internal_energy(contributions) / self.total_molar_weight() + } + + /// Specific Gibbs energy: $g^{(m)}=\frac{G}{m}$ + pub fn specific_gibbs_energy(&self, contributions: Contributions) -> SpecificEnergy { + self.molar_gibbs_energy(contributions) / self.total_molar_weight() + } /// Speed of sound: $c=\sqrt{\left(\frac{\partial p}{\partial\rho^{(m)}}\right)_{S,N_i}}$ pub fn speed_of_sound(&self) -> Velocity { diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 047b04668..51fc1918b 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -1,5 +1,5 @@ use super::{Contributions, Derivative::*, PartialDerivative, State}; -use crate::equation_of_state::{EntropyScaling, Residual}; +use crate::equation_of_state::{EntropyScaling, Molarweight, Residual}; use crate::errors::EosResult; use crate::phase_equilibria::PhaseEquilibrium; use crate::ReferenceSystem; @@ -484,7 +484,9 @@ impl State { pub fn residual_molar_gibbs_energy(&self) -> MolarEnergy { self.residual_gibbs_energy() / self.total_moles } +} +impl State { /// Total molar weight: $MW=\sum_ix_iMW_i$ pub fn total_molar_weight(&self) -> MolarWeight { (self.eos.molar_weight() * Dimensionless::new(&self.molefracs)).sum() diff --git a/feos-core/src/state/statevec.rs b/feos-core/src/state/statevec.rs index d90b8faaa..d8bb00961 100644 --- a/feos-core/src/state/statevec.rs +++ b/feos-core/src/state/statevec.rs @@ -1,10 +1,10 @@ use super::{Contributions, State}; -use crate::equation_of_state::{IdealGas, Residual}; +use crate::equation_of_state::{IdealGas, Molarweight, Residual}; +use ndarray::{Array1, Array2}; use quantity::{ Density, MassDensity, MolarEnergy, MolarEntropy, Moles, Pressure, SpecificEnergy, SpecificEntropy, Temperature, }; -use ndarray::{Array1, Array2}; use std::iter::FromIterator; use std::ops::Deref; @@ -54,10 +54,6 @@ impl<'a, E: Residual> StateVec<'a, E> { Density::from_shape_fn(self.0.len(), |i| self.0[i].density) } - pub fn mass_density(&self) -> MassDensity> { - MassDensity::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) - } - pub fn moles(&self) -> Moles> { Moles::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { self.0[i].moles.get(j) @@ -71,6 +67,18 @@ impl<'a, E: Residual> StateVec<'a, E> { } } +impl<'a, E: Residual + Molarweight> StateVec<'a, E> { + pub fn mass_density(&self) -> MassDensity> { + MassDensity::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) + } + + pub fn massfracs(&self) -> Array2 { + Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].massfracs()[j] + }) + } +} + impl<'a, E: Residual + IdealGas> StateVec<'a, E> { pub fn molar_enthalpy(&self, contributions: Contributions) -> MolarEnergy> { MolarEnergy::from_shape_fn(self.0.len(), |i| self.0[i].molar_enthalpy(contributions)) @@ -79,13 +87,9 @@ impl<'a, E: Residual + IdealGas> StateVec<'a, E> { pub fn molar_entropy(&self, contributions: Contributions) -> MolarEntropy> { MolarEntropy::from_shape_fn(self.0.len(), |i| self.0[i].molar_entropy(contributions)) } +} - pub fn massfracs(&self) -> Array2 { - Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].massfracs()[j] - }) - } - +impl<'a, E: Residual + Molarweight + IdealGas> StateVec<'a, E> { pub fn specific_enthalpy(&self, contributions: Contributions) -> SpecificEnergy> { SpecificEnergy::from_shape_fn(self.0.len(), |i| self.0[i].specific_enthalpy(contributions)) } diff --git a/feos-derive/src/dft.rs b/feos-derive/src/dft.rs index 0b5772e99..91225a8b9 100644 --- a/feos-derive/src/dft.rs +++ b/feos-derive/src/dft.rs @@ -100,12 +100,16 @@ fn impl_helmholtz_energy_functional( }); let mut molar_weight = Vec::new(); + let mut has_molar_weight = Vec::new(); for v in variants.iter() { if implement("molar_weight", v, &OPT_IMPLS)? { let name = &v.ident; molar_weight.push(quote! { Self::#name(functional) => functional.molar_weight() }); + has_molar_weight.push(quote! { + Self::#name(functional) => true + }); } } @@ -137,16 +141,28 @@ fn impl_helmholtz_energy_functional( #(#contributions,)* } } + fn bond_lengths(&self, temperature: f64) -> UnGraph<(), f64> { + match self { + #(#bond_lengths,)* + _ => Graph::with_capacity(0, 0), + } + } + } + + impl Molarweight for FunctionalVariant { fn molar_weight(&self) -> MolarWeight> { match self { #(#molar_weight,)* _ => unimplemented!() } } - fn bond_lengths(&self, temperature: f64) -> UnGraph<(), f64> { + } + + impl FunctionalVariant { + pub fn has_molar_weight(&self) -> bool { match self { - #(#bond_lengths,)* - _ => Graph::with_capacity(0, 0), + #(#has_molar_weight,)* + _ => false, } } } diff --git a/feos-derive/src/residual.rs b/feos-derive/src/residual.rs index 4604e87a1..9d772be67 100644 --- a/feos-derive/src/residual.rs +++ b/feos-derive/src/residual.rs @@ -3,7 +3,7 @@ use quote::quote; use syn::DeriveInput; // possible additional traits to implement -const OPT_IMPLS: [&str; 1] = ["entropy_scaling"]; +const OPT_IMPLS: [&str; 2] = ["molar_weight", "entropy_scaling"]; pub(crate) fn expand_residual(input: DeriveInput) -> syn::Result { let variants = match input.data { @@ -12,9 +12,11 @@ pub(crate) fn expand_residual(input: DeriveInput) -> syn::Result residual.residual_helmholtz_energy_contributions(state) } }); - let molar_weight = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(residual) => residual.molar_weight() - } - }); quote! { impl Residual for ResidualModel { @@ -53,13 +49,47 @@ fn impl_residual( #(#residual_helmholtz_energy_contributions,)* } } + } + } +} + +fn impl_molar_weight( + variants: &syn::punctuated::Punctuated, +) -> syn::Result { + let mut molar_weight = Vec::new(); + let mut has_molar_weight = Vec::new(); + + for v in variants.iter() { + if implement("molar_weight", v, &OPT_IMPLS)? { + let name = &v.ident; + molar_weight.push(quote! { + Self::#name(eos) => eos.molar_weight() + }); + has_molar_weight.push(quote! { + Self::#name(_) => true + }); + } + } + + Ok(quote! { + impl Molarweight for ResidualModel { fn molar_weight(&self) -> MolarWeight> { match self { #(#molar_weight,)* + _ => unimplemented!(), } } } - } + + impl ResidualModel { + pub fn has_molar_weight(&self) -> bool { + match self { + #(#has_molar_weight,)* + _ => false, + } + } + } + }) } fn impl_entropy_scaling( diff --git a/feos-dft/src/adsorption/pore.rs b/feos-dft/src/adsorption/pore.rs index 9890424b8..57fbc017a 100644 --- a/feos-dft/src/adsorption/pore.rs +++ b/feos-dft/src/adsorption/pore.rs @@ -12,9 +12,7 @@ use ndarray::Axis as Axis_nd; use ndarray::RemoveAxis; use num_dual::linalg::LU; use num_dual::DualNum; -use quantity::{ - Density, Dimensionless, Energy, Length, MolarEnergy, MolarWeight, Temperature, Volume, KELVIN, -}; +use quantity::{Density, Dimensionless, Energy, Length, MolarEnergy, Temperature, Volume, KELVIN}; use std::fmt::Display; use std::sync::Arc; @@ -299,10 +297,6 @@ impl HelmholtzEnergyFunctional for Helium { fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::Spherical(1) } - - fn molar_weight(&self) -> MolarWeight> { - unreachable!() - } } impl FluidParameters for Helium { diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index 188202769..8492daf7c 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -4,7 +4,7 @@ use crate::functional_contribution::*; use crate::ideal_chain_contribution::IdealChainContribution; use crate::solvation::PairPotential; use crate::weight_functions::{WeightFunction, WeightFunctionInfo, WeightFunctionShape}; -use feos_core::{Components, EosResult, EquationOfState, IdealGas, Residual, StateHD}; +use feos_core::{Components, EosResult, EquationOfState, IdealGas, Molarweight, Residual, StateHD}; use ndarray::*; use num_dual::*; use petgraph::graph::{Graph, UnGraph}; @@ -28,10 +28,6 @@ impl HelmholtzEnergyF self.residual.molecule_shape() } - fn molar_weight(&self) -> MolarWeight> { - self.residual.molar_weight() - } - fn compute_max_density(&self, moles: &Array1) -> f64 { self.residual.compute_max_density(moles) } @@ -98,10 +94,6 @@ impl Residual for DFT { self.0.compute_max_density(moles) } - fn molar_weight(&self) -> MolarWeight> { - self.0.molar_weight() - } - fn residual_helmholtz_energy_contributions + Copy + ScalarOperand>( &self, state: &StateHD, @@ -119,6 +111,12 @@ impl Residual for DFT { } } +impl Molarweight for DFT { + fn molar_weight(&self) -> MolarWeight> { + self.0.molar_weight() + } +} + impl IdealGas for DFT { fn ln_lambda3 + Copy>(&self, temperature: D) -> Array1 { self.0.ln_lambda3(temperature) @@ -151,11 +149,6 @@ pub trait HelmholtzEnergyFunctional: Components + Sized + Send + Sync { /// Return the shape of the molecules and the necessary specifications. fn molecule_shape(&self) -> MoleculeShape; - /// Molar weight of all components. - /// - /// Enables calculation of (mass) specific properties. - fn molar_weight(&self) -> MolarWeight>; - /// Return the maximum density in Angstrom^-3. /// /// This value is used as an estimate for a liquid phase for phase diff --git a/src/eos.rs b/src/eos.rs index 52abf3b65..cd795cb46 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -15,11 +15,11 @@ use crate::uvtheory::UVTheory; use feos_core::cubic::PengRobinson; #[cfg(feature = "python")] use feos_core::python::user_defined::PyResidual; -use quantity::*; use feos_core::*; use feos_derive::{Components, Residual}; use ndarray::{Array1, ScalarOperand}; use num_dual::DualNum; +use quantity::*; /// Collection of different [Residual] implementations. /// @@ -29,22 +29,29 @@ use num_dual::DualNum; pub enum ResidualModel { NoResidual(NoResidual), #[cfg(feature = "pcsaft")] - #[implement(entropy_scaling)] + #[implement(entropy_scaling, molar_weight)] PcSaft(PcSaft), #[cfg(feature = "epcsaft")] + #[implement(molar_weight)] ElectrolytePcSaft(ElectrolytePcSaft), #[cfg(feature = "gc_pcsaft")] + #[implement(molar_weight)] GcPcSaft(GcPcSaft), + #[implement(molar_weight)] PengRobinson(PengRobinson), #[cfg(feature = "python")] + #[implement(molar_weight)] Python(PyResidual), #[cfg(feature = "saftvrqmie")] - #[implement(entropy_scaling)] + #[implement(entropy_scaling, molar_weight)] SaftVRQMie(SaftVRQMie), #[cfg(feature = "saftvrmie")] + #[implement(molar_weight)] SaftVRMie(SaftVRMie), #[cfg(feature = "pets")] + #[implement(molar_weight)] Pets(Pets), #[cfg(feature = "uvtheory")] + #[implement(molar_weight)] UVTheory(UVTheory), } diff --git a/src/epcsaft/eos/mod.rs b/src/epcsaft/eos/mod.rs index ce4a7ef5e..e6a90b8c8 100644 --- a/src/epcsaft/eos/mod.rs +++ b/src/epcsaft/eos/mod.rs @@ -2,8 +2,8 @@ use crate::association::Association; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; use crate::hard_sphere::{HardSphere, HardSphereProperties}; use feos_core::parameter::Parameter; -use feos_core::StateHD; use feos_core::{Components, Residual}; +use feos_core::{Molarweight, StateHD}; use ndarray::Array1; use num_dual::DualNum; use quantity::*; @@ -187,7 +187,9 @@ impl Residual for ElectrolytePcSaft { }; v } +} +impl Molarweight for ElectrolytePcSaft { fn molar_weight(&self) -> MolarWeight> { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/estimator/liquid_density.rs b/src/estimator/liquid_density.rs index 3c20084d2..f4441ba2c 100644 --- a/src/estimator/liquid_density.rs +++ b/src/estimator/liquid_density.rs @@ -1,6 +1,7 @@ use super::{DataSet, EstimatorError}; use feos_core::{ - DensityInitialization, PhaseEquilibrium, ReferenceSystem, Residual, SolverOptions, State, + DensityInitialization, Molarweight, PhaseEquilibrium, ReferenceSystem, Residual, SolverOptions, + State, }; use ndarray::{arr1, Array1}; use quantity::{MassDensity, Moles, Pressure, Temperature, KILOGRAM, METER}; @@ -47,7 +48,7 @@ impl LiquidDensity { } } -impl DataSet for LiquidDensity { +impl DataSet for LiquidDensity { fn target(&self) -> &Array1 { &self.target } @@ -119,7 +120,7 @@ impl EquilibriumLiquidDensity { } } -impl DataSet for EquilibriumLiquidDensity { +impl DataSet for EquilibriumLiquidDensity { fn target(&self) -> &Array1 { &self.target } diff --git a/src/gc_pcsaft/dft/mod.rs b/src/gc_pcsaft/dft/mod.rs index 127f4b7d9..3f753dcc4 100644 --- a/src/gc_pcsaft/dft/mod.rs +++ b/src/gc_pcsaft/dft/mod.rs @@ -3,8 +3,7 @@ use super::record::GcPcSaftAssociationRecord; use crate::association::{Association, AssociationStrength}; use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, MonomerShape}; use feos_core::parameter::ParameterHetero; -use quantity::{MolarWeight, GRAM, MOL}; -use feos_core::{Components, EosResult}; +use feos_core::{Components, EosResult, Molarweight}; use feos_derive::FunctionalContribution; use feos_dft::adsorption::FluidParameters; use feos_dft::{ @@ -13,6 +12,7 @@ use feos_dft::{ use ndarray::{Array1, ArrayView2, ScalarOperand}; use num_dual::DualNum; use petgraph::graph::UnGraph; +use quantity::{MolarWeight, GRAM, MOL}; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -110,10 +110,6 @@ impl HelmholtzEnergyFunctional for GcPcSaftFunctional { Box::new(contributions.into_iter()) } - fn molar_weight(&self) -> MolarWeight> { - self.parameters.molarweight.clone() * GRAM / MOL - } - fn bond_lengths(&self, temperature: f64) -> UnGraph<(), f64> { // temperature dependent segment diameter let d = self.parameters.hs_diameter(temperature); @@ -130,6 +126,12 @@ impl HelmholtzEnergyFunctional for GcPcSaftFunctional { } } +impl Molarweight for GcPcSaftFunctional { + fn molar_weight(&self) -> MolarWeight> { + self.parameters.molarweight.clone() * GRAM / MOL + } +} + impl HardSphereProperties for GcPcSaftFunctionalParameters { fn monomer_shape>(&self, _: N) -> MonomerShape { let m = self.m.mapv(N::from); diff --git a/src/gc_pcsaft/eos/mod.rs b/src/gc_pcsaft/eos/mod.rs index df7760f1d..76e204d91 100644 --- a/src/gc_pcsaft/eos/mod.rs +++ b/src/gc_pcsaft/eos/mod.rs @@ -1,7 +1,7 @@ use crate::association::Association; use crate::hard_sphere::{HardSphere, HardSphereProperties}; use feos_core::parameter::ParameterHetero; -use feos_core::{Components, Residual}; +use feos_core::{Components, Molarweight, Residual}; use ndarray::Array1; use quantity::{MolarWeight, GRAM, MOL}; use std::f64::consts::FRAC_PI_6; @@ -139,7 +139,9 @@ impl Residual for GcPcSaft { } v } +} +impl Molarweight for GcPcSaft { fn molar_weight(&self) -> MolarWeight> { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/hard_sphere/dft.rs b/src/hard_sphere/dft.rs index a5095069b..8be7677a9 100644 --- a/src/hard_sphere/dft.rs +++ b/src/hard_sphere/dft.rs @@ -7,7 +7,6 @@ use feos_dft::{ }; use ndarray::*; use num_dual::DualNum; -use quantity::MolarWeight; use std::f64::consts::PI; use std::fmt; use std::sync::Arc; @@ -353,10 +352,6 @@ impl HelmholtzEnergyFunctional for FMTFunctional { moles.sum() / (moles * &self.properties.sigma).sum() * 1.2 } - fn molar_weight(&self) -> MolarWeight> { - panic!("No mass specific properties are available for this model!") - } - fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::Spherical(self.properties.sigma.len()) } diff --git a/src/pcsaft/dft/mod.rs b/src/pcsaft/dft/mod.rs index 81e3d241e..d2d186960 100644 --- a/src/pcsaft/dft/mod.rs +++ b/src/pcsaft/dft/mod.rs @@ -3,8 +3,7 @@ use crate::association::Association; use crate::hard_sphere::{FMTContribution, FMTVersion}; use crate::pcsaft::eos::PcSaftOptions; use feos_core::parameter::Parameter; -use quantity::{MolarWeight, GRAM, MOL}; -use feos_core::{Components, EosResult}; +use feos_core::{Components, EosResult, Molarweight}; use feos_derive::FunctionalContribution; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; @@ -14,6 +13,7 @@ use feos_dft::{ use ndarray::{Array1, Array2, ArrayView2, ScalarOperand}; use num_dual::DualNum; use num_traits::One; +use quantity::{MolarWeight, GRAM, MOL}; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -126,7 +126,9 @@ impl HelmholtzEnergyFunctional for PcSaftFunctional { fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::NonSpherical(&self.parameters.m) } +} +impl Molarweight for PcSaftFunctional { fn molar_weight(&self) -> MolarWeight> { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index 08f641ed8..6df06192d 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -3,7 +3,8 @@ use crate::association::Association; use crate::hard_sphere::{HardSphere, HardSphereProperties}; use feos_core::parameter::Parameter; use feos_core::{ - Components, EntropyScaling, EosError, EosResult, ReferenceSystem, Residual, State, StateHD, + Components, EntropyScaling, EosError, EosResult, Molarweight, ReferenceSystem, Residual, State, + StateHD, }; use ndarray::Array1; use num_dual::DualNum; @@ -180,7 +181,9 @@ impl Residual for PcSaft { } v } +} +impl Molarweight for PcSaft { fn molar_weight(&self) -> MolarWeight> { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/pets/dft/mod.rs b/src/pets/dft/mod.rs index 44cb18622..bc76ae905 100644 --- a/src/pets/dft/mod.rs +++ b/src/pets/dft/mod.rs @@ -3,8 +3,7 @@ use super::parameters::PetsParameters; use crate::hard_sphere::{FMTContribution, FMTVersion}; use dispersion::AttractiveFunctional; use feos_core::parameter::Parameter; -use quantity::{MolarWeight, GRAM, MOL}; -use feos_core::{Components, EosResult}; +use feos_core::{Components, EosResult, Molarweight}; use feos_derive::FunctionalContribution; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; @@ -14,6 +13,7 @@ use feos_dft::{ use ndarray::{Array1, Array2, ArrayView2, ScalarOperand}; use num_dual::DualNum; use pure_pets_functional::*; +use quantity::{MolarWeight, GRAM, MOL}; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -111,7 +111,9 @@ impl HelmholtzEnergyFunctional for PetsFunctional { Box::new(contributions.into_iter()) } +} +impl Molarweight for PetsFunctional { fn molar_weight(&self) -> MolarWeight> { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/pets/eos/mod.rs b/src/pets/eos/mod.rs index 3166d3de8..623cfde6b 100644 --- a/src/pets/eos/mod.rs +++ b/src/pets/eos/mod.rs @@ -1,9 +1,9 @@ use super::parameters::PetsParameters; use crate::hard_sphere::HardSphere; use feos_core::parameter::Parameter; -use quantity::{MolarWeight, GRAM, MOL}; -use feos_core::{Components, Residual}; +use feos_core::{Components, Molarweight, Residual}; use ndarray::Array1; +use quantity::{MolarWeight, GRAM, MOL}; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -89,7 +89,9 @@ impl Residual for Pets { ), ] } +} +impl Molarweight for Pets { fn molar_weight(&self) -> MolarWeight> { self.parameters.molarweight.clone() * GRAM / MOL } @@ -314,9 +316,9 @@ mod tests { argon_krypton_parameters, argon_parameters, krypton_parameters, }; use approx::assert_relative_eq; - use quantity::{BAR, KELVIN, METER, PASCAL, RGAS}; use feos_core::{Contributions, DensityInitialization, PhaseEquilibrium, State, StateHD}; use ndarray::arr1; + use quantity::{BAR, KELVIN, METER, PASCAL, RGAS}; use typenum::P3; #[test] diff --git a/src/saftvrmie/eos/mod.rs b/src/saftvrmie/eos/mod.rs index 4b730b196..304b1cffe 100644 --- a/src/saftvrmie/eos/mod.rs +++ b/src/saftvrmie/eos/mod.rs @@ -3,7 +3,7 @@ use crate::hard_sphere::HardSphere; use super::SaftVRMieParameters; use association::Association; use feos_core::parameter::Parameter; -use feos_core::{Components, Residual, StateHD}; +use feos_core::{Components, Molarweight, Residual, StateHD}; use ndarray::{Array1, ScalarOperand}; use num_dual::DualNum; use quantity::{MolarWeight, GRAM, MOL}; @@ -83,10 +83,6 @@ impl Residual for SaftVRMie { .sum() } - fn molar_weight(&self) -> MolarWeight> { - self.parameters.molarweight.clone() * GRAM / MOL - } - fn residual_helmholtz_energy_contributions + Copy + ScalarOperand>( &self, state: &StateHD, @@ -110,3 +106,9 @@ impl Residual for SaftVRMie { a } } + +impl Molarweight for SaftVRMie { + fn molar_weight(&self) -> MolarWeight> { + self.parameters.molarweight.clone() * GRAM / MOL + } +} diff --git a/src/saftvrqmie/dft/mod.rs b/src/saftvrqmie/dft/mod.rs index 346dfaf53..dd81e02eb 100644 --- a/src/saftvrqmie/dft/mod.rs +++ b/src/saftvrqmie/dft/mod.rs @@ -3,8 +3,7 @@ use crate::saftvrqmie::eos::SaftVRQMieOptions; use crate::saftvrqmie::parameters::SaftVRQMieParameters; use dispersion::AttractiveFunctional; use feos_core::parameter::Parameter; -use quantity::{MolarWeight, GRAM, MOL}; -use feos_core::{Components, EosResult}; +use feos_core::{Components, EosResult, Molarweight}; use feos_derive::FunctionalContribution; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; @@ -14,6 +13,7 @@ use feos_dft::{ use ndarray::{Array, Array1, Array2, ArrayView2, ScalarOperand}; use non_additive_hs::NonAddHardSphereFunctional; use num_dual::DualNum; +use quantity::{MolarWeight, GRAM, MOL}; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -97,15 +97,17 @@ impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { Box::new(contributions.into_iter()) } - fn molar_weight(&self) -> MolarWeight> { - self.parameters.molarweight.clone() * GRAM / MOL - } - fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::NonSpherical(&self.parameters.m) } } +impl Molarweight for SaftVRQMieFunctional { + fn molar_weight(&self) -> MolarWeight> { + self.parameters.molarweight.clone() * GRAM / MOL + } +} + impl HardSphereProperties for SaftVRQMieParameters { fn monomer_shape>(&self, _: N) -> MonomerShape { MonomerShape::Spherical(self.m.len()) diff --git a/src/saftvrqmie/eos/mod.rs b/src/saftvrqmie/eos/mod.rs index d34675e58..205606a32 100644 --- a/src/saftvrqmie/eos/mod.rs +++ b/src/saftvrqmie/eos/mod.rs @@ -1,7 +1,7 @@ use super::parameters::SaftVRQMieParameters; use feos_core::parameter::{Parameter, ParameterError}; use feos_core::{ - Components, EntropyScaling, EosError, EosResult, ReferenceSystem, Residual, State, + Components, EntropyScaling, EosError, EosResult, Molarweight, ReferenceSystem, Residual, State, }; use ndarray::{Array1, Array2}; use num_dual::DualNum; @@ -184,7 +184,9 @@ impl Residual for SaftVRQMie { } v } +} +impl Molarweight for SaftVRQMie { fn molar_weight(&self) -> MolarWeight> { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/uvtheory/eos/mod.rs b/src/uvtheory/eos/mod.rs index d317c96b0..421fcf189 100644 --- a/src/uvtheory/eos/mod.rs +++ b/src/uvtheory/eos/mod.rs @@ -1,18 +1,17 @@ #![allow(clippy::excessive_precision)] #![allow(clippy::needless_range_loop)] - -use self::bh::BarkerHenderson; -use self::wca::{WeeksChandlerAndersen, WeeksChandlerAndersenB3}; - use super::parameters::UVTheoryParameters; -use feos_core::{parameter::Parameter, Components, Residual}; +use feos_core::parameter::Parameter; +use feos_core::{Components, Molarweight, Residual}; use ndarray::Array1; use quantity::{MolarWeight, GRAM, MOL}; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; mod bh; +use bh::BarkerHenderson; mod wca; +use wca::{WeeksChandlerAndersen, WeeksChandlerAndersenB3}; /// Type of perturbation. #[derive(Clone, Copy, PartialEq)] @@ -118,7 +117,9 @@ impl Residual for UVTheory { } } } +} +impl Molarweight for UVTheory { fn molar_weight(&self) -> MolarWeight> { self.parameters.molarweight.clone() * GRAM / MOL }