From ecc6f54f8e32bc6973c33b0b2645c192c7c06fad Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 1 Jun 2023 17:11:59 +0200 Subject: [PATCH 01/47] Revised handling of ideal gas contribution --- feos-core/src/density_iteration.rs | 6 +- feos-core/src/equation_of_state/debroglie.rs | 47 ++ .../src/equation_of_state/helmholtz_energy.rs | 59 +++ feos-core/src/equation_of_state/ideal_gas.rs | 48 ++ feos-core/src/equation_of_state/mod.rs | 176 +++++++ feos-core/src/equation_of_state/residual.rs | 155 ++++++ ...n_of_state.rs => equation_of_state_old.rs} | 0 feos-core/src/errors.rs | 6 +- feos-core/src/lib.rs | 31 +- feos-core/src/state/mod.rs | 175 +++++-- feos-core/src/state/properties.rs | 454 ++---------------- feos-core/src/state/residual_properties.rs | 343 +++++++++++++ feos-core/src/state/statevec.rs | 103 ++++ 13 files changed, 1125 insertions(+), 478 deletions(-) create mode 100644 feos-core/src/equation_of_state/debroglie.rs create mode 100644 feos-core/src/equation_of_state/helmholtz_energy.rs create mode 100644 feos-core/src/equation_of_state/ideal_gas.rs create mode 100644 feos-core/src/equation_of_state/mod.rs create mode 100644 feos-core/src/equation_of_state/residual.rs rename feos-core/src/{equation_of_state.rs => equation_of_state_old.rs} (100%) create mode 100644 feos-core/src/state/residual_properties.rs create mode 100644 feos-core/src/state/statevec.rs diff --git a/feos-core/src/density_iteration.rs b/feos-core/src/density_iteration.rs index 54c236274..541362493 100644 --- a/feos-core/src/density_iteration.rs +++ b/feos-core/src/density_iteration.rs @@ -1,11 +1,11 @@ -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::State; use crate::EosUnit; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::sync::Arc; -pub fn density_iteration( +pub fn density_iteration( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -144,7 +144,7 @@ pub fn density_iteration( } } -fn pressure_spinodal( +fn pressure_spinodal( eos: &Arc, temperature: SINumber, rho_init: SINumber, diff --git a/feos-core/src/equation_of_state/debroglie.rs b/feos-core/src/equation_of_state/debroglie.rs new file mode 100644 index 000000000..fa0ca9c65 --- /dev/null +++ b/feos-core/src/equation_of_state/debroglie.rs @@ -0,0 +1,47 @@ +use ndarray::Array1; +use num_dual::*; +use std::fmt; + +pub trait DeBroglieWavelengthDual> { + fn de_broglie_wavelength(&self, temperature: D) -> Array1; +} + +pub trait DeBroglieWavelength: + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + fmt::Display + + Send + + Sync +{ +} + +impl DeBroglieWavelength for T where + T: DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + fmt::Display + + Send + + Sync +{ +} diff --git a/feos-core/src/equation_of_state/helmholtz_energy.rs b/feos-core/src/equation_of_state/helmholtz_energy.rs new file mode 100644 index 000000000..11d2c2703 --- /dev/null +++ b/feos-core/src/equation_of_state/helmholtz_energy.rs @@ -0,0 +1,59 @@ +use crate::StateHD; +use num_dual::*; +use std::fmt; + +/// Individual Helmholtz energy contribution that can +/// be evaluated using generalized (hyper) dual numbers. +/// +/// This trait needs to be implemented generically or for +/// the specific types in the supertraits of [HelmholtzEnergy] +/// so that the implementor can be used as a Helmholtz energy +/// contribution in the equation of state. +pub trait HelmholtzEnergyDual> { + /// The Helmholtz energy contribution $\beta A$ of a given state in reduced units. + fn helmholtz_energy(&self, state: &StateHD) -> D; +} + +/// Object safe version of the [HelmholtzEnergyDual] trait. +/// +/// The trait is implemented automatically for every struct that implements +/// the supertraits. +pub trait HelmholtzEnergy: + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + fmt::Display + + Send + + Sync +{ +} + +impl HelmholtzEnergy for T where + T: HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + fmt::Display + + Send + + Sync +{ +} diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs new file mode 100644 index 000000000..ac85b23e2 --- /dev/null +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -0,0 +1,48 @@ +use crate::StateHD; +use ndarray::Array1; +use num_dual::DualNum; +use std::fmt; + +use super::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; + +/// Ideal gas Helmholtz energy contribution that can +/// be evaluated using generalized (hyper) dual numbers. +/// +/// This trait needs to be implemented generically or for +/// the specific types in the supertraits of [IdealGasContribution] +/// so that the implementor can be used as an ideal gas +/// contribution in the equation of state. +pub trait IdealGas: Sync + Send + fmt::Display { + // /// Return the number of components + // fn components(&self) -> usize; + + // /// Return an equation of state consisting of the components + // /// contained in component_list. + // fn subset(&self, component_list: &[usize]) -> Self; + + fn de_broglie_wavelength(&self) -> &Box; + + /// Evaluate the ideal gas contribution for a given state. + /// + /// In some cases it could be advantageous to overwrite this + /// implementation instead of implementing the de Broglie + /// wavelength. + fn evaluate_ideal_gas>(&self, state: &StateHD) -> D + where + dyn DeBroglieWavelength: DeBroglieWavelengthDual, + { + let lambda = self + .de_broglie_wavelength() + .de_broglie_wavelength(state.temperature); + ((lambda + + state.partial_density.mapv(|x| { + if x.re() == 0.0 { + D::from(0.0) + } else { + x.ln() - 1.0 + } + })) + * &state.moles) + .sum() + } +} diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs new file mode 100644 index 000000000..a408c48ca --- /dev/null +++ b/feos-core/src/equation_of_state/mod.rs @@ -0,0 +1,176 @@ +use crate::{EosError, EosResult, EosUnit, StateHD}; +use ndarray::{Array, Array1}; +use num_dual::{Dual3, Dual3_64, Dual64, DualNum, HyperDual, HyperDual64}; +use num_traits::{One, Zero}; +use quantity::si::{SIArray1, SINumber, SIUnit, MOL}; +use std::sync::Arc; + +pub use ideal_gas::IdealGas; +pub use residual::Residual; +pub mod debroglie; +pub mod helmholtz_energy; +pub mod ideal_gas; +pub mod residual; +pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; +pub use ideal_gas::{DefaultIdealGas, IdealGas}; +pub use residual::Residual; + +pub use self::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; + +/// Molar weight of all components. +/// +/// The trait is required to be able to calculate (mass) +/// specific properties. +pub trait MolarWeight { + fn molar_weight(&self) -> SIArray1; +} + +#[derive(Clone)] +pub struct Model { + pub ideal_gas: Arc, + pub residual: Arc, + components: usize, +} + +impl Model { + pub fn new(ideal_gas: Arc, residual: Arc) -> Self { + assert_eq!(residual.components(), ideal_gas.components()); + let components = residual.components(); + Self { + ideal_gas, + residual, + components, + } + } + + pub fn components(&self) -> usize { + self.components + } + + pub fn subset(&self, component_list: &[usize]) -> Self { + Self::new( + Arc::new(self.ideal_gas.subset(component_list)), + Arc::new(self.residual.subset(component_list)), + ) + } + + /// Check if the provided optional mole number is consistent with the + /// equation of state. + /// + /// In general, the number of elements in `moles` needs to match the number + /// of components of the equation of state. For a pure component, however, + /// no moles need to be provided. In that case, it is set to the constant + /// reference value. + pub fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { + let l = moles.map_or(1, |m| m.len()); + if self.components() == l { + match moles { + Some(m) => Ok(m.to_owned()), + None => Ok(Array::ones(1) * SIUnit::reference_moles()), + } + } else { + Err(EosError::IncompatibleComponents(self.components(), l)) + } + } + + /// Calculate the maximum density. + /// + /// This value is used as an estimate for a liquid phase for phase + /// equilibria and other iterations. It is not explicitly meant to + /// be a mathematical limit for the density (if those exist in the + /// equation of state anyways). + pub fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { + let mr = self + .residual + .validate_moles(moles)? + .to_reduced(SIUnit::reference_moles())?; + Ok(self.residual.compute_max_density(&mr) * SIUnit::reference_density()) + } + + pub fn evaluate_residual>(&self, state: &StateHD) -> D + where + dyn HelmholtzEnergy: HelmholtzEnergyDual, + { + self.residual.helmholtz_energy(state) + } + + pub fn evaluate_ideal_gas>(&self, state: &StateHD) -> D + where + dyn DeBroglieWavelength: DeBroglieWavelengthDual, + { + self.ideal_gas.evaluate_ideal_gas(state) + } + + /// Calculate the second virial coefficient $B(T)$ + pub fn second_virial_coefficient( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let mut rho = HyperDual64::zero(); + rho.eps1[0] = 1.0; + rho.eps2[0] = 1.0; + let t = HyperDual64::from(temperature.to_reduced(SIUnit::reference_temperature())?); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)] * 0.5 / SIUnit::reference_density()) + } + + /// Calculate the third virial coefficient $C(T)$ + pub fn third_virial_coefficient( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let rho = Dual3_64::zero().derive(); + let t = Dual3_64::from(temperature.to_reduced(SIUnit::reference_temperature())?); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).v3 / 3.0 / SIUnit::reference_density().powi(2)) + } + + /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ + pub fn second_virial_coefficient_temperature_derivative( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let mut rho = HyperDual::zero(); + rho.eps1[0] = Dual64::one(); + rho.eps2[0] = Dual64::one(); + let t = HyperDual::from_re( + Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), + ); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)].eps[0] * 0.5 + / (SIUnit::reference_density() * SIUnit::reference_temperature())) + } + + /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ + pub fn third_virial_coefficient_temperature_derivative( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let rho = Dual3::zero().derive(); + let t = Dual3::from_re( + Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), + ); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).v3.eps[0] + / 3.0 + / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) + } +} + +impl Model { + pub fn molar_weight(&self) -> Array1 { + self.residual.molar_weight().to_reduced(MOL).unwrap() + } +} diff --git a/feos-core/src/equation_of_state/residual.rs b/feos-core/src/equation_of_state/residual.rs new file mode 100644 index 000000000..63ed066f9 --- /dev/null +++ b/feos-core/src/equation_of_state/residual.rs @@ -0,0 +1,155 @@ +use super::{HelmholtzEnergy, HelmholtzEnergyDual}; +use crate::StateHD; +use crate::{EosError, EosResult, EosUnit}; +use ndarray::prelude::*; +use num_dual::*; +use num_traits::{One, Zero}; +use quantity::si::{SIArray1, SINumber, SIUnit}; +use std::fmt; + +/// A general equation of state. +pub trait Residual: Send + Sync + fmt::Display { + /// Return the number of components of the equation of state. + fn components(&self) -> usize; + + /// Return an equation of state consisting of the components + /// contained in component_list. + fn subset(&self, component_list: &[usize]) -> Self; + + /// Return the maximum density in Angstrom^-3. + /// + /// This value is used as an estimate for a liquid phase for phase + /// equilibria and other iterations. It is not explicitly meant to + /// be a mathematical limit for the density (if those exist in the + /// equation of state anyways). + fn compute_max_density(&self, moles: &Array1) -> f64; + + /// Return a slice of the individual contributions (excluding the ideal gas) + /// of the equation of state. + fn contributions(&self) -> &[Box]; + + /// Evaluate the residual reduced Helmholtz energy $\beta A^\mathrm{res}$. + fn evaluate_residual>(&self, state: &StateHD) -> D + where + dyn HelmholtzEnergy: HelmholtzEnergyDual, + { + self.contributions() + .iter() + .map(|c| c.helmholtz_energy(state)) + .sum() + } + + /// Evaluate the reduced Helmholtz energy of each individual contribution + /// and return them together with a string representation of the contribution. + fn evaluate_residual_contributions>( + &self, + state: &StateHD, + ) -> Vec<(String, D)> + where + dyn HelmholtzEnergy: HelmholtzEnergyDual, + { + self.contributions() + .iter() + .map(|c| (c.to_string(), c.helmholtz_energy(state))) + .collect() + } + + /// Check if the provided optional mole number is consistent with the + /// equation of state. + /// + /// In general, the number of elements in `moles` needs to match the number + /// of components of the equation of state. For a pure component, however, + /// no moles need to be provided. In that case, it is set to the constant + /// reference value. + fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { + let l = moles.map_or(1, |m| m.len()); + if self.components() == l { + match moles { + Some(m) => Ok(m.to_owned()), + None => Ok(Array::ones(1) * SIUnit::reference_moles()), + } + } else { + Err(EosError::IncompatibleComponents(self.components(), l)) + } + } + + /// Calculate the maximum density. + /// + /// This value is used as an estimate for a liquid phase for phase + /// equilibria and other iterations. It is not explicitly meant to + /// be a mathematical limit for the density (if those exist in the + /// equation of state anyways). + fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { + let mr = self + .validate_moles(moles)? + .to_reduced(SIUnit::reference_moles())?; + Ok(self.compute_max_density(&mr) * SIUnit::reference_density()) + } + + /// Calculate the second virial coefficient $B(T)$ + fn second_virial_coefficient( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let mut rho = HyperDual64::zero(); + rho.eps1 = 1.0; + rho.eps2 = 1.0; + let t = HyperDual64::from(temperature.to_reduced(SIUnit::reference_temperature())?); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).eps1eps2 * 0.5 / SIUnit::reference_density()) + } + + /// Calculate the third virial coefficient $C(T)$ + fn third_virial_coefficient( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let rho = Dual3_64::zero().derivative(); + let t = Dual3_64::from(temperature.to_reduced(SIUnit::reference_temperature())?); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).v3 / 3.0 / SIUnit::reference_density().powi(2)) + } + + /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ + fn second_virial_coefficient_temperature_derivative( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let mut rho = HyperDual::zero(); + rho.eps1 = Dual64::one(); + rho.eps2 = Dual64::one(); + let t = HyperDual::from_re( + Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derivative(), + ); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).eps1eps2.eps * 0.5 + / (SIUnit::reference_density() * SIUnit::reference_temperature())) + } + + /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ + fn third_virial_coefficient_temperature_derivative( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let rho = Dual3::zero().derivative(); + let t = Dual3::from_re( + Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derivative(), + ); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).v3.eps + / 3.0 + / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) + } +} diff --git a/feos-core/src/equation_of_state.rs b/feos-core/src/equation_of_state_old.rs similarity index 100% rename from feos-core/src/equation_of_state.rs rename to feos-core/src/equation_of_state_old.rs diff --git a/feos-core/src/errors.rs b/feos-core/src/errors.rs index 37936a4e6..d93060035 100644 --- a/feos-core/src/errors.rs +++ b/feos-core/src/errors.rs @@ -1,4 +1,4 @@ -use crate::parameter::ParameterError; +// use crate::parameter::ParameterError; use num_dual::linalg::LinAlgError; use quantity::QuantityError; use thiserror::Error; @@ -28,8 +28,8 @@ pub enum EosError { WrongUnits(String, String), #[error(transparent)] QuantityError(#[from] QuantityError), - #[error(transparent)] - ParameterError(#[from] ParameterError), + // #[error(transparent)] + // ParameterError(#[from] ParameterError), #[error(transparent)] LinAlgError(#[from] LinAlgError), #[cfg(feature = "rayon")] diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 54cb98ff1..03d0d0973 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::reversed_empty_ranges)] #![allow(clippy::many_single_char_names)] #![allow(clippy::too_many_arguments)] +#![allow(deprecated)] use quantity::si::*; use quantity::*; @@ -26,24 +27,30 @@ macro_rules! log_result { } } -pub mod cubic; +// pub mod cubic; mod density_iteration; mod equation_of_state; mod errors; -pub mod joback; -pub mod parameter; -mod phase_equilibria; +// pub mod joback; +// pub mod parameter; +// mod phase_equilibria; mod state; -pub use equation_of_state::{ - EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGasContribution, - IdealGasContributionDual, MolarWeight, -}; +// pub use equation_of_state::{ +// EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGasContribution, +// IdealGasContributionDual, MolarWeight, +// }; pub use errors::{EosError, EosResult}; -pub use phase_equilibria::{ - PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, -}; +// pub use phase_equilibria::{ +// PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, +// }; pub use state::{ - Contributions, DensityInitialization, Derivative, State, StateBuilder, StateHD, StateVec, + Contributions, + DensityInitialization, + Derivative, + State, + // StateBuilder, + StateHD, + StateVec, }; #[cfg(feature = "python")] diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index ca566add3..175d8fb05 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -7,7 +7,7 @@ //! //! Internally, all properties are computed using such states as input. use crate::density_iteration::density_iteration; -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::{IdealGas, Residual}; use crate::errors::{EosError, EosResult}; use crate::EosUnit; use cache::Cache; @@ -19,11 +19,27 @@ use std::convert::TryFrom; use std::fmt; use std::sync::{Arc, Mutex}; -mod builder; +// mod builder; mod cache; mod properties; -pub use builder::StateBuilder; -pub use properties::{Contributions, StateVec}; +mod residual_properties; +mod statevec; +// pub use builder::StateBuilder; +pub use statevec::StateVec; + +/// Possible contributions that can be computed. +#[derive(Clone, Copy)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum Contributions { + /// Only compute the ideal gas contribution + IdealGas, + /// Only compute the difference between the total and the ideal gas contribution + Residual, + // /// Compute the differnce between the total and the ideal gas contribution for a (N,p,T) reference state + // ResidualNpt, + /// Compute ideal gas and residual contributions + Total, +} /// Initial values in a density iteration. #[derive(Clone, Copy)] @@ -166,11 +182,10 @@ impl Clone for State { } } -impl fmt::Display for State +impl fmt::Display for State where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.eos.components() == 1 { @@ -217,7 +232,7 @@ pub(crate) enum PartialDerivative { } /// # State constructors -impl State { +impl State { /// Return a new `State` given a temperature, an array of mole numbers and a volume. /// /// This function will perform a validation of the given properties, i.e. test for signs @@ -290,7 +305,6 @@ impl State { /// is overdetermined, it will choose a method based on the following hierarchy. /// 1. Create a state non-iteratively from the set of $T$, $V$, $\rho$, $\rho_i$, $N$, $N_i$ and $x_i$. /// 2. Use a density iteration for a given pressure. - /// 3. Determine the state using a Newton iteration from (in this order): $(p, h)$, $(p, s)$, $(T, h)$, $(T, s)$, $(V, u)$ /// /// The [StateBuilder] provides a convenient way of calling this function without the need to provide /// all the optional input values. @@ -308,12 +322,38 @@ impl State { moles: Option<&SIArray1>, molefracs: Option<&Array1>, pressure: Option, - molar_enthalpy: Option, - molar_entropy: Option, - molar_internal_energy: Option, density_initialization: DensityInitialization, initial_temperature: Option, ) -> EosResult { + Self::_new( + eos, + temperature, + volume, + density, + partial_density, + total_moles, + moles, + molefracs, + pressure, + density_initialization, + initial_temperature, + )? + .map_err(|_| EosError::UndeterminedState(String::from("Missing input parameters."))) + } + + fn _new( + eos: &Arc, + temperature: Option, + volume: Option, + density: Option, + partial_density: Option<&SIArray1>, + total_moles: Option, + moles: Option<&SIArray1>, + molefracs: Option<&Array1>, + pressure: Option, + density_initialization: DensityInitialization, + initial_temperature: Option, + ) -> EosResult>> { // Check if the provided densities have correct units. if let DensityInitialization::InitialDensity(rho0) = density_initialization { if !rho0.has_unit(&SIUnit::reference_density()) { @@ -396,36 +436,24 @@ impl State { // check if new state can be created using default constructor if let (Some(v), Some(t), Some(n_i)) = (v, temperature, &n_i) { - return State::new_nvt(eos, t, v, n_i); + return Ok(Ok(State::new_nvt(eos, t, v, n_i)?)); } // Check if new state can be created using density iteration if let (Some(p), Some(t), Some(n_i)) = (pressure, temperature, &n_i) { - return State::new_npt(eos, t, p, n_i, density_initialization); + return Ok(Ok(State::new_npt(eos, t, p, n_i, density_initialization)?)); } if let (Some(p), Some(t), Some(v)) = (pressure, temperature, v) { - return State::new_npvx(eos, t, p, v, &x_u, density_initialization); - } - - // Check if new state can be created using molar_enthalpy and temperature - if let (Some(p), Some(h), Some(n_i)) = (pressure, molar_enthalpy, &n_i) { - return State::new_nph(eos, p, h, n_i, density_initialization, initial_temperature); - } - if let (Some(p), Some(s), Some(n_i)) = (pressure, molar_entropy, &n_i) { - return State::new_nps(eos, p, s, n_i, density_initialization, initial_temperature); - } - if let (Some(t), Some(h), Some(n_i)) = (temperature, molar_enthalpy, &n_i) { - return State::new_nth(eos, t, h, n_i, density_initialization); - } - if let (Some(t), Some(s), Some(n_i)) = (temperature, molar_entropy, &n_i) { - return State::new_nts(eos, t, s, n_i, density_initialization); - } - if let (Some(u), Some(v), Some(n_i)) = (molar_internal_energy, volume, &n_i) { - return State::new_nvu(eos, v, u, n_i, initial_temperature); + return Ok(Ok(State::new_npvx( + eos, + t, + p, + v, + &x_u, + density_initialization, + )?)); } - Err(EosError::UndeterminedState(String::from( - "Missing input parameters.", - ))) + Ok(Err(n_i)) } /// Return a new `State` using a density iteration. [DensityInitialization] is used to @@ -510,6 +538,79 @@ impl State { let moles = state.partial_density * volume; Self::new_nvt(eos, temperature, volume, &moles) } +} + +impl State { + /// Return a new `State` for the combination of inputs. + /// + /// The function attempts to create a new state using the given input values. If the state + /// is overdetermined, it will choose a method based on the following hierarchy. + /// 1. Create a state non-iteratively from the set of $T$, $V$, $\rho$, $\rho_i$, $N$, $N_i$ and $x_i$. + /// 2. Use a density iteration for a given pressure. + /// 3. Determine the state using a Newton iteration from (in this order): $(p, h)$, $(p, s)$, $(T, h)$, $(T, s)$, $(V, u)$ + /// + /// The [StateBuilder] provides a convenient way of calling this function without the need to provide + /// all the optional input values. + /// + /// # Errors + /// + /// When the state cannot be created using the combination of inputs. + pub fn new_full( + eos: &Arc, + temperature: Option, + volume: Option, + density: Option, + partial_density: Option<&SIArray1>, + total_moles: Option, + moles: Option<&SIArray1>, + molefracs: Option<&Array1>, + pressure: Option, + molar_enthalpy: Option, + molar_entropy: Option, + molar_internal_energy: Option, + density_initialization: DensityInitialization, + initial_temperature: Option, + ) -> EosResult { + let state = Self::_new( + eos, + temperature, + volume, + density, + partial_density, + total_moles, + moles, + molefracs, + pressure, + density_initialization, + initial_temperature, + )?; + + let ti = initial_temperature; + match state { + Ok(state) => return Ok(state), + Err(n_i) => { + // Check if new state can be created using molar_enthalpy and temperature + if let (Some(p), Some(h), Some(n_i)) = (pressure, molar_enthalpy, &n_i) { + return State::new_nph(eos, p, h, n_i, density_initialization, ti); + } + if let (Some(p), Some(s), Some(n_i)) = (pressure, molar_entropy, &n_i) { + return State::new_nps(eos, p, s, n_i, density_initialization, ti); + } + if let (Some(t), Some(h), Some(n_i)) = (temperature, molar_enthalpy, &n_i) { + return State::new_nth(eos, t, h, n_i, density_initialization); + } + if let (Some(t), Some(s), Some(n_i)) = (temperature, molar_entropy, &n_i) { + return State::new_nts(eos, t, s, n_i, density_initialization); + } + if let (Some(u), Some(v), Some(n_i)) = (molar_internal_energy, volume, &n_i) { + return State::new_nvu(eos, v, u, n_i, ti); + } + Err(EosError::UndeterminedState(String::from( + "Missing input parameters.", + ))) + } + } + } /// Return a new `State` for given pressure $p$ and molar enthalpy $h$. pub fn new_nph( @@ -621,7 +722,9 @@ impl State { }; newton(t0, f, 1.0e-8 * SIUnit::reference_temperature()) } +} +impl State { /// Update the state with the given temperature pub fn update_temperature(&self, temperature: SINumber) -> EosResult { Self::new_nvt(&self.eos, temperature, self.volume, &self.moles) @@ -746,7 +849,7 @@ fn is_close(x: SINumber, y: SINumber, atol: SINumber, rtol: f64) -> bool { (x - y).abs() <= atol + rtol * y.abs() } -fn newton(mut x0: SINumber, mut f: F, atol: SINumber) -> EosResult> +fn newton(mut x0: SINumber, mut f: F, atol: SINumber) -> EosResult> where F: FnMut(SINumber) -> EosResult<(SINumber, SINumber, State)>, { @@ -827,7 +930,7 @@ where } } -mod critical_point; +// mod critical_point; #[cfg(test)] mod tests { diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index dcaee2010..e3c26a58a 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -1,11 +1,11 @@ -use super::{Derivative::*, PartialDerivative, State}; -use crate::equation_of_state::{EntropyScaling, EquationOfState, MolarWeight}; +use super::{Contributions, Derivative::*, PartialDerivative, State}; +// use crate::equation_of_state::{EntropyScaling, MolarWeight, Residual}; +use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; use crate::errors::EosResult; use crate::EosUnit; use ndarray::{arr1, Array1, Array2}; use num_dual::DualNum; use quantity::si::*; -use std::iter::FromIterator; use std::ops::{Add, Deref, Sub}; use std::sync::Arc; @@ -17,22 +17,7 @@ pub(crate) enum Evaluate { IdealGasDelta, } -/// Possible contributions that can be computed. -#[derive(Clone, Copy)] -#[cfg_attr(feature = "python", pyo3::pyclass)] -pub enum Contributions { - /// Only compute the ideal gas contribution - IdealGas, - /// Only compute the difference between the total and the ideal gas contribution - ResidualNvt, - /// Compute the differnce between the total and the ideal gas contribution for a (N,p,T) reference state - ResidualNpt, - /// Compute ideal gas and residual contributions - Total, -} - -/// # State properties -impl State { +impl State { fn get_or_compute_derivative( &self, derivative: PartialDerivative, @@ -70,46 +55,9 @@ impl State { }; } - let mut cache = self.cache.lock().unwrap(); - let residual = match evaluate { Evaluate::IdealGas => None, - _ => Some(match derivative { - PartialDerivative::Zeroth => { - let new_state = self.derive0(); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_f64(computation) * SIUnit::reference_energy() - } - PartialDerivative::First(v) => { - let new_state = self.derive1(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_d64(v, computation) * SIUnit::reference_energy() - / v.reference() - } - PartialDerivative::Second(v) => { - let new_state = self.derive2(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_d2_64(v, computation) * SIUnit::reference_energy() - / (v.reference() * v.reference()) - } - PartialDerivative::SecondMixed(v1, v2) => { - let new_state = self.derive2_mixed(v1, v2); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_hd64(v1, v2, computation) * SIUnit::reference_energy() - / (v1.reference() * v2.reference()) - } - PartialDerivative::Third(v) => { - let new_state = self.derive3(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_hd364(v, computation) * SIUnit::reference_energy() - / (v.reference() * v.reference() * v.reference()) - } - }), + _ => Some(self.get_or_compute_derivative_residual(derivative)), }; let ideal_gas = match evaluate { @@ -117,31 +65,31 @@ impl State { _ => Some(match derivative { PartialDerivative::Zeroth => { let new_state = self.derive0(); - self.eos.ideal_gas().evaluate(&new_state) + self.eos.evaluate_ideal_gas(&new_state) * SIUnit::reference_energy() * new_state.temperature } PartialDerivative::First(v) => { let new_state = self.derive1(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps * SIUnit::reference_energy() / v.reference() } PartialDerivative::Second(v) => { let new_state = self.derive2(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).v2 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).v2 * SIUnit::reference_energy() / (v.reference() * v.reference()) } PartialDerivative::SecondMixed(v1, v2) => { let new_state = self.derive2_mixed(v1, v2); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps1eps2 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps1eps2 * SIUnit::reference_energy() / (v1.reference() * v2.reference()) } PartialDerivative::Third(v) => { let new_state = self.derive3(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).v3 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).v3 * SIUnit::reference_energy() / (v.reference() * v.reference() * v.reference()) } @@ -164,28 +112,27 @@ impl State { match contributions { Contributions::IdealGas => f(self, Evaluate::IdealGas), Contributions::Total => f(self, Evaluate::Total), - Contributions::ResidualNvt => { + Contributions::Residual => { if additive { f(self, Evaluate::Residual) } else { f(self, Evaluate::Total) - f(self, Evaluate::IdealGas) } - } - Contributions::ResidualNpt => { - let p = self.pressure_(Evaluate::Total); - let state_p = Self::new_nvt_unchecked( - &self.eos, - self.temperature, - self.total_moles * SIUnit::gas_constant() * self.temperature / p, - &self.moles, - ); - if additive { - f(self, Evaluate::Residual) + f(self, Evaluate::IdealGasDelta) - - f(&state_p, Evaluate::IdealGasDelta) - } else { - f(self, Evaluate::Total) - f(&state_p, Evaluate::IdealGas) - } - } + } // Contributions::ResidualNpt => { + // let p = self.pressure_(Evaluate::Total); + // let state_p = Self::new_nvt_unchecked( + // &self.eos, + // self.temperature, + // self.total_moles * SIUnit::gas_constant() * self.temperature / p, + // &self.moles, + // ); + // if additive { + // f(self, Evaluate::Residual) + f(self, Evaluate::IdealGasDelta) + // - f(&state_p, Evaluate::IdealGasDelta) + // } else { + // f(self, Evaluate::Total) - f(&state_p, Evaluate::IdealGas) + // } + // } } } @@ -246,55 +193,6 @@ impl State { -self.get_or_compute_derivative(PartialDerivative::Third(DT), evaluate) } - /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$ - pub fn pressure(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::pressure_, contributions, true) - } - - /// Compressibility factor: $Z=\frac{pV}{NRT}$ - pub fn compressibility(&self, contributions: Contributions) -> f64 { - (self.pressure(contributions) / (self.density * self.temperature * SIUnit::gas_constant())) - .into_value() - .unwrap() - } - - /// Partial derivative of pressure w.r.t. volume: $\left(\frac{\partial p}{\partial V}\right)_{T,N_i}$ - pub fn dp_dv(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::dp_dv_, contributions, true) - } - - /// Partial derivative of pressure w.r.t. density: $\left(\frac{\partial p}{\partial \rho}\right)_{T,N_i}$ - pub fn dp_drho(&self, contributions: Contributions) -> SINumber { - -self.volume / self.density * self.dp_dv(contributions) - } - - /// Partial derivative of pressure w.r.t. temperature: $\left(\frac{\partial p}{\partial T}\right)_{V,N_i}$ - pub fn dp_dt(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::dp_dt_, contributions, true) - } - - /// Partial derivative of pressure w.r.t. moles: $\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$ - pub fn dp_dni(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::dp_dni_, contributions, true) - } - - /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$ - pub fn d2p_dv2(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::d2p_dv2_, contributions, true) - } - - /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$ - pub fn d2p_drho2(&self, contributions: Contributions) -> SINumber { - self.volume / (self.density * self.density) - * (self.volume * self.d2p_dv2(contributions) + 2.0 * self.dp_dv(contributions)) - } - - /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_volume(&self, contributions: Contributions) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| -s.dp_dni_(evaluate) / s.dp_dv_(evaluate); - self.evaluate_property(func, contributions, false) - } - /// Chemical potential: $\mu_i=\left(\frac{\partial A}{\partial N_i}\right)_{T,V,N_j}$ pub fn chemical_potential(&self, contributions: Contributions) -> SIArray1 { self.evaluate_property(Self::chemical_potential_, contributions, true) @@ -310,80 +208,6 @@ impl State { self.evaluate_property(Self::dmu_dni_, contributions, true) } - /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$ - pub fn ln_phi(&self) -> Array1 { - (self.chemical_potential(Contributions::ResidualNpt) - / (SIUnit::gas_constant() * self.temperature)) - .into_value() - .unwrap() - } - - /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure. - pub fn ln_phi_pure_liquid(&self) -> EosResult> { - let pressure = self.pressure(Contributions::Total); - (0..self.eos.components()) - .map(|i| { - let eos = Arc::new(self.eos.subset(&[i])); - let state = Self::new_npt( - &eos, - self.temperature, - pressure, - &(arr1(&[1.0]) * SIUnit::reference_moles()), - crate::DensityInitialization::Liquid, - )?; - Ok(state.ln_phi()[0]) - }) - .collect() - } - - /// Activity coefficient $\ln \gamma_i = \ln \varphi_i(T, p, \mathbf{N}) - \ln \varphi_i(T, p)$ - pub fn ln_symmetric_activity_coefficient(&self) -> EosResult> { - match self.eos.components() { - 1 => Ok(arr1(&[0.0])), - _ => Ok(self.ln_phi() - &self.ln_phi_pure_liquid()?), - } - } - - /// 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}$ - pub fn dln_phi_dt(&self) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| { - (s.dmu_dt_(evaluate) + s.dp_dni_(evaluate) * (s.dp_dt_(evaluate) / s.dp_dv_(evaluate)) - - s.chemical_potential_(evaluate) / self.temperature) - / (SIUnit::gas_constant() * self.temperature) - }; - self.evaluate_property(func, Contributions::ResidualNpt, false) - } - - /// 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}$ - pub fn dln_phi_dp(&self) -> SIArray1 { - self.partial_molar_volume(Contributions::ResidualNpt) - / (SIUnit::gas_constant() * self.temperature) - } - - /// 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}$ - pub fn dln_phi_dnj(&self) -> SIArray2 { - let n = self.eos.components(); - let dmu_dni = self.dmu_dni(Contributions::ResidualNvt); - let dp_dni = self.dp_dni(Contributions::Total); - let dp_dv = self.dp_dv(Contributions::Total); - let dp_dn_2 = SIArray::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j)); - (dmu_dni + dp_dn_2 / dp_dv) / (SIUnit::gas_constant() * self.temperature) - + 1.0 / self.total_moles - } - - /// Thermodynamic factor: $\Gamma_{ij}=\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ - pub fn thermodynamic_factor(&self) -> Array2 { - let dln_phi_dnj = self - .dln_phi_dnj() - .to_reduced(SIUnit::reference_moles().powi(-1)) - .unwrap(); - let moles = self.moles.to_reduced(SIUnit::reference_moles()).unwrap(); - let n = self.eos.components() - 1; - Array2::from_shape_fn((n, n), |(i, j)| { - moles[i] * (dln_phi_dnj[[i, j]] - dln_phi_dnj[[i, n]]) + if i == j { 1.0 } else { 0.0 } - }) - } - /// Molar isochoric heat capacity: $c_v=\left(\frac{\partial u}{\partial T}\right)_{V,N_i}$ pub fn c_v(&self, contributions: Contributions) -> SINumber { let func = @@ -509,19 +333,12 @@ impl State { -1.0 / (self.dp_dv(c) * self.volume) } - /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ - pub fn structure_factor(&self) -> f64 { - -(SIUnit::gas_constant() * self.temperature * self.density) - .to_reduced(self.volume * self.dp_dv(Contributions::Total)) - .unwrap() - } - /// Helmholtz energy $A$ evaluated for each contribution of the equation of state. pub fn helmholtz_energy_contributions(&self) -> Vec<(String, SINumber)> { let new_state = self.derive0(); let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); + let ig = self.eos.evaluate_ideal_gas(); res.push(( ig.to_string(), ig.evaluate(&new_state) * new_state.temperature * SIUnit::reference_energy(), @@ -537,7 +354,7 @@ impl State { let new_state = self.derive1(DV); let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); + let ig = self.eos.evaluate_ideal_gas(); res.push(( ig.to_string(), -(ig.evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_pressure(), @@ -556,7 +373,7 @@ impl State { let new_state = self.derive1(DN(component)); let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); + let ig = self.eos.evaluate_ideal_gas(); res.push(( ig.to_string(), (ig.evaluate(&new_state) * new_state.temperature).eps @@ -576,7 +393,7 @@ impl State { /// /// These properties are available for equations of state /// that implement the [MolarWeight] trait. -impl State { +impl State { /// Total molar weight: $MW=\sum_ix_iMW_i$ pub fn total_molar_weight(&self) -> SINumber { (self.eos.molar_weight() * &self.molefracs).sum() @@ -634,214 +451,3 @@ impl State { .unwrap() } } - -impl State { - // This function is designed specifically for use in density iterations - pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { - let dp_dv = self.dp_dv(Contributions::Total); - ( - self.pressure(Contributions::Total), - (-self.volume * dp_dv / self.density), - ) - } - - // This function is designed specifically for use in spinodal iterations - pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { - let d2p_dv2 = self.d2p_dv2(Contributions::Total); - let dp_dv = self.dp_dv(Contributions::Total); - ( - self.pressure(Contributions::Total), - (-self.volume * dp_dv / self.density), - (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)), - ) - } -} - -/// # Transport properties -/// -/// These properties are available for equations of state -/// that implement the [EntropyScaling] trait. -impl State { - /// Return the viscosity via entropy scaling. - pub fn viscosity(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .viscosity_reference(self.temperature, self.volume, &self.moles)? - * self.eos.viscosity_correlation(s, &self.molefracs)?.exp()) - } - - /// Return the logarithm of the reduced viscosity. - /// - /// This term equals the viscosity correlation function - /// that is used for entropy scaling. - pub fn ln_viscosity_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos.viscosity_correlation(s, &self.molefracs) - } - - /// Return the viscosity reference as used in entropy scaling. - pub fn viscosity_reference(&self) -> EosResult { - self.eos - .viscosity_reference(self.temperature, self.volume, &self.moles) - } - - /// Return the diffusion via entropy scaling. - pub fn diffusion(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .diffusion_reference(self.temperature, self.volume, &self.moles)? - * self.eos.diffusion_correlation(s, &self.molefracs)?.exp()) - } - - /// Return the logarithm of the reduced diffusion. - /// - /// This term equals the diffusion correlation function - /// that is used for entropy scaling. - pub fn ln_diffusion_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos.diffusion_correlation(s, &self.molefracs) - } - - /// Return the diffusion reference as used in entropy scaling. - pub fn diffusion_reference(&self) -> EosResult { - self.eos - .diffusion_reference(self.temperature, self.volume, &self.moles) - } - - /// Return the thermal conductivity via entropy scaling. - pub fn thermal_conductivity(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? - * self - .eos - .thermal_conductivity_correlation(s, &self.molefracs)? - .exp()) - } - - /// Return the logarithm of the reduced thermal conductivity. - /// - /// This term equals the thermal conductivity correlation function - /// that is used for entropy scaling. - pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos - .thermal_conductivity_correlation(s, &self.molefracs) - } - - /// Return the thermal conductivity reference as used in entropy scaling. - pub fn thermal_conductivity_reference(&self) -> EosResult { - self.eos - .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) - } -} - -/// A list of states for a simple access to properties -/// of multiple states. -pub struct StateVec<'a, E>(pub Vec<&'a State>); - -impl<'a, E> FromIterator<&'a State> for StateVec<'a, E> { - fn from_iter>>(iter: I) -> Self { - Self(iter.into_iter().collect()) - } -} - -impl<'a, E> IntoIterator for StateVec<'a, E> { - type Item = &'a State; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a, E> Deref for StateVec<'a, E> { - type Target = Vec<&'a State>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a, E: EquationOfState> StateVec<'a, E> { - pub fn temperature(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].temperature) - } - - pub fn pressure(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].pressure(Contributions::Total)) - } - - pub fn compressibility(&self) -> Array1 { - Array1::from_shape_fn(self.0.len(), |i| { - self.0[i].compressibility(Contributions::Total) - }) - } - - pub fn density(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].density) - } - - pub fn moles(&self) -> SIArray2 { - SIArray2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].moles.get(j) - }) - } - - pub fn molefracs(&self) -> Array2 { - Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].molefracs[j] - }) - } - - pub fn molar_enthalpy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].molar_enthalpy(Contributions::Total) - }) - } - - pub fn molar_entropy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].molar_entropy(Contributions::Total) - }) - } -} - -impl<'a, E: EquationOfState + MolarWeight> StateVec<'a, E> { - pub fn mass_density(&self) -> SIArray1 { - SIArray1::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] - }) - } - - pub fn specific_enthalpy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].specific_enthalpy(Contributions::Total) - }) - } - - pub fn specific_entropy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].specific_entropy(Contributions::Total) - }) - } -} diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs new file mode 100644 index 000000000..119fe3320 --- /dev/null +++ b/feos-core/src/state/residual_properties.rs @@ -0,0 +1,343 @@ +use super::{Contributions, Derivative::*, PartialDerivative, State}; +// use crate::equation_of_state::{EntropyScaling, MolarWeight, Residual}; +use crate::equation_of_state::Residual; +use crate::errors::EosResult; +use crate::EosUnit; +use ndarray::{arr1, Array1, Array2}; +use quantity::si::*; +use std::sync::Arc; + +/// # State properties +impl State { + pub(super) fn get_or_compute_derivative_residual( + &self, + derivative: PartialDerivative, + ) -> SINumber { + let mut cache = self.cache.lock().unwrap(); + + match derivative { + PartialDerivative::Zeroth => { + let new_state = self.derive0(); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_f64(computation) * SIUnit::reference_energy() + } + PartialDerivative::First(v) => { + let new_state = self.derive1(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_d64(v, computation) * SIUnit::reference_energy() + / v.reference() + } + PartialDerivative::Second(v) => { + let new_state = self.derive2(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_d2_64(v, computation) * SIUnit::reference_energy() + / (v.reference() * v.reference()) + } + PartialDerivative::SecondMixed(v1, v2) => { + let new_state = self.derive2_mixed(v1, v2); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_hd64(v1, v2, computation) * SIUnit::reference_energy() + / (v1.reference() * v2.reference()) + } + PartialDerivative::Third(v) => { + let new_state = self.derive3(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_hd364(v, computation) * SIUnit::reference_energy() + / (v.reference() * v.reference() * v.reference()) + } + } + } +} + +impl State { + fn contributions( + ideal_gas: SINumber, + residual: SINumber, + contributions: Contributions, + ) -> SINumber { + match contributions { + Contributions::IdealGas => ideal_gas, + Contributions::Total => ideal_gas + residual, + Contributions::Residual => residual, + } + } + + /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$ + pub fn pressure(&self, contributions: Contributions) -> SINumber { + let ideal_gas = self.density * SIUnit::gas_constant() * self.temperature; + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::First(DV)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Compressibility factor: $Z=\frac{pV}{NRT}$ + pub fn compressibility(&self, contributions: Contributions) -> f64 { + (self.pressure(contributions) / (self.density * self.temperature * SIUnit::gas_constant())) + .into_value() + .unwrap() + } + + /// Partial derivative of pressure w.r.t. volume: $\left(\frac{\partial p}{\partial V}\right)_{T,N_i}$ + pub fn dp_dv(&self, contributions: Contributions) -> SINumber { + let ideal_gas = -self.density * SIUnit::gas_constant() * self.temperature / self.volume; + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::Second(DV)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Partial derivative of pressure w.r.t. density: $\left(\frac{\partial p}{\partial \rho}\right)_{T,N_i}$ + pub fn dp_drho(&self, contributions: Contributions) -> SINumber { + -self.volume / self.density * self.dp_dv(contributions) + } + + /// Partial derivative of pressure w.r.t. temperature: $\left(\frac{\partial p}{\partial T}\right)_{V,N_i}$ + pub fn dp_dt(&self, contributions: Contributions) -> SINumber { + let ideal_gas = self.density * SIUnit::gas_constant(); + let residual = + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DT)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Partial derivative of pressure w.r.t. moles: $\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$ + pub fn dp_dni(&self, contributions: Contributions) -> SIArray1 { + todo!(); + // self.evaluate_property(Self::dp_dni_, contributions, true) + } + + /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$ + pub fn d2p_dv2(&self, contributions: Contributions) -> SINumber { + todo!(); + // self.evaluate_property(Self::d2p_dv2_, contributions, true) + } + + /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$ + pub fn d2p_drho2(&self, contributions: Contributions) -> SINumber { + self.volume / (self.density * self.density) + * (self.volume * self.d2p_dv2(contributions) + 2.0 * self.dp_dv(contributions)) + } + + /// Residual chemical potential: $\mu_i^\text{res}=\left(\frac{\partial A^\text{res}}{\partial N_i}\right)_{T,V,N_j}$ + fn residual_chemical_potential(&self) -> SIArray1 { + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative_residual(PartialDerivative::First(DN(i))) + }) + } + + /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$ + pub fn dmu_res_dt(&self) -> SIArray1 { + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DT, DN(i))) + }) + } + + /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ + pub fn dmu_res_dni(&self) -> SIArray2 { + let n = self.eos.components(); + SIArray::from_shape_fn((n, n), |(i, j)| { + self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DN(i), DN(j))) + }) + } + + /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ + pub fn partial_molar_volume(&self, contributions: Contributions) -> SIArray1 { + let func = |s: &Self, evaluate: Evaluate| -s.dp_dni_(evaluate) / s.dp_dv_(evaluate); + self.evaluate_property(func, contributions, false) + } + + /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$ + pub fn ln_phi(&self) -> Array1 { + (self.residual_chemical_potential() / (SIUnit::gas_constant() * self.temperature)) + .into_value() + .unwrap() + - self.compressibility(Contributions::Total) + // (self.chemical_potential(Contributions::ResidualNpt) + // / (SIUnit::gas_constant() * self.temperature)) + // .into_value() + // .unwrap() + } + + /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure. + pub fn ln_phi_pure_liquid(&self) -> EosResult> { + let pressure = self.pressure(Contributions::Total); + (0..self.eos.components()) + .map(|i| { + let eos = Arc::new(self.eos.subset(&[i])); + let state = Self::new_npt( + &eos, + self.temperature, + pressure, + &(arr1(&[1.0]) * SIUnit::reference_moles()), + crate::DensityInitialization::Liquid, + )?; + Ok(state.ln_phi()[0]) + }) + .collect() + } + + /// Activity coefficient $\ln \gamma_i = \ln \varphi_i(T, p, \mathbf{N}) - \ln \varphi_i(T, p)$ + pub fn ln_symmetric_activity_coefficient(&self) -> EosResult> { + match self.eos.components() { + 1 => Ok(arr1(&[0.0])), + _ => Ok(self.ln_phi() - &self.ln_phi_pure_liquid()?), + } + } + + /// 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}$ + pub fn dln_phi_dt(&self) -> SIArray1 { + let func = |s: &Self, evaluate: Evaluate| { + (s.dmu_dt_(evaluate) + s.dp_dni_(evaluate) * (s.dp_dt_(evaluate) / s.dp_dv_(evaluate)) + - s.chemical_potential_(evaluate) / self.temperature) + / (SIUnit::gas_constant() * self.temperature) + }; + self.evaluate_property(func, Contributions::ResidualNpt, false) + } + + /// 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}$ + pub fn dln_phi_dp(&self) -> SIArray1 { + self.partial_molar_volume(Contributions::ResidualNpt) + / (SIUnit::gas_constant() * self.temperature) + } + + /// 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}$ + pub fn dln_phi_dnj(&self) -> SIArray2 { + let n = self.eos.components(); + let dmu_dni = self.dmu_res_dni(); + let dp_dni = self.dp_dni(Contributions::Total); + let dp_dv = self.dp_dv(Contributions::Total); + let dp_dn_2 = SIArray::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j)); + (dmu_dni + dp_dn_2 / dp_dv) / (SIUnit::gas_constant() * self.temperature) + + 1.0 / self.total_moles + } + + /// Thermodynamic factor: $\Gamma_{ij}=\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ + pub fn thermodynamic_factor(&self) -> Array2 { + let dln_phi_dnj = self + .dln_phi_dnj() + .to_reduced(SIUnit::reference_moles().powi(-1)) + .unwrap(); + let moles = self.moles.to_reduced(SIUnit::reference_moles()).unwrap(); + let n = self.eos.components() - 1; + Array2::from_shape_fn((n, n), |(i, j)| { + moles[i] * (dln_phi_dnj[[i, j]] - dln_phi_dnj[[i, n]]) + if i == j { 1.0 } else { 0.0 } + }) + } + + /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ + pub fn structure_factor(&self) -> f64 { + -(SIUnit::gas_constant() * self.temperature * self.density) + .to_reduced(self.volume * self.dp_dv(Contributions::Total)) + .unwrap() + } + + // This function is designed specifically for use in density iterations + pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { + let dp_dv = self.dp_dv(Contributions::Total); + ( + self.pressure(Contributions::Total), + (-self.volume * dp_dv / self.density), + ) + } + + // This function is designed specifically for use in spinodal iterations + pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { + let d2p_dv2 = self.d2p_dv2(Contributions::Total); + let dp_dv = self.dp_dv(Contributions::Total); + ( + self.pressure(Contributions::Total), + (-self.volume * dp_dv / self.density), + (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)), + ) + } +} + +// /// # Transport properties +// /// +// /// These properties are available for equations of state +// /// that implement the [EntropyScaling] trait. +// impl State { +// /// Return the viscosity via entropy scaling. +// pub fn viscosity(&self) -> EosResult { +// let s = self +// .molar_entropy(Contributions::ResidualNvt) +// .to_reduced(SIUnit::reference_molar_entropy())?; +// Ok(self +// .eos +// .viscosity_reference(self.temperature, self.volume, &self.moles)? +// * self.eos.viscosity_correlation(s, &self.molefracs)?.exp()) +// } + +// /// Return the logarithm of the reduced viscosity. +// /// +// /// This term equals the viscosity correlation function +// /// that is used for entropy scaling. +// pub fn ln_viscosity_reduced(&self) -> EosResult { +// let s = self +// .molar_entropy(Contributions::ResidualNvt) +// .to_reduced(SIUnit::reference_molar_entropy())?; +// self.eos.viscosity_correlation(s, &self.molefracs) +// } + +// /// Return the viscosity reference as used in entropy scaling. +// pub fn viscosity_reference(&self) -> EosResult { +// self.eos +// .viscosity_reference(self.temperature, self.volume, &self.moles) +// } + +// /// Return the diffusion via entropy scaling. +// pub fn diffusion(&self) -> EosResult { +// let s = self +// .molar_entropy(Contributions::ResidualNvt) +// .to_reduced(SIUnit::reference_molar_entropy())?; +// Ok(self +// .eos +// .diffusion_reference(self.temperature, self.volume, &self.moles)? +// * self.eos.diffusion_correlation(s, &self.molefracs)?.exp()) +// } + +// /// Return the logarithm of the reduced diffusion. +// /// +// /// This term equals the diffusion correlation function +// /// that is used for entropy scaling. +// pub fn ln_diffusion_reduced(&self) -> EosResult { +// let s = self +// .molar_entropy(Contributions::ResidualNvt) +// .to_reduced(SIUnit::reference_molar_entropy())?; +// self.eos.diffusion_correlation(s, &self.molefracs) +// } + +// /// Return the diffusion reference as used in entropy scaling. +// pub fn diffusion_reference(&self) -> EosResult { +// self.eos +// .diffusion_reference(self.temperature, self.volume, &self.moles) +// } + +// /// Return the thermal conductivity via entropy scaling. +// pub fn thermal_conductivity(&self) -> EosResult { +// let s = self +// .molar_entropy(Contributions::ResidualNvt) +// .to_reduced(SIUnit::reference_molar_entropy())?; +// Ok(self +// .eos +// .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? +// * self +// .eos +// .thermal_conductivity_correlation(s, &self.molefracs)? +// .exp()) +// } + +// /// Return the logarithm of the reduced thermal conductivity. +// /// +// /// This term equals the thermal conductivity correlation function +// /// that is used for entropy scaling. +// pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { +// let s = self +// .molar_entropy(Contributions::ResidualNvt) +// .to_reduced(SIUnit::reference_molar_entropy())?; +// self.eos +// .thermal_conductivity_correlation(s, &self.molefracs) +// } + +// /// Return the thermal conductivity reference as used in entropy scaling. +// pub fn thermal_conductivity_reference(&self) -> EosResult { +// self.eos +// .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) +// } +// } diff --git a/feos-core/src/state/statevec.rs b/feos-core/src/state/statevec.rs new file mode 100644 index 000000000..cb752fe63 --- /dev/null +++ b/feos-core/src/state/statevec.rs @@ -0,0 +1,103 @@ +use super::{Contributions, State}; +use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; +use ndarray::{Array1, Array2}; +use quantity::si::{SIArray1, SIArray2}; +use std::iter::FromIterator; +use std::ops::Deref; + +/// A list of states for a simple access to properties +/// of multiple states. +pub struct StateVec<'a, E>(pub Vec<&'a State>); + +impl<'a, E> FromIterator<&'a State> for StateVec<'a, E> { + fn from_iter>>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl<'a, E> IntoIterator for StateVec<'a, E> { + type Item = &'a State; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a, E> Deref for StateVec<'a, E> { + type Target = Vec<&'a State>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a, E: Residual> StateVec<'a, E> { + pub fn temperature(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].temperature) + } + + pub fn pressure(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].pressure(Contributions::Total)) + } + + pub fn compressibility(&self) -> Array1 { + Array1::from_shape_fn(self.0.len(), |i| { + self.0[i].compressibility(Contributions::Total) + }) + } + + pub fn density(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].density) + } + + pub fn moles(&self) -> SIArray2 { + SIArray2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].moles.get(j) + }) + } + + pub fn molefracs(&self) -> Array2 { + Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].molefracs[j] + }) + } +} + +impl<'a, E: Residual + IdealGas> StateVec<'a, E> { + pub fn molar_enthalpy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].molar_enthalpy(Contributions::Total) + }) + } + + pub fn molar_entropy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].molar_entropy(Contributions::Total) + }) + } +} + +impl<'a, E: Residual + IdealGas + MolarWeight> StateVec<'a, E> { + pub fn mass_density(&self) -> SIArray1 { + SIArray1::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] + }) + } + + pub fn specific_enthalpy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].specific_enthalpy(Contributions::Total) + }) + } + + pub fn specific_entropy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].specific_entropy(Contributions::Total) + }) + } +} From 5e55d55522e8c16e1a5e37d699c973f0f56cea42 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 1 Jun 2023 17:23:51 +0200 Subject: [PATCH 02/47] ignore some specialized functionalities --- feos-core/src/state/mod.rs | 88 +++++++++++------------ feos-core/src/state/properties.rs | 114 +++++++++++++++--------------- 2 files changed, 100 insertions(+), 102 deletions(-) diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index 175d8fb05..c610d086d 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -12,7 +12,7 @@ use crate::errors::{EosError, EosResult}; use crate::EosUnit; use cache::Cache; use ndarray::prelude::*; -use num_dual::linalg::{norm, LU}; +// use num_dual::linalg::{norm, LU}; use num_dual::*; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::convert::TryFrom; @@ -730,49 +730,49 @@ impl State { Self::new_nvt(&self.eos, temperature, self.volume, &self.moles) } - /// Update the state with the given chemical potential. - pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { - for _ in 0..50 { - let dmu_drho = self.dmu_dni(Contributions::Total) * self.volume; - let f = self.chemical_potential(Contributions::Total) - chemical_potential; - let dmu_drho_r = dmu_drho - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let f_r = f.to_reduced(SIUnit::reference_molar_energy())?; - let rho = &self.partial_density - - &(LU::new(dmu_drho_r)?.solve(&f_r) * SIUnit::reference_density()); - *self = State::new_nvt( - &self.eos, - self.temperature, - self.volume, - &(rho * self.volume), - )?; - if norm(&f.to_reduced(SIUnit::reference_molar_energy())?) < 1e-8 { - return Ok(()); - } - } - Err(EosError::NotConverged( - "State::update_chemical_potential".into(), - )) - } - - /// Update the state with the given molar Gibbs energy. - pub fn update_gibbs_energy(mut self, molar_gibbs_energy: SINumber) -> EosResult { - for _ in 0..50 { - let df = self.volume / self.density * self.dp_dv(Contributions::Total); - let f = self.molar_gibbs_energy(Contributions::Total) - molar_gibbs_energy; - let rho = self.density * (f.to_reduced(df)?).exp(); - self = State::new_nvt( - &self.eos, - self.temperature, - self.total_moles / rho, - &self.moles, - )?; - if f.to_reduced(SIUnit::reference_molar_energy())?.abs() < 1e-8 { - return Ok(self); - } - } - Err(EosError::NotConverged("State::update_gibbs_energy".into())) - } + // /// Update the state with the given chemical potential. + // pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { + // for _ in 0..50 { + // let dmu_drho = self.dmu_dni(Contributions::Total) * self.volume; + // let f = self.chemical_potential(Contributions::Total) - chemical_potential; + // let dmu_drho_r = dmu_drho + // .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; + // let f_r = f.to_reduced(SIUnit::reference_molar_energy())?; + // let rho = &self.partial_density + // - &(LU::new(dmu_drho_r)?.solve(&f_r) * SIUnit::reference_density()); + // *self = State::new_nvt( + // &self.eos, + // self.temperature, + // self.volume, + // &(rho * self.volume), + // )?; + // if norm(&f.to_reduced(SIUnit::reference_molar_energy())?) < 1e-8 { + // return Ok(()); + // } + // } + // Err(EosError::NotConverged( + // "State::update_chemical_potential".into(), + // )) + // } + + // /// Update the state with the given molar Gibbs energy. + // pub fn update_gibbs_energy(mut self, molar_gibbs_energy: SINumber) -> EosResult { + // for _ in 0..50 { + // let df = self.volume / self.density * self.dp_dv(Contributions::Total); + // let f = self.molar_gibbs_energy(Contributions::Total) - molar_gibbs_energy; + // let rho = self.density * (f.to_reduced(df)?).exp(); + // self = State::new_nvt( + // &self.eos, + // self.temperature, + // self.total_moles / rho, + // &self.moles, + // )?; + // if f.to_reduced(SIUnit::reference_molar_energy())?.abs() < 1e-8 { + // return Ok(self); + // } + // } + // Err(EosError::NotConverged("State::update_gibbs_energy".into())) + // } /// Creates a [StateHD] cloning temperature, volume and moles. pub fn derive0(&self) -> StateHD { diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index e3c26a58a..e941fc303 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -1,13 +1,11 @@ use super::{Contributions, Derivative::*, PartialDerivative, State}; // use crate::equation_of_state::{EntropyScaling, MolarWeight, Residual}; use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; -use crate::errors::EosResult; use crate::EosUnit; -use ndarray::{arr1, Array1, Array2}; +use ndarray::Array1; use num_dual::DualNum; use quantity::si::*; -use std::ops::{Add, Deref, Sub}; -use std::sync::Arc; +use std::ops::{Add, Sub}; #[derive(Clone, Copy)] pub(crate) enum Evaluate { @@ -333,60 +331,60 @@ impl State { -1.0 / (self.dp_dv(c) * self.volume) } - /// Helmholtz energy $A$ evaluated for each contribution of the equation of state. - pub fn helmholtz_energy_contributions(&self) -> Vec<(String, SINumber)> { - let new_state = self.derive0(); - let contributions = self.eos.evaluate_residual_contributions(&new_state); - let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.evaluate_ideal_gas(); - res.push(( - ig.to_string(), - ig.evaluate(&new_state) * new_state.temperature * SIUnit::reference_energy(), - )); - for (s, v) in contributions { - res.push((s, v * new_state.temperature * SIUnit::reference_energy())); - } - res - } - - /// Pressure $p$ evaluated for each contribution of the equation of state. - pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { - let new_state = self.derive1(DV); - let contributions = self.eos.evaluate_residual_contributions(&new_state); - let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.evaluate_ideal_gas(); - res.push(( - ig.to_string(), - -(ig.evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_pressure(), - )); - for (s, v) in contributions { - res.push(( - s, - -(v * new_state.temperature).eps * SIUnit::reference_pressure(), - )); - } - res - } - - /// Chemical potential $\mu_i$ evaluated for each contribution of the equation of state. - pub fn chemical_potential_contributions(&self, component: usize) -> Vec<(String, SINumber)> { - let new_state = self.derive1(DN(component)); - let contributions = self.eos.evaluate_residual_contributions(&new_state); - let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.evaluate_ideal_gas(); - res.push(( - ig.to_string(), - (ig.evaluate(&new_state) * new_state.temperature).eps - * SIUnit::reference_molar_energy(), - )); - for (s, v) in contributions { - res.push(( - s, - (v * new_state.temperature).eps * SIUnit::reference_molar_energy(), - )); - } - res - } + // /// Helmholtz energy $A$ evaluated for each contribution of the equation of state. + // pub fn helmholtz_energy_contributions(&self) -> Vec<(String, SINumber)> { + // let new_state = self.derive0(); + // let contributions = self.eos.evaluate_residual_contributions(&new_state); + // let mut res = Vec::with_capacity(contributions.len() + 1); + // let ig = self.eos.evaluate_ideal_gas(); + // res.push(( + // ig.to_string(), + // ig.evaluate(&new_state) * new_state.temperature * SIUnit::reference_energy(), + // )); + // for (s, v) in contributions { + // res.push((s, v * new_state.temperature * SIUnit::reference_energy())); + // } + // res + // } + + // /// Pressure $p$ evaluated for each contribution of the equation of state. + // pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { + // let new_state = self.derive1(DV); + // let contributions = self.eos.evaluate_residual_contributions(&new_state); + // let mut res = Vec::with_capacity(contributions.len() + 1); + // let ig = self.eos.evaluate_ideal_gas(); + // res.push(( + // ig.to_string(), + // -(ig.evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_pressure(), + // )); + // for (s, v) in contributions { + // res.push(( + // s, + // -(v * new_state.temperature).eps * SIUnit::reference_pressure(), + // )); + // } + // res + // } + + // /// Chemical potential $\mu_i$ evaluated for each contribution of the equation of state. + // pub fn chemical_potential_contributions(&self, component: usize) -> Vec<(String, SINumber)> { + // let new_state = self.derive1(DN(component)); + // let contributions = self.eos.evaluate_residual_contributions(&new_state); + // let mut res = Vec::with_capacity(contributions.len() + 1); + // let ig = self.eos.evaluate_ideal_gas(); + // res.push(( + // ig.to_string(), + // (ig.evaluate(&new_state) * new_state.temperature).eps + // * SIUnit::reference_molar_energy(), + // )); + // for (s, v) in contributions { + // res.push(( + // s, + // (v * new_state.temperature).eps * SIUnit::reference_molar_energy(), + // )); + // } + // res + // } } /// # Mass specific state properties From eb01939c105016e314ab7df050efe242f9a7989d Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Thu, 1 Jun 2023 20:05:14 +0200 Subject: [PATCH 03/47] added some residual properties.todo: check ideal gas parts --- feos-core/src/equation_of_state/ideal_gas.rs | 5 +- feos-core/src/equation_of_state/mod.rs | 301 +++++++++---------- feos-core/src/state/properties.rs | 111 ++++--- feos-core/src/state/residual_properties.rs | 158 +++++++--- 4 files changed, 319 insertions(+), 256 deletions(-) diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs index ac85b23e2..581f0211e 100644 --- a/feos-core/src/equation_of_state/ideal_gas.rs +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -1,5 +1,4 @@ use crate::StateHD; -use ndarray::Array1; use num_dual::DualNum; use std::fmt; @@ -20,7 +19,7 @@ pub trait IdealGas: Sync + Send + fmt::Display { // /// contained in component_list. // fn subset(&self, component_list: &[usize]) -> Self; - fn de_broglie_wavelength(&self) -> &Box; + fn ideal_gas_model(&self) -> &Box; /// Evaluate the ideal gas contribution for a given state. /// @@ -32,7 +31,7 @@ pub trait IdealGas: Sync + Send + fmt::Display { dyn DeBroglieWavelength: DeBroglieWavelengthDual, { let lambda = self - .de_broglie_wavelength() + .ideal_gas_model() .de_broglie_wavelength(state.temperature); ((lambda + state.partial_density.mapv(|x| { diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index a408c48ca..2d4afb397 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -12,9 +12,6 @@ pub mod helmholtz_energy; pub mod ideal_gas; pub mod residual; pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; -pub use ideal_gas::{DefaultIdealGas, IdealGas}; -pub use residual::Residual; - pub use self::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; /// Molar weight of all components. @@ -25,152 +22,152 @@ pub trait MolarWeight { fn molar_weight(&self) -> SIArray1; } -#[derive(Clone)] -pub struct Model { - pub ideal_gas: Arc, - pub residual: Arc, - components: usize, -} - -impl Model { - pub fn new(ideal_gas: Arc, residual: Arc) -> Self { - assert_eq!(residual.components(), ideal_gas.components()); - let components = residual.components(); - Self { - ideal_gas, - residual, - components, - } - } - - pub fn components(&self) -> usize { - self.components - } - - pub fn subset(&self, component_list: &[usize]) -> Self { - Self::new( - Arc::new(self.ideal_gas.subset(component_list)), - Arc::new(self.residual.subset(component_list)), - ) - } - - /// Check if the provided optional mole number is consistent with the - /// equation of state. - /// - /// In general, the number of elements in `moles` needs to match the number - /// of components of the equation of state. For a pure component, however, - /// no moles need to be provided. In that case, it is set to the constant - /// reference value. - pub fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { - let l = moles.map_or(1, |m| m.len()); - if self.components() == l { - match moles { - Some(m) => Ok(m.to_owned()), - None => Ok(Array::ones(1) * SIUnit::reference_moles()), - } - } else { - Err(EosError::IncompatibleComponents(self.components(), l)) - } - } - - /// Calculate the maximum density. - /// - /// This value is used as an estimate for a liquid phase for phase - /// equilibria and other iterations. It is not explicitly meant to - /// be a mathematical limit for the density (if those exist in the - /// equation of state anyways). - pub fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { - let mr = self - .residual - .validate_moles(moles)? - .to_reduced(SIUnit::reference_moles())?; - Ok(self.residual.compute_max_density(&mr) * SIUnit::reference_density()) - } - - pub fn evaluate_residual>(&self, state: &StateHD) -> D - where - dyn HelmholtzEnergy: HelmholtzEnergyDual, - { - self.residual.helmholtz_energy(state) - } - - pub fn evaluate_ideal_gas>(&self, state: &StateHD) -> D - where - dyn DeBroglieWavelength: DeBroglieWavelengthDual, - { - self.ideal_gas.evaluate_ideal_gas(state) - } - - /// Calculate the second virial coefficient $B(T)$ - pub fn second_virial_coefficient( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let mut rho = HyperDual64::zero(); - rho.eps1[0] = 1.0; - rho.eps2[0] = 1.0; - let t = HyperDual64::from(temperature.to_reduced(SIUnit::reference_temperature())?); - let s = StateHD::new_virial(t, rho, x); - Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)] * 0.5 / SIUnit::reference_density()) - } - - /// Calculate the third virial coefficient $C(T)$ - pub fn third_virial_coefficient( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let rho = Dual3_64::zero().derive(); - let t = Dual3_64::from(temperature.to_reduced(SIUnit::reference_temperature())?); - let s = StateHD::new_virial(t, rho, x); - Ok(self.evaluate_residual(&s).v3 / 3.0 / SIUnit::reference_density().powi(2)) - } - - /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ - pub fn second_virial_coefficient_temperature_derivative( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let mut rho = HyperDual::zero(); - rho.eps1[0] = Dual64::one(); - rho.eps2[0] = Dual64::one(); - let t = HyperDual::from_re( - Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), - ); - let s = StateHD::new_virial(t, rho, x); - Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)].eps[0] * 0.5 - / (SIUnit::reference_density() * SIUnit::reference_temperature())) - } - - /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ - pub fn third_virial_coefficient_temperature_derivative( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let rho = Dual3::zero().derive(); - let t = Dual3::from_re( - Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), - ); - let s = StateHD::new_virial(t, rho, x); - Ok(self.evaluate_residual(&s).v3.eps[0] - / 3.0 - / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) - } -} - -impl Model { - pub fn molar_weight(&self) -> Array1 { - self.residual.molar_weight().to_reduced(MOL).unwrap() - } -} +// #[derive(Clone)] +// pub struct Model { +// pub ideal_gas: Arc, +// pub residual: Arc, +// components: usize, +// } + +// impl Model { +// pub fn new(ideal_gas: Arc, residual: Arc) -> Self { +// assert_eq!(residual.components(), ideal_gas.components()); +// let components = residual.components(); +// Self { +// ideal_gas, +// residual, +// components, +// } +// } + +// pub fn components(&self) -> usize { +// self.components +// } + +// pub fn subset(&self, component_list: &[usize]) -> Self { +// Self::new( +// Arc::new(self.ideal_gas.subset(component_list)), +// Arc::new(self.residual.subset(component_list)), +// ) +// } + +// /// Check if the provided optional mole number is consistent with the +// /// equation of state. +// /// +// /// In general, the number of elements in `moles` needs to match the number +// /// of components of the equation of state. For a pure component, however, +// /// no moles need to be provided. In that case, it is set to the constant +// /// reference value. +// pub fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { +// let l = moles.map_or(1, |m| m.len()); +// if self.components() == l { +// match moles { +// Some(m) => Ok(m.to_owned()), +// None => Ok(Array::ones(1) * SIUnit::reference_moles()), +// } +// } else { +// Err(EosError::IncompatibleComponents(self.components(), l)) +// } +// } + +// /// Calculate the maximum density. +// /// +// /// This value is used as an estimate for a liquid phase for phase +// /// equilibria and other iterations. It is not explicitly meant to +// /// be a mathematical limit for the density (if those exist in the +// /// equation of state anyways). +// pub fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { +// let mr = self +// .residual +// .validate_moles(moles)? +// .to_reduced(SIUnit::reference_moles())?; +// Ok(self.residual.compute_max_density(&mr) * SIUnit::reference_density()) +// } + +// pub fn evaluate_residual>(&self, state: &StateHD) -> D +// where +// dyn HelmholtzEnergy: HelmholtzEnergyDual, +// { +// self.residual.helmholtz_energy(state) +// } + +// pub fn evaluate_ideal_gas>(&self, state: &StateHD) -> D +// where +// dyn DeBroglieWavelength: DeBroglieWavelengthDual, +// { +// self.ideal_gas.evaluate_ideal_gas(state) +// } + +// /// Calculate the second virial coefficient $B(T)$ +// pub fn second_virial_coefficient( +// &self, +// temperature: SINumber, +// moles: Option<&SIArray1>, +// ) -> EosResult { +// let mr = self.validate_moles(moles)?; +// let x = mr.to_reduced(mr.sum())?; +// let mut rho = HyperDual64::zero(); +// rho.eps1[0] = 1.0; +// rho.eps2[0] = 1.0; +// let t = HyperDual64::from(temperature.to_reduced(SIUnit::reference_temperature())?); +// let s = StateHD::new_virial(t, rho, x); +// Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)] * 0.5 / SIUnit::reference_density()) +// } + +// /// Calculate the third virial coefficient $C(T)$ +// pub fn third_virial_coefficient( +// &self, +// temperature: SINumber, +// moles: Option<&SIArray1>, +// ) -> EosResult { +// let mr = self.validate_moles(moles)?; +// let x = mr.to_reduced(mr.sum())?; +// let rho = Dual3_64::zero().derive(); +// let t = Dual3_64::from(temperature.to_reduced(SIUnit::reference_temperature())?); +// let s = StateHD::new_virial(t, rho, x); +// Ok(self.evaluate_residual(&s).v3 / 3.0 / SIUnit::reference_density().powi(2)) +// } + +// /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ +// pub fn second_virial_coefficient_temperature_derivative( +// &self, +// temperature: SINumber, +// moles: Option<&SIArray1>, +// ) -> EosResult { +// let mr = self.validate_moles(moles)?; +// let x = mr.to_reduced(mr.sum())?; +// let mut rho = HyperDual::zero(); +// rho.eps1[0] = Dual64::one(); +// rho.eps2[0] = Dual64::one(); +// let t = HyperDual::from_re( +// Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), +// ); +// let s = StateHD::new_virial(t, rho, x); +// Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)].eps[0] * 0.5 +// / (SIUnit::reference_density() * SIUnit::reference_temperature())) +// } + +// /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ +// pub fn third_virial_coefficient_temperature_derivative( +// &self, +// temperature: SINumber, +// moles: Option<&SIArray1>, +// ) -> EosResult { +// let mr = self.validate_moles(moles)?; +// let x = mr.to_reduced(mr.sum())?; +// let rho = Dual3::zero().derive(); +// let t = Dual3::from_re( +// Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), +// ); +// let s = StateHD::new_virial(t, rho, x); +// Ok(self.evaluate_residual(&s).v3.eps[0] +// / 3.0 +// / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) +// } +// } + +// impl Model { +// pub fn molar_weight(&self) -> Array1 { +// self.residual.molar_weight().to_reduced(MOL).unwrap() +// } +// } diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index e941fc303..330c0b928 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -225,8 +225,7 @@ impl State { pub fn c_p(&self, contributions: Contributions) -> SINumber { let func = |s: &Self, evaluate: Evaluate| { s.temperature / s.total_moles - * (s.ds_dt_(evaluate) - - s.dp_dt_(evaluate) * s.dp_dt_(evaluate) / s.dp_dv_(evaluate)) + * (s.ds_dt_(evaluate) - s.dp_dt_(evaluate).powi(2) / s.dp_dv_(evaluate)) }; self.evaluate_property(func, contributions, false) } @@ -331,60 +330,60 @@ impl State { -1.0 / (self.dp_dv(c) * self.volume) } - // /// Helmholtz energy $A$ evaluated for each contribution of the equation of state. - // pub fn helmholtz_energy_contributions(&self) -> Vec<(String, SINumber)> { - // let new_state = self.derive0(); - // let contributions = self.eos.evaluate_residual_contributions(&new_state); - // let mut res = Vec::with_capacity(contributions.len() + 1); - // let ig = self.eos.evaluate_ideal_gas(); - // res.push(( - // ig.to_string(), - // ig.evaluate(&new_state) * new_state.temperature * SIUnit::reference_energy(), - // )); - // for (s, v) in contributions { - // res.push((s, v * new_state.temperature * SIUnit::reference_energy())); - // } - // res - // } - - // /// Pressure $p$ evaluated for each contribution of the equation of state. - // pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { - // let new_state = self.derive1(DV); - // let contributions = self.eos.evaluate_residual_contributions(&new_state); - // let mut res = Vec::with_capacity(contributions.len() + 1); - // let ig = self.eos.evaluate_ideal_gas(); - // res.push(( - // ig.to_string(), - // -(ig.evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_pressure(), - // )); - // for (s, v) in contributions { - // res.push(( - // s, - // -(v * new_state.temperature).eps * SIUnit::reference_pressure(), - // )); - // } - // res - // } - - // /// Chemical potential $\mu_i$ evaluated for each contribution of the equation of state. - // pub fn chemical_potential_contributions(&self, component: usize) -> Vec<(String, SINumber)> { - // let new_state = self.derive1(DN(component)); - // let contributions = self.eos.evaluate_residual_contributions(&new_state); - // let mut res = Vec::with_capacity(contributions.len() + 1); - // let ig = self.eos.evaluate_ideal_gas(); - // res.push(( - // ig.to_string(), - // (ig.evaluate(&new_state) * new_state.temperature).eps - // * SIUnit::reference_molar_energy(), - // )); - // for (s, v) in contributions { - // res.push(( - // s, - // (v * new_state.temperature).eps * SIUnit::reference_molar_energy(), - // )); - // } - // res - // } + /// Helmholtz energy $A$ evaluated for each contribution of the equation of state. + pub fn helmholtz_energy_contributions(&self) -> Vec<(String, SINumber)> { + let new_state = self.derive0(); + let contributions = self.eos.evaluate_residual_contributions(&new_state); + let mut res = Vec::with_capacity(contributions.len() + 1); + res.push(( + self.eos.ideal_gas_model().to_string(), + self.eos.evaluate_ideal_gas(&new_state) + * new_state.temperature + * SIUnit::reference_energy(), + )); + for (s, v) in contributions { + res.push((s, v * new_state.temperature * SIUnit::reference_energy())); + } + res + } + + /// Pressure $p$ evaluated for each contribution of the equation of state. + pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { + let new_state = self.derive1(DV); + let contributions = self.eos.evaluate_residual_contributions(&new_state); + let mut res = Vec::with_capacity(contributions.len() + 1); + res.push(( + self.eos.ideal_gas_model().to_string(), + -(self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps + * SIUnit::reference_pressure(), + )); + for (s, v) in contributions { + res.push(( + s, + -(v * new_state.temperature).eps * SIUnit::reference_pressure(), + )); + } + res + } + + /// Chemical potential $\mu_i$ evaluated for each contribution of the equation of state. + pub fn chemical_potential_contributions(&self, component: usize) -> Vec<(String, SINumber)> { + let new_state = self.derive1(DN(component)); + let contributions = self.eos.evaluate_residual_contributions(&new_state); + let mut res = Vec::with_capacity(contributions.len() + 1); + res.push(( + self.eos.ideal_gas_model().to_string(), + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps + * SIUnit::reference_molar_energy(), + )); + for (s, v) in contributions { + res.push(( + s, + (v * new_state.temperature).eps * SIUnit::reference_molar_energy(), + )); + } + res + } } /// # Mass specific state properties diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 119fe3320..51a20906f 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -1,6 +1,6 @@ use super::{Contributions, Derivative::*, PartialDerivative, State}; // use crate::equation_of_state::{EntropyScaling, MolarWeight, Residual}; -use crate::equation_of_state::Residual; +use crate::equation_of_state::{ideal_gas, residual, Residual}; use crate::errors::EosResult; use crate::EosUnit; use ndarray::{arr1, Array1, Array2}; @@ -62,6 +62,14 @@ impl State { } } + fn residual_helmholtz_energy(&self) -> SINumber { + self.get_or_compute_derivative_residual(PartialDerivative::Zeroth) + } + + fn residual_entropy(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::First(DT)) + } + /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$ pub fn pressure(&self, contributions: Contributions) -> SINumber { let ideal_gas = self.density * SIUnit::gas_constant() * self.temperature; @@ -69,6 +77,13 @@ impl State { Self::contributions(ideal_gas, residual, contributions) } + /// Residual chemical potential: $\mu_i^\text{res}=\left(\frac{\partial A^\text{res}}{\partial N_i}\right)_{T,V,N_j}$ + fn residual_chemical_potential(&self) -> SIArray1 { + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative_residual(PartialDerivative::First(DN(i))) + }) + } + /// Compressibility factor: $Z=\frac{pV}{NRT}$ pub fn compressibility(&self, contributions: Contributions) -> f64 { (self.pressure(contributions) / (self.density * self.temperature * SIUnit::gas_constant())) @@ -76,6 +91,8 @@ impl State { .unwrap() } + // pressure derivatives + /// Partial derivative of pressure w.r.t. volume: $\left(\frac{\partial p}{\partial V}\right)_{T,N_i}$ pub fn dp_dv(&self, contributions: Contributions) -> SINumber { let ideal_gas = -self.density * SIUnit::gas_constant() * self.temperature / self.volume; @@ -98,14 +115,29 @@ impl State { /// Partial derivative of pressure w.r.t. moles: $\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$ pub fn dp_dni(&self, contributions: Contributions) -> SIArray1 { - todo!(); - // self.evaluate_property(Self::dp_dni_, contributions, true) + match contributions { + Contributions::IdealGas => { + SIArray::from_vec(vec![ + SIUnit::gas_constant() * self.temperature / self.volume; + self.eos.components() + ]) + } + Contributions::Residual => SIArray::from_shape_fn(self.eos.components(), |i| { + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DN(i))) + }), + Contributions::Total => SIArray::from_shape_fn(self.eos.components(), |i| { + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DN(i))) + + SIUnit::gas_constant() * self.temperature / self.volume + }), + } } /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$ pub fn d2p_dv2(&self, contributions: Contributions) -> SINumber { - todo!(); - // self.evaluate_property(Self::d2p_dv2_, contributions, true) + let ideal_gas = 2.0 * self.density * SIUnit::gas_constant() * self.temperature + / (self.volume * self.volume); + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::Second(DV)); + Self::contributions(ideal_gas, residual, contributions) } /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$ @@ -114,11 +146,41 @@ impl State { * (self.volume * self.d2p_dv2(contributions) + 2.0 * self.dp_dv(contributions)) } - /// Residual chemical potential: $\mu_i^\text{res}=\left(\frac{\partial A^\text{res}}{\partial N_i}\right)_{T,V,N_j}$ - fn residual_chemical_potential(&self) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - self.get_or_compute_derivative_residual(PartialDerivative::First(DN(i))) - }) + /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ + pub fn structure_factor(&self) -> f64 { + -(SIUnit::gas_constant() * self.temperature * self.density) + .to_reduced(self.volume * self.dp_dv(Contributions::Total)) + .unwrap() + } + + // This function is designed specifically for use in density iterations + pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { + let dp_dv = self.dp_dv(Contributions::Total); + ( + self.pressure(Contributions::Total), + (-self.volume * dp_dv / self.density), + ) + } + + // This function is designed specifically for use in spinodal iterations + pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { + let d2p_dv2 = self.d2p_dv2(Contributions::Total); + let dp_dv = self.dp_dv(Contributions::Total); + ( + self.pressure(Contributions::Total), + (-self.volume * dp_dv / self.density), + (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)), + ) + } + + // entropy derivatives + + fn ds_res_dt(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::Second(DT)) + } + + fn d2s_res_dt2(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::Third(DT)) } /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$ @@ -136,12 +198,6 @@ impl State { }) } - /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_volume(&self, contributions: Contributions) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| -s.dp_dni_(evaluate) / s.dp_dv_(evaluate); - self.evaluate_property(func, contributions, false) - } - /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$ pub fn ln_phi(&self) -> Array1 { (self.residual_chemical_potential() / (SIUnit::gas_constant() * self.temperature)) @@ -182,18 +238,18 @@ impl State { /// 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}$ pub fn dln_phi_dt(&self) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| { - (s.dmu_dt_(evaluate) + s.dp_dni_(evaluate) * (s.dp_dt_(evaluate) / s.dp_dv_(evaluate)) - - s.chemical_potential_(evaluate) / self.temperature) - / (SIUnit::gas_constant() * self.temperature) - }; - self.evaluate_property(func, Contributions::ResidualNpt, false) + let vi_rt = -self.dp_dni(Contributions::Total) + / self.dp_dv(Contributions::Total) + / (SIUnit::gas_constant() * self.temperature); + self.dmu_res_dt() + 1.0 / self.temperature - vi_rt * self.dp_dt(Contributions::Total) } /// 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}$ pub fn dln_phi_dp(&self) -> SIArray1 { - self.partial_molar_volume(Contributions::ResidualNpt) - / (SIUnit::gas_constant() * self.temperature) + let vi_rt = -self.dp_dni(Contributions::Total) + / self.dp_dv(Contributions::Total) + / (SIUnit::gas_constant() * self.temperature); + vi_rt - 1.0 / self.pressure(Contributions::Total) } /// 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}$ @@ -220,31 +276,43 @@ impl State { }) } - /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ - pub fn structure_factor(&self) -> f64 { - -(SIUnit::gas_constant() * self.temperature * self.density) - .to_reduced(self.volume * self.dp_dv(Contributions::Total)) - .unwrap() + /// Molar residual isochoric heat capacity: $c_v^\text{res}=\left(\frac{\partial u^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn c_v_res(&self) -> SINumber { + self.temperature * self.ds_res_dt() / self.total_moles } - // This function is designed specifically for use in density iterations - pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { - let dp_dv = self.dp_dv(Contributions::Total); - ( - self.pressure(Contributions::Total), - (-self.volume * dp_dv / self.density), - ) + /// Partial derivative of the molar residual isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn dc_v_res_dt(&self, contributions: Contributions) -> SINumber { + (self.temperature * self.d2s_res_dt2() + self.ds_res_dt()) / self.total_moles } - // This function is designed specifically for use in spinodal iterations - pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { - let d2p_dv2 = self.d2p_dv2(Contributions::Total); - let dp_dv = self.dp_dv(Contributions::Total); - ( - self.pressure(Contributions::Total), - (-self.volume * dp_dv / self.density), - (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)), - ) + /// Molar residual isobaric heat capacity: $c_p^\text{res}=\left(\frac{\partial h^\text{res}}{\partial T}\right)_{p,N_i}$ + pub fn c_p_res(&self, contributions: Contributions) -> SINumber { + self.temperature / self.total_moles + * (self.ds_res_dt() + - self.dp_dt(Contributions::Total).powi(2) / self.dp_dv(Contributions::Total)) + - SIUnit::gas_constant() + } + + /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+pV-nRT$ + pub fn residual_enthalpy(&self) -> SINumber { + self.temperature * self.residual_entropy() + + self.residual_helmholtz_energy() + + self.pressure(Contributions::Residual) * self.volume + } + + /// Residual internal energy: $U\text{res}(T, V, \mathbf{n})=A\text{res}+TS\text{res}$ + pub fn residual_internal_energy(&self, contributions: Contributions) -> SINumber { + self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() + } + + /// Residual Gibbs energy: $G\text{res}(T,p,\mathbf{n})=A\text{res}+pV-NRT-NRT \ln Z$ + pub fn residual_gibbs_energy(&self, contributions: Contributions) -> SINumber { + self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy() + - self.total_moles + * SIUnit::gas_constant() + * self.temperature + * self.compressibility(Contributions::Total).ln() } } From d4bd1272c9defa243ee80bb5caaec6722978af20 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 1 Jun 2023 21:37:36 +0200 Subject: [PATCH 04/47] feos-core compiles again --- feos-core/src/equation_of_state/ideal_gas.rs | 2 +- feos-core/src/state/mod.rs | 14 +++++++------- feos-core/src/state/residual_properties.rs | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs index 581f0211e..adc690e0f 100644 --- a/feos-core/src/equation_of_state/ideal_gas.rs +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -26,7 +26,7 @@ pub trait IdealGas: Sync + Send + fmt::Display { /// In some cases it could be advantageous to overwrite this /// implementation instead of implementing the de Broglie /// wavelength. - fn evaluate_ideal_gas>(&self, state: &StateHD) -> D + fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D where dyn DeBroglieWavelength: DeBroglieWavelengthDual, { diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index c610d086d..73d9b1c2b 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -323,7 +323,6 @@ impl State { molefracs: Option<&Array1>, pressure: Option, density_initialization: DensityInitialization, - initial_temperature: Option, ) -> EosResult { Self::_new( eos, @@ -336,7 +335,6 @@ impl State { molefracs, pressure, density_initialization, - initial_temperature, )? .map_err(|_| EosError::UndeterminedState(String::from("Missing input parameters."))) } @@ -352,7 +350,6 @@ impl State { molefracs: Option<&Array1>, pressure: Option, density_initialization: DensityInitialization, - initial_temperature: Option, ) -> EosResult>> { // Check if the provided densities have correct units. if let DensityInitialization::InitialDensity(rho0) = density_initialization { @@ -507,8 +504,12 @@ impl State { (Ok(_), Err(_)) => liquid, (Err(_), Ok(_)) => vapor, (Ok(l), Ok(v)) => { - if l.molar_gibbs_energy(Contributions::Total) - > v.molar_gibbs_energy(Contributions::Total) + if l.residual_gibbs_energy() + > v.residual_gibbs_energy() + + moles.sum() + * SIUnit::gas_constant() + * v.temperature + * (l.volume.to_reduced(v.volume)?.ln()) { vapor } else { @@ -582,12 +583,11 @@ impl State { molefracs, pressure, density_initialization, - initial_temperature, )?; let ti = initial_temperature; match state { - Ok(state) => return Ok(state), + Ok(state) => Ok(state), Err(n_i) => { // Check if new state can be created using molar_enthalpy and temperature if let (Some(p), Some(h), Some(n_i)) = (pressure, molar_enthalpy, &n_i) { diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 51a20906f..40703f6bb 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -1,6 +1,6 @@ use super::{Contributions, Derivative::*, PartialDerivative, State}; // use crate::equation_of_state::{EntropyScaling, MolarWeight, Residual}; -use crate::equation_of_state::{ideal_gas, residual, Residual}; +use crate::equation_of_state::Residual; use crate::errors::EosResult; use crate::EosUnit; use ndarray::{arr1, Array1, Array2}; @@ -282,12 +282,12 @@ impl State { } /// Partial derivative of the molar residual isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V^\text{res}}{\partial T}\right)_{V,N_i}$ - pub fn dc_v_res_dt(&self, contributions: Contributions) -> SINumber { + pub fn dc_v_res_dt(&self) -> SINumber { (self.temperature * self.d2s_res_dt2() + self.ds_res_dt()) / self.total_moles } /// Molar residual isobaric heat capacity: $c_p^\text{res}=\left(\frac{\partial h^\text{res}}{\partial T}\right)_{p,N_i}$ - pub fn c_p_res(&self, contributions: Contributions) -> SINumber { + pub fn c_p_res(&self) -> SINumber { self.temperature / self.total_moles * (self.ds_res_dt() - self.dp_dt(Contributions::Total).powi(2) / self.dp_dv(Contributions::Total)) @@ -302,12 +302,12 @@ impl State { } /// Residual internal energy: $U\text{res}(T, V, \mathbf{n})=A\text{res}+TS\text{res}$ - pub fn residual_internal_energy(&self, contributions: Contributions) -> SINumber { + pub fn residual_internal_energy(&self) -> SINumber { self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() } /// Residual Gibbs energy: $G\text{res}(T,p,\mathbf{n})=A\text{res}+pV-NRT-NRT \ln Z$ - pub fn residual_gibbs_energy(&self, contributions: Contributions) -> SINumber { + pub fn residual_gibbs_energy(&self) -> SINumber { self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy() - self.total_moles * SIUnit::gas_constant() From 75e24a32f41b229a75adc68288303c6e512f1027 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 1 Jun 2023 22:01:30 +0200 Subject: [PATCH 05/47] Fancy StateBuilder --- feos-core/src/lib.rs | 8 +-- feos-core/src/state/builder.rs | 97 +++++++++++++++++++++++++--------- feos-core/src/state/mod.rs | 4 +- 3 files changed, 76 insertions(+), 33 deletions(-) diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 03d0d0973..35ba805ae 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -44,13 +44,7 @@ pub use errors::{EosError, EosResult}; // PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, // }; pub use state::{ - Contributions, - DensityInitialization, - Derivative, - State, - // StateBuilder, - StateHD, - StateVec, + Contributions, DensityInitialization, Derivative, State, StateBuilder, StateHD, StateVec, }; #[cfg(feature = "python")] diff --git a/feos-core/src/state/builder.rs b/feos-core/src/state/builder.rs index 40406862b..b9a598b4c 100644 --- a/feos-core/src/state/builder.rs +++ b/feos-core/src/state/builder.rs @@ -1,5 +1,5 @@ use super::{DensityInitialization, State}; -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::{IdealGas, Residual}; use crate::errors::EosResult; use ndarray::Array1; use quantity::si::{SIArray1, SINumber}; @@ -52,7 +52,7 @@ use std::sync::Arc; /// # Ok(()) /// # } /// ``` -pub struct StateBuilder<'a, E: EquationOfState> { +pub struct StateBuilder<'a, E, const IG: bool> { eos: Arc, temperature: Option, volume: Option, @@ -69,7 +69,7 @@ pub struct StateBuilder<'a, E: EquationOfState> { initial_temperature: Option, } -impl<'a, E: EquationOfState> StateBuilder<'a, E> { +impl<'a, E: Residual> StateBuilder<'a, E, false> { /// Create a new `StateBuilder` for the given equation of state. pub fn new(eos: &Arc) -> Self { StateBuilder { @@ -89,7 +89,9 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { initial_temperature: None, } } +} +impl<'a, E: Residual, const IG: bool> StateBuilder<'a, E, IG> { /// Provide the temperature for the new state. pub fn temperature(mut self, temperature: SINumber) -> Self { self.temperature = Some(temperature); @@ -138,24 +140,6 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { self } - /// Provide the molar enthalpy for the new state. - pub fn molar_enthalpy(mut self, molar_enthalpy: SINumber) -> Self { - self.molar_enthalpy = Some(molar_enthalpy); - self - } - - /// Provide the molar entropy for the new state. - pub fn molar_entropy(mut self, molar_entropy: SINumber) -> Self { - self.molar_entropy = Some(molar_entropy); - self - } - - /// Provide the molar internal energy for the new state. - pub fn molar_internal_energy(mut self, molar_internal_energy: SINumber) -> Self { - self.molar_internal_energy = Some(molar_internal_energy); - self - } - /// Specify a vapor state. pub fn vapor(mut self) -> Self { self.density_initialization = DensityInitialization::Vapor; @@ -173,16 +157,81 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { self.density_initialization = DensityInitialization::InitialDensity(initial_density); self } +} + +impl<'a, E: Residual + IdealGas, const IG: bool> StateBuilder<'a, E, IG> { + /// Provide the molar enthalpy for the new state. + pub fn molar_enthalpy(mut self, molar_enthalpy: SINumber) -> StateBuilder<'a, E, true> { + self.molar_enthalpy = Some(molar_enthalpy); + self.convert() + } + + /// Provide the molar entropy for the new state. + pub fn molar_entropy(mut self, molar_entropy: SINumber) -> StateBuilder<'a, E, true> { + self.molar_entropy = Some(molar_entropy); + self.convert() + } + + /// Provide the molar internal energy for the new state. + pub fn molar_internal_energy( + mut self, + molar_internal_energy: SINumber, + ) -> StateBuilder<'a, E, true> { + self.molar_internal_energy = Some(molar_internal_energy); + self.convert() + } /// Provide an initial temperature used in the Newton solver. - pub fn initial_temperature(mut self, initial_temperature: SINumber) -> Self { + pub fn initial_temperature( + mut self, + initial_temperature: SINumber, + ) -> StateBuilder<'a, E, true> { self.initial_temperature = Some(initial_temperature); - self + self.convert() + } + + fn convert(self) -> StateBuilder<'a, E, true> { + StateBuilder { + eos: self.eos, + temperature: self.temperature, + volume: self.volume, + density: self.density, + partial_density: self.partial_density, + total_moles: self.total_moles, + moles: self.moles, + molefracs: self.molefracs, + pressure: self.pressure, + molar_enthalpy: self.molar_enthalpy, + molar_entropy: self.molar_entropy, + molar_internal_energy: self.molar_internal_energy, + density_initialization: self.density_initialization, + initial_temperature: self.initial_temperature, + } } +} +impl<'a, E: Residual> StateBuilder<'a, E, false> { /// Try to build the state with the given inputs. pub fn build(self) -> EosResult> { State::new( + &self.eos, + self.temperature, + self.volume, + self.density, + self.partial_density, + self.total_moles, + self.moles, + self.molefracs, + self.pressure, + self.density_initialization, + ) + } +} + +impl<'a, E: Residual + IdealGas> StateBuilder<'a, E, true> { + /// Try to build the state with the given inputs. + pub fn build(self) -> EosResult> { + State::new_full( &self.eos, self.temperature, self.volume, @@ -201,7 +250,7 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { } } -impl<'a, E: EquationOfState> Clone for StateBuilder<'a, E> { +impl<'a, E, const IG: bool> Clone for StateBuilder<'a, E, IG> { fn clone(&self) -> Self { Self { eos: self.eos.clone(), diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index 73d9b1c2b..3219e3867 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -19,12 +19,12 @@ use std::convert::TryFrom; use std::fmt; use std::sync::{Arc, Mutex}; -// mod builder; +mod builder; mod cache; mod properties; mod residual_properties; mod statevec; -// pub use builder::StateBuilder; +pub use builder::StateBuilder; pub use statevec::StateVec; /// Possible contributions that can be computed. From 89d6d8e7f478430927969f446146f28b3fe5cb52 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Fri, 2 Jun 2023 13:15:16 +0200 Subject: [PATCH 06/47] Adjusted parameters, cubic and Joback. Added EquationOfState and tests for residual properties --- feos-core/src/cubic.rs | 177 +++---- feos-core/src/equation_of_state/ideal_gas.rs | 6 +- feos-core/src/equation_of_state/mod.rs | 219 +++------ feos-core/src/errors.rs | 6 +- feos-core/src/joback.rs | 354 +++++++------- feos-core/src/lib.rs | 141 +++++- feos-core/src/parameter/chemical_record.rs | 8 +- feos-core/src/parameter/mod.rs | 484 +++++++++---------- feos-core/src/parameter/model_record.rs | 153 +++--- feos-core/src/parameter/segment.rs | 25 +- feos-core/src/state/residual_properties.rs | 24 +- 11 files changed, 790 insertions(+), 807 deletions(-) diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index f0dc05862..6ef1adf9f 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -4,10 +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::{ - EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGasContribution, -}; -use crate::joback::{Joback, JobackRecord}; +use crate::equation_of_state::{HelmholtzEnergy, HelmholtzEnergyDual, Residual}; use crate::parameter::{Identifier, Parameter, ParameterError, PureRecord}; use crate::si::{GRAM, MOL}; use crate::state::StateHD; @@ -64,9 +61,7 @@ pub struct PengRobinsonParameters { /// Molar weight in units of g/mol molarweight: Array1, /// List of pure component records - pure_records: Vec>, - /// List of ideal gas Joback records - joback_records: Option>, + pure_records: Vec>, } impl std::fmt::Display for PengRobinsonParameters { @@ -102,7 +97,7 @@ impl PengRobinsonParameters { acentric_factor: acentric_factor[i], }; let id = Identifier::default(); - PureRecord::new(id, molarweight[i], record, None) + PureRecord::new(id, molarweight[i], record) }) .collect(); Ok(PengRobinsonParameters::from_records( @@ -114,12 +109,11 @@ impl PengRobinsonParameters { impl Parameter for PengRobinsonParameters { type Pure = PengRobinsonRecord; - type IdealGas = JobackRecord; type Binary = f64; /// Creates parameters from pure component records. fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -139,11 +133,6 @@ impl Parameter for PengRobinsonParameters { kappa[i] = 0.37464 + (1.54226 - 0.26992 * r.acentric_factor) * r.acentric_factor; } - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { tc, a, @@ -152,14 +141,13 @@ impl Parameter for PengRobinsonParameters { kappa, molarweight, pure_records, - joback_records, } } fn records( &self, ) -> ( - &[PureRecord], + &[PureRecord], &Array2, ) { (&self.pure_records, &self.k_ij) @@ -207,8 +195,6 @@ impl fmt::Display for PengRobinsonContribution { pub struct PengRobinson { /// Parameters parameters: Arc, - /// Ideal gas contributions to the Helmholtz energy - ideal_gas: Joback, /// Non-ideal contributions to the Helmholtz energy contributions: Vec>, } @@ -216,23 +202,24 @@ pub struct PengRobinson { impl PengRobinson { /// Create a new equation of state from a set of parameters. pub fn new(parameters: Arc) -> Self { - let ideal_gas = parameters.joback_records.as_ref().map_or_else( - || Joback::default(parameters.tc.len()), - |j| Joback::new(j.clone()), - ); let contributions: Vec> = vec![Box::new(PengRobinsonContribution { parameters: parameters.clone(), })]; Self { parameters, - ideal_gas, contributions, } } } -impl EquationOfState for PengRobinson { +impl fmt::Display for PengRobinson { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Peng Robinson") + } +} + +impl Residual for PengRobinson { fn components(&self) -> usize { self.parameters.b.len() } @@ -246,13 +233,9 @@ impl EquationOfState for PengRobinson { 0.9 / b } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.ideal_gas - } } impl MolarWeight for PengRobinson { @@ -261,72 +244,72 @@ impl MolarWeight for PengRobinson { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::phase_equilibria::SolverOptions; - use crate::state::State; - use crate::Contributions; - use crate::{EosResult, Verbosity}; - use approx::*; - use quantity::si::*; - use std::sync::Arc; +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::phase_equilibria::SolverOptions; +// use crate::state::State; +// use crate::Contributions; +// use crate::{EosResult, Verbosity}; +// use approx::*; +// use quantity::si::*; +// use std::sync::Arc; - fn pure_record_vec() -> Vec> { - let records = r#"[ - { - "identifier": { - "cas": "74-98-6", - "name": "propane", - "iupac_name": "propane", - "smiles": "CCC", - "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", - "formula": "C3H8" - }, - "model_record": { - "tc": 369.96, - "pc": 4250000.0, - "acentric_factor": 0.153 - }, - "molarweight": 44.0962 - }, - { - "identifier": { - "cas": "106-97-8", - "name": "butane", - "iupac_name": "butane", - "smiles": "CCCC", - "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", - "formula": "C4H10" - }, - "model_record": { - "tc": 425.2, - "pc": 3800000.0, - "acentric_factor": 0.199 - }, - "molarweight": 58.123 - } - ]"#; - serde_json::from_str(records).expect("Unable to parse json.") - } +// fn pure_record_vec() -> Vec> { +// let records = r#"[ +// { +// "identifier": { +// "cas": "74-98-6", +// "name": "propane", +// "iupac_name": "propane", +// "smiles": "CCC", +// "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", +// "formula": "C3H8" +// }, +// "model_record": { +// "tc": 369.96, +// "pc": 4250000.0, +// "acentric_factor": 0.153 +// }, +// "molarweight": 44.0962 +// }, +// { +// "identifier": { +// "cas": "106-97-8", +// "name": "butane", +// "iupac_name": "butane", +// "smiles": "CCCC", +// "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", +// "formula": "C4H10" +// }, +// "model_record": { +// "tc": 425.2, +// "pc": 3800000.0, +// "acentric_factor": 0.199 +// }, +// "molarweight": 58.123 +// } +// ]"#; +// serde_json::from_str(records).expect("Unable to parse json.") +// } - #[test] - fn peng_robinson() -> EosResult<()> { - let mixture = pure_record_vec(); - let propane = mixture[0].clone(); - let tc = propane.model_record.tc; - let pc = propane.model_record.pc; - let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); - let pr = Arc::new(PengRobinson::new(Arc::new(parameters))); - let options = SolverOptions::new().verbosity(Verbosity::Iter); - let cp = State::critical_point(&pr, None, None, options)?; - println!("{} {}", cp.temperature, cp.pressure(Contributions::Total)); - assert_relative_eq!(cp.temperature, tc * KELVIN, max_relative = 1e-4); - assert_relative_eq!( - cp.pressure(Contributions::Total), - pc * PASCAL, - max_relative = 1e-4 - ); - Ok(()) - } -} +// #[test] +// fn peng_robinson() -> EosResult<()> { +// let mixture = pure_record_vec(); +// let propane = mixture[0].clone(); +// let tc = propane.model_record.tc; +// let pc = propane.model_record.pc; +// let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); +// let pr = Arc::new(PengRobinson::new(Arc::new(parameters))); +// let options = SolverOptions::new().verbosity(Verbosity::Iter); +// let cp = State::critical_point(&pr, None, None, options)?; +// println!("{} {}", cp.temperature, cp.pressure(Contributions::Total)); +// assert_relative_eq!(cp.temperature, tc * KELVIN, max_relative = 1e-4); +// assert_relative_eq!( +// cp.pressure(Contributions::Total), +// pc * PASCAL, +// max_relative = 1e-4 +// ); +// Ok(()) +// } +// } diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs index adc690e0f..112590868 100644 --- a/feos-core/src/equation_of_state/ideal_gas.rs +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -15,9 +15,9 @@ pub trait IdealGas: Sync + Send + fmt::Display { // /// Return the number of components // fn components(&self) -> usize; - // /// Return an equation of state consisting of the components - // /// contained in component_list. - // fn subset(&self, component_list: &[usize]) -> Self; + /// Return an equation of state consisting of the components + /// contained in component_list. + fn subset(&self, component_list: &[usize]) -> Self; fn ideal_gas_model(&self) -> &Box; diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 2d4afb397..0c7440d78 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -1,9 +1,7 @@ -use crate::{EosError, EosResult, EosUnit, StateHD}; -use ndarray::{Array, Array1}; -use num_dual::{Dual3, Dual3_64, Dual64, DualNum, HyperDual, HyperDual64}; -use num_traits::{One, Zero}; -use quantity::si::{SIArray1, SINumber, SIUnit, MOL}; -use std::sync::Arc; +use ndarray::Array1; +use num_dual::DualNum; +use quantity::si::{SIArray1, MOL}; +use std::{fmt::Display, sync::Arc}; pub use ideal_gas::IdealGas; pub use residual::Residual; @@ -11,8 +9,10 @@ pub mod debroglie; pub mod helmholtz_energy; pub mod ideal_gas; pub mod residual; -pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; +use crate::StateHD; + pub use self::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; +pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; /// Molar weight of all components. /// @@ -22,152 +22,79 @@ pub trait MolarWeight { fn molar_weight(&self) -> SIArray1; } -// #[derive(Clone)] -// pub struct Model { -// pub ideal_gas: Arc, -// pub residual: Arc, -// components: usize, -// } - -// impl Model { -// pub fn new(ideal_gas: Arc, residual: Arc) -> Self { -// assert_eq!(residual.components(), ideal_gas.components()); -// let components = residual.components(); -// Self { -// ideal_gas, -// residual, -// components, -// } -// } - -// pub fn components(&self) -> usize { -// self.components -// } +#[derive(Clone)] +pub struct EquationOfState { + pub ideal_gas: Arc, + pub residual: Arc, + components: usize, +} -// pub fn subset(&self, component_list: &[usize]) -> Self { -// Self::new( -// Arc::new(self.ideal_gas.subset(component_list)), -// Arc::new(self.residual.subset(component_list)), -// ) -// } +impl Display for EquationOfState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} {}", + self.ideal_gas.to_string(), + self.residual.to_string() + ) + } +} -// /// Check if the provided optional mole number is consistent with the -// /// equation of state. -// /// -// /// In general, the number of elements in `moles` needs to match the number -// /// of components of the equation of state. For a pure component, however, -// /// no moles need to be provided. In that case, it is set to the constant -// /// reference value. -// pub fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { -// let l = moles.map_or(1, |m| m.len()); -// if self.components() == l { -// match moles { -// Some(m) => Ok(m.to_owned()), -// None => Ok(Array::ones(1) * SIUnit::reference_moles()), -// } -// } else { -// Err(EosError::IncompatibleComponents(self.components(), l)) -// } -// } +impl EquationOfState { + pub fn new(ideal_gas: Arc, residual: Arc) -> Self { + // assert_eq!(residual.components(), ideal_gas.components()); + let components = residual.components(); + Self { + ideal_gas, + residual, + components, + } + } +} -// /// Calculate the maximum density. -// /// -// /// This value is used as an estimate for a liquid phase for phase -// /// equilibria and other iterations. It is not explicitly meant to -// /// be a mathematical limit for the density (if those exist in the -// /// equation of state anyways). -// pub fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { -// let mr = self -// .residual -// .validate_moles(moles)? -// .to_reduced(SIUnit::reference_moles())?; -// Ok(self.residual.compute_max_density(&mr) * SIUnit::reference_density()) -// } +impl IdealGas for EquationOfState { + fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D + where + dyn DeBroglieWavelength: DeBroglieWavelengthDual, + { + self.ideal_gas.evaluate_ideal_gas(state) + } -// pub fn evaluate_residual>(&self, state: &StateHD) -> D -// where -// dyn HelmholtzEnergy: HelmholtzEnergyDual, -// { -// self.residual.helmholtz_energy(state) -// } + fn subset(&self, component_list: &[usize]) -> Self { + Self::new( + Arc::new(self.ideal_gas.subset(component_list)), + Arc::new(self.residual.subset(component_list)), + ) + } -// pub fn evaluate_ideal_gas>(&self, state: &StateHD) -> D -// where -// dyn DeBroglieWavelength: DeBroglieWavelengthDual, -// { -// self.ideal_gas.evaluate_ideal_gas(state) -// } + fn ideal_gas_model(&self) -> &Box { + self.ideal_gas.ideal_gas_model() + } +} -// /// Calculate the second virial coefficient $B(T)$ -// pub fn second_virial_coefficient( -// &self, -// temperature: SINumber, -// moles: Option<&SIArray1>, -// ) -> EosResult { -// let mr = self.validate_moles(moles)?; -// let x = mr.to_reduced(mr.sum())?; -// let mut rho = HyperDual64::zero(); -// rho.eps1[0] = 1.0; -// rho.eps2[0] = 1.0; -// let t = HyperDual64::from(temperature.to_reduced(SIUnit::reference_temperature())?); -// let s = StateHD::new_virial(t, rho, x); -// Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)] * 0.5 / SIUnit::reference_density()) -// } +impl Residual for EquationOfState { + fn components(&self) -> usize { + self.residual.components() + } -// /// Calculate the third virial coefficient $C(T)$ -// pub fn third_virial_coefficient( -// &self, -// temperature: SINumber, -// moles: Option<&SIArray1>, -// ) -> EosResult { -// let mr = self.validate_moles(moles)?; -// let x = mr.to_reduced(mr.sum())?; -// let rho = Dual3_64::zero().derive(); -// let t = Dual3_64::from(temperature.to_reduced(SIUnit::reference_temperature())?); -// let s = StateHD::new_virial(t, rho, x); -// Ok(self.evaluate_residual(&s).v3 / 3.0 / SIUnit::reference_density().powi(2)) -// } + fn subset(&self, component_list: &[usize]) -> Self { + Self::new( + Arc::new(self.ideal_gas.subset(component_list)), + Arc::new(self.residual.subset(component_list)), + ) + } -// /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ -// pub fn second_virial_coefficient_temperature_derivative( -// &self, -// temperature: SINumber, -// moles: Option<&SIArray1>, -// ) -> EosResult { -// let mr = self.validate_moles(moles)?; -// let x = mr.to_reduced(mr.sum())?; -// let mut rho = HyperDual::zero(); -// rho.eps1[0] = Dual64::one(); -// rho.eps2[0] = Dual64::one(); -// let t = HyperDual::from_re( -// Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), -// ); -// let s = StateHD::new_virial(t, rho, x); -// Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)].eps[0] * 0.5 -// / (SIUnit::reference_density() * SIUnit::reference_temperature())) -// } + fn compute_max_density(&self, moles: &Array1) -> f64 { + self.residual.compute_max_density(moles) + } -// /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ -// pub fn third_virial_coefficient_temperature_derivative( -// &self, -// temperature: SINumber, -// moles: Option<&SIArray1>, -// ) -> EosResult { -// let mr = self.validate_moles(moles)?; -// let x = mr.to_reduced(mr.sum())?; -// let rho = Dual3::zero().derive(); -// let t = Dual3::from_re( -// Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derive(), -// ); -// let s = StateHD::new_virial(t, rho, x); -// Ok(self.evaluate_residual(&s).v3.eps[0] -// / 3.0 -// / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) -// } -// } + fn contributions(&self) -> &[Box] { + self.residual.contributions() + } +} -// impl Model { -// pub fn molar_weight(&self) -> Array1 { -// self.residual.molar_weight().to_reduced(MOL).unwrap() -// } -// } +impl EquationOfState { + pub fn molar_weight(&self) -> Array1 { + self.residual.molar_weight().to_reduced(MOL).unwrap() + } +} diff --git a/feos-core/src/errors.rs b/feos-core/src/errors.rs index d93060035..8bc344268 100644 --- a/feos-core/src/errors.rs +++ b/feos-core/src/errors.rs @@ -3,6 +3,8 @@ use num_dual::linalg::LinAlgError; use quantity::QuantityError; use thiserror::Error; +use crate::parameter::ParameterError; + /// Error type for improperly defined states and convergence problems. #[derive(Error, Debug)] pub enum EosError { @@ -28,8 +30,8 @@ pub enum EosError { WrongUnits(String, String), #[error(transparent)] QuantityError(#[from] QuantityError), - // #[error(transparent)] - // ParameterError(#[from] ParameterError), + #[error(transparent)] + ParameterError(#[from] ParameterError), #[error(transparent)] LinAlgError(#[from] LinAlgError), #[cfg(feature = "rayon")] diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index b8d73e90e..d7dab952e 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -1,17 +1,16 @@ //! Implementation of the ideal gas heat capacity (de Broglie wavelength) //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). +use crate::equation_of_state::{DeBroglieWavelength, DeBroglieWavelengthDual}; use crate::parameter::*; -use crate::{ - EosResult, EosUnit, EquationOfState, HelmholtzEnergy, IdealGasContribution, - IdealGasContributionDual, -}; +use crate::{EosResult, EosUnit, IdealGas}; use conv::ValueInto; use ndarray::Array1; use num_dual::*; use quantity::si::{SINumber, SIUnit}; use serde::{Deserialize, Serialize}; use std::fmt; +use std::sync::Arc; /// Coefficients used in the Joback model. /// @@ -67,21 +66,18 @@ impl> FromSegments for JobackRecord { /// The ideal gas contribution according to /// [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). -#[derive(Debug, Clone)] pub struct Joback { - pub records: Vec, + pub records: Arc>, + de_broglie: Box, } impl Joback { /// Creates a new Joback contribution. - pub fn new(records: Vec) -> Self { - Self { records } - } - - /// Creates a default ($c_p^\mathrm{ig}=0$) ideal gas contribution for the - /// given number of components. - pub fn default(components: usize) -> Self { - Self::new(vec![JobackRecord::default(); components]) + pub fn new(records: Arc>) -> Self { + Self { + records: records.clone(), + de_broglie: Box::new(JobackDeBroglie(records)), + } } /// Directly calculates the ideal gas heat capacity from the Joback model. @@ -101,19 +97,42 @@ impl fmt::Display for Joback { } } +impl IdealGas for Joback { + fn subset(&self, component_list: &[usize]) -> Self { + let mut records = Vec::with_capacity(component_list.len()); + component_list + .iter() + .for_each(|&i| records.push(self.records[i].clone())); + Self::new(Arc::new(records)) + } + + fn ideal_gas_model(&self) -> &Box { + &self.de_broglie + } +} + const RGAS: f64 = 6.022140857 * 1.38064852; const T0: f64 = 298.15; const P0: f64 = 1.0e5; const A3: f64 = 1e-30; const KB: f64 = 1.38064852e-23; -impl + Copy> IdealGasContributionDual for Joback { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { +#[derive(Debug, Clone)] +struct JobackDeBroglie(Arc>); + +impl fmt::Display for JobackDeBroglie { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal gas (Joback)") + } +} + +impl + Copy> DeBroglieWavelengthDual for JobackDeBroglie { + fn de_broglie_wavelength(&self, temperature: D) -> Array1 { let t = temperature; let t2 = t * t; let f = (temperature * KB / (P0 * A3)).ln(); - Array1::from_shape_fn(components, |i| { - let j = &self.records[i]; + Array1::from_shape_fn(self.0.len(), |i| { + let j = &self.0[i]; let h = (t2 - T0 * T0) * 0.5 * j.b + (t * t2 - T0.powi(3)) * j.c / 3.0 + (t2 * t2 - T0.powi(4)) * j.d / 4.0 @@ -129,173 +148,144 @@ impl + Copy> IdealGasContributionDual for Joback { } } -impl EquationOfState for Joback { - fn components(&self) -> usize { - self.records.len() - } - - fn subset(&self, component_list: &[usize]) -> Self { - let records = component_list - .iter() - .map(|&i| self.records[i].clone()) - .collect(); - Self::new(records) - } - - fn compute_max_density(&self, _moles: &Array1) -> f64 { - 1.0 - } - - fn residual(&self) -> &[Box] { - &[] - } +// #[cfg(test)] +// mod tests { +// use crate::{Contributions, State, StateBuilder}; +// use approx::assert_relative_eq; +// use ndarray::arr1; +// use quantity::si::*; +// use std::sync::Arc; - fn ideal_gas(&self) -> &dyn IdealGasContribution { - self - } -} +// use super::*; -#[cfg(test)] -mod tests { - use crate::{Contributions, State, StateBuilder}; - use approx::assert_relative_eq; - use ndarray::arr1; - use quantity::si::*; - use std::sync::Arc; +// #[derive(Deserialize, Clone, Debug)] +// struct ModelRecord; - use super::*; +// #[test] +// fn paper_example() -> EosResult<()> { +// let segments_json = r#"[ +// { +// "identifier": "-Cl", +// "model_record": null, +// "ideal_gas_record": { +// "a": 33.3, +// "b": -0.0963, +// "c": 0.000187, +// "d": -9.96e-8, +// "e": 0.0 +// }, +// "molarweight": 35.453 +// }, +// { +// "identifier": "-CH=(ring)", +// "model_record": null, +// "ideal_gas_record": { +// "a": -2.14, +// "b": 5.74e-2, +// "c": -1.64e-6, +// "d": -1.59e-8, +// "e": 0.0 +// }, +// "molarweight": 13.01864 +// }, +// { +// "identifier": "=CH<(ring)", +// "model_record": null, +// "ideal_gas_record": { +// "a": -8.25, +// "b": 1.01e-1, +// "c": -1.42e-4, +// "d": 6.78e-8, +// "e": 0.0 +// }, +// "molarweight": 13.01864 +// } +// ]"#; +// let segment_records: Vec> = +// serde_json::from_str(segments_json).expect("Unable to parse json."); +// let segments = ChemicalRecord::new( +// Identifier::default(), +// vec![ +// String::from("-Cl"), +// String::from("-Cl"), +// String::from("-CH=(ring)"), +// String::from("-CH=(ring)"), +// String::from("-CH=(ring)"), +// String::from("-CH=(ring)"), +// String::from("=CH<(ring)"), +// String::from("=CH<(ring)"), +// ], +// None, +// ) +// .segment_map(&segment_records)?; +// assert_eq!(segments.get(&segment_records[0]), Some(&2)); +// assert_eq!(segments.get(&segment_records[1]), Some(&4)); +// assert_eq!(segments.get(&segment_records[2]), Some(&2)); - #[derive(Deserialize, Clone, Debug)] - struct ModelRecord; +// let jr = JobackRecord::from_segments(&joback_segments)?; +// assert_relative_eq!( +// jr.a, +// 33.3 * 2.0 - 2.14 * 4.0 - 8.25 * 2.0 - 37.93, +// epsilon = 1e-10 +// ); +// assert_relative_eq!( +// jr.b, +// -0.0963 * 2.0 + 5.74e-2 * 4.0 + 1.01e-1 * 2.0 + 0.21, +// epsilon = 1e-10 +// ); +// assert_relative_eq!( +// jr.c, +// 0.000187 * 2.0 - 1.64e-6 * 4.0 - 1.42e-4 * 2.0 - 3.91e-4, +// epsilon = 1e-10 +// ); +// assert_relative_eq!( +// jr.d, +// -9.96e-8 * 2.0 - 1.59e-8 * 4.0 + 6.78e-8 * 2.0 + 2.06e-7, +// epsilon = 1e-10 +// ); +// assert_relative_eq!(jr.e, 0.0); - #[test] - fn paper_example() -> EosResult<()> { - let segments_json = r#"[ - { - "identifier": "-Cl", - "model_record": null, - "ideal_gas_record": { - "a": 33.3, - "b": -0.0963, - "c": 0.000187, - "d": -9.96e-8, - "e": 0.0 - }, - "molarweight": 35.453 - }, - { - "identifier": "-CH=(ring)", - "model_record": null, - "ideal_gas_record": { - "a": -2.14, - "b": 5.74e-2, - "c": -1.64e-6, - "d": -1.59e-8, - "e": 0.0 - }, - "molarweight": 13.01864 - }, - { - "identifier": "=CH<(ring)", - "model_record": null, - "ideal_gas_record": { - "a": -8.25, - "b": 1.01e-1, - "c": -1.42e-4, - "d": 6.78e-8, - "e": 0.0 - }, - "molarweight": 13.01864 - } - ]"#; - let segment_records: Vec> = - serde_json::from_str(segments_json).expect("Unable to parse json."); - let segments = ChemicalRecord::new( - Identifier::default(), - vec![ - String::from("-Cl"), - String::from("-Cl"), - String::from("-CH=(ring)"), - String::from("-CH=(ring)"), - String::from("-CH=(ring)"), - String::from("-CH=(ring)"), - String::from("=CH<(ring)"), - String::from("=CH<(ring)"), - ], - None, - ) - .segment_map(&segment_records)?; - assert_eq!(segments.get(&segment_records[0]), Some(&2)); - assert_eq!(segments.get(&segment_records[1]), Some(&4)); - assert_eq!(segments.get(&segment_records[2]), Some(&2)); - let joback_segments: Vec<_> = segments - .iter() - .map(|(s, &n)| (s.ideal_gas_record.clone().unwrap(), n)) - .collect(); - let jr = JobackRecord::from_segments(&joback_segments)?; - assert_relative_eq!( - jr.a, - 33.3 * 2.0 - 2.14 * 4.0 - 8.25 * 2.0 - 37.93, - epsilon = 1e-10 - ); - assert_relative_eq!( - jr.b, - -0.0963 * 2.0 + 5.74e-2 * 4.0 + 1.01e-1 * 2.0 + 0.21, - epsilon = 1e-10 - ); - assert_relative_eq!( - jr.c, - 0.000187 * 2.0 - 1.64e-6 * 4.0 - 1.42e-4 * 2.0 - 3.91e-4, - epsilon = 1e-10 - ); - assert_relative_eq!( - jr.d, - -9.96e-8 * 2.0 - 1.59e-8 * 4.0 + 6.78e-8 * 2.0 + 2.06e-7, - epsilon = 1e-10 - ); - assert_relative_eq!(jr.e, 0.0); +// let eos = Arc::new(Joback::new(vec![jr])); +// let state = State::new_nvt( +// &eos, +// 1000.0 * KELVIN, +// 1.0 * ANGSTROM.powi(3), +// &(arr1(&[1.0]) * MOL), +// )?; +// assert!( +// (state +// .c_p(Contributions::IdealGas) +// .to_reduced(JOULE / MOL / KELVIN)? +// - 224.6) +// .abs() +// < 1.0 +// ); +// Ok(()) +// } - let eos = Arc::new(Joback::new(vec![jr])); - let state = State::new_nvt( - &eos, - 1000.0 * KELVIN, - 1.0 * ANGSTROM.powi(3), - &(arr1(&[1.0]) * MOL), - )?; - assert!( - (state - .c_p(Contributions::IdealGas) - .to_reduced(JOULE / MOL / KELVIN)? - - 224.6) - .abs() - < 1.0 - ); - Ok(()) - } - - #[test] - fn c_p_comparison() -> EosResult<()> { - let record1 = JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005); - let record2 = JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001); - let joback = Arc::new(Joback::new(vec![record1, record2])); - let temperature = 300.0 * KELVIN; - let volume = METER.powi(3); - let moles = arr1(&[1.0, 3.0]) * MOL; - let state = StateBuilder::new(&joback) - .temperature(temperature) - .volume(volume) - .moles(&moles) - .build()?; - println!( - "{} {}", - joback.c_p(temperature, &state.molefracs)?, - state.c_p(Contributions::IdealGas) - ); - assert_relative_eq!( - joback.c_p(temperature, &state.molefracs)?, - state.c_p(Contributions::IdealGas), - max_relative = 1e-10 - ); - Ok(()) - } -} +// #[test] +// fn c_p_comparison() -> EosResult<()> { +// let record1 = JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005); +// let record2 = JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001); +// let joback = Arc::new(Joback::new(vec![record1, record2])); +// let temperature = 300.0 * KELVIN; +// let volume = METER.powi(3); +// let moles = arr1(&[1.0, 3.0]) * MOL; +// let state = StateBuilder::new(&joback) +// .temperature(temperature) +// .volume(volume) +// .moles(&moles) +// .build()?; +// println!( +// "{} {}", +// joback.c_p(temperature, &state.molefracs)?, +// state.c_p(Contributions::IdealGas) +// ); +// assert_relative_eq!( +// joback.c_p(temperature, &state.molefracs)?, +// state.c_p(Contributions::IdealGas), +// max_relative = 1e-10 +// ); +// Ok(()) +// } +// } diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 35ba805ae..e1edbee7c 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -27,18 +27,17 @@ macro_rules! log_result { } } -// pub mod cubic; +pub mod cubic; mod density_iteration; mod equation_of_state; mod errors; -// pub mod joback; -// pub mod parameter; +pub mod joback; +pub mod parameter; // mod phase_equilibria; mod state; -// pub use equation_of_state::{ -// EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGasContribution, -// IdealGasContributionDual, MolarWeight, -// }; +pub use equation_of_state::{ + EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, +}; pub use errors::{EosError, EosResult}; // pub use phase_equilibria::{ // PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, @@ -122,3 +121,131 @@ impl EosUnit for SIUnit { RGAS } } + +#[cfg(test)] +mod tests { + use crate::cubic::*; + use crate::equation_of_state::ideal_gas; + use crate::equation_of_state::EquationOfState; + use crate::joback::Joback; + use crate::joback::JobackRecord; + use crate::parameter::*; + use crate::state::State; + use crate::Contributions; + use crate::EosResult; + use crate::StateBuilder; + use approx::*; + use ndarray::Array2; + use quantity::si::*; + use std::sync::Arc; + + fn pure_record_vec() -> Vec> { + let records = r#"[ + { + "identifier": { + "cas": "74-98-6", + "name": "propane", + "iupac_name": "propane", + "smiles": "CCC", + "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", + "formula": "C3H8" + }, + "model_record": { + "tc": 369.96, + "pc": 4250000.0, + "acentric_factor": 0.153 + }, + "molarweight": 44.0962 + }, + { + "identifier": { + "cas": "106-97-8", + "name": "butane", + "iupac_name": "butane", + "smiles": "CCCC", + "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", + "formula": "C4H10" + }, + "model_record": { + "tc": 425.2, + "pc": 3800000.0, + "acentric_factor": 0.199 + }, + "molarweight": 58.123 + } + ]"#; + serde_json::from_str(records).expect("Unable to parse json.") + } + + #[test] + fn validate_residual_properties() -> EosResult<()> { + let mixture = pure_record_vec(); + let propane = mixture[0].clone(); + let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); + let residual = Arc::new(PengRobinson::new(Arc::new(parameters))); + let ideal_gas = Arc::new(Joback::new(Arc::new(vec![JobackRecord::new( + 1.0, 1.0, 1.0, 1.0, 1.0, + )]))); + let eos = Arc::new(EquationOfState::new(ideal_gas, residual.clone())); + + let sr = StateBuilder::new(&residual) + .temperature(300.0 * KELVIN) + .pressure(1.0 * BAR) + .build()?; + + let s = StateBuilder::new(&eos) + .temperature(300.0 * KELVIN) + .pressure(1.0 * BAR) + .build()?; + + // pressure + assert_relative_eq!(s.pressure(Contributions::Total), sr.pressure(Contributions::Total), max_relative = 1e-15); + assert_relative_eq!(s.pressure(Contributions::Residual), sr.pressure(Contributions::Residual), max_relative = 1e-15); + assert_relative_eq!(s.compressibility(Contributions::Total), sr.compressibility(Contributions::Total), max_relative = 1e-15); + assert_relative_eq!(s.compressibility(Contributions::Residual), sr.compressibility(Contributions::Residual), max_relative = 1e-15); + + // residual properties + assert_relative_eq!(s.helmholtz_energy(Contributions::Residual), sr.residual_helmholtz_energy(), max_relative = 1e-15); + assert_relative_eq!(s.entropy(Contributions::Residual), sr.residual_entropy(), max_relative = 1e-15); + assert_relative_eq!(s.enthalpy(Contributions::Residual), sr.residual_enthalpy(), max_relative = 1e-15); + assert_relative_eq!(s.internal_energy(Contributions::Residual), sr.residual_internal_energy(), max_relative = 1e-15); + assert_relative_eq!(s.gibbs_energy(Contributions::Residual), sr.residual_gibbs_energy(), max_relative = 1e-15); + assert_relative_eq!(s.chemical_potential(Contributions::Residual), sr.residual_chemical_potential(), max_relative = 1e-15); + + // pressure derivatives + assert_relative_eq!(s.structure_factor(), sr.structure_factor(), max_relative = 1e-15); + assert_relative_eq!(s.dp_dt(Contributions::Total), sr.dp_dt(Contributions::Total), max_relative = 1e-15); + assert_relative_eq!(s.dp_dt(Contributions::Residual), sr.dp_dt(Contributions::Residual), max_relative = 1e-15); + assert_relative_eq!(s.dp_dv(Contributions::Total), sr.dp_dv(Contributions::Total), max_relative = 1e-15); + assert_relative_eq!(s.dp_dv(Contributions::Residual), sr.dp_dv(Contributions::Residual), max_relative = 1e-15); + assert_relative_eq!(s.dp_drho(Contributions::Total), sr.dp_drho(Contributions::Total), max_relative = 1e-15); + assert_relative_eq!(s.dp_drho(Contributions::Residual), sr.dp_drho(Contributions::Residual), max_relative = 1e-15); + assert_relative_eq!(s.d2p_dv2(Contributions::Total), sr.d2p_dv2(Contributions::Total), max_relative = 1e-15); + assert_relative_eq!(s.d2p_dv2(Contributions::Residual), sr.d2p_dv2(Contributions::Residual), max_relative = 1e-15); + assert_relative_eq!(s.d2p_drho2(Contributions::Total), sr.d2p_drho2(Contributions::Total), max_relative = 1e-15); + assert_relative_eq!(s.d2p_drho2(Contributions::Residual), sr.d2p_drho2(Contributions::Residual), max_relative = 1e-15); + assert_relative_eq!(s.dp_dni(Contributions::Total), sr.dp_dni(Contributions::Total), max_relative = 1e-15); + assert_relative_eq!(s.dp_dni(Contributions::Residual), sr.dp_dni(Contributions::Residual), max_relative = 1e-15); + + // entropy + assert_relative_eq!(s.ds_dt(Contributions::Residual), sr.ds_res_dt(), max_relative = 1e-15); + + // chemical potential + assert_relative_eq!(s.dmu_dt(Contributions::Residual), sr.dmu_res_dt(), max_relative = 1e-15); + assert_relative_eq!(s.dmu_dni(Contributions::Residual), sr.dmu_res_dni(), max_relative = 1e-15); + assert_relative_eq!(s.dmu_dt(Contributions::Residual), sr.dmu_res_dt(), max_relative = 1e-15); + + // fugacity + assert_relative_eq!(s.ln_phi(), sr.ln_phi(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dt(), sr.dln_phi_dt(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dp(), sr.dln_phi_dp(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dnj(), sr.dln_phi_dnj(), max_relative = 1e-15); + assert_relative_eq!(s.thermodynamic_factor(), sr.thermodynamic_factor(), max_relative = 1e-15); + + // residual properties using multiple derivatives + assert_relative_eq!(s.c_v(Contributions::Residual), sr.c_v_res(), max_relative = 1e-15); + assert_relative_eq!(s.dc_v_dt(Contributions::Residual), sr.dc_v_res_dt(), max_relative = 1e-15); + assert_relative_eq!(s.c_p(Contributions::Residual), sr.c_p_res(), max_relative = 1e-15); + Ok(()) + } +} diff --git a/feos-core/src/parameter/chemical_record.rs b/feos-core/src/parameter/chemical_record.rs index f498b1ba9..900a9ee87 100644 --- a/feos-core/src/parameter/chemical_record.rs +++ b/feos-core/src/parameter/chemical_record.rs @@ -120,13 +120,13 @@ pub trait SegmentCount { /// molecule. /// /// The map contains the segment record as key and the count as value. - fn segment_map( + fn segment_map( &self, - segment_records: &[SegmentRecord], - ) -> Result, Self::Count>, ParameterError> { + segment_records: &[SegmentRecord], + ) -> Result, Self::Count>, ParameterError> { let count = self.segment_count(); let queried: HashSet<_> = count.keys().cloned().collect(); - let mut segments: HashMap> = segment_records + let mut segments: HashMap> = segment_records .iter() .map(|r| (r.identifier.clone(), r.clone())) .collect(); diff --git a/feos-core/src/parameter/mod.rs b/feos-core/src/parameter/mod.rs index 48c62c303..7270af288 100644 --- a/feos-core/src/parameter/mod.rs +++ b/feos-core/src/parameter/mod.rs @@ -30,17 +30,16 @@ where Self: Sized, { type Pure: Clone + DeserializeOwned; - type IdealGas: Clone + DeserializeOwned; type Binary: Clone + DeserializeOwned + Default; /// Creates parameters from records for pure substances and possibly binary parameters. fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self; /// Creates parameters for a pure component from a pure record. - fn new_pure(pure_record: PureRecord) -> Self { + fn new_pure(pure_record: PureRecord) -> Self { let binary_record = Array2::from_elem([1, 1], Self::Binary::default()); Self::from_records(vec![pure_record], binary_record) } @@ -48,7 +47,7 @@ where /// Creates parameters for a binary system from pure records and an optional /// binary interaction parameter. fn new_binary( - pure_records: Vec>, + pure_records: Vec>, binary_record: Option, ) -> Self { let binary_record = Array2::from_shape_fn([2, 2], |(i, j)| { @@ -66,7 +65,7 @@ where fn records( &self, ) -> ( - &[PureRecord], + &[PureRecord], &Array2, ); @@ -76,7 +75,7 @@ where /// `pure_records`, the `Default` implementation of Self::Binary is used. #[allow(clippy::expect_fun_call)] fn binary_matrix_from_records( - pure_records: &Vec>, + pure_records: &Vec>, binary_records: &[BinaryRecord], search_option: IdentifierOption, ) -> Array2 { @@ -138,7 +137,7 @@ where P: AsRef, { let mut queried: IndexSet = IndexSet::new(); - let mut record_map: HashMap> = + let mut record_map: HashMap> = HashMap::new(); for (substances, file) in input { @@ -154,7 +153,7 @@ where let f = File::open(file)?; let reader = BufReader::new(f); - let pure_records: Vec> = + let pure_records: Vec> = serde_json::from_reader(reader)?; pure_records @@ -202,12 +201,11 @@ where /// and the ideal gas record. fn from_segments( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result where Self::Pure: FromSegments, - Self::IdealGas: FromSegments, Self::Binary: FromSegmentsBinary, { // update the pure records with model and ideal gas records @@ -276,7 +274,6 @@ where where P: AsRef, Self::Pure: FromSegments, - Self::IdealGas: FromSegments, Self::Binary: FromSegmentsBinary, { let queried: IndexSet = substances @@ -315,7 +312,7 @@ where .collect(); // Read segment records - let segment_records: Vec> = + let segment_records: Vec> = SegmentRecord::from_json(file_segments)?; // Read binary records @@ -353,13 +350,12 @@ where pub trait ParameterHetero: Sized { type Chemical: Clone; type Pure: Clone + DeserializeOwned; - type IdealGas: Clone + DeserializeOwned; type Binary: Clone + DeserializeOwned; /// Creates parameters from the molecular structure and segment information. fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result; @@ -369,7 +365,7 @@ pub trait ParameterHetero: Sized { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ); @@ -419,7 +415,7 @@ pub trait ParameterHetero: Sized { .collect(); // Read segment records - let segment_records: Vec> = + let segment_records: Vec> = SegmentRecord::from_json(file_segments)?; // Read binary records @@ -470,231 +466,231 @@ pub enum ParameterError { IncompatibleParameters(String), } -#[cfg(test)] -mod test { - use super::*; - use crate::joback::JobackRecord; - use serde::{Deserialize, Serialize}; - use std::convert::TryFrom; - - #[derive(Debug, Clone, Serialize, Deserialize, Default)] - struct MyPureModel { - a: f64, - } - - #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] - struct MyBinaryModel { - b: f64, - } - - impl TryFrom for MyBinaryModel { - type Error = &'static str; - fn try_from(f: f64) -> Result { - Ok(Self { b: f }) - } - } - - struct MyParameter { - pure_records: Vec>, - binary_records: Array2, - } - - impl Parameter for MyParameter { - type Pure = MyPureModel; - type IdealGas = JobackRecord; - type Binary = MyBinaryModel; - fn from_records( - pure_records: Vec>, - binary_records: Array2, - ) -> Self { - Self { - pure_records, - binary_records, - } - } - - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { - (&self.pure_records, &self.binary_records) - } - } - - #[test] - fn from_records() { - let pr_json = r#" - [ - { - "identifier": { - "cas": "123-4-5" - }, - "molarweight": 16.0426, - "model_record": { - "a": 0.1 - } - }, - { - "identifier": { - "cas": "678-9-1" - }, - "molarweight": 32.08412, - "model_record": { - "a": 0.2 - } - } - ] - "#; - let br_json = r#" - [ - { - "id1": { - "cas": "123-4-5" - }, - "id2": { - "cas": "678-9-1" - }, - "model_record": { - "b": 12.0 - } - } - ] - "#; - let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); - let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); - let binary_matrix = MyParameter::binary_matrix_from_records( - &pure_records, - &binary_records, - IdentifierOption::Cas, - ); - let p = MyParameter::from_records(pure_records, binary_matrix); - - assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); - assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); - assert_eq!(p.binary_records[[0, 1]].b, 12.0) - } - - #[test] - fn from_records_missing_binary() { - let pr_json = r#" - [ - { - "identifier": { - "cas": "123-4-5" - }, - "molarweight": 16.0426, - "model_record": { - "a": 0.1 - } - }, - { - "identifier": { - "cas": "678-9-1" - }, - "molarweight": 32.08412, - "model_record": { - "a": 0.2 - } - } - ] - "#; - let br_json = r#" - [ - { - "id1": { - "cas": "123-4-5" - }, - "id2": { - "cas": "000-00-0" - }, - "model_record": { - "b": 12.0 - } - } - ] - "#; - let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); - let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); - let binary_matrix = MyParameter::binary_matrix_from_records( - &pure_records, - &binary_records, - IdentifierOption::Cas, - ); - let p = MyParameter::from_records(pure_records, binary_matrix); - - assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); - assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); - assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[0, 1]].b, 0.0) - } - - #[test] - fn from_records_correct_binary_order() { - let pr_json = r#" - [ - { - "identifier": { - "cas": "000-0-0" - }, - "molarweight": 32.08412, - "model_record": { - "a": 0.2 - } - }, - { - "identifier": { - "cas": "123-4-5" - }, - "molarweight": 16.0426, - "model_record": { - "a": 0.1 - } - }, - { - "identifier": { - "cas": "678-9-1" - }, - "molarweight": 32.08412, - "model_record": { - "a": 0.2 - } - } - ] - "#; - let br_json = r#" - [ - { - "id1": { - "cas": "123-4-5" - }, - "id2": { - "cas": "678-9-1" - }, - "model_record": { - "b": 12.0 - } - } - ] - "#; - let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); - let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); - let binary_matrix = MyParameter::binary_matrix_from_records( - &pure_records, - &binary_records, - IdentifierOption::Cas, - ); - let p = MyParameter::from_records(pure_records, binary_matrix); - - assert_eq!(p.pure_records[0].identifier.cas, Some("000-0-0".into())); - assert_eq!(p.pure_records[1].identifier.cas, Some("123-4-5".into())); - assert_eq!(p.pure_records[2].identifier.cas, Some("678-9-1".into())); - assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[1, 0]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[0, 2]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[2, 0]], MyBinaryModel::default()); - assert_eq!(p.binary_records[[2, 1]].b, 12.0); - assert_eq!(p.binary_records[[1, 2]].b, 12.0); - } -} +// #[cfg(test)] +// mod test { +// use super::*; +// use crate::joback::JobackRecord; +// use serde::{Deserialize, Serialize}; +// use std::convert::TryFrom; + +// #[derive(Debug, Clone, Serialize, Deserialize, Default)] +// struct MyPureModel { +// a: f64, +// } + +// #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +// struct MyBinaryModel { +// b: f64, +// } + +// impl TryFrom for MyBinaryModel { +// type Error = &'static str; +// fn try_from(f: f64) -> Result { +// Ok(Self { b: f }) +// } +// } + +// struct MyParameter { +// pure_records: Vec>, +// binary_records: Array2, +// } + +// impl Parameter for MyParameter { +// type Pure = MyPureModel; +// type IdealGas = JobackRecord; +// type Binary = MyBinaryModel; +// fn from_records( +// pure_records: Vec>, +// binary_records: Array2, +// ) -> Self { +// Self { +// pure_records, +// binary_records, +// } +// } + +// fn records( +// &self, +// ) -> ( +// &[PureRecord], +// &Array2, +// ) { +// (&self.pure_records, &self.binary_records) +// } +// } + +// #[test] +// fn from_records() { +// let pr_json = r#" +// [ +// { +// "identifier": { +// "cas": "123-4-5" +// }, +// "molarweight": 16.0426, +// "model_record": { +// "a": 0.1 +// } +// }, +// { +// "identifier": { +// "cas": "678-9-1" +// }, +// "molarweight": 32.08412, +// "model_record": { +// "a": 0.2 +// } +// } +// ] +// "#; +// let br_json = r#" +// [ +// { +// "id1": { +// "cas": "123-4-5" +// }, +// "id2": { +// "cas": "678-9-1" +// }, +// "model_record": { +// "b": 12.0 +// } +// } +// ] +// "#; +// let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); +// let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); +// let binary_matrix = MyParameter::binary_matrix_from_records( +// &pure_records, +// &binary_records, +// IdentifierOption::Cas, +// ); +// let p = MyParameter::from_records(pure_records, binary_matrix); + +// assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); +// assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); +// assert_eq!(p.binary_records[[0, 1]].b, 12.0) +// } + +// #[test] +// fn from_records_missing_binary() { +// let pr_json = r#" +// [ +// { +// "identifier": { +// "cas": "123-4-5" +// }, +// "molarweight": 16.0426, +// "model_record": { +// "a": 0.1 +// } +// }, +// { +// "identifier": { +// "cas": "678-9-1" +// }, +// "molarweight": 32.08412, +// "model_record": { +// "a": 0.2 +// } +// } +// ] +// "#; +// let br_json = r#" +// [ +// { +// "id1": { +// "cas": "123-4-5" +// }, +// "id2": { +// "cas": "000-00-0" +// }, +// "model_record": { +// "b": 12.0 +// } +// } +// ] +// "#; +// let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); +// let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); +// let binary_matrix = MyParameter::binary_matrix_from_records( +// &pure_records, +// &binary_records, +// IdentifierOption::Cas, +// ); +// let p = MyParameter::from_records(pure_records, binary_matrix); + +// assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); +// assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); +// assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); +// assert_eq!(p.binary_records[[0, 1]].b, 0.0) +// } + +// #[test] +// fn from_records_correct_binary_order() { +// let pr_json = r#" +// [ +// { +// "identifier": { +// "cas": "000-0-0" +// }, +// "molarweight": 32.08412, +// "model_record": { +// "a": 0.2 +// } +// }, +// { +// "identifier": { +// "cas": "123-4-5" +// }, +// "molarweight": 16.0426, +// "model_record": { +// "a": 0.1 +// } +// }, +// { +// "identifier": { +// "cas": "678-9-1" +// }, +// "molarweight": 32.08412, +// "model_record": { +// "a": 0.2 +// } +// } +// ] +// "#; +// let br_json = r#" +// [ +// { +// "id1": { +// "cas": "123-4-5" +// }, +// "id2": { +// "cas": "678-9-1" +// }, +// "model_record": { +// "b": 12.0 +// } +// } +// ] +// "#; +// let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); +// let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); +// let binary_matrix = MyParameter::binary_matrix_from_records( +// &pure_records, +// &binary_records, +// IdentifierOption::Cas, +// ); +// let p = MyParameter::from_records(pure_records, binary_matrix); + +// assert_eq!(p.pure_records[0].identifier.cas, Some("000-0-0".into())); +// assert_eq!(p.pure_records[1].identifier.cas, Some("123-4-5".into())); +// assert_eq!(p.pure_records[2].identifier.cas, Some("678-9-1".into())); +// assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); +// assert_eq!(p.binary_records[[1, 0]], MyBinaryModel::default()); +// assert_eq!(p.binary_records[[0, 2]], MyBinaryModel::default()); +// assert_eq!(p.binary_records[[2, 0]], MyBinaryModel::default()); +// assert_eq!(p.binary_records[[2, 1]].b, 12.0); +// assert_eq!(p.binary_records[[1, 2]].b, 12.0); +// } +// } diff --git a/feos-core/src/parameter/model_record.rs b/feos-core/src/parameter/model_record.rs index 382b68936..f516f18df 100644 --- a/feos-core/src/parameter/model_record.rs +++ b/feos-core/src/parameter/model_record.rs @@ -10,28 +10,19 @@ use std::path::Path; /// A collection of parameters of a pure substance. #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PureRecord { +pub struct PureRecord { pub identifier: Identifier, pub molarweight: f64, pub model_record: M, - #[serde(default = "Default::default")] - #[serde(skip_serializing_if = "Option::is_none")] - pub ideal_gas_record: Option, } -impl PureRecord { +impl PureRecord { /// Create a new `PureRecord`. - pub fn new( - identifier: Identifier, - molarweight: f64, - model_record: M, - ideal_gas_record: Option, - ) -> Self { + pub fn new(identifier: Identifier, molarweight: f64, model_record: M) -> Self { Self { identifier, molarweight, model_record, - ideal_gas_record, } } @@ -43,47 +34,29 @@ impl PureRecord { where T: Copy + ValueInto, M: FromSegments, - I: FromSegments, - S: IntoIterator, T)>, + S: IntoIterator, T)>, { let mut molarweight = 0.0; let mut model_segments = Vec::new(); - let mut ideal_gas_segments = Vec::new(); for (s, n) in segments { molarweight += s.molarweight * n.value_into().unwrap(); model_segments.push((s.model_record, n)); - ideal_gas_segments.push(s.ideal_gas_record.map(|ig| (ig, n))); } let model_record = M::from_segments(&model_segments)?; - let ideal_gas_segments: Option> = ideal_gas_segments.into_iter().collect(); - let ideal_gas_record = ideal_gas_segments - .as_deref() - .map(I::from_segments) - .transpose()?; - - Ok(Self::new( - identifier, - molarweight, - model_record, - ideal_gas_record, - )) + Ok(Self::new(identifier, molarweight, model_record)) } } -impl std::fmt::Display for PureRecord +impl std::fmt::Display for PureRecord where M: std::fmt::Display, - I: std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PureRecord(")?; write!(f, "\n\tidentifier={},", self.identifier)?; write!(f, "\n\tmolarweight={},", self.molarweight)?; write!(f, "\n\tmodel_record={},", self.model_record)?; - if let Some(i) = self.ideal_gas_record.as_ref() { - write!(f, "\n\tideal_gas_record={},", i)?; - } write!(f, "\n)") } } @@ -150,60 +123,60 @@ where } } -#[cfg(test)] -mod test { - use super::*; - use crate::joback::JobackRecord; - - #[derive(Serialize, Deserialize, Debug, Default, Clone)] - struct TestModelRecordSegments { - a: f64, - } - - #[test] - fn deserialize() { - let r = r#" - { - "identifier": { - "cas": "123-4-5" - }, - "molarweight": 16.0426, - "model_record": { - "a": 0.1 - } - } - "#; - let record: PureRecord = - serde_json::from_str(r).expect("Unable to parse json."); - assert_eq!(record.identifier.cas, Some("123-4-5".into())) - } - - #[test] - fn deserialize_list() { - let r = r#" - [ - { - "identifier": { - "cas": "1" - }, - "molarweight": 1.0, - "model_record": { - "a": 1.0 - } - }, - { - "identifier": { - "cas": "2" - }, - "molarweight": 2.0, - "model_record": { - "a": 2.0 - } - } - ]"#; - let records: Vec> = - serde_json::from_str(r).expect("Unable to parse json."); - assert_eq!(records[0].identifier.cas, Some("1".into())); - assert_eq!(records[1].identifier.cas, Some("2".into())) - } -} +// #[cfg(test)] +// mod test { +// use super::*; +// use crate::joback::JobackRecord; + +// #[derive(Serialize, Deserialize, Debug, Default, Clone)] +// struct TestModelRecordSegments { +// a: f64, +// } + +// #[test] +// fn deserialize() { +// let r = r#" +// { +// "identifier": { +// "cas": "123-4-5" +// }, +// "molarweight": 16.0426, +// "model_record": { +// "a": 0.1 +// } +// } +// "#; +// let record: PureRecord = +// serde_json::from_str(r).expect("Unable to parse json."); +// assert_eq!(record.identifier.cas, Some("123-4-5".into())) +// } + +// #[test] +// fn deserialize_list() { +// let r = r#" +// [ +// { +// "identifier": { +// "cas": "1" +// }, +// "molarweight": 1.0, +// "model_record": { +// "a": 1.0 +// } +// }, +// { +// "identifier": { +// "cas": "2" +// }, +// "molarweight": 2.0, +// "model_record": { +// "a": 2.0 +// } +// } +// ]"#; +// let records: Vec> = +// serde_json::from_str(r).expect("Unable to parse json."); +// assert_eq!(records[0].identifier.cas, Some("1".into())); +// assert_eq!(records[1].identifier.cas, Some("2".into())) +// } +// } diff --git a/feos-core/src/parameter/segment.rs b/feos-core/src/parameter/segment.rs index f87b7a8d7..f7718c054 100644 --- a/feos-core/src/parameter/segment.rs +++ b/feos-core/src/parameter/segment.rs @@ -8,60 +8,49 @@ use std::path::Path; /// Parameters describing an individual segment of a molecule. #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct SegmentRecord { +pub struct SegmentRecord { pub identifier: String, pub molarweight: f64, pub model_record: M, - pub ideal_gas_record: Option, } -impl SegmentRecord { +impl SegmentRecord { /// Creates a new `SegmentRecord`. - pub fn new( - identifier: String, - molarweight: f64, - model_record: M, - ideal_gas_record: Option, - ) -> Self { + pub fn new(identifier: String, molarweight: f64, model_record: M) -> Self { Self { identifier, molarweight, model_record, - ideal_gas_record, } } /// Read a list of `SegmentRecord`s from a JSON file. pub fn from_json>(file: P) -> Result, ParameterError> where - I: DeserializeOwned, M: DeserializeOwned, { Ok(serde_json::from_reader(BufReader::new(File::open(file)?))?) } } -impl Hash for SegmentRecord { +impl Hash for SegmentRecord { fn hash(&self, state: &mut H) { self.identifier.hash(state); } } -impl PartialEq for SegmentRecord { +impl PartialEq for SegmentRecord { fn eq(&self, other: &Self) -> bool { self.identifier == other.identifier } } -impl Eq for SegmentRecord {} +impl Eq for SegmentRecord {} -impl std::fmt::Display for SegmentRecord { +impl std::fmt::Display for SegmentRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SegmentRecord(\n\tidentifier={}", self.identifier)?; write!(f, "\n\tmolarweight={}", self.molarweight)?; write!(f, "\n\tmodel_record={}", self.model_record)?; - if let Some(i) = self.ideal_gas_record.as_ref() { - write!(f, "\n\tideal_gas_record={},", i)?; - } write!(f, "\n)") } } diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 40703f6bb..3d12446b4 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -62,11 +62,11 @@ impl State { } } - fn residual_helmholtz_energy(&self) -> SINumber { + pub fn residual_helmholtz_energy(&self) -> SINumber { self.get_or_compute_derivative_residual(PartialDerivative::Zeroth) } - fn residual_entropy(&self) -> SINumber { + pub fn residual_entropy(&self) -> SINumber { -self.get_or_compute_derivative_residual(PartialDerivative::First(DT)) } @@ -78,7 +78,7 @@ impl State { } /// Residual chemical potential: $\mu_i^\text{res}=\left(\frac{\partial A^\text{res}}{\partial N_i}\right)_{T,V,N_j}$ - fn residual_chemical_potential(&self) -> SIArray1 { + pub fn residual_chemical_potential(&self) -> SIArray1 { SIArray::from_shape_fn(self.eos.components(), |i| { self.get_or_compute_derivative_residual(PartialDerivative::First(DN(i))) }) @@ -136,7 +136,7 @@ impl State { pub fn d2p_dv2(&self, contributions: Contributions) -> SINumber { let ideal_gas = 2.0 * self.density * SIUnit::gas_constant() * self.temperature / (self.volume * self.volume); - let residual = -self.get_or_compute_derivative_residual(PartialDerivative::Second(DV)); + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::Third(DV)); Self::contributions(ideal_gas, residual, contributions) } @@ -175,11 +175,11 @@ impl State { // entropy derivatives - fn ds_res_dt(&self) -> SINumber { + pub fn ds_res_dt(&self) -> SINumber { -self.get_or_compute_derivative_residual(PartialDerivative::Second(DT)) } - fn d2s_res_dt2(&self) -> SINumber { + pub fn d2s_res_dt2(&self) -> SINumber { -self.get_or_compute_derivative_residual(PartialDerivative::Third(DT)) } @@ -204,10 +204,6 @@ impl State { .into_value() .unwrap() - self.compressibility(Contributions::Total) - // (self.chemical_potential(Contributions::ResidualNpt) - // / (SIUnit::gas_constant() * self.temperature)) - // .into_value() - // .unwrap() } /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure. @@ -238,10 +234,10 @@ impl State { /// 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}$ pub fn dln_phi_dt(&self) -> SIArray1 { - let vi_rt = -self.dp_dni(Contributions::Total) - / self.dp_dv(Contributions::Total) - / (SIUnit::gas_constant() * self.temperature); - self.dmu_res_dt() + 1.0 / self.temperature - vi_rt * self.dp_dt(Contributions::Total) + let vi = -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total); + (self.dmu_res_dt() - vi * self.dp_dt(Contributions::Total)) + / (SIUnit::gas_constant() * self.temperature) + + 1.0 / self.temperature } /// 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}$ From 4476475948480c22ce482146da63ecd643b75556 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Fri, 2 Jun 2023 14:36:42 +0200 Subject: [PATCH 07/47] Added EntropyScaling in core, adjusted derive macros, started adjusting PC-SAFT in feos, moved SolverOptions and Verbosity to state/mod.rs --- .../src/equation_of_state/__ideal_gas.rs | 100 ++++ feos-core/src/equation_of_state/mod.rs | 4 +- feos-core/src/equation_of_state/residual.rs | 25 + feos-core/src/lib.rs | 193 +++++-- feos-core/src/state/critical_point.rs | 64 +-- feos-core/src/state/mod.rs | 73 ++- feos-core/src/state/residual_properties.rs | 182 +++---- feos-derive/src/lib.rs | 20 +- src/eos.rs | 21 +- src/lib.rs | 2 +- src/pcsaft/eos/mod.rs | 509 +++++++++--------- src/pcsaft/eos/qspr.rs | 132 ----- src/pcsaft/parameters.rs | 37 +- 13 files changed, 771 insertions(+), 591 deletions(-) create mode 100644 feos-core/src/equation_of_state/__ideal_gas.rs delete mode 100644 src/pcsaft/eos/qspr.rs diff --git a/feos-core/src/equation_of_state/__ideal_gas.rs b/feos-core/src/equation_of_state/__ideal_gas.rs new file mode 100644 index 000000000..96a676352 --- /dev/null +++ b/feos-core/src/equation_of_state/__ideal_gas.rs @@ -0,0 +1,100 @@ +use crate::StateHD; +use ndarray::Array1; +use num_dual::DualNum; +use num_dual::*; +use std::fmt; + +/// Ideal gas Helmholtz energy contribution that can +/// be evaluated using generalized (hyper) dual numbers. +/// +/// This trait needs to be implemented generically or for +/// the specific types in the supertraits of [IdealGasContribution] +/// so that the implementor can be used as an ideal gas +/// contribution in the equation of state. +pub trait IdealGasDual> { + /// Return the number of components + fn components(&self) -> usize; + + /// Return an equation of state consisting of the components + /// contained in component_list. + fn subset(&self, component_list: &[usize]) -> Self; + + fn de_broglie_wavelength(&self, temperature: D) -> Array1; + + /// Evaluate the ideal gas contribution for a given state. + /// + /// In some cases it could be advantageous to overwrite this + /// implementation instead of implementing the de Broglie + /// wavelength. + fn helmholtz_energy(&self, state: &StateHD) -> D { + let lambda = self.de_broglie_wavelength(state.temperature); + ((lambda + + state.partial_density.mapv(|x| { + if x.re() == 0.0 { + D::from(0.0) + } else { + x.ln() - 1.0 + } + })) + * &state.moles) + .sum() + } +} + +pub struct DefaultIdealGas(pub usize); + +impl> IdealGasDual for DefaultIdealGas { + fn components(&self) -> usize { + self.0 + } + fn subset(&self, component_list: &[usize]) -> Self { + Self(component_list.len()) + } + fn de_broglie_wavelength(&self, temperature: D) -> Array1 { + Array1::zeros(self.0) + } +} + +impl fmt::Display for DefaultIdealGas { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal gas (default)") + } +} + +pub trait IdealGas: + IdealGasDual + + IdealGasDual + + IdealGasDual, f64>> + + IdealGasDual + + IdealGasDual + + IdealGasDual + + IdealGasDual> + + IdealGasDual, f64>> + + IdealGasDual, f64>> + + IdealGasDual> + + IdealGasDual, f64>> + + IdealGasDual, f64>> + + fmt::Display + + Send + + Sync +{ +} + +impl IdealGas for T where + T: IdealGasDual + + IdealGasDual + + IdealGasDual, f64>> + + IdealGasDual + + IdealGasDual + + IdealGasDual + + IdealGasDual> + + IdealGasDual, f64>> + + IdealGasDual, f64>> + + IdealGasDual> + + IdealGasDual, f64>> + + IdealGasDual, f64>> + + fmt::Display + + Send + + Sync +{ +} diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 0c7440d78..62a566a2f 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -3,14 +3,14 @@ use num_dual::DualNum; use quantity::si::{SIArray1, MOL}; use std::{fmt::Display, sync::Arc}; -pub use ideal_gas::IdealGas; -pub use residual::Residual; pub mod debroglie; pub mod helmholtz_energy; pub mod ideal_gas; pub mod residual; use crate::StateHD; +pub use ideal_gas::IdealGas; +pub use residual::{EntropyScaling, Residual}; pub use self::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; diff --git a/feos-core/src/equation_of_state/residual.rs b/feos-core/src/equation_of_state/residual.rs index 63ed066f9..e0212b01f 100644 --- a/feos-core/src/equation_of_state/residual.rs +++ b/feos-core/src/equation_of_state/residual.rs @@ -153,3 +153,28 @@ pub trait Residual: Send + Sync + fmt::Display { / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) } } + +/// Reference values and residual entropy correlations for entropy scaling. +pub trait EntropyScaling { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult; + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; +} \ No newline at end of file diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index e1edbee7c..063ff50ff 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -36,7 +36,8 @@ pub mod parameter; // mod phase_equilibria; mod state; pub use equation_of_state::{ - EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, + DeBroglieWavelength, EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, + IdealGas, MolarWeight, Residual, }; pub use errors::{EosError, EosResult}; // pub use phase_equilibria::{ @@ -199,53 +200,177 @@ mod tests { .build()?; // pressure - assert_relative_eq!(s.pressure(Contributions::Total), sr.pressure(Contributions::Total), max_relative = 1e-15); - assert_relative_eq!(s.pressure(Contributions::Residual), sr.pressure(Contributions::Residual), max_relative = 1e-15); - assert_relative_eq!(s.compressibility(Contributions::Total), sr.compressibility(Contributions::Total), max_relative = 1e-15); - assert_relative_eq!(s.compressibility(Contributions::Residual), sr.compressibility(Contributions::Residual), max_relative = 1e-15); - + assert_relative_eq!( + s.pressure(Contributions::Total), + sr.pressure(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.pressure(Contributions::Residual), + sr.pressure(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.compressibility(Contributions::Total), + sr.compressibility(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.compressibility(Contributions::Residual), + sr.compressibility(Contributions::Residual), + max_relative = 1e-15 + ); + // residual properties - assert_relative_eq!(s.helmholtz_energy(Contributions::Residual), sr.residual_helmholtz_energy(), max_relative = 1e-15); - assert_relative_eq!(s.entropy(Contributions::Residual), sr.residual_entropy(), max_relative = 1e-15); - assert_relative_eq!(s.enthalpy(Contributions::Residual), sr.residual_enthalpy(), max_relative = 1e-15); - assert_relative_eq!(s.internal_energy(Contributions::Residual), sr.residual_internal_energy(), max_relative = 1e-15); - assert_relative_eq!(s.gibbs_energy(Contributions::Residual), sr.residual_gibbs_energy(), max_relative = 1e-15); - assert_relative_eq!(s.chemical_potential(Contributions::Residual), sr.residual_chemical_potential(), max_relative = 1e-15); + assert_relative_eq!( + s.helmholtz_energy(Contributions::Residual), + sr.residual_helmholtz_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.entropy(Contributions::Residual), + sr.residual_entropy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.enthalpy(Contributions::Residual), + sr.residual_enthalpy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.internal_energy(Contributions::Residual), + sr.residual_internal_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.gibbs_energy(Contributions::Residual), + sr.residual_gibbs_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.chemical_potential(Contributions::Residual), + sr.residual_chemical_potential(), + max_relative = 1e-15 + ); // pressure derivatives - assert_relative_eq!(s.structure_factor(), sr.structure_factor(), max_relative = 1e-15); - assert_relative_eq!(s.dp_dt(Contributions::Total), sr.dp_dt(Contributions::Total), max_relative = 1e-15); - assert_relative_eq!(s.dp_dt(Contributions::Residual), sr.dp_dt(Contributions::Residual), max_relative = 1e-15); - assert_relative_eq!(s.dp_dv(Contributions::Total), sr.dp_dv(Contributions::Total), max_relative = 1e-15); - assert_relative_eq!(s.dp_dv(Contributions::Residual), sr.dp_dv(Contributions::Residual), max_relative = 1e-15); - assert_relative_eq!(s.dp_drho(Contributions::Total), sr.dp_drho(Contributions::Total), max_relative = 1e-15); - assert_relative_eq!(s.dp_drho(Contributions::Residual), sr.dp_drho(Contributions::Residual), max_relative = 1e-15); - assert_relative_eq!(s.d2p_dv2(Contributions::Total), sr.d2p_dv2(Contributions::Total), max_relative = 1e-15); - assert_relative_eq!(s.d2p_dv2(Contributions::Residual), sr.d2p_dv2(Contributions::Residual), max_relative = 1e-15); - assert_relative_eq!(s.d2p_drho2(Contributions::Total), sr.d2p_drho2(Contributions::Total), max_relative = 1e-15); - assert_relative_eq!(s.d2p_drho2(Contributions::Residual), sr.d2p_drho2(Contributions::Residual), max_relative = 1e-15); - assert_relative_eq!(s.dp_dni(Contributions::Total), sr.dp_dni(Contributions::Total), max_relative = 1e-15); - assert_relative_eq!(s.dp_dni(Contributions::Residual), sr.dp_dni(Contributions::Residual), max_relative = 1e-15); - + assert_relative_eq!( + s.structure_factor(), + sr.structure_factor(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dt(Contributions::Total), + sr.dp_dt(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dt(Contributions::Residual), + sr.dp_dt(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dv(Contributions::Total), + sr.dp_dv(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dv(Contributions::Residual), + sr.dp_dv(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_drho(Contributions::Total), + sr.dp_drho(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_drho(Contributions::Residual), + sr.dp_drho(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_dv2(Contributions::Total), + sr.d2p_dv2(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_dv2(Contributions::Residual), + sr.d2p_dv2(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_drho2(Contributions::Total), + sr.d2p_drho2(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_drho2(Contributions::Residual), + sr.d2p_drho2(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dni(Contributions::Total), + sr.dp_dni(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dni(Contributions::Residual), + sr.dp_dni(Contributions::Residual), + max_relative = 1e-15 + ); + // entropy - assert_relative_eq!(s.ds_dt(Contributions::Residual), sr.ds_res_dt(), max_relative = 1e-15); + assert_relative_eq!( + s.ds_dt(Contributions::Residual), + sr.ds_res_dt(), + max_relative = 1e-15 + ); // chemical potential - assert_relative_eq!(s.dmu_dt(Contributions::Residual), sr.dmu_res_dt(), max_relative = 1e-15); - assert_relative_eq!(s.dmu_dni(Contributions::Residual), sr.dmu_res_dni(), max_relative = 1e-15); - assert_relative_eq!(s.dmu_dt(Contributions::Residual), sr.dmu_res_dt(), max_relative = 1e-15); + assert_relative_eq!( + s.dmu_dt(Contributions::Residual), + sr.dmu_res_dt(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dmu_dni(Contributions::Residual), + sr.dmu_res_dni(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dmu_dt(Contributions::Residual), + sr.dmu_res_dt(), + max_relative = 1e-15 + ); // fugacity assert_relative_eq!(s.ln_phi(), sr.ln_phi(), max_relative = 1e-15); assert_relative_eq!(s.dln_phi_dt(), sr.dln_phi_dt(), max_relative = 1e-15); assert_relative_eq!(s.dln_phi_dp(), sr.dln_phi_dp(), max_relative = 1e-15); assert_relative_eq!(s.dln_phi_dnj(), sr.dln_phi_dnj(), max_relative = 1e-15); - assert_relative_eq!(s.thermodynamic_factor(), sr.thermodynamic_factor(), max_relative = 1e-15); + assert_relative_eq!( + s.thermodynamic_factor(), + sr.thermodynamic_factor(), + max_relative = 1e-15 + ); // residual properties using multiple derivatives - assert_relative_eq!(s.c_v(Contributions::Residual), sr.c_v_res(), max_relative = 1e-15); - assert_relative_eq!(s.dc_v_dt(Contributions::Residual), sr.dc_v_res_dt(), max_relative = 1e-15); - assert_relative_eq!(s.c_p(Contributions::Residual), sr.c_p_res(), max_relative = 1e-15); + assert_relative_eq!( + s.c_v(Contributions::Residual), + sr.c_v_res(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dc_v_dt(Contributions::Residual), + sr.dc_v_res_dt(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.c_p(Contributions::Residual), + sr.c_p_res(), + max_relative = 1e-15 + ); Ok(()) } } diff --git a/feos-core/src/state/critical_point.rs b/feos-core/src/state/critical_point.rs index c215e928e..7d90b4bc9 100644 --- a/feos-core/src/state/critical_point.rs +++ b/feos-core/src/state/critical_point.rs @@ -1,7 +1,6 @@ -use super::{State, StateHD, TPSpec}; -use crate::equation_of_state::EquationOfState; +use super::{SolverOptions, State, StateHD, TPSpec, Verbosity}; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::phase_equilibria::{SolverOptions, Verbosity}; use crate::{DensityInitialization, EosUnit}; use nalgebra::{DMatrix, DVector, SVector, SymmetricEigen}; use ndarray::{arr1, Array1}; @@ -19,10 +18,10 @@ const MAX_ITER_CRIT_POINT_BINARY: usize = 200; const TOL_CRIT_POINT: f64 = 1e-8; /// # Critical points -impl State { +impl State { /// Calculate the pure component critical point of all components. pub fn critical_point_pure( - eos: &Arc, + eos: &Arc, initial_temperature: Option, options: SolverOptions, ) -> EosResult> @@ -42,7 +41,7 @@ impl State { } pub fn critical_point_binary( - eos: &Arc, + eos: &Arc, temperature_or_pressure: SINumber, initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, @@ -67,7 +66,7 @@ impl State { /// Calculate the critical point of a system for given moles. pub fn critical_point( - eos: &Arc, + eos: &Arc, moles: Option<&SIArray1>, initial_temperature: Option, options: SolverOptions, @@ -94,7 +93,7 @@ impl State { } fn critical_point_hkm( - eos: &Arc, + eos: &Arc, moles: &SIArray1, initial_temperature: SINumber, options: SolverOptions, @@ -175,7 +174,7 @@ impl State { /// Calculate the critical point of a binary system for given temperature. fn critical_point_binary_t( - eos: &Arc, + eos: &Arc, temperature: SINumber, initial_molefracs: Option<[f64; 2]>, options: SolverOptions, @@ -256,7 +255,7 @@ impl State { /// Calculate the critical point of a binary system for given pressure. fn critical_point_binary_p( - eos: &Arc, + eos: &Arc, pressure: SINumber, initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, @@ -352,7 +351,7 @@ impl State { } pub fn spinodal( - eos: &Arc, + eos: &Arc, temperature: SINumber, moles: Option<&SIArray1>, options: SolverOptions, @@ -381,7 +380,7 @@ impl State { } fn calculate_spinodal( - eos: &Arc, + eos: &Arc, temperature: SINumber, moles: &SIArray1, density_initialization: DensityInitialization, @@ -459,8 +458,8 @@ impl State { } } -fn critical_point_objective( - eos: &Arc, +fn critical_point_objective( + eos: &Arc, temperature: DualSVec64<2>, density: DualSVec64<2>, moles: &Array1, @@ -473,8 +472,8 @@ fn critical_point_objective( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, m); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (moles[i] * moles[j]).sqrt() + // (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) + eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -494,12 +493,13 @@ fn critical_point_objective( Dual3::from_re(density.recip() * moles.sum()), moles_hd, ); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); + // let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); + let res = eos.evaluate_residual(&state_s); Ok(SVector::from([eval, res.v3])) } -fn critical_point_objective_t( - eos: &Arc, +fn critical_point_objective_t( + eos: &Arc, temperature: f64, density: SVector, 2>, ) -> EosResult, 2>> { @@ -511,8 +511,8 @@ fn critical_point_objective_t( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (density[i] * density[j]).sqrt() + // (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) + eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -528,12 +528,12 @@ fn critical_point_objective_t( ) }); let state_s = StateHD::new(Dual3::from(temperature), Dual3::from(1.0), moles_hd); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); + let res = eos.evaluate_residual(&state_s); // + eos.ideal_gas().evaluate(&state_s); Ok(SVector::from([eval, res.v3])) } -fn critical_point_objective_p( - eos: &Arc, +fn critical_point_objective_p( + eos: &Arc, pressure: f64, temperature: DualSVec64<3>, density: SVector, 2>, @@ -546,8 +546,8 @@ fn critical_point_objective_p( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (density[i] * density[j]).sqrt() + // (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) + eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -563,21 +563,21 @@ fn critical_point_objective_p( ) }); let state_s = StateHD::new(Dual3::from_re(temperature), Dual3::from(1.0), moles_hd); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); + let res = eos.evaluate_residual(&state_s); // + eos.ideal_gas().evaluate(&state_s); // calculate pressure let a = |v| { let m = arr1(&[Dual::from_re(density[0]), Dual::from_re(density[1])]); let state_p = StateHD::new(Dual::from_re(temperature), v, m); - eos.evaluate_residual(&state_p) + eos.ideal_gas().evaluate(&state_p) + eos.evaluate_residual(&state_p) // + eos.ideal_gas().evaluate(&state_p) }; let (_, p) = first_derivative(a, DualVec::one()); Ok(SVector::from([eval, res.v3, p * temperature + pressure])) } -fn spinodal_objective( - eos: &Arc, +fn spinodal_objective( + eos: &Arc, temperature: Dual64, density: Dual64, moles: &Array1, @@ -590,8 +590,8 @@ fn spinodal_objective( m[i].eps1 = Dual64::one(); m[j].eps2 = Dual64::one(); let state = StateHD::new(t, v, m); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (moles[i] * moles[j]).sqrt() + // (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) + eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() }); // calculate smallest eigenvalue of q diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index 3219e3867..543e475a0 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -27,6 +27,77 @@ mod statevec; pub use builder::StateBuilder; pub use statevec::StateVec; +/// Level of detail in the iteration output. +#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum Verbosity { + /// Do not print output. + None, + /// Print information about the success of failure of the iteration. + Result, + /// Print a detailed outpur for every iteration. + Iter, +} + +impl Default for Verbosity { + fn default() -> Self { + Self::None + } +} + +/// Options for the various phase equilibria solvers. +/// +/// If the values are [None], solver specific default +/// values are used. +#[derive(Copy, Clone, Default)] +pub struct SolverOptions { + /// Maximum number of iterations. + pub max_iter: Option, + /// Tolerance. + pub tol: Option, + /// Iteration outpput indicated by the [Verbosity] enum. + pub verbosity: Verbosity, +} + +impl From<(Option, Option, Option)> for SolverOptions { + fn from(options: (Option, Option, Option)) -> Self { + Self { + max_iter: options.0, + tol: options.1, + verbosity: options.2.unwrap_or(Verbosity::None), + } + } +} + +impl SolverOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn max_iter(mut self, max_iter: usize) -> Self { + self.max_iter = Some(max_iter); + self + } + + pub fn tol(mut self, tol: f64) -> Self { + self.tol = Some(tol); + self + } + + pub fn verbosity(mut self, verbosity: Verbosity) -> Self { + self.verbosity = verbosity; + self + } + + pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { + ( + self.max_iter.unwrap_or(max_iter), + self.tol.unwrap_or(tol), + self.verbosity, + ) + } +} + /// Possible contributions that can be computed. #[derive(Clone, Copy)] #[cfg_attr(feature = "python", pyo3::pyclass)] @@ -930,7 +1001,7 @@ where } } -// mod critical_point; +mod critical_point; #[cfg(test)] mod tests { diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 3d12446b4..97407c34a 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -1,6 +1,6 @@ use super::{Contributions, Derivative::*, PartialDerivative, State}; // use crate::equation_of_state::{EntropyScaling, MolarWeight, Residual}; -use crate::equation_of_state::Residual; +use crate::equation_of_state::{EntropyScaling, Residual}; use crate::errors::EosResult; use crate::EosUnit; use ndarray::{arr1, Array1, Array2}; @@ -312,96 +312,90 @@ impl State { } } -// /// # Transport properties -// /// -// /// These properties are available for equations of state -// /// that implement the [EntropyScaling] trait. -// impl State { -// /// Return the viscosity via entropy scaling. -// pub fn viscosity(&self) -> EosResult { -// let s = self -// .molar_entropy(Contributions::ResidualNvt) -// .to_reduced(SIUnit::reference_molar_entropy())?; -// Ok(self -// .eos -// .viscosity_reference(self.temperature, self.volume, &self.moles)? -// * self.eos.viscosity_correlation(s, &self.molefracs)?.exp()) -// } - -// /// Return the logarithm of the reduced viscosity. -// /// -// /// This term equals the viscosity correlation function -// /// that is used for entropy scaling. -// pub fn ln_viscosity_reduced(&self) -> EosResult { -// let s = self -// .molar_entropy(Contributions::ResidualNvt) -// .to_reduced(SIUnit::reference_molar_entropy())?; -// self.eos.viscosity_correlation(s, &self.molefracs) -// } - -// /// Return the viscosity reference as used in entropy scaling. -// pub fn viscosity_reference(&self) -> EosResult { -// self.eos -// .viscosity_reference(self.temperature, self.volume, &self.moles) -// } - -// /// Return the diffusion via entropy scaling. -// pub fn diffusion(&self) -> EosResult { -// let s = self -// .molar_entropy(Contributions::ResidualNvt) -// .to_reduced(SIUnit::reference_molar_entropy())?; -// Ok(self -// .eos -// .diffusion_reference(self.temperature, self.volume, &self.moles)? -// * self.eos.diffusion_correlation(s, &self.molefracs)?.exp()) -// } - -// /// Return the logarithm of the reduced diffusion. -// /// -// /// This term equals the diffusion correlation function -// /// that is used for entropy scaling. -// pub fn ln_diffusion_reduced(&self) -> EosResult { -// let s = self -// .molar_entropy(Contributions::ResidualNvt) -// .to_reduced(SIUnit::reference_molar_entropy())?; -// self.eos.diffusion_correlation(s, &self.molefracs) -// } - -// /// Return the diffusion reference as used in entropy scaling. -// pub fn diffusion_reference(&self) -> EosResult { -// self.eos -// .diffusion_reference(self.temperature, self.volume, &self.moles) -// } - -// /// Return the thermal conductivity via entropy scaling. -// pub fn thermal_conductivity(&self) -> EosResult { -// let s = self -// .molar_entropy(Contributions::ResidualNvt) -// .to_reduced(SIUnit::reference_molar_entropy())?; -// Ok(self -// .eos -// .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? -// * self -// .eos -// .thermal_conductivity_correlation(s, &self.molefracs)? -// .exp()) -// } - -// /// Return the logarithm of the reduced thermal conductivity. -// /// -// /// This term equals the thermal conductivity correlation function -// /// that is used for entropy scaling. -// pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { -// let s = self -// .molar_entropy(Contributions::ResidualNvt) -// .to_reduced(SIUnit::reference_molar_entropy())?; -// self.eos -// .thermal_conductivity_correlation(s, &self.molefracs) -// } - -// /// Return the thermal conductivity reference as used in entropy scaling. -// pub fn thermal_conductivity_reference(&self) -> EosResult { -// self.eos -// .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) -// } -// } +/// # Transport properties +/// +/// These properties are available for equations of state +/// that implement the [EntropyScaling] trait. +impl State { + /// Return the viscosity via entropy scaling. + pub fn viscosity(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .viscosity_reference(self.temperature, self.volume, &self.moles)? + * self.eos.viscosity_correlation(s, &self.molefracs)?.exp()) + } + + /// Return the logarithm of the reduced viscosity. + /// + /// This term equals the viscosity correlation function + /// that is used for entropy scaling. + pub fn ln_viscosity_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos.viscosity_correlation(s, &self.molefracs) + } + + /// Return the viscosity reference as used in entropy scaling. + pub fn viscosity_reference(&self) -> EosResult { + self.eos + .viscosity_reference(self.temperature, self.volume, &self.moles) + } + + /// Return the diffusion via entropy scaling. + pub fn diffusion(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .diffusion_reference(self.temperature, self.volume, &self.moles)? + * self.eos.diffusion_correlation(s, &self.molefracs)?.exp()) + } + + /// Return the logarithm of the reduced diffusion. + /// + /// This term equals the diffusion correlation function + /// that is used for entropy scaling. + pub fn ln_diffusion_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos.diffusion_correlation(s, &self.molefracs) + } + + /// Return the diffusion reference as used in entropy scaling. + pub fn diffusion_reference(&self) -> EosResult { + self.eos + .diffusion_reference(self.temperature, self.volume, &self.moles) + } + + /// Return the thermal conductivity via entropy scaling. + pub fn thermal_conductivity(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? + * self + .eos + .thermal_conductivity_correlation(s, &self.molefracs)? + .exp()) + } + + /// Return the logarithm of the reduced thermal conductivity. + /// + /// This term equals the thermal conductivity correlation function + /// that is used for entropy scaling. + pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos + .thermal_conductivity_correlation(s, &self.molefracs) + } + + /// Return the thermal conductivity reference as used in entropy scaling. + pub fn thermal_conductivity_reference(&self) -> EosResult { + self.eos + .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) + } +} diff --git a/feos-derive/src/lib.rs b/feos-derive/src/lib.rs index 667b93ec6..1f40a308c 100644 --- a/feos-derive/src/lib.rs +++ b/feos-derive/src/lib.rs @@ -2,12 +2,14 @@ //! FunctionalVariant enums in FeOs. The macros implement //! the boilerplate for the EquationOfState and HelmholtzEnergyFunctional traits. use dft::expand_helmholtz_energy_functional; -use eos::expand_equation_of_state; +use ideal_gas::expand_ideal_gas; +use residual::expand_residual; use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; mod dft; -mod eos; +mod ideal_gas; +mod residual; fn implement(name: &str, variant: &syn::Variant, opts: &[&'static str]) -> syn::Result { let syn::Variant { attrs, .. } = variant; @@ -43,10 +45,18 @@ fn implement(name: &str, variant: &syn::Variant, opts: &[&'static str]) -> syn:: implement } -#[proc_macro_derive(EquationOfState, attributes(implement))] -pub fn derive_equation_of_state(input: TokenStream) -> TokenStream { +#[proc_macro_derive(IdealGas, attributes(implement))] +pub fn derive_ideal_gas(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_equation_of_state(input) + expand_ideal_gas(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro_derive(Residual, attributes(implement))] +pub fn derive_residual(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand_residual(input) .unwrap_or_else(syn::Error::into_compile_error) .into() } diff --git a/src/eos.rs b/src/eos.rs index c93505db5..a78cd2124 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -12,18 +12,22 @@ use feos_core::cubic::PengRobinson; #[cfg(feature = "python")] use feos_core::python::user_defined::PyEoSObj; use feos_core::*; -use feos_derive::EquationOfState; +use feos_derive; +use feos_core::{DeBroglieWavelength, IdealGas, Residual}; +use feos_core::joback::Joback; use ndarray::Array1; use quantity::si::*; +use std::fmt; /// Collection of different [EquationOfState] implementations. /// /// Particularly relevant for situations in which generic types /// are undesirable (e.g. FFI). -#[derive(EquationOfState)] -pub enum EosVariant { +#[derive(feos_derive::Residual)] +pub enum ResidualModel { #[cfg(feature = "pcsaft")] - #[implement(entropy_scaling, molar_weight)] + // #[implement(entropy_scaling, molar_weight)] + #[implement(molar_weight)] PcSaft(PcSaft), #[cfg(feature = "gc_pcsaft")] #[implement(molar_weight)] @@ -32,7 +36,7 @@ pub enum EosVariant { PengRobinson(PengRobinson), #[cfg(feature = "python")] #[implement(molar_weight)] - Python(PyEoSObj), + Python(PyResidual), #[cfg(feature = "saftvrqmie")] #[implement(molar_weight)] SaftVRQMie(SaftVRQMie), @@ -42,3 +46,10 @@ pub enum EosVariant { #[cfg(feature = "uvtheory")] UVTheory(UVTheory), } + +#[derive(feos_derive::IdealGas)] +pub enum IdealGasModel { + Joback(Joback), + #[cfg(feature = "python")] + Python(PyIdealGas), +} diff --git a/src/lib.rs b/src/lib.rs index 24af2ff02..cdcc1bc23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ mod dft; #[cfg(feature = "dft")] pub use dft::FunctionalVariant; mod eos; -pub use eos::EosVariant; +// pub use eos::EosVariant; #[cfg(feature = "estimator")] pub mod estimator; diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index c11a7714a..4e626739c 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -4,29 +4,28 @@ use crate::hard_sphere::HardSphere; use feos_core::joback::Joback; use feos_core::parameter::Parameter; use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EquationOfState, HelmholtzEnergy, - IdealGasContribution, MolarWeight, State, + Contributions, + EosError, + EosResult, + EquationOfState, + HelmholtzEnergy, + MolarWeight, + Residual, //EntropyScaling + State, }; use ndarray::Array1; use quantity::si::*; use std::f64::consts::{FRAC_PI_6, PI}; +use std::fmt; use std::sync::Arc; pub(crate) mod dispersion; pub(crate) mod hard_chain; pub(crate) mod polar; -mod qspr; use dispersion::Dispersion; use hard_chain::HardChain; pub use polar::DQVariants; use polar::{Dipole, DipoleQuadrupole, Quadrupole}; -use qspr::QSPR; - -#[allow(clippy::upper_case_acronyms)] -enum IdealGasContributions { - QSPR(QSPR), - Joback(Joback), -} /// Customization options for the PC-SAFT equation of state and functional. #[derive(Copy, Clone)] @@ -53,7 +52,6 @@ pub struct PcSaft { parameters: Arc, options: PcSaftOptions, contributions: Vec>, - ideal_gas: IdealGasContributions, } impl PcSaft { @@ -95,21 +93,15 @@ impl PcSaft { ))); }; - let joback_records = parameters.joback_records.clone(); - Self { parameters: parameters.clone(), options, contributions, - ideal_gas: joback_records.map_or( - IdealGasContributions::QSPR(QSPR { parameters }), - |joback_records| IdealGasContributions::Joback(Joback::new(joback_records)), - ), } } } -impl EquationOfState for PcSaft { +impl Residual for PcSaft { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -127,15 +119,14 @@ impl EquationOfState for PcSaft { .sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } +} - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match &self.ideal_gas { - IdealGasContributions::QSPR(qspr) => qspr, - IdealGasContributions::Joback(joback) => joback, - } +impl fmt::Display for PcSaft { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PC-SAFT") } } @@ -174,168 +165,168 @@ fn chapman_enskog_thermal_conductivity( / KELVIN } -impl EntropyScaling for PcSaft { - fn viscosity_reference( - &self, - temperature: SINumber, - _: SINumber, - moles: &SIArray1, - ) -> EosResult { - let p = &self.parameters; - let mw = &p.molarweight; - let x = moles.to_reduced(moles.sum())?; - let ce: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 5.0 / 16.0 - * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) - .sqrt() - .unwrap() - / omega22(tr) - / (p.sigma[i] * ANGSTROM).powi(2) - }) - .collect(); - let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; - for i in 0..self.components() { - let denom: f64 = (0..self.components()) - .map(|j| { - x[j] * (1.0 - + (ce[i] / ce[j]).into_value().unwrap().sqrt() - * (mw[j] / mw[i]).powf(1.0 / 4.0)) - .powi(2) - / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() - }) - .sum(); - ce_mix += ce[i] * x[i] / denom - } - Ok(ce_mix) - } - - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - let coefficients = self - .parameters - .viscosity - .as_ref() - .expect("Missing viscosity coefficients."); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m) / m; - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - Ok(a + b * s + c * s.powi(2) + d * s.powi(3)) - } - - fn diffusion_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let density = moles.sum() / volume; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) - * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) - .sqrt() - .unwrap() - }) - .collect(); - Ok(res[0]) - } - - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .diffusion - .as_ref() - .expect("Missing diffusion coefficients."); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m).mapv(|v| v / m); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - let e: f64 = (&coefficients.row(4) * &pref).sum(); - Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8)) - } - - // Equation 4 of DOI: 10.1021/acs.iecr.9b04289 - fn thermal_conductivity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let mws = self.molar_weight(); - let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - let s_res_reduced = state - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(RGAS) - .unwrap() - / p.m[i]; - let ref_ce = chapman_enskog_thermal_conductivity( - temperature, - mws.get(i), - p.m[i], - p.sigma[i], - p.epsilon_k[i], - ); - let alpha_visc = (-s_res_reduced / -0.5).exp(); - let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2)) - * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i]) - * 1e-5 - * WATT - / METER - / KELVIN; - ref_ce + ref_ts * alpha_visc - }) - .collect(); - Ok(res[0]) - } - - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .thermal_conductivity - .as_ref() - .expect("Missing thermal conductivity coefficients"); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m).mapv(|v| v / m); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2)) - } -} +// impl EntropyScaling for PcSaft { +// fn viscosity_reference( +// &self, +// temperature: SINumber, +// _: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// let p = &self.parameters; +// let mw = &p.molarweight; +// let x = moles.to_reduced(moles.sum())?; +// let ce: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 5.0 / 16.0 +// * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) +// .sqrt() +// .unwrap() +// / omega22(tr) +// / (p.sigma[i] * ANGSTROM).powi(2) +// }) +// .collect(); +// let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; +// for i in 0..self.components() { +// let denom: f64 = (0..self.components()) +// .map(|j| { +// x[j] * (1.0 +// + (ce[i] / ce[j]).into_value().unwrap().sqrt() +// * (mw[j] / mw[i]).powf(1.0 / 4.0)) +// .powi(2) +// / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() +// }) +// .sum(); +// ce_mix += ce[i] * x[i] / denom +// } +// Ok(ce_mix) +// } + +// fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// let coefficients = self +// .parameters +// .viscosity +// .as_ref() +// .expect("Missing viscosity coefficients."); +// let m = (x * &self.parameters.m).sum(); +// let s = s_res / m; +// let pref = (x * &self.parameters.m) / m; +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * &pref).sum(); +// let c: f64 = (&coefficients.row(2) * &pref).sum(); +// let d: f64 = (&coefficients.row(3) * &pref).sum(); +// Ok(a + b * s + c * s.powi(2) + d * s.powi(3)) +// } + +// fn diffusion_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let density = moles.sum() / volume; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) +// * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) +// .sqrt() +// .unwrap() +// }) +// .collect(); +// Ok(res[0]) +// } + +// fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .diffusion +// .as_ref() +// .expect("Missing diffusion coefficients."); +// let m = (x * &self.parameters.m).sum(); +// let s = s_res / m; +// let pref = (x * &self.parameters.m).mapv(|v| v / m); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * &pref).sum(); +// let c: f64 = (&coefficients.row(2) * &pref).sum(); +// let d: f64 = (&coefficients.row(3) * &pref).sum(); +// let e: f64 = (&coefficients.row(4) * &pref).sum(); +// Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8)) +// } + +// // Equation 4 of DOI: 10.1021/acs.iecr.9b04289 +// fn thermal_conductivity_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let mws = self.molar_weight(); +// let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// let s_res_reduced = state +// .molar_entropy(Contributions::Residual) +// .to_reduced(RGAS) +// .unwrap() +// / p.m[i]; +// let ref_ce = chapman_enskog_thermal_conductivity( +// temperature, +// mws.get(i), +// p.m[i], +// p.sigma[i], +// p.epsilon_k[i], +// ); +// let alpha_visc = (-s_res_reduced / -0.5).exp(); +// let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2)) +// * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i]) +// * 1e-5 +// * WATT +// / METER +// / KELVIN; +// ref_ce + ref_ts * alpha_visc +// }) +// .collect(); +// Ok(res[0]) +// } + +// fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .thermal_conductivity +// .as_ref() +// .expect("Missing thermal conductivity coefficients"); +// let m = (x * &self.parameters.m).sum(); +// let s = s_res / m; +// let pref = (x * &self.parameters.m).mapv(|v| v / m); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * &pref).sum(); +// let c: f64 = (&coefficients.row(2) * &pref).sum(); +// let d: f64 = (&coefficients.row(3) * &pref).sum(); +// Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2)) +// } +// } #[cfg(test)] mod tests { @@ -358,7 +349,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); @@ -374,7 +365,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); @@ -450,19 +441,19 @@ mod tests { assert_relative_eq!(p, p_calc, epsilon = 1e-6); } - #[test] - fn vle_pure() { - let e = Arc::new(PcSaft::new(propane_parameters())); - let t = 300.0 * KELVIN; - let vle = PhaseEquilibrium::pure(&e, t, None, Default::default()); - if let Ok(v) = vle { - assert_relative_eq!( - v.vapor().pressure(Contributions::Total), - v.liquid().pressure(Contributions::Total), - epsilon = 1e-6 - ) - } - } + // #[test] + // fn vle_pure() { + // let e = Arc::new(PcSaft::new(propane_parameters())); + // let t = 300.0 * KELVIN; + // let vle = PhaseEquilibrium::pure(&e, t, None, Default::default()); + // if let Ok(v) = vle { + // assert_relative_eq!( + // v.vapor().pressure(Contributions::Total), + // v.liquid().pressure(Contributions::Total), + // epsilon = 1e-6 + // ) + // } + // } #[test] fn critical_point() { @@ -474,19 +465,19 @@ mod tests { } } - #[test] - fn speed_of_sound() { - let e = Arc::new(PcSaft::new(propane_parameters())); - let t = 300.0 * KELVIN; - let p = BAR; - let m = arr1(&[1.0]) * MOL; - let s = State::new_npt(&e, t, p, &m, DensityInitialization::None).unwrap(); - assert_relative_eq!( - s.speed_of_sound(), - 245.00185709137546 * METER / SECOND, - epsilon = 1e-4 - ) - } + // #[test] + // fn speed_of_sound() { + // let e = Arc::new(PcSaft::new(propane_parameters())); + // let t = 300.0 * KELVIN; + // let p = BAR; + // let m = arr1(&[1.0]) * MOL; + // let s = State::new_npt(&e, t, p, &m, DensityInitialization::None).unwrap(); + // assert_relative_eq!( + // s.speed_of_sound(), + // 245.00185709137546 * METER / SECOND, + // epsilon = 1e-4 + // ) + // } #[test] fn mix_single() { @@ -514,49 +505,49 @@ mod tests { ) } - #[test] - fn viscosity() -> EosResult<()> { - let e = Arc::new(PcSaft::new(propane_parameters())); - let t = 300.0 * KELVIN; - let p = BAR; - let n = arr1(&[1.0]) * MOL; - let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); - assert_relative_eq!( - s.viscosity()?, - 0.00797 * MILLI * PASCAL * SECOND, - epsilon = 1e-5 - ); - assert_relative_eq!( - s.ln_viscosity_reduced()?, - (s.viscosity()? / e.viscosity_reference(s.temperature, s.volume, &s.moles)?) - .into_value() - .unwrap() - .ln(), - epsilon = 1e-15 - ); - Ok(()) - } - - #[test] - fn diffusion() -> EosResult<()> { - let e = Arc::new(PcSaft::new(propane_parameters())); - let t = 300.0 * KELVIN; - let p = BAR; - let n = arr1(&[1.0]) * MOL; - let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); - assert_relative_eq!( - s.diffusion()?, - 0.01505 * (CENTI * METER).powi(2) / SECOND, - epsilon = 1e-5 - ); - assert_relative_eq!( - s.ln_diffusion_reduced()?, - (s.diffusion()? / e.diffusion_reference(s.temperature, s.volume, &s.moles)?) - .into_value() - .unwrap() - .ln(), - epsilon = 1e-15 - ); - Ok(()) - } + // #[test] + // fn viscosity() -> EosResult<()> { + // let e = Arc::new(PcSaft::new(propane_parameters())); + // let t = 300.0 * KELVIN; + // let p = BAR; + // let n = arr1(&[1.0]) * MOL; + // let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); + // assert_relative_eq!( + // s.viscosity()?, + // 0.00797 * MILLI * PASCAL * SECOND, + // epsilon = 1e-5 + // ); + // assert_relative_eq!( + // s.ln_viscosity_reduced()?, + // (s.viscosity()? / e.viscosity_reference(s.temperature, s.volume, &s.moles)?) + // .into_value() + // .unwrap() + // .ln(), + // epsilon = 1e-15 + // ); + // Ok(()) + // } + + // #[test] + // fn diffusion() -> EosResult<()> { + // let e = Arc::new(PcSaft::new(propane_parameters())); + // let t = 300.0 * KELVIN; + // let p = BAR; + // let n = arr1(&[1.0]) * MOL; + // let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); + // assert_relative_eq!( + // s.diffusion()?, + // 0.01505 * (CENTI * METER).powi(2) / SECOND, + // epsilon = 1e-5 + // ); + // assert_relative_eq!( + // s.ln_diffusion_reduced()?, + // (s.diffusion()? / e.diffusion_reference(s.temperature, s.volume, &s.moles)?) + // .into_value() + // .unwrap() + // .ln(), + // epsilon = 1e-15 + // ); + // Ok(()) + // } } diff --git a/src/pcsaft/eos/qspr.rs b/src/pcsaft/eos/qspr.rs deleted file mode 100644 index 844c21ce5..000000000 --- a/src/pcsaft/eos/qspr.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::PcSaftParameters; -use feos_core::IdealGasContributionDual; -use ndarray::Array1; -use num_dual::*; -use std::fmt; -use std::sync::Arc; - -const RGAS: f64 = 6.022140857 * 1.38064852; -const KB: f64 = 1.38064852e-23; -const T300: f64 = 300.0; -const T400: f64 = 400.0; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; - -// Heat capacity parameters @ T = 300 K (col 1) and T = 400 K (col 2) -const NA_NP_300: [f64; 6] = [ - -5763.04893, - 1232.30607, - -239.3513996, - 0.0, - 0.0, - -15174.28321, -]; -const NA_NP_400: [f64; 6] = [ - -8171.26676935062, - 1498.01217504596, - -315.515836223387, - 0.0, - 0.0, - -19389.5468655708, -]; -const NA_P_300: [f64; 6] = [ - 5177.19095226181, - 919.565206504576, - -108.829105648889, - 0.0, - -3.93917830677682, - -13504.5671858292, -]; -const NA_P_400: [f64; 6] = [ - 10656.1018362315, - 1146.10782703748, - -131.023645998081, - 0.0, - -9.93789225413177, - -24430.12952497, -]; -const AP_300: [f64; 6] = [ - 3600.32322462175, - 1006.20461224949, - -151.688378113974, - 7.81876773647109e-07, - 8.01001754473385, - -8959.37140957179, -]; -const AP_400: [f64; 6] = [ - 7248.0697641199, - 1267.44346171358, - -208.738557800023, - 0.000170238690157906, - -6.7841792685616, - -12669.4196622924, -]; - -#[allow(clippy::upper_case_acronyms)] -pub struct QSPR { - pub parameters: Arc, -} - -impl + Copy> IdealGasContributionDual for QSPR { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { - let (c_300, c_400) = if self.parameters.association.is_empty() { - match self.parameters.ndipole + self.parameters.nquadpole { - 0 => (NA_NP_300, NA_NP_400), - _ => (NA_P_300, NA_P_400), - } - } else { - (AP_300, AP_400) - }; - - Array1::from_shape_fn(components, |i| { - let epsilon_kt = temperature.recip() * self.parameters.epsilon_k[i]; - let sigma3 = self.parameters.sigma[i].powi(3); - - let p1 = epsilon_kt * self.parameters.m[i]; - let p2 = sigma3 * self.parameters.m[i]; - let p3 = epsilon_kt * p2; - let p4 = self.parameters.pure_records[i] - .model_record - .association_record - .as_ref() - .map_or(D::zero(), |a| { - (temperature.recip() * a.epsilon_k_ab).exp_m1() * p2 * sigma3 * a.kappa_ab - }); - let p5 = p2 * self.parameters.q[i]; - let p6 = 1.0; - - let icpc300 = (p1 * c_300[0] / T300 - + p2 * c_300[1] - + p3 * c_300[2] / T300 - + p4 * c_300[3] / T300 - + p5 * c_300[4] - + p6 * c_300[5]) - * 0.001; - let icpc400 = (p1 * c_400[0] / T400 - + p2 * c_400[1] - + p3 * c_400[2] / T400 - + p4 * c_400[3] / T400 - + p5 * c_400[4] - + p6 * c_400[5]) - * 0.001; - - // linear approximation - let b = (icpc400 - icpc300) / (T400 - T300); - let a = icpc300 - b * T300; - - // integration - let k = a * (temperature - T0 - temperature * (temperature / T0).ln()) - - b * (temperature - T0).powi(2) * 0.5; - - // de Broglie wavelength - k / (temperature * RGAS) + (temperature * KB / (P0 * A3)).ln() - }) - } -} - -impl fmt::Display for QSPR { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (QSPR)") - } -} diff --git a/src/pcsaft/parameters.rs b/src/pcsaft/parameters.rs index 892cbee6b..097617a5a 100644 --- a/src/pcsaft/parameters.rs +++ b/src/pcsaft/parameters.rs @@ -1,7 +1,6 @@ use crate::association::{AssociationParameters, AssociationRecord}; use crate::hard_sphere::{HardSphereProperties, MonomerShape}; use conv::ValueInto; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ FromSegments, FromSegmentsBinary, Parameter, ParameterError, PureRecord, }; @@ -314,19 +313,17 @@ pub struct PcSaftParameters { pub viscosity: Option>, pub diffusion: Option>, pub thermal_conductivity: Option>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, - pub joback_records: Option>, } impl Parameter for PcSaftParameters { type Pure = PcSaftRecord; - type IdealGas = JobackRecord; type Binary = PcSaftBinaryRecord; fn from_records( - pure_records: Vec>, - binary_records: Array2, + pure_records: Vec>, + binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -422,11 +419,6 @@ impl Parameter for PcSaftParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, m, @@ -450,16 +442,10 @@ impl Parameter for PcSaftParameters { thermal_conductivity: thermal_conductivity_coefficients, pure_records, binary_records, - joback_records, } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } @@ -519,7 +505,6 @@ impl PcSaftParameters { #[cfg(test)] pub mod utils { use super::*; - use feos_core::joback::JobackRecord; use feos_core::parameter::{BinaryRecord, ChemicalRecord, SegmentRecord}; use std::sync::Arc; @@ -544,7 +529,7 @@ pub mod utils { }, "molarweight": 44.0962 }"#; - let propane_record: PureRecord = + let propane_record: PureRecord = serde_json::from_str(propane_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_pure(propane_record)) } @@ -568,7 +553,7 @@ pub mod utils { "q": 4.4 } }"#; - let co2_record: PureRecord = + let co2_record: PureRecord = serde_json::from_str(co2_json).expect("Unable to parse json."); PcSaftParameters::new_pure(co2_record) } @@ -591,7 +576,7 @@ pub mod utils { }, "molarweight": 58.123 }"#; - let butane_record: PureRecord = + let butane_record: PureRecord = serde_json::from_str(butane_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_pure(butane_record)) } @@ -615,7 +600,7 @@ pub mod utils { }, "molarweight": 46.0688 }"#; - let dme_record: PureRecord = + let dme_record: PureRecord = serde_json::from_str(dme_json).expect("Unable to parse json."); PcSaftParameters::new_pure(dme_record) } @@ -642,7 +627,7 @@ pub mod utils { }, "molarweight": 18.0152 }"#; - let water_record: PureRecord = + let water_record: PureRecord = serde_json::from_str(water_json).expect("Unable to parse json."); PcSaftParameters::new_pure(water_record) } @@ -684,7 +669,7 @@ pub mod utils { } } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); PcSaftParameters::new_binary(binary_record, None) } @@ -729,7 +714,7 @@ pub mod utils { "molarweight": 58.123 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_binary(binary_record, None)) } From a8cb08974f9e3e5b2ba6f1d998f3d7e59e066c96 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Fri, 2 Jun 2023 16:49:15 +0200 Subject: [PATCH 08/47] Adjusted python interface, currently panics for residual properties without ideal gas contribution in eos --- examples/core_user_defined_eos.ipynb | 59 ++++- examples/pcsaft_state.ipynb | 116 ++++++++- feos-core/src/equation_of_state/mod.rs | 53 +++- feos-core/src/lib.rs | 7 +- feos-core/src/python/cubic.rs | 6 +- feos-core/src/python/mod.rs | 2 +- feos-core/src/python/parameter.rs | 40 +-- feos-core/src/python/state.rs | 106 ++++---- feos-core/src/python/user_defined.rs | 109 ++++++++- feos-core/src/state/properties.rs | 6 + src/eos.rs | 10 +- src/pcsaft/eos/mod.rs | 324 ++++++++++++------------- src/pcsaft/python.rs | 5 +- src/python/cubic.rs | 1 - src/python/eos.rs | 117 ++++++--- src/python/ideal_gas.rs | 8 + src/python/mod.rs | 4 + 17 files changed, 641 insertions(+), 332 deletions(-) create mode 100644 src/python/ideal_gas.rs diff --git a/examples/core_user_defined_eos.ipynb b/examples/core_user_defined_eos.ipynb index 2c67ffc0e..1d8fe99ca 100644 --- a/examples/core_user_defined_eos.ipynb +++ b/examples/core_user_defined_eos.ipynb @@ -80,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -207,7 +207,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -218,9 +218,16 @@ "\n", "# create an instance of our python class and hand it over to rust\n", "pr = PyPengRobinson(tc, pc, omega, molar_weight)\n", - "eos = EquationOfState.python(pr)" + "eos = EquationOfState.python_residual(pr)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -234,7 +241,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -309,7 +316,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -321,7 +328,7 @@ "1.6605390671738466e-24 mol" ] }, - "execution_count": 23, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -334,7 +341,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -346,7 +353,7 @@ "1 mol" ] }, - "execution_count": 24, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -370,14 +377,40 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "thread '' panicked at 'No ideal gas model initialized!', src/eos.rs:" + ] + }, + { + "ename": "PanicException", + "evalue": "No ideal gas model initialized!", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mPanicException\u001b[0m Traceback (most recent call last)", + "Input \u001b[0;32mIn [9]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0m s_ph \u001b[38;5;241m=\u001b[39m \u001b[43mState\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[43meos\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mpressure\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mBAR\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mmolar_enthalpy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43ms_pt\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmolar_enthalpy\u001b[49m\u001b[43m(\u001b[49m\u001b[43mContributions\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mResidual\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m)\u001b[49m\n", + "\u001b[0;31mPanicException\u001b[0m: No ideal gas model initialized!" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "49:10\n" + ] + } + ], "source": [ "s_ph = State(\n", " eos, \n", " pressure=1*BAR, \n", - " molar_enthalpy=s_pt.molar_enthalpy()\n", + " molar_enthalpy=s_pt.molar_enthalpy(Contributions.Residual)\n", ")" ] }, @@ -1248,7 +1281,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1262,7 +1295,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/examples/pcsaft_state.ipynb b/examples/pcsaft_state.ipynb index 1bcc42050..4a631e857 100644 --- a/examples/pcsaft_state.ipynb +++ b/examples/pcsaft_state.ipynb @@ -99,6 +99,118 @@ "state_nvt" ] }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(array([-1.52564778]), array([-1.52564778]))" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "state_nvt.chemical_potential(Contributions.ResidualNvt) / RGAS / state_nvt.temperature - np.log(state_nvt.compressibility()), state_nvt.ln_phi()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0.07682387846270378] K^-1" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beta = 1 / (RGAS * state_nvt.temperature)\n", + "a1 = - state_nvt.chemical_potential(Contributions.ResidualNvt) * beta / state_nvt.temperature\n", + "a2 = beta * state_nvt.dmu_dt(Contributions.ResidualNvt)\n", + "a3 = 1 / state_nvt.temperature\n", + "a4 = - state_nvt.dp_dt() / state_nvt.pressure()\n", + "a1 + a2 + a3 #+ a4" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.022509944563452417 K^-1,\n", + " [0.05098226639966782] K^-1,\n", + " 3.331667499583542e-3 K^-1,\n", + " -6.476301239431868 K^-1)" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a1, a2, a3, a4" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "([0.0001330105610385462] m³/mol, [0.0001330105610385462] m³/mol)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state_nvt.partial_molar_volume(), -state_nvt.dp_dni() / state_nvt.dp_dv()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-3.69618646800518 nPa,\n", + " 100.37302562357485 kPa,\n", + " 100.37302562357485 kPa,\n", + " 18.76231432566827 MPa)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state_nvt.pressure(Contributions.ResidualNpt), state_nvt.pressure(Contributions.ResidualNvt) + state_nvt.pressure(Contributions.IdealGas), state_nvt.pressure(Contributions.Total), state_nvt.pressure(Contributions.IdealGas)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -800,7 +912,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -814,7 +926,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.9.12" }, "vscode": { "interpreter": { diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 62a566a2f..080e050d9 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -1,18 +1,18 @@ use ndarray::Array1; use num_dual::DualNum; -use quantity::si::{SIArray1, MOL}; +use quantity::si::{SIArray1, SINumber}; use std::{fmt::Display, sync::Arc}; pub mod debroglie; pub mod helmholtz_energy; pub mod ideal_gas; pub mod residual; -use crate::StateHD; +use crate::{EosResult, StateHD}; -pub use ideal_gas::IdealGas; -pub use residual::{EntropyScaling, Residual}; pub use self::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; +pub use ideal_gas::IdealGas; +pub use residual::{EntropyScaling, Residual}; /// Molar weight of all components. /// @@ -93,8 +93,47 @@ impl Residual for EquationOfState { } } -impl EquationOfState { - pub fn molar_weight(&self) -> Array1 { - self.residual.molar_weight().to_reduced(MOL).unwrap() +impl MolarWeight for EquationOfState { + fn molar_weight(&self) -> SIArray1 { + self.residual.molar_weight() + } +} + +impl EntropyScaling for EquationOfState { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .viscosity_reference(temperature, volume, moles) + } + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.viscosity_correlation(s_res, x) + } + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .diffusion_reference(temperature, volume, moles) + } + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.diffusion_correlation(s_res, x) + } + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .thermal_conductivity_reference(temperature, volume, moles) + } + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.thermal_conductivity_correlation(s_res, x) } } diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 063ff50ff..d602a71f7 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -36,15 +36,16 @@ pub mod parameter; // mod phase_equilibria; mod state; pub use equation_of_state::{ - DeBroglieWavelength, EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, - IdealGas, MolarWeight, Residual, + DeBroglieWavelength, DeBroglieWavelengthDual, EntropyScaling, EquationOfState, HelmholtzEnergy, + HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, }; pub use errors::{EosError, EosResult}; // pub use phase_equilibria::{ // PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, // }; pub use state::{ - Contributions, DensityInitialization, Derivative, State, StateBuilder, StateHD, StateVec, + Contributions, DensityInitialization, Derivative, SolverOptions, State, StateBuilder, StateHD, + StateVec, Verbosity, }; #[cfg(feature = "python")] diff --git a/feos-core/src/python/cubic.rs b/feos-core/src/python/cubic.rs index 80a82b3f7..f2b2b8cae 100644 --- a/feos-core/src/python/cubic.rs +++ b/feos-core/src/python/cubic.rs @@ -1,9 +1,7 @@ use crate::cubic::{PengRobinsonParameters, PengRobinsonRecord}; -use crate::joback::JobackRecord; use crate::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, }; -use crate::python::joback::PyJobackRecord; use crate::python::parameter::PyIdentifier; use crate::*; use ndarray::Array2; @@ -34,9 +32,7 @@ impl_json_handling!(PyPengRobinsonRecord); impl_pure_record!( PengRobinsonRecord, - PyPengRobinsonRecord, - JobackRecord, - PyJobackRecord + PyPengRobinsonRecord ); impl_binary_record!(); diff --git a/feos-core/src/python/mod.rs b/feos-core/src/python/mod.rs index 0df76142f..283df3dd0 100644 --- a/feos-core/src/python/mod.rs +++ b/feos-core/src/python/mod.rs @@ -6,7 +6,7 @@ pub mod cubic; mod equation_of_state; pub mod joback; pub mod parameter; -mod phase_equilibria; +// mod phase_equilibria; mod state; pub mod user_defined; diff --git a/feos-core/src/python/parameter.rs b/feos-core/src/python/parameter.rs index d0a55ad2a..9c0e80e94 100644 --- a/feos-core/src/python/parameter.rs +++ b/feos-core/src/python/parameter.rs @@ -370,7 +370,7 @@ impl_json_handling!(PyBinarySegmentRecord); #[macro_export] macro_rules! impl_pure_record { - ($model_record:ident, $py_model_record:ident, $ideal_gas_record:ident, $py_ideal_gas_record:ident) => { + ($model_record:ident, $py_model_record:ident) => { /// All information required to characterize a pure component. /// /// Parameters @@ -381,16 +381,14 @@ macro_rules! impl_pure_record { /// The molar weight (in g/mol) of the pure component. /// model_record : ModelRecord /// The pure component model parameters. - /// ideal_gas_record: IdealGasRecord, optional - /// The pure component parameters for the ideal gas model. /// /// Returns /// ------- /// PureRecord #[pyclass(name = "PureRecord")] - #[pyo3(text_signature = "(identifier, molarweight, model_record, ideal_gas_record=None)")] + #[pyo3(text_signature = "(identifier, molarweight, model_record)")] #[derive(Clone)] - pub struct PyPureRecord(pub PureRecord<$model_record, $ideal_gas_record>); + pub struct PyPureRecord(pub PureRecord<$model_record>); #[pymethods] impl PyPureRecord { @@ -399,13 +397,11 @@ macro_rules! impl_pure_record { identifier: PyIdentifier, molarweight: f64, model_record: $py_model_record, - ideal_gas_record: Option<$py_ideal_gas_record>, ) -> PyResult { Ok(Self(PureRecord::new( identifier.0, molarweight, model_record.0, - ideal_gas_record.map(|ig| ig.0), ))) } @@ -439,16 +435,6 @@ macro_rules! impl_pure_record { self.0.model_record = model_record.0; } - #[getter] - fn get_ideal_gas_record(&self) -> Option<$py_ideal_gas_record> { - self.0.ideal_gas_record.clone().map($py_ideal_gas_record) - } - - #[setter] - fn set_ideal_gas_record(&mut self, ideal_gas_record: $py_ideal_gas_record) { - self.0.ideal_gas_record = Some(ideal_gas_record.0); - } - fn __repr__(&self) -> PyResult { Ok(self.0.to_string()) } @@ -460,7 +446,7 @@ macro_rules! impl_pure_record { #[macro_export] macro_rules! impl_segment_record { - ($model_record:ident, $py_model_record:ident, $ideal_gas_record:ident, $py_ideal_gas_record:ident) => { + ($model_record:ident, $py_model_record:ident) => { /// All information required to characterize a single segment. /// /// Parameters @@ -471,16 +457,14 @@ macro_rules! impl_segment_record { /// The molar weight (in g/mol) of the segment. /// model_record : ModelRecord /// The segment model parameters. - /// ideal_gas_record: IdealGasRecord, optional - /// The segment ideal gas parameters. /// /// Returns /// ------- /// SegmentRecord #[pyclass(name = "SegmentRecord")] - #[pyo3(text_signature = "(identifier, molarweight, model_record, ideal_gas_record=None)")] + #[pyo3(text_signature = "(identifier, molarweight)")] #[derive(Clone)] - pub struct PySegmentRecord(SegmentRecord<$model_record, $ideal_gas_record>); + pub struct PySegmentRecord(SegmentRecord<$model_record>); #[pymethods] impl PySegmentRecord { @@ -489,13 +473,11 @@ macro_rules! impl_segment_record { identifier: String, molarweight: f64, model_record: $py_model_record, - ideal_gas_record: Option<$py_ideal_gas_record>, ) -> PyResult { Ok(Self(SegmentRecord::new( identifier, molarweight, model_record.0, - ideal_gas_record.map(|ig| ig.0), ))) } @@ -547,16 +529,6 @@ macro_rules! impl_segment_record { self.0.model_record = model_record.0; } - #[getter] - fn get_ideal_gas_record(&self) -> Option<$py_ideal_gas_record> { - self.0.ideal_gas_record.clone().map($py_ideal_gas_record) - } - - #[setter] - fn set_ideal_gas_record(&mut self, ideal_gas_record: $py_ideal_gas_record) { - self.0.ideal_gas_record = Some(ideal_gas_record.0); - } - fn __repr__(&self) -> PyResult { Ok(self.0.to_string()) } diff --git a/feos-core/src/python/state.rs b/feos-core/src/python/state.rs index 9e0045cea..79566e322 100644 --- a/feos-core/src/python/state.rs +++ b/feos-core/src/python/state.rs @@ -90,7 +90,7 @@ macro_rules! impl_state { } else { Ok(DensityInitialization::None) }; - let s = State::new( + let s = State::new_full( &eos.0, temperature.map(|t| t.into()), volume.map(|t| t.into()), @@ -263,58 +263,58 @@ macro_rules! impl_state { Ok((PyState(state1), PyState(state2))) } - /// Performs a stability analysis and returns a list of stable - /// candidate states. - /// - /// Parameters - /// ---------- - /// max_iter : int, optional - /// The maximum number of iterations. - /// tol: float, optional - /// The solution tolerance. - /// verbosity : Verbosity, optional - /// The verbosity. - /// - /// Returns - /// ------- - /// State - #[pyo3(text_signature = "(max_iter=None, tol=None, verbosity=None)")] - fn stability_analysis(&self, - max_iter: Option, - tol: Option, - verbosity: Option, - ) -> PyResult> { - Ok(self - .0 - .stability_analysis((max_iter, tol, verbosity).into())? - .into_iter() - .map(Self) - .collect()) - } - - /// Performs a stability analysis and returns whether the state - /// is stable - /// - /// Parameters - /// ---------- - /// max_iter : int, optional - /// The maximum number of iterations. - /// tol: float, optional - /// The solution tolerance. - /// verbosity : Verbosity, optional - /// The verbosity. - /// - /// Returns - /// ------- - /// bool - #[pyo3(text_signature = "(max_iter=None, tol=None, verbosity=None)")] - fn is_stable(&self, - max_iter: Option, - tol: Option, - verbosity: Option, - ) -> PyResult { - Ok(self.0.is_stable((max_iter, tol, verbosity).into())?) - } + // /// Performs a stability analysis and returns a list of stable + // /// candidate states. + // /// + // /// Parameters + // /// ---------- + // /// max_iter : int, optional + // /// The maximum number of iterations. + // /// tol: float, optional + // /// The solution tolerance. + // /// verbosity : Verbosity, optional + // /// The verbosity. + // /// + // /// Returns + // /// ------- + // /// State + // #[pyo3(text_signature = "(max_iter=None, tol=None, verbosity=None)")] + // fn stability_analysis(&self, + // max_iter: Option, + // tol: Option, + // verbosity: Option, + // ) -> PyResult> { + // Ok(self + // .0 + // .stability_analysis((max_iter, tol, verbosity).into())? + // .into_iter() + // .map(Self) + // .collect()) + // } + + // /// Performs a stability analysis and returns whether the state + // /// is stable + // /// + // /// Parameters + // /// ---------- + // /// max_iter : int, optional + // /// The maximum number of iterations. + // /// tol: float, optional + // /// The solution tolerance. + // /// verbosity : Verbosity, optional + // /// The verbosity. + // /// + // /// Returns + // /// ------- + // /// bool + // #[pyo3(text_signature = "(max_iter=None, tol=None, verbosity=None)")] + // fn is_stable(&self, + // max_iter: Option, + // tol: Option, + // verbosity: Option, + // ) -> PyResult { + // Ok(self.0.is_stable((max_iter, tol, verbosity).into())?) + // } /// Return pressure. /// diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index ace9713ac..1391f95ae 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -1,8 +1,11 @@ -use crate::{EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, MolarWeight, StateHD}; -use ndarray::Array1; +use crate::{ + DeBroglieWavelength, DeBroglieWavelengthDual, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, + MolarWeight, Residual, StateHD, +}; +use ndarray::{arr1, Array1}; use num_dual::*; use numpy::convert::IntoPyArray; -use numpy::{PyArray, PyReadonlyArrayDyn}; +use numpy::{PyArray, PyReadonlyArray1, PyReadonlyArrayDyn}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use quantity::python::PySIArray1; @@ -10,14 +13,73 @@ use quantity::si::SIArray1; use std::fmt; struct PyHelmholtzEnergy(Py); +struct PyDeBroglieWavelength(Py); +impl fmt::Display for PyDeBroglieWavelength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Python de Broglie") + } +} + +pub struct PyIdealGas { + obj: Py, + de_broglie: Box, +} + +impl fmt::Display for PyIdealGas { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Python ideal gas") + } +} + +impl PyIdealGas { + pub fn new(obj: Py) -> PyResult { + Python::with_gil(|py| { + let attr = obj.as_ref(py).hasattr("subset")?; + if !attr { + panic!("Python Class has to have a method 'subset' with signature:\n\tdef subset(self, component_list: List[int]) -> Self") + } + let attr = obj.as_ref(py).hasattr("ideal_gas_model")?; + if !attr { + panic!("{}", "Python Class has to have a method 'ideal_gas_model' with signature:\n\tdef ideal_gas_model(self, state: StateHD) -> HD\nwhere 'HD' has to be any of {{float, Dual64, HyperDual64, HyperDualDual64, Dual3Dual64, Dual3_64}}.") + } + Ok(Self { + obj: obj.clone(), + de_broglie: Box::new(PyDeBroglieWavelength(obj)), + }) + }) + } +} + +impl IdealGas for PyIdealGas { + fn subset(&self, component_list: &[usize]) -> Self { + Python::with_gil(|py| { + let py_result = self + .obj + .as_ref(py) + .call_method1("subset", (component_list.to_vec(),)) + .unwrap(); + Self::new(py_result.extract().unwrap()).unwrap() + }) + } + + fn ideal_gas_model(&self) -> &Box { + &self.de_broglie + } +} /// Struct containing pointer to Python Class that implements Helmholtz energy. -pub struct PyEoSObj { +pub struct PyResidual { obj: Py, contributions: Vec>, } -impl PyEoSObj { +impl fmt::Display for PyResidual { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Python residual") + } +} + +impl PyResidual { pub fn new(obj: Py) -> PyResult { Python::with_gil(|py| { let attr = obj.as_ref(py).hasattr("components")?; @@ -48,7 +110,7 @@ impl PyEoSObj { } } -impl MolarWeight for PyEoSObj { +impl MolarWeight for PyResidual { fn molar_weight(&self) -> SIArray1 { Python::with_gil(|py| { let py_result = self.obj.as_ref(py).call_method0("molar_weight").unwrap(); @@ -63,7 +125,7 @@ impl MolarWeight for PyEoSObj { } } -impl EquationOfState for PyEoSObj { +impl Residual for PyResidual { fn components(&self) -> usize { Python::with_gil(|py| { let py_result = self.obj.as_ref(py).call_method0("components").unwrap(); @@ -99,7 +161,7 @@ impl EquationOfState for PyEoSObj { }) } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -192,17 +254,48 @@ macro_rules! helmholtz_energy { }; } +macro_rules! de_broglie_wavelength { + ($py_hd_id:ident, $hd_ty:ty) => { + impl DeBroglieWavelengthDual<$hd_ty> for PyDeBroglieWavelength { + fn de_broglie_wavelength(&self, temperature: $hd_ty) -> Array1<$hd_ty> { + Python::with_gil(|py| { + let py_result = self + .0 + .as_ref(py) + .call_method1("ideal_gas_model", (<$py_hd_id>::from(temperature),)) + .unwrap(); + let rr = if let Ok(r) = py_result.extract::>() { + dbg!("Array1"); + r.to_owned_array() + .mapv(|ri| <$hd_ty>::from(ri.extract::<$py_hd_id>(py).unwrap())) + } else if let Ok(r) = py_result.extract::>() { + dbg!("Array0"); + assert!(r.ndim() == 0); + let scalar = &r.to_owned_array()[0]; + arr1(&[<$hd_ty>::from(scalar.extract::<$py_hd_id>(py).unwrap())]) + } else { + panic!("ideal_gas_model: input data type must be numpy ndarray of dimension 1") + }; + rr + }) + } + } + }; +} + macro_rules! impl_dual_state_helmholtz_energy { ($py_state_id:ident, $py_hd_id:ident, $hd_ty:ty, $py_field_ty:ty) => { dual_number!($py_hd_id, $hd_ty, $py_field_ty); state!($py_state_id, $py_hd_id, $hd_ty); helmholtz_energy!($py_state_id, $py_hd_id, $hd_ty); + de_broglie_wavelength!($py_hd_id, $hd_ty); }; } // No definition of dual number necessary for f64 state!(PyStateF, f64, f64); helmholtz_energy!(PyStateF, f64, f64); +de_broglie_wavelength!(f64, f64); impl_dual_state_helmholtz_energy!(PyStateD, PyDual64, Dual64, f64); dual_number!(PyDualVec3, DualSVec64<3>, f64); diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index 330c0b928..c6c898bc2 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -296,6 +296,12 @@ impl State { self.gibbs_energy(contributions) / self.total_moles } + /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ + pub fn partial_molar_volume(&self, contributions: Contributions) -> SIArray1 { + let func = |s: &Self, evaluate: Evaluate| -s.dp_dni_(evaluate) / s.dp_dv_(evaluate); + self.evaluate_property(func, contributions, false) + } + /// Partial molar entropy: $s_i=\left(\frac{\partial S}{\partial N_i}\right)_{T,p,N_j}$ pub fn partial_molar_entropy(&self, contributions: Contributions) -> SIArray1 { let func = |s: &Self, evaluate: Evaluate| { diff --git a/src/eos.rs b/src/eos.rs index a78cd2124..b7f192421 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -9,12 +9,12 @@ use crate::saftvrqmie::SaftVRQMie; #[cfg(feature = "uvtheory")] use crate::uvtheory::UVTheory; use feos_core::cubic::PengRobinson; +use feos_core::joback::Joback; #[cfg(feature = "python")] -use feos_core::python::user_defined::PyEoSObj; +use feos_core::python::user_defined::{PyIdealGas, PyResidual}; use feos_core::*; -use feos_derive; use feos_core::{DeBroglieWavelength, IdealGas, Residual}; -use feos_core::joback::Joback; +use feos_derive; use ndarray::Array1; use quantity::si::*; use std::fmt; @@ -26,8 +26,7 @@ use std::fmt; #[derive(feos_derive::Residual)] pub enum ResidualModel { #[cfg(feature = "pcsaft")] - // #[implement(entropy_scaling, molar_weight)] - #[implement(molar_weight)] + #[implement(entropy_scaling, molar_weight)] PcSaft(PcSaft), #[cfg(feature = "gc_pcsaft")] #[implement(molar_weight)] @@ -49,6 +48,7 @@ pub enum ResidualModel { #[derive(feos_derive::IdealGas)] pub enum IdealGasModel { + NoModel, Joback(Joback), #[cfg(feature = "python")] Python(PyIdealGas), diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index 4e626739c..90136c2ce 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -5,6 +5,7 @@ use feos_core::joback::Joback; use feos_core::parameter::Parameter; use feos_core::{ Contributions, + EntropyScaling, EosError, EosResult, EquationOfState, @@ -165,168 +166,167 @@ fn chapman_enskog_thermal_conductivity( / KELVIN } -// impl EntropyScaling for PcSaft { -// fn viscosity_reference( -// &self, -// temperature: SINumber, -// _: SINumber, -// moles: &SIArray1, -// ) -> EosResult { -// let p = &self.parameters; -// let mw = &p.molarweight; -// let x = moles.to_reduced(moles.sum())?; -// let ce: Array1 = (0..self.components()) -// .map(|i| { -// let tr = (temperature / p.epsilon_k[i] / KELVIN) -// .into_value() -// .unwrap(); -// 5.0 / 16.0 -// * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) -// .sqrt() -// .unwrap() -// / omega22(tr) -// / (p.sigma[i] * ANGSTROM).powi(2) -// }) -// .collect(); -// let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; -// for i in 0..self.components() { -// let denom: f64 = (0..self.components()) -// .map(|j| { -// x[j] * (1.0 -// + (ce[i] / ce[j]).into_value().unwrap().sqrt() -// * (mw[j] / mw[i]).powf(1.0 / 4.0)) -// .powi(2) -// / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() -// }) -// .sum(); -// ce_mix += ce[i] * x[i] / denom -// } -// Ok(ce_mix) -// } - -// fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { -// let coefficients = self -// .parameters -// .viscosity -// .as_ref() -// .expect("Missing viscosity coefficients."); -// let m = (x * &self.parameters.m).sum(); -// let s = s_res / m; -// let pref = (x * &self.parameters.m) / m; -// let a: f64 = (&coefficients.row(0) * x).sum(); -// let b: f64 = (&coefficients.row(1) * &pref).sum(); -// let c: f64 = (&coefficients.row(2) * &pref).sum(); -// let d: f64 = (&coefficients.row(3) * &pref).sum(); -// Ok(a + b * s + c * s.powi(2) + d * s.powi(3)) -// } - -// fn diffusion_reference( -// &self, -// temperature: SINumber, -// volume: SINumber, -// moles: &SIArray1, -// ) -> EosResult { -// if self.components() != 1 { -// return Err(EosError::IncompatibleComponents(self.components(), 1)); -// } -// let p = &self.parameters; -// let density = moles.sum() / volume; -// let res: Array1 = (0..self.components()) -// .map(|i| { -// let tr = (temperature / p.epsilon_k[i] / KELVIN) -// .into_value() -// .unwrap(); -// 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) -// * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) -// .sqrt() -// .unwrap() -// }) -// .collect(); -// Ok(res[0]) -// } - -// fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { -// if self.components() != 1 { -// return Err(EosError::IncompatibleComponents(self.components(), 1)); -// } -// let coefficients = self -// .parameters -// .diffusion -// .as_ref() -// .expect("Missing diffusion coefficients."); -// let m = (x * &self.parameters.m).sum(); -// let s = s_res / m; -// let pref = (x * &self.parameters.m).mapv(|v| v / m); -// let a: f64 = (&coefficients.row(0) * x).sum(); -// let b: f64 = (&coefficients.row(1) * &pref).sum(); -// let c: f64 = (&coefficients.row(2) * &pref).sum(); -// let d: f64 = (&coefficients.row(3) * &pref).sum(); -// let e: f64 = (&coefficients.row(4) * &pref).sum(); -// Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8)) -// } - -// // Equation 4 of DOI: 10.1021/acs.iecr.9b04289 -// fn thermal_conductivity_reference( -// &self, -// temperature: SINumber, -// volume: SINumber, -// moles: &SIArray1, -// ) -> EosResult { -// if self.components() != 1 { -// return Err(EosError::IncompatibleComponents(self.components(), 1)); -// } -// let p = &self.parameters; -// let mws = self.molar_weight(); -// let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?; -// let res: Array1 = (0..self.components()) -// .map(|i| { -// let tr = (temperature / p.epsilon_k[i] / KELVIN) -// .into_value() -// .unwrap(); -// let s_res_reduced = state -// .molar_entropy(Contributions::Residual) -// .to_reduced(RGAS) -// .unwrap() -// / p.m[i]; -// let ref_ce = chapman_enskog_thermal_conductivity( -// temperature, -// mws.get(i), -// p.m[i], -// p.sigma[i], -// p.epsilon_k[i], -// ); -// let alpha_visc = (-s_res_reduced / -0.5).exp(); -// let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2)) -// * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i]) -// * 1e-5 -// * WATT -// / METER -// / KELVIN; -// ref_ce + ref_ts * alpha_visc -// }) -// .collect(); -// Ok(res[0]) -// } - -// fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { -// if self.components() != 1 { -// return Err(EosError::IncompatibleComponents(self.components(), 1)); -// } -// let coefficients = self -// .parameters -// .thermal_conductivity -// .as_ref() -// .expect("Missing thermal conductivity coefficients"); -// let m = (x * &self.parameters.m).sum(); -// let s = s_res / m; -// let pref = (x * &self.parameters.m).mapv(|v| v / m); -// let a: f64 = (&coefficients.row(0) * x).sum(); -// let b: f64 = (&coefficients.row(1) * &pref).sum(); -// let c: f64 = (&coefficients.row(2) * &pref).sum(); -// let d: f64 = (&coefficients.row(3) * &pref).sum(); -// Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2)) -// } -// } +impl EntropyScaling for PcSaft { + fn viscosity_reference( + &self, + temperature: SINumber, + _: SINumber, + moles: &SIArray1, + ) -> EosResult { + let p = &self.parameters; + let mw = &p.molarweight; + let x = moles.to_reduced(moles.sum())?; + let ce: Array1 = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN) + .into_value() + .unwrap(); + 5.0 / 16.0 + * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) + .sqrt() + .unwrap() + / omega22(tr) + / (p.sigma[i] * ANGSTROM).powi(2) + }) + .collect(); + let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; + for i in 0..self.components() { + let denom: f64 = (0..self.components()) + .map(|j| { + x[j] * (1.0 + + (ce[i] / ce[j]).into_value().unwrap().sqrt() + * (mw[j] / mw[i]).powf(1.0 / 4.0)) + .powi(2) + / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() + }) + .sum(); + ce_mix += ce[i] * x[i] / denom + } + Ok(ce_mix) + } + + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + let coefficients = self + .parameters + .viscosity + .as_ref() + .expect("Missing viscosity coefficients."); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m) / m; + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + Ok(a + b * s + c * s.powi(2) + d * s.powi(3)) + } + + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let p = &self.parameters; + let density = moles.sum() / volume; + let res: Array1 = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN) + .into_value() + .unwrap(); + 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) + * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) + .sqrt() + .unwrap() + }) + .collect(); + Ok(res[0]) + } + + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let coefficients = self + .parameters + .diffusion + .as_ref() + .expect("Missing diffusion coefficients."); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m).mapv(|v| v / m); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + let e: f64 = (&coefficients.row(4) * &pref).sum(); + Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8)) + } + + // Equation 4 of DOI: 10.1021/acs.iecr.9b04289 + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let p = &self.parameters; + let mws = self.molar_weight(); + let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?; + let res: Array1 = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN) + .into_value() + .unwrap(); + let s_res_reduced = (state.residual_entropy() / state.total_moles) + .to_reduced(RGAS) + .unwrap() + / p.m[i]; + let ref_ce = chapman_enskog_thermal_conductivity( + temperature, + mws.get(i), + p.m[i], + p.sigma[i], + p.epsilon_k[i], + ); + let alpha_visc = (-s_res_reduced / -0.5).exp(); + let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2)) + * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i]) + * 1e-5 + * WATT + / METER + / KELVIN; + ref_ce + ref_ts * alpha_visc + }) + .collect(); + Ok(res[0]) + } + + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let coefficients = self + .parameters + .thermal_conductivity + .as_ref() + .expect("Missing thermal conductivity coefficients"); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m).mapv(|v| v / m); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2)) + } +} #[cfg(test)] mod tests { diff --git a/src/pcsaft/python.rs b/src/pcsaft/python.rs index 077e4d7e6..13a3d44c6 100644 --- a/src/pcsaft/python.rs +++ b/src/pcsaft/python.rs @@ -130,8 +130,8 @@ impl PyPcSaftRecord { impl_json_handling!(PyPcSaftRecord); -impl_pure_record!(PcSaftRecord, PyPcSaftRecord, JobackRecord, PyJobackRecord); -impl_segment_record!(PcSaftRecord, PyPcSaftRecord, JobackRecord, PyJobackRecord); +impl_pure_record!(PcSaftRecord, PyPcSaftRecord); +impl_segment_record!(PcSaftRecord, PyPcSaftRecord); #[pyclass(name = "PcSaftBinaryRecord")] #[pyo3( @@ -186,7 +186,6 @@ pub fn pcsaft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/cubic.rs b/src/python/cubic.rs index adcced3c3..9edc46568 100644 --- a/src/python/cubic.rs +++ b/src/python/cubic.rs @@ -7,7 +7,6 @@ use pyo3::prelude::*; pub fn cubic(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/eos.rs b/src/python/eos.rs index 94d1fc8a2..0ca700322 100644 --- a/src/python/eos.rs +++ b/src/python/eos.rs @@ -1,4 +1,4 @@ -use crate::eos::EosVariant; +use crate::eos::{IdealGasModel, ResidualModel}; #[cfg(feature = "estimator")] use crate::estimator::*; #[cfg(feature = "gc_pcsaft")] @@ -27,9 +27,11 @@ use crate::uvtheory::python::PyUVParameters; use crate::uvtheory::{Perturbation, UVTheory, UVTheoryOptions, VirialOrder}; use feos_core::cubic::PengRobinson; +use feos_core::joback::Joback; use feos_core::python::cubic::PyPengRobinsonParameters; -use feos_core::python::user_defined::PyEoSObj; use feos_core::*; +use feos_core::python::joback::PyJobackRecord; +use feos_core::python::user_defined::{PyResidual, PyIdealGas}; use numpy::convert::ToPyArray; use numpy::{PyArray1, PyArray2}; use pyo3::exceptions::{PyIndexError, PyValueError}; @@ -44,10 +46,10 @@ use std::sync::Arc; /// Collection of equations of state. #[pyclass(name = "EquationOfState")] #[derive(Clone)] -pub struct PyEosVariant(pub Arc); +pub struct PyEquationOfState(pub Arc>); #[pymethods] -impl PyEosVariant { +impl PyEquationOfState { /// PC-SAFT equation of state. /// /// Parameters @@ -87,10 +89,12 @@ impl PyEosVariant { tol_cross_assoc, dq_variant, }; - Self(Arc::new(EosVariant::PcSaft(PcSaft::with_options( + let residual = Arc::new(ResidualModel::PcSaft(PcSaft::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// (heterosegmented) group contribution PC-SAFT equation of state. @@ -128,10 +132,12 @@ impl PyEosVariant { max_iter_cross_assoc, tol_cross_assoc, }; - Self(Arc::new(EosVariant::GcPcSaft(GcPcSaft::with_options( + let residual = Arc::new(ResidualModel::GcPcSaft(GcPcSaft::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// Peng-Robinson equation of state. @@ -148,25 +154,27 @@ impl PyEosVariant { /// states. #[staticmethod] pub fn peng_robinson(parameters: PyPengRobinsonParameters) -> Self { - Self(Arc::new(EosVariant::PengRobinson(PengRobinson::new( - parameters.0, - )))) + let residual = Arc::new(ResidualModel::PengRobinson(PengRobinson::new(parameters.0))); + let ideal_gas = Arc::new(IdealGasModel::NoModel); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } - /// Equation of state from a Python class. + /// Residual Helmholtz energy model from a Python class. /// /// Parameters /// ---------- - /// obj : Class + /// residual : Class /// A python class implementing the necessary methods - /// to be used as equation of state. + /// to be used as residual equation of state. /// /// Returns /// ------- /// EquationOfState #[staticmethod] - fn python(obj: Py) -> PyResult { - Ok(Self(Arc::new(EosVariant::Python(PyEoSObj::new(obj)?)))) + fn python_residual(residual: Py) -> PyResult { + let residual = Arc::new(ResidualModel::Python(PyResidual::new(residual)?)); + let ideal_gas = Arc::new(IdealGasModel::NoModel); + Ok(Self(Arc::new(EquationOfState::new(ideal_gas, residual)))) } /// PeTS equation of state. @@ -188,10 +196,12 @@ impl PyEosVariant { #[pyo3(signature = (parameters, max_eta=0.5), text_signature = "(parameters, max_eta=0.5)")] fn pets(parameters: PyPetsParameters, max_eta: f64) -> Self { let options = PetsOptions { max_eta }; - Self(Arc::new(EosVariant::Pets(Pets::with_options( + let residual = Arc::new(ResidualModel::Pets(Pets::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// UV-Theory equation of state. @@ -230,9 +240,12 @@ impl PyEosVariant { perturbation, virial_order, }; - Ok(Self(Arc::new(EosVariant::UVTheory( - UVTheory::with_options(parameters.0, options)?, - )))) + let residual = Arc::new(ResidualModel::UVTheory(UVTheory::with_options( + parameters.0, + options, + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// SAFT-VRQ Mie equation of state. @@ -271,36 +284,70 @@ impl PyEosVariant { fh_order, inc_nonadd_term, }; - Self(Arc::new(EosVariant::SaftVRQMie(SaftVRQMie::with_options( + let residual = Arc::new(ResidualModel::SaftVRQMie(SaftVRQMie::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) + } + + /// Ideal gas equation of state from a Python class. + /// + /// Parameters + /// ---------- + /// ideal_gas : Class, optional + /// A python class implementing the necessary methods + /// to be used as a ideal gas model. + /// + /// Returns + /// ------- + /// EquationOfState + fn python_ideal_gas(&self, ideal_gas: Py) -> PyResult { + let ig = Arc::new(IdealGasModel::Python(PyIdealGas::new(ideal_gas)?)); + Ok(Self(Arc::new(EquationOfState::new(ig, self.0.residual.clone())))) + } + + /// Ideal gas equation of state from a Python class. + /// + /// Parameters + /// ---------- + /// ideal_gas : Class, optional + /// A python class implementing the necessary methods + /// to be used as a ideal gas model. + /// + /// Returns + /// ------- + /// EquationOfState + fn joback(&self, parameters: Vec) -> Self { + let records = Arc::new(parameters.iter().map(|p| p.0.clone()).collect()); + let ideal_gas = Arc::new(IdealGasModel::Joback(Joback::new(records))); + Self(Arc::new(EquationOfState::new(ideal_gas, self.0.residual.clone()))) } } -impl_equation_of_state!(PyEosVariant); -impl_virial_coefficients!(PyEosVariant); -impl_state!(EosVariant, PyEosVariant); -impl_state_molarweight!(EosVariant, PyEosVariant); -#[cfg(feature = "pcsaft")] -impl_state_entropy_scaling!(EosVariant, PyEosVariant); -impl_phase_equilibrium!(EosVariant, PyEosVariant); +impl_equation_of_state!(PyEquationOfState); +impl_virial_coefficients!(PyEquationOfState); +impl_state!(EquationOfState, PyEquationOfState); +impl_state_molarweight!(EquationOfState, PyEquationOfState); +impl_state_entropy_scaling!(EquationOfState, PyEquationOfState); +// impl_phase_equilibrium!(IdealGasModel, ResidualModel, PyEquationOfState); #[cfg(feature = "estimator")] -impl_estimator!(EosVariant, PyEosVariant); +impl_estimator!(EquationOfState, PyEquationOfState); #[cfg(all(feature = "estimator", feature = "pcsaft"))] -impl_estimator_entropy_scaling!(EosVariant, PyEosVariant); +impl_estimator_entropy_scaling!(EquationOfState, PyEquationOfState); #[pymodule] pub fn eos(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + // m.add_class::()?; + // m.add_class::()?; #[cfg(feature = "estimator")] m.add_wrapped(wrap_pymodule!(estimator_eos))?; diff --git a/src/python/ideal_gas.rs b/src/python/ideal_gas.rs new file mode 100644 index 000000000..d071e04e9 --- /dev/null +++ b/src/python/ideal_gas.rs @@ -0,0 +1,8 @@ +use feos_core::python::joback::PyJobackRecord; +use feos_core::python::parameter::*; +use pyo3::prelude::*; + +#[pymodule] +pub fn ideal_gas(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::() +} diff --git a/src/python/mod.rs b/src/python/mod.rs index ec6e9adb0..7bf974cc8 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -13,8 +13,10 @@ use pyo3::prelude::*; use pyo3::wrap_pymodule; use quantity::python::quantity as quantity_module; +mod ideal_gas; mod cubic; mod eos; +use ideal_gas::ideal_gas as ideal_gas_module; use cubic::cubic as cubic_module; use eos::eos as eos_module; @@ -31,6 +33,7 @@ pub fn feos(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(eos_module))?; #[cfg(feature = "dft")] m.add_wrapped(wrap_pymodule!(dft_module))?; + m.add_wrapped(wrap_pymodule!(ideal_gas_module))?; m.add_wrapped(wrap_pymodule!(cubic_module))?; #[cfg(feature = "pcsaft")] m.add_wrapped(wrap_pymodule!(pcsaft_module))?; @@ -51,6 +54,7 @@ pub fn feos(py: Python<'_>, m: &PyModule) -> PyResult<()> { set_path(py, m, "feos.dft", "dft")?; #[cfg(all(feature = "dft", feature = "estimator"))] set_path(py, m, "feos.dft.estimator", "dft.estimator_dft")?; + set_path(py, m, "feos.ideal_gas", "ideal_gas")?; set_path(py, m, "feos.cubic", "cubic")?; #[cfg(feature = "pcsaft")] set_path(py, m, "feos.pcsaft", "pcsaft")?; From 0668a63b0c91f796d8197f335bcb7f1a4767aadb Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 2 Jun 2023 21:00:28 +0200 Subject: [PATCH 09/47] Phase equilibria routine; moved dmu_dni to residual_properties; UNTESTED --- feos-core/src/lib.rs | 10 +- feos-core/src/phase_equilibria/bubble_dew.rs | 80 +++++---- feos-core/src/phase_equilibria/mod.rs | 164 +++++++++--------- .../phase_equilibria/phase_diagram_binary.rs | 85 +++++---- .../phase_equilibria/phase_diagram_pure.rs | 10 +- .../src/phase_equilibria/phase_envelope.rs | 8 +- .../phase_equilibria/stability_analysis.rs | 8 +- feos-core/src/phase_equilibria/tp_flash.rs | 12 +- feos-core/src/phase_equilibria/vle_pure.rs | 52 +++--- feos-core/src/state/properties.rs | 14 +- feos-core/src/state/residual_properties.rs | 30 +++- 11 files changed, 260 insertions(+), 213 deletions(-) diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index d602a71f7..869745ee7 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -33,16 +33,14 @@ mod equation_of_state; mod errors; pub mod joback; pub mod parameter; -// mod phase_equilibria; +mod phase_equilibria; mod state; pub use equation_of_state::{ DeBroglieWavelength, DeBroglieWavelengthDual, EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, }; pub use errors::{EosError, EosResult}; -// pub use phase_equilibria::{ -// PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, -// }; +pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium}; pub use state::{ Contributions, DensityInitialization, Derivative, SolverOptions, State, StateBuilder, StateHD, StateVec, Verbosity, @@ -127,12 +125,10 @@ impl EosUnit for SIUnit { #[cfg(test)] mod tests { use crate::cubic::*; - use crate::equation_of_state::ideal_gas; use crate::equation_of_state::EquationOfState; use crate::joback::Joback; use crate::joback::JobackRecord; use crate::parameter::*; - use crate::state::State; use crate::Contributions; use crate::EosResult; use crate::StateBuilder; @@ -336,7 +332,7 @@ mod tests { ); assert_relative_eq!( s.dmu_dni(Contributions::Residual), - sr.dmu_res_dni(), + sr.dmu_dni(Contributions::Residual), max_relative = 1e-15 ); assert_relative_eq!( diff --git a/feos-core/src/phase_equilibria/bubble_dew.rs b/feos-core/src/phase_equilibria/bubble_dew.rs index e64920c8e..e5b499a45 100644 --- a/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/feos-core/src/phase_equilibria/bubble_dew.rs @@ -1,10 +1,10 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{ Contributions, DensityInitialization::{InitialDensity, Liquid, Vapor}, - State, StateBuilder, TPSpec, + SolverOptions, State, StateBuilder, TPSpec, Verbosity, }; use crate::EosUnit; use ndarray::*; @@ -55,7 +55,7 @@ where } /// # Bubble and dew point calculations -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate a phase equilibrium for a given temperature /// or pressure and composition of the liquid phase. pub fn bubble_point( @@ -225,9 +225,9 @@ impl PhaseEquilibrium { let m = liquid_molefracs * SIUnit::reference_moles(); let density = 0.75 * eos.max_density(Some(&m))?; let liquid = State::new_nvt(eos, temperature, m.sum() / density, &m)?; - let v_l = liquid.partial_molar_volume(Contributions::Total); + let v_l = liquid.partial_molar_volume(); let p_l = liquid.pressure(Contributions::Total); - let mu_l = liquid.chemical_potential(Contributions::ResidualNvt); + let mu_l = liquid.residual_chemical_potential(); let p_i = (temperature * density * SIUnit::gas_constant() * liquid_molefracs) * (mu_l - p_l * v_l) .to_reduced(SIUnit::gas_constant() * temperature)? @@ -251,9 +251,9 @@ impl PhaseEquilibrium { let m = x * SIUnit::reference_moles(); let density = 0.75 * eos.max_density(Some(&m))?; let liquid = State::new_nvt(eos, temperature, m.sum() / density, &m)?; - let v_l = liquid.partial_molar_volume(Contributions::Total); + let v_l = liquid.partial_molar_volume(); let p_l = liquid.pressure(Contributions::Total); - let mu_l = liquid.chemical_potential(Contributions::ResidualNvt); + let mu_l = liquid.residual_chemical_potential(); let k = vapor_molefracs / (mu_l - p_l * v_l) .to_reduced(SIUnit::gas_constant() * temperature)? @@ -287,7 +287,7 @@ impl PhaseEquilibrium { } } -fn starting_x2_bubble( +fn starting_x2_bubble( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -315,7 +315,7 @@ fn starting_x2_bubble( Ok([liquid_state, vapor_state]) } -fn starting_x2_dew( +fn starting_x2_dew( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -353,7 +353,7 @@ fn starting_x2_dew( Ok([vapor_state, liquid_state]) } -fn bubble_dew( +fn bubble_dew( tp_spec: TPSpec, mut var_tp: TPSpec, mut state1: State, @@ -441,7 +441,7 @@ where } } -fn adjust_t_p( +fn adjust_t_p( var: &mut TPSpec, state1: &mut State, state2: &mut State, @@ -509,7 +509,7 @@ where Ok(f.abs()) } -fn adjust_states( +fn adjust_states( var: &TPSpec, state1: &mut State, state2: &mut State, @@ -536,7 +536,7 @@ fn adjust_states( Ok(()) } -fn adjust_x2( +fn adjust_x2( state1: &State, state2: &mut State, verbosity: Verbosity, @@ -558,7 +558,7 @@ fn adjust_x2( Ok(err_out) } -fn newton_step( +fn newton_step( tp_spec: TPSpec, var: &mut TPSpec, state1: &mut State, @@ -574,7 +574,7 @@ where } } -fn newton_step_t( +fn newton_step_t( pressure: &mut TPSpec, state1: &mut State, state2: &mut State, @@ -593,11 +593,11 @@ where .dot(&state1.molefracs); let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_1 = state1 - .chemical_potential(Contributions::Total) + let mu_1_res = state1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_2 = state2 - .chemical_potential(Contributions::Total) + let mu_2_res = state2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_1 = state1 .pressure(Contributions::Total) @@ -607,7 +607,12 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual - let res = concatenate![Axis(0), mu_1 - &mu_2, arr1(&[p_1 - p_2])]; + let dmu_ig = (SIUnit::gas_constant() * state1.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&state1.partial_density / &state2.partial_density) + .into_value()? + .mapv(f64::ln); + let res = concatenate![Axis(0), mu_1_res - mu_2_res + dmu_ig, arr1(&[p_1 - p_2])]; let error = norm(&res); // calculate Jacobian @@ -651,7 +656,7 @@ where Ok(error) } -fn newton_step_p( +fn newton_step_p( pressure: SINumber, temperature: &mut TPSpec, state1: &mut State, @@ -666,12 +671,8 @@ where .dot(&state1.molefracs); let dmu_drho_2 = (state2.dmu_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let dmu_dt_1 = state1 - .dmu_dt(Contributions::Total) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_2 = state2 - .dmu_dt(Contributions::Total) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; + let dmu_res_dt_1 = state1.dmu_res_dt().to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_2 = state2.dmu_res_dt().to_reduced(SIUnit::gas_constant())?; let dp_drho_1 = (state1.dp_dni(Contributions::Total) * state1.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())? .dot(&state1.molefracs); @@ -683,11 +684,11 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_1 = state1 - .chemical_potential(Contributions::Total) + let mu_1_res = state1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_2 = state2 - .chemical_potential(Contributions::Total) + let mu_2_res = state2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_1 = state1 .pressure(Contributions::Total) @@ -698,7 +699,18 @@ where let p = pressure.to_reduced(SIUnit::reference_pressure())?; // calculate residual - let res = concatenate![Axis(0), mu_1 - &mu_2, arr1(&[p_1 - p]), arr1(&[p_2 - p])]; + let delta_dmu_ig_dt = (&state1.partial_density / &state2.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_mu_ig = (SIUnit::gas_constant() * state1.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_dmu_ig_dt; + let res = concatenate![ + Axis(0), + mu_1_res - mu_2_res + delta_mu_ig, + arr1(&[p_1 - p]), + arr1(&[p_2 - p]) + ]; let error = norm(&res); // calculate Jacobian @@ -717,7 +729,7 @@ where ], concatenate![ Axis(0), - (dmu_dt_1 - dmu_dt_2).insert_axis(Axis(1)), + (dmu_res_dt_1 - dmu_res_dt_2 + delta_dmu_ig_dt).insert_axis(Axis(1)), arr2(&[[dp_dt_1], [dp_dt_2]]) ] ]; diff --git a/feos-core/src/phase_equilibria/mod.rs b/feos-core/src/phase_equilibria/mod.rs index 7c020e352..b17a25337 100644 --- a/feos-core/src/phase_equilibria/mod.rs +++ b/feos-core/src/phase_equilibria/mod.rs @@ -1,6 +1,6 @@ -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State}; +use crate::state::{DensityInitialization, State}; use crate::EosUnit; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::fmt; @@ -17,76 +17,76 @@ mod vle_pure; pub use phase_diagram_binary::PhaseDiagramHetero; pub use phase_diagram_pure::PhaseDiagram; -/// Level of detail in the iteration output. -#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] -#[cfg_attr(feature = "python", pyo3::pyclass)] -pub enum Verbosity { - /// Do not print output. - None, - /// Print information about the success of failure of the iteration. - Result, - /// Print a detailed outpur for every iteration. - Iter, -} +// /// Level of detail in the iteration output. +// #[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] +// #[cfg_attr(feature = "python", pyo3::pyclass)] +// pub enum Verbosity { +// /// Do not print output. +// None, +// /// Print information about the success of failure of the iteration. +// Result, +// /// Print a detailed outpur for every iteration. +// Iter, +// } -impl Default for Verbosity { - fn default() -> Self { - Self::None - } -} +// impl Default for Verbosity { +// fn default() -> Self { +// Self::None +// } +// } -/// Options for the various phase equilibria solvers. -/// -/// If the values are [None], solver specific default -/// values are used. -#[derive(Copy, Clone, Default)] -pub struct SolverOptions { - /// Maximum number of iterations. - pub max_iter: Option, - /// Tolerance. - pub tol: Option, - /// Iteration outpput indicated by the [Verbosity] enum. - pub verbosity: Verbosity, -} +// /// Options for the various phase equilibria solvers. +// /// +// /// If the values are [None], solver specific default +// /// values are used. +// #[derive(Copy, Clone, Default)] +// pub struct SolverOptions { +// /// Maximum number of iterations. +// pub max_iter: Option, +// /// Tolerance. +// pub tol: Option, +// /// Iteration outpput indicated by the [Verbosity] enum. +// pub verbosity: Verbosity, +// } -impl From<(Option, Option, Option)> for SolverOptions { - fn from(options: (Option, Option, Option)) -> Self { - Self { - max_iter: options.0, - tol: options.1, - verbosity: options.2.unwrap_or(Verbosity::None), - } - } -} +// impl From<(Option, Option, Option)> for SolverOptions { +// fn from(options: (Option, Option, Option)) -> Self { +// Self { +// max_iter: options.0, +// tol: options.1, +// verbosity: options.2.unwrap_or(Verbosity::None), +// } +// } +// } -impl SolverOptions { - pub fn new() -> Self { - Self::default() - } +// impl SolverOptions { +// pub fn new() -> Self { +// Self::default() +// } - pub fn max_iter(mut self, max_iter: usize) -> Self { - self.max_iter = Some(max_iter); - self - } +// pub fn max_iter(mut self, max_iter: usize) -> Self { +// self.max_iter = Some(max_iter); +// self +// } - pub fn tol(mut self, tol: f64) -> Self { - self.tol = Some(tol); - self - } +// pub fn tol(mut self, tol: f64) -> Self { +// self.tol = Some(tol); +// self +// } - pub fn verbosity(mut self, verbosity: Verbosity) -> Self { - self.verbosity = verbosity; - self - } +// pub fn verbosity(mut self, verbosity: Verbosity) -> Self { +// self.verbosity = verbosity; +// self +// } - pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { - ( - self.max_iter.unwrap_or(max_iter), - self.tol.unwrap_or(tol), - self.verbosity, - ) - } -} +// pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { +// ( +// self.max_iter.unwrap_or(max_iter), +// self.tol.unwrap_or(tol), +// self.verbosity, +// ) +// } +// } /// A thermodynamic equilibrium state. /// @@ -109,11 +109,10 @@ impl Clone for PhaseEquilibrium { } } -impl fmt::Display for PhaseEquilibrium +impl fmt::Display for PhaseEquilibrium where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (i, s) in self.0.iter().enumerate() { @@ -123,11 +122,10 @@ where } } -impl PhaseEquilibrium +impl PhaseEquilibrium where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { pub fn _repr_markdown_(&self) -> String { if self.0[0].eos.components() == 1 { @@ -161,7 +159,7 @@ where } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub fn vapor(&self) -> &State { &self.0[0] } @@ -171,7 +169,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub fn vapor(&self) -> &State { &self.0[0] } @@ -185,7 +183,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn from_states(state1: State, state2: State) -> Self { let (vapor, liquid) = if state1.density < state2.density { (state1, state2) @@ -226,7 +224,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn update_pressure( mut self, temperature: SINumber, @@ -261,18 +259,24 @@ impl PhaseEquilibrium { Ok(()) } - pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { - for s in self.0.iter_mut() { - s.update_chemical_potential(chemical_potential)?; - } - Ok(()) - } + // pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { + // for s in self.0.iter_mut() { + // s.update_chemical_potential(chemical_potential)?; + // } + // Ok(()) + // } + // Total Gibbs energy excluding the constant contribution RT sum_i N_i ln(\Lambda_i^3) pub(super) fn total_gibbs_energy(&self) -> SINumber { self.0 .iter() .fold(0.0 * SIUnit::reference_energy(), |acc, s| { - acc + s.gibbs_energy(Contributions::Total) + let ln_rho = s + .partial_density + .to_reduced(SIUnit::reference_density()) + .unwrap(); + acc + s.residual_gibbs_energy() + + SIUnit::gas_constant() * s.temperature * (s.moles.clone() * ln_rho).sum() }) } } @@ -280,7 +284,7 @@ impl PhaseEquilibrium { const TRIVIAL_REL_DEVIATION: f64 = 1e-5; /// # Utility functions -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn check_trivial_solution(self) -> EosResult { if Self::is_trivial_solution(self.vapor(), self.liquid()) { Err(EosError::TrivialSolution) diff --git a/feos-core/src/phase_equilibria/phase_diagram_binary.rs b/feos-core/src/phase_equilibria/phase_diagram_binary.rs index 746fc5da0..ca53e25e0 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_binary.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_binary.rs @@ -1,7 +1,9 @@ -use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::{PhaseDiagram, PhaseEquilibrium}; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State, StateBuilder, TPSpec}; +use crate::state::{ + Contributions, DensityInitialization, SolverOptions, State, StateBuilder, TPSpec, +}; use crate::EosUnit; use ndarray::{arr1, arr2, concatenate, s, Array1, Array2, Axis}; use num_dual::linalg::{norm, LU}; @@ -11,7 +13,7 @@ use std::sync::Arc; const DEFAULT_POINTS: usize = 51; -impl PhaseDiagram { +impl PhaseDiagram { /// Create a new binary phase diagram exhibiting a /// vapor/liquid equilibrium. /// @@ -171,7 +173,7 @@ impl PhaseDiagram { } } -fn iterate_vle( +fn iterate_vle( eos: &Arc, tp: TPSpec, x_lim: &[f64], @@ -231,7 +233,7 @@ where vle_vec } -impl State { +impl State { fn tp(&self, tp: TPSpec) -> SINumber { match tp { TPSpec::Temperature(_) => self.pressure(Contributions::Total), @@ -247,7 +249,7 @@ pub struct PhaseDiagramHetero { pub lle: Option>, } -impl PhaseDiagram { +impl PhaseDiagram { /// Create a new binary phase diagram exhibiting a /// vapor/liquid/liquid equilibrium. /// @@ -346,7 +348,7 @@ const MAX_ITER_HETERO: usize = 50; const TOL_HETERO: f64 = 1e-8; /// # Heteroazeotropes -impl PhaseEquilibrium +impl PhaseEquilibrium where SINumber: std::fmt::Display + std::fmt::LowerExp, { @@ -421,14 +423,14 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; let dp_drho_v = (v.dp_dni(Contributions::Total) * v.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_l1 = l1 - .chemical_potential(Contributions::Total) + let mu_l1_res = l1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_l2 = l2 - .chemical_potential(Contributions::Total) + let mu_l2_res = l2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_v = v - .chemical_potential(Contributions::Total) + let mu_v_res = v + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_l1 = l1 .pressure(Contributions::Total) @@ -441,10 +443,20 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual + let delta_l1v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&l1.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l2v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&l2.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); let res = concatenate![ Axis(0), - mu_l1 - &mu_v, - mu_l2 - &mu_v, + mu_l1_res - &mu_v_res + delta_l1v_mu_ig, + mu_l2_res - &mu_v_res + delta_l2v_mu_ig, arr1(&[p_l1 - p_v]), arr1(&[p_l2 - p_v]) ]; @@ -555,12 +567,9 @@ where .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; let dmu_drho_v = (v.dmu_dni(Contributions::Total) * v.volume) .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let dmu_dt_l1 = (l1.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_l2 = (l2.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_v = (v.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; + let dmu_res_dt_l1 = (l1.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_l2 = (l2.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_v = (v.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; let dp_drho_l1 = (l1.dp_dni(Contributions::Total) * l1.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; let dp_drho_l2 = (l2.dp_dni(Contributions::Total) * l2.volume) @@ -573,14 +582,14 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; let dp_dt_v = (v.dp_dt(Contributions::Total)) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; - let mu_l1 = l1 - .chemical_potential(Contributions::Total) + let mu_l1_res = l1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_l2 = l2 - .chemical_potential(Contributions::Total) + let mu_l2_res = l2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_v = v - .chemical_potential(Contributions::Total) + let mu_v_res = v + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_l1 = l1 .pressure(Contributions::Total) @@ -593,10 +602,22 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual + let delta_l1v_dmu_ig_dt = (&l1.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l2v_dmu_ig_dt = (&l2.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l1v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_l1v_dmu_ig_dt; + let delta_l2v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_l2v_dmu_ig_dt; let res = concatenate![ Axis(0), - mu_l1 - &mu_v, - mu_l2 - &mu_v, + mu_l1_res - &mu_v_res + delta_l1v_mu_ig, + mu_l2_res - &mu_v_res + delta_l2v_mu_ig, arr1(&[p_l1 - p]), arr1(&[p_l2 - p]), arr1(&[p_v - p]) @@ -636,8 +657,8 @@ where ], concatenate![ Axis(0), - (dmu_dt_l1 - &dmu_dt_v).insert_axis(Axis(1)), - (dmu_dt_l2 - &dmu_dt_v).insert_axis(Axis(1)), + (dmu_res_dt_l1 - &dmu_res_dt_v + delta_l1v_dmu_ig_dt).insert_axis(Axis(1)), + (dmu_res_dt_l2 - &dmu_res_dt_v + delta_l2v_dmu_ig_dt).insert_axis(Axis(1)), arr2(&[[dp_dt_l1]]), arr2(&[[dp_dt_l2]]), arr2(&[[dp_dt_v]]) diff --git a/feos-core/src/phase_equilibria/phase_diagram_pure.rs b/feos-core/src/phase_equilibria/phase_diagram_pure.rs index c7b823fed..dfb689505 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_pure.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_pure.rs @@ -1,7 +1,7 @@ -use super::{PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::EosResult; -use crate::state::{State, StateVec}; +use crate::state::{SolverOptions, State, StateVec}; #[cfg(feature = "rayon")] use crate::EosUnit; #[cfg(feature = "rayon")] @@ -33,7 +33,7 @@ impl PhaseDiagram { } } -impl PhaseDiagram { +impl PhaseDiagram { /// Calculate a phase diagram for a pure component. pub fn pure( eos: &Arc, @@ -74,7 +74,7 @@ impl PhaseDiagram { } #[cfg(feature = "rayon")] -impl PhaseDiagram { +impl PhaseDiagram { fn solve_temperatures( eos: &Arc, temperatures: ArrayView1, diff --git a/feos-core/src/phase_equilibria/phase_envelope.rs b/feos-core/src/phase_equilibria/phase_envelope.rs index f05ed3afe..2c56122e2 100644 --- a/feos-core/src/phase_equilibria/phase_envelope.rs +++ b/feos-core/src/phase_equilibria/phase_envelope.rs @@ -1,12 +1,12 @@ -use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::{PhaseDiagram, PhaseEquilibrium}; +use crate::equation_of_state::Residual; use crate::errors::EosResult; -use crate::state::State; +use crate::state::{SolverOptions, State}; use crate::Contributions; use quantity::si::{SIArray1, SINumber}; use std::sync::Arc; -impl PhaseDiagram { +impl PhaseDiagram { /// Calculate the bubble point line of a mixture with given composition. pub fn bubble_point_line( eos: &Arc, diff --git a/feos-core/src/phase_equilibria/stability_analysis.rs b/feos-core/src/phase_equilibria/stability_analysis.rs index 47da90f3f..fd4055d93 100644 --- a/feos-core/src/phase_equilibria/stability_analysis.rs +++ b/feos-core/src/phase_equilibria/stability_analysis.rs @@ -1,7 +1,7 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State}; +use crate::state::{Contributions, DensityInitialization, SolverOptions, State, Verbosity}; use crate::EosUnit; use ndarray::*; use num_dual::linalg::smallest_ev; @@ -18,7 +18,7 @@ const MINIMIZE_KMAX: usize = 100; const ZERO_TPD: f64 = -1E-08; /// # Stability analysis -impl State { +impl State { /// Determine if the state is stable, i.e. if a phase split should /// occur or not. pub fn is_stable(&self, options: SolverOptions) -> EosResult { diff --git a/feos-core/src/phase_equilibria/tp_flash.rs b/feos-core/src/phase_equilibria/tp_flash.rs index ef53cd189..af87455be 100644 --- a/feos-core/src/phase_equilibria/tp_flash.rs +++ b/feos-core/src/phase_equilibria/tp_flash.rs @@ -1,7 +1,7 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State}; +use crate::state::{Contributions, DensityInitialization, SolverOptions, State, Verbosity}; use ndarray::*; use num_dual::linalg::norm; use quantity::si::{SIArray1, SINumber}; @@ -11,7 +11,7 @@ const MAX_ITER_TP: usize = 400; const TOL_TP: f64 = 1e-8; /// # Flash calculations -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Perform a Tp-flash calculation. If no initial values are /// given, the solution is initialized using a stability analysis. /// @@ -38,7 +38,7 @@ impl PhaseEquilibrium { } /// # Flash calculations -impl State { +impl State { /// Perform a Tp-flash calculation using the [State] as feed. /// If no initial values are given, the solution is initialized /// using a stability analysis. @@ -157,7 +157,7 @@ impl State { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { fn accelerated_successive_substitution( &mut self, feed_state: &State, diff --git a/feos-core/src/phase_equilibria/vle_pure.rs b/feos-core/src/phase_equilibria/vle_pure.rs index 7d89a62ed..e2ab5c3f9 100644 --- a/feos-core/src/phase_equilibria/vle_pure.rs +++ b/feos-core/src/phase_equilibria/vle_pure.rs @@ -1,7 +1,7 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State, TPSpec}; +use crate::state::{Contributions, DensityInitialization, SolverOptions, State, TPSpec, Verbosity}; use crate::EosUnit; use ndarray::{arr1, Array1}; use quantity::si::{SINumber, SIUnit}; @@ -13,7 +13,7 @@ const MAX_ITER_PURE: usize = 50; const TOL_PURE: f64 = 1e-12; /// # Pure component phase equilibria -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate a phase equilibrium for a pure component. pub fn pure( eos: &Arc, @@ -83,21 +83,24 @@ impl PhaseEquilibrium { let (p_l, p_rho_l) = liquid.p_dpdrho(); let (p_v, p_rho_v) = vapor.p_dpdrho(); // calculate the molar Helmholtz energies (already cached) - let a_l = liquid.molar_helmholtz_energy(Contributions::Total); - let a_v = vapor.molar_helmholtz_energy(Contributions::Total); + // let a_l = liquid.molar_helmholtz_energy(Contributions::Total); + let a_l_res = liquid.residual_helmholtz_energy() / liquid.total_moles; + // let a_v = vapor.molar_helmholtz_energy(Contributions::Total); + let a_v_res = vapor.residual_helmholtz_energy() / vapor.total_moles; // Estimate the new pressure + let kt = SIUnit::gas_constant() * vapor.temperature; let delta_v = 1.0 / vapor.density - 1.0 / liquid.density; - let delta_a = a_v - a_l; + let delta_a = + a_v_res - a_l_res + kt * vapor.density.to_reduced(liquid.temperature)?.ln(); let mut p_new = -delta_a / delta_v; // If the pressure becomes negative, assume the gas phase is ideal. The // resulting pressure is always positive. if p_new.is_sign_negative() { - let mu_v = vapor.chemical_potential(Contributions::Total).get(0); p_new = p_v - * (a_l - mu_v) - .to_reduced(vapor.temperature * SIUnit::gas_constant())? + * (-delta_a - p_v * vapor.volume / vapor.total_moles) + .to_reduced(kt)? .exp(); } @@ -193,20 +196,23 @@ impl PhaseEquilibrium { let p_t_l = vle.liquid().dp_dt(Contributions::Total); let p_t_v = vle.vapor().dp_dt(Contributions::Total); - // calculate the molar entropies (already cached) - let s_l = vle.liquid().molar_entropy(Contributions::Total); - let s_v = vle.vapor().molar_entropy(Contributions::Total); + // calculate the residual molar entropies (already cached) + let s_l_res = vle.liquid().residual_entropy() / vle.liquid().total_moles; + let s_v_res = vle.vapor().residual_entropy() / vle.vapor().total_moles; - // calculate the molar Helmholtz energies (already cached) - let a_l = vle.liquid().molar_helmholtz_energy(Contributions::Total); - let a_v = vle.vapor().molar_helmholtz_energy(Contributions::Total); + // calculate the residual molar Helmholtz energies (already cached) + let a_l_res = vle.liquid().residual_helmholtz_energy() / vle.liquid().total_moles; + let a_v_res = vle.vapor().residual_helmholtz_energy() / vle.vapor().total_moles; // calculate the molar volumes let v_l = 1.0 / vle.liquid().density; let v_v = 1.0 / vle.vapor().density; // estimate the temperature steps - let delta_t = (pressure * (v_v - v_l) + (a_v - a_l)) / (s_v - s_l); + let kt = SIUnit::gas_constant() * vle.vapor().temperature; + let ln_rho = v_l.to_reduced(v_v)?.ln(); + let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + kt * ln_rho)) + / (s_v_res - s_l_res - SIUnit::gas_constant() * ln_rho); let t_new = vle.vapor().temperature + delta_t; // calculate Newton steps for the densities and update state. @@ -319,10 +325,12 @@ impl PhaseEquilibrium { } for _ in 0..20 { - t0 = (e.vapor().enthalpy(Contributions::Total) - - e.liquid().enthalpy(Contributions::Total)) - / (e.vapor().entropy(Contributions::Total) - - e.liquid().entropy(Contributions::Total)); + t0 = (e.vapor().residual_enthalpy() - e.liquid().residual_enthalpy()) + / (e.vapor().residual_entropy() + - e.liquid().residual_entropy() + - SIUnit::gas_constant() + * e.vapor().total_moles + * (e.vapor().density.to_reduced(e.liquid().density)?.ln())); let trial_state = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?; if trial_state.density < cp.density { @@ -346,7 +354,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate the pure component vapor pressures of all /// components in the system for the given temperature. pub fn vapor_pressure(eos: &Arc, temperature: SINumber) -> Vec> { diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index c6c898bc2..ede6d29eb 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -201,10 +201,10 @@ impl State { self.evaluate_property(Self::dmu_dt_, contributions, true) } - /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ - pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { - self.evaluate_property(Self::dmu_dni_, contributions, true) - } + // /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ + // pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { + // self.evaluate_property(Self::dmu_dni_, contributions, true) + // } /// Molar isochoric heat capacity: $c_v=\left(\frac{\partial u}{\partial T}\right)_{V,N_i}$ pub fn c_v(&self, contributions: Contributions) -> SINumber { @@ -296,12 +296,6 @@ impl State { self.gibbs_energy(contributions) / self.total_moles } - /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_volume(&self, contributions: Contributions) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| -s.dp_dni_(evaluate) / s.dp_dv_(evaluate); - self.evaluate_property(func, contributions, false) - } - /// Partial molar entropy: $s_i=\left(\frac{\partial S}{\partial N_i}\right)_{T,p,N_j}$ pub fn partial_molar_entropy(&self, contributions: Contributions) -> SIArray1 { let func = |s: &Self, evaluate: Evaluate| { diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 97407c34a..5a4ca2b49 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -162,6 +162,26 @@ impl State { ) } + /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ + pub fn partial_molar_volume(&self) -> SIArray1 { + -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total) + } + + /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ + pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { + let n = self.eos.components(); + SIArray::from_shape_fn((n, n), |(i, j)| { + let ideal_gas = if i == j { + SIUnit::gas_constant() * self.temperature / self.moles.get(i) + } else { + 0.0 * SIUnit::reference_molar_energy() / SIUnit::reference_moles() + }; + let residual = self + .get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DN(i), DN(j))); + Self::contributions(ideal_gas, residual, contributions) + }) + } + // This function is designed specifically for use in spinodal iterations pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { let d2p_dv2 = self.d2p_dv2(Contributions::Total); @@ -190,14 +210,6 @@ impl State { }) } - /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ - pub fn dmu_res_dni(&self) -> SIArray2 { - let n = self.eos.components(); - SIArray::from_shape_fn((n, n), |(i, j)| { - self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DN(i), DN(j))) - }) - } - /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$ pub fn ln_phi(&self) -> Array1 { (self.residual_chemical_potential() / (SIUnit::gas_constant() * self.temperature)) @@ -251,7 +263,7 @@ impl State { /// 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}$ pub fn dln_phi_dnj(&self) -> SIArray2 { let n = self.eos.components(); - let dmu_dni = self.dmu_res_dni(); + let dmu_dni = self.dmu_dni(Contributions::Residual); let dp_dni = self.dp_dni(Contributions::Total); let dp_dv = self.dp_dv(Contributions::Total); let dp_dn_2 = SIArray::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j)); From 9d18ad5a8339dcf54ee703f78b3423fd23cd55a8 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Fri, 2 Jun 2023 21:17:47 +0200 Subject: [PATCH 10/47] Added missing files for feos-derive --- feos-derive/src/ideal_gas.rs | 79 +++++++++++++ feos-derive/src/residual.rs | 211 +++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 feos-derive/src/ideal_gas.rs create mode 100644 feos-derive/src/residual.rs diff --git a/feos-derive/src/ideal_gas.rs b/feos-derive/src/ideal_gas.rs new file mode 100644 index 000000000..87d9a5778 --- /dev/null +++ b/feos-derive/src/ideal_gas.rs @@ -0,0 +1,79 @@ +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn expand_ideal_gas(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let ideal_gas = impl_ideal_gas(variants); + // let entropy_scaling = impl_entropy_scaling(variants)?; + Ok(quote! { + #ideal_gas + }) +} + +fn impl_ideal_gas( + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let subset = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name => panic!("No ideal gas model initialized!") + } + } else { + quote! { + Self::#name(ideal_gas) => Self::#name(ideal_gas.subset(component_list)) + } + } + }); + let ideal_gas_model = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name => panic!("No ideal gas model initialized!") + } + } else { + quote! { + Self::#name(ideal_gas) => ideal_gas.ideal_gas_model() + } + } + }); + let display = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name => write!(f, "no ideal gas model initialized") + } + } else { + quote! { + Self::#name(ideal_gas) => write!(f, "{}", ideal_gas.to_string()) + } + } + }); + + quote! { + impl IdealGas for IdealGasModel { + fn subset(&self, component_list: &[usize]) -> Self { + match self { + #(#subset,)* + } + } + fn ideal_gas_model(&self) -> &Box { + match self { + #(#ideal_gas_model,)* + } + } + } + + impl fmt::Display for IdealGasModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #(#display,)* + } + } + } + } +} diff --git a/feos-derive/src/residual.rs b/feos-derive/src/residual.rs new file mode 100644 index 000000000..1932bc251 --- /dev/null +++ b/feos-derive/src/residual.rs @@ -0,0 +1,211 @@ +use super::implement; +use quote::quote; +use syn::DeriveInput; + +// possible additional traits to implement +const OPT_IMPLS: [&str; 2] = ["molar_weight", "entropy_scaling"]; + +pub(crate) fn expand_residual(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let residual = impl_residual(variants); + let molar_weight = impl_molar_weight(variants)?; + let entropy_scaling = impl_entropy_scaling(variants)?; + Ok(quote! { + #residual + #molar_weight + #entropy_scaling + }) +} + +fn impl_residual( + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let components = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => residual.components() + } + }); + let subset = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => Self::#name(residual.subset(component_list)) + } + }); + let compute_max_density = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => residual.compute_max_density(moles) + } + }); + let contributions = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => residual.contributions() + } + }); + let display = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => write!(f, "{}", residual.to_string()) + } + }); + + quote! { + impl Residual for ResidualModel { + fn components(&self) -> usize { + match self { + #(#components,)* + } + } + fn compute_max_density(&self, moles: &Array1) -> f64 { + match self { + #(#compute_max_density,)* + } + } + fn subset(&self, component_list: &[usize]) -> Self { + match self { + #(#subset,)* + } + } + fn contributions(&self) -> &[Box] { + match self { + #(#contributions,)* + } + } + } + + impl fmt::Display for ResidualModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #(#display,)* + } + } + } + } +} + +fn impl_molar_weight( + variants: &syn::punctuated::Punctuated, +) -> syn::Result { + let mut 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(residual) => residual.molar_weight() + }); + } + } + Ok(quote! { + impl MolarWeight for ResidualModel { + fn molar_weight(&self) -> SIArray1 { + match self { + #(#molar_weight,)* + _ => unimplemented!() + } + } + } + }) +} + +fn impl_entropy_scaling( + variants: &syn::punctuated::Punctuated, +) -> syn::Result { + let mut etar = Vec::new(); + let mut etac = Vec::new(); + let mut dr = Vec::new(); + let mut dc = Vec::new(); + let mut thcr = Vec::new(); + let mut thcc = Vec::new(); + + for v in variants.iter() { + if implement("entropy_scaling", v, &OPT_IMPLS)? { + let name = &v.ident; + etar.push(quote! { + Self::#name(eos) => eos.viscosity_reference(temperature, volume, moles) + }); + etac.push(quote! { + Self::#name(eos) => eos.viscosity_correlation(s_res, x) + }); + dr.push(quote! { + Self::#name(eos) => eos.diffusion_reference(temperature, volume, moles) + }); + dc.push(quote! { + Self::#name(eos) => eos.diffusion_correlation(s_res, x) + }); + thcr.push(quote! { + Self::#name(eos) => eos.thermal_conductivity_reference(temperature, volume, moles) + }); + thcc.push(quote! { + Self::#name(eos) => eos.thermal_conductivity_correlation(s_res, x) + }); + } + } + + Ok(quote! { + impl EntropyScaling for ResidualModel { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#etar,)* + _ => unimplemented!(), + } + } + + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#etac,)* + _ => unimplemented!(), + } + } + + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#dr,)* + _ => unimplemented!(), + } + } + + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#dc,)* + _ => unimplemented!(), + } + } + + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#thcr,)* + _ => unimplemented!(), + } + } + + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#thcc,)* + _ => unimplemented!(), + } + } + } + }) +} \ No newline at end of file From 48e98bafc059be447fe844ef47100dbf9faf5ac4 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Sun, 4 Jun 2023 11:22:58 +0200 Subject: [PATCH 11/47] moved subset to new Components trait, eliminated DeBroglieWavelength trait --- feos-core/src/cubic.rs | 13 +- .../src/equation_of_state/__ideal_gas.rs | 100 ----- feos-core/src/equation_of_state/debroglie.rs | 47 --- feos-core/src/equation_of_state/ideal_gas.rs | 25 +- feos-core/src/equation_of_state/mod.rs | 61 ++-- feos-core/src/equation_of_state/residual.rs | 14 +- feos-core/src/equation_of_state_old.rs | 343 ------------------ feos-core/src/joback.rs | 54 +-- feos-core/src/lib.rs | 10 +- feos-core/src/python/user_defined.rs | 107 +++--- feos-core/src/state/properties.rs | 90 +++-- feos-derive/src/ideal_gas.rs | 35 +- feos-derive/src/lib.rs | 14 +- feos-derive/src/residual.rs | 48 +-- src/eos.rs | 10 +- 15 files changed, 224 insertions(+), 747 deletions(-) delete mode 100644 feos-core/src/equation_of_state/__ideal_gas.rs delete mode 100644 feos-core/src/equation_of_state/debroglie.rs delete mode 100644 feos-core/src/equation_of_state_old.rs diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index 6ef1adf9f..d3ca40bf9 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::{HelmholtzEnergy, HelmholtzEnergyDual, Residual}; +use crate::equation_of_state::{Components, HelmholtzEnergy, HelmholtzEnergyDual, Residual}; use crate::parameter::{Identifier, Parameter, ParameterError, PureRecord}; use crate::si::{GRAM, MOL}; use crate::state::StateHD; @@ -144,12 +144,7 @@ impl Parameter for PengRobinsonParameters { } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.k_ij) } } @@ -219,7 +214,7 @@ impl fmt::Display for PengRobinson { } } -impl Residual for PengRobinson { +impl Components for PengRobinson { fn components(&self) -> usize { self.parameters.b.len() } @@ -227,7 +222,9 @@ impl Residual for PengRobinson { fn subset(&self, component_list: &[usize]) -> Self { Self::new(Arc::new(self.parameters.subset(component_list))) } +} +impl Residual for PengRobinson { fn compute_max_density(&self, moles: &Array1) -> f64 { let b = (moles * &self.parameters.b).sum() / moles.sum(); 0.9 / b diff --git a/feos-core/src/equation_of_state/__ideal_gas.rs b/feos-core/src/equation_of_state/__ideal_gas.rs deleted file mode 100644 index 96a676352..000000000 --- a/feos-core/src/equation_of_state/__ideal_gas.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::StateHD; -use ndarray::Array1; -use num_dual::DualNum; -use num_dual::*; -use std::fmt; - -/// Ideal gas Helmholtz energy contribution that can -/// be evaluated using generalized (hyper) dual numbers. -/// -/// This trait needs to be implemented generically or for -/// the specific types in the supertraits of [IdealGasContribution] -/// so that the implementor can be used as an ideal gas -/// contribution in the equation of state. -pub trait IdealGasDual> { - /// Return the number of components - fn components(&self) -> usize; - - /// Return an equation of state consisting of the components - /// contained in component_list. - fn subset(&self, component_list: &[usize]) -> Self; - - fn de_broglie_wavelength(&self, temperature: D) -> Array1; - - /// Evaluate the ideal gas contribution for a given state. - /// - /// In some cases it could be advantageous to overwrite this - /// implementation instead of implementing the de Broglie - /// wavelength. - fn helmholtz_energy(&self, state: &StateHD) -> D { - let lambda = self.de_broglie_wavelength(state.temperature); - ((lambda - + state.partial_density.mapv(|x| { - if x.re() == 0.0 { - D::from(0.0) - } else { - x.ln() - 1.0 - } - })) - * &state.moles) - .sum() - } -} - -pub struct DefaultIdealGas(pub usize); - -impl> IdealGasDual for DefaultIdealGas { - fn components(&self) -> usize { - self.0 - } - fn subset(&self, component_list: &[usize]) -> Self { - Self(component_list.len()) - } - fn de_broglie_wavelength(&self, temperature: D) -> Array1 { - Array1::zeros(self.0) - } -} - -impl fmt::Display for DefaultIdealGas { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (default)") - } -} - -pub trait IdealGas: - IdealGasDual - + IdealGasDual - + IdealGasDual, f64>> - + IdealGasDual - + IdealGasDual - + IdealGasDual - + IdealGasDual> - + IdealGasDual, f64>> - + IdealGasDual, f64>> - + IdealGasDual> - + IdealGasDual, f64>> - + IdealGasDual, f64>> - + fmt::Display - + Send - + Sync -{ -} - -impl IdealGas for T where - T: IdealGasDual - + IdealGasDual - + IdealGasDual, f64>> - + IdealGasDual - + IdealGasDual - + IdealGasDual - + IdealGasDual> - + IdealGasDual, f64>> - + IdealGasDual, f64>> - + IdealGasDual> - + IdealGasDual, f64>> - + IdealGasDual, f64>> - + fmt::Display - + Send - + Sync -{ -} diff --git a/feos-core/src/equation_of_state/debroglie.rs b/feos-core/src/equation_of_state/debroglie.rs deleted file mode 100644 index fa0ca9c65..000000000 --- a/feos-core/src/equation_of_state/debroglie.rs +++ /dev/null @@ -1,47 +0,0 @@ -use ndarray::Array1; -use num_dual::*; -use std::fmt; - -pub trait DeBroglieWavelengthDual> { - fn de_broglie_wavelength(&self, temperature: D) -> Array1; -} - -pub trait DeBroglieWavelength: - DeBroglieWavelengthDual - + DeBroglieWavelengthDual - + DeBroglieWavelengthDual, f64>> - + DeBroglieWavelengthDual - + DeBroglieWavelengthDual - + DeBroglieWavelengthDual - + DeBroglieWavelengthDual> - + DeBroglieWavelengthDual, f64>> - + DeBroglieWavelengthDual, f64>> - + DeBroglieWavelengthDual> - + DeBroglieWavelengthDual> - + DeBroglieWavelengthDual, f64>> - + DeBroglieWavelengthDual, f64>> - + fmt::Display - + Send - + Sync -{ -} - -impl DeBroglieWavelength for T where - T: DeBroglieWavelengthDual - + DeBroglieWavelengthDual - + DeBroglieWavelengthDual, f64>> - + DeBroglieWavelengthDual - + DeBroglieWavelengthDual - + DeBroglieWavelengthDual - + DeBroglieWavelengthDual> - + DeBroglieWavelengthDual, f64>> - + DeBroglieWavelengthDual, f64>> - + DeBroglieWavelengthDual> - + DeBroglieWavelengthDual> - + DeBroglieWavelengthDual, f64>> - + DeBroglieWavelengthDual, f64>> - + fmt::Display - + Send - + Sync -{ -} diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs index 112590868..67304236d 100644 --- a/feos-core/src/equation_of_state/ideal_gas.rs +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -1,8 +1,7 @@ +use super::Components; use crate::StateHD; +use ndarray::Array1; use num_dual::DualNum; -use std::fmt; - -use super::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; /// Ideal gas Helmholtz energy contribution that can /// be evaluated using generalized (hyper) dual numbers. @@ -11,28 +10,18 @@ use super::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; /// the specific types in the supertraits of [IdealGasContribution] /// so that the implementor can be used as an ideal gas /// contribution in the equation of state. -pub trait IdealGas: Sync + Send + fmt::Display { - // /// Return the number of components - // fn components(&self) -> usize; - - /// Return an equation of state consisting of the components - /// contained in component_list. - fn subset(&self, component_list: &[usize]) -> Self; +pub trait IdealGas: Components + Sync + Send { + fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1; - fn ideal_gas_model(&self) -> &Box; + fn ideal_gas_model(&self) -> String; /// Evaluate the ideal gas contribution for a given state. /// /// In some cases it could be advantageous to overwrite this /// implementation instead of implementing the de Broglie /// wavelength. - fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D - where - dyn DeBroglieWavelength: DeBroglieWavelengthDual, - { - let lambda = self - .ideal_gas_model() - .de_broglie_wavelength(state.temperature); + fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D { + let lambda = self.de_broglie_wavelength(state.temperature); ((lambda + state.partial_density.mapv(|x| { if x.re() == 0.0 { diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 080e050d9..2e591d80f 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -1,15 +1,13 @@ use ndarray::Array1; use num_dual::DualNum; use quantity::si::{SIArray1, SINumber}; -use std::{fmt::Display, sync::Arc}; +use std::sync::Arc; -pub mod debroglie; pub mod helmholtz_energy; pub mod ideal_gas; pub mod residual; -use crate::{EosResult, StateHD}; +use crate::EosResult; -pub use self::debroglie::{DeBroglieWavelength, DeBroglieWavelengthDual}; pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; pub use ideal_gas::IdealGas; pub use residual::{EntropyScaling, Residual}; @@ -22,42 +20,34 @@ pub trait MolarWeight { fn molar_weight(&self) -> SIArray1; } +pub trait Components { + /// Return the number of components of the model. + fn components(&self) -> usize; + + /// Return a model consisting of the components + /// contained in component_list. + fn subset(&self, component_list: &[usize]) -> Self; +} + #[derive(Clone)] pub struct EquationOfState { pub ideal_gas: Arc, pub residual: Arc, - components: usize, -} - -impl Display for EquationOfState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} {}", - self.ideal_gas.to_string(), - self.residual.to_string() - ) - } } -impl EquationOfState { +impl EquationOfState { pub fn new(ideal_gas: Arc, residual: Arc) -> Self { - // assert_eq!(residual.components(), ideal_gas.components()); - let components = residual.components(); Self { ideal_gas, residual, - components, } } } -impl IdealGas for EquationOfState { - fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D - where - dyn DeBroglieWavelength: DeBroglieWavelengthDual, - { - self.ideal_gas.evaluate_ideal_gas(state) +impl Components for EquationOfState { + fn components(&self) -> usize { + assert_eq!(self.residual.components(), self.ideal_gas.components()); + self.residual.components() } fn subset(&self, component_list: &[usize]) -> Self { @@ -66,24 +56,19 @@ impl IdealGas for EquationOfState { Arc::new(self.residual.subset(component_list)), ) } - - fn ideal_gas_model(&self) -> &Box { - self.ideal_gas.ideal_gas_model() - } } -impl Residual for EquationOfState { - fn components(&self) -> usize { - self.residual.components() +impl IdealGas for EquationOfState { + fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { + self.ideal_gas.de_broglie_wavelength(temperature) } - fn subset(&self, component_list: &[usize]) -> Self { - Self::new( - Arc::new(self.ideal_gas.subset(component_list)), - Arc::new(self.residual.subset(component_list)), - ) + fn ideal_gas_model(&self) -> String { + self.ideal_gas.ideal_gas_model() } +} +impl Residual for EquationOfState { fn compute_max_density(&self, moles: &Array1) -> f64 { self.residual.compute_max_density(moles) } diff --git a/feos-core/src/equation_of_state/residual.rs b/feos-core/src/equation_of_state/residual.rs index e0212b01f..7cb879f81 100644 --- a/feos-core/src/equation_of_state/residual.rs +++ b/feos-core/src/equation_of_state/residual.rs @@ -1,21 +1,13 @@ -use super::{HelmholtzEnergy, HelmholtzEnergyDual}; +use super::{Components, HelmholtzEnergy, HelmholtzEnergyDual}; use crate::StateHD; use crate::{EosError, EosResult, EosUnit}; use ndarray::prelude::*; use num_dual::*; use num_traits::{One, Zero}; use quantity::si::{SIArray1, SINumber, SIUnit}; -use std::fmt; /// A general equation of state. -pub trait Residual: Send + Sync + fmt::Display { - /// Return the number of components of the equation of state. - fn components(&self) -> usize; - - /// Return an equation of state consisting of the components - /// contained in component_list. - fn subset(&self, component_list: &[usize]) -> Self; - +pub trait Residual: Components + Send + Sync { /// Return the maximum density in Angstrom^-3. /// /// This value is used as an estimate for a liquid phase for phase @@ -177,4 +169,4 @@ pub trait EntropyScaling { moles: &SIArray1, ) -> EosResult; fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; -} \ No newline at end of file +} diff --git a/feos-core/src/equation_of_state_old.rs b/feos-core/src/equation_of_state_old.rs deleted file mode 100644 index 6d7b523fc..000000000 --- a/feos-core/src/equation_of_state_old.rs +++ /dev/null @@ -1,343 +0,0 @@ -use crate::errors::{EosError, EosResult}; -use crate::state::StateHD; -use crate::EosUnit; -use ndarray::prelude::*; -use num_dual::{ - first_derivative, second_derivative, third_derivative, Dual, Dual2, Dual2_64, Dual3, Dual3_64, - Dual64, DualNum, DualSVec64, HyperDual, HyperDual64, -}; -use num_traits::Zero; -use quantity::si::{SIArray1, SINumber, SIUnit}; -use std::fmt; - -/// Individual Helmholtz energy contribution that can -/// be evaluated using generalized (hyper) dual numbers. -/// -/// This trait needs to be implemented generically or for -/// the specific types in the supertraits of [HelmholtzEnergy] -/// so that the implementor can be used as a Helmholtz energy -/// contribution in the equation of state. -pub trait HelmholtzEnergyDual> { - /// The Helmholtz energy contribution $\beta A$ of a given state in reduced units. - fn helmholtz_energy(&self, state: &StateHD) -> D; -} - -/// Object safe version of the [HelmholtzEnergyDual] trait. -/// -/// The trait is implemented automatically for every struct that implements -/// the supertraits. -pub trait HelmholtzEnergy: - HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + fmt::Display - + Send - + Sync -{ -} - -impl HelmholtzEnergy for T where - T: HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + fmt::Display - + Send - + Sync -{ -} - -/// Ideal gas Helmholtz energy contribution that can -/// be evaluated using generalized (hyper) dual numbers. -/// -/// This trait needs to be implemented generically or for -/// the specific types in the supertraits of [IdealGasContribution] -/// so that the implementor can be used as an ideal gas -/// contribution in the equation of state. -pub trait IdealGasContributionDual + Copy> { - /// The thermal de Broglie wavelength of each component in the form $\ln\left(\frac{\Lambda^3}{\AA^3}\right)$ - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1; - - /// Evaluate the ideal gas contribution for a given state. - /// - /// In some cases it could be advantageous to overwrite this - /// implementation instead of implementing the de Broglie - /// wavelength. - fn evaluate(&self, state: &StateHD) -> D { - let lambda = self.de_broglie_wavelength(state.temperature, state.moles.len()); - ((lambda - + state.partial_density.mapv(|x| { - if x.re() == 0.0 { - D::from(0.0) - } else { - x.ln() - 1.0 - } - })) - * &state.moles) - .sum() - } -} - -/// Object safe version of the [IdealGasContributionDual] trait. -/// -/// The trait is implemented automatically for every struct that implements -/// the supertraits. -pub trait IdealGasContribution: - IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual, f64>> - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual> - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + fmt::Display -{ -} - -impl IdealGasContribution for T where - T: IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual, f64>> - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual> - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + fmt::Display -{ -} - -struct DefaultIdealGasContribution; -impl + Copy> IdealGasContributionDual for DefaultIdealGasContribution { - fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1 { - Array1::zeros(components) - } -} - -impl fmt::Display for DefaultIdealGasContribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (default)") - } -} - -/// Molar weight of all components. -/// -/// The trait is required to be able to calculate (mass) -/// specific properties. -pub trait MolarWeight { - fn molar_weight(&self) -> SIArray1; -} - -/// A general equation of state. -pub trait EquationOfState: Send + Sync { - /// Return the number of components of the equation of state. - fn components(&self) -> usize; - - /// Return an equation of state consisting of the components - /// contained in component_list. - fn subset(&self, component_list: &[usize]) -> Self; - - /// Return the maximum density in Angstrom^-3. - /// - /// This value is used as an estimate for a liquid phase for phase - /// equilibria and other iterations. It is not explicitly meant to - /// be a mathematical limit for the density (if those exist in the - /// equation of state anyways). - fn compute_max_density(&self, moles: &Array1) -> f64; - - /// Return a slice of the individual contributions (excluding the ideal gas) - /// of the equation of state. - fn residual(&self) -> &[Box]; - - /// Evaluate the residual reduced Helmholtz energy $\beta A^\mathrm{res}$. - fn evaluate_residual + Copy>(&self, state: &StateHD) -> D - where - dyn HelmholtzEnergy: HelmholtzEnergyDual, - { - self.residual() - .iter() - .map(|c| c.helmholtz_energy(state)) - .sum() - } - - /// Evaluate the reduced Helmholtz energy of each individual contribution - /// and return them together with a string representation of the contribution. - fn evaluate_residual_contributions + Copy>( - &self, - state: &StateHD, - ) -> Vec<(String, D)> - where - dyn HelmholtzEnergy: HelmholtzEnergyDual, - { - self.residual() - .iter() - .map(|c| (c.to_string(), c.helmholtz_energy(state))) - .collect() - } - - /// Return the ideal gas contribution. - /// - /// Per default this function returns an ideal gas contribution - /// in which the de Broglie wavelength is 1 for every component. - /// Therefore, the correct ideal gas pressure is obtained even - /// with no explicit ideal gas term. If a more detailed model is - /// required (e.g. for the calculation of enthalpies) this function - /// has to be overwritten. - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &DefaultIdealGasContribution - } - - /// Check if the provided optional mole number is consistent with the - /// equation of state. - /// - /// In general, the number of elements in `moles` needs to match the number - /// of components of the equation of state. For a pure component, however, - /// no moles need to be provided. In that case, it is set to the constant - /// reference value. - fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { - let l = moles.map_or(1, |m| m.len()); - if self.components() == l { - match moles { - Some(m) => Ok(m.to_owned()), - None => Ok(Array::ones(1) * SIUnit::reference_moles()), - } - } else { - Err(EosError::IncompatibleComponents(self.components(), l)) - } - } - - /// Calculate the maximum density. - /// - /// This value is used as an estimate for a liquid phase for phase - /// equilibria and other iterations. It is not explicitly meant to - /// be a mathematical limit for the density (if those exist in the - /// equation of state anyways). - fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { - let mr = self - .validate_moles(moles)? - .to_reduced(SIUnit::reference_moles())?; - Ok(self.compute_max_density(&mr) * SIUnit::reference_density()) - } - - /// Calculate the second virial coefficient $B(T)$ - fn second_virial_coefficient( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let a_res = |rho| self.evaluate_residual(&StateHD::new_virial(t.into(), rho, x)); - let (_, _, b) = second_derivative(a_res, 0.0); - Ok(b * 0.5 / SIUnit::reference_density()) - } - - /// Calculate the third virial coefficient $C(T)$ - fn third_virial_coefficient( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let a_res = |rho| self.evaluate_residual(&StateHD::new_virial(t.into(), rho, x)); - let (_, _, _, c) = third_derivative(a_res, 0.0); - Ok(c / 3.0 / SIUnit::reference_density().powi(2)) - } - - /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ - fn second_virial_coefficient_temperature_derivative( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let b = |t| { - let a_res = |rho: Dual2| { - self.evaluate_residual(&StateHD::new_virial(Dual2::from_re(t), rho, x)) - }; - let (_, _, b) = second_derivative(a_res, Dual64::zero()); - b - }; - let (_, b_t) = first_derivative(b, t); - Ok(b_t * 0.5 / (SIUnit::reference_density() * SIUnit::reference_temperature())) - } - - /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ - fn third_virial_coefficient_temperature_derivative( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let c = |t| { - let a_res = - |rho| self.evaluate_residual(&StateHD::new_virial(Dual3::from_re(t), rho, x)); - let (_, _, _, c) = third_derivative(a_res, Dual64::zero()); - c - }; - let (_, c_t) = first_derivative(c, t); - Ok(c_t / 3.0 / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) - } -} - -/// Reference values and residual entropy correlations for entropy scaling. -pub trait EntropyScaling { - fn viscosity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; - fn diffusion_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult; - fn thermal_conductivity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; -} diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index d7dab952e..0ce830a6f 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -1,7 +1,7 @@ //! Implementation of the ideal gas heat capacity (de Broglie wavelength) //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). -use crate::equation_of_state::{DeBroglieWavelength, DeBroglieWavelengthDual}; +use crate::equation_of_state::Components; use crate::parameter::*; use crate::{EosResult, EosUnit, IdealGas}; use conv::ValueInto; @@ -68,16 +68,13 @@ impl> FromSegments for JobackRecord { /// [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). pub struct Joback { pub records: Arc>, - de_broglie: Box, + // de_broglie: Box, } impl Joback { /// Creates a new Joback contribution. pub fn new(records: Arc>) -> Self { - Self { - records: records.clone(), - de_broglie: Box::new(JobackDeBroglie(records)), - } + Self { records } } /// Directly calculates the ideal gas heat capacity from the Joback model. @@ -91,13 +88,11 @@ impl Joback { } } -impl fmt::Display for Joback { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (Joback)") +impl Components for Joback { + fn components(&self) -> usize { + self.records.len() } -} -impl IdealGas for Joback { fn subset(&self, component_list: &[usize]) -> Self { let mut records = Vec::with_capacity(component_list.len()); component_list @@ -105,34 +100,15 @@ impl IdealGas for Joback { .for_each(|&i| records.push(self.records[i].clone())); Self::new(Arc::new(records)) } - - fn ideal_gas_model(&self) -> &Box { - &self.de_broglie - } -} - -const RGAS: f64 = 6.022140857 * 1.38064852; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; -const KB: f64 = 1.38064852e-23; - -#[derive(Debug, Clone)] -struct JobackDeBroglie(Arc>); - -impl fmt::Display for JobackDeBroglie { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (Joback)") - } } -impl + Copy> DeBroglieWavelengthDual for JobackDeBroglie { - fn de_broglie_wavelength(&self, temperature: D) -> Array1 { +impl IdealGas for Joback { + fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { let t = temperature; let t2 = t * t; let f = (temperature * KB / (P0 * A3)).ln(); - Array1::from_shape_fn(self.0.len(), |i| { - let j = &self.0[i]; + Array1::from_shape_fn(self.records.len(), |i| { + let j = &self.records[i]; let h = (t2 - T0 * T0) * 0.5 * j.b + (t * t2 - T0.powi(3)) * j.c / 3.0 + (t2 * t2 - T0.powi(4)) * j.d / 4.0 @@ -146,8 +122,18 @@ impl + Copy> DeBroglieWavelengthDual for JobackDeBroglie { (h - t * s) / (t * RGAS) + f }) } + + fn ideal_gas_model(&self) -> String { + "Ideal gas (Joback)".into() + } } +const RGAS: f64 = 6.022140857 * 1.38064852; +const T0: f64 = 298.15; +const P0: f64 = 1.0e5; +const A3: f64 = 1e-30; +const KB: f64 = 1.38064852e-23; + // #[cfg(test)] // mod tests { // use crate::{Contributions, State, StateBuilder}; diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 869745ee7..9f908048d 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -36,8 +36,8 @@ pub mod parameter; mod phase_equilibria; mod state; pub use equation_of_state::{ - DeBroglieWavelength, DeBroglieWavelengthDual, EntropyScaling, EquationOfState, HelmholtzEnergy, - HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, + Components, EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, + MolarWeight, Residual, }; pub use errors::{EosError, EosResult}; pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium}; @@ -363,6 +363,12 @@ mod tests { sr.dc_v_res_dt(), max_relative = 1e-15 ); + println!( + "{}\n{}\n{}", + s.c_p(Contributions::Residual), + s.c_p(Contributions::IdealGas), + s.c_p(Contributions::Total) + ); assert_relative_eq!( s.c_p(Contributions::Residual), sr.c_p_res(), diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index 1391f95ae..b6931d6e3 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -1,6 +1,5 @@ use crate::{ - DeBroglieWavelength, DeBroglieWavelengthDual, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, - MolarWeight, Residual, StateHD, + Components, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, StateHD, }; use ndarray::{arr1, Array1}; use num_dual::*; @@ -10,6 +9,7 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use quantity::python::PySIArray1; use quantity::si::SIArray1; +use std::any::TypeId; use std::fmt; struct PyHelmholtzEnergy(Py); @@ -23,7 +23,7 @@ impl fmt::Display for PyDeBroglieWavelength { pub struct PyIdealGas { obj: Py, - de_broglie: Box, + // de_broglie: Box, } impl fmt::Display for PyIdealGas { @@ -35,6 +35,10 @@ impl fmt::Display for PyIdealGas { impl PyIdealGas { pub fn new(obj: Py) -> PyResult { Python::with_gil(|py| { + let attr = obj.as_ref(py).hasattr("components")?; + if !attr { + panic!("Python Class has to have a method 'components' with signature:\n\tdef signature(self) -> int") + } let attr = obj.as_ref(py).hasattr("subset")?; if !attr { panic!("Python Class has to have a method 'subset' with signature:\n\tdef subset(self, component_list: List[int]) -> Self") @@ -43,15 +47,25 @@ impl PyIdealGas { if !attr { panic!("{}", "Python Class has to have a method 'ideal_gas_model' with signature:\n\tdef ideal_gas_model(self, state: StateHD) -> HD\nwhere 'HD' has to be any of {{float, Dual64, HyperDual64, HyperDualDual64, Dual3Dual64, Dual3_64}}.") } - Ok(Self { - obj: obj.clone(), - de_broglie: Box::new(PyDeBroglieWavelength(obj)), - }) + Ok(Self { obj }) }) } } -impl IdealGas for PyIdealGas { +impl Components for PyIdealGas { + fn components(&self) -> usize { + Python::with_gil(|py| { + let py_result = self.obj.as_ref(py).call_method0("components").unwrap(); + if py_result.get_type().name().unwrap() != "int" { + panic!( + "Expected an integer for the components() method signature, got {}", + py_result.get_type().name().unwrap() + ); + } + py_result.extract().unwrap() + }) + } + fn subset(&self, component_list: &[usize]) -> Self { Python::with_gil(|py| { let py_result = self @@ -62,11 +76,20 @@ impl IdealGas for PyIdealGas { Self::new(py_result.extract().unwrap()).unwrap() }) } +} + +impl IdealGas for PyIdealGas { + fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { + let y = TypeId::of::(); + if y == TypeId::of::() {} + todo!() + } - fn ideal_gas_model(&self) -> &Box { - &self.de_broglie + fn ideal_gas_model(&self) -> String { + todo!() } } + /// Struct containing pointer to Python Class that implements Helmholtz energy. pub struct PyResidual { obj: Py, @@ -125,7 +148,7 @@ impl MolarWeight for PyResidual { } } -impl Residual for PyResidual { +impl Components for PyResidual { fn components(&self) -> usize { Python::with_gil(|py| { let py_result = self.obj.as_ref(py).call_method0("components").unwrap(); @@ -149,7 +172,9 @@ impl Residual for PyResidual { Self::new(py_result.extract().unwrap()).unwrap() }) } +} +impl Residual for PyResidual { fn compute_max_density(&self, moles: &Array1) -> f64 { Python::with_gil(|py| { let py_result = self @@ -254,48 +279,48 @@ macro_rules! helmholtz_energy { }; } -macro_rules! de_broglie_wavelength { - ($py_hd_id:ident, $hd_ty:ty) => { - impl DeBroglieWavelengthDual<$hd_ty> for PyDeBroglieWavelength { - fn de_broglie_wavelength(&self, temperature: $hd_ty) -> Array1<$hd_ty> { - Python::with_gil(|py| { - let py_result = self - .0 - .as_ref(py) - .call_method1("ideal_gas_model", (<$py_hd_id>::from(temperature),)) - .unwrap(); - let rr = if let Ok(r) = py_result.extract::>() { - dbg!("Array1"); - r.to_owned_array() - .mapv(|ri| <$hd_ty>::from(ri.extract::<$py_hd_id>(py).unwrap())) - } else if let Ok(r) = py_result.extract::>() { - dbg!("Array0"); - assert!(r.ndim() == 0); - let scalar = &r.to_owned_array()[0]; - arr1(&[<$hd_ty>::from(scalar.extract::<$py_hd_id>(py).unwrap())]) - } else { - panic!("ideal_gas_model: input data type must be numpy ndarray of dimension 1") - }; - rr - }) - } - } - }; -} +// macro_rules! de_broglie_wavelength { +// ($py_hd_id:ident, $hd_ty:ty) => { +// impl DeBroglieWavelengthDual<$hd_ty> for PyDeBroglieWavelength { +// fn de_broglie_wavelength(&self, temperature: $hd_ty) -> Array1<$hd_ty> { +// Python::with_gil(|py| { +// let py_result = self +// .0 +// .as_ref(py) +// .call_method1("ideal_gas_model", (<$py_hd_id>::from(temperature),)) +// .unwrap(); +// let rr = if let Ok(r) = py_result.extract::>() { +// dbg!("Array1"); +// r.to_owned_array() +// .mapv(|ri| <$hd_ty>::from(ri.extract::<$py_hd_id>(py).unwrap())) +// } else if let Ok(r) = py_result.extract::>() { +// dbg!("Array0"); +// assert!(r.ndim() == 0); +// let scalar = &r.to_owned_array()[0]; +// arr1(&[<$hd_ty>::from(scalar.extract::<$py_hd_id>(py).unwrap())]) +// } else { +// panic!("ideal_gas_model: input data type must be numpy ndarray of dimension 1") +// }; +// rr +// }) +// } +// } +// }; +// } macro_rules! impl_dual_state_helmholtz_energy { ($py_state_id:ident, $py_hd_id:ident, $hd_ty:ty, $py_field_ty:ty) => { dual_number!($py_hd_id, $hd_ty, $py_field_ty); state!($py_state_id, $py_hd_id, $hd_ty); helmholtz_energy!($py_state_id, $py_hd_id, $hd_ty); - de_broglie_wavelength!($py_hd_id, $hd_ty); + // de_broglie_wavelength!($py_hd_id, $hd_ty); }; } // No definition of dual number necessary for f64 state!(PyStateF, f64, f64); helmholtz_energy!(PyStateF, f64, f64); -de_broglie_wavelength!(f64, f64); +// de_broglie_wavelength!(f64, f64); impl_dual_state_helmholtz_energy!(PyStateD, PyDual64, Dual64, f64); dual_number!(PyDualVec3, DualSVec64<3>, f64); diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index ede6d29eb..afab24168 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -1,9 +1,7 @@ use super::{Contributions, Derivative::*, PartialDerivative, State}; -// use crate::equation_of_state::{EntropyScaling, MolarWeight, Residual}; use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; use crate::EosUnit; use ndarray::Array1; -use num_dual::DualNum; use quantity::si::*; use std::ops::{Add, Sub}; @@ -12,7 +10,7 @@ pub(crate) enum Evaluate { IdealGas, Residual, Total, - IdealGasDelta, + // IdealGasDelta, } impl State { @@ -21,37 +19,37 @@ impl State { derivative: PartialDerivative, evaluate: Evaluate, ) -> SINumber { - if let Evaluate::IdealGasDelta = evaluate { - return match derivative { - PartialDerivative::Zeroth => { - let new_state = self.derive0(); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - * SIUnit::reference_energy() - } - PartialDerivative::First(v) => { - let new_state = self.derive1(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).eps - * (SIUnit::reference_energy() / v.reference()) - } - PartialDerivative::Second(v) => { - let new_state = self.derive2(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v2 - * (SIUnit::reference_energy() / (v.reference() * v.reference())) - } - PartialDerivative::SecondMixed(v1, v2) => { - let new_state = self.derive2_mixed(v1, v2); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - .eps1eps2 - * (SIUnit::reference_energy() / (v1.reference() * v2.reference())) - } - PartialDerivative::Third(v) => { - let new_state = self.derive3(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v3 - * (SIUnit::reference_energy() - / (v.reference() * v.reference() * v.reference())) - } - }; - } + // if let Evaluate::IdealGasDelta = evaluate { + // return match derivative { + // PartialDerivative::Zeroth => { + // let new_state = self.derive0(); + // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) + // * SIUnit::reference_energy() + // } + // PartialDerivative::First(v) => { + // let new_state = self.derive1(v); + // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).eps + // * (SIUnit::reference_energy() / v.reference()) + // } + // PartialDerivative::Second(v) => { + // let new_state = self.derive2(v); + // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v2 + // * (SIUnit::reference_energy() / (v.reference() * v.reference())) + // } + // PartialDerivative::SecondMixed(v1, v2) => { + // let new_state = self.derive2_mixed(v1, v2); + // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) + // .eps1eps2 + // * (SIUnit::reference_energy() / (v1.reference() * v2.reference())) + // } + // PartialDerivative::Third(v) => { + // let new_state = self.derive3(v); + // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v3 + // * (SIUnit::reference_energy() + // / (v.reference() * v.reference() * v.reference())) + // } + // }; + // } let residual = match evaluate { Evaluate::IdealGas => None, @@ -166,9 +164,9 @@ impl State { }) } - fn d2p_dv2_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Third(DV), evaluate) - } + // fn d2p_dv2_(&self, evaluate: Evaluate) -> SINumber { + // -self.get_or_compute_derivative(PartialDerivative::Third(DV), evaluate) + // } fn dmu_dt_(&self, evaluate: Evaluate) -> SIArray1 { SIArray::from_shape_fn(self.eos.components(), |i| { @@ -176,12 +174,12 @@ impl State { }) } - fn dmu_dni_(&self, evaluate: Evaluate) -> SIArray2 { - let n = self.eos.components(); - SIArray::from_shape_fn((n, n), |(i, j)| { - self.get_or_compute_derivative(PartialDerivative::SecondMixed(DN(i), DN(j)), evaluate) - }) - } + // fn dmu_dni_(&self, evaluate: Evaluate) -> SIArray2 { + // let n = self.eos.components(); + // SIArray::from_shape_fn((n, n), |(i, j)| { + // self.get_or_compute_derivative(PartialDerivative::SecondMixed(DN(i), DN(j)), evaluate) + // }) + // } fn ds_dt_(&self, evaluate: Evaluate) -> SINumber { -self.get_or_compute_derivative(PartialDerivative::Second(DT), evaluate) @@ -336,7 +334,7 @@ impl State { let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); res.push(( - self.eos.ideal_gas_model().to_string(), + self.eos.ideal_gas_model(), self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature * SIUnit::reference_energy(), @@ -353,7 +351,7 @@ impl State { let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); res.push(( - self.eos.ideal_gas_model().to_string(), + self.eos.ideal_gas_model(), -(self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps * SIUnit::reference_pressure(), )); @@ -372,7 +370,7 @@ impl State { let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); res.push(( - self.eos.ideal_gas_model().to_string(), + self.eos.ideal_gas_model(), (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps * SIUnit::reference_molar_energy(), )); diff --git a/feos-derive/src/ideal_gas.rs b/feos-derive/src/ideal_gas.rs index 87d9a5778..e77148427 100644 --- a/feos-derive/src/ideal_gas.rs +++ b/feos-derive/src/ideal_gas.rs @@ -8,7 +8,6 @@ pub(crate) fn expand_ideal_gas(input: DeriveInput) -> syn::Result syn::Result, ) -> proc_macro2::TokenStream { - let subset = variants.iter().map(|v| { - let name = &v.ident; - if name == "NoModel" { - quote! { - Self::#name => panic!("No ideal gas model initialized!") - } - } else { - quote! { - Self::#name(ideal_gas) => Self::#name(ideal_gas.subset(component_list)) - } - } - }); let ideal_gas_model = variants.iter().map(|v| { let name = &v.ident; if name == "NoModel" { quote! { - Self::#name => panic!("No ideal gas model initialized!") + Self::#name(_) => panic!("No ideal gas model initialized!") } } else { quote! { @@ -41,37 +28,29 @@ fn impl_ideal_gas( } } }); - let display = variants.iter().map(|v| { + let de_broglie_wavelength = variants.iter().map(|v| { let name = &v.ident; if name == "NoModel" { quote! { - Self::#name => write!(f, "no ideal gas model initialized") + Self::#name(_) => panic!("No ideal gas model initialized!") } } else { quote! { - Self::#name(ideal_gas) => write!(f, "{}", ideal_gas.to_string()) + Self::#name(ideal_gas) => ideal_gas.de_broglie_wavelength(temperature) } } }); quote! { impl IdealGas for IdealGasModel { - fn subset(&self, component_list: &[usize]) -> Self { - match self { - #(#subset,)* - } - } - fn ideal_gas_model(&self) -> &Box { + fn ideal_gas_model(&self) -> String { match self { #(#ideal_gas_model,)* } } - } - - impl fmt::Display for IdealGasModel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { match self { - #(#display,)* + #(#de_broglie_wavelength,)* } } } diff --git a/feos-derive/src/lib.rs b/feos-derive/src/lib.rs index 1f40a308c..e91a82b76 100644 --- a/feos-derive/src/lib.rs +++ b/feos-derive/src/lib.rs @@ -1,12 +1,14 @@ //! This crate provides derive macros used for the EosVariant and //! FunctionalVariant enums in FeOs. The macros implement //! the boilerplate for the EquationOfState and HelmholtzEnergyFunctional traits. +use components::expand_components; use dft::expand_helmholtz_energy_functional; use ideal_gas::expand_ideal_gas; -use residual::expand_residual; use proc_macro::TokenStream; +use residual::expand_residual; use syn::{parse_macro_input, DeriveInput}; +mod components; mod dft; mod ideal_gas; mod residual; @@ -45,7 +47,15 @@ fn implement(name: &str, variant: &syn::Variant, opts: &[&'static str]) -> syn:: implement } -#[proc_macro_derive(IdealGas, attributes(implement))] +#[proc_macro_derive(Components)] +pub fn derive_components(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand_components(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro_derive(IdealGas)] pub fn derive_ideal_gas(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); expand_ideal_gas(input) diff --git a/feos-derive/src/residual.rs b/feos-derive/src/residual.rs index 1932bc251..98b709737 100644 --- a/feos-derive/src/residual.rs +++ b/feos-derive/src/residual.rs @@ -24,18 +24,18 @@ pub(crate) fn expand_residual(input: DeriveInput) -> syn::Result, ) -> proc_macro2::TokenStream { - let components = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(residual) => residual.components() - } - }); - let subset = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(residual) => Self::#name(residual.subset(component_list)) - } - }); + // let components = variants.iter().map(|v| { + // let name = &v.ident; + // quote! { + // Self::#name(residual) => residual.components() + // } + // }); + // let subset = variants.iter().map(|v| { + // let name = &v.ident; + // quote! { + // Self::#name(residual) => Self::#name(residual.subset(component_list)) + // } + // }); let compute_max_density = variants.iter().map(|v| { let name = &v.ident; quote! { @@ -53,25 +53,25 @@ fn impl_residual( quote! { Self::#name(residual) => write!(f, "{}", residual.to_string()) } - }); + }); quote! { impl Residual for ResidualModel { - fn components(&self) -> usize { - match self { - #(#components,)* - } - } + // fn components(&self) -> usize { + // match self { + // #(#components,)* + // } + // } fn compute_max_density(&self, moles: &Array1) -> f64 { match self { #(#compute_max_density,)* } } - fn subset(&self, component_list: &[usize]) -> Self { - match self { - #(#subset,)* - } - } + // fn subset(&self, component_list: &[usize]) -> Self { + // match self { + // #(#subset,)* + // } + // } fn contributions(&self) -> &[Box] { match self { #(#contributions,)* @@ -208,4 +208,4 @@ fn impl_entropy_scaling( } } }) -} \ No newline at end of file +} diff --git a/src/eos.rs b/src/eos.rs index b7f192421..d386b45bd 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -13,9 +13,9 @@ use feos_core::joback::Joback; #[cfg(feature = "python")] use feos_core::python::user_defined::{PyIdealGas, PyResidual}; use feos_core::*; -use feos_core::{DeBroglieWavelength, IdealGas, Residual}; -use feos_derive; +use feos_derive::{Components, IdealGas, Residual}; use ndarray::Array1; +use num_dual::DualNum; use quantity::si::*; use std::fmt; @@ -23,7 +23,7 @@ use std::fmt; /// /// Particularly relevant for situations in which generic types /// are undesirable (e.g. FFI). -#[derive(feos_derive::Residual)] +#[derive(Components, Residual)] pub enum ResidualModel { #[cfg(feature = "pcsaft")] #[implement(entropy_scaling, molar_weight)] @@ -46,9 +46,9 @@ pub enum ResidualModel { UVTheory(UVTheory), } -#[derive(feos_derive::IdealGas)] +#[derive(Components, IdealGas)] pub enum IdealGasModel { - NoModel, + NoModel(usize), Joback(Joback), #[cfg(feature = "python")] Python(PyIdealGas), From 5b6eed1c1558d7a6c4a934220e5fbb44f5dd3fea Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Sun, 4 Jun 2023 11:23:51 +0200 Subject: [PATCH 12/47] new file in feos-derive --- feos-derive/src/components.rs | 59 +++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 feos-derive/src/components.rs diff --git a/feos-derive/src/components.rs b/feos-derive/src/components.rs new file mode 100644 index 000000000..aac441690 --- /dev/null +++ b/feos-derive/src/components.rs @@ -0,0 +1,59 @@ +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn expand_components(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let components = impl_components(input.ident, variants); + Ok(quote! { + #components + }) +} + +fn impl_components( + ident: syn::Ident, + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let components = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name(n) => *n + } + } else { + quote! { + Self::#name(residual) => residual.components() + } + } + }); + let subset = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name(n) => Self::#name(component_list.len()) + } + } else { + quote! { + Self::#name(residual) => Self::#name(residual.subset(component_list)) + } + } + }); + + quote! { + impl Components for #ident { + fn components(&self) -> usize { + match self { + #(#components,)* + } + } + fn subset(&self, component_list: &[usize]) -> Self { + match self { + #(#subset,)* + } + } + } + } +} From 8191f15aa7d74ad8df9fc04f7463c826c54a4901 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Sun, 4 Jun 2023 13:33:02 +0200 Subject: [PATCH 13/47] First tiny steps for DFT implementation --- feos-core/src/equation_of_state/residual.rs | 4 +- feos-dft/src/functional.rs | 43 +++++---------------- 2 files changed, 11 insertions(+), 36 deletions(-) diff --git a/feos-core/src/equation_of_state/residual.rs b/feos-core/src/equation_of_state/residual.rs index 7cb879f81..2d80c80d8 100644 --- a/feos-core/src/equation_of_state/residual.rs +++ b/feos-core/src/equation_of_state/residual.rs @@ -21,7 +21,7 @@ pub trait Residual: Components + Send + Sync { fn contributions(&self) -> &[Box]; /// Evaluate the residual reduced Helmholtz energy $\beta A^\mathrm{res}$. - fn evaluate_residual>(&self, state: &StateHD) -> D + fn evaluate_residual + Copy>(&self, state: &StateHD) -> D where dyn HelmholtzEnergy: HelmholtzEnergyDual, { @@ -33,7 +33,7 @@ pub trait Residual: Components + Send + Sync { /// Evaluate the reduced Helmholtz energy of each individual contribution /// and return them together with a string representation of the contribution. - fn evaluate_residual_contributions>( + fn evaluate_residual_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(String, D)> diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index 430b6e608..99e6723ba 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -3,8 +3,8 @@ use crate::functional_contribution::*; use crate::ideal_chain_contribution::IdealChainContribution; use crate::weight_functions::{WeightFunction, WeightFunctionInfo, WeightFunctionShape}; use feos_core::{ - Contributions, EosResult, EosUnit, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, - IdealGasContribution, IdealGasContributionDual, MolarWeight, StateHD, + Components, Contributions, EosResult, EosUnit, HelmholtzEnergy, HelmholtzEnergyDual, + MolarWeight, Residual, StateHD, }; use ndarray::*; use num_dual::*; @@ -18,9 +18,11 @@ use std::fmt; use std::ops::{AddAssign, Deref, MulAssign}; use std::sync::Arc; +impl Residual for F {} + /// Wrapper struct for the [HelmholtzEnergyFunctional] trait. /// -/// Needed (for now) to generically implement the `EquationOfState` +/// Needed (for now) to generically implement the `Residual` /// trait for Helmholtz energy functionals. #[derive(Clone)] pub struct DFT(F); @@ -50,20 +52,7 @@ impl MolarWeight for DFT { } } -struct DefaultIdealGasContribution(); -impl + Copy> IdealGasContributionDual for DefaultIdealGasContribution { - fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1 { - Array1::zeros(components) - } -} - -impl fmt::Display for DefaultIdealGasContribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (default)") - } -} - -impl EquationOfState for DFT { +impl Components for DFT { fn components(&self) -> usize { self.component_index()[self.component_index().len() - 1] + 1 } @@ -71,12 +60,14 @@ impl EquationOfState for DFT { fn subset(&self, component_list: &[usize]) -> Self { (self as &T).subset(component_list) } +} +impl Residual for DFT { fn compute_max_density(&self, moles: &Array1) -> f64 { (self as &T).compute_max_density(moles) } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { unreachable!() } @@ -114,10 +105,6 @@ impl EquationOfState for DFT { )); res } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - (self as &T).ideal_gas() - } } /// Different representations for molecules within DFT. @@ -151,18 +138,6 @@ pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { /// equation of state anyways). fn compute_max_density(&self, moles: &Array1) -> f64; - /// Return the ideal gas contribution. - /// - /// Per default this function returns an ideal gas contribution - /// in which the de Broglie wavelength is 1 for every component. - /// Therefore, the correct ideal gas pressure is obtained even - /// with no explicit ideal gas term. If a more detailed model is - /// required (e.g. for the calculation of internal energies) this - /// function has to be overwritten. - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &DefaultIdealGasContribution() - } - /// Overwrite this, if the functional consists of heterosegmented chains. fn bond_lengths(&self, _temperature: f64) -> UnGraph<(), f64> { Graph::with_capacity(0, 0) From c8e453af564cd1fe9743f52d301e263610ed27f7 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 5 Jun 2023 22:10:05 +0200 Subject: [PATCH 14/47] DFT almost implemented --- Cargo.toml | 2 +- benches/contributions.rs | 2 +- benches/dual_numbers.rs | 6 +- benches/state_creation.rs | 18 +- benches/state_properties.rs | 36 +- feos-core/src/state/properties.rs | 29 +- feos-core/src/state/residual_properties.rs | 33 +- feos-derive/src/residual.rs | 36 -- feos-dft/src/adsorption/mod.rs | 228 +++++---- feos-dft/src/adsorption/pore.rs | 9 +- feos-dft/src/functional.rs | 274 ++--------- feos-dft/src/lib.rs | 1 + feos-dft/src/pdgt.rs | 20 +- feos-dft/src/{profile.rs => profile/mod.rs} | 223 +-------- feos-dft/src/profile/properties.rs | 397 ++++++++++++++++ src/eos.rs | 1 - src/gc_pcsaft/dft/parameter.rs | 8 +- src/gc_pcsaft/eos/mod.rs | 20 +- src/gc_pcsaft/eos/parameter.rs | 37 +- src/lib.rs | 9 +- src/pcsaft/dft/mod.rs | 14 +- src/pcsaft/eos/mod.rs | 131 +++--- tests/pcsaft/dft.rs | 114 +++-- tests/pcsaft/properties.rs | 3 +- tests/pcsaft/state_creation_mixture.rs | 60 +-- tests/pcsaft/state_creation_pure.rs | 490 ++++++++++---------- 26 files changed, 1045 insertions(+), 1156 deletions(-) rename feos-dft/src/{profile.rs => profile/mod.rs} (61%) create mode 100644 feos-dft/src/profile/properties.rs diff --git a/Cargo.toml b/Cargo.toml index ff44725de..0f4525df9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ inherits = "release" lto = true [features] -default = [] +default = ["pcsaft"] dft = ["feos-dft", "petgraph"] estimator = [] association = [] diff --git a/benches/contributions.rs b/benches/contributions.rs index 268ad6681..a572ed597 100644 --- a/benches/contributions.rs +++ b/benches/contributions.rs @@ -8,7 +8,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; -use feos_core::{DensityInitialization, Derivative, EquationOfState, State}; +use feos_core::{DensityInitialization, Derivative, Residual, State}; use ndarray::arr1; use quantity::si::*; use std::sync::Arc; diff --git a/benches/dual_numbers.rs b/benches/dual_numbers.rs index f78ca03f7..9b447a0b2 100644 --- a/benches/dual_numbers.rs +++ b/benches/dual_numbers.rs @@ -7,7 +7,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Derivative, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, State, StateHD, + Derivative, HelmholtzEnergy, HelmholtzEnergyDual, Residual, State, StateHD, }; use ndarray::{arr1, Array}; use num_dual::DualNum; @@ -28,7 +28,7 @@ fn state_pcsaft(parameters: PcSaftParameters) -> State { } /// Residual Helmholtz energy given an equation of state and a StateHD. -fn a_res + Copy, E: EquationOfState>(inp: (&Arc, &StateHD)) -> D +fn a_res + Copy, E: Residual>(inp: (&Arc, &StateHD)) -> D where (dyn HelmholtzEnergy + 'static): HelmholtzEnergyDual, { @@ -36,7 +36,7 @@ where } /// Benchmark for evaluation of the Helmholtz energy for different dual number types. -fn bench_dual_numbers(c: &mut Criterion, group_name: &str, state: State) { +fn bench_dual_numbers(c: &mut Criterion, group_name: &str, state: State) { let mut group = c.benchmark_group(group_name); group.bench_function("a_f64", |b| { b.iter(|| a_res((&state.eos, &state.derive0()))) diff --git a/benches/state_creation.rs b/benches/state_creation.rs index 68c7b0904..b79627f32 100644 --- a/benches/state_creation.rs +++ b/benches/state_creation.rs @@ -2,14 +2,14 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Contributions, DensityInitialization, EquationOfState, PhaseEquilibrium, State, + Contributions, DensityInitialization, PhaseEquilibrium, Residual, State, }; use ndarray::{Array, Array1}; use quantity::si::*; use std::sync::Arc; /// Evaluate NPT constructor -fn npt( +fn npt( (eos, t, p, n, rho0): ( &Arc, SINumber, @@ -22,26 +22,26 @@ fn npt( } /// Evaluate critical point constructor -fn critical_point((eos, n): (&Arc, Option<&SIArray1>)) { +fn critical_point((eos, n): (&Arc, Option<&SIArray1>)) { State::critical_point(eos, n, None, Default::default()).unwrap(); } /// Evaluate critical point constructor for binary systems at given T or p -fn critical_point_binary((eos, tp): (&Arc, SINumber)) { +fn critical_point_binary((eos, tp): (&Arc, SINumber)) { State::critical_point_binary(eos, tp, None, None, Default::default()).unwrap(); } /// VLE for pure substance for given temperature or pressure -fn pure((eos, t_or_p): (&Arc, SINumber)) { +fn pure((eos, t_or_p): (&Arc, SINumber)) { PhaseEquilibrium::pure(eos, t_or_p, None, Default::default()).unwrap(); } /// Evaluate temperature, pressure flash. -fn tp_flash((eos, t, p, feed): (&Arc, SINumber, SINumber, &SIArray1)) { +fn tp_flash((eos, t, p, feed): (&Arc, SINumber, SINumber, &SIArray1)) { PhaseEquilibrium::tp_flash(eos, t, p, feed, None, Default::default(), None).unwrap(); } -fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1)) { +fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1)) { PhaseEquilibrium::bubble_point( eos, t, @@ -53,7 +53,7 @@ fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1((eos, t, y): (&Arc, SINumber, &Array1)) { +fn dew_point((eos, t, y): (&Arc, SINumber, &Array1)) { PhaseEquilibrium::dew_point( eos, t, @@ -65,7 +65,7 @@ fn dew_point((eos, t, y): (&Arc, SINumber, &Array1)) .unwrap(); } -fn bench_states(c: &mut Criterion, group_name: &str, eos: &Arc) { +fn bench_states(c: &mut Criterion, group_name: &str, eos: &Arc) { let ncomponents = eos.components(); let x = Array::from_elem(ncomponents, 1.0 / ncomponents as f64); let n = &x * 100.0 * MOL; diff --git a/benches/state_properties.rs b/benches/state_properties.rs index 06c419fcb..43f0d2587 100644 --- a/benches/state_properties.rs +++ b/benches/state_properties.rs @@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Contributions, EquationOfState, State, + Contributions, Residual, State, }; use ndarray::arr1; use quantity::si::*; @@ -12,7 +12,7 @@ type S = State; /// Evaluate a property of a state given the EoS, the property to compute, /// temperature, volume, moles, and the contributions to consider. -fn property, Contributions) -> T>( +fn property, Contributions) -> T>( (eos, property, t, v, n, contributions): ( &Arc, F, @@ -28,7 +28,7 @@ fn property, Contributions) -> T>( /// Evaluate a property with of a state given the EoS, the property to compute, /// temperature, volume, moles. -fn property_no_contributions) -> T>( +fn property_no_contributions) -> T>( (eos, property, t, v, n): (&Arc, F, SINumber, SINumber, &SIArray1), ) -> T { let state = State::new_nvt(eos, t, v, n).unwrap(); @@ -52,7 +52,7 @@ fn properties_pcsaft(c: &mut Criterion) { let mut group = c.benchmark_group("state_properties_pcsaft_methane_ethane_propane"); group.bench_function("a", |b| { - b.iter(|| property((&eos, S::helmholtz_energy, t, v, &m, Contributions::Total))) + b.iter(|| property_no_contributions((&eos, S::residual_helmholtz_energy, t, v, &m))) }); group.bench_function("compressibility", |b| { b.iter(|| property((&eos, S::compressibility, t, v, &m, Contributions::Total))) @@ -61,19 +61,10 @@ fn properties_pcsaft(c: &mut Criterion) { b.iter(|| property_no_contributions((&eos, S::ln_phi, t, v, &m))) }); group.bench_function("c_v", |b| { - b.iter(|| property((&eos, S::c_v, t, v, &m, Contributions::ResidualNvt))) + b.iter(|| property_no_contributions((&eos, S::c_v_res, t, v, &m))) }); group.bench_function("partial_molar_volume", |b| { - b.iter(|| { - property(( - &eos, - S::partial_molar_volume, - t, - v, - &m, - Contributions::ResidualNvt, - )) - }) + b.iter(|| property_no_contributions((&eos, S::partial_molar_volume, t, v, &m))) }); } @@ -94,7 +85,7 @@ fn properties_pcsaft_polar(c: &mut Criterion) { let mut group = c.benchmark_group("state_properties_pcsaft_polar"); group.bench_function("a", |b| { - b.iter(|| property((&eos, S::helmholtz_energy, t, v, &m, Contributions::Total))) + b.iter(|| property_no_contributions((&eos, S::residual_helmholtz_energy, t, v, &m))) }); group.bench_function("compressibility", |b| { b.iter(|| property((&eos, S::compressibility, t, v, &m, Contributions::Total))) @@ -103,19 +94,10 @@ fn properties_pcsaft_polar(c: &mut Criterion) { b.iter(|| property_no_contributions((&eos, S::ln_phi, t, v, &m))) }); group.bench_function("c_v", |b| { - b.iter(|| property((&eos, S::c_v, t, v, &m, Contributions::ResidualNvt))) + b.iter(|| property_no_contributions((&eos, S::c_v_res, t, v, &m))) }); group.bench_function("partial_molar_volume", |b| { - b.iter(|| { - property(( - &eos, - S::partial_molar_volume, - t, - v, - &m, - Contributions::ResidualNvt, - )) - }) + b.iter(|| property_no_contributions((&eos, S::partial_molar_volume, t, v, &m))) }); } diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index afab24168..f59f9746a 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -322,12 +322,6 @@ impl State { -self.c_v(c) / (self.c_p(c) * self.dp_dv(c) * self.volume) } - /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$ - pub fn isothermal_compressibility(&self) -> SINumber { - let c = Contributions::Total; - -1.0 / (self.dp_dv(c) * self.volume) - } - /// Helmholtz energy $A$ evaluated for each contribution of the equation of state. pub fn helmholtz_energy_contributions(&self) -> Vec<(String, SINumber)> { let new_state = self.derive0(); @@ -345,25 +339,6 @@ impl State { res } - /// Pressure $p$ evaluated for each contribution of the equation of state. - pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { - let new_state = self.derive1(DV); - let contributions = self.eos.evaluate_residual_contributions(&new_state); - let mut res = Vec::with_capacity(contributions.len() + 1); - res.push(( - self.eos.ideal_gas_model(), - -(self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps - * SIUnit::reference_pressure(), - )); - for (s, v) in contributions { - res.push(( - s, - -(v * new_state.temperature).eps * SIUnit::reference_pressure(), - )); - } - res - } - /// Chemical potential $\mu_i$ evaluated for each contribution of the equation of state. pub fn chemical_potential_contributions(&self, component: usize) -> Vec<(String, SINumber)> { let new_state = self.derive1(DN(component)); @@ -388,7 +363,7 @@ impl State { /// /// These properties are available for equations of state /// that implement the [MolarWeight] trait. -impl State { +impl State { /// Total molar weight: $MW=\sum_ix_iMW_i$ pub fn total_molar_weight(&self) -> SINumber { (self.eos.molar_weight() * &self.molefracs).sum() @@ -413,7 +388,9 @@ impl State { pub fn massfracs(&self) -> Array1 { self.mass().to_reduced(self.total_mass()).unwrap() } +} +impl State { /// Specific entropy: $s^{(m)}=\frac{S}{m}$ pub fn specific_entropy(&self, contributions: Contributions) -> SINumber { self.molar_entropy(contributions) / self.total_molar_weight() diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 5a4ca2b49..e50af92a1 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -193,6 +193,29 @@ impl State { ) } + /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$ + pub fn isothermal_compressibility(&self) -> SINumber { + -1.0 / (self.dp_dv(Contributions::Total) * self.volume) + } + + /// Pressure $p$ evaluated for each contribution of the equation of state. + pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { + let new_state = self.derive1(DV); + let contributions = self.eos.evaluate_residual_contributions(&new_state); + let mut res = Vec::with_capacity(contributions.len() + 1); + res.push(( + "Ideal gas".into(), + self.density * SIUnit::gas_constant() * self.temperature, + )); + for (s, v) in contributions { + res.push(( + s, + -(v * new_state.temperature).eps * SIUnit::reference_pressure(), + )); + } + res + } + // entropy derivatives pub fn ds_res_dt(&self) -> SINumber { @@ -254,10 +277,8 @@ impl State { /// 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}$ pub fn dln_phi_dp(&self) -> SIArray1 { - let vi_rt = -self.dp_dni(Contributions::Total) - / self.dp_dv(Contributions::Total) - / (SIUnit::gas_constant() * self.temperature); - vi_rt - 1.0 / self.pressure(Contributions::Total) + self.partial_molar_volume() / (SIUnit::gas_constant() * self.temperature) + - 1.0 / self.pressure(Contributions::Total) } /// 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}$ @@ -309,12 +330,12 @@ impl State { + self.pressure(Contributions::Residual) * self.volume } - /// Residual internal energy: $U\text{res}(T, V, \mathbf{n})=A\text{res}+TS\text{res}$ + /// Residual internal energy: $U^\text{res}(T, V, \mathbf{n})=A^\text{res}+TS^\text{res}$ pub fn residual_internal_energy(&self) -> SINumber { self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() } - /// Residual Gibbs energy: $G\text{res}(T,p,\mathbf{n})=A\text{res}+pV-NRT-NRT \ln Z$ + /// Residual Gibbs energy: $G^\text{res}(T,p,\mathbf{n})=A^\text{res}+pV-NRT-NRT \ln Z$ pub fn residual_gibbs_energy(&self) -> SINumber { self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy() - self.total_moles diff --git a/feos-derive/src/residual.rs b/feos-derive/src/residual.rs index 98b709737..6dd9476d6 100644 --- a/feos-derive/src/residual.rs +++ b/feos-derive/src/residual.rs @@ -24,18 +24,6 @@ pub(crate) fn expand_residual(input: DeriveInput) -> syn::Result, ) -> proc_macro2::TokenStream { - // let components = variants.iter().map(|v| { - // let name = &v.ident; - // quote! { - // Self::#name(residual) => residual.components() - // } - // }); - // let subset = variants.iter().map(|v| { - // let name = &v.ident; - // quote! { - // Self::#name(residual) => Self::#name(residual.subset(component_list)) - // } - // }); let compute_max_density = variants.iter().map(|v| { let name = &v.ident; quote! { @@ -48,44 +36,20 @@ fn impl_residual( Self::#name(residual) => residual.contributions() } }); - let display = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(residual) => write!(f, "{}", residual.to_string()) - } - }); quote! { impl Residual for ResidualModel { - // fn components(&self) -> usize { - // match self { - // #(#components,)* - // } - // } fn compute_max_density(&self, moles: &Array1) -> f64 { match self { #(#compute_max_density,)* } } - // fn subset(&self, component_list: &[usize]) -> Self { - // match self { - // #(#subset,)* - // } - // } fn contributions(&self) -> &[Box] { match self { #(#contributions,)* } } } - - impl fmt::Display for ResidualModel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - #(#display,)* - } - } - } } } diff --git a/feos-dft/src/adsorption/mod.rs b/feos-dft/src/adsorption/mod.rs index 3f517a079..07e5b9148 100644 --- a/feos-dft/src/adsorption/mod.rs +++ b/feos-dft/src/adsorption/mod.rs @@ -2,7 +2,7 @@ use super::functional::{HelmholtzEnergyFunctional, DFT}; use super::solver::DFTSolver; use feos_core::{ - Contributions, DensityInitialization, EosError, EosResult, EosUnit, EquationOfState, + Components, Contributions, DensityInitialization, EosError, EosResult, EosUnit, Residual, SolverOptions, State, StateBuilder, }; use ndarray::{Array1, Dimension, Ix1, Ix3, RemoveAxis}; @@ -263,111 +263,92 @@ where solver: Option<&DFTSolver>, options: SolverOptions, ) -> EosResult> { - let moles = - functional.validate_moles(molefracs.map(|x| x * SIUnit::reference_moles()).as_ref())?; - - // calculate density profiles for the minimum and maximum pressure - let vapor_bulk = StateBuilder::new(functional) - .temperature(temperature) - .pressure(p_min) - .moles(&moles) - .vapor() - .build()?; - let liquid_bulk = StateBuilder::new(functional) - .temperature(temperature) - .pressure(p_max) - .moles(&moles) - .liquid() - .build()?; - - let mut vapor = pore.initialize(&vapor_bulk, None, None)?.solve(None)?; - let mut liquid = pore.initialize(&liquid_bulk, None, None)?.solve(solver)?; - - // calculate initial value for the molar gibbs energy - let nv = vapor.profile.bulk.density - * (vapor.profile.moles() - * vapor - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let nl = liquid.profile.bulk.density - * (liquid.profile.moles() - * liquid - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let f = |s: &PoreProfile, n: SINumber| -> EosResult<_> { - Ok(s.grand_potential.unwrap() - + s.profile.bulk.molar_gibbs_energy(Contributions::Total) * n) - }; - let mut g = (f(&liquid, nl)? - f(&vapor, nv)?) / (nl - nv); - - // update filled pore with limited step size - let mut bulk = StateBuilder::new(functional) - .temperature(temperature) - .pressure(p_max) - .moles(&moles) - .vapor() - .build()?; - let g_liquid = liquid.profile.bulk.molar_gibbs_energy(Contributions::Total); - let steps = (10.0 * (g - g_liquid)).to_reduced(g_liquid)?.abs().ceil() as usize; - let delta_g = (g - g_liquid) / steps as f64; - for i in 1..=steps { - let g_i = g_liquid + i as f64 * delta_g; - bulk = bulk.update_gibbs_energy(g_i)?; - liquid = liquid.update_bulk(&bulk).solve(solver)?; - } - - for _ in 0..options.max_iter.unwrap_or(MAX_ITER_ADSORPTION_EQUILIBRIUM) { - // update empty pore - vapor = vapor.update_bulk(&bulk).solve(None)?; - - // update filled pore - liquid = liquid.update_bulk(&bulk).solve(solver)?; - - // calculate moles - let nv = vapor.profile.bulk.density - * (vapor.profile.moles() - * vapor - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let nl = liquid.profile.bulk.density - * (liquid.profile.moles() - * liquid - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - - // check for a trivial solution - if nl.to_reduced(nv)? - 1.0 < 1e-5 { - return Err(EosError::TrivialSolution); - } - - // Newton step - let delta_g = - (vapor.grand_potential.unwrap() - liquid.grand_potential.unwrap()) / (nv - nl); - if delta_g.to_reduced(SIUnit::reference_molar_energy())?.abs() - < options.tol.unwrap_or(TOL_ADSORPTION_EQUILIBRIUM) - { - return Ok(Adsorption::new( - functional, - pore, - vec![Ok(vapor), Ok(liquid)], - )); - } - g += delta_g; - - // update bulk phase - bulk = bulk.update_gibbs_energy(g)?; - } - Err(EosError::NotConverged( - "Adsorption::phase_equilibrium".into(), - )) + unimplemented!(); + // let moles = + // functional.validate_moles(molefracs.map(|x| x * SIUnit::reference_moles()).as_ref())?; + + // // calculate density profiles for the minimum and maximum pressure + // let vapor_bulk = StateBuilder::new(functional) + // .temperature(temperature) + // .pressure(p_min) + // .moles(&moles) + // .vapor() + // .build()?; + // let liquid_bulk = StateBuilder::new(functional) + // .temperature(temperature) + // .pressure(p_max) + // .moles(&moles) + // .liquid() + // .build()?; + + // let mut vapor = pore.initialize(&vapor_bulk, None, None)?.solve(None)?; + // let mut liquid = pore.initialize(&liquid_bulk, None, None)?.solve(solver)?; + + // // calculate initial value for the molar gibbs energy + // let nv = vapor.profile.bulk.density + // * (vapor.profile.moles() * vapor.profile.bulk.partial_molar_volume()).sum(); + // let nl = liquid.profile.bulk.density + // * (liquid.profile.moles() * liquid.profile.bulk.partial_molar_volume()).sum(); + // let f = |s: &PoreProfile, n: SINumber| -> EosResult<_> { + // Ok(s.grand_potential.unwrap() + // + s.profile.bulk.molar_gibbs_energy(Contributions::Total) * n) + // }; + // let mut g = (f(&liquid, nl)? - f(&vapor, nv)?) / (nl - nv); + + // // update filled pore with limited step size + // let mut bulk = StateBuilder::new(functional) + // .temperature(temperature) + // .pressure(p_max) + // .moles(&moles) + // .vapor() + // .build()?; + // let g_liquid = liquid.profile.bulk.molar_gibbs_energy(Contributions::Total); + // let steps = (10.0 * (g - g_liquid)).to_reduced(g_liquid)?.abs().ceil() as usize; + // let delta_g = (g - g_liquid) / steps as f64; + // for i in 1..=steps { + // let g_i = g_liquid + i as f64 * delta_g; + // bulk = bulk.update_gibbs_energy(g_i)?; + // liquid = liquid.update_bulk(&bulk).solve(solver)?; + // } + + // for _ in 0..options.max_iter.unwrap_or(MAX_ITER_ADSORPTION_EQUILIBRIUM) { + // // update empty pore + // vapor = vapor.update_bulk(&bulk).solve(None)?; + + // // update filled pore + // liquid = liquid.update_bulk(&bulk).solve(solver)?; + + // // calculate moles + // let nv = vapor.profile.bulk.density + // * (vapor.profile.moles() * vapor.profile.bulk.partial_molar_volume()).sum(); + // let nl = liquid.profile.bulk.density + // * (liquid.profile.moles() * liquid.profile.bulk.partial_molar_volume()).sum(); + + // // check for a trivial solution + // if nl.to_reduced(nv)? - 1.0 < 1e-5 { + // return Err(EosError::TrivialSolution); + // } + + // // Newton step + // let delta_g = + // (vapor.grand_potential.unwrap() - liquid.grand_potential.unwrap()) / (nv - nl); + // if delta_g.to_reduced(SIUnit::reference_molar_energy())?.abs() + // < options.tol.unwrap_or(TOL_ADSORPTION_EQUILIBRIUM) + // { + // return Ok(Adsorption::new( + // functional, + // pore, + // vec![Ok(vapor), Ok(liquid)], + // )); + // } + // g += delta_g; + + // // update bulk phase + // bulk = bulk.update_gibbs_energy(g)?; + // } + // Err(EosError::NotConverged( + // "Adsorption::phase_equilibrium".into(), + // )) } pub fn pressure(&self) -> SIArray1 { @@ -391,23 +372,24 @@ where } pub fn molar_gibbs_energy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { - Ok(p) => { - if p.profile.bulk.eos.components() > 1 - && !p.profile.bulk.is_stable(SolverOptions::default()).unwrap() - { - p.profile - .bulk - .tp_flash(None, SolverOptions::default(), None) - .unwrap() - .vapor() - .molar_gibbs_energy(Contributions::Total) - } else { - p.profile.bulk.molar_gibbs_energy(Contributions::Total) - } - } - Err(_) => f64::NAN * SIUnit::reference_molar_energy(), - }) + unimplemented!(); + // SIArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { + // Ok(p) => { + // if p.profile.bulk.eos.components() > 1 + // && !p.profile.bulk.is_stable(SolverOptions::default()).unwrap() + // { + // p.profile + // .bulk + // .tp_flash(None, SolverOptions::default(), None) + // .unwrap() + // .vapor() + // .molar_gibbs_energy(Contributions::Total) + // } else { + // p.profile.bulk.molar_gibbs_energy(Contributions::Total) + // } + // } + // Err(_) => f64::NAN * SIUnit::reference_molar_energy(), + // }) } pub fn adsorption(&self) -> SIArray2 { diff --git a/feos-dft/src/adsorption/pore.rs b/feos-dft/src/adsorption/pore.rs index 5f3ac4501..3f751b5bd 100644 --- a/feos-dft/src/adsorption/pore.rs +++ b/feos-dft/src/adsorption/pore.rs @@ -264,6 +264,7 @@ fn external_potential_1d( const EPSILON_HE: f64 = 10.9; const SIGMA_HE: f64 = 2.64; +#[derive(Clone)] struct Helium { epsilon: Array1, sigma: Array1, @@ -282,10 +283,6 @@ impl HelmholtzEnergyFunctional for Helium { &[] } - fn subset(&self, _: &[usize]) -> DFT { - Self::new() - } - fn compute_max_density(&self, _: &Array1) -> f64 { 1.0 } @@ -293,6 +290,10 @@ impl HelmholtzEnergyFunctional for Helium { fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::Spherical(1) } + + fn subset(&self, _: &[usize]) -> DFT { + self.clone().into() + } } impl FluidParameters for Helium { diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index 99e6723ba..ffff885c4 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -3,7 +3,7 @@ use crate::functional_contribution::*; use crate::ideal_chain_contribution::IdealChainContribution; use crate::weight_functions::{WeightFunction, WeightFunctionInfo, WeightFunctionShape}; use feos_core::{ - Components, Contributions, EosResult, EosUnit, HelmholtzEnergy, HelmholtzEnergyDual, + Components, EosResult, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, StateHD, }; use ndarray::*; @@ -11,14 +11,34 @@ use num_dual::*; use petgraph::graph::{Graph, UnGraph}; use petgraph::visit::EdgeRef; use petgraph::Directed; -// use quantity::{QuantityArray, SIArray1, SINumber}; -use quantity::si::{SIArray, SIArray1, SINumber, SIUnit}; +use quantity::si::SIArray1; use std::borrow::Cow; -use std::fmt; -use std::ops::{AddAssign, Deref, MulAssign}; +use std::ops::{Deref, MulAssign}; use std::sync::Arc; -impl Residual for F {} +impl HelmholtzEnergyFunctional + for EquationOfState +{ + fn contributions(&self) -> &[Box] { + self.residual.contributions() + } + + fn molecule_shape(&self) -> MoleculeShape { + self.residual.molecule_shape() + } + + fn subset(&self, component_list: &[usize]) -> DFT { + Self::new( + Arc::new(self.ideal_gas.subset(component_list)), + Arc::new(self.residual.subset(component_list).0), + ) + .into() + } + + fn compute_max_density(&self, moles: &Array1) -> f64 { + self.residual.compute_max_density(moles) + } +} /// Wrapper struct for the [HelmholtzEnergyFunctional] trait. /// @@ -46,25 +66,25 @@ impl Deref for DFT { } } -impl MolarWeight for DFT { +impl MolarWeight for DFT { fn molar_weight(&self) -> SIArray1 { - (self as &T).molar_weight() + self.0.molar_weight() } } -impl Components for DFT { +impl Components for DFT { fn components(&self) -> usize { self.component_index()[self.component_index().len() - 1] + 1 } fn subset(&self, component_list: &[usize]) -> Self { - (self as &T).subset(component_list) + self.0.subset(component_list) } } -impl Residual for DFT { +impl Residual for DFT { fn compute_max_density(&self, moles: &Array1) -> f64 { - (self as &T).compute_max_density(moles) + self.0.compute_max_density(moles) } fn contributions(&self) -> &[Box] { @@ -75,7 +95,8 @@ impl Residual for DFT { where dyn HelmholtzEnergy: HelmholtzEnergyDual, { - self.contributions() + self.0 + .contributions() .iter() .map(|c| (c as &dyn HelmholtzEnergy).helmholtz_energy(state)) .sum::() @@ -90,6 +111,7 @@ impl Residual for DFT { dyn HelmholtzEnergy: HelmholtzEnergyDual, { let mut res: Vec<(String, D)> = self + .0 .contributions() .iter() .map(|c| { @@ -107,6 +129,16 @@ impl Residual for DFT { } } +impl IdealGas for DFT { + fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { + self.0.de_broglie_wavelength(temperature) + } + + fn ideal_gas_model(&self) -> String { + self.0.ideal_gas_model() + } +} + /// Different representations for molecules within DFT. pub enum MoleculeShape<'a> { /// For spherical molecules, the number of components. @@ -171,218 +203,10 @@ pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { fn ideal_chain_contribution(&self) -> IdealChainContribution { IdealChainContribution::new(&self.component_index(), &self.m()) } -} - -impl DFT { - /// Calculate the grand potential density $\omega$. - pub fn grand_potential_density( - &self, - temperature: SINumber, - density: &SIArray, - convolver: &Arc>, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - // Calculate residual Helmholtz energy density and functional derivative - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let rho = density.to_reduced(SIUnit::reference_density())?; - let (mut f, dfdrho) = self.functional_derivative(t, &rho, convolver)?; - - // Calculate the grand potential density - for ((rho, dfdrho), &m) in rho - .outer_iter() - .zip(dfdrho.outer_iter()) - .zip(self.m().iter()) - { - f -= &((&dfdrho + m) * &rho); - } - - let bond_lengths = self.bond_lengths(t); - for segment in bond_lengths.node_indices() { - let n = bond_lengths.neighbors(segment).count(); - f += &(&rho.index_axis(Axis(0), segment.index()) * (0.5 * n as f64)); - } - - Ok(f * t * SIUnit::reference_pressure()) - } - - pub(crate) fn ideal_gas_contribution( - &self, - temperature: f64, - density: &Array, - ) -> Array - where - D: Dimension, - D::Larger: Dimension, - { - let n = self.components(); - let ig = self.ideal_gas(); - let lambda = ig.de_broglie_wavelength(temperature, n); - let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); - for (i, rhoi) in density.outer_iter().enumerate() { - phi += &rhoi.mapv(|rhoi| (rhoi.ln() + lambda[i] - 1.0) * rhoi); - } - phi * temperature - } - - fn ideal_gas_contribution_dual( - &self, - temperature: Dual64, - density: &Array, - ) -> Array - where - D: Dimension, - D::Larger: Dimension, - { - let n = self.components(); - let ig = self.ideal_gas(); - let lambda = ig.de_broglie_wavelength(temperature, n); - let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); - for (i, rhoi) in density.outer_iter().enumerate() { - phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); - } - phi * temperature - } - - fn intrinsic_helmholtz_energy_density( - &self, - temperature: N, - density: &Array, - convolver: &Arc>, - ) -> EosResult> - where - N: DualNum + Copy + ScalarOperand, - dyn FunctionalContribution: FunctionalContributionDual, - D: Dimension, - D::Larger: Dimension, - { - let density_dual = density.mapv(N::from); - let weighted_densities = convolver.weighted_densities(&density_dual); - let functional_contributions = self.contributions(); - let mut helmholtz_energy_density: Array = self - .ideal_chain_contribution() - .calculate_helmholtz_energy_density(&density.mapv(N::from))?; - for (c, wd) in functional_contributions.iter().zip(weighted_densities) { - let nwd = wd.shape()[0]; - let ngrid = wd.len() / nwd; - helmholtz_energy_density - .view_mut() - .into_shape(ngrid) - .unwrap() - .add_assign(&c.calculate_helmholtz_energy_density( - temperature, - wd.into_shape((nwd, ngrid)).unwrap().view(), - )?); - } - Ok(helmholtz_energy_density * temperature) - } - - /// Calculate the entropy density $s$. - /// - /// Untested with heterosegmented functionals. - pub fn entropy_density( - &self, - temperature: f64, - density: &Array, - convolver: &Arc>, - contributions: Contributions, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - let temperature_dual = Dual64::from(temperature).derivative(); - let mut helmholtz_energy_density = - self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?; - match contributions { - Contributions::Total => { - helmholtz_energy_density += &self.ideal_gas_contribution_dual::(temperature_dual, density); - }, - Contributions::ResidualNpt|Contributions::IdealGas => panic!("Entropy density can only be calculated for Contributions::Residual or Contributions::Total"), - Contributions::ResidualNvt => (), - } - Ok(helmholtz_energy_density.mapv(|f| -f.eps)) - } - - /// Calculate the individual contributions to the entropy density. - /// - /// Untested with heterosegmented functionals. - pub fn entropy_density_contributions( - &self, - temperature: f64, - density: &Array, - convolver: &Arc>, - ) -> EosResult>> - where - D: Dimension, - D::Larger: Dimension, - ::Larger: Dimension, - { - let density_dual = density.mapv(Dual64::from); - let temperature_dual = Dual64::from(temperature).derivative(); - let weighted_densities = convolver.weighted_densities(&density_dual); - let functional_contributions = self.contributions(); - let mut helmholtz_energy_density: Vec> = - Vec::with_capacity(functional_contributions.len() + 1); - helmholtz_energy_density.push( - self.ideal_chain_contribution() - .calculate_helmholtz_energy_density(&density.mapv(Dual64::from))?, - ); - - for (c, wd) in functional_contributions.iter().zip(weighted_densities) { - let nwd = wd.shape()[0]; - let ngrid = wd.len() / nwd; - helmholtz_energy_density.push( - c.calculate_helmholtz_energy_density( - temperature_dual, - wd.into_shape((nwd, ngrid)).unwrap().view(), - )? - .into_shape(density.raw_dim().remove_axis(Axis(0))) - .unwrap(), - ); - } - Ok(helmholtz_energy_density - .iter() - .map(|v| v.mapv(|f| -(f * temperature_dual).eps)) - .collect()) - } - - /// Calculate the internal energy density $u$. - /// - /// Untested with heterosegmented functionals. - pub fn internal_energy_density( - &self, - temperature: f64, - density: &Array, - external_potential: &Array, - convolver: &Arc>, - contributions: Contributions, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - let temperature_dual = Dual64::from(temperature).derivative(); - let mut helmholtz_energy_density_dual = - self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?; - match contributions { - Contributions::Total => { - helmholtz_energy_density_dual += &self.ideal_gas_contribution_dual::(temperature_dual, density); - }, - Contributions::ResidualNpt|Contributions::IdealGas => panic!("Internal energy density can only be calculated for Contributions::Residual or Contributions::Total"), - Contributions::ResidualNvt => (), - } - let helmholtz_energy_density = helmholtz_energy_density_dual - .mapv(|f| f.re - f.eps * temperature) - + (external_potential * density).sum_axis(Axis(0)) * temperature; - Ok(helmholtz_energy_density) - } - /// Calculate the (residual) functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. + /// Calculate the (residual) intrinsic functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. #[allow(clippy::type_complexity)] - pub fn functional_derivative( + fn functional_derivative( &self, temperature: f64, density: &Array, @@ -417,7 +241,7 @@ impl DFT { } #[allow(clippy::type_complexity)] - pub(crate) fn functional_derivative_dual( + fn functional_derivative_dual( &self, temperature: f64, density: &Array, @@ -454,7 +278,7 @@ impl DFT { } /// Calculate the bond integrals $I_{\alpha\alpha'}(\mathbf{r})$ - pub fn bond_integrals( + fn bond_integrals( &self, temperature: f64, exponential: &Array, diff --git a/feos-dft/src/lib.rs b/feos-dft/src/lib.rs index 2962fbeea..4323b2303 100644 --- a/feos-dft/src/lib.rs +++ b/feos-dft/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::suspicious_operation_groupings)] #![allow(clippy::too_many_arguments)] #![allow(clippy::new_ret_no_self)] +#![allow(deprecated)] pub mod adsorption; mod convolver; diff --git a/feos-dft/src/pdgt.rs b/feos-dft/src/pdgt.rs index 44dc07a9d..30b0af291 100644 --- a/feos-dft/src/pdgt.rs +++ b/feos-dft/src/pdgt.rs @@ -1,7 +1,7 @@ use super::functional::{HelmholtzEnergyFunctional, DFT}; use super::functional_contribution::FunctionalContribution; use super::weight_functions::WeightFunctionInfo; -use feos_core::{Contributions, EosResult, EosUnit, EquationOfState, PhaseEquilibrium}; +use feos_core::{Components, Contributions, EosResult, EosUnit, PhaseEquilibrium}; use ndarray::*; use num_dual::Dual2_64; use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; @@ -167,20 +167,14 @@ impl DFT { .ideal_chain_contribution() .helmholtz_energy_density::(vle.vapor().temperature, &density)?; - let t = vle - .vapor() - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let rho = density.to_reduced(SIUnit::reference_density())?; - delta_omega += - &(self.ideal_gas_contribution::(t, &rho) * SIUnit::reference_pressure()); - // calculate excess grand potential density - let mu = vle.vapor().chemical_potential(Contributions::Total); + let mu_res = vle.vapor().residual_chemical_potential(); for i in 0..self.components() { - let rhoi = density.index_axis(Axis(0), i); - let mui = mu.get(i); - delta_omega -= &(&rhoi * mui); + let rhoi = density.index_axis(Axis(0), i).to_owned(); + let rhoi_b = vle.vapor().partial_density.get(i); + let mui_res = mu_res.get(i); + let kt = SIUnit::gas_constant() * vle.vapor().temperature; + delta_omega += &(&rhoi * (kt * rhoi.to_reduced(rhoi_b)?.mapv(f64::ln) - mui_res)); } delta_omega += vle.vapor().pressure(Contributions::Total); diff --git a/feos-dft/src/profile.rs b/feos-dft/src/profile/mod.rs similarity index 61% rename from feos-dft/src/profile.rs rename to feos-dft/src/profile/mod.rs index 1c8ae407e..e51c8323d 100644 --- a/feos-dft/src/profile.rs +++ b/feos-dft/src/profile/mod.rs @@ -1,18 +1,16 @@ -use crate::convolver::{BulkConvolver, Convolver, ConvolverFFT}; +use crate::convolver::{BulkConvolver, Convolver}; use crate::functional::{HelmholtzEnergyFunctional, DFT}; use crate::geometry::Grid; use crate::solver::{DFTSolver, DFTSolverLog}; -use crate::weight_functions::WeightFunctionInfo; -use feos_core::{Contributions, EosError, EosResult, EosUnit, EquationOfState, State, Verbosity}; -use ndarray::{ - Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3, RemoveAxis, -}; -use num_dual::Dual64; -use quantity::si::{SIArray, SIArray1, SIArray2, SINumber, SIUnit}; +use feos_core::{Components, EosError, EosResult, EosUnit, State}; +use ndarray::{Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3}; +use quantity::si::{SIArray, SIArray1, SINumber, SIUnit}; use quantity::Quantity; use std::ops::MulAssign; use std::sync::Arc; +mod properties; + pub(crate) const MAX_POTENTIAL: f64 = 50.0; #[cfg(feature = "rayon")] pub(crate) const CUTOFF_RADIUS: f64 = 14.0; @@ -291,10 +289,10 @@ where self.moles().sum() } - /// Return the chemical potential of the system - pub fn chemical_potential(&self) -> SIArray1 { - self.bulk.chemical_potential(Contributions::Total) - } + // /// Return the chemical potential of the system + // pub fn chemical_potential(&self) -> SIArray1 { + // self.bulk.chemical_potential(Contributions::Total) + // } } impl Clone for DFTProfile { @@ -326,16 +324,6 @@ where .weighted_densities(&self.density.to_reduced(SIUnit::reference_density())?)) } - pub fn functional_derivative(&self) -> EosResult> { - let (_, dfdrho) = self.dft.functional_derivative( - self.temperature - .to_reduced(SIUnit::reference_temperature())?, - &self.density.to_reduced(SIUnit::reference_density())?, - &self.convolver, - )?; - Ok(dfdrho) - } - #[allow(clippy::type_complexity)] pub fn residual(&self, log: bool) -> EosResult<(Array, Array1, f64)> { // Read from profile @@ -470,194 +458,3 @@ where Ok(()) } } - -impl DFTProfile -where - D::Larger: Dimension, - D::Smaller: Dimension, - ::Larger: Dimension, -{ - pub fn entropy_density(&self, contributions: Contributions) -> EosResult> { - // initialize convolver - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); - - Ok(self.dft.entropy_density( - t, - &self.density.to_reduced(SIUnit::reference_density())?, - &convolver, - contributions, - )? * (SIUnit::reference_entropy() / SIUnit::reference_volume())) - } - - pub fn entropy(&self, contributions: Contributions) -> EosResult { - Ok(self.integrate(&self.entropy_density(contributions)?)) - } - - pub fn grand_potential_density(&self) -> EosResult> { - self.dft - .grand_potential_density(self.temperature, &self.density, &self.convolver) - } - - pub fn grand_potential(&self) -> EosResult { - Ok(self.integrate(&self.grand_potential_density()?)) - } - - pub fn internal_energy(&self, contributions: Contributions) -> EosResult { - // initialize convolver - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); - - let internal_energy_density = self.dft.internal_energy_density( - t, - &self.density.to_reduced(SIUnit::reference_density())?, - &self.external_potential, - &convolver, - contributions, - )? * SIUnit::reference_pressure(); - Ok(self.integrate(&internal_energy_density)) - } - - fn density_derivative(&self, lhs: &Array) -> EosResult> { - let rho = self.density.to_reduced(SIUnit::reference_density())?; - let partial_density = self - .bulk - .partial_density - .to_reduced(SIUnit::reference_density())?; - let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); - - let second_partial_derivatives = self.second_partial_derivatives(&rho)?; - let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &rho_bulk, false)?; - - let rhs = |x: &_| { - let delta_functional_derivative = - self.delta_functional_derivative(x, &second_partial_derivatives); - let mut xm = x.clone(); - xm.outer_iter_mut() - .zip(self.dft.m().iter()) - .for_each(|(mut x, &m)| x *= m); - let delta_i = self.delta_bond_integrals(&exp_dfdrho, &delta_functional_derivative); - xm + (delta_functional_derivative - delta_i) * &rho - }; - let mut log = DFTSolverLog::new(Verbosity::None); - Self::gmres(rhs, lhs, 200, 1e-13, &mut log) - } - - /// Return the partial derivatives of the density profiles w.r.t. the chemical potentials $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial\mu_k}\right)_T$ - pub fn drho_dmu(&self) -> EosResult::Larger>> { - let shape = self.density.shape(); - let shape: Vec<_> = std::iter::once(&shape[0]).chain(shape).copied().collect(); - let mut drho_dmu = Array::zeros(shape).into_dimensionality().unwrap(); - for (k, mut d) in drho_dmu.outer_iter_mut().enumerate() { - let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; - for (i, mut l) in lhs.outer_iter_mut().enumerate() { - if i != k { - l.fill(0.0); - } - } - d.assign(&self.density_derivative(&lhs)?); - } - Ok(drho_dmu - * (SIUnit::reference_density() / SIUnit::reference_molar_entropy() / self.temperature)) - } - - /// Return the partial derivatives of the number of moles w.r.t. the chemical potentials $\left(\frac{\partial N_i}{\partial\mu_k}\right)_T$ - pub fn dn_dmu(&self) -> EosResult { - let drho_dmu = self.drho_dmu()?; - let n = drho_dmu.shape()[0]; - let dn_dmu = SIArray2::from_shape_fn([n; 2], |(i, j)| { - self.integrate(&drho_dmu.index_axis(Axis_nd(0), i).index_axis(Axis_nd(0), j)) - }); - Ok(dn_dmu) - } - - /// Return the partial derivatives of the density profiles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial p}\right)_{T,\mathbf{x}}$ - pub fn drho_dp(&self) -> EosResult> { - let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; - let v = self - .bulk - .partial_molar_volume(Contributions::Total) - .to_reduced(SIUnit::reference_volume() / SIUnit::reference_moles())?; - for (mut l, &c) in lhs.outer_iter_mut().zip(self.dft.component_index().iter()) { - l *= v[c]; - } - self.density_derivative(&lhs) - .map(|x| x / (SIUnit::reference_molar_entropy() * self.temperature)) - } - - /// Return the partial derivatives of the number of moles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial N_i}{\partial p}\right)_{T,\mathbf{x}}$ - pub fn dn_dp(&self) -> EosResult { - Ok(self.integrate_segments(&self.drho_dp()?)) - } - - /// Return the partial derivatives of the density profiles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial T}\right)_{p,\mathbf{x}}$ - /// - /// Not compatible with heterosegmented DFT. - pub fn drho_dt(&self) -> EosResult> { - let rho = self.density.to_reduced(SIUnit::reference_density())?; - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - - // calculate temperature derivative of functional derivative - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver: Arc> = - ConvolverFFT::plan(&self.grid, &weight_functions, None); - let (_, dfdrhodt) = self.dft.functional_derivative_dual(t, &rho, &convolver)?; - - // calculate temperature derivative of bulk functional derivative - let partial_density = self - .bulk - .partial_density - .to_reduced(SIUnit::reference_density())?; - let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); - let bulk_convolver = BulkConvolver::new(weight_functions); - let (_, dfdrhodt_bulk) = - self.dft - .functional_derivative_dual(t, &rho_bulk, &bulk_convolver)?; - - // solve for drho_dt - let x = (self.bulk.partial_molar_volume(Contributions::Total) - * self.bulk.dp_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_entropy())?; - let mut lhs = dfdrhodt.mapv(|d| d.eps); - lhs.outer_iter_mut() - .zip(dfdrhodt_bulk.into_iter()) - .zip(x.into_iter()) - .for_each(|((mut lhs, d), x)| lhs -= d.eps - x); - lhs.outer_iter_mut() - .zip(rho.outer_iter()) - .zip(rho_bulk.into_iter()) - .zip(self.dft.m().iter()) - .for_each(|(((mut lhs, rho), rho_b), &m)| lhs += &((&rho / rho_b).mapv(f64::ln) * m)); - - lhs *= &(-&rho / t); - self.density_derivative(&lhs) - .map(|x| x * (SIUnit::reference_density() / SIUnit::reference_temperature())) - } - - /// Return the partial derivatives of the number of moles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial N_i}{\partial T}\right)_{p,\mathbf{x}}$ - /// - /// Not compatible with heterosegmented DFT. - pub fn dn_dt(&self) -> EosResult { - Ok(self.integrate_segments(&self.drho_dt()?)) - } -} diff --git a/feos-dft/src/profile/properties.rs b/feos-dft/src/profile/properties.rs new file mode 100644 index 000000000..48b4475df --- /dev/null +++ b/feos-dft/src/profile/properties.rs @@ -0,0 +1,397 @@ +use super::DFTProfile; +use crate::convolver::{BulkConvolver, Convolver}; +use crate::functional_contribution::{FunctionalContribution, FunctionalContributionDual}; +use crate::{ConvolverFFT, DFTSolverLog, HelmholtzEnergyFunctional, WeightFunctionInfo}; +use feos_core::{Contributions, EosResult, EosUnit, IdealGas, Verbosity}; +use ndarray::{Array, Axis, Dimension, RemoveAxis, ScalarOperand}; +use num_dual::{Dual64, DualNum}; +use quantity::si::{SIArray, SIArray1, SIArray2, SINumber, SIUnit}; +use std::ops::AddAssign; +use std::sync::Arc; + +impl DFTProfile +where + D::Larger: Dimension, +{ + /// Calculate the grand potential density $\omega$. + pub fn grand_potential_density(&self) -> EosResult> { + // Calculate residual Helmholtz energy density and functional derivative + let t = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let (mut f, dfdrho) = self.dft.functional_derivative(t, &rho, &self.convolver)?; + + // Calculate the grand potential density + for ((rho, dfdrho), &m) in rho + .outer_iter() + .zip(dfdrho.outer_iter()) + .zip(self.dft.m().iter()) + { + f -= &((&dfdrho + m) * &rho); + } + + let bond_lengths = self.dft.bond_lengths(t); + for segment in bond_lengths.node_indices() { + let n = bond_lengths.neighbors(segment).count(); + f += &(&rho.index_axis(Axis(0), segment.index()) * (0.5 * n as f64)); + } + + Ok(f * t * SIUnit::reference_pressure()) + } + + /// Calculate the grand potential $\Omega$. + pub fn grand_potential(&self) -> EosResult { + Ok(self.integrate(&self.grand_potential_density()?)) + } + + /// Calculate the (residual) intrinsic functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. + pub fn functional_derivative(&self) -> EosResult> { + let (_, dfdrho) = self.dft.functional_derivative( + self.temperature + .to_reduced(SIUnit::reference_temperature())?, + &self.density.to_reduced(SIUnit::reference_density())?, + &self.convolver, + )?; + Ok(dfdrho) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn intrinsic_helmholtz_energy_density( + &self, + temperature: N, + density: &Array, + convolver: &Arc>, + ) -> EosResult> + where + N: DualNum + Copy + ScalarOperand, + dyn FunctionalContribution: FunctionalContributionDual, + { + let density_dual = density.mapv(N::from); + let weighted_densities = convolver.weighted_densities(&density_dual); + let functional_contributions = self.dft.contributions(); + let mut helmholtz_energy_density: Array = self + .dft + .ideal_chain_contribution() + .calculate_helmholtz_energy_density(&density.mapv(N::from))?; + for (c, wd) in functional_contributions.iter().zip(weighted_densities) { + let nwd = wd.shape()[0]; + let ngrid = wd.len() / nwd; + helmholtz_energy_density + .view_mut() + .into_shape(ngrid) + .unwrap() + .add_assign(&c.calculate_helmholtz_energy_density( + temperature, + wd.into_shape((nwd, ngrid)).unwrap().view(), + )?); + } + Ok(helmholtz_energy_density * temperature) + } + + /// Calculate the residual entropy density $s^\mathrm{res}(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn residual_entropy_density(&self) -> EosResult> { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let helmholtz_energy_density = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + Ok(helmholtz_energy_density.mapv(|f| -f.eps) + * (SIUnit::reference_entropy() / SIUnit::reference_volume())) + } + + /// Calculate the individual contributions to the entropy density. + /// + /// Untested with heterosegmented functionals. + pub fn entropy_density_contributions( + &self, + temperature: f64, + density: &Array, + convolver: &Arc>, + ) -> EosResult>> { + let density_dual = density.mapv(Dual64::from); + let temperature_dual = Dual64::from(temperature).derivative(); + let weighted_densities = convolver.weighted_densities(&density_dual); + let functional_contributions = self.dft.contributions(); + let mut helmholtz_energy_density: Vec> = + Vec::with_capacity(functional_contributions.len() + 1); + helmholtz_energy_density.push( + self.dft + .ideal_chain_contribution() + .calculate_helmholtz_energy_density(&density.mapv(Dual64::from))?, + ); + + for (c, wd) in functional_contributions.iter().zip(weighted_densities) { + let nwd = wd.shape()[0]; + let ngrid = wd.len() / nwd; + helmholtz_energy_density.push( + c.calculate_helmholtz_energy_density( + temperature_dual, + wd.into_shape((nwd, ngrid)).unwrap().view(), + )? + .into_shape(density.raw_dim().remove_axis(Axis(0))) + .unwrap(), + ); + } + Ok(helmholtz_energy_density + .iter() + .map(|v| v.mapv(|f| -(f * temperature_dual).eps)) + .collect()) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn ideal_gas_contribution_dual( + &self, + temperature: Dual64, + density: &Array, + ) -> Array { + let lambda = self.dft.de_broglie_wavelength(temperature); + let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); + for (i, rhoi) in density.outer_iter().enumerate() { + phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); + } + phi * temperature + } + + /// Calculate the entropy density $s(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn entropy_density(&self, contributions: Contributions) -> EosResult> { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let mut helmholtz_energy_density = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + match contributions { + Contributions::Total => { + helmholtz_energy_density += &self.ideal_gas_contribution_dual(temperature_dual, &density); + }, + Contributions::IdealGas => panic!("Entropy density can only be calculated for Contributions::Residual or Contributions::Total"), + Contributions::Residual => (), + } + Ok(helmholtz_energy_density.mapv(|f| -f.eps) + * (SIUnit::reference_entropy() / SIUnit::reference_volume())) + } + + /// Calculate the entropy $S$. + /// + /// Untested with heterosegmented functionals. + pub fn entropy(&self, contributions: Contributions) -> EosResult { + Ok(self.integrate(&self.entropy_density(contributions)?)) + } + + /// Calculate the internal energy density $u(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn internal_energy_density(&self, contributions: Contributions) -> EosResult> + where + D: Dimension, + D::Larger: Dimension, + { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let mut helmholtz_energy_density_dual = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + match contributions { + Contributions::Total => { + helmholtz_energy_density_dual += &self.ideal_gas_contribution_dual(temperature_dual, &density); + }, + Contributions::IdealGas => panic!("Internal energy density can only be calculated for Contributions::Residual or Contributions::Total"), + Contributions::Residual => (), + } + let helmholtz_energy_density = helmholtz_energy_density_dual + .mapv(|f| f.re - f.eps * temperature) + + (&self.external_potential * density).sum_axis(Axis(0)) * temperature; + Ok(helmholtz_energy_density * (SIUnit::reference_energy() / SIUnit::reference_volume())) + } + + /// Calculate the internal energy $U$. + /// + /// Untested with heterosegmented functionals. + pub fn internal_energy(&self, contributions: Contributions) -> EosResult { + Ok(self.integrate(&self.internal_energy_density(contributions)?)) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn density_derivative(&self, lhs: &Array) -> EosResult> { + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let partial_density = self + .bulk + .partial_density + .to_reduced(SIUnit::reference_density())?; + let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); + + let second_partial_derivatives = self.second_partial_derivatives(&rho)?; + let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &rho_bulk, false)?; + + let rhs = |x: &_| { + let delta_functional_derivative = + self.delta_functional_derivative(x, &second_partial_derivatives); + let mut xm = x.clone(); + xm.outer_iter_mut() + .zip(self.dft.m().iter()) + .for_each(|(mut x, &m)| x *= m); + let delta_i = self.delta_bond_integrals(&exp_dfdrho, &delta_functional_derivative); + xm + (delta_functional_derivative - delta_i) * &rho + }; + let mut log = DFTSolverLog::new(Verbosity::None); + Self::gmres(rhs, lhs, 200, 1e-13, &mut log) + } + + /// Return the partial derivatives of the density profiles w.r.t. the chemical potentials $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial\mu_k}\right)_T$ + pub fn drho_dmu(&self) -> EosResult::Larger>> { + let shape = self.density.shape(); + let shape: Vec<_> = std::iter::once(&shape[0]).chain(shape).copied().collect(); + let mut drho_dmu = Array::zeros(shape).into_dimensionality().unwrap(); + for (k, mut d) in drho_dmu.outer_iter_mut().enumerate() { + let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; + for (i, mut l) in lhs.outer_iter_mut().enumerate() { + if i != k { + l.fill(0.0); + } + } + d.assign(&self.density_derivative(&lhs)?); + } + Ok(drho_dmu + * (SIUnit::reference_density() / SIUnit::reference_molar_entropy() / self.temperature)) + } + + /// Return the partial derivatives of the number of moles w.r.t. the chemical potentials $\left(\frac{\partial N_i}{\partial\mu_k}\right)_T$ + pub fn dn_dmu(&self) -> EosResult { + let drho_dmu = self.drho_dmu()?; + let n = drho_dmu.shape()[0]; + let dn_dmu = SIArray2::from_shape_fn([n; 2], |(i, j)| { + self.integrate(&drho_dmu.index_axis(Axis(0), i).index_axis(Axis(0), j)) + }); + Ok(dn_dmu) + } + + /// Return the partial derivatives of the density profiles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial p}\right)_{T,\mathbf{x}}$ + pub fn drho_dp(&self) -> EosResult> { + let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; + let v = self + .bulk + .partial_molar_volume() + .to_reduced(SIUnit::reference_volume() / SIUnit::reference_moles())?; + for (mut l, &c) in lhs.outer_iter_mut().zip(self.dft.component_index().iter()) { + l *= v[c]; + } + self.density_derivative(&lhs) + .map(|x| x / (SIUnit::reference_molar_entropy() * self.temperature)) + } + + /// Return the partial derivatives of the number of moles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial N_i}{\partial p}\right)_{T,\mathbf{x}}$ + pub fn dn_dp(&self) -> EosResult { + Ok(self.integrate_segments(&self.drho_dp()?)) + } + + /// Return the partial derivatives of the density profiles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial T}\right)_{p,\mathbf{x}}$ + /// + /// Not compatible with heterosegmented DFT. + pub fn drho_dt(&self) -> EosResult> { + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let t = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + + // calculate temperature derivative of functional derivative + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(Dual64::from(t).derivative())) + .collect(); + let convolver: Arc> = + ConvolverFFT::plan(&self.grid, &weight_functions, None); + let (_, dfdrhodt) = self.dft.functional_derivative_dual(t, &rho, &convolver)?; + + // calculate temperature derivative of bulk functional derivative + let partial_density = self + .bulk + .partial_density + .to_reduced(SIUnit::reference_density())?; + let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); + let bulk_convolver = BulkConvolver::new(weight_functions); + let (_, dfdrhodt_bulk) = + self.dft + .functional_derivative_dual(t, &rho_bulk, &bulk_convolver)?; + + // solve for drho_dt + let x = (self.bulk.partial_molar_volume() * self.bulk.dp_dt(Contributions::Total)) + .to_reduced(SIUnit::reference_molar_entropy())?; + let mut lhs = dfdrhodt.mapv(|d| d.eps); + lhs.outer_iter_mut() + .zip(dfdrhodt_bulk.into_iter()) + .zip(x.into_iter()) + .for_each(|((mut lhs, d), x)| lhs -= d.eps - x); + lhs.outer_iter_mut() + .zip(rho.outer_iter()) + .zip(rho_bulk.into_iter()) + .zip(self.dft.m().iter()) + .for_each(|(((mut lhs, rho), rho_b), &m)| lhs += &((&rho / rho_b).mapv(f64::ln) * m)); + + lhs *= &(-&rho / t); + self.density_derivative(&lhs) + .map(|x| x * (SIUnit::reference_density() / SIUnit::reference_temperature())) + } + + /// Return the partial derivatives of the number of moles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial N_i}{\partial T}\right)_{p,\mathbf{x}}$ + /// + /// Not compatible with heterosegmented DFT. + pub fn dn_dt(&self) -> EosResult { + Ok(self.integrate_segments(&self.drho_dt()?)) + } +} diff --git a/src/eos.rs b/src/eos.rs index d386b45bd..a6e5c9f85 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -17,7 +17,6 @@ use feos_derive::{Components, IdealGas, Residual}; use ndarray::Array1; use num_dual::DualNum; use quantity::si::*; -use std::fmt; /// Collection of different [EquationOfState] implementations. /// diff --git a/src/gc_pcsaft/dft/parameter.rs b/src/gc_pcsaft/dft/parameter.rs index d0931dfac..04211ec20 100644 --- a/src/gc_pcsaft/dft/parameter.rs +++ b/src/gc_pcsaft/dft/parameter.rs @@ -1,6 +1,5 @@ use crate::association::AssociationParameters; use crate::gc_pcsaft::record::GcPcSaftRecord; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, ChemicalRecord, ParameterError, ParameterHetero, SegmentRecord, }; @@ -27,19 +26,18 @@ pub struct GcPcSaftFunctionalParameters { pub sigma_ij: Array2, pub epsilon_k_ij: Array2, chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, } impl ParameterHetero for GcPcSaftFunctionalParameters { type Chemical = ChemicalRecord; type Pure = GcPcSaftRecord; - type IdealGas = JobackRecord; type Binary = f64; fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result { let chemical_records: Vec<_> = chemical_records.into_iter().map(|cr| cr.into()).collect(); @@ -155,7 +153,7 @@ impl ParameterHetero for GcPcSaftFunctionalParameters { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ) { ( diff --git a/src/gc_pcsaft/eos/mod.rs b/src/gc_pcsaft/eos/mod.rs index 67be9e25c..32b917dc9 100644 --- a/src/gc_pcsaft/eos/mod.rs +++ b/src/gc_pcsaft/eos/mod.rs @@ -1,8 +1,7 @@ use crate::association::Association; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::ParameterHetero; -use feos_core::{EquationOfState, HelmholtzEnergy, IdealGasContribution, MolarWeight}; +use feos_core::{Components, HelmholtzEnergy, MolarWeight, Residual}; use ndarray::Array1; use quantity::si::*; use std::f64::consts::FRAC_PI_6; @@ -43,7 +42,6 @@ pub struct GcPcSaft { pub parameters: Arc, options: GcPcSaftOptions, contributions: Vec>, - joback: Joback, } impl GcPcSaft { @@ -72,18 +70,14 @@ impl GcPcSaft { contributions.push(Box::new(Dipole::new(¶meters))) } Self { - parameters: parameters.clone(), + parameters, options, contributions, - joback: parameters.joback_records.clone().map_or_else( - || Joback::default(parameters.chemical_records.len()), - Joback::new, - ), } } } -impl EquationOfState for GcPcSaft { +impl Components for GcPcSaft { fn components(&self) -> usize { self.parameters.molarweight.len() } @@ -94,7 +88,9 @@ impl EquationOfState for GcPcSaft { self.options, ) } +} +impl Residual for GcPcSaft { fn compute_max_density(&self, moles: &Array1) -> f64 { let p = &self.parameters; let moles_segments: Array1 = p.component_index.iter().map(|&i| moles[i]).collect(); @@ -102,13 +98,9 @@ impl EquationOfState for GcPcSaft { / (FRAC_PI_6 * &p.m * p.sigma.mapv(|v| v.powi(3)) * moles_segments).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } } impl MolarWeight for GcPcSaft { diff --git a/src/gc_pcsaft/eos/parameter.rs b/src/gc_pcsaft/eos/parameter.rs index e71bca0b0..537b0ba64 100644 --- a/src/gc_pcsaft/eos/parameter.rs +++ b/src/gc_pcsaft/eos/parameter.rs @@ -1,10 +1,9 @@ use crate::association::AssociationParameters; use crate::gc_pcsaft::record::GcPcSaftRecord; use crate::hard_sphere::{HardSphereProperties, MonomerShape}; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ - BinaryRecord, ChemicalRecord, FromSegments, Identifier, ParameterError, ParameterHetero, - SegmentCount, SegmentRecord, + BinaryRecord, ChemicalRecord, Identifier, ParameterError, ParameterHetero, SegmentCount, + SegmentRecord, }; use indexmap::IndexMap; use ndarray::{Array1, Array2}; @@ -87,20 +86,18 @@ pub struct GcPcSaftEosParameters { pub epsilon_k_ij: Array2, pub chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, - pub joback_records: Option>, } impl ParameterHetero for GcPcSaftEosParameters { type Chemical = GcPcSaftChemicalRecord; type Pure = GcPcSaftRecord; - type IdealGas = JobackRecord; type Binary = f64; fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result { let chemical_records: Vec<_> = chemical_records.into_iter().map(|c| c.into()).collect(); @@ -123,8 +120,6 @@ impl ParameterHetero for GcPcSaftEosParameters { let mut phi = Vec::new(); - let mut joback_records = Vec::new(); - for (i, chemical_record) in chemical_records.iter().cloned().enumerate() { let mut segment_indices = IndexMap::with_capacity(segment_records.len()); let segment_map = chemical_record.segment_map(&segment_records)?; @@ -179,18 +174,6 @@ impl ParameterHetero for GcPcSaftEosParameters { *bond += count; } } - - let ideal_gas_segments: Option> = segment_map - .iter() - .map(|(s, &n)| s.ideal_gas_record.clone().map(|ig| (ig, n))) - .collect(); - - joback_records.push( - ideal_gas_segments - .as_ref() - .map(|s| JobackRecord::from_segments(s)) - .transpose()?, - ); } // Binary interaction parameter @@ -260,7 +243,6 @@ impl ParameterHetero for GcPcSaftEosParameters { chemical_records, segment_records, binary_segment_records, - joback_records: joback_records.into_iter().collect(), }) } @@ -268,7 +250,7 @@ impl ParameterHetero for GcPcSaftEosParameters { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ) { ( @@ -423,25 +405,23 @@ pub mod test { use crate::association::AssociationRecord; use feos_core::parameter::{ChemicalRecord, Identifier}; - fn ch3() -> SegmentRecord { + fn ch3() -> SegmentRecord { SegmentRecord::new( "CH3".into(), 15.0, GcPcSaftRecord::new(0.77247, 3.6937, 181.49, None, None, None), - None, ) } - fn ch2() -> SegmentRecord { + fn ch2() -> SegmentRecord { SegmentRecord::new( "CH2".into(), 14.0, GcPcSaftRecord::new(0.7912, 3.0207, 157.23, None, None, None), - None, ) } - fn oh() -> SegmentRecord { + fn oh() -> SegmentRecord { SegmentRecord::new( "OH".into(), 0.0, @@ -453,7 +433,6 @@ pub mod test { Some(AssociationRecord::new(0.009583, 2575.9, 1.0, 1.0, 0.0)), None, ), - None, ) } diff --git a/src/lib.rs b/src/lib.rs index cdcc1bc23..0e7387a2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,10 +33,11 @@ #![warn(clippy::all)] #![allow(clippy::too_many_arguments)] -#[cfg(feature = "dft")] -mod dft; -#[cfg(feature = "dft")] -pub use dft::FunctionalVariant; +#![allow(deprecated)] +// #[cfg(feature = "dft")] +// mod dft; +// #[cfg(feature = "dft")] +// pub use dft::FunctionalVariant; mod eos; // pub use eos::EosVariant; diff --git a/src/pcsaft/dft/mod.rs b/src/pcsaft/dft/mod.rs index deefc121d..7bcca9517 100644 --- a/src/pcsaft/dft/mod.rs +++ b/src/pcsaft/dft/mod.rs @@ -2,9 +2,8 @@ use super::PcSaftParameters; use crate::association::Association; use crate::hard_sphere::{FMTContribution, FMTVersion}; use crate::pcsaft::eos::PcSaftOptions; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::MolarWeight; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -28,7 +27,6 @@ pub struct PcSaftFunctional { fmt_version: FMTVersion, options: PcSaftOptions, contributions: Vec>, - joback: Joback, } impl PcSaftFunctional { @@ -87,17 +85,11 @@ impl PcSaftFunctional { } } - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.m.len()), - }; - (Self { parameters, fmt_version, options: saft_options, contributions, - joback, }) .into() } @@ -122,10 +114,6 @@ impl HelmholtzEnergyFunctional for PcSaftFunctional { &self.contributions } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } - fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::NonSpherical(&self.parameters.m) } diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index 90136c2ce..b58663242 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -1,18 +1,9 @@ use super::parameters::PcSaftParameters; use crate::association::Association; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; use feos_core::{ - Contributions, - EntropyScaling, - EosError, - EosResult, - EquationOfState, - HelmholtzEnergy, - MolarWeight, - Residual, //EntropyScaling - State, + Components, EntropyScaling, EosError, EosResult, HelmholtzEnergy, MolarWeight, Residual, State, }; use ndarray::Array1; use quantity::si::*; @@ -95,14 +86,14 @@ impl PcSaft { }; Self { - parameters: parameters.clone(), + parameters, options, contributions, } } } -impl Residual for PcSaft { +impl Components for PcSaft { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -113,7 +104,9 @@ impl Residual for PcSaft { self.options, ) } +} +impl Residual for PcSaft { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) @@ -441,19 +434,19 @@ mod tests { assert_relative_eq!(p, p_calc, epsilon = 1e-6); } - // #[test] - // fn vle_pure() { - // let e = Arc::new(PcSaft::new(propane_parameters())); - // let t = 300.0 * KELVIN; - // let vle = PhaseEquilibrium::pure(&e, t, None, Default::default()); - // if let Ok(v) = vle { - // assert_relative_eq!( - // v.vapor().pressure(Contributions::Total), - // v.liquid().pressure(Contributions::Total), - // epsilon = 1e-6 - // ) - // } - // } + #[test] + fn vle_pure() { + let e = Arc::new(PcSaft::new(propane_parameters())); + let t = 300.0 * KELVIN; + let vle = PhaseEquilibrium::pure(&e, t, None, Default::default()); + if let Ok(v) = vle { + assert_relative_eq!( + v.vapor().pressure(Contributions::Total), + v.liquid().pressure(Contributions::Total), + epsilon = 1e-6 + ) + } + } #[test] fn critical_point() { @@ -505,49 +498,49 @@ mod tests { ) } - // #[test] - // fn viscosity() -> EosResult<()> { - // let e = Arc::new(PcSaft::new(propane_parameters())); - // let t = 300.0 * KELVIN; - // let p = BAR; - // let n = arr1(&[1.0]) * MOL; - // let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); - // assert_relative_eq!( - // s.viscosity()?, - // 0.00797 * MILLI * PASCAL * SECOND, - // epsilon = 1e-5 - // ); - // assert_relative_eq!( - // s.ln_viscosity_reduced()?, - // (s.viscosity()? / e.viscosity_reference(s.temperature, s.volume, &s.moles)?) - // .into_value() - // .unwrap() - // .ln(), - // epsilon = 1e-15 - // ); - // Ok(()) - // } + #[test] + fn viscosity() -> EosResult<()> { + let e = Arc::new(PcSaft::new(propane_parameters())); + let t = 300.0 * KELVIN; + let p = BAR; + let n = arr1(&[1.0]) * MOL; + let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); + assert_relative_eq!( + s.viscosity()?, + 0.00797 * MILLI * PASCAL * SECOND, + epsilon = 1e-5 + ); + assert_relative_eq!( + s.ln_viscosity_reduced()?, + (s.viscosity()? / e.viscosity_reference(s.temperature, s.volume, &s.moles)?) + .into_value() + .unwrap() + .ln(), + epsilon = 1e-15 + ); + Ok(()) + } - // #[test] - // fn diffusion() -> EosResult<()> { - // let e = Arc::new(PcSaft::new(propane_parameters())); - // let t = 300.0 * KELVIN; - // let p = BAR; - // let n = arr1(&[1.0]) * MOL; - // let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); - // assert_relative_eq!( - // s.diffusion()?, - // 0.01505 * (CENTI * METER).powi(2) / SECOND, - // epsilon = 1e-5 - // ); - // assert_relative_eq!( - // s.ln_diffusion_reduced()?, - // (s.diffusion()? / e.diffusion_reference(s.temperature, s.volume, &s.moles)?) - // .into_value() - // .unwrap() - // .ln(), - // epsilon = 1e-15 - // ); - // Ok(()) - // } + #[test] + fn diffusion() -> EosResult<()> { + let e = Arc::new(PcSaft::new(propane_parameters())); + let t = 300.0 * KELVIN; + let p = BAR; + let n = arr1(&[1.0]) * MOL; + let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); + assert_relative_eq!( + s.diffusion()?, + 0.01505 * (CENTI * METER).powi(2) / SECOND, + epsilon = 1e-5 + ); + assert_relative_eq!( + s.ln_diffusion_reduced()?, + (s.diffusion()? / e.diffusion_reference(s.temperature, s.volume, &s.moles)?) + .into_value() + .unwrap() + .ln(), + epsilon = 1e-15 + ); + Ok(()) + } } diff --git a/tests/pcsaft/dft.rs b/tests/pcsaft/dft.rs index 066f874ff..1de474aa1 100644 --- a/tests/pcsaft/dft.rs +++ b/tests/pcsaft/dft.rs @@ -321,61 +321,59 @@ fn test_dft_water() -> Result<(), Box> { Ok(()) } -#[test] -fn test_entropy_bulk_values() -> Result<(), Box> { - let params = PcSaftParameters::from_json( - vec!["water_np"], - "tests/pcsaft/test_parameters.json", - None, - IdentifierOption::Name, - )?; - let func = Arc::new(PcSaftFunctional::new(Arc::new(params))); - let vle = PhaseEquilibrium::pure(&func, 350.0 * KELVIN, None, Default::default())?; - let profile = PlanarInterface::from_pdgt(&vle, 2048, false)?.solve(None)?; - let s_res = profile - .profile - .entropy_density(Contributions::ResidualNvt)?; - let s_tot = profile.profile.entropy_density(Contributions::Total)?; - println!( - "Density:\n{}", - profile.profile.density.index_axis(Axis(0), 0).to_owned() - ); - println!( - "liquid: {}, vapor: {}", - profile.vle.liquid().density, - profile.vle.vapor().density - ); - println!("\nResidual:\n{}", s_res); - println!( - "liquid: {}, vapor: {}", - profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, - profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume - ); - println!("\nTotal:\n{}", s_tot); - println!( - "liquid: {}, vapor: {}", - profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, - profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume - ); - assert_relative_eq!( - s_res.get(0), - profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, - max_relative = 1e-8, - ); - assert_relative_eq!( - s_res.get(2047), - profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume, - max_relative = 1e-8, - ); - assert_relative_eq!( - s_tot.get(0), - profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, - max_relative = 1e-8, - ); - assert_relative_eq!( - s_tot.get(2047), - profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume, - max_relative = 1e-8, - ); - Ok(()) -} +// #[test] +// fn test_entropy_bulk_values() -> Result<(), Box> { +// let params = PcSaftParameters::from_json( +// vec!["water_np"], +// "tests/pcsaft/test_parameters.json", +// None, +// IdentifierOption::Name, +// )?; +// let func = Arc::new(PcSaftFunctional::new(Arc::new(params)).into()); +// let vle = PhaseEquilibrium::pure(&func, 350.0 * KELVIN, None, Default::default())?; +// let profile = PlanarInterface::from_pdgt(&vle, 2048, false)?.solve(None)?; +// let s_res = profile.profile.residual_entropy_density()?; +// let s_tot = profile.profile.entropy_density(Contributions::Total)?; +// println!( +// "Density:\n{}", +// profile.profile.density.index_axis(Axis(0), 0).to_owned() +// ); +// println!( +// "liquid: {}, vapor: {}", +// profile.vle.liquid().density, +// profile.vle.vapor().density +// ); +// println!("\nResidual:\n{}", s_res); +// println!( +// "liquid: {}, vapor: {}", +// profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, +// profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume +// ); +// println!("\nTotal:\n{}", s_tot); +// println!( +// "liquid: {}, vapor: {}", +// profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, +// profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume +// ); +// assert_relative_eq!( +// s_res.get(0), +// profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, +// max_relative = 1e-8, +// ); +// assert_relative_eq!( +// s_res.get(2047), +// profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume, +// max_relative = 1e-8, +// ); +// assert_relative_eq!( +// s_tot.get(0), +// profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, +// max_relative = 1e-8, +// ); +// assert_relative_eq!( +// s_tot.get(2047), +// profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume, +// max_relative = 1e-8, +// ); +// Ok(()) +// } diff --git a/tests/pcsaft/properties.rs b/tests/pcsaft/properties.rs index f27c33712..a9e098128 100644 --- a/tests/pcsaft/properties.rs +++ b/tests/pcsaft/properties.rs @@ -1,7 +1,7 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; -use feos_core::{EquationOfState, StateBuilder}; +use feos_core::{Residual, StateBuilder}; use ndarray::*; use quantity::si::*; use std::error::Error; @@ -36,6 +36,7 @@ fn test_dln_phi_dp() -> Result<(), Box> { let ln_phi_h = sh.ln_phi()[0]; let dln_phi_dp = s.dln_phi_dp().get(0); let dln_phi_dp_h = (ln_phi_h - ln_phi) / h; + println!("{}", s.partial_molar_volume()); assert_relative_eq!(dln_phi_dp, dln_phi_dp_h, max_relative = 1e-6); Ok(()) } diff --git a/tests/pcsaft/state_creation_mixture.rs b/tests/pcsaft/state_creation_mixture.rs index 8b40d8cb6..849c5140f 100644 --- a/tests/pcsaft/state_creation_mixture.rs +++ b/tests/pcsaft/state_creation_mixture.rs @@ -17,36 +17,36 @@ fn propane_butane_parameters() -> Result, ParameterError> )?)) } -#[test] -fn pressure_entropy_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); - let pressure = BAR; - let temperature = 300.0 * KELVIN; - let x = arr1(&[0.3, 0.7]); - let state = StateBuilder::new(&saft) - .temperature(temperature) - .pressure(pressure) - .molefracs(&x) - .build()?; - let molar_entropy = state.molar_entropy(Contributions::Total); - let state = StateBuilder::new(&saft) - .pressure(pressure) - .molar_entropy(molar_entropy) - .molefracs(&x) - .build()?; - assert_relative_eq!( - state.molar_entropy(Contributions::Total), - molar_entropy, - max_relative = 1e-8 - ); - assert_relative_eq!(state.temperature, temperature, max_relative = 1e-10); - assert_relative_eq!( - state.pressure(Contributions::Total), - pressure, - max_relative = 1e-8 - ); - Ok(()) -} +// #[test] +// fn pressure_entropy_molefracs() -> Result<(), Box> { +// let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); +// let pressure = BAR; +// let temperature = 300.0 * KELVIN; +// let x = arr1(&[0.3, 0.7]); +// let state = StateBuilder::new(&saft) +// .temperature(temperature) +// .pressure(pressure) +// .molefracs(&x) +// .build()?; +// let molar_entropy = state.molar_entropy(Contributions::Total); +// let state = StateBuilder::new(&saft) +// .pressure(pressure) +// .molar_entropy(molar_entropy) +// .molefracs(&x) +// .build()?; +// assert_relative_eq!( +// state.molar_entropy(Contributions::Total), +// molar_entropy, +// max_relative = 1e-8 +// ); +// assert_relative_eq!(state.temperature, temperature, max_relative = 1e-10); +// assert_relative_eq!( +// state.pressure(Contributions::Total), +// pressure, +// max_relative = 1e-8 +// ); +// Ok(()) +// } #[test] fn volume_temperature_molefracs() -> Result<(), Box> { diff --git a/tests/pcsaft/state_creation_pure.rs b/tests/pcsaft/state_creation_pure.rs index 2299d95cf..3cb66a46e 100644 --- a/tests/pcsaft/state_creation_pure.rs +++ b/tests/pcsaft/state_creation_pure.rs @@ -2,7 +2,7 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; use feos_core::{ - Contributions, DensityInitialization, EquationOfState, PhaseEquilibrium, State, StateBuilder, + Contributions, DensityInitialization, IdealGas, PhaseEquilibrium, Residual, State, StateBuilder, }; use quantity::si::*; use std::error::Error; @@ -133,181 +133,181 @@ fn pressure_temperature_initial_density() -> Result<(), Box> { Ok(()) } -#[test] -fn pressure_enthalpy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let pressure = 0.3 * BAR; - let molar_enthalpy = 2000.0 * JOULE / MOL; - let state = StateBuilder::new(&saft) - .pressure(pressure) - .molar_enthalpy(molar_enthalpy) - .vapor() - .build()?; - assert_relative_eq!( - state.molar_enthalpy(Contributions::Total), - molar_enthalpy, - max_relative = 1e-10 - ); - assert_relative_eq!( - state.pressure(Contributions::Total), - pressure, - max_relative = 1e-10 - ); +// #[test] +// fn pressure_enthalpy_vapor() -> Result<(), Box> { +// let saft = Arc::new(PcSaft::new(propane_parameters()?)); +// let pressure = 0.3 * BAR; +// let molar_enthalpy = 2000.0 * JOULE / MOL; +// let state = StateBuilder::new(&saft) +// .pressure(pressure) +// .molar_enthalpy(molar_enthalpy) +// .vapor() +// .build()?; +// assert_relative_eq!( +// state.molar_enthalpy(Contributions::Total), +// molar_enthalpy, +// max_relative = 1e-10 +// ); +// assert_relative_eq!( +// state.pressure(Contributions::Total), +// pressure, +// max_relative = 1e-10 +// ); - let state = StateBuilder::new(&saft) - .volume(state.volume) - .temperature(state.temperature) - .moles(&state.moles) - .build()?; - assert_relative_eq!( - state.molar_enthalpy(Contributions::Total), - molar_enthalpy, - max_relative = 1e-10 - ); - assert_relative_eq!( - state.pressure(Contributions::Total), - pressure, - max_relative = 1e-10 - ); - Ok(()) -} +// let state = StateBuilder::new(&saft) +// .volume(state.volume) +// .temperature(state.temperature) +// .moles(&state.moles) +// .build()?; +// assert_relative_eq!( +// state.molar_enthalpy(Contributions::Total), +// molar_enthalpy, +// max_relative = 1e-10 +// ); +// assert_relative_eq!( +// state.pressure(Contributions::Total), +// pressure, +// max_relative = 1e-10 +// ); +// Ok(()) +// } -#[test] -fn density_internal_energy() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let pressure = 5.0 * BAR; - let temperature = 315.0 * KELVIN; - let total_moles = 2.5 * MOL; - let state = StateBuilder::new(&saft) - .pressure(pressure) - .temperature(temperature) - .total_moles(total_moles) - .build()?; - let molar_internal_energy = state.molar_internal_energy(Contributions::Total); - let state_nvu = StateBuilder::new(&saft) - .volume(state.volume) - .molar_internal_energy(molar_internal_energy) - .total_moles(total_moles) - .build()?; - assert_relative_eq!( - molar_internal_energy, - state_nvu.molar_internal_energy(Contributions::Total), - max_relative = 1e-10 - ); - assert_relative_eq!(temperature, state_nvu.temperature, max_relative = 1e-10); - assert_relative_eq!(state.density, state_nvu.density, max_relative = 1e-10); - Ok(()) -} +// #[test] +// fn density_internal_energy() -> Result<(), Box> { +// let saft = Arc::new(PcSaft::new(propane_parameters()?)); +// let pressure = 5.0 * BAR; +// let temperature = 315.0 * KELVIN; +// let total_moles = 2.5 * MOL; +// let state = StateBuilder::new(&saft) +// .pressure(pressure) +// .temperature(temperature) +// .total_moles(total_moles) +// .build()?; +// let molar_internal_energy = state.molar_internal_energy(Contributions::Total); +// let state_nvu = StateBuilder::new(&saft) +// .volume(state.volume) +// .molar_internal_energy(molar_internal_energy) +// .total_moles(total_moles) +// .build()?; +// assert_relative_eq!( +// molar_internal_energy, +// state_nvu.molar_internal_energy(Contributions::Total), +// max_relative = 1e-10 +// ); +// assert_relative_eq!(temperature, state_nvu.temperature, max_relative = 1e-10); +// assert_relative_eq!(state.density, state_nvu.density, max_relative = 1e-10); +// Ok(()) +// } -#[test] -fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let pressure = 0.3 * BAR; - let molar_enthalpy = 2000.0 * JOULE / MOL; - let total_moles = 2.5 * MOL; - let state = StateBuilder::new(&saft) - .pressure(pressure) - .molar_enthalpy(molar_enthalpy) - .total_moles(total_moles) - .vapor() - .build()?; - assert_relative_eq!( - state.molar_enthalpy(Contributions::Total), - molar_enthalpy, - max_relative = 1e-10 - ); - assert_relative_eq!( - state.pressure(Contributions::Total), - pressure, - max_relative = 1e-10 - ); +// #[test] +// fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { +// let saft = Arc::new(PcSaft::new(propane_parameters()?)); +// let pressure = 0.3 * BAR; +// let molar_enthalpy = 2000.0 * JOULE / MOL; +// let total_moles = 2.5 * MOL; +// let state = StateBuilder::new(&saft) +// .pressure(pressure) +// .molar_enthalpy(molar_enthalpy) +// .total_moles(total_moles) +// .vapor() +// .build()?; +// assert_relative_eq!( +// state.molar_enthalpy(Contributions::Total), +// molar_enthalpy, +// max_relative = 1e-10 +// ); +// assert_relative_eq!( +// state.pressure(Contributions::Total), +// pressure, +// max_relative = 1e-10 +// ); - let state = StateBuilder::new(&saft) - .volume(state.volume) - .temperature(state.temperature) - .total_moles(state.total_moles) - .build()?; - assert_relative_eq!( - state.molar_enthalpy(Contributions::Total), - molar_enthalpy, - max_relative = 1e-10 - ); - assert_relative_eq!( - state.pressure(Contributions::Total), - pressure, - max_relative = 1e-10 - ); - Ok(()) -} +// let state = StateBuilder::new(&saft) +// .volume(state.volume) +// .temperature(state.temperature) +// .total_moles(state.total_moles) +// .build()?; +// assert_relative_eq!( +// state.molar_enthalpy(Contributions::Total), +// molar_enthalpy, +// max_relative = 1e-10 +// ); +// assert_relative_eq!( +// state.pressure(Contributions::Total), +// pressure, +// max_relative = 1e-10 +// ); +// Ok(()) +// } -#[test] -fn pressure_entropy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let pressure = 0.3 * BAR; - let molar_entropy = -2.0 * JOULE / MOL / KELVIN; - let state = StateBuilder::new(&saft) - .pressure(pressure) - .molar_entropy(molar_entropy) - .vapor() - .build()?; - assert_relative_eq!( - state.molar_entropy(Contributions::Total), - molar_entropy, - max_relative = 1e-10 - ); - assert_relative_eq!( - state.pressure(Contributions::Total), - pressure, - max_relative = 1e-10 - ); +// #[test] +// fn pressure_entropy_vapor() -> Result<(), Box> { +// let saft = Arc::new(PcSaft::new(propane_parameters()?)); +// let pressure = 0.3 * BAR; +// let molar_entropy = -2.0 * JOULE / MOL / KELVIN; +// let state = StateBuilder::new(&saft) +// .pressure(pressure) +// .molar_entropy(molar_entropy) +// .vapor() +// .build()?; +// assert_relative_eq!( +// state.molar_entropy(Contributions::Total), +// molar_entropy, +// max_relative = 1e-10 +// ); +// assert_relative_eq!( +// state.pressure(Contributions::Total), +// pressure, +// max_relative = 1e-10 +// ); - let state = StateBuilder::new(&saft) - .volume(state.volume) - .temperature(state.temperature) - .moles(&state.moles) - .build()?; - assert_relative_eq!( - state.molar_entropy(Contributions::Total), - molar_entropy, - max_relative = 1e-10 - ); - assert_relative_eq!( - state.pressure(Contributions::Total), - pressure, - max_relative = 1e-10 - ); - Ok(()) -} +// let state = StateBuilder::new(&saft) +// .volume(state.volume) +// .temperature(state.temperature) +// .moles(&state.moles) +// .build()?; +// assert_relative_eq!( +// state.molar_entropy(Contributions::Total), +// molar_entropy, +// max_relative = 1e-10 +// ); +// assert_relative_eq!( +// state.pressure(Contributions::Total), +// pressure, +// max_relative = 1e-10 +// ); +// Ok(()) +// } -#[test] -fn temperature_entropy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let pressure = 3.0 * BAR; - let temperature = 315.15 * KELVIN; - let total_moles = 3.0 * MOL; - let state = StateBuilder::new(&saft) - .temperature(temperature) - .pressure(pressure) - .total_moles(total_moles) - .build()?; +// #[test] +// fn temperature_entropy_vapor() -> Result<(), Box> { +// let saft = Arc::new(PcSaft::new(propane_parameters()?)); +// let pressure = 3.0 * BAR; +// let temperature = 315.15 * KELVIN; +// let total_moles = 3.0 * MOL; +// let state = StateBuilder::new(&saft) +// .temperature(temperature) +// .pressure(pressure) +// .total_moles(total_moles) +// .build()?; - let s = State::new_nts( - &saft, - temperature, - state.molar_entropy(Contributions::Total), - &state.moles, - DensityInitialization::None, - )?; - assert_relative_eq!( - state.molar_entropy(Contributions::Total), - s.molar_entropy(Contributions::Total), - max_relative = 1e-10 - ); - assert_relative_eq!(state.density, s.density, max_relative = 1e-10); - Ok(()) -} +// let s = State::new_nts( +// &saft, +// temperature, +// state.molar_entropy(Contributions::Total), +// &state.moles, +// DensityInitialization::None, +// )?; +// assert_relative_eq!( +// state.molar_entropy(Contributions::Total), +// s.molar_entropy(Contributions::Total), +// max_relative = 1e-10 +// ); +// assert_relative_eq!(state.density, s.density, max_relative = 1e-10); +// Ok(()) +// } -fn assert_multiple_states( +fn assert_multiple_states( states: &[(&State, &str)], pressure: SINumber, enthalpy: SINumber, @@ -336,89 +336,89 @@ fn assert_multiple_states( } } -#[test] -fn test_consistency() -> Result<(), Box> { - let p = propane_parameters()?; - let saft = Arc::new(PcSaft::new(p)); - let temperatures = [350.0 * KELVIN, 400.0 * KELVIN, 450.0 * KELVIN]; - let pressures = [1.0 * BAR, 2.0 * BAR, 3.0 * BAR]; +// #[test] +// fn test_consistency() -> Result<(), Box> { +// let p = propane_parameters()?; +// let saft = Arc::new(PcSaft::new(p)); +// let temperatures = [350.0 * KELVIN, 400.0 * KELVIN, 450.0 * KELVIN]; +// let pressures = [1.0 * BAR, 2.0 * BAR, 3.0 * BAR]; - for (&temperature, &pressure) in temperatures.iter().zip(pressures.iter()) { - let state = StateBuilder::new(&saft) - .pressure(pressure) - .temperature(temperature) - .build()?; - assert_relative_eq!( - state.pressure(Contributions::Total), - pressure, - max_relative = 1e-10 - ); - println!( - "temperature: {}\npressure: {}\ndensity: {}", - temperature, pressure, state.density - ); - let molar_enthalpy = state.molar_enthalpy(Contributions::Total); - let molar_entropy = state.molar_entropy(Contributions::Total); - let density = state.density; +// for (&temperature, &pressure) in temperatures.iter().zip(pressures.iter()) { +// let state = StateBuilder::new(&saft) +// .pressure(pressure) +// .temperature(temperature) +// .build()?; +// assert_relative_eq!( +// state.pressure(Contributions::Total), +// pressure, +// max_relative = 1e-10 +// ); +// println!( +// "temperature: {}\npressure: {}\ndensity: {}", +// temperature, pressure, state.density +// ); +// let molar_enthalpy = state.molar_enthalpy(Contributions::Total); +// let molar_entropy = state.molar_entropy(Contributions::Total); +// let density = state.density; - let state_tv = StateBuilder::new(&saft) - .temperature(temperature) - .density(density) - .build()?; +// let state_tv = StateBuilder::new(&saft) +// .temperature(temperature) +// .density(density) +// .build()?; - let vle = PhaseEquilibrium::pure(&saft, temperature, None, Default::default()); - let builder = if let Ok(ps) = vle { - let p_sat = ps.liquid().pressure(Contributions::Total); - if pressure > p_sat { - StateBuilder::new(&saft).liquid() - } else { - StateBuilder::new(&saft).vapor() - } - } else { - StateBuilder::new(&saft).vapor() - }; +// let vle = PhaseEquilibrium::pure(&saft, temperature, None, Default::default()); +// let builder = if let Ok(ps) = vle { +// let p_sat = ps.liquid().pressure(Contributions::Total); +// if pressure > p_sat { +// StateBuilder::new(&saft).liquid() +// } else { +// StateBuilder::new(&saft).vapor() +// } +// } else { +// StateBuilder::new(&saft).vapor() +// }; - let state_ts = builder - .clone() - .temperature(temperature) - .molar_entropy(molar_entropy) - .build()?; +// let state_ts = builder +// .clone() +// .temperature(temperature) +// .molar_entropy(molar_entropy) +// .build()?; - let state_ps = builder - .clone() - .pressure(pressure) - .molar_entropy(molar_entropy) - .build()?; +// let state_ps = builder +// .clone() +// .pressure(pressure) +// .molar_entropy(molar_entropy) +// .build()?; - dbg!("ph"); - let state_ph = builder - .clone() - .pressure(pressure) - .molar_enthalpy(molar_enthalpy) - .build()?; +// dbg!("ph"); +// let state_ph = builder +// .clone() +// .pressure(pressure) +// .molar_enthalpy(molar_enthalpy) +// .build()?; - dbg!("th"); - let state_th = builder - .clone() - .temperature(temperature) - .molar_enthalpy(molar_enthalpy) - .build()?; +// dbg!("th"); +// let state_th = builder +// .clone() +// .temperature(temperature) +// .molar_enthalpy(molar_enthalpy) +// .build()?; - dbg!("assertions"); - assert_multiple_states( - &[ - (&state_ph, "p, h"), - (&state_tv, "T, V"), - (&state_ts, "T, s"), - (&state_th, "T, h"), - (&state_ps, "p, s"), - ], - pressure, - molar_enthalpy, - molar_entropy, - density, - 1e-7, - ); - } - Ok(()) -} +// dbg!("assertions"); +// assert_multiple_states( +// &[ +// (&state_ph, "p, h"), +// (&state_tv, "T, V"), +// (&state_ts, "T, s"), +// (&state_th, "T, h"), +// (&state_ps, "p, s"), +// ], +// pressure, +// molar_enthalpy, +// molar_entropy, +// density, +// 1e-7, +// ); +// } +// Ok(()) +// } From c538018c8393440a012cb32ada6487ec37c5c1c3 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 5 Jun 2023 22:19:34 +0200 Subject: [PATCH 15/47] fix ln_phi --- feos-core/src/state/residual_properties.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index e50af92a1..1991f76fa 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -238,7 +238,7 @@ impl State { (self.residual_chemical_potential() / (SIUnit::gas_constant() * self.temperature)) .into_value() .unwrap() - - self.compressibility(Contributions::Total) + - self.compressibility(Contributions::Total).ln() } /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure. From b68538000f229536dde6951447e910d78367d6ca Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Tue, 6 Jun 2023 12:48:22 +0200 Subject: [PATCH 16/47] Fixed rest of models. Python part still needs to be done. --- src/pets/eos/mod.rs | 477 ++++++++++++++++------------------- src/pets/eos/qspr.rs | 122 --------- src/pets/parameters.rs | 23 +- src/saftvrqmie/eos/mod.rs | 13 +- src/saftvrqmie/parameters.rs | 23 +- src/uvtheory/eos/mod.rs | 34 +-- src/uvtheory/parameters.rs | 17 +- 7 files changed, 263 insertions(+), 446 deletions(-) delete mode 100644 src/pets/eos/qspr.rs diff --git a/src/pets/eos/mod.rs b/src/pets/eos/mod.rs index 93596d75d..4f85e90c0 100644 --- a/src/pets/eos/mod.rs +++ b/src/pets/eos/mod.rs @@ -1,26 +1,14 @@ use super::parameters::PetsParameters; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EosUnit, EquationOfState, HelmholtzEnergy, - IdealGasContribution, MolarWeight, State, -}; +use feos_core::{Components, HelmholtzEnergy, MolarWeight, Residual}; use ndarray::Array1; use quantity::si::*; -use std::f64::consts::{FRAC_PI_6, PI}; +use std::f64::consts::FRAC_PI_6; use std::sync::Arc; pub(crate) mod dispersion; -mod qspr; use dispersion::Dispersion; -use qspr::QSPR; - -#[allow(clippy::upper_case_acronyms)] -enum IdealGasContributions { - QSPR(QSPR), - Joback(Joback), -} /// Configuration options for the PeTS equation of state and Helmholtz energy functional. /// @@ -43,7 +31,6 @@ pub struct Pets { parameters: Arc, options: PetsOptions, contributions: Vec>, - ideal_gas: IdealGasContributions, } impl Pets { @@ -60,22 +47,15 @@ impl Pets { parameters: parameters.clone(), }), ]; - - let joback_records = parameters.joback_records.clone(); - Self { parameters: parameters.clone(), options, contributions, - ideal_gas: joback_records.map_or( - IdealGasContributions::QSPR(QSPR { parameters }), - |joback_records| IdealGasContributions::Joback(Joback::new(joback_records)), - ), } } } -impl EquationOfState for Pets { +impl Components for Pets { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -86,22 +66,17 @@ impl EquationOfState for Pets { self.options, ) } +} +impl Residual for Pets { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * self.parameters.sigma.mapv(|v| v.powi(3)) * moles).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match &self.ideal_gas { - IdealGasContributions::QSPR(qspr) => qspr, - IdealGasContributions::Joback(joback) => joback, - } - } } impl MolarWeight for Pets { @@ -110,217 +85,217 @@ impl MolarWeight for Pets { } } -fn omega11(t: f64) -> f64 { - 1.06036 * t.powf(-0.15610) - + 0.19300 * (-0.47635 * t).exp() - + 1.03587 * (-1.52996 * t).exp() - + 1.76474 * (-3.89411 * t).exp() -} - -fn omega22(t: f64) -> f64 { - 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() - - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() -} - -impl EntropyScaling for Pets { - fn viscosity_reference( - &self, - temperature: SINumber, - _: SINumber, - moles: &SIArray1, - ) -> EosResult { - let x = moles.to_reduced(moles.sum())?; - let p = &self.parameters; - let mw = &p.molarweight; - let ce: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 5.0 / 16.0 - * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) - .sqrt() - .unwrap() - / omega22(tr) - / (p.sigma[i] * ANGSTROM).powi(2) - }) - .collect(); - let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; - for i in 0..self.components() { - let denom: f64 = (0..self.components()) - .map(|j| { - x[j] * (1.0 - + (ce[i] / ce[j]).into_value().unwrap().sqrt() - * (mw[j] / mw[i]).powf(1.0 / 4.0)) - .powi(2) - / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() - }) - .sum(); - ce_mix += ce[i] * x[i] / denom - } - Ok(ce_mix) - } - - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - let coefficients = self - .parameters - .viscosity - .as_ref() - .expect("Missing viscosity coefficients."); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) - } - - fn diffusion_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let density = moles.sum() / volume; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) - * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) - .sqrt() - .unwrap() - }) - .collect(); - Ok(res[0]) - } - - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .diffusion - .as_ref() - .expect("Missing diffusion coefficients."); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - let e: f64 = (&coefficients.row(4) * x).sum(); - Ok(a + b * s_res - - c * (1.0 - s_res.exp()) * s_res.powi(2) - - d * s_res.powi(4) - - e * s_res.powi(8)) - } - - // fn thermal_conductivity_reference( - // &self, - // state: &State, - // ) -> EosResult { - // if self.components() != 1 { - // return Err(EosError::IncompatibleComponents(self.components(), 1)); - // } - // let p = &self.parameters; - // let res: Array1 = (0..self.components()) - // .map(|i| { - // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) - // .into_value() - // .unwrap(); - // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); - // let s_res_cp_reduced = cp - // .entropy(Contributions::Residual) - // .to_reduced(SIUnit::reference_entropy()) - // .unwrap(); - // let s_res_reduced = cp - // .entropy(Contributions::Residual) - // .to_reduced(SIUnit::reference_entropy()) - // .unwrap(); - // let ref_ce = 0.083235 - // * ((state.temperature / KELVIN).into_value().unwrap() - // / (p.molarweight[0])) - // .sqrt() - // / p.sigma[0] - // / p.sigma[0] - // / omega22(tr); - // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); - // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) - // * (p.sigma[i].powi(3) * p.epsilon_k[0]) - // / 100000.0; - // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN - // }) - // .collect(); - // Ok(res[0]) - // } - - // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 - fn thermal_conductivity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let state = State::new_nvt( - &Arc::new(Self::new(self.parameters.clone())), - temperature, - volume, - moles, - )?; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - let ce = 83.235 - * f64::powf(10.0, -1.5) - * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() - / (p.sigma[0] * p.sigma[0]) - / omega22(tr); - ce * WATT / METER / KELVIN - + state.density - * self - .diffusion_reference(temperature, volume, moles) - .unwrap() - * self - .diffusion_correlation( - state - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy()) - .unwrap(), - &state.molefracs, - ) - .unwrap() - * (state.c_v(Contributions::Total) - 1.5 * RGAS) - }) - .collect(); - Ok(res[0]) - } - - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .thermal_conductivity - .as_ref() - .expect("Missing thermal conductivity coefficients"); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) - } -} +// fn omega11(t: f64) -> f64 { +// 1.06036 * t.powf(-0.15610) +// + 0.19300 * (-0.47635 * t).exp() +// + 1.03587 * (-1.52996 * t).exp() +// + 1.76474 * (-3.89411 * t).exp() +// } + +// fn omega22(t: f64) -> f64 { +// 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() +// - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() +// } + +// impl EntropyScaling for Pets { +// fn viscosity_reference( +// &self, +// temperature: SINumber, +// _: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// let x = moles.to_reduced(moles.sum())?; +// let p = &self.parameters; +// let mw = &p.molarweight; +// let ce: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 5.0 / 16.0 +// * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) +// .sqrt() +// .unwrap() +// / omega22(tr) +// / (p.sigma[i] * ANGSTROM).powi(2) +// }) +// .collect(); +// let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; +// for i in 0..self.components() { +// let denom: f64 = (0..self.components()) +// .map(|j| { +// x[j] * (1.0 +// + (ce[i] / ce[j]).into_value().unwrap().sqrt() +// * (mw[j] / mw[i]).powf(1.0 / 4.0)) +// .powi(2) +// / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() +// }) +// .sum(); +// ce_mix += ce[i] * x[i] / denom +// } +// Ok(ce_mix) +// } + +// fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// let coefficients = self +// .parameters +// .viscosity +// .as_ref() +// .expect("Missing viscosity coefficients."); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) +// } + +// fn diffusion_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let density = moles.sum() / volume; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) +// * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) +// .sqrt() +// .unwrap() +// }) +// .collect(); +// Ok(res[0]) +// } + +// fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .diffusion +// .as_ref() +// .expect("Missing diffusion coefficients."); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// let e: f64 = (&coefficients.row(4) * x).sum(); +// Ok(a + b * s_res +// - c * (1.0 - s_res.exp()) * s_res.powi(2) +// - d * s_res.powi(4) +// - e * s_res.powi(8)) +// } + +// // fn thermal_conductivity_reference( +// // &self, +// // state: &State, +// // ) -> EosResult { +// // if self.components() != 1 { +// // return Err(EosError::IncompatibleComponents(self.components(), 1)); +// // } +// // let p = &self.parameters; +// // let res: Array1 = (0..self.components()) +// // .map(|i| { +// // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) +// // .into_value() +// // .unwrap(); +// // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); +// // let s_res_cp_reduced = cp +// // .entropy(Contributions::Residual) +// // .to_reduced(SIUnit::reference_entropy()) +// // .unwrap(); +// // let s_res_reduced = cp +// // .entropy(Contributions::Residual) +// // .to_reduced(SIUnit::reference_entropy()) +// // .unwrap(); +// // let ref_ce = 0.083235 +// // * ((state.temperature / KELVIN).into_value().unwrap() +// // / (p.molarweight[0])) +// // .sqrt() +// // / p.sigma[0] +// // / p.sigma[0] +// // / omega22(tr); +// // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); +// // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) +// // * (p.sigma[i].powi(3) * p.epsilon_k[0]) +// // / 100000.0; +// // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN +// // }) +// // .collect(); +// // Ok(res[0]) +// // } + +// // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 +// fn thermal_conductivity_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let state = State::new_nvt( +// &Arc::new(Self::new(self.parameters.clone())), +// temperature, +// volume, +// moles, +// )?; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// let ce = 83.235 +// * f64::powf(10.0, -1.5) +// * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() +// / (p.sigma[0] * p.sigma[0]) +// / omega22(tr); +// ce * WATT / METER / KELVIN +// + state.density +// * self +// .diffusion_reference(temperature, volume, moles) +// .unwrap() +// * self +// .diffusion_correlation( +// state +// .residual_entropy() +// .to_reduced(SIUnit::reference_molar_entropy() * state.total_moles) +// .unwrap(), +// &state.molefracs, +// ) +// .unwrap() +// * (state.c_v(Contributions::Total) - 1.5 * RGAS) +// }) +// .collect(); +// Ok(res[0]) +// } + +// fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .thermal_conductivity +// .as_ref() +// .expect("Missing thermal conductivity coefficients"); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) +// } +// } #[cfg(test)] mod tests { @@ -345,23 +320,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), - s.pressure(Contributions::Total), - epsilon = 1e-10 - ); - } - - #[test] - fn ideal_gas_heat_capacity_joback() { - let e = Arc::new(Pets::new(argon_parameters())); - let t = 200.0 * KELVIN; - let v = 1e-3 * METER.powi(3); - let n = arr1(&[1.0]) * MOL; - let s = State::new_nvt(&e, t, v, &n).unwrap(); - let p_ig = s.total_moles * RGAS * t / v; - assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); - assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); diff --git a/src/pets/eos/qspr.rs b/src/pets/eos/qspr.rs deleted file mode 100644 index e4d5aa6e4..000000000 --- a/src/pets/eos/qspr.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::pets::parameters::PetsParameters; -use feos_core::IdealGasContributionDual; -use ndarray::Array1; -use num_dual::*; -use std::fmt; -use std::sync::Arc; - -const RGAS: f64 = 6.022140857 * 1.38064852; -const KB: f64 = 1.38064852e-23; -const T300: f64 = 300.0; -const T400: f64 = 400.0; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; - -// Heat capacity parameters @ T = 300 K (col 1) and T = 400 K (col 2) -const NA_NP_300: [f64; 6] = [ - -5763.04893, - 1232.30607, - -239.3513996, - 0.0, - 0.0, - -15174.28321, -]; -const NA_NP_400: [f64; 6] = [ - -8171.26676935062, - 1498.01217504596, - -315.515836223387, - 0.0, - 0.0, - -19389.5468655708, -]; -// const NA_P_300: [f64; 6] = [ -// 5177.19095226181, -// 919.565206504576, -// -108.829105648889, -// 0.0, -// -3.93917830677682, -// -13504.5671858292, -// ]; -// const NA_P_400: [f64; 6] = [ -// 10656.1018362315, -// 1146.10782703748, -// -131.023645998081, -// 0.0, -// -9.93789225413177, -// -24430.12952497, -// ]; -// const AP_300: [f64; 6] = [ -// 3600.32322462175, -// 1006.20461224949, -// -151.688378113974, -// 7.81876773647109e-07, -// 8.01001754473385, -// -8959.37140957179, -// ]; -// const AP_400: [f64; 6] = [ -// 7248.0697641199, -// 1267.44346171358, -// -208.738557800023, -// 0.000170238690157906, -// -6.7841792685616, -// -12669.4196622924, -// ]; - -#[allow(clippy::upper_case_acronyms)] -pub struct QSPR { - pub parameters: Arc, -} - -impl + Copy> IdealGasContributionDual for QSPR { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { - let (c_300, c_400) = (NA_NP_300, NA_NP_400); - - Array1::from_shape_fn(components, |i| { - let epsilon_kt = temperature.recip() * self.parameters.epsilon_k[i]; - let sigma3 = self.parameters.sigma[i].powi(3); - - let p1 = epsilon_kt; - let p2 = sigma3; - let p3 = epsilon_kt * p2; - // let p4 = (temperature.recip() * self.parameters.epsilon_k_ab[i]).exp_m1() - // * p2 - // * sigma3 - // * self.parameters.kappa_ab[i]; - // let p5 = p2 * self.parameters.q[i]; - let p6 = 1.0; - - let icpc300 = (p1 * c_300[0] / T300 - + p2 * c_300[1] - + p3 * c_300[2] / T300 - // + p4 * c_300[3] / T300 - // + p5 * c_300[4] - + p6 * c_300[5]) - * 0.001; - let icpc400 = (p1 * c_400[0] / T400 - + p2 * c_400[1] - + p3 * c_400[2] / T400 - // + p4 * c_400[3] / T400 - // + p5 * c_400[4] - + p6 * c_400[5]) - * 0.001; - - // linear approximation - let b = (icpc400 - icpc300) / (T400 - T300); - let a = icpc300 - b * T300; - - // integration - let k = a * (temperature - T0 - temperature * (temperature / T0).ln()) - - b * (temperature - T0).powi(2) * 0.5; - - // de Broglie wavelength - k / (temperature * RGAS) + (temperature * KB / (P0 * A3)).ln() - }) - } -} - -impl fmt::Display for QSPR { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (QSPR)") - } -} diff --git a/src/pets/parameters.rs b/src/pets/parameters.rs index 436c9dc3a..365374f0f 100644 --- a/src/pets/parameters.rs +++ b/src/pets/parameters.rs @@ -1,5 +1,4 @@ use crate::hard_sphere::{HardSphereProperties, MonomerShape}; -use feos_core::joback::JobackRecord; use feos_core::parameter::{Parameter, PureRecord}; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; @@ -119,20 +118,17 @@ pub struct PetsParameters { /// thermal conductivity parameters for entropy scaling pub thermal_conductivity: Option>, /// records of all pure substances of the system - pub pure_records: Vec>, - /// records of parameters for Joback method - pub joback_records: Option>, + pub pure_records: Vec>, /// records of all binary interaction parameters pub binary_records: Array2, } impl Parameter for PetsParameters { type Pure = PetsRecord; - type IdealGas = JobackRecord; type Binary = PetsBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -200,11 +196,6 @@ impl Parameter for PetsParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, sigma, @@ -217,7 +208,6 @@ impl Parameter for PetsParameters { diffusion: diffusion_coefficients, thermal_conductivity: thermal_conductivity_coefficients, pure_records, - joback_records, binary_records, } } @@ -225,7 +215,7 @@ impl Parameter for PetsParameters { fn records( &self, ) -> ( - &[PureRecord], + &[PureRecord], &Array2, ) { (&self.pure_records, &self.binary_records) @@ -285,7 +275,6 @@ impl std::fmt::Display for PetsParameters { #[cfg(test)] pub mod utils { use super::*; - use feos_core::joback::JobackRecord; use std::sync::Arc; pub fn argon_parameters() -> Arc { @@ -308,7 +297,7 @@ pub mod utils { }, "molarweight": 39.948 }"#; - let argon_record: PureRecord = + let argon_record: PureRecord = serde_json::from_str(argon_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_pure(argon_record)) } @@ -330,7 +319,7 @@ pub mod utils { }, "molarweight": 83.798 }"#; - let krypton_record: PureRecord = + let krypton_record: PureRecord = serde_json::from_str(krypton_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_pure(krypton_record)) } @@ -374,7 +363,7 @@ pub mod utils { "molarweight": 83.798 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_binary(binary_record, None)) } diff --git a/src/saftvrqmie/eos/mod.rs b/src/saftvrqmie/eos/mod.rs index 42330a9e3..a7b7100e1 100644 --- a/src/saftvrqmie/eos/mod.rs +++ b/src/saftvrqmie/eos/mod.rs @@ -1,8 +1,7 @@ use super::parameters::SaftVRQMieParameters; use feos_core::parameter::Parameter; use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EquationOfState, HelmholtzEnergy, - MolarWeight, State, + Components, EntropyScaling, EosError, EosResult, HelmholtzEnergy, MolarWeight, Residual, State, }; use ndarray::Array1; use quantity::si::*; @@ -85,7 +84,7 @@ impl SaftVRQMie { } } -impl EquationOfState for SaftVRQMie { +impl Components for SaftVRQMie { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -96,14 +95,16 @@ impl EquationOfState for SaftVRQMie { self.options, ) } +} +impl Residual for SaftVRQMie { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) .sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -264,8 +265,8 @@ impl EntropyScaling for SaftVRQMie { .into_value() .unwrap(); let s_res_reduced = state - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(RGAS) + .residual_entropy() + .to_reduced(RGAS * state.total_moles) .unwrap() / p.m[i]; let ref_ce = chapman_enskog_thermal_conductivity( diff --git a/src/saftvrqmie/parameters.rs b/src/saftvrqmie/parameters.rs index ed59cd9d5..13b49eb7e 100644 --- a/src/saftvrqmie/parameters.rs +++ b/src/saftvrqmie/parameters.rs @@ -1,4 +1,3 @@ -use feos_core::joback::JobackRecord; use feos_core::parameter::{Parameter, ParameterError, PureRecord}; use ndarray::{Array, Array1, Array2}; use num_traits::Zero; @@ -134,18 +133,16 @@ pub struct SaftVRQMieParameters { pub viscosity: Option>, pub diffusion: Option>, pub thermal_conductivity: Option>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, - pub joback_records: Option>, } impl Parameter for SaftVRQMieParameters { type Pure = SaftVRQMieRecord; - type IdealGas = JobackRecord; type Binary = SaftVRQMieBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -235,11 +232,6 @@ impl Parameter for SaftVRQMieParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, m, @@ -261,14 +253,13 @@ impl Parameter for SaftVRQMieParameters { thermal_conductivity: thermal_conductivity_coefficients, pure_records, binary_records, - joback_records, } } fn records( &self, ) -> ( - &[PureRecord], + &[PureRecord], &Array2, ) { (&self.pure_records, &self.binary_records) @@ -431,7 +422,7 @@ pub mod utils { }, "molarweight": 2.0157309551872 }"#; - let hydrogen_record: PureRecord = + let hydrogen_record: PureRecord = serde_json::from_str(hydrogen_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(hydrogen_record)) } @@ -457,7 +448,7 @@ pub mod utils { }, "molarweight": 4.002601643881807 }"#; - let helium_record: PureRecord = + let helium_record: PureRecord = serde_json::from_str(helium_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(helium_record)) } @@ -483,7 +474,7 @@ pub mod utils { }, "molarweight": 20.17969806457545 }"#; - let neon_record: PureRecord = + let neon_record: PureRecord = serde_json::from_str(neon_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(neon_record)) } @@ -527,7 +518,7 @@ pub mod utils { "molarweight": 20.17969806457545 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_binary( binary_record, diff --git a/src/uvtheory/eos/mod.rs b/src/uvtheory/eos/mod.rs index 143b859c0..c2c49955c 100644 --- a/src/uvtheory/eos/mod.rs +++ b/src/uvtheory/eos/mod.rs @@ -2,7 +2,7 @@ #![allow(clippy::needless_range_loop)] use super::parameters::UVParameters; -use feos_core::{parameter::Parameter, EosError, EosResult, EquationOfState, HelmholtzEnergy}; +use feos_core::{parameter::Parameter, Components, EosError, EosResult, HelmholtzEnergy, Residual}; use ndarray::Array1; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -144,7 +144,7 @@ impl UVTheory { } } -impl EquationOfState for UVTheory { +impl Components for UVTheory { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -156,13 +156,15 @@ impl EquationOfState for UVTheory { ) .expect("Not defined for mixture") } +} +impl Residual for UVTheory { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * self.parameters.sigma.mapv(|v| v.powi(3)) * moles).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -192,8 +194,7 @@ mod test { let moles = arr1(&[2.0]) * MOL; let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); - let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) + let a = (s.residual_helmholtz_energy() / s.total_moles) .to_reduced(RGAS * temperature) .unwrap(); assert_relative_eq!(a, 2.972986567516, max_relative = 1e-12); //wca @@ -221,8 +222,7 @@ mod test { let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); - let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) + let a = (s.residual_helmholtz_energy() / s.total_moles) .to_reduced(RGAS * temperature) .unwrap(); @@ -251,8 +251,8 @@ mod test { let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * temperature) + .residual_helmholtz_energy() + .to_reduced(RGAS * temperature * s.total_moles) .unwrap(); dbg!(a); assert_relative_eq!(a, 0.37659379124271003, max_relative = 1e-12); @@ -276,8 +276,8 @@ mod test { let j = Identifier::new(None, None, None, None, None, None); ////////////// - let pr1 = PureRecord::new(i, 1.0, r1, None); - let pr2 = PureRecord::new(j, 1.0, r2, None); + let pr1 = PureRecord::new(i, 1.0, r1); + let pr2 = PureRecord::new(j, 1.0, r2); let pure_records = vec![pr1, pr2]; let uv_parameters = UVParameters::new_binary(pure_records, None); // state @@ -301,8 +301,8 @@ mod test { let state_bh = State::new_nvt(&eos_bh, t_x, volume, &moles).unwrap(); let a_bh = state_bh - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_bh.total_moles) .unwrap(); assert_relative_eq!(a_bh, 2.993577305779432, max_relative = 1e-12); @@ -330,8 +330,8 @@ mod test { let eos_wca = Arc::new(UVTheory::new(Arc::new(p))?); let state_wca = State::new_nvt(&eos_wca, t_x, volume, &moles).unwrap(); let a_wca = state_wca - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_wca.total_moles) .unwrap(); assert_relative_eq!(a_wca, -0.597791038364405, max_relative = 1e-5); @@ -360,8 +360,8 @@ mod test { let eos_wca = Arc::new(UVTheory::new(Arc::new(p))?); let state_wca = State::new_nvt(&eos_wca, t_x, volume, &moles).unwrap(); let a_wca = state_wca - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_wca.total_moles) .unwrap(); assert_relative_eq!(a_wca, -0.034206207363139396, max_relative = 1e-5); Ok(()) diff --git a/src/uvtheory/parameters.rs b/src/uvtheory/parameters.rs index f602084d6..e6dbd1fec 100644 --- a/src/uvtheory/parameters.rs +++ b/src/uvtheory/parameters.rs @@ -118,17 +118,16 @@ pub struct UVParameters { pub eps_k_ij: Array2, pub cd_bh_pure: Vec>, pub cd_bh_binary: Array2>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, } impl Parameter for UVParameters { type Pure = UVRecord; - type IdealGas = NoRecord; type Binary = UVBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -198,7 +197,7 @@ impl Parameter for UVParameters { } } - fn records(&self) -> (&[PureRecord], &Array2) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } @@ -207,7 +206,7 @@ impl UVParameters { /// Parameters for a single substance with molar weight one and no (default) ideal gas contributions. pub fn new_simple(rep: f64, att: f64, sigma: f64, epsilon_k: f64) -> Self { let model_record = UVRecord::new(rep, att, sigma, epsilon_k); - let pure_record = PureRecord::new(Identifier::default(), 1.0, model_record, None); + let pure_record = PureRecord::new(Identifier::default(), 1.0, model_record); Self::new_pure(pure_record) } @@ -256,7 +255,7 @@ pub mod utils { pub fn test_parameters(rep: f64, att: f64, sigma: f64, epsilon: f64) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep, att, sigma, epsilon); - let pr = PureRecord::new(identifier, 1.0, model_record, None); + let pr = PureRecord::new(identifier, 1.0, model_record); UVParameters::new_pure(pr) } @@ -268,11 +267,11 @@ pub mod utils { ) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep[0], att[0], sigma[0], epsilon[0]); - let pr1 = PureRecord::new(identifier, 1.0, model_record, None); + let pr1 = PureRecord::new(identifier, 1.0, model_record); // let identifier2 = Identifier::new(Some("1"), None, None, None, None, None); let model_record2 = UVRecord::new(rep[1], att[1], sigma[1], epsilon[1]); - let pr2 = PureRecord::new(identifier2, 1.0, model_record2, None); + let pr2 = PureRecord::new(identifier2, 1.0, model_record2); let pure_records = vec![pr1, pr2]; UVParameters::new_binary(pure_records, None) } @@ -280,7 +279,7 @@ pub mod utils { pub fn methane_parameters(rep: f64, att: f64) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep, att, 3.7039, 150.03); - let pr = PureRecord::new(identifier, 1.0, model_record, None); + let pr = PureRecord::new(identifier, 1.0, model_record); UVParameters::new_pure(pr) } } From 134b7887bad4987a283c59c026e6c52706bea9ed Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Tue, 6 Jun 2023 13:00:53 +0200 Subject: [PATCH 17/47] Change Joback parameters for test case --- feos-core/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 9f908048d..b429dcb5c 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -182,7 +182,7 @@ mod tests { let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); let residual = Arc::new(PengRobinson::new(Arc::new(parameters))); let ideal_gas = Arc::new(Joback::new(Arc::new(vec![JobackRecord::new( - 1.0, 1.0, 1.0, 1.0, 1.0, + 0.0, 0.0, 0.0, 0.0, 0.0, )]))); let eos = Arc::new(EquationOfState::new(ideal_gas, residual.clone())); @@ -372,7 +372,7 @@ mod tests { assert_relative_eq!( s.c_p(Contributions::Residual), sr.c_p_res(), - max_relative = 1e-15 + max_relative = 1e-14 ); Ok(()) } From 5f63e75340fa91623411d89d325bdb7f06be8e52 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Tue, 6 Jun 2023 13:09:38 +0200 Subject: [PATCH 18/47] Expose IdealGasModel and ResidualModel --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 0e7387a2f..1d5cd4258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ // #[cfg(feature = "dft")] // pub use dft::FunctionalVariant; mod eos; -// pub use eos::EosVariant; +pub use eos::{IdealGasModel, ResidualModel}; #[cfg(feature = "estimator")] pub mod estimator; From 454100f0dcdf100adf4022d0bc113cd31a662e39 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 6 Jun 2023 13:40:18 +0200 Subject: [PATCH 19/47] Fix calculation of critical points and some phase equilibria --- Cargo.toml | 2 +- feos-core/src/phase_equilibria/vle_pure.rs | 5 +- feos-core/src/state/critical_point.rs | 37 ++++++---- feos-core/src/state/mod.rs | 8 +-- tests/pcsaft/vle_pure.rs | 82 +++++++++++----------- 5 files changed, 66 insertions(+), 68 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0f4525df9..ff44725de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ inherits = "release" lto = true [features] -default = ["pcsaft"] +default = [] dft = ["feos-dft", "petgraph"] estimator = [] association = [] diff --git a/feos-core/src/phase_equilibria/vle_pure.rs b/feos-core/src/phase_equilibria/vle_pure.rs index e2ab5c3f9..422ac06ff 100644 --- a/feos-core/src/phase_equilibria/vle_pure.rs +++ b/feos-core/src/phase_equilibria/vle_pure.rs @@ -83,16 +83,13 @@ impl PhaseEquilibrium { let (p_l, p_rho_l) = liquid.p_dpdrho(); let (p_v, p_rho_v) = vapor.p_dpdrho(); // calculate the molar Helmholtz energies (already cached) - // let a_l = liquid.molar_helmholtz_energy(Contributions::Total); let a_l_res = liquid.residual_helmholtz_energy() / liquid.total_moles; - // let a_v = vapor.molar_helmholtz_energy(Contributions::Total); let a_v_res = vapor.residual_helmholtz_energy() / vapor.total_moles; // Estimate the new pressure let kt = SIUnit::gas_constant() * vapor.temperature; let delta_v = 1.0 / vapor.density - 1.0 / liquid.density; - let delta_a = - a_v_res - a_l_res + kt * vapor.density.to_reduced(liquid.temperature)?.ln(); + let delta_a = a_v_res - a_l_res + kt * vapor.density.to_reduced(liquid.density)?.ln(); let mut p_new = -delta_a / delta_v; // If the pressure becomes negative, assume the gas phase is ideal. The diff --git a/feos-core/src/state/critical_point.rs b/feos-core/src/state/critical_point.rs index 7d90b4bc9..e314861bb 100644 --- a/feos-core/src/state/critical_point.rs +++ b/feos-core/src/state/critical_point.rs @@ -472,8 +472,7 @@ fn critical_point_objective( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, m); - // (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -493,9 +492,9 @@ fn critical_point_objective( Dual3::from_re(density.recip() * moles.sum()), moles_hd, ); - // let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); let res = eos.evaluate_residual(&state_s); - Ok(SVector::from([eval, res.v3])) + Ok(SVector::from([eval, (res + ig).v3])) } fn critical_point_objective_t( @@ -511,8 +510,7 @@ fn critical_point_objective_t( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - // (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -528,8 +526,9 @@ fn critical_point_objective_t( ) }); let state_s = StateHD::new(Dual3::from(temperature), Dual3::from(1.0), moles_hd); - let res = eos.evaluate_residual(&state_s); // + eos.ideal_gas().evaluate(&state_s); - Ok(SVector::from([eval, res.v3])) + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); + let res = eos.evaluate_residual(&state_s); + Ok(SVector::from([eval, (res + ig).v3])) } fn critical_point_objective_p( @@ -546,8 +545,7 @@ fn critical_point_objective_p( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - // (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -563,17 +561,19 @@ fn critical_point_objective_p( ) }); let state_s = StateHD::new(Dual3::from_re(temperature), Dual3::from(1.0), moles_hd); - let res = eos.evaluate_residual(&state_s); // + eos.ideal_gas().evaluate(&state_s); + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); + let res = eos.evaluate_residual(&state_s); // calculate pressure let a = |v| { let m = arr1(&[Dual::from_re(density[0]), Dual::from_re(density[1])]); let state_p = StateHD::new(Dual::from_re(temperature), v, m); - eos.evaluate_residual(&state_p) // + eos.ideal_gas().evaluate(&state_p) + -eos.evaluate_residual(&state_p) + state_p.partial_density.sum() }; let (_, p) = first_derivative(a, DualVec::one()); + let p = (p - density.sum()) * temperature; - Ok(SVector::from([eval, res.v3, p * temperature + pressure])) + Ok(SVector::from([eval, (res + ig).v3, p + pressure])) } fn spinodal_objective( @@ -590,8 +590,7 @@ fn spinodal_objective( m[i].eps1 = Dual64::one(); m[j].eps2 = Dual64::one(); let state = StateHD::new(t, v, m); - // (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue of q @@ -623,3 +622,11 @@ fn smallest_ev_scalar(m: DMatrix) -> (Dual64, DVector) { .unwrap(); (*e, ev.into()) } + +fn kronecker(i: usize, j: usize) -> f64 { + if i == j { + 1.0 + } else { + 0.0 + } +} diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index 543e475a0..d47da2cb5 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -575,13 +575,7 @@ impl State { (Ok(_), Err(_)) => liquid, (Err(_), Ok(_)) => vapor, (Ok(l), Ok(v)) => { - if l.residual_gibbs_energy() - > v.residual_gibbs_energy() - + moles.sum() - * SIUnit::gas_constant() - * v.temperature - * (l.volume.to_reduced(v.volume)?.ln()) - { + if l.residual_gibbs_energy() > v.residual_gibbs_energy() { vapor } else { liquid diff --git a/tests/pcsaft/vle_pure.rs b/tests/pcsaft/vle_pure.rs index eb31d5078..59b138ed1 100644 --- a/tests/pcsaft/vle_pure.rs +++ b/tests/pcsaft/vle_pure.rs @@ -34,44 +34,44 @@ fn vle_pure_temperature() -> Result<(), Box> { Ok(()) } -#[test] -fn vle_pure_pressure() -> Result<(), Box> { - let params = PcSaftParameters::from_json( - vec!["propane"], - "tests/pcsaft/test_parameters.json", - None, - IdentifierOption::Name, - )?; - let saft = Arc::new(PcSaft::new(Arc::new(params))); - let pressures = [0.1 * BAR, 1.0 * BAR, 10.0 * BAR, 30.0 * BAR, 44.0 * BAR]; - for &p in pressures.iter() { - let state = PhaseEquilibrium::pure(&saft, p, None, Default::default())?; - println!( - "liquid-p: {} vapor-p: {} p:{}", - state.liquid().pressure(Contributions::Total), - state.vapor().pressure(Contributions::Total), - p - ); - println!( - "liquid-T: {} vapor-T: {}", - state.liquid().temperature, - state.vapor().temperature - ); - assert_relative_eq!( - state.liquid().pressure(Contributions::Total), - p, - max_relative = 1e-8 - ); - assert_relative_eq!( - state.vapor().pressure(Contributions::Total), - state.liquid().pressure(Contributions::Total), - max_relative = 1e-8 - ); - assert_relative_eq!( - state.vapor().temperature, - state.liquid().temperature, - max_relative = 1e-10 - ); - } - Ok(()) -} +// #[test] +// fn vle_pure_pressure() -> Result<(), Box> { +// let params = PcSaftParameters::from_json( +// vec!["propane"], +// "tests/pcsaft/test_parameters.json", +// None, +// IdentifierOption::Name, +// )?; +// let saft = Arc::new(PcSaft::new(Arc::new(params))); +// let pressures = [0.1 * BAR, 1.0 * BAR, 10.0 * BAR, 30.0 * BAR, 44.0 * BAR]; +// for &p in pressures.iter() { +// let state = PhaseEquilibrium::pure(&saft, p, None, Default::default())?; +// println!( +// "liquid-p: {} vapor-p: {} p:{}", +// state.liquid().pressure(Contributions::Total), +// state.vapor().pressure(Contributions::Total), +// p +// ); +// println!( +// "liquid-T: {} vapor-T: {}", +// state.liquid().temperature, +// state.vapor().temperature +// ); +// assert_relative_eq!( +// state.liquid().pressure(Contributions::Total), +// p, +// max_relative = 1e-8 +// ); +// assert_relative_eq!( +// state.vapor().pressure(Contributions::Total), +// state.liquid().pressure(Contributions::Total), +// max_relative = 1e-8 +// ); +// assert_relative_eq!( +// state.vapor().temperature, +// state.liquid().temperature, +// max_relative = 1e-10 +// ); +// } +// Ok(()) +// } From 61fc6f3b43ad61980a78ec5976a6d6c8f0dde6cf Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 6 Jun 2023 14:00:53 +0200 Subject: [PATCH 20/47] Fix `init_pure_p` --- feos-core/src/phase_equilibria/vle_pure.rs | 5 +- tests/pcsaft/vle_pure.rs | 82 +++++++++++----------- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/feos-core/src/phase_equilibria/vle_pure.rs b/feos-core/src/phase_equilibria/vle_pure.rs index 422ac06ff..7c3f30fc4 100644 --- a/feos-core/src/phase_equilibria/vle_pure.rs +++ b/feos-core/src/phase_equilibria/vle_pure.rs @@ -322,7 +322,10 @@ impl PhaseEquilibrium { } for _ in 0..20 { - t0 = (e.vapor().residual_enthalpy() - e.liquid().residual_enthalpy()) + let h = |s: &State<_>| { + s.residual_enthalpy() + s.total_moles * SIUnit::gas_constant() * s.temperature + }; + t0 = (h(e.vapor()) - h(e.liquid())) / (e.vapor().residual_entropy() - e.liquid().residual_entropy() - SIUnit::gas_constant() diff --git a/tests/pcsaft/vle_pure.rs b/tests/pcsaft/vle_pure.rs index 59b138ed1..eb31d5078 100644 --- a/tests/pcsaft/vle_pure.rs +++ b/tests/pcsaft/vle_pure.rs @@ -34,44 +34,44 @@ fn vle_pure_temperature() -> Result<(), Box> { Ok(()) } -// #[test] -// fn vle_pure_pressure() -> Result<(), Box> { -// let params = PcSaftParameters::from_json( -// vec!["propane"], -// "tests/pcsaft/test_parameters.json", -// None, -// IdentifierOption::Name, -// )?; -// let saft = Arc::new(PcSaft::new(Arc::new(params))); -// let pressures = [0.1 * BAR, 1.0 * BAR, 10.0 * BAR, 30.0 * BAR, 44.0 * BAR]; -// for &p in pressures.iter() { -// let state = PhaseEquilibrium::pure(&saft, p, None, Default::default())?; -// println!( -// "liquid-p: {} vapor-p: {} p:{}", -// state.liquid().pressure(Contributions::Total), -// state.vapor().pressure(Contributions::Total), -// p -// ); -// println!( -// "liquid-T: {} vapor-T: {}", -// state.liquid().temperature, -// state.vapor().temperature -// ); -// assert_relative_eq!( -// state.liquid().pressure(Contributions::Total), -// p, -// max_relative = 1e-8 -// ); -// assert_relative_eq!( -// state.vapor().pressure(Contributions::Total), -// state.liquid().pressure(Contributions::Total), -// max_relative = 1e-8 -// ); -// assert_relative_eq!( -// state.vapor().temperature, -// state.liquid().temperature, -// max_relative = 1e-10 -// ); -// } -// Ok(()) -// } +#[test] +fn vle_pure_pressure() -> Result<(), Box> { + let params = PcSaftParameters::from_json( + vec!["propane"], + "tests/pcsaft/test_parameters.json", + None, + IdentifierOption::Name, + )?; + let saft = Arc::new(PcSaft::new(Arc::new(params))); + let pressures = [0.1 * BAR, 1.0 * BAR, 10.0 * BAR, 30.0 * BAR, 44.0 * BAR]; + for &p in pressures.iter() { + let state = PhaseEquilibrium::pure(&saft, p, None, Default::default())?; + println!( + "liquid-p: {} vapor-p: {} p:{}", + state.liquid().pressure(Contributions::Total), + state.vapor().pressure(Contributions::Total), + p + ); + println!( + "liquid-T: {} vapor-T: {}", + state.liquid().temperature, + state.vapor().temperature + ); + assert_relative_eq!( + state.liquid().pressure(Contributions::Total), + p, + max_relative = 1e-8 + ); + assert_relative_eq!( + state.vapor().pressure(Contributions::Total), + state.liquid().pressure(Contributions::Total), + max_relative = 1e-8 + ); + assert_relative_eq!( + state.vapor().temperature, + state.liquid().temperature, + max_relative = 1e-10 + ); + } + Ok(()) +} From 6ce52dff784b4a0c3817e26abb07b291271cd3d0 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 6 Jun 2023 14:28:01 +0200 Subject: [PATCH 21/47] fix pdgt --- feos-dft/src/pdgt.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feos-dft/src/pdgt.rs b/feos-dft/src/pdgt.rs index 30b0af291..d9c138660 100644 --- a/feos-dft/src/pdgt.rs +++ b/feos-dft/src/pdgt.rs @@ -174,7 +174,8 @@ impl DFT { let rhoi_b = vle.vapor().partial_density.get(i); let mui_res = mu_res.get(i); let kt = SIUnit::gas_constant() * vle.vapor().temperature; - delta_omega += &(&rhoi * (kt * rhoi.to_reduced(rhoi_b)?.mapv(f64::ln) - mui_res)); + delta_omega += + &(&rhoi * (kt * (rhoi.to_reduced(rhoi_b)?.mapv(f64::ln) - 1.0) - mui_res)); } delta_omega += vle.vapor().pressure(Contributions::Total); From b62cd1b9aede2d3c85e5f262d5c28639b0a9c75f Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 6 Jun 2023 14:57:00 +0200 Subject: [PATCH 22/47] revive DeBroglieWavelength traits --- feos-core/src/equation_of_state/ideal_gas.rs | 59 ++++++++++++++++++-- feos-core/src/equation_of_state/mod.rs | 17 ++---- feos-core/src/joback.rs | 18 ++++-- feos-core/src/lib.rs | 4 +- feos-core/src/state/properties.rs | 4 +- feos-derive/src/ideal_gas.rs | 36 ++++++------ feos-dft/src/functional.rs | 10 +--- feos-dft/src/profile/properties.rs | 5 +- src/eos.rs | 1 - 9 files changed, 103 insertions(+), 51 deletions(-) diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs index 67304236d..577322243 100644 --- a/feos-core/src/equation_of_state/ideal_gas.rs +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -2,6 +2,8 @@ use super::Components; use crate::StateHD; use ndarray::Array1; use num_dual::DualNum; +use num_dual::*; +use std::fmt; /// Ideal gas Helmholtz energy contribution that can /// be evaluated using generalized (hyper) dual numbers. @@ -11,17 +13,22 @@ use num_dual::DualNum; /// so that the implementor can be used as an ideal gas /// contribution in the equation of state. pub trait IdealGas: Components + Sync + Send { - fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1; + // fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1; - fn ideal_gas_model(&self) -> String; + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength; /// Evaluate the ideal gas contribution for a given state. /// /// In some cases it could be advantageous to overwrite this /// implementation instead of implementing the de Broglie /// wavelength. - fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D { - let lambda = self.de_broglie_wavelength(state.temperature); + fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D + where + for<'a> dyn DeBroglieWavelength + 'a: DeBroglieWavelengthDual, + { + let lambda = self + .ideal_gas_model() + .de_broglie_wavelength(state.temperature); ((lambda + state.partial_density.mapv(|x| { if x.re() == 0.0 { @@ -34,3 +41,47 @@ pub trait IdealGas: Components + Sync + Send { .sum() } } + +pub trait DeBroglieWavelengthDual> { + fn de_broglie_wavelength(&self, temperature: D) -> Array1; +} + +pub trait DeBroglieWavelength: + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + fmt::Display + + Send + + Sync +{ +} + +impl DeBroglieWavelength for T where + T: DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + fmt::Display + + Send + + Sync +{ +} diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 2e591d80f..0b0009752 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -1,15 +1,14 @@ +use crate::EosResult; use ndarray::Array1; -use num_dual::DualNum; use quantity::si::{SIArray1, SINumber}; use std::sync::Arc; -pub mod helmholtz_energy; -pub mod ideal_gas; -pub mod residual; -use crate::EosResult; +mod helmholtz_energy; +mod ideal_gas; +mod residual; pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; -pub use ideal_gas::IdealGas; +pub use ideal_gas::{DeBroglieWavelength, DeBroglieWavelengthDual, IdealGas}; pub use residual::{EntropyScaling, Residual}; /// Molar weight of all components. @@ -59,11 +58,7 @@ impl Components for EquationOfState { } impl IdealGas for EquationOfState { - fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { - self.ideal_gas.de_broglie_wavelength(temperature) - } - - fn ideal_gas_model(&self) -> String { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { self.ideal_gas.ideal_gas_model() } } diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index 0ce830a6f..50594c872 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -1,7 +1,7 @@ //! Implementation of the ideal gas heat capacity (de Broglie wavelength) //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). -use crate::equation_of_state::Components; +use crate::equation_of_state::{Components, DeBroglieWavelength, DeBroglieWavelengthDual}; use crate::parameter::*; use crate::{EosResult, EosUnit, IdealGas}; use conv::ValueInto; @@ -9,7 +9,7 @@ use ndarray::Array1; use num_dual::*; use quantity::si::{SINumber, SIUnit}; use serde::{Deserialize, Serialize}; -use std::fmt; +use std::fmt::{self}; use std::sync::Arc; /// Coefficients used in the Joback model. @@ -103,7 +103,13 @@ impl Components for Joback { } impl IdealGas for Joback { - fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self + } +} + +impl + Copy> DeBroglieWavelengthDual for Joback { + fn de_broglie_wavelength(&self, temperature: D) -> Array1 { let t = temperature; let t2 = t * t; let f = (temperature * KB / (P0 * A3)).ln(); @@ -122,9 +128,11 @@ impl IdealGas for Joback { (h - t * s) / (t * RGAS) + f }) } +} - fn ideal_gas_model(&self) -> String { - "Ideal gas (Joback)".into() +impl fmt::Display for Joback { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal gas (Joback)") } } diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index b429dcb5c..72e763808 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -36,8 +36,8 @@ pub mod parameter; mod phase_equilibria; mod state; pub use equation_of_state::{ - Components, EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, - MolarWeight, Residual, + Components, DeBroglieWavelength, DeBroglieWavelengthDual, EntropyScaling, EquationOfState, + HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, }; pub use errors::{EosError, EosResult}; pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium}; diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index f59f9746a..2e1df7e73 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -328,7 +328,7 @@ impl State { let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); res.push(( - self.eos.ideal_gas_model(), + self.eos.ideal_gas_model().to_string(), self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature * SIUnit::reference_energy(), @@ -345,7 +345,7 @@ impl State { let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); res.push(( - self.eos.ideal_gas_model(), + self.eos.ideal_gas_model().to_string(), (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps * SIUnit::reference_molar_energy(), )); diff --git a/feos-derive/src/ideal_gas.rs b/feos-derive/src/ideal_gas.rs index e77148427..617ad0af3 100644 --- a/feos-derive/src/ideal_gas.rs +++ b/feos-derive/src/ideal_gas.rs @@ -28,31 +28,31 @@ fn impl_ideal_gas( } } }); - let de_broglie_wavelength = variants.iter().map(|v| { - let name = &v.ident; - if name == "NoModel" { - quote! { - Self::#name(_) => panic!("No ideal gas model initialized!") - } - } else { - quote! { - Self::#name(ideal_gas) => ideal_gas.de_broglie_wavelength(temperature) - } - } - }); + // let de_broglie_wavelength = variants.iter().map(|v| { + // let name = &v.ident; + // if name == "NoModel" { + // quote! { + // Self::#name(_) => panic!("No ideal gas model initialized!") + // } + // } else { + // quote! { + // Self::#name(ideal_gas) => ideal_gas.de_broglie_wavelength(temperature) + // } + // } + // }); quote! { impl IdealGas for IdealGasModel { - fn ideal_gas_model(&self) -> String { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { match self { #(#ideal_gas_model,)* } } - fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { - match self { - #(#de_broglie_wavelength,)* - } - } + // fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { + // match self { + // #(#de_broglie_wavelength,)* + // } + // } } } } diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index ffff885c4..879a08ba1 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -3,8 +3,8 @@ use crate::functional_contribution::*; use crate::ideal_chain_contribution::IdealChainContribution; use crate::weight_functions::{WeightFunction, WeightFunctionInfo, WeightFunctionShape}; use feos_core::{ - Components, EosResult, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, - MolarWeight, Residual, StateHD, + Components, DeBroglieWavelength, EosResult, EquationOfState, HelmholtzEnergy, + HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, StateHD, }; use ndarray::*; use num_dual::*; @@ -130,11 +130,7 @@ impl Residual for DFT { } impl IdealGas for DFT { - fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { - self.0.de_broglie_wavelength(temperature) - } - - fn ideal_gas_model(&self) -> String { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { self.0.ideal_gas_model() } } diff --git a/feos-dft/src/profile/properties.rs b/feos-dft/src/profile/properties.rs index 48b4475df..2b0cfd170 100644 --- a/feos-dft/src/profile/properties.rs +++ b/feos-dft/src/profile/properties.rs @@ -170,7 +170,10 @@ where temperature: Dual64, density: &Array, ) -> Array { - let lambda = self.dft.de_broglie_wavelength(temperature); + let lambda = self + .dft + .ideal_gas_model() + .de_broglie_wavelength(temperature); let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); for (i, rhoi) in density.outer_iter().enumerate() { phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); diff --git a/src/eos.rs b/src/eos.rs index a6e5c9f85..f5cd0d2de 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -15,7 +15,6 @@ use feos_core::python::user_defined::{PyIdealGas, PyResidual}; use feos_core::*; use feos_derive::{Components, IdealGas, Residual}; use ndarray::Array1; -use num_dual::DualNum; use quantity::si::*; /// Collection of different [EquationOfState] implementations. From 6b2b895cf8f0d18ee0a7f19b78701566257f72cc Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 6 Jun 2023 15:48:23 +0200 Subject: [PATCH 23/47] Fix Python for basic equations of state (including user defined) --- feos-core/src/python/state.rs | 11 +-- feos-core/src/python/user_defined.rs | 100 +++++++++++---------------- src/python/eos.rs | 4 +- 3 files changed, 46 insertions(+), 69 deletions(-) diff --git a/feos-core/src/python/state.rs b/feos-core/src/python/state.rs index 79566e322..2bdbab26e 100644 --- a/feos-core/src/python/state.rs +++ b/feos-core/src/python/state.rs @@ -459,18 +459,11 @@ macro_rules! impl_state { /// Return partial molar volume of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_volume(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_volume(contributions)) + fn partial_molar_volume(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_volume()) } /// Return chemical potential of each component. diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index b6931d6e3..8d13559dc 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -1,5 +1,6 @@ use crate::{ - Components, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, StateHD, + Components, DeBroglieWavelength, DeBroglieWavelengthDual, HelmholtzEnergy, HelmholtzEnergyDual, + IdealGas, MolarWeight, Residual, StateHD, }; use ndarray::{arr1, Array1}; use num_dual::*; @@ -9,28 +10,11 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use quantity::python::PySIArray1; use quantity::si::SIArray1; -use std::any::TypeId; use std::fmt; struct PyHelmholtzEnergy(Py); -struct PyDeBroglieWavelength(Py); -impl fmt::Display for PyDeBroglieWavelength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Python de Broglie") - } -} - -pub struct PyIdealGas { - obj: Py, - // de_broglie: Box, -} - -impl fmt::Display for PyIdealGas { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Python ideal gas") - } -} +pub struct PyIdealGas(Py); impl PyIdealGas { pub fn new(obj: Py) -> PyResult { @@ -47,7 +31,7 @@ impl PyIdealGas { if !attr { panic!("{}", "Python Class has to have a method 'ideal_gas_model' with signature:\n\tdef ideal_gas_model(self, state: StateHD) -> HD\nwhere 'HD' has to be any of {{float, Dual64, HyperDual64, HyperDualDual64, Dual3Dual64, Dual3_64}}.") } - Ok(Self { obj }) + Ok(Self(obj)) }) } } @@ -55,7 +39,7 @@ impl PyIdealGas { impl Components for PyIdealGas { fn components(&self) -> usize { Python::with_gil(|py| { - let py_result = self.obj.as_ref(py).call_method0("components").unwrap(); + let py_result = self.0.as_ref(py).call_method0("components").unwrap(); if py_result.get_type().name().unwrap() != "int" { panic!( "Expected an integer for the components() method signature, got {}", @@ -69,7 +53,7 @@ impl Components for PyIdealGas { fn subset(&self, component_list: &[usize]) -> Self { Python::with_gil(|py| { let py_result = self - .obj + .0 .as_ref(py) .call_method1("subset", (component_list.to_vec(),)) .unwrap(); @@ -79,14 +63,14 @@ impl Components for PyIdealGas { } impl IdealGas for PyIdealGas { - fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { - let y = TypeId::of::(); - if y == TypeId::of::() {} - todo!() + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self } +} - fn ideal_gas_model(&self) -> String { - todo!() +impl fmt::Display for PyIdealGas { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal gas (Python)") } } @@ -279,48 +263,48 @@ macro_rules! helmholtz_energy { }; } -// macro_rules! de_broglie_wavelength { -// ($py_hd_id:ident, $hd_ty:ty) => { -// impl DeBroglieWavelengthDual<$hd_ty> for PyDeBroglieWavelength { -// fn de_broglie_wavelength(&self, temperature: $hd_ty) -> Array1<$hd_ty> { -// Python::with_gil(|py| { -// let py_result = self -// .0 -// .as_ref(py) -// .call_method1("ideal_gas_model", (<$py_hd_id>::from(temperature),)) -// .unwrap(); -// let rr = if let Ok(r) = py_result.extract::>() { -// dbg!("Array1"); -// r.to_owned_array() -// .mapv(|ri| <$hd_ty>::from(ri.extract::<$py_hd_id>(py).unwrap())) -// } else if let Ok(r) = py_result.extract::>() { -// dbg!("Array0"); -// assert!(r.ndim() == 0); -// let scalar = &r.to_owned_array()[0]; -// arr1(&[<$hd_ty>::from(scalar.extract::<$py_hd_id>(py).unwrap())]) -// } else { -// panic!("ideal_gas_model: input data type must be numpy ndarray of dimension 1") -// }; -// rr -// }) -// } -// } -// }; -// } +macro_rules! de_broglie_wavelength { + ($py_hd_id:ident, $hd_ty:ty) => { + impl DeBroglieWavelengthDual<$hd_ty> for PyIdealGas { + fn de_broglie_wavelength(&self, temperature: $hd_ty) -> Array1<$hd_ty> { + Python::with_gil(|py| { + let py_result = self + .0 + .as_ref(py) + .call_method1("ideal_gas_model", (<$py_hd_id>::from(temperature),)) + .unwrap(); + let rr = if let Ok(r) = py_result.extract::>() { + dbg!("Array1"); + r.to_owned_array() + .mapv(|ri| <$hd_ty>::from(ri.extract::<$py_hd_id>(py).unwrap())) + } else if let Ok(r) = py_result.extract::>() { + dbg!("Array0"); + assert!(r.ndim() == 0); + let scalar = &r.to_owned_array()[0]; + arr1(&[<$hd_ty>::from(scalar.extract::<$py_hd_id>(py).unwrap())]) + } else { + panic!("ideal_gas_model: input data type must be numpy ndarray of dimension 1") + }; + rr + }) + } + } + }; +} macro_rules! impl_dual_state_helmholtz_energy { ($py_state_id:ident, $py_hd_id:ident, $hd_ty:ty, $py_field_ty:ty) => { dual_number!($py_hd_id, $hd_ty, $py_field_ty); state!($py_state_id, $py_hd_id, $hd_ty); helmholtz_energy!($py_state_id, $py_hd_id, $hd_ty); - // de_broglie_wavelength!($py_hd_id, $hd_ty); + de_broglie_wavelength!($py_hd_id, $hd_ty); }; } // No definition of dual number necessary for f64 state!(PyStateF, f64, f64); helmholtz_energy!(PyStateF, f64, f64); -// de_broglie_wavelength!(f64, f64); +de_broglie_wavelength!(f64, f64); impl_dual_state_helmholtz_energy!(PyStateD, PyDual64, Dual64, f64); dual_number!(PyDualVec3, DualSVec64<3>, f64); diff --git a/src/python/eos.rs b/src/python/eos.rs index 0ca700322..8ee6c234a 100644 --- a/src/python/eos.rs +++ b/src/python/eos.rs @@ -155,7 +155,7 @@ impl PyEquationOfState { #[staticmethod] pub fn peng_robinson(parameters: PyPengRobinsonParameters) -> Self { let residual = Arc::new(ResidualModel::PengRobinson(PengRobinson::new(parameters.0))); - let ideal_gas = Arc::new(IdealGasModel::NoModel); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } @@ -173,7 +173,7 @@ impl PyEquationOfState { #[staticmethod] fn python_residual(residual: Py) -> PyResult { let residual = Arc::new(ResidualModel::Python(PyResidual::new(residual)?)); - let ideal_gas = Arc::new(IdealGasModel::NoModel); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); Ok(Self(Arc::new(EquationOfState::new(ideal_gas, residual)))) } From 40707324ab676b1fc71e21cd7a4111700f1ec3ba Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 6 Jun 2023 20:22:37 +0200 Subject: [PATCH 24/47] docstrings, cleanup and tests --- feos-core/src/cubic.rs | 133 +++++---- feos-core/src/equation_of_state/ideal_gas.rs | 22 +- feos-core/src/equation_of_state/mod.rs | 5 + feos-core/src/equation_of_state/residual.rs | 2 +- feos-core/src/errors.rs | 4 +- feos-core/src/joback.rs | 291 ++++++++++--------- feos-core/src/lib.rs | 4 +- feos-core/src/phase_equilibria/mod.rs | 7 - feos-core/src/state/properties.rs | 65 +---- feos-core/src/state/residual_properties.rs | 7 +- 10 files changed, 244 insertions(+), 296 deletions(-) diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index d3ca40bf9..3d1d555b2 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -241,72 +241,71 @@ impl MolarWeight for PengRobinson { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::phase_equilibria::SolverOptions; -// use crate::state::State; -// use crate::Contributions; -// use crate::{EosResult, Verbosity}; -// use approx::*; -// use quantity::si::*; -// use std::sync::Arc; +#[cfg(test)] +mod tests { + use super::*; + use crate::state::{SolverOptions, State}; + use crate::Contributions; + use crate::{EosResult, Verbosity}; + use approx::*; + use quantity::si::*; + use std::sync::Arc; -// fn pure_record_vec() -> Vec> { -// let records = r#"[ -// { -// "identifier": { -// "cas": "74-98-6", -// "name": "propane", -// "iupac_name": "propane", -// "smiles": "CCC", -// "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", -// "formula": "C3H8" -// }, -// "model_record": { -// "tc": 369.96, -// "pc": 4250000.0, -// "acentric_factor": 0.153 -// }, -// "molarweight": 44.0962 -// }, -// { -// "identifier": { -// "cas": "106-97-8", -// "name": "butane", -// "iupac_name": "butane", -// "smiles": "CCCC", -// "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", -// "formula": "C4H10" -// }, -// "model_record": { -// "tc": 425.2, -// "pc": 3800000.0, -// "acentric_factor": 0.199 -// }, -// "molarweight": 58.123 -// } -// ]"#; -// serde_json::from_str(records).expect("Unable to parse json.") -// } + fn pure_record_vec() -> Vec> { + let records = r#"[ + { + "identifier": { + "cas": "74-98-6", + "name": "propane", + "iupac_name": "propane", + "smiles": "CCC", + "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", + "formula": "C3H8" + }, + "model_record": { + "tc": 369.96, + "pc": 4250000.0, + "acentric_factor": 0.153 + }, + "molarweight": 44.0962 + }, + { + "identifier": { + "cas": "106-97-8", + "name": "butane", + "iupac_name": "butane", + "smiles": "CCCC", + "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", + "formula": "C4H10" + }, + "model_record": { + "tc": 425.2, + "pc": 3800000.0, + "acentric_factor": 0.199 + }, + "molarweight": 58.123 + } + ]"#; + serde_json::from_str(records).expect("Unable to parse json.") + } -// #[test] -// fn peng_robinson() -> EosResult<()> { -// let mixture = pure_record_vec(); -// let propane = mixture[0].clone(); -// let tc = propane.model_record.tc; -// let pc = propane.model_record.pc; -// let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); -// let pr = Arc::new(PengRobinson::new(Arc::new(parameters))); -// let options = SolverOptions::new().verbosity(Verbosity::Iter); -// let cp = State::critical_point(&pr, None, None, options)?; -// println!("{} {}", cp.temperature, cp.pressure(Contributions::Total)); -// assert_relative_eq!(cp.temperature, tc * KELVIN, max_relative = 1e-4); -// assert_relative_eq!( -// cp.pressure(Contributions::Total), -// pc * PASCAL, -// max_relative = 1e-4 -// ); -// Ok(()) -// } -// } + #[test] + fn peng_robinson() -> EosResult<()> { + let mixture = pure_record_vec(); + let propane = mixture[0].clone(); + let tc = propane.model_record.tc; + let pc = propane.model_record.pc; + let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); + let pr = Arc::new(PengRobinson::new(Arc::new(parameters))); + let options = SolverOptions::new().verbosity(Verbosity::Iter); + let cp = State::critical_point(&pr, None, None, options)?; + println!("{} {}", cp.temperature, cp.pressure(Contributions::Total)); + assert_relative_eq!(cp.temperature, tc * KELVIN, max_relative = 1e-4); + assert_relative_eq!( + cp.pressure(Contributions::Total), + pc * PASCAL, + max_relative = 1e-4 + ); + Ok(()) + } +} diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs index 577322243..c2c09062d 100644 --- a/feos-core/src/equation_of_state/ideal_gas.rs +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -5,16 +5,9 @@ use num_dual::DualNum; use num_dual::*; use std::fmt; -/// Ideal gas Helmholtz energy contribution that can -/// be evaluated using generalized (hyper) dual numbers. -/// -/// This trait needs to be implemented generically or for -/// the specific types in the supertraits of [IdealGasContribution] -/// so that the implementor can be used as an ideal gas -/// contribution in the equation of state. +/// Ideal gas Helmholtz energy contribution. pub trait IdealGas: Components + Sync + Send { - // fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1; - + // Return a reference to the implementation of the de Broglie wavelength. fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength; /// Evaluate the ideal gas contribution for a given state. @@ -42,10 +35,21 @@ pub trait IdealGas: Components + Sync + Send { } } +/// Implementation of an ideal gas model in terms of the +/// thermal de Broglie wavelength. +/// +/// This trait needs to be implemented generically or for +/// the specific types in the supertraits of [DeBroglieWavelength] +/// so that the implementor can be used as an ideal gas +/// contribution in the equation of state. pub trait DeBroglieWavelengthDual> { fn de_broglie_wavelength(&self, temperature: D) -> Array1; } +/// Object safe version of the [DeBroglieWavelengthDual] trait. +/// +/// The trait is implemented automatically for every struct that implements +/// the supertraits. pub trait DeBroglieWavelength: DeBroglieWavelengthDual + DeBroglieWavelengthDual diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 0b0009752..2f5d77c11 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -19,6 +19,7 @@ pub trait MolarWeight { fn molar_weight(&self) -> SIArray1; } +/// The number of components that the model is initialized for. pub trait Components { /// Return the number of components of the model. fn components(&self) -> usize; @@ -28,6 +29,8 @@ pub trait Components { fn subset(&self, component_list: &[usize]) -> Self; } +/// An equation of state consisting of an ideal gas model +/// and a residual Helmholtz energy model. #[derive(Clone)] pub struct EquationOfState { pub ideal_gas: Arc, @@ -35,6 +38,8 @@ pub struct EquationOfState { } impl EquationOfState { + /// Return a new [EquationOfState] with the given ideal gas + /// and residual models. pub fn new(ideal_gas: Arc, residual: Arc) -> Self { Self { ideal_gas, diff --git a/feos-core/src/equation_of_state/residual.rs b/feos-core/src/equation_of_state/residual.rs index 2d80c80d8..6746c3718 100644 --- a/feos-core/src/equation_of_state/residual.rs +++ b/feos-core/src/equation_of_state/residual.rs @@ -6,7 +6,7 @@ use num_dual::*; use num_traits::{One, Zero}; use quantity::si::{SIArray1, SINumber, SIUnit}; -/// A general equation of state. +/// A reisdual Helmholtz energy model. pub trait Residual: Components + Send + Sync { /// Return the maximum density in Angstrom^-3. /// diff --git a/feos-core/src/errors.rs b/feos-core/src/errors.rs index 8bc344268..37936a4e6 100644 --- a/feos-core/src/errors.rs +++ b/feos-core/src/errors.rs @@ -1,10 +1,8 @@ -// use crate::parameter::ParameterError; +use crate::parameter::ParameterError; use num_dual::linalg::LinAlgError; use quantity::QuantityError; use thiserror::Error; -use crate::parameter::ParameterError; - /// Error type for improperly defined states and convergence problems. #[derive(Error, Debug)] pub enum EosError { diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index 50594c872..3d84a7574 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -2,7 +2,7 @@ //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). use crate::equation_of_state::{Components, DeBroglieWavelength, DeBroglieWavelengthDual}; -use crate::parameter::*; +use crate::{parameter::*, Residual}; use crate::{EosResult, EosUnit, IdealGas}; use conv::ValueInto; use ndarray::Array1; @@ -10,7 +10,6 @@ use num_dual::*; use quantity::si::{SINumber, SIUnit}; use serde::{Deserialize, Serialize}; use std::fmt::{self}; -use std::sync::Arc; /// Coefficients used in the Joback model. /// @@ -67,13 +66,13 @@ impl> FromSegments for JobackRecord { /// The ideal gas contribution according to /// [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). pub struct Joback { - pub records: Arc>, + pub records: Vec, // de_broglie: Box, } impl Joback { /// Creates a new Joback contribution. - pub fn new(records: Arc>) -> Self { + pub fn new(records: Vec) -> Self { Self { records } } @@ -98,7 +97,7 @@ impl Components for Joback { component_list .iter() .for_each(|&i| records.push(self.records[i].clone())); - Self::new(Arc::new(records)) + Self::new(records) } } @@ -108,6 +107,16 @@ impl IdealGas for Joback { } } +impl Residual for Joback { + fn compute_max_density(&self, _moles: &Array1) -> f64 { + 1.0 + } + + fn contributions(&self) -> &[Box] { + &[] + } +} + impl + Copy> DeBroglieWavelengthDual for Joback { fn de_broglie_wavelength(&self, temperature: D) -> Array1 { let t = temperature; @@ -142,144 +151,144 @@ const P0: f64 = 1.0e5; const A3: f64 = 1e-30; const KB: f64 = 1.38064852e-23; -// #[cfg(test)] -// mod tests { -// use crate::{Contributions, State, StateBuilder}; -// use approx::assert_relative_eq; -// use ndarray::arr1; -// use quantity::si::*; -// use std::sync::Arc; - -// use super::*; +#[cfg(test)] +mod tests { + use crate::{Contributions, State, StateBuilder}; + use approx::assert_relative_eq; + use ndarray::arr1; + use quantity::si::*; + use std::sync::Arc; -// #[derive(Deserialize, Clone, Debug)] -// struct ModelRecord; + use super::*; -// #[test] -// fn paper_example() -> EosResult<()> { -// let segments_json = r#"[ -// { -// "identifier": "-Cl", -// "model_record": null, -// "ideal_gas_record": { -// "a": 33.3, -// "b": -0.0963, -// "c": 0.000187, -// "d": -9.96e-8, -// "e": 0.0 -// }, -// "molarweight": 35.453 -// }, -// { -// "identifier": "-CH=(ring)", -// "model_record": null, -// "ideal_gas_record": { -// "a": -2.14, -// "b": 5.74e-2, -// "c": -1.64e-6, -// "d": -1.59e-8, -// "e": 0.0 -// }, -// "molarweight": 13.01864 -// }, -// { -// "identifier": "=CH<(ring)", -// "model_record": null, -// "ideal_gas_record": { -// "a": -8.25, -// "b": 1.01e-1, -// "c": -1.42e-4, -// "d": 6.78e-8, -// "e": 0.0 -// }, -// "molarweight": 13.01864 -// } -// ]"#; -// let segment_records: Vec> = -// serde_json::from_str(segments_json).expect("Unable to parse json."); -// let segments = ChemicalRecord::new( -// Identifier::default(), -// vec![ -// String::from("-Cl"), -// String::from("-Cl"), -// String::from("-CH=(ring)"), -// String::from("-CH=(ring)"), -// String::from("-CH=(ring)"), -// String::from("-CH=(ring)"), -// String::from("=CH<(ring)"), -// String::from("=CH<(ring)"), -// ], -// None, -// ) -// .segment_map(&segment_records)?; -// assert_eq!(segments.get(&segment_records[0]), Some(&2)); -// assert_eq!(segments.get(&segment_records[1]), Some(&4)); -// assert_eq!(segments.get(&segment_records[2]), Some(&2)); + #[derive(Deserialize, Clone, Debug)] + struct ModelRecord; -// let jr = JobackRecord::from_segments(&joback_segments)?; -// assert_relative_eq!( -// jr.a, -// 33.3 * 2.0 - 2.14 * 4.0 - 8.25 * 2.0 - 37.93, -// epsilon = 1e-10 -// ); -// assert_relative_eq!( -// jr.b, -// -0.0963 * 2.0 + 5.74e-2 * 4.0 + 1.01e-1 * 2.0 + 0.21, -// epsilon = 1e-10 -// ); -// assert_relative_eq!( -// jr.c, -// 0.000187 * 2.0 - 1.64e-6 * 4.0 - 1.42e-4 * 2.0 - 3.91e-4, -// epsilon = 1e-10 -// ); -// assert_relative_eq!( -// jr.d, -// -9.96e-8 * 2.0 - 1.59e-8 * 4.0 + 6.78e-8 * 2.0 + 2.06e-7, -// epsilon = 1e-10 -// ); -// assert_relative_eq!(jr.e, 0.0); + #[test] + fn paper_example() -> EosResult<()> { + let segments_json = r#"[ + { + "identifier": "-Cl", + "model_record": { + "a": 33.3, + "b": -0.0963, + "c": 0.000187, + "d": -9.96e-8, + "e": 0.0 + }, + "molarweight": 35.453 + }, + { + "identifier": "-CH=(ring)", + "model_record": { + "a": -2.14, + "b": 5.74e-2, + "c": -1.64e-6, + "d": -1.59e-8, + "e": 0.0 + }, + "molarweight": 13.01864 + }, + { + "identifier": "=CH<(ring)", + "model_record": { + "a": -8.25, + "b": 1.01e-1, + "c": -1.42e-4, + "d": 6.78e-8, + "e": 0.0 + }, + "molarweight": 13.01864 + } + ]"#; + let segment_records: Vec> = + serde_json::from_str(segments_json).expect("Unable to parse json."); + let segments = ChemicalRecord::new( + Identifier::default(), + vec![ + String::from("-Cl"), + String::from("-Cl"), + String::from("-CH=(ring)"), + String::from("-CH=(ring)"), + String::from("-CH=(ring)"), + String::from("-CH=(ring)"), + String::from("=CH<(ring)"), + String::from("=CH<(ring)"), + ], + None, + ) + .segment_map(&segment_records)?; + assert_eq!(segments.get(&segment_records[0]), Some(&2)); + assert_eq!(segments.get(&segment_records[1]), Some(&4)); + assert_eq!(segments.get(&segment_records[2]), Some(&2)); + let joback_segments: Vec<_> = segments + .iter() + .map(|(s, &n)| (s.model_record.clone(), n)) + .collect(); + let jr = JobackRecord::from_segments(&joback_segments)?; + assert_relative_eq!( + jr.a, + 33.3 * 2.0 - 2.14 * 4.0 - 8.25 * 2.0 - 37.93, + epsilon = 1e-10 + ); + assert_relative_eq!( + jr.b, + -0.0963 * 2.0 + 5.74e-2 * 4.0 + 1.01e-1 * 2.0 + 0.21, + epsilon = 1e-10 + ); + assert_relative_eq!( + jr.c, + 0.000187 * 2.0 - 1.64e-6 * 4.0 - 1.42e-4 * 2.0 - 3.91e-4, + epsilon = 1e-10 + ); + assert_relative_eq!( + jr.d, + -9.96e-8 * 2.0 - 1.59e-8 * 4.0 + 6.78e-8 * 2.0 + 2.06e-7, + epsilon = 1e-10 + ); + assert_relative_eq!(jr.e, 0.0); -// let eos = Arc::new(Joback::new(vec![jr])); -// let state = State::new_nvt( -// &eos, -// 1000.0 * KELVIN, -// 1.0 * ANGSTROM.powi(3), -// &(arr1(&[1.0]) * MOL), -// )?; -// assert!( -// (state -// .c_p(Contributions::IdealGas) -// .to_reduced(JOULE / MOL / KELVIN)? -// - 224.6) -// .abs() -// < 1.0 -// ); -// Ok(()) -// } + let eos = Arc::new(Joback::new(vec![jr])); + let state = State::new_nvt( + &eos, + 1000.0 * KELVIN, + 1.0 * ANGSTROM.powi(3), + &(arr1(&[1.0]) * MOL), + )?; + assert!( + (state + .c_p(Contributions::IdealGas) + .to_reduced(JOULE / MOL / KELVIN)? + - 224.6) + .abs() + < 1.0 + ); + Ok(()) + } -// #[test] -// fn c_p_comparison() -> EosResult<()> { -// let record1 = JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005); -// let record2 = JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001); -// let joback = Arc::new(Joback::new(vec![record1, record2])); -// let temperature = 300.0 * KELVIN; -// let volume = METER.powi(3); -// let moles = arr1(&[1.0, 3.0]) * MOL; -// let state = StateBuilder::new(&joback) -// .temperature(temperature) -// .volume(volume) -// .moles(&moles) -// .build()?; -// println!( -// "{} {}", -// joback.c_p(temperature, &state.molefracs)?, -// state.c_p(Contributions::IdealGas) -// ); -// assert_relative_eq!( -// joback.c_p(temperature, &state.molefracs)?, -// state.c_p(Contributions::IdealGas), -// max_relative = 1e-10 -// ); -// Ok(()) -// } -// } + #[test] + fn c_p_comparison() -> EosResult<()> { + let record1 = JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005); + let record2 = JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001); + let joback = Arc::new(Joback::new(vec![record1, record2])); + let temperature = 300.0 * KELVIN; + let volume = METER.powi(3); + let moles = arr1(&[1.0, 3.0]) * MOL; + let state = StateBuilder::new(&joback) + .temperature(temperature) + .volume(volume) + .moles(&moles) + .build()?; + println!( + "{} {}", + joback.c_p(temperature, &state.molefracs)?, + state.c_p(Contributions::IdealGas) + ); + assert_relative_eq!( + joback.c_p(temperature, &state.molefracs)?, + state.c_p(Contributions::IdealGas), + max_relative = 1e-10 + ); + Ok(()) + } +} diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 72e763808..8e917c2e8 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -181,9 +181,9 @@ mod tests { let propane = mixture[0].clone(); let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); let residual = Arc::new(PengRobinson::new(Arc::new(parameters))); - let ideal_gas = Arc::new(Joback::new(Arc::new(vec![JobackRecord::new( + let ideal_gas = Arc::new(Joback::new(vec![JobackRecord::new( 0.0, 0.0, 0.0, 0.0, 0.0, - )]))); + )])); let eos = Arc::new(EquationOfState::new(ideal_gas, residual.clone())); let sr = StateBuilder::new(&residual) diff --git a/feos-core/src/phase_equilibria/mod.rs b/feos-core/src/phase_equilibria/mod.rs index b17a25337..9b7a76baa 100644 --- a/feos-core/src/phase_equilibria/mod.rs +++ b/feos-core/src/phase_equilibria/mod.rs @@ -259,13 +259,6 @@ impl PhaseEquilibrium { Ok(()) } - // pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { - // for s in self.0.iter_mut() { - // s.update_chemical_potential(chemical_potential)?; - // } - // Ok(()) - // } - // Total Gibbs energy excluding the constant contribution RT sum_i N_i ln(\Lambda_i^3) pub(super) fn total_gibbs_energy(&self) -> SINumber { self.0 diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index 2e1df7e73..d806c6557 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -10,7 +10,6 @@ pub(crate) enum Evaluate { IdealGas, Residual, Total, - // IdealGasDelta, } impl State { @@ -19,38 +18,6 @@ impl State { derivative: PartialDerivative, evaluate: Evaluate, ) -> SINumber { - // if let Evaluate::IdealGasDelta = evaluate { - // return match derivative { - // PartialDerivative::Zeroth => { - // let new_state = self.derive0(); - // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - // * SIUnit::reference_energy() - // } - // PartialDerivative::First(v) => { - // let new_state = self.derive1(v); - // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).eps - // * (SIUnit::reference_energy() / v.reference()) - // } - // PartialDerivative::Second(v) => { - // let new_state = self.derive2(v); - // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v2 - // * (SIUnit::reference_energy() / (v.reference() * v.reference())) - // } - // PartialDerivative::SecondMixed(v1, v2) => { - // let new_state = self.derive2_mixed(v1, v2); - // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - // .eps1eps2 - // * (SIUnit::reference_energy() / (v1.reference() * v2.reference())) - // } - // PartialDerivative::Third(v) => { - // let new_state = self.derive3(v); - // -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v3 - // * (SIUnit::reference_energy() - // / (v.reference() * v.reference() * v.reference())) - // } - // }; - // } - let residual = match evaluate { Evaluate::IdealGas => None, _ => Some(self.get_or_compute_derivative_residual(derivative)), @@ -114,21 +81,7 @@ impl State { } else { f(self, Evaluate::Total) - f(self, Evaluate::IdealGas) } - } // Contributions::ResidualNpt => { - // let p = self.pressure_(Evaluate::Total); - // let state_p = Self::new_nvt_unchecked( - // &self.eos, - // self.temperature, - // self.total_moles * SIUnit::gas_constant() * self.temperature / p, - // &self.moles, - // ); - // if additive { - // f(self, Evaluate::Residual) + f(self, Evaluate::IdealGasDelta) - // - f(&state_p, Evaluate::IdealGasDelta) - // } else { - // f(self, Evaluate::Total) - f(&state_p, Evaluate::IdealGas) - // } - // } + } } } @@ -164,23 +117,12 @@ impl State { }) } - // fn d2p_dv2_(&self, evaluate: Evaluate) -> SINumber { - // -self.get_or_compute_derivative(PartialDerivative::Third(DV), evaluate) - // } - fn dmu_dt_(&self, evaluate: Evaluate) -> SIArray1 { SIArray::from_shape_fn(self.eos.components(), |i| { self.get_or_compute_derivative(PartialDerivative::SecondMixed(DT, DN(i)), evaluate) }) } - // fn dmu_dni_(&self, evaluate: Evaluate) -> SIArray2 { - // let n = self.eos.components(); - // SIArray::from_shape_fn((n, n), |(i, j)| { - // self.get_or_compute_derivative(PartialDerivative::SecondMixed(DN(i), DN(j)), evaluate) - // }) - // } - fn ds_dt_(&self, evaluate: Evaluate) -> SINumber { -self.get_or_compute_derivative(PartialDerivative::Second(DT), evaluate) } @@ -199,11 +141,6 @@ impl State { self.evaluate_property(Self::dmu_dt_, contributions, true) } - // /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ - // pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { - // self.evaluate_property(Self::dmu_dni_, contributions, true) - // } - /// Molar isochoric heat capacity: $c_v=\left(\frac{\partial u}{\partial T}\right)_{V,N_i}$ pub fn c_v(&self, contributions: Contributions) -> SINumber { let func = diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 1991f76fa..0533f8a8b 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -1,5 +1,4 @@ use super::{Contributions, Derivative::*, PartialDerivative, State}; -// use crate::equation_of_state::{EntropyScaling, MolarWeight, Residual}; use crate::equation_of_state::{EntropyScaling, Residual}; use crate::errors::EosResult; use crate::EosUnit; @@ -62,10 +61,12 @@ impl State { } } + /// Residual Helmholtz energy $A^\text{res}$ pub fn residual_helmholtz_energy(&self) -> SINumber { self.get_or_compute_derivative_residual(PartialDerivative::Zeroth) } + /// Residual entropy $S^\text{res}=\left(\frac{\partial A^\text{res}}{\partial T}\right)_{V,N_i}$ pub fn residual_entropy(&self) -> SINumber { -self.get_or_compute_derivative_residual(PartialDerivative::First(DT)) } @@ -218,10 +219,12 @@ impl State { // entropy derivatives + /// Partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial S^\text{res}}{\partial T}\right)_{V,N_i}$ pub fn ds_res_dt(&self) -> SINumber { -self.get_or_compute_derivative_residual(PartialDerivative::Second(DT)) } + /// Second partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial^2S^\text{res}}{\partial T^2}\right)_{V,N_i}$ pub fn d2s_res_dt2(&self) -> SINumber { -self.get_or_compute_derivative_residual(PartialDerivative::Third(DT)) } @@ -330,7 +333,7 @@ impl State { + self.pressure(Contributions::Residual) * self.volume } - /// Residual internal energy: $U^\text{res}(T, V, \mathbf{n})=A^\text{res}+TS^\text{res}$ + /// Residual internal energy: $U^\text{res}(T,V,\mathbf{n})=A^\text{res}+TS^\text{res}$ pub fn residual_internal_energy(&self) -> SINumber { self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() } From 159870531ea03c91c442bb4770267e8d1b0ef9c6 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 6 Jun 2023 20:52:54 +0200 Subject: [PATCH 25/47] Explicitly handle non-additive properties and simplify the rest --- feos-core/src/state/properties.rs | 157 ++++++++---------------------- 1 file changed, 41 insertions(+), 116 deletions(-) diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index d806c6557..5567bb142 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -3,28 +3,20 @@ use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; use crate::EosUnit; use ndarray::Array1; use quantity::si::*; -use std::ops::{Add, Sub}; - -#[derive(Clone, Copy)] -pub(crate) enum Evaluate { - IdealGas, - Residual, - Total, -} impl State { fn get_or_compute_derivative( &self, derivative: PartialDerivative, - evaluate: Evaluate, + contributions: Contributions, ) -> SINumber { - let residual = match evaluate { - Evaluate::IdealGas => None, + let residual = match contributions { + Contributions::IdealGas => None, _ => Some(self.get_or_compute_derivative_residual(derivative)), }; - let ideal_gas = match evaluate { - Evaluate::Residual => None, + let ideal_gas = match contributions { + Contributions::Residual => None, _ => Some(match derivative { PartialDerivative::Zeroth => { let new_state = self.derive0(); @@ -67,112 +59,56 @@ impl State { } } - fn evaluate_property(&self, f: F, contributions: Contributions, additive: bool) -> R - where - R: Add + Sub, - F: Fn(&Self, Evaluate) -> R, - { - match contributions { - Contributions::IdealGas => f(self, Evaluate::IdealGas), - Contributions::Total => f(self, Evaluate::Total), - Contributions::Residual => { - if additive { - f(self, Evaluate::Residual) - } else { - f(self, Evaluate::Total) - f(self, Evaluate::IdealGas) - } - } - } - } - - fn helmholtz_energy_(&self, evaluate: Evaluate) -> SINumber { - self.get_or_compute_derivative(PartialDerivative::Zeroth, evaluate) - } - - fn pressure_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::First(DV), evaluate) - } - - fn entropy_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::First(DT), evaluate) - } - - fn chemical_potential_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - self.get_or_compute_derivative(PartialDerivative::First(DN(i)), evaluate) - }) - } - - fn dp_dv_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Second(DV), evaluate) - } - - fn dp_dt_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::SecondMixed(DV, DT), evaluate) - } - - fn dp_dni_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - -self.get_or_compute_derivative(PartialDerivative::SecondMixed(DV, DN(i)), evaluate) - }) - } - - fn dmu_dt_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - self.get_or_compute_derivative(PartialDerivative::SecondMixed(DT, DN(i)), evaluate) - }) - } - - fn ds_dt_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Second(DT), evaluate) - } - - fn d2s_dt2_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Third(DT), evaluate) - } - /// Chemical potential: $\mu_i=\left(\frac{\partial A}{\partial N_i}\right)_{T,V,N_j}$ pub fn chemical_potential(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::chemical_potential_, contributions, true) + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative(PartialDerivative::First(DN(i)), contributions) + }) } /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$ pub fn dmu_dt(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::dmu_dt_, contributions, true) + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative(PartialDerivative::SecondMixed(DT, DN(i)), contributions) + }) } /// Molar isochoric heat capacity: $c_v=\left(\frac{\partial u}{\partial T}\right)_{V,N_i}$ pub fn c_v(&self, contributions: Contributions) -> SINumber { - let func = - |s: &Self, evaluate: Evaluate| s.temperature * s.ds_dt_(evaluate) / s.total_moles; - self.evaluate_property(func, contributions, true) + self.temperature * self.ds_dt(contributions) / self.total_moles } /// 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, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - (s.temperature * s.d2s_dt2_(evaluate) + s.ds_dt_(evaluate)) / s.total_moles - }; - self.evaluate_property(func, contributions, true) + (self.temperature * self.d2s_dt2(contributions) + self.ds_dt(contributions)) + / self.total_moles } /// Molar isobaric heat capacity: $c_p=\left(\frac{\partial h}{\partial T}\right)_{p,N_i}$ pub fn c_p(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature / s.total_moles - * (s.ds_dt_(evaluate) - s.dp_dt_(evaluate).powi(2) / s.dp_dv_(evaluate)) - }; - self.evaluate_property(func, contributions, false) + match contributions { + Contributions::Residual => self.c_p_res(), + _ => { + self.temperature / self.total_moles + * (self.ds_dt(contributions) + - self.dp_dt(contributions).powi(2) / self.dp_dv(contributions)) + } + } } /// Entropy: $S=-\left(\frac{\partial A}{\partial T}\right)_{V,N_i}$ pub fn entropy(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::entropy_, contributions, true) + -self.get_or_compute_derivative(PartialDerivative::First(DT), contributions) } /// Partial derivative of the entropy w.r.t. temperature: $\left(\frac{\partial S}{\partial T}\right)_{V,N_i}$ pub fn ds_dt(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::ds_dt_, contributions, true) + -self.get_or_compute_derivative(PartialDerivative::Second(DT), contributions) + } + + /// Second partial derivative of the entropy w.r.t. temperature: $\left(\frac{\partial^2 S}{\partial T^2}\right)_{V,N_i}$ + pub fn d2s_dt2(&self, contributions: Contributions) -> SINumber { + -self.get_or_compute_derivative(PartialDerivative::Third(DT), contributions) } /// molar entropy: $s=\frac{S}{N}$ @@ -182,12 +118,9 @@ impl State { /// Enthalpy: $H=A+TS+pV$ pub fn enthalpy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature * s.entropy_(evaluate) - + s.helmholtz_energy_(evaluate) - + s.pressure_(evaluate) * s.volume - }; - self.evaluate_property(func, contributions, true) + self.temperature * self.entropy(contributions) + + self.helmholtz_energy(contributions) + + self.pressure(contributions) * self.volume } /// molar enthalpy: $h=\frac{H}{N}$ @@ -197,7 +130,7 @@ impl State { /// Helmholtz energy: $A$ pub fn helmholtz_energy(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::helmholtz_energy_, contributions, true) + self.get_or_compute_derivative(PartialDerivative::Zeroth, contributions) } /// molar Helmholtz energy: $a=\frac{A}{N}$ @@ -207,10 +140,7 @@ impl State { /// Internal energy: $U=A+TS$ pub fn internal_energy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature * s.entropy_(evaluate) + s.helmholtz_energy_(evaluate) - }; - self.evaluate_property(func, contributions, true) + self.temperature * self.entropy(contributions) + self.helmholtz_energy(contributions) } /// Molar internal energy: $u=\frac{U}{N}$ @@ -220,10 +150,7 @@ impl State { /// Gibbs energy: $G=A+pV$ pub fn gibbs_energy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.pressure_(evaluate) * s.volume + s.helmholtz_energy_(evaluate) - }; - self.evaluate_property(func, contributions, true) + self.pressure(contributions) * self.volume + self.helmholtz_energy(contributions) } /// Molar Gibbs energy: $g=\frac{G}{N}$ @@ -232,17 +159,15 @@ impl State { } /// Partial molar entropy: $s_i=\left(\frac{\partial S}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_entropy(&self, contributions: Contributions) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| { - -(s.dmu_dt_(evaluate) + s.dp_dni_(evaluate) * (s.dp_dt_(evaluate) / s.dp_dv_(evaluate))) - }; - self.evaluate_property(func, contributions, false) + pub fn partial_molar_entropy(&self) -> SIArray1 { + let c = Contributions::Total; + -(self.dmu_dt(c) + self.dp_dni(c) * (self.dp_dt(c) / self.dp_dv(c))) } /// Partial molar enthalpy: $h_i=\left(\frac{\partial H}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_enthalpy(&self, contributions: Contributions) -> SIArray1 { - let s = self.partial_molar_entropy(contributions); - let mu = self.chemical_potential(contributions); + pub fn partial_molar_enthalpy(&self) -> SIArray1 { + let s = self.partial_molar_entropy(); + let mu = self.chemical_potential(Contributions::Total); s * self.temperature + mu } From cae0991a81a6436e4f1f839a8f8bcdbb312ba474 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Wed, 7 Jun 2023 14:08:03 +0200 Subject: [PATCH 26/47] updated theory guide --- docs/theory/eos/properties.md | 117 ++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 55 deletions(-) diff --git a/docs/theory/eos/properties.md b/docs/theory/eos/properties.md index 5826c5db6..ae1486042 100644 --- a/docs/theory/eos/properties.md +++ b/docs/theory/eos/properties.md @@ -42,7 +42,7 @@ $$X^\mathrm{res,p}=\mathcal{D}\left(A\right)-\mathcal{D}\left(A^\mathrm{ig,p}\ri For linear operators $\mathcal{D}$ eqs. {eq}`eqn:a_ig` and {eq}`eqn:a_res` can be used to simplify the expression -$$X^\mathrm{res,p}=\mathcal{D}\left(A-A^\mathrm{ig,p}\right)=\mathcal{D}\left(A\right)-\mathcal{D}\left(nRT\ln Z\right)$$ +$$X^\mathrm{res,p}=\mathcal{D}\left(A-A^\mathrm{ig,p}\right)=\mathcal{D}\left(A^\mathrm{ig,V}\right)-\mathcal{D}\left(nRT\ln Z\right)$$ with the compressiblity factor $Z=\frac{pV}{nRT}$. @@ -52,62 +52,69 @@ For details on how the evaluation of properties from Helmholtz energy models is The table below lists all properties that are available in $\text{FeO}_\text{s}$, their definition, and whether they can be evaluated as residual contributions as well. -| Name | definition | residual? | -|-|:-:|-| -| Pressure $p$ | $-\left(\frac{\partial A}{\partial V}\right)_{T,n_i}$ | yes | -| Compressibility factor $Z$ | $\frac{pV}{nRT}$ | yes | -| Partial derivative of pressure w.r.t. volume | $\left(\frac{\partial p}{\partial V}\right)_{T,n_i}$ | yes | -| Partial derivative of pressure w.r.t. density | $\left(\frac{\partial p}{\partial \rho}\right)_{T,n_i}$ | yes | -| Partial derivative of pressure w.r.t. temperature | $\left(\frac{\partial p}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of pressure w.r.t. moles | $\left(\frac{\partial p}{\partial n_i}\right)_{T,V,n_j}$ | yes | -| Second partial derivative of pressure w.r.t. volume | $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,n_i}$ | yes | -| Second partial derivative of pressure w.r.t. density | $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,n_i}$ | yes | -| Partial molar volume $v_i$ | $\left(\frac{\partial V}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Chemical potential $\mu_i$ | $\left(\frac{\partial A}{\partial n_i}\right)_{T,V,n_j}$ | yes | -| Partial derivative of chemical potential w.r.t. temperature | $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of chemical potential w.r.t. moles | $\left(\frac{\partial\mu_i}{\partial n_j}\right)_{V,n_k}$ | yes | -| Logarithmic fugacity coefficient $\ln\varphi_i$ | $\beta\mu_i^\mathrm{res}\left(T,p,\lbrace n_i\rbrace\right)$ | no | -| Pure component logarithmic fugacity coefficient $\ln\varphi_i^\mathrm{pure}$ | $\lim_{x_i\to 1}\ln\varphi_i$ | no | -| Logarithmic (symmetric) activity coefficient $\ln\gamma_i$ | $\ln\left(\frac{\varphi_i}{\varphi_i^\mathrm{pure}}\right)$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. temperature | $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,n_i}$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. pressure | $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,n_i}=\frac{v_i^\mathrm{res,p}}{RT}$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. moles | $\left(\frac{\partial\ln\varphi_i}{\partial n_j}\right)_{T,p,n_k}$ | no | -| Thermodynamic factor $\Gamma_{ij}$ | $\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ | no | -| Molar isochoric heat capacity $c_v$ | $\left(\frac{\partial u}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of the molar isochoric heat capacity w.r.t. temperature | $\left(\frac{\partial c_V}{\partial T}\right)_{V,n_i}$ | yes | -| Molar isobaric heat capacity $c_p$ | $\left(\frac{\partial h}{\partial T}\right)_{p,n_i}$ | yes | -| Entropy $S$ | $-\left(\frac{\partial A}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial S}{\partial T}\right)_{V,n_i}$ | yes | -| Molar entropy $s$ | $\frac{S}{n}$ | yes | -| Enthalpy $H$ | $A+TS+pV$ | yes | -| Molar enthalpy $h$ | $\frac{H}{n}$ | yes | -| Helmholtz energy $A$ | | yes | -| Molar Helmholtz energy $a$ | $\frac{A}{n}$ | yes | -| Internal energy $U$ | $A+TS$ | yes | -| Molar internal energy $u$ | $\frac{U}{n}$ | yes | -| Gibbs energy $G$ | $A+pV$ | yes | -| Molar Gibbs energy $g$ | $\frac{G}{n}$ | yes | -| Partial molar entropy $s_i$ | $\left(\frac{\partial S}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Partial molar enthalpy $h_i$ | $\left(\frac{\partial H}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Joule Thomson coefficient $\mu_\mathrm{JT}$ | $\left(\frac{\partial T}{\partial p}\right)_{H,n_i}$ | no | -| Isentropic compressibility $\kappa_s$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{S,n_i}$ | no | -| Isothermal compressibility $\kappa_T$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,n_i}$ | no | -| (Static) structure factor $S(0)$ | $RT\left(\frac{\partial\rho}{\partial p}\right)_{T,n_i}$ | no | +In general, the evaluation of (total) Helmholtz energies and their derivatives requires a model for the residual Helmholtz energy and a model for the ideal gas contribution, specifically for the temperature dependence of the thermal de Broglie wavelength $\Lambda_i$. However, for many properties like the pressure including its derivatives and fugacity coefficients, the de Broglie wavelength cancels out. + +Due to different language paradigms, $\text{FeO}_\text{s}$ handles the ideal gas term slightly different in Rust and Python. +- In **Rust**, if no ideal gas model is provided, users can only evaluate properties for which no ideal gas model is required because the de Broglie wavelength cancels. For those properties that require an ideal gas model but the table below indicates that they can be evaluated as residual, extra functions are provided. +- In **Python**, no additional functions are required, instead the property evaluation will throw an exception if an ideal gas contribution is required but not provided. + +| Name | definition | ideal gas model required? | residual? | +|-|:-:|-|-| +| Pressure $p$ | $-\left(\frac{\partial A}{\partial V}\right)_{T,n_i}$ | no | yes | +| Compressibility factor $Z$ | $\frac{pV}{nRT}$ | no | yes | +| Partial derivative of pressure w.r.t. volume | $\left(\frac{\partial p}{\partial V}\right)_{T,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. density | $\left(\frac{\partial p}{\partial \rho}\right)_{T,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. temperature | $\left(\frac{\partial p}{\partial T}\right)_{V,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. moles | $\left(\frac{\partial p}{\partial n_i}\right)_{T,V,n_j}$ | no | yes | +| Second partial derivative of pressure w.r.t. volume | $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,n_i}$ | no | yes | +| Second partial derivative of pressure w.r.t. density | $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,n_i}$ | no | yes | +| Partial molar volume $v_i$ | $\left(\frac{\partial V}{\partial n_i}\right)_{T,p,n_j}$ | no | no | +| Chemical potential $\mu_i$ | $\left(\frac{\partial A}{\partial n_i}\right)_{T,V,n_j}$ | yes | yes | +| Partial derivative of chemical potential w.r.t. temperature | $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of chemical potential w.r.t. moles | $\left(\frac{\partial\mu_i}{\partial n_j}\right)_{V,n_k}$ | no | yes | +| Logarithmic fugacity coefficient $\ln\varphi_i$ | $\beta\mu_i^\mathrm{res}\left(T,p,\lbrace n_i\rbrace\right)$ | no | no | +| Pure component logarithmic fugacity coefficient $\ln\varphi_i^\mathrm{pure}$ | $\lim_{x_i\to 1}\ln\varphi_i$ | no | no | +| Logarithmic (symmetric) activity coefficient $\ln\gamma_i$ | $\ln\left(\frac{\varphi_i}{\varphi_i^\mathrm{pure}}\right)$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. temperature | $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,n_i}$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. pressure | $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,n_i}=\frac{v_i^\mathrm{res,p}}{RT}$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. moles | $\left(\frac{\partial\ln\varphi_i}{\partial n_j}\right)_{T,p,n_k}$ | no | no | +| Thermodynamic factor $\Gamma_{ij}$ | $\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ | no | no | +| Molar isochoric heat capacity $c_v$ | $\left(\frac{\partial u}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of the molar isochoric heat capacity w.r.t. temperature | $\left(\frac{\partial c_V}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Molar isobaric heat capacity $c_p$ | $\left(\frac{\partial h}{\partial T}\right)_{p,n_i}$ | yes | yes | +| Entropy $S$ | $-\left(\frac{\partial A}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial S}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Second partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial^2 S}{\partial T^2}\right)_{V,n_i}$ | yes | yes | +| Molar entropy $s$ | $\frac{S}{n}$ | yes | yes +| Enthalpy $H$ | $A+TS+pV$ | yes | yes | +| Molar enthalpy $h$ | $\frac{H}{n}$ | yes | yes | +| Helmholtz energy $A$ | | yes | yes | +| Molar Helmholtz energy $a$ | $\frac{A}{n}$ | yes | yes | +| Internal energy $U$ | $A+TS$ | yes | yes | +| Molar internal energy $u$ | $\frac{U}{n}$ | yes | yes | +| Gibbs energy $G$ | $A+pV$ | yes | yes | +| Molar Gibbs energy $g$ | $\frac{G}{n}$ | yes | yes | +| Partial molar entropy $s_i$ | $\left(\frac{\partial S}{\partial n_i}\right)_{T,p,n_j}$ | yes | no | +| Partial molar enthalpy $h_i$ | $\left(\frac{\partial H}{\partial n_i}\right)_{T,p,n_j}$ | yes | no | +| Joule Thomson coefficient $\mu_\mathrm{JT}$ | $\left(\frac{\partial T}{\partial p}\right)_{H,n_i}$ | yes | no | +| Isentropic compressibility $\kappa_s$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{S,n_i}$ | yes | no | +| Isothermal compressibility $\kappa_T$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,n_i}$ | no | no | +| (Static) structure factor $S(0)$ | $RT\left(\frac{\partial\rho}{\partial p}\right)_{T,n_i}$ | no | no | ## Additional properties for fluids with known molar weights If the Helmholtz energy model includes information about the molar weigt $MW_i$ of each species, additional properties are available in $\text{FeO}_\text{s}$ -| Name | definition | residual? | -|-|:-:|-| -| Total molar weight $MW$ | $\sum_ix_iMW_i$ | no | -| Mass of each component $m_i$ | $n_iMW_i$ | no | -| Total mass $m$ | $\sum_im_i=nMW$ | no | -| Mass density $\rho^{(m)}$ | $\frac{m}{V}$ | no | -| Mass fractions $w_i$ | $\frac{m_i}{m}$ | no | -| Specific entropy $s^{(m)}$ | $\frac{S}{m}$ | yes | -| Specific enthalpy $h^{(m)}$ | $\frac{H}{m}$ | yes | -| Specific Helmholtz energy $a^{(m)}$ | $\frac{A}{m}$ | yes | -| Specific internal energy $u^{(m)}$ | $\frac{U}{m}$ | yes | -| Specific Gibbs energy $g^{(m)}$ | $\frac{G}{m}$ | yes | -| Speed of sound $c$ | $\sqrt{\left(\frac{\partial p}{\partial\rho^{(m)}}\right)_{S,n_i}}$ | no | \ No newline at end of file +| Name | definition | ideal gas model required? | residual? | +|-|:-:|-|-| +| Total molar weight $MW$ | $\sum_ix_iMW_i$ | no | no | +| Mass of each component $m_i$ | $n_iMW_i$ | no | no | +| Total mass $m$ | $\sum_im_i=nMW$ | no | no | +| Mass density $\rho^{(m)}$ | $\frac{m}{V}$ | no | no | +| Mass fractions $w_i$ | $\frac{m_i}{m}$ | no | no | +| Specific entropy $s^{(m)}$ | $\frac{S}{m}$ | yes | yes | +| Specific enthalpy $h^{(m)}$ | $\frac{H}{m}$ | yes | yes | +| Specific Helmholtz energy $a^{(m)}$ | $\frac{A}{m}$ | yes | yes | +| Specific internal energy $u^{(m)}$ | $\frac{U}{m}$ | yes | yes | +| Specific Gibbs energy $g^{(m)}$ | $\frac{G}{m}$ | yes | yes | +| Speed of sound $c$ | $\sqrt{\left(\frac{\partial p}{\partial\rho^{(m)}}\right)_{S,n_i}}$ | yes | no | \ No newline at end of file From 4cc8d31c89b1547e89f1c3acadb35e1ea868fdb7 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Wed, 7 Jun 2023 16:10:41 +0200 Subject: [PATCH 27/47] Adjustment for Python interface. DFT + Python not yet working --- feos-core/src/python/mod.rs | 2 +- feos-core/src/python/phase_equilibria.rs | 14 -------------- feos-core/src/python/state.rs | 22 ++++------------------ feos-derive/src/dft.rs | 11 ----------- feos-dft/src/adsorption/mod.rs | 4 ++-- src/gc_pcsaft/python/mod.rs | 10 +--------- src/lib.rs | 8 ++++---- src/pcsaft/python.rs | 2 -- src/pets/dft/mod.rs | 14 +------------- src/pets/python.rs | 7 +------ src/python/cubic.rs | 1 - src/python/dft.rs | 15 ++++++++++----- src/python/eos.rs | 20 ++++++++++---------- src/python/ideal_gas.rs | 1 - src/saftvrqmie/dft/mod.rs | 14 +------------- src/saftvrqmie/python.rs | 10 +--------- src/uvtheory/eos/mod.rs | 2 +- src/uvtheory/python.rs | 4 ++-- 18 files changed, 39 insertions(+), 122 deletions(-) diff --git a/feos-core/src/python/mod.rs b/feos-core/src/python/mod.rs index 283df3dd0..0df76142f 100644 --- a/feos-core/src/python/mod.rs +++ b/feos-core/src/python/mod.rs @@ -6,7 +6,7 @@ pub mod cubic; mod equation_of_state; pub mod joback; pub mod parameter; -// mod phase_equilibria; +mod phase_equilibria; mod state; pub mod user_defined; diff --git a/feos-core/src/python/phase_equilibria.rs b/feos-core/src/python/phase_equilibria.rs index 72951802d..c988a429f 100644 --- a/feos-core/src/python/phase_equilibria.rs +++ b/feos-core/src/python/phase_equilibria.rs @@ -237,20 +237,6 @@ macro_rules! impl_phase_equilibrium { PyState(self.0.liquid().clone()) } - /// Calculate a new PhaseEquilibrium with the given chemical potential. - /// The temperature remains constant, but the states are not in - /// a mechanical equilibrium anymore. - /// - /// Parameters - /// ---------- - /// chemical_potential: SIArray1 - /// The new chemical potential - /// - fn update_chemical_potential(slf: &PyCell, chemical_potential: &PySIArray1) -> PyResult<()> { - slf.borrow_mut().0.update_chemical_potential(chemical_potential)?; - Ok(()) - } - /// Calculate the pure component vapor-liquid equilibria for all /// components in the system. /// diff --git a/feos-core/src/python/state.rs b/feos-core/src/python/state.rs index 2bdbab26e..b3d67af9f 100644 --- a/feos-core/src/python/state.rs +++ b/feos-core/src/python/state.rs @@ -696,18 +696,11 @@ macro_rules! impl_state { /// Return partial molar entropy of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_entropy(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_entropy(contributions)) + fn partial_molar_entropy(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_entropy()) } /// Return enthalpy. @@ -745,18 +738,11 @@ macro_rules! impl_state { /// Return partial molar enthalpy of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_enthalpy(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_enthalpy(contributions)) + fn partial_molar_enthalpy(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_enthalpy()) } /// Return helmholtz_energy. diff --git a/feos-derive/src/dft.rs b/feos-derive/src/dft.rs index 00ce3788d..8a166bb51 100644 --- a/feos-derive/src/dft.rs +++ b/feos-derive/src/dft.rs @@ -106,12 +106,6 @@ fn impl_helmholtz_energy_functional( Self::#name(functional) => functional.contributions() } }); - let ideal_gas = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(functional) => functional.ideal_gas() - } - }); let mut bond_lengths = Vec::new(); for v in variants.iter() { @@ -145,11 +139,6 @@ fn impl_helmholtz_energy_functional( #(#contributions,)* } } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match self { - #(#ideal_gas,)* - } - } fn bond_lengths(&self, temperature: f64) -> UnGraph<(), f64> { match self { #(#bond_lengths,)* diff --git a/feos-dft/src/adsorption/mod.rs b/feos-dft/src/adsorption/mod.rs index 07e5b9148..1132ca7b2 100644 --- a/feos-dft/src/adsorption/mod.rs +++ b/feos-dft/src/adsorption/mod.rs @@ -2,8 +2,8 @@ use super::functional::{HelmholtzEnergyFunctional, DFT}; use super::solver::DFTSolver; use feos_core::{ - Components, Contributions, DensityInitialization, EosError, EosResult, EosUnit, Residual, - SolverOptions, State, StateBuilder, + Components, Contributions, DensityInitialization, EosResult, EosUnit, Residual, SolverOptions, + State, StateBuilder, }; use ndarray::{Array1, Dimension, Ix1, Ix3, RemoveAxis}; use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; diff --git a/src/gc_pcsaft/python/mod.rs b/src/gc_pcsaft/python/mod.rs index eed44690d..86d3b3074 100644 --- a/src/gc_pcsaft/python/mod.rs +++ b/src/gc_pcsaft/python/mod.rs @@ -3,11 +3,9 @@ use super::dft::GcPcSaftFunctionalParameters; use super::eos::GcPcSaftEosParameters; use super::record::GcPcSaftRecord; use crate::association::PyAssociationRecord; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, IdentifierOption, ParameterError, ParameterHetero, SegmentRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::{PyBinarySegmentRecord, PyChemicalRecord, PyIdentifier}; use feos_core::{impl_json_handling, impl_parameter_from_segments, impl_segment_record}; #[cfg(feature = "dft")] @@ -77,12 +75,7 @@ impl PyGcPcSaftRecord { impl_json_handling!(PyGcPcSaftRecord); -impl_segment_record!( - GcPcSaftRecord, - PyGcPcSaftRecord, - JobackRecord, - PyJobackRecord -); +impl_segment_record!(GcPcSaftRecord, PyGcPcSaftRecord); #[pyclass(name = "GcPcSaftEosParameters")] #[pyo3( @@ -156,7 +149,6 @@ pub fn gc_pcsaft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/lib.rs b/src/lib.rs index 1d5cd4258..2a4165808 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,10 +34,10 @@ #![warn(clippy::all)] #![allow(clippy::too_many_arguments)] #![allow(deprecated)] -// #[cfg(feature = "dft")] -// mod dft; -// #[cfg(feature = "dft")] -// pub use dft::FunctionalVariant; +#[cfg(feature = "dft")] +mod dft; +#[cfg(feature = "dft")] +pub use dft::FunctionalVariant; mod eos; pub use eos::{IdealGasModel, ResidualModel}; diff --git a/src/pcsaft/python.rs b/src/pcsaft/python.rs index 13a3d44c6..b714962f8 100644 --- a/src/pcsaft/python.rs +++ b/src/pcsaft/python.rs @@ -1,11 +1,9 @@ use super::parameters::{PcSaftBinaryRecord, PcSaftParameters, PcSaftRecord}; use super::DQVariants; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, SegmentRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use feos_core::*; use ndarray::Array2; diff --git a/src/pets/dft/mod.rs b/src/pets/dft/mod.rs index f14fd3aa5..1d05675e3 100644 --- a/src/pets/dft/mod.rs +++ b/src/pets/dft/mod.rs @@ -2,9 +2,8 @@ use super::eos::PetsOptions; use super::parameters::PetsParameters; use crate::hard_sphere::{FMTContribution, FMTVersion}; use dispersion::AttractiveFunctional; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::MolarWeight; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -25,7 +24,6 @@ pub struct PetsFunctional { fmt_version: FMTVersion, options: PetsOptions, contributions: Vec>, - joback: Joback, } impl PetsFunctional { @@ -74,17 +72,11 @@ impl PetsFunctional { contributions.push(Box::new(att)); } - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.sigma.len()), - }; - Self { parameters, fmt_version, options: pets_options, contributions, - joback, } .into() } @@ -111,10 +103,6 @@ impl HelmholtzEnergyFunctional for PetsFunctional { fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } } impl MolarWeight for PetsFunctional { diff --git a/src/pets/python.rs b/src/pets/python.rs index f0b365fa1..39a78a977 100644 --- a/src/pets/python.rs +++ b/src/pets/python.rs @@ -1,7 +1,5 @@ use super::parameters::*; -use feos_core::joback::JobackRecord; use feos_core::parameter::*; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use feos_core::{impl_binary_record, impl_json_handling, impl_parameter, impl_pure_record}; use ndarray::Array2; @@ -69,7 +67,7 @@ impl PyPetsRecord { } impl_json_handling!(PyPetsRecord); -impl_pure_record!(PetsRecord, PyPetsRecord, JobackRecord, PyJobackRecord); +impl_pure_record!(PetsRecord, PyPetsRecord); #[pyclass(name = "PetsBinaryRecord")] #[pyo3( @@ -180,7 +178,6 @@ impl PyPetsParameters { identifier, molarweight.as_ref().map_or(1.0, |v| v[i]), model_record, - None, ) // Hier Ideal Gas anstatt None??? }) @@ -239,7 +236,6 @@ impl PyPetsParameters { ), molarweight.map_or(1.0, |v| v), PetsRecord::new(sigma, epsilon_k, viscosity, diffusion, thermal_conductivity), - None, ); Self(Arc::new(PetsParameters::new_pure(pure_record))) } @@ -265,7 +261,6 @@ pub fn pets(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/cubic.rs b/src/python/cubic.rs index 9edc46568..e73ed374e 100644 --- a/src/python/cubic.rs +++ b/src/python/cubic.rs @@ -1,5 +1,4 @@ use feos_core::python::cubic::*; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use pyo3::prelude::*; diff --git a/src/python/dft.rs b/src/python/dft.rs index 8be59bcbc..375a65bb3 100644 --- a/src/python/dft.rs +++ b/src/python/dft.rs @@ -21,6 +21,7 @@ use crate::saftvrqmie::python::PySaftVRQMieParameters; #[cfg(feature = "saftvrqmie")] use crate::saftvrqmie::{FeynmanHibbsOrder, SaftVRQMieFunctional, SaftVRQMieOptions}; +use crate::eos::IdealGasModel; use feos_core::*; use feos_dft::adsorption::*; use feos_dft::interface::*; @@ -33,14 +34,14 @@ use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; #[cfg(feature = "estimator")] use pyo3::wrap_pymodule; -use quantity::python::{PySINumber, PySIArray1, PySIArray2, PySIArray3, PySIArray4}; +use quantity::python::{PySIArray1, PySIArray2, PySIArray3, PySIArray4, PySINumber}; use quantity::si::*; use std::collections::HashMap; use std::sync::Arc; #[pyclass(name = "HelmholtzEnergyFunctional")] #[derive(Clone)] -pub struct PyFunctionalVariant(pub Arc>); +pub struct PyFunctionalVariant(pub Arc>>); #[pymethods] impl PyFunctionalVariant { @@ -84,9 +85,13 @@ impl PyFunctionalVariant { tol_cross_assoc, dq_variant, }; - Self(Arc::new( - PcSaftFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let functional = Arc::new(PcSaftFunctional::with_options( + parameters.0, + fmt_version, + options, + )); + let ideal_gas = Arc::new(IdealGasModel::NoModel(functional.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, functional).into())) } /// (heterosegmented) group contribution PC-SAFT Helmholtz energy functional. diff --git a/src/python/eos.rs b/src/python/eos.rs index 8ee6c234a..39f57e94d 100644 --- a/src/python/eos.rs +++ b/src/python/eos.rs @@ -39,9 +39,9 @@ use pyo3::prelude::*; #[cfg(feature = "estimator")] use pyo3::wrap_pymodule; use quantity::python::{PySINumber, PySIArray1, PySIArray2}; +use std::sync::Arc; use quantity::si::*; use std::collections::HashMap; -use std::sync::Arc; /// Collection of equations of state. #[pyclass(name = "EquationOfState")] @@ -93,7 +93,7 @@ impl PyEquationOfState { parameters.0, options, ))); - let ideal_gas = Arc::new(IdealGasModel::NoModel); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } @@ -136,7 +136,7 @@ impl PyEquationOfState { parameters.0, options, ))); - let ideal_gas = Arc::new(IdealGasModel::NoModel); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } @@ -200,7 +200,7 @@ impl PyEquationOfState { parameters.0, options, ))); - let ideal_gas = Arc::new(IdealGasModel::NoModel); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } @@ -243,9 +243,9 @@ impl PyEquationOfState { let residual = Arc::new(ResidualModel::UVTheory(UVTheory::with_options( parameters.0, options, - ))); - let ideal_gas = Arc::new(IdealGasModel::NoModel); - Self(Arc::new(EquationOfState::new(ideal_gas, residual))) + )?)); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Ok(Self(Arc::new(EquationOfState::new(ideal_gas, residual)))) } /// SAFT-VRQ Mie equation of state. @@ -288,7 +288,7 @@ impl PyEquationOfState { parameters.0, options, ))); - let ideal_gas = Arc::new(IdealGasModel::NoModel); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } @@ -320,7 +320,7 @@ impl PyEquationOfState { /// ------- /// EquationOfState fn joback(&self, parameters: Vec) -> Self { - let records = Arc::new(parameters.iter().map(|p| p.0.clone()).collect()); + let records = parameters.iter().map(|p| p.0.clone()).collect(); let ideal_gas = Arc::new(IdealGasModel::Joback(Joback::new(records))); Self(Arc::new(EquationOfState::new(ideal_gas, self.0.residual.clone()))) } @@ -331,7 +331,7 @@ impl_virial_coefficients!(PyEquationOfState); impl_state!(EquationOfState, PyEquationOfState); impl_state_molarweight!(EquationOfState, PyEquationOfState); impl_state_entropy_scaling!(EquationOfState, PyEquationOfState); -// impl_phase_equilibrium!(IdealGasModel, ResidualModel, PyEquationOfState); +impl_phase_equilibrium!(EquationOfState, PyEquationOfState); #[cfg(feature = "estimator")] impl_estimator!(EquationOfState, PyEquationOfState); diff --git a/src/python/ideal_gas.rs b/src/python/ideal_gas.rs index d071e04e9..c13a79e60 100644 --- a/src/python/ideal_gas.rs +++ b/src/python/ideal_gas.rs @@ -1,5 +1,4 @@ use feos_core::python::joback::PyJobackRecord; -use feos_core::python::parameter::*; use pyo3::prelude::*; #[pymodule] diff --git a/src/saftvrqmie/dft/mod.rs b/src/saftvrqmie/dft/mod.rs index d728fe9db..df2648fc2 100644 --- a/src/saftvrqmie/dft/mod.rs +++ b/src/saftvrqmie/dft/mod.rs @@ -2,9 +2,8 @@ use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, Mono use crate::saftvrqmie::eos::SaftVRQMieOptions; use crate::saftvrqmie::parameters::SaftVRQMieParameters; use dispersion::AttractiveFunctional; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::MolarWeight; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -24,7 +23,6 @@ pub struct SaftVRQMieFunctional { fmt_version: FMTVersion, options: SaftVRQMieOptions, contributions: Vec>, - joback: Joback, } impl SaftVRQMieFunctional { @@ -61,17 +59,11 @@ impl SaftVRQMieFunctional { let att = AttractiveFunctional::new(parameters.clone()); contributions.push(Box::new(att)); - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.m.len()), - }; - (Self { parameters, fmt_version, options: saft_options, contributions, - joback, }) .into() } @@ -96,10 +88,6 @@ impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { &self.contributions } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } - fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::NonSpherical(&self.parameters.m) } diff --git a/src/saftvrqmie/python.rs b/src/saftvrqmie/python.rs index 699269d29..8063e4992 100644 --- a/src/saftvrqmie/python.rs +++ b/src/saftvrqmie/python.rs @@ -3,11 +3,9 @@ use crate::saftvrqmie::eos::FeynmanHibbsOrder; use crate::saftvrqmie::parameters::{ SaftVRQMieBinaryRecord, SaftVRQMieParameters, SaftVRQMieRecord, }; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::PyIdentifier; use feos_core::*; use ndarray::Array2; @@ -147,12 +145,7 @@ impl PySaftVRQMieBinaryRecord { pub struct PySaftVRQMieParameters(pub Arc); impl_json_handling!(PySaftVRQMieRecord); -impl_pure_record!( - SaftVRQMieRecord, - PySaftVRQMieRecord, - JobackRecord, - PyJobackRecord -); +impl_pure_record!(SaftVRQMieRecord, PySaftVRQMieRecord); impl_binary_record!(SaftVRQMieBinaryRecord, PySaftVRQMieBinaryRecord); impl_parameter!(SaftVRQMieParameters, PySaftVRQMieParameters); @@ -229,7 +222,6 @@ impl PySaftVRQMieParameters { pub fn saftvrqmie(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/uvtheory/eos/mod.rs b/src/uvtheory/eos/mod.rs index c2c49955c..1be4f9674 100644 --- a/src/uvtheory/eos/mod.rs +++ b/src/uvtheory/eos/mod.rs @@ -177,7 +177,7 @@ mod test { use crate::uvtheory::parameters::*; use approx::assert_relative_eq; use feos_core::parameter::{Identifier, Parameter, PureRecord}; - use feos_core::{Contributions, State}; + use feos_core::State; use ndarray::arr1; use quantity::si::{ANGSTROM, KELVIN, MOL, NAV, RGAS}; diff --git a/src/uvtheory/python.rs b/src/uvtheory/python.rs index fa235b6a5..04a4425e0 100644 --- a/src/uvtheory/python.rs +++ b/src/uvtheory/python.rs @@ -94,7 +94,7 @@ impl PyUVParameters { None, ); let model_record = UVRecord::new(rep[i], att[i], sigma[i], epsilon_k[i]); - PureRecord::new(identifier, 1.0, model_record, None) + PureRecord::new(identifier, 1.0, model_record) }) .collect(); let binary = Array2::from_shape_fn((n, n), |(_, _)| UVBinaryRecord { k_ij: 0.0 }); @@ -130,7 +130,7 @@ impl PyUVParameters { } } -impl_pure_record!(UVRecord, PyUVRecord, NoRecord, PyNoRecord); +impl_pure_record!(UVRecord, PyUVRecord); impl_parameter!(UVParameters, PyUVParameters); #[pymodule] From 9d48a17773931db1ab1d9f805c62da16f0dee801 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Wed, 7 Jun 2023 17:28:49 +0200 Subject: [PATCH 28/47] make ideal gas in DFT actually usable --- feos-core/src/equation_of_state/mod.rs | 2 +- feos-dft/src/adsorption/pore.rs | 18 ++-- feos-dft/src/functional.rs | 31 +++---- src/hard_sphere/dft.rs | 21 +++-- src/lib.rs | 9 +- src/pcsaft/dft/mod.rs | 16 ++-- tests/pcsaft/dft.rs | 114 +++++++++++++------------ 7 files changed, 110 insertions(+), 101 deletions(-) diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 2f5d77c11..143362959 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -62,7 +62,7 @@ impl Components for EquationOfState { } } -impl IdealGas for EquationOfState { +impl IdealGas for EquationOfState { fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { self.ideal_gas.ideal_gas_model() } diff --git a/feos-dft/src/adsorption/pore.rs b/feos-dft/src/adsorption/pore.rs index 3f751b5bd..713f6efae 100644 --- a/feos-dft/src/adsorption/pore.rs +++ b/feos-dft/src/adsorption/pore.rs @@ -5,7 +5,7 @@ use crate::functional_contribution::FunctionalContribution; use crate::geometry::{Axis, Geometry, Grid}; use crate::profile::{DFTProfile, MAX_POTENTIAL}; use crate::solver::DFTSolver; -use feos_core::{Contributions, EosResult, EosUnit, State, StateBuilder}; +use feos_core::{Components, Contributions, EosResult, EosUnit, State, StateBuilder}; use ndarray::prelude::*; use ndarray::Axis as Axis_nd; use ndarray::RemoveAxis; @@ -274,7 +274,17 @@ impl Helium { fn new() -> DFT { let epsilon = arr1(&[EPSILON_HE]); let sigma = arr1(&[SIGMA_HE]); - (Self { epsilon, sigma }).into() + DFT(Self { epsilon, sigma }) + } +} + +impl Components for Helium { + fn components(&self) -> usize { + 1 + } + + fn subset(&self, _: &[usize]) -> Self { + self.clone() } } @@ -290,10 +300,6 @@ impl HelmholtzEnergyFunctional for Helium { fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::Spherical(1) } - - fn subset(&self, _: &[usize]) -> DFT { - self.clone().into() - } } impl FluidParameters for Helium { diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index 879a08ba1..bd4d59666 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -27,14 +27,6 @@ impl HelmholtzEnergyF self.residual.molecule_shape() } - fn subset(&self, component_list: &[usize]) -> DFT { - Self::new( - Arc::new(self.ideal_gas.subset(component_list)), - Arc::new(self.residual.subset(component_list).0), - ) - .into() - } - fn compute_max_density(&self, moles: &Array1) -> f64 { self.residual.compute_max_density(moles) } @@ -45,13 +37,7 @@ impl HelmholtzEnergyF /// Needed (for now) to generically implement the `Residual` /// trait for Helmholtz energy functionals. #[derive(Clone)] -pub struct DFT(F); - -impl From for DFT { - fn from(functional: F) -> Self { - Self(functional) - } -} +pub struct DFT(pub F); impl DFT { pub fn into>(self) -> DFT { @@ -66,6 +52,12 @@ impl Deref for DFT { } } +impl DFT { + pub fn ideal_gas(self, ideal_gas: I) -> DFT> { + DFT(EquationOfState::new(Arc::new(ideal_gas), Arc::new(self.0))) + } +} + impl MolarWeight for DFT { fn molar_weight(&self) -> SIArray1 { self.0.molar_weight() @@ -74,11 +66,11 @@ impl MolarWeight for DFT { impl Components for DFT { fn components(&self) -> usize { - self.component_index()[self.component_index().len() - 1] + 1 + self.0.components() } fn subset(&self, component_list: &[usize]) -> Self { - self.0.subset(component_list) + Self(self.0.subset(component_list)) } } @@ -148,16 +140,13 @@ pub enum MoleculeShape<'a> { } /// A general Helmholtz energy functional. -pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { +pub trait HelmholtzEnergyFunctional: Components + Sized + Send + Sync { /// Return a slice of [FunctionalContribution]s. fn contributions(&self) -> &[Box]; /// Return the shape of the molecules and the necessary specifications. fn molecule_shape(&self) -> MoleculeShape; - /// Return a functional for the specified subset of components. - fn subset(&self, component_list: &[usize]) -> DFT; - /// Return the maximum density in Angstrom^-3. /// /// This value is used as an estimate for a liquid phase for phase diff --git a/src/hard_sphere/dft.rs b/src/hard_sphere/dft.rs index c886e2c95..720da02a8 100644 --- a/src/hard_sphere/dft.rs +++ b/src/hard_sphere/dft.rs @@ -1,4 +1,4 @@ -use feos_core::EosResult; +use feos_core::{Components, EosResult}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{ @@ -322,26 +322,31 @@ impl FMTFunctional { }); let contributions: Vec> = vec![Box::new(FMTContribution::new(&properties, version))]; - (Self { + DFT(Self { properties, contributions, version, }) - .into() } } -impl HelmholtzEnergyFunctional for FMTFunctional { - fn contributions(&self) -> &[Box] { - &self.contributions +impl Components for FMTFunctional { + fn components(&self) -> usize { + self.properties.sigma.len() } - fn subset(&self, component_list: &[usize]) -> DFT { + fn subset(&self, component_list: &[usize]) -> Self { let sigma = component_list .iter() .map(|&c| self.properties.sigma[c]) .collect(); - Self::new(&sigma, self.version) + Self::new(&sigma, self.version).0 + } +} + +impl HelmholtzEnergyFunctional for FMTFunctional { + fn contributions(&self) -> &[Box] { + &self.contributions } fn compute_max_density(&self, moles: &Array1) -> f64 { diff --git a/src/lib.rs b/src/lib.rs index 2a4165808..25377008a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,10 +34,11 @@ #![warn(clippy::all)] #![allow(clippy::too_many_arguments)] #![allow(deprecated)] -#[cfg(feature = "dft")] -mod dft; -#[cfg(feature = "dft")] -pub use dft::FunctionalVariant; + +// #[cfg(feature = "dft")] +// mod dft; +// #[cfg(feature = "dft")] +// pub use dft::FunctionalVariant; mod eos; pub use eos::{IdealGasModel, ResidualModel}; diff --git a/src/pcsaft/dft/mod.rs b/src/pcsaft/dft/mod.rs index 7bcca9517..f87d4b0c6 100644 --- a/src/pcsaft/dft/mod.rs +++ b/src/pcsaft/dft/mod.rs @@ -3,7 +3,7 @@ use crate::association::Association; use crate::hard_sphere::{FMTContribution, FMTVersion}; use crate::pcsaft::eos::PcSaftOptions; use feos_core::parameter::Parameter; -use feos_core::MolarWeight; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -85,25 +85,31 @@ impl PcSaftFunctional { } } - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, }) - .into() } } -impl HelmholtzEnergyFunctional for PcSaftFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for PcSaftFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for PcSaftFunctional { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) diff --git a/tests/pcsaft/dft.rs b/tests/pcsaft/dft.rs index 1de474aa1..107b5c74d 100644 --- a/tests/pcsaft/dft.rs +++ b/tests/pcsaft/dft.rs @@ -3,6 +3,7 @@ use approx::assert_relative_eq; use feos::hard_sphere::FMTVersion; use feos::pcsaft::{PcSaft, PcSaftFunctional, PcSaftParameters}; +use feos_core::joback::{Joback, JobackRecord}; use feos_core::parameter::{IdentifierOption, Parameter}; use feos_core::{Contributions, PhaseEquilibrium, State, Verbosity}; use feos_dft::interface::PlanarInterface; @@ -321,59 +322,60 @@ fn test_dft_water() -> Result<(), Box> { Ok(()) } -// #[test] -// fn test_entropy_bulk_values() -> Result<(), Box> { -// let params = PcSaftParameters::from_json( -// vec!["water_np"], -// "tests/pcsaft/test_parameters.json", -// None, -// IdentifierOption::Name, -// )?; -// let func = Arc::new(PcSaftFunctional::new(Arc::new(params)).into()); -// let vle = PhaseEquilibrium::pure(&func, 350.0 * KELVIN, None, Default::default())?; -// let profile = PlanarInterface::from_pdgt(&vle, 2048, false)?.solve(None)?; -// let s_res = profile.profile.residual_entropy_density()?; -// let s_tot = profile.profile.entropy_density(Contributions::Total)?; -// println!( -// "Density:\n{}", -// profile.profile.density.index_axis(Axis(0), 0).to_owned() -// ); -// println!( -// "liquid: {}, vapor: {}", -// profile.vle.liquid().density, -// profile.vle.vapor().density -// ); -// println!("\nResidual:\n{}", s_res); -// println!( -// "liquid: {}, vapor: {}", -// profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, -// profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume -// ); -// println!("\nTotal:\n{}", s_tot); -// println!( -// "liquid: {}, vapor: {}", -// profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, -// profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume -// ); -// assert_relative_eq!( -// s_res.get(0), -// profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, -// max_relative = 1e-8, -// ); -// assert_relative_eq!( -// s_res.get(2047), -// profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume, -// max_relative = 1e-8, -// ); -// assert_relative_eq!( -// s_tot.get(0), -// profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, -// max_relative = 1e-8, -// ); -// assert_relative_eq!( -// s_tot.get(2047), -// profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume, -// max_relative = 1e-8, -// ); -// Ok(()) -// } +#[test] +fn test_entropy_bulk_values() -> Result<(), Box> { + let params = PcSaftParameters::from_json( + vec!["water_np"], + "tests/pcsaft/test_parameters.json", + None, + IdentifierOption::Name, + )?; + let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let func = Arc::new(PcSaftFunctional::new(Arc::new(params)).ideal_gas(joback)); + let vle = PhaseEquilibrium::pure(&func, 350.0 * KELVIN, None, Default::default())?; + let profile = PlanarInterface::from_pdgt(&vle, 2048, false)?.solve(None)?; + let s_res = profile.profile.entropy_density(Contributions::Residual)?; + let s_tot = profile.profile.entropy_density(Contributions::Total)?; + println!( + "Density:\n{}", + profile.profile.density.index_axis(Axis(0), 0).to_owned() + ); + println!( + "liquid: {}, vapor: {}", + profile.vle.liquid().density, + profile.vle.vapor().density + ); + println!("\nResidual:\n{}", s_res); + println!( + "liquid: {}, vapor: {}", + profile.vle.liquid().entropy(Contributions::Residual) / profile.vle.liquid().volume, + profile.vle.vapor().entropy(Contributions::Residual) / profile.vle.vapor().volume + ); + println!("\nTotal:\n{}", s_tot); + println!( + "liquid: {}, vapor: {}", + profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, + profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume + ); + assert_relative_eq!( + s_res.get(0), + profile.vle.liquid().entropy(Contributions::Residual) / profile.vle.liquid().volume, + max_relative = 1e-8, + ); + assert_relative_eq!( + s_res.get(2047), + profile.vle.vapor().entropy(Contributions::Residual) / profile.vle.vapor().volume, + max_relative = 1e-8, + ); + assert_relative_eq!( + s_tot.get(0), + profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, + max_relative = 1e-8, + ); + assert_relative_eq!( + s_tot.get(2047), + profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume, + max_relative = 1e-8, + ); + Ok(()) +} From c6e9d35c8e889aa97c30e7b1b436777c41b5cbf9 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Wed, 7 Jun 2023 17:56:03 +0200 Subject: [PATCH 29/47] Fix remaining tests and functionals --- Cargo.toml | 2 +- src/gc_pcsaft/dft/mod.rs | 20 +- src/gc_pcsaft/dft/parameter.rs | 2 +- src/pets/dft/mod.rs | 18 +- src/saftvrqmie/dft/mod.rs | 16 +- tests/pcsaft/state_creation_mixture.rs | 65 ++-- tests/pcsaft/state_creation_pure.rs | 502 +++++++++++++------------ 7 files changed, 330 insertions(+), 295 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ff44725de..dcc77664c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ inherits = "release" lto = true [features] -default = [] +default = ["pcsaft", "pets", "gc_pcsaft", "dft", "uvtheory", "saftvrqmie"] dft = ["feos-dft", "petgraph"] estimator = [] association = [] diff --git a/src/gc_pcsaft/dft/mod.rs b/src/gc_pcsaft/dft/mod.rs index 2b0f43b38..1c66a6716 100644 --- a/src/gc_pcsaft/dft/mod.rs +++ b/src/gc_pcsaft/dft/mod.rs @@ -2,7 +2,7 @@ use super::eos::GcPcSaftOptions; use crate::association::Association; use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, MonomerShape}; use feos_core::parameter::ParameterHetero; -use feos_core::MolarWeight; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; use ndarray::Array1; @@ -66,27 +66,33 @@ impl GcPcSaftFunctional { contributions.push(Box::new(assoc)); } - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, }) - .into() } } -impl HelmholtzEnergyFunctional for GcPcSaftFunctional { - fn molecule_shape(&self) -> MoleculeShape { - MoleculeShape::Heterosegmented(&self.parameters.component_index) +impl Components for GcPcSaftFunctional { + fn components(&self) -> usize { + self.parameters.chemical_records.len() } - fn subset(&self, component_list: &[usize]) -> DFT { + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 + } +} + +impl HelmholtzEnergyFunctional for GcPcSaftFunctional { + fn molecule_shape(&self) -> MoleculeShape { + MoleculeShape::Heterosegmented(&self.parameters.component_index) } fn compute_max_density(&self, moles: &Array1) -> f64 { diff --git a/src/gc_pcsaft/dft/parameter.rs b/src/gc_pcsaft/dft/parameter.rs index 04211ec20..43fc9016e 100644 --- a/src/gc_pcsaft/dft/parameter.rs +++ b/src/gc_pcsaft/dft/parameter.rs @@ -25,7 +25,7 @@ pub struct GcPcSaftFunctionalParameters { pub k_ij: Array2, pub sigma_ij: Array2, pub epsilon_k_ij: Array2, - chemical_records: Vec, + pub chemical_records: Vec, segment_records: Vec>, binary_segment_records: Option>>, } diff --git a/src/pets/dft/mod.rs b/src/pets/dft/mod.rs index 1d05675e3..abf76fa82 100644 --- a/src/pets/dft/mod.rs +++ b/src/pets/dft/mod.rs @@ -3,7 +3,7 @@ use super::parameters::PetsParameters; use crate::hard_sphere::{FMTContribution, FMTVersion}; use dispersion::AttractiveFunctional; use feos_core::parameter::Parameter; -use feos_core::MolarWeight; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -72,25 +72,31 @@ impl PetsFunctional { contributions.push(Box::new(att)); } - Self { + DFT(Self { parameters, fmt_version, options: pets_options, contributions, - } - .into() + }) } } -impl HelmholtzEnergyFunctional for PetsFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for PetsFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for PetsFunctional { fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::Spherical(self.parameters.sigma.len()) } diff --git a/src/saftvrqmie/dft/mod.rs b/src/saftvrqmie/dft/mod.rs index df2648fc2..92bfe3a28 100644 --- a/src/saftvrqmie/dft/mod.rs +++ b/src/saftvrqmie/dft/mod.rs @@ -3,7 +3,7 @@ use crate::saftvrqmie::eos::SaftVRQMieOptions; use crate::saftvrqmie::parameters::SaftVRQMieParameters; use dispersion::AttractiveFunctional; use feos_core::parameter::Parameter; -use feos_core::MolarWeight; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -59,25 +59,31 @@ impl SaftVRQMieFunctional { let att = AttractiveFunctional::new(parameters.clone()); contributions.push(Box::new(att)); - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, }) - .into() } } -impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for SaftVRQMieFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) diff --git a/tests/pcsaft/state_creation_mixture.rs b/tests/pcsaft/state_creation_mixture.rs index 849c5140f..62a653560 100644 --- a/tests/pcsaft/state_creation_mixture.rs +++ b/tests/pcsaft/state_creation_mixture.rs @@ -1,7 +1,8 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; +use feos_core::joback::{Joback, JobackRecord}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; -use feos_core::{Contributions, StateBuilder}; +use feos_core::{Contributions, EquationOfState, StateBuilder}; use ndarray::prelude::*; use ndarray::Zip; use quantity::si::*; @@ -17,36 +18,38 @@ fn propane_butane_parameters() -> Result, ParameterError> )?)) } -// #[test] -// fn pressure_entropy_molefracs() -> Result<(), Box> { -// let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); -// let pressure = BAR; -// let temperature = 300.0 * KELVIN; -// let x = arr1(&[0.3, 0.7]); -// let state = StateBuilder::new(&saft) -// .temperature(temperature) -// .pressure(pressure) -// .molefracs(&x) -// .build()?; -// let molar_entropy = state.molar_entropy(Contributions::Total); -// let state = StateBuilder::new(&saft) -// .pressure(pressure) -// .molar_entropy(molar_entropy) -// .molefracs(&x) -// .build()?; -// assert_relative_eq!( -// state.molar_entropy(Contributions::Total), -// molar_entropy, -// max_relative = 1e-8 -// ); -// assert_relative_eq!(state.temperature, temperature, max_relative = 1e-10); -// assert_relative_eq!( -// state.pressure(Contributions::Total), -// pressure, -// max_relative = 1e-8 -// ); -// Ok(()) -// } +#[test] +fn pressure_entropy_molefracs() -> Result<(), Box> { + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8); 2]); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); + let pressure = BAR; + let temperature = 300.0 * KELVIN; + let x = arr1(&[0.3, 0.7]); + let state = StateBuilder::new(&eos) + .temperature(temperature) + .pressure(pressure) + .molefracs(&x) + .build()?; + let molar_entropy = state.molar_entropy(Contributions::Total); + let state = StateBuilder::new(&eos) + .pressure(pressure) + .molar_entropy(molar_entropy) + .molefracs(&x) + .build()?; + assert_relative_eq!( + state.molar_entropy(Contributions::Total), + molar_entropy, + max_relative = 1e-8 + ); + assert_relative_eq!(state.temperature, temperature, max_relative = 1e-10); + assert_relative_eq!( + state.pressure(Contributions::Total), + pressure, + max_relative = 1e-8 + ); + Ok(()) +} #[test] fn volume_temperature_molefracs() -> Result<(), Box> { diff --git a/tests/pcsaft/state_creation_pure.rs b/tests/pcsaft/state_creation_pure.rs index 3cb66a46e..3cbbab54e 100644 --- a/tests/pcsaft/state_creation_pure.rs +++ b/tests/pcsaft/state_creation_pure.rs @@ -1,8 +1,10 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; +use feos_core::joback::{Joback, JobackRecord}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; use feos_core::{ - Contributions, DensityInitialization, IdealGas, PhaseEquilibrium, Residual, State, StateBuilder, + Contributions, DensityInitialization, EquationOfState, IdealGas, PhaseEquilibrium, Residual, + State, StateBuilder, }; use quantity::si::*; use std::error::Error; @@ -133,179 +135,189 @@ fn pressure_temperature_initial_density() -> Result<(), Box> { Ok(()) } -// #[test] -// fn pressure_enthalpy_vapor() -> Result<(), Box> { -// let saft = Arc::new(PcSaft::new(propane_parameters()?)); -// let pressure = 0.3 * BAR; -// let molar_enthalpy = 2000.0 * JOULE / MOL; -// let state = StateBuilder::new(&saft) -// .pressure(pressure) -// .molar_enthalpy(molar_enthalpy) -// .vapor() -// .build()?; -// assert_relative_eq!( -// state.molar_enthalpy(Contributions::Total), -// molar_enthalpy, -// max_relative = 1e-10 -// ); -// assert_relative_eq!( -// state.pressure(Contributions::Total), -// pressure, -// max_relative = 1e-10 -// ); +#[test] +fn pressure_enthalpy_vapor() -> Result<(), Box> { + let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); + let pressure = 0.3 * BAR; + let molar_enthalpy = 2000.0 * JOULE / MOL; + let state = StateBuilder::new(&eos) + .pressure(pressure) + .molar_enthalpy(molar_enthalpy) + .vapor() + .build()?; + assert_relative_eq!( + state.molar_enthalpy(Contributions::Total), + molar_enthalpy, + max_relative = 1e-10 + ); + assert_relative_eq!( + state.pressure(Contributions::Total), + pressure, + max_relative = 1e-10 + ); -// let state = StateBuilder::new(&saft) -// .volume(state.volume) -// .temperature(state.temperature) -// .moles(&state.moles) -// .build()?; -// assert_relative_eq!( -// state.molar_enthalpy(Contributions::Total), -// molar_enthalpy, -// max_relative = 1e-10 -// ); -// assert_relative_eq!( -// state.pressure(Contributions::Total), -// pressure, -// max_relative = 1e-10 -// ); -// Ok(()) -// } + let state = StateBuilder::new(&eos) + .volume(state.volume) + .temperature(state.temperature) + .moles(&state.moles) + .build()?; + assert_relative_eq!( + state.molar_enthalpy(Contributions::Total), + molar_enthalpy, + max_relative = 1e-10 + ); + assert_relative_eq!( + state.pressure(Contributions::Total), + pressure, + max_relative = 1e-10 + ); + Ok(()) +} -// #[test] -// fn density_internal_energy() -> Result<(), Box> { -// let saft = Arc::new(PcSaft::new(propane_parameters()?)); -// let pressure = 5.0 * BAR; -// let temperature = 315.0 * KELVIN; -// let total_moles = 2.5 * MOL; -// let state = StateBuilder::new(&saft) -// .pressure(pressure) -// .temperature(temperature) -// .total_moles(total_moles) -// .build()?; -// let molar_internal_energy = state.molar_internal_energy(Contributions::Total); -// let state_nvu = StateBuilder::new(&saft) -// .volume(state.volume) -// .molar_internal_energy(molar_internal_energy) -// .total_moles(total_moles) -// .build()?; -// assert_relative_eq!( -// molar_internal_energy, -// state_nvu.molar_internal_energy(Contributions::Total), -// max_relative = 1e-10 -// ); -// assert_relative_eq!(temperature, state_nvu.temperature, max_relative = 1e-10); -// assert_relative_eq!(state.density, state_nvu.density, max_relative = 1e-10); -// Ok(()) -// } +#[test] +fn density_internal_energy() -> Result<(), Box> { + let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); + let pressure = 5.0 * BAR; + let temperature = 315.0 * KELVIN; + let total_moles = 2.5 * MOL; + let state = StateBuilder::new(&eos) + .pressure(pressure) + .temperature(temperature) + .total_moles(total_moles) + .build()?; + let molar_internal_energy = state.molar_internal_energy(Contributions::Total); + let state_nvu = StateBuilder::new(&eos) + .volume(state.volume) + .molar_internal_energy(molar_internal_energy) + .total_moles(total_moles) + .build()?; + assert_relative_eq!( + molar_internal_energy, + state_nvu.molar_internal_energy(Contributions::Total), + max_relative = 1e-10 + ); + assert_relative_eq!(temperature, state_nvu.temperature, max_relative = 1e-10); + assert_relative_eq!(state.density, state_nvu.density, max_relative = 1e-10); + Ok(()) +} -// #[test] -// fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { -// let saft = Arc::new(PcSaft::new(propane_parameters()?)); -// let pressure = 0.3 * BAR; -// let molar_enthalpy = 2000.0 * JOULE / MOL; -// let total_moles = 2.5 * MOL; -// let state = StateBuilder::new(&saft) -// .pressure(pressure) -// .molar_enthalpy(molar_enthalpy) -// .total_moles(total_moles) -// .vapor() -// .build()?; -// assert_relative_eq!( -// state.molar_enthalpy(Contributions::Total), -// molar_enthalpy, -// max_relative = 1e-10 -// ); -// assert_relative_eq!( -// state.pressure(Contributions::Total), -// pressure, -// max_relative = 1e-10 -// ); +#[test] +fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { + let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); + let pressure = 0.3 * BAR; + let molar_enthalpy = 2000.0 * JOULE / MOL; + let total_moles = 2.5 * MOL; + let state = StateBuilder::new(&eos) + .pressure(pressure) + .molar_enthalpy(molar_enthalpy) + .total_moles(total_moles) + .vapor() + .build()?; + assert_relative_eq!( + state.molar_enthalpy(Contributions::Total), + molar_enthalpy, + max_relative = 1e-10 + ); + assert_relative_eq!( + state.pressure(Contributions::Total), + pressure, + max_relative = 1e-10 + ); -// let state = StateBuilder::new(&saft) -// .volume(state.volume) -// .temperature(state.temperature) -// .total_moles(state.total_moles) -// .build()?; -// assert_relative_eq!( -// state.molar_enthalpy(Contributions::Total), -// molar_enthalpy, -// max_relative = 1e-10 -// ); -// assert_relative_eq!( -// state.pressure(Contributions::Total), -// pressure, -// max_relative = 1e-10 -// ); -// Ok(()) -// } + let state = StateBuilder::new(&eos) + .volume(state.volume) + .temperature(state.temperature) + .total_moles(state.total_moles) + .build()?; + assert_relative_eq!( + state.molar_enthalpy(Contributions::Total), + molar_enthalpy, + max_relative = 1e-10 + ); + assert_relative_eq!( + state.pressure(Contributions::Total), + pressure, + max_relative = 1e-10 + ); + Ok(()) +} -// #[test] -// fn pressure_entropy_vapor() -> Result<(), Box> { -// let saft = Arc::new(PcSaft::new(propane_parameters()?)); -// let pressure = 0.3 * BAR; -// let molar_entropy = -2.0 * JOULE / MOL / KELVIN; -// let state = StateBuilder::new(&saft) -// .pressure(pressure) -// .molar_entropy(molar_entropy) -// .vapor() -// .build()?; -// assert_relative_eq!( -// state.molar_entropy(Contributions::Total), -// molar_entropy, -// max_relative = 1e-10 -// ); -// assert_relative_eq!( -// state.pressure(Contributions::Total), -// pressure, -// max_relative = 1e-10 -// ); +#[test] +fn pressure_entropy_vapor() -> Result<(), Box> { + let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); + let pressure = 0.3 * BAR; + let molar_entropy = -2.0 * JOULE / MOL / KELVIN; + let state = StateBuilder::new(&eos) + .pressure(pressure) + .molar_entropy(molar_entropy) + .vapor() + .build()?; + assert_relative_eq!( + state.molar_entropy(Contributions::Total), + molar_entropy, + max_relative = 1e-10 + ); + assert_relative_eq!( + state.pressure(Contributions::Total), + pressure, + max_relative = 1e-10 + ); -// let state = StateBuilder::new(&saft) -// .volume(state.volume) -// .temperature(state.temperature) -// .moles(&state.moles) -// .build()?; -// assert_relative_eq!( -// state.molar_entropy(Contributions::Total), -// molar_entropy, -// max_relative = 1e-10 -// ); -// assert_relative_eq!( -// state.pressure(Contributions::Total), -// pressure, -// max_relative = 1e-10 -// ); -// Ok(()) -// } + let state = StateBuilder::new(&eos) + .volume(state.volume) + .temperature(state.temperature) + .moles(&state.moles) + .build()?; + assert_relative_eq!( + state.molar_entropy(Contributions::Total), + molar_entropy, + max_relative = 1e-10 + ); + assert_relative_eq!( + state.pressure(Contributions::Total), + pressure, + max_relative = 1e-10 + ); + Ok(()) +} -// #[test] -// fn temperature_entropy_vapor() -> Result<(), Box> { -// let saft = Arc::new(PcSaft::new(propane_parameters()?)); -// let pressure = 3.0 * BAR; -// let temperature = 315.15 * KELVIN; -// let total_moles = 3.0 * MOL; -// let state = StateBuilder::new(&saft) -// .temperature(temperature) -// .pressure(pressure) -// .total_moles(total_moles) -// .build()?; +#[test] +fn temperature_entropy_vapor() -> Result<(), Box> { + let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); + let pressure = 3.0 * BAR; + let temperature = 315.15 * KELVIN; + let total_moles = 3.0 * MOL; + let state = StateBuilder::new(&eos) + .temperature(temperature) + .pressure(pressure) + .total_moles(total_moles) + .build()?; -// let s = State::new_nts( -// &saft, -// temperature, -// state.molar_entropy(Contributions::Total), -// &state.moles, -// DensityInitialization::None, -// )?; -// assert_relative_eq!( -// state.molar_entropy(Contributions::Total), -// s.molar_entropy(Contributions::Total), -// max_relative = 1e-10 -// ); -// assert_relative_eq!(state.density, s.density, max_relative = 1e-10); -// Ok(()) -// } + let s = State::new_nts( + &eos, + temperature, + state.molar_entropy(Contributions::Total), + &state.moles, + DensityInitialization::None, + )?; + assert_relative_eq!( + state.molar_entropy(Contributions::Total), + s.molar_entropy(Contributions::Total), + max_relative = 1e-10 + ); + assert_relative_eq!(state.density, s.density, max_relative = 1e-10); + Ok(()) +} fn assert_multiple_states( states: &[(&State, &str)], @@ -336,89 +348,91 @@ fn assert_multiple_states( } } -// #[test] -// fn test_consistency() -> Result<(), Box> { -// let p = propane_parameters()?; -// let saft = Arc::new(PcSaft::new(p)); -// let temperatures = [350.0 * KELVIN, 400.0 * KELVIN, 450.0 * KELVIN]; -// let pressures = [1.0 * BAR, 2.0 * BAR, 3.0 * BAR]; +#[test] +fn test_consistency() -> Result<(), Box> { + let p = propane_parameters()?; + let saft = Arc::new(PcSaft::new(p)); + let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); + let temperatures = [350.0 * KELVIN, 400.0 * KELVIN, 450.0 * KELVIN]; + let pressures = [1.0 * BAR, 2.0 * BAR, 3.0 * BAR]; -// for (&temperature, &pressure) in temperatures.iter().zip(pressures.iter()) { -// let state = StateBuilder::new(&saft) -// .pressure(pressure) -// .temperature(temperature) -// .build()?; -// assert_relative_eq!( -// state.pressure(Contributions::Total), -// pressure, -// max_relative = 1e-10 -// ); -// println!( -// "temperature: {}\npressure: {}\ndensity: {}", -// temperature, pressure, state.density -// ); -// let molar_enthalpy = state.molar_enthalpy(Contributions::Total); -// let molar_entropy = state.molar_entropy(Contributions::Total); -// let density = state.density; + for (&temperature, &pressure) in temperatures.iter().zip(pressures.iter()) { + let state = StateBuilder::new(&eos) + .pressure(pressure) + .temperature(temperature) + .build()?; + assert_relative_eq!( + state.pressure(Contributions::Total), + pressure, + max_relative = 1e-10 + ); + println!( + "temperature: {}\npressure: {}\ndensity: {}", + temperature, pressure, state.density + ); + let molar_enthalpy = state.molar_enthalpy(Contributions::Total); + let molar_entropy = state.molar_entropy(Contributions::Total); + let density = state.density; -// let state_tv = StateBuilder::new(&saft) -// .temperature(temperature) -// .density(density) -// .build()?; + let state_tv = StateBuilder::new(&eos) + .temperature(temperature) + .density(density) + .build()?; -// let vle = PhaseEquilibrium::pure(&saft, temperature, None, Default::default()); -// let builder = if let Ok(ps) = vle { -// let p_sat = ps.liquid().pressure(Contributions::Total); -// if pressure > p_sat { -// StateBuilder::new(&saft).liquid() -// } else { -// StateBuilder::new(&saft).vapor() -// } -// } else { -// StateBuilder::new(&saft).vapor() -// }; + let vle = PhaseEquilibrium::pure(&eos, temperature, None, Default::default()); + let builder = if let Ok(ps) = vle { + let p_sat = ps.liquid().pressure(Contributions::Total); + if pressure > p_sat { + StateBuilder::new(&eos).liquid() + } else { + StateBuilder::new(&eos).vapor() + } + } else { + StateBuilder::new(&eos).vapor() + }; -// let state_ts = builder -// .clone() -// .temperature(temperature) -// .molar_entropy(molar_entropy) -// .build()?; + let state_ts = builder + .clone() + .temperature(temperature) + .molar_entropy(molar_entropy) + .build()?; -// let state_ps = builder -// .clone() -// .pressure(pressure) -// .molar_entropy(molar_entropy) -// .build()?; + let state_ps = builder + .clone() + .pressure(pressure) + .molar_entropy(molar_entropy) + .build()?; -// dbg!("ph"); -// let state_ph = builder -// .clone() -// .pressure(pressure) -// .molar_enthalpy(molar_enthalpy) -// .build()?; + dbg!("ph"); + let state_ph = builder + .clone() + .pressure(pressure) + .molar_enthalpy(molar_enthalpy) + .build()?; -// dbg!("th"); -// let state_th = builder -// .clone() -// .temperature(temperature) -// .molar_enthalpy(molar_enthalpy) -// .build()?; + dbg!("th"); + let state_th = builder + .clone() + .temperature(temperature) + .molar_enthalpy(molar_enthalpy) + .build()?; -// dbg!("assertions"); -// assert_multiple_states( -// &[ -// (&state_ph, "p, h"), -// (&state_tv, "T, V"), -// (&state_ts, "T, s"), -// (&state_th, "T, h"), -// (&state_ps, "p, s"), -// ], -// pressure, -// molar_enthalpy, -// molar_entropy, -// density, -// 1e-7, -// ); -// } -// Ok(()) -// } + dbg!("assertions"); + assert_multiple_states( + &[ + (&state_ph, "p, h"), + (&state_tv, "T, V"), + (&state_ts, "T, s"), + (&state_th, "T, h"), + (&state_ps, "p, s"), + ], + pressure, + molar_enthalpy, + molar_entropy, + density, + 1e-7, + ); + } + Ok(()) +} From 21e471e6c6161b2e67867bf6ea651b9c58449390 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Wed, 7 Jun 2023 18:07:04 +0200 Subject: [PATCH 30/47] fix estimator --- Cargo.toml | 2 +- src/estimator/binary_vle.rs | 23 ++++++++++++++--------- src/estimator/dataset.rs | 6 +++--- src/estimator/diffusion.rs | 4 ++-- src/estimator/estimator.rs | 8 ++++---- src/estimator/liquid_density.rs | 7 +++---- src/estimator/thermal_conductivity.rs | 4 ++-- src/estimator/vapor_pressure.rs | 4 ++-- src/estimator/viscosity.rs | 4 ++-- 9 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dcc77664c..87c8ebe32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ inherits = "release" lto = true [features] -default = ["pcsaft", "pets", "gc_pcsaft", "dft", "uvtheory", "saftvrqmie"] +default = ["all_models"] dft = ["feos-dft", "petgraph"] estimator = [] association = [] diff --git a/src/estimator/binary_vle.rs b/src/estimator/binary_vle.rs index 200925d68..63c668677 100644 --- a/src/estimator/binary_vle.rs +++ b/src/estimator/binary_vle.rs @@ -1,7 +1,6 @@ use super::{DataSet, EstimatorError}; use feos_core::{ - Contributions, DensityInitialization, EosUnit, EquationOfState, PhaseDiagram, PhaseEquilibrium, - State, + Contributions, DensityInitialization, EosUnit, PhaseDiagram, PhaseEquilibrium, Residual, State, }; use ndarray::{arr1, s, Array1, ArrayView1, Axis}; use quantity::si::{SIArray1, SINumber, SIUnit}; @@ -44,7 +43,7 @@ impl BinaryVleChemicalPotential { } } -impl DataSet for BinaryVleChemicalPotential { +impl DataSet for BinaryVleChemicalPotential { fn target(&self) -> &SIArray1 { &self.target } @@ -73,16 +72,22 @@ impl DataSet for BinaryVleChemicalPotential { { let liquid_moles = arr1(&[xi, 1.0 - xi]) * SIUnit::reference_moles(); let liquid = State::new_npt(eos, t, p, &liquid_moles, DensityInitialization::Liquid)?; - let mu_liquid = liquid.chemical_potential(Contributions::Total); + let mu_res_liquid = liquid.residual_chemical_potential(); let vapor_moles = arr1(&[yi, 1.0 - yi]) * SIUnit::reference_moles(); let vapor = State::new_npt(eos, t, p, &vapor_moles, DensityInitialization::Vapor)?; - let mu_vapor = vapor.chemical_potential(Contributions::Total); + let mu_res_vapor = vapor.residual_chemical_potential(); + let kt = SIUnit::gas_constant() * t; + let rho_frac = (&liquid.partial_density / &vapor.partial_density).into_value()?; prediction.push( - mu_liquid.get(0) - mu_vapor.get(0) + 500.0 * SIUnit::reference_molar_energy(), + mu_res_liquid.get(0) - mu_res_vapor.get(0) + + kt * rho_frac[0].ln() + + 500.0 * SIUnit::reference_molar_energy(), ); prediction.push( - mu_liquid.get(1) - mu_vapor.get(1) + 500.0 * SIUnit::reference_molar_energy(), + mu_res_liquid.get(1) - mu_res_vapor.get(1) + + kt * rho_frac[1].ln() + + 500.0 * SIUnit::reference_molar_energy(), ); } Ok(SIArray1::from_vec(prediction)) @@ -129,7 +134,7 @@ impl BinaryVlePressure { } } -impl DataSet for BinaryVlePressure { +impl DataSet for BinaryVlePressure { fn target(&self) -> &SIArray1 { &self.pressure } @@ -227,7 +232,7 @@ impl BinaryPhaseDiagram { } } -impl DataSet for BinaryPhaseDiagram { +impl DataSet for BinaryPhaseDiagram { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/dataset.rs b/src/estimator/dataset.rs index 4e8f78aa2..cf496b8c0 100644 --- a/src/estimator/dataset.rs +++ b/src/estimator/dataset.rs @@ -3,7 +3,7 @@ //! a `target` which can be values from experimental data or //! other models. use super::{EstimatorError, Loss}; -use feos_core::EquationOfState; +use feos_core::Residual; use ndarray::Array1; use quantity::si::SIArray1; use std::collections::HashMap; @@ -14,7 +14,7 @@ use std::sync::Arc; /// /// Functionalities in the context of optimizations of /// parameters of equations of state. -pub trait DataSet: Send + Sync { +pub trait DataSet: Send + Sync { /// Return target quantity. fn target(&self) -> &SIArray1; @@ -61,7 +61,7 @@ pub trait DataSet: Send + Sync { } } -impl fmt::Display for dyn DataSet { +impl fmt::Display for dyn DataSet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, diff --git a/src/estimator/diffusion.rs b/src/estimator/diffusion.rs index 25c9db749..d921ead42 100644 --- a/src/estimator/diffusion.rs +++ b/src/estimator/diffusion.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl Diffusion { } } -impl DataSet for Diffusion { +impl DataSet for Diffusion { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/estimator.rs b/src/estimator/estimator.rs index 300d26314..69299e5e3 100644 --- a/src/estimator/estimator.rs +++ b/src/estimator/estimator.rs @@ -1,7 +1,7 @@ //! The [`Estimator`] struct can be used to store multiple [`DataSet`]s for convenient parameter //! optimization. use super::{DataSet, EstimatorError, Loss}; -use feos_core::EquationOfState; +use feos_core::Residual; use ndarray::{arr1, concatenate, Array1, ArrayView1, Axis}; use quantity::si::SIArray1; use std::fmt; @@ -11,13 +11,13 @@ use std::sync::Arc; /// A collection of [`DataSet`]s and weights that can be used to /// evaluate an equation of state versus experimental data. -pub struct Estimator { +pub struct Estimator { data: Vec>>, weights: Vec, losses: Vec, } -impl Estimator { +impl Estimator { /// Create a new `Estimator` given `DataSet`s and weights. /// /// The weights are normalized and used as multiplicator when the @@ -99,7 +99,7 @@ impl Estimator { } } -impl Display for Estimator { +impl Display for Estimator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for d in self.data.iter() { writeln!(f, "{}", d)?; diff --git a/src/estimator/liquid_density.rs b/src/estimator/liquid_density.rs index 71a2e36eb..f1290b6a9 100644 --- a/src/estimator/liquid_density.rs +++ b/src/estimator/liquid_density.rs @@ -1,7 +1,6 @@ use super::{DataSet, EstimatorError}; use feos_core::{ - DensityInitialization, EosUnit, EquationOfState, MolarWeight, PhaseEquilibrium, SolverOptions, - State, + DensityInitialization, EosUnit, MolarWeight, PhaseEquilibrium, Residual, SolverOptions, State, }; use ndarray::arr1; use quantity::si::{SIArray1, SIUnit}; @@ -44,7 +43,7 @@ impl LiquidDensity { } } -impl DataSet for LiquidDensity { +impl DataSet for LiquidDensity { fn target(&self) -> &SIArray1 { &self.target } @@ -110,7 +109,7 @@ impl EquilibriumLiquidDensity { } } -impl DataSet for EquilibriumLiquidDensity { +impl DataSet for EquilibriumLiquidDensity { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/thermal_conductivity.rs b/src/estimator/thermal_conductivity.rs index 45b3e33f4..ec7003edd 100644 --- a/src/estimator/thermal_conductivity.rs +++ b/src/estimator/thermal_conductivity.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl ThermalConductivity { } } -impl DataSet for ThermalConductivity { +impl DataSet for ThermalConductivity { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/vapor_pressure.rs b/src/estimator/vapor_pressure.rs index 19d9e2fb7..0545c73da 100644 --- a/src/estimator/vapor_pressure.rs +++ b/src/estimator/vapor_pressure.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{Contributions, EosUnit, EquationOfState, PhaseEquilibrium, SolverOptions, State}; +use feos_core::{Contributions, EosUnit, PhaseEquilibrium, Residual, SolverOptions, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::collections::HashMap; @@ -57,7 +57,7 @@ impl VaporPressure { } } -impl DataSet for VaporPressure { +impl DataSet for VaporPressure { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/viscosity.rs b/src/estimator/viscosity.rs index 8e57c56d4..ad05b168f 100644 --- a/src/estimator/viscosity.rs +++ b/src/estimator/viscosity.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::arr1; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl Viscosity { } } -impl DataSet for Viscosity { +impl DataSet for Viscosity { fn target(&self) -> &SIArray1 { &self.target } From cfeb4885e6cc9012902044c9cbf66608f12efba9 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Wed, 7 Jun 2023 18:07:36 +0200 Subject: [PATCH 31/47] clear Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 87c8ebe32..ff44725de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ inherits = "release" lto = true [features] -default = ["all_models"] +default = [] dft = ["feos-dft", "petgraph"] estimator = [] association = [] From 25ed0c9c25df66c6b9e0d38bd1f2c268eb7c17c3 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Wed, 7 Jun 2023 18:29:22 +0200 Subject: [PATCH 32/47] WIP Python interface for DFT --- feos-derive/src/dft.rs | 27 +++++++++++++++++---- src/dft.rs | 6 +++-- src/lib.rs | 8 +++---- src/python/dft.rs | 54 +++++++++++++++++++----------------------- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/feos-derive/src/dft.rs b/feos-derive/src/dft.rs index 8a166bb51..73c8daaf8 100644 --- a/feos-derive/src/dft.rs +++ b/feos-derive/src/dft.rs @@ -79,6 +79,28 @@ fn impl_from( .collect() } +// fn impl_from( +// variants: &syn::punctuated::Punctuated, +// ) -> syn::Result { +// variants +// .iter() +// .map(|v| { +// let (variant_name, functional_name) = extract_names(v)?; +// Ok(quote! { +// impl From> for EquationOfState { +// fn from(f: EquationOfState) -> Self { +// EquationOfState::new( +// f.ideal_gas.clone(), +// Arc::new(FunctionalVariant::#variant_name(f.residual.clone())) +// ) +// } +// } +// }) +// }) +// .collect() +// } + + fn impl_helmholtz_energy_functional( variants: &syn::punctuated::Punctuated, ) -> syn::Result { @@ -119,11 +141,6 @@ fn impl_helmholtz_energy_functional( Ok(quote! { impl HelmholtzEnergyFunctional for FunctionalVariant { - fn subset(&self, component_list: &[usize]) -> DFT { - match self { - #(#subset,)* - } - } fn molecule_shape(&self) -> MoleculeShape { match self { #(#molecule_shape,)* diff --git a/src/dft.rs b/src/dft.rs index 352ce0c27..43d980f33 100644 --- a/src/dft.rs +++ b/src/dft.rs @@ -7,8 +7,9 @@ use crate::pcsaft::PcSaftFunctional; use crate::pets::PetsFunctional; #[cfg(feature = "saftvrqmie")] use crate::saftvrqmie::SaftVRQMieFunctional; +use crate::{IdealGasModel, ResidualModel}; use feos_core::*; -use feos_derive::HelmholtzEnergyFunctional; +use feos_derive::{Components, HelmholtzEnergyFunctional}; use feos_dft::adsorption::*; use feos_dft::solvation::*; use feos_dft::*; @@ -16,12 +17,13 @@ use ndarray::{Array1, Array2}; use petgraph::graph::UnGraph; use petgraph::Graph; use quantity::si::*; +use std::sync::Arc; /// Collection of different [HelmholtzEnergyFunctional] implementations. /// /// Particularly relevant for situations in which generic types /// are undesirable (e.g. FFI). -#[derive(HelmholtzEnergyFunctional)] +#[derive(Components, HelmholtzEnergyFunctional)] pub enum FunctionalVariant { #[cfg(feature = "pcsaft")] #[implement(fluid_parameters, molar_weight, pair_potential)] diff --git a/src/lib.rs b/src/lib.rs index 25377008a..ddb0e7cba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,10 +35,10 @@ #![allow(clippy::too_many_arguments)] #![allow(deprecated)] -// #[cfg(feature = "dft")] -// mod dft; -// #[cfg(feature = "dft")] -// pub use dft::FunctionalVariant; +#[cfg(feature = "dft")] +mod dft; +#[cfg(feature = "dft")] +pub use dft::FunctionalVariant; mod eos; pub use eos::{IdealGasModel, ResidualModel}; diff --git a/src/python/dft.rs b/src/python/dft.rs index 375a65bb3..2342c08fd 100644 --- a/src/python/dft.rs +++ b/src/python/dft.rs @@ -85,13 +85,9 @@ impl PyFunctionalVariant { tol_cross_assoc, dq_variant, }; - let functional = Arc::new(PcSaftFunctional::with_options( - parameters.0, - fmt_version, - options, - )); - let ideal_gas = Arc::new(IdealGasModel::NoModel(functional.components())); - Self(Arc::new(EquationOfState::new(ideal_gas, functional).into())) + let functional: DFT = PcSaftFunctional::with_options(parameters.0, fmt_version, options).into(); + let eos = functional.ideal_gas(IdealGasModel::NoModel(functional.components())); + Self(Arc::new(eos)) } /// (heterosegmented) group contribution PC-SAFT Helmholtz energy functional. @@ -163,24 +159,24 @@ impl PyFunctionalVariant { )) } - /// Helmholtz energy functional for hard sphere systems. - /// - /// Parameters - /// ---------- - /// sigma : numpy.ndarray[float] - /// The diameters of the hard spheres in Angstrom. - /// fmt_version : FMTVersion - /// The specific variant of the FMT term. - /// - /// Returns - /// ------- - /// HelmholtzEnergyFunctional - #[staticmethod] - fn fmt(sigma: &PyArray1, fmt_version: FMTVersion) -> Self { - Self(Arc::new( - FMTFunctional::new(&sigma.to_owned_array(), fmt_version).into(), - )) - } + // /// Helmholtz energy functional for hard sphere systems. + // /// + // /// Parameters + // /// ---------- + // /// sigma : numpy.ndarray[float] + // /// The diameters of the hard spheres in Angstrom. + // /// fmt_version : FMTVersion + // /// The specific variant of the FMT term. + // /// + // /// Returns + // /// ------- + // /// HelmholtzEnergyFunctional + // #[staticmethod] + // fn fmt(sigma: &PyArray1, fmt_version: FMTVersion) -> Self { + // Self(Arc::new( + // FMTFunctional::new(&sigma.to_owned_array(), fmt_version).into(), + // )) + // } /// SAFT-VRQ Mie Helmholtz energy functional. /// @@ -227,9 +223,9 @@ impl PyFunctionalVariant { impl_equation_of_state!(PyFunctionalVariant); -impl_state!(DFT, PyFunctionalVariant); -impl_state_molarweight!(DFT, PyFunctionalVariant); -impl_phase_equilibrium!(DFT, PyFunctionalVariant); +impl_state!(DFT>, PyFunctionalVariant); +impl_state_molarweight!(DFT>, PyFunctionalVariant); +impl_phase_equilibrium!(DFT>, PyFunctionalVariant); impl_planar_interface!(FunctionalVariant); impl_surface_tension_diagram!(FunctionalVariant); @@ -241,7 +237,7 @@ impl_pair_correlation!(FunctionalVariant); impl_solvation_profile!(FunctionalVariant); #[cfg(feature = "estimator")] -impl_estimator!(DFT, PyFunctionalVariant); +impl_estimator!(DFT>, PyFunctionalVariant); #[pymodule] pub fn dft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { From 84a1a0d7a0ceeb2f86633bfd8a1e9107493c72c9 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Wed, 7 Jun 2023 19:10:57 +0200 Subject: [PATCH 33/47] Python wheels online --- feos-core/src/equation_of_state/mod.rs | 2 +- feos-derive/src/dft.rs | 28 -------- feos-dft/src/functional.rs | 18 +++++ feos-dft/src/python/profile.rs | 5 -- src/dft.rs | 2 - src/python/dft.rs | 91 ++++++++++++++------------ 6 files changed, 69 insertions(+), 77 deletions(-) diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index 143362959..a695d08be 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -78,7 +78,7 @@ impl Residual for EquationOfState { } } -impl MolarWeight for EquationOfState { +impl MolarWeight for EquationOfState { fn molar_weight(&self) -> SIArray1 { self.residual.molar_weight() } diff --git a/feos-derive/src/dft.rs b/feos-derive/src/dft.rs index 73c8daaf8..647ef5110 100644 --- a/feos-derive/src/dft.rs +++ b/feos-derive/src/dft.rs @@ -79,37 +79,9 @@ fn impl_from( .collect() } -// fn impl_from( -// variants: &syn::punctuated::Punctuated, -// ) -> syn::Result { -// variants -// .iter() -// .map(|v| { -// let (variant_name, functional_name) = extract_names(v)?; -// Ok(quote! { -// impl From> for EquationOfState { -// fn from(f: EquationOfState) -> Self { -// EquationOfState::new( -// f.ideal_gas.clone(), -// Arc::new(FunctionalVariant::#variant_name(f.residual.clone())) -// ) -// } -// } -// }) -// }) -// .collect() -// } - - fn impl_helmholtz_energy_functional( variants: &syn::punctuated::Punctuated, ) -> syn::Result { - let subset = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(functional) => functional.subset(component_list).into() - } - }); let molecule_shape = variants.iter().map(|v| { let name = &v.ident; quote! { diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index bd4d59666..9cd3d4e5c 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -1,6 +1,8 @@ +use crate::adsorption::FluidParameters; use crate::convolver::Convolver; 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, DeBroglieWavelength, EosResult, EquationOfState, HelmholtzEnergy, @@ -32,6 +34,22 @@ impl HelmholtzEnergyF } } +impl PairPotential for EquationOfState { + fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Array2 { + self.residual.pair_potential(i, r, temperature) + } +} + +impl FluidParameters for EquationOfState { + fn epsilon_k_ff(&self) -> Array1 { + self.residual.epsilon_k_ff() + } + + fn sigma_ff(&self) -> &Array1 { + self.residual.sigma_ff() + } +} + /// Wrapper struct for the [HelmholtzEnergyFunctional] trait. /// /// Needed (for now) to generically implement the `Residual` diff --git a/feos-dft/src/python/profile.rs b/feos-dft/src/python/profile.rs index 4223f4dab..46c2f378f 100644 --- a/feos-dft/src/python/profile.rs +++ b/feos-dft/src/python/profile.rs @@ -77,11 +77,6 @@ macro_rules! impl_profile { self.0.profile.external_potential.view().to_pyarray(py) } - #[getter] - fn get_chemical_potential(&self) -> PySIArray1 { - PySIArray1::from(self.0.profile.chemical_potential()) - } - #[getter] fn get_bulk(&self) -> PyState { PyState(self.0.profile.bulk.clone()) diff --git a/src/dft.rs b/src/dft.rs index 43d980f33..585681f4d 100644 --- a/src/dft.rs +++ b/src/dft.rs @@ -7,7 +7,6 @@ use crate::pcsaft::PcSaftFunctional; use crate::pets::PetsFunctional; #[cfg(feature = "saftvrqmie")] use crate::saftvrqmie::SaftVRQMieFunctional; -use crate::{IdealGasModel, ResidualModel}; use feos_core::*; use feos_derive::{Components, HelmholtzEnergyFunctional}; use feos_dft::adsorption::*; @@ -17,7 +16,6 @@ use ndarray::{Array1, Array2}; use petgraph::graph::UnGraph; use petgraph::Graph; use quantity::si::*; -use std::sync::Arc; /// Collection of different [HelmholtzEnergyFunctional] implementations. /// diff --git a/src/python/dft.rs b/src/python/dft.rs index 2342c08fd..4a2b45ca8 100644 --- a/src/python/dft.rs +++ b/src/python/dft.rs @@ -39,9 +39,23 @@ use quantity::si::*; use std::collections::HashMap; use std::sync::Arc; +type Functional = EquationOfState; + #[pyclass(name = "HelmholtzEnergyFunctional")] #[derive(Clone)] -pub struct PyFunctionalVariant(pub Arc>>); +pub struct PyFunctionalVariant(pub Arc>); + +impl PyFunctionalVariant { + fn new(functional: DFT) -> Self + where + FunctionalVariant: From, + { + let functional: DFT = functional.into(); + let n = functional.components(); + let eos = functional.ideal_gas(IdealGasModel::NoModel(n)); + Self(Arc::new(eos)) + } +} #[pymethods] impl PyFunctionalVariant { @@ -85,9 +99,8 @@ impl PyFunctionalVariant { tol_cross_assoc, dq_variant, }; - let functional: DFT = PcSaftFunctional::with_options(parameters.0, fmt_version, options).into(); - let eos = functional.ideal_gas(IdealGasModel::NoModel(functional.components())); - Self(Arc::new(eos)) + let func = PcSaftFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } /// (heterosegmented) group contribution PC-SAFT Helmholtz energy functional. @@ -126,9 +139,8 @@ impl PyFunctionalVariant { max_iter_cross_assoc, tol_cross_assoc, }; - Self(Arc::new( - GcPcSaftFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = GcPcSaftFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } /// PeTS Helmholtz energy functional without simplifications @@ -154,29 +166,27 @@ impl PyFunctionalVariant { )] fn pets(parameters: PyPetsParameters, fmt_version: FMTVersion, max_eta: f64) -> Self { let options = PetsOptions { max_eta }; - Self(Arc::new( - PetsFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = PetsFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } - // /// Helmholtz energy functional for hard sphere systems. - // /// - // /// Parameters - // /// ---------- - // /// sigma : numpy.ndarray[float] - // /// The diameters of the hard spheres in Angstrom. - // /// fmt_version : FMTVersion - // /// The specific variant of the FMT term. - // /// - // /// Returns - // /// ------- - // /// HelmholtzEnergyFunctional - // #[staticmethod] - // fn fmt(sigma: &PyArray1, fmt_version: FMTVersion) -> Self { - // Self(Arc::new( - // FMTFunctional::new(&sigma.to_owned_array(), fmt_version).into(), - // )) - // } + /// Helmholtz energy functional for hard sphere systems. + /// + /// Parameters + /// ---------- + /// sigma : numpy.ndarray[float] + /// The diameters of the hard spheres in Angstrom. + /// fmt_version : FMTVersion + /// The specific variant of the FMT term. + /// + /// Returns + /// ------- + /// HelmholtzEnergyFunctional + #[staticmethod] + fn fmt(sigma: &PyArray1, fmt_version: FMTVersion) -> Self { + let func = FMTFunctional::new(&sigma.to_owned_array(), fmt_version); + Self::new(func) + } /// SAFT-VRQ Mie Helmholtz energy functional. /// @@ -215,29 +225,28 @@ impl PyFunctionalVariant { fh_order, inc_nonadd_term, }; - Self(Arc::new( - SaftVRQMieFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = SaftVRQMieFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } } impl_equation_of_state!(PyFunctionalVariant); -impl_state!(DFT>, PyFunctionalVariant); -impl_state_molarweight!(DFT>, PyFunctionalVariant); -impl_phase_equilibrium!(DFT>, PyFunctionalVariant); +impl_state!(DFT, PyFunctionalVariant); +impl_state_molarweight!(DFT, PyFunctionalVariant); +impl_phase_equilibrium!(DFT, PyFunctionalVariant); -impl_planar_interface!(FunctionalVariant); -impl_surface_tension_diagram!(FunctionalVariant); +impl_planar_interface!(Functional); +impl_surface_tension_diagram!(Functional); -impl_pore!(FunctionalVariant, PyFunctionalVariant); -impl_adsorption!(FunctionalVariant, PyFunctionalVariant); +impl_pore!(Functional, PyFunctionalVariant); +impl_adsorption!(Functional, PyFunctionalVariant); -impl_pair_correlation!(FunctionalVariant); -impl_solvation_profile!(FunctionalVariant); +impl_pair_correlation!(Functional); +impl_solvation_profile!(Functional); #[cfg(feature = "estimator")] -impl_estimator!(DFT>, PyFunctionalVariant); +impl_estimator!(DFT, PyFunctionalVariant); #[pymodule] pub fn dft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { From 5da49731e971982333d51879127293e3a0546e91 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 9 Jun 2023 13:16:19 +0200 Subject: [PATCH 34/47] Pore equilibrium algorithm that does not rely on the ideal gas model. Appears to be slightly less robust --- feos-core/src/state/mod.rs | 44 ------- feos-dft/src/adsorption/mod.rs | 182 +++++++++++--------------- feos-dft/src/python/adsorption/mod.rs | 8 +- 3 files changed, 83 insertions(+), 151 deletions(-) diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index d47da2cb5..96d1f6385 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -795,50 +795,6 @@ impl State { Self::new_nvt(&self.eos, temperature, self.volume, &self.moles) } - // /// Update the state with the given chemical potential. - // pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { - // for _ in 0..50 { - // let dmu_drho = self.dmu_dni(Contributions::Total) * self.volume; - // let f = self.chemical_potential(Contributions::Total) - chemical_potential; - // let dmu_drho_r = dmu_drho - // .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - // let f_r = f.to_reduced(SIUnit::reference_molar_energy())?; - // let rho = &self.partial_density - // - &(LU::new(dmu_drho_r)?.solve(&f_r) * SIUnit::reference_density()); - // *self = State::new_nvt( - // &self.eos, - // self.temperature, - // self.volume, - // &(rho * self.volume), - // )?; - // if norm(&f.to_reduced(SIUnit::reference_molar_energy())?) < 1e-8 { - // return Ok(()); - // } - // } - // Err(EosError::NotConverged( - // "State::update_chemical_potential".into(), - // )) - // } - - // /// Update the state with the given molar Gibbs energy. - // pub fn update_gibbs_energy(mut self, molar_gibbs_energy: SINumber) -> EosResult { - // for _ in 0..50 { - // let df = self.volume / self.density * self.dp_dv(Contributions::Total); - // let f = self.molar_gibbs_energy(Contributions::Total) - molar_gibbs_energy; - // let rho = self.density * (f.to_reduced(df)?).exp(); - // self = State::new_nvt( - // &self.eos, - // self.temperature, - // self.total_moles / rho, - // &self.moles, - // )?; - // if f.to_reduced(SIUnit::reference_molar_energy())?.abs() < 1e-8 { - // return Ok(self); - // } - // } - // Err(EosError::NotConverged("State::update_gibbs_energy".into())) - // } - /// Creates a [StateHD] cloning temperature, volume and moles. pub fn derive0(&self) -> StateHD { StateHD::new( diff --git a/feos-dft/src/adsorption/mod.rs b/feos-dft/src/adsorption/mod.rs index 1132ca7b2..b79227403 100644 --- a/feos-dft/src/adsorption/mod.rs +++ b/feos-dft/src/adsorption/mod.rs @@ -2,8 +2,8 @@ use super::functional::{HelmholtzEnergyFunctional, DFT}; use super::solver::DFTSolver; use feos_core::{ - Components, Contributions, DensityInitialization, EosResult, EosUnit, Residual, SolverOptions, - State, StateBuilder, + Components, Contributions, DensityInitialization, EosError, EosResult, EosUnit, Residual, + SolverOptions, State, StateBuilder, }; use ndarray::{Array1, Dimension, Ix1, Ix3, RemoveAxis}; use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; @@ -263,92 +263,89 @@ where solver: Option<&DFTSolver>, options: SolverOptions, ) -> EosResult> { - unimplemented!(); - // let moles = - // functional.validate_moles(molefracs.map(|x| x * SIUnit::reference_moles()).as_ref())?; - - // // calculate density profiles for the minimum and maximum pressure - // let vapor_bulk = StateBuilder::new(functional) - // .temperature(temperature) - // .pressure(p_min) - // .moles(&moles) - // .vapor() - // .build()?; - // let liquid_bulk = StateBuilder::new(functional) - // .temperature(temperature) - // .pressure(p_max) - // .moles(&moles) - // .liquid() - // .build()?; - - // let mut vapor = pore.initialize(&vapor_bulk, None, None)?.solve(None)?; - // let mut liquid = pore.initialize(&liquid_bulk, None, None)?.solve(solver)?; - - // // calculate initial value for the molar gibbs energy - // let nv = vapor.profile.bulk.density - // * (vapor.profile.moles() * vapor.profile.bulk.partial_molar_volume()).sum(); - // let nl = liquid.profile.bulk.density - // * (liquid.profile.moles() * liquid.profile.bulk.partial_molar_volume()).sum(); - // let f = |s: &PoreProfile, n: SINumber| -> EosResult<_> { - // Ok(s.grand_potential.unwrap() - // + s.profile.bulk.molar_gibbs_energy(Contributions::Total) * n) - // }; - // let mut g = (f(&liquid, nl)? - f(&vapor, nv)?) / (nl - nv); - - // // update filled pore with limited step size - // let mut bulk = StateBuilder::new(functional) - // .temperature(temperature) - // .pressure(p_max) - // .moles(&moles) - // .vapor() - // .build()?; - // let g_liquid = liquid.profile.bulk.molar_gibbs_energy(Contributions::Total); - // let steps = (10.0 * (g - g_liquid)).to_reduced(g_liquid)?.abs().ceil() as usize; - // let delta_g = (g - g_liquid) / steps as f64; - // for i in 1..=steps { - // let g_i = g_liquid + i as f64 * delta_g; - // bulk = bulk.update_gibbs_energy(g_i)?; - // liquid = liquid.update_bulk(&bulk).solve(solver)?; - // } + let moles = + functional.validate_moles(molefracs.map(|x| x * SIUnit::reference_moles()).as_ref())?; - // for _ in 0..options.max_iter.unwrap_or(MAX_ITER_ADSORPTION_EQUILIBRIUM) { - // // update empty pore - // vapor = vapor.update_bulk(&bulk).solve(None)?; + // calculate density profiles for the minimum and maximum pressure + let vapor_bulk = StateBuilder::new(functional) + .temperature(temperature) + .pressure(p_min) + .moles(&moles) + .vapor() + .build()?; + let bulk_init = StateBuilder::new(functional) + .temperature(temperature) + .pressure(p_max) + .moles(&moles) + .liquid() + .build()?; + let liquid_bulk = StateBuilder::new(functional) + .temperature(temperature) + .pressure(p_max) + .moles(&moles) + .vapor() + .build()?; + + let mut vapor = pore.initialize(&vapor_bulk, None, None)?.solve(None)?; + let mut liquid = pore.initialize(&bulk_init, None, None)?.solve(solver)?; + + // calculate initial value for the molar gibbs energy + let n_dp_drho_v = (vapor.profile.moles() * vapor_bulk.dp_drho(Contributions::Total)).sum(); + let n_dp_drho_l = + (liquid.profile.moles() * liquid_bulk.dp_drho(Contributions::Total)).sum(); + let mut rho = (vapor.grand_potential.unwrap() + n_dp_drho_v + - (liquid.grand_potential.unwrap() + n_dp_drho_l)) + / (n_dp_drho_v / vapor_bulk.density - n_dp_drho_l / liquid_bulk.density); + + // update filled pore with limited step size + let mut bulk = StateBuilder::new(functional) + .temperature(temperature) + .pressure(p_max) + .moles(&moles) + .vapor() + .build()?; + let rho0 = liquid_bulk.density; + let steps = (10.0 * (rho - rho0)).to_reduced(rho0)?.abs().ceil() as usize; + let delta_rho = (rho - rho0) / steps as f64; + for i in 1..=steps { + let rho_i = rho0 + i as f64 * delta_rho; + bulk = State::new_nvt(functional, temperature, moles.sum() / rho_i, &moles)?; + liquid = liquid.update_bulk(&bulk).solve(solver)?; + } - // // update filled pore - // liquid = liquid.update_bulk(&bulk).solve(solver)?; + for _ in 0..options.max_iter.unwrap_or(MAX_ITER_ADSORPTION_EQUILIBRIUM) { + // update empty pore + vapor = vapor.update_bulk(&bulk).solve(None)?; - // // calculate moles - // let nv = vapor.profile.bulk.density - // * (vapor.profile.moles() * vapor.profile.bulk.partial_molar_volume()).sum(); - // let nl = liquid.profile.bulk.density - // * (liquid.profile.moles() * liquid.profile.bulk.partial_molar_volume()).sum(); + // update filled pore + liquid = liquid.update_bulk(&bulk).solve(solver)?; - // // check for a trivial solution - // if nl.to_reduced(nv)? - 1.0 < 1e-5 { - // return Err(EosError::TrivialSolution); - // } + // calculate moles + let n_dp_drho = ((liquid.profile.moles() - vapor.profile.moles()) + * bulk.dp_drho(Contributions::Total)) + .sum(); - // // Newton step - // let delta_g = - // (vapor.grand_potential.unwrap() - liquid.grand_potential.unwrap()) / (nv - nl); - // if delta_g.to_reduced(SIUnit::reference_molar_energy())?.abs() - // < options.tol.unwrap_or(TOL_ADSORPTION_EQUILIBRIUM) - // { - // return Ok(Adsorption::new( - // functional, - // pore, - // vec![Ok(vapor), Ok(liquid)], - // )); - // } - // g += delta_g; + // Newton step + let delta_rho = (liquid.grand_potential.unwrap() - vapor.grand_potential.unwrap()) + / n_dp_drho + * bulk.density; + if delta_rho.to_reduced(SIUnit::reference_density())?.abs() + < options.tol.unwrap_or(TOL_ADSORPTION_EQUILIBRIUM) + { + return Ok(Adsorption::new( + functional, + pore, + vec![Ok(vapor), Ok(liquid)], + )); + } + rho += delta_rho; - // // update bulk phase - // bulk = bulk.update_gibbs_energy(g)?; - // } - // Err(EosError::NotConverged( - // "Adsorption::phase_equilibrium".into(), - // )) + // update bulk phase + bulk = State::new_nvt(functional, temperature, moles.sum() / rho, &moles)?; + } + Err(EosError::NotConverged( + "Adsorption::phase_equilibrium".into(), + )) } pub fn pressure(&self) -> SIArray1 { @@ -371,27 +368,6 @@ where }) } - pub fn molar_gibbs_energy(&self) -> SIArray1 { - unimplemented!(); - // SIArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { - // Ok(p) => { - // if p.profile.bulk.eos.components() > 1 - // && !p.profile.bulk.is_stable(SolverOptions::default()).unwrap() - // { - // p.profile - // .bulk - // .tp_flash(None, SolverOptions::default(), None) - // .unwrap() - // .vapor() - // .molar_gibbs_energy(Contributions::Total) - // } else { - // p.profile.bulk.molar_gibbs_energy(Contributions::Total) - // } - // } - // Err(_) => f64::NAN * SIUnit::reference_molar_energy(), - // }) - } - pub fn adsorption(&self) -> SIArray2 { SIArray2::from_shape_fn((self.components, self.profiles.len()), |(j, i)| match &self .profiles[i] diff --git a/feos-dft/src/python/adsorption/mod.rs b/feos-dft/src/python/adsorption/mod.rs index 99970cb55..03482549c 100644 --- a/feos-dft/src/python/adsorption/mod.rs +++ b/feos-dft/src/python/adsorption/mod.rs @@ -229,10 +229,10 @@ macro_rules! impl_adsorption_isotherm { self.0.pressure().into() } - #[getter] - fn get_molar_gibbs_energy(&self) -> PySIArray1 { - self.0.molar_gibbs_energy().into() - } + // #[getter] + // fn get_molar_gibbs_energy(&self) -> PySIArray1 { + // self.0.molar_gibbs_energy().into() + // } #[getter] fn get_adsorption(&self) -> PySIArray2 { From a072af113112aa12eefba52dafcda692af667dbc Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 9 Jun 2023 15:29:30 +0200 Subject: [PATCH 35/47] Final cleanup of rust code, moved Verbosity and SolverOptions to lib.rs --- Cargo.toml | 2 +- feos-core/src/cubic.rs | 5 +- feos-core/src/joback.rs | 9 +- feos-core/src/lib.rs | 74 ++- feos-core/src/parameter/mod.rs | 462 +++++++++--------- feos-core/src/parameter/model_record.rs | 107 ++-- feos-core/src/phase_equilibria/bubble_dew.rs | 4 +- feos-core/src/phase_equilibria/mod.rs | 71 --- .../phase_equilibria/phase_diagram_binary.rs | 6 +- .../phase_equilibria/phase_diagram_pure.rs | 3 +- .../src/phase_equilibria/phase_envelope.rs | 4 +- .../phase_equilibria/stability_analysis.rs | 4 +- feos-core/src/phase_equilibria/tp_flash.rs | 3 +- feos-core/src/phase_equilibria/vle_pure.rs | 4 +- feos-core/src/python/state.rs | 104 ++-- feos-core/src/state/critical_point.rs | 4 +- feos-core/src/state/mod.rs | 72 --- feos-derive/src/ideal_gas.rs | 17 - feos-dft/src/profile/mod.rs | 5 - feos-dft/src/python/adsorption/mod.rs | 5 - src/pcsaft/eos/mod.rs | 14 - src/pets/eos/mod.rs | 408 ++++++++-------- src/python/eos.rs | 4 +- tests/pcsaft/properties.rs | 1 - 24 files changed, 632 insertions(+), 760 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ff44725de..0f4525df9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ inherits = "release" lto = true [features] -default = [] +default = ["pcsaft"] dft = ["feos-dft", "petgraph"] estimator = [] association = [] diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index 3d1d555b2..be399d67d 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -244,9 +244,8 @@ impl MolarWeight for PengRobinson { #[cfg(test)] mod tests { use super::*; - use crate::state::{SolverOptions, State}; - use crate::Contributions; - use crate::{EosResult, Verbosity}; + use crate::state::{Contributions, State}; + use crate::{EosResult, SolverOptions, Verbosity}; use approx::*; use quantity::si::*; use std::sync::Arc; diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index 3d84a7574..73b159543 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -1,15 +1,17 @@ //! Implementation of the ideal gas heat capacity (de Broglie wavelength) //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). -use crate::equation_of_state::{Components, DeBroglieWavelength, DeBroglieWavelengthDual}; -use crate::{parameter::*, Residual}; +use crate::equation_of_state::{ + Components, DeBroglieWavelength, DeBroglieWavelengthDual, Residual, +}; +use crate::parameter::*; use crate::{EosResult, EosUnit, IdealGas}; use conv::ValueInto; use ndarray::Array1; use num_dual::*; use quantity::si::{SINumber, SIUnit}; use serde::{Deserialize, Serialize}; -use std::fmt::{self}; +use std::fmt; /// Coefficients used in the Joback model. /// @@ -67,7 +69,6 @@ impl> FromSegments for JobackRecord { /// [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). pub struct Joback { pub records: Vec, - // de_broglie: Box, } impl Joback { diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 8e917c2e8..837b7846a 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -42,13 +42,83 @@ pub use equation_of_state::{ pub use errors::{EosError, EosResult}; pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium}; pub use state::{ - Contributions, DensityInitialization, Derivative, SolverOptions, State, StateBuilder, StateHD, - StateVec, Verbosity, + Contributions, DensityInitialization, Derivative, State, StateBuilder, StateHD, StateVec, }; #[cfg(feature = "python")] pub mod python; +/// Level of detail in the iteration output. +#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum Verbosity { + /// Do not print output. + None, + /// Print information about the success of failure of the iteration. + Result, + /// Print a detailed outpur for every iteration. + Iter, +} + +impl Default for Verbosity { + fn default() -> Self { + Self::None + } +} + +/// Options for the various phase equilibria solvers. +/// +/// If the values are [None], solver specific default +/// values are used. +#[derive(Copy, Clone, Default)] +pub struct SolverOptions { + /// Maximum number of iterations. + pub max_iter: Option, + /// Tolerance. + pub tol: Option, + /// Iteration outpput indicated by the [Verbosity] enum. + pub verbosity: Verbosity, +} + +impl From<(Option, Option, Option)> for SolverOptions { + fn from(options: (Option, Option, Option)) -> Self { + Self { + max_iter: options.0, + tol: options.1, + verbosity: options.2.unwrap_or(Verbosity::None), + } + } +} + +impl SolverOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn max_iter(mut self, max_iter: usize) -> Self { + self.max_iter = Some(max_iter); + self + } + + pub fn tol(mut self, tol: f64) -> Self { + self.tol = Some(tol); + self + } + + pub fn verbosity(mut self, verbosity: Verbosity) -> Self { + self.verbosity = verbosity; + self + } + + pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { + ( + self.max_iter.unwrap_or(max_iter), + self.tol.unwrap_or(tol), + self.verbosity, + ) + } +} + /// Consistent conversions between quantities and reduced properties. pub trait EosUnit: Unit + Send + Sync { fn reference_temperature() -> QuantityScalar; diff --git a/feos-core/src/parameter/mod.rs b/feos-core/src/parameter/mod.rs index 7270af288..5782db00f 100644 --- a/feos-core/src/parameter/mod.rs +++ b/feos-core/src/parameter/mod.rs @@ -62,12 +62,7 @@ where /// Return the original pure and binary records that were used to construct the parameters. #[allow(clippy::type_complexity)] - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ); + fn records(&self) -> (&[PureRecord], &Array2); /// Helper function to build matrix from list of records in correct order. /// @@ -137,8 +132,7 @@ where P: AsRef, { let mut queried: IndexSet = IndexSet::new(); - let mut record_map: HashMap> = - HashMap::new(); + let mut record_map: HashMap> = HashMap::new(); for (substances, file) in input { substances.iter().try_for_each(|identifier| { @@ -153,8 +147,7 @@ where let f = File::open(file)?; let reader = BufReader::new(f); - let pure_records: Vec> = - serde_json::from_reader(reader)?; + let pure_records: Vec> = serde_json::from_reader(reader)?; pure_records .into_iter() @@ -466,231 +459,224 @@ pub enum ParameterError { IncompatibleParameters(String), } -// #[cfg(test)] -// mod test { -// use super::*; -// use crate::joback::JobackRecord; -// use serde::{Deserialize, Serialize}; -// use std::convert::TryFrom; - -// #[derive(Debug, Clone, Serialize, Deserialize, Default)] -// struct MyPureModel { -// a: f64, -// } - -// #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] -// struct MyBinaryModel { -// b: f64, -// } - -// impl TryFrom for MyBinaryModel { -// type Error = &'static str; -// fn try_from(f: f64) -> Result { -// Ok(Self { b: f }) -// } -// } - -// struct MyParameter { -// pure_records: Vec>, -// binary_records: Array2, -// } - -// impl Parameter for MyParameter { -// type Pure = MyPureModel; -// type IdealGas = JobackRecord; -// type Binary = MyBinaryModel; -// fn from_records( -// pure_records: Vec>, -// binary_records: Array2, -// ) -> Self { -// Self { -// pure_records, -// binary_records, -// } -// } - -// fn records( -// &self, -// ) -> ( -// &[PureRecord], -// &Array2, -// ) { -// (&self.pure_records, &self.binary_records) -// } -// } - -// #[test] -// fn from_records() { -// let pr_json = r#" -// [ -// { -// "identifier": { -// "cas": "123-4-5" -// }, -// "molarweight": 16.0426, -// "model_record": { -// "a": 0.1 -// } -// }, -// { -// "identifier": { -// "cas": "678-9-1" -// }, -// "molarweight": 32.08412, -// "model_record": { -// "a": 0.2 -// } -// } -// ] -// "#; -// let br_json = r#" -// [ -// { -// "id1": { -// "cas": "123-4-5" -// }, -// "id2": { -// "cas": "678-9-1" -// }, -// "model_record": { -// "b": 12.0 -// } -// } -// ] -// "#; -// let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); -// let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); -// let binary_matrix = MyParameter::binary_matrix_from_records( -// &pure_records, -// &binary_records, -// IdentifierOption::Cas, -// ); -// let p = MyParameter::from_records(pure_records, binary_matrix); - -// assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); -// assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); -// assert_eq!(p.binary_records[[0, 1]].b, 12.0) -// } - -// #[test] -// fn from_records_missing_binary() { -// let pr_json = r#" -// [ -// { -// "identifier": { -// "cas": "123-4-5" -// }, -// "molarweight": 16.0426, -// "model_record": { -// "a": 0.1 -// } -// }, -// { -// "identifier": { -// "cas": "678-9-1" -// }, -// "molarweight": 32.08412, -// "model_record": { -// "a": 0.2 -// } -// } -// ] -// "#; -// let br_json = r#" -// [ -// { -// "id1": { -// "cas": "123-4-5" -// }, -// "id2": { -// "cas": "000-00-0" -// }, -// "model_record": { -// "b": 12.0 -// } -// } -// ] -// "#; -// let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); -// let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); -// let binary_matrix = MyParameter::binary_matrix_from_records( -// &pure_records, -// &binary_records, -// IdentifierOption::Cas, -// ); -// let p = MyParameter::from_records(pure_records, binary_matrix); - -// assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); -// assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); -// assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); -// assert_eq!(p.binary_records[[0, 1]].b, 0.0) -// } - -// #[test] -// fn from_records_correct_binary_order() { -// let pr_json = r#" -// [ -// { -// "identifier": { -// "cas": "000-0-0" -// }, -// "molarweight": 32.08412, -// "model_record": { -// "a": 0.2 -// } -// }, -// { -// "identifier": { -// "cas": "123-4-5" -// }, -// "molarweight": 16.0426, -// "model_record": { -// "a": 0.1 -// } -// }, -// { -// "identifier": { -// "cas": "678-9-1" -// }, -// "molarweight": 32.08412, -// "model_record": { -// "a": 0.2 -// } -// } -// ] -// "#; -// let br_json = r#" -// [ -// { -// "id1": { -// "cas": "123-4-5" -// }, -// "id2": { -// "cas": "678-9-1" -// }, -// "model_record": { -// "b": 12.0 -// } -// } -// ] -// "#; -// let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); -// let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); -// let binary_matrix = MyParameter::binary_matrix_from_records( -// &pure_records, -// &binary_records, -// IdentifierOption::Cas, -// ); -// let p = MyParameter::from_records(pure_records, binary_matrix); - -// assert_eq!(p.pure_records[0].identifier.cas, Some("000-0-0".into())); -// assert_eq!(p.pure_records[1].identifier.cas, Some("123-4-5".into())); -// assert_eq!(p.pure_records[2].identifier.cas, Some("678-9-1".into())); -// assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); -// assert_eq!(p.binary_records[[1, 0]], MyBinaryModel::default()); -// assert_eq!(p.binary_records[[0, 2]], MyBinaryModel::default()); -// assert_eq!(p.binary_records[[2, 0]], MyBinaryModel::default()); -// assert_eq!(p.binary_records[[2, 1]].b, 12.0); -// assert_eq!(p.binary_records[[1, 2]].b, 12.0); -// } -// } +#[cfg(test)] +mod test { + use super::*; + use serde::{Deserialize, Serialize}; + use std::convert::TryFrom; + + #[derive(Debug, Clone, Serialize, Deserialize, Default)] + struct MyPureModel { + a: f64, + } + + #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] + struct MyBinaryModel { + b: f64, + } + + impl TryFrom for MyBinaryModel { + type Error = &'static str; + fn try_from(f: f64) -> Result { + Ok(Self { b: f }) + } + } + + struct MyParameter { + pure_records: Vec>, + binary_records: Array2, + } + + impl Parameter for MyParameter { + type Pure = MyPureModel; + type Binary = MyBinaryModel; + fn from_records( + pure_records: Vec>, + binary_records: Array2, + ) -> Self { + Self { + pure_records, + binary_records, + } + } + + fn records(&self) -> (&[PureRecord], &Array2) { + (&self.pure_records, &self.binary_records) + } + } + + #[test] + fn from_records() { + let pr_json = r#" + [ + { + "identifier": { + "cas": "123-4-5" + }, + "molarweight": 16.0426, + "model_record": { + "a": 0.1 + } + }, + { + "identifier": { + "cas": "678-9-1" + }, + "molarweight": 32.08412, + "model_record": { + "a": 0.2 + } + } + ] + "#; + let br_json = r#" + [ + { + "id1": { + "cas": "123-4-5" + }, + "id2": { + "cas": "678-9-1" + }, + "model_record": { + "b": 12.0 + } + } + ] + "#; + let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); + let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); + let binary_matrix = MyParameter::binary_matrix_from_records( + &pure_records, + &binary_records, + IdentifierOption::Cas, + ); + let p = MyParameter::from_records(pure_records, binary_matrix); + + assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); + assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); + assert_eq!(p.binary_records[[0, 1]].b, 12.0) + } + + #[test] + fn from_records_missing_binary() { + let pr_json = r#" + [ + { + "identifier": { + "cas": "123-4-5" + }, + "molarweight": 16.0426, + "model_record": { + "a": 0.1 + } + }, + { + "identifier": { + "cas": "678-9-1" + }, + "molarweight": 32.08412, + "model_record": { + "a": 0.2 + } + } + ] + "#; + let br_json = r#" + [ + { + "id1": { + "cas": "123-4-5" + }, + "id2": { + "cas": "000-00-0" + }, + "model_record": { + "b": 12.0 + } + } + ] + "#; + let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); + let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); + let binary_matrix = MyParameter::binary_matrix_from_records( + &pure_records, + &binary_records, + IdentifierOption::Cas, + ); + let p = MyParameter::from_records(pure_records, binary_matrix); + + assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); + assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); + assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); + assert_eq!(p.binary_records[[0, 1]].b, 0.0) + } + + #[test] + fn from_records_correct_binary_order() { + let pr_json = r#" + [ + { + "identifier": { + "cas": "000-0-0" + }, + "molarweight": 32.08412, + "model_record": { + "a": 0.2 + } + }, + { + "identifier": { + "cas": "123-4-5" + }, + "molarweight": 16.0426, + "model_record": { + "a": 0.1 + } + }, + { + "identifier": { + "cas": "678-9-1" + }, + "molarweight": 32.08412, + "model_record": { + "a": 0.2 + } + } + ] + "#; + let br_json = r#" + [ + { + "id1": { + "cas": "123-4-5" + }, + "id2": { + "cas": "678-9-1" + }, + "model_record": { + "b": 12.0 + } + } + ] + "#; + let pure_records = serde_json::from_str(pr_json).expect("Unable to parse json."); + let binary_records: Vec<_> = serde_json::from_str(br_json).expect("Unable to parse json."); + let binary_matrix = MyParameter::binary_matrix_from_records( + &pure_records, + &binary_records, + IdentifierOption::Cas, + ); + let p = MyParameter::from_records(pure_records, binary_matrix); + + assert_eq!(p.pure_records[0].identifier.cas, Some("000-0-0".into())); + assert_eq!(p.pure_records[1].identifier.cas, Some("123-4-5".into())); + assert_eq!(p.pure_records[2].identifier.cas, Some("678-9-1".into())); + assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); + assert_eq!(p.binary_records[[1, 0]], MyBinaryModel::default()); + assert_eq!(p.binary_records[[0, 2]], MyBinaryModel::default()); + assert_eq!(p.binary_records[[2, 0]], MyBinaryModel::default()); + assert_eq!(p.binary_records[[2, 1]].b, 12.0); + assert_eq!(p.binary_records[[1, 2]].b, 12.0); + } +} diff --git a/feos-core/src/parameter/model_record.rs b/feos-core/src/parameter/model_record.rs index f516f18df..ee86f61dc 100644 --- a/feos-core/src/parameter/model_record.rs +++ b/feos-core/src/parameter/model_record.rs @@ -123,60 +123,59 @@ where } } -// #[cfg(test)] -// mod test { -// use super::*; -// use crate::joback::JobackRecord; +#[cfg(test)] +mod test { + use super::*; -// #[derive(Serialize, Deserialize, Debug, Default, Clone)] -// struct TestModelRecordSegments { -// a: f64, -// } + #[derive(Serialize, Deserialize, Debug, Default, Clone)] + struct TestModelRecordSegments { + a: f64, + } -// #[test] -// fn deserialize() { -// let r = r#" -// { -// "identifier": { -// "cas": "123-4-5" -// }, -// "molarweight": 16.0426, -// "model_record": { -// "a": 0.1 -// } -// } -// "#; -// let record: PureRecord = -// serde_json::from_str(r).expect("Unable to parse json."); -// assert_eq!(record.identifier.cas, Some("123-4-5".into())) -// } + #[test] + fn deserialize() { + let r = r#" + { + "identifier": { + "cas": "123-4-5" + }, + "molarweight": 16.0426, + "model_record": { + "a": 0.1 + } + } + "#; + let record: PureRecord = + serde_json::from_str(r).expect("Unable to parse json."); + assert_eq!(record.identifier.cas, Some("123-4-5".into())) + } -// #[test] -// fn deserialize_list() { -// let r = r#" -// [ -// { -// "identifier": { -// "cas": "1" -// }, -// "molarweight": 1.0, -// "model_record": { -// "a": 1.0 -// } -// }, -// { -// "identifier": { -// "cas": "2" -// }, -// "molarweight": 2.0, -// "model_record": { -// "a": 2.0 -// } -// } -// ]"#; -// let records: Vec> = -// serde_json::from_str(r).expect("Unable to parse json."); -// assert_eq!(records[0].identifier.cas, Some("1".into())); -// assert_eq!(records[1].identifier.cas, Some("2".into())) -// } -// } + #[test] + fn deserialize_list() { + let r = r#" + [ + { + "identifier": { + "cas": "1" + }, + "molarweight": 1.0, + "model_record": { + "a": 1.0 + } + }, + { + "identifier": { + "cas": "2" + }, + "molarweight": 2.0, + "model_record": { + "a": 2.0 + } + } + ]"#; + let records: Vec> = + serde_json::from_str(r).expect("Unable to parse json."); + assert_eq!(records[0].identifier.cas, Some("1".into())); + assert_eq!(records[1].identifier.cas, Some("2".into())) + } +} diff --git a/feos-core/src/phase_equilibria/bubble_dew.rs b/feos-core/src/phase_equilibria/bubble_dew.rs index e5b499a45..ee9a133db 100644 --- a/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/feos-core/src/phase_equilibria/bubble_dew.rs @@ -4,9 +4,9 @@ use crate::errors::{EosError, EosResult}; use crate::state::{ Contributions, DensityInitialization::{InitialDensity, Liquid, Vapor}, - SolverOptions, State, StateBuilder, TPSpec, Verbosity, + State, StateBuilder, TPSpec, }; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::{norm, LU}; use quantity::si::{SIArray1, SINumber, SIUnit}; diff --git a/feos-core/src/phase_equilibria/mod.rs b/feos-core/src/phase_equilibria/mod.rs index 9b7a76baa..7b302f6ff 100644 --- a/feos-core/src/phase_equilibria/mod.rs +++ b/feos-core/src/phase_equilibria/mod.rs @@ -17,77 +17,6 @@ mod vle_pure; pub use phase_diagram_binary::PhaseDiagramHetero; pub use phase_diagram_pure::PhaseDiagram; -// /// Level of detail in the iteration output. -// #[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] -// #[cfg_attr(feature = "python", pyo3::pyclass)] -// pub enum Verbosity { -// /// Do not print output. -// None, -// /// Print information about the success of failure of the iteration. -// Result, -// /// Print a detailed outpur for every iteration. -// Iter, -// } - -// impl Default for Verbosity { -// fn default() -> Self { -// Self::None -// } -// } - -// /// Options for the various phase equilibria solvers. -// /// -// /// If the values are [None], solver specific default -// /// values are used. -// #[derive(Copy, Clone, Default)] -// pub struct SolverOptions { -// /// Maximum number of iterations. -// pub max_iter: Option, -// /// Tolerance. -// pub tol: Option, -// /// Iteration outpput indicated by the [Verbosity] enum. -// pub verbosity: Verbosity, -// } - -// impl From<(Option, Option, Option)> for SolverOptions { -// fn from(options: (Option, Option, Option)) -> Self { -// Self { -// max_iter: options.0, -// tol: options.1, -// verbosity: options.2.unwrap_or(Verbosity::None), -// } -// } -// } - -// impl SolverOptions { -// pub fn new() -> Self { -// Self::default() -// } - -// pub fn max_iter(mut self, max_iter: usize) -> Self { -// self.max_iter = Some(max_iter); -// self -// } - -// pub fn tol(mut self, tol: f64) -> Self { -// self.tol = Some(tol); -// self -// } - -// pub fn verbosity(mut self, verbosity: Verbosity) -> Self { -// self.verbosity = verbosity; -// self -// } - -// pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { -// ( -// self.max_iter.unwrap_or(max_iter), -// self.tol.unwrap_or(tol), -// self.verbosity, -// ) -// } -// } - /// A thermodynamic equilibrium state. /// /// The struct is parametrized over the number of phases with most features diff --git a/feos-core/src/phase_equilibria/phase_diagram_binary.rs b/feos-core/src/phase_equilibria/phase_diagram_binary.rs index ca53e25e0..d9ae810ce 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_binary.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_binary.rs @@ -1,10 +1,8 @@ use super::{PhaseDiagram, PhaseEquilibrium}; use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{ - Contributions, DensityInitialization, SolverOptions, State, StateBuilder, TPSpec, -}; -use crate::EosUnit; +use crate::state::{Contributions, DensityInitialization, State, StateBuilder, TPSpec}; +use crate::{EosUnit, SolverOptions}; use ndarray::{arr1, arr2, concatenate, s, Array1, Array2, Axis}; use num_dual::linalg::{norm, LU}; use quantity::si::{SIArray1, SINumber, SIUnit}; diff --git a/feos-core/src/phase_equilibria/phase_diagram_pure.rs b/feos-core/src/phase_equilibria/phase_diagram_pure.rs index dfb689505..1a25d8ac9 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_pure.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_pure.rs @@ -1,9 +1,10 @@ use super::PhaseEquilibrium; use crate::equation_of_state::Residual; use crate::errors::EosResult; -use crate::state::{SolverOptions, State, StateVec}; +use crate::state::{State, StateVec}; #[cfg(feature = "rayon")] use crate::EosUnit; +use crate::SolverOptions; #[cfg(feature = "rayon")] use ndarray::{Array1, ArrayView1, Axis}; #[cfg(feature = "rayon")] diff --git a/feos-core/src/phase_equilibria/phase_envelope.rs b/feos-core/src/phase_equilibria/phase_envelope.rs index 2c56122e2..9161eb75e 100644 --- a/feos-core/src/phase_equilibria/phase_envelope.rs +++ b/feos-core/src/phase_equilibria/phase_envelope.rs @@ -1,8 +1,8 @@ use super::{PhaseDiagram, PhaseEquilibrium}; use crate::equation_of_state::Residual; use crate::errors::EosResult; -use crate::state::{SolverOptions, State}; -use crate::Contributions; +use crate::state::{Contributions, State}; +use crate::SolverOptions; use quantity::si::{SIArray1, SINumber}; use std::sync::Arc; diff --git a/feos-core/src/phase_equilibria/stability_analysis.rs b/feos-core/src/phase_equilibria/stability_analysis.rs index fd4055d93..c0df11f98 100644 --- a/feos-core/src/phase_equilibria/stability_analysis.rs +++ b/feos-core/src/phase_equilibria/stability_analysis.rs @@ -1,8 +1,8 @@ use super::PhaseEquilibrium; use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, SolverOptions, State, Verbosity}; -use crate::EosUnit; +use crate::state::{Contributions, DensityInitialization, State}; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::smallest_ev; use num_dual::linalg::LU; diff --git a/feos-core/src/phase_equilibria/tp_flash.rs b/feos-core/src/phase_equilibria/tp_flash.rs index af87455be..41262bfd0 100644 --- a/feos-core/src/phase_equilibria/tp_flash.rs +++ b/feos-core/src/phase_equilibria/tp_flash.rs @@ -1,7 +1,8 @@ use super::PhaseEquilibrium; use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, SolverOptions, State, Verbosity}; +use crate::state::{Contributions, DensityInitialization, State}; +use crate::{SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::norm; use quantity::si::{SIArray1, SINumber}; diff --git a/feos-core/src/phase_equilibria/vle_pure.rs b/feos-core/src/phase_equilibria/vle_pure.rs index 7c3f30fc4..eb3cd2ce5 100644 --- a/feos-core/src/phase_equilibria/vle_pure.rs +++ b/feos-core/src/phase_equilibria/vle_pure.rs @@ -1,8 +1,8 @@ use super::PhaseEquilibrium; use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, SolverOptions, State, TPSpec, Verbosity}; -use crate::EosUnit; +use crate::state::{Contributions, DensityInitialization, State, TPSpec}; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::{arr1, Array1}; use quantity::si::{SINumber, SIUnit}; use std::convert::TryFrom; diff --git a/feos-core/src/python/state.rs b/feos-core/src/python/state.rs index b3d67af9f..59644acdd 100644 --- a/feos-core/src/python/state.rs +++ b/feos-core/src/python/state.rs @@ -263,58 +263,58 @@ macro_rules! impl_state { Ok((PyState(state1), PyState(state2))) } - // /// Performs a stability analysis and returns a list of stable - // /// candidate states. - // /// - // /// Parameters - // /// ---------- - // /// max_iter : int, optional - // /// The maximum number of iterations. - // /// tol: float, optional - // /// The solution tolerance. - // /// verbosity : Verbosity, optional - // /// The verbosity. - // /// - // /// Returns - // /// ------- - // /// State - // #[pyo3(text_signature = "(max_iter=None, tol=None, verbosity=None)")] - // fn stability_analysis(&self, - // max_iter: Option, - // tol: Option, - // verbosity: Option, - // ) -> PyResult> { - // Ok(self - // .0 - // .stability_analysis((max_iter, tol, verbosity).into())? - // .into_iter() - // .map(Self) - // .collect()) - // } - - // /// Performs a stability analysis and returns whether the state - // /// is stable - // /// - // /// Parameters - // /// ---------- - // /// max_iter : int, optional - // /// The maximum number of iterations. - // /// tol: float, optional - // /// The solution tolerance. - // /// verbosity : Verbosity, optional - // /// The verbosity. - // /// - // /// Returns - // /// ------- - // /// bool - // #[pyo3(text_signature = "(max_iter=None, tol=None, verbosity=None)")] - // fn is_stable(&self, - // max_iter: Option, - // tol: Option, - // verbosity: Option, - // ) -> PyResult { - // Ok(self.0.is_stable((max_iter, tol, verbosity).into())?) - // } + /// Performs a stability analysis and returns a list of stable + /// candidate states. + /// + /// Parameters + /// ---------- + /// max_iter : int, optional + /// The maximum number of iterations. + /// tol: float, optional + /// The solution tolerance. + /// verbosity : Verbosity, optional + /// The verbosity. + /// + /// Returns + /// ------- + /// State + #[pyo3(text_signature = "(max_iter=None, tol=None, verbosity=None)")] + fn stability_analysis(&self, + max_iter: Option, + tol: Option, + verbosity: Option, + ) -> PyResult> { + Ok(self + .0 + .stability_analysis((max_iter, tol, verbosity).into())? + .into_iter() + .map(Self) + .collect()) + } + + /// Performs a stability analysis and returns whether the state + /// is stable + /// + /// Parameters + /// ---------- + /// max_iter : int, optional + /// The maximum number of iterations. + /// tol: float, optional + /// The solution tolerance. + /// verbosity : Verbosity, optional + /// The verbosity. + /// + /// Returns + /// ------- + /// bool + #[pyo3(text_signature = "(max_iter=None, tol=None, verbosity=None)")] + fn is_stable(&self, + max_iter: Option, + tol: Option, + verbosity: Option, + ) -> PyResult { + Ok(self.0.is_stable((max_iter, tol, verbosity).into())?) + } /// Return pressure. /// diff --git a/feos-core/src/state/critical_point.rs b/feos-core/src/state/critical_point.rs index e314861bb..db8e4de99 100644 --- a/feos-core/src/state/critical_point.rs +++ b/feos-core/src/state/critical_point.rs @@ -1,7 +1,7 @@ -use super::{SolverOptions, State, StateHD, TPSpec, Verbosity}; +use super::{DensityInitialization, State, StateHD, TPSpec}; use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::{DensityInitialization, EosUnit}; +use crate::{EosUnit, SolverOptions, Verbosity}; use nalgebra::{DMatrix, DVector, SVector, SymmetricEigen}; use ndarray::{arr1, Array1}; use num_dual::{ diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index 96d1f6385..893952eb1 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -12,7 +12,6 @@ use crate::errors::{EosError, EosResult}; use crate::EosUnit; use cache::Cache; use ndarray::prelude::*; -// use num_dual::linalg::{norm, LU}; use num_dual::*; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::convert::TryFrom; @@ -27,77 +26,6 @@ mod statevec; pub use builder::StateBuilder; pub use statevec::StateVec; -/// Level of detail in the iteration output. -#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] -#[cfg_attr(feature = "python", pyo3::pyclass)] -pub enum Verbosity { - /// Do not print output. - None, - /// Print information about the success of failure of the iteration. - Result, - /// Print a detailed outpur for every iteration. - Iter, -} - -impl Default for Verbosity { - fn default() -> Self { - Self::None - } -} - -/// Options for the various phase equilibria solvers. -/// -/// If the values are [None], solver specific default -/// values are used. -#[derive(Copy, Clone, Default)] -pub struct SolverOptions { - /// Maximum number of iterations. - pub max_iter: Option, - /// Tolerance. - pub tol: Option, - /// Iteration outpput indicated by the [Verbosity] enum. - pub verbosity: Verbosity, -} - -impl From<(Option, Option, Option)> for SolverOptions { - fn from(options: (Option, Option, Option)) -> Self { - Self { - max_iter: options.0, - tol: options.1, - verbosity: options.2.unwrap_or(Verbosity::None), - } - } -} - -impl SolverOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn max_iter(mut self, max_iter: usize) -> Self { - self.max_iter = Some(max_iter); - self - } - - pub fn tol(mut self, tol: f64) -> Self { - self.tol = Some(tol); - self - } - - pub fn verbosity(mut self, verbosity: Verbosity) -> Self { - self.verbosity = verbosity; - self - } - - pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { - ( - self.max_iter.unwrap_or(max_iter), - self.tol.unwrap_or(tol), - self.verbosity, - ) - } -} - /// Possible contributions that can be computed. #[derive(Clone, Copy)] #[cfg_attr(feature = "python", pyo3::pyclass)] diff --git a/feos-derive/src/ideal_gas.rs b/feos-derive/src/ideal_gas.rs index 617ad0af3..69f8d9b63 100644 --- a/feos-derive/src/ideal_gas.rs +++ b/feos-derive/src/ideal_gas.rs @@ -28,18 +28,6 @@ fn impl_ideal_gas( } } }); - // let de_broglie_wavelength = variants.iter().map(|v| { - // let name = &v.ident; - // if name == "NoModel" { - // quote! { - // Self::#name(_) => panic!("No ideal gas model initialized!") - // } - // } else { - // quote! { - // Self::#name(ideal_gas) => ideal_gas.de_broglie_wavelength(temperature) - // } - // } - // }); quote! { impl IdealGas for IdealGasModel { @@ -48,11 +36,6 @@ fn impl_ideal_gas( #(#ideal_gas_model,)* } } - // fn de_broglie_wavelength + Copy>(&self, temperature: D) -> Array1 { - // match self { - // #(#de_broglie_wavelength,)* - // } - // } } } } diff --git a/feos-dft/src/profile/mod.rs b/feos-dft/src/profile/mod.rs index e51c8323d..9886923f2 100644 --- a/feos-dft/src/profile/mod.rs +++ b/feos-dft/src/profile/mod.rs @@ -288,11 +288,6 @@ where pub fn total_moles(&self) -> SINumber { self.moles().sum() } - - // /// Return the chemical potential of the system - // pub fn chemical_potential(&self) -> SIArray1 { - // self.bulk.chemical_potential(Contributions::Total) - // } } impl Clone for DFTProfile { diff --git a/feos-dft/src/python/adsorption/mod.rs b/feos-dft/src/python/adsorption/mod.rs index 03482549c..688b9b0d3 100644 --- a/feos-dft/src/python/adsorption/mod.rs +++ b/feos-dft/src/python/adsorption/mod.rs @@ -229,11 +229,6 @@ macro_rules! impl_adsorption_isotherm { self.0.pressure().into() } - // #[getter] - // fn get_molar_gibbs_energy(&self) -> PySIArray1 { - // self.0.molar_gibbs_energy().into() - // } - #[getter] fn get_adsorption(&self) -> PySIArray2 { self.0.adsorption().into() diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index b58663242..323563377 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -458,20 +458,6 @@ mod tests { } } - // #[test] - // fn speed_of_sound() { - // let e = Arc::new(PcSaft::new(propane_parameters())); - // let t = 300.0 * KELVIN; - // let p = BAR; - // let m = arr1(&[1.0]) * MOL; - // let s = State::new_npt(&e, t, p, &m, DensityInitialization::None).unwrap(); - // assert_relative_eq!( - // s.speed_of_sound(), - // 245.00185709137546 * METER / SECOND, - // epsilon = 1e-4 - // ) - // } - #[test] fn mix_single() { let e1 = Arc::new(PcSaft::new(propane_parameters())); diff --git a/src/pets/eos/mod.rs b/src/pets/eos/mod.rs index 4f85e90c0..77b126692 100644 --- a/src/pets/eos/mod.rs +++ b/src/pets/eos/mod.rs @@ -85,217 +85,219 @@ impl MolarWeight for Pets { } } -// fn omega11(t: f64) -> f64 { -// 1.06036 * t.powf(-0.15610) -// + 0.19300 * (-0.47635 * t).exp() -// + 1.03587 * (-1.52996 * t).exp() -// + 1.76474 * (-3.89411 * t).exp() -// } +fn omega11(t: f64) -> f64 { + 1.06036 * t.powf(-0.15610) + + 0.19300 * (-0.47635 * t).exp() + + 1.03587 * (-1.52996 * t).exp() + + 1.76474 * (-3.89411 * t).exp() +} -// fn omega22(t: f64) -> f64 { -// 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() -// - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() -// } +fn omega22(t: f64) -> f64 { + 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() + - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() +} -// impl EntropyScaling for Pets { -// fn viscosity_reference( -// &self, -// temperature: SINumber, -// _: SINumber, -// moles: &SIArray1, -// ) -> EosResult { -// let x = moles.to_reduced(moles.sum())?; -// let p = &self.parameters; -// let mw = &p.molarweight; -// let ce: Array1 = (0..self.components()) -// .map(|i| { -// let tr = (temperature / p.epsilon_k[i] / KELVIN) -// .into_value() -// .unwrap(); -// 5.0 / 16.0 -// * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) -// .sqrt() -// .unwrap() -// / omega22(tr) -// / (p.sigma[i] * ANGSTROM).powi(2) -// }) -// .collect(); -// let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; -// for i in 0..self.components() { -// let denom: f64 = (0..self.components()) -// .map(|j| { -// x[j] * (1.0 -// + (ce[i] / ce[j]).into_value().unwrap().sqrt() -// * (mw[j] / mw[i]).powf(1.0 / 4.0)) -// .powi(2) -// / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() -// }) -// .sum(); -// ce_mix += ce[i] * x[i] / denom -// } -// Ok(ce_mix) -// } +impl EntropyScaling for Pets { + fn viscosity_reference( + &self, + temperature: SINumber, + _: SINumber, + moles: &SIArray1, + ) -> EosResult { + let x = moles.to_reduced(moles.sum())?; + let p = &self.parameters; + let mw = &p.molarweight; + let ce: Array1 = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN) + .into_value() + .unwrap(); + 5.0 / 16.0 + * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) + .sqrt() + .unwrap() + / omega22(tr) + / (p.sigma[i] * ANGSTROM).powi(2) + }) + .collect(); + let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; + for i in 0..self.components() { + let denom: f64 = (0..self.components()) + .map(|j| { + x[j] * (1.0 + + (ce[i] / ce[j]).into_value().unwrap().sqrt() + * (mw[j] / mw[i]).powf(1.0 / 4.0)) + .powi(2) + / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() + }) + .sum(); + ce_mix += ce[i] * x[i] / denom + } + Ok(ce_mix) + } -// fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { -// let coefficients = self -// .parameters -// .viscosity -// .as_ref() -// .expect("Missing viscosity coefficients."); -// let a: f64 = (&coefficients.row(0) * x).sum(); -// let b: f64 = (&coefficients.row(1) * x).sum(); -// let c: f64 = (&coefficients.row(2) * x).sum(); -// let d: f64 = (&coefficients.row(3) * x).sum(); -// Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) -// } + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + let coefficients = self + .parameters + .viscosity + .as_ref() + .expect("Missing viscosity coefficients."); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * x).sum(); + let c: f64 = (&coefficients.row(2) * x).sum(); + let d: f64 = (&coefficients.row(3) * x).sum(); + Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) + } -// fn diffusion_reference( -// &self, -// temperature: SINumber, -// volume: SINumber, -// moles: &SIArray1, -// ) -> EosResult { -// if self.components() != 1 { -// return Err(EosError::IncompatibleComponents(self.components(), 1)); -// } -// let p = &self.parameters; -// let density = moles.sum() / volume; -// let res: Array1 = (0..self.components()) -// .map(|i| { -// let tr = (temperature / p.epsilon_k[i] / KELVIN) -// .into_value() -// .unwrap(); -// 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) -// * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) -// .sqrt() -// .unwrap() -// }) -// .collect(); -// Ok(res[0]) -// } + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let p = &self.parameters; + let density = moles.sum() / volume; + let res: Array1 = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN) + .into_value() + .unwrap(); + 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) + * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) + .sqrt() + .unwrap() + }) + .collect(); + Ok(res[0]) + } -// fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { -// if self.components() != 1 { -// return Err(EosError::IncompatibleComponents(self.components(), 1)); -// } -// let coefficients = self -// .parameters -// .diffusion -// .as_ref() -// .expect("Missing diffusion coefficients."); -// let a: f64 = (&coefficients.row(0) * x).sum(); -// let b: f64 = (&coefficients.row(1) * x).sum(); -// let c: f64 = (&coefficients.row(2) * x).sum(); -// let d: f64 = (&coefficients.row(3) * x).sum(); -// let e: f64 = (&coefficients.row(4) * x).sum(); -// Ok(a + b * s_res -// - c * (1.0 - s_res.exp()) * s_res.powi(2) -// - d * s_res.powi(4) -// - e * s_res.powi(8)) -// } + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let coefficients = self + .parameters + .diffusion + .as_ref() + .expect("Missing diffusion coefficients."); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * x).sum(); + let c: f64 = (&coefficients.row(2) * x).sum(); + let d: f64 = (&coefficients.row(3) * x).sum(); + let e: f64 = (&coefficients.row(4) * x).sum(); + Ok(a + b * s_res + - c * (1.0 - s_res.exp()) * s_res.powi(2) + - d * s_res.powi(4) + - e * s_res.powi(8)) + } -// // fn thermal_conductivity_reference( -// // &self, -// // state: &State, -// // ) -> EosResult { -// // if self.components() != 1 { -// // return Err(EosError::IncompatibleComponents(self.components(), 1)); -// // } -// // let p = &self.parameters; -// // let res: Array1 = (0..self.components()) -// // .map(|i| { -// // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) -// // .into_value() -// // .unwrap(); -// // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); -// // let s_res_cp_reduced = cp -// // .entropy(Contributions::Residual) -// // .to_reduced(SIUnit::reference_entropy()) -// // .unwrap(); -// // let s_res_reduced = cp -// // .entropy(Contributions::Residual) -// // .to_reduced(SIUnit::reference_entropy()) -// // .unwrap(); -// // let ref_ce = 0.083235 -// // * ((state.temperature / KELVIN).into_value().unwrap() -// // / (p.molarweight[0])) -// // .sqrt() -// // / p.sigma[0] -// // / p.sigma[0] -// // / omega22(tr); -// // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); -// // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) -// // * (p.sigma[i].powi(3) * p.epsilon_k[0]) -// // / 100000.0; -// // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN -// // }) -// // .collect(); -// // Ok(res[0]) -// // } + // fn thermal_conductivity_reference( + // &self, + // state: &State, + // ) -> EosResult { + // if self.components() != 1 { + // return Err(EosError::IncompatibleComponents(self.components(), 1)); + // } + // let p = &self.parameters; + // let res: Array1 = (0..self.components()) + // .map(|i| { + // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) + // .into_value() + // .unwrap(); + // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); + // let s_res_cp_reduced = cp + // .entropy(Contributions::Residual) + // .to_reduced(SIUnit::reference_entropy()) + // .unwrap(); + // let s_res_reduced = cp + // .entropy(Contributions::Residual) + // .to_reduced(SIUnit::reference_entropy()) + // .unwrap(); + // let ref_ce = 0.083235 + // * ((state.temperature / KELVIN).into_value().unwrap() + // / (p.molarweight[0])) + // .sqrt() + // / p.sigma[0] + // / p.sigma[0] + // / omega22(tr); + // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); + // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) + // * (p.sigma[i].powi(3) * p.epsilon_k[0]) + // / 100000.0; + // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN + // }) + // .collect(); + // Ok(res[0]) + // } -// // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 -// fn thermal_conductivity_reference( -// &self, -// temperature: SINumber, -// volume: SINumber, -// moles: &SIArray1, -// ) -> EosResult { -// if self.components() != 1 { -// return Err(EosError::IncompatibleComponents(self.components(), 1)); -// } -// let p = &self.parameters; -// let state = State::new_nvt( -// &Arc::new(Self::new(self.parameters.clone())), -// temperature, -// volume, -// moles, -// )?; -// let res: Array1 = (0..self.components()) -// .map(|i| { -// let tr = (temperature / p.epsilon_k[i] / KELVIN) -// .into_value() -// .unwrap(); -// let ce = 83.235 -// * f64::powf(10.0, -1.5) -// * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() -// / (p.sigma[0] * p.sigma[0]) -// / omega22(tr); -// ce * WATT / METER / KELVIN -// + state.density -// * self -// .diffusion_reference(temperature, volume, moles) -// .unwrap() -// * self -// .diffusion_correlation( -// state -// .residual_entropy() -// .to_reduced(SIUnit::reference_molar_entropy() * state.total_moles) -// .unwrap(), -// &state.molefracs, -// ) -// .unwrap() -// * (state.c_v(Contributions::Total) - 1.5 * RGAS) -// }) -// .collect(); -// Ok(res[0]) -// } + // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let p = &self.parameters; + let state = State::new_nvt( + &Arc::new(Self::new(self.parameters.clone())), + temperature, + volume, + moles, + )?; + let res: Array1 = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN) + .into_value() + .unwrap(); + let ce = 83.235 + * f64::powf(10.0, -1.5) + * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() + / (p.sigma[0] * p.sigma[0]) + / omega22(tr); + ce * WATT / METER / KELVIN + + state.density + * self + .diffusion_reference(temperature, volume, moles) + .unwrap() + * self + .diffusion_correlation( + state + .residual_entropy() + .to_reduced( + SIUnit::reference_molar_entropy() * state.total_moles, + ) + .unwrap(), + &state.molefracs, + ) + .unwrap() + * (state.c_v(Contributions::Total) - 1.5 * RGAS) + }) + .collect(); + Ok(res[0]) + } -// fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { -// if self.components() != 1 { -// return Err(EosError::IncompatibleComponents(self.components(), 1)); -// } -// let coefficients = self -// .parameters -// .thermal_conductivity -// .as_ref() -// .expect("Missing thermal conductivity coefficients"); -// let a: f64 = (&coefficients.row(0) * x).sum(); -// let b: f64 = (&coefficients.row(1) * x).sum(); -// let c: f64 = (&coefficients.row(2) * x).sum(); -// let d: f64 = (&coefficients.row(3) * x).sum(); -// Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) -// } -// } + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let coefficients = self + .parameters + .thermal_conductivity + .as_ref() + .expect("Missing thermal conductivity coefficients"); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * x).sum(); + let c: f64 = (&coefficients.row(2) * x).sum(); + let d: f64 = (&coefficients.row(3) * x).sum(); + Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) + } +} #[cfg(test)] mod tests { diff --git a/src/python/eos.rs b/src/python/eos.rs index 39f57e94d..0013ee736 100644 --- a/src/python/eos.rs +++ b/src/python/eos.rs @@ -346,8 +346,8 @@ pub fn eos(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - // m.add_class::()?; - // m.add_class::()?; + m.add_class::()?; + m.add_class::()?; #[cfg(feature = "estimator")] m.add_wrapped(wrap_pymodule!(estimator_eos))?; diff --git a/tests/pcsaft/properties.rs b/tests/pcsaft/properties.rs index a9e098128..70b527065 100644 --- a/tests/pcsaft/properties.rs +++ b/tests/pcsaft/properties.rs @@ -36,7 +36,6 @@ fn test_dln_phi_dp() -> Result<(), Box> { let ln_phi_h = sh.ln_phi()[0]; let dln_phi_dp = s.dln_phi_dp().get(0); let dln_phi_dp_h = (ln_phi_h - ln_phi) / h; - println!("{}", s.partial_molar_volume()); assert_relative_eq!(dln_phi_dp, dln_phi_dp_h, max_relative = 1e-6); Ok(()) } From dc02390b34842e6a38499348c031e28650827ad1 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 9 Jun 2023 15:30:05 +0200 Subject: [PATCH 36/47] Cargo.toml once again --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0f4525df9..ff44725de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ inherits = "release" lto = true [features] -default = ["pcsaft"] +default = [] dft = ["feos-dft", "petgraph"] estimator = [] association = [] From 46f7403bd3677db37ed4dcba5312f2ef4f773b17 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 9 Jun 2023 15:36:21 +0200 Subject: [PATCH 37/47] revert changes to pets --- src/pets/eos/mod.rs | 410 ++++++++++++++++++++++---------------------- 1 file changed, 204 insertions(+), 206 deletions(-) diff --git a/src/pets/eos/mod.rs b/src/pets/eos/mod.rs index 77b126692..4f806139b 100644 --- a/src/pets/eos/mod.rs +++ b/src/pets/eos/mod.rs @@ -48,7 +48,7 @@ impl Pets { }), ]; Self { - parameters: parameters.clone(), + parameters, options, contributions, } @@ -85,219 +85,217 @@ impl MolarWeight for Pets { } } -fn omega11(t: f64) -> f64 { - 1.06036 * t.powf(-0.15610) - + 0.19300 * (-0.47635 * t).exp() - + 1.03587 * (-1.52996 * t).exp() - + 1.76474 * (-3.89411 * t).exp() -} +// fn omega11(t: f64) -> f64 { +// 1.06036 * t.powf(-0.15610) +// + 0.19300 * (-0.47635 * t).exp() +// + 1.03587 * (-1.52996 * t).exp() +// + 1.76474 * (-3.89411 * t).exp() +// } -fn omega22(t: f64) -> f64 { - 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() - - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() -} +// fn omega22(t: f64) -> f64 { +// 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() +// - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() +// } -impl EntropyScaling for Pets { - fn viscosity_reference( - &self, - temperature: SINumber, - _: SINumber, - moles: &SIArray1, - ) -> EosResult { - let x = moles.to_reduced(moles.sum())?; - let p = &self.parameters; - let mw = &p.molarweight; - let ce: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 5.0 / 16.0 - * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) - .sqrt() - .unwrap() - / omega22(tr) - / (p.sigma[i] * ANGSTROM).powi(2) - }) - .collect(); - let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; - for i in 0..self.components() { - let denom: f64 = (0..self.components()) - .map(|j| { - x[j] * (1.0 - + (ce[i] / ce[j]).into_value().unwrap().sqrt() - * (mw[j] / mw[i]).powf(1.0 / 4.0)) - .powi(2) - / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() - }) - .sum(); - ce_mix += ce[i] * x[i] / denom - } - Ok(ce_mix) - } +// impl EntropyScaling for Pets { +// fn viscosity_reference( +// &self, +// temperature: SINumber, +// _: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// let x = moles.to_reduced(moles.sum())?; +// let p = &self.parameters; +// let mw = &p.molarweight; +// let ce: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 5.0 / 16.0 +// * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) +// .sqrt() +// .unwrap() +// / omega22(tr) +// / (p.sigma[i] * ANGSTROM).powi(2) +// }) +// .collect(); +// let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; +// for i in 0..self.components() { +// let denom: f64 = (0..self.components()) +// .map(|j| { +// x[j] * (1.0 +// + (ce[i] / ce[j]).into_value().unwrap().sqrt() +// * (mw[j] / mw[i]).powf(1.0 / 4.0)) +// .powi(2) +// / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() +// }) +// .sum(); +// ce_mix += ce[i] * x[i] / denom +// } +// Ok(ce_mix) +// } - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - let coefficients = self - .parameters - .viscosity - .as_ref() - .expect("Missing viscosity coefficients."); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) - } +// fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// let coefficients = self +// .parameters +// .viscosity +// .as_ref() +// .expect("Missing viscosity coefficients."); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) +// } - fn diffusion_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let density = moles.sum() / volume; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) - * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) - .sqrt() - .unwrap() - }) - .collect(); - Ok(res[0]) - } +// fn diffusion_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let density = moles.sum() / volume; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) +// * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) +// .sqrt() +// .unwrap() +// }) +// .collect(); +// Ok(res[0]) +// } - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .diffusion - .as_ref() - .expect("Missing diffusion coefficients."); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - let e: f64 = (&coefficients.row(4) * x).sum(); - Ok(a + b * s_res - - c * (1.0 - s_res.exp()) * s_res.powi(2) - - d * s_res.powi(4) - - e * s_res.powi(8)) - } +// fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .diffusion +// .as_ref() +// .expect("Missing diffusion coefficients."); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// let e: f64 = (&coefficients.row(4) * x).sum(); +// Ok(a + b * s_res +// - c * (1.0 - s_res.exp()) * s_res.powi(2) +// - d * s_res.powi(4) +// - e * s_res.powi(8)) +// } - // fn thermal_conductivity_reference( - // &self, - // state: &State, - // ) -> EosResult { - // if self.components() != 1 { - // return Err(EosError::IncompatibleComponents(self.components(), 1)); - // } - // let p = &self.parameters; - // let res: Array1 = (0..self.components()) - // .map(|i| { - // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) - // .into_value() - // .unwrap(); - // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); - // let s_res_cp_reduced = cp - // .entropy(Contributions::Residual) - // .to_reduced(SIUnit::reference_entropy()) - // .unwrap(); - // let s_res_reduced = cp - // .entropy(Contributions::Residual) - // .to_reduced(SIUnit::reference_entropy()) - // .unwrap(); - // let ref_ce = 0.083235 - // * ((state.temperature / KELVIN).into_value().unwrap() - // / (p.molarweight[0])) - // .sqrt() - // / p.sigma[0] - // / p.sigma[0] - // / omega22(tr); - // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); - // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) - // * (p.sigma[i].powi(3) * p.epsilon_k[0]) - // / 100000.0; - // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN - // }) - // .collect(); - // Ok(res[0]) - // } +// // fn thermal_conductivity_reference( +// // &self, +// // state: &State, +// // ) -> EosResult { +// // if self.components() != 1 { +// // return Err(EosError::IncompatibleComponents(self.components(), 1)); +// // } +// // let p = &self.parameters; +// // let res: Array1 = (0..self.components()) +// // .map(|i| { +// // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) +// // .into_value() +// // .unwrap(); +// // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); +// // let s_res_cp_reduced = cp +// // .entropy(Contributions::Residual) +// // .to_reduced(SIUnit::reference_entropy()) +// // .unwrap(); +// // let s_res_reduced = cp +// // .entropy(Contributions::Residual) +// // .to_reduced(SIUnit::reference_entropy()) +// // .unwrap(); +// // let ref_ce = 0.083235 +// // * ((state.temperature / KELVIN).into_value().unwrap() +// // / (p.molarweight[0])) +// // .sqrt() +// // / p.sigma[0] +// // / p.sigma[0] +// // / omega22(tr); +// // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); +// // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) +// // * (p.sigma[i].powi(3) * p.epsilon_k[0]) +// // / 100000.0; +// // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN +// // }) +// // .collect(); +// // Ok(res[0]) +// // } - // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 - fn thermal_conductivity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let state = State::new_nvt( - &Arc::new(Self::new(self.parameters.clone())), - temperature, - volume, - moles, - )?; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - let ce = 83.235 - * f64::powf(10.0, -1.5) - * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() - / (p.sigma[0] * p.sigma[0]) - / omega22(tr); - ce * WATT / METER / KELVIN - + state.density - * self - .diffusion_reference(temperature, volume, moles) - .unwrap() - * self - .diffusion_correlation( - state - .residual_entropy() - .to_reduced( - SIUnit::reference_molar_entropy() * state.total_moles, - ) - .unwrap(), - &state.molefracs, - ) - .unwrap() - * (state.c_v(Contributions::Total) - 1.5 * RGAS) - }) - .collect(); - Ok(res[0]) - } +// // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 +// fn thermal_conductivity_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let state = State::new_nvt( +// &Arc::new(Self::new(self.parameters.clone())), +// temperature, +// volume, +// moles, +// )?; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// let ce = 83.235 +// * f64::powf(10.0, -1.5) +// * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() +// / (p.sigma[0] * p.sigma[0]) +// / omega22(tr); +// ce * WATT / METER / KELVIN +// + state.density +// * self +// .diffusion_reference(temperature, volume, moles) +// .unwrap() +// * self +// .diffusion_correlation( +// state +// .residual_entropy() +// .to_reduced(SIUnit::reference_molar_entropy() * state.total_moles) +// .unwrap(), +// &state.molefracs, +// ) +// .unwrap() +// * (state.c_v(Contributions::Total) - 1.5 * RGAS) +// }) +// .collect(); +// Ok(res[0]) +// } - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .thermal_conductivity - .as_ref() - .expect("Missing thermal conductivity coefficients"); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) - } -} +// fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .thermal_conductivity +// .as_ref() +// .expect("Missing thermal conductivity coefficients"); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) +// } +// } #[cfg(test)] mod tests { From 003a2142726939c856078505190b49bc30aa9ad0 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Thu, 15 Jun 2023 15:30:43 +0200 Subject: [PATCH 38/47] Proper parameter treatment for Joback, renamed function in deBroglieWavelength trait, formatting --- feos-core/src/equation_of_state/ideal_gas.rs | 13 +- feos-core/src/equation_of_state/mod.rs | 6 +- feos-core/src/joback.rs | 189 +++++++++++++++---- feos-core/src/lib.rs | 12 +- feos-core/src/python/cubic.rs | 5 +- feos-core/src/python/joback.rs | 54 +++++- feos-core/src/python/user_defined.rs | 26 +-- feos-dft/src/profile/properties.rs | 5 +- src/pets/parameters.rs | 7 +- src/python/eos.rs | 34 ++-- src/python/ideal_gas.rs | 5 +- src/python/mod.rs | 4 +- 12 files changed, 257 insertions(+), 103 deletions(-) diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs index c2c09062d..12a2653be 100644 --- a/feos-core/src/equation_of_state/ideal_gas.rs +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -10,7 +10,7 @@ pub trait IdealGas: Components + Sync + Send { // Return a reference to the implementation of the de Broglie wavelength. fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength; - /// Evaluate the ideal gas contribution for a given state. + /// Evaluate the ideal gas Helmholtz energy contribution for a given state. /// /// In some cases it could be advantageous to overwrite this /// implementation instead of implementing the de Broglie @@ -19,10 +19,8 @@ pub trait IdealGas: Components + Sync + Send { where for<'a> dyn DeBroglieWavelength + 'a: DeBroglieWavelengthDual, { - let lambda = self - .ideal_gas_model() - .de_broglie_wavelength(state.temperature); - ((lambda + let ln_lambda3 = self.ideal_gas_model().ln_lambda3(state.temperature); + ((ln_lambda3 + state.partial_density.mapv(|x| { if x.re() == 0.0 { D::from(0.0) @@ -36,14 +34,15 @@ pub trait IdealGas: Components + Sync + Send { } /// Implementation of an ideal gas model in terms of the -/// thermal de Broglie wavelength. +/// logarithm of the cubic thermal de Broglie wavelength +/// in units ln(A³). /// /// This trait needs to be implemented generically or for /// the specific types in the supertraits of [DeBroglieWavelength] /// so that the implementor can be used as an ideal gas /// contribution in the equation of state. pub trait DeBroglieWavelengthDual> { - fn de_broglie_wavelength(&self, temperature: D) -> Array1; + fn ln_lambda3(&self, temperature: D) -> Array1; } /// Object safe version of the [DeBroglieWavelengthDual] trait. diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs index a695d08be..bb7b237cb 100644 --- a/feos-core/src/equation_of_state/mod.rs +++ b/feos-core/src/equation_of_state/mod.rs @@ -50,7 +50,11 @@ impl EquationOfState { impl Components for EquationOfState { fn components(&self) -> usize { - assert_eq!(self.residual.components(), self.ideal_gas.components()); + assert_eq!( + self.residual.components(), + self.ideal_gas.components(), + "residual and ideal gas model differ in the number of components" + ); self.residual.components() } diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index 73b159543..d1a6bdb85 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -1,17 +1,16 @@ //! Implementation of the ideal gas heat capacity (de Broglie wavelength) //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). -use crate::equation_of_state::{ - Components, DeBroglieWavelength, DeBroglieWavelengthDual, Residual, -}; +use crate::equation_of_state::{Components, DeBroglieWavelength, DeBroglieWavelengthDual}; use crate::parameter::*; use crate::{EosResult, EosUnit, IdealGas}; use conv::ValueInto; -use ndarray::Array1; +use ndarray::{Array, Array1, Array2}; use num_dual::*; use quantity::si::{SINumber, SIUnit}; use serde::{Deserialize, Serialize}; use std::fmt; +use std::sync::Arc; /// Coefficients used in the Joback model. /// @@ -65,40 +64,135 @@ impl> FromSegments for JobackRecord { } } +/// Parameters for one or more components for the Joback and Reid model. +pub struct JobackParameters { + a: Array1, + b: Array1, + c: Array1, + d: Array1, + e: Array1, + pure_records: Vec>, + binary_records: Array2, +} + +impl Parameter for JobackParameters { + type Pure = JobackRecord; + type Binary = JobackBinaryRecord; + + fn from_records( + pure_records: Vec>, + _binary_records: Array2, + ) -> Self { + let n = pure_records.len(); + + let binary_records = Array::from_elem((n, n), JobackBinaryRecord); + let mut a = Array::zeros(n); + let mut b = Array::zeros(n); + let mut c = Array::zeros(n); + let mut d = Array::zeros(n); + let mut e = Array::zeros(n); + + for (i, record) in pure_records.iter().enumerate() { + let r = &record.model_record; + a[i] = r.a; + b[i] = r.b; + c[i] = r.c; + d[i] = r.d; + e[i] = r.e; + } + + Self { + a, + b, + c, + d, + e, + pure_records, + binary_records, + } + } + + fn records(&self) -> (&[PureRecord], &Array2) { + (&self.pure_records, &self.binary_records) + } +} + +/// Dummy implementation to satisfy traits for parameter handling. +/// Not intended to be used. +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct JobackBinaryRecord; + +impl From for JobackBinaryRecord { + fn from(_: f64) -> Self { + Self + } +} + +impl From for f64 { + fn from(_: JobackBinaryRecord) -> Self { + 0.0 // nasty hack - panic crashes Ipython kernel, actual value is never used + } +} + +impl> FromSegmentsBinary for JobackBinaryRecord { + fn from_segments_binary(_segments: &[(Self, T, T)]) -> Result { + Err(ParameterError::IncompatibleParameters( + "No binary interaction parameters implemented for Joback".to_string(), + )) + } +} + +impl std::fmt::Display for JobackBinaryRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + /// The ideal gas contribution according to /// [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). +/// +/// The (cubic) de Broglie wavelength is calculated by integrating +/// the heat capacity with the following reference values: +/// +/// - T = 289.15 K +/// - p = 1e5 Pa +/// - V = 1e-30 A³ pub struct Joback { - pub records: Vec, + pub parameters: Arc, } impl Joback { /// Creates a new Joback contribution. - pub fn new(records: Vec) -> Self { - Self { records } + pub fn new(parameters: Arc) -> Self { + Self { parameters } } /// Directly calculates the ideal gas heat capacity from the Joback model. pub fn c_p(&self, temperature: SINumber, molefracs: &Array1) -> EosResult { let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let mut c_p = 0.0; - for (j, &x) in self.records.iter().zip(molefracs.iter()) { - c_p += x * (j.a + j.b * t + j.c * t.powi(2) + j.d * t.powi(3) + j.e * t.powi(4)); - } + let p = &self.parameters; + let c_p = (molefracs + * &(&p.a + &p.b * t + &p.c * t.powi(2) + &p.d * t.powi(3) + &p.e * t.powi(4))) + .sum(); Ok(c_p / RGAS * SIUnit::gas_constant()) } } impl Components for Joback { fn components(&self) -> usize { - self.records.len() + self.parameters.pure_records.len() } fn subset(&self, component_list: &[usize]) -> Self { let mut records = Vec::with_capacity(component_list.len()); component_list .iter() - .for_each(|&i| records.push(self.records[i].clone())); - Self::new(records) + .for_each(|&i| records.push(self.parameters.pure_records[i].clone())); + let n = component_list.len(); + Self::new(Arc::new(JobackParameters::from_records( + records, + Array::from_elem((n, n), JobackBinaryRecord), + ))) } } @@ -108,32 +202,23 @@ impl IdealGas for Joback { } } -impl Residual for Joback { - fn compute_max_density(&self, _moles: &Array1) -> f64 { - 1.0 - } - - fn contributions(&self) -> &[Box] { - &[] - } -} - impl + Copy> DeBroglieWavelengthDual for Joback { - fn de_broglie_wavelength(&self, temperature: D) -> Array1 { + fn ln_lambda3(&self, temperature: D) -> Array1 { let t = temperature; let t2 = t * t; + let t4 = t2 * t2; let f = (temperature * KB / (P0 * A3)).ln(); - Array1::from_shape_fn(self.records.len(), |i| { - let j = &self.records[i]; - let h = (t2 - T0 * T0) * 0.5 * j.b - + (t * t2 - T0.powi(3)) * j.c / 3.0 - + (t2 * t2 - T0.powi(4)) * j.d / 4.0 - + (t2 * t2 * t - T0.powi(5)) * j.e / 5.0 + Array1::from_shape_fn(self.parameters.pure_records.len(), |i| { + let j = &self.parameters.pure_records[i].model_record; + let h = (t2 - T0_2) * 0.5 * j.b + + (t * t2 - T0_3) * j.c / 3.0 + + (t4 - T0_4) * j.d / 4.0 + + (t4 * t - T0_5) * j.e / 5.0 + (t - T0) * j.a; let s = (t - T0) * j.b - + (t2 - T0.powi(2)) * 0.5 * j.c - + (t2 * t - T0.powi(3)) * j.d / 3.0 - + (t2 * t2 - T0.powi(4)) * j.e / 4.0 + + (t2 - T0_2) * 0.5 * j.c + + (t2 * t - T0_3) * j.d / 3.0 + + (t4 - T0_4) * j.e / 4.0 + (t / T0).ln() * j.a; (h - t * s) / (t * RGAS) + f }) @@ -148,13 +233,17 @@ impl fmt::Display for Joback { const RGAS: f64 = 6.022140857 * 1.38064852; const T0: f64 = 298.15; +const T0_2: f64 = 298.15 * 298.15; +const T0_3: f64 = T0 * T0_2; +const T0_4: f64 = T0_2 * T0_2; +const T0_5: f64 = T0 * T0_4; const P0: f64 = 1.0e5; const A3: f64 = 1e-30; const KB: f64 = 1.38064852e-23; #[cfg(test)] mod tests { - use crate::{Contributions, State, StateBuilder}; + use crate::{Contributions, Residual, State, StateBuilder}; use approx::assert_relative_eq; use ndarray::arr1; use quantity::si::*; @@ -162,8 +251,16 @@ mod tests { use super::*; - #[derive(Deserialize, Clone, Debug)] - struct ModelRecord; + // implement Residual to test Joback as equation of state + impl Residual for Joback { + fn compute_max_density(&self, _moles: &Array1) -> f64 { + 1.0 + } + + fn contributions(&self) -> &[Box] { + &[] + } + } #[test] fn paper_example() -> EosResult<()> { @@ -249,7 +346,8 @@ mod tests { ); assert_relative_eq!(jr.e, 0.0); - let eos = Arc::new(Joback::new(vec![jr])); + let pr = PureRecord::new(Identifier::default(), 1.0, jr); + let eos = Arc::new(Joback::new(Arc::new(JobackParameters::new_pure(pr)))); let state = State::new_nvt( &eos, 1000.0 * KELVIN, @@ -269,9 +367,18 @@ mod tests { #[test] fn c_p_comparison() -> EosResult<()> { - let record1 = JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005); - let record2 = JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001); - let joback = Arc::new(Joback::new(vec![record1, record2])); + let record1 = PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005), + ); + let record2 = PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001), + ); + let parameters = Arc::new(JobackParameters::new_binary(vec![record1, record2], None)); + let joback = Arc::new(Joback::new(parameters)); let temperature = 300.0 * KELVIN; let volume = METER.powi(3); let moles = arr1(&[1.0, 3.0]) * MOL; diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 837b7846a..ddea14018 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -196,8 +196,7 @@ impl EosUnit for SIUnit { mod tests { use crate::cubic::*; use crate::equation_of_state::EquationOfState; - use crate::joback::Joback; - use crate::joback::JobackRecord; + use crate::joback::{Joback, JobackParameters, JobackRecord}; use crate::parameter::*; use crate::Contributions; use crate::EosResult; @@ -251,9 +250,12 @@ mod tests { let propane = mixture[0].clone(); let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); let residual = Arc::new(PengRobinson::new(Arc::new(parameters))); - let ideal_gas = Arc::new(Joback::new(vec![JobackRecord::new( - 0.0, 0.0, 0.0, 0.0, 0.0, - )])); + let joback_parameters = Arc::new(JobackParameters::new_pure(PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(0.0, 0.0, 0.0, 0.0, 0.0), + ))); + let ideal_gas = Arc::new(Joback::new(joback_parameters)); let eos = Arc::new(EquationOfState::new(ideal_gas, residual.clone())); let sr = StateBuilder::new(&residual) diff --git a/feos-core/src/python/cubic.rs b/feos-core/src/python/cubic.rs index f2b2b8cae..1973a1bf1 100644 --- a/feos-core/src/python/cubic.rs +++ b/feos-core/src/python/cubic.rs @@ -30,10 +30,7 @@ impl PyPengRobinsonRecord { impl_json_handling!(PyPengRobinsonRecord); -impl_pure_record!( - PengRobinsonRecord, - PyPengRobinsonRecord -); +impl_pure_record!(PengRobinsonRecord, PyPengRobinsonRecord); impl_binary_record!(); diff --git a/feos-core/src/python/joback.rs b/feos-core/src/python/joback.rs index d6e670ed8..c54257b6c 100644 --- a/feos-core/src/python/joback.rs +++ b/feos-core/src/python/joback.rs @@ -1,7 +1,17 @@ -use crate::impl_json_handling; -use crate::joback::JobackRecord; -use crate::parameter::ParameterError; +use std::sync::Arc; + +use crate::joback::{JobackBinaryRecord, JobackParameters, JobackRecord}; +use crate::parameter::*; +use crate::python::parameter::*; +use crate::{ + impl_binary_record, impl_json_handling, impl_parameter, impl_parameter_from_segments, + impl_pure_record, impl_segment_record, +}; +use ndarray::Array2; +use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; +use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; +use std::convert::{TryFrom, TryInto}; /// Create a set of Joback ideal gas heat capacity parameters /// for a segment or a pure component. @@ -44,3 +54,41 @@ impl PyJobackRecord { } impl_json_handling!(PyJobackRecord); +impl_pure_record!(JobackRecord, PyJobackRecord); +impl_segment_record!(JobackRecord, PyJobackRecord); + +#[pyclass(name = "JobackBinaryRecord")] +#[derive(Clone)] +pub struct PyJobackBinaryRecord(pub JobackBinaryRecord); + +impl_binary_record!(JobackBinaryRecord, PyJobackBinaryRecord); +/// Create a set of Joback parameters from records. +/// +/// Parameters +/// ---------- +/// pure_records : List[PureRecord] +/// pure substance records. +/// substances : List[str], optional +/// The substances to use. Filters substances from `pure_records` according to +/// `search_option`. +/// When not provided, all entries of `pure_records` are used. +/// search_option : {'Name', 'Cas', 'Inchi', 'IupacName', 'Formula', 'Smiles'}, optional, defaults to 'Name'. +/// Identifier that is used to search substance. +/// +/// Returns +/// ------- +/// JobackParameters +#[pyclass(name = "JobackParameters")] +#[pyo3(text_signature = "(pure_records, substances=None, search_option='Name')")] +#[derive(Clone)] +pub struct PyJobackParameters(pub Arc); + +impl_parameter!(JobackParameters, PyJobackParameters); +impl_parameter_from_segments!(JobackParameters, PyJobackParameters); + +#[pymethods] +impl PyJobackParameters { + // fn _repr_markdown_(&self) -> String { + // self.0.to_markdown() + // } +} diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index 8d13559dc..5e5a97ac5 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -2,7 +2,7 @@ use crate::{ Components, DeBroglieWavelength, DeBroglieWavelengthDual, HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, StateHD, }; -use ndarray::{arr1, Array1}; +use ndarray::Array1; use num_dual::*; use numpy::convert::IntoPyArray; use numpy::{PyArray, PyReadonlyArray1, PyReadonlyArrayDyn}; @@ -27,9 +27,9 @@ impl PyIdealGas { if !attr { panic!("Python Class has to have a method 'subset' with signature:\n\tdef subset(self, component_list: List[int]) -> Self") } - let attr = obj.as_ref(py).hasattr("ideal_gas_model")?; + let attr = obj.as_ref(py).hasattr("ln_lambda3")?; if !attr { - panic!("{}", "Python Class has to have a method 'ideal_gas_model' with signature:\n\tdef ideal_gas_model(self, state: StateHD) -> HD\nwhere 'HD' has to be any of {{float, Dual64, HyperDual64, HyperDualDual64, Dual3Dual64, Dual3_64}}.") + panic!("{}", "Python Class has to have a method 'ln_lambda3' with signature:\n\tdef ln_lambda3(self, temperature: HD) -> HD\nwhere 'HD' has to be any (hyper-) dual number.") } Ok(Self(obj)) }) @@ -266,24 +266,24 @@ macro_rules! helmholtz_energy { macro_rules! de_broglie_wavelength { ($py_hd_id:ident, $hd_ty:ty) => { impl DeBroglieWavelengthDual<$hd_ty> for PyIdealGas { - fn de_broglie_wavelength(&self, temperature: $hd_ty) -> Array1<$hd_ty> { + fn ln_lambda3(&self, temperature: $hd_ty) -> Array1<$hd_ty> { Python::with_gil(|py| { let py_result = self .0 .as_ref(py) - .call_method1("ideal_gas_model", (<$py_hd_id>::from(temperature),)) + .call_method1("ln_lambda3", (<$py_hd_id>::from(temperature),)) .unwrap(); - let rr = if let Ok(r) = py_result.extract::>() { - dbg!("Array1"); + + // f64 + let rr = if let Ok(r) = py_result.extract::>() { + r.to_owned_array() + .mapv(|ri| <$hd_ty>::from(ri)) + // anything but f64 + } else if let Ok(r) = py_result.extract::>() { r.to_owned_array() .mapv(|ri| <$hd_ty>::from(ri.extract::<$py_hd_id>(py).unwrap())) - } else if let Ok(r) = py_result.extract::>() { - dbg!("Array0"); - assert!(r.ndim() == 0); - let scalar = &r.to_owned_array()[0]; - arr1(&[<$hd_ty>::from(scalar.extract::<$py_hd_id>(py).unwrap())]) } else { - panic!("ideal_gas_model: input data type must be numpy ndarray of dimension 1") + panic!("ln_lambda3: data type of result must be one-dimensional numpy ndarray") }; rr }) diff --git a/feos-dft/src/profile/properties.rs b/feos-dft/src/profile/properties.rs index 2b0cfd170..2348fbd0f 100644 --- a/feos-dft/src/profile/properties.rs +++ b/feos-dft/src/profile/properties.rs @@ -170,10 +170,7 @@ where temperature: Dual64, density: &Array, ) -> Array { - let lambda = self - .dft - .ideal_gas_model() - .de_broglie_wavelength(temperature); + let lambda = self.dft.ideal_gas_model().ln_lambda3(temperature); let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); for (i, rhoi) in density.outer_iter().enumerate() { phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); diff --git a/src/pets/parameters.rs b/src/pets/parameters.rs index 365374f0f..36391a037 100644 --- a/src/pets/parameters.rs +++ b/src/pets/parameters.rs @@ -212,12 +212,7 @@ impl Parameter for PetsParameters { } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } diff --git a/src/python/eos.rs b/src/python/eos.rs index 0013ee736..5aa19b504 100644 --- a/src/python/eos.rs +++ b/src/python/eos.rs @@ -29,19 +29,19 @@ use crate::uvtheory::{Perturbation, UVTheory, UVTheoryOptions, VirialOrder}; use feos_core::cubic::PengRobinson; use feos_core::joback::Joback; use feos_core::python::cubic::PyPengRobinsonParameters; +use feos_core::python::joback::PyJobackParameters; +use feos_core::python::user_defined::{PyIdealGas, PyResidual}; use feos_core::*; -use feos_core::python::joback::PyJobackRecord; -use feos_core::python::user_defined::{PyResidual, PyIdealGas}; use numpy::convert::ToPyArray; use numpy::{PyArray1, PyArray2}; use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; #[cfg(feature = "estimator")] use pyo3::wrap_pymodule; -use quantity::python::{PySINumber, PySIArray1, PySIArray2}; -use std::sync::Arc; +use quantity::python::{PySIArray1, PySIArray2, PySINumber}; use quantity::si::*; use std::collections::HashMap; +use std::sync::Arc; /// Collection of equations of state. #[pyclass(name = "EquationOfState")] @@ -296,33 +296,37 @@ impl PyEquationOfState { /// /// Parameters /// ---------- - /// ideal_gas : Class, optional + /// ideal_gas : Class /// A python class implementing the necessary methods - /// to be used as a ideal gas model. + /// to be used as an ideal gas model. /// /// Returns /// ------- /// EquationOfState fn python_ideal_gas(&self, ideal_gas: Py) -> PyResult { let ig = Arc::new(IdealGasModel::Python(PyIdealGas::new(ideal_gas)?)); - Ok(Self(Arc::new(EquationOfState::new(ig, self.0.residual.clone())))) + Ok(Self(Arc::new(EquationOfState::new( + ig, + self.0.residual.clone(), + )))) } - /// Ideal gas equation of state from a Python class. + /// Ideal gas model of Joback and Reid. /// /// Parameters /// ---------- - /// ideal_gas : Class, optional - /// A python class implementing the necessary methods - /// to be used as a ideal gas model. + /// parameters : List[JobackRecord] + /// List containing /// /// Returns /// ------- /// EquationOfState - fn joback(&self, parameters: Vec) -> Self { - let records = parameters.iter().map(|p| p.0.clone()).collect(); - let ideal_gas = Arc::new(IdealGasModel::Joback(Joback::new(records))); - Self(Arc::new(EquationOfState::new(ideal_gas, self.0.residual.clone()))) + fn joback(&self, parameters: PyJobackParameters) -> Self { + let ideal_gas = Arc::new(IdealGasModel::Joback(Joback::new(parameters.0))); + Self(Arc::new(EquationOfState::new( + ideal_gas, + self.0.residual.clone(), + ))) } } diff --git a/src/python/ideal_gas.rs b/src/python/ideal_gas.rs index c13a79e60..04e0d177c 100644 --- a/src/python/ideal_gas.rs +++ b/src/python/ideal_gas.rs @@ -1,7 +1,8 @@ -use feos_core::python::joback::PyJobackRecord; +use feos_core::python::joback::{PyJobackParameters, PyJobackRecord}; use pyo3::prelude::*; #[pymodule] pub fn ideal_gas(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - m.add_class::() + m.add_class::()?; + m.add_class::() } diff --git a/src/python/mod.rs b/src/python/mod.rs index 7bf974cc8..e4a3e4f8a 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -13,12 +13,12 @@ use pyo3::prelude::*; use pyo3::wrap_pymodule; use quantity::python::quantity as quantity_module; -mod ideal_gas; mod cubic; mod eos; -use ideal_gas::ideal_gas as ideal_gas_module; +mod ideal_gas; use cubic::cubic as cubic_module; use eos::eos as eos_module; +use ideal_gas::ideal_gas as ideal_gas_module; #[cfg(feature = "dft")] mod dft; From e60e551c78f1fa3dcb6a0d617ac4a78f21abe4c7 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Thu, 15 Jun 2023 16:01:04 +0200 Subject: [PATCH 39/47] Updated tests --- tests/pcsaft/dft.rs | 10 ++++- tests/pcsaft/state_creation_mixture.rs | 29 ++++++++----- tests/pcsaft/state_creation_pure.rs | 60 +++++++++++++++----------- 3 files changed, 63 insertions(+), 36 deletions(-) diff --git a/tests/pcsaft/dft.rs b/tests/pcsaft/dft.rs index 107b5c74d..d993011a0 100644 --- a/tests/pcsaft/dft.rs +++ b/tests/pcsaft/dft.rs @@ -3,7 +3,7 @@ use approx::assert_relative_eq; use feos::hard_sphere::FMTVersion; use feos::pcsaft::{PcSaft, PcSaftFunctional, PcSaftParameters}; -use feos_core::joback::{Joback, JobackRecord}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; use feos_core::{Contributions, PhaseEquilibrium, State, Verbosity}; use feos_dft::interface::PlanarInterface; @@ -330,7 +330,13 @@ fn test_entropy_bulk_values() -> Result<(), Box> { None, IdentifierOption::Name, )?; - let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let joback_params = JobackParameters::from_json( + vec!["water_np"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name + )?; + let joback = Joback::new(Arc::new(joback_params)); let func = Arc::new(PcSaftFunctional::new(Arc::new(params)).ideal_gas(joback)); let vle = PhaseEquilibrium::pure(&func, 350.0 * KELVIN, None, Default::default())?; let profile = PlanarInterface::from_pdgt(&vle, 2048, false)?.solve(None)?; diff --git a/tests/pcsaft/state_creation_mixture.rs b/tests/pcsaft/state_creation_mixture.rs index 62a653560..5497d4b6b 100644 --- a/tests/pcsaft/state_creation_mixture.rs +++ b/tests/pcsaft/state_creation_mixture.rs @@ -1,6 +1,6 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; -use feos_core::joback::{Joback, JobackRecord}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; use feos_core::{Contributions, EquationOfState, StateBuilder}; use ndarray::prelude::*; @@ -9,19 +9,28 @@ use quantity::si::*; use std::error::Error; use std::sync::Arc; -fn propane_butane_parameters() -> Result, ParameterError> { - Ok(Arc::new(PcSaftParameters::from_json( +fn propane_butane_parameters( +) -> Result<(Arc, Arc), ParameterError> { + let saft = Arc::new(PcSaftParameters::from_json( vec!["propane", "butane"], "tests/pcsaft/test_parameters.json", None, IdentifierOption::Name, - )?)) + )?); + let joback = Arc::new(JobackParameters::from_json( + vec!["propane", "butane"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name, + )?); + Ok((saft, joback)) } #[test] fn pressure_entropy_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); - let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8); 2]); + let (saft_params, joback_params) = propane_butane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = BAR; let temperature = 300.0 * KELVIN; @@ -53,7 +62,7 @@ fn pressure_entropy_molefracs() -> Result<(), Box> { #[test] fn volume_temperature_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let volume = 1.5e-3 * METER.powi(3); let moles = MOL; @@ -70,7 +79,7 @@ fn volume_temperature_molefracs() -> Result<(), Box> { #[test] fn temperature_partial_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let x = arr1(&[0.3, 0.7]); let partial_density = x.clone() * MOL / METER.powi(3); @@ -89,7 +98,7 @@ fn temperature_partial_density() -> Result<(), Box> { #[test] fn temperature_density_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let x = arr1(&[0.3, 0.7]); let density = MOL / METER.powi(3); @@ -107,7 +116,7 @@ fn temperature_density_molefracs() -> Result<(), Box> { #[test] fn temperature_pressure_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let pressure = BAR; let x = arr1(&[0.3, 0.7]); diff --git a/tests/pcsaft/state_creation_pure.rs b/tests/pcsaft/state_creation_pure.rs index 3cbbab54e..ea7dcafc7 100644 --- a/tests/pcsaft/state_creation_pure.rs +++ b/tests/pcsaft/state_creation_pure.rs @@ -1,6 +1,6 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; -use feos_core::joback::{Joback, JobackRecord}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; use feos_core::{ Contributions, DensityInitialization, EquationOfState, IdealGas, PhaseEquilibrium, Residual, @@ -10,18 +10,25 @@ use quantity::si::*; use std::error::Error; use std::sync::Arc; -fn propane_parameters() -> Result, ParameterError> { - Ok(Arc::new(PcSaftParameters::from_json( +fn propane_parameters() -> Result<(Arc, Arc), ParameterError> { + let saft = Arc::new(PcSaftParameters::from_json( vec!["propane"], "tests/pcsaft/test_parameters.json", None, IdentifierOption::Name, - )?)) + )?); + let joback = Arc::new(JobackParameters::from_json( + vec!["propane"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name, + )?); + Ok((saft, joback)) } #[test] fn temperature_volume() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let volume = 1.5e-3 * METER.powi(3); let moles = MOL; @@ -36,7 +43,7 @@ fn temperature_volume() -> Result<(), Box> { #[test] fn temperature_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let density = MOL / METER.powi(3); let state = StateBuilder::new(&saft) @@ -49,7 +56,7 @@ fn temperature_density() -> Result<(), Box> { #[test] fn temperature_total_moles_volume() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let total_moles = MOL; let volume = METER.powi(3); @@ -65,7 +72,7 @@ fn temperature_total_moles_volume() -> Result<(), Box> { #[test] fn temperature_total_moles_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let total_moles = MOL; let density = MOL / METER.powi(3); @@ -84,7 +91,7 @@ fn temperature_total_moles_density() -> Result<(), Box> { #[test] fn pressure_temperature() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -101,7 +108,7 @@ fn pressure_temperature() -> Result<(), Box> { #[test] fn pressure_temperature_phase() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -119,7 +126,7 @@ fn pressure_temperature_phase() -> Result<(), Box> { #[test] fn pressure_temperature_initial_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -137,8 +144,9 @@ fn pressure_temperature_initial_density() -> Result<(), Box> { #[test] fn pressure_enthalpy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_enthalpy = 2000.0 * JOULE / MOL; @@ -178,8 +186,9 @@ fn pressure_enthalpy_vapor() -> Result<(), Box> { #[test] fn density_internal_energy() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 5.0 * BAR; let temperature = 315.0 * KELVIN; @@ -207,8 +216,9 @@ fn density_internal_energy() -> Result<(), Box> { #[test] fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_enthalpy = 2000.0 * JOULE / MOL; @@ -250,8 +260,9 @@ fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { #[test] fn pressure_entropy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_entropy = -2.0 * JOULE / MOL / KELVIN; @@ -291,8 +302,9 @@ fn pressure_entropy_vapor() -> Result<(), Box> { #[test] fn temperature_entropy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); - let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 3.0 * BAR; let temperature = 315.15 * KELVIN; @@ -350,9 +362,9 @@ fn assert_multiple_states( #[test] fn test_consistency() -> Result<(), Box> { - let p = propane_parameters()?; - let saft = Arc::new(PcSaft::new(p)); - let joback = Joback::new(vec![JobackRecord::new(1.0, 1e-2, 1e-4, 1e-6, 1e-8)]); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let temperatures = [350.0 * KELVIN, 400.0 * KELVIN, 450.0 * KELVIN]; let pressures = [1.0 * BAR, 2.0 * BAR, 3.0 * BAR]; From daad0cf4ab90dcfa3e76d0d6f82e3751cf9e944a Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Thu, 15 Jun 2023 16:05:23 +0200 Subject: [PATCH 40/47] Added Joback parameters for tests --- tests/pcsaft/test_parameters_joback.json | 135 +++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 tests/pcsaft/test_parameters_joback.json diff --git a/tests/pcsaft/test_parameters_joback.json b/tests/pcsaft/test_parameters_joback.json new file mode 100644 index 000000000..afb9c81f4 --- /dev/null +++ b/tests/pcsaft/test_parameters_joback.json @@ -0,0 +1,135 @@ +[ + { + "identifier": { + "cas": "74-98-6", + "name": "propane", + "iupac_name": "propane", + "smiles": "CCC", + "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", + "formula": "C3H8" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 44.0962, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH3" + ] + } + }, + { + "identifier": { + "cas": "106-97-8", + "name": "butane", + "iupac_name": "butane", + "smiles": "CCCC", + "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", + "formula": "C4H10" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 58.123, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH2", + "CH3" + ] + } + }, + { + "identifier": { + "cas": "74-82-8", + "name": "methane", + "iupac_name": "methane", + "smiles": "C", + "inchi": "InChI=1/CH4/h1H4", + "formula": "CH4" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 16.0426 + }, + { + "identifier": { + "cas": "124-38-9", + "name": "carbon-dioxide", + "iupac_name": "carbon dioxide", + "smiles": "O=C=O", + "inchi": "InChI=1/CO2/c2-1-3", + "formula": "CO2" + }, + "molarweight": 44.0098, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + } + }, + { + "identifier": { + "cas": "7732-18-5", + "name": "water_np", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 18.0152 + }, + { + "identifier": { + "cas": "110-54-3", + "name": "hexane", + "iupac_name": "hexane", + "smiles": "CCCCCC", + "inchi": "InChI=1/C6H14/c1-3-5-6-4-2/h3-6H2,1-2H3", + "formula": "C6H14" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH2", + "CH2", + "CH2", + "CH3" + ] + }, + "molarweight": 86.177 + } +] \ No newline at end of file From dd9e23d77fd0640b5b6786fd5a0c61ce5afa6062 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 16 Jun 2023 14:27:30 +0200 Subject: [PATCH 41/47] Fix calculation of dln_phi_dt --- feos-core/src/state/residual_properties.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 0533f8a8b..25e37fccb 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -272,8 +272,10 @@ impl State { /// 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}$ pub fn dln_phi_dt(&self) -> SIArray1 { - let vi = -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total); - (self.dmu_res_dt() - vi * self.dp_dt(Contributions::Total)) + let vi = self.partial_molar_volume(); + (self.dmu_res_dt() + - self.residual_chemical_potential() / self.temperature + - vi * self.dp_dt(Contributions::Total)) / (SIUnit::gas_constant() * self.temperature) + 1.0 / self.temperature } From 42f9501fc950394793fe2202cc8a58ab595aac6f Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 16 Jun 2023 15:13:16 +0200 Subject: [PATCH 42/47] Fix critical_point_binary_p --- feos-core/src/state/critical_point.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feos-core/src/state/critical_point.rs b/feos-core/src/state/critical_point.rs index db8e4de99..d5b5ad103 100644 --- a/feos-core/src/state/critical_point.rs +++ b/feos-core/src/state/critical_point.rs @@ -568,7 +568,7 @@ fn critical_point_objective_p( let a = |v| { let m = arr1(&[Dual::from_re(density[0]), Dual::from_re(density[1])]); let state_p = StateHD::new(Dual::from_re(temperature), v, m); - -eos.evaluate_residual(&state_p) + state_p.partial_density.sum() + eos.evaluate_residual(&state_p) }; let (_, p) = first_derivative(a, DualVec::one()); let p = (p - density.sum()) * temperature; From 3f111972eba24563a4ae6d004df96fb939c902ea Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Mon, 19 Jun 2023 13:07:53 +0200 Subject: [PATCH 43/47] Updated changelog for feos-core --- feos-core/CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/feos-core/CHANGELOG.md b/feos-core/CHANGELOG.md index 7c14acda2..42be12707 100644 --- a/feos-core/CHANGELOG.md +++ b/feos-core/CHANGELOG.md @@ -5,6 +5,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Added `Components`, `Residual`, `IdealGas` and `DeBroglieWavelength` traits to decouple ideal gas models from residual models. [#158](https://github.com/feos-org/feos/pull/158) +- Added `JobackParameters` struct that implements `Parameters` including Python bindings. [#158](https://github.com/feos-org/feos/pull/) + +### Changed +- Changed `EquationOfState` from a trait to a `struct` that is generic over `Residual` and `IdealGas` and implements all necessary traits to be used as equation of state including the ideal gas contribution. [#158](https://github.com/feos-org/feos/pull/) +- The `Parameter` trait no longer has an associated type `IdealGas`. [#158](https://github.com/feos-org/feos/pull/) +- Split properties of `State` into those that require the `Residual` trait (`residual_properties.rs`) and those that require both `Residual + IdealGas` (`properties.rs`). [#158](https://github.com/feos-org/feos/pull/) +- State creation routines are split into those that can be used with `Residual` and those that require `Residual + IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) +- `Contributions` enum no longer includes the `ResidualNpt` variant. `ResidualNvt` variant is rename to `Residual`. [#158](https://github.com/feos-org/feos/pull/158) +- Moved `Verbosity` and `SolverOption` from `phase_equilibria` module to `lib.rs`. [#158](https://github.com/feos-org/feos/pull/) +- Moved `StateVec` into own file and module. [#158](https://github.com/feos-org/feos/pull/158) +- Ideal gas and residual Helmholtz energy models can now be separately implemented in Python via the `PyIdealGas` and `PyResidual` structs. [#158](https://github.com/feos-org/feos/pull/) + +### Removed +- Removed `EquationOfState` trait. [#158](https://github.com/feos-org/feos/pull/158) +- Removed ideal gas dependencies from `PureRecord` and `SegmentRecord`. [#158](https://github.com/feos-org/feos/pull/) +- Removed Python getter and setter functions and optional arguments for ideal gas records in macros. [#158](https://github.com/feos-org/feos/pull/) + ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) From 5f2b0a0c9dd7aa6e917c53e5498eec1956db8d5b Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Mon, 19 Jun 2023 13:30:08 +0200 Subject: [PATCH 44/47] Updated changelog for feos-dft --- feos-dft/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/feos-dft/CHANGELOG.md b/feos-dft/CHANGELOG.md index 9a8fd9824..1464fdf1e 100644 --- a/feos-dft/CHANGELOG.md +++ b/feos-dft/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Implemented `HelmholtzEnergyFunctional` for `EquationOfState` to be able to use functionals as equations of state. [#158](https://github.com/feos-org/feos/pull/158) + +### Changed +- `HelmholtzEnergyFunctional`: added `Components` trait as trait bound and removed `ideal_gas` method. [#158](https://github.com/feos-org/feos/pull/158) +- `DFT` now implements `Residual` and furthermore `IdealGas` if `F` implements `IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) +- What properties (and contributions) of `DFTProfile` are available now depends on whether an ideal gas model is provided or not. [#158](https://github.com/feos-org/feos/pull/158) + +### Removed +- Removed `DefaultIdealGasContribution` [#158](https://github.com/feos-org/feos/pull/158) +- Removed getters for `chemical_potential` (for profiles) and `molar_gibbs_energy` (for `Adsorption1D` and `Adsorption3D`) from Python interface. [#158](https://github.com/feos-org/feos/pull/158) + ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) From a3e7f03e5c72710e8a2eb0df9995a3cc2aa2b4cb Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 3 Jul 2023 14:35:47 +0200 Subject: [PATCH 45/47] fix total_gibbs energy and hence accelerated successive substitution --- feos-core/src/phase_equilibria/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/feos-core/src/phase_equilibria/mod.rs b/feos-core/src/phase_equilibria/mod.rs index 7b302f6ff..7f8a27624 100644 --- a/feos-core/src/phase_equilibria/mod.rs +++ b/feos-core/src/phase_equilibria/mod.rs @@ -1,7 +1,7 @@ use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{DensityInitialization, State}; -use crate::EosUnit; +use crate::{Contributions, EosUnit}; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::fmt; use std::fmt::Write; @@ -196,9 +196,13 @@ impl PhaseEquilibrium { let ln_rho = s .partial_density .to_reduced(SIUnit::reference_density()) - .unwrap(); - acc + s.residual_gibbs_energy() - + SIUnit::gas_constant() * s.temperature * (s.moles.clone() * ln_rho).sum() + .unwrap() + .mapv(f64::ln); + acc + s.residual_helmholtz_energy() + + s.pressure(Contributions::Total) * s.volume + + SIUnit::gas_constant() + * s.temperature + * (s.moles.clone() * (ln_rho - 1.0)).sum() }) } } From 6b7cdb9fcd356ded64db2ca9c144a59fe4c587aa Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Wed, 5 Jul 2023 14:46:06 +0200 Subject: [PATCH 46/47] clarify docstring of residual Helmholtz and Gibbs energy --- feos-core/src/state/residual_properties.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs index 25e37fccb..42f2f06f1 100644 --- a/feos-core/src/state/residual_properties.rs +++ b/feos-core/src/state/residual_properties.rs @@ -328,7 +328,7 @@ impl State { - SIUnit::gas_constant() } - /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+pV-nRT$ + /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+p^\text{res}V$ pub fn residual_enthalpy(&self) -> SINumber { self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() @@ -340,7 +340,7 @@ impl State { self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() } - /// Residual Gibbs energy: $G^\text{res}(T,p,\mathbf{n})=A^\text{res}+pV-NRT-NRT \ln Z$ + /// Residual Gibbs energy: $G^\text{res}(T,p,\mathbf{n})=A^\text{res}+p^\text{res}V-NRT \ln Z$ pub fn residual_gibbs_energy(&self) -> SINumber { self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy() - self.total_moles From 6c6f023cdc87b923502ff78fbc8ca72caf824999 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 7 Jul 2023 13:08:27 +0200 Subject: [PATCH 47/47] update changelog --- CHANGELOG.md | 6 ++++++ feos-core/CHANGELOG.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba4edacde..3d4d9932f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added `IdealGasModel` enum that collects all implementors of the `IdealGas` trait. [#158](https://github.com/feos-org/feos/pull/158) +- Added `feos.ideal_gas` module in Python from which (currently) `Joback` and `JobackParameters` are available. [#158](https://github.com/feos-org/feos/pull/158) + ### Changed - Changed the internal implementation of the association contribution to accomodate more general association schemes. [#150](https://github.com/feos-org/feos/pull/150) - To comply with the new association implementation, the default values of `na` and `nb` are now `0` rather than `1`. Parameter files have been adapted accordingly. [#150](https://github.com/feos-org/feos/pull/150) - Added the possibility to specify a pure component correction parameter `phi` for the heterosegmented gc PC-SAFT equation of state. [#157](https://github.com/feos-org/feos/pull/157) +- Renamed `EosVariant` to `ResidualModel`. [#158](https://github.com/feos-org/feos/pull/158) +- Added methods to add an ideal gas contribution to an initialized equation of state object in Python. [#158](https://github.com/feos-org/feos/pull/158) ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) diff --git a/feos-core/CHANGELOG.md b/feos-core/CHANGELOG.md index 42be12707..d44de111c 100644 --- a/feos-core/CHANGELOG.md +++ b/feos-core/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `Parameter` trait no longer has an associated type `IdealGas`. [#158](https://github.com/feos-org/feos/pull/) - Split properties of `State` into those that require the `Residual` trait (`residual_properties.rs`) and those that require both `Residual + IdealGas` (`properties.rs`). [#158](https://github.com/feos-org/feos/pull/) - State creation routines are split into those that can be used with `Residual` and those that require `Residual + IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) -- `Contributions` enum no longer includes the `ResidualNpt` variant. `ResidualNvt` variant is rename to `Residual`. [#158](https://github.com/feos-org/feos/pull/158) +- `Contributions` enum no longer includes the `ResidualNpt` variant. `ResidualNvt` variant is renamed to `Residual`. [#158](https://github.com/feos-org/feos/pull/158) - Moved `Verbosity` and `SolverOption` from `phase_equilibria` module to `lib.rs`. [#158](https://github.com/feos-org/feos/pull/) - Moved `StateVec` into own file and module. [#158](https://github.com/feos-org/feos/pull/158) - Ideal gas and residual Helmholtz energy models can now be separately implemented in Python via the `PyIdealGas` and `PyResidual` structs. [#158](https://github.com/feos-org/feos/pull/)