diff --git a/CHANGELOG.md b/CHANGELOG.md index ab92152c9..54c8c745f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Export `EosVariant` and `FunctionalVariant` directly in the crate root instead of their own modules. [#62](https://github.com/feos-org/feos/pull/62) - Changed constructors `VaporPressure::new` and `DataSet.vapor_pressure` (Python) to take a new optional argument `critical_temperature`. [#86](https://github.com/feos-org/feos/pull/86) - The limitations of the homo gc method for PC-SAFT are enforced more strictly. [#88](https://github.com/feos-org/feos/pull/88) +- Removed generics for units in all structs and traits in favor of static SI units. [#115](https://github.com/feos-org/feos/pull/115) ## [0.3.0] - 2022-09-14 - Major restructuring of the entire `feos` project. All individual models are reunited in the `feos` crate. `feos-core` and `feos-dft` still live as individual crates within the `feos` workspace. diff --git a/benches/dual_numbers.rs b/benches/dual_numbers.rs index 49eebed96..0eff389fa 100644 --- a/benches/dual_numbers.rs +++ b/benches/dual_numbers.rs @@ -18,7 +18,7 @@ use std::sync::Arc; /// - temperature is 80% of critical temperature, /// - volume is critical volume, /// - molefracs (or moles) for equimolar mixture. -fn state_pcsaft(parameters: PcSaftParameters) -> State { +fn state_pcsaft(parameters: PcSaftParameters) -> State { let n = parameters.pure_records.len(); let eos = Arc::new(PcSaft::new(Arc::new(parameters))); let moles = Array::from_elem(n, 1.0 / n as f64) * 10.0 * MOL; @@ -36,11 +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 e1ecf9879..68c7b0904 100644 --- a/benches/state_creation.rs +++ b/benches/state_creation.rs @@ -15,7 +15,7 @@ fn npt( SINumber, SINumber, &SIArray1, - DensityInitialization, + DensityInitialization, ), ) { State::new_npt(eos, t, p, n, rho0).unwrap(); diff --git a/benches/state_properties.rs b/benches/state_properties.rs index 41c6366a5..06c419fcb 100644 --- a/benches/state_properties.rs +++ b/benches/state_properties.rs @@ -8,11 +8,11 @@ use ndarray::arr1; use quantity::si::*; use std::sync::Arc; -type S = State; +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(); diff --git a/feos-core/CHANGELOG.md b/feos-core/CHANGELOG.md index 1741b4d10..9016ba9fb 100644 --- a/feos-core/CHANGELOG.md +++ b/feos-core/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `StateVec::moles` getter. [#113](https://github.com/feos-org/feos/pull/113) - Added public constructors `PhaseDiagram::new` and `StateVec::new` that allow the creation of the respective structs from a list of `PhaseEquilibrium`s or `State`s in Rust and Python. [#113](https://github.com/feos-org/feos/pull/113) - Added new variant `EosError::Error` which allows dispatching generic errors that are not covered by the existing variants. [#98](https://github.com/feos-org/feos/pull/98) +- Removed generics for units in all structs and traits in favor of static SI units. [#115](https://github.com/feos-org/feos/pull/115) ### Changed - Added `Sync` and `Send` as supertraits to `EquationOfState`. [#57](https://github.com/feos-org/feos/pull/57) diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index d78774b4d..da2795aed 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -14,7 +14,7 @@ use crate::state::StateHD; use crate::MolarWeight; use ndarray::{Array1, Array2}; use num_dual::DualNum; -use quantity::si::{SIArray1, SIUnit}; +use quantity::si::SIArray1; use serde::{Deserialize, Serialize}; use std::f64::consts::SQRT_2; use std::fmt; @@ -255,7 +255,7 @@ impl EquationOfState for PengRobinson { } } -impl MolarWeight for PengRobinson { +impl MolarWeight for PengRobinson { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/feos-core/src/density_iteration.rs b/feos-core/src/density_iteration.rs index c1ce504f9..54c236274 100644 --- a/feos-core/src/density_iteration.rs +++ b/feos-core/src/density_iteration.rs @@ -2,26 +2,26 @@ use crate::equation_of_state::EquationOfState; use crate::errors::{EosError, EosResult}; use crate::state::State; use crate::EosUnit; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; use std::sync::Arc; -pub fn density_iteration( +pub fn density_iteration( eos: &Arc, - temperature: QuantityScalar, - pressure: QuantityScalar, - moles: &QuantityArray1, - initial_density: QuantityScalar, -) -> EosResult> { + temperature: SINumber, + pressure: SINumber, + moles: &SIArray1, + initial_density: SINumber, +) -> EosResult> { let maxdensity = eos.max_density(Some(moles))?; let (abstol, reltol) = (1e-12, 1e-14); let n = moles.sum(); let mut rho = initial_density; - if rho <= 0.0 * U::reference_density() { + if rho <= 0.0 * SIUnit::reference_density() { return Err(EosError::InvalidState( String::from("density iteration"), String::from("density"), - rho.to_reduced(U::reference_density())?, + rho.to_reduced(SIUnit::reference_density())?, )); } @@ -117,7 +117,7 @@ pub fn density_iteration( } else { rho = (rho + initial_density) * 0.5; if (rho - initial_density) - .to_reduced(U::reference_density())? + .to_reduced(SIUnit::reference_density())? .abs() < 1e-8 { @@ -128,8 +128,11 @@ pub fn density_iteration( } // Newton step rho += delta_rho; - if error.to_reduced(U::reference_pressure())?.abs() - < f64::max(abstol, (rho * reltol).to_reduced(U::reference_density())?) + if error.to_reduced(SIUnit::reference_pressure())?.abs() + < f64::max( + abstol, + (rho * reltol).to_reduced(SIUnit::reference_density())?, + ) { break 'iteration; } @@ -141,12 +144,12 @@ pub fn density_iteration( } } -fn pressure_spinodal( +fn pressure_spinodal( eos: &Arc, - temperature: QuantityScalar, - rho_init: QuantityScalar, - moles: &QuantityArray1, -) -> EosResult<[QuantityScalar; 2]> { + temperature: SINumber, + rho_init: SINumber, + moles: &SIArray1, +) -> EosResult<[SINumber; 2]> { let maxiter = 30; let abstol = 1e-8; @@ -154,11 +157,11 @@ fn pressure_spinodal( let n = moles.sum(); let mut rho = rho_init; - if rho <= 0.0 * U::reference_density() { + if rho <= 0.0 * SIUnit::reference_density() { return Err(EosError::InvalidState( String::from("pressure spinodal"), String::from("density"), - rho.to_reduced(U::reference_density())?, + rho.to_reduced(SIUnit::reference_density())?, )); } @@ -174,7 +177,7 @@ fn pressure_spinodal( rho += delta_rho; if dpdrho - .to_reduced(U::reference_pressure() / U::reference_density())? + .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())? .abs() < abstol { diff --git a/feos-core/src/equation_of_state.rs b/feos-core/src/equation_of_state.rs index 8677be69e..abb500012 100644 --- a/feos-core/src/equation_of_state.rs +++ b/feos-core/src/equation_of_state.rs @@ -6,7 +6,7 @@ use num_dual::{ Dual, Dual2_64, Dual3, Dual3_64, Dual64, DualNum, DualVec64, HyperDual, HyperDual64, }; use num_traits::{One, Zero}; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; use std::fmt; /// Individual Helmholtz energy contribution that can @@ -149,8 +149,8 @@ impl fmt::Display for DefaultIdealGasContribution { /// /// The trait is required to be able to calculate (mass) /// specific properties. -pub trait MolarWeight { - fn molar_weight(&self) -> QuantityArray1; +pub trait MolarWeight { + fn molar_weight(&self) -> SIArray1; } /// A general equation of state. @@ -219,15 +219,12 @@ pub trait EquationOfState: Send + Sync { /// 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<&QuantityArray1>, - ) -> EosResult> { + 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) * U::reference_moles()), + None => Ok(Array::ones(1) * SIUnit::reference_moles()), } } else { Err(EosError::IncompatibleComponents(self.components(), l)) @@ -240,105 +237,102 @@ pub trait EquationOfState: Send + Sync { /// 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<&QuantityArray1>, - ) -> EosResult> { + fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { let mr = self .validate_moles(moles)? - .to_reduced(U::reference_moles())?; - Ok(self.compute_max_density(&mr) * U::reference_density()) + .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( + fn second_virial_coefficient( &self, - temperature: QuantityScalar, - moles: Option<&QuantityArray1>, - ) -> EosResult> { + 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(U::reference_temperature())?); + 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 / U::reference_density()) + Ok(self.evaluate_residual(&s).eps1eps2[(0, 0)] * 0.5 / SIUnit::reference_density()) } /// Calculate the third virial coefficient $C(T)$ - fn third_virial_coefficient( + fn third_virial_coefficient( &self, - temperature: QuantityScalar, - moles: Option<&QuantityArray1>, - ) -> EosResult> { + 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(U::reference_temperature())?); + 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 / U::reference_density().powi(2)) + 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( + fn second_virial_coefficient_temperature_derivative( &self, - temperature: QuantityScalar, - moles: Option<&QuantityArray1>, - ) -> EosResult> { + 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(U::reference_temperature())?).derive(), + 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 - / (U::reference_density() * U::reference_temperature())) + / (SIUnit::reference_density() * SIUnit::reference_temperature())) } /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ - fn third_virial_coefficient_temperature_derivative( + fn third_virial_coefficient_temperature_derivative( &self, - temperature: QuantityScalar, - moles: Option<&QuantityArray1>, - ) -> EosResult> { + 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(U::reference_temperature())?).derive(), + 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 - / (U::reference_density().powi(2) * U::reference_temperature())) + / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) } } /// Reference values and residual entropy correlations for entropy scaling. -pub trait EntropyScaling { +pub trait EntropyScaling { fn viscosity_reference( &self, - temperature: QuantityScalar, - volume: QuantityScalar, - moles: &QuantityArray1, - ) -> EosResult>; + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; fn diffusion_reference( &self, - temperature: QuantityScalar, - volume: QuantityScalar, - moles: &QuantityArray1, - ) -> EosResult>; + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult; fn thermal_conductivity_reference( &self, - temperature: QuantityScalar, - volume: QuantityScalar, - moles: &QuantityArray1, - ) -> EosResult>; + 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 fa1c2bd23..643f7bc66 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -9,7 +9,7 @@ use crate::{ use conv::ValueInto; use ndarray::Array1; use num_dual::*; -use quantity::QuantityScalar; +use quantity::si::{SINumber, SIUnit}; use serde::{Deserialize, Serialize}; use std::fmt; @@ -85,17 +85,13 @@ impl Joback { } /// Directly calculates the ideal gas heat capacity from the Joback model. - pub fn c_p( - &self, - temperature: QuantityScalar, - molefracs: &Array1, - ) -> EosResult> { - let t = temperature.to_reduced(U::reference_temperature())?; + 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)); } - Ok(c_p / RGAS * U::gas_constant()) + Ok(c_p / RGAS * SIUnit::gas_constant()) } } diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 3f2084ba5..54cb98ff1 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -42,7 +42,9 @@ pub use errors::{EosError, EosResult}; pub use phase_equilibria::{ PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, }; -pub use state::{Contributions, DensityInitialization, State, StateBuilder, StateHD, StateVec, Derivative}; +pub use state::{ + Contributions, DensityInitialization, Derivative, State, StateBuilder, StateHD, StateVec, +}; #[cfg(feature = "python")] pub mod python; diff --git a/feos-core/src/phase_equilibria/bubble_dew.rs b/feos-core/src/phase_equilibria/bubble_dew.rs index edbb930be..f6f8a6c79 100644 --- a/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/feos-core/src/phase_equilibria/bubble_dew.rs @@ -1,14 +1,15 @@ use super::{PhaseEquilibrium, SolverOptions, Verbosity}; +use crate::equation_of_state::EquationOfState; use crate::errors::{EosError, EosResult}; use crate::state::{ Contributions, DensityInitialization::{InitialDensity, Liquid, Vapor}, State, StateBuilder, TPSpec, }; -use crate::{equation_of_state::EquationOfState, EosUnit}; +use crate::EosUnit; use ndarray::*; use num_dual::linalg::{norm, LU}; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; use std::convert::TryFrom; use std::sync::Arc; @@ -21,11 +22,8 @@ const MAX_TSTEP: f64 = 20.0; const MAX_LNPSTEP: f64 = 0.1; const NEWTON_TOL: f64 = 1e-3; -impl TPSpec { - pub(super) fn temperature_pressure( - &self, - tp_init: QuantityScalar, - ) -> (Self, QuantityScalar, QuantityScalar) { +impl TPSpec { + pub(super) fn temperature_pressure(&self, tp_init: SINumber) -> (Self, SINumber, SINumber) { match self { Self::Temperature(t) => (Self::Pressure(tp_init), *t, tp_init), Self::Pressure(p) => (Self::Temperature(tp_init), tp_init, *p), @@ -40,9 +38,9 @@ impl TPSpec { } } -impl std::fmt::Display for TPSpec +impl std::fmt::Display for TPSpec where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -57,19 +55,19 @@ 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( eos: &Arc, - temperature_or_pressure: QuantityScalar, + temperature_or_pressure: SINumber, liquid_molefracs: &Array1, - tp_init: Option>, + tp_init: Option, vapor_molefracs: Option<&Array1>, options: (SolverOptions, SolverOptions), ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { Self::bubble_dew_point( eos, @@ -86,14 +84,14 @@ impl PhaseEquilibrium { /// or pressure and composition of the vapor phase. pub fn dew_point( eos: &Arc, - temperature_or_pressure: QuantityScalar, + temperature_or_pressure: SINumber, vapor_molefracs: &Array1, - tp_init: Option>, + tp_init: Option, liquid_molefracs: Option<&Array1>, options: (SolverOptions, SolverOptions), ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { Self::bubble_dew_point( eos, @@ -108,15 +106,15 @@ impl PhaseEquilibrium { pub(super) fn bubble_dew_point( eos: &Arc, - tp_spec: TPSpec, - tp_init: Option>, + tp_spec: TPSpec, + tp_init: Option, molefracs_spec: &Array1, molefracs_init: Option<&Array1>, bubble: bool, options: (SolverOptions, SolverOptions), ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { match tp_spec { TPSpec::Temperature(t) => { @@ -184,15 +182,15 @@ impl PhaseEquilibrium { fn iterate_bubble_dew( eos: &Arc, - tp_spec: TPSpec, - tp_init: QuantityScalar, + tp_spec: TPSpec, + tp_init: SINumber, molefracs_spec: &Array1, molefracs_init: Option<&Array1>, bubble: bool, options: (SolverOptions, SolverOptions), ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let (var, t, p) = tp_spec.temperature_pressure(tp_init); let [state1, state2] = if bubble { @@ -205,12 +203,12 @@ impl PhaseEquilibrium { fn starting_pressure_ideal_gas( eos: &Arc, - temperature: QuantityScalar, + temperature: SINumber, molefracs_spec: &Array1, bubble: bool, - ) -> EosResult<(QuantityScalar, Array1)> + ) -> EosResult<(SINumber, Array1)> where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { if bubble { Self::starting_pressure_ideal_gas_bubble(eos, temperature, molefracs_spec) @@ -221,18 +219,18 @@ impl PhaseEquilibrium { pub(super) fn starting_pressure_ideal_gas_bubble( eos: &Arc, - temperature: QuantityScalar, + temperature: SINumber, liquid_molefracs: &Array1, - ) -> EosResult<(QuantityScalar, Array1)> { - let m = liquid_molefracs * U::reference_moles(); + ) -> EosResult<(SINumber, Array1)> { + 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 p_l = liquid.pressure(Contributions::Total); let mu_l = liquid.chemical_potential(Contributions::ResidualNvt); - let p_i = (temperature * density * U::gas_constant() * liquid_molefracs) + let p_i = (temperature * density * SIUnit::gas_constant() * liquid_molefracs) * (mu_l - p_l * v_l) - .to_reduced(U::gas_constant() * temperature)? + .to_reduced(SIUnit::gas_constant() * temperature)? .mapv(f64::exp); let y = p_i.to_reduced(p_i.sum())?; Ok((p_i.sum(), y)) @@ -240,17 +238,17 @@ impl PhaseEquilibrium { fn starting_pressure_ideal_gas_dew( eos: &Arc, - temperature: QuantityScalar, + temperature: SINumber, vapor_molefracs: &Array1, - ) -> EosResult<(QuantityScalar, Array1)> + ) -> EosResult<(SINumber, Array1)> where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { - let mut p: Option> = None; + let mut p: Option = None; let mut x = vapor_molefracs.clone(); for _ in 0..5 { - let m = x * U::reference_moles(); + 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); @@ -258,9 +256,9 @@ impl PhaseEquilibrium { let mu_l = liquid.chemical_potential(Contributions::ResidualNvt); let k = vapor_molefracs / (mu_l - p_l * v_l) - .to_reduced(U::gas_constant() * temperature)? + .to_reduced(SIUnit::gas_constant() * temperature)? .mapv(f64::exp); - let p_new = U::gas_constant() * temperature * density / k.sum(); + let p_new = SIUnit::gas_constant() * temperature * density / k.sum(); x = &k / k.sum(); if let Some(p_old) = p { if (p_new - p_old).to_reduced(p_old)?.abs() < 1e-5 { @@ -275,32 +273,32 @@ impl PhaseEquilibrium { pub(super) fn starting_pressure_spinodal( eos: &Arc, - temperature: QuantityScalar, + temperature: SINumber, molefracs: &Array1, - ) -> EosResult> + ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { - let moles = molefracs * U::reference_moles(); + let moles = molefracs * SIUnit::reference_moles(); let [sp_v, sp_l] = State::spinodal(eos, temperature, Some(&moles), Default::default())?; let pv = sp_v.pressure(Contributions::Total); let pl = sp_l.pressure(Contributions::Total); - Ok(0.5 * ((0.0 * U::reference_pressure()).max(pl)? + pv)) + Ok(0.5 * ((0.0 * SIUnit::reference_pressure()).max(pl)? + pv)) } } -fn starting_x2_bubble( +fn starting_x2_bubble( eos: &Arc, - temperature: QuantityScalar, - pressure: QuantityScalar, + temperature: SINumber, + pressure: SINumber, liquid_molefracs: &Array1, vapor_molefracs: Option<&Array1>, -) -> EosResult<[State; 2]> { +) -> EosResult<[State; 2]> { let liquid_state = State::new_npt( eos, temperature, pressure, - &(liquid_molefracs.clone() * U::reference_moles()), + &(liquid_molefracs.clone() * SIUnit::reference_moles()), Liquid, )?; let xv = match vapor_molefracs { @@ -311,24 +309,24 @@ fn starting_x2_bubble( eos, temperature, pressure, - &(xv * U::reference_moles()), + &(xv * SIUnit::reference_moles()), Vapor, )?; Ok([liquid_state, vapor_state]) } -fn starting_x2_dew( +fn starting_x2_dew( eos: &Arc, - temperature: QuantityScalar, - pressure: QuantityScalar, + temperature: SINumber, + pressure: SINumber, vapor_molefracs: &Array1, liquid_molefracs: Option<&Array1>, -) -> EosResult<[State; 2]> { +) -> EosResult<[State; 2]> { let vapor_state = State::new_npt( eos, temperature, pressure, - &(vapor_molefracs.clone() * U::reference_moles()), + &(vapor_molefracs.clone() * SIUnit::reference_moles()), Vapor, )?; let xl = match liquid_molefracs { @@ -339,7 +337,7 @@ fn starting_x2_dew( eos, temperature, pressure, - &(xl * U::reference_moles()), + &(xl * SIUnit::reference_moles()), Liquid, )?; (vapor_state.ln_phi() - liquid_state.ln_phi()).mapv(f64::exp) * vapor_molefracs @@ -349,21 +347,21 @@ fn starting_x2_dew( eos, temperature, pressure, - &(xl * U::reference_moles()), + &(xl * SIUnit::reference_moles()), Liquid, )?; Ok([vapor_state, liquid_state]) } -fn bubble_dew( - tp_spec: TPSpec, - mut var_tp: TPSpec, - mut state1: State, - mut state2: State, +fn bubble_dew( + tp_spec: TPSpec, + mut var_tp: TPSpec, + mut state1: State, + mut state2: State, options: (SolverOptions, SolverOptions), -) -> EosResult> +) -> EosResult> where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let (options_inner, options_outer) = options; @@ -443,14 +441,14 @@ where } } -fn adjust_t_p( - var: &mut TPSpec, - state1: &mut State, - state2: &mut State, +fn adjust_t_p( + var: &mut TPSpec, + state1: &mut State, + state2: &mut State, verbosity: Verbosity, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { // calculate K = phi_1/phi_2 = x_2/x_1 let ln_phi_1 = state1.ln_phi(); @@ -469,10 +467,10 @@ where let mut tstep = -f / df; // catch too big t-steps - if tstep < -MAX_TSTEP * U::reference_temperature() { - tstep = -MAX_TSTEP * U::reference_temperature(); - } else if tstep > MAX_TSTEP * U::reference_temperature() { - tstep = MAX_TSTEP * U::reference_temperature(); + if tstep < -MAX_TSTEP * SIUnit::reference_temperature() { + tstep = -MAX_TSTEP * SIUnit::reference_temperature(); + } else if tstep > MAX_TSTEP * SIUnit::reference_temperature() { + tstep = MAX_TSTEP * SIUnit::reference_temperature(); } // Update t @@ -511,11 +509,11 @@ where Ok(f.abs()) } -fn adjust_states( - var: &TPSpec, - state1: &mut State, - state2: &mut State, - moles_state2: Option<&QuantityArray1>, +fn adjust_states( + var: &TPSpec, + state1: &mut State, + state2: &mut State, + moles_state2: Option<&SIArray1>, ) -> EosResult<()> { let (temperature, pressure) = match var { TPSpec::Pressure(p) => (state1.temperature, *p), @@ -538,9 +536,9 @@ fn adjust_states( Ok(()) } -fn adjust_x2( - state1: &State, - state2: &mut State, +fn adjust_x2( + state1: &State, + state2: &mut State, verbosity: Verbosity, ) -> EosResult { let x1 = &state1.molefracs; @@ -554,21 +552,21 @@ fn adjust_x2( &state2.eos, state2.temperature, state2.pressure(Contributions::Total), - &(x2 * U::reference_moles()), + &(x2 * SIUnit::reference_moles()), InitialDensity(state2.density), )?; Ok(err_out) } -fn newton_step( - tp_spec: TPSpec, - var: &mut TPSpec, - state1: &mut State, - state2: &mut State, +fn newton_step( + tp_spec: TPSpec, + var: &mut TPSpec, + state1: &mut State, + state2: &mut State, verbosity: Verbosity, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { match tp_spec { TPSpec::Temperature(_) => newton_step_t(var, state1, state2, verbosity), @@ -576,37 +574,37 @@ where } } -fn newton_step_t( - pressure: &mut TPSpec, - state1: &mut State, - state2: &mut State, +fn newton_step_t( + pressure: &mut TPSpec, + state1: &mut State, + state2: &mut State, verbosity: Verbosity, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let dmu_drho_1 = (state1.dmu_dni(Contributions::Total) * state1.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())? + .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())? .dot(&state1.molefracs); let dmu_drho_2 = (state2.dmu_dni(Contributions::Total) * state2.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())?; + .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; let dp_drho_1 = (state1.dp_dni(Contributions::Total) * state1.volume) - .to_reduced(U::reference_pressure() / U::reference_density())? + .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())? .dot(&state1.molefracs); let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume) - .to_reduced(U::reference_pressure() / U::reference_density())?; + .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; let mu_1 = state1 .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; + .to_reduced(SIUnit::reference_molar_energy())?; let mu_2 = state2 .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; + .to_reduced(SIUnit::reference_molar_energy())?; let p_1 = state1 .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; + .to_reduced(SIUnit::reference_pressure())?; let p_2 = state2 .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; + .to_reduced(SIUnit::reference_pressure())?; // calculate residual let res = concatenate![Axis(0), mu_1 - &mu_2, arr1(&[p_1 - p_2])]; @@ -627,9 +625,9 @@ where let dx = LU::new(jacobian)?.solve(&res); // apply Newton step - let rho_l1 = state1.density - dx[dx.len() - 1] * U::reference_density(); + let rho_l1 = state1.density - dx[dx.len() - 1] * SIUnit::reference_density(); let rho_l2 = - &state2.partial_density - &(dx.slice(s![0..-1]).to_owned() * U::reference_density()); + &state2.partial_density - &(dx.slice(s![0..-1]).to_owned() * SIUnit::reference_density()); // update states *state1 = StateBuilder::new(&state1.eos) @@ -653,51 +651,51 @@ where Ok(error) } -fn newton_step_p( - pressure: QuantityScalar, - temperature: &mut TPSpec, - state1: &mut State, - state2: &mut State, +fn newton_step_p( + pressure: SINumber, + temperature: &mut TPSpec, + state1: &mut State, + state2: &mut State, verbosity: Verbosity, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let dmu_drho_1 = (state1.dmu_dni(Contributions::Total) * state1.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())? + .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())? .dot(&state1.molefracs); let dmu_drho_2 = (state2.dmu_dni(Contributions::Total) * state2.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())?; + .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; let dmu_dt_1 = state1 .dmu_dt(Contributions::Total) - .to_reduced(U::reference_molar_energy() / U::reference_temperature())?; + .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; let dmu_dt_2 = state2 .dmu_dt(Contributions::Total) - .to_reduced(U::reference_molar_energy() / U::reference_temperature())?; + .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; let dp_drho_1 = (state1.dp_dni(Contributions::Total) * state1.volume) - .to_reduced(U::reference_pressure() / U::reference_density())? + .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())? .dot(&state1.molefracs); let dp_dt_1 = state1 .dp_dt(Contributions::Total) - .to_reduced(U::reference_pressure() / U::reference_temperature())?; + .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; let dp_dt_2 = state2 .dp_dt(Contributions::Total) - .to_reduced(U::reference_pressure() / U::reference_temperature())?; + .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume) - .to_reduced(U::reference_pressure() / U::reference_density())?; + .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; let mu_1 = state1 .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; + .to_reduced(SIUnit::reference_molar_energy())?; let mu_2 = state2 .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; + .to_reduced(SIUnit::reference_molar_energy())?; let p_1 = state1 .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; + .to_reduced(SIUnit::reference_pressure())?; let p_2 = state2 .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; - let p = pressure.to_reduced(U::reference_pressure())?; + .to_reduced(SIUnit::reference_pressure())?; + 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])]; @@ -728,10 +726,10 @@ where let dx = LU::new(jacobian)?.solve(&res); // apply Newton step - let rho_l1 = state1.density - dx[dx.len() - 2] * U::reference_density(); + let rho_l1 = state1.density - dx[dx.len() - 2] * SIUnit::reference_density(); let rho_l2 = - &state2.partial_density - &(dx.slice(s![0..-2]).to_owned() * U::reference_density()); - let t = state1.temperature - dx[dx.len() - 1] * U::reference_temperature(); + &state2.partial_density - &(dx.slice(s![0..-2]).to_owned() * SIUnit::reference_density()); + let t = state1.temperature - dx[dx.len() - 1] * SIUnit::reference_temperature(); // update states *state1 = StateBuilder::new(&state1.eos) diff --git a/feos-core/src/phase_equilibria/mod.rs b/feos-core/src/phase_equilibria/mod.rs index 91c3d75d4..7c020e352 100644 --- a/feos-core/src/phase_equilibria/mod.rs +++ b/feos-core/src/phase_equilibria/mod.rs @@ -2,7 +2,7 @@ use crate::equation_of_state::EquationOfState; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State}; use crate::EosUnit; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; use std::fmt; use std::fmt::Write; use std::sync::Arc; @@ -101,18 +101,18 @@ impl SolverOptions { /// + [Pure component phase equilibria](#pure-component-phase-equilibria) /// + [Utility functions](#utility-functions) #[derive(Debug)] -pub struct PhaseEquilibrium([State; N]); +pub struct PhaseEquilibrium([State; N]); -impl Clone for PhaseEquilibrium { +impl Clone for PhaseEquilibrium { fn clone(&self) -> Self { Self(self.0.clone()) } } -impl fmt::Display for PhaseEquilibrium +impl fmt::Display for PhaseEquilibrium where - QuantityScalar: fmt::Display, - QuantityArray1: fmt::Display, + SINumber: fmt::Display, + SIArray1: fmt::Display, E: EquationOfState, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -123,10 +123,10 @@ where } } -impl PhaseEquilibrium +impl PhaseEquilibrium where - QuantityScalar: fmt::Display, - QuantityArray1: fmt::Display, + SINumber: fmt::Display, + SIArray1: fmt::Display, E: EquationOfState, { pub fn _repr_markdown_(&self) -> String { @@ -161,32 +161,32 @@ where } } -impl PhaseEquilibrium { - pub fn vapor(&self) -> &State { +impl PhaseEquilibrium { + pub fn vapor(&self) -> &State { &self.0[0] } - pub fn liquid(&self) -> &State { + pub fn liquid(&self) -> &State { &self.0[1] } } -impl PhaseEquilibrium { - pub fn vapor(&self) -> &State { +impl PhaseEquilibrium { + pub fn vapor(&self) -> &State { &self.0[0] } - pub fn liquid1(&self) -> &State { + pub fn liquid1(&self) -> &State { &self.0[1] } - pub fn liquid2(&self) -> &State { + pub fn liquid2(&self) -> &State { &self.0[2] } } -impl PhaseEquilibrium { - pub(super) fn from_states(state1: State, state2: State) -> Self { +impl PhaseEquilibrium { + pub(super) fn from_states(state1: State, state2: State) -> Self { let (vapor, liquid) = if state1.density < state2.density { (state1, state2) } else { @@ -197,10 +197,10 @@ impl PhaseEquilibrium { pub(super) fn new_npt( eos: &Arc, - temperature: QuantityScalar, - pressure: QuantityScalar, - vapor_moles: &QuantityArray1, - liquid_moles: &QuantityArray1, + temperature: SINumber, + pressure: SINumber, + vapor_moles: &SIArray1, + liquid_moles: &SIArray1, ) -> EosResult { let liquid = State::new_npt( eos, @@ -226,11 +226,11 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn update_pressure( mut self, - temperature: QuantityScalar, - pressure: QuantityScalar, + temperature: SINumber, + pressure: SINumber, ) -> EosResult { for s in self.0.iter_mut() { *s = State::new_npt( @@ -246,8 +246,8 @@ impl PhaseEquilibrium { pub(super) fn update_moles( &mut self, - pressure: QuantityScalar, - moles: [&QuantityArray1; N], + pressure: SINumber, + moles: [&SIArray1; N], ) -> EosResult<()> { for (i, s) in self.0.iter_mut().enumerate() { *s = State::new_npt( @@ -261,27 +261,26 @@ impl PhaseEquilibrium { Ok(()) } - pub fn update_chemical_potential( - &mut self, - chemical_potential: &QuantityArray1, - ) -> EosResult<()> { + 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(super) fn total_gibbs_energy(&self) -> QuantityScalar { - self.0.iter().fold(0.0 * U::reference_energy(), |acc, s| { - acc + s.gibbs_energy(Contributions::Total) - }) + 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) + }) } } 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) @@ -291,14 +290,14 @@ impl PhaseEquilibrium { } /// Check if the two states form a trivial solution - pub fn is_trivial_solution(state1: &State, state2: &State) -> bool { + pub fn is_trivial_solution(state1: &State, state2: &State) -> bool { let rho1 = state1 .partial_density - .to_reduced(U::reference_density()) + .to_reduced(SIUnit::reference_density()) .unwrap(); let rho2 = state2 .partial_density - .to_reduced(U::reference_density()) + .to_reduced(SIUnit::reference_density()) .unwrap(); rho1.iter() diff --git a/feos-core/src/phase_equilibria/phase_diagram_binary.rs b/feos-core/src/phase_equilibria/phase_diagram_binary.rs index bb0be4836..746fc5da0 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_binary.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_binary.rs @@ -1,705 +1,690 @@ -use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; -use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State, StateBuilder, TPSpec}; -use crate::EosUnit; -use ndarray::{arr1, arr2, concatenate, s, Array1, Array2, Axis}; -use num_dual::linalg::{norm, LU}; -use quantity::{QuantityArray1, QuantityScalar}; -use std::convert::{TryFrom, TryInto}; -use std::sync::Arc; - -const DEFAULT_POINTS: usize = 51; - -impl PhaseDiagram { - /// Create a new binary phase diagram exhibiting a - /// vapor/liquid equilibrium. - /// - /// If a heteroazeotrope occurs and the composition of the liquid - /// phases are known, they can be passed as `x_lle` to avoid - /// the calculation of unstable branches. - pub fn binary_vle( - eos: &Arc, - temperature_or_pressure: QuantityScalar, - npoints: Option, - x_lle: Option<(f64, f64)>, - bubble_dew_options: (SolverOptions, SolverOptions), - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let npoints = npoints.unwrap_or(DEFAULT_POINTS); - let tp = temperature_or_pressure.try_into()?; - - // calculate boiling temperature/vapor pressure of pure components - let vle_sat = PhaseEquilibrium::vle_pure_comps(eos, temperature_or_pressure); - let vle_sat = [vle_sat[1].clone(), vle_sat[0].clone()]; - - // Only calculate up to specified compositions - if let Some(x_lle) = x_lle { - let (states1, states2) = - Self::calculate_vlle(eos, tp, npoints, x_lle, vle_sat, bubble_dew_options)?; - - let states = states1 - .into_iter() - .chain(states2.into_iter().rev()) - .collect(); - return Ok(Self { states }); - } - - // use dew point when calculating a supercritical tx diagram - let bubble = match tp { - TPSpec::Temperature(_) => true, - TPSpec::Pressure(_) => false, - }; - - // look for supercritical components - let (x_lim, vle_lim, bubble) = match vle_sat { - [None, None] => return Err(EosError::SuperCritical), - [Some(vle2), None] => { - let cp = State::critical_point_binary( - eos, - temperature_or_pressure, - None, - None, - SolverOptions::default(), - )?; - let cp_vle = PhaseEquilibrium::from_states(cp.clone(), cp.clone()); - ([0.0, cp.molefracs[0]], (vle2, cp_vle), bubble) - } - [None, Some(vle1)] => { - let cp = State::critical_point_binary( - eos, - temperature_or_pressure, - None, - None, - SolverOptions::default(), - )?; - let cp_vle = PhaseEquilibrium::from_states(cp.clone(), cp.clone()); - ([1.0, cp.molefracs[0]], (vle1, cp_vle), bubble) - } - [Some(vle2), Some(vle1)] => ([0.0, 1.0], (vle2, vle1), true), - }; - - let mut states = iterate_vle( - eos, - tp, - &x_lim, - vle_lim.0, - Some(vle_lim.1), - npoints, - bubble, - bubble_dew_options, - ); - if !bubble { - states = states.into_iter().rev().collect(); - } - Ok(Self { states }) - } - - #[allow(clippy::type_complexity)] - fn calculate_vlle( - eos: &Arc, - tp: TPSpec, - npoints: usize, - x_lle: (f64, f64), - vle_sat: [Option>; 2], - bubble_dew_options: (SolverOptions, SolverOptions), - ) -> EosResult<( - Vec>, - Vec>, - )> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - match vle_sat { - [Some(vle2), Some(vle1)] => { - let states1 = iterate_vle( - eos, - tp, - &[0.0, x_lle.0], - vle2, - None, - npoints / 2, - true, - bubble_dew_options, - ); - let states2 = iterate_vle( - eos, - tp, - &[1.0, x_lle.1], - vle1, - None, - npoints - npoints / 2, - true, - bubble_dew_options, - ); - Ok((states1, states2)) - } - _ => Err(EosError::SuperCritical), - } - } - - /// Create a new phase diagram using Tp flash calculations. - /// - /// The usual use case for this function is the calculation of - /// liquid-liquid phase diagrams, but it can be used for vapor- - /// liquid diagrams as well, as long as the feed composition is - /// in a two phase region. - pub fn lle( - eos: &Arc, - temperature_or_pressure: QuantityScalar, - feed: &QuantityArray1, - min_tp: QuantityScalar, - max_tp: QuantityScalar, - npoints: Option, - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let npoints = npoints.unwrap_or(DEFAULT_POINTS); - let mut states = Vec::with_capacity(npoints); - let tp: TPSpec = temperature_or_pressure.try_into()?; - - let tp_vec = QuantityArray1::linspace(min_tp, max_tp, npoints)?; - let mut vle = None; - for i in 0..npoints { - let (_, t, p) = tp.temperature_pressure(tp_vec.get(i)); - vle = PhaseEquilibrium::tp_flash( - eos, - t, - p, - feed, - vle.as_ref(), - SolverOptions::default(), - None, - ) - .ok(); - if let Some(vle) = &vle { - states.push(vle.clone()); - } - } - Ok(Self { states }) - } -} - -fn iterate_vle( - eos: &Arc, - tp: TPSpec, - x_lim: &[f64], - vle_0: PhaseEquilibrium, - vle_1: Option>, - npoints: usize, - bubble: bool, - bubble_dew_options: (SolverOptions, SolverOptions), -) -> Vec> -where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, -{ - let mut vle_vec = Vec::with_capacity(npoints); - - let x = Array1::linspace(x_lim[0], x_lim[1], npoints); - let x = if vle_1.is_some() { - x.slice(s![1..-1]) - } else { - x.slice(s![1..]) - }; - - let tp_0 = Some(vle_0.vapor().tp(tp)); - let mut tp_old = tp_0; - let mut y_old = None; - vle_vec.push(vle_0); - for xi in x { - let vle = PhaseEquilibrium::bubble_dew_point( - eos, - tp, - tp_old, - &arr1(&[*xi, 1.0 - xi]), - y_old.as_ref(), - bubble, - bubble_dew_options, - ); - - if let Ok(vle) = vle { - y_old = Some(if bubble { - vle.vapor().molefracs.clone() - } else { - vle.liquid().molefracs.clone() - }); - tp_old = Some(match tp { - TPSpec::Temperature(_) => vle.vapor().pressure(Contributions::Total), - TPSpec::Pressure(_) => vle.vapor().temperature, - }); - vle_vec.push(vle.clone()); - } else { - y_old = None; - tp_old = tp_0; - } - } - if let Some(vle_1) = vle_1 { - vle_vec.push(vle_1); - } - - vle_vec -} - -impl State { - fn tp(&self, tp: TPSpec) -> QuantityScalar { - match tp { - TPSpec::Temperature(_) => self.pressure(Contributions::Total), - TPSpec::Pressure(_) => self.temperature, - } - } -} - -/// Phase diagram (Txy or pxy) for a system with heteroazeotropic phase behavior. -pub struct PhaseDiagramHetero { - pub vle1: PhaseDiagram, - pub vle2: PhaseDiagram, - pub lle: Option>, -} - -impl PhaseDiagram { - /// Create a new binary phase diagram exhibiting a - /// vapor/liquid/liquid equilibrium. - /// - /// The `x_lle` parameter is used as initial values for the calculation - /// of the heteroazeotrope. - pub fn binary_vlle( - eos: &Arc, - temperature_or_pressure: QuantityScalar, - x_lle: (f64, f64), - tp_lim_lle: Option>, - tp_init_vlle: Option>, - npoints_vle: Option, - npoints_lle: Option, - bubble_dew_options: (SolverOptions, SolverOptions), - ) -> EosResult> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let npoints_vle = npoints_vle.unwrap_or(DEFAULT_POINTS); - let tp = temperature_or_pressure.try_into()?; - - // calculate pure components - let vle_sat = PhaseEquilibrium::vle_pure_comps(eos, temperature_or_pressure); - let vle_sat = [vle_sat[1].clone(), vle_sat[0].clone()]; - - // calculate heteroazeotrope - let vlle = match tp { - TPSpec::Temperature(t) => PhaseEquilibrium::heteroazeotrope_t( - eos, - t, - x_lle, - tp_init_vlle, - SolverOptions::default(), - bubble_dew_options, - ), - TPSpec::Pressure(p) => PhaseEquilibrium::heteroazeotrope_p( - eos, - p, - x_lle, - tp_init_vlle, - SolverOptions::default(), - bubble_dew_options, - ), - }?; - let x_hetero = (vlle.liquid1().molefracs[0], vlle.liquid2().molefracs[0]); - - // calculate vapor liquid equilibria - let (dia1, dia2) = PhaseDiagram::calculate_vlle( - eos, - tp, - npoints_vle, - x_hetero, - vle_sat, - bubble_dew_options, - )?; - - // calculate liquid liquid equilibrium - let lle = tp_lim_lle - .map(|tp_lim| { - let tp_hetero = match tp { - TPSpec::Pressure(_) => vlle.vapor().temperature, - TPSpec::Temperature(_) => vlle.vapor().pressure(Contributions::Total), - }; - let x_feed = 0.5 * (x_hetero.0 + x_hetero.1); - let feed = arr1(&[x_feed, 1.0 - x_feed]) * U::reference_moles(); - PhaseDiagram::lle( - eos, - temperature_or_pressure, - &feed, - tp_lim, - tp_hetero, - npoints_lle, - ) - }) - .transpose()?; - - Ok(PhaseDiagramHetero { - vle1: PhaseDiagram::new(dia1), - vle2: PhaseDiagram::new(dia2), - lle, - }) - } -} - -impl PhaseDiagramHetero { - pub fn vle(&self) -> PhaseDiagram { - PhaseDiagram::new( - self.vle1 - .states - .iter() - .chain(self.vle2.states.iter().rev()) - .cloned() - .collect(), - ) - } -} - -const MAX_ITER_HETERO: usize = 50; -const TOL_HETERO: f64 = 1e-8; - -/// # Heteroazeotropes -impl PhaseEquilibrium -where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, -{ - /// Calculate a heteroazeotrope (three phase equilbrium) for a binary - /// system and given pressure. - pub fn heteroazeotrope( - eos: &Arc, - temperature_or_pressure: QuantityScalar, - x_init: (f64, f64), - tp_init: Option>, - options: SolverOptions, - bubble_dew_options: (SolverOptions, SolverOptions), - ) -> EosResult { - match TPSpec::try_from(temperature_or_pressure)? { - TPSpec::Temperature(t) => { - Self::heteroazeotrope_t(eos, t, x_init, tp_init, options, bubble_dew_options) - } - TPSpec::Pressure(p) => { - Self::heteroazeotrope_p(eos, p, x_init, tp_init, options, bubble_dew_options) - } - } - } - - /// Calculate a heteroazeotrope (three phase equilbrium) for a binary - /// system and given temperature. - fn heteroazeotrope_t( - eos: &Arc, - temperature: QuantityScalar, - x_init: (f64, f64), - p_init: Option>, - options: SolverOptions, - bubble_dew_options: (SolverOptions, SolverOptions), - ) -> EosResult { - // calculate initial values using bubble point - let x1 = arr1(&[x_init.0, 1.0 - x_init.0]); - let x2 = arr1(&[x_init.1, 1.0 - x_init.1]); - let vle1 = PhaseEquilibrium::bubble_point( - eos, - temperature, - &x1, - p_init, - None, - bubble_dew_options, - )?; - let vle2 = PhaseEquilibrium::bubble_point( - eos, - temperature, - &x2, - p_init, - None, - bubble_dew_options, - )?; - let mut l1 = vle1.liquid().clone(); - let mut l2 = vle2.liquid().clone(); - let p0 = (vle1.vapor().pressure(Contributions::Total) - + vle2.vapor().pressure(Contributions::Total)) - * 0.5; - let nv0 = (&vle1.vapor().moles + &vle2.vapor().moles) * 0.5; - let mut v = State::new_npt(eos, temperature, p0, &nv0, DensityInitialization::Vapor)?; - - for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) { - // calculate properties - let dmu_drho_l1 = (l1.dmu_dni(Contributions::Total) * l1.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())?; - let dmu_drho_l2 = (l2.dmu_dni(Contributions::Total) * l2.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())?; - let dmu_drho_v = (v.dmu_dni(Contributions::Total) * v.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())?; - let dp_drho_l1 = (l1.dp_dni(Contributions::Total) * l1.volume) - .to_reduced(U::reference_pressure() / U::reference_density())?; - let dp_drho_l2 = (l2.dp_dni(Contributions::Total) * l2.volume) - .to_reduced(U::reference_pressure() / U::reference_density())?; - let dp_drho_v = (v.dp_dni(Contributions::Total) * v.volume) - .to_reduced(U::reference_pressure() / U::reference_density())?; - let mu_l1 = l1 - .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; - let mu_l2 = l2 - .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; - let mu_v = v - .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; - let p_l1 = l1 - .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; - let p_l2 = l2 - .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; - let p_v = v - .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; - - // calculate residual - let res = concatenate![ - Axis(0), - mu_l1 - &mu_v, - mu_l2 - &mu_v, - arr1(&[p_l1 - p_v]), - arr1(&[p_l2 - p_v]) - ]; - - // check for convergence - if norm(&res) < options.tol.unwrap_or(TOL_HETERO) { - return Ok(Self([v, l1, l2])); - } - - // calculate Jacobian - let jacobian = concatenate![ - Axis(1), - concatenate![ - Axis(0), - dmu_drho_l1, - Array2::zeros((2, 2)), - dp_drho_l1.insert_axis(Axis(0)), - Array2::zeros((1, 2)) - ], - concatenate![ - Axis(0), - Array2::zeros((2, 2)), - dmu_drho_l2, - Array2::zeros((1, 2)), - dp_drho_l2.insert_axis(Axis(0)) - ], - concatenate![ - Axis(0), - -&dmu_drho_v, - -dmu_drho_v, - -dp_drho_v.clone().insert_axis(Axis(0)), - -dp_drho_v.insert_axis(Axis(0)) - ] - ]; - - // calculate Newton step - let dx = LU::new(jacobian)?.solve(&res); - - // apply Newton step - let rho_l1 = - &l1.partial_density - &(dx.slice(s![0..2]).to_owned() * U::reference_density()); - let rho_l2 = - &l2.partial_density - &(dx.slice(s![2..4]).to_owned() * U::reference_density()); - let rho_v = - &v.partial_density - &(dx.slice(s![4..6]).to_owned() * U::reference_density()); - - // check for negative densities - for i in 0..2 { - if rho_l1.get(i).is_sign_negative() - || rho_l2.get(i).is_sign_negative() - || rho_v.get(i).is_sign_negative() - { - return Err(EosError::IterationFailed(String::from( - "PhaseEquilibrium::heteroazeotrope_t", - ))); - } - } - - // update states - l1 = StateBuilder::new(eos) - .temperature(temperature) - .partial_density(&rho_l1) - .build()?; - l2 = StateBuilder::new(eos) - .temperature(temperature) - .partial_density(&rho_l2) - .build()?; - v = StateBuilder::new(eos) - .temperature(temperature) - .partial_density(&rho_v) - .build()?; - } - Err(EosError::NotConverged(String::from( - "PhaseEquilibrium::heteroazeotrope_t", - ))) - } - - /// Calculate a heteroazeotrope (three phase equilbrium) for a binary - /// system and given pressure. - fn heteroazeotrope_p( - eos: &Arc, - pressure: QuantityScalar, - x_init: (f64, f64), - t_init: Option>, - options: SolverOptions, - bubble_dew_options: (SolverOptions, SolverOptions), - ) -> EosResult { - let p = pressure.to_reduced(U::reference_pressure())?; - - // calculate initial values using bubble point - let x1 = arr1(&[x_init.0, 1.0 - x_init.0]); - let x2 = arr1(&[x_init.1, 1.0 - x_init.1]); - let vle1 = - PhaseEquilibrium::bubble_point(eos, pressure, &x1, t_init, None, bubble_dew_options)?; - let vle2 = - PhaseEquilibrium::bubble_point(eos, pressure, &x2, t_init, None, bubble_dew_options)?; - let mut l1 = vle1.liquid().clone(); - let mut l2 = vle2.liquid().clone(); - let t0 = (vle1.vapor().temperature + vle2.vapor().temperature) * 0.5; - let nv0 = (&vle1.vapor().moles + &vle2.vapor().moles) * 0.5; - let mut v = State::new_npt(eos, t0, pressure, &nv0, DensityInitialization::Vapor)?; - - for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) { - // calculate properties - let dmu_drho_l1 = (l1.dmu_dni(Contributions::Total) * l1.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())?; - let dmu_drho_l2 = (l2.dmu_dni(Contributions::Total) * l2.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())?; - let dmu_drho_v = (v.dmu_dni(Contributions::Total) * v.volume) - .to_reduced(U::reference_molar_energy() / U::reference_density())?; - let dmu_dt_l1 = (l1.dmu_dt(Contributions::Total)) - .to_reduced(U::reference_molar_energy() / U::reference_temperature())?; - let dmu_dt_l2 = (l2.dmu_dt(Contributions::Total)) - .to_reduced(U::reference_molar_energy() / U::reference_temperature())?; - let dmu_dt_v = (v.dmu_dt(Contributions::Total)) - .to_reduced(U::reference_molar_energy() / U::reference_temperature())?; - let dp_drho_l1 = (l1.dp_dni(Contributions::Total) * l1.volume) - .to_reduced(U::reference_pressure() / U::reference_density())?; - let dp_drho_l2 = (l2.dp_dni(Contributions::Total) * l2.volume) - .to_reduced(U::reference_pressure() / U::reference_density())?; - let dp_drho_v = (v.dp_dni(Contributions::Total) * v.volume) - .to_reduced(U::reference_pressure() / U::reference_density())?; - let dp_dt_l1 = (l1.dp_dt(Contributions::Total)) - .to_reduced(U::reference_pressure() / U::reference_temperature())?; - let dp_dt_l2 = (l2.dp_dt(Contributions::Total)) - .to_reduced(U::reference_pressure() / U::reference_temperature())?; - let dp_dt_v = (v.dp_dt(Contributions::Total)) - .to_reduced(U::reference_pressure() / U::reference_temperature())?; - let mu_l1 = l1 - .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; - let mu_l2 = l2 - .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; - let mu_v = v - .chemical_potential(Contributions::Total) - .to_reduced(U::reference_molar_energy())?; - let p_l1 = l1 - .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; - let p_l2 = l2 - .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; - let p_v = v - .pressure(Contributions::Total) - .to_reduced(U::reference_pressure())?; - - // calculate residual - let res = concatenate![ - Axis(0), - mu_l1 - &mu_v, - mu_l2 - &mu_v, - arr1(&[p_l1 - p]), - arr1(&[p_l2 - p]), - arr1(&[p_v - p]) - ]; - - // check for convergence - if norm(&res) < options.tol.unwrap_or(TOL_HETERO) { - return Ok(Self([v, l1, l2])); - } - - // calculate Jacobian - let jacobian = concatenate![ - Axis(1), - concatenate![ - Axis(0), - dmu_drho_l1, - Array2::zeros((2, 2)), - dp_drho_l1.insert_axis(Axis(0)), - Array2::zeros((1, 2)), - Array2::zeros((1, 2)) - ], - concatenate![ - Axis(0), - Array2::zeros((2, 2)), - dmu_drho_l2, - Array2::zeros((1, 2)), - dp_drho_l2.insert_axis(Axis(0)), - Array2::zeros((1, 2)) - ], - concatenate![ - Axis(0), - -&dmu_drho_v, - -dmu_drho_v, - Array2::zeros((1, 2)), - Array2::zeros((1, 2)), - dp_drho_v.insert_axis(Axis(0)) - ], - concatenate![ - Axis(0), - (dmu_dt_l1 - &dmu_dt_v).insert_axis(Axis(1)), - (dmu_dt_l2 - &dmu_dt_v).insert_axis(Axis(1)), - arr2(&[[dp_dt_l1]]), - arr2(&[[dp_dt_l2]]), - arr2(&[[dp_dt_v]]) - ] - ]; - - // calculate Newton step - let dx = LU::new(jacobian)?.solve(&res); - - // apply Newton step - let rho_l1 = - &l1.partial_density - &(dx.slice(s![0..2]).to_owned() * U::reference_density()); - let rho_l2 = - &l2.partial_density - &(dx.slice(s![2..4]).to_owned() * U::reference_density()); - let rho_v = - &v.partial_density - &(dx.slice(s![4..6]).to_owned() * U::reference_density()); - let t = v.temperature - dx[6] * U::reference_temperature(); - - // check for negative densities and temperatures - for i in 0..2 { - if rho_l1.get(i).is_sign_negative() - || rho_l2.get(i).is_sign_negative() - || rho_v.get(i).is_sign_negative() - || t.is_sign_negative() - { - return Err(EosError::IterationFailed(String::from( - "PhaseEquilibrium::heteroazeotrope_t", - ))); - } - } - - // update states - l1 = StateBuilder::new(eos) - .temperature(t) - .partial_density(&rho_l1) - .build()?; - l2 = StateBuilder::new(eos) - .temperature(t) - .partial_density(&rho_l2) - .build()?; - v = StateBuilder::new(eos) - .temperature(t) - .partial_density(&rho_v) - .build()?; - } - Err(EosError::NotConverged(String::from( - "PhaseEquilibrium::heteroazeotrope_t", - ))) - } -} +use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; +use crate::equation_of_state::EquationOfState; +use crate::errors::{EosError, EosResult}; +use crate::state::{Contributions, DensityInitialization, State, StateBuilder, TPSpec}; +use crate::EosUnit; +use ndarray::{arr1, arr2, concatenate, s, Array1, Array2, Axis}; +use num_dual::linalg::{norm, LU}; +use quantity::si::{SIArray1, SINumber, SIUnit}; +use std::convert::{TryFrom, TryInto}; +use std::sync::Arc; + +const DEFAULT_POINTS: usize = 51; + +impl PhaseDiagram { + /// Create a new binary phase diagram exhibiting a + /// vapor/liquid equilibrium. + /// + /// If a heteroazeotrope occurs and the composition of the liquid + /// phases are known, they can be passed as `x_lle` to avoid + /// the calculation of unstable branches. + pub fn binary_vle( + eos: &Arc, + temperature_or_pressure: SINumber, + npoints: Option, + x_lle: Option<(f64, f64)>, + bubble_dew_options: (SolverOptions, SolverOptions), + ) -> EosResult { + let npoints = npoints.unwrap_or(DEFAULT_POINTS); + let tp = temperature_or_pressure.try_into()?; + + // calculate boiling temperature/vapor pressure of pure components + let vle_sat = PhaseEquilibrium::vle_pure_comps(eos, temperature_or_pressure); + let vle_sat = [vle_sat[1].clone(), vle_sat[0].clone()]; + + // Only calculate up to specified compositions + if let Some(x_lle) = x_lle { + let (states1, states2) = + Self::calculate_vlle(eos, tp, npoints, x_lle, vle_sat, bubble_dew_options)?; + + let states = states1 + .into_iter() + .chain(states2.into_iter().rev()) + .collect(); + return Ok(Self { states }); + } + + // use dew point when calculating a supercritical tx diagram + let bubble = match tp { + TPSpec::Temperature(_) => true, + TPSpec::Pressure(_) => false, + }; + + // look for supercritical components + let (x_lim, vle_lim, bubble) = match vle_sat { + [None, None] => return Err(EosError::SuperCritical), + [Some(vle2), None] => { + let cp = State::critical_point_binary( + eos, + temperature_or_pressure, + None, + None, + SolverOptions::default(), + )?; + let cp_vle = PhaseEquilibrium::from_states(cp.clone(), cp.clone()); + ([0.0, cp.molefracs[0]], (vle2, cp_vle), bubble) + } + [None, Some(vle1)] => { + let cp = State::critical_point_binary( + eos, + temperature_or_pressure, + None, + None, + SolverOptions::default(), + )?; + let cp_vle = PhaseEquilibrium::from_states(cp.clone(), cp.clone()); + ([1.0, cp.molefracs[0]], (vle1, cp_vle), bubble) + } + [Some(vle2), Some(vle1)] => ([0.0, 1.0], (vle2, vle1), true), + }; + + let mut states = iterate_vle( + eos, + tp, + &x_lim, + vle_lim.0, + Some(vle_lim.1), + npoints, + bubble, + bubble_dew_options, + ); + if !bubble { + states = states.into_iter().rev().collect(); + } + Ok(Self { states }) + } + + #[allow(clippy::type_complexity)] + fn calculate_vlle( + eos: &Arc, + tp: TPSpec, + npoints: usize, + x_lle: (f64, f64), + vle_sat: [Option>; 2], + bubble_dew_options: (SolverOptions, SolverOptions), + ) -> EosResult<(Vec>, Vec>)> { + match vle_sat { + [Some(vle2), Some(vle1)] => { + let states1 = iterate_vle( + eos, + tp, + &[0.0, x_lle.0], + vle2, + None, + npoints / 2, + true, + bubble_dew_options, + ); + let states2 = iterate_vle( + eos, + tp, + &[1.0, x_lle.1], + vle1, + None, + npoints - npoints / 2, + true, + bubble_dew_options, + ); + Ok((states1, states2)) + } + _ => Err(EosError::SuperCritical), + } + } + + /// Create a new phase diagram using Tp flash calculations. + /// + /// The usual use case for this function is the calculation of + /// liquid-liquid phase diagrams, but it can be used for vapor- + /// liquid diagrams as well, as long as the feed composition is + /// in a two phase region. + pub fn lle( + eos: &Arc, + temperature_or_pressure: SINumber, + feed: &SIArray1, + min_tp: SINumber, + max_tp: SINumber, + npoints: Option, + ) -> EosResult { + let npoints = npoints.unwrap_or(DEFAULT_POINTS); + let mut states = Vec::with_capacity(npoints); + let tp: TPSpec = temperature_or_pressure.try_into()?; + + let tp_vec = SIArray1::linspace(min_tp, max_tp, npoints)?; + let mut vle = None; + for i in 0..npoints { + let (_, t, p) = tp.temperature_pressure(tp_vec.get(i)); + vle = PhaseEquilibrium::tp_flash( + eos, + t, + p, + feed, + vle.as_ref(), + SolverOptions::default(), + None, + ) + .ok(); + if let Some(vle) = &vle { + states.push(vle.clone()); + } + } + Ok(Self { states }) + } +} + +fn iterate_vle( + eos: &Arc, + tp: TPSpec, + x_lim: &[f64], + vle_0: PhaseEquilibrium, + vle_1: Option>, + npoints: usize, + bubble: bool, + bubble_dew_options: (SolverOptions, SolverOptions), +) -> Vec> +where + SINumber: std::fmt::Display + std::fmt::LowerExp, +{ + let mut vle_vec = Vec::with_capacity(npoints); + + let x = Array1::linspace(x_lim[0], x_lim[1], npoints); + let x = if vle_1.is_some() { + x.slice(s![1..-1]) + } else { + x.slice(s![1..]) + }; + + let tp_0 = Some(vle_0.vapor().tp(tp)); + let mut tp_old = tp_0; + let mut y_old = None; + vle_vec.push(vle_0); + for xi in x { + let vle = PhaseEquilibrium::bubble_dew_point( + eos, + tp, + tp_old, + &arr1(&[*xi, 1.0 - xi]), + y_old.as_ref(), + bubble, + bubble_dew_options, + ); + + if let Ok(vle) = vle { + y_old = Some(if bubble { + vle.vapor().molefracs.clone() + } else { + vle.liquid().molefracs.clone() + }); + tp_old = Some(match tp { + TPSpec::Temperature(_) => vle.vapor().pressure(Contributions::Total), + TPSpec::Pressure(_) => vle.vapor().temperature, + }); + vle_vec.push(vle.clone()); + } else { + y_old = None; + tp_old = tp_0; + } + } + if let Some(vle_1) = vle_1 { + vle_vec.push(vle_1); + } + + vle_vec +} + +impl State { + fn tp(&self, tp: TPSpec) -> SINumber { + match tp { + TPSpec::Temperature(_) => self.pressure(Contributions::Total), + TPSpec::Pressure(_) => self.temperature, + } + } +} + +/// Phase diagram (Txy or pxy) for a system with heteroazeotropic phase behavior. +pub struct PhaseDiagramHetero { + pub vle1: PhaseDiagram, + pub vle2: PhaseDiagram, + pub lle: Option>, +} + +impl PhaseDiagram { + /// Create a new binary phase diagram exhibiting a + /// vapor/liquid/liquid equilibrium. + /// + /// The `x_lle` parameter is used as initial values for the calculation + /// of the heteroazeotrope. + pub fn binary_vlle( + eos: &Arc, + temperature_or_pressure: SINumber, + x_lle: (f64, f64), + tp_lim_lle: Option, + tp_init_vlle: Option, + npoints_vle: Option, + npoints_lle: Option, + bubble_dew_options: (SolverOptions, SolverOptions), + ) -> EosResult> { + let npoints_vle = npoints_vle.unwrap_or(DEFAULT_POINTS); + let tp = temperature_or_pressure.try_into()?; + + // calculate pure components + let vle_sat = PhaseEquilibrium::vle_pure_comps(eos, temperature_or_pressure); + let vle_sat = [vle_sat[1].clone(), vle_sat[0].clone()]; + + // calculate heteroazeotrope + let vlle = match tp { + TPSpec::Temperature(t) => PhaseEquilibrium::heteroazeotrope_t( + eos, + t, + x_lle, + tp_init_vlle, + SolverOptions::default(), + bubble_dew_options, + ), + TPSpec::Pressure(p) => PhaseEquilibrium::heteroazeotrope_p( + eos, + p, + x_lle, + tp_init_vlle, + SolverOptions::default(), + bubble_dew_options, + ), + }?; + let x_hetero = (vlle.liquid1().molefracs[0], vlle.liquid2().molefracs[0]); + + // calculate vapor liquid equilibria + let (dia1, dia2) = PhaseDiagram::calculate_vlle( + eos, + tp, + npoints_vle, + x_hetero, + vle_sat, + bubble_dew_options, + )?; + + // calculate liquid liquid equilibrium + let lle = tp_lim_lle + .map(|tp_lim| { + let tp_hetero = match tp { + TPSpec::Pressure(_) => vlle.vapor().temperature, + TPSpec::Temperature(_) => vlle.vapor().pressure(Contributions::Total), + }; + let x_feed = 0.5 * (x_hetero.0 + x_hetero.1); + let feed = arr1(&[x_feed, 1.0 - x_feed]) * SIUnit::reference_moles(); + PhaseDiagram::lle( + eos, + temperature_or_pressure, + &feed, + tp_lim, + tp_hetero, + npoints_lle, + ) + }) + .transpose()?; + + Ok(PhaseDiagramHetero { + vle1: PhaseDiagram::new(dia1), + vle2: PhaseDiagram::new(dia2), + lle, + }) + } +} + +impl PhaseDiagramHetero { + pub fn vle(&self) -> PhaseDiagram { + PhaseDiagram::new( + self.vle1 + .states + .iter() + .chain(self.vle2.states.iter().rev()) + .cloned() + .collect(), + ) + } +} + +const MAX_ITER_HETERO: usize = 50; +const TOL_HETERO: f64 = 1e-8; + +/// # Heteroazeotropes +impl PhaseEquilibrium +where + SINumber: std::fmt::Display + std::fmt::LowerExp, +{ + /// Calculate a heteroazeotrope (three phase equilbrium) for a binary + /// system and given pressure. + pub fn heteroazeotrope( + eos: &Arc, + temperature_or_pressure: SINumber, + x_init: (f64, f64), + tp_init: Option, + options: SolverOptions, + bubble_dew_options: (SolverOptions, SolverOptions), + ) -> EosResult { + match TPSpec::try_from(temperature_or_pressure)? { + TPSpec::Temperature(t) => { + Self::heteroazeotrope_t(eos, t, x_init, tp_init, options, bubble_dew_options) + } + TPSpec::Pressure(p) => { + Self::heteroazeotrope_p(eos, p, x_init, tp_init, options, bubble_dew_options) + } + } + } + + /// Calculate a heteroazeotrope (three phase equilbrium) for a binary + /// system and given temperature. + fn heteroazeotrope_t( + eos: &Arc, + temperature: SINumber, + x_init: (f64, f64), + p_init: Option, + options: SolverOptions, + bubble_dew_options: (SolverOptions, SolverOptions), + ) -> EosResult { + // calculate initial values using bubble point + let x1 = arr1(&[x_init.0, 1.0 - x_init.0]); + let x2 = arr1(&[x_init.1, 1.0 - x_init.1]); + let vle1 = PhaseEquilibrium::bubble_point( + eos, + temperature, + &x1, + p_init, + None, + bubble_dew_options, + )?; + let vle2 = PhaseEquilibrium::bubble_point( + eos, + temperature, + &x2, + p_init, + None, + bubble_dew_options, + )?; + let mut l1 = vle1.liquid().clone(); + let mut l2 = vle2.liquid().clone(); + let p0 = (vle1.vapor().pressure(Contributions::Total) + + vle2.vapor().pressure(Contributions::Total)) + * 0.5; + let nv0 = (&vle1.vapor().moles + &vle2.vapor().moles) * 0.5; + let mut v = State::new_npt(eos, temperature, p0, &nv0, DensityInitialization::Vapor)?; + + for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) { + // calculate properties + let dmu_drho_l1 = (l1.dmu_dni(Contributions::Total) * l1.volume) + .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; + let dmu_drho_l2 = (l2.dmu_dni(Contributions::Total) * l2.volume) + .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 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) + .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) + .to_reduced(SIUnit::reference_molar_energy())?; + let mu_l2 = l2 + .chemical_potential(Contributions::Total) + .to_reduced(SIUnit::reference_molar_energy())?; + let mu_v = v + .chemical_potential(Contributions::Total) + .to_reduced(SIUnit::reference_molar_energy())?; + let p_l1 = l1 + .pressure(Contributions::Total) + .to_reduced(SIUnit::reference_pressure())?; + let p_l2 = l2 + .pressure(Contributions::Total) + .to_reduced(SIUnit::reference_pressure())?; + let p_v = v + .pressure(Contributions::Total) + .to_reduced(SIUnit::reference_pressure())?; + + // calculate residual + let res = concatenate![ + Axis(0), + mu_l1 - &mu_v, + mu_l2 - &mu_v, + arr1(&[p_l1 - p_v]), + arr1(&[p_l2 - p_v]) + ]; + + // check for convergence + if norm(&res) < options.tol.unwrap_or(TOL_HETERO) { + return Ok(Self([v, l1, l2])); + } + + // calculate Jacobian + let jacobian = concatenate![ + Axis(1), + concatenate![ + Axis(0), + dmu_drho_l1, + Array2::zeros((2, 2)), + dp_drho_l1.insert_axis(Axis(0)), + Array2::zeros((1, 2)) + ], + concatenate![ + Axis(0), + Array2::zeros((2, 2)), + dmu_drho_l2, + Array2::zeros((1, 2)), + dp_drho_l2.insert_axis(Axis(0)) + ], + concatenate![ + Axis(0), + -&dmu_drho_v, + -dmu_drho_v, + -dp_drho_v.clone().insert_axis(Axis(0)), + -dp_drho_v.insert_axis(Axis(0)) + ] + ]; + + // calculate Newton step + let dx = LU::new(jacobian)?.solve(&res); + + // apply Newton step + let rho_l1 = &l1.partial_density + - &(dx.slice(s![0..2]).to_owned() * SIUnit::reference_density()); + let rho_l2 = &l2.partial_density + - &(dx.slice(s![2..4]).to_owned() * SIUnit::reference_density()); + let rho_v = + &v.partial_density - &(dx.slice(s![4..6]).to_owned() * SIUnit::reference_density()); + + // check for negative densities + for i in 0..2 { + if rho_l1.get(i).is_sign_negative() + || rho_l2.get(i).is_sign_negative() + || rho_v.get(i).is_sign_negative() + { + return Err(EosError::IterationFailed(String::from( + "PhaseEquilibrium::heteroazeotrope_t", + ))); + } + } + + // update states + l1 = StateBuilder::new(eos) + .temperature(temperature) + .partial_density(&rho_l1) + .build()?; + l2 = StateBuilder::new(eos) + .temperature(temperature) + .partial_density(&rho_l2) + .build()?; + v = StateBuilder::new(eos) + .temperature(temperature) + .partial_density(&rho_v) + .build()?; + } + Err(EosError::NotConverged(String::from( + "PhaseEquilibrium::heteroazeotrope_t", + ))) + } + + /// Calculate a heteroazeotrope (three phase equilbrium) for a binary + /// system and given pressure. + fn heteroazeotrope_p( + eos: &Arc, + pressure: SINumber, + x_init: (f64, f64), + t_init: Option, + options: SolverOptions, + bubble_dew_options: (SolverOptions, SolverOptions), + ) -> EosResult { + let p = pressure.to_reduced(SIUnit::reference_pressure())?; + + // calculate initial values using bubble point + let x1 = arr1(&[x_init.0, 1.0 - x_init.0]); + let x2 = arr1(&[x_init.1, 1.0 - x_init.1]); + let vle1 = + PhaseEquilibrium::bubble_point(eos, pressure, &x1, t_init, None, bubble_dew_options)?; + let vle2 = + PhaseEquilibrium::bubble_point(eos, pressure, &x2, t_init, None, bubble_dew_options)?; + let mut l1 = vle1.liquid().clone(); + let mut l2 = vle2.liquid().clone(); + let t0 = (vle1.vapor().temperature + vle2.vapor().temperature) * 0.5; + let nv0 = (&vle1.vapor().moles + &vle2.vapor().moles) * 0.5; + let mut v = State::new_npt(eos, t0, pressure, &nv0, DensityInitialization::Vapor)?; + + for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) { + // calculate properties + let dmu_drho_l1 = (l1.dmu_dni(Contributions::Total) * l1.volume) + .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; + let dmu_drho_l2 = (l2.dmu_dni(Contributions::Total) * l2.volume) + .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 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) + .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 dp_dt_l1 = (l1.dp_dt(Contributions::Total)) + .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; + let dp_dt_l2 = (l2.dp_dt(Contributions::Total)) + .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) + .to_reduced(SIUnit::reference_molar_energy())?; + let mu_l2 = l2 + .chemical_potential(Contributions::Total) + .to_reduced(SIUnit::reference_molar_energy())?; + let mu_v = v + .chemical_potential(Contributions::Total) + .to_reduced(SIUnit::reference_molar_energy())?; + let p_l1 = l1 + .pressure(Contributions::Total) + .to_reduced(SIUnit::reference_pressure())?; + let p_l2 = l2 + .pressure(Contributions::Total) + .to_reduced(SIUnit::reference_pressure())?; + let p_v = v + .pressure(Contributions::Total) + .to_reduced(SIUnit::reference_pressure())?; + + // calculate residual + let res = concatenate![ + Axis(0), + mu_l1 - &mu_v, + mu_l2 - &mu_v, + arr1(&[p_l1 - p]), + arr1(&[p_l2 - p]), + arr1(&[p_v - p]) + ]; + + // check for convergence + if norm(&res) < options.tol.unwrap_or(TOL_HETERO) { + return Ok(Self([v, l1, l2])); + } + + // calculate Jacobian + let jacobian = concatenate![ + Axis(1), + concatenate![ + Axis(0), + dmu_drho_l1, + Array2::zeros((2, 2)), + dp_drho_l1.insert_axis(Axis(0)), + Array2::zeros((1, 2)), + Array2::zeros((1, 2)) + ], + concatenate![ + Axis(0), + Array2::zeros((2, 2)), + dmu_drho_l2, + Array2::zeros((1, 2)), + dp_drho_l2.insert_axis(Axis(0)), + Array2::zeros((1, 2)) + ], + concatenate![ + Axis(0), + -&dmu_drho_v, + -dmu_drho_v, + Array2::zeros((1, 2)), + Array2::zeros((1, 2)), + dp_drho_v.insert_axis(Axis(0)) + ], + concatenate![ + Axis(0), + (dmu_dt_l1 - &dmu_dt_v).insert_axis(Axis(1)), + (dmu_dt_l2 - &dmu_dt_v).insert_axis(Axis(1)), + arr2(&[[dp_dt_l1]]), + arr2(&[[dp_dt_l2]]), + arr2(&[[dp_dt_v]]) + ] + ]; + + // calculate Newton step + let dx = LU::new(jacobian)?.solve(&res); + + // apply Newton step + let rho_l1 = &l1.partial_density + - &(dx.slice(s![0..2]).to_owned() * SIUnit::reference_density()); + let rho_l2 = &l2.partial_density + - &(dx.slice(s![2..4]).to_owned() * SIUnit::reference_density()); + let rho_v = + &v.partial_density - &(dx.slice(s![4..6]).to_owned() * SIUnit::reference_density()); + let t = v.temperature - dx[6] * SIUnit::reference_temperature(); + + // check for negative densities and temperatures + for i in 0..2 { + if rho_l1.get(i).is_sign_negative() + || rho_l2.get(i).is_sign_negative() + || rho_v.get(i).is_sign_negative() + || t.is_sign_negative() + { + return Err(EosError::IterationFailed(String::from( + "PhaseEquilibrium::heteroazeotrope_t", + ))); + } + } + + // update states + l1 = StateBuilder::new(eos) + .temperature(t) + .partial_density(&rho_l1) + .build()?; + l2 = StateBuilder::new(eos) + .temperature(t) + .partial_density(&rho_l2) + .build()?; + v = StateBuilder::new(eos) + .temperature(t) + .partial_density(&rho_v) + .build()?; + } + Err(EosError::NotConverged(String::from( + "PhaseEquilibrium::heteroazeotrope_t", + ))) + } +} diff --git a/feos-core/src/phase_equilibria/phase_diagram_pure.rs b/feos-core/src/phase_equilibria/phase_diagram_pure.rs index 016e0bc16..08a986650 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_pure.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_pure.rs @@ -5,17 +5,17 @@ use crate::state::{State, StateVec}; use crate::EosUnit; #[cfg(feature = "rayon")] use ndarray::{Array1, ArrayView1, Axis}; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; #[cfg(feature = "rayon")] use rayon::{prelude::*, ThreadPool}; use std::sync::Arc; /// Pure component and binary mixture phase diagrams. -pub struct PhaseDiagram { - pub states: Vec>, +pub struct PhaseDiagram { + pub states: Vec>, } -impl Clone for PhaseDiagram { +impl Clone for PhaseDiagram { fn clone(&self) -> Self { Self { states: self.states.clone(), @@ -23,32 +23,29 @@ impl Clone for PhaseDiagram { } } -impl PhaseDiagram { +impl PhaseDiagram { /// Create a phase diagram from a list of phase equilibria. - pub fn new(states: Vec>) -> Self { + pub fn new(states: Vec>) -> Self { Self { states } } } -impl PhaseDiagram { +impl PhaseDiagram { /// Calculate a phase diagram for a pure component. pub fn pure( eos: &Arc, - min_temperature: QuantityScalar, + min_temperature: SINumber, npoints: usize, - critical_temperature: Option>, + critical_temperature: Option, options: SolverOptions, - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + ) -> EosResult { let mut states = Vec::with_capacity(npoints); let sc = State::critical_point(eos, None, critical_temperature, SolverOptions::default())?; let max_temperature = min_temperature + (sc.temperature - min_temperature) * ((npoints - 2) as f64 / (npoints - 1) as f64); - let temperatures = QuantityArray1::linspace(min_temperature, max_temperature, npoints - 1)?; + let temperatures = SIArray1::linspace(min_temperature, max_temperature, npoints - 1)?; let mut vle = None; for ti in &temperatures { @@ -63,32 +60,29 @@ impl PhaseDiagram { } /// Return the vapor states of the diagram. - pub fn vapor(&self) -> StateVec<'_, U, E> { + pub fn vapor(&self) -> StateVec<'_, E> { self.states.iter().map(|s| s.vapor()).collect() } /// Return the liquid states of the diagram. - pub fn liquid(&self) -> StateVec<'_, U, E> { + pub fn liquid(&self) -> StateVec<'_, E> { self.states.iter().map(|s| s.liquid()).collect() } } #[cfg(feature = "rayon")] -impl PhaseDiagram { +impl PhaseDiagram { fn solve_temperatures( eos: &Arc, temperatures: ArrayView1, options: SolverOptions, - ) -> EosResult>> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + ) -> EosResult>> { let mut states = Vec::with_capacity(temperatures.len()); let mut vle = None; for ti in temperatures { vle = PhaseEquilibrium::pure( eos, - *ti * U::reference_temperature(), + *ti * SIUnit::reference_temperature(), vle.as_ref(), options, ) @@ -102,27 +96,24 @@ impl PhaseDiagram { pub fn par_pure( eos: &Arc, - min_temperature: QuantityScalar, + min_temperature: SINumber, npoints: usize, chunksize: usize, thread_pool: ThreadPool, - critical_temperature: Option>, + critical_temperature: Option, options: SolverOptions, - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + ) -> EosResult { let sc = State::critical_point(eos, None, critical_temperature, SolverOptions::default())?; let max_temperature = min_temperature + (sc.temperature - min_temperature) * ((npoints - 2) as f64 / (npoints - 1) as f64); let temperatures = Array1::linspace( - min_temperature.to_reduced(U::reference_temperature())?, - max_temperature.to_reduced(U::reference_temperature())?, + min_temperature.to_reduced(SIUnit::reference_temperature())?, + max_temperature.to_reduced(SIUnit::reference_temperature())?, npoints - 1, ); - let mut states: Vec> = thread_pool.install(|| { + let mut states: Vec> = thread_pool.install(|| { temperatures .axis_chunks_iter(Axis(0), chunksize) .into_par_iter() diff --git a/feos-core/src/phase_equilibria/phase_envelope.rs b/feos-core/src/phase_equilibria/phase_envelope.rs index c05406423..f05ed3afe 100644 --- a/feos-core/src/phase_equilibria/phase_envelope.rs +++ b/feos-core/src/phase_equilibria/phase_envelope.rs @@ -2,23 +2,20 @@ use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; use crate::equation_of_state::EquationOfState; use crate::errors::EosResult; use crate::state::State; -use crate::{Contributions, EosUnit}; -use quantity::{QuantityArray1, QuantityScalar}; +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, - moles: &QuantityArray1, - min_temperature: QuantityScalar, + moles: &SIArray1, + min_temperature: SINumber, npoints: usize, - critical_temperature: Option>, + critical_temperature: Option, options: (SolverOptions, SolverOptions), - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + ) -> EosResult { let mut states = Vec::with_capacity(npoints); let sc = State::critical_point( @@ -30,10 +27,10 @@ impl PhaseDiagram { let max_temperature = min_temperature + (sc.temperature - min_temperature) * ((npoints - 2) as f64 / (npoints - 1) as f64); - let temperatures = QuantityArray1::linspace(min_temperature, max_temperature, npoints - 1)?; + let temperatures = SIArray1::linspace(min_temperature, max_temperature, npoints - 1)?; let molefracs = moles.to_reduced(moles.sum())?; - let mut vle: Option> = None; + let mut vle: Option> = None; for ti in &temperatures { // calculate new liquid point let p_init = vle @@ -62,15 +59,12 @@ impl PhaseDiagram { /// Calculate the dew point line of a mixture with given composition. pub fn dew_point_line( eos: &Arc, - moles: &QuantityArray1, - min_temperature: QuantityScalar, + moles: &SIArray1, + min_temperature: SINumber, npoints: usize, - critical_temperature: Option>, + critical_temperature: Option, options: (SolverOptions, SolverOptions), - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + ) -> EosResult { let mut states = Vec::with_capacity(npoints); let sc = State::critical_point( @@ -83,10 +77,10 @@ impl PhaseDiagram { let n_t = npoints / 2; let max_temperature = min_temperature + (sc.temperature - min_temperature) * ((n_t - 2) as f64 / (n_t - 1) as f64); - let temperatures = QuantityArray1::linspace(min_temperature, max_temperature, n_t - 1)?; + let temperatures = SIArray1::linspace(min_temperature, max_temperature, n_t - 1)?; let molefracs = moles.to_reduced(moles.sum())?; - let mut vle: Option> = None; + let mut vle: Option> = None; for ti in &temperatures { let p_init = vle .as_ref() @@ -109,7 +103,7 @@ impl PhaseDiagram { let p_c = sc.pressure(Contributions::Total); let max_pressure = min_pressure + (p_c - min_pressure) * ((n_p - 2) as f64 / (n_p - 1) as f64); - let pressures = QuantityArray1::linspace(min_pressure, max_pressure, n_p)?; + let pressures = SIArray1::linspace(min_pressure, max_pressure, n_p)?; for pi in &pressures { let t_init = vle.as_ref().map(|vle| vle.vapor().temperature); @@ -130,15 +124,12 @@ impl PhaseDiagram { /// Calculate the spinodal lines for a mixture with fixed composition. pub fn spinodal( eos: &Arc, - moles: &QuantityArray1, - min_temperature: QuantityScalar, + moles: &SIArray1, + min_temperature: SINumber, npoints: usize, - critical_temperature: Option>, + critical_temperature: Option, options: SolverOptions, - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + ) -> EosResult { let mut states = Vec::with_capacity(npoints); let sc = State::critical_point( @@ -150,7 +141,7 @@ impl PhaseDiagram { let max_temperature = min_temperature + (sc.temperature - min_temperature) * ((npoints - 2) as f64 / (npoints - 1) as f64); - let temperatures = QuantityArray1::linspace(min_temperature, max_temperature, npoints - 1)?; + let temperatures = SIArray1::linspace(min_temperature, max_temperature, npoints - 1)?; for ti in &temperatures { let spinodal = State::spinodal(eos, ti, Some(moles), options).ok(); diff --git a/feos-core/src/phase_equilibria/stability_analysis.rs b/feos-core/src/phase_equilibria/stability_analysis.rs index 204a3d174..47da90f3f 100644 --- a/feos-core/src/phase_equilibria/stability_analysis.rs +++ b/feos-core/src/phase_equilibria/stability_analysis.rs @@ -6,6 +6,7 @@ use crate::EosUnit; use ndarray::*; use num_dual::linalg::smallest_ev; use num_dual::linalg::LU; +use quantity::si::SIUnit; use std::f64::EPSILON; use std::ops::MulAssign; @@ -17,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 { @@ -27,7 +28,7 @@ impl State { /// Perform a stability analysis. The result is a list of [State]s with /// negative tangent plane distance (i.e. lower Gibbs energy) that can be /// used as initial estimates for a phase equilibrium calculation. - pub fn stability_analysis(&self, options: SolverOptions) -> EosResult>> { + pub fn stability_analysis(&self, options: SolverOptions) -> EosResult>> { let mut result = Vec::new(); for i_trial in 0..self.eos.components() + 1 { let phase = if i_trial == self.eos.components() { @@ -60,7 +61,7 @@ impl State { Ok(result) } - fn define_trial_state(&self, dominant_component: usize) -> EosResult> { + fn define_trial_state(&self, dominant_component: usize) -> EosResult> { let x_feed = &self.molefracs; let (x_trial, phase) = if dominant_component == self.eos.components() { @@ -86,14 +87,14 @@ impl State { &self.eos, self.temperature, self.pressure(Contributions::Total), - &(x_trial * U::reference_moles()), + &(x_trial * SIUnit::reference_moles()), phase, ) } fn minimize_tpd( &self, - trial: &mut State, + trial: &mut State, options: SolverOptions, ) -> EosResult<(Option, usize)> { let (max_iter, tol, verbosity) = options.unwrap_or(MINIMIZE_KMAX, MINIMIZE_TOL); @@ -117,7 +118,7 @@ impl State { &trial.eos, trial.temperature, trial.pressure(Contributions::Total), - &(U::reference_moles() * &y), + &(SIUnit::reference_moles() * &y), DensityInitialization::InitialDensity(trial.density), )?; if (i > 4 && error > scaled_tol) || (tpd > tpd_old + 1E-05 && i > 2) { @@ -160,9 +161,9 @@ impl State { let tpd_old = *tpd; // calculate residual and ideal hesse matrix - let mut hesse = (self.dln_phi_dnj() * U::reference_moles()).into_value()?; + let mut hesse = (self.dln_phi_dnj() * SIUnit::reference_moles()).into_value()?; let lnphi = self.ln_phi(); - let y = self.moles.to_reduced(U::reference_moles())?; + let y = self.moles.to_reduced(SIUnit::reference_moles())?; let ln_y = Zip::from(&y).map_collect(|&y| if y > EPSILON { y.ln() } else { 0.0 }); let sq_y = y.mapv(f64::sqrt); let gradient = (&ln_y + &lnphi - di) * &sq_y; @@ -224,7 +225,7 @@ impl State { &self.eos, self.temperature, self.pressure(Contributions::Total), - &(U::reference_moles() * y), + &(SIUnit::reference_moles() * y), DensityInitialization::InitialDensity(self.density), )?; } diff --git a/feos-core/src/phase_equilibria/tp_flash.rs b/feos-core/src/phase_equilibria/tp_flash.rs index 305416828..ef53cd189 100644 --- a/feos-core/src/phase_equilibria/tp_flash.rs +++ b/feos-core/src/phase_equilibria/tp_flash.rs @@ -2,17 +2,16 @@ use super::{PhaseEquilibrium, SolverOptions, Verbosity}; use crate::equation_of_state::EquationOfState; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State}; -use crate::EosUnit; use ndarray::*; use num_dual::linalg::norm; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber}; use std::sync::Arc; 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. /// @@ -20,10 +19,10 @@ impl PhaseEquilibrium { /// containing non-volatile components (e.g. ions). pub fn tp_flash( eos: &Arc, - temperature: QuantityScalar, - pressure: QuantityScalar, - feed: &QuantityArray1, - initial_state: Option<&PhaseEquilibrium>, + temperature: SINumber, + pressure: SINumber, + feed: &SIArray1, + initial_state: Option<&PhaseEquilibrium>, options: SolverOptions, non_volatile_components: Option>, ) -> EosResult { @@ -39,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. @@ -48,10 +47,10 @@ impl State { /// containing non-volatile components (e.g. ions). pub fn tp_flash( &self, - initial_state: Option<&PhaseEquilibrium>, + initial_state: Option<&PhaseEquilibrium>, options: SolverOptions, non_volatile_components: Option>, - ) -> EosResult> { + ) -> EosResult> { // set options let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_TP, TOL_TP); @@ -149,7 +148,7 @@ impl State { Ok(new_vle_state) } - fn tangent_plane_distance(&self, trial_state: &State) -> f64 { + fn tangent_plane_distance(&self, trial_state: &State) -> f64 { let ln_phi_z = self.ln_phi(); let ln_phi_w = trial_state.ln_phi(); let z = &self.molefracs; @@ -158,10 +157,10 @@ impl State { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { fn accelerated_successive_substitution( &mut self, - feed_state: &State, + feed_state: &State, iter: &mut usize, max_iter: usize, tol: f64, @@ -226,7 +225,7 @@ impl PhaseEquilibrium { fn successive_substitution( &mut self, - feed_state: &State, + feed_state: &State, iterations: usize, iter: &mut usize, k_vec: &mut Option<&mut Array2>, @@ -284,7 +283,7 @@ impl PhaseEquilibrium { Ok(false) } - fn update_states(&mut self, feed_state: &State, k: &Array1) -> EosResult<()> { + fn update_states(&mut self, feed_state: &State, k: &Array1) -> EosResult<()> { // calculate vapor phase fraction using Rachford-Rice algorithm let mut beta = self.vapor_phase_fraction(); beta = rachford_rice(&feed_state.molefracs, k, Some(beta))?; @@ -296,7 +295,7 @@ impl PhaseEquilibrium { Ok(()) } - fn vle_init_stability(feed_state: &State) -> EosResult { + fn vle_init_stability(feed_state: &State) -> EosResult { let mut stable_states = feed_state.stability_analysis(SolverOptions::default())?; let state1 = stable_states.pop(); let state2 = stable_states.pop(); diff --git a/feos-core/src/phase_equilibria/vle_pure.rs b/feos-core/src/phase_equilibria/vle_pure.rs index d5cdafd7d..7d89a62ed 100644 --- a/feos-core/src/phase_equilibria/vle_pure.rs +++ b/feos-core/src/phase_equilibria/vle_pure.rs @@ -1,448 +1,421 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; -use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State, TPSpec}; -use crate::EosUnit; -use ndarray::{arr1, Array1}; -use quantity::QuantityScalar; -use std::convert::TryFrom; -use std::sync::Arc; - -const SCALE_T_NEW: f64 = 0.7; - -const MAX_ITER_PURE: usize = 50; -const TOL_PURE: f64 = 1e-12; - -/// # Pure component phase equilibria -impl PhaseEquilibrium { - /// Calculate a phase equilibrium for a pure component. - pub fn pure( - eos: &Arc, - temperature_or_pressure: QuantityScalar, - initial_state: Option<&PhaseEquilibrium>, - options: SolverOptions, - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - match TPSpec::try_from(temperature_or_pressure)? { - TPSpec::Temperature(t) => Self::pure_t(eos, t, initial_state, options), - TPSpec::Pressure(p) => Self::pure_p(eos, p, initial_state, options), - } - } - - /// Calculate a phase equilibrium for a pure component - /// and given temperature. - fn pure_t( - eos: &Arc, - temperature: QuantityScalar, - initial_state: Option<&PhaseEquilibrium>, - options: SolverOptions, - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE); - - // First use given initial state if applicable - let mut vle = initial_state.and_then(|init| { - Self::init_pure_state(init, temperature) - .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity)) - .ok() - }); - - // Next try to initialize with an ideal gas assumption - vle = vle.or_else(|| { - Self::init_pure_ideal_gas(eos, temperature) - .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity)) - .ok() - }); - - // Finally use the spinodal to initialize the calculation - vle.map_or_else( - || { - Self::init_pure_spinodal(eos, temperature) - .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity)) - }, - Ok, - ) - } - - fn iterate_pure_t(self, max_iter: usize, tol: f64, verbosity: Verbosity) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let mut p_old = self.vapor().pressure(Contributions::Total); - let [mut vapor, mut liquid] = self.0; - - log_iter!(verbosity, - " iter | residual | pressure | liquid density | vapor density | Newton steps" - ); - log_iter!(verbosity, "{:-<106}", ""); - log_iter!( - verbosity, - " {:4} | | {:12.8} | {:12.8} | {:12.8} |", - 0, - p_old, - liquid.density, - vapor.density - ); - - for i in 1..=max_iter { - // calculate the pressures and derivatives - 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); - - // Estimate the new pressure - let delta_v = 1.0 / vapor.density - 1.0 / liquid.density; - let delta_a = a_v - a_l; - 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 * U::gas_constant())? - .exp(); - } - - // Improve the estimate by exploiting the almost ideal behavior of the gas phase - let kt = U::gas_constant() * vapor.temperature; - let mut newton_iter = 0; - let newton_tol = p_old * delta_v * tol; - for _ in 0..20 { - let p_frac = p_new.to_reduced(p_old)?; - let f = p_new * delta_v + delta_a + (p_frac.ln() + 1.0 - p_frac) * kt; - let df_dp = delta_v + (1.0 / p_new - 1.0 / p_old) * kt; - p_new -= f / df_dp; - newton_iter += 1; - if f.abs() < newton_tol { - break; - } - } - - // Emergency brake if the implementation of the EOS is not safe. - if p_new.is_nan() { - return Err(EosError::IterationFailed("pure_t".to_owned())); - } - - // Calculate Newton steps for the densities and update state. - let rho_l = liquid.density + (p_new - p_l) / p_rho_l; - let rho_v = vapor.density + (p_new - p_v) / p_rho_v; - liquid = State::new_pure(&liquid.eos, liquid.temperature, rho_l)?; - vapor = State::new_pure(&vapor.eos, vapor.temperature, rho_v)?; - if Self::is_trivial_solution(&vapor, &liquid) { - return Err(EosError::TrivialSolution); - } - - // Check for convergence - let res = (p_new - p_old).abs(); - log_iter!( - verbosity, - " {:4} | {:14.8e} | {:12.8} | {:12.8} | {:12.8} | {}", - i, - res, - p_new, - liquid.density, - vapor.density, - newton_iter - ); - if res < p_old * tol { - log_result!( - verbosity, - "PhaseEquilibrium::pure_t: calculation converged in {} step(s)\n", - i - ); - return Ok(Self([vapor, liquid])); - } - p_old = p_new; - } - Err(EosError::NotConverged("pure_t".to_owned())) - } - - /// Calculate a phase equilibrium for a pure component - /// and given pressure. - fn pure_p( - eos: &Arc, - pressure: QuantityScalar, - initial_state: Option<&Self>, - options: SolverOptions, - ) -> EosResult - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE); - - // Initialize the phase equilibrium - let mut vle = match initial_state { - Some(init) => init - .clone() - .update_pressure(init.vapor().temperature, pressure)?, - None => PhaseEquilibrium::init_pure_p(eos, pressure)?, - }; - - log_iter!( - verbosity, - " iter | residual | temperature | liquid density | vapor density " - ); - log_iter!(verbosity, "{:-<89}", ""); - log_iter!( - verbosity, - " {:4} | | {:13.8} | {:12.8} | {:12.8}", - 0, - vle.vapor().temperature, - vle.liquid().density, - vle.vapor().density - ); - for i in 1..=max_iter { - // calculate the pressures and derivatives - let (p_l, p_rho_l) = vle.liquid().p_dpdrho(); - let (p_v, p_rho_v) = vle.vapor().p_dpdrho(); - 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 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 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 t_new = vle.vapor().temperature + delta_t; - - // calculate Newton steps for the densities and update state. - let rho_l = vle.liquid().density + (pressure - p_l - p_t_l * delta_t) / p_rho_l; - let rho_v = vle.vapor().density + (pressure - p_v - p_t_v * delta_t) / p_rho_v; - - if rho_l.is_sign_negative() - || rho_v.is_sign_negative() - || delta_t.abs() > U::reference_temperature() - { - // if densities are negative or the temperature step is large use density iteration instead - vle = vle - .update_pressure(t_new, pressure)? - .check_trivial_solution()?; - } else { - // update state - vle = Self([ - State::new_pure(eos, t_new, rho_v)?, - State::new_pure(eos, t_new, rho_l)?, - ]); - } - - // check for convergence - let res = delta_t.abs(); - log_iter!( - verbosity, - " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}", - i, - res, - vle.vapor().temperature, - vle.liquid().density, - vle.vapor().density - ); - if res < vle.vapor().temperature * tol { - log_result!( - verbosity, - "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n", - i - ); - return Ok(vle); - } - } - Err(EosError::NotConverged("pure_p".to_owned())) - } - - fn init_pure_state(initial_state: &Self, temperature: QuantityScalar) -> EosResult { - let vapor = initial_state.vapor().update_temperature(temperature)?; - let liquid = initial_state.liquid().update_temperature(temperature)?; - Ok(Self([vapor, liquid])) - } - - fn init_pure_ideal_gas(eos: &Arc, temperature: QuantityScalar) -> EosResult { - let m = arr1(&[1.0]) * U::reference_moles(); - let p = Self::starting_pressure_ideal_gas_bubble(eos, temperature, &arr1(&[1.0]))?.0; - PhaseEquilibrium::new_npt(eos, temperature, p, &m, &m)?.check_trivial_solution() - } - - fn init_pure_spinodal(eos: &Arc, temperature: QuantityScalar) -> EosResult - where - QuantityScalar: std::fmt::Display, - { - let p = Self::starting_pressure_spinodal(eos, temperature, &arr1(&[1.0]))?; - let m = arr1(&[1.0]) * U::reference_moles(); - PhaseEquilibrium::new_npt(eos, temperature, p, &m, &m) - } - - /// Initialize a new VLE for a pure substance for a given pressure. - fn init_pure_p(eos: &Arc, pressure: QuantityScalar) -> EosResult - where - QuantityScalar: std::fmt::Display, - { - let trial_temperatures = [ - 300.0 * U::reference_temperature(), - 500.0 * U::reference_temperature(), - 200.0 * U::reference_temperature(), - ]; - let m = arr1(&[1.0]) * U::reference_moles(); - let mut vle = None; - let mut t0 = U::reference_temperature(); - for t in trial_temperatures.iter() { - t0 = *t; - let _vle = PhaseEquilibrium::new_npt(eos, *t, pressure, &m, &m)?; - if !Self::is_trivial_solution(_vle.vapor(), _vle.liquid()) { - return Ok(_vle); - } - vle = Some(_vle); - } - - let cp = State::critical_point(eos, None, None, SolverOptions::default())?; - if pressure > cp.pressure(Contributions::Total) { - return Err(EosError::SuperCritical); - }; - if let Some(mut e) = vle { - if e.vapor().density < cp.density { - for _ in 0..8 { - t0 = t0 * SCALE_T_NEW; - e.0[1] = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Liquid)?; - if e.liquid().density > cp.density { - break; - } - } - } else { - for _ in 0..8 { - t0 = t0 / SCALE_T_NEW; - e.0[0] = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?; - if e.vapor().density < cp.density { - break; - } - } - } - - 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)); - let trial_state = - State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?; - if trial_state.density < cp.density { - e.0[0] = trial_state; - } - let trial_state = - State::new_npt(eos, t0, pressure, &m, DensityInitialization::Liquid)?; - if trial_state.density > cp.density { - e.0[1] = trial_state; - } - if e.liquid().temperature == e.vapor().temperature { - return Ok(e); - } - } - Err(EosError::IterationFailed( - "new_init_p: could not find proper initial state".to_owned(), - )) - } else { - unreachable!() - } - } -} - -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: QuantityScalar, - ) -> Vec>> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - (0..eos.components()) - .map(|i| { - let pure_eos = Arc::new(eos.subset(&[i])); - PhaseEquilibrium::pure_t(&pure_eos, temperature, None, SolverOptions::default()) - .map(|vle| vle.vapor().pressure(Contributions::Total)) - .ok() - }) - .collect() - } - - /// Calculate the pure component boiling temperatures of all - /// components in the system for the given pressure. - pub fn boiling_temperature( - eos: &Arc, - pressure: QuantityScalar, - ) -> Vec>> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - (0..eos.components()) - .map(|i| { - let pure_eos = Arc::new(eos.subset(&[i])); - PhaseEquilibrium::pure_p(&pure_eos, pressure, None, SolverOptions::default()) - .map(|vle| vle.vapor().temperature) - .ok() - }) - .collect() - } - - /// Calculate the pure component phase equilibria of all - /// components in the system. - pub fn vle_pure_comps( - eos: &Arc, - temperature_or_pressure: QuantityScalar, - ) -> Vec>> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - (0..eos.components()) - .map(|i| { - let pure_eos = Arc::new(eos.subset(&[i])); - PhaseEquilibrium::pure( - &pure_eos, - temperature_or_pressure, - None, - SolverOptions::default(), - ) - .ok() - .map(|vle_pure| { - let mut moles_vapor = Array1::zeros(eos.components()) * U::reference_moles(); - let mut moles_liquid = moles_vapor.clone(); - moles_vapor - .try_set(i, vle_pure.vapor().total_moles) - .unwrap(); - moles_liquid - .try_set(i, vle_pure.liquid().total_moles) - .unwrap(); - let vapor = State::new_nvt( - eos, - vle_pure.vapor().temperature, - vle_pure.vapor().volume, - &moles_vapor, - ) - .unwrap(); - let liquid = State::new_nvt( - eos, - vle_pure.liquid().temperature, - vle_pure.liquid().volume, - &moles_liquid, - ) - .unwrap(); - PhaseEquilibrium::from_states(vapor, liquid) - }) - }) - .collect() - } -} +use super::{PhaseEquilibrium, SolverOptions, Verbosity}; +use crate::equation_of_state::EquationOfState; +use crate::errors::{EosError, EosResult}; +use crate::state::{Contributions, DensityInitialization, State, TPSpec}; +use crate::EosUnit; +use ndarray::{arr1, Array1}; +use quantity::si::{SINumber, SIUnit}; +use std::convert::TryFrom; +use std::sync::Arc; + +const SCALE_T_NEW: f64 = 0.7; +const MAX_ITER_PURE: usize = 50; +const TOL_PURE: f64 = 1e-12; + +/// # Pure component phase equilibria +impl PhaseEquilibrium { + /// Calculate a phase equilibrium for a pure component. + pub fn pure( + eos: &Arc, + temperature_or_pressure: SINumber, + initial_state: Option<&PhaseEquilibrium>, + options: SolverOptions, + ) -> EosResult { + match TPSpec::try_from(temperature_or_pressure)? { + TPSpec::Temperature(t) => Self::pure_t(eos, t, initial_state, options), + TPSpec::Pressure(p) => Self::pure_p(eos, p, initial_state, options), + } + } + + /// Calculate a phase equilibrium for a pure component + /// and given temperature. + fn pure_t( + eos: &Arc, + temperature: SINumber, + initial_state: Option<&PhaseEquilibrium>, + options: SolverOptions, + ) -> EosResult { + let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE); + + // First use given initial state if applicable + let mut vle = initial_state.and_then(|init| { + Self::init_pure_state(init, temperature) + .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity)) + .ok() + }); + + // Next try to initialize with an ideal gas assumption + vle = vle.or_else(|| { + Self::init_pure_ideal_gas(eos, temperature) + .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity)) + .ok() + }); + + // Finally use the spinodal to initialize the calculation + vle.map_or_else( + || { + Self::init_pure_spinodal(eos, temperature) + .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity)) + }, + Ok, + ) + } + + fn iterate_pure_t(self, max_iter: usize, tol: f64, verbosity: Verbosity) -> EosResult { + let mut p_old = self.vapor().pressure(Contributions::Total); + let [mut vapor, mut liquid] = self.0; + + log_iter!(verbosity, + " iter | residual | pressure | liquid density | vapor density | Newton steps" + ); + log_iter!(verbosity, "{:-<106}", ""); + log_iter!( + verbosity, + " {:4} | | {:12.8} | {:12.8} | {:12.8} |", + 0, + p_old, + liquid.density, + vapor.density + ); + + for i in 1..=max_iter { + // calculate the pressures and derivatives + 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); + + // Estimate the new pressure + let delta_v = 1.0 / vapor.density - 1.0 / liquid.density; + let delta_a = a_v - a_l; + 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())? + .exp(); + } + + // Improve the estimate by exploiting the almost ideal behavior of the gas phase + let kt = SIUnit::gas_constant() * vapor.temperature; + let mut newton_iter = 0; + let newton_tol = p_old * delta_v * tol; + for _ in 0..20 { + let p_frac = p_new.to_reduced(p_old)?; + let f = p_new * delta_v + delta_a + (p_frac.ln() + 1.0 - p_frac) * kt; + let df_dp = delta_v + (1.0 / p_new - 1.0 / p_old) * kt; + p_new -= f / df_dp; + newton_iter += 1; + if f.abs() < newton_tol { + break; + } + } + + // Emergency brake if the implementation of the EOS is not safe. + if p_new.is_nan() { + return Err(EosError::IterationFailed("pure_t".to_owned())); + } + + // Calculate Newton steps for the densities and update state. + let rho_l = liquid.density + (p_new - p_l) / p_rho_l; + let rho_v = vapor.density + (p_new - p_v) / p_rho_v; + liquid = State::new_pure(&liquid.eos, liquid.temperature, rho_l)?; + vapor = State::new_pure(&vapor.eos, vapor.temperature, rho_v)?; + if Self::is_trivial_solution(&vapor, &liquid) { + return Err(EosError::TrivialSolution); + } + + // Check for convergence + let res = (p_new - p_old).abs(); + log_iter!( + verbosity, + " {:4} | {:14.8e} | {:12.8} | {:12.8} | {:12.8} | {}", + i, + res, + p_new, + liquid.density, + vapor.density, + newton_iter + ); + if res < p_old * tol { + log_result!( + verbosity, + "PhaseEquilibrium::pure_t: calculation converged in {} step(s)\n", + i + ); + return Ok(Self([vapor, liquid])); + } + p_old = p_new; + } + Err(EosError::NotConverged("pure_t".to_owned())) + } + + /// Calculate a phase equilibrium for a pure component + /// and given pressure. + fn pure_p( + eos: &Arc, + pressure: SINumber, + initial_state: Option<&Self>, + options: SolverOptions, + ) -> EosResult { + let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE); + + // Initialize the phase equilibrium + let mut vle = match initial_state { + Some(init) => init + .clone() + .update_pressure(init.vapor().temperature, pressure)?, + None => PhaseEquilibrium::init_pure_p(eos, pressure)?, + }; + + log_iter!( + verbosity, + " iter | residual | temperature | liquid density | vapor density " + ); + log_iter!(verbosity, "{:-<89}", ""); + log_iter!( + verbosity, + " {:4} | | {:13.8} | {:12.8} | {:12.8}", + 0, + vle.vapor().temperature, + vle.liquid().density, + vle.vapor().density + ); + for i in 1..=max_iter { + // calculate the pressures and derivatives + let (p_l, p_rho_l) = vle.liquid().p_dpdrho(); + let (p_v, p_rho_v) = vle.vapor().p_dpdrho(); + 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 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 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 t_new = vle.vapor().temperature + delta_t; + + // calculate Newton steps for the densities and update state. + let rho_l = vle.liquid().density + (pressure - p_l - p_t_l * delta_t) / p_rho_l; + let rho_v = vle.vapor().density + (pressure - p_v - p_t_v * delta_t) / p_rho_v; + + if rho_l.is_sign_negative() + || rho_v.is_sign_negative() + || delta_t.abs() > SIUnit::reference_temperature() + { + // if densities are negative or the temperature step is large use density iteration instead + vle = vle + .update_pressure(t_new, pressure)? + .check_trivial_solution()?; + } else { + // update state + vle = Self([ + State::new_pure(eos, t_new, rho_v)?, + State::new_pure(eos, t_new, rho_l)?, + ]); + } + + // check for convergence + let res = delta_t.abs(); + log_iter!( + verbosity, + " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}", + i, + res, + vle.vapor().temperature, + vle.liquid().density, + vle.vapor().density + ); + if res < vle.vapor().temperature * tol { + log_result!( + verbosity, + "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n", + i + ); + return Ok(vle); + } + } + Err(EosError::NotConverged("pure_p".to_owned())) + } + + fn init_pure_state(initial_state: &Self, temperature: SINumber) -> EosResult { + let vapor = initial_state.vapor().update_temperature(temperature)?; + let liquid = initial_state.liquid().update_temperature(temperature)?; + Ok(Self([vapor, liquid])) + } + + fn init_pure_ideal_gas(eos: &Arc, temperature: SINumber) -> EosResult { + let m = arr1(&[1.0]) * SIUnit::reference_moles(); + let p = Self::starting_pressure_ideal_gas_bubble(eos, temperature, &arr1(&[1.0]))?.0; + PhaseEquilibrium::new_npt(eos, temperature, p, &m, &m)?.check_trivial_solution() + } + + fn init_pure_spinodal(eos: &Arc, temperature: SINumber) -> EosResult + where + SINumber: std::fmt::Display, + { + let p = Self::starting_pressure_spinodal(eos, temperature, &arr1(&[1.0]))?; + let m = arr1(&[1.0]) * SIUnit::reference_moles(); + PhaseEquilibrium::new_npt(eos, temperature, p, &m, &m) + } + + /// Initialize a new VLE for a pure substance for a given pressure. + fn init_pure_p(eos: &Arc, pressure: SINumber) -> EosResult + where + SINumber: std::fmt::Display, + { + let trial_temperatures = [ + 300.0 * SIUnit::reference_temperature(), + 500.0 * SIUnit::reference_temperature(), + 200.0 * SIUnit::reference_temperature(), + ]; + let m = arr1(&[1.0]) * SIUnit::reference_moles(); + let mut vle = None; + let mut t0 = SIUnit::reference_temperature(); + for t in trial_temperatures.iter() { + t0 = *t; + let _vle = PhaseEquilibrium::new_npt(eos, *t, pressure, &m, &m)?; + if !Self::is_trivial_solution(_vle.vapor(), _vle.liquid()) { + return Ok(_vle); + } + vle = Some(_vle); + } + + let cp = State::critical_point(eos, None, None, SolverOptions::default())?; + if pressure > cp.pressure(Contributions::Total) { + return Err(EosError::SuperCritical); + }; + if let Some(mut e) = vle { + if e.vapor().density < cp.density { + for _ in 0..8 { + t0 = t0 * SCALE_T_NEW; + e.0[1] = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Liquid)?; + if e.liquid().density > cp.density { + break; + } + } + } else { + for _ in 0..8 { + t0 = t0 / SCALE_T_NEW; + e.0[0] = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?; + if e.vapor().density < cp.density { + break; + } + } + } + + 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)); + let trial_state = + State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?; + if trial_state.density < cp.density { + e.0[0] = trial_state; + } + let trial_state = + State::new_npt(eos, t0, pressure, &m, DensityInitialization::Liquid)?; + if trial_state.density > cp.density { + e.0[1] = trial_state; + } + if e.liquid().temperature == e.vapor().temperature { + return Ok(e); + } + } + Err(EosError::IterationFailed( + "new_init_p: could not find proper initial state".to_owned(), + )) + } else { + unreachable!() + } + } +} + +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> { + (0..eos.components()) + .map(|i| { + let pure_eos = Arc::new(eos.subset(&[i])); + PhaseEquilibrium::pure_t(&pure_eos, temperature, None, SolverOptions::default()) + .map(|vle| vle.vapor().pressure(Contributions::Total)) + .ok() + }) + .collect() + } + + /// Calculate the pure component boiling temperatures of all + /// components in the system for the given pressure. + pub fn boiling_temperature(eos: &Arc, pressure: SINumber) -> Vec> { + (0..eos.components()) + .map(|i| { + let pure_eos = Arc::new(eos.subset(&[i])); + PhaseEquilibrium::pure_p(&pure_eos, pressure, None, SolverOptions::default()) + .map(|vle| vle.vapor().temperature) + .ok() + }) + .collect() + } + + /// Calculate the pure component phase equilibria of all + /// components in the system. + pub fn vle_pure_comps( + eos: &Arc, + temperature_or_pressure: SINumber, + ) -> Vec>> { + (0..eos.components()) + .map(|i| { + let pure_eos = Arc::new(eos.subset(&[i])); + PhaseEquilibrium::pure( + &pure_eos, + temperature_or_pressure, + None, + SolverOptions::default(), + ) + .ok() + .map(|vle_pure| { + let mut moles_vapor = + Array1::zeros(eos.components()) * SIUnit::reference_moles(); + let mut moles_liquid = moles_vapor.clone(); + moles_vapor + .try_set(i, vle_pure.vapor().total_moles) + .unwrap(); + moles_liquid + .try_set(i, vle_pure.liquid().total_moles) + .unwrap(); + let vapor = State::new_nvt( + eos, + vle_pure.vapor().temperature, + vle_pure.vapor().volume, + &moles_vapor, + ) + .unwrap(); + let liquid = State::new_nvt( + eos, + vle_pure.liquid().temperature, + vle_pure.liquid().volume, + &moles_liquid, + ) + .unwrap(); + PhaseEquilibrium::from_states(vapor, liquid) + }) + }) + .collect() + } +} diff --git a/feos-core/src/python/phase_equilibria.rs b/feos-core/src/python/phase_equilibria.rs index 017c71920..2d18d4d37 100644 --- a/feos-core/src/python/phase_equilibria.rs +++ b/feos-core/src/python/phase_equilibria.rs @@ -4,7 +4,7 @@ macro_rules! impl_phase_equilibrium { /// A thermodynamic two phase equilibrium state. #[pyclass(name = "PhaseEquilibrium")] #[derive(Clone)] - pub struct PyPhaseEquilibrium(PhaseEquilibrium); + pub struct PyPhaseEquilibrium(PhaseEquilibrium<$eos, 2>); #[pymethods] impl PyPhaseEquilibrium { @@ -330,7 +330,7 @@ macro_rules! impl_phase_equilibrium { /// A thermodynamic three phase equilibrium state. #[pyclass(name = "ThreePhaseEquilibrium")] #[derive(Clone)] - struct PyThreePhaseEquilibrium(PhaseEquilibrium); + struct PyThreePhaseEquilibrium(PhaseEquilibrium<$eos, 3>); #[pymethods] impl PyPhaseEquilibrium { @@ -473,7 +473,7 @@ macro_rules! impl_phase_equilibrium { /// ------- /// PhaseDiagram : the resulting phase diagram #[pyclass(name = "PhaseDiagram")] - pub struct PyPhaseDiagram(PhaseDiagram); + pub struct PyPhaseDiagram(PhaseDiagram<$eos, 2>); #[pymethods] impl PyPhaseDiagram { @@ -921,7 +921,7 @@ macro_rules! impl_phase_equilibrium { /// Phase diagram for a binary mixture exhibiting a heteroazeotrope. #[pyclass(name = "PhaseDiagramHetero")] - pub struct PyPhaseDiagramHetero(PhaseDiagramHetero); + pub struct PyPhaseDiagramHetero(PhaseDiagramHetero<$eos>); #[pymethods] impl PyPhaseDiagram { diff --git a/feos-core/src/python/state.rs b/feos-core/src/python/state.rs index 6b29b2ff8..1bb2eab71 100644 --- a/feos-core/src/python/state.rs +++ b/feos-core/src/python/state.rs @@ -49,7 +49,7 @@ macro_rules! impl_state { #[pyclass(name = "State")] #[derive(Clone)] #[pyo3(text_signature = "(eos, temperature=None, volume=None, density=None, partial_density=None, total_moles=None, moles=None, molefracs=None, pressure=None, molar_enthalpy=None, molar_entropy=None, molar_internal_energy=None, density_initialization=None, initial_temperature=None)")] - pub struct PyState(pub State); + pub struct PyState(pub State<$eos>); #[pymethods] impl PyState { @@ -1022,15 +1022,15 @@ macro_rules! impl_state { /// ------- /// StateVec #[pyclass(name = "StateVec")] - pub struct PyStateVec(Vec>); + pub struct PyStateVec(Vec>); - impl From> for PyStateVec { - fn from(vec: StateVec) -> Self { + impl From> for PyStateVec { + fn from(vec: StateVec<$eos>) -> Self { Self(vec.into_iter().map(|s| s.clone()).collect()) } } - impl<'a> From<&'a PyStateVec> for StateVec<'a, SIUnit, $eos> { + impl<'a> From<&'a PyStateVec> for StateVec<'a, $eos> { fn from(vec: &'a PyStateVec) -> Self { Self(vec.0.iter().collect()) } diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index 939e73fd8..94a035c5d 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -48,7 +48,7 @@ impl PyEoSObj { } } -impl MolarWeight for PyEoSObj { +impl MolarWeight for PyEoSObj { fn molar_weight(&self) -> SIArray1 { let gil = Python::acquire_gil(); let py = gil.python(); diff --git a/feos-core/src/state/builder.rs b/feos-core/src/state/builder.rs index 406cc3d04..40406862b 100644 --- a/feos-core/src/state/builder.rs +++ b/feos-core/src/state/builder.rs @@ -1,9 +1,8 @@ use super::{DensityInitialization, State}; use crate::equation_of_state::EquationOfState; use crate::errors::EosResult; -use crate::EosUnit; use ndarray::Array1; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber}; use std::sync::Arc; /// A simple tool to construct [State]s with arbitrary input parameters. @@ -53,24 +52,24 @@ use std::sync::Arc; /// # Ok(()) /// # } /// ``` -pub struct StateBuilder<'a, U: EosUnit, E: EquationOfState> { +pub struct StateBuilder<'a, E: EquationOfState> { eos: Arc, - temperature: Option>, - volume: Option>, - density: Option>, - partial_density: Option<&'a QuantityArray1>, - total_moles: Option>, - moles: Option<&'a QuantityArray1>, + temperature: Option, + volume: Option, + density: Option, + partial_density: Option<&'a SIArray1>, + total_moles: Option, + moles: Option<&'a SIArray1>, molefracs: Option<&'a Array1>, - pressure: Option>, - molar_enthalpy: Option>, - molar_entropy: Option>, - molar_internal_energy: Option>, - density_initialization: DensityInitialization, - initial_temperature: Option>, + pressure: Option, + molar_enthalpy: Option, + molar_entropy: Option, + molar_internal_energy: Option, + density_initialization: DensityInitialization, + initial_temperature: Option, } -impl<'a, U: EosUnit, E: EquationOfState> StateBuilder<'a, U, E> { +impl<'a, E: EquationOfState> StateBuilder<'a, E> { /// Create a new `StateBuilder` for the given equation of state. pub fn new(eos: &Arc) -> Self { StateBuilder { @@ -92,37 +91,37 @@ impl<'a, U: EosUnit, E: EquationOfState> StateBuilder<'a, U, E> { } /// Provide the temperature for the new state. - pub fn temperature(mut self, temperature: QuantityScalar) -> Self { + pub fn temperature(mut self, temperature: SINumber) -> Self { self.temperature = Some(temperature); self } /// Provide the volume for the new state. - pub fn volume(mut self, volume: QuantityScalar) -> Self { + pub fn volume(mut self, volume: SINumber) -> Self { self.volume = Some(volume); self } /// Provide the density for the new state. - pub fn density(mut self, density: QuantityScalar) -> Self { + pub fn density(mut self, density: SINumber) -> Self { self.density = Some(density); self } /// Provide partial densities for the new state. - pub fn partial_density(mut self, partial_density: &'a QuantityArray1) -> Self { + pub fn partial_density(mut self, partial_density: &'a SIArray1) -> Self { self.partial_density = Some(partial_density); self } /// Provide the total moles for the new state. - pub fn total_moles(mut self, total_moles: QuantityScalar) -> Self { + pub fn total_moles(mut self, total_moles: SINumber) -> Self { self.total_moles = Some(total_moles); self } /// Provide the moles for the new state. - pub fn moles(mut self, moles: &'a QuantityArray1) -> Self { + pub fn moles(mut self, moles: &'a SIArray1) -> Self { self.moles = Some(moles); self } @@ -134,25 +133,25 @@ impl<'a, U: EosUnit, E: EquationOfState> StateBuilder<'a, U, E> { } /// Provide the pressure for the new state. - pub fn pressure(mut self, pressure: QuantityScalar) -> Self { + pub fn pressure(mut self, pressure: SINumber) -> Self { self.pressure = Some(pressure); self } /// Provide the molar enthalpy for the new state. - pub fn molar_enthalpy(mut self, molar_enthalpy: QuantityScalar) -> Self { + 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: QuantityScalar) -> Self { + 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: QuantityScalar) -> Self { + pub fn molar_internal_energy(mut self, molar_internal_energy: SINumber) -> Self { self.molar_internal_energy = Some(molar_internal_energy); self } @@ -170,19 +169,19 @@ impl<'a, U: EosUnit, E: EquationOfState> StateBuilder<'a, U, E> { } /// Provide an initial density used in density iterations. - pub fn initial_density(mut self, initial_density: QuantityScalar) -> Self { + pub fn initial_density(mut self, initial_density: SINumber) -> Self { self.density_initialization = DensityInitialization::InitialDensity(initial_density); self } /// Provide an initial temperature used in the Newton solver. - pub fn initial_temperature(mut self, initial_temperature: QuantityScalar) -> Self { + pub fn initial_temperature(mut self, initial_temperature: SINumber) -> Self { self.initial_temperature = Some(initial_temperature); self } /// Try to build the state with the given inputs. - pub fn build(self) -> EosResult> { + pub fn build(self) -> EosResult> { State::new( &self.eos, self.temperature, @@ -202,7 +201,7 @@ impl<'a, U: EosUnit, E: EquationOfState> StateBuilder<'a, U, E> { } } -impl<'a, U: EosUnit, E: EquationOfState> Clone for StateBuilder<'a, U, E> { +impl<'a, E: EquationOfState> Clone for StateBuilder<'a, E> { fn clone(&self) -> Self { Self { eos: self.eos.clone(), diff --git a/feos-core/src/state/cache.rs b/feos-core/src/state/cache.rs index daf2a3e4c..f0c41eabd 100644 --- a/feos-core/src/state/cache.rs +++ b/feos-core/src/state/cache.rs @@ -55,7 +55,10 @@ impl Cache { derivative: Derivative, f: F, ) -> f64 { - if let Some(&value) = self.map.get(&PartialDerivative::SecondMixed(derivative, derivative)) { + if let Some(&value) = self + .map + .get(&PartialDerivative::SecondMixed(derivative, derivative)) + { self.hit += 1; value } else { @@ -64,8 +67,10 @@ impl Cache { self.map.insert(PartialDerivative::Zeroth, value.re); self.map .insert(PartialDerivative::First(derivative), value.v1[0]); - self.map - .insert(PartialDerivative::SecondMixed(derivative, derivative), value.v2[0]); + self.map.insert( + PartialDerivative::SecondMixed(derivative, derivative), + value.v2[0], + ); value.v2[0] } } diff --git a/feos-core/src/state/critical_point.rs b/feos-core/src/state/critical_point.rs index 3007b11fc..b2cb1b3c3 100644 --- a/feos-core/src/state/critical_point.rs +++ b/feos-core/src/state/critical_point.rs @@ -7,7 +7,7 @@ use ndarray::{arr1, arr2, Array1, Array2}; use num_dual::linalg::{norm, smallest_ev, LU}; use num_dual::{Dual, Dual3, Dual64, DualNum, DualVec64, HyperDual, StaticVec}; use num_traits::{One, Zero}; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; use std::convert::TryFrom; use std::sync::Arc; @@ -16,15 +16,15 @@ 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, - initial_temperature: Option>, + initial_temperature: Option, options: SolverOptions, ) -> EosResult> where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { (0..eos.components()) .map(|i| { @@ -40,13 +40,13 @@ impl State { pub fn critical_point_binary( eos: &Arc, - temperature_or_pressure: QuantityScalar, - initial_temperature: Option>, + temperature_or_pressure: SINumber, + initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, options: SolverOptions, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { match TPSpec::try_from(temperature_or_pressure)? { TPSpec::Temperature(t) => { @@ -65,18 +65,18 @@ impl State { /// Calculate the critical point of a system for given moles. pub fn critical_point( eos: &Arc, - moles: Option<&QuantityArray1>, - initial_temperature: Option>, + moles: Option<&SIArray1>, + initial_temperature: Option, options: SolverOptions, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let moles = eos.validate_moles(moles)?; let trial_temperatures = [ - 300.0 * U::reference_temperature(), - 700.0 * U::reference_temperature(), - 500.0 * U::reference_temperature(), + 300.0 * SIUnit::reference_temperature(), + 700.0 * SIUnit::reference_temperature(), + 500.0 * SIUnit::reference_temperature(), ]; if let Some(t) = initial_temperature { return Self::critical_point_hkm(eos, &moles, t, options); @@ -92,21 +92,21 @@ impl State { fn critical_point_hkm( eos: &Arc, - moles: &QuantityArray1, - initial_temperature: QuantityScalar, + moles: &SIArray1, + initial_temperature: SINumber, options: SolverOptions, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT, TOL_CRIT_POINT); - let mut t = initial_temperature.to_reduced(U::reference_temperature())?; + let mut t = initial_temperature.to_reduced(SIUnit::reference_temperature())?; let max_density = eos .max_density(Some(moles))? - .to_reduced(U::reference_density())?; + .to_reduced(SIUnit::reference_density())?; let mut rho = 0.3 * max_density; - let n = moles.to_reduced(U::reference_moles())?; + let n = moles.to_reduced(SIUnit::reference_moles())?; log_iter!( verbosity, @@ -117,8 +117,8 @@ impl State { verbosity, " {:4} | | {:13.8} | {:12.8}", 0, - t * U::reference_temperature(), - rho * U::reference_density(), + t * SIUnit::reference_temperature(), + rho * SIUnit::reference_density(), ); for i in 1..=max_iter { @@ -152,8 +152,8 @@ impl State { " {:4} | {:14.8e} | {:13.8} | {:12.8}", i, norm(&res), - t * U::reference_temperature(), - rho * U::reference_density(), + t * SIUnit::reference_temperature(), + rho * SIUnit::reference_density(), ); // check convergence @@ -165,8 +165,8 @@ impl State { ); return State::new_nvt( eos, - t * U::reference_temperature(), - moles.sum() / (rho * U::reference_density()), + t * SIUnit::reference_temperature(), + moles.sum() / (rho * SIUnit::reference_density()), moles, ); } @@ -177,21 +177,21 @@ impl State { /// Calculate the critical point of a binary system for given temperature. fn critical_point_binary_t( eos: &Arc, - temperature: QuantityScalar, + temperature: SINumber, initial_molefracs: Option<[f64; 2]>, options: SolverOptions, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT_BINARY, TOL_CRIT_POINT); - let t = temperature.to_reduced(U::reference_temperature())?; + let t = temperature.to_reduced(SIUnit::reference_temperature())?; let x = StaticVec::new_vec(initial_molefracs.unwrap_or([0.5, 0.5])); let max_density = eos - .max_density(Some(&(arr1(x.raw_array()) * U::reference_moles())))? - .to_reduced(U::reference_density())?; + .max_density(Some(&(arr1(x.raw_array()) * SIUnit::reference_moles())))? + .to_reduced(SIUnit::reference_density())?; let mut rho = x * 0.3 * max_density; log_iter!( @@ -203,8 +203,8 @@ impl State { verbosity, " {:4} | | {:12.8} | {:12.8}", 0, - rho[0] * U::reference_density(), - rho[1] * U::reference_density(), + rho[0] * SIUnit::reference_density(), + rho[1] * SIUnit::reference_density(), ); for i in 1..=max_iter { @@ -238,8 +238,8 @@ impl State { " {:4} | {:14.8e} | {:12.8} | {:12.8}", i, res.norm(), - rho[0] * U::reference_density(), - rho[1] * U::reference_density(), + rho[0] * SIUnit::reference_density(), + rho[1] * SIUnit::reference_density(), ); // check convergence @@ -251,9 +251,9 @@ impl State { ); return State::new_nvt( eos, - t * U::reference_temperature(), - U::reference_volume(), - &(arr1(rho.raw_array()) * U::reference_moles()), + t * SIUnit::reference_temperature(), + SIUnit::reference_volume(), + &(arr1(rho.raw_array()) * SIUnit::reference_moles()), ); } } @@ -263,26 +263,26 @@ impl State { /// Calculate the critical point of a binary system for given pressure. fn critical_point_binary_p( eos: &Arc, - pressure: QuantityScalar, - initial_temperature: Option>, + pressure: SINumber, + initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, options: SolverOptions, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT_BINARY, TOL_CRIT_POINT); - let p = pressure.to_reduced(U::reference_pressure())?; + let p = pressure.to_reduced(SIUnit::reference_pressure())?; let mut t = initial_temperature - .map(|t| t.to_reduced(U::reference_temperature())) + .map(|t| t.to_reduced(SIUnit::reference_temperature())) .transpose()? .unwrap_or(300.0); let x = StaticVec::new_vec(initial_molefracs.unwrap_or([0.5, 0.5])); let max_density = eos - .max_density(Some(&(arr1(x.raw_array()) * U::reference_moles())))? - .to_reduced(U::reference_density())?; + .max_density(Some(&(arr1(x.raw_array()) * SIUnit::reference_moles())))? + .to_reduced(SIUnit::reference_density())?; let mut rho = x * 0.3 * max_density; log_iter!( @@ -294,9 +294,9 @@ impl State { verbosity, " {:4} | | {:13.8} | {:12.8} | {:12.8}", 0, - t * U::reference_temperature(), - rho[0] * U::reference_density(), - rho[1] * U::reference_density(), + t * SIUnit::reference_temperature(), + rho[0] * SIUnit::reference_density(), + rho[1] * SIUnit::reference_density(), ); for i in 1..=max_iter { @@ -338,9 +338,9 @@ impl State { " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}", i, norm(&res), - t * U::reference_temperature(), - rho[0] * U::reference_density(), - rho[1] * U::reference_density(), + t * SIUnit::reference_temperature(), + rho[0] * SIUnit::reference_density(), + rho[1] * SIUnit::reference_density(), ); // check convergence @@ -352,9 +352,9 @@ impl State { ); return State::new_nvt( eos, - t * U::reference_temperature(), - U::reference_volume(), - &(arr1(rho.raw_array()) * U::reference_moles()), + t * SIUnit::reference_temperature(), + SIUnit::reference_volume(), + &(arr1(rho.raw_array()) * SIUnit::reference_moles()), ); } } @@ -363,12 +363,12 @@ impl State { pub fn spinodal( eos: &Arc, - temperature: QuantityScalar, - moles: Option<&QuantityArray1>, + temperature: SINumber, + moles: Option<&SIArray1>, options: SolverOptions, ) -> EosResult<[Self; 2]> where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let critical_point = Self::critical_point(eos, moles, None, options)?; let moles = eos.validate_moles(moles)?; @@ -392,27 +392,29 @@ impl State { fn calculate_spinodal( eos: &Arc, - temperature: QuantityScalar, - moles: &QuantityArray1, - density_initialization: DensityInitialization, + temperature: SINumber, + moles: &SIArray1, + density_initialization: DensityInitialization, options: SolverOptions, ) -> EosResult where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT, TOL_CRIT_POINT); let max_density = eos .max_density(Some(moles))? - .to_reduced(U::reference_density())?; - let t = temperature.to_reduced(U::reference_temperature())?; + .to_reduced(SIUnit::reference_density())?; + let t = temperature.to_reduced(SIUnit::reference_temperature())?; let mut rho = match density_initialization { DensityInitialization::Vapor => 1e-5 * max_density, DensityInitialization::Liquid => max_density, - DensityInitialization::InitialDensity(rho) => rho.to_reduced(U::reference_density())?, + DensityInitialization::InitialDensity(rho) => { + rho.to_reduced(SIUnit::reference_density())? + } DensityInitialization::None => unreachable!(), }; - let n = moles.to_reduced(U::reference_moles())?; + let n = moles.to_reduced(SIUnit::reference_moles())?; log_iter!(verbosity, " iter | residual | density "); log_iter!(verbosity, "{:-<46}", ""); @@ -420,7 +422,7 @@ impl State { verbosity, " {:4} | | {:12.8}", 0, - rho * U::reference_density(), + rho * SIUnit::reference_density(), ); for i in 1..=max_iter { @@ -444,7 +446,7 @@ impl State { " {:4} | {:14.8e} | {:12.8}", i, res.re.abs(), - rho * U::reference_density(), + rho * SIUnit::reference_density(), ); // check convergence @@ -457,7 +459,7 @@ impl State { return State::new_nvt( eos, temperature, - moles.sum() / (rho * U::reference_density()), + moles.sum() / (rho * SIUnit::reference_density()), moles, ); } diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index db6778e5a..2133a0165 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -14,7 +14,7 @@ use cache::Cache; use ndarray::prelude::*; use num_dual::linalg::{norm, LU}; use num_dual::*; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; use std::convert::TryFrom; use std::fmt; use std::sync::{Arc, Mutex}; @@ -27,13 +27,13 @@ pub use properties::{Contributions, StateVec}; /// Initial values in a density iteration. #[derive(Clone, Copy)] -pub enum DensityInitialization { +pub enum DensityInitialization { /// Calculate a vapor phase by initializing using the ideal gas. Vapor, /// Calculate a liquid phase by using the `max_density`. Liquid, /// Use the given density as initial value. - InitialDensity(QuantityScalar), + InitialDensity(SINumber), /// Calculate the most stable phase by calculating both a vapor and a liquid /// and return the one with the lower molar Gibbs energy. None, @@ -120,21 +120,21 @@ impl> StateHD { /// + [Stability analysis](#stability-analysis) /// + [Flash calculations](#flash-calculations) #[derive(Debug)] -pub struct State { +pub struct State { /// Equation of state pub eos: Arc, /// Temperature $T$ - pub temperature: QuantityScalar, + pub temperature: SINumber, /// Volume $V$ - pub volume: QuantityScalar, + pub volume: SINumber, /// Mole numbers $N_i$ - pub moles: QuantityArray1, + pub moles: SIArray1, /// Total number of moles $N=\sum_iN_i$ - pub total_moles: QuantityScalar, + pub total_moles: SINumber, /// Partial densities $\rho_i=\frac{N_i}{V}$ - pub partial_density: QuantityArray1, + pub partial_density: SIArray1, /// Total density $\rho=\frac{N}{V}=\sum_i\rho_i$ - pub density: QuantityScalar, + pub density: SINumber, /// Mole fractions $x_i=\frac{N_i}{N}=\frac{\rho_i}{\rho}$ pub molefracs: Array1, /// Reduced temperature @@ -147,16 +147,16 @@ pub struct State { cache: Mutex, } -impl Clone for State { +impl Clone for State { fn clone(&self) -> Self { Self { eos: self.eos.clone(), - total_moles: self.total_moles.clone(), - temperature: self.temperature.clone(), - volume: self.volume.clone(), + total_moles: self.total_moles, + temperature: self.temperature, + volume: self.volume, moles: self.moles.clone(), partial_density: self.partial_density.clone(), - density: self.density.clone(), + density: self.density, molefracs: self.molefracs.clone(), reduced_temperature: self.reduced_temperature, reduced_volume: self.reduced_volume, @@ -166,10 +166,10 @@ impl Clone for State { } } -impl fmt::Display for State +impl fmt::Display for State where - QuantityScalar: fmt::Display, - QuantityArray1: fmt::Display, + SINumber: fmt::Display, + SIArray1: fmt::Display, E: EquationOfState, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -198,11 +198,11 @@ pub enum Derivative { } impl Derivative { - pub fn reference(&self) -> QuantityScalar { + pub fn reference(&self) -> SINumber { match self { - Derivative::DV => U::reference_volume(), - Derivative::DT => U::reference_temperature(), - Derivative::DN(_) => U::reference_moles(), + Derivative::DV => SIUnit::reference_volume(), + Derivative::DT => SIUnit::reference_temperature(), + Derivative::DN(_) => SIUnit::reference_moles(), } } } @@ -217,7 +217,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 @@ -225,9 +225,9 @@ impl State { /// densities are below the maximum packing fraction. pub fn new_nvt( eos: &Arc, - temperature: QuantityScalar, - volume: QuantityScalar, - moles: &QuantityArray1, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, ) -> EosResult { eos.validate_moles(Some(moles))?; validate(temperature, volume, moles)?; @@ -237,18 +237,20 @@ impl State { pub(super) fn new_nvt_unchecked( eos: &Arc, - temperature: QuantityScalar, - volume: QuantityScalar, - moles: &QuantityArray1, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, ) -> Self { - let t = temperature.to_reduced(U::reference_temperature()).unwrap(); - let v = volume.to_reduced(U::reference_volume()).unwrap(); - let m = moles.to_reduced(U::reference_moles()).unwrap(); + let t = temperature + .to_reduced(SIUnit::reference_temperature()) + .unwrap(); + let v = volume.to_reduced(SIUnit::reference_volume()).unwrap(); + let m = moles.to_reduced(SIUnit::reference_moles()).unwrap(); let total_moles = moles.sum(); let partial_density = moles / volume; let density = total_moles / volume; - let molefracs = &m / total_moles.to_reduced(U::reference_moles()).unwrap(); + let molefracs = &m / total_moles.to_reduced(SIUnit::reference_moles()).unwrap(); State { eos: eos.clone(), @@ -272,13 +274,14 @@ impl State { /// This function will perform a validation of the given properties, i.e. test for signs /// and if values are finite. It will **not** validate physics, i.e. if the resulting /// densities are below the maximum packing fraction. - pub fn new_pure( - eos: &Arc, - temperature: QuantityScalar, - density: QuantityScalar, - ) -> EosResult { - let moles = arr1(&[1.0]) * U::reference_moles(); - Self::new_nvt(eos, temperature, U::reference_moles() / density, &moles) + pub fn new_pure(eos: &Arc, temperature: SINumber, density: SINumber) -> EosResult { + let moles = arr1(&[1.0]) * SIUnit::reference_moles(); + Self::new_nvt( + eos, + temperature, + SIUnit::reference_moles() / density, + &moles, + ) } /// Return a new `State` for the combination of inputs. @@ -297,37 +300,37 @@ impl State { /// When the state cannot be created using the combination of inputs. pub fn new( eos: &Arc, - temperature: Option>, - volume: Option>, - density: Option>, - partial_density: Option<&QuantityArray1>, - total_moles: Option>, - moles: Option<&QuantityArray1>, + 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>, + pressure: Option, + molar_enthalpy: Option, + molar_entropy: Option, + molar_internal_energy: 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(&U::reference_density()) { + if !rho0.has_unit(&SIUnit::reference_density()) { return Err(EosError::UndeterminedState(String::from( "The provided initial density has to be the molar density", ))); } } if let Some(rho) = density { - if !rho.has_unit(&U::reference_density()) { + if !rho.has_unit(&SIUnit::reference_density()) { return Err(EosError::UndeterminedState(String::from( "The provided density has to be the molar density", ))); } } if let Some(rho) = partial_density { - if !rho.has_unit(&U::reference_density()) { + if !rho.has_unit(&SIUnit::reference_density()) { return Err(EosError::UndeterminedState(String::from( "The provided partial density has to be the molar density", ))); @@ -386,7 +389,7 @@ impl State { // If no extensive property is given, moles is set to the reference value. if let (None, None) = (volume, n) { - n = Some(U::reference_moles()) + n = Some(SIUnit::reference_moles()) } let n_i = n.map(|n| &x_u * n); let v = volume.or_else(|| rho.and_then(|d| n.map(|n| n / d))); @@ -429,10 +432,10 @@ impl State { /// influence the calculation with respect to the possible solutions. pub fn new_npt( eos: &Arc, - temperature: QuantityScalar, - pressure: QuantityScalar, - moles: &QuantityArray1, - density_initialization: DensityInitialization, + temperature: SINumber, + pressure: SINumber, + moles: &SIArray1, + density_initialization: DensityInitialization, ) -> EosResult { // calculate state from initial density or given phase match density_initialization { @@ -445,7 +448,7 @@ impl State { temperature, pressure, moles, - pressure / temperature / U::gas_constant(), + pressure / temperature / SIUnit::gas_constant(), ) } DensityInitialization::Liquid => { @@ -464,13 +467,13 @@ impl State { let max_density = eos.max_density(Some(moles))?; let liquid = density_iteration(eos, temperature, pressure, moles, max_density); - if pressure < max_density * temperature * U::gas_constant() { + if pressure < max_density * temperature * SIUnit::gas_constant() { let vapor = density_iteration( eos, temperature, pressure, moles, - pressure / temperature / U::gas_constant(), + pressure / temperature / SIUnit::gas_constant(), ); match (&liquid, &vapor) { (Ok(_), Err(_)) => liquid, @@ -496,13 +499,13 @@ impl State { /// Return a new `State` for given pressure $p$, volume $V$, temperature $T$ and composition $x_i$. pub fn new_npvx( eos: &Arc, - temperature: QuantityScalar, - pressure: QuantityScalar, - volume: QuantityScalar, + temperature: SINumber, + pressure: SINumber, + volume: SINumber, molefracs: &Array1, - density_initialization: DensityInitialization, + density_initialization: DensityInitialization, ) -> EosResult { - let moles = molefracs * U::reference_moles(); + let moles = molefracs * SIUnit::reference_moles(); let state = Self::new_npt(eos, temperature, pressure, &moles, density_initialization)?; let moles = state.partial_density * volume; Self::new_nvt(eos, temperature, volume, &moles) @@ -511,13 +514,13 @@ impl State { /// Return a new `State` for given pressure $p$ and molar enthalpy $h$. pub fn new_nph( eos: &Arc, - pressure: QuantityScalar, - molar_enthalpy: QuantityScalar, - moles: &QuantityArray1, - density_initialization: DensityInitialization, - initial_temperature: Option>, + pressure: SINumber, + molar_enthalpy: SINumber, + moles: &SIArray1, + density_initialization: DensityInitialization, + initial_temperature: Option, ) -> EosResult { - let t0 = initial_temperature.unwrap_or(298.15 * U::reference_temperature()); + let t0 = initial_temperature.unwrap_or(298.15 * SIUnit::reference_temperature()); let mut density = density_initialization; let f = |x0| { let s = State::new_npt(eos, x0, pressure, moles, density)?; @@ -526,16 +529,16 @@ impl State { density = DensityInitialization::InitialDensity(s.density); Ok((fx, dfx, s)) }; - newton(t0, f, 1.0e-8 * U::reference_temperature()) + newton(t0, f, 1.0e-8 * SIUnit::reference_temperature()) } /// Return a new `State` for given temperature $T$ and molar enthalpy $h$. pub fn new_nth( eos: &Arc, - temperature: QuantityScalar, - molar_enthalpy: QuantityScalar, - moles: &QuantityArray1, - density_initialization: DensityInitialization, + temperature: SINumber, + molar_enthalpy: SINumber, + moles: &SIArray1, + density_initialization: DensityInitialization, ) -> EosResult { let rho0 = match density_initialization { DensityInitialization::InitialDensity(r) => r, @@ -553,16 +556,16 @@ impl State { let fx = s.molar_enthalpy(Contributions::Total) - molar_enthalpy; Ok((fx, dfx, s)) }; - newton(rho0, f, 1.0e-12 * U::reference_density()) + newton(rho0, f, 1.0e-12 * SIUnit::reference_density()) } /// Return a new `State` for given temperature $T$ and molar entropy $s$. pub fn new_nts( eos: &Arc, - temperature: QuantityScalar, - molar_entropy: QuantityScalar, - moles: &QuantityArray1, - density_initialization: DensityInitialization, + temperature: SINumber, + molar_entropy: SINumber, + moles: &SIArray1, + density_initialization: DensityInitialization, ) -> EosResult { let rho0 = match density_initialization { DensityInitialization::InitialDensity(r) => r, @@ -577,19 +580,19 @@ impl State { let fx = s.molar_entropy(Contributions::Total) - molar_entropy; Ok((fx, dfx, s)) }; - newton(rho0, f, 1.0e-12 * U::reference_density()) + newton(rho0, f, 1.0e-12 * SIUnit::reference_density()) } /// Return a new `State` for given pressure $p$ and molar entropy $s$. pub fn new_nps( eos: &Arc, - pressure: QuantityScalar, - molar_entropy: QuantityScalar, - moles: &QuantityArray1, - density_initialization: DensityInitialization, - initial_temperature: Option>, + pressure: SINumber, + molar_entropy: SINumber, + moles: &SIArray1, + density_initialization: DensityInitialization, + initial_temperature: Option, ) -> EosResult { - let t0 = initial_temperature.unwrap_or(298.15 * U::reference_temperature()); + let t0 = initial_temperature.unwrap_or(298.15 * SIUnit::reference_temperature()); let mut density = density_initialization; let f = |x0| { let s = State::new_npt(eos, x0, pressure, moles, density)?; @@ -598,52 +601,49 @@ impl State { density = DensityInitialization::InitialDensity(s.density); Ok((fx, dfx, s)) }; - newton(t0, f, 1.0e-8 * U::reference_temperature()) + newton(t0, f, 1.0e-8 * SIUnit::reference_temperature()) } /// Return a new `State` for given volume $V$ and molar internal energy $u$. pub fn new_nvu( eos: &Arc, - volume: QuantityScalar, - molar_internal_energy: QuantityScalar, - moles: &QuantityArray1, - initial_temperature: Option>, + volume: SINumber, + molar_internal_energy: SINumber, + moles: &SIArray1, + initial_temperature: Option, ) -> EosResult { - let t0 = initial_temperature.unwrap_or(298.15 * U::reference_temperature()); + let t0 = initial_temperature.unwrap_or(298.15 * SIUnit::reference_temperature()); let f = |x0| { let s = State::new_nvt(eos, x0, volume, moles)?; let fx = s.molar_internal_energy(Contributions::Total) - molar_internal_energy; let dfx = s.c_v(Contributions::Total); Ok((fx, dfx, s)) }; - newton(t0, f, 1.0e-8 * U::reference_temperature()) + newton(t0, f, 1.0e-8 * SIUnit::reference_temperature()) } /// Update the state with the given temperature - pub fn update_temperature(&self, temperature: QuantityScalar) -> EosResult { + pub fn update_temperature(&self, temperature: SINumber) -> EosResult { 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: &QuantityArray1, - ) -> EosResult<()> { + 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(U::reference_molar_energy() / U::reference_density())?; - let f_r = f.to_reduced(U::reference_molar_energy())?; + 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) * U::reference_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(U::reference_molar_energy())?) < 1e-8 { + if norm(&f.to_reduced(SIUnit::reference_molar_energy())?) < 1e-8 { return Ok(()); } } @@ -653,7 +653,7 @@ impl State { } /// Update the state with the given molar Gibbs energy. - pub fn update_gibbs_energy(mut self, molar_gibbs_energy: QuantityScalar) -> EosResult { + 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; @@ -664,7 +664,7 @@ impl State { self.total_moles / rho, &self.moles, )?; - if f.to_reduced(U::reference_molar_energy())?.abs() < 1e-8 { + if f.to_reduced(SIUnit::reference_molar_energy())?.abs() < 1e-8 { return Ok(self); } } @@ -742,22 +742,13 @@ impl State { } } -fn is_close( - x: QuantityScalar, - y: QuantityScalar, - atol: QuantityScalar, - rtol: f64, -) -> bool { +fn is_close(x: SINumber, y: SINumber, atol: SINumber, rtol: f64) -> bool { (x - y).abs() <= atol + rtol * y.abs() } -fn newton( - mut x0: QuantityScalar, - mut f: F, - atol: QuantityScalar, -) -> EosResult> +fn newton(mut x0: SINumber, mut f: F, atol: SINumber) -> EosResult> where - F: FnMut(QuantityScalar) -> EosResult<(QuantityScalar, QuantityScalar, State)>, + F: FnMut(SINumber) -> EosResult<(SINumber, SINumber, State)>, { let rtol = 1e-10; let maxiter = 50; @@ -781,14 +772,10 @@ where /// /// There is no validation of the physical state, e.g. /// if resulting densities are below maximum packing fraction. -fn validate( - temperature: QuantityScalar, - volume: QuantityScalar, - moles: &QuantityArray1, -) -> EosResult<()> { - let t = temperature.to_reduced(U::reference_temperature())?; - let v = volume.to_reduced(U::reference_volume())?; - let m = moles.to_reduced(U::reference_moles())?; +fn validate(temperature: SINumber, volume: SINumber, moles: &SIArray1) -> EosResult<()> { + let t = temperature.to_reduced(SIUnit::reference_temperature())?; + let v = volume.to_reduced(SIUnit::reference_volume())?; + let m = moles.to_reduced(SIUnit::reference_moles())?; if !t.is_finite() || t.is_sign_negative() { return Err(EosError::InvalidState( String::from("validate"), @@ -816,20 +803,20 @@ fn validate( } #[derive(Clone, Copy)] -pub enum TPSpec { - Temperature(QuantityScalar), - Pressure(QuantityScalar), +pub enum TPSpec { + Temperature(SINumber), + Pressure(SINumber), } -impl TryFrom> for TPSpec +impl TryFrom for TPSpec where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { type Error = EosError; - fn try_from(quantity: QuantityScalar) -> EosResult { - if quantity.has_unit(&U::reference_temperature()) { + fn try_from(quantity: SINumber) -> EosResult { + if quantity.has_unit(&SIUnit::reference_temperature()) { Ok(Self::Temperature(quantity)) - } else if quantity.has_unit(&U::reference_pressure()) { + } else if quantity.has_unit(&SIUnit::reference_pressure()) { Ok(Self::Pressure(quantity)) } else { Err(EosError::WrongUnits( diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index 6901c109b..baf749718 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -4,7 +4,7 @@ use crate::errors::EosResult; use crate::EosUnit; use ndarray::{arr1, Array1, Array2}; use num_dual::DualNum; -use quantity::{QuantityArray, QuantityArray1, QuantityArray2, QuantityScalar}; +use quantity::si::*; use std::iter::FromIterator; use std::ops::{Add, Deref, Sub}; use std::sync::Arc; @@ -32,39 +32,40 @@ pub enum Contributions { } /// # State properties -impl State { +impl State { fn get_or_compute_derivative( &self, derivative: PartialDerivative, evaluate: Evaluate, - ) -> QuantityScalar { + ) -> 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()) - * U::reference_energy() + * SIUnit::reference_energy() } PartialDerivative::First(v) => { let new_state = self.derive1(v); -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).eps[0] - * (U::reference_energy() / v.reference()) + * (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[0] - * (U::reference_energy() / (v.reference() * v.reference())) + * (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[(0, 0)] - * (U::reference_energy() / (v1.reference() * v2.reference())) + * (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 - * (U::reference_energy() / (v.reference() * v.reference() * v.reference())) + * (SIUnit::reference_energy() + / (v.reference() * v.reference() * v.reference())) } }; } @@ -78,34 +79,34 @@ impl State { let new_state = self.derive0(); let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_f64(computation) * U::reference_energy() + 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) * U::reference_energy() + 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) * U::reference_energy() + 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) * U::reference_energy() + 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) * U::reference_energy() + cache.get_or_insert_with_hd364(v, computation) * SIUnit::reference_energy() / (v.reference() * v.reference() * v.reference()) } }), @@ -117,32 +118,32 @@ impl State { PartialDerivative::Zeroth => { let new_state = self.derive0(); self.eos.ideal_gas().evaluate(&new_state) - * U::reference_energy() + * 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[0] - * U::reference_energy() + * 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[0] - * U::reference_energy() + * 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 [(0, 0)] - * U::reference_energy() + * 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 - * U::reference_energy() + * SIUnit::reference_energy() / (v.reference() * v.reference() * v.reference()) } }), @@ -176,7 +177,7 @@ impl State { let state_p = Self::new_nvt_unchecked( &self.eos, self.temperature, - self.total_moles * U::gas_constant() * self.temperature / p, + self.total_moles * SIUnit::gas_constant() * self.temperature / p, &self.moles, ); if additive { @@ -189,131 +190,131 @@ impl State { } } - fn helmholtz_energy_(&self, evaluate: Evaluate) -> QuantityScalar { + fn helmholtz_energy_(&self, evaluate: Evaluate) -> SINumber { self.get_or_compute_derivative(PartialDerivative::Zeroth, evaluate) } - fn pressure_(&self, evaluate: Evaluate) -> QuantityScalar { + fn pressure_(&self, evaluate: Evaluate) -> SINumber { -self.get_or_compute_derivative(PartialDerivative::First(DV), evaluate) } - fn entropy_(&self, evaluate: Evaluate) -> QuantityScalar { + fn entropy_(&self, evaluate: Evaluate) -> SINumber { -self.get_or_compute_derivative(PartialDerivative::First(DT), evaluate) } - fn chemical_potential_(&self, evaluate: Evaluate) -> QuantityArray1 { - QuantityArray::from_shape_fn(self.eos.components(), |i| { + 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) -> QuantityScalar { + fn dp_dv_(&self, evaluate: Evaluate) -> SINumber { -self.get_or_compute_derivative(PartialDerivative::Second(DV), evaluate) } - fn dp_dt_(&self, evaluate: Evaluate) -> QuantityScalar { + fn dp_dt_(&self, evaluate: Evaluate) -> SINumber { -self.get_or_compute_derivative(PartialDerivative::SecondMixed(DV, DT), evaluate) } - fn dp_dni_(&self, evaluate: Evaluate) -> QuantityArray1 { - QuantityArray::from_shape_fn(self.eos.components(), |i| { + 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 d2p_dv2_(&self, evaluate: Evaluate) -> QuantityScalar { + fn d2p_dv2_(&self, evaluate: Evaluate) -> SINumber { -self.get_or_compute_derivative(PartialDerivative::Third(DV), evaluate) } - fn dmu_dt_(&self, evaluate: Evaluate) -> QuantityArray1 { - QuantityArray::from_shape_fn(self.eos.components(), |i| { + 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) -> QuantityArray2 { + fn dmu_dni_(&self, evaluate: Evaluate) -> SIArray2 { let n = self.eos.components(); - QuantityArray::from_shape_fn((n, n), |(i, j)| { + 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) -> QuantityScalar { + fn ds_dt_(&self, evaluate: Evaluate) -> SINumber { -self.get_or_compute_derivative(PartialDerivative::Second(DT), evaluate) } - fn d2s_dt2_(&self, evaluate: Evaluate) -> QuantityScalar { + fn d2s_dt2_(&self, evaluate: Evaluate) -> SINumber { -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) -> QuantityScalar { + 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 * U::gas_constant())) + (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) -> QuantityScalar { + 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) -> QuantityScalar { + 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) -> QuantityScalar { + 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) -> QuantityArray1 { + 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) -> QuantityScalar { + 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) -> QuantityScalar { + 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) -> QuantityArray1 { + 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) -> QuantityArray1 { + pub fn chemical_potential(&self, contributions: Contributions) -> SIArray1 { self.evaluate_property(Self::chemical_potential_, contributions, true) } /// 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) -> QuantityArray1 { + pub fn dmu_dt(&self, contributions: Contributions) -> SIArray1 { 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) -> QuantityArray2 { + pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { 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) - / (U::gas_constant() * self.temperature)) + / (SIUnit::gas_constant() * self.temperature)) .into_value() .unwrap() } @@ -328,7 +329,7 @@ impl State { &eos, self.temperature, pressure, - &(arr1(&[1.0]) * U::reference_moles()), + &(arr1(&[1.0]) * SIUnit::reference_moles()), crate::DensityInitialization::Liquid, )?; Ok(state.ln_phi()[0]) @@ -345,29 +346,29 @@ 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) -> QuantityArray1 { + 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) - / (U::gas_constant() * 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) -> QuantityArray1 { + pub fn dln_phi_dp(&self) -> SIArray1 { self.partial_molar_volume(Contributions::ResidualNpt) - / (U::gas_constant() * self.temperature) + / (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) -> QuantityArray2 { + 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 = QuantityArray::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j)); - (dmu_dni + dp_dn_2 / dp_dv) / (U::gas_constant() * self.temperature) + 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 } @@ -375,9 +376,9 @@ impl State { pub fn thermodynamic_factor(&self) -> Array2 { let dln_phi_dnj = self .dln_phi_dnj() - .to_reduced(U::reference_moles().powi(-1)) + .to_reduced(SIUnit::reference_moles().powi(-1)) .unwrap(); - let moles = self.moles.to_reduced(U::reference_moles()).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 } @@ -385,14 +386,14 @@ impl State { } /// Molar isochoric heat capacity: $c_v=\left(\frac{\partial u}{\partial T}\right)_{V,N_i}$ - pub fn c_v(&self, contributions: Contributions) -> QuantityScalar { + 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) } /// 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) -> QuantityScalar { + 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 }; @@ -400,7 +401,7 @@ impl State { } /// Molar isobaric heat capacity: $c_p=\left(\frac{\partial h}{\partial T}\right)_{p,N_i}$ - pub fn c_p(&self, contributions: Contributions) -> QuantityScalar { + pub fn c_p(&self, contributions: Contributions) -> SINumber { let func = |s: &Self, evaluate: Evaluate| { s.temperature / s.total_moles * (s.ds_dt_(evaluate) @@ -410,22 +411,22 @@ impl State { } /// Entropy: $S=-\left(\frac{\partial A}{\partial T}\right)_{V,N_i}$ - pub fn entropy(&self, contributions: Contributions) -> QuantityScalar { + pub fn entropy(&self, contributions: Contributions) -> SINumber { self.evaluate_property(Self::entropy_, contributions, true) } /// 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) -> QuantityScalar { + pub fn ds_dt(&self, contributions: Contributions) -> SINumber { self.evaluate_property(Self::ds_dt_, contributions, true) } /// molar entropy: $s=\frac{S}{N}$ - pub fn molar_entropy(&self, contributions: Contributions) -> QuantityScalar { + pub fn molar_entropy(&self, contributions: Contributions) -> SINumber { self.entropy(contributions) / self.total_moles } /// Enthalpy: $H=A+TS+pV$ - pub fn enthalpy(&self, contributions: Contributions) -> QuantityScalar { + pub fn enthalpy(&self, contributions: Contributions) -> SINumber { let func = |s: &Self, evaluate: Evaluate| { s.temperature * s.entropy_(evaluate) + s.helmholtz_energy_(evaluate) @@ -435,22 +436,22 @@ impl State { } /// molar enthalpy: $h=\frac{H}{N}$ - pub fn molar_enthalpy(&self, contributions: Contributions) -> QuantityScalar { + pub fn molar_enthalpy(&self, contributions: Contributions) -> SINumber { self.enthalpy(contributions) / self.total_moles } /// Helmholtz energy: $A$ - pub fn helmholtz_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn helmholtz_energy(&self, contributions: Contributions) -> SINumber { self.evaluate_property(Self::helmholtz_energy_, contributions, true) } /// molar Helmholtz energy: $a=\frac{A}{N}$ - pub fn molar_helmholtz_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn molar_helmholtz_energy(&self, contributions: Contributions) -> SINumber { self.helmholtz_energy(contributions) / self.total_moles } /// Internal energy: $U=A+TS$ - pub fn internal_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn internal_energy(&self, contributions: Contributions) -> SINumber { let func = |s: &Self, evaluate: Evaluate| { s.temperature * s.entropy_(evaluate) + s.helmholtz_energy_(evaluate) }; @@ -458,12 +459,12 @@ impl State { } /// Molar internal energy: $u=\frac{U}{N}$ - pub fn molar_internal_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn molar_internal_energy(&self, contributions: Contributions) -> SINumber { self.internal_energy(contributions) / self.total_moles } /// Gibbs energy: $G=A+pV$ - pub fn gibbs_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn gibbs_energy(&self, contributions: Contributions) -> SINumber { let func = |s: &Self, evaluate: Evaluate| { s.pressure_(evaluate) * s.volume + s.helmholtz_energy_(evaluate) }; @@ -471,12 +472,12 @@ impl State { } /// Molar Gibbs energy: $g=\frac{G}{N}$ - pub fn molar_gibbs_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn molar_gibbs_energy(&self, contributions: Contributions) -> SINumber { self.gibbs_energy(contributions) / self.total_moles } /// 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) -> QuantityArray1 { + 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))) }; @@ -484,90 +485,89 @@ impl State { } /// 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) -> QuantityArray1 { + pub fn partial_molar_enthalpy(&self, contributions: Contributions) -> SIArray1 { let s = self.partial_molar_entropy(contributions); let mu = self.chemical_potential(contributions); s * self.temperature + mu } /// Joule Thomson coefficient: $\mu_{JT}=\left(\frac{\partial T}{\partial p}\right)_{H,N_i}$ - pub fn joule_thomson(&self) -> QuantityScalar { + pub fn joule_thomson(&self) -> SINumber { let c = Contributions::Total; -(self.volume + self.temperature * self.dp_dt(c) / self.dp_dv(c)) / (self.total_moles * self.c_p(c)) } /// Isentropic compressibility: $\kappa_s=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{S,N_i}$ - pub fn isentropic_compressibility(&self) -> QuantityScalar { + pub fn isentropic_compressibility(&self) -> SINumber { let c = Contributions::Total; -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) -> QuantityScalar { + pub fn isothermal_compressibility(&self) -> SINumber { let c = Contributions::Total; -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 { - -(U::gas_constant() * self.temperature * self.density) + -(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, QuantityScalar)> { + 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(); res.push(( ig.to_string(), - ig.evaluate(&new_state) * new_state.temperature * U::reference_energy(), + ig.evaluate(&new_state) * new_state.temperature * SIUnit::reference_energy(), )); for (s, v) in contributions { - res.push((s, v * new_state.temperature * U::reference_energy())); + 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, QuantityScalar)> { + 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.ideal_gas(); res.push(( ig.to_string(), - -(ig.evaluate(&new_state) * new_state.temperature).eps[0] * U::reference_pressure(), + -(ig.evaluate(&new_state) * new_state.temperature).eps[0] + * SIUnit::reference_pressure(), )); for (s, v) in contributions { res.push(( s, - -(v * new_state.temperature).eps[0] * U::reference_pressure(), + -(v * new_state.temperature).eps[0] * 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, QuantityScalar)> { + 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.ideal_gas(); res.push(( ig.to_string(), - (ig.evaluate(&new_state) * new_state.temperature).eps[0] * U::reference_molar_energy(), + (ig.evaluate(&new_state) * new_state.temperature).eps[0] + * SIUnit::reference_molar_energy(), )); for (s, v) in contributions { res.push(( s, - (v * new_state.temperature).eps[0] * U::reference_molar_energy(), + (v * new_state.temperature).eps[0] * SIUnit::reference_molar_energy(), )); } res @@ -578,24 +578,24 @@ 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) -> QuantityScalar { + pub fn total_molar_weight(&self) -> SINumber { (self.eos.molar_weight() * &self.molefracs).sum() } /// Mass of each component: $m_i=n_iMW_i$ - pub fn mass(&self) -> QuantityArray1 { + pub fn mass(&self) -> SIArray1 { self.moles.clone() * self.eos.molar_weight() } /// Total mass: $m=\sum_im_i=nMW$ - pub fn total_mass(&self) -> QuantityScalar { + pub fn total_mass(&self) -> SINumber { self.total_moles * self.total_molar_weight() } /// Mass density: $\rho^{(m)}=\frac{m}{V}$ - pub fn mass_density(&self) -> QuantityScalar { + pub fn mass_density(&self) -> SINumber { self.density * self.total_molar_weight() } @@ -605,41 +605,41 @@ impl> State { } /// Specific entropy: $s^{(m)}=\frac{S}{m}$ - pub fn specific_entropy(&self, contributions: Contributions) -> QuantityScalar { + pub fn specific_entropy(&self, contributions: Contributions) -> SINumber { self.molar_entropy(contributions) / self.total_molar_weight() } /// Specific enthalpy: $h^{(m)}=\frac{H}{m}$ - pub fn specific_enthalpy(&self, contributions: Contributions) -> QuantityScalar { + pub fn specific_enthalpy(&self, contributions: Contributions) -> SINumber { self.molar_enthalpy(contributions) / self.total_molar_weight() } /// Specific Helmholtz energy: $a^{(m)}=\frac{A}{m}$ - pub fn specific_helmholtz_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn specific_helmholtz_energy(&self, contributions: Contributions) -> SINumber { self.molar_helmholtz_energy(contributions) / self.total_molar_weight() } /// Specific internal energy: $u^{(m)}=\frac{U}{m}$ - pub fn specific_internal_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn specific_internal_energy(&self, contributions: Contributions) -> SINumber { self.molar_internal_energy(contributions) / self.total_molar_weight() } /// Specific Gibbs energy: $g^{(m)}=\frac{G}{m}$ - pub fn specific_gibbs_energy(&self, contributions: Contributions) -> QuantityScalar { + pub fn specific_gibbs_energy(&self, contributions: Contributions) -> SINumber { self.molar_gibbs_energy(contributions) / self.total_molar_weight() } /// Speed of sound: $c=\sqrt{\left(\frac{\partial p}{\partial\rho^{(m)}}\right)_{S,N_i}}$ - pub fn speed_of_sound(&self) -> QuantityScalar { + pub fn speed_of_sound(&self) -> SINumber { (1.0 / (self.density * self.total_molar_weight() * self.isentropic_compressibility())) .sqrt() .unwrap() } } -impl State { +impl State { // This function is designed specifically for use in density iterations - pub(crate) fn p_dpdrho(&self) -> (QuantityScalar, QuantityScalar) { + pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { let dp_dv = self.dp_dv(Contributions::Total); ( self.pressure(Contributions::Total), @@ -648,7 +648,7 @@ impl State { } // This function is designed specifically for use in spinodal iterations - pub(crate) fn d2pdrho2(&self) -> (QuantityScalar, QuantityScalar, QuantityScalar) { + pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { let d2p_dv2 = self.d2p_dv2(Contributions::Total); let dp_dv = self.dp_dv(Contributions::Total); ( @@ -663,12 +663,12 @@ impl State { /// /// These properties are available for equations of state /// that implement the [EntropyScaling] trait. -impl> State { +impl State { /// Return the viscosity via entropy scaling. - pub fn viscosity(&self) -> EosResult> { + pub fn viscosity(&self) -> EosResult { let s = self .molar_entropy(Contributions::ResidualNvt) - .to_reduced(U::reference_molar_entropy())?; + .to_reduced(SIUnit::reference_molar_entropy())?; Ok(self .eos .viscosity_reference(self.temperature, self.volume, &self.moles)? @@ -682,21 +682,21 @@ impl> State { pub fn ln_viscosity_reduced(&self) -> EosResult { let s = self .molar_entropy(Contributions::ResidualNvt) - .to_reduced(U::reference_molar_entropy())?; + .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> { + 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> { + pub fn diffusion(&self) -> EosResult { let s = self .molar_entropy(Contributions::ResidualNvt) - .to_reduced(U::reference_molar_entropy())?; + .to_reduced(SIUnit::reference_molar_entropy())?; Ok(self .eos .diffusion_reference(self.temperature, self.volume, &self.moles)? @@ -710,21 +710,21 @@ impl> State { pub fn ln_diffusion_reduced(&self) -> EosResult { let s = self .molar_entropy(Contributions::ResidualNvt) - .to_reduced(U::reference_molar_entropy())?; + .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> { + 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> { + pub fn thermal_conductivity(&self) -> EosResult { let s = self .molar_entropy(Contributions::ResidualNvt) - .to_reduced(U::reference_molar_entropy())?; + .to_reduced(SIUnit::reference_molar_entropy())?; Ok(self .eos .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? @@ -741,13 +741,13 @@ impl> State { pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { let s = self .molar_entropy(Contributions::ResidualNvt) - .to_reduced(U::reference_molar_entropy())?; + .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> { + pub fn thermal_conductivity_reference(&self) -> EosResult { self.eos .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) } @@ -755,16 +755,16 @@ impl> State { /// A list of states for a simple access to properties /// of multiple states. -pub struct StateVec<'a, U, E>(pub Vec<&'a State>); +pub struct StateVec<'a, E>(pub Vec<&'a State>); -impl<'a, U, E> FromIterator<&'a State> for StateVec<'a, U, E> { - fn from_iter>>(iter: I) -> Self { +impl<'a, E> FromIterator<&'a State> for StateVec<'a, E> { + fn from_iter>>(iter: I) -> Self { Self(iter.into_iter().collect()) } } -impl<'a, U, E> IntoIterator for StateVec<'a, U, E> { - type Item = &'a State; +impl<'a, E> IntoIterator for StateVec<'a, E> { + type Item = &'a State; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { @@ -772,21 +772,21 @@ impl<'a, U, E> IntoIterator for StateVec<'a, U, E> { } } -impl<'a, U, E> Deref for StateVec<'a, U, E> { - type Target = Vec<&'a State>; +impl<'a, E> Deref for StateVec<'a, E> { + type Target = Vec<&'a State>; fn deref(&self) -> &Self::Target { &self.0 } } -impl<'a, U: EosUnit, E: EquationOfState> StateVec<'a, U, E> { - pub fn temperature(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.0.len(), |i| self.0[i].temperature) +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) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.0.len(), |i| self.0[i].pressure(Contributions::Total)) + pub fn pressure(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].pressure(Contributions::Total)) } pub fn compressibility(&self) -> Array1 { @@ -795,12 +795,12 @@ impl<'a, U: EosUnit, E: EquationOfState> StateVec<'a, U, E> { }) } - pub fn density(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.0.len(), |i| self.0[i].density) + pub fn density(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].density) } - pub fn moles(&self) -> QuantityArray2 { - QuantityArray2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + 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) }) } @@ -811,22 +811,22 @@ impl<'a, U: EosUnit, E: EquationOfState> StateVec<'a, U, E> { }) } - pub fn molar_enthalpy(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.0.len(), |i| { + 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) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.0.len(), |i| { + pub fn molar_entropy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { self.0[i].molar_entropy(Contributions::Total) }) } } -impl<'a, U: EosUnit, E: EquationOfState + MolarWeight> StateVec<'a, U, E> { - pub fn mass_density(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) +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 { @@ -835,14 +835,14 @@ impl<'a, U: EosUnit, E: EquationOfState + MolarWeight> StateVec<'a, U, E> { }) } - pub fn specific_enthalpy(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.0.len(), |i| { + 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) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.0.len(), |i| { + 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-derive/src/dft.rs b/feos-derive/src/dft.rs index f16033242..00ce3788d 100644 --- a/feos-derive/src/dft.rs +++ b/feos-derive/src/dft.rs @@ -174,7 +174,7 @@ fn impl_molar_weight( } } Ok(quote! { - impl MolarWeight for FunctionalVariant { + impl MolarWeight for FunctionalVariant { fn molar_weight(&self) -> SIArray1 { match self { #(#molar_weight,)* diff --git a/feos-derive/src/eos.rs b/feos-derive/src/eos.rs index d7ee7459a..6dc4afea1 100644 --- a/feos-derive/src/eos.rs +++ b/feos-derive/src/eos.rs @@ -102,7 +102,7 @@ fn impl_molar_weight( } } Ok(quote! { - impl MolarWeight for EosVariant { + impl MolarWeight for EosVariant { fn molar_weight(&self) -> SIArray1 { match self { #(#molar_weight,)* @@ -148,7 +148,7 @@ fn impl_entropy_scaling( } Ok(quote! { - impl EntropyScaling for EosVariant { + impl EntropyScaling for EosVariant { fn viscosity_reference( &self, temperature: SINumber, diff --git a/feos-dft/CHANGELOG.md b/feos-dft/CHANGELOG.md index 7c34cd709..889275978 100644 --- a/feos-dft/CHANGELOG.md +++ b/feos-dft/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed the `3d_dft` feature to `rayon` in accordance to the other feos crates. [#62](https://github.com/feos-org/feos/pull/62) - internally rewrote the implementation of the Euler-Lagrange equation to use a bulk density instead of the chemical potential as variable. [#60](https://github.com/feos-org/feos/pull/60) - Renamed the parameter `beta` of the Picard iteration and Anderson mixing solvers to `damping_coefficient`. [#75](https://github.com/feos-org/feos/pull/75) +- Removed generics for units in all structs and traits in favor of static SI units. [#115](https://github.com/feos-org/feos/pull/115) ## [0.3.2] - 2022-10-13 ### Changed diff --git a/feos-dft/src/adsorption/external_potential.rs b/feos-dft/src/adsorption/external_potential.rs index 51ac41ad4..0e7ecca30 100644 --- a/feos-dft/src/adsorption/external_potential.rs +++ b/feos-dft/src/adsorption/external_potential.rs @@ -3,18 +3,18 @@ use crate::adsorption::fea_potential::calculate_fea_potential; use crate::functional::HelmholtzEnergyFunctional; #[cfg(feature = "rayon")] use crate::geometry::Geometry; -use feos_core::EosUnit; use libm::tgamma; use ndarray::{Array1, Array2, Axis as Axis_nd}; +use quantity::si::SIUnit; #[cfg(feature = "rayon")] -use quantity::{QuantityArray2, QuantityScalar}; +use quantity::si::{SIArray2, SINumber}; use std::{f64::consts::PI, marker::PhantomData}; const DELTA_STEELE: f64 = 3.35; /// A collection of external potentials. #[derive(Clone)] -pub enum ExternalPotential { +pub enum ExternalPotential { /// Hard wall potential: $V_i^\mathrm{ext}(z)=\begin{cases}\infty&z\leq\sigma_{si}\\\\0&z>\sigma_{si}\end{cases},~~~~\sigma_{si}=\frac{1}{2}\left(\sigma_{ss}+\sigma_{ii}\right)$ HardWall { sigma_ss: f64 }, /// 9-3 Lennard-Jones potential: $V_i^\mathrm{ext}(z)=\frac{2\pi}{45} m_i\varepsilon_{si}\sigma_{si}^3\rho_s\left(2\left(\frac{\sigma_{si}}{z}\right)^9-15\left(\frac{\sigma_{si}}{z}\right)^3\right),~~~~\varepsilon_{si}=\sqrt{\varepsilon_{ss}\varepsilon_{ii}},~~~~\sigma_{si}=\frac{1}{2}\left(\sigma_{ss}+\sigma_{ii}\right)$ @@ -54,11 +54,11 @@ pub enum ExternalPotential { /// Free-energy averaged potential: #[cfg(feature = "rayon")] FreeEnergyAveraged { - coordinates: QuantityArray2, + coordinates: SIArray2, sigma_ss: Array1, epsilon_k_ss: Array1, pore_center: [f64; 3], - system_size: [QuantityScalar; 3], + system_size: [SINumber; 3], n_grid: [usize; 2], cutoff_radius: Option, }, @@ -68,7 +68,7 @@ pub enum ExternalPotential { /// Needed to keep `FreeEnergyAveraged` optional #[doc(hidden)] - Phantom(PhantomData), + Phantom(PhantomData), } /// Parameters of the fluid required to evaluate the external potential. @@ -78,7 +78,7 @@ pub trait FluidParameters: HelmholtzEnergyFunctional { } #[allow(unused_variables)] -impl ExternalPotential { +impl ExternalPotential { // Evaluate the external potential in cartesian coordinates for a given grid and fluid parameters. pub fn calculate_cartesian_potential( &self, diff --git a/feos-dft/src/adsorption/fea_potential.rs b/feos-dft/src/adsorption/fea_potential.rs index 39a026446..a9cbb8cb2 100644 --- a/feos-dft/src/adsorption/fea_potential.rs +++ b/feos-dft/src/adsorption/fea_potential.rs @@ -4,19 +4,19 @@ use crate::Geometry; use feos_core::EosUnit; use gauss_quad::GaussLegendre; use ndarray::{Array1, Array2, Zip}; -use quantity::{QuantityArray2, QuantityScalar}; +use quantity::si::{SIArray2, SINumber, SIUnit}; use std::f64::consts::PI; use std::usize; // Calculate free-energy average potential for given solid structure. -pub fn calculate_fea_potential( +pub fn calculate_fea_potential( grid: &Array1, mi: f64, - coordinates: &QuantityArray2, + coordinates: &SIArray2, sigma_sf: Array1, epsilon_k_sf: Array1, pore_center: &[f64; 3], - system_size: &[QuantityScalar; 3], + system_size: &[SINumber; 3], n_grid: &[usize; 2], temperature: f64, geometry: Geometry, @@ -31,14 +31,20 @@ pub fn calculate_fea_potential( // dimensionless solid coordinates let coordinates = Array2::from_shape_fn(coordinates.raw_dim(), |(i, j)| { (coordinates.get((i, j))) - .to_reduced(U::reference_length()) + .to_reduced(SIUnit::reference_length()) .unwrap() }); let system_size = [ - system_size[0].to_reduced(U::reference_length()).unwrap(), - system_size[1].to_reduced(U::reference_length()).unwrap(), - system_size[2].to_reduced(U::reference_length()).unwrap(), + system_size[0] + .to_reduced(SIUnit::reference_length()) + .unwrap(), + system_size[1] + .to_reduced(SIUnit::reference_length()) + .unwrap(), + system_size[2] + .to_reduced(SIUnit::reference_length()) + .unwrap(), ]; // Create secondary axis: diff --git a/feos-dft/src/adsorption/mod.rs b/feos-dft/src/adsorption/mod.rs index e2fdaa306..921a30b2a 100644 --- a/feos-dft/src/adsorption/mod.rs +++ b/feos-dft/src/adsorption/mod.rs @@ -6,7 +6,7 @@ use feos_core::{ SolverOptions, State, StateBuilder, }; use ndarray::{Array1, Dimension, Ix1, Ix3, RemoveAxis}; -use quantity::{QuantityArray1, QuantityArray2, QuantityScalar}; +use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; use std::iter; use std::sync::Arc; @@ -26,32 +26,29 @@ const MAX_ITER_ADSORPTION_EQUILIBRIUM: usize = 50; const TOL_ADSORPTION_EQUILIBRIUM: f64 = 1e-8; /// Container structure for the calculation of adsorption isotherms. -pub struct Adsorption { +pub struct Adsorption { components: usize, dimension: i32, - pub profiles: Vec>>, + pub profiles: Vec>>, } /// Container structure for adsorption isotherms in 1D pores. -pub type Adsorption1D = Adsorption; +pub type Adsorption1D = Adsorption; /// Container structure for adsorption isotherms in 3D pores. -pub type Adsorption3D = Adsorption; +pub type Adsorption3D = Adsorption; -impl< - U: EosUnit, - D: Dimension + RemoveAxis + 'static, - F: HelmholtzEnergyFunctional + FluidParameters, - > Adsorption +impl + Adsorption where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, D::Larger: Dimension, D::Smaller: Dimension, ::Larger: Dimension, { - fn new>( + fn new>( functional: &Arc>, pore: &S, - profiles: Vec>>, + profiles: Vec>>, ) -> Self { Self { components: functional.components(), @@ -61,14 +58,14 @@ where } /// Calculate an adsorption isotherm (starting at low pressure) - pub fn adsorption_isotherm>( + pub fn adsorption_isotherm>( functional: &Arc>, - temperature: QuantityScalar, - pressure: &QuantityArray1, + temperature: SINumber, + pressure: &SIArray1, pore: &S, molefracs: Option<&Array1>, solver: Option<&DFTSolver>, - ) -> EosResult> { + ) -> EosResult> { Self::isotherm( functional, temperature, @@ -81,14 +78,14 @@ where } /// Calculate an desorption isotherm (starting at high pressure) - pub fn desorption_isotherm>( + pub fn desorption_isotherm>( functional: &Arc>, - temperature: QuantityScalar, - pressure: &QuantityArray1, + temperature: SINumber, + pressure: &SIArray1, pore: &S, molefracs: Option<&Array1>, solver: Option<&DFTSolver>, - ) -> EosResult> { + ) -> EosResult> { let pressure = pressure.into_iter().rev().collect(); let isotherm = Self::isotherm( functional, @@ -107,14 +104,14 @@ where } /// Calculate an equilibrium isotherm - pub fn equilibrium_isotherm>( + pub fn equilibrium_isotherm>( functional: &Arc>, - temperature: QuantityScalar, - pressure: &QuantityArray1, + temperature: SINumber, + pressure: &SIArray1, pore: &S, molefracs: Option<&Array1>, solver: Option<&DFTSolver>, - ) -> EosResult> { + ) -> EosResult> { let (p_min, p_max) = (pressure.get(0), pressure.get(pressure.len() - 1)); let equilibrium = Self::phase_equilibrium( functional, @@ -194,18 +191,18 @@ where } } - fn isotherm>( + fn isotherm>( functional: &Arc>, - temperature: QuantityScalar, - pressure: &QuantityArray1, + temperature: SINumber, + pressure: &SIArray1, pore: &S, molefracs: Option<&Array1>, - density_initialization: DensityInitialization, + density_initialization: DensityInitialization, solver: Option<&DFTSolver>, - ) -> EosResult> { + ) -> EosResult> { let moles = - functional.validate_moles(molefracs.map(|x| x * U::reference_moles()).as_ref())?; - let mut profiles: Vec>> = Vec::with_capacity(pressure.len()); + functional.validate_moles(molefracs.map(|x| x * SIUnit::reference_moles()).as_ref())?; + let mut profiles: Vec>> = Vec::with_capacity(pressure.len()); // On the first iteration, initialize the density profile according to the direction // and calculate the external potential once. @@ -256,18 +253,18 @@ where } /// Calculate the phase transition from an empty to a filled pore. - pub fn phase_equilibrium>( + pub fn phase_equilibrium>( functional: &Arc>, - temperature: QuantityScalar, - p_min: QuantityScalar, - p_max: QuantityScalar, + temperature: SINumber, + p_min: SINumber, + p_max: SINumber, pore: &S, molefracs: Option<&Array1>, solver: Option<&DFTSolver>, options: SolverOptions, - ) -> EosResult> { + ) -> EosResult> { let moles = - functional.validate_moles(molefracs.map(|x| x * U::reference_moles()).as_ref())?; + 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) @@ -301,7 +298,7 @@ where .bulk .partial_molar_volume(Contributions::Total)) .sum(); - let f = |s: &PoreProfile, n: QuantityScalar| -> EosResult<_> { + let f = |s: &PoreProfile, n: SINumber| -> EosResult<_> { Ok(s.grand_potential.unwrap() + s.profile.bulk.molar_gibbs_energy(Contributions::Total) * n) }; @@ -354,7 +351,7 @@ where // Newton step let delta_g = (vapor.grand_potential.unwrap() - liquid.grand_potential.unwrap()) / (nv - nl); - if delta_g.to_reduced(U::reference_molar_energy())?.abs() + if delta_g.to_reduced(SIUnit::reference_molar_energy())?.abs() < options.tol.unwrap_or(TOL_ADSORPTION_EQUILIBRIUM) { return Ok(Adsorption::new( @@ -373,8 +370,8 @@ where )) } - pub fn pressure(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { + pub fn pressure(&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() @@ -389,12 +386,12 @@ where p.profile.bulk.pressure(Contributions::Total) } } - Err(_) => f64::NAN * U::reference_pressure(), + Err(_) => f64::NAN * SIUnit::reference_pressure(), }) } - pub fn molar_gibbs_energy(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { + 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() @@ -409,35 +406,41 @@ where p.profile.bulk.molar_gibbs_energy(Contributions::Total) } } - Err(_) => f64::NAN * U::reference_molar_energy(), + Err(_) => f64::NAN * SIUnit::reference_molar_energy(), }) } - pub fn adsorption(&self) -> QuantityArray2 { - QuantityArray2::from_shape_fn((self.components, self.profiles.len()), |(j, i)| match &self + pub fn adsorption(&self) -> SIArray2 { + SIArray2::from_shape_fn((self.components, self.profiles.len()), |(j, i)| match &self .profiles[i] { Ok(p) => p.profile.moles().get(j), Err(_) => { - f64::NAN * U::reference_density() * U::reference_length().powi(self.dimension) + f64::NAN + * SIUnit::reference_density() + * SIUnit::reference_length().powi(self.dimension) } }) } - pub fn total_adsorption(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { + pub fn total_adsorption(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { Ok(p) => p.profile.total_moles(), Err(_) => { - f64::NAN * U::reference_density() * U::reference_length().powi(self.dimension) + f64::NAN + * SIUnit::reference_density() + * SIUnit::reference_length().powi(self.dimension) } }) } - pub fn grand_potential(&self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { + pub fn grand_potential(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { Ok(p) => p.grand_potential.unwrap(), Err(_) => { - f64::NAN * U::reference_pressure() * U::reference_length().powi(self.dimension) + f64::NAN + * SIUnit::reference_pressure() + * SIUnit::reference_length().powi(self.dimension) } }) } diff --git a/feos-dft/src/adsorption/pore.rs b/feos-dft/src/adsorption/pore.rs index 2a384d8cc..ac25fd97f 100644 --- a/feos-dft/src/adsorption/pore.rs +++ b/feos-dft/src/adsorption/pore.rs @@ -9,26 +9,26 @@ use feos_core::{Contributions, EosResult, EosUnit, State, StateBuilder}; use ndarray::prelude::*; use ndarray::Axis as Axis_nd; use ndarray::RemoveAxis; -use quantity::{QuantityArray, QuantityArray2, QuantityScalar}; +use quantity::si::{SIArray, SIArray2, SINumber, SIUnit}; use std::sync::Arc; const POTENTIAL_OFFSET: f64 = 2.0; const DEFAULT_GRID_POINTS: usize = 2048; /// Parameters required to specify a 1D pore. -pub struct Pore1D { +pub struct Pore1D { pub geometry: Geometry, - pub pore_size: QuantityScalar, - pub potential: ExternalPotential, + pub pore_size: SINumber, + pub potential: ExternalPotential, pub n_grid: Option, pub potential_cutoff: Option, } -impl Pore1D { +impl Pore1D { pub fn new( geometry: Geometry, - pore_size: QuantityScalar, - potential: ExternalPotential, + pore_size: SINumber, + potential: ExternalPotential, n_grid: Option, potential_cutoff: Option, ) -> Self { @@ -43,26 +43,26 @@ impl Pore1D { } /// Trait for the generic implementation of adsorption applications. -pub trait PoreSpecification { +pub trait PoreSpecification { /// Initialize a new single pore. fn initialize( &self, - bulk: &State>, - density: Option<&QuantityArray>, + bulk: &State>, + density: Option<&SIArray>, external_potential: Option<&Array>, - ) -> EosResult>; + ) -> EosResult>; /// Return the number of spatial dimensions of the pore. fn dimension(&self) -> i32; /// Return the pore volume using Helium at 298 K as reference. - fn pore_volume(&self) -> EosResult> + fn pore_volume(&self) -> EosResult where D::Larger: Dimension, { let bulk = StateBuilder::new(&Arc::new(Helium::new())) - .temperature(298.0 * U::reference_temperature()) - .density(U::reference_density()) + .temperature(298.0 * SIUnit::reference_temperature()) + .density(SIUnit::reference_density()) .build()?; let pore = self.initialize(&bulk, None, None)?; let pot = pore @@ -70,23 +70,23 @@ pub trait PoreSpecification { .external_potential .index_axis(Axis(0), 0) .mapv(|v| (-v).exp()) - * U::reference_temperature() - / U::reference_temperature(); + * SIUnit::reference_temperature() + / SIUnit::reference_temperature(); Ok(pore.profile.integrate(&pot)) } } /// Density profile and properties of a confined system in arbitrary dimensions. -pub struct PoreProfile { - pub profile: DFTProfile, - pub grand_potential: Option>, - pub interfacial_tension: Option>, +pub struct PoreProfile { + pub profile: DFTProfile, + pub grand_potential: Option, + pub interfacial_tension: Option, } /// Density profile and properties of a 1D confined system. -pub type PoreProfile1D = PoreProfile; +pub type PoreProfile1D = PoreProfile; -impl Clone for PoreProfile { +impl Clone for PoreProfile { fn clone(&self) -> Self { Self { profile: self.profile.clone(), @@ -96,8 +96,7 @@ impl Clone for PoreProfile { } } -impl - PoreProfile +impl PoreProfile where D::Larger: Dimension, D::Smaller: Dimension, @@ -123,7 +122,7 @@ where Ok(self) } - pub fn update_bulk(mut self, bulk: &State>) -> Self { + pub fn update_bulk(mut self, bulk: &State>) -> Self { self.profile.bulk = bulk.clone(); self.grand_potential = None; self.interfacial_tension = None; @@ -131,13 +130,13 @@ where } } -impl PoreSpecification for Pore1D { +impl PoreSpecification for Pore1D { fn initialize( &self, - bulk: &State>, - density: Option<&QuantityArray2>, + bulk: &State>, + density: Option<&SIArray2>, external_potential: Option<&Array2>, - ) -> EosResult> { + ) -> EosResult> { let dft: &F = &bulk.eos; let n_grid = self.n_grid.unwrap_or(DEFAULT_GRID_POINTS); @@ -173,7 +172,9 @@ impl PoreSpecification for Pore1D { // initialize convolver let grid = Grid::new_1d(axis); - let t = bulk.temperature.to_reduced(U::reference_temperature())?; + let t = bulk + .temperature + .to_reduced(SIUnit::reference_temperature())?; let weight_functions = dft.weight_functions(t); let convolver = ConvolverFFT::plan(&grid, &weight_functions, Some(1)); @@ -189,21 +190,21 @@ impl PoreSpecification for Pore1D { } } -fn external_potential_1d( - pore_width: QuantityScalar, - temperature: QuantityScalar, - potential: &ExternalPotential, +fn external_potential_1d( + pore_width: SINumber, + temperature: SINumber, + potential: &ExternalPotential, fluid_parameters: &P, axis: &Axis, potential_cutoff: Option, ) -> EosResult> { let potential_cutoff = potential_cutoff.unwrap_or(MAX_POTENTIAL); let effective_pore_size = match axis.geometry { - Geometry::Spherical => pore_width.to_reduced(U::reference_length())?, - Geometry::Cylindrical => pore_width.to_reduced(U::reference_length())?, - Geometry::Cartesian => 0.5 * pore_width.to_reduced(U::reference_length())?, + Geometry::Spherical => pore_width.to_reduced(SIUnit::reference_length())?, + Geometry::Cylindrical => pore_width.to_reduced(SIUnit::reference_length())?, + Geometry::Cartesian => 0.5 * pore_width.to_reduced(SIUnit::reference_length())?, }; - let t = temperature.to_reduced(U::reference_temperature())?; + let t = temperature.to_reduced(SIUnit::reference_temperature())?; let mut external_potential = match &axis.geometry { Geometry::Cartesian => { potential.calculate_cartesian_potential( diff --git a/feos-dft/src/adsorption/pore3d.rs b/feos-dft/src/adsorption/pore3d.rs index 47d823bb7..7340ed22a 100644 --- a/feos-dft/src/adsorption/pore3d.rs +++ b/feos-dft/src/adsorption/pore3d.rs @@ -7,28 +7,28 @@ use crate::profile::{DFTProfile, CUTOFF_RADIUS, MAX_POTENTIAL}; use feos_core::{EosResult, EosUnit, State}; use ndarray::prelude::*; use ndarray::Zip; -use quantity::{QuantityArray2, QuantityArray4, QuantityScalar}; +use quantity::si::{SIArray2, SIArray4, SINumber, SIUnit}; /// Parameters required to specify a 3D pore. -pub struct Pore3D { - system_size: [QuantityScalar; 3], +pub struct Pore3D { + system_size: [SINumber; 3], n_grid: [usize; 3], - coordinates: QuantityArray2, + coordinates: SIArray2, sigma_ss: Array1, epsilon_k_ss: Array1, potential_cutoff: Option, - cutoff_radius: Option>, + cutoff_radius: Option, } -impl Pore3D { +impl Pore3D { pub fn new( - system_size: [QuantityScalar; 3], + system_size: [SINumber; 3], n_grid: [usize; 3], - coordinates: QuantityArray2, + coordinates: SIArray2, sigma_ss: Array1, epsilon_k_ss: Array1, potential_cutoff: Option, - cutoff_radius: Option>, + cutoff_radius: Option, ) -> Self { Self { system_size, @@ -43,15 +43,15 @@ impl Pore3D { } /// Density profile and properties of a 3D confined system. -pub type PoreProfile3D = PoreProfile; +pub type PoreProfile3D = PoreProfile; -impl PoreSpecification for Pore3D { +impl PoreSpecification for Pore3D { fn initialize( &self, - bulk: &State>, - density: Option<&QuantityArray4>, + bulk: &State>, + density: Option<&SIArray4>, external_potential: Option<&Array4>, - ) -> EosResult> { + ) -> EosResult> { let dft: &F = &bulk.eos; // generate grid @@ -62,12 +62,14 @@ impl PoreSpecification for Pore3D { // move center of geometry of solute to box center let coordinates = Array2::from_shape_fn(self.coordinates.raw_dim(), |(i, j)| { (self.coordinates.get((i, j))) - .to_reduced(U::reference_length()) + .to_reduced(SIUnit::reference_length()) .unwrap() }); // temperature - let t = bulk.temperature.to_reduced(U::reference_temperature())?; + let t = bulk + .temperature + .to_reduced(SIUnit::reference_temperature())?; // calculate external potential let external_potential = external_potential.map_or_else( @@ -104,14 +106,14 @@ impl PoreSpecification for Pore3D { } } -pub fn external_potential_3d( +pub fn external_potential_3d( functional: &F, axis: [&Axis; 3], - system_size: [QuantityScalar; 3], + system_size: [SINumber; 3], coordinates: Array2, sigma_ss: &Array1, epsilon_ss: &Array1, - cutoff_radius: Option>, + cutoff_radius: Option, potential_cutoff: Option, reduced_temperature: f64, ) -> EosResult> { @@ -125,14 +127,14 @@ pub fn external_potential_3d( )); let system_size = [ - system_size[0].to_reduced(U::reference_length())?, - system_size[1].to_reduced(U::reference_length())?, - system_size[2].to_reduced(U::reference_length())?, + system_size[0].to_reduced(SIUnit::reference_length())?, + system_size[1].to_reduced(SIUnit::reference_length())?, + system_size[2].to_reduced(SIUnit::reference_length())?, ]; let cutoff_radius = cutoff_radius - .unwrap_or(CUTOFF_RADIUS * U::reference_length()) - .to_reduced(U::reference_length())?; + .unwrap_or(CUTOFF_RADIUS * SIUnit::reference_length()) + .to_reduced(SIUnit::reference_length())?; // square cut-off radius let cutoff_radius2 = cutoff_radius.powi(2); diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index 89711012a..0a19d8a19 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -11,7 +11,8 @@ use num_dual::*; use petgraph::graph::{Graph, UnGraph}; use petgraph::visit::EdgeRef; use petgraph::Directed; -use quantity::{QuantityArray, QuantityArray1, QuantityScalar}; +// use quantity::{QuantityArray, SIArray1, SINumber}; +use quantity::si::{SIArray, SIArray1, SINumber, SIUnit}; use std::borrow::Cow; use std::fmt; use std::ops::{AddAssign, Deref, MulAssign}; @@ -43,8 +44,8 @@ impl Deref for DFT { } } -impl, U: EosUnit> MolarWeight for DFT { - fn molar_weight(&self) -> QuantityArray1 { +impl MolarWeight for DFT { + fn molar_weight(&self) -> SIArray1 { (self as &T).molar_weight() } } @@ -199,20 +200,19 @@ pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { impl DFT { /// Calculate the grand potential density $\omega$. - pub fn grand_potential_density( + pub fn grand_potential_density( &self, - temperature: QuantityScalar, - density: &QuantityArray, + temperature: SINumber, + density: &SIArray, convolver: &Arc>, - ) -> EosResult> + ) -> EosResult> where - U: EosUnit, D: Dimension, D::Larger: Dimension, { // Calculate residual Helmholtz energy density and functional derivative - let t = temperature.to_reduced(U::reference_temperature())?; - let rho = density.to_reduced(U::reference_density())?; + 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 @@ -230,7 +230,7 @@ impl DFT { f += &(&rho.index_axis(Axis(0), segment.index()) * (0.5 * n as f64)); } - Ok(f * t * U::reference_pressure()) + Ok(f * t * SIUnit::reference_pressure()) } pub(crate) fn ideal_gas_contribution( diff --git a/feos-dft/src/geometry.rs b/feos-dft/src/geometry.rs index f4dae50c3..e28c292ef 100644 --- a/feos-dft/src/geometry.rs +++ b/feos-dft/src/geometry.rs @@ -1,238 +1,235 @@ -use feos_core::{EosResult, EosUnit}; -use ndarray::Array1; -use quantity::{QuantityArray1, QuantityArray2, QuantityScalar}; -use std::f64::consts::{FRAC_PI_3, PI}; - -/// Grids with up to three dimensions. -#[derive(Clone)] -pub enum Grid { - Cartesian1(Axis), - Cartesian2(Axis, Axis), - Periodical2(Axis, Axis), - Cartesian3(Axis, Axis, Axis), - Periodical3(Axis, Axis, Axis), - Spherical(Axis), - Polar(Axis), - Cylindrical { r: Axis, z: Axis }, -} - -impl Grid { - pub fn new_1d(axis: Axis) -> Self { - match axis.geometry { - Geometry::Cartesian => Self::Cartesian1(axis), - Geometry::Cylindrical => Self::Polar(axis), - Geometry::Spherical => Self::Spherical(axis), - } - } - - pub fn axes(&self) -> Vec<&Axis> { - match self { - Self::Cartesian1(x) => vec![x], - Self::Cartesian2(x, y) | Self::Periodical2(x, y) => vec![x, y], - Self::Cartesian3(x, y, z) | Self::Periodical3(x, y, z) => vec![x, y, z], - Self::Spherical(r) | Self::Polar(r) => vec![r], - Self::Cylindrical { r, z } => vec![r, z], - } - } - - pub fn axes_mut(&mut self) -> Vec<&mut Axis> { - match self { - Self::Cartesian1(x) => vec![x], - Self::Cartesian2(x, y) | Self::Periodical2(x, y) => vec![x, y], - Self::Cartesian3(x, y, z) | Self::Periodical3(x, y, z) => vec![x, y, z], - Self::Spherical(r) | Self::Polar(r) => vec![r], - Self::Cylindrical { r, z } => vec![r, z], - } - } - - pub fn grids(&self) -> Vec<&Array1> { - self.axes().iter().map(|ax| &ax.grid).collect() - } - - pub(crate) fn integration_weights(&self) -> Vec<&Array1> { - self.axes() - .iter() - .map(|ax| &ax.integration_weights) - .collect() - } - - pub(crate) fn integration_weights_unit(&self) -> Vec> { - self.axes() - .iter() - .map(|ax| &ax.integration_weights * U::reference_length().powi(ax.geometry.dimension())) - .collect() - } -} - -/// Geometries of individual axes. -#[derive(Copy, Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass)] -pub enum Geometry { - Cartesian, - Cylindrical, - Spherical, -} - -impl Geometry { - /// Return the number of spatial dimensions for this geometry. - pub fn dimension(&self) -> i32 { - match self { - Self::Cartesian => 1, - Self::Cylindrical => 2, - Self::Spherical => 3, - } - } -} - -/// An individual discretized axis. -#[derive(Clone)] -pub struct Axis { - pub geometry: Geometry, - pub grid: Array1, - pub edges: Array1, - integration_weights: Array1, - potential_offset: f64, -} - -impl Axis { - /// Create a new (equidistant) cartesian axis. - /// - /// The potential_offset is required to make sure that particles - /// can not interact through walls. - pub fn new_cartesian( - points: usize, - length: QuantityScalar, - potential_offset: Option, - ) -> EosResult { - let potential_offset = potential_offset.unwrap_or(0.0); - let l = length.to_reduced(U::reference_length())? + potential_offset; - let cell_size = l / points as f64; - let grid = Array1::linspace(0.5 * cell_size, l - 0.5 * cell_size, points); - let edges = Array1::linspace(0.0, l, points + 1); - let integration_weights = Array1::from_elem(points, cell_size); - Ok(Self { - geometry: Geometry::Cartesian, - grid, - edges, - integration_weights, - potential_offset, - }) - } - - /// Create a new (equidistant) spherical axis. - pub fn new_spherical(points: usize, length: QuantityScalar) -> EosResult { - let l = length.to_reduced(U::reference_length())?; - let cell_size = l / points as f64; - let grid = Array1::linspace(0.5 * cell_size, l - 0.5 * cell_size, points); - let edges = Array1::linspace(0.0, l, points + 1); - let integration_weights = Array1::from_shape_fn(points, |k| { - 4.0 * FRAC_PI_3 * cell_size.powi(3) * (3 * k * k + 3 * k + 1) as f64 - }); - Ok(Self { - geometry: Geometry::Spherical, - grid, - edges, - integration_weights, - potential_offset: 0.0, - }) - } - - /// Create a new logarithmically scaled cylindrical axis. - pub fn new_polar(points: usize, length: QuantityScalar) -> EosResult { - let l = length.to_reduced(U::reference_length())?; - - let mut alpha = 0.002_f64; - for _ in 0..20 { - alpha = -(1.0 - (-alpha).exp()).ln() / (points - 1) as f64; - } - let x0 = 0.5 * ((-alpha * points as f64).exp() + (-alpha * (points - 1) as f64).exp()); - let grid = (0..points) - .into_iter() - .map(|i| l * x0 * (alpha * i as f64).exp()) - .collect(); - let edges = (0..=points) - .into_iter() - .map(|i| { - if i == 0 { - 0.0 - } else { - l * (-alpha * (points - i) as f64).exp() - } - }) - .collect(); - - let k0 = (2.0 * alpha).exp() * (2.0 * alpha.exp() + (2.0 * alpha).exp() - 1.0) - / ((1.0 + alpha.exp()).powi(2) * ((2.0 * alpha).exp() - 1.0)); - let integration_weights = (0..points) - .into_iter() - .map(|i| { - (match i { - 0 => k0 * (2.0 * alpha).exp(), - 1 => ((2.0 * alpha).exp() - k0) * (2.0 * alpha).exp(), - _ => (2.0 * alpha * i as f64).exp() * ((2.0 * alpha).exp() - 1.0), - }) * ((-2.0 * alpha * points as f64).exp() * PI * l * l) - }) - .collect(); - - Ok(Self { - geometry: Geometry::Cylindrical, - grid, - edges, - integration_weights, - potential_offset: 0.0, - }) - } - - /// Returns the total length of the axis. - /// - /// This includes the `potential_offset` and used e.g. - /// to determine the correct frequency vector in FFT. - pub fn length(&self) -> f64 { - self.edges[self.grid.len()] - self.edges[0] - } - - /// Returns the volume of the axis. - /// - /// Depending on the geometry, the result is in m, m² or m³. - /// The `potential_offset` is not included in the volume, as - /// it is mainly used to calculate excess properties. - pub fn volume(&self) -> QuantityScalar { - let length = (self.edges[self.grid.len()] - self.potential_offset - self.edges[0]) - * U::reference_length(); - (match self.geometry { - Geometry::Cartesian => 1.0, - Geometry::Cylindrical => 4.0 * PI, - Geometry::Spherical => 4.0 * FRAC_PI_3, - }) * length.powi(self.geometry.dimension()) - } - - /// Interpolate a function on the given axis. - pub fn interpolate( - &self, - x: f64, - y: &QuantityArray2, - i: usize, - ) -> QuantityScalar { - let n = self.grid.len(); - y.get(( - i, - if x >= self.edges[n] { - n - 1 - } else { - match self.geometry { - Geometry::Cartesian | Geometry::Spherical => (x / self.edges[1]) as usize, - Geometry::Cylindrical => { - if x < self.edges[1] { - 0 - } else { - (n as f64 - - (n - 1) as f64 * (x / self.edges[n]).ln() - / (self.edges[1] / self.edges[n]).ln()) - as usize - } - } - } - }, - )) - } -} +use feos_core::{EosResult, EosUnit}; +use ndarray::Array1; +use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; +use std::f64::consts::{FRAC_PI_3, PI}; + +/// Grids with up to three dimensions. +#[derive(Clone)] +pub enum Grid { + Cartesian1(Axis), + Cartesian2(Axis, Axis), + Periodical2(Axis, Axis), + Cartesian3(Axis, Axis, Axis), + Periodical3(Axis, Axis, Axis), + Spherical(Axis), + Polar(Axis), + Cylindrical { r: Axis, z: Axis }, +} + +impl Grid { + pub fn new_1d(axis: Axis) -> Self { + match axis.geometry { + Geometry::Cartesian => Self::Cartesian1(axis), + Geometry::Cylindrical => Self::Polar(axis), + Geometry::Spherical => Self::Spherical(axis), + } + } + + pub fn axes(&self) -> Vec<&Axis> { + match self { + Self::Cartesian1(x) => vec![x], + Self::Cartesian2(x, y) | Self::Periodical2(x, y) => vec![x, y], + Self::Cartesian3(x, y, z) | Self::Periodical3(x, y, z) => vec![x, y, z], + Self::Spherical(r) | Self::Polar(r) => vec![r], + Self::Cylindrical { r, z } => vec![r, z], + } + } + + pub fn axes_mut(&mut self) -> Vec<&mut Axis> { + match self { + Self::Cartesian1(x) => vec![x], + Self::Cartesian2(x, y) | Self::Periodical2(x, y) => vec![x, y], + Self::Cartesian3(x, y, z) | Self::Periodical3(x, y, z) => vec![x, y, z], + Self::Spherical(r) | Self::Polar(r) => vec![r], + Self::Cylindrical { r, z } => vec![r, z], + } + } + + pub fn grids(&self) -> Vec<&Array1> { + self.axes().iter().map(|ax| &ax.grid).collect() + } + + pub(crate) fn integration_weights(&self) -> Vec<&Array1> { + self.axes() + .iter() + .map(|ax| &ax.integration_weights) + .collect() + } + + pub(crate) fn integration_weights_unit(&self) -> Vec { + self.axes() + .iter() + .map(|ax| { + &ax.integration_weights * SIUnit::reference_length().powi(ax.geometry.dimension()) + }) + .collect() + } +} + +/// Geometries of individual axes. +#[derive(Copy, Clone)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum Geometry { + Cartesian, + Cylindrical, + Spherical, +} + +impl Geometry { + /// Return the number of spatial dimensions for this geometry. + pub fn dimension(&self) -> i32 { + match self { + Self::Cartesian => 1, + Self::Cylindrical => 2, + Self::Spherical => 3, + } + } +} + +/// An individual discretized axis. +#[derive(Clone)] +pub struct Axis { + pub geometry: Geometry, + pub grid: Array1, + pub edges: Array1, + integration_weights: Array1, + potential_offset: f64, +} + +impl Axis { + /// Create a new (equidistant) cartesian axis. + /// + /// The potential_offset is required to make sure that particles + /// can not interact through walls. + pub fn new_cartesian( + points: usize, + length: SINumber, + potential_offset: Option, + ) -> EosResult { + let potential_offset = potential_offset.unwrap_or(0.0); + let l = length.to_reduced(SIUnit::reference_length())? + potential_offset; + let cell_size = l / points as f64; + let grid = Array1::linspace(0.5 * cell_size, l - 0.5 * cell_size, points); + let edges = Array1::linspace(0.0, l, points + 1); + let integration_weights = Array1::from_elem(points, cell_size); + Ok(Self { + geometry: Geometry::Cartesian, + grid, + edges, + integration_weights, + potential_offset, + }) + } + + /// Create a new (equidistant) spherical axis. + pub fn new_spherical(points: usize, length: SINumber) -> EosResult { + let l = length.to_reduced(SIUnit::reference_length())?; + let cell_size = l / points as f64; + let grid = Array1::linspace(0.5 * cell_size, l - 0.5 * cell_size, points); + let edges = Array1::linspace(0.0, l, points + 1); + let integration_weights = Array1::from_shape_fn(points, |k| { + 4.0 * FRAC_PI_3 * cell_size.powi(3) * (3 * k * k + 3 * k + 1) as f64 + }); + Ok(Self { + geometry: Geometry::Spherical, + grid, + edges, + integration_weights, + potential_offset: 0.0, + }) + } + + /// Create a new logarithmically scaled cylindrical axis. + pub fn new_polar(points: usize, length: SINumber) -> EosResult { + let l = length.to_reduced(SIUnit::reference_length())?; + + let mut alpha = 0.002_f64; + for _ in 0..20 { + alpha = -(1.0 - (-alpha).exp()).ln() / (points - 1) as f64; + } + let x0 = 0.5 * ((-alpha * points as f64).exp() + (-alpha * (points - 1) as f64).exp()); + let grid = (0..points) + .into_iter() + .map(|i| l * x0 * (alpha * i as f64).exp()) + .collect(); + let edges = (0..=points) + .into_iter() + .map(|i| { + if i == 0 { + 0.0 + } else { + l * (-alpha * (points - i) as f64).exp() + } + }) + .collect(); + + let k0 = (2.0 * alpha).exp() * (2.0 * alpha.exp() + (2.0 * alpha).exp() - 1.0) + / ((1.0 + alpha.exp()).powi(2) * ((2.0 * alpha).exp() - 1.0)); + let integration_weights = (0..points) + .into_iter() + .map(|i| { + (match i { + 0 => k0 * (2.0 * alpha).exp(), + 1 => ((2.0 * alpha).exp() - k0) * (2.0 * alpha).exp(), + _ => (2.0 * alpha * i as f64).exp() * ((2.0 * alpha).exp() - 1.0), + }) * ((-2.0 * alpha * points as f64).exp() * PI * l * l) + }) + .collect(); + + Ok(Self { + geometry: Geometry::Cylindrical, + grid, + edges, + integration_weights, + potential_offset: 0.0, + }) + } + + /// Returns the total length of the axis. + /// + /// This includes the `potential_offset` and used e.g. + /// to determine the correct frequency vector in FFT. + pub fn length(&self) -> f64 { + self.edges[self.grid.len()] - self.edges[0] + } + + /// Returns the volume of the axis. + /// + /// Depending on the geometry, the result is in m, m² or m³. + /// The `potential_offset` is not included in the volume, as + /// it is mainly used to calculate excess properties. + pub fn volume(&self) -> SINumber { + let length = (self.edges[self.grid.len()] - self.potential_offset - self.edges[0]) + * SIUnit::reference_length(); + (match self.geometry { + Geometry::Cartesian => 1.0, + Geometry::Cylindrical => 4.0 * PI, + Geometry::Spherical => 4.0 * FRAC_PI_3, + }) * length.powi(self.geometry.dimension()) + } + + /// Interpolate a function on the given axis. + pub fn interpolate(&self, x: f64, y: &SIArray2, i: usize) -> SINumber { + let n = self.grid.len(); + y.get(( + i, + if x >= self.edges[n] { + n - 1 + } else { + match self.geometry { + Geometry::Cartesian | Geometry::Spherical => (x / self.edges[1]) as usize, + Geometry::Cylindrical => { + if x < self.edges[1] { + 0 + } else { + (n as f64 + - (n - 1) as f64 * (x / self.edges[n]).ln() + / (self.edges[1] / self.edges[n]).ln()) + as usize + } + } + } + }, + )) + } +} diff --git a/feos-dft/src/ideal_chain_contribution.rs b/feos-dft/src/ideal_chain_contribution.rs index 7fa9a00ff..9eece53aa 100644 --- a/feos-dft/src/ideal_chain_contribution.rs +++ b/feos-dft/src/ideal_chain_contribution.rs @@ -1,77 +1,77 @@ -use feos_core::{EosResult, EosUnit, HelmholtzEnergyDual, StateHD}; -use ndarray::*; -use num_dual::DualNum; -use quantity::{QuantityArray, QuantityScalar}; -use std::fmt; - -#[derive(Clone)] -pub struct IdealChainContribution { - component_index: Array1, - m: Array1, -} - -impl IdealChainContribution { - pub fn new(component_index: &Array1, m: &Array1) -> Self { - Self { - component_index: component_index.clone(), - m: m.clone(), - } - } -} - -impl> HelmholtzEnergyDual for IdealChainContribution { - fn helmholtz_energy(&self, state: &StateHD) -> D { - let segments = self.component_index.len(); - if self.component_index[segments - 1] + 1 != segments { - return D::zero(); - } - - // calculate segment density - let density = self.component_index.mapv(|c| state.partial_density[c]); - - // calculate Helmholtz energy - (&density - * &(&self.m - 1.0) - * density.mapv(|r| (r.abs() + D::from(f64::EPSILON)).ln() - 1.0)) - .sum() - * state.volume - } -} - -impl fmt::Display for IdealChainContribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal chain") - } -} - -impl IdealChainContribution { - pub fn calculate_helmholtz_energy_density( - &self, - density: &Array, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - N: DualNum, - { - 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() - 1.0) * (self.m[i] - 1.0) * rhoi); - } - Ok(phi) - } - - pub fn helmholtz_energy_density( - &self, - temperature: QuantityScalar, - density: &QuantityArray, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - let rho = density.to_reduced(U::reference_density())?; - let t = temperature.to_reduced(U::reference_temperature())?; - Ok(self.calculate_helmholtz_energy_density(&rho)? * t * U::reference_pressure()) - } -} +use feos_core::{EosResult, EosUnit, HelmholtzEnergyDual, StateHD}; +use ndarray::*; +use num_dual::DualNum; +use quantity::si::{SIArray, SINumber, SIUnit}; +use std::fmt; + +#[derive(Clone)] +pub struct IdealChainContribution { + component_index: Array1, + m: Array1, +} + +impl IdealChainContribution { + pub fn new(component_index: &Array1, m: &Array1) -> Self { + Self { + component_index: component_index.clone(), + m: m.clone(), + } + } +} + +impl> HelmholtzEnergyDual for IdealChainContribution { + fn helmholtz_energy(&self, state: &StateHD) -> D { + let segments = self.component_index.len(); + if self.component_index[segments - 1] + 1 != segments { + return D::zero(); + } + + // calculate segment density + let density = self.component_index.mapv(|c| state.partial_density[c]); + + // calculate Helmholtz energy + (&density + * &(&self.m - 1.0) + * density.mapv(|r| (r.abs() + D::from(f64::EPSILON)).ln() - 1.0)) + .sum() + * state.volume + } +} + +impl fmt::Display for IdealChainContribution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal chain") + } +} + +impl IdealChainContribution { + pub fn calculate_helmholtz_energy_density( + &self, + density: &Array, + ) -> EosResult> + where + D: Dimension, + D::Larger: Dimension, + N: DualNum, + { + 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() - 1.0) * (self.m[i] - 1.0) * rhoi); + } + Ok(phi) + } + + pub fn helmholtz_energy_density( + &self, + temperature: SINumber, + density: &SIArray, + ) -> EosResult> + where + D: Dimension, + D::Larger: Dimension, + { + let rho = density.to_reduced(SIUnit::reference_density())?; + let t = temperature.to_reduced(SIUnit::reference_temperature())?; + Ok(self.calculate_helmholtz_energy_density(&rho)? * t * SIUnit::reference_pressure()) + } +} diff --git a/feos-dft/src/interface/mod.rs b/feos-dft/src/interface/mod.rs index a5c929fb6..476dbc541 100644 --- a/feos-dft/src/interface/mod.rs +++ b/feos-dft/src/interface/mod.rs @@ -1,463 +1,465 @@ -//! Density profiles at planar interfaces and interfacial tensions. -use crate::convolver::ConvolverFFT; -use crate::functional::{HelmholtzEnergyFunctional, DFT}; -use crate::geometry::{Axis, Grid}; -use crate::profile::{DFTProfile, DFTSpecifications}; -use crate::solver::DFTSolver; -use feos_core::{Contributions, EosError, EosResult, EosUnit, PhaseEquilibrium}; -use ndarray::{s, Array, Array1, Array2, Axis as Axis_nd, Ix1}; -use quantity::{QuantityArray1, QuantityArray2, QuantityScalar}; - -mod surface_tension_diagram; -pub use surface_tension_diagram::SurfaceTensionDiagram; - -const RELATIVE_WIDTH: f64 = 6.0; -const MIN_WIDTH: f64 = 100.0; - -/// Density profile and properties of a planar interface. -pub struct PlanarInterface { - pub profile: DFTProfile, - pub vle: PhaseEquilibrium, 2>, - pub surface_tension: Option>, - pub equimolar_radius: Option>, -} - -impl Clone for PlanarInterface { - fn clone(&self) -> Self { - Self { - profile: self.profile.clone(), - vle: self.vle.clone(), - surface_tension: self.surface_tension, - equimolar_radius: self.equimolar_radius, - } - } -} - -impl PlanarInterface { - pub fn solve_inplace(&mut self, solver: Option<&DFTSolver>, debug: bool) -> EosResult<()> { - // Solve the profile - self.profile.solve(solver, debug)?; - - // postprocess - self.surface_tension = Some(self.profile.integrate( - &(self.profile.grand_potential_density()? - + self.vle.vapor().pressure(Contributions::Total)), - )); - let delta_rho = self.vle.liquid().density - self.vle.vapor().density; - self.equimolar_radius = Some( - self.profile - .integrate(&(self.profile.density.sum_axis(Axis_nd(0)) - self.vle.vapor().density)) - / delta_rho, - ); - - Ok(()) - } - - pub fn solve(mut self, solver: Option<&DFTSolver>) -> EosResult { - self.solve_inplace(solver, false)?; - Ok(self) - } -} - -impl PlanarInterface { - pub fn new( - vle: &PhaseEquilibrium, 2>, - n_grid: usize, - l_grid: QuantityScalar, - ) -> EosResult { - let dft = &vle.vapor().eos; - - // generate grid - let grid = Grid::Cartesian1(Axis::new_cartesian(n_grid, l_grid, None)?); - - // initialize convolver - let t = vle - .vapor() - .temperature - .to_reduced(U::reference_temperature())?; - let weight_functions = dft.weight_functions(t); - let convolver = ConvolverFFT::plan(&grid, &weight_functions, None); - - Ok(Self { - profile: DFTProfile::new(grid, convolver, vle.vapor(), None, None)?, - vle: vle.clone(), - surface_tension: None, - equimolar_radius: None, - }) - } - - pub fn from_tanh( - vle: &PhaseEquilibrium, 2>, - n_grid: usize, - l_grid: QuantityScalar, - critical_temperature: QuantityScalar, - fix_equimolar_surface: bool, - ) -> EosResult { - let mut profile = Self::new(vle, n_grid, l_grid)?; - - // calculate segment indices - let indices = &profile.profile.dft.component_index(); - - // calculate density profile - let z0 = 0.5 * l_grid.to_reduced(U::reference_length())?; - let (z0, sign) = (z0.abs(), -z0.signum()); - let reduced_temperature = vle.vapor().temperature.to_reduced(critical_temperature)?; - profile.profile.density = - QuantityArray2::from_shape_fn(profile.profile.density.raw_dim(), |(i, z)| { - let rho_v = profile.vle.vapor().partial_density.get(indices[i]); - let rho_l = profile.vle.liquid().partial_density.get(indices[i]); - 0.5 * (rho_l - rho_v) - * (sign * (profile.profile.grid.grids()[0][z] - z0) / 3.0 - * (2.4728 - 2.3625 * reduced_temperature)) - .tanh() - + 0.5 * (rho_l + rho_v) - }); - - // specify specification - if fix_equimolar_surface { - profile.profile.specification = - DFTSpecifications::total_moles_from_profile(&profile.profile)?; - } - - Ok(profile) - } - - pub fn from_pdgt( - vle: &PhaseEquilibrium, 2>, - n_grid: usize, - fix_equimolar_surface: bool, - ) -> EosResult { - let dft = &vle.vapor().eos; - - if dft.component_index().len() != 1 { - panic!("Initialization from pDGT not possible for segment DFT or mixtures"); - } - - // calculate density profile from pDGT - let n_grid_pdgt = 20; - let mut z_pdgt = Array1::zeros(n_grid_pdgt) * U::reference_length(); - let mut w_pdgt = U::reference_length(); - let (rho_pdgt, gamma_pdgt) = - dft.solve_pdgt(vle, 20, 0, Some((&mut z_pdgt, &mut w_pdgt)))?; - if !gamma_pdgt - .to_reduced(U::reference_surface_tension())? - .is_normal() - { - return Err(EosError::InvalidState( - String::from("DFTProfile::from_pdgt"), - String::from("gamma_pdgt"), - gamma_pdgt.to_reduced(U::reference_surface_tension())?, - )); - } - - // create PlanarInterface - let l_grid = (MIN_WIDTH * U::reference_length()) - .max(w_pdgt * RELATIVE_WIDTH) - .unwrap(); - let mut profile = Self::new(vle, n_grid, l_grid)?; - - // interpolate density profile from pDGT to DFT - let r = l_grid * 0.5; - profile.profile.density = interp_symmetric( - vle, - z_pdgt, - rho_pdgt, - &profile.vle, - profile.profile.grid.grids()[0], - r, - )?; - - // specify specification - if fix_equimolar_surface { - profile.profile.specification = - DFTSpecifications::total_moles_from_profile(&profile.profile)?; - } - - Ok(profile) - } -} - -impl PlanarInterface { - pub fn shift_equimolar_inplace(&mut self) { - let s = self.profile.density.shape(); - let m = &self.profile.dft.m(); - let mut rho_l = 0.0 * U::reference_density(); - let mut rho_v = 0.0 * U::reference_density(); - let mut rho = Array::zeros(s[1]) * U::reference_density(); - for i in 0..s[0] { - rho_l += self.profile.density.get((i, 0)) * m[i]; - rho_v += self.profile.density.get((i, s[1] - 1)) * m[i]; - rho += &(&self.profile.density.index_axis(Axis_nd(0), i) * m[i]); - } - - let x = (rho - rho_v) / (rho_l - rho_v); - let ze = self.profile.grid.axes()[0].edges[0] - + self - .profile - .integrate(&x) - .to_reduced(U::reference_length()) - .unwrap(); - self.profile.grid.axes_mut()[0].grid -= ze; - } - - pub fn shift_equimolar(mut self) -> Self { - self.shift_equimolar_inplace(); - self - } - - /// Relative adsorption of component `i' with respect to `j': \Gamma_i^(j) - pub fn relative_adsorption(&self) -> EosResult> { - let s = self.profile.density.shape(); - let mut rho_l = Array1::zeros(s[0]) * U::reference_density(); - let mut rho_v = Array1::zeros(s[0]) * U::reference_density(); - - // Calculate the partial densities in the liquid and in the vapor phase - for i in 0..s[0] { - rho_l.try_set(i, self.profile.density.get((i, 0)))?; - rho_v.try_set(i, self.profile.density.get((i, s[1] - 1)))?; - } - - // Calculate \Gamma_i^(j) - let gamma = QuantityArray2::from_shape_fn((s[0], s[0]), |(i, j)| { - if i == j { - 0.0 * U::reference_density() * U::reference_length() - } else { - -(rho_l.get(i) - rho_v.get(i)) - * ((&self.profile.density.index_axis(Axis_nd(0), j) - rho_l.get(j)) - / (rho_l.get(j) - rho_v.get(j)) - - (&self.profile.density.index_axis(Axis_nd(0), i) - rho_l.get(i)) - / (rho_l.get(i) - rho_v.get(i))) - .integrate(&self.profile.grid.integration_weights_unit()) - } - }); - - Ok(gamma) - } - - /// Interfacial enrichment of component `i': E_i - pub fn interfacial_enrichment(&self) -> EosResult> { - let s = self.profile.density.shape(); - let density = self.profile.density.to_reduced(U::reference_density())?; - let rho_l = density.index_axis(Axis_nd(1), 0); - let rho_v = density.index_axis(Axis_nd(1), s[1] - 1); - - let enrichment = Array1::from_shape_fn(s[0], |i| { - *(density - .index_axis(Axis_nd(0), i) - .iter() - .max_by(|&a, &b| a.total_cmp(b)) - .unwrap()) // panics only of iterator is empty - / rho_l[i].max(rho_v[i]) - }); - - Ok(enrichment) - } - - /// Interface thickness (90-10 number density difference) - pub fn interfacial_thickness(&self) -> EosResult> { - let s = self.profile.density.shape(); - let rho = self - .profile - .density - .sum_axis(Axis_nd(0)) - .to_reduced(U::reference_density())?; - let z = self.profile.grid.grids()[0]; - let dz = z[1] - z[0]; - - let limits = (0.9_f64, 0.1_f64); - let (limit_upper, limit_lower) = if limits.0 > limits.1 { - (limits.0, limits.1) - } else { - (limits.1, limits.0) - }; - - if limit_upper >= 1.0 || limit_upper.is_sign_negative() { - return Err(EosError::IterationFailed(String::from( - "Upper limit 'l' of interface thickness needs to satisfy 0 < l < 1.", - ))); - } - if limit_lower >= 1.0 || limit_lower.is_sign_negative() { - return Err(EosError::IterationFailed(String::from( - "Lower limit 'l' of interface thickness needs to satisfy 0 < l < 1.", - ))); - } - - // Get the densities in the liquid and in the vapor phase - let rho_v = rho[0].min(rho[s[1] - 1]); - let rho_l = rho[0].max(rho[s[1] - 1]); - - if (rho_l - rho_v).abs() < 1.0e-10 { - return Ok(0.0 * U::reference_length()); - } - - // Density boundaries for interface definition - let rho_upper = rho_v + limit_upper * (rho_l - rho_v); - let rho_lower = rho_v + limit_lower * (rho_l - rho_v); - - // Get indizes right of intersection between density profile and - // constant density boundaries - let index_upper_plus = if rho[0] >= rho[s[1] - 1] { - rho.iter() - .enumerate() - .find(|(_, &x)| (x - rho_upper).is_sign_negative()) - .expect("Could not find rho_upper value!") - .0 - } else { - rho.iter() - .enumerate() - .find(|(_, &x)| (rho_upper - x).is_sign_negative()) - .expect("Could not find rho_upper value!") - .0 - }; - let index_lower_plus = if rho[0] >= rho[s[1] - 1] { - rho.iter() - .enumerate() - .find(|(_, &x)| (x - rho_lower).is_sign_negative()) - .expect("Could not find rho_lower value!") - .0 - } else { - rho.iter() - .enumerate() - .find(|(_, &x)| (rho_lower - x).is_sign_negative()) - .expect("Could not find rho_lower value!") - .0 - }; - - // Calculate distance between two density points using a linear - // interpolated density profiles between the two grid points where the - // density profile crosses the limiting densities - let z_upper = z[index_upper_plus - 1] - + (rho_upper - rho[index_upper_plus - 1]) - / (rho[index_upper_plus] - rho[index_upper_plus - 1]) - * dz; - let z_lower = z[index_lower_plus - 1] - + (rho_lower - rho[index_lower_plus - 1]) - / (rho[index_lower_plus] - rho[index_lower_plus - 1]) - * dz; - - // Return - Ok((z_lower - z_upper) * U::reference_length()) - } - - fn set_density_scale(&mut self, init: &QuantityArray2) { - assert_eq!(self.profile.density.shape(), init.shape()); - let n_grid = self.profile.density.shape()[1]; - let drho_init = &init.index_axis(Axis_nd(1), 0) - &init.index_axis(Axis_nd(1), n_grid - 1); - let rho_init_0 = init.index_axis(Axis_nd(1), n_grid - 1); - let drho = &self.profile.density.index_axis(Axis_nd(1), 0) - - &self.profile.density.index_axis(Axis_nd(1), n_grid - 1); - let rho_0 = self.profile.density.index_axis(Axis_nd(1), n_grid - 1); - - self.profile.density = - QuantityArray2::from_shape_fn(self.profile.density.raw_dim(), |(i, j)| { - (init.get((i, j)) - rho_init_0.get(i)) - .to_reduced(drho_init.get(i)) - .unwrap() - * drho.get(i) - + rho_0.get(i) - }); - } - - pub fn set_density_inplace(&mut self, init: &QuantityArray2, scale: bool) { - if scale { - self.set_density_scale(init) - } else { - assert_eq!(self.profile.density.shape(), init.shape()); - self.profile.density = init.clone(); - } - } - - pub fn set_density(mut self, init: &QuantityArray2, scale: bool) -> Self { - self.set_density_inplace(init, scale); - self - } -} - -fn interp_symmetric( - vle_pdgt: &PhaseEquilibrium, 2>, - z_pdgt: QuantityArray1, - rho_pdgt: QuantityArray2, - vle: &PhaseEquilibrium, 2>, - z: &Array1, - radius: QuantityScalar, -) -> EosResult> { - let reduced_density = Array2::from_shape_fn(rho_pdgt.raw_dim(), |(i, j)| { - (rho_pdgt.get((i, j)) - vle_pdgt.vapor().partial_density.get(i)) - .to_reduced( - vle_pdgt.liquid().partial_density.get(i) - vle_pdgt.vapor().partial_density.get(i), - ) - .unwrap() - - 0.5 - }); - let segments = vle_pdgt.vapor().eos.component_index().len(); - let mut reduced_density = interp( - &z_pdgt.to_reduced(U::reference_length())?, - &reduced_density, - &(z - radius.to_reduced(U::reference_length())?), - &Array1::from_elem(segments, 0.5), - &Array1::from_elem(segments, -0.5), - false, - ) + interp( - &z_pdgt.to_reduced(U::reference_length())?, - &reduced_density, - &(z + radius.to_reduced(U::reference_length())?), - &Array1::from_elem(segments, -0.5), - &Array1::from_elem(segments, 0.5), - true, - ); - if radius < 0.0 * U::reference_length() { - reduced_density += 1.0; - } - Ok(QuantityArray2::from_shape_fn( - reduced_density.raw_dim(), - |(i, j)| { - reduced_density[(i, j)] - * (vle.liquid().partial_density.get(i) - vle.vapor().partial_density.get(i)) - + vle.vapor().partial_density.get(i) - }, - )) -} - -fn interp( - x_old: &Array1, - y_old: &Array2, - x_new: &Array1, - y_left: &Array1, - y_right: &Array1, - reverse: bool, -) -> Array2 { - let n = x_old.len(); - - let (x_rev, y_rev) = if reverse { - (-&x_old.slice(s![..;-1]), y_old.slice(s![.., ..;-1])) - } else { - (x_old.to_owned(), y_old.view()) - }; - - let mut y_new = Array2::zeros((y_rev.shape()[0], x_new.len())); - let mut k = 0; - for i in 0..x_new.len() { - while k < n && x_new[i] > x_rev[k] { - k += 1; - } - y_new.slice_mut(s![.., i]).assign(&if k == 0 { - y_left - + &((&y_rev.slice(s![.., 0]) - y_left) - * ((&y_rev.slice(s![.., 1]) - y_left) / (&y_rev.slice(s![.., 0]) - y_left)) - .mapv(|x| x.powf((x_new[i] - x_rev[0]) / (x_rev[1] - x_rev[0])))) - } else if k == n { - y_right - + &((&y_rev.slice(s![.., n - 2]) - y_right) - * ((&y_rev.slice(s![.., n - 1]) - y_right) - / (&y_rev.slice(s![.., n - 2]) - y_right)) - .mapv(|x| { - x.powf((x_new[i] - x_rev[n - 2]) / (x_rev[n - 1] - x_rev[n - 2])) - })) - } else { - &y_rev.slice(s![.., k - 1]) - + &((x_new[i] - x_rev[k - 1]) / (x_rev[k] - x_rev[k - 1]) - * (&y_rev.slice(s![.., k]) - &y_rev.slice(s![.., k - 1]))) - }); - } - y_new -} +//! Density profiles at planar interfaces and interfacial tensions. +use crate::convolver::ConvolverFFT; +use crate::functional::{HelmholtzEnergyFunctional, DFT}; +use crate::geometry::{Axis, Grid}; +use crate::profile::{DFTProfile, DFTSpecifications}; +use crate::solver::DFTSolver; +use feos_core::{Contributions, EosError, EosResult, EosUnit, PhaseEquilibrium}; +use ndarray::{s, Array, Array1, Array2, Axis as Axis_nd, Ix1}; +use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; + +mod surface_tension_diagram; +pub use surface_tension_diagram::SurfaceTensionDiagram; + +const RELATIVE_WIDTH: f64 = 6.0; +const MIN_WIDTH: f64 = 100.0; + +/// Density profile and properties of a planar interface. +pub struct PlanarInterface { + pub profile: DFTProfile, + pub vle: PhaseEquilibrium, 2>, + pub surface_tension: Option, + pub equimolar_radius: Option, +} + +impl Clone for PlanarInterface { + fn clone(&self) -> Self { + Self { + profile: self.profile.clone(), + vle: self.vle.clone(), + surface_tension: self.surface_tension, + equimolar_radius: self.equimolar_radius, + } + } +} + +impl PlanarInterface { + pub fn solve_inplace(&mut self, solver: Option<&DFTSolver>, debug: bool) -> EosResult<()> { + // Solve the profile + self.profile.solve(solver, debug)?; + + // postprocess + self.surface_tension = Some(self.profile.integrate( + &(self.profile.grand_potential_density()? + + self.vle.vapor().pressure(Contributions::Total)), + )); + let delta_rho = self.vle.liquid().density - self.vle.vapor().density; + self.equimolar_radius = Some( + self.profile + .integrate(&(self.profile.density.sum_axis(Axis_nd(0)) - self.vle.vapor().density)) + / delta_rho, + ); + + Ok(()) + } + + pub fn solve(mut self, solver: Option<&DFTSolver>) -> EosResult { + self.solve_inplace(solver, false)?; + Ok(self) + } +} + +impl PlanarInterface { + pub fn new( + vle: &PhaseEquilibrium, 2>, + n_grid: usize, + l_grid: SINumber, + ) -> EosResult { + let dft = &vle.vapor().eos; + + // generate grid + let grid = Grid::Cartesian1(Axis::new_cartesian(n_grid, l_grid, None)?); + + // initialize convolver + let t = vle + .vapor() + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let weight_functions = dft.weight_functions(t); + let convolver = ConvolverFFT::plan(&grid, &weight_functions, None); + + Ok(Self { + profile: DFTProfile::new(grid, convolver, vle.vapor(), None, None)?, + vle: vle.clone(), + surface_tension: None, + equimolar_radius: None, + }) + } + + pub fn from_tanh( + vle: &PhaseEquilibrium, 2>, + n_grid: usize, + l_grid: SINumber, + critical_temperature: SINumber, + fix_equimolar_surface: bool, + ) -> EosResult { + let mut profile = Self::new(vle, n_grid, l_grid)?; + + // calculate segment indices + let indices = &profile.profile.dft.component_index(); + + // calculate density profile + let z0 = 0.5 * l_grid.to_reduced(SIUnit::reference_length())?; + let (z0, sign) = (z0.abs(), -z0.signum()); + let reduced_temperature = vle.vapor().temperature.to_reduced(critical_temperature)?; + profile.profile.density = + SIArray2::from_shape_fn(profile.profile.density.raw_dim(), |(i, z)| { + let rho_v = profile.vle.vapor().partial_density.get(indices[i]); + let rho_l = profile.vle.liquid().partial_density.get(indices[i]); + 0.5 * (rho_l - rho_v) + * (sign * (profile.profile.grid.grids()[0][z] - z0) / 3.0 + * (2.4728 - 2.3625 * reduced_temperature)) + .tanh() + + 0.5 * (rho_l + rho_v) + }); + + // specify specification + if fix_equimolar_surface { + profile.profile.specification = + DFTSpecifications::total_moles_from_profile(&profile.profile)?; + } + + Ok(profile) + } + + pub fn from_pdgt( + vle: &PhaseEquilibrium, 2>, + n_grid: usize, + fix_equimolar_surface: bool, + ) -> EosResult { + let dft = &vle.vapor().eos; + + if dft.component_index().len() != 1 { + panic!("Initialization from pDGT not possible for segment DFT or mixtures"); + } + + // calculate density profile from pDGT + let n_grid_pdgt = 20; + let mut z_pdgt = Array1::zeros(n_grid_pdgt) * SIUnit::reference_length(); + let mut w_pdgt = SIUnit::reference_length(); + let (rho_pdgt, gamma_pdgt) = + dft.solve_pdgt(vle, 20, 0, Some((&mut z_pdgt, &mut w_pdgt)))?; + if !gamma_pdgt + .to_reduced(SIUnit::reference_surface_tension())? + .is_normal() + { + return Err(EosError::InvalidState( + String::from("DFTProfile::from_pdgt"), + String::from("gamma_pdgt"), + gamma_pdgt.to_reduced(SIUnit::reference_surface_tension())?, + )); + } + + // create PlanarInterface + let l_grid = (MIN_WIDTH * SIUnit::reference_length()) + .max(w_pdgt * RELATIVE_WIDTH) + .unwrap(); + let mut profile = Self::new(vle, n_grid, l_grid)?; + + // interpolate density profile from pDGT to DFT + let r = l_grid * 0.5; + profile.profile.density = interp_symmetric( + vle, + z_pdgt, + rho_pdgt, + &profile.vle, + profile.profile.grid.grids()[0], + r, + )?; + + // specify specification + if fix_equimolar_surface { + profile.profile.specification = + DFTSpecifications::total_moles_from_profile(&profile.profile)?; + } + + Ok(profile) + } +} + +impl PlanarInterface { + pub fn shift_equimolar_inplace(&mut self) { + let s = self.profile.density.shape(); + let m = &self.profile.dft.m(); + let mut rho_l = 0.0 * SIUnit::reference_density(); + let mut rho_v = 0.0 * SIUnit::reference_density(); + let mut rho = Array::zeros(s[1]) * SIUnit::reference_density(); + for i in 0..s[0] { + rho_l += self.profile.density.get((i, 0)) * m[i]; + rho_v += self.profile.density.get((i, s[1] - 1)) * m[i]; + rho += &(&self.profile.density.index_axis(Axis_nd(0), i) * m[i]); + } + + let x = (rho - rho_v) / (rho_l - rho_v); + let ze = self.profile.grid.axes()[0].edges[0] + + self + .profile + .integrate(&x) + .to_reduced(SIUnit::reference_length()) + .unwrap(); + self.profile.grid.axes_mut()[0].grid -= ze; + } + + pub fn shift_equimolar(mut self) -> Self { + self.shift_equimolar_inplace(); + self + } + + /// Relative adsorption of component `i' with respect to `j': \Gamma_i^(j) + pub fn relative_adsorption(&self) -> EosResult { + let s = self.profile.density.shape(); + let mut rho_l = Array1::zeros(s[0]) * SIUnit::reference_density(); + let mut rho_v = Array1::zeros(s[0]) * SIUnit::reference_density(); + + // Calculate the partial densities in the liquid and in the vapor phase + for i in 0..s[0] { + rho_l.try_set(i, self.profile.density.get((i, 0)))?; + rho_v.try_set(i, self.profile.density.get((i, s[1] - 1)))?; + } + + // Calculate \Gamma_i^(j) + let gamma = SIArray2::from_shape_fn((s[0], s[0]), |(i, j)| { + if i == j { + 0.0 * SIUnit::reference_density() * SIUnit::reference_length() + } else { + -(rho_l.get(i) - rho_v.get(i)) + * ((&self.profile.density.index_axis(Axis_nd(0), j) - rho_l.get(j)) + / (rho_l.get(j) - rho_v.get(j)) + - (&self.profile.density.index_axis(Axis_nd(0), i) - rho_l.get(i)) + / (rho_l.get(i) - rho_v.get(i))) + .integrate(&self.profile.grid.integration_weights_unit()) + } + }); + + Ok(gamma) + } + + /// Interfacial enrichment of component `i': E_i + pub fn interfacial_enrichment(&self) -> EosResult> { + let s = self.profile.density.shape(); + let density = self + .profile + .density + .to_reduced(SIUnit::reference_density())?; + let rho_l = density.index_axis(Axis_nd(1), 0); + let rho_v = density.index_axis(Axis_nd(1), s[1] - 1); + + let enrichment = Array1::from_shape_fn(s[0], |i| { + *(density + .index_axis(Axis_nd(0), i) + .iter() + .max_by(|&a, &b| a.total_cmp(b)) + .unwrap()) // panics only of iterator is empty + / rho_l[i].max(rho_v[i]) + }); + + Ok(enrichment) + } + + /// Interface thickness (90-10 number density difference) + pub fn interfacial_thickness(&self) -> EosResult { + let s = self.profile.density.shape(); + let rho = self + .profile + .density + .sum_axis(Axis_nd(0)) + .to_reduced(SIUnit::reference_density())?; + let z = self.profile.grid.grids()[0]; + let dz = z[1] - z[0]; + + let limits = (0.9_f64, 0.1_f64); + let (limit_upper, limit_lower) = if limits.0 > limits.1 { + (limits.0, limits.1) + } else { + (limits.1, limits.0) + }; + + if limit_upper >= 1.0 || limit_upper.is_sign_negative() { + return Err(EosError::IterationFailed(String::from( + "Upper limit 'l' of interface thickness needs to satisfy 0 < l < 1.", + ))); + } + if limit_lower >= 1.0 || limit_lower.is_sign_negative() { + return Err(EosError::IterationFailed(String::from( + "Lower limit 'l' of interface thickness needs to satisfy 0 < l < 1.", + ))); + } + + // Get the densities in the liquid and in the vapor phase + let rho_v = rho[0].min(rho[s[1] - 1]); + let rho_l = rho[0].max(rho[s[1] - 1]); + + if (rho_l - rho_v).abs() < 1.0e-10 { + return Ok(0.0 * SIUnit::reference_length()); + } + + // Density boundaries for interface definition + let rho_upper = rho_v + limit_upper * (rho_l - rho_v); + let rho_lower = rho_v + limit_lower * (rho_l - rho_v); + + // Get indizes right of intersection between density profile and + // constant density boundaries + let index_upper_plus = if rho[0] >= rho[s[1] - 1] { + rho.iter() + .enumerate() + .find(|(_, &x)| (x - rho_upper).is_sign_negative()) + .expect("Could not find rho_upper value!") + .0 + } else { + rho.iter() + .enumerate() + .find(|(_, &x)| (rho_upper - x).is_sign_negative()) + .expect("Could not find rho_upper value!") + .0 + }; + let index_lower_plus = if rho[0] >= rho[s[1] - 1] { + rho.iter() + .enumerate() + .find(|(_, &x)| (x - rho_lower).is_sign_negative()) + .expect("Could not find rho_lower value!") + .0 + } else { + rho.iter() + .enumerate() + .find(|(_, &x)| (rho_lower - x).is_sign_negative()) + .expect("Could not find rho_lower value!") + .0 + }; + + // Calculate distance between two density points using a linear + // interpolated density profiles between the two grid points where the + // density profile crosses the limiting densities + let z_upper = z[index_upper_plus - 1] + + (rho_upper - rho[index_upper_plus - 1]) + / (rho[index_upper_plus] - rho[index_upper_plus - 1]) + * dz; + let z_lower = z[index_lower_plus - 1] + + (rho_lower - rho[index_lower_plus - 1]) + / (rho[index_lower_plus] - rho[index_lower_plus - 1]) + * dz; + + // Return + Ok((z_lower - z_upper) * SIUnit::reference_length()) + } + + fn set_density_scale(&mut self, init: &SIArray2) { + assert_eq!(self.profile.density.shape(), init.shape()); + let n_grid = self.profile.density.shape()[1]; + let drho_init = &init.index_axis(Axis_nd(1), 0) - &init.index_axis(Axis_nd(1), n_grid - 1); + let rho_init_0 = init.index_axis(Axis_nd(1), n_grid - 1); + let drho = &self.profile.density.index_axis(Axis_nd(1), 0) + - &self.profile.density.index_axis(Axis_nd(1), n_grid - 1); + let rho_0 = self.profile.density.index_axis(Axis_nd(1), n_grid - 1); + + self.profile.density = SIArray2::from_shape_fn(self.profile.density.raw_dim(), |(i, j)| { + (init.get((i, j)) - rho_init_0.get(i)) + .to_reduced(drho_init.get(i)) + .unwrap() + * drho.get(i) + + rho_0.get(i) + }); + } + + pub fn set_density_inplace(&mut self, init: &SIArray2, scale: bool) { + if scale { + self.set_density_scale(init) + } else { + assert_eq!(self.profile.density.shape(), init.shape()); + self.profile.density = init.clone(); + } + } + + pub fn set_density(mut self, init: &SIArray2, scale: bool) -> Self { + self.set_density_inplace(init, scale); + self + } +} + +fn interp_symmetric( + vle_pdgt: &PhaseEquilibrium, 2>, + z_pdgt: SIArray1, + rho_pdgt: SIArray2, + vle: &PhaseEquilibrium, 2>, + z: &Array1, + radius: SINumber, +) -> EosResult { + let reduced_density = Array2::from_shape_fn(rho_pdgt.raw_dim(), |(i, j)| { + (rho_pdgt.get((i, j)) - vle_pdgt.vapor().partial_density.get(i)) + .to_reduced( + vle_pdgt.liquid().partial_density.get(i) - vle_pdgt.vapor().partial_density.get(i), + ) + .unwrap() + - 0.5 + }); + let segments = vle_pdgt.vapor().eos.component_index().len(); + let mut reduced_density = interp( + &z_pdgt.to_reduced(SIUnit::reference_length())?, + &reduced_density, + &(z - radius.to_reduced(SIUnit::reference_length())?), + &Array1::from_elem(segments, 0.5), + &Array1::from_elem(segments, -0.5), + false, + ) + interp( + &z_pdgt.to_reduced(SIUnit::reference_length())?, + &reduced_density, + &(z + radius.to_reduced(SIUnit::reference_length())?), + &Array1::from_elem(segments, -0.5), + &Array1::from_elem(segments, 0.5), + true, + ); + if radius < 0.0 * SIUnit::reference_length() { + reduced_density += 1.0; + } + Ok(SIArray2::from_shape_fn( + reduced_density.raw_dim(), + |(i, j)| { + reduced_density[(i, j)] + * (vle.liquid().partial_density.get(i) - vle.vapor().partial_density.get(i)) + + vle.vapor().partial_density.get(i) + }, + )) +} + +fn interp( + x_old: &Array1, + y_old: &Array2, + x_new: &Array1, + y_left: &Array1, + y_right: &Array1, + reverse: bool, +) -> Array2 { + let n = x_old.len(); + + let (x_rev, y_rev) = if reverse { + (-&x_old.slice(s![..;-1]), y_old.slice(s![.., ..;-1])) + } else { + (x_old.to_owned(), y_old.view()) + }; + + let mut y_new = Array2::zeros((y_rev.shape()[0], x_new.len())); + let mut k = 0; + for i in 0..x_new.len() { + while k < n && x_new[i] > x_rev[k] { + k += 1; + } + y_new.slice_mut(s![.., i]).assign(&if k == 0 { + y_left + + &((&y_rev.slice(s![.., 0]) - y_left) + * ((&y_rev.slice(s![.., 1]) - y_left) / (&y_rev.slice(s![.., 0]) - y_left)) + .mapv(|x| x.powf((x_new[i] - x_rev[0]) / (x_rev[1] - x_rev[0])))) + } else if k == n { + y_right + + &((&y_rev.slice(s![.., n - 2]) - y_right) + * ((&y_rev.slice(s![.., n - 1]) - y_right) + / (&y_rev.slice(s![.., n - 2]) - y_right)) + .mapv(|x| { + x.powf((x_new[i] - x_rev[n - 2]) / (x_rev[n - 1] - x_rev[n - 2])) + })) + } else { + &y_rev.slice(s![.., k - 1]) + + &((x_new[i] - x_rev[k - 1]) / (x_rev[k] - x_rev[k - 1]) + * (&y_rev.slice(s![.., k]) - &y_rev.slice(s![.., k - 1]))) + }); + } + y_new +} diff --git a/feos-dft/src/interface/surface_tension_diagram.rs b/feos-dft/src/interface/surface_tension_diagram.rs index f14d203a1..95c07b3b6 100644 --- a/feos-dft/src/interface/surface_tension_diagram.rs +++ b/feos-dft/src/interface/surface_tension_diagram.rs @@ -3,36 +3,36 @@ use crate::functional::{HelmholtzEnergyFunctional, DFT}; use crate::solver::DFTSolver; use feos_core::{EosUnit, PhaseEquilibrium, StateVec}; use ndarray::Array1; -use quantity::{QuantityArray1, QuantityArray2, QuantityScalar}; +use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; const DEFAULT_GRID_POINTS: usize = 2048; /// Container structure for the efficient calculation of surface tension diagrams. -pub struct SurfaceTensionDiagram { - pub profiles: Vec>, +pub struct SurfaceTensionDiagram { + pub profiles: Vec>, } #[allow(clippy::ptr_arg)] -impl SurfaceTensionDiagram { +impl SurfaceTensionDiagram { pub fn new( - dia: &Vec, 2>>, + dia: &Vec, 2>>, init_densities: Option, n_grid: Option, - l_grid: Option>, - critical_temperature: Option>, + l_grid: Option, + critical_temperature: Option, fix_equimolar_surface: Option, solver: Option<&DFTSolver>, ) -> Self { let n_grid = n_grid.unwrap_or(DEFAULT_GRID_POINTS); - let mut profiles: Vec> = Vec::with_capacity(dia.len()); + let mut profiles: Vec> = Vec::with_capacity(dia.len()); for vle in dia.iter() { // check for a critical point let profile = if PhaseEquilibrium::is_trivial_solution(vle.vapor(), vle.liquid()) { PlanarInterface::from_tanh( vle, 10, - 100.0 * U::reference_length(), - 500.0 * U::reference_temperature(), + 100.0 * SIUnit::reference_length(), + 500.0 * SIUnit::reference_temperature(), fix_equimolar_surface.unwrap_or(false), ) } else { @@ -43,8 +43,8 @@ impl SurfaceTensionDiagram { PlanarInterface::from_tanh( vle, n_grid, - l_grid.unwrap_or(100.0 * U::reference_length()), - critical_temperature.unwrap_or(500.0 * U::reference_temperature()), + l_grid.unwrap_or(100.0 * SIUnit::reference_length()), + critical_temperature.unwrap_or(500.0 * SIUnit::reference_temperature()), fix_equimolar_surface.unwrap_or(false), ) } @@ -67,21 +67,21 @@ impl SurfaceTensionDiagram { Self { profiles } } - pub fn vapor(&self) -> StateVec<'_, U, DFT> { + pub fn vapor(&self) -> StateVec<'_, DFT> { self.profiles.iter().map(|p| p.vle.vapor()).collect() } - pub fn liquid(&self) -> StateVec<'_, U, DFT> { + pub fn liquid(&self) -> StateVec<'_, DFT> { self.profiles.iter().map(|p| p.vle.liquid()).collect() } - pub fn surface_tension(&mut self) -> QuantityArray1 { - QuantityArray1::from_shape_fn(self.profiles.len(), |i| { + pub fn surface_tension(&mut self) -> SIArray1 { + SIArray1::from_shape_fn(self.profiles.len(), |i| { self.profiles[i].surface_tension.unwrap() }) } - pub fn relative_adsorption(&self) -> Vec> { + pub fn relative_adsorption(&self) -> Vec { self.profiles .iter() .map(|planar_interf| planar_interf.relative_adsorption().unwrap()) @@ -95,7 +95,7 @@ impl SurfaceTensionDiagram { .collect() } - pub fn interfacial_thickness(&self) -> QuantityArray1 { + pub fn interfacial_thickness(&self) -> SIArray1 { self.profiles .iter() .map(|planar_interf| planar_interf.interfacial_thickness().unwrap()) diff --git a/feos-dft/src/pdgt.rs b/feos-dft/src/pdgt.rs index 28da1853f..bdea179f2 100644 --- a/feos-dft/src/pdgt.rs +++ b/feos-dft/src/pdgt.rs @@ -4,7 +4,8 @@ use super::weight_functions::WeightFunctionInfo; use feos_core::{Contributions, EosResult, EosUnit, EquationOfState, PhaseEquilibrium}; use ndarray::*; use num_dual::HyperDual64; -use quantity::{QuantityArray1, QuantityArray2, QuantityScalar}; +// use quantity::{SIArray2, SINumber}; +use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; use std::ops::AddAssign; impl WeightFunctionInfo { @@ -112,18 +113,18 @@ impl dyn FunctionalContribution { Ok(()) } - pub fn influence_diagonal( + pub fn influence_diagonal( &self, - temperature: QuantityScalar, - density: &QuantityArray2, - ) -> EosResult<(QuantityArray1, QuantityArray2)> { - let t = temperature.to_reduced(U::reference_temperature())?; + temperature: SINumber, + density: &SIArray2, + ) -> EosResult<(SIArray1, SIArray2)> { + let t = temperature.to_reduced(SIUnit::reference_temperature())?; let n = density.shape()[1]; let mut f = Array::zeros(n); let mut c = Array::zeros(density.raw_dim()); self.pdgt_properties( t, - &density.to_reduced(U::reference_density())?, + &density.to_reduced(SIUnit::reference_density())?, &mut f, None, None, @@ -131,24 +132,24 @@ impl dyn FunctionalContribution { None, )?; Ok(( - f * t * U::reference_pressure(), - c * t * U::reference_influence_parameter(), + f * t * SIUnit::reference_pressure(), + c * t * SIUnit::reference_influence_parameter(), )) } } impl DFT { - pub fn solve_pdgt( + pub fn solve_pdgt( &self, - vle: &PhaseEquilibrium, + vle: &PhaseEquilibrium, n_grid: usize, reference_component: usize, - z: Option<(&mut QuantityArray1, &mut QuantityScalar)>, - ) -> EosResult<(QuantityArray2, QuantityScalar)> { + z: Option<(&mut SIArray1, &mut SINumber)>, + ) -> EosResult<(SIArray2, SINumber)> { // calculate density profile let density = if self.components() == 1 { let delta_rho = (vle.vapor().density - vle.liquid().density) / (n_grid + 1) as f64; - QuantityArray1::linspace( + SIArray1::linspace( vle.liquid().density + delta_rho, vle.vapor().density - delta_rho, n_grid, @@ -159,9 +160,9 @@ impl DFT { }; // calculate Helmholtz energy density and influence parameter - let mut delta_omega = Array::zeros(n_grid) * U::reference_pressure(); + let mut delta_omega = Array::zeros(n_grid) * SIUnit::reference_pressure(); let mut influence_diagonal = - Array::zeros(density.raw_dim()) * U::reference_influence_parameter(); + Array::zeros(density.raw_dim()) * SIUnit::reference_influence_parameter(); for contribution in self.contributions() { let (f, c) = contribution.influence_diagonal(vle.vapor().temperature, &density)?; delta_omega += &f; @@ -169,14 +170,15 @@ impl DFT { } delta_omega += &self .ideal_chain_contribution() - .helmholtz_energy_density::<_, Ix1>(vle.vapor().temperature, &density)?; + .helmholtz_energy_density::(vle.vapor().temperature, &density)?; let t = vle .vapor() .temperature - .to_reduced(U::reference_temperature())?; - let rho = density.to_reduced(U::reference_density())?; - delta_omega += &(self.ideal_gas_contribution::(t, &rho) * U::reference_pressure()); + .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); @@ -230,12 +232,12 @@ impl DFT { Ok((density, gamma_int.integrate(&[weights]))) } - fn pdgt_density_profile_mix( + fn pdgt_density_profile_mix( &self, - _vle: &PhaseEquilibrium, + _vle: &PhaseEquilibrium, _n_grid: usize, _reference_component: usize, - ) -> EosResult> { + ) -> EosResult { unimplemented!() } } diff --git a/feos-dft/src/profile.rs b/feos-dft/src/profile.rs index 871d7ece1..fcf4b3b33 100644 --- a/feos-dft/src/profile.rs +++ b/feos-dft/src/profile.rs @@ -8,7 +8,9 @@ use ndarray::{ Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3, RemoveAxis, }; use num_dual::Dual64; -use quantity::{Quantity, QuantityArray, QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray, SIArray1, SINumber, SIUnit}; +use quantity::Quantity; +// use quantity::{Quantity, QuantityArray, SIArray1, SINumber}; use std::ops::MulAssign; use std::sync::Arc; @@ -21,10 +23,10 @@ pub(crate) const CUTOFF_RADIUS: f64 = 14.0; /// In the most basic case, the chemical potential is specified in a DFT calculation, /// for more general systems, this trait provides the possibility to declare additional /// equations for the calculation of the chemical potential during the iteration. -pub trait DFTSpecification: Send + Sync { +pub trait DFTSpecification: Send + Sync { fn calculate_bulk_density( &self, - profile: &DFTProfile, + profile: &DFTProfile, bulk_density: &Array1, z: &Array1, ) -> EosResult>; @@ -49,13 +51,13 @@ impl DFTSpecifications { /// /// Call this after initializing the density profile to keep the number of /// particles constant in systems, where the number itself is difficult to obtain. - pub fn moles_from_profile( - profile: &DFTProfile, + pub fn moles_from_profile( + profile: &DFTProfile, ) -> EosResult> where ::Larger: Dimension, { - let rho = profile.density.to_reduced(U::reference_density())?; + let rho = profile.density.to_reduced(SIUnit::reference_density())?; Ok(Arc::new(Self::Moles { moles: profile.integrate_reduced_comp(&rho), })) @@ -65,24 +67,22 @@ impl DFTSpecifications { /// /// Call this after initializing the density profile to keep the total number of /// particles constant in systems, e.g. to fix the equimolar dividing surface. - pub fn total_moles_from_profile( - profile: &DFTProfile, + pub fn total_moles_from_profile( + profile: &DFTProfile, ) -> EosResult> where ::Larger: Dimension, { - let rho = profile.density.to_reduced(U::reference_density())?; + let rho = profile.density.to_reduced(SIUnit::reference_density())?; let moles = profile.integrate_reduced_comp(&rho).sum(); Ok(Arc::new(Self::TotalMoles { total_moles: moles })) } } -impl DFTSpecification - for DFTSpecifications -{ +impl DFTSpecification for DFTSpecifications { fn calculate_bulk_density( &self, - _profile: &DFTProfile, + _profile: &DFTProfile, bulk_density: &Array1, z: &Array1, ) -> EosResult> { @@ -97,68 +97,68 @@ impl DFTSpecification { +pub struct DFTProfile { pub grid: Grid, pub convolver: Arc>, pub dft: Arc>, - pub temperature: QuantityScalar, - pub density: QuantityArray, - pub specification: Arc>, + pub temperature: SINumber, + pub density: SIArray, + pub specification: Arc>, pub external_potential: Array, - pub bulk: State>, + pub bulk: State>, pub solver_log: Option, } -impl DFTProfile { - pub fn r(&self) -> QuantityArray1 { - self.grid.grids()[0] * U::reference_length() +impl DFTProfile { + pub fn r(&self) -> SIArray1 { + self.grid.grids()[0] * SIUnit::reference_length() } - pub fn z(&self) -> QuantityArray1 { - self.grid.grids()[0] * U::reference_length() + pub fn z(&self) -> SIArray1 { + self.grid.grids()[0] * SIUnit::reference_length() } } -impl DFTProfile { - pub fn edges(&self) -> (QuantityArray1, QuantityArray1) { +impl DFTProfile { + pub fn edges(&self) -> (SIArray1, SIArray1) { ( - &self.grid.axes()[0].edges * U::reference_length(), - &self.grid.axes()[1].edges * U::reference_length(), + &self.grid.axes()[0].edges * SIUnit::reference_length(), + &self.grid.axes()[1].edges * SIUnit::reference_length(), ) } - pub fn r(&self) -> QuantityArray1 { - self.grid.grids()[0] * U::reference_length() + pub fn r(&self) -> SIArray1 { + self.grid.grids()[0] * SIUnit::reference_length() } - pub fn z(&self) -> QuantityArray1 { - self.grid.grids()[1] * U::reference_length() + pub fn z(&self) -> SIArray1 { + self.grid.grids()[1] * SIUnit::reference_length() } } -impl DFTProfile { - pub fn edges(&self) -> (QuantityArray1, QuantityArray1, QuantityArray1) { +impl DFTProfile { + pub fn edges(&self) -> (SIArray1, SIArray1, SIArray1) { ( - &self.grid.axes()[0].edges * U::reference_length(), - &self.grid.axes()[1].edges * U::reference_length(), - &self.grid.axes()[2].edges * U::reference_length(), + &self.grid.axes()[0].edges * SIUnit::reference_length(), + &self.grid.axes()[1].edges * SIUnit::reference_length(), + &self.grid.axes()[2].edges * SIUnit::reference_length(), ) } - pub fn x(&self) -> QuantityArray1 { - self.grid.grids()[0] * U::reference_length() + pub fn x(&self) -> SIArray1 { + self.grid.grids()[0] * SIUnit::reference_length() } - pub fn y(&self) -> QuantityArray1 { - self.grid.grids()[1] * U::reference_length() + pub fn y(&self) -> SIArray1 { + self.grid.grids()[1] * SIUnit::reference_length() } - pub fn z(&self) -> QuantityArray1 { - self.grid.grids()[2] * U::reference_length() + pub fn z(&self) -> SIArray1 { + self.grid.grids()[2] * SIUnit::reference_length() } } -impl DFTProfile +impl DFTProfile where ::Larger: Dimension, { @@ -171,9 +171,9 @@ where pub fn new( grid: Grid, convolver: Arc>, - bulk: &State>, + bulk: &State>, external_potential: Option>, - density: Option<&QuantityArray>, + density: Option<&SIArray>, ) -> EosResult { let dft = bulk.eos.clone(); @@ -190,18 +190,22 @@ where let density = if let Some(density) = density { density.clone() } else { - let t = bulk.temperature.to_reduced(U::reference_temperature())?; + let t = bulk + .temperature + .to_reduced(SIUnit::reference_temperature())?; let exp_dfdrho = (-&external_potential).mapv(f64::exp); let mut bonds = dft.bond_integrals(t, &exp_dfdrho, &convolver); bonds *= &exp_dfdrho; let mut density = Array::zeros(external_potential.raw_dim()); - let bulk_density = bulk.partial_density.to_reduced(U::reference_density())?; + let bulk_density = bulk + .partial_density + .to_reduced(SIUnit::reference_density())?; for (s, &c) in dft.component_index().iter().enumerate() { density.index_axis_mut(Axis_nd(0), s).assign( &(bonds.index_axis(Axis_nd(0), s).map(|is| is.min(1.0)) * bulk_density[c]), ); } - density * U::reference_density() + density * SIUnit::reference_density() }; Ok(Self { @@ -237,7 +241,7 @@ where /// Return the volume of the profile. /// /// Depending on the geometry, the result is in m, m² or m³. - pub fn volume(&self) -> QuantityScalar { + pub fn volume(&self) -> SINumber { self.grid .axes() .iter() @@ -250,24 +254,27 @@ where /// Integrate a given profile over the iteration domain. pub fn integrate>( &self, - profile: &Quantity, U>, - ) -> QuantityScalar { + profile: &Quantity, SIUnit>, + ) -> SINumber { profile.integrate(&self.grid.integration_weights_unit()) } /// Integrate each component individually. pub fn integrate_comp>( &self, - profile: &Quantity, U>, - ) -> QuantityArray1 { - QuantityArray1::from_shape_fn(profile.shape()[0], |i| { + profile: &Quantity, SIUnit>, + ) -> SIArray1 { + SIArray1::from_shape_fn(profile.shape()[0], |i| { self.integrate(&profile.index_axis(Axis_nd(0), i)) }) } /// Return the number of moles of each component in the system. - pub fn moles(&self) -> QuantityArray1 { - let rho = self.density.to_reduced(U::reference_density()).unwrap(); + pub fn moles(&self) -> SIArray1 { + let rho = self + .density + .to_reduced(SIUnit::reference_density()) + .unwrap(); let mut d = rho.raw_dim(); d[0] = self.dft.components(); let mut density_comps = Array::zeros(d); @@ -276,27 +283,27 @@ where .index_axis_mut(Axis_nd(0), j) .assign(&rho.index_axis(Axis_nd(0), i)); } - self.integrate_comp(&(density_comps * U::reference_density())) + self.integrate_comp(&(density_comps * SIUnit::reference_density())) } /// Return the total number of moles in the system. - pub fn total_moles(&self) -> QuantityScalar { + pub fn total_moles(&self) -> SINumber { self.moles().sum() } /// Return the chemical potential of the system - pub fn chemical_potential(&self) -> QuantityArray1 { + pub fn chemical_potential(&self) -> SIArray1 { self.bulk.chemical_potential(Contributions::Total) } } -impl Clone for DFTProfile { +impl Clone for DFTProfile { fn clone(&self) -> Self { Self { grid: self.grid.clone(), convolver: self.convolver.clone(), dft: self.dft.clone(), - temperature: self.temperature.clone(), + temperature: self.temperature, density: self.density.clone(), specification: self.specification.clone(), external_potential: self.external_potential.clone(), @@ -306,9 +313,8 @@ impl Clone for DFTProfile { } } -impl DFTProfile +impl DFTProfile where - U: EosUnit, D: Dimension, D::Larger: Dimension, ::Larger: Dimension, @@ -317,13 +323,14 @@ where pub fn weighted_densities(&self) -> EosResult>> { Ok(self .convolver - .weighted_densities(&self.density.to_reduced(U::reference_density())?)) + .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(U::reference_temperature())?, - &self.density.to_reduced(U::reference_density())?, + self.temperature + .to_reduced(SIUnit::reference_temperature())?, + &self.density.to_reduced(SIUnit::reference_density())?, &self.convolver, )?; Ok(dfdrho) @@ -332,11 +339,11 @@ where #[allow(clippy::type_complexity)] pub fn residual(&self, log: bool) -> EosResult<(Array, Array1, f64)> { // Read from profile - let density = self.density.to_reduced(U::reference_density())?; + let density = self.density.to_reduced(SIUnit::reference_density())?; let partial_density = self .bulk .partial_density - .to_reduced(U::reference_density())?; + .to_reduced(SIUnit::reference_density())?; let bulk_density = self.dft.component_index().mapv(|i| partial_density[i]); let (res, res_bulk, res_norm, _, _) = @@ -358,7 +365,9 @@ where Array, )> { // calculate reduced temperature - let temperature = self.temperature.to_reduced(U::reference_temperature())?; + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; // calculate intrinsic functional derivative let (_, mut dfdrho) = @@ -436,25 +445,25 @@ where // Read from profile let component_index = self.dft.component_index().into_owned(); - let mut density = self.density.to_reduced(U::reference_density())?; + let mut density = self.density.to_reduced(SIUnit::reference_density())?; let partial_density = self .bulk .partial_density - .to_reduced(U::reference_density())?; + .to_reduced(SIUnit::reference_density())?; let mut bulk_density = component_index.mapv(|i| partial_density[i]); // Call solver(s) self.call_solver(&mut density, &mut bulk_density, &solver, debug)?; // Update profile - self.density = density * U::reference_density(); - let volume = U::reference_volume(); + self.density = density * SIUnit::reference_density(); + let volume = SIUnit::reference_volume(); let mut moles = self.bulk.moles.clone(); bulk_density .into_iter() .enumerate() .try_for_each(|(i, r)| { - moles.try_set(component_index[i], r * U::reference_density() * volume) + moles.try_set(component_index[i], r * SIUnit::reference_density() * volume) })?; self.bulk = State::new_nvt(&self.bulk.eos, self.bulk.temperature, volume, &moles)?; @@ -462,16 +471,17 @@ where } } -impl - DFTProfile +impl DFTProfile where D::Larger: Dimension, D::Smaller: Dimension, ::Larger: Dimension, { - pub fn entropy_density(&self, contributions: Contributions) -> EosResult> { + pub fn entropy_density(&self, contributions: Contributions) -> EosResult> { // initialize convolver - let t = self.temperature.to_reduced(U::reference_temperature())?; + let t = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; let functional_contributions = self.dft.contributions(); let weight_functions: Vec> = functional_contributions .iter() @@ -481,28 +491,30 @@ where Ok(self.dft.entropy_density( t, - &self.density.to_reduced(U::reference_density())?, + &self.density.to_reduced(SIUnit::reference_density())?, &convolver, contributions, - )? * (U::reference_entropy() / U::reference_volume())) + )? * (SIUnit::reference_entropy() / SIUnit::reference_volume())) } - pub fn entropy(&self, contributions: Contributions) -> EosResult> { + pub fn entropy(&self, contributions: Contributions) -> EosResult { Ok(self.integrate(&self.entropy_density(contributions)?)) } - pub fn grand_potential_density(&self) -> EosResult> { + pub fn grand_potential_density(&self) -> EosResult> { self.dft .grand_potential_density(self.temperature, &self.density, &self.convolver) } - pub fn grand_potential(&self) -> EosResult> { + pub fn grand_potential(&self) -> EosResult { Ok(self.integrate(&self.grand_potential_density()?)) } - pub fn internal_energy(&self, contributions: Contributions) -> EosResult> { + pub fn internal_energy(&self, contributions: Contributions) -> EosResult { // initialize convolver - let t = self.temperature.to_reduced(U::reference_temperature())?; + let t = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; let functional_contributions = self.dft.contributions(); let weight_functions: Vec> = functional_contributions .iter() @@ -512,11 +524,11 @@ where let internal_energy_density = self.dft.internal_energy_density( t, - &self.density.to_reduced(U::reference_density())?, + &self.density.to_reduced(SIUnit::reference_density())?, &self.external_potential, &convolver, contributions, - )? * U::reference_pressure(); + )? * SIUnit::reference_pressure(); Ok(self.integrate(&internal_energy_density)) } } diff --git a/feos-dft/src/python/adsorption/external_potential.rs b/feos-dft/src/python/adsorption/external_potential.rs index 4226e7589..0bc9fa760 100644 --- a/feos-dft/src/python/adsorption/external_potential.rs +++ b/feos-dft/src/python/adsorption/external_potential.rs @@ -2,12 +2,11 @@ use crate::adsorption::ExternalPotential; use numpy::PyArray1; use pyo3::prelude::*; use quantity::python::{PySIArray2, PySINumber}; -use quantity::si::*; /// A collection of external potentials. #[pyclass(name = "ExternalPotential")] #[derive(Clone)] -pub struct PyExternalPotential(pub ExternalPotential); +pub struct PyExternalPotential(pub ExternalPotential); #[pymethods] #[allow(non_snake_case)] diff --git a/feos-dft/src/python/adsorption/mod.rs b/feos-dft/src/python/adsorption/mod.rs index bb67130ab..225acca10 100644 --- a/feos-dft/src/python/adsorption/mod.rs +++ b/feos-dft/src/python/adsorption/mod.rs @@ -8,11 +8,11 @@ macro_rules! impl_adsorption { ($func:ty, $py_func:ty) => { /// Container structure for adsorption isotherms in 1D pores. #[pyclass(name = "Adsorption1D")] - pub struct PyAdsorption1D(Adsorption1D); + pub struct PyAdsorption1D(Adsorption1D<$func>); /// Container structure for adsorption isotherms in 3D pores. #[pyclass(name = "Adsorption3D")] - pub struct PyAdsorption3D(Adsorption3D); + pub struct PyAdsorption3D(Adsorption3D<$func>); impl_adsorption_isotherm!($func, $py_func, PyAdsorption1D, PyPore1D, PyPoreProfile1D); impl_adsorption_isotherm!($func, $py_func, PyAdsorption3D, PyPore3D, PyPoreProfile3D); diff --git a/feos-dft/src/python/adsorption/pore.rs b/feos-dft/src/python/adsorption/pore.rs index 72aa616e4..bab576c6e 100644 --- a/feos-dft/src/python/adsorption/pore.rs +++ b/feos-dft/src/python/adsorption/pore.rs @@ -23,10 +23,10 @@ macro_rules! impl_pore { /// #[pyclass(name = "Pore1D")] #[pyo3(text_signature = "(geometry, pore_size, potential, n_grid=None, potential_cutoff=None)")] - pub struct PyPore1D(Pore1D); + pub struct PyPore1D(Pore1D); #[pyclass(name = "PoreProfile1D")] - pub struct PyPoreProfile1D(PoreProfile1D); + pub struct PyPoreProfile1D(PoreProfile1D<$func>); impl_1d_profile!(PyPoreProfile1D, [get_r, get_z]); @@ -149,10 +149,10 @@ macro_rules! impl_pore { /// #[pyclass(name = "Pore3D")] #[pyo3(text_signature = "(system_size, n_grid, coordinates, sigma_ss, epsilon_k_ss, potential_cutoff=None, cutoff_radius=None)")] - pub struct PyPore3D(Pore3D); + pub struct PyPore3D(Pore3D); #[pyclass(name = "PoreProfile3D")] - pub struct PyPoreProfile3D(PoreProfile3D); + pub struct PyPoreProfile3D(PoreProfile3D<$func>); impl_3d_profile!(PyPoreProfile3D, get_x, get_y, get_z); diff --git a/feos-dft/src/python/interface/mod.rs b/feos-dft/src/python/interface/mod.rs index 5581eb986..435e635cf 100644 --- a/feos-dft/src/python/interface/mod.rs +++ b/feos-dft/src/python/interface/mod.rs @@ -5,7 +5,7 @@ macro_rules! impl_planar_interface { ($func:ty) => { /// A one-dimensional density profile of a vapor-liquid or liquid-liquid interface. #[pyclass(name = "PlanarInterface")] - pub struct PyPlanarInterface(PlanarInterface); + pub struct PyPlanarInterface(PlanarInterface<$func>); impl_1d_profile!(PyPlanarInterface, [get_z]); diff --git a/feos-dft/src/python/interface/surface_tension_diagram.rs b/feos-dft/src/python/interface/surface_tension_diagram.rs index 0e126c56e..08b17c7a0 100644 --- a/feos-dft/src/python/interface/surface_tension_diagram.rs +++ b/feos-dft/src/python/interface/surface_tension_diagram.rs @@ -32,7 +32,7 @@ macro_rules! impl_surface_tension_diagram { /// #[pyclass(name = "SurfaceTensionDiagram")] #[pyo3(text_signature = "(dia, init_densities=None, n_grid=None, l_grid=None, critical_temperature=None, fix_equimolar_surface=None, solver=None)")] - pub struct PySurfaceTensionDiagram(SurfaceTensionDiagram); + pub struct PySurfaceTensionDiagram(SurfaceTensionDiagram<$func>); #[pymethods] impl PySurfaceTensionDiagram { diff --git a/feos-dft/src/python/solvation.rs b/feos-dft/src/python/solvation.rs index 6bd6d1959..3885b947a 100644 --- a/feos-dft/src/python/solvation.rs +++ b/feos-dft/src/python/solvation.rs @@ -28,7 +28,7 @@ macro_rules! impl_solvation_profile { /// #[pyclass(name = "SolvationProfile")] #[pyo3(text_signature = "(bulk, n_grid, coordinates, sigma, epsilon_k, system_size=None, cutoff_radius=None, potential_cutoff=None)")] - pub struct PySolvationProfile(SolvationProfile); + pub struct PySolvationProfile(SolvationProfile<$func>); impl_3d_profile!(PySolvationProfile, get_x, get_y, get_z); @@ -94,7 +94,7 @@ macro_rules! impl_pair_correlation { /// #[pyclass(name = "PairCorrelation")] #[pyo3(text_signature = "(bulk, test_particle, n_grid, width)")] - pub struct PyPairCorrelation(PairCorrelation); + pub struct PyPairCorrelation(PairCorrelation<$func>); impl_1d_profile!(PyPairCorrelation, [get_r]); diff --git a/feos-dft/src/solvation/pair_correlation.rs b/feos-dft/src/solvation/pair_correlation.rs index b8a9e8720..46642931e 100644 --- a/feos-dft/src/solvation/pair_correlation.rs +++ b/feos-dft/src/solvation/pair_correlation.rs @@ -6,7 +6,7 @@ use crate::solver::DFTSolver; use crate::{Axis, DFTProfile, Grid}; use feos_core::{Contributions, EosResult, EosUnit, State}; use ndarray::prelude::*; -use quantity::QuantityScalar; +use quantity::si::{SINumber, SIUnit}; /// The underlying pair potential, that the Helmholtz energy functional /// models. @@ -16,30 +16,30 @@ pub trait PairPotential { } /// Density profile and properties of a test particle system. -pub struct PairCorrelation { - pub profile: DFTProfile, +pub struct PairCorrelation { + pub profile: DFTProfile, pub pair_correlation_function: Option>, - pub self_solvation_free_energy: Option>, + pub self_solvation_free_energy: Option, pub structure_factor: Option, } -impl Clone for PairCorrelation { +impl Clone for PairCorrelation { fn clone(&self) -> Self { Self { profile: self.profile.clone(), pair_correlation_function: self.pair_correlation_function.clone(), - self_solvation_free_energy: self.self_solvation_free_energy.clone(), + self_solvation_free_energy: self.self_solvation_free_energy, structure_factor: self.structure_factor, } } } -impl PairCorrelation { +impl PairCorrelation { pub fn new( - bulk: &State>, + bulk: &State>, test_particle: usize, n_grid: usize, - width: QuantityScalar, + width: SINumber, ) -> EosResult { let dft = &bulk.eos; @@ -47,7 +47,9 @@ impl PairCorrelation MAX_POTENTIAL { @@ -93,7 +95,7 @@ impl PairCorrelation { - pub profile: DFTProfile, - pub grand_potential: Option>, - pub solvation_free_energy: Option>, +pub struct SolvationProfile { + pub profile: DFTProfile, + pub grand_potential: Option, + pub solvation_free_energy: Option, } -impl Clone for SolvationProfile { +impl Clone for SolvationProfile { fn clone(&self) -> Self { Self { profile: self.profile.clone(), @@ -26,7 +26,7 @@ impl Clone for SolvationProfile } } -impl SolvationProfile { +impl SolvationProfile { pub fn solve_inplace(&mut self, solver: Option<&DFTSolver>, debug: bool) -> EosResult<()> { // Solve the profile self.profile.solve(solver, debug)?; @@ -38,7 +38,7 @@ impl SolvationProfile { // calculate solvation free energy self.solvation_free_energy = Some( (omega + self.profile.bulk.pressure(Contributions::Total) * self.profile.volume()) - / U::reference_moles(), + / SIUnit::reference_moles(), ); Ok(()) @@ -50,20 +50,20 @@ impl SolvationProfile { } } -impl SolvationProfile { +impl SolvationProfile { pub fn new( - bulk: &State>, + bulk: &State>, n_grid: [usize; 3], - coordinates: QuantityArray2, + coordinates: SIArray2, sigma_ss: Array1, epsilon_ss: Array1, - system_size: Option<[QuantityScalar; 3]>, - cutoff_radius: Option>, + system_size: Option<[SINumber; 3]>, + cutoff_radius: Option, potential_cutoff: Option, ) -> EosResult { let dft: &F = &bulk.eos; - let system_size = system_size.unwrap_or([40.0 * U::reference_length(); 3]); + let system_size = system_size.unwrap_or([40.0 * SIUnit::reference_length(); 3]); // generate grid let x = Axis::new_cartesian(n_grid[0], system_size[0], None)?; @@ -73,14 +73,14 @@ impl SolvationProfil // move center of geometry of solute to box center let mut coordinates = Array2::from_shape_fn(coordinates.raw_dim(), |(i, j)| { (coordinates.get((i, j))) - .to_reduced(U::reference_length()) + .to_reduced(SIUnit::reference_length()) .unwrap() }); let center = [ - system_size[0].to_reduced(U::reference_length())? / 2.0, - system_size[1].to_reduced(U::reference_length())? / 2.0, - system_size[2].to_reduced(U::reference_length())? / 2.0, + system_size[0].to_reduced(SIUnit::reference_length())? / 2.0, + system_size[1].to_reduced(SIUnit::reference_length())? / 2.0, + system_size[2].to_reduced(SIUnit::reference_length())? / 2.0, ]; let shift: Array2 = Array2::from_shape_fn((3, 1), |(i, _)| { @@ -90,7 +90,9 @@ impl SolvationProfil coordinates = coordinates + shift; // temperature - let t = bulk.temperature.to_reduced(U::reference_temperature())?; + let t = bulk + .temperature + .to_reduced(SIUnit::reference_temperature())?; // calculate external potential let external_potential = external_potential_3d( @@ -117,13 +119,13 @@ impl SolvationProfil } } -fn external_potential_3d( +fn external_potential_3d( functional: &F, axis: [&Axis; 3], coordinates: Array2, sigma_ss: Array1, epsilon_ss: Array1, - cutoff_radius: Option>, + cutoff_radius: Option, potential_cutoff: Option, reduced_temperature: f64, ) -> EosResult> { @@ -137,8 +139,8 @@ fn external_potential_3d( )); let cutoff_radius = cutoff_radius - .unwrap_or(CUTOFF_RADIUS * U::reference_length()) - .to_reduced(U::reference_length())?; + .unwrap_or(CUTOFF_RADIUS * SIUnit::reference_length()) + .to_reduced(SIUnit::reference_length())?; // square cut-off radius let cutoff_radius2 = cutoff_radius.powi(2); diff --git a/feos-dft/src/solver.rs b/feos-dft/src/solver.rs index cc899375c..0fbce2cce 100644 --- a/feos-dft/src/solver.rs +++ b/feos-dft/src/solver.rs @@ -6,6 +6,7 @@ use num_dual::linalg::LU; use petgraph::graph::Graph; use petgraph::visit::EdgeRef; use petgraph::Directed; +use quantity::si::SIUnit; use quantity::si::{SIArray1, SECOND}; use std::collections::VecDeque; use std::fmt; @@ -204,7 +205,7 @@ impl DFTSolverLog { } } -impl DFTProfile +impl DFTProfile where D::Larger: Dimension, ::Larger: Dimension, @@ -550,7 +551,9 @@ where &self, density: &Array, ) -> EosResult::Larger>>> { - let temperature = self.temperature.to_reduced(U::reference_temperature())?; + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; let contributions = self.dft.contributions(); let weighted_densities = self.convolver.weighted_densities(density); let mut second_partial_derivatives = Vec::with_capacity(contributions.len()); @@ -611,7 +614,7 @@ where ) -> Array { let temperature = self .temperature - .to_reduced(U::reference_temperature()) + .to_reduced(SIUnit::reference_temperature()) .unwrap(); // calculate weight functions diff --git a/src/estimator/binary_vle.rs b/src/estimator/binary_vle.rs index 1053533df..200925d68 100644 --- a/src/estimator/binary_vle.rs +++ b/src/estimator/binary_vle.rs @@ -4,9 +4,8 @@ use feos_core::{ State, }; use ndarray::{arr1, s, Array1, ArrayView1, Axis}; -use quantity::{Quantity, QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; use std::collections::HashMap; -use std::fmt::LowerExp; use std::sync::Arc; /// Different phases of experimental data points in the `BinaryVlePressure` data set. @@ -19,22 +18,22 @@ pub enum Phase { /// Store experimental binary VLE data for the calculation of chemical potential residuals. #[derive(Clone)] -pub struct BinaryVleChemicalPotential { - temperature: QuantityArray1, - pressure: QuantityArray1, +pub struct BinaryVleChemicalPotential { + temperature: SIArray1, + pressure: SIArray1, liquid_molefracs: Array1, vapor_molefracs: Array1, - target: QuantityArray1, + target: SIArray1, } -impl BinaryVleChemicalPotential { +impl BinaryVleChemicalPotential { pub fn new( - temperature: QuantityArray1, - pressure: QuantityArray1, + temperature: SIArray1, + pressure: SIArray1, liquid_molefracs: Array1, vapor_molefracs: Array1, ) -> Self { - let target = Array1::ones(temperature.len() * 2) * 500.0 * U::reference_molar_energy(); + let target = Array1::ones(temperature.len() * 2) * 500.0 * SIUnit::reference_molar_energy(); Self { temperature, pressure, @@ -45,11 +44,8 @@ impl BinaryVleChemicalPotential { } } -impl DataSet for BinaryVleChemicalPotential -where - Quantity: std::fmt::Display + LowerExp, -{ - fn target(&self) -> &QuantityArray1 { +impl DataSet for BinaryVleChemicalPotential { + fn target(&self) -> &SIArray1 { &self.target } @@ -66,10 +62,7 @@ where ] } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + fn predict(&self, eos: &Arc) -> Result { let mut prediction = Vec::new(); for (((&xi, &yi), t), p) in self .liquid_molefracs @@ -78,32 +71,34 @@ where .zip(self.temperature.into_iter()) .zip(self.pressure.into_iter()) { - let liquid_moles = arr1(&[xi, 1.0 - xi]) * U::reference_moles(); + 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 vapor_moles = arr1(&[yi, 1.0 - yi]) * U::reference_moles(); + 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); - prediction - .push(mu_liquid.get(0) - mu_vapor.get(0) + 500.0 * U::reference_molar_energy()); - prediction - .push(mu_liquid.get(1) - mu_vapor.get(1) + 500.0 * U::reference_molar_energy()); + prediction.push( + mu_liquid.get(0) - mu_vapor.get(0) + 500.0 * SIUnit::reference_molar_energy(), + ); + prediction.push( + mu_liquid.get(1) - mu_vapor.get(1) + 500.0 * SIUnit::reference_molar_energy(), + ); } - Ok(QuantityArray1::from_vec(prediction)) + Ok(SIArray1::from_vec(prediction)) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(4); m.insert("temperature".to_owned(), self.temperature.clone()); m.insert("pressure".to_owned(), self.pressure.clone()); m.insert( "liquid_molefracs".to_owned(), - &self.liquid_molefracs * U::reference_moles() / U::reference_moles(), + &self.liquid_molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(), ); m.insert( "vapor_molefracs".to_owned(), - &self.vapor_molefracs * U::reference_moles() / U::reference_moles(), + &self.vapor_molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(), ); m } @@ -111,17 +106,17 @@ where /// Store experimental binary VLE data for the calculation of pressure residuals. #[derive(Clone)] -pub struct BinaryVlePressure { - temperature: QuantityArray1, - pressure: QuantityArray1, +pub struct BinaryVlePressure { + temperature: SIArray1, + pressure: SIArray1, molefracs: Array1, phase: Phase, } -impl BinaryVlePressure { +impl BinaryVlePressure { pub fn new( - temperature: QuantityArray1, - pressure: QuantityArray1, + temperature: SIArray1, + pressure: SIArray1, molefracs: Array1, phase: Phase, ) -> Self { @@ -134,11 +129,8 @@ impl BinaryVlePressure { } } -impl DataSet for BinaryVlePressure -where - Quantity: std::fmt::Display + LowerExp, -{ - fn target(&self) -> &QuantityArray1 { +impl DataSet for BinaryVlePressure { + fn target(&self) -> &SIArray1 { &self.pressure } @@ -155,10 +147,7 @@ where vec } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + fn predict(&self, eos: &Arc) -> Result { let options = Default::default(); self.molefracs .iter() @@ -188,7 +177,7 @@ where .collect() } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(4); m.insert("temperature".to_owned(), self.temperature.clone()); m.insert("pressure".to_owned(), self.pressure.clone()); @@ -198,7 +187,7 @@ where Phase::Liquid => "liquid_molefracs", }) .to_owned(), - &self.molefracs * U::reference_moles() / U::reference_moles(), + &self.molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(), ); m } @@ -206,27 +195,27 @@ where /// Store experimental binary phase diagrams for the calculation of distance residuals. #[derive(Clone)] -pub struct BinaryPhaseDiagram { - specification: QuantityScalar, - temperature_or_pressure: QuantityArray1, +pub struct BinaryPhaseDiagram { + specification: SINumber, + temperature_or_pressure: SIArray1, liquid_molefracs: Option>, vapor_molefracs: Option>, npoints: Option, - target: QuantityArray1, + target: SIArray1, } -impl BinaryPhaseDiagram { +impl BinaryPhaseDiagram { pub fn new( - specification: QuantityScalar, - temperature_or_pressure: QuantityArray1, + specification: SINumber, + temperature_or_pressure: SIArray1, liquid_molefracs: Option>, vapor_molefracs: Option>, npoints: Option, ) -> Self { let count = liquid_molefracs.as_ref().map_or(0, |x| 2 * x.len()) + vapor_molefracs.as_ref().map_or(0, |x| 2 * x.len()); - let target = - Array1::from_elem(count, 1.0) * U::reference_temperature() / U::reference_temperature(); + let target = Array1::from_elem(count, 1.0) * SIUnit::reference_temperature() + / SIUnit::reference_temperature(); Self { specification, temperature_or_pressure, @@ -238,11 +227,8 @@ impl BinaryPhaseDiagram { } } -impl DataSet for BinaryPhaseDiagram -where - Quantity: std::fmt::Display + LowerExp, -{ - fn target(&self) -> &QuantityArray1 { +impl DataSet for BinaryPhaseDiagram { + fn target(&self) -> &SIArray1 { &self.target } @@ -251,7 +237,10 @@ where } fn input_str(&self) -> Vec<&str> { - let mut vec = if self.specification.has_unit(&U::reference_temperature()) { + let mut vec = if self + .specification + .has_unit(&SIUnit::reference_temperature()) + { vec!["temperature", "pressure"] } else { vec!["pressure", "temperature"] @@ -265,10 +254,7 @@ where vec } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + fn predict(&self, eos: &Arc) -> Result { let mut res = Vec::new(); let dia = PhaseDiagram::binary_vle( @@ -284,7 +270,7 @@ where let x_vec_vap = x_vap.index_axis(Axis(1), 0); let tp_vec = if self .temperature_or_pressure - .has_unit(&U::reference_temperature()) + .has_unit(&SIUnit::reference_temperature()) { dia.vapor().temperature() } else { @@ -303,21 +289,25 @@ where )?); } } - Ok(Array1::from_vec(res) * (U::reference_temperature() / U::reference_temperature())) + Ok(Array1::from_vec(res) + * (SIUnit::reference_temperature() / SIUnit::reference_temperature())) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(4); - if self.specification.has_unit(&U::reference_temperature()) { + if self + .specification + .has_unit(&SIUnit::reference_temperature()) + { m.insert( "temperature".to_owned(), - QuantityArray1::from_vec(vec![self.specification]), + SIArray1::from_vec(vec![self.specification]), ); m.insert("pressure".to_owned(), self.temperature_or_pressure.clone()); } else { m.insert( "pressure".to_owned(), - QuantityArray1::from_vec(vec![self.specification]), + SIArray1::from_vec(vec![self.specification]), ); m.insert( "temperature".to_owned(), @@ -327,27 +317,27 @@ where if let Some(liquid_molefracs) = &self.liquid_molefracs { m.insert( "liquid_molefracs".to_owned(), - liquid_molefracs * U::reference_moles() / U::reference_moles(), + liquid_molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(), ); } if let Some(vapor_molefracs) = &self.vapor_molefracs { m.insert( "vapor_molefracs".to_owned(), - vapor_molefracs * U::reference_moles() / U::reference_moles(), + vapor_molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(), ); } m } } -fn predict_distance( +fn predict_distance( x_vec: ArrayView1, - tp_vec: &QuantityArray1, + tp_vec: &SIArray1, x_exp: &Array1, - tp_exp: &QuantityArray1, + tp_exp: &SIArray1, ) -> Result, EstimatorError> where - QuantityScalar: std::fmt::Display, + SINumber: std::fmt::Display, { let mut res = Vec::new(); for (tp, &x) in tp_exp.into_iter().zip(x_exp.iter()) { diff --git a/src/estimator/dataset.rs b/src/estimator/dataset.rs index 657b5eee5..4e8f78aa2 100644 --- a/src/estimator/dataset.rs +++ b/src/estimator/dataset.rs @@ -3,10 +3,9 @@ //! a `target` which can be values from experimental data or //! other models. use super::{EstimatorError, Loss}; -use feos_core::EosUnit; use feos_core::EquationOfState; use ndarray::Array1; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::SIArray1; use std::collections::HashMap; use std::fmt; use std::sync::Arc; @@ -15,9 +14,9 @@ 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) -> &QuantityArray1; + fn target(&self) -> &SIArray1; /// Return the description of the target quantity. fn target_str(&self) -> &str; @@ -26,15 +25,10 @@ pub trait DataSet: Send + Sync { fn input_str(&self) -> Vec<&str>; /// Evaluation of the equation of state for the target quantity. - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp; + fn predict(&self, eos: &Arc) -> Result; /// Evaluate the cost function. - fn cost(&self, eos: &Arc, loss: Loss) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + fn cost(&self, eos: &Arc, loss: Loss) -> Result, EstimatorError> { let mut cost = self.relative_difference(eos)?; loss.apply(&mut cost); let datapoints = cost.len(); @@ -42,7 +36,7 @@ pub trait DataSet: Send + Sync { } /// Returns the input quantities as HashMap. The keys are the input's descriptions. - fn get_input(&self) -> HashMap>; + fn get_input(&self) -> HashMap; /// Returns the number of experimental data points. fn datapoints(&self) -> usize { @@ -50,20 +44,14 @@ pub trait DataSet: Send + Sync { } /// Returns the relative difference between the equation of state and the experimental values. - fn relative_difference(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + fn relative_difference(&self, eos: &Arc) -> Result, EstimatorError> { let prediction = &self.predict(eos)?; let target = self.target(); Ok(((prediction - target) / target).into_value()?) } /// Returns the mean of the absolute relative difference between the equation of state and the experimental values. - fn mean_absolute_relative_difference(&self, eos: &Arc) -> Result - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + fn mean_absolute_relative_difference(&self, eos: &Arc) -> Result { Ok(self .relative_difference(eos)? .into_iter() @@ -73,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 a86dff3ce..25c9db749 100644 --- a/src/estimator/diffusion.rs +++ b/src/estimator/diffusion.rs @@ -1,24 +1,24 @@ use super::{DataSet, EstimatorError}; use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; use ndarray::{arr1, Array1}; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; use std::sync::Arc; /// Store experimental diffusion data. #[derive(Clone)] -pub struct Diffusion { - pub target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, +pub struct Diffusion { + pub target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, } -impl Diffusion { +impl Diffusion { /// Create a new data set for experimental diffusion data. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, + target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, ) -> Result { Ok(Self { target, @@ -28,18 +28,18 @@ impl Diffusion { } /// Return temperature. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } /// Return pressure. - pub fn pressure(&self) -> QuantityArray1 { + pub fn pressure(&self) -> SIArray1 { self.pressure.clone() } } -impl> DataSet for Diffusion { - fn target(&self) -> &QuantityArray1 { +impl DataSet for Diffusion { + fn target(&self) -> &SIArray1 { &self.target } @@ -51,16 +51,16 @@ impl> DataSet for Diffu vec!["temperature", "pressure"] } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let moles = arr1(&[1.0]) * U::reference_moles(); + fn predict(&self, eos: &Arc) -> Result { + let moles = arr1(&[1.0]) * SIUnit::reference_moles(); let ts = self .temperature - .to_reduced(U::reference_temperature()) + .to_reduced(SIUnit::reference_temperature()) + .unwrap(); + let ps = self + .pressure + .to_reduced(SIUnit::reference_pressure()) .unwrap(); - let ps = self.pressure.to_reduced(U::reference_pressure()).unwrap(); let res = ts .iter() @@ -68,20 +68,20 @@ impl> DataSet for Diffu .map(|(&t, &p)| { State::new_npt( eos, - t * U::reference_temperature(), - p * U::reference_pressure(), + t * SIUnit::reference_temperature(), + p * SIUnit::reference_pressure(), &moles, DensityInitialization::None, )? .diffusion()? - .to_reduced(U::reference_diffusion()) + .to_reduced(SIUnit::reference_diffusion()) .map_err(EstimatorError::from) }) .collect::, EstimatorError>>(); - Ok(Array1::from_vec(res?) * U::reference_diffusion()) + Ok(Array1::from_vec(res?) * SIUnit::reference_diffusion()) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(1); m.insert("temperature".to_owned(), self.temperature()); m.insert("pressure".to_owned(), self.pressure()); diff --git a/src/estimator/estimator.rs b/src/estimator/estimator.rs index 6e7cb4212..300d26314 100644 --- a/src/estimator/estimator.rs +++ b/src/estimator/estimator.rs @@ -1,11 +1,9 @@ //! The [`Estimator`] struct can be used to store multiple [`DataSet`]s for convenient parameter //! optimization. use super::{DataSet, EstimatorError, Loss}; -use feos_core::EosUnit; use feos_core::EquationOfState; use ndarray::{arr1, concatenate, Array1, ArrayView1, Axis}; -use quantity::QuantityArray1; -use quantity::QuantityScalar; +use quantity::si::SIArray1; use std::fmt; use std::fmt::Display; use std::fmt::Write; @@ -13,21 +11,18 @@ 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 { - data: Vec>>, +pub struct Estimator { + data: Vec>>, weights: Vec, losses: Vec, } -impl Estimator -where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, -{ +impl Estimator { /// Create a new `Estimator` given `DataSet`s and weights. /// /// The weights are normalized and used as multiplicator when the /// cost function across all `DataSet`s is evaluated. - pub fn new(data: Vec>>, weights: Vec, losses: Vec) -> Self { + pub fn new(data: Vec>>, weights: Vec, losses: Vec) -> Self { Self { data, weights, @@ -36,7 +31,7 @@ where } /// Add a `DataSet` and its weight. - pub fn add_data(&mut self, data: &Arc>, weight: f64, loss: Loss) { + pub fn add_data(&mut self, data: &Arc>, weight: f64, loss: Loss) { self.data.push(data.clone()); self.weights.push(weight); self.losses.push(loss); @@ -58,7 +53,7 @@ where } /// Returns the properties as computed by the equation of state for each `DataSet`. - pub fn predict(&self, eos: &Arc) -> Result>, EstimatorError> { + pub fn predict(&self, eos: &Arc) -> Result, EstimatorError> { self.data.iter().map(|d| d.predict(eos)).collect() } @@ -82,7 +77,7 @@ where } /// Returns the stored `DataSet`s. - pub fn datasets(&self) -> Vec>> { + pub fn datasets(&self) -> Vec>> { self.data.to_vec() } @@ -104,7 +99,7 @@ where } } -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/impl_dataset.rs b/src/estimator/impl_dataset.rs index f692acb01..2345e7641 100644 --- a/src/estimator/impl_dataset.rs +++ b/src/estimator/impl_dataset.rs @@ -1,14 +1,14 @@ /// Store experimental vapor pressure data and compare to the equation of state. #[derive(Clone)] -pub struct VaporPressure { - pub target: QuantityArray1, - temperature: QuantityArray1, - max_temperature: QuantityScalar, +pub struct VaporPressure { + pub target: SIArray1, + temperature: SIArray1, + max_temperature: SINumber, datapoints: usize, std_parameters: Vec, } -impl VaporPressure { +impl VaporPressure { /// Create a new vapor pressure data set. /// /// Takes the temperature as input and possibly parameters @@ -16,16 +16,16 @@ impl VaporPressure { /// function of temperature. This standard deviation can be used /// as inverse weights in the cost function. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, + target: SIArray1, + temperature: SIArray1, std_parameters: Vec, ) -> Result { let datapoints = target.len(); let max_temperature = *temperature - .to_reduced(U::reference_temperature())? + .to_reduced(SIUnit::reference_temperature())? .max() .map_err(|_| FitError::IncompatibleInput)? - * U::reference_temperature(); + * SIUnit::reference_temperature(); Ok(Self { target, temperature, @@ -36,7 +36,7 @@ impl VaporPressure { } /// Return temperature. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } @@ -49,8 +49,8 @@ impl VaporPressure { } } -impl DataSet for VaporPressure { - fn target(&self) -> QuantityArray1 { +impl DataSet for VaporPressure { + fn target(&self) -> SIArray1 { self.target.clone() } @@ -63,9 +63,8 @@ impl DataSet for VaporPressure { vec!["temperature"] } - fn predict(&self, eos: &Rc) -> Result, FitError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, + fn predict(&self, eos: &Rc) -> Result + { let tc = State::critical_point(eos, None, Some(self.max_temperature), VLEOptions::default())? @@ -99,8 +98,7 @@ impl DataSet for VaporPressure { } fn cost(&self, eos: &Rc) -> Result, FitError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, + { let tc_inv = 1.0 / State::critical_point(eos, None, Some(self.max_temperature), VLEOptions::default())? @@ -119,7 +117,7 @@ impl DataSet for VaporPressure { cost[i] = weights[i] * 5.0 * (self.temperature.get(i) - 1.0 / tc_inv) - .to_reduced(U::reference_temperature())?; + .to_reduced(SIUnit::reference_temperature())?; } else { cost[i] = weights[i] * ((self.target.get(i) - prediction.get(i)) / self.target.get(i)) @@ -129,7 +127,7 @@ impl DataSet for VaporPressure { Ok(cost) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(1); m.insert("temperature".to_owned(), self.temperature()); m @@ -138,19 +136,19 @@ impl DataSet for VaporPressure { /// Store experimental data of liquid densities and compare to the equation of state. #[derive(Clone)] -pub struct LiquidDensity { - pub target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, +pub struct LiquidDensity { + pub target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, datapoints: usize, } -impl LiquidDensity { +impl LiquidDensity { /// A new data set for liquid densities with pressures and temperatures as input. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, + target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, ) -> Result { let datapoints = target.len(); Ok(Self { @@ -162,18 +160,18 @@ impl LiquidDensity { } /// Returns temperature of data points. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } /// Returns pressure of data points. - pub fn pressure(&self) -> QuantityArray1 { + pub fn pressure(&self) -> SIArray1 { self.pressure.clone() } } -impl> DataSet for LiquidDensity { - fn target(&self) -> QuantityArray1 { +impl DataSet for LiquidDensity { + fn target(&self) -> SIArray1 { self.target.clone() } @@ -186,9 +184,9 @@ impl> DataSet for LiquidDe vec!["temperature", "pressure"] } - fn predict(&self, eos: &Rc) -> Result, FitError> { + fn predict(&self, eos: &Rc) -> Result { assert_eq!(1, eos.components()); - let moles = arr1(&[1.0]) * U::reference_moles(); + let moles = arr1(&[1.0]) * SIUnit::reference_moles(); let unit = self.target.get(0); let mut prediction = Array1::zeros(self.datapoints) * unit; for i in 0..self.datapoints { @@ -209,8 +207,7 @@ impl> DataSet for LiquidDe } fn cost(&self, eos: &Rc) -> Result, FitError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, + { let n_inv = 1.0 / self.datapoints as f64; let prediction = &self.predict(eos)?; @@ -222,7 +219,7 @@ impl> DataSet for LiquidDe Ok(cost) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(2); m.insert("temperature".to_owned(), self.temperature()); m.insert("pressure".to_owned(), self.pressure()); @@ -232,25 +229,25 @@ impl> DataSet for LiquidDe /// Store experimental data of liquid densities at VLE and compare to the equation of state. #[derive(Clone)] -pub struct EquilibriumLiquidDensity { - pub target: QuantityArray1, - temperature: QuantityArray1, - max_temperature: QuantityScalar, +pub struct EquilibriumLiquidDensity { + pub target: SIArray1, + temperature: SIArray1, + max_temperature: SINumber, datapoints: usize, } -impl EquilibriumLiquidDensity { +impl EquilibriumLiquidDensity { /// A new data set of liquid densities at VLE given temperatures. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, + target: SIArray1, + temperature: SIArray1, ) -> Result { let datapoints = target.len(); let max_temperature = *temperature - .to_reduced(U::reference_temperature())? + .to_reduced(SIUnit::reference_temperature())? .max() .map_err(|_| FitError::IncompatibleInput)? - * U::reference_temperature(); + * SIUnit::reference_temperature(); Ok(Self { target, temperature, @@ -260,15 +257,15 @@ impl EquilibriumLiquidDensity { } /// Returns the temperature of data points. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } } -impl> DataSet - for EquilibriumLiquidDensity +impl DataSet + for EquilibriumLiquidDensity { - fn target(&self) -> QuantityArray1 { + fn target(&self) -> SIArray1 { self.target.clone() } @@ -281,9 +278,8 @@ impl> DataSet vec!["temperature"] } - fn predict(&self, eos: &Rc) -> Result, FitError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, + fn predict(&self, eos: &Rc) -> Result + { let tc = State::critical_point(eos, None, Some(self.max_temperature), VLEOptions::default())? @@ -292,7 +288,7 @@ impl> DataSet let unit = self.target.get(0); let mut prediction = Array1::zeros(self.datapoints) * unit; for i in 0..self.datapoints { - let t: QuantityScalar = self.temperature.get(i); + let t: SINumber = self.temperature.get(i); if t < tc { let state: PhaseEquilibrium = PhaseEquilibrium::pure_t(eos, t, None, VLEOptions::default())?; @@ -305,8 +301,7 @@ impl> DataSet } fn cost(&self, eos: &Rc) -> Result, FitError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, + { let tc = State::critical_point(eos, None, Some(self.max_temperature), VLEOptions::default())? @@ -318,7 +313,7 @@ impl> DataSet if prediction.get(i).is_nan() { cost[i] = n_inv * 5.0 - * (self.temperature.get(i) - tc).to_reduced(U::reference_temperature())?; + * (self.temperature.get(i) - tc).to_reduced(SIUnit::reference_temperature())?; } else { cost[i] = n_inv * ((self.target.get(i) - prediction.get(i)) / self.target.get(i)) @@ -328,7 +323,7 @@ impl> DataSet Ok(cost) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(2); m.insert("temperature".to_owned(), self.temperature()); m diff --git a/src/estimator/liquid_density.rs b/src/estimator/liquid_density.rs index c82066c1c..71a2e36eb 100644 --- a/src/estimator/liquid_density.rs +++ b/src/estimator/liquid_density.rs @@ -4,27 +4,27 @@ use feos_core::{ State, }; use ndarray::arr1; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; use std::sync::Arc; /// Liquid mass density data as function of pressure and temperature. #[derive(Clone)] -pub struct LiquidDensity { +pub struct LiquidDensity { /// mass density - pub target: QuantityArray1, + pub target: SIArray1, /// temperature - temperature: QuantityArray1, + temperature: SIArray1, /// pressure - pressure: QuantityArray1, + pressure: SIArray1, } -impl LiquidDensity { +impl LiquidDensity { /// A new data set for liquid densities with pressures and temperatures as input. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, + target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, ) -> Result { Ok(Self { target, @@ -34,18 +34,18 @@ impl LiquidDensity { } /// Returns temperature of data points. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } /// Returns pressure of data points. - pub fn pressure(&self) -> QuantityArray1 { + pub fn pressure(&self) -> SIArray1 { self.pressure.clone() } } -impl> DataSet for LiquidDensity { - fn target(&self) -> &QuantityArray1 { +impl DataSet for LiquidDensity { + fn target(&self) -> &SIArray1 { &self.target } @@ -57,8 +57,8 @@ impl> DataSet for LiquidDe vec!["temperature", "pressure"] } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> { - let moles = arr1(&[1.0]) * U::reference_moles(); + fn predict(&self, eos: &Arc) -> Result { + let moles = arr1(&[1.0]) * SIUnit::reference_moles(); Ok(self .temperature .into_iter() @@ -68,13 +68,13 @@ impl> DataSet for LiquidDe if let Ok(s) = state { s.mass_density() } else { - f64::NAN * U::reference_mass() / U::reference_volume() + f64::NAN * SIUnit::reference_mass() / SIUnit::reference_volume() } }) .collect()) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(2); m.insert("temperature".to_owned(), self.temperature()); m.insert("pressure".to_owned(), self.pressure()); @@ -84,17 +84,17 @@ impl> DataSet for LiquidDe /// Store experimental data of liquid densities and compare to the equation of state. #[derive(Clone)] -pub struct EquilibriumLiquidDensity { - pub target: QuantityArray1, - temperature: QuantityArray1, +pub struct EquilibriumLiquidDensity { + pub target: SIArray1, + temperature: SIArray1, solver_options: SolverOptions, } -impl EquilibriumLiquidDensity { +impl EquilibriumLiquidDensity { /// A new data set for liquid densities with pressures and temperatures as input. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, + target: SIArray1, + temperature: SIArray1, vle_options: Option, ) -> Result { Ok(Self { @@ -105,15 +105,13 @@ impl EquilibriumLiquidDensity { } /// Returns temperature of data points. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } } -impl> DataSet - for EquilibriumLiquidDensity -{ - fn target(&self) -> &QuantityArray1 { +impl DataSet for EquilibriumLiquidDensity { + fn target(&self) -> &SIArray1 { &self.target } @@ -125,10 +123,7 @@ impl> DataSet vec!["temperature"] } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + fn predict(&self, eos: &Arc) -> Result { Ok(self .temperature .into_iter() @@ -136,13 +131,13 @@ impl> DataSet if let Ok(state) = PhaseEquilibrium::pure(eos, t, None, self.solver_options) { state.liquid().mass_density() } else { - f64::NAN * U::reference_mass() / U::reference_volume() + f64::NAN * SIUnit::reference_mass() / SIUnit::reference_volume() } }) .collect()) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(2); m.insert("temperature".to_owned(), self.temperature()); m diff --git a/src/estimator/python.rs b/src/estimator/python.rs index 646dfd428..c7b77a72e 100644 --- a/src/estimator/python.rs +++ b/src/estimator/python.rs @@ -117,7 +117,7 @@ macro_rules! impl_estimator { /// cost functions and make predictions using an equation of state. #[pyclass(name = "DataSet")] #[derive(Clone)] - pub struct PyDataSet(Arc>); + pub struct PyDataSet(Arc>); #[pymethods] impl PyDataSet { @@ -135,7 +135,7 @@ macro_rules! impl_estimator { /// ------- /// numpy.ndarray[Float] /// The cost function evaluated for each experimental data point. - /// + /// /// Note /// ---- /// The cost function that is used depends on the @@ -257,7 +257,7 @@ macro_rules! impl_estimator { tol: Option, verbosity: Option, ) -> PyResult { - Ok(Self(Arc::new(VaporPressure::::new( + Ok(Self(Arc::new(VaporPressure::new( target.clone().into(), temperature.clone().into(), extrapolate.unwrap_or(false), @@ -287,7 +287,7 @@ macro_rules! impl_estimator { temperature: &PySIArray1, pressure: &PySIArray1, ) -> PyResult { - Ok(Self(Arc::new(LiquidDensity::::new( + Ok(Self(Arc::new(LiquidDensity::new( target.clone().into(), temperature.clone().into(), pressure.clone().into(), @@ -325,7 +325,7 @@ macro_rules! impl_estimator { tol: Option, verbosity: Option, ) -> PyResult { - Ok(Self(Arc::new(EquilibriumLiquidDensity::::new( + Ok(Self(Arc::new(EquilibriumLiquidDensity::new( target.clone().into(), temperature.clone().into(), Some((max_iter, tol, verbosity).into()), @@ -482,7 +482,7 @@ macro_rules! impl_estimator { /// Estimator #[pyclass(name = "Estimator")] #[pyo3(text_signature = "(data, weights, losses)")] - pub struct PyEstimator(Estimator); + pub struct PyEstimator(Estimator<$eos>); #[pymethods] impl PyEstimator { @@ -507,11 +507,11 @@ macro_rules! impl_estimator { /// numpy.ndarray[Float] /// The cost function evaluated for each experimental data point /// of each ``DataSet``. - /// + /// /// Note /// ---- /// The cost function is: - /// + /// /// - The relative difference between prediction and target value, /// - to which a loss function is applied, /// - and which is weighted according to the number of datapoints, @@ -650,7 +650,7 @@ macro_rules! impl_estimator_entropy_scaling { temperature: &PySIArray1, pressure: &PySIArray1, ) -> PyResult { - Ok(Self(Arc::new(Viscosity::::new( + Ok(Self(Arc::new(Viscosity::new( target.clone().into(), temperature.clone().into(), pressure.clone().into(), @@ -678,7 +678,7 @@ macro_rules! impl_estimator_entropy_scaling { temperature: &PySIArray1, pressure: &PySIArray1, ) -> PyResult { - Ok(Self(Arc::new(ThermalConductivity::::new( + Ok(Self(Arc::new(ThermalConductivity::new( target.clone().into(), temperature.clone().into(), pressure.clone().into(), @@ -706,7 +706,7 @@ macro_rules! impl_estimator_entropy_scaling { temperature: &PySIArray1, pressure: &PySIArray1, ) -> PyResult { - Ok(Self(Arc::new(Diffusion::::new( + Ok(Self(Arc::new(Diffusion::new( target.clone().into(), temperature.clone().into(), pressure.clone().into(), diff --git a/src/estimator/thermal_conductivity.rs b/src/estimator/thermal_conductivity.rs index 3d64465c9..45b3e33f4 100644 --- a/src/estimator/thermal_conductivity.rs +++ b/src/estimator/thermal_conductivity.rs @@ -1,24 +1,24 @@ use super::{DataSet, EstimatorError}; use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; use ndarray::{arr1, Array1}; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; use std::sync::Arc; /// Store experimental thermal conductivity data. #[derive(Clone)] -pub struct ThermalConductivity { - pub target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, +pub struct ThermalConductivity { + pub target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, } -impl ThermalConductivity { +impl ThermalConductivity { /// Create a new data set for experimental thermal conductivity data. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, + target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, ) -> Result { Ok(Self { target, @@ -28,18 +28,18 @@ impl ThermalConductivity { } /// Return temperature. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } /// Return pressure. - pub fn pressure(&self) -> QuantityArray1 { + pub fn pressure(&self) -> SIArray1 { self.pressure.clone() } } -impl> DataSet for ThermalConductivity { - fn target(&self) -> &QuantityArray1 { +impl DataSet for ThermalConductivity { + fn target(&self) -> &SIArray1 { &self.target } @@ -51,20 +51,20 @@ impl> DataSet for Therm vec!["temperature", "pressure"] } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let moles = arr1(&[1.0]) * U::reference_moles(); + fn predict(&self, eos: &Arc) -> Result { + let moles = arr1(&[1.0]) * SIUnit::reference_moles(); let ts = self .temperature - .to_reduced(U::reference_temperature()) + .to_reduced(SIUnit::reference_temperature()) .unwrap(); - let ps = self.pressure.to_reduced(U::reference_pressure()).unwrap(); - let unit = U::reference_energy() - / U::reference_time() - / U::reference_temperature() - / U::reference_length(); + let ps = self + .pressure + .to_reduced(SIUnit::reference_pressure()) + .unwrap(); + let unit = SIUnit::reference_energy() + / SIUnit::reference_time() + / SIUnit::reference_temperature() + / SIUnit::reference_length(); let res = ts .iter() @@ -72,8 +72,8 @@ impl> DataSet for Therm .map(|(&t, &p)| { State::new_npt( eos, - t * U::reference_temperature(), - p * U::reference_pressure(), + t * SIUnit::reference_temperature(), + p * SIUnit::reference_pressure(), &moles, DensityInitialization::None, )? @@ -85,7 +85,7 @@ impl> DataSet for Therm Ok(Array1::from_vec(res?) * unit) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(1); m.insert("temperature".to_owned(), self.temperature()); m.insert("pressure".to_owned(), self.pressure()); diff --git a/src/estimator/vapor_pressure.rs b/src/estimator/vapor_pressure.rs index e9ecd5d92..25168355d 100644 --- a/src/estimator/vapor_pressure.rs +++ b/src/estimator/vapor_pressure.rs @@ -1,22 +1,22 @@ use super::{DataSet, EstimatorError}; use feos_core::{Contributions, EosUnit, EquationOfState, PhaseEquilibrium, SolverOptions, State}; use ndarray::Array1; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SINumber, SIUnit}; use std::collections::HashMap; use std::sync::Arc; /// Store experimental vapor pressure data. #[derive(Clone)] -pub struct VaporPressure { - pub target: QuantityArray1, - temperature: QuantityArray1, - max_temperature: QuantityScalar, +pub struct VaporPressure { + pub target: SIArray1, + temperature: SIArray1, + max_temperature: SINumber, datapoints: usize, extrapolate: bool, solver_options: SolverOptions, } -impl VaporPressure { +impl VaporPressure { /// Create a new data set for vapor pressure. /// /// If the equation of state fails to compute the vapor pressure @@ -26,20 +26,20 @@ impl VaporPressure { /// calculating the slope of ln(p) over 1/T. /// If `extrapolate` is `false`, it is set to `NAN`. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, + target: SIArray1, + temperature: SIArray1, extrapolate: bool, - critical_temperature: Option>, + critical_temperature: Option, solver_options: Option, ) -> Result { let datapoints = target.len(); let max_temperature = critical_temperature.unwrap_or( temperature - .to_reduced(U::reference_temperature())? + .to_reduced(SIUnit::reference_temperature())? .into_iter() .reduce(|a, b| a.max(b)) .unwrap() - * U::reference_temperature(), + * SIUnit::reference_temperature(), ); Ok(Self { target, @@ -52,13 +52,13 @@ impl VaporPressure { } /// Return temperature. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } } -impl DataSet for VaporPressure { - fn target(&self) -> &QuantityArray1 { +impl DataSet for VaporPressure { + fn target(&self) -> &SIArray1 { &self.target } @@ -70,10 +70,7 @@ impl DataSet for VaporPressure { vec!["temperature"] } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { + fn predict(&self, eos: &Arc) -> Result { let critical_point = State::critical_point(eos, None, Some(self.max_temperature), self.solver_options) .or_else(|_| State::critical_point(eos, None, None, self.solver_options))?; @@ -86,7 +83,7 @@ impl DataSet for VaporPressure { .pressure(Contributions::Total); let b = pc.to_reduced(p0)?.ln() / (1.0 / tc - 1.0 / t0); - let a = pc.to_reduced(U::reference_pressure())?.ln() - b.to_reduced(tc)?; + let a = pc.to_reduced(SIUnit::reference_pressure())?.ln() - b.to_reduced(tc)?; let unit = self.target.get(0); let mut prediction = Array1::zeros(self.datapoints) * unit; @@ -95,15 +92,18 @@ impl DataSet for VaporPressure { if let Some(pvap) = PhaseEquilibrium::vapor_pressure(eos, t)[0] { prediction.try_set(i, pvap)?; } else if self.extrapolate { - prediction.try_set(i, (a + b.to_reduced(t)?).exp() * U::reference_pressure())?; + prediction.try_set( + i, + (a + b.to_reduced(t)?).exp() * SIUnit::reference_pressure(), + )?; } else { - prediction.try_set(i, f64::NAN * U::reference_pressure())? + prediction.try_set(i, f64::NAN * SIUnit::reference_pressure())? } } Ok(prediction) } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(1); m.insert("temperature".to_owned(), self.temperature()); m diff --git a/src/estimator/viscosity.rs b/src/estimator/viscosity.rs index 2677c36ef..8e57c56d4 100644 --- a/src/estimator/viscosity.rs +++ b/src/estimator/viscosity.rs @@ -1,24 +1,24 @@ use super::{DataSet, EstimatorError}; use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; use ndarray::arr1; -use quantity::{QuantityArray1, QuantityScalar}; +use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; use std::sync::Arc; /// Store experimental viscosity data. #[derive(Clone)] -pub struct Viscosity { - pub target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, +pub struct Viscosity { + pub target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, } -impl Viscosity { +impl Viscosity { /// Create a new data set for experimental viscosity data. pub fn new( - target: QuantityArray1, - temperature: QuantityArray1, - pressure: QuantityArray1, + target: SIArray1, + temperature: SIArray1, + pressure: SIArray1, ) -> Result { Ok(Self { target, @@ -28,18 +28,18 @@ impl Viscosity { } /// Return temperature. - pub fn temperature(&self) -> QuantityArray1 { + pub fn temperature(&self) -> SIArray1 { self.temperature.clone() } /// Return pressure. - pub fn pressure(&self) -> QuantityArray1 { + pub fn pressure(&self) -> SIArray1 { self.pressure.clone() } } -impl> DataSet for Viscosity { - fn target(&self) -> &QuantityArray1 { +impl DataSet for Viscosity { + fn target(&self) -> &SIArray1 { &self.target } @@ -51,11 +51,8 @@ impl> DataSet for Visco vec!["temperature", "pressure"] } - fn predict(&self, eos: &Arc) -> Result, EstimatorError> - where - QuantityScalar: std::fmt::Display + std::fmt::LowerExp, - { - let moles = arr1(&[1.0]) * U::reference_moles(); + fn predict(&self, eos: &Arc) -> Result { + let moles = arr1(&[1.0]) * SIUnit::reference_moles(); self.temperature .into_iter() .zip(self.pressure.into_iter()) @@ -67,7 +64,7 @@ impl> DataSet for Visco .collect() } - fn get_input(&self) -> HashMap> { + fn get_input(&self) -> HashMap { let mut m = HashMap::with_capacity(1); m.insert("temperature".to_owned(), self.temperature()); m.insert("pressure".to_owned(), self.pressure()); diff --git a/src/gc_pcsaft/dft/mod.rs b/src/gc_pcsaft/dft/mod.rs index c201c90c7..438af9ba2 100644 --- a/src/gc_pcsaft/dft/mod.rs +++ b/src/gc_pcsaft/dft/mod.rs @@ -8,7 +8,7 @@ use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, use ndarray::Array1; use num_dual::DualNum; use petgraph::graph::UnGraph; -use quantity::si::{SIArray1, SIUnit, GRAM, MOL}; +use quantity::si::{SIArray1, GRAM, MOL}; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -116,7 +116,7 @@ impl HelmholtzEnergyFunctional for GcPcSaftFunctional { } } -impl MolarWeight for GcPcSaftFunctional { +impl MolarWeight for GcPcSaftFunctional { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/gc_pcsaft/eos/mod.rs b/src/gc_pcsaft/eos/mod.rs index 58d32cd38..c6e0f9906 100644 --- a/src/gc_pcsaft/eos/mod.rs +++ b/src/gc_pcsaft/eos/mod.rs @@ -111,7 +111,7 @@ impl EquationOfState for GcPcSaft { } } -impl MolarWeight for GcPcSaft { +impl MolarWeight for GcPcSaft { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/gc_pcsaft/micelles.rs b/src/gc_pcsaft/micelles.rs index 3c16a1690..671b7c35e 100644 --- a/src/gc_pcsaft/micelles.rs +++ b/src/gc_pcsaft/micelles.rs @@ -4,16 +4,16 @@ use feos_dft::{ HelmholtzEnergyFunctional, WeightFunctionInfo, DFT, }; use ndarray::prelude::*; -use quantity::{QuantityArray1, QuantityArray2, QuantityScalar}; +use quantity::{QuantityArray2, QuantityScalar, SIArray1}; use std::sync::Arc; -pub enum MicelleInitialization { +pub enum MicelleInitialization { ExternalPotential(f64, f64), - Density(QuantityArray2), + Density(QuantityArray2), } -impl MicelleInitialization { - fn density(&self) -> Option<&QuantityArray2> { +impl MicelleInitialization { + fn density(&self) -> Option<&QuantityArray2> { match self { Self::ExternalPotential(_, _) => None, Self::Density(density) => Some(density), @@ -21,16 +21,16 @@ impl MicelleInitialization { } } -pub enum MicelleSpecification { +pub enum MicelleSpecification { ChemicalPotential, Size { delta_n_surfactant: f64, - pressure: QuantityScalar, + pressure: SINumber, }, } impl DFTSpecification - for MicelleSpecification + for MicelleSpecification { fn calculate_bulk_density( &self, @@ -46,17 +46,18 @@ impl DFTSpecification } => { let rho_s_bulk = bulk_density[1]; let rho_w_bulk = bulk_density[0]; - let volume = U::reference_volume(); - let moles = arr1(&[rho_w_bulk, rho_s_bulk]) * U::reference_density() * volume; + let volume = SIUnit::reference_volume(); + let moles = arr1(&[rho_w_bulk, rho_s_bulk]) * SIUnit::reference_density() * volume; let bulk = State::new_nvt(&profile.dft, profile.temperature, volume, &moles)?; let f_bulk = bulk.helmholtz_energy(Contributions::Total) / bulk.volume; let mu_bulk = bulk.chemical_potential(Contributions::Total); let mu_s_bulk = mu_bulk.get(1); let mu_w_bulk = mu_bulk.get(0); - let n_s_bulk = (rho_s_bulk * profile.volume()).to_reduced(U::reference_moles())?; + let n_s_bulk = + (rho_s_bulk * profile.volume()).to_reduced(SIUnit::reference_moles())?; let mut spec = (delta_n_surfactant + n_s_bulk) / z; spec[0] = ((pressure + f_bulk - rho_s_bulk * mu_s_bulk) / mu_w_bulk) - .to_reduced(U::reference_density())?; + .to_reduced(SIUnit::reference_density())?; spec } }) @@ -65,8 +66,8 @@ impl DFTSpecification pub struct MicelleProfile { pub profile: DFTProfile, - pub delta_omega: Option>, - pub delta_n: Option>, + pub delta_omega: Option, + pub delta_n: Option, } impl Clone for MicelleProfile { @@ -133,13 +134,15 @@ impl MicelleProfile { fn new( bulk: &State>, axis: Axis, - initialization: MicelleInitialization, - specification: MicelleSpecification, + initialization: MicelleInitialization, + specification: MicelleSpecification, ) -> EosResult { let dft = &bulk.eos; // calculate external potential - let t = bulk.temperature.to_reduced(U::reference_temperature())?; + let t = bulk + .temperature + .to_reduced(SIUnit::reference_temperature())?; let mut external_potential = Array2::zeros((dft.component_index().len(), axis.grid.len())); if let MicelleInitialization::ExternalPotential(peak, width) = initialization { external_potential.index_axis_mut(Axis(0), 0).assign( @@ -184,9 +187,9 @@ impl MicelleProfile { pub fn new_spherical( bulk: &State>, n_grid: usize, - width: QuantityScalar, - initialization: MicelleInitialization, - specification: MicelleSpecification, + width: SINumber, + initialization: MicelleInitialization, + specification: MicelleSpecification, ) -> EosResult { Self::new( bulk, @@ -199,9 +202,9 @@ impl MicelleProfile { pub fn new_cylindrical( bulk: &State>, n_grid: usize, - width: QuantityScalar, - initialization: MicelleInitialization, - specification: MicelleSpecification, + width: SINumber, + initialization: MicelleInitialization, + specification: MicelleSpecification, ) -> EosResult { Self::new( bulk, @@ -211,7 +214,7 @@ impl MicelleProfile { ) } - pub fn update_specification(&self, specification: MicelleSpecification) -> Self { + pub fn update_specification(&self, specification: MicelleSpecification) -> Self { let mut profile = self.clone(); profile.profile.specification = Arc::new(specification); profile.delta_omega = None; @@ -231,7 +234,7 @@ impl MicelleProfile { ) -> EosResult { let n_grid = self.profile.r().len(); let temperature = self.profile.bulk.temperature; - let t = temperature.to_reduced(U::reference_temperature())?; + let t = temperature.to_reduced(SIUnit::reference_temperature())?; let pressure = self.profile.bulk.pressure(Contributions::Total); let eos = self.profile.bulk.eos.clone(); let indices = self.profile.bulk.eos.component_index().into_owned(); @@ -242,7 +245,7 @@ impl MicelleProfile { if self .delta_omega .unwrap() - .to_reduced(U::reference_energy())? + .to_reduced(SIUnit::reference_energy())? .abs() < options.tol.unwrap_or(TOL_MICELLE) * t { diff --git a/src/pcsaft/dft/mod.rs b/src/pcsaft/dft/mod.rs index 593e1dc78..fab80baad 100644 --- a/src/pcsaft/dft/mod.rs +++ b/src/pcsaft/dft/mod.rs @@ -131,7 +131,7 @@ impl HelmholtzEnergyFunctional for PcSaftFunctional { } } -impl MolarWeight for PcSaftFunctional { +impl MolarWeight for PcSaftFunctional { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index 3e6d2b384..48f0b05fa 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -139,7 +139,7 @@ impl EquationOfState for PcSaft { } } -impl MolarWeight for PcSaft { +impl MolarWeight for PcSaft { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } @@ -174,7 +174,7 @@ fn chapman_enskog_thermal_conductivity( / KELVIN } -impl EntropyScaling for PcSaft { +impl EntropyScaling for PcSaft { fn viscosity_reference( &self, temperature: SINumber, diff --git a/src/pcsaft/python.rs b/src/pcsaft/python.rs index 43bfe2303..658722422 100644 --- a/src/pcsaft/python.rs +++ b/src/pcsaft/python.rs @@ -1,5 +1,5 @@ -use super::DQVariants; use super::parameters::{PcSaftBinaryRecord, PcSaftParameters, PcSaftRecord}; +use super::DQVariants; use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, @@ -184,7 +184,7 @@ 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/pets/dft/mod.rs b/src/pets/dft/mod.rs index 33e350ead..f14fd3aa5 100644 --- a/src/pets/dft/mod.rs +++ b/src/pets/dft/mod.rs @@ -117,7 +117,7 @@ impl HelmholtzEnergyFunctional for PetsFunctional { } } -impl MolarWeight for PetsFunctional { +impl MolarWeight for PetsFunctional { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/pets/eos/mod.rs b/src/pets/eos/mod.rs index e1f8c9d4b..93596d75d 100644 --- a/src/pets/eos/mod.rs +++ b/src/pets/eos/mod.rs @@ -104,7 +104,7 @@ impl EquationOfState for Pets { } } -impl MolarWeight for Pets { +impl MolarWeight for Pets { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } @@ -122,7 +122,7 @@ fn omega22(t: f64) -> f64 { - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() } -impl EntropyScaling for Pets { +impl EntropyScaling for Pets { fn viscosity_reference( &self, temperature: SINumber, @@ -221,7 +221,7 @@ impl EntropyScaling for Pets { // fn thermal_conductivity_reference( // &self, - // state: &State, + // state: &State, // ) -> EosResult { // if self.components() != 1 { // return Err(EosError::IncompatibleComponents(self.components(), 1)); diff --git a/src/saftvrqmie/dft/mod.rs b/src/saftvrqmie/dft/mod.rs index a13693b6b..da9d32855 100644 --- a/src/saftvrqmie/dft/mod.rs +++ b/src/saftvrqmie/dft/mod.rs @@ -105,7 +105,7 @@ impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { } } -impl MolarWeight for SaftVRQMieFunctional { +impl MolarWeight for SaftVRQMieFunctional { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } diff --git a/src/saftvrqmie/eos/mod.rs b/src/saftvrqmie/eos/mod.rs index 8087c7723..42330a9e3 100644 --- a/src/saftvrqmie/eos/mod.rs +++ b/src/saftvrqmie/eos/mod.rs @@ -108,7 +108,7 @@ impl EquationOfState for SaftVRQMie { } } -impl MolarWeight for SaftVRQMie { +impl MolarWeight for SaftVRQMie { fn molar_weight(&self) -> SIArray1 { self.parameters.molarweight.clone() * GRAM / MOL } @@ -143,7 +143,7 @@ fn chapman_enskog_thermal_conductivity( / KELVIN } -impl EntropyScaling for SaftVRQMie { +impl EntropyScaling for SaftVRQMie { fn viscosity_reference( &self, temperature: SINumber, diff --git a/src/saftvrqmie/mod.rs b/src/saftvrqmie/mod.rs index f05095813..e56adc16c 100644 --- a/src/saftvrqmie/mod.rs +++ b/src/saftvrqmie/mod.rs @@ -1,5 +1,5 @@ //! SAFT-VRQ Mie equation of state. -//! +//! //! Quantum effects are described by the first order Feynman–Hibbs corrections to Mie fluids. //! The model accurately predicts properties for pure substances and mixtures down to 20K. //! For mixtures, the additive hard-sphere reference contribution is extended with a non-additive correction. diff --git a/tests/pcsaft/state_creation_pure.rs b/tests/pcsaft/state_creation_pure.rs index a4a8015e5..2299d95cf 100644 --- a/tests/pcsaft/state_creation_pure.rs +++ b/tests/pcsaft/state_creation_pure.rs @@ -2,11 +2,9 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; use feos_core::{ - Contributions, DensityInitialization, EosUnit, EquationOfState, PhaseEquilibrium, State, - StateBuilder, + Contributions, DensityInitialization, EquationOfState, PhaseEquilibrium, State, StateBuilder, }; use quantity::si::*; -use quantity::QuantityScalar; use std::error::Error; use std::sync::Arc; @@ -309,12 +307,12 @@ fn temperature_entropy_vapor() -> Result<(), Box> { Ok(()) } -fn assert_multiple_states( - states: &[(&State, &str)], - pressure: QuantityScalar, - enthalpy: QuantityScalar, - entropy: QuantityScalar, - density: QuantityScalar, +fn assert_multiple_states( + states: &[(&State, &str)], + pressure: SINumber, + enthalpy: SINumber, + entropy: SINumber, + density: SINumber, max_relative: f64, ) { for (s, name) in states {