diff --git a/CHANGELOG.md b/CHANGELOG.md index ba4edacde..3d4d9932f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added `IdealGasModel` enum that collects all implementors of the `IdealGas` trait. [#158](https://github.com/feos-org/feos/pull/158) +- Added `feos.ideal_gas` module in Python from which (currently) `Joback` and `JobackParameters` are available. [#158](https://github.com/feos-org/feos/pull/158) + ### Changed - Changed the internal implementation of the association contribution to accomodate more general association schemes. [#150](https://github.com/feos-org/feos/pull/150) - To comply with the new association implementation, the default values of `na` and `nb` are now `0` rather than `1`. Parameter files have been adapted accordingly. [#150](https://github.com/feos-org/feos/pull/150) - Added the possibility to specify a pure component correction parameter `phi` for the heterosegmented gc PC-SAFT equation of state. [#157](https://github.com/feos-org/feos/pull/157) +- Renamed `EosVariant` to `ResidualModel`. [#158](https://github.com/feos-org/feos/pull/158) +- Added methods to add an ideal gas contribution to an initialized equation of state object in Python. [#158](https://github.com/feos-org/feos/pull/158) ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) diff --git a/benches/contributions.rs b/benches/contributions.rs index 268ad6681..a572ed597 100644 --- a/benches/contributions.rs +++ b/benches/contributions.rs @@ -8,7 +8,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; -use feos_core::{DensityInitialization, Derivative, EquationOfState, State}; +use feos_core::{DensityInitialization, Derivative, Residual, State}; use ndarray::arr1; use quantity::si::*; use std::sync::Arc; diff --git a/benches/dual_numbers.rs b/benches/dual_numbers.rs index f78ca03f7..9b447a0b2 100644 --- a/benches/dual_numbers.rs +++ b/benches/dual_numbers.rs @@ -7,7 +7,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Derivative, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, State, StateHD, + Derivative, HelmholtzEnergy, HelmholtzEnergyDual, Residual, State, StateHD, }; use ndarray::{arr1, Array}; use num_dual::DualNum; @@ -28,7 +28,7 @@ fn state_pcsaft(parameters: PcSaftParameters) -> State { } /// Residual Helmholtz energy given an equation of state and a StateHD. -fn a_res + Copy, E: EquationOfState>(inp: (&Arc, &StateHD)) -> D +fn a_res + Copy, E: Residual>(inp: (&Arc, &StateHD)) -> D where (dyn HelmholtzEnergy + 'static): HelmholtzEnergyDual, { @@ -36,7 +36,7 @@ where } /// Benchmark for evaluation of the Helmholtz energy for different dual number types. -fn bench_dual_numbers(c: &mut Criterion, group_name: &str, state: State) { +fn bench_dual_numbers(c: &mut Criterion, group_name: &str, state: State) { let mut group = c.benchmark_group(group_name); group.bench_function("a_f64", |b| { b.iter(|| a_res((&state.eos, &state.derive0()))) diff --git a/benches/state_creation.rs b/benches/state_creation.rs index 68c7b0904..b79627f32 100644 --- a/benches/state_creation.rs +++ b/benches/state_creation.rs @@ -2,14 +2,14 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Contributions, DensityInitialization, EquationOfState, PhaseEquilibrium, State, + Contributions, DensityInitialization, PhaseEquilibrium, Residual, State, }; use ndarray::{Array, Array1}; use quantity::si::*; use std::sync::Arc; /// Evaluate NPT constructor -fn npt( +fn npt( (eos, t, p, n, rho0): ( &Arc, SINumber, @@ -22,26 +22,26 @@ fn npt( } /// Evaluate critical point constructor -fn critical_point((eos, n): (&Arc, Option<&SIArray1>)) { +fn critical_point((eos, n): (&Arc, Option<&SIArray1>)) { State::critical_point(eos, n, None, Default::default()).unwrap(); } /// Evaluate critical point constructor for binary systems at given T or p -fn critical_point_binary((eos, tp): (&Arc, SINumber)) { +fn critical_point_binary((eos, tp): (&Arc, SINumber)) { State::critical_point_binary(eos, tp, None, None, Default::default()).unwrap(); } /// VLE for pure substance for given temperature or pressure -fn pure((eos, t_or_p): (&Arc, SINumber)) { +fn pure((eos, t_or_p): (&Arc, SINumber)) { PhaseEquilibrium::pure(eos, t_or_p, None, Default::default()).unwrap(); } /// Evaluate temperature, pressure flash. -fn tp_flash((eos, t, p, feed): (&Arc, SINumber, SINumber, &SIArray1)) { +fn tp_flash((eos, t, p, feed): (&Arc, SINumber, SINumber, &SIArray1)) { PhaseEquilibrium::tp_flash(eos, t, p, feed, None, Default::default(), None).unwrap(); } -fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1)) { +fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1)) { PhaseEquilibrium::bubble_point( eos, t, @@ -53,7 +53,7 @@ fn bubble_point((eos, t, x): (&Arc, SINumber, &Array1((eos, t, y): (&Arc, SINumber, &Array1)) { +fn dew_point((eos, t, y): (&Arc, SINumber, &Array1)) { PhaseEquilibrium::dew_point( eos, t, @@ -65,7 +65,7 @@ fn dew_point((eos, t, y): (&Arc, SINumber, &Array1)) .unwrap(); } -fn bench_states(c: &mut Criterion, group_name: &str, eos: &Arc) { +fn bench_states(c: &mut Criterion, group_name: &str, eos: &Arc) { let ncomponents = eos.components(); let x = Array::from_elem(ncomponents, 1.0 / ncomponents as f64); let n = &x * 100.0 * MOL; diff --git a/benches/state_properties.rs b/benches/state_properties.rs index 06c419fcb..43f0d2587 100644 --- a/benches/state_properties.rs +++ b/benches/state_properties.rs @@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::{ parameter::{IdentifierOption, Parameter}, - Contributions, EquationOfState, State, + Contributions, Residual, State, }; use ndarray::arr1; use quantity::si::*; @@ -12,7 +12,7 @@ type S = State; /// Evaluate a property of a state given the EoS, the property to compute, /// temperature, volume, moles, and the contributions to consider. -fn property, Contributions) -> T>( +fn property, Contributions) -> T>( (eos, property, t, v, n, contributions): ( &Arc, F, @@ -28,7 +28,7 @@ fn property, Contributions) -> T>( /// Evaluate a property with of a state given the EoS, the property to compute, /// temperature, volume, moles. -fn property_no_contributions) -> T>( +fn property_no_contributions) -> T>( (eos, property, t, v, n): (&Arc, F, SINumber, SINumber, &SIArray1), ) -> T { let state = State::new_nvt(eos, t, v, n).unwrap(); @@ -52,7 +52,7 @@ fn properties_pcsaft(c: &mut Criterion) { let mut group = c.benchmark_group("state_properties_pcsaft_methane_ethane_propane"); group.bench_function("a", |b| { - b.iter(|| property((&eos, S::helmholtz_energy, t, v, &m, Contributions::Total))) + b.iter(|| property_no_contributions((&eos, S::residual_helmholtz_energy, t, v, &m))) }); group.bench_function("compressibility", |b| { b.iter(|| property((&eos, S::compressibility, t, v, &m, Contributions::Total))) @@ -61,19 +61,10 @@ fn properties_pcsaft(c: &mut Criterion) { b.iter(|| property_no_contributions((&eos, S::ln_phi, t, v, &m))) }); group.bench_function("c_v", |b| { - b.iter(|| property((&eos, S::c_v, t, v, &m, Contributions::ResidualNvt))) + b.iter(|| property_no_contributions((&eos, S::c_v_res, t, v, &m))) }); group.bench_function("partial_molar_volume", |b| { - b.iter(|| { - property(( - &eos, - S::partial_molar_volume, - t, - v, - &m, - Contributions::ResidualNvt, - )) - }) + b.iter(|| property_no_contributions((&eos, S::partial_molar_volume, t, v, &m))) }); } @@ -94,7 +85,7 @@ fn properties_pcsaft_polar(c: &mut Criterion) { let mut group = c.benchmark_group("state_properties_pcsaft_polar"); group.bench_function("a", |b| { - b.iter(|| property((&eos, S::helmholtz_energy, t, v, &m, Contributions::Total))) + b.iter(|| property_no_contributions((&eos, S::residual_helmholtz_energy, t, v, &m))) }); group.bench_function("compressibility", |b| { b.iter(|| property((&eos, S::compressibility, t, v, &m, Contributions::Total))) @@ -103,19 +94,10 @@ fn properties_pcsaft_polar(c: &mut Criterion) { b.iter(|| property_no_contributions((&eos, S::ln_phi, t, v, &m))) }); group.bench_function("c_v", |b| { - b.iter(|| property((&eos, S::c_v, t, v, &m, Contributions::ResidualNvt))) + b.iter(|| property_no_contributions((&eos, S::c_v_res, t, v, &m))) }); group.bench_function("partial_molar_volume", |b| { - b.iter(|| { - property(( - &eos, - S::partial_molar_volume, - t, - v, - &m, - Contributions::ResidualNvt, - )) - }) + b.iter(|| property_no_contributions((&eos, S::partial_molar_volume, t, v, &m))) }); } diff --git a/docs/theory/eos/properties.md b/docs/theory/eos/properties.md index 5826c5db6..ae1486042 100644 --- a/docs/theory/eos/properties.md +++ b/docs/theory/eos/properties.md @@ -42,7 +42,7 @@ $$X^\mathrm{res,p}=\mathcal{D}\left(A\right)-\mathcal{D}\left(A^\mathrm{ig,p}\ri For linear operators $\mathcal{D}$ eqs. {eq}`eqn:a_ig` and {eq}`eqn:a_res` can be used to simplify the expression -$$X^\mathrm{res,p}=\mathcal{D}\left(A-A^\mathrm{ig,p}\right)=\mathcal{D}\left(A\right)-\mathcal{D}\left(nRT\ln Z\right)$$ +$$X^\mathrm{res,p}=\mathcal{D}\left(A-A^\mathrm{ig,p}\right)=\mathcal{D}\left(A^\mathrm{ig,V}\right)-\mathcal{D}\left(nRT\ln Z\right)$$ with the compressiblity factor $Z=\frac{pV}{nRT}$. @@ -52,62 +52,69 @@ For details on how the evaluation of properties from Helmholtz energy models is The table below lists all properties that are available in $\text{FeO}_\text{s}$, their definition, and whether they can be evaluated as residual contributions as well. -| Name | definition | residual? | -|-|:-:|-| -| Pressure $p$ | $-\left(\frac{\partial A}{\partial V}\right)_{T,n_i}$ | yes | -| Compressibility factor $Z$ | $\frac{pV}{nRT}$ | yes | -| Partial derivative of pressure w.r.t. volume | $\left(\frac{\partial p}{\partial V}\right)_{T,n_i}$ | yes | -| Partial derivative of pressure w.r.t. density | $\left(\frac{\partial p}{\partial \rho}\right)_{T,n_i}$ | yes | -| Partial derivative of pressure w.r.t. temperature | $\left(\frac{\partial p}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of pressure w.r.t. moles | $\left(\frac{\partial p}{\partial n_i}\right)_{T,V,n_j}$ | yes | -| Second partial derivative of pressure w.r.t. volume | $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,n_i}$ | yes | -| Second partial derivative of pressure w.r.t. density | $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,n_i}$ | yes | -| Partial molar volume $v_i$ | $\left(\frac{\partial V}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Chemical potential $\mu_i$ | $\left(\frac{\partial A}{\partial n_i}\right)_{T,V,n_j}$ | yes | -| Partial derivative of chemical potential w.r.t. temperature | $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of chemical potential w.r.t. moles | $\left(\frac{\partial\mu_i}{\partial n_j}\right)_{V,n_k}$ | yes | -| Logarithmic fugacity coefficient $\ln\varphi_i$ | $\beta\mu_i^\mathrm{res}\left(T,p,\lbrace n_i\rbrace\right)$ | no | -| Pure component logarithmic fugacity coefficient $\ln\varphi_i^\mathrm{pure}$ | $\lim_{x_i\to 1}\ln\varphi_i$ | no | -| Logarithmic (symmetric) activity coefficient $\ln\gamma_i$ | $\ln\left(\frac{\varphi_i}{\varphi_i^\mathrm{pure}}\right)$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. temperature | $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,n_i}$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. pressure | $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,n_i}=\frac{v_i^\mathrm{res,p}}{RT}$ | no | -| Partial derivative of the logarithmic fugacity coefficient w.r.t. moles | $\left(\frac{\partial\ln\varphi_i}{\partial n_j}\right)_{T,p,n_k}$ | no | -| Thermodynamic factor $\Gamma_{ij}$ | $\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ | no | -| Molar isochoric heat capacity $c_v$ | $\left(\frac{\partial u}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of the molar isochoric heat capacity w.r.t. temperature | $\left(\frac{\partial c_V}{\partial T}\right)_{V,n_i}$ | yes | -| Molar isobaric heat capacity $c_p$ | $\left(\frac{\partial h}{\partial T}\right)_{p,n_i}$ | yes | -| Entropy $S$ | $-\left(\frac{\partial A}{\partial T}\right)_{V,n_i}$ | yes | -| Partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial S}{\partial T}\right)_{V,n_i}$ | yes | -| Molar entropy $s$ | $\frac{S}{n}$ | yes | -| Enthalpy $H$ | $A+TS+pV$ | yes | -| Molar enthalpy $h$ | $\frac{H}{n}$ | yes | -| Helmholtz energy $A$ | | yes | -| Molar Helmholtz energy $a$ | $\frac{A}{n}$ | yes | -| Internal energy $U$ | $A+TS$ | yes | -| Molar internal energy $u$ | $\frac{U}{n}$ | yes | -| Gibbs energy $G$ | $A+pV$ | yes | -| Molar Gibbs energy $g$ | $\frac{G}{n}$ | yes | -| Partial molar entropy $s_i$ | $\left(\frac{\partial S}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Partial molar enthalpy $h_i$ | $\left(\frac{\partial H}{\partial n_i}\right)_{T,p,n_j}$ | yes | -| Joule Thomson coefficient $\mu_\mathrm{JT}$ | $\left(\frac{\partial T}{\partial p}\right)_{H,n_i}$ | no | -| Isentropic compressibility $\kappa_s$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{S,n_i}$ | no | -| Isothermal compressibility $\kappa_T$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,n_i}$ | no | -| (Static) structure factor $S(0)$ | $RT\left(\frac{\partial\rho}{\partial p}\right)_{T,n_i}$ | no | +In general, the evaluation of (total) Helmholtz energies and their derivatives requires a model for the residual Helmholtz energy and a model for the ideal gas contribution, specifically for the temperature dependence of the thermal de Broglie wavelength $\Lambda_i$. However, for many properties like the pressure including its derivatives and fugacity coefficients, the de Broglie wavelength cancels out. + +Due to different language paradigms, $\text{FeO}_\text{s}$ handles the ideal gas term slightly different in Rust and Python. +- In **Rust**, if no ideal gas model is provided, users can only evaluate properties for which no ideal gas model is required because the de Broglie wavelength cancels. For those properties that require an ideal gas model but the table below indicates that they can be evaluated as residual, extra functions are provided. +- In **Python**, no additional functions are required, instead the property evaluation will throw an exception if an ideal gas contribution is required but not provided. + +| Name | definition | ideal gas model required? | residual? | +|-|:-:|-|-| +| Pressure $p$ | $-\left(\frac{\partial A}{\partial V}\right)_{T,n_i}$ | no | yes | +| Compressibility factor $Z$ | $\frac{pV}{nRT}$ | no | yes | +| Partial derivative of pressure w.r.t. volume | $\left(\frac{\partial p}{\partial V}\right)_{T,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. density | $\left(\frac{\partial p}{\partial \rho}\right)_{T,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. temperature | $\left(\frac{\partial p}{\partial T}\right)_{V,n_i}$ | no | yes | +| Partial derivative of pressure w.r.t. moles | $\left(\frac{\partial p}{\partial n_i}\right)_{T,V,n_j}$ | no | yes | +| Second partial derivative of pressure w.r.t. volume | $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,n_i}$ | no | yes | +| Second partial derivative of pressure w.r.t. density | $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,n_i}$ | no | yes | +| Partial molar volume $v_i$ | $\left(\frac{\partial V}{\partial n_i}\right)_{T,p,n_j}$ | no | no | +| Chemical potential $\mu_i$ | $\left(\frac{\partial A}{\partial n_i}\right)_{T,V,n_j}$ | yes | yes | +| Partial derivative of chemical potential w.r.t. temperature | $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of chemical potential w.r.t. moles | $\left(\frac{\partial\mu_i}{\partial n_j}\right)_{V,n_k}$ | no | yes | +| Logarithmic fugacity coefficient $\ln\varphi_i$ | $\beta\mu_i^\mathrm{res}\left(T,p,\lbrace n_i\rbrace\right)$ | no | no | +| Pure component logarithmic fugacity coefficient $\ln\varphi_i^\mathrm{pure}$ | $\lim_{x_i\to 1}\ln\varphi_i$ | no | no | +| Logarithmic (symmetric) activity coefficient $\ln\gamma_i$ | $\ln\left(\frac{\varphi_i}{\varphi_i^\mathrm{pure}}\right)$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. temperature | $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,n_i}$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. pressure | $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,n_i}=\frac{v_i^\mathrm{res,p}}{RT}$ | no | no | +| Partial derivative of the logarithmic fugacity coefficient w.r.t. moles | $\left(\frac{\partial\ln\varphi_i}{\partial n_j}\right)_{T,p,n_k}$ | no | no | +| Thermodynamic factor $\Gamma_{ij}$ | $\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ | no | no | +| Molar isochoric heat capacity $c_v$ | $\left(\frac{\partial u}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of the molar isochoric heat capacity w.r.t. temperature | $\left(\frac{\partial c_V}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Molar isobaric heat capacity $c_p$ | $\left(\frac{\partial h}{\partial T}\right)_{p,n_i}$ | yes | yes | +| Entropy $S$ | $-\left(\frac{\partial A}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial S}{\partial T}\right)_{V,n_i}$ | yes | yes | +| Second partial derivative of the entropy w.r.t. temperature | $\left(\frac{\partial^2 S}{\partial T^2}\right)_{V,n_i}$ | yes | yes | +| Molar entropy $s$ | $\frac{S}{n}$ | yes | yes +| Enthalpy $H$ | $A+TS+pV$ | yes | yes | +| Molar enthalpy $h$ | $\frac{H}{n}$ | yes | yes | +| Helmholtz energy $A$ | | yes | yes | +| Molar Helmholtz energy $a$ | $\frac{A}{n}$ | yes | yes | +| Internal energy $U$ | $A+TS$ | yes | yes | +| Molar internal energy $u$ | $\frac{U}{n}$ | yes | yes | +| Gibbs energy $G$ | $A+pV$ | yes | yes | +| Molar Gibbs energy $g$ | $\frac{G}{n}$ | yes | yes | +| Partial molar entropy $s_i$ | $\left(\frac{\partial S}{\partial n_i}\right)_{T,p,n_j}$ | yes | no | +| Partial molar enthalpy $h_i$ | $\left(\frac{\partial H}{\partial n_i}\right)_{T,p,n_j}$ | yes | no | +| Joule Thomson coefficient $\mu_\mathrm{JT}$ | $\left(\frac{\partial T}{\partial p}\right)_{H,n_i}$ | yes | no | +| Isentropic compressibility $\kappa_s$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{S,n_i}$ | yes | no | +| Isothermal compressibility $\kappa_T$ | $-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,n_i}$ | no | no | +| (Static) structure factor $S(0)$ | $RT\left(\frac{\partial\rho}{\partial p}\right)_{T,n_i}$ | no | no | ## Additional properties for fluids with known molar weights If the Helmholtz energy model includes information about the molar weigt $MW_i$ of each species, additional properties are available in $\text{FeO}_\text{s}$ -| Name | definition | residual? | -|-|:-:|-| -| Total molar weight $MW$ | $\sum_ix_iMW_i$ | no | -| Mass of each component $m_i$ | $n_iMW_i$ | no | -| Total mass $m$ | $\sum_im_i=nMW$ | no | -| Mass density $\rho^{(m)}$ | $\frac{m}{V}$ | no | -| Mass fractions $w_i$ | $\frac{m_i}{m}$ | no | -| Specific entropy $s^{(m)}$ | $\frac{S}{m}$ | yes | -| Specific enthalpy $h^{(m)}$ | $\frac{H}{m}$ | yes | -| Specific Helmholtz energy $a^{(m)}$ | $\frac{A}{m}$ | yes | -| Specific internal energy $u^{(m)}$ | $\frac{U}{m}$ | yes | -| Specific Gibbs energy $g^{(m)}$ | $\frac{G}{m}$ | yes | -| Speed of sound $c$ | $\sqrt{\left(\frac{\partial p}{\partial\rho^{(m)}}\right)_{S,n_i}}$ | no | \ No newline at end of file +| Name | definition | ideal gas model required? | residual? | +|-|:-:|-|-| +| Total molar weight $MW$ | $\sum_ix_iMW_i$ | no | no | +| Mass of each component $m_i$ | $n_iMW_i$ | no | no | +| Total mass $m$ | $\sum_im_i=nMW$ | no | no | +| Mass density $\rho^{(m)}$ | $\frac{m}{V}$ | no | no | +| Mass fractions $w_i$ | $\frac{m_i}{m}$ | no | no | +| Specific entropy $s^{(m)}$ | $\frac{S}{m}$ | yes | yes | +| Specific enthalpy $h^{(m)}$ | $\frac{H}{m}$ | yes | yes | +| Specific Helmholtz energy $a^{(m)}$ | $\frac{A}{m}$ | yes | yes | +| Specific internal energy $u^{(m)}$ | $\frac{U}{m}$ | yes | yes | +| Specific Gibbs energy $g^{(m)}$ | $\frac{G}{m}$ | yes | yes | +| Speed of sound $c$ | $\sqrt{\left(\frac{\partial p}{\partial\rho^{(m)}}\right)_{S,n_i}}$ | yes | no | \ No newline at end of file diff --git a/examples/core_user_defined_eos.ipynb b/examples/core_user_defined_eos.ipynb index 2c67ffc0e..1d8fe99ca 100644 --- a/examples/core_user_defined_eos.ipynb +++ b/examples/core_user_defined_eos.ipynb @@ -80,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -207,7 +207,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -218,9 +218,16 @@ "\n", "# create an instance of our python class and hand it over to rust\n", "pr = PyPengRobinson(tc, pc, omega, molar_weight)\n", - "eos = EquationOfState.python(pr)" + "eos = EquationOfState.python_residual(pr)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -234,7 +241,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -309,7 +316,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -321,7 +328,7 @@ "1.6605390671738466e-24 mol" ] }, - "execution_count": 23, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -334,7 +341,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -346,7 +353,7 @@ "1 mol" ] }, - "execution_count": 24, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -370,14 +377,40 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "thread '' panicked at 'No ideal gas model initialized!', src/eos.rs:" + ] + }, + { + "ename": "PanicException", + "evalue": "No ideal gas model initialized!", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mPanicException\u001b[0m Traceback (most recent call last)", + "Input \u001b[0;32mIn [9]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0m s_ph \u001b[38;5;241m=\u001b[39m \u001b[43mState\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[43meos\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mpressure\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mBAR\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mmolar_enthalpy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43ms_pt\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmolar_enthalpy\u001b[49m\u001b[43m(\u001b[49m\u001b[43mContributions\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mResidual\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m)\u001b[49m\n", + "\u001b[0;31mPanicException\u001b[0m: No ideal gas model initialized!" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "49:10\n" + ] + } + ], "source": [ "s_ph = State(\n", " eos, \n", " pressure=1*BAR, \n", - " molar_enthalpy=s_pt.molar_enthalpy()\n", + " molar_enthalpy=s_pt.molar_enthalpy(Contributions.Residual)\n", ")" ] }, @@ -1248,7 +1281,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1262,7 +1295,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/examples/pcsaft_state.ipynb b/examples/pcsaft_state.ipynb index 1bcc42050..4a631e857 100644 --- a/examples/pcsaft_state.ipynb +++ b/examples/pcsaft_state.ipynb @@ -99,6 +99,118 @@ "state_nvt" ] }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(array([-1.52564778]), array([-1.52564778]))" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "state_nvt.chemical_potential(Contributions.ResidualNvt) / RGAS / state_nvt.temperature - np.log(state_nvt.compressibility()), state_nvt.ln_phi()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0.07682387846270378] K^-1" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beta = 1 / (RGAS * state_nvt.temperature)\n", + "a1 = - state_nvt.chemical_potential(Contributions.ResidualNvt) * beta / state_nvt.temperature\n", + "a2 = beta * state_nvt.dmu_dt(Contributions.ResidualNvt)\n", + "a3 = 1 / state_nvt.temperature\n", + "a4 = - state_nvt.dp_dt() / state_nvt.pressure()\n", + "a1 + a2 + a3 #+ a4" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.022509944563452417 K^-1,\n", + " [0.05098226639966782] K^-1,\n", + " 3.331667499583542e-3 K^-1,\n", + " -6.476301239431868 K^-1)" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a1, a2, a3, a4" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "([0.0001330105610385462] m³/mol, [0.0001330105610385462] m³/mol)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state_nvt.partial_molar_volume(), -state_nvt.dp_dni() / state_nvt.dp_dv()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-3.69618646800518 nPa,\n", + " 100.37302562357485 kPa,\n", + " 100.37302562357485 kPa,\n", + " 18.76231432566827 MPa)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state_nvt.pressure(Contributions.ResidualNpt), state_nvt.pressure(Contributions.ResidualNvt) + state_nvt.pressure(Contributions.IdealGas), state_nvt.pressure(Contributions.Total), state_nvt.pressure(Contributions.IdealGas)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -800,7 +912,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -814,7 +926,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.9.12" }, "vscode": { "interpreter": { diff --git a/feos-core/CHANGELOG.md b/feos-core/CHANGELOG.md index 7c14acda2..d44de111c 100644 --- a/feos-core/CHANGELOG.md +++ b/feos-core/CHANGELOG.md @@ -5,6 +5,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Added `Components`, `Residual`, `IdealGas` and `DeBroglieWavelength` traits to decouple ideal gas models from residual models. [#158](https://github.com/feos-org/feos/pull/158) +- Added `JobackParameters` struct that implements `Parameters` including Python bindings. [#158](https://github.com/feos-org/feos/pull/) + +### Changed +- Changed `EquationOfState` from a trait to a `struct` that is generic over `Residual` and `IdealGas` and implements all necessary traits to be used as equation of state including the ideal gas contribution. [#158](https://github.com/feos-org/feos/pull/) +- The `Parameter` trait no longer has an associated type `IdealGas`. [#158](https://github.com/feos-org/feos/pull/) +- Split properties of `State` into those that require the `Residual` trait (`residual_properties.rs`) and those that require both `Residual + IdealGas` (`properties.rs`). [#158](https://github.com/feos-org/feos/pull/) +- State creation routines are split into those that can be used with `Residual` and those that require `Residual + IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) +- `Contributions` enum no longer includes the `ResidualNpt` variant. `ResidualNvt` variant is renamed to `Residual`. [#158](https://github.com/feos-org/feos/pull/158) +- Moved `Verbosity` and `SolverOption` from `phase_equilibria` module to `lib.rs`. [#158](https://github.com/feos-org/feos/pull/) +- Moved `StateVec` into own file and module. [#158](https://github.com/feos-org/feos/pull/158) +- Ideal gas and residual Helmholtz energy models can now be separately implemented in Python via the `PyIdealGas` and `PyResidual` structs. [#158](https://github.com/feos-org/feos/pull/) + +### Removed +- Removed `EquationOfState` trait. [#158](https://github.com/feos-org/feos/pull/158) +- Removed ideal gas dependencies from `PureRecord` and `SegmentRecord`. [#158](https://github.com/feos-org/feos/pull/) +- Removed Python getter and setter functions and optional arguments for ideal gas records in macros. [#158](https://github.com/feos-org/feos/pull/) + ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) diff --git a/feos-core/src/cubic.rs b/feos-core/src/cubic.rs index f0dc05862..be399d67d 100644 --- a/feos-core/src/cubic.rs +++ b/feos-core/src/cubic.rs @@ -4,10 +4,7 @@ //! of state - with a single contribution to the Helmholtz energy - can be implemented. //! The implementation closely follows the form of the equations given in //! [this wikipedia article](https://en.wikipedia.org/wiki/Cubic_equations_of_state#Peng%E2%80%93Robinson_equation_of_state). -use crate::equation_of_state::{ - EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGasContribution, -}; -use crate::joback::{Joback, JobackRecord}; +use crate::equation_of_state::{Components, HelmholtzEnergy, HelmholtzEnergyDual, Residual}; use crate::parameter::{Identifier, Parameter, ParameterError, PureRecord}; use crate::si::{GRAM, MOL}; use crate::state::StateHD; @@ -64,9 +61,7 @@ pub struct PengRobinsonParameters { /// Molar weight in units of g/mol molarweight: Array1, /// List of pure component records - pure_records: Vec>, - /// List of ideal gas Joback records - joback_records: Option>, + pure_records: Vec>, } impl std::fmt::Display for PengRobinsonParameters { @@ -102,7 +97,7 @@ impl PengRobinsonParameters { acentric_factor: acentric_factor[i], }; let id = Identifier::default(); - PureRecord::new(id, molarweight[i], record, None) + PureRecord::new(id, molarweight[i], record) }) .collect(); Ok(PengRobinsonParameters::from_records( @@ -114,12 +109,11 @@ impl PengRobinsonParameters { impl Parameter for PengRobinsonParameters { type Pure = PengRobinsonRecord; - type IdealGas = JobackRecord; type Binary = f64; /// Creates parameters from pure component records. fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -139,11 +133,6 @@ impl Parameter for PengRobinsonParameters { kappa[i] = 0.37464 + (1.54226 - 0.26992 * r.acentric_factor) * r.acentric_factor; } - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { tc, a, @@ -152,16 +141,10 @@ impl Parameter for PengRobinsonParameters { kappa, molarweight, pure_records, - joback_records, } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.k_ij) } } @@ -207,8 +190,6 @@ impl fmt::Display for PengRobinsonContribution { pub struct PengRobinson { /// Parameters parameters: Arc, - /// Ideal gas contributions to the Helmholtz energy - ideal_gas: Joback, /// Non-ideal contributions to the Helmholtz energy contributions: Vec>, } @@ -216,23 +197,24 @@ pub struct PengRobinson { impl PengRobinson { /// Create a new equation of state from a set of parameters. pub fn new(parameters: Arc) -> Self { - let ideal_gas = parameters.joback_records.as_ref().map_or_else( - || Joback::default(parameters.tc.len()), - |j| Joback::new(j.clone()), - ); let contributions: Vec> = vec![Box::new(PengRobinsonContribution { parameters: parameters.clone(), })]; Self { parameters, - ideal_gas, contributions, } } } -impl EquationOfState for PengRobinson { +impl fmt::Display for PengRobinson { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Peng Robinson") + } +} + +impl Components for PengRobinson { fn components(&self) -> usize { self.parameters.b.len() } @@ -240,19 +222,17 @@ impl EquationOfState for PengRobinson { fn subset(&self, component_list: &[usize]) -> Self { Self::new(Arc::new(self.parameters.subset(component_list))) } +} +impl Residual for PengRobinson { fn compute_max_density(&self, moles: &Array1) -> f64 { let b = (moles * &self.parameters.b).sum() / moles.sum(); 0.9 / b } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.ideal_gas - } } impl MolarWeight for PengRobinson { @@ -264,15 +244,13 @@ impl MolarWeight for PengRobinson { #[cfg(test)] mod tests { use super::*; - use crate::phase_equilibria::SolverOptions; - use crate::state::State; - use crate::Contributions; - use crate::{EosResult, Verbosity}; + use crate::state::{Contributions, State}; + use crate::{EosResult, SolverOptions, Verbosity}; use approx::*; use quantity::si::*; use std::sync::Arc; - fn pure_record_vec() -> Vec> { + fn pure_record_vec() -> Vec> { let records = r#"[ { "identifier": { diff --git a/feos-core/src/density_iteration.rs b/feos-core/src/density_iteration.rs index 54c236274..541362493 100644 --- a/feos-core/src/density_iteration.rs +++ b/feos-core/src/density_iteration.rs @@ -1,11 +1,11 @@ -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::State; use crate::EosUnit; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::sync::Arc; -pub fn density_iteration( +pub fn density_iteration( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -144,7 +144,7 @@ pub fn density_iteration( } } -fn pressure_spinodal( +fn pressure_spinodal( eos: &Arc, temperature: SINumber, rho_init: SINumber, diff --git a/feos-core/src/equation_of_state.rs b/feos-core/src/equation_of_state.rs deleted file mode 100644 index 6d7b523fc..000000000 --- a/feos-core/src/equation_of_state.rs +++ /dev/null @@ -1,343 +0,0 @@ -use crate::errors::{EosError, EosResult}; -use crate::state::StateHD; -use crate::EosUnit; -use ndarray::prelude::*; -use num_dual::{ - first_derivative, second_derivative, third_derivative, Dual, Dual2, Dual2_64, Dual3, Dual3_64, - Dual64, DualNum, DualSVec64, HyperDual, HyperDual64, -}; -use num_traits::Zero; -use quantity::si::{SIArray1, SINumber, SIUnit}; -use std::fmt; - -/// Individual Helmholtz energy contribution that can -/// be evaluated using generalized (hyper) dual numbers. -/// -/// This trait needs to be implemented generically or for -/// the specific types in the supertraits of [HelmholtzEnergy] -/// so that the implementor can be used as a Helmholtz energy -/// contribution in the equation of state. -pub trait HelmholtzEnergyDual> { - /// The Helmholtz energy contribution $\beta A$ of a given state in reduced units. - fn helmholtz_energy(&self, state: &StateHD) -> D; -} - -/// Object safe version of the [HelmholtzEnergyDual] trait. -/// -/// The trait is implemented automatically for every struct that implements -/// the supertraits. -pub trait HelmholtzEnergy: - HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + fmt::Display - + Send - + Sync -{ -} - -impl HelmholtzEnergy for T where - T: HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual> - + HelmholtzEnergyDual, f64>> - + HelmholtzEnergyDual, f64>> - + fmt::Display - + Send - + Sync -{ -} - -/// Ideal gas Helmholtz energy contribution that can -/// be evaluated using generalized (hyper) dual numbers. -/// -/// This trait needs to be implemented generically or for -/// the specific types in the supertraits of [IdealGasContribution] -/// so that the implementor can be used as an ideal gas -/// contribution in the equation of state. -pub trait IdealGasContributionDual + Copy> { - /// The thermal de Broglie wavelength of each component in the form $\ln\left(\frac{\Lambda^3}{\AA^3}\right)$ - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1; - - /// Evaluate the ideal gas contribution for a given state. - /// - /// In some cases it could be advantageous to overwrite this - /// implementation instead of implementing the de Broglie - /// wavelength. - fn evaluate(&self, state: &StateHD) -> D { - let lambda = self.de_broglie_wavelength(state.temperature, state.moles.len()); - ((lambda - + state.partial_density.mapv(|x| { - if x.re() == 0.0 { - D::from(0.0) - } else { - x.ln() - 1.0 - } - })) - * &state.moles) - .sum() - } -} - -/// Object safe version of the [IdealGasContributionDual] trait. -/// -/// The trait is implemented automatically for every struct that implements -/// the supertraits. -pub trait IdealGasContribution: - IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual, f64>> - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual> - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + fmt::Display -{ -} - -impl IdealGasContribution for T where - T: IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual, f64>> - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual> - + IdealGasContributionDual> - + IdealGasContributionDual, f64>> - + IdealGasContributionDual, f64>> - + fmt::Display -{ -} - -struct DefaultIdealGasContribution; -impl + Copy> IdealGasContributionDual for DefaultIdealGasContribution { - fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1 { - Array1::zeros(components) - } -} - -impl fmt::Display for DefaultIdealGasContribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (default)") - } -} - -/// Molar weight of all components. -/// -/// The trait is required to be able to calculate (mass) -/// specific properties. -pub trait MolarWeight { - fn molar_weight(&self) -> SIArray1; -} - -/// A general equation of state. -pub trait EquationOfState: Send + Sync { - /// Return the number of components of the equation of state. - fn components(&self) -> usize; - - /// Return an equation of state consisting of the components - /// contained in component_list. - fn subset(&self, component_list: &[usize]) -> Self; - - /// Return the maximum density in Angstrom^-3. - /// - /// This value is used as an estimate for a liquid phase for phase - /// equilibria and other iterations. It is not explicitly meant to - /// be a mathematical limit for the density (if those exist in the - /// equation of state anyways). - fn compute_max_density(&self, moles: &Array1) -> f64; - - /// Return a slice of the individual contributions (excluding the ideal gas) - /// of the equation of state. - fn residual(&self) -> &[Box]; - - /// Evaluate the residual reduced Helmholtz energy $\beta A^\mathrm{res}$. - fn evaluate_residual + Copy>(&self, state: &StateHD) -> D - where - dyn HelmholtzEnergy: HelmholtzEnergyDual, - { - self.residual() - .iter() - .map(|c| c.helmholtz_energy(state)) - .sum() - } - - /// Evaluate the reduced Helmholtz energy of each individual contribution - /// and return them together with a string representation of the contribution. - fn evaluate_residual_contributions + Copy>( - &self, - state: &StateHD, - ) -> Vec<(String, D)> - where - dyn HelmholtzEnergy: HelmholtzEnergyDual, - { - self.residual() - .iter() - .map(|c| (c.to_string(), c.helmholtz_energy(state))) - .collect() - } - - /// Return the ideal gas contribution. - /// - /// Per default this function returns an ideal gas contribution - /// in which the de Broglie wavelength is 1 for every component. - /// Therefore, the correct ideal gas pressure is obtained even - /// with no explicit ideal gas term. If a more detailed model is - /// required (e.g. for the calculation of enthalpies) this function - /// has to be overwritten. - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &DefaultIdealGasContribution - } - - /// Check if the provided optional mole number is consistent with the - /// equation of state. - /// - /// In general, the number of elements in `moles` needs to match the number - /// of components of the equation of state. For a pure component, however, - /// no moles need to be provided. In that case, it is set to the constant - /// reference value. - fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { - let l = moles.map_or(1, |m| m.len()); - if self.components() == l { - match moles { - Some(m) => Ok(m.to_owned()), - None => Ok(Array::ones(1) * SIUnit::reference_moles()), - } - } else { - Err(EosError::IncompatibleComponents(self.components(), l)) - } - } - - /// Calculate the maximum density. - /// - /// This value is used as an estimate for a liquid phase for phase - /// equilibria and other iterations. It is not explicitly meant to - /// be a mathematical limit for the density (if those exist in the - /// equation of state anyways). - fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { - let mr = self - .validate_moles(moles)? - .to_reduced(SIUnit::reference_moles())?; - Ok(self.compute_max_density(&mr) * SIUnit::reference_density()) - } - - /// Calculate the second virial coefficient $B(T)$ - fn second_virial_coefficient( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let a_res = |rho| self.evaluate_residual(&StateHD::new_virial(t.into(), rho, x)); - let (_, _, b) = second_derivative(a_res, 0.0); - Ok(b * 0.5 / SIUnit::reference_density()) - } - - /// Calculate the third virial coefficient $C(T)$ - fn third_virial_coefficient( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let a_res = |rho| self.evaluate_residual(&StateHD::new_virial(t.into(), rho, x)); - let (_, _, _, c) = third_derivative(a_res, 0.0); - Ok(c / 3.0 / SIUnit::reference_density().powi(2)) - } - - /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ - fn second_virial_coefficient_temperature_derivative( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let b = |t| { - let a_res = |rho: Dual2| { - self.evaluate_residual(&StateHD::new_virial(Dual2::from_re(t), rho, x)) - }; - let (_, _, b) = second_derivative(a_res, Dual64::zero()); - b - }; - let (_, b_t) = first_derivative(b, t); - Ok(b_t * 0.5 / (SIUnit::reference_density() * SIUnit::reference_temperature())) - } - - /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ - fn third_virial_coefficient_temperature_derivative( - &self, - temperature: SINumber, - moles: Option<&SIArray1>, - ) -> EosResult { - let mr = self.validate_moles(moles)?; - let x = mr.to_reduced(mr.sum())?; - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let c = |t| { - let a_res = - |rho| self.evaluate_residual(&StateHD::new_virial(Dual3::from_re(t), rho, x)); - let (_, _, _, c) = third_derivative(a_res, Dual64::zero()); - c - }; - let (_, c_t) = first_derivative(c, t); - Ok(c_t / 3.0 / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) - } -} - -/// Reference values and residual entropy correlations for entropy scaling. -pub trait EntropyScaling { - fn viscosity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; - fn diffusion_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult; - fn thermal_conductivity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult; - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; -} diff --git a/feos-core/src/equation_of_state/helmholtz_energy.rs b/feos-core/src/equation_of_state/helmholtz_energy.rs new file mode 100644 index 000000000..11d2c2703 --- /dev/null +++ b/feos-core/src/equation_of_state/helmholtz_energy.rs @@ -0,0 +1,59 @@ +use crate::StateHD; +use num_dual::*; +use std::fmt; + +/// Individual Helmholtz energy contribution that can +/// be evaluated using generalized (hyper) dual numbers. +/// +/// This trait needs to be implemented generically or for +/// the specific types in the supertraits of [HelmholtzEnergy] +/// so that the implementor can be used as a Helmholtz energy +/// contribution in the equation of state. +pub trait HelmholtzEnergyDual> { + /// The Helmholtz energy contribution $\beta A$ of a given state in reduced units. + fn helmholtz_energy(&self, state: &StateHD) -> D; +} + +/// Object safe version of the [HelmholtzEnergyDual] trait. +/// +/// The trait is implemented automatically for every struct that implements +/// the supertraits. +pub trait HelmholtzEnergy: + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + fmt::Display + + Send + + Sync +{ +} + +impl HelmholtzEnergy for T where + T: HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual> + + HelmholtzEnergyDual, f64>> + + HelmholtzEnergyDual, f64>> + + fmt::Display + + Send + + Sync +{ +} diff --git a/feos-core/src/equation_of_state/ideal_gas.rs b/feos-core/src/equation_of_state/ideal_gas.rs new file mode 100644 index 000000000..12a2653be --- /dev/null +++ b/feos-core/src/equation_of_state/ideal_gas.rs @@ -0,0 +1,90 @@ +use super::Components; +use crate::StateHD; +use ndarray::Array1; +use num_dual::DualNum; +use num_dual::*; +use std::fmt; + +/// Ideal gas Helmholtz energy contribution. +pub trait IdealGas: Components + Sync + Send { + // Return a reference to the implementation of the de Broglie wavelength. + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength; + + /// Evaluate the ideal gas Helmholtz energy contribution for a given state. + /// + /// In some cases it could be advantageous to overwrite this + /// implementation instead of implementing the de Broglie + /// wavelength. + fn evaluate_ideal_gas + Copy>(&self, state: &StateHD) -> D + where + for<'a> dyn DeBroglieWavelength + 'a: DeBroglieWavelengthDual, + { + let ln_lambda3 = self.ideal_gas_model().ln_lambda3(state.temperature); + ((ln_lambda3 + + state.partial_density.mapv(|x| { + if x.re() == 0.0 { + D::from(0.0) + } else { + x.ln() - 1.0 + } + })) + * &state.moles) + .sum() + } +} + +/// Implementation of an ideal gas model in terms of the +/// logarithm of the cubic thermal de Broglie wavelength +/// in units ln(A³). +/// +/// This trait needs to be implemented generically or for +/// the specific types in the supertraits of [DeBroglieWavelength] +/// so that the implementor can be used as an ideal gas +/// contribution in the equation of state. +pub trait DeBroglieWavelengthDual> { + fn ln_lambda3(&self, temperature: D) -> Array1; +} + +/// Object safe version of the [DeBroglieWavelengthDual] trait. +/// +/// The trait is implemented automatically for every struct that implements +/// the supertraits. +pub trait DeBroglieWavelength: + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + fmt::Display + + Send + + Sync +{ +} + +impl DeBroglieWavelength for T where + T: DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual> + + DeBroglieWavelengthDual, f64>> + + DeBroglieWavelengthDual, f64>> + + fmt::Display + + Send + + Sync +{ +} diff --git a/feos-core/src/equation_of_state/mod.rs b/feos-core/src/equation_of_state/mod.rs new file mode 100644 index 000000000..bb7b237cb --- /dev/null +++ b/feos-core/src/equation_of_state/mod.rs @@ -0,0 +1,128 @@ +use crate::EosResult; +use ndarray::Array1; +use quantity::si::{SIArray1, SINumber}; +use std::sync::Arc; + +mod helmholtz_energy; +mod ideal_gas; +mod residual; + +pub use helmholtz_energy::{HelmholtzEnergy, HelmholtzEnergyDual}; +pub use ideal_gas::{DeBroglieWavelength, DeBroglieWavelengthDual, IdealGas}; +pub use residual::{EntropyScaling, Residual}; + +/// Molar weight of all components. +/// +/// The trait is required to be able to calculate (mass) +/// specific properties. +pub trait MolarWeight { + fn molar_weight(&self) -> SIArray1; +} + +/// The number of components that the model is initialized for. +pub trait Components { + /// Return the number of components of the model. + fn components(&self) -> usize; + + /// Return a model consisting of the components + /// contained in component_list. + fn subset(&self, component_list: &[usize]) -> Self; +} + +/// An equation of state consisting of an ideal gas model +/// and a residual Helmholtz energy model. +#[derive(Clone)] +pub struct EquationOfState { + pub ideal_gas: Arc, + pub residual: Arc, +} + +impl EquationOfState { + /// Return a new [EquationOfState] with the given ideal gas + /// and residual models. + pub fn new(ideal_gas: Arc, residual: Arc) -> Self { + Self { + ideal_gas, + residual, + } + } +} + +impl Components for EquationOfState { + fn components(&self) -> usize { + assert_eq!( + self.residual.components(), + self.ideal_gas.components(), + "residual and ideal gas model differ in the number of components" + ); + self.residual.components() + } + + fn subset(&self, component_list: &[usize]) -> Self { + Self::new( + Arc::new(self.ideal_gas.subset(component_list)), + Arc::new(self.residual.subset(component_list)), + ) + } +} + +impl IdealGas for EquationOfState { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self.ideal_gas.ideal_gas_model() + } +} + +impl Residual for EquationOfState { + fn compute_max_density(&self, moles: &Array1) -> f64 { + self.residual.compute_max_density(moles) + } + + fn contributions(&self) -> &[Box] { + self.residual.contributions() + } +} + +impl MolarWeight for EquationOfState { + fn molar_weight(&self) -> SIArray1 { + self.residual.molar_weight() + } +} + +impl EntropyScaling for EquationOfState { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .viscosity_reference(temperature, volume, moles) + } + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.viscosity_correlation(s_res, x) + } + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .diffusion_reference(temperature, volume, moles) + } + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.diffusion_correlation(s_res, x) + } + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + self.residual + .thermal_conductivity_reference(temperature, volume, moles) + } + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + self.residual.thermal_conductivity_correlation(s_res, x) + } +} diff --git a/feos-core/src/equation_of_state/residual.rs b/feos-core/src/equation_of_state/residual.rs new file mode 100644 index 000000000..6746c3718 --- /dev/null +++ b/feos-core/src/equation_of_state/residual.rs @@ -0,0 +1,172 @@ +use super::{Components, HelmholtzEnergy, HelmholtzEnergyDual}; +use crate::StateHD; +use crate::{EosError, EosResult, EosUnit}; +use ndarray::prelude::*; +use num_dual::*; +use num_traits::{One, Zero}; +use quantity::si::{SIArray1, SINumber, SIUnit}; + +/// A reisdual Helmholtz energy model. +pub trait Residual: Components + Send + Sync { + /// Return the maximum density in Angstrom^-3. + /// + /// This value is used as an estimate for a liquid phase for phase + /// equilibria and other iterations. It is not explicitly meant to + /// be a mathematical limit for the density (if those exist in the + /// equation of state anyways). + fn compute_max_density(&self, moles: &Array1) -> f64; + + /// Return a slice of the individual contributions (excluding the ideal gas) + /// of the equation of state. + fn contributions(&self) -> &[Box]; + + /// Evaluate the residual reduced Helmholtz energy $\beta A^\mathrm{res}$. + fn evaluate_residual + Copy>(&self, state: &StateHD) -> D + where + dyn HelmholtzEnergy: HelmholtzEnergyDual, + { + self.contributions() + .iter() + .map(|c| c.helmholtz_energy(state)) + .sum() + } + + /// Evaluate the reduced Helmholtz energy of each individual contribution + /// and return them together with a string representation of the contribution. + fn evaluate_residual_contributions + Copy>( + &self, + state: &StateHD, + ) -> Vec<(String, D)> + where + dyn HelmholtzEnergy: HelmholtzEnergyDual, + { + self.contributions() + .iter() + .map(|c| (c.to_string(), c.helmholtz_energy(state))) + .collect() + } + + /// Check if the provided optional mole number is consistent with the + /// equation of state. + /// + /// In general, the number of elements in `moles` needs to match the number + /// of components of the equation of state. For a pure component, however, + /// no moles need to be provided. In that case, it is set to the constant + /// reference value. + fn validate_moles(&self, moles: Option<&SIArray1>) -> EosResult { + let l = moles.map_or(1, |m| m.len()); + if self.components() == l { + match moles { + Some(m) => Ok(m.to_owned()), + None => Ok(Array::ones(1) * SIUnit::reference_moles()), + } + } else { + Err(EosError::IncompatibleComponents(self.components(), l)) + } + } + + /// Calculate the maximum density. + /// + /// This value is used as an estimate for a liquid phase for phase + /// equilibria and other iterations. It is not explicitly meant to + /// be a mathematical limit for the density (if those exist in the + /// equation of state anyways). + fn max_density(&self, moles: Option<&SIArray1>) -> EosResult { + let mr = self + .validate_moles(moles)? + .to_reduced(SIUnit::reference_moles())?; + Ok(self.compute_max_density(&mr) * SIUnit::reference_density()) + } + + /// Calculate the second virial coefficient $B(T)$ + fn second_virial_coefficient( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let mut rho = HyperDual64::zero(); + rho.eps1 = 1.0; + rho.eps2 = 1.0; + let t = HyperDual64::from(temperature.to_reduced(SIUnit::reference_temperature())?); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).eps1eps2 * 0.5 / SIUnit::reference_density()) + } + + /// Calculate the third virial coefficient $C(T)$ + fn third_virial_coefficient( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let rho = Dual3_64::zero().derivative(); + let t = Dual3_64::from(temperature.to_reduced(SIUnit::reference_temperature())?); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).v3 / 3.0 / SIUnit::reference_density().powi(2)) + } + + /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ + fn second_virial_coefficient_temperature_derivative( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let mut rho = HyperDual::zero(); + rho.eps1 = Dual64::one(); + rho.eps2 = Dual64::one(); + let t = HyperDual::from_re( + Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derivative(), + ); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).eps1eps2.eps * 0.5 + / (SIUnit::reference_density() * SIUnit::reference_temperature())) + } + + /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ + fn third_virial_coefficient_temperature_derivative( + &self, + temperature: SINumber, + moles: Option<&SIArray1>, + ) -> EosResult { + let mr = self.validate_moles(moles)?; + let x = mr.to_reduced(mr.sum())?; + let rho = Dual3::zero().derivative(); + let t = Dual3::from_re( + Dual64::from(temperature.to_reduced(SIUnit::reference_temperature())?).derivative(), + ); + let s = StateHD::new_virial(t, rho, x); + Ok(self.evaluate_residual(&s).v3.eps + / 3.0 + / (SIUnit::reference_density().powi(2) * SIUnit::reference_temperature())) + } +} + +/// Reference values and residual entropy correlations for entropy scaling. +pub trait EntropyScaling { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult; + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult; + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult; +} diff --git a/feos-core/src/joback.rs b/feos-core/src/joback.rs index b8d73e90e..d1a6bdb85 100644 --- a/feos-core/src/joback.rs +++ b/feos-core/src/joback.rs @@ -1,17 +1,16 @@ //! Implementation of the ideal gas heat capacity (de Broglie wavelength) //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). +use crate::equation_of_state::{Components, DeBroglieWavelength, DeBroglieWavelengthDual}; use crate::parameter::*; -use crate::{ - EosResult, EosUnit, EquationOfState, HelmholtzEnergy, IdealGasContribution, - IdealGasContributionDual, -}; +use crate::{EosResult, EosUnit, IdealGas}; use conv::ValueInto; -use ndarray::Array1; +use ndarray::{Array, Array1, Array2}; use num_dual::*; use quantity::si::{SINumber, SIUnit}; use serde::{Deserialize, Serialize}; use std::fmt; +use std::sync::Arc; /// Coefficients used in the Joback model. /// @@ -65,99 +64,186 @@ impl> FromSegments for JobackRecord { } } +/// Parameters for one or more components for the Joback and Reid model. +pub struct JobackParameters { + a: Array1, + b: Array1, + c: Array1, + d: Array1, + e: Array1, + pure_records: Vec>, + binary_records: Array2, +} + +impl Parameter for JobackParameters { + type Pure = JobackRecord; + type Binary = JobackBinaryRecord; + + fn from_records( + pure_records: Vec>, + _binary_records: Array2, + ) -> Self { + let n = pure_records.len(); + + let binary_records = Array::from_elem((n, n), JobackBinaryRecord); + let mut a = Array::zeros(n); + let mut b = Array::zeros(n); + let mut c = Array::zeros(n); + let mut d = Array::zeros(n); + let mut e = Array::zeros(n); + + for (i, record) in pure_records.iter().enumerate() { + let r = &record.model_record; + a[i] = r.a; + b[i] = r.b; + c[i] = r.c; + d[i] = r.d; + e[i] = r.e; + } + + Self { + a, + b, + c, + d, + e, + pure_records, + binary_records, + } + } + + fn records(&self) -> (&[PureRecord], &Array2) { + (&self.pure_records, &self.binary_records) + } +} + +/// Dummy implementation to satisfy traits for parameter handling. +/// Not intended to be used. +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct JobackBinaryRecord; + +impl From for JobackBinaryRecord { + fn from(_: f64) -> Self { + Self + } +} + +impl From for f64 { + fn from(_: JobackBinaryRecord) -> Self { + 0.0 // nasty hack - panic crashes Ipython kernel, actual value is never used + } +} + +impl> FromSegmentsBinary for JobackBinaryRecord { + fn from_segments_binary(_segments: &[(Self, T, T)]) -> Result { + Err(ParameterError::IncompatibleParameters( + "No binary interaction parameters implemented for Joback".to_string(), + )) + } +} + +impl std::fmt::Display for JobackBinaryRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + /// The ideal gas contribution according to /// [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). -#[derive(Debug, Clone)] +/// +/// The (cubic) de Broglie wavelength is calculated by integrating +/// the heat capacity with the following reference values: +/// +/// - T = 289.15 K +/// - p = 1e5 Pa +/// - V = 1e-30 A³ pub struct Joback { - pub records: Vec, + pub parameters: Arc, } impl Joback { /// Creates a new Joback contribution. - pub fn new(records: Vec) -> Self { - Self { records } - } - - /// Creates a default ($c_p^\mathrm{ig}=0$) ideal gas contribution for the - /// given number of components. - pub fn default(components: usize) -> Self { - Self::new(vec![JobackRecord::default(); components]) + pub fn new(parameters: Arc) -> Self { + Self { parameters } } /// Directly calculates the ideal gas heat capacity from the Joback model. pub fn c_p(&self, temperature: SINumber, molefracs: &Array1) -> EosResult { let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let mut c_p = 0.0; - for (j, &x) in self.records.iter().zip(molefracs.iter()) { - c_p += x * (j.a + j.b * t + j.c * t.powi(2) + j.d * t.powi(3) + j.e * t.powi(4)); - } + let p = &self.parameters; + let c_p = (molefracs + * &(&p.a + &p.b * t + &p.c * t.powi(2) + &p.d * t.powi(3) + &p.e * t.powi(4))) + .sum(); Ok(c_p / RGAS * SIUnit::gas_constant()) } } -impl fmt::Display for Joback { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (Joback)") +impl Components for Joback { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { + let mut records = Vec::with_capacity(component_list.len()); + component_list + .iter() + .for_each(|&i| records.push(self.parameters.pure_records[i].clone())); + let n = component_list.len(); + Self::new(Arc::new(JobackParameters::from_records( + records, + Array::from_elem((n, n), JobackBinaryRecord), + ))) } } -const RGAS: f64 = 6.022140857 * 1.38064852; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; -const KB: f64 = 1.38064852e-23; +impl IdealGas for Joback { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self + } +} -impl + Copy> IdealGasContributionDual for Joback { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { +impl + Copy> DeBroglieWavelengthDual for Joback { + fn ln_lambda3(&self, temperature: D) -> Array1 { let t = temperature; let t2 = t * t; + let t4 = t2 * t2; let f = (temperature * KB / (P0 * A3)).ln(); - Array1::from_shape_fn(components, |i| { - let j = &self.records[i]; - let h = (t2 - T0 * T0) * 0.5 * j.b - + (t * t2 - T0.powi(3)) * j.c / 3.0 - + (t2 * t2 - T0.powi(4)) * j.d / 4.0 - + (t2 * t2 * t - T0.powi(5)) * j.e / 5.0 + Array1::from_shape_fn(self.parameters.pure_records.len(), |i| { + let j = &self.parameters.pure_records[i].model_record; + let h = (t2 - T0_2) * 0.5 * j.b + + (t * t2 - T0_3) * j.c / 3.0 + + (t4 - T0_4) * j.d / 4.0 + + (t4 * t - T0_5) * j.e / 5.0 + (t - T0) * j.a; let s = (t - T0) * j.b - + (t2 - T0.powi(2)) * 0.5 * j.c - + (t2 * t - T0.powi(3)) * j.d / 3.0 - + (t2 * t2 - T0.powi(4)) * j.e / 4.0 + + (t2 - T0_2) * 0.5 * j.c + + (t2 * t - T0_3) * j.d / 3.0 + + (t4 - T0_4) * j.e / 4.0 + (t / T0).ln() * j.a; (h - t * s) / (t * RGAS) + f }) } } -impl EquationOfState for Joback { - fn components(&self) -> usize { - self.records.len() - } - - fn subset(&self, component_list: &[usize]) -> Self { - let records = component_list - .iter() - .map(|&i| self.records[i].clone()) - .collect(); - Self::new(records) - } - - fn compute_max_density(&self, _moles: &Array1) -> f64 { - 1.0 - } - - fn residual(&self) -> &[Box] { - &[] - } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - self +impl fmt::Display for Joback { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal gas (Joback)") } } +const RGAS: f64 = 6.022140857 * 1.38064852; +const T0: f64 = 298.15; +const T0_2: f64 = 298.15 * 298.15; +const T0_3: f64 = T0 * T0_2; +const T0_4: f64 = T0_2 * T0_2; +const T0_5: f64 = T0 * T0_4; +const P0: f64 = 1.0e5; +const A3: f64 = 1e-30; +const KB: f64 = 1.38064852e-23; + #[cfg(test)] mod tests { - use crate::{Contributions, State, StateBuilder}; + use crate::{Contributions, Residual, State, StateBuilder}; use approx::assert_relative_eq; use ndarray::arr1; use quantity::si::*; @@ -165,16 +251,23 @@ mod tests { use super::*; - #[derive(Deserialize, Clone, Debug)] - struct ModelRecord; + // implement Residual to test Joback as equation of state + impl Residual for Joback { + fn compute_max_density(&self, _moles: &Array1) -> f64 { + 1.0 + } + + fn contributions(&self) -> &[Box] { + &[] + } + } #[test] fn paper_example() -> EosResult<()> { let segments_json = r#"[ { "identifier": "-Cl", - "model_record": null, - "ideal_gas_record": { + "model_record": { "a": 33.3, "b": -0.0963, "c": 0.000187, @@ -185,8 +278,7 @@ mod tests { }, { "identifier": "-CH=(ring)", - "model_record": null, - "ideal_gas_record": { + "model_record": { "a": -2.14, "b": 5.74e-2, "c": -1.64e-6, @@ -197,8 +289,7 @@ mod tests { }, { "identifier": "=CH<(ring)", - "model_record": null, - "ideal_gas_record": { + "model_record": { "a": -8.25, "b": 1.01e-1, "c": -1.42e-4, @@ -208,7 +299,7 @@ mod tests { "molarweight": 13.01864 } ]"#; - let segment_records: Vec> = + let segment_records: Vec> = serde_json::from_str(segments_json).expect("Unable to parse json."); let segments = ChemicalRecord::new( Identifier::default(), @@ -230,7 +321,7 @@ mod tests { assert_eq!(segments.get(&segment_records[2]), Some(&2)); let joback_segments: Vec<_> = segments .iter() - .map(|(s, &n)| (s.ideal_gas_record.clone().unwrap(), n)) + .map(|(s, &n)| (s.model_record.clone(), n)) .collect(); let jr = JobackRecord::from_segments(&joback_segments)?; assert_relative_eq!( @@ -255,7 +346,8 @@ mod tests { ); assert_relative_eq!(jr.e, 0.0); - let eos = Arc::new(Joback::new(vec![jr])); + let pr = PureRecord::new(Identifier::default(), 1.0, jr); + let eos = Arc::new(Joback::new(Arc::new(JobackParameters::new_pure(pr)))); let state = State::new_nvt( &eos, 1000.0 * KELVIN, @@ -275,9 +367,18 @@ mod tests { #[test] fn c_p_comparison() -> EosResult<()> { - let record1 = JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005); - let record2 = JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001); - let joback = Arc::new(Joback::new(vec![record1, record2])); + let record1 = PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(1.0, 0.2, 0.03, 0.004, 0.005), + ); + let record2 = PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(-5.0, 0.4, 0.03, 0.002, 0.001), + ); + let parameters = Arc::new(JobackParameters::new_binary(vec![record1, record2], None)); + let joback = Arc::new(Joback::new(parameters)); let temperature = 300.0 * KELVIN; let volume = METER.powi(3); let moles = arr1(&[1.0, 3.0]) * MOL; diff --git a/feos-core/src/lib.rs b/feos-core/src/lib.rs index 54cb98ff1..ddea14018 100644 --- a/feos-core/src/lib.rs +++ b/feos-core/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::reversed_empty_ranges)] #![allow(clippy::many_single_char_names)] #![allow(clippy::too_many_arguments)] +#![allow(deprecated)] use quantity::si::*; use quantity::*; @@ -35,13 +36,11 @@ pub mod parameter; mod phase_equilibria; mod state; pub use equation_of_state::{ - EntropyScaling, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, IdealGasContribution, - IdealGasContributionDual, MolarWeight, + Components, DeBroglieWavelength, DeBroglieWavelengthDual, EntropyScaling, EquationOfState, + HelmholtzEnergy, HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, }; pub use errors::{EosError, EosResult}; -pub use phase_equilibria::{ - PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, SolverOptions, Verbosity, -}; +pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium}; pub use state::{ Contributions, DensityInitialization, Derivative, State, StateBuilder, StateHD, StateVec, }; @@ -49,6 +48,77 @@ pub use state::{ #[cfg(feature = "python")] pub mod python; +/// Level of detail in the iteration output. +#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum Verbosity { + /// Do not print output. + None, + /// Print information about the success of failure of the iteration. + Result, + /// Print a detailed outpur for every iteration. + Iter, +} + +impl Default for Verbosity { + fn default() -> Self { + Self::None + } +} + +/// Options for the various phase equilibria solvers. +/// +/// If the values are [None], solver specific default +/// values are used. +#[derive(Copy, Clone, Default)] +pub struct SolverOptions { + /// Maximum number of iterations. + pub max_iter: Option, + /// Tolerance. + pub tol: Option, + /// Iteration outpput indicated by the [Verbosity] enum. + pub verbosity: Verbosity, +} + +impl From<(Option, Option, Option)> for SolverOptions { + fn from(options: (Option, Option, Option)) -> Self { + Self { + max_iter: options.0, + tol: options.1, + verbosity: options.2.unwrap_or(Verbosity::None), + } + } +} + +impl SolverOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn max_iter(mut self, max_iter: usize) -> Self { + self.max_iter = Some(max_iter); + self + } + + pub fn tol(mut self, tol: f64) -> Self { + self.tol = Some(tol); + self + } + + pub fn verbosity(mut self, verbosity: Verbosity) -> Self { + self.verbosity = verbosity; + self + } + + pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { + ( + self.max_iter.unwrap_or(max_iter), + self.tol.unwrap_or(tol), + self.verbosity, + ) + } +} + /// Consistent conversions between quantities and reduced properties. pub trait EosUnit: Unit + Send + Sync { fn reference_temperature() -> QuantityScalar; @@ -121,3 +191,261 @@ impl EosUnit for SIUnit { RGAS } } + +#[cfg(test)] +mod tests { + use crate::cubic::*; + use crate::equation_of_state::EquationOfState; + use crate::joback::{Joback, JobackParameters, JobackRecord}; + use crate::parameter::*; + use crate::Contributions; + use crate::EosResult; + use crate::StateBuilder; + use approx::*; + use ndarray::Array2; + use quantity::si::*; + use std::sync::Arc; + + fn pure_record_vec() -> Vec> { + let records = r#"[ + { + "identifier": { + "cas": "74-98-6", + "name": "propane", + "iupac_name": "propane", + "smiles": "CCC", + "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", + "formula": "C3H8" + }, + "model_record": { + "tc": 369.96, + "pc": 4250000.0, + "acentric_factor": 0.153 + }, + "molarweight": 44.0962 + }, + { + "identifier": { + "cas": "106-97-8", + "name": "butane", + "iupac_name": "butane", + "smiles": "CCCC", + "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", + "formula": "C4H10" + }, + "model_record": { + "tc": 425.2, + "pc": 3800000.0, + "acentric_factor": 0.199 + }, + "molarweight": 58.123 + } + ]"#; + serde_json::from_str(records).expect("Unable to parse json.") + } + + #[test] + fn validate_residual_properties() -> EosResult<()> { + let mixture = pure_record_vec(); + let propane = mixture[0].clone(); + let parameters = PengRobinsonParameters::from_records(vec![propane], Array2::zeros((1, 1))); + let residual = Arc::new(PengRobinson::new(Arc::new(parameters))); + let joback_parameters = Arc::new(JobackParameters::new_pure(PureRecord::new( + Identifier::default(), + 1.0, + JobackRecord::new(0.0, 0.0, 0.0, 0.0, 0.0), + ))); + let ideal_gas = Arc::new(Joback::new(joback_parameters)); + let eos = Arc::new(EquationOfState::new(ideal_gas, residual.clone())); + + let sr = StateBuilder::new(&residual) + .temperature(300.0 * KELVIN) + .pressure(1.0 * BAR) + .build()?; + + let s = StateBuilder::new(&eos) + .temperature(300.0 * KELVIN) + .pressure(1.0 * BAR) + .build()?; + + // pressure + assert_relative_eq!( + s.pressure(Contributions::Total), + sr.pressure(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.pressure(Contributions::Residual), + sr.pressure(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.compressibility(Contributions::Total), + sr.compressibility(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.compressibility(Contributions::Residual), + sr.compressibility(Contributions::Residual), + max_relative = 1e-15 + ); + + // residual properties + assert_relative_eq!( + s.helmholtz_energy(Contributions::Residual), + sr.residual_helmholtz_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.entropy(Contributions::Residual), + sr.residual_entropy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.enthalpy(Contributions::Residual), + sr.residual_enthalpy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.internal_energy(Contributions::Residual), + sr.residual_internal_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.gibbs_energy(Contributions::Residual), + sr.residual_gibbs_energy(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.chemical_potential(Contributions::Residual), + sr.residual_chemical_potential(), + max_relative = 1e-15 + ); + + // pressure derivatives + assert_relative_eq!( + s.structure_factor(), + sr.structure_factor(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dt(Contributions::Total), + sr.dp_dt(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dt(Contributions::Residual), + sr.dp_dt(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dv(Contributions::Total), + sr.dp_dv(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dv(Contributions::Residual), + sr.dp_dv(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_drho(Contributions::Total), + sr.dp_drho(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_drho(Contributions::Residual), + sr.dp_drho(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_dv2(Contributions::Total), + sr.d2p_dv2(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_dv2(Contributions::Residual), + sr.d2p_dv2(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_drho2(Contributions::Total), + sr.d2p_drho2(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.d2p_drho2(Contributions::Residual), + sr.d2p_drho2(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dni(Contributions::Total), + sr.dp_dni(Contributions::Total), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dp_dni(Contributions::Residual), + sr.dp_dni(Contributions::Residual), + max_relative = 1e-15 + ); + + // entropy + assert_relative_eq!( + s.ds_dt(Contributions::Residual), + sr.ds_res_dt(), + max_relative = 1e-15 + ); + + // chemical potential + assert_relative_eq!( + s.dmu_dt(Contributions::Residual), + sr.dmu_res_dt(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dmu_dni(Contributions::Residual), + sr.dmu_dni(Contributions::Residual), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dmu_dt(Contributions::Residual), + sr.dmu_res_dt(), + max_relative = 1e-15 + ); + + // fugacity + assert_relative_eq!(s.ln_phi(), sr.ln_phi(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dt(), sr.dln_phi_dt(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dp(), sr.dln_phi_dp(), max_relative = 1e-15); + assert_relative_eq!(s.dln_phi_dnj(), sr.dln_phi_dnj(), max_relative = 1e-15); + assert_relative_eq!( + s.thermodynamic_factor(), + sr.thermodynamic_factor(), + max_relative = 1e-15 + ); + + // residual properties using multiple derivatives + assert_relative_eq!( + s.c_v(Contributions::Residual), + sr.c_v_res(), + max_relative = 1e-15 + ); + assert_relative_eq!( + s.dc_v_dt(Contributions::Residual), + sr.dc_v_res_dt(), + max_relative = 1e-15 + ); + println!( + "{}\n{}\n{}", + s.c_p(Contributions::Residual), + s.c_p(Contributions::IdealGas), + s.c_p(Contributions::Total) + ); + assert_relative_eq!( + s.c_p(Contributions::Residual), + sr.c_p_res(), + max_relative = 1e-14 + ); + Ok(()) + } +} diff --git a/feos-core/src/parameter/chemical_record.rs b/feos-core/src/parameter/chemical_record.rs index f498b1ba9..900a9ee87 100644 --- a/feos-core/src/parameter/chemical_record.rs +++ b/feos-core/src/parameter/chemical_record.rs @@ -120,13 +120,13 @@ pub trait SegmentCount { /// molecule. /// /// The map contains the segment record as key and the count as value. - fn segment_map( + fn segment_map( &self, - segment_records: &[SegmentRecord], - ) -> Result, Self::Count>, ParameterError> { + segment_records: &[SegmentRecord], + ) -> Result, Self::Count>, ParameterError> { let count = self.segment_count(); let queried: HashSet<_> = count.keys().cloned().collect(); - let mut segments: HashMap> = segment_records + let mut segments: HashMap> = segment_records .iter() .map(|r| (r.identifier.clone(), r.clone())) .collect(); diff --git a/feos-core/src/parameter/mod.rs b/feos-core/src/parameter/mod.rs index 48c62c303..5782db00f 100644 --- a/feos-core/src/parameter/mod.rs +++ b/feos-core/src/parameter/mod.rs @@ -30,17 +30,16 @@ where Self: Sized, { type Pure: Clone + DeserializeOwned; - type IdealGas: Clone + DeserializeOwned; type Binary: Clone + DeserializeOwned + Default; /// Creates parameters from records for pure substances and possibly binary parameters. fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self; /// Creates parameters for a pure component from a pure record. - fn new_pure(pure_record: PureRecord) -> Self { + fn new_pure(pure_record: PureRecord) -> Self { let binary_record = Array2::from_elem([1, 1], Self::Binary::default()); Self::from_records(vec![pure_record], binary_record) } @@ -48,7 +47,7 @@ where /// Creates parameters for a binary system from pure records and an optional /// binary interaction parameter. fn new_binary( - pure_records: Vec>, + pure_records: Vec>, binary_record: Option, ) -> Self { let binary_record = Array2::from_shape_fn([2, 2], |(i, j)| { @@ -63,12 +62,7 @@ where /// Return the original pure and binary records that were used to construct the parameters. #[allow(clippy::type_complexity)] - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ); + fn records(&self) -> (&[PureRecord], &Array2); /// Helper function to build matrix from list of records in correct order. /// @@ -76,7 +70,7 @@ where /// `pure_records`, the `Default` implementation of Self::Binary is used. #[allow(clippy::expect_fun_call)] fn binary_matrix_from_records( - pure_records: &Vec>, + pure_records: &Vec>, binary_records: &[BinaryRecord], search_option: IdentifierOption, ) -> Array2 { @@ -138,8 +132,7 @@ where P: AsRef, { let mut queried: IndexSet = IndexSet::new(); - let mut record_map: HashMap> = - HashMap::new(); + let mut record_map: HashMap> = HashMap::new(); for (substances, file) in input { substances.iter().try_for_each(|identifier| { @@ -154,8 +147,7 @@ where let f = File::open(file)?; let reader = BufReader::new(f); - let pure_records: Vec> = - serde_json::from_reader(reader)?; + let pure_records: Vec> = serde_json::from_reader(reader)?; pure_records .into_iter() @@ -202,12 +194,11 @@ where /// and the ideal gas record. fn from_segments( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result where Self::Pure: FromSegments, - Self::IdealGas: FromSegments, Self::Binary: FromSegmentsBinary, { // update the pure records with model and ideal gas records @@ -276,7 +267,6 @@ where where P: AsRef, Self::Pure: FromSegments, - Self::IdealGas: FromSegments, Self::Binary: FromSegmentsBinary, { let queried: IndexSet = substances @@ -315,7 +305,7 @@ where .collect(); // Read segment records - let segment_records: Vec> = + let segment_records: Vec> = SegmentRecord::from_json(file_segments)?; // Read binary records @@ -353,13 +343,12 @@ where pub trait ParameterHetero: Sized { type Chemical: Clone; type Pure: Clone + DeserializeOwned; - type IdealGas: Clone + DeserializeOwned; type Binary: Clone + DeserializeOwned; /// Creates parameters from the molecular structure and segment information. fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result; @@ -369,7 +358,7 @@ pub trait ParameterHetero: Sized { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ); @@ -419,7 +408,7 @@ pub trait ParameterHetero: Sized { .collect(); // Read segment records - let segment_records: Vec> = + let segment_records: Vec> = SegmentRecord::from_json(file_segments)?; // Read binary records @@ -473,7 +462,6 @@ pub enum ParameterError { #[cfg(test)] mod test { use super::*; - use crate::joback::JobackRecord; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -495,16 +483,15 @@ mod test { } struct MyParameter { - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, } impl Parameter for MyParameter { type Pure = MyPureModel; - type IdealGas = JobackRecord; type Binary = MyBinaryModel; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { Self { @@ -513,12 +500,7 @@ mod test { } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } diff --git a/feos-core/src/parameter/model_record.rs b/feos-core/src/parameter/model_record.rs index 382b68936..ee86f61dc 100644 --- a/feos-core/src/parameter/model_record.rs +++ b/feos-core/src/parameter/model_record.rs @@ -10,28 +10,19 @@ use std::path::Path; /// A collection of parameters of a pure substance. #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PureRecord { +pub struct PureRecord { pub identifier: Identifier, pub molarweight: f64, pub model_record: M, - #[serde(default = "Default::default")] - #[serde(skip_serializing_if = "Option::is_none")] - pub ideal_gas_record: Option, } -impl PureRecord { +impl PureRecord { /// Create a new `PureRecord`. - pub fn new( - identifier: Identifier, - molarweight: f64, - model_record: M, - ideal_gas_record: Option, - ) -> Self { + pub fn new(identifier: Identifier, molarweight: f64, model_record: M) -> Self { Self { identifier, molarweight, model_record, - ideal_gas_record, } } @@ -43,47 +34,29 @@ impl PureRecord { where T: Copy + ValueInto, M: FromSegments, - I: FromSegments, - S: IntoIterator, T)>, + S: IntoIterator, T)>, { let mut molarweight = 0.0; let mut model_segments = Vec::new(); - let mut ideal_gas_segments = Vec::new(); for (s, n) in segments { molarweight += s.molarweight * n.value_into().unwrap(); model_segments.push((s.model_record, n)); - ideal_gas_segments.push(s.ideal_gas_record.map(|ig| (ig, n))); } let model_record = M::from_segments(&model_segments)?; - let ideal_gas_segments: Option> = ideal_gas_segments.into_iter().collect(); - let ideal_gas_record = ideal_gas_segments - .as_deref() - .map(I::from_segments) - .transpose()?; - - Ok(Self::new( - identifier, - molarweight, - model_record, - ideal_gas_record, - )) + Ok(Self::new(identifier, molarweight, model_record)) } } -impl std::fmt::Display for PureRecord +impl std::fmt::Display for PureRecord where M: std::fmt::Display, - I: std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PureRecord(")?; write!(f, "\n\tidentifier={},", self.identifier)?; write!(f, "\n\tmolarweight={},", self.molarweight)?; write!(f, "\n\tmodel_record={},", self.model_record)?; - if let Some(i) = self.ideal_gas_record.as_ref() { - write!(f, "\n\tideal_gas_record={},", i)?; - } write!(f, "\n)") } } @@ -153,7 +126,6 @@ where #[cfg(test)] mod test { use super::*; - use crate::joback::JobackRecord; #[derive(Serialize, Deserialize, Debug, Default, Clone)] struct TestModelRecordSegments { @@ -173,7 +145,7 @@ mod test { } } "#; - let record: PureRecord = + let record: PureRecord = serde_json::from_str(r).expect("Unable to parse json."); assert_eq!(record.identifier.cas, Some("123-4-5".into())) } @@ -201,7 +173,7 @@ mod test { } } ]"#; - let records: Vec> = + let records: Vec> = serde_json::from_str(r).expect("Unable to parse json."); assert_eq!(records[0].identifier.cas, Some("1".into())); assert_eq!(records[1].identifier.cas, Some("2".into())) diff --git a/feos-core/src/parameter/segment.rs b/feos-core/src/parameter/segment.rs index f87b7a8d7..f7718c054 100644 --- a/feos-core/src/parameter/segment.rs +++ b/feos-core/src/parameter/segment.rs @@ -8,60 +8,49 @@ use std::path::Path; /// Parameters describing an individual segment of a molecule. #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct SegmentRecord { +pub struct SegmentRecord { pub identifier: String, pub molarweight: f64, pub model_record: M, - pub ideal_gas_record: Option, } -impl SegmentRecord { +impl SegmentRecord { /// Creates a new `SegmentRecord`. - pub fn new( - identifier: String, - molarweight: f64, - model_record: M, - ideal_gas_record: Option, - ) -> Self { + pub fn new(identifier: String, molarweight: f64, model_record: M) -> Self { Self { identifier, molarweight, model_record, - ideal_gas_record, } } /// Read a list of `SegmentRecord`s from a JSON file. pub fn from_json>(file: P) -> Result, ParameterError> where - I: DeserializeOwned, M: DeserializeOwned, { Ok(serde_json::from_reader(BufReader::new(File::open(file)?))?) } } -impl Hash for SegmentRecord { +impl Hash for SegmentRecord { fn hash(&self, state: &mut H) { self.identifier.hash(state); } } -impl PartialEq for SegmentRecord { +impl PartialEq for SegmentRecord { fn eq(&self, other: &Self) -> bool { self.identifier == other.identifier } } -impl Eq for SegmentRecord {} +impl Eq for SegmentRecord {} -impl std::fmt::Display for SegmentRecord { +impl std::fmt::Display for SegmentRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SegmentRecord(\n\tidentifier={}", self.identifier)?; write!(f, "\n\tmolarweight={}", self.molarweight)?; write!(f, "\n\tmodel_record={}", self.model_record)?; - if let Some(i) = self.ideal_gas_record.as_ref() { - write!(f, "\n\tideal_gas_record={},", i)?; - } write!(f, "\n)") } } diff --git a/feos-core/src/phase_equilibria/bubble_dew.rs b/feos-core/src/phase_equilibria/bubble_dew.rs index e64920c8e..ee9a133db 100644 --- a/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/feos-core/src/phase_equilibria/bubble_dew.rs @@ -1,12 +1,12 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{ Contributions, DensityInitialization::{InitialDensity, Liquid, Vapor}, State, StateBuilder, TPSpec, }; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::{norm, LU}; use quantity::si::{SIArray1, SINumber, SIUnit}; @@ -55,7 +55,7 @@ where } /// # Bubble and dew point calculations -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate a phase equilibrium for a given temperature /// or pressure and composition of the liquid phase. pub fn bubble_point( @@ -225,9 +225,9 @@ impl PhaseEquilibrium { let m = liquid_molefracs * SIUnit::reference_moles(); let density = 0.75 * eos.max_density(Some(&m))?; let liquid = State::new_nvt(eos, temperature, m.sum() / density, &m)?; - let v_l = liquid.partial_molar_volume(Contributions::Total); + let v_l = liquid.partial_molar_volume(); let p_l = liquid.pressure(Contributions::Total); - let mu_l = liquid.chemical_potential(Contributions::ResidualNvt); + let mu_l = liquid.residual_chemical_potential(); let p_i = (temperature * density * SIUnit::gas_constant() * liquid_molefracs) * (mu_l - p_l * v_l) .to_reduced(SIUnit::gas_constant() * temperature)? @@ -251,9 +251,9 @@ impl PhaseEquilibrium { let m = x * SIUnit::reference_moles(); let density = 0.75 * eos.max_density(Some(&m))?; let liquid = State::new_nvt(eos, temperature, m.sum() / density, &m)?; - let v_l = liquid.partial_molar_volume(Contributions::Total); + let v_l = liquid.partial_molar_volume(); let p_l = liquid.pressure(Contributions::Total); - let mu_l = liquid.chemical_potential(Contributions::ResidualNvt); + let mu_l = liquid.residual_chemical_potential(); let k = vapor_molefracs / (mu_l - p_l * v_l) .to_reduced(SIUnit::gas_constant() * temperature)? @@ -287,7 +287,7 @@ impl PhaseEquilibrium { } } -fn starting_x2_bubble( +fn starting_x2_bubble( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -315,7 +315,7 @@ fn starting_x2_bubble( Ok([liquid_state, vapor_state]) } -fn starting_x2_dew( +fn starting_x2_dew( eos: &Arc, temperature: SINumber, pressure: SINumber, @@ -353,7 +353,7 @@ fn starting_x2_dew( Ok([vapor_state, liquid_state]) } -fn bubble_dew( +fn bubble_dew( tp_spec: TPSpec, mut var_tp: TPSpec, mut state1: State, @@ -441,7 +441,7 @@ where } } -fn adjust_t_p( +fn adjust_t_p( var: &mut TPSpec, state1: &mut State, state2: &mut State, @@ -509,7 +509,7 @@ where Ok(f.abs()) } -fn adjust_states( +fn adjust_states( var: &TPSpec, state1: &mut State, state2: &mut State, @@ -536,7 +536,7 @@ fn adjust_states( Ok(()) } -fn adjust_x2( +fn adjust_x2( state1: &State, state2: &mut State, verbosity: Verbosity, @@ -558,7 +558,7 @@ fn adjust_x2( Ok(err_out) } -fn newton_step( +fn newton_step( tp_spec: TPSpec, var: &mut TPSpec, state1: &mut State, @@ -574,7 +574,7 @@ where } } -fn newton_step_t( +fn newton_step_t( pressure: &mut TPSpec, state1: &mut State, state2: &mut State, @@ -593,11 +593,11 @@ where .dot(&state1.molefracs); let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_1 = state1 - .chemical_potential(Contributions::Total) + let mu_1_res = state1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_2 = state2 - .chemical_potential(Contributions::Total) + let mu_2_res = state2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_1 = state1 .pressure(Contributions::Total) @@ -607,7 +607,12 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual - let res = concatenate![Axis(0), mu_1 - &mu_2, arr1(&[p_1 - p_2])]; + let dmu_ig = (SIUnit::gas_constant() * state1.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&state1.partial_density / &state2.partial_density) + .into_value()? + .mapv(f64::ln); + let res = concatenate![Axis(0), mu_1_res - mu_2_res + dmu_ig, arr1(&[p_1 - p_2])]; let error = norm(&res); // calculate Jacobian @@ -651,7 +656,7 @@ where Ok(error) } -fn newton_step_p( +fn newton_step_p( pressure: SINumber, temperature: &mut TPSpec, state1: &mut State, @@ -666,12 +671,8 @@ where .dot(&state1.molefracs); let dmu_drho_2 = (state2.dmu_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let dmu_dt_1 = state1 - .dmu_dt(Contributions::Total) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_2 = state2 - .dmu_dt(Contributions::Total) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; + let dmu_res_dt_1 = state1.dmu_res_dt().to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_2 = state2.dmu_res_dt().to_reduced(SIUnit::gas_constant())?; let dp_drho_1 = (state1.dp_dni(Contributions::Total) * state1.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())? .dot(&state1.molefracs); @@ -683,11 +684,11 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_1 = state1 - .chemical_potential(Contributions::Total) + let mu_1_res = state1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_2 = state2 - .chemical_potential(Contributions::Total) + let mu_2_res = state2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_1 = state1 .pressure(Contributions::Total) @@ -698,7 +699,18 @@ where let p = pressure.to_reduced(SIUnit::reference_pressure())?; // calculate residual - let res = concatenate![Axis(0), mu_1 - &mu_2, arr1(&[p_1 - p]), arr1(&[p_2 - p])]; + let delta_dmu_ig_dt = (&state1.partial_density / &state2.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_mu_ig = (SIUnit::gas_constant() * state1.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_dmu_ig_dt; + let res = concatenate![ + Axis(0), + mu_1_res - mu_2_res + delta_mu_ig, + arr1(&[p_1 - p]), + arr1(&[p_2 - p]) + ]; let error = norm(&res); // calculate Jacobian @@ -717,7 +729,7 @@ where ], concatenate![ Axis(0), - (dmu_dt_1 - dmu_dt_2).insert_axis(Axis(1)), + (dmu_res_dt_1 - dmu_res_dt_2 + delta_dmu_ig_dt).insert_axis(Axis(1)), arr2(&[[dp_dt_1], [dp_dt_2]]) ] ]; diff --git a/feos-core/src/phase_equilibria/mod.rs b/feos-core/src/phase_equilibria/mod.rs index 7c020e352..7f8a27624 100644 --- a/feos-core/src/phase_equilibria/mod.rs +++ b/feos-core/src/phase_equilibria/mod.rs @@ -1,7 +1,7 @@ -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::state::{Contributions, DensityInitialization, State}; -use crate::EosUnit; +use crate::state::{DensityInitialization, State}; +use crate::{Contributions, EosUnit}; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::fmt; use std::fmt::Write; @@ -17,77 +17,6 @@ mod vle_pure; pub use phase_diagram_binary::PhaseDiagramHetero; pub use phase_diagram_pure::PhaseDiagram; -/// Level of detail in the iteration output. -#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] -#[cfg_attr(feature = "python", pyo3::pyclass)] -pub enum Verbosity { - /// Do not print output. - None, - /// Print information about the success of failure of the iteration. - Result, - /// Print a detailed outpur for every iteration. - Iter, -} - -impl Default for Verbosity { - fn default() -> Self { - Self::None - } -} - -/// Options for the various phase equilibria solvers. -/// -/// If the values are [None], solver specific default -/// values are used. -#[derive(Copy, Clone, Default)] -pub struct SolverOptions { - /// Maximum number of iterations. - pub max_iter: Option, - /// Tolerance. - pub tol: Option, - /// Iteration outpput indicated by the [Verbosity] enum. - pub verbosity: Verbosity, -} - -impl From<(Option, Option, Option)> for SolverOptions { - fn from(options: (Option, Option, Option)) -> Self { - Self { - max_iter: options.0, - tol: options.1, - verbosity: options.2.unwrap_or(Verbosity::None), - } - } -} - -impl SolverOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn max_iter(mut self, max_iter: usize) -> Self { - self.max_iter = Some(max_iter); - self - } - - pub fn tol(mut self, tol: f64) -> Self { - self.tol = Some(tol); - self - } - - pub fn verbosity(mut self, verbosity: Verbosity) -> Self { - self.verbosity = verbosity; - self - } - - pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) { - ( - self.max_iter.unwrap_or(max_iter), - self.tol.unwrap_or(tol), - self.verbosity, - ) - } -} - /// A thermodynamic equilibrium state. /// /// The struct is parametrized over the number of phases with most features @@ -109,11 +38,10 @@ impl Clone for PhaseEquilibrium { } } -impl fmt::Display for PhaseEquilibrium +impl fmt::Display for PhaseEquilibrium where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (i, s) in self.0.iter().enumerate() { @@ -123,11 +51,10 @@ where } } -impl PhaseEquilibrium +impl PhaseEquilibrium where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { pub fn _repr_markdown_(&self) -> String { if self.0[0].eos.components() == 1 { @@ -161,7 +88,7 @@ where } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub fn vapor(&self) -> &State { &self.0[0] } @@ -171,7 +98,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub fn vapor(&self) -> &State { &self.0[0] } @@ -185,7 +112,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn from_states(state1: State, state2: State) -> Self { let (vapor, liquid) = if state1.density < state2.density { (state1, state2) @@ -226,7 +153,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn update_pressure( mut self, temperature: SINumber, @@ -261,18 +188,21 @@ impl PhaseEquilibrium { Ok(()) } - pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { - for s in self.0.iter_mut() { - s.update_chemical_potential(chemical_potential)?; - } - Ok(()) - } - + // Total Gibbs energy excluding the constant contribution RT sum_i N_i ln(\Lambda_i^3) pub(super) fn total_gibbs_energy(&self) -> SINumber { self.0 .iter() .fold(0.0 * SIUnit::reference_energy(), |acc, s| { - acc + s.gibbs_energy(Contributions::Total) + let ln_rho = s + .partial_density + .to_reduced(SIUnit::reference_density()) + .unwrap() + .mapv(f64::ln); + acc + s.residual_helmholtz_energy() + + s.pressure(Contributions::Total) * s.volume + + SIUnit::gas_constant() + * s.temperature + * (s.moles.clone() * (ln_rho - 1.0)).sum() }) } } @@ -280,7 +210,7 @@ impl PhaseEquilibrium { const TRIVIAL_REL_DEVIATION: f64 = 1e-5; /// # Utility functions -impl PhaseEquilibrium { +impl PhaseEquilibrium { pub(super) fn check_trivial_solution(self) -> EosResult { if Self::is_trivial_solution(self.vapor(), self.liquid()) { Err(EosError::TrivialSolution) diff --git a/feos-core/src/phase_equilibria/phase_diagram_binary.rs b/feos-core/src/phase_equilibria/phase_diagram_binary.rs index 746fc5da0..d9ae810ce 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_binary.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_binary.rs @@ -1,8 +1,8 @@ -use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::{PhaseDiagram, PhaseEquilibrium}; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State, StateBuilder, TPSpec}; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions}; use ndarray::{arr1, arr2, concatenate, s, Array1, Array2, Axis}; use num_dual::linalg::{norm, LU}; use quantity::si::{SIArray1, SINumber, SIUnit}; @@ -11,7 +11,7 @@ use std::sync::Arc; const DEFAULT_POINTS: usize = 51; -impl PhaseDiagram { +impl PhaseDiagram { /// Create a new binary phase diagram exhibiting a /// vapor/liquid equilibrium. /// @@ -171,7 +171,7 @@ impl PhaseDiagram { } } -fn iterate_vle( +fn iterate_vle( eos: &Arc, tp: TPSpec, x_lim: &[f64], @@ -231,7 +231,7 @@ where vle_vec } -impl State { +impl State { fn tp(&self, tp: TPSpec) -> SINumber { match tp { TPSpec::Temperature(_) => self.pressure(Contributions::Total), @@ -247,7 +247,7 @@ pub struct PhaseDiagramHetero { pub lle: Option>, } -impl PhaseDiagram { +impl PhaseDiagram { /// Create a new binary phase diagram exhibiting a /// vapor/liquid/liquid equilibrium. /// @@ -346,7 +346,7 @@ const MAX_ITER_HETERO: usize = 50; const TOL_HETERO: f64 = 1e-8; /// # Heteroazeotropes -impl PhaseEquilibrium +impl PhaseEquilibrium where SINumber: std::fmt::Display + std::fmt::LowerExp, { @@ -421,14 +421,14 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; let dp_drho_v = (v.dp_dni(Contributions::Total) * v.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; - let mu_l1 = l1 - .chemical_potential(Contributions::Total) + let mu_l1_res = l1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_l2 = l2 - .chemical_potential(Contributions::Total) + let mu_l2_res = l2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_v = v - .chemical_potential(Contributions::Total) + let mu_v_res = v + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_l1 = l1 .pressure(Contributions::Total) @@ -441,10 +441,20 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual + let delta_l1v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&l1.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l2v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * (&l2.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); let res = concatenate![ Axis(0), - mu_l1 - &mu_v, - mu_l2 - &mu_v, + mu_l1_res - &mu_v_res + delta_l1v_mu_ig, + mu_l2_res - &mu_v_res + delta_l2v_mu_ig, arr1(&[p_l1 - p_v]), arr1(&[p_l2 - p_v]) ]; @@ -555,12 +565,9 @@ where .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; let dmu_drho_v = (v.dmu_dni(Contributions::Total) * v.volume) .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let dmu_dt_l1 = (l1.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_l2 = (l2.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; - let dmu_dt_v = (v.dmu_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_temperature())?; + let dmu_res_dt_l1 = (l1.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_l2 = (l2.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; + let dmu_res_dt_v = (v.dmu_res_dt()).to_reduced(SIUnit::gas_constant())?; let dp_drho_l1 = (l1.dp_dni(Contributions::Total) * l1.volume) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_density())?; let dp_drho_l2 = (l2.dp_dni(Contributions::Total) * l2.volume) @@ -573,14 +580,14 @@ where .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; let dp_dt_v = (v.dp_dt(Contributions::Total)) .to_reduced(SIUnit::reference_pressure() / SIUnit::reference_temperature())?; - let mu_l1 = l1 - .chemical_potential(Contributions::Total) + let mu_l1_res = l1 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_l2 = l2 - .chemical_potential(Contributions::Total) + let mu_l2_res = l2 + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; - let mu_v = v - .chemical_potential(Contributions::Total) + let mu_v_res = v + .residual_chemical_potential() .to_reduced(SIUnit::reference_molar_energy())?; let p_l1 = l1 .pressure(Contributions::Total) @@ -593,10 +600,22 @@ where .to_reduced(SIUnit::reference_pressure())?; // calculate residual + let delta_l1v_dmu_ig_dt = (&l1.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l2v_dmu_ig_dt = (&l2.partial_density / &v.partial_density) + .into_value()? + .mapv(f64::ln); + let delta_l1v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_l1v_dmu_ig_dt; + let delta_l2v_mu_ig = (SIUnit::gas_constant() * v.temperature) + .to_reduced(SIUnit::reference_molar_energy())? + * &delta_l2v_dmu_ig_dt; let res = concatenate![ Axis(0), - mu_l1 - &mu_v, - mu_l2 - &mu_v, + mu_l1_res - &mu_v_res + delta_l1v_mu_ig, + mu_l2_res - &mu_v_res + delta_l2v_mu_ig, arr1(&[p_l1 - p]), arr1(&[p_l2 - p]), arr1(&[p_v - p]) @@ -636,8 +655,8 @@ where ], concatenate![ Axis(0), - (dmu_dt_l1 - &dmu_dt_v).insert_axis(Axis(1)), - (dmu_dt_l2 - &dmu_dt_v).insert_axis(Axis(1)), + (dmu_res_dt_l1 - &dmu_res_dt_v + delta_l1v_dmu_ig_dt).insert_axis(Axis(1)), + (dmu_res_dt_l2 - &dmu_res_dt_v + delta_l2v_dmu_ig_dt).insert_axis(Axis(1)), arr2(&[[dp_dt_l1]]), arr2(&[[dp_dt_l2]]), arr2(&[[dp_dt_v]]) diff --git a/feos-core/src/phase_equilibria/phase_diagram_pure.rs b/feos-core/src/phase_equilibria/phase_diagram_pure.rs index c7b823fed..1a25d8ac9 100644 --- a/feos-core/src/phase_equilibria/phase_diagram_pure.rs +++ b/feos-core/src/phase_equilibria/phase_diagram_pure.rs @@ -1,9 +1,10 @@ -use super::{PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::EosResult; use crate::state::{State, StateVec}; #[cfg(feature = "rayon")] use crate::EosUnit; +use crate::SolverOptions; #[cfg(feature = "rayon")] use ndarray::{Array1, ArrayView1, Axis}; #[cfg(feature = "rayon")] @@ -33,7 +34,7 @@ impl PhaseDiagram { } } -impl PhaseDiagram { +impl PhaseDiagram { /// Calculate a phase diagram for a pure component. pub fn pure( eos: &Arc, @@ -74,7 +75,7 @@ impl PhaseDiagram { } #[cfg(feature = "rayon")] -impl PhaseDiagram { +impl PhaseDiagram { fn solve_temperatures( eos: &Arc, temperatures: ArrayView1, diff --git a/feos-core/src/phase_equilibria/phase_envelope.rs b/feos-core/src/phase_equilibria/phase_envelope.rs index f05ed3afe..9161eb75e 100644 --- a/feos-core/src/phase_equilibria/phase_envelope.rs +++ b/feos-core/src/phase_equilibria/phase_envelope.rs @@ -1,12 +1,12 @@ -use super::{PhaseDiagram, PhaseEquilibrium, SolverOptions}; -use crate::equation_of_state::EquationOfState; +use super::{PhaseDiagram, PhaseEquilibrium}; +use crate::equation_of_state::Residual; use crate::errors::EosResult; -use crate::state::State; -use crate::Contributions; +use crate::state::{Contributions, State}; +use crate::SolverOptions; use quantity::si::{SIArray1, SINumber}; use std::sync::Arc; -impl PhaseDiagram { +impl PhaseDiagram { /// Calculate the bubble point line of a mixture with given composition. pub fn bubble_point_line( eos: &Arc, diff --git a/feos-core/src/phase_equilibria/stability_analysis.rs b/feos-core/src/phase_equilibria/stability_analysis.rs index 47da90f3f..c0df11f98 100644 --- a/feos-core/src/phase_equilibria/stability_analysis.rs +++ b/feos-core/src/phase_equilibria/stability_analysis.rs @@ -1,8 +1,8 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State}; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::smallest_ev; use num_dual::linalg::LU; @@ -18,7 +18,7 @@ const MINIMIZE_KMAX: usize = 100; const ZERO_TPD: f64 = -1E-08; /// # Stability analysis -impl State { +impl State { /// Determine if the state is stable, i.e. if a phase split should /// occur or not. pub fn is_stable(&self, options: SolverOptions) -> EosResult { diff --git a/feos-core/src/phase_equilibria/tp_flash.rs b/feos-core/src/phase_equilibria/tp_flash.rs index ef53cd189..41262bfd0 100644 --- a/feos-core/src/phase_equilibria/tp_flash.rs +++ b/feos-core/src/phase_equilibria/tp_flash.rs @@ -1,7 +1,8 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State}; +use crate::{SolverOptions, Verbosity}; use ndarray::*; use num_dual::linalg::norm; use quantity::si::{SIArray1, SINumber}; @@ -11,7 +12,7 @@ const MAX_ITER_TP: usize = 400; const TOL_TP: f64 = 1e-8; /// # Flash calculations -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Perform a Tp-flash calculation. If no initial values are /// given, the solution is initialized using a stability analysis. /// @@ -38,7 +39,7 @@ impl PhaseEquilibrium { } /// # Flash calculations -impl State { +impl State { /// Perform a Tp-flash calculation using the [State] as feed. /// If no initial values are given, the solution is initialized /// using a stability analysis. @@ -157,7 +158,7 @@ impl State { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { fn accelerated_successive_substitution( &mut self, feed_state: &State, diff --git a/feos-core/src/phase_equilibria/vle_pure.rs b/feos-core/src/phase_equilibria/vle_pure.rs index 7d89a62ed..eb3cd2ce5 100644 --- a/feos-core/src/phase_equilibria/vle_pure.rs +++ b/feos-core/src/phase_equilibria/vle_pure.rs @@ -1,8 +1,8 @@ -use super::{PhaseEquilibrium, SolverOptions, Verbosity}; -use crate::equation_of_state::EquationOfState; +use super::PhaseEquilibrium; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; use crate::state::{Contributions, DensityInitialization, State, TPSpec}; -use crate::EosUnit; +use crate::{EosUnit, SolverOptions, Verbosity}; use ndarray::{arr1, Array1}; use quantity::si::{SINumber, SIUnit}; use std::convert::TryFrom; @@ -13,7 +13,7 @@ const MAX_ITER_PURE: usize = 50; const TOL_PURE: f64 = 1e-12; /// # Pure component phase equilibria -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate a phase equilibrium for a pure component. pub fn pure( eos: &Arc, @@ -83,21 +83,21 @@ impl PhaseEquilibrium { let (p_l, p_rho_l) = liquid.p_dpdrho(); let (p_v, p_rho_v) = vapor.p_dpdrho(); // calculate the molar Helmholtz energies (already cached) - let a_l = liquid.molar_helmholtz_energy(Contributions::Total); - let a_v = vapor.molar_helmholtz_energy(Contributions::Total); + let a_l_res = liquid.residual_helmholtz_energy() / liquid.total_moles; + let a_v_res = vapor.residual_helmholtz_energy() / vapor.total_moles; // Estimate the new pressure + let kt = SIUnit::gas_constant() * vapor.temperature; let delta_v = 1.0 / vapor.density - 1.0 / liquid.density; - let delta_a = a_v - a_l; + let delta_a = a_v_res - a_l_res + kt * vapor.density.to_reduced(liquid.density)?.ln(); let mut p_new = -delta_a / delta_v; // If the pressure becomes negative, assume the gas phase is ideal. The // resulting pressure is always positive. if p_new.is_sign_negative() { - let mu_v = vapor.chemical_potential(Contributions::Total).get(0); p_new = p_v - * (a_l - mu_v) - .to_reduced(vapor.temperature * SIUnit::gas_constant())? + * (-delta_a - p_v * vapor.volume / vapor.total_moles) + .to_reduced(kt)? .exp(); } @@ -193,20 +193,23 @@ impl PhaseEquilibrium { let p_t_l = vle.liquid().dp_dt(Contributions::Total); let p_t_v = vle.vapor().dp_dt(Contributions::Total); - // calculate the molar entropies (already cached) - let s_l = vle.liquid().molar_entropy(Contributions::Total); - let s_v = vle.vapor().molar_entropy(Contributions::Total); + // calculate the residual molar entropies (already cached) + let s_l_res = vle.liquid().residual_entropy() / vle.liquid().total_moles; + let s_v_res = vle.vapor().residual_entropy() / vle.vapor().total_moles; - // calculate the molar Helmholtz energies (already cached) - let a_l = vle.liquid().molar_helmholtz_energy(Contributions::Total); - let a_v = vle.vapor().molar_helmholtz_energy(Contributions::Total); + // calculate the residual molar Helmholtz energies (already cached) + let a_l_res = vle.liquid().residual_helmholtz_energy() / vle.liquid().total_moles; + let a_v_res = vle.vapor().residual_helmholtz_energy() / vle.vapor().total_moles; // calculate the molar volumes let v_l = 1.0 / vle.liquid().density; let v_v = 1.0 / vle.vapor().density; // estimate the temperature steps - let delta_t = (pressure * (v_v - v_l) + (a_v - a_l)) / (s_v - s_l); + let kt = SIUnit::gas_constant() * vle.vapor().temperature; + let ln_rho = v_l.to_reduced(v_v)?.ln(); + let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + kt * ln_rho)) + / (s_v_res - s_l_res - SIUnit::gas_constant() * ln_rho); let t_new = vle.vapor().temperature + delta_t; // calculate Newton steps for the densities and update state. @@ -319,10 +322,15 @@ impl PhaseEquilibrium { } for _ in 0..20 { - t0 = (e.vapor().enthalpy(Contributions::Total) - - e.liquid().enthalpy(Contributions::Total)) - / (e.vapor().entropy(Contributions::Total) - - e.liquid().entropy(Contributions::Total)); + let h = |s: &State<_>| { + s.residual_enthalpy() + s.total_moles * SIUnit::gas_constant() * s.temperature + }; + t0 = (h(e.vapor()) - h(e.liquid())) + / (e.vapor().residual_entropy() + - e.liquid().residual_entropy() + - SIUnit::gas_constant() + * e.vapor().total_moles + * (e.vapor().density.to_reduced(e.liquid().density)?.ln())); let trial_state = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?; if trial_state.density < cp.density { @@ -346,7 +354,7 @@ impl PhaseEquilibrium { } } -impl PhaseEquilibrium { +impl PhaseEquilibrium { /// Calculate the pure component vapor pressures of all /// components in the system for the given temperature. pub fn vapor_pressure(eos: &Arc, temperature: SINumber) -> Vec> { diff --git a/feos-core/src/python/cubic.rs b/feos-core/src/python/cubic.rs index 80a82b3f7..1973a1bf1 100644 --- a/feos-core/src/python/cubic.rs +++ b/feos-core/src/python/cubic.rs @@ -1,9 +1,7 @@ use crate::cubic::{PengRobinsonParameters, PengRobinsonRecord}; -use crate::joback::JobackRecord; use crate::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, }; -use crate::python::joback::PyJobackRecord; use crate::python::parameter::PyIdentifier; use crate::*; use ndarray::Array2; @@ -32,12 +30,7 @@ impl PyPengRobinsonRecord { impl_json_handling!(PyPengRobinsonRecord); -impl_pure_record!( - PengRobinsonRecord, - PyPengRobinsonRecord, - JobackRecord, - PyJobackRecord -); +impl_pure_record!(PengRobinsonRecord, PyPengRobinsonRecord); impl_binary_record!(); diff --git a/feos-core/src/python/joback.rs b/feos-core/src/python/joback.rs index d6e670ed8..c54257b6c 100644 --- a/feos-core/src/python/joback.rs +++ b/feos-core/src/python/joback.rs @@ -1,7 +1,17 @@ -use crate::impl_json_handling; -use crate::joback::JobackRecord; -use crate::parameter::ParameterError; +use std::sync::Arc; + +use crate::joback::{JobackBinaryRecord, JobackParameters, JobackRecord}; +use crate::parameter::*; +use crate::python::parameter::*; +use crate::{ + impl_binary_record, impl_json_handling, impl_parameter, impl_parameter_from_segments, + impl_pure_record, impl_segment_record, +}; +use ndarray::Array2; +use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; +use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; +use std::convert::{TryFrom, TryInto}; /// Create a set of Joback ideal gas heat capacity parameters /// for a segment or a pure component. @@ -44,3 +54,41 @@ impl PyJobackRecord { } impl_json_handling!(PyJobackRecord); +impl_pure_record!(JobackRecord, PyJobackRecord); +impl_segment_record!(JobackRecord, PyJobackRecord); + +#[pyclass(name = "JobackBinaryRecord")] +#[derive(Clone)] +pub struct PyJobackBinaryRecord(pub JobackBinaryRecord); + +impl_binary_record!(JobackBinaryRecord, PyJobackBinaryRecord); +/// Create a set of Joback parameters from records. +/// +/// Parameters +/// ---------- +/// pure_records : List[PureRecord] +/// pure substance records. +/// substances : List[str], optional +/// The substances to use. Filters substances from `pure_records` according to +/// `search_option`. +/// When not provided, all entries of `pure_records` are used. +/// search_option : {'Name', 'Cas', 'Inchi', 'IupacName', 'Formula', 'Smiles'}, optional, defaults to 'Name'. +/// Identifier that is used to search substance. +/// +/// Returns +/// ------- +/// JobackParameters +#[pyclass(name = "JobackParameters")] +#[pyo3(text_signature = "(pure_records, substances=None, search_option='Name')")] +#[derive(Clone)] +pub struct PyJobackParameters(pub Arc); + +impl_parameter!(JobackParameters, PyJobackParameters); +impl_parameter_from_segments!(JobackParameters, PyJobackParameters); + +#[pymethods] +impl PyJobackParameters { + // fn _repr_markdown_(&self) -> String { + // self.0.to_markdown() + // } +} diff --git a/feos-core/src/python/parameter.rs b/feos-core/src/python/parameter.rs index d0a55ad2a..9c0e80e94 100644 --- a/feos-core/src/python/parameter.rs +++ b/feos-core/src/python/parameter.rs @@ -370,7 +370,7 @@ impl_json_handling!(PyBinarySegmentRecord); #[macro_export] macro_rules! impl_pure_record { - ($model_record:ident, $py_model_record:ident, $ideal_gas_record:ident, $py_ideal_gas_record:ident) => { + ($model_record:ident, $py_model_record:ident) => { /// All information required to characterize a pure component. /// /// Parameters @@ -381,16 +381,14 @@ macro_rules! impl_pure_record { /// The molar weight (in g/mol) of the pure component. /// model_record : ModelRecord /// The pure component model parameters. - /// ideal_gas_record: IdealGasRecord, optional - /// The pure component parameters for the ideal gas model. /// /// Returns /// ------- /// PureRecord #[pyclass(name = "PureRecord")] - #[pyo3(text_signature = "(identifier, molarweight, model_record, ideal_gas_record=None)")] + #[pyo3(text_signature = "(identifier, molarweight, model_record)")] #[derive(Clone)] - pub struct PyPureRecord(pub PureRecord<$model_record, $ideal_gas_record>); + pub struct PyPureRecord(pub PureRecord<$model_record>); #[pymethods] impl PyPureRecord { @@ -399,13 +397,11 @@ macro_rules! impl_pure_record { identifier: PyIdentifier, molarweight: f64, model_record: $py_model_record, - ideal_gas_record: Option<$py_ideal_gas_record>, ) -> PyResult { Ok(Self(PureRecord::new( identifier.0, molarweight, model_record.0, - ideal_gas_record.map(|ig| ig.0), ))) } @@ -439,16 +435,6 @@ macro_rules! impl_pure_record { self.0.model_record = model_record.0; } - #[getter] - fn get_ideal_gas_record(&self) -> Option<$py_ideal_gas_record> { - self.0.ideal_gas_record.clone().map($py_ideal_gas_record) - } - - #[setter] - fn set_ideal_gas_record(&mut self, ideal_gas_record: $py_ideal_gas_record) { - self.0.ideal_gas_record = Some(ideal_gas_record.0); - } - fn __repr__(&self) -> PyResult { Ok(self.0.to_string()) } @@ -460,7 +446,7 @@ macro_rules! impl_pure_record { #[macro_export] macro_rules! impl_segment_record { - ($model_record:ident, $py_model_record:ident, $ideal_gas_record:ident, $py_ideal_gas_record:ident) => { + ($model_record:ident, $py_model_record:ident) => { /// All information required to characterize a single segment. /// /// Parameters @@ -471,16 +457,14 @@ macro_rules! impl_segment_record { /// The molar weight (in g/mol) of the segment. /// model_record : ModelRecord /// The segment model parameters. - /// ideal_gas_record: IdealGasRecord, optional - /// The segment ideal gas parameters. /// /// Returns /// ------- /// SegmentRecord #[pyclass(name = "SegmentRecord")] - #[pyo3(text_signature = "(identifier, molarweight, model_record, ideal_gas_record=None)")] + #[pyo3(text_signature = "(identifier, molarweight)")] #[derive(Clone)] - pub struct PySegmentRecord(SegmentRecord<$model_record, $ideal_gas_record>); + pub struct PySegmentRecord(SegmentRecord<$model_record>); #[pymethods] impl PySegmentRecord { @@ -489,13 +473,11 @@ macro_rules! impl_segment_record { identifier: String, molarweight: f64, model_record: $py_model_record, - ideal_gas_record: Option<$py_ideal_gas_record>, ) -> PyResult { Ok(Self(SegmentRecord::new( identifier, molarweight, model_record.0, - ideal_gas_record.map(|ig| ig.0), ))) } @@ -547,16 +529,6 @@ macro_rules! impl_segment_record { self.0.model_record = model_record.0; } - #[getter] - fn get_ideal_gas_record(&self) -> Option<$py_ideal_gas_record> { - self.0.ideal_gas_record.clone().map($py_ideal_gas_record) - } - - #[setter] - fn set_ideal_gas_record(&mut self, ideal_gas_record: $py_ideal_gas_record) { - self.0.ideal_gas_record = Some(ideal_gas_record.0); - } - fn __repr__(&self) -> PyResult { Ok(self.0.to_string()) } diff --git a/feos-core/src/python/phase_equilibria.rs b/feos-core/src/python/phase_equilibria.rs index 72951802d..c988a429f 100644 --- a/feos-core/src/python/phase_equilibria.rs +++ b/feos-core/src/python/phase_equilibria.rs @@ -237,20 +237,6 @@ macro_rules! impl_phase_equilibrium { PyState(self.0.liquid().clone()) } - /// Calculate a new PhaseEquilibrium with the given chemical potential. - /// The temperature remains constant, but the states are not in - /// a mechanical equilibrium anymore. - /// - /// Parameters - /// ---------- - /// chemical_potential: SIArray1 - /// The new chemical potential - /// - fn update_chemical_potential(slf: &PyCell, chemical_potential: &PySIArray1) -> PyResult<()> { - slf.borrow_mut().0.update_chemical_potential(chemical_potential)?; - Ok(()) - } - /// Calculate the pure component vapor-liquid equilibria for all /// components in the system. /// diff --git a/feos-core/src/python/state.rs b/feos-core/src/python/state.rs index 9e0045cea..59644acdd 100644 --- a/feos-core/src/python/state.rs +++ b/feos-core/src/python/state.rs @@ -90,7 +90,7 @@ macro_rules! impl_state { } else { Ok(DensityInitialization::None) }; - let s = State::new( + let s = State::new_full( &eos.0, temperature.map(|t| t.into()), volume.map(|t| t.into()), @@ -459,18 +459,11 @@ macro_rules! impl_state { /// Return partial molar volume of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_volume(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_volume(contributions)) + fn partial_molar_volume(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_volume()) } /// Return chemical potential of each component. @@ -703,18 +696,11 @@ macro_rules! impl_state { /// Return partial molar entropy of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_entropy(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_entropy(contributions)) + fn partial_molar_entropy(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_entropy()) } /// Return enthalpy. @@ -752,18 +738,11 @@ macro_rules! impl_state { /// Return partial molar enthalpy of each component. /// - /// Parameters - /// ---------- - /// contributions: Contributions, optional - /// the contributions of the helmholtz energy. - /// Defaults to Contributions.Total. - /// /// Returns /// ------- /// SIArray1 - #[pyo3(signature = (contributions=Contributions::Total), text_signature = "($self, contributions)")] - fn partial_molar_enthalpy(&self, contributions: Contributions) -> PySIArray1 { - PySIArray1::from(self.0.partial_molar_enthalpy(contributions)) + fn partial_molar_enthalpy(&self) -> PySIArray1 { + PySIArray1::from(self.0.partial_molar_enthalpy()) } /// Return helmholtz_energy. diff --git a/feos-core/src/python/user_defined.rs b/feos-core/src/python/user_defined.rs index ace9713ac..5e5a97ac5 100644 --- a/feos-core/src/python/user_defined.rs +++ b/feos-core/src/python/user_defined.rs @@ -1,8 +1,11 @@ -use crate::{EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, MolarWeight, StateHD}; +use crate::{ + Components, DeBroglieWavelength, DeBroglieWavelengthDual, HelmholtzEnergy, HelmholtzEnergyDual, + IdealGas, MolarWeight, Residual, StateHD, +}; use ndarray::Array1; use num_dual::*; use numpy::convert::IntoPyArray; -use numpy::{PyArray, PyReadonlyArrayDyn}; +use numpy::{PyArray, PyReadonlyArray1, PyReadonlyArrayDyn}; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use quantity::python::PySIArray1; @@ -11,13 +14,79 @@ use std::fmt; struct PyHelmholtzEnergy(Py); +pub struct PyIdealGas(Py); + +impl PyIdealGas { + pub fn new(obj: Py) -> PyResult { + Python::with_gil(|py| { + let attr = obj.as_ref(py).hasattr("components")?; + if !attr { + panic!("Python Class has to have a method 'components' with signature:\n\tdef signature(self) -> int") + } + let attr = obj.as_ref(py).hasattr("subset")?; + if !attr { + panic!("Python Class has to have a method 'subset' with signature:\n\tdef subset(self, component_list: List[int]) -> Self") + } + let attr = obj.as_ref(py).hasattr("ln_lambda3")?; + if !attr { + panic!("{}", "Python Class has to have a method 'ln_lambda3' with signature:\n\tdef ln_lambda3(self, temperature: HD) -> HD\nwhere 'HD' has to be any (hyper-) dual number.") + } + Ok(Self(obj)) + }) + } +} + +impl Components for PyIdealGas { + fn components(&self) -> usize { + Python::with_gil(|py| { + let py_result = self.0.as_ref(py).call_method0("components").unwrap(); + if py_result.get_type().name().unwrap() != "int" { + panic!( + "Expected an integer for the components() method signature, got {}", + py_result.get_type().name().unwrap() + ); + } + py_result.extract().unwrap() + }) + } + + fn subset(&self, component_list: &[usize]) -> Self { + Python::with_gil(|py| { + let py_result = self + .0 + .as_ref(py) + .call_method1("subset", (component_list.to_vec(),)) + .unwrap(); + Self::new(py_result.extract().unwrap()).unwrap() + }) + } +} + +impl IdealGas for PyIdealGas { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self + } +} + +impl fmt::Display for PyIdealGas { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ideal gas (Python)") + } +} + /// Struct containing pointer to Python Class that implements Helmholtz energy. -pub struct PyEoSObj { +pub struct PyResidual { obj: Py, contributions: Vec>, } -impl PyEoSObj { +impl fmt::Display for PyResidual { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Python residual") + } +} + +impl PyResidual { pub fn new(obj: Py) -> PyResult { Python::with_gil(|py| { let attr = obj.as_ref(py).hasattr("components")?; @@ -48,7 +117,7 @@ impl PyEoSObj { } } -impl MolarWeight for PyEoSObj { +impl MolarWeight for PyResidual { fn molar_weight(&self) -> SIArray1 { Python::with_gil(|py| { let py_result = self.obj.as_ref(py).call_method0("molar_weight").unwrap(); @@ -63,7 +132,7 @@ impl MolarWeight for PyEoSObj { } } -impl EquationOfState for PyEoSObj { +impl Components for PyResidual { fn components(&self) -> usize { Python::with_gil(|py| { let py_result = self.obj.as_ref(py).call_method0("components").unwrap(); @@ -87,7 +156,9 @@ impl EquationOfState for PyEoSObj { Self::new(py_result.extract().unwrap()).unwrap() }) } +} +impl Residual for PyResidual { fn compute_max_density(&self, moles: &Array1) -> f64 { Python::with_gil(|py| { let py_result = self @@ -99,7 +170,7 @@ impl EquationOfState for PyEoSObj { }) } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -192,17 +263,48 @@ macro_rules! helmholtz_energy { }; } +macro_rules! de_broglie_wavelength { + ($py_hd_id:ident, $hd_ty:ty) => { + impl DeBroglieWavelengthDual<$hd_ty> for PyIdealGas { + fn ln_lambda3(&self, temperature: $hd_ty) -> Array1<$hd_ty> { + Python::with_gil(|py| { + let py_result = self + .0 + .as_ref(py) + .call_method1("ln_lambda3", (<$py_hd_id>::from(temperature),)) + .unwrap(); + + // f64 + let rr = if let Ok(r) = py_result.extract::>() { + r.to_owned_array() + .mapv(|ri| <$hd_ty>::from(ri)) + // anything but f64 + } else if let Ok(r) = py_result.extract::>() { + r.to_owned_array() + .mapv(|ri| <$hd_ty>::from(ri.extract::<$py_hd_id>(py).unwrap())) + } else { + panic!("ln_lambda3: data type of result must be one-dimensional numpy ndarray") + }; + rr + }) + } + } + }; +} + macro_rules! impl_dual_state_helmholtz_energy { ($py_state_id:ident, $py_hd_id:ident, $hd_ty:ty, $py_field_ty:ty) => { dual_number!($py_hd_id, $hd_ty, $py_field_ty); state!($py_state_id, $py_hd_id, $hd_ty); helmholtz_energy!($py_state_id, $py_hd_id, $hd_ty); + de_broglie_wavelength!($py_hd_id, $hd_ty); }; } // No definition of dual number necessary for f64 state!(PyStateF, f64, f64); helmholtz_energy!(PyStateF, f64, f64); +de_broglie_wavelength!(f64, f64); impl_dual_state_helmholtz_energy!(PyStateD, PyDual64, Dual64, f64); dual_number!(PyDualVec3, DualSVec64<3>, f64); diff --git a/feos-core/src/state/builder.rs b/feos-core/src/state/builder.rs index 40406862b..b9a598b4c 100644 --- a/feos-core/src/state/builder.rs +++ b/feos-core/src/state/builder.rs @@ -1,5 +1,5 @@ use super::{DensityInitialization, State}; -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::{IdealGas, Residual}; use crate::errors::EosResult; use ndarray::Array1; use quantity::si::{SIArray1, SINumber}; @@ -52,7 +52,7 @@ use std::sync::Arc; /// # Ok(()) /// # } /// ``` -pub struct StateBuilder<'a, E: EquationOfState> { +pub struct StateBuilder<'a, E, const IG: bool> { eos: Arc, temperature: Option, volume: Option, @@ -69,7 +69,7 @@ pub struct StateBuilder<'a, E: EquationOfState> { initial_temperature: Option, } -impl<'a, E: EquationOfState> StateBuilder<'a, E> { +impl<'a, E: Residual> StateBuilder<'a, E, false> { /// Create a new `StateBuilder` for the given equation of state. pub fn new(eos: &Arc) -> Self { StateBuilder { @@ -89,7 +89,9 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { initial_temperature: None, } } +} +impl<'a, E: Residual, const IG: bool> StateBuilder<'a, E, IG> { /// Provide the temperature for the new state. pub fn temperature(mut self, temperature: SINumber) -> Self { self.temperature = Some(temperature); @@ -138,24 +140,6 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { self } - /// Provide the molar enthalpy for the new state. - pub fn molar_enthalpy(mut self, molar_enthalpy: SINumber) -> Self { - self.molar_enthalpy = Some(molar_enthalpy); - self - } - - /// Provide the molar entropy for the new state. - pub fn molar_entropy(mut self, molar_entropy: SINumber) -> Self { - self.molar_entropy = Some(molar_entropy); - self - } - - /// Provide the molar internal energy for the new state. - pub fn molar_internal_energy(mut self, molar_internal_energy: SINumber) -> Self { - self.molar_internal_energy = Some(molar_internal_energy); - self - } - /// Specify a vapor state. pub fn vapor(mut self) -> Self { self.density_initialization = DensityInitialization::Vapor; @@ -173,16 +157,81 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { self.density_initialization = DensityInitialization::InitialDensity(initial_density); self } +} + +impl<'a, E: Residual + IdealGas, const IG: bool> StateBuilder<'a, E, IG> { + /// Provide the molar enthalpy for the new state. + pub fn molar_enthalpy(mut self, molar_enthalpy: SINumber) -> StateBuilder<'a, E, true> { + self.molar_enthalpy = Some(molar_enthalpy); + self.convert() + } + + /// Provide the molar entropy for the new state. + pub fn molar_entropy(mut self, molar_entropy: SINumber) -> StateBuilder<'a, E, true> { + self.molar_entropy = Some(molar_entropy); + self.convert() + } + + /// Provide the molar internal energy for the new state. + pub fn molar_internal_energy( + mut self, + molar_internal_energy: SINumber, + ) -> StateBuilder<'a, E, true> { + self.molar_internal_energy = Some(molar_internal_energy); + self.convert() + } /// Provide an initial temperature used in the Newton solver. - pub fn initial_temperature(mut self, initial_temperature: SINumber) -> Self { + pub fn initial_temperature( + mut self, + initial_temperature: SINumber, + ) -> StateBuilder<'a, E, true> { self.initial_temperature = Some(initial_temperature); - self + self.convert() + } + + fn convert(self) -> StateBuilder<'a, E, true> { + StateBuilder { + eos: self.eos, + temperature: self.temperature, + volume: self.volume, + density: self.density, + partial_density: self.partial_density, + total_moles: self.total_moles, + moles: self.moles, + molefracs: self.molefracs, + pressure: self.pressure, + molar_enthalpy: self.molar_enthalpy, + molar_entropy: self.molar_entropy, + molar_internal_energy: self.molar_internal_energy, + density_initialization: self.density_initialization, + initial_temperature: self.initial_temperature, + } } +} +impl<'a, E: Residual> StateBuilder<'a, E, false> { /// Try to build the state with the given inputs. pub fn build(self) -> EosResult> { State::new( + &self.eos, + self.temperature, + self.volume, + self.density, + self.partial_density, + self.total_moles, + self.moles, + self.molefracs, + self.pressure, + self.density_initialization, + ) + } +} + +impl<'a, E: Residual + IdealGas> StateBuilder<'a, E, true> { + /// Try to build the state with the given inputs. + pub fn build(self) -> EosResult> { + State::new_full( &self.eos, self.temperature, self.volume, @@ -201,7 +250,7 @@ impl<'a, E: EquationOfState> StateBuilder<'a, E> { } } -impl<'a, E: EquationOfState> Clone for StateBuilder<'a, E> { +impl<'a, E, const IG: bool> Clone for StateBuilder<'a, E, IG> { fn clone(&self) -> Self { Self { eos: self.eos.clone(), diff --git a/feos-core/src/state/critical_point.rs b/feos-core/src/state/critical_point.rs index c215e928e..d5b5ad103 100644 --- a/feos-core/src/state/critical_point.rs +++ b/feos-core/src/state/critical_point.rs @@ -1,8 +1,7 @@ -use super::{State, StateHD, TPSpec}; -use crate::equation_of_state::EquationOfState; +use super::{DensityInitialization, State, StateHD, TPSpec}; +use crate::equation_of_state::Residual; use crate::errors::{EosError, EosResult}; -use crate::phase_equilibria::{SolverOptions, Verbosity}; -use crate::{DensityInitialization, EosUnit}; +use crate::{EosUnit, SolverOptions, Verbosity}; use nalgebra::{DMatrix, DVector, SVector, SymmetricEigen}; use ndarray::{arr1, Array1}; use num_dual::{ @@ -19,10 +18,10 @@ const MAX_ITER_CRIT_POINT_BINARY: usize = 200; const TOL_CRIT_POINT: f64 = 1e-8; /// # Critical points -impl State { +impl State { /// Calculate the pure component critical point of all components. pub fn critical_point_pure( - eos: &Arc, + eos: &Arc, initial_temperature: Option, options: SolverOptions, ) -> EosResult> @@ -42,7 +41,7 @@ impl State { } pub fn critical_point_binary( - eos: &Arc, + eos: &Arc, temperature_or_pressure: SINumber, initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, @@ -67,7 +66,7 @@ impl State { /// Calculate the critical point of a system for given moles. pub fn critical_point( - eos: &Arc, + eos: &Arc, moles: Option<&SIArray1>, initial_temperature: Option, options: SolverOptions, @@ -94,7 +93,7 @@ impl State { } fn critical_point_hkm( - eos: &Arc, + eos: &Arc, moles: &SIArray1, initial_temperature: SINumber, options: SolverOptions, @@ -175,7 +174,7 @@ impl State { /// Calculate the critical point of a binary system for given temperature. fn critical_point_binary_t( - eos: &Arc, + eos: &Arc, temperature: SINumber, initial_molefracs: Option<[f64; 2]>, options: SolverOptions, @@ -256,7 +255,7 @@ impl State { /// Calculate the critical point of a binary system for given pressure. fn critical_point_binary_p( - eos: &Arc, + eos: &Arc, pressure: SINumber, initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, @@ -352,7 +351,7 @@ impl State { } pub fn spinodal( - eos: &Arc, + eos: &Arc, temperature: SINumber, moles: Option<&SIArray1>, options: SolverOptions, @@ -381,7 +380,7 @@ impl State { } fn calculate_spinodal( - eos: &Arc, + eos: &Arc, temperature: SINumber, moles: &SIArray1, density_initialization: DensityInitialization, @@ -459,8 +458,8 @@ impl State { } } -fn critical_point_objective( - eos: &Arc, +fn critical_point_objective( + eos: &Arc, temperature: DualSVec64<2>, density: DualSVec64<2>, moles: &Array1, @@ -473,8 +472,7 @@ fn critical_point_objective( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, m); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (moles[i] * moles[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -494,12 +492,13 @@ fn critical_point_objective( Dual3::from_re(density.recip() * moles.sum()), moles_hd, ); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); - Ok(SVector::from([eval, res.v3])) + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); + let res = eos.evaluate_residual(&state_s); + Ok(SVector::from([eval, (res + ig).v3])) } -fn critical_point_objective_t( - eos: &Arc, +fn critical_point_objective_t( + eos: &Arc, temperature: f64, density: SVector, 2>, ) -> EosResult, 2>> { @@ -511,8 +510,7 @@ fn critical_point_objective_t( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (density[i] * density[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -528,12 +526,13 @@ fn critical_point_objective_t( ) }); let state_s = StateHD::new(Dual3::from(temperature), Dual3::from(1.0), moles_hd); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); - Ok(SVector::from([eval, res.v3])) + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); + let res = eos.evaluate_residual(&state_s); + Ok(SVector::from([eval, (res + ig).v3])) } -fn critical_point_objective_p( - eos: &Arc, +fn critical_point_objective_p( + eos: &Arc, pressure: f64, temperature: DualSVec64<3>, density: SVector, 2>, @@ -546,8 +545,7 @@ fn critical_point_objective_p( m[i].eps1 = DualSVec64::one(); m[j].eps2 = DualSVec64::one(); let state = StateHD::new(t, v, arr1(&[m[0], m[1]])); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (density[i] * density[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (density[i] * density[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue and corresponding eigenvector of q @@ -563,21 +561,23 @@ fn critical_point_objective_p( ) }); let state_s = StateHD::new(Dual3::from_re(temperature), Dual3::from(1.0), moles_hd); - let res = eos.evaluate_residual(&state_s) + eos.ideal_gas().evaluate(&state_s); + let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum(); + let res = eos.evaluate_residual(&state_s); // calculate pressure let a = |v| { let m = arr1(&[Dual::from_re(density[0]), Dual::from_re(density[1])]); let state_p = StateHD::new(Dual::from_re(temperature), v, m); - eos.evaluate_residual(&state_p) + eos.ideal_gas().evaluate(&state_p) + eos.evaluate_residual(&state_p) }; let (_, p) = first_derivative(a, DualVec::one()); + let p = (p - density.sum()) * temperature; - Ok(SVector::from([eval, res.v3, p * temperature + pressure])) + Ok(SVector::from([eval, (res + ig).v3, p + pressure])) } -fn spinodal_objective( - eos: &Arc, +fn spinodal_objective( + eos: &Arc, temperature: Dual64, density: Dual64, moles: &Array1, @@ -590,8 +590,7 @@ fn spinodal_objective( m[i].eps1 = Dual64::one(); m[j].eps2 = Dual64::one(); let state = StateHD::new(t, v, m); - (eos.evaluate_residual(&state).eps1eps2 + eos.ideal_gas().evaluate(&state).eps1eps2) - * (moles[i] * moles[j]).sqrt() + eos.evaluate_residual(&state).eps1eps2 * (moles[i] * moles[j]).sqrt() + kronecker(i, j) }); // calculate smallest eigenvalue of q @@ -623,3 +622,11 @@ fn smallest_ev_scalar(m: DMatrix) -> (Dual64, DVector) { .unwrap(); (*e, ev.into()) } + +fn kronecker(i: usize, j: usize) -> f64 { + if i == j { + 1.0 + } else { + 0.0 + } +} diff --git a/feos-core/src/state/mod.rs b/feos-core/src/state/mod.rs index ca566add3..893952eb1 100644 --- a/feos-core/src/state/mod.rs +++ b/feos-core/src/state/mod.rs @@ -7,12 +7,11 @@ //! //! Internally, all properties are computed using such states as input. use crate::density_iteration::density_iteration; -use crate::equation_of_state::EquationOfState; +use crate::equation_of_state::{IdealGas, Residual}; use crate::errors::{EosError, EosResult}; use crate::EosUnit; use cache::Cache; use ndarray::prelude::*; -use num_dual::linalg::{norm, LU}; use num_dual::*; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::convert::TryFrom; @@ -22,8 +21,24 @@ use std::sync::{Arc, Mutex}; mod builder; mod cache; mod properties; +mod residual_properties; +mod statevec; pub use builder::StateBuilder; -pub use properties::{Contributions, StateVec}; +pub use statevec::StateVec; + +/// Possible contributions that can be computed. +#[derive(Clone, Copy)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum Contributions { + /// Only compute the ideal gas contribution + IdealGas, + /// Only compute the difference between the total and the ideal gas contribution + Residual, + // /// Compute the differnce between the total and the ideal gas contribution for a (N,p,T) reference state + // ResidualNpt, + /// Compute ideal gas and residual contributions + Total, +} /// Initial values in a density iteration. #[derive(Clone, Copy)] @@ -166,11 +181,10 @@ impl Clone for State { } } -impl fmt::Display for State +impl fmt::Display for State where SINumber: fmt::Display, SIArray1: fmt::Display, - E: EquationOfState, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.eos.components() == 1 { @@ -217,7 +231,7 @@ pub(crate) enum PartialDerivative { } /// # State constructors -impl State { +impl State { /// Return a new `State` given a temperature, an array of mole numbers and a volume. /// /// This function will perform a validation of the given properties, i.e. test for signs @@ -290,7 +304,6 @@ impl State { /// is overdetermined, it will choose a method based on the following hierarchy. /// 1. Create a state non-iteratively from the set of $T$, $V$, $\rho$, $\rho_i$, $N$, $N_i$ and $x_i$. /// 2. Use a density iteration for a given pressure. - /// 3. Determine the state using a Newton iteration from (in this order): $(p, h)$, $(p, s)$, $(T, h)$, $(T, s)$, $(V, u)$ /// /// The [StateBuilder] provides a convenient way of calling this function without the need to provide /// all the optional input values. @@ -308,12 +321,35 @@ impl State { moles: Option<&SIArray1>, molefracs: Option<&Array1>, pressure: Option, - molar_enthalpy: Option, - molar_entropy: Option, - molar_internal_energy: Option, density_initialization: DensityInitialization, - initial_temperature: Option, ) -> EosResult { + Self::_new( + eos, + temperature, + volume, + density, + partial_density, + total_moles, + moles, + molefracs, + pressure, + density_initialization, + )? + .map_err(|_| EosError::UndeterminedState(String::from("Missing input parameters."))) + } + + fn _new( + eos: &Arc, + temperature: Option, + volume: Option, + density: Option, + partial_density: Option<&SIArray1>, + total_moles: Option, + moles: Option<&SIArray1>, + molefracs: Option<&Array1>, + pressure: Option, + density_initialization: DensityInitialization, + ) -> EosResult>> { // Check if the provided densities have correct units. if let DensityInitialization::InitialDensity(rho0) = density_initialization { if !rho0.has_unit(&SIUnit::reference_density()) { @@ -396,36 +432,24 @@ impl State { // check if new state can be created using default constructor if let (Some(v), Some(t), Some(n_i)) = (v, temperature, &n_i) { - return State::new_nvt(eos, t, v, n_i); + return Ok(Ok(State::new_nvt(eos, t, v, n_i)?)); } // Check if new state can be created using density iteration if let (Some(p), Some(t), Some(n_i)) = (pressure, temperature, &n_i) { - return State::new_npt(eos, t, p, n_i, density_initialization); + return Ok(Ok(State::new_npt(eos, t, p, n_i, density_initialization)?)); } if let (Some(p), Some(t), Some(v)) = (pressure, temperature, v) { - return State::new_npvx(eos, t, p, v, &x_u, density_initialization); - } - - // Check if new state can be created using molar_enthalpy and temperature - if let (Some(p), Some(h), Some(n_i)) = (pressure, molar_enthalpy, &n_i) { - return State::new_nph(eos, p, h, n_i, density_initialization, initial_temperature); - } - if let (Some(p), Some(s), Some(n_i)) = (pressure, molar_entropy, &n_i) { - return State::new_nps(eos, p, s, n_i, density_initialization, initial_temperature); - } - if let (Some(t), Some(h), Some(n_i)) = (temperature, molar_enthalpy, &n_i) { - return State::new_nth(eos, t, h, n_i, density_initialization); - } - if let (Some(t), Some(s), Some(n_i)) = (temperature, molar_entropy, &n_i) { - return State::new_nts(eos, t, s, n_i, density_initialization); - } - if let (Some(u), Some(v), Some(n_i)) = (molar_internal_energy, volume, &n_i) { - return State::new_nvu(eos, v, u, n_i, initial_temperature); + return Ok(Ok(State::new_npvx( + eos, + t, + p, + v, + &x_u, + density_initialization, + )?)); } - Err(EosError::UndeterminedState(String::from( - "Missing input parameters.", - ))) + Ok(Err(n_i)) } /// Return a new `State` using a density iteration. [DensityInitialization] is used to @@ -479,9 +503,7 @@ impl State { (Ok(_), Err(_)) => liquid, (Err(_), Ok(_)) => vapor, (Ok(l), Ok(v)) => { - if l.molar_gibbs_energy(Contributions::Total) - > v.molar_gibbs_energy(Contributions::Total) - { + if l.residual_gibbs_energy() > v.residual_gibbs_energy() { vapor } else { liquid @@ -510,6 +532,78 @@ impl State { let moles = state.partial_density * volume; Self::new_nvt(eos, temperature, volume, &moles) } +} + +impl State { + /// Return a new `State` for the combination of inputs. + /// + /// The function attempts to create a new state using the given input values. If the state + /// is overdetermined, it will choose a method based on the following hierarchy. + /// 1. Create a state non-iteratively from the set of $T$, $V$, $\rho$, $\rho_i$, $N$, $N_i$ and $x_i$. + /// 2. Use a density iteration for a given pressure. + /// 3. Determine the state using a Newton iteration from (in this order): $(p, h)$, $(p, s)$, $(T, h)$, $(T, s)$, $(V, u)$ + /// + /// The [StateBuilder] provides a convenient way of calling this function without the need to provide + /// all the optional input values. + /// + /// # Errors + /// + /// When the state cannot be created using the combination of inputs. + pub fn new_full( + eos: &Arc, + temperature: Option, + volume: Option, + density: Option, + partial_density: Option<&SIArray1>, + total_moles: Option, + moles: Option<&SIArray1>, + molefracs: Option<&Array1>, + pressure: Option, + molar_enthalpy: Option, + molar_entropy: Option, + molar_internal_energy: Option, + density_initialization: DensityInitialization, + initial_temperature: Option, + ) -> EosResult { + let state = Self::_new( + eos, + temperature, + volume, + density, + partial_density, + total_moles, + moles, + molefracs, + pressure, + density_initialization, + )?; + + let ti = initial_temperature; + match state { + Ok(state) => Ok(state), + Err(n_i) => { + // Check if new state can be created using molar_enthalpy and temperature + if let (Some(p), Some(h), Some(n_i)) = (pressure, molar_enthalpy, &n_i) { + return State::new_nph(eos, p, h, n_i, density_initialization, ti); + } + if let (Some(p), Some(s), Some(n_i)) = (pressure, molar_entropy, &n_i) { + return State::new_nps(eos, p, s, n_i, density_initialization, ti); + } + if let (Some(t), Some(h), Some(n_i)) = (temperature, molar_enthalpy, &n_i) { + return State::new_nth(eos, t, h, n_i, density_initialization); + } + if let (Some(t), Some(s), Some(n_i)) = (temperature, molar_entropy, &n_i) { + return State::new_nts(eos, t, s, n_i, density_initialization); + } + if let (Some(u), Some(v), Some(n_i)) = (molar_internal_energy, volume, &n_i) { + return State::new_nvu(eos, v, u, n_i, ti); + } + Err(EosError::UndeterminedState(String::from( + "Missing input parameters.", + ))) + } + } + } /// Return a new `State` for given pressure $p$ and molar enthalpy $h$. pub fn new_nph( @@ -621,56 +715,14 @@ impl State { }; newton(t0, f, 1.0e-8 * SIUnit::reference_temperature()) } +} +impl State { /// Update the state with the given temperature pub fn update_temperature(&self, temperature: SINumber) -> EosResult { Self::new_nvt(&self.eos, temperature, self.volume, &self.moles) } - /// Update the state with the given chemical potential. - pub fn update_chemical_potential(&mut self, chemical_potential: &SIArray1) -> EosResult<()> { - for _ in 0..50 { - let dmu_drho = self.dmu_dni(Contributions::Total) * self.volume; - let f = self.chemical_potential(Contributions::Total) - chemical_potential; - let dmu_drho_r = dmu_drho - .to_reduced(SIUnit::reference_molar_energy() / SIUnit::reference_density())?; - let f_r = f.to_reduced(SIUnit::reference_molar_energy())?; - let rho = &self.partial_density - - &(LU::new(dmu_drho_r)?.solve(&f_r) * SIUnit::reference_density()); - *self = State::new_nvt( - &self.eos, - self.temperature, - self.volume, - &(rho * self.volume), - )?; - if norm(&f.to_reduced(SIUnit::reference_molar_energy())?) < 1e-8 { - return Ok(()); - } - } - Err(EosError::NotConverged( - "State::update_chemical_potential".into(), - )) - } - - /// Update the state with the given molar Gibbs energy. - pub fn update_gibbs_energy(mut self, molar_gibbs_energy: SINumber) -> EosResult { - for _ in 0..50 { - let df = self.volume / self.density * self.dp_dv(Contributions::Total); - let f = self.molar_gibbs_energy(Contributions::Total) - molar_gibbs_energy; - let rho = self.density * (f.to_reduced(df)?).exp(); - self = State::new_nvt( - &self.eos, - self.temperature, - self.total_moles / rho, - &self.moles, - )?; - if f.to_reduced(SIUnit::reference_molar_energy())?.abs() < 1e-8 { - return Ok(self); - } - } - Err(EosError::NotConverged("State::update_gibbs_energy".into())) - } - /// Creates a [StateHD] cloning temperature, volume and moles. pub fn derive0(&self) -> StateHD { StateHD::new( @@ -746,7 +798,7 @@ fn is_close(x: SINumber, y: SINumber, atol: SINumber, rtol: f64) -> bool { (x - y).abs() <= atol + rtol * y.abs() } -fn newton(mut x0: SINumber, mut f: F, atol: SINumber) -> EosResult> +fn newton(mut x0: SINumber, mut f: F, atol: SINumber) -> EosResult> where F: FnMut(SINumber) -> EosResult<(SINumber, SINumber, State)>, { diff --git a/feos-core/src/state/properties.rs b/feos-core/src/state/properties.rs index dcaee2010..5567bb142 100644 --- a/feos-core/src/state/properties.rs +++ b/feos-core/src/state/properties.rs @@ -1,147 +1,50 @@ -use super::{Derivative::*, PartialDerivative, State}; -use crate::equation_of_state::{EntropyScaling, EquationOfState, MolarWeight}; -use crate::errors::EosResult; +use super::{Contributions, Derivative::*, PartialDerivative, State}; +use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; use crate::EosUnit; -use ndarray::{arr1, Array1, Array2}; -use num_dual::DualNum; +use ndarray::Array1; use quantity::si::*; -use std::iter::FromIterator; -use std::ops::{Add, Deref, Sub}; -use std::sync::Arc; - -#[derive(Clone, Copy)] -pub(crate) enum Evaluate { - IdealGas, - Residual, - Total, - IdealGasDelta, -} - -/// Possible contributions that can be computed. -#[derive(Clone, Copy)] -#[cfg_attr(feature = "python", pyo3::pyclass)] -pub enum Contributions { - /// Only compute the ideal gas contribution - IdealGas, - /// Only compute the difference between the total and the ideal gas contribution - ResidualNvt, - /// Compute the differnce between the total and the ideal gas contribution for a (N,p,T) reference state - ResidualNpt, - /// Compute ideal gas and residual contributions - Total, -} -/// # State properties -impl State { +impl State { fn get_or_compute_derivative( &self, derivative: PartialDerivative, - evaluate: Evaluate, + contributions: Contributions, ) -> SINumber { - if let Evaluate::IdealGasDelta = evaluate { - return match derivative { - PartialDerivative::Zeroth => { - let new_state = self.derive0(); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - * SIUnit::reference_energy() - } - PartialDerivative::First(v) => { - let new_state = self.derive1(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).eps - * (SIUnit::reference_energy() / v.reference()) - } - PartialDerivative::Second(v) => { - let new_state = self.derive2(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v2 - * (SIUnit::reference_energy() / (v.reference() * v.reference())) - } - PartialDerivative::SecondMixed(v1, v2) => { - let new_state = self.derive2_mixed(v1, v2); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()) - .eps1eps2 - * (SIUnit::reference_energy() / (v1.reference() * v2.reference())) - } - PartialDerivative::Third(v) => { - let new_state = self.derive3(v); - -(new_state.moles.sum() * new_state.temperature * new_state.volume.ln()).v3 - * (SIUnit::reference_energy() - / (v.reference() * v.reference() * v.reference())) - } - }; - } - - let mut cache = self.cache.lock().unwrap(); - - let residual = match evaluate { - Evaluate::IdealGas => None, - _ => Some(match derivative { - PartialDerivative::Zeroth => { - let new_state = self.derive0(); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_f64(computation) * SIUnit::reference_energy() - } - PartialDerivative::First(v) => { - let new_state = self.derive1(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_d64(v, computation) * SIUnit::reference_energy() - / v.reference() - } - PartialDerivative::Second(v) => { - let new_state = self.derive2(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_d2_64(v, computation) * SIUnit::reference_energy() - / (v.reference() * v.reference()) - } - PartialDerivative::SecondMixed(v1, v2) => { - let new_state = self.derive2_mixed(v1, v2); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_hd64(v1, v2, computation) * SIUnit::reference_energy() - / (v1.reference() * v2.reference()) - } - PartialDerivative::Third(v) => { - let new_state = self.derive3(v); - let computation = - || self.eos.evaluate_residual(&new_state) * new_state.temperature; - cache.get_or_insert_with_hd364(v, computation) * SIUnit::reference_energy() - / (v.reference() * v.reference() * v.reference()) - } - }), + let residual = match contributions { + Contributions::IdealGas => None, + _ => Some(self.get_or_compute_derivative_residual(derivative)), }; - let ideal_gas = match evaluate { - Evaluate::Residual => None, + let ideal_gas = match contributions { + Contributions::Residual => None, _ => Some(match derivative { PartialDerivative::Zeroth => { let new_state = self.derive0(); - self.eos.ideal_gas().evaluate(&new_state) + self.eos.evaluate_ideal_gas(&new_state) * SIUnit::reference_energy() * new_state.temperature } PartialDerivative::First(v) => { let new_state = self.derive1(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps * SIUnit::reference_energy() / v.reference() } PartialDerivative::Second(v) => { let new_state = self.derive2(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).v2 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).v2 * SIUnit::reference_energy() / (v.reference() * v.reference()) } PartialDerivative::SecondMixed(v1, v2) => { let new_state = self.derive2_mixed(v1, v2); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).eps1eps2 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps1eps2 * SIUnit::reference_energy() / (v1.reference() * v2.reference()) } PartialDerivative::Third(v) => { let new_state = self.derive3(v); - (self.eos.ideal_gas().evaluate(&new_state) * new_state.temperature).v3 + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).v3 * SIUnit::reference_energy() / (v.reference() * v.reference() * v.reference()) } @@ -156,267 +59,56 @@ impl State { } } - fn evaluate_property(&self, f: F, contributions: Contributions, additive: bool) -> R - where - R: Add + Sub, - F: Fn(&Self, Evaluate) -> R, - { - match contributions { - Contributions::IdealGas => f(self, Evaluate::IdealGas), - Contributions::Total => f(self, Evaluate::Total), - Contributions::ResidualNvt => { - if additive { - f(self, Evaluate::Residual) - } else { - f(self, Evaluate::Total) - f(self, Evaluate::IdealGas) - } - } - Contributions::ResidualNpt => { - let p = self.pressure_(Evaluate::Total); - let state_p = Self::new_nvt_unchecked( - &self.eos, - self.temperature, - self.total_moles * SIUnit::gas_constant() * self.temperature / p, - &self.moles, - ); - if additive { - f(self, Evaluate::Residual) + f(self, Evaluate::IdealGasDelta) - - f(&state_p, Evaluate::IdealGasDelta) - } else { - f(self, Evaluate::Total) - f(&state_p, Evaluate::IdealGas) - } - } - } - } - - fn helmholtz_energy_(&self, evaluate: Evaluate) -> SINumber { - self.get_or_compute_derivative(PartialDerivative::Zeroth, evaluate) - } - - fn pressure_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::First(DV), evaluate) - } - - fn entropy_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::First(DT), evaluate) - } - - fn chemical_potential_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - self.get_or_compute_derivative(PartialDerivative::First(DN(i)), evaluate) - }) - } - - fn dp_dv_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Second(DV), evaluate) - } - - fn dp_dt_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::SecondMixed(DV, DT), evaluate) - } - - fn dp_dni_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - -self.get_or_compute_derivative(PartialDerivative::SecondMixed(DV, DN(i)), evaluate) - }) - } - - fn d2p_dv2_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Third(DV), evaluate) - } - - fn dmu_dt_(&self, evaluate: Evaluate) -> SIArray1 { - SIArray::from_shape_fn(self.eos.components(), |i| { - self.get_or_compute_derivative(PartialDerivative::SecondMixed(DT, DN(i)), evaluate) - }) - } - - fn dmu_dni_(&self, evaluate: Evaluate) -> SIArray2 { - let n = self.eos.components(); - SIArray::from_shape_fn((n, n), |(i, j)| { - self.get_or_compute_derivative(PartialDerivative::SecondMixed(DN(i), DN(j)), evaluate) - }) - } - - fn ds_dt_(&self, evaluate: Evaluate) -> SINumber { - -self.get_or_compute_derivative(PartialDerivative::Second(DT), evaluate) - } - - 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) -> SINumber { - self.evaluate_property(Self::pressure_, contributions, true) - } - - /// Compressibility factor: $Z=\frac{pV}{NRT}$ - pub fn compressibility(&self, contributions: Contributions) -> f64 { - (self.pressure(contributions) / (self.density * self.temperature * SIUnit::gas_constant())) - .into_value() - .unwrap() - } - - /// Partial derivative of pressure w.r.t. volume: $\left(\frac{\partial p}{\partial V}\right)_{T,N_i}$ - pub fn dp_dv(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::dp_dv_, contributions, true) - } - - /// Partial derivative of pressure w.r.t. density: $\left(\frac{\partial p}{\partial \rho}\right)_{T,N_i}$ - pub fn dp_drho(&self, contributions: Contributions) -> SINumber { - -self.volume / self.density * self.dp_dv(contributions) - } - - /// Partial derivative of pressure w.r.t. temperature: $\left(\frac{\partial p}{\partial T}\right)_{V,N_i}$ - pub fn dp_dt(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::dp_dt_, contributions, true) - } - - /// Partial derivative of pressure w.r.t. moles: $\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$ - pub fn dp_dni(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::dp_dni_, contributions, true) - } - - /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$ - pub fn d2p_dv2(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::d2p_dv2_, contributions, true) - } - - /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$ - pub fn d2p_drho2(&self, contributions: Contributions) -> SINumber { - self.volume / (self.density * self.density) - * (self.volume * self.d2p_dv2(contributions) + 2.0 * self.dp_dv(contributions)) - } - - /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_volume(&self, contributions: Contributions) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| -s.dp_dni_(evaluate) / s.dp_dv_(evaluate); - self.evaluate_property(func, contributions, false) - } - /// Chemical potential: $\mu_i=\left(\frac{\partial A}{\partial N_i}\right)_{T,V,N_j}$ pub fn chemical_potential(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::chemical_potential_, contributions, true) + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative(PartialDerivative::First(DN(i)), contributions) + }) } /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$ pub fn dmu_dt(&self, contributions: Contributions) -> SIArray1 { - self.evaluate_property(Self::dmu_dt_, contributions, true) - } - - /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ - pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { - self.evaluate_property(Self::dmu_dni_, contributions, true) - } - - /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$ - pub fn ln_phi(&self) -> Array1 { - (self.chemical_potential(Contributions::ResidualNpt) - / (SIUnit::gas_constant() * self.temperature)) - .into_value() - .unwrap() - } - - /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure. - pub fn ln_phi_pure_liquid(&self) -> EosResult> { - let pressure = self.pressure(Contributions::Total); - (0..self.eos.components()) - .map(|i| { - let eos = Arc::new(self.eos.subset(&[i])); - let state = Self::new_npt( - &eos, - self.temperature, - pressure, - &(arr1(&[1.0]) * SIUnit::reference_moles()), - crate::DensityInitialization::Liquid, - )?; - Ok(state.ln_phi()[0]) - }) - .collect() - } - - /// Activity coefficient $\ln \gamma_i = \ln \varphi_i(T, p, \mathbf{N}) - \ln \varphi_i(T, p)$ - pub fn ln_symmetric_activity_coefficient(&self) -> EosResult> { - match self.eos.components() { - 1 => Ok(arr1(&[0.0])), - _ => Ok(self.ln_phi() - &self.ln_phi_pure_liquid()?), - } - } - - /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. temperature: $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,N_i}$ - pub fn dln_phi_dt(&self) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| { - (s.dmu_dt_(evaluate) + s.dp_dni_(evaluate) * (s.dp_dt_(evaluate) / s.dp_dv_(evaluate)) - - s.chemical_potential_(evaluate) / self.temperature) - / (SIUnit::gas_constant() * self.temperature) - }; - self.evaluate_property(func, Contributions::ResidualNpt, false) - } - - /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. pressure: $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,N_i}$ - pub fn dln_phi_dp(&self) -> SIArray1 { - self.partial_molar_volume(Contributions::ResidualNpt) - / (SIUnit::gas_constant() * self.temperature) - } - - /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. moles: $\left(\frac{\partial\ln\varphi_i}{\partial N_j}\right)_{T,p,N_k}$ - pub fn dln_phi_dnj(&self) -> SIArray2 { - let n = self.eos.components(); - let dmu_dni = self.dmu_dni(Contributions::ResidualNvt); - let dp_dni = self.dp_dni(Contributions::Total); - let dp_dv = self.dp_dv(Contributions::Total); - let dp_dn_2 = SIArray::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j)); - (dmu_dni + dp_dn_2 / dp_dv) / (SIUnit::gas_constant() * self.temperature) - + 1.0 / self.total_moles - } - - /// Thermodynamic factor: $\Gamma_{ij}=\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ - pub fn thermodynamic_factor(&self) -> Array2 { - let dln_phi_dnj = self - .dln_phi_dnj() - .to_reduced(SIUnit::reference_moles().powi(-1)) - .unwrap(); - let moles = self.moles.to_reduced(SIUnit::reference_moles()).unwrap(); - let n = self.eos.components() - 1; - Array2::from_shape_fn((n, n), |(i, j)| { - moles[i] * (dln_phi_dnj[[i, j]] - dln_phi_dnj[[i, n]]) + if i == j { 1.0 } else { 0.0 } + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative(PartialDerivative::SecondMixed(DT, DN(i)), contributions) }) } /// Molar isochoric heat capacity: $c_v=\left(\frac{\partial u}{\partial T}\right)_{V,N_i}$ pub fn c_v(&self, contributions: Contributions) -> SINumber { - let func = - |s: &Self, evaluate: Evaluate| s.temperature * s.ds_dt_(evaluate) / s.total_moles; - self.evaluate_property(func, contributions, true) + self.temperature * self.ds_dt(contributions) / self.total_moles } /// Partial derivative of the molar isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V}{\partial T}\right)_{V,N_i}$ pub fn dc_v_dt(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - (s.temperature * s.d2s_dt2_(evaluate) + s.ds_dt_(evaluate)) / s.total_moles - }; - self.evaluate_property(func, contributions, true) + (self.temperature * self.d2s_dt2(contributions) + self.ds_dt(contributions)) + / self.total_moles } /// Molar isobaric heat capacity: $c_p=\left(\frac{\partial h}{\partial T}\right)_{p,N_i}$ pub fn c_p(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature / s.total_moles - * (s.ds_dt_(evaluate) - - s.dp_dt_(evaluate) * s.dp_dt_(evaluate) / s.dp_dv_(evaluate)) - }; - self.evaluate_property(func, contributions, false) + match contributions { + Contributions::Residual => self.c_p_res(), + _ => { + self.temperature / self.total_moles + * (self.ds_dt(contributions) + - self.dp_dt(contributions).powi(2) / self.dp_dv(contributions)) + } + } } /// Entropy: $S=-\left(\frac{\partial A}{\partial T}\right)_{V,N_i}$ pub fn entropy(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::entropy_, contributions, true) + -self.get_or_compute_derivative(PartialDerivative::First(DT), contributions) } /// Partial derivative of the entropy w.r.t. temperature: $\left(\frac{\partial S}{\partial T}\right)_{V,N_i}$ pub fn ds_dt(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::ds_dt_, contributions, true) + -self.get_or_compute_derivative(PartialDerivative::Second(DT), contributions) + } + + /// Second partial derivative of the entropy w.r.t. temperature: $\left(\frac{\partial^2 S}{\partial T^2}\right)_{V,N_i}$ + pub fn d2s_dt2(&self, contributions: Contributions) -> SINumber { + -self.get_or_compute_derivative(PartialDerivative::Third(DT), contributions) } /// molar entropy: $s=\frac{S}{N}$ @@ -426,12 +118,9 @@ impl State { /// Enthalpy: $H=A+TS+pV$ pub fn enthalpy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature * s.entropy_(evaluate) - + s.helmholtz_energy_(evaluate) - + s.pressure_(evaluate) * s.volume - }; - self.evaluate_property(func, contributions, true) + self.temperature * self.entropy(contributions) + + self.helmholtz_energy(contributions) + + self.pressure(contributions) * self.volume } /// molar enthalpy: $h=\frac{H}{N}$ @@ -441,7 +130,7 @@ impl State { /// Helmholtz energy: $A$ pub fn helmholtz_energy(&self, contributions: Contributions) -> SINumber { - self.evaluate_property(Self::helmholtz_energy_, contributions, true) + self.get_or_compute_derivative(PartialDerivative::Zeroth, contributions) } /// molar Helmholtz energy: $a=\frac{A}{N}$ @@ -451,10 +140,7 @@ impl State { /// Internal energy: $U=A+TS$ pub fn internal_energy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.temperature * s.entropy_(evaluate) + s.helmholtz_energy_(evaluate) - }; - self.evaluate_property(func, contributions, true) + self.temperature * self.entropy(contributions) + self.helmholtz_energy(contributions) } /// Molar internal energy: $u=\frac{U}{N}$ @@ -464,10 +150,7 @@ impl State { /// Gibbs energy: $G=A+pV$ pub fn gibbs_energy(&self, contributions: Contributions) -> SINumber { - let func = |s: &Self, evaluate: Evaluate| { - s.pressure_(evaluate) * s.volume + s.helmholtz_energy_(evaluate) - }; - self.evaluate_property(func, contributions, true) + self.pressure(contributions) * self.volume + self.helmholtz_energy(contributions) } /// Molar Gibbs energy: $g=\frac{G}{N}$ @@ -476,17 +159,15 @@ impl State { } /// Partial molar entropy: $s_i=\left(\frac{\partial S}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_entropy(&self, contributions: Contributions) -> SIArray1 { - let func = |s: &Self, evaluate: Evaluate| { - -(s.dmu_dt_(evaluate) + s.dp_dni_(evaluate) * (s.dp_dt_(evaluate) / s.dp_dv_(evaluate))) - }; - self.evaluate_property(func, contributions, false) + pub fn partial_molar_entropy(&self) -> SIArray1 { + let c = Contributions::Total; + -(self.dmu_dt(c) + self.dp_dni(c) * (self.dp_dt(c) / self.dp_dv(c))) } /// Partial molar enthalpy: $h_i=\left(\frac{\partial H}{\partial N_i}\right)_{T,p,N_j}$ - pub fn partial_molar_enthalpy(&self, contributions: Contributions) -> SIArray1 { - let s = self.partial_molar_entropy(contributions); - let mu = self.chemical_potential(contributions); + pub fn partial_molar_enthalpy(&self) -> SIArray1 { + let s = self.partial_molar_entropy(); + let mu = self.chemical_potential(Contributions::Total); s * self.temperature + mu } @@ -503,28 +184,16 @@ impl State { -self.c_v(c) / (self.c_p(c) * self.dp_dv(c) * self.volume) } - /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$ - pub fn isothermal_compressibility(&self) -> SINumber { - let c = Contributions::Total; - -1.0 / (self.dp_dv(c) * self.volume) - } - - /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ - pub fn structure_factor(&self) -> f64 { - -(SIUnit::gas_constant() * self.temperature * self.density) - .to_reduced(self.volume * self.dp_dv(Contributions::Total)) - .unwrap() - } - /// Helmholtz energy $A$ evaluated for each contribution of the equation of state. pub fn helmholtz_energy_contributions(&self) -> Vec<(String, SINumber)> { let new_state = self.derive0(); let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); res.push(( - ig.to_string(), - ig.evaluate(&new_state) * new_state.temperature * SIUnit::reference_energy(), + self.eos.ideal_gas_model().to_string(), + self.eos.evaluate_ideal_gas(&new_state) + * new_state.temperature + * SIUnit::reference_energy(), )); for (s, v) in contributions { res.push((s, v * new_state.temperature * SIUnit::reference_energy())); @@ -532,34 +201,14 @@ impl State { res } - /// Pressure $p$ evaluated for each contribution of the equation of state. - pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { - let new_state = self.derive1(DV); - let contributions = self.eos.evaluate_residual_contributions(&new_state); - let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); - res.push(( - ig.to_string(), - -(ig.evaluate(&new_state) * new_state.temperature).eps * SIUnit::reference_pressure(), - )); - for (s, v) in contributions { - res.push(( - s, - -(v * new_state.temperature).eps * SIUnit::reference_pressure(), - )); - } - res - } - /// Chemical potential $\mu_i$ evaluated for each contribution of the equation of state. pub fn chemical_potential_contributions(&self, component: usize) -> Vec<(String, SINumber)> { let new_state = self.derive1(DN(component)); let contributions = self.eos.evaluate_residual_contributions(&new_state); let mut res = Vec::with_capacity(contributions.len() + 1); - let ig = self.eos.ideal_gas(); res.push(( - ig.to_string(), - (ig.evaluate(&new_state) * new_state.temperature).eps + self.eos.ideal_gas_model().to_string(), + (self.eos.evaluate_ideal_gas(&new_state) * new_state.temperature).eps * SIUnit::reference_molar_energy(), )); for (s, v) in contributions { @@ -576,7 +225,7 @@ impl State { /// /// These properties are available for equations of state /// that implement the [MolarWeight] trait. -impl State { +impl State { /// Total molar weight: $MW=\sum_ix_iMW_i$ pub fn total_molar_weight(&self) -> SINumber { (self.eos.molar_weight() * &self.molefracs).sum() @@ -601,7 +250,9 @@ impl State { pub fn massfracs(&self) -> Array1 { self.mass().to_reduced(self.total_mass()).unwrap() } +} +impl State { /// Specific entropy: $s^{(m)}=\frac{S}{m}$ pub fn specific_entropy(&self, contributions: Contributions) -> SINumber { self.molar_entropy(contributions) / self.total_molar_weight() @@ -634,214 +285,3 @@ impl State { .unwrap() } } - -impl State { - // This function is designed specifically for use in density iterations - pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { - let dp_dv = self.dp_dv(Contributions::Total); - ( - self.pressure(Contributions::Total), - (-self.volume * dp_dv / self.density), - ) - } - - // This function is designed specifically for use in spinodal iterations - pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { - let d2p_dv2 = self.d2p_dv2(Contributions::Total); - let dp_dv = self.dp_dv(Contributions::Total); - ( - self.pressure(Contributions::Total), - (-self.volume * dp_dv / self.density), - (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)), - ) - } -} - -/// # Transport properties -/// -/// These properties are available for equations of state -/// that implement the [EntropyScaling] trait. -impl State { - /// Return the viscosity via entropy scaling. - pub fn viscosity(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .viscosity_reference(self.temperature, self.volume, &self.moles)? - * self.eos.viscosity_correlation(s, &self.molefracs)?.exp()) - } - - /// Return the logarithm of the reduced viscosity. - /// - /// This term equals the viscosity correlation function - /// that is used for entropy scaling. - pub fn ln_viscosity_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos.viscosity_correlation(s, &self.molefracs) - } - - /// Return the viscosity reference as used in entropy scaling. - pub fn viscosity_reference(&self) -> EosResult { - self.eos - .viscosity_reference(self.temperature, self.volume, &self.moles) - } - - /// Return the diffusion via entropy scaling. - pub fn diffusion(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .diffusion_reference(self.temperature, self.volume, &self.moles)? - * self.eos.diffusion_correlation(s, &self.molefracs)?.exp()) - } - - /// Return the logarithm of the reduced diffusion. - /// - /// This term equals the diffusion correlation function - /// that is used for entropy scaling. - pub fn ln_diffusion_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos.diffusion_correlation(s, &self.molefracs) - } - - /// Return the diffusion reference as used in entropy scaling. - pub fn diffusion_reference(&self) -> EosResult { - self.eos - .diffusion_reference(self.temperature, self.volume, &self.moles) - } - - /// Return the thermal conductivity via entropy scaling. - pub fn thermal_conductivity(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - Ok(self - .eos - .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? - * self - .eos - .thermal_conductivity_correlation(s, &self.molefracs)? - .exp()) - } - - /// Return the logarithm of the reduced thermal conductivity. - /// - /// This term equals the thermal conductivity correlation function - /// that is used for entropy scaling. - pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { - let s = self - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy())?; - self.eos - .thermal_conductivity_correlation(s, &self.molefracs) - } - - /// Return the thermal conductivity reference as used in entropy scaling. - pub fn thermal_conductivity_reference(&self) -> EosResult { - self.eos - .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) - } -} - -/// A list of states for a simple access to properties -/// of multiple states. -pub struct StateVec<'a, E>(pub Vec<&'a State>); - -impl<'a, E> FromIterator<&'a State> for StateVec<'a, E> { - fn from_iter>>(iter: I) -> Self { - Self(iter.into_iter().collect()) - } -} - -impl<'a, E> IntoIterator for StateVec<'a, E> { - type Item = &'a State; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a, E> Deref for StateVec<'a, E> { - type Target = Vec<&'a State>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a, E: EquationOfState> StateVec<'a, E> { - pub fn temperature(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].temperature) - } - - pub fn pressure(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].pressure(Contributions::Total)) - } - - pub fn compressibility(&self) -> Array1 { - Array1::from_shape_fn(self.0.len(), |i| { - self.0[i].compressibility(Contributions::Total) - }) - } - - pub fn density(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].density) - } - - pub fn moles(&self) -> SIArray2 { - SIArray2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].moles.get(j) - }) - } - - pub fn molefracs(&self) -> Array2 { - Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].molefracs[j] - }) - } - - pub fn molar_enthalpy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].molar_enthalpy(Contributions::Total) - }) - } - - pub fn molar_entropy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].molar_entropy(Contributions::Total) - }) - } -} - -impl<'a, E: EquationOfState + MolarWeight> StateVec<'a, E> { - pub fn mass_density(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) - } - - pub fn massfracs(&self) -> Array2 { - Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].massfracs()[j] - }) - } - - pub fn specific_enthalpy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].specific_enthalpy(Contributions::Total) - }) - } - - pub fn specific_entropy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.0.len(), |i| { - self.0[i].specific_entropy(Contributions::Total) - }) - } -} diff --git a/feos-core/src/state/residual_properties.rs b/feos-core/src/state/residual_properties.rs new file mode 100644 index 000000000..42f2f06f1 --- /dev/null +++ b/feos-core/src/state/residual_properties.rs @@ -0,0 +1,439 @@ +use super::{Contributions, Derivative::*, PartialDerivative, State}; +use crate::equation_of_state::{EntropyScaling, Residual}; +use crate::errors::EosResult; +use crate::EosUnit; +use ndarray::{arr1, Array1, Array2}; +use quantity::si::*; +use std::sync::Arc; + +/// # State properties +impl State { + pub(super) fn get_or_compute_derivative_residual( + &self, + derivative: PartialDerivative, + ) -> SINumber { + let mut cache = self.cache.lock().unwrap(); + + match derivative { + PartialDerivative::Zeroth => { + let new_state = self.derive0(); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_f64(computation) * SIUnit::reference_energy() + } + PartialDerivative::First(v) => { + let new_state = self.derive1(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_d64(v, computation) * SIUnit::reference_energy() + / v.reference() + } + PartialDerivative::Second(v) => { + let new_state = self.derive2(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_d2_64(v, computation) * SIUnit::reference_energy() + / (v.reference() * v.reference()) + } + PartialDerivative::SecondMixed(v1, v2) => { + let new_state = self.derive2_mixed(v1, v2); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_hd64(v1, v2, computation) * SIUnit::reference_energy() + / (v1.reference() * v2.reference()) + } + PartialDerivative::Third(v) => { + let new_state = self.derive3(v); + let computation = || self.eos.evaluate_residual(&new_state) * new_state.temperature; + cache.get_or_insert_with_hd364(v, computation) * SIUnit::reference_energy() + / (v.reference() * v.reference() * v.reference()) + } + } + } +} + +impl State { + fn contributions( + ideal_gas: SINumber, + residual: SINumber, + contributions: Contributions, + ) -> SINumber { + match contributions { + Contributions::IdealGas => ideal_gas, + Contributions::Total => ideal_gas + residual, + Contributions::Residual => residual, + } + } + + /// Residual Helmholtz energy $A^\text{res}$ + pub fn residual_helmholtz_energy(&self) -> SINumber { + self.get_or_compute_derivative_residual(PartialDerivative::Zeroth) + } + + /// Residual entropy $S^\text{res}=\left(\frac{\partial A^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn residual_entropy(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::First(DT)) + } + + /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$ + pub fn pressure(&self, contributions: Contributions) -> SINumber { + let ideal_gas = self.density * SIUnit::gas_constant() * self.temperature; + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::First(DV)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Residual chemical potential: $\mu_i^\text{res}=\left(\frac{\partial A^\text{res}}{\partial N_i}\right)_{T,V,N_j}$ + pub fn residual_chemical_potential(&self) -> SIArray1 { + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative_residual(PartialDerivative::First(DN(i))) + }) + } + + /// Compressibility factor: $Z=\frac{pV}{NRT}$ + pub fn compressibility(&self, contributions: Contributions) -> f64 { + (self.pressure(contributions) / (self.density * self.temperature * SIUnit::gas_constant())) + .into_value() + .unwrap() + } + + // pressure derivatives + + /// Partial derivative of pressure w.r.t. volume: $\left(\frac{\partial p}{\partial V}\right)_{T,N_i}$ + pub fn dp_dv(&self, contributions: Contributions) -> SINumber { + let ideal_gas = -self.density * SIUnit::gas_constant() * self.temperature / self.volume; + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::Second(DV)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Partial derivative of pressure w.r.t. density: $\left(\frac{\partial p}{\partial \rho}\right)_{T,N_i}$ + pub fn dp_drho(&self, contributions: Contributions) -> SINumber { + -self.volume / self.density * self.dp_dv(contributions) + } + + /// Partial derivative of pressure w.r.t. temperature: $\left(\frac{\partial p}{\partial T}\right)_{V,N_i}$ + pub fn dp_dt(&self, contributions: Contributions) -> SINumber { + let ideal_gas = self.density * SIUnit::gas_constant(); + let residual = + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DT)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Partial derivative of pressure w.r.t. moles: $\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$ + pub fn dp_dni(&self, contributions: Contributions) -> SIArray1 { + match contributions { + Contributions::IdealGas => { + SIArray::from_vec(vec![ + SIUnit::gas_constant() * self.temperature / self.volume; + self.eos.components() + ]) + } + Contributions::Residual => SIArray::from_shape_fn(self.eos.components(), |i| { + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DN(i))) + }), + Contributions::Total => SIArray::from_shape_fn(self.eos.components(), |i| { + -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DN(i))) + + SIUnit::gas_constant() * self.temperature / self.volume + }), + } + } + + /// Second partial derivative of pressure w.r.t. volume: $\left(\frac{\partial^2 p}{\partial V^2}\right)_{T,N_j}$ + pub fn d2p_dv2(&self, contributions: Contributions) -> SINumber { + let ideal_gas = 2.0 * self.density * SIUnit::gas_constant() * self.temperature + / (self.volume * self.volume); + let residual = -self.get_or_compute_derivative_residual(PartialDerivative::Third(DV)); + Self::contributions(ideal_gas, residual, contributions) + } + + /// Second partial derivative of pressure w.r.t. density: $\left(\frac{\partial^2 p}{\partial \rho^2}\right)_{T,N_j}$ + 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)) + } + + /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ + pub fn structure_factor(&self) -> f64 { + -(SIUnit::gas_constant() * self.temperature * self.density) + .to_reduced(self.volume * self.dp_dv(Contributions::Total)) + .unwrap() + } + + // This function is designed specifically for use in density iterations + pub(crate) fn p_dpdrho(&self) -> (SINumber, SINumber) { + let dp_dv = self.dp_dv(Contributions::Total); + ( + self.pressure(Contributions::Total), + (-self.volume * dp_dv / self.density), + ) + } + + /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ + pub fn partial_molar_volume(&self) -> SIArray1 { + -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total) + } + + /// Partial derivative of chemical potential w.r.t. moles: $\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ + pub fn dmu_dni(&self, contributions: Contributions) -> SIArray2 { + let n = self.eos.components(); + SIArray::from_shape_fn((n, n), |(i, j)| { + let ideal_gas = if i == j { + SIUnit::gas_constant() * self.temperature / self.moles.get(i) + } else { + 0.0 * SIUnit::reference_molar_energy() / SIUnit::reference_moles() + }; + let residual = self + .get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DN(i), DN(j))); + Self::contributions(ideal_gas, residual, contributions) + }) + } + + // This function is designed specifically for use in spinodal iterations + pub(crate) fn d2pdrho2(&self) -> (SINumber, SINumber, SINumber) { + let d2p_dv2 = self.d2p_dv2(Contributions::Total); + let dp_dv = self.dp_dv(Contributions::Total); + ( + self.pressure(Contributions::Total), + (-self.volume * dp_dv / self.density), + (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)), + ) + } + + /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$ + pub fn isothermal_compressibility(&self) -> SINumber { + -1.0 / (self.dp_dv(Contributions::Total) * self.volume) + } + + /// Pressure $p$ evaluated for each contribution of the equation of state. + pub fn pressure_contributions(&self) -> Vec<(String, SINumber)> { + let new_state = self.derive1(DV); + let contributions = self.eos.evaluate_residual_contributions(&new_state); + let mut res = Vec::with_capacity(contributions.len() + 1); + res.push(( + "Ideal gas".into(), + self.density * SIUnit::gas_constant() * self.temperature, + )); + for (s, v) in contributions { + res.push(( + s, + -(v * new_state.temperature).eps * SIUnit::reference_pressure(), + )); + } + res + } + + // entropy derivatives + + /// Partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial S^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn ds_res_dt(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::Second(DT)) + } + + /// Second partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial^2S^\text{res}}{\partial T^2}\right)_{V,N_i}$ + pub fn d2s_res_dt2(&self) -> SINumber { + -self.get_or_compute_derivative_residual(PartialDerivative::Third(DT)) + } + + /// Partial derivative of chemical potential w.r.t. temperature: $\left(\frac{\partial\mu_i}{\partial T}\right)_{V,N_i}$ + pub fn dmu_res_dt(&self) -> SIArray1 { + SIArray::from_shape_fn(self.eos.components(), |i| { + self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DT, DN(i))) + }) + } + + /// Logarithm of the fugacity coefficient: $\ln\varphi_i=\beta\mu_i^\mathrm{res}\left(T,p,\lbrace N_i\rbrace\right)$ + pub fn ln_phi(&self) -> Array1 { + (self.residual_chemical_potential() / (SIUnit::gas_constant() * self.temperature)) + .into_value() + .unwrap() + - self.compressibility(Contributions::Total).ln() + } + + /// Logarithm of the fugacity coefficient of all components treated as pure substance at mixture temperature and pressure. + pub fn ln_phi_pure_liquid(&self) -> EosResult> { + let pressure = self.pressure(Contributions::Total); + (0..self.eos.components()) + .map(|i| { + let eos = Arc::new(self.eos.subset(&[i])); + let state = Self::new_npt( + &eos, + self.temperature, + pressure, + &(arr1(&[1.0]) * SIUnit::reference_moles()), + crate::DensityInitialization::Liquid, + )?; + Ok(state.ln_phi()[0]) + }) + .collect() + } + + /// Activity coefficient $\ln \gamma_i = \ln \varphi_i(T, p, \mathbf{N}) - \ln \varphi_i(T, p)$ + pub fn ln_symmetric_activity_coefficient(&self) -> EosResult> { + match self.eos.components() { + 1 => Ok(arr1(&[0.0])), + _ => Ok(self.ln_phi() - &self.ln_phi_pure_liquid()?), + } + } + + /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. temperature: $\left(\frac{\partial\ln\varphi_i}{\partial T}\right)_{p,N_i}$ + pub fn dln_phi_dt(&self) -> SIArray1 { + let vi = self.partial_molar_volume(); + (self.dmu_res_dt() + - self.residual_chemical_potential() / self.temperature + - vi * self.dp_dt(Contributions::Total)) + / (SIUnit::gas_constant() * self.temperature) + + 1.0 / self.temperature + } + + /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. pressure: $\left(\frac{\partial\ln\varphi_i}{\partial p}\right)_{T,N_i}$ + pub fn dln_phi_dp(&self) -> SIArray1 { + self.partial_molar_volume() / (SIUnit::gas_constant() * self.temperature) + - 1.0 / self.pressure(Contributions::Total) + } + + /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. moles: $\left(\frac{\partial\ln\varphi_i}{\partial N_j}\right)_{T,p,N_k}$ + pub fn dln_phi_dnj(&self) -> SIArray2 { + let n = self.eos.components(); + let dmu_dni = self.dmu_dni(Contributions::Residual); + let dp_dni = self.dp_dni(Contributions::Total); + let dp_dv = self.dp_dv(Contributions::Total); + let dp_dn_2 = SIArray::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j)); + (dmu_dni + dp_dn_2 / dp_dv) / (SIUnit::gas_constant() * self.temperature) + + 1.0 / self.total_moles + } + + /// Thermodynamic factor: $\Gamma_{ij}=\delta_{ij}+x_i\left(\frac{\partial\ln\varphi_i}{\partial x_j}\right)_{T,p,\Sigma}$ + pub fn thermodynamic_factor(&self) -> Array2 { + let dln_phi_dnj = self + .dln_phi_dnj() + .to_reduced(SIUnit::reference_moles().powi(-1)) + .unwrap(); + let moles = self.moles.to_reduced(SIUnit::reference_moles()).unwrap(); + let n = self.eos.components() - 1; + Array2::from_shape_fn((n, n), |(i, j)| { + moles[i] * (dln_phi_dnj[[i, j]] - dln_phi_dnj[[i, n]]) + if i == j { 1.0 } else { 0.0 } + }) + } + + /// Molar residual isochoric heat capacity: $c_v^\text{res}=\left(\frac{\partial u^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn c_v_res(&self) -> SINumber { + self.temperature * self.ds_res_dt() / self.total_moles + } + + /// Partial derivative of the molar residual isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn dc_v_res_dt(&self) -> SINumber { + (self.temperature * self.d2s_res_dt2() + self.ds_res_dt()) / self.total_moles + } + + /// Molar residual isobaric heat capacity: $c_p^\text{res}=\left(\frac{\partial h^\text{res}}{\partial T}\right)_{p,N_i}$ + pub fn c_p_res(&self) -> SINumber { + self.temperature / self.total_moles + * (self.ds_res_dt() + - self.dp_dt(Contributions::Total).powi(2) / self.dp_dv(Contributions::Total)) + - SIUnit::gas_constant() + } + + /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+p^\text{res}V$ + pub fn residual_enthalpy(&self) -> SINumber { + self.temperature * self.residual_entropy() + + self.residual_helmholtz_energy() + + self.pressure(Contributions::Residual) * self.volume + } + + /// Residual internal energy: $U^\text{res}(T,V,\mathbf{n})=A^\text{res}+TS^\text{res}$ + pub fn residual_internal_energy(&self) -> SINumber { + self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() + } + + /// Residual Gibbs energy: $G^\text{res}(T,p,\mathbf{n})=A^\text{res}+p^\text{res}V-NRT \ln Z$ + pub fn residual_gibbs_energy(&self) -> SINumber { + self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy() + - self.total_moles + * SIUnit::gas_constant() + * self.temperature + * self.compressibility(Contributions::Total).ln() + } +} + +/// # Transport properties +/// +/// These properties are available for equations of state +/// that implement the [EntropyScaling] trait. +impl State { + /// Return the viscosity via entropy scaling. + pub fn viscosity(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .viscosity_reference(self.temperature, self.volume, &self.moles)? + * self.eos.viscosity_correlation(s, &self.molefracs)?.exp()) + } + + /// Return the logarithm of the reduced viscosity. + /// + /// This term equals the viscosity correlation function + /// that is used for entropy scaling. + pub fn ln_viscosity_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos.viscosity_correlation(s, &self.molefracs) + } + + /// Return the viscosity reference as used in entropy scaling. + pub fn viscosity_reference(&self) -> EosResult { + self.eos + .viscosity_reference(self.temperature, self.volume, &self.moles) + } + + /// Return the diffusion via entropy scaling. + pub fn diffusion(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .diffusion_reference(self.temperature, self.volume, &self.moles)? + * self.eos.diffusion_correlation(s, &self.molefracs)?.exp()) + } + + /// Return the logarithm of the reduced diffusion. + /// + /// This term equals the diffusion correlation function + /// that is used for entropy scaling. + pub fn ln_diffusion_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos.diffusion_correlation(s, &self.molefracs) + } + + /// Return the diffusion reference as used in entropy scaling. + pub fn diffusion_reference(&self) -> EosResult { + self.eos + .diffusion_reference(self.temperature, self.volume, &self.moles) + } + + /// Return the thermal conductivity via entropy scaling. + pub fn thermal_conductivity(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + Ok(self + .eos + .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)? + * self + .eos + .thermal_conductivity_correlation(s, &self.molefracs)? + .exp()) + } + + /// Return the logarithm of the reduced thermal conductivity. + /// + /// This term equals the thermal conductivity correlation function + /// that is used for entropy scaling. + pub fn ln_thermal_conductivity_reduced(&self) -> EosResult { + let s = (self.residual_entropy() / self.total_moles) + .to_reduced(SIUnit::reference_molar_entropy())?; + self.eos + .thermal_conductivity_correlation(s, &self.molefracs) + } + + /// Return the thermal conductivity reference as used in entropy scaling. + pub fn thermal_conductivity_reference(&self) -> EosResult { + self.eos + .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) + } +} diff --git a/feos-core/src/state/statevec.rs b/feos-core/src/state/statevec.rs new file mode 100644 index 000000000..cb752fe63 --- /dev/null +++ b/feos-core/src/state/statevec.rs @@ -0,0 +1,103 @@ +use super::{Contributions, State}; +use crate::equation_of_state::{IdealGas, MolarWeight, Residual}; +use ndarray::{Array1, Array2}; +use quantity::si::{SIArray1, SIArray2}; +use std::iter::FromIterator; +use std::ops::Deref; + +/// A list of states for a simple access to properties +/// of multiple states. +pub struct StateVec<'a, E>(pub Vec<&'a State>); + +impl<'a, E> FromIterator<&'a State> for StateVec<'a, E> { + fn from_iter>>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl<'a, E> IntoIterator for StateVec<'a, E> { + type Item = &'a State; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a, E> Deref for StateVec<'a, E> { + type Target = Vec<&'a State>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a, E: Residual> StateVec<'a, E> { + pub fn temperature(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].temperature) + } + + pub fn pressure(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].pressure(Contributions::Total)) + } + + pub fn compressibility(&self) -> Array1 { + Array1::from_shape_fn(self.0.len(), |i| { + self.0[i].compressibility(Contributions::Total) + }) + } + + pub fn density(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].density) + } + + pub fn moles(&self) -> SIArray2 { + SIArray2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].moles.get(j) + }) + } + + pub fn molefracs(&self) -> Array2 { + Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].molefracs[j] + }) + } +} + +impl<'a, E: Residual + IdealGas> StateVec<'a, E> { + pub fn molar_enthalpy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].molar_enthalpy(Contributions::Total) + }) + } + + pub fn molar_entropy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].molar_entropy(Contributions::Total) + }) + } +} + +impl<'a, E: Residual + IdealGas + MolarWeight> StateVec<'a, E> { + pub fn mass_density(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) + } + + pub fn massfracs(&self) -> Array2 { + Array2::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { + self.0[i].massfracs()[j] + }) + } + + pub fn specific_enthalpy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].specific_enthalpy(Contributions::Total) + }) + } + + pub fn specific_entropy(&self) -> SIArray1 { + SIArray1::from_shape_fn(self.0.len(), |i| { + self.0[i].specific_entropy(Contributions::Total) + }) + } +} diff --git a/feos-derive/src/components.rs b/feos-derive/src/components.rs new file mode 100644 index 000000000..aac441690 --- /dev/null +++ b/feos-derive/src/components.rs @@ -0,0 +1,59 @@ +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn expand_components(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let components = impl_components(input.ident, variants); + Ok(quote! { + #components + }) +} + +fn impl_components( + ident: syn::Ident, + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let components = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name(n) => *n + } + } else { + quote! { + Self::#name(residual) => residual.components() + } + } + }); + let subset = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name(n) => Self::#name(component_list.len()) + } + } else { + quote! { + Self::#name(residual) => Self::#name(residual.subset(component_list)) + } + } + }); + + quote! { + impl Components for #ident { + fn components(&self) -> usize { + match self { + #(#components,)* + } + } + fn subset(&self, component_list: &[usize]) -> Self { + match self { + #(#subset,)* + } + } + } + } +} diff --git a/feos-derive/src/dft.rs b/feos-derive/src/dft.rs index 00ce3788d..647ef5110 100644 --- a/feos-derive/src/dft.rs +++ b/feos-derive/src/dft.rs @@ -82,12 +82,6 @@ fn impl_from( fn impl_helmholtz_energy_functional( variants: &syn::punctuated::Punctuated, ) -> syn::Result { - let subset = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(functional) => functional.subset(component_list).into() - } - }); let molecule_shape = variants.iter().map(|v| { let name = &v.ident; quote! { @@ -106,12 +100,6 @@ fn impl_helmholtz_energy_functional( Self::#name(functional) => functional.contributions() } }); - let ideal_gas = variants.iter().map(|v| { - let name = &v.ident; - quote! { - Self::#name(functional) => functional.ideal_gas() - } - }); let mut bond_lengths = Vec::new(); for v in variants.iter() { @@ -125,11 +113,6 @@ fn impl_helmholtz_energy_functional( Ok(quote! { impl HelmholtzEnergyFunctional for FunctionalVariant { - fn subset(&self, component_list: &[usize]) -> DFT { - match self { - #(#subset,)* - } - } fn molecule_shape(&self) -> MoleculeShape { match self { #(#molecule_shape,)* @@ -145,11 +128,6 @@ fn impl_helmholtz_energy_functional( #(#contributions,)* } } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match self { - #(#ideal_gas,)* - } - } fn bond_lengths(&self, temperature: f64) -> UnGraph<(), f64> { match self { #(#bond_lengths,)* diff --git a/feos-derive/src/ideal_gas.rs b/feos-derive/src/ideal_gas.rs new file mode 100644 index 000000000..69f8d9b63 --- /dev/null +++ b/feos-derive/src/ideal_gas.rs @@ -0,0 +1,41 @@ +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn expand_ideal_gas(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let ideal_gas = impl_ideal_gas(variants); + Ok(quote! { + #ideal_gas + }) +} + +fn impl_ideal_gas( + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let ideal_gas_model = variants.iter().map(|v| { + let name = &v.ident; + if name == "NoModel" { + quote! { + Self::#name(_) => panic!("No ideal gas model initialized!") + } + } else { + quote! { + Self::#name(ideal_gas) => ideal_gas.ideal_gas_model() + } + } + }); + + quote! { + impl IdealGas for IdealGasModel { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + match self { + #(#ideal_gas_model,)* + } + } + } + } +} diff --git a/feos-derive/src/lib.rs b/feos-derive/src/lib.rs index 667b93ec6..e91a82b76 100644 --- a/feos-derive/src/lib.rs +++ b/feos-derive/src/lib.rs @@ -1,13 +1,17 @@ //! This crate provides derive macros used for the EosVariant and //! FunctionalVariant enums in FeOs. The macros implement //! the boilerplate for the EquationOfState and HelmholtzEnergyFunctional traits. +use components::expand_components; use dft::expand_helmholtz_energy_functional; -use eos::expand_equation_of_state; +use ideal_gas::expand_ideal_gas; use proc_macro::TokenStream; +use residual::expand_residual; use syn::{parse_macro_input, DeriveInput}; +mod components; mod dft; -mod eos; +mod ideal_gas; +mod residual; fn implement(name: &str, variant: &syn::Variant, opts: &[&'static str]) -> syn::Result { let syn::Variant { attrs, .. } = variant; @@ -43,10 +47,26 @@ fn implement(name: &str, variant: &syn::Variant, opts: &[&'static str]) -> syn:: implement } -#[proc_macro_derive(EquationOfState, attributes(implement))] -pub fn derive_equation_of_state(input: TokenStream) -> TokenStream { +#[proc_macro_derive(Components)] +pub fn derive_components(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_equation_of_state(input) + expand_components(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro_derive(IdealGas)] +pub fn derive_ideal_gas(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand_ideal_gas(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro_derive(Residual, attributes(implement))] +pub fn derive_residual(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand_residual(input) .unwrap_or_else(syn::Error::into_compile_error) .into() } diff --git a/feos-derive/src/residual.rs b/feos-derive/src/residual.rs new file mode 100644 index 000000000..6dd9476d6 --- /dev/null +++ b/feos-derive/src/residual.rs @@ -0,0 +1,175 @@ +use super::implement; +use quote::quote; +use syn::DeriveInput; + +// possible additional traits to implement +const OPT_IMPLS: [&str; 2] = ["molar_weight", "entropy_scaling"]; + +pub(crate) fn expand_residual(input: DeriveInput) -> syn::Result { + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => variants, + _ => panic!("this derive macro only works on enums"), + }; + + let residual = impl_residual(variants); + let molar_weight = impl_molar_weight(variants)?; + let entropy_scaling = impl_entropy_scaling(variants)?; + Ok(quote! { + #residual + #molar_weight + #entropy_scaling + }) +} + +fn impl_residual( + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let compute_max_density = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => residual.compute_max_density(moles) + } + }); + let contributions = variants.iter().map(|v| { + let name = &v.ident; + quote! { + Self::#name(residual) => residual.contributions() + } + }); + + quote! { + impl Residual for ResidualModel { + fn compute_max_density(&self, moles: &Array1) -> f64 { + match self { + #(#compute_max_density,)* + } + } + fn contributions(&self) -> &[Box] { + match self { + #(#contributions,)* + } + } + } + } +} + +fn impl_molar_weight( + variants: &syn::punctuated::Punctuated, +) -> syn::Result { + let mut molar_weight = Vec::new(); + + for v in variants.iter() { + if implement("molar_weight", v, &OPT_IMPLS)? { + let name = &v.ident; + molar_weight.push(quote! { + Self::#name(residual) => residual.molar_weight() + }); + } + } + Ok(quote! { + impl MolarWeight for ResidualModel { + fn molar_weight(&self) -> SIArray1 { + match self { + #(#molar_weight,)* + _ => unimplemented!() + } + } + } + }) +} + +fn impl_entropy_scaling( + variants: &syn::punctuated::Punctuated, +) -> syn::Result { + let mut etar = Vec::new(); + let mut etac = Vec::new(); + let mut dr = Vec::new(); + let mut dc = Vec::new(); + let mut thcr = Vec::new(); + let mut thcc = Vec::new(); + + for v in variants.iter() { + if implement("entropy_scaling", v, &OPT_IMPLS)? { + let name = &v.ident; + etar.push(quote! { + Self::#name(eos) => eos.viscosity_reference(temperature, volume, moles) + }); + etac.push(quote! { + Self::#name(eos) => eos.viscosity_correlation(s_res, x) + }); + dr.push(quote! { + Self::#name(eos) => eos.diffusion_reference(temperature, volume, moles) + }); + dc.push(quote! { + Self::#name(eos) => eos.diffusion_correlation(s_res, x) + }); + thcr.push(quote! { + Self::#name(eos) => eos.thermal_conductivity_reference(temperature, volume, moles) + }); + thcc.push(quote! { + Self::#name(eos) => eos.thermal_conductivity_correlation(s_res, x) + }); + } + } + + Ok(quote! { + impl EntropyScaling for ResidualModel { + fn viscosity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#etar,)* + _ => unimplemented!(), + } + } + + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#etac,)* + _ => unimplemented!(), + } + } + + fn diffusion_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#dr,)* + _ => unimplemented!(), + } + } + + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#dc,)* + _ => unimplemented!(), + } + } + + fn thermal_conductivity_reference( + &self, + temperature: SINumber, + volume: SINumber, + moles: &SIArray1, + ) -> EosResult { + match self { + #(#thcr,)* + _ => unimplemented!(), + } + } + + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + match self { + #(#thcc,)* + _ => unimplemented!(), + } + } + } + }) +} diff --git a/feos-dft/CHANGELOG.md b/feos-dft/CHANGELOG.md index 9a8fd9824..1464fdf1e 100644 --- a/feos-dft/CHANGELOG.md +++ b/feos-dft/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Implemented `HelmholtzEnergyFunctional` for `EquationOfState` to be able to use functionals as equations of state. [#158](https://github.com/feos-org/feos/pull/158) + +### Changed +- `HelmholtzEnergyFunctional`: added `Components` trait as trait bound and removed `ideal_gas` method. [#158](https://github.com/feos-org/feos/pull/158) +- `DFT` now implements `Residual` and furthermore `IdealGas` if `F` implements `IdealGas`. [#158](https://github.com/feos-org/feos/pull/158) +- What properties (and contributions) of `DFTProfile` are available now depends on whether an ideal gas model is provided or not. [#158](https://github.com/feos-org/feos/pull/158) + +### Removed +- Removed `DefaultIdealGasContribution` [#158](https://github.com/feos-org/feos/pull/158) +- Removed getters for `chemical_potential` (for profiles) and `molar_gibbs_energy` (for `Adsorption1D` and `Adsorption3D`) from Python interface. [#158](https://github.com/feos-org/feos/pull/158) + ### Packaging - Updated `num-dual` dependency to 0.7. [#137](https://github.com/feos-org/feos/pull/137) diff --git a/feos-dft/src/adsorption/mod.rs b/feos-dft/src/adsorption/mod.rs index 3f517a079..b79227403 100644 --- a/feos-dft/src/adsorption/mod.rs +++ b/feos-dft/src/adsorption/mod.rs @@ -2,7 +2,7 @@ use super::functional::{HelmholtzEnergyFunctional, DFT}; use super::solver::DFTSolver; use feos_core::{ - Contributions, DensityInitialization, EosError, EosResult, EosUnit, EquationOfState, + Components, Contributions, DensityInitialization, EosError, EosResult, EosUnit, Residual, SolverOptions, State, StateBuilder, }; use ndarray::{Array1, Dimension, Ix1, Ix3, RemoveAxis}; @@ -273,36 +273,29 @@ where .moles(&moles) .vapor() .build()?; - let liquid_bulk = StateBuilder::new(functional) + let bulk_init = StateBuilder::new(functional) .temperature(temperature) .pressure(p_max) .moles(&moles) .liquid() .build()?; + let liquid_bulk = StateBuilder::new(functional) + .temperature(temperature) + .pressure(p_max) + .moles(&moles) + .vapor() + .build()?; let mut vapor = pore.initialize(&vapor_bulk, None, None)?.solve(None)?; - let mut liquid = pore.initialize(&liquid_bulk, None, None)?.solve(solver)?; + let mut liquid = pore.initialize(&bulk_init, None, None)?.solve(solver)?; // calculate initial value for the molar gibbs energy - let nv = vapor.profile.bulk.density - * (vapor.profile.moles() - * vapor - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let nl = liquid.profile.bulk.density - * (liquid.profile.moles() - * liquid - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let f = |s: &PoreProfile, n: SINumber| -> EosResult<_> { - Ok(s.grand_potential.unwrap() - + s.profile.bulk.molar_gibbs_energy(Contributions::Total) * n) - }; - let mut g = (f(&liquid, nl)? - f(&vapor, nv)?) / (nl - nv); + let n_dp_drho_v = (vapor.profile.moles() * vapor_bulk.dp_drho(Contributions::Total)).sum(); + let n_dp_drho_l = + (liquid.profile.moles() * liquid_bulk.dp_drho(Contributions::Total)).sum(); + let mut rho = (vapor.grand_potential.unwrap() + n_dp_drho_v + - (liquid.grand_potential.unwrap() + n_dp_drho_l)) + / (n_dp_drho_v / vapor_bulk.density - n_dp_drho_l / liquid_bulk.density); // update filled pore with limited step size let mut bulk = StateBuilder::new(functional) @@ -311,12 +304,12 @@ where .moles(&moles) .vapor() .build()?; - let g_liquid = liquid.profile.bulk.molar_gibbs_energy(Contributions::Total); - let steps = (10.0 * (g - g_liquid)).to_reduced(g_liquid)?.abs().ceil() as usize; - let delta_g = (g - g_liquid) / steps as f64; + let rho0 = liquid_bulk.density; + let steps = (10.0 * (rho - rho0)).to_reduced(rho0)?.abs().ceil() as usize; + let delta_rho = (rho - rho0) / steps as f64; for i in 1..=steps { - let g_i = g_liquid + i as f64 * delta_g; - bulk = bulk.update_gibbs_energy(g_i)?; + let rho_i = rho0 + i as f64 * delta_rho; + bulk = State::new_nvt(functional, temperature, moles.sum() / rho_i, &moles)?; liquid = liquid.update_bulk(&bulk).solve(solver)?; } @@ -328,30 +321,15 @@ where liquid = liquid.update_bulk(&bulk).solve(solver)?; // calculate moles - let nv = vapor.profile.bulk.density - * (vapor.profile.moles() - * vapor - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - let nl = liquid.profile.bulk.density - * (liquid.profile.moles() - * liquid - .profile - .bulk - .partial_molar_volume(Contributions::Total)) - .sum(); - - // check for a trivial solution - if nl.to_reduced(nv)? - 1.0 < 1e-5 { - return Err(EosError::TrivialSolution); - } + let n_dp_drho = ((liquid.profile.moles() - vapor.profile.moles()) + * bulk.dp_drho(Contributions::Total)) + .sum(); // Newton step - let delta_g = - (vapor.grand_potential.unwrap() - liquid.grand_potential.unwrap()) / (nv - nl); - if delta_g.to_reduced(SIUnit::reference_molar_energy())?.abs() + let delta_rho = (liquid.grand_potential.unwrap() - vapor.grand_potential.unwrap()) + / n_dp_drho + * bulk.density; + if delta_rho.to_reduced(SIUnit::reference_density())?.abs() < options.tol.unwrap_or(TOL_ADSORPTION_EQUILIBRIUM) { return Ok(Adsorption::new( @@ -360,10 +338,10 @@ where vec![Ok(vapor), Ok(liquid)], )); } - g += delta_g; + rho += delta_rho; // update bulk phase - bulk = bulk.update_gibbs_energy(g)?; + bulk = State::new_nvt(functional, temperature, moles.sum() / rho, &moles)?; } Err(EosError::NotConverged( "Adsorption::phase_equilibrium".into(), @@ -390,26 +368,6 @@ where }) } - pub fn molar_gibbs_energy(&self) -> SIArray1 { - SIArray1::from_shape_fn(self.profiles.len(), |i| match &self.profiles[i] { - Ok(p) => { - if p.profile.bulk.eos.components() > 1 - && !p.profile.bulk.is_stable(SolverOptions::default()).unwrap() - { - p.profile - .bulk - .tp_flash(None, SolverOptions::default(), None) - .unwrap() - .vapor() - .molar_gibbs_energy(Contributions::Total) - } else { - p.profile.bulk.molar_gibbs_energy(Contributions::Total) - } - } - Err(_) => f64::NAN * SIUnit::reference_molar_energy(), - }) - } - pub fn adsorption(&self) -> SIArray2 { SIArray2::from_shape_fn((self.components, self.profiles.len()), |(j, i)| match &self .profiles[i] diff --git a/feos-dft/src/adsorption/pore.rs b/feos-dft/src/adsorption/pore.rs index 5f3ac4501..713f6efae 100644 --- a/feos-dft/src/adsorption/pore.rs +++ b/feos-dft/src/adsorption/pore.rs @@ -5,7 +5,7 @@ use crate::functional_contribution::FunctionalContribution; use crate::geometry::{Axis, Geometry, Grid}; use crate::profile::{DFTProfile, MAX_POTENTIAL}; use crate::solver::DFTSolver; -use feos_core::{Contributions, EosResult, EosUnit, State, StateBuilder}; +use feos_core::{Components, Contributions, EosResult, EosUnit, State, StateBuilder}; use ndarray::prelude::*; use ndarray::Axis as Axis_nd; use ndarray::RemoveAxis; @@ -264,6 +264,7 @@ fn external_potential_1d( const EPSILON_HE: f64 = 10.9; const SIGMA_HE: f64 = 2.64; +#[derive(Clone)] struct Helium { epsilon: Array1, sigma: Array1, @@ -273,7 +274,17 @@ impl Helium { fn new() -> DFT { let epsilon = arr1(&[EPSILON_HE]); let sigma = arr1(&[SIGMA_HE]); - (Self { epsilon, sigma }).into() + DFT(Self { epsilon, sigma }) + } +} + +impl Components for Helium { + fn components(&self) -> usize { + 1 + } + + fn subset(&self, _: &[usize]) -> Self { + self.clone() } } @@ -282,10 +293,6 @@ impl HelmholtzEnergyFunctional for Helium { &[] } - fn subset(&self, _: &[usize]) -> DFT { - Self::new() - } - fn compute_max_density(&self, _: &Array1) -> f64 { 1.0 } diff --git a/feos-dft/src/functional.rs b/feos-dft/src/functional.rs index 430b6e608..9cd3d4e5c 100644 --- a/feos-dft/src/functional.rs +++ b/feos-dft/src/functional.rs @@ -1,35 +1,61 @@ +use crate::adsorption::FluidParameters; use crate::convolver::Convolver; use crate::functional_contribution::*; use crate::ideal_chain_contribution::IdealChainContribution; +use crate::solvation::PairPotential; use crate::weight_functions::{WeightFunction, WeightFunctionInfo, WeightFunctionShape}; use feos_core::{ - Contributions, EosResult, EosUnit, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual, - IdealGasContribution, IdealGasContributionDual, MolarWeight, StateHD, + Components, DeBroglieWavelength, EosResult, EquationOfState, HelmholtzEnergy, + HelmholtzEnergyDual, IdealGas, MolarWeight, Residual, StateHD, }; use ndarray::*; use num_dual::*; use petgraph::graph::{Graph, UnGraph}; use petgraph::visit::EdgeRef; use petgraph::Directed; -// use quantity::{QuantityArray, SIArray1, SINumber}; -use quantity::si::{SIArray, SIArray1, SINumber, SIUnit}; +use quantity::si::SIArray1; use std::borrow::Cow; -use std::fmt; -use std::ops::{AddAssign, Deref, MulAssign}; +use std::ops::{Deref, MulAssign}; use std::sync::Arc; +impl HelmholtzEnergyFunctional + for EquationOfState +{ + fn contributions(&self) -> &[Box] { + self.residual.contributions() + } + + fn molecule_shape(&self) -> MoleculeShape { + self.residual.molecule_shape() + } + + fn compute_max_density(&self, moles: &Array1) -> f64 { + self.residual.compute_max_density(moles) + } +} + +impl PairPotential for EquationOfState { + fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Array2 { + self.residual.pair_potential(i, r, temperature) + } +} + +impl FluidParameters for EquationOfState { + fn epsilon_k_ff(&self) -> Array1 { + self.residual.epsilon_k_ff() + } + + fn sigma_ff(&self) -> &Array1 { + self.residual.sigma_ff() + } +} + /// Wrapper struct for the [HelmholtzEnergyFunctional] trait. /// -/// Needed (for now) to generically implement the `EquationOfState` +/// Needed (for now) to generically implement the `Residual` /// trait for Helmholtz energy functionals. #[derive(Clone)] -pub struct DFT(F); - -impl From for DFT { - fn from(functional: F) -> Self { - Self(functional) - } -} +pub struct DFT(pub F); impl DFT { pub fn into>(self) -> DFT { @@ -44,39 +70,34 @@ impl Deref for DFT { } } -impl MolarWeight for DFT { - fn molar_weight(&self) -> SIArray1 { - (self as &T).molar_weight() - } -} - -struct DefaultIdealGasContribution(); -impl + Copy> IdealGasContributionDual for DefaultIdealGasContribution { - fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1 { - Array1::zeros(components) +impl DFT { + pub fn ideal_gas(self, ideal_gas: I) -> DFT> { + DFT(EquationOfState::new(Arc::new(ideal_gas), Arc::new(self.0))) } } -impl fmt::Display for DefaultIdealGasContribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (default)") +impl MolarWeight for DFT { + fn molar_weight(&self) -> SIArray1 { + self.0.molar_weight() } } -impl EquationOfState for DFT { +impl Components for DFT { fn components(&self) -> usize { - self.component_index()[self.component_index().len() - 1] + 1 + self.0.components() } fn subset(&self, component_list: &[usize]) -> Self { - (self as &T).subset(component_list) + Self(self.0.subset(component_list)) } +} +impl Residual for DFT { fn compute_max_density(&self, moles: &Array1) -> f64 { - (self as &T).compute_max_density(moles) + self.0.compute_max_density(moles) } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { unreachable!() } @@ -84,7 +105,8 @@ impl EquationOfState for DFT { where dyn HelmholtzEnergy: HelmholtzEnergyDual, { - self.contributions() + self.0 + .contributions() .iter() .map(|c| (c as &dyn HelmholtzEnergy).helmholtz_energy(state)) .sum::() @@ -99,6 +121,7 @@ impl EquationOfState for DFT { dyn HelmholtzEnergy: HelmholtzEnergyDual, { let mut res: Vec<(String, D)> = self + .0 .contributions() .iter() .map(|c| { @@ -114,9 +137,11 @@ impl EquationOfState for DFT { )); res } +} - fn ideal_gas(&self) -> &dyn IdealGasContribution { - (self as &T).ideal_gas() +impl IdealGas for DFT { + fn ideal_gas_model(&self) -> &dyn DeBroglieWavelength { + self.0.ideal_gas_model() } } @@ -133,16 +158,13 @@ pub enum MoleculeShape<'a> { } /// A general Helmholtz energy functional. -pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { +pub trait HelmholtzEnergyFunctional: Components + Sized + Send + Sync { /// Return a slice of [FunctionalContribution]s. fn contributions(&self) -> &[Box]; /// Return the shape of the molecules and the necessary specifications. fn molecule_shape(&self) -> MoleculeShape; - /// Return a functional for the specified subset of components. - fn subset(&self, component_list: &[usize]) -> DFT; - /// Return the maximum density in Angstrom^-3. /// /// This value is used as an estimate for a liquid phase for phase @@ -151,18 +173,6 @@ pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { /// equation of state anyways). fn compute_max_density(&self, moles: &Array1) -> f64; - /// Return the ideal gas contribution. - /// - /// Per default this function returns an ideal gas contribution - /// in which the de Broglie wavelength is 1 for every component. - /// Therefore, the correct ideal gas pressure is obtained even - /// with no explicit ideal gas term. If a more detailed model is - /// required (e.g. for the calculation of internal energies) this - /// function has to be overwritten. - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &DefaultIdealGasContribution() - } - /// Overwrite this, if the functional consists of heterosegmented chains. fn bond_lengths(&self, _temperature: f64) -> UnGraph<(), f64> { Graph::with_capacity(0, 0) @@ -196,218 +206,10 @@ pub trait HelmholtzEnergyFunctional: Sized + Send + Sync { fn ideal_chain_contribution(&self) -> IdealChainContribution { IdealChainContribution::new(&self.component_index(), &self.m()) } -} - -impl DFT { - /// Calculate the grand potential density $\omega$. - pub fn grand_potential_density( - &self, - temperature: SINumber, - density: &SIArray, - convolver: &Arc>, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - // Calculate residual Helmholtz energy density and functional derivative - let t = temperature.to_reduced(SIUnit::reference_temperature())?; - let rho = density.to_reduced(SIUnit::reference_density())?; - let (mut f, dfdrho) = self.functional_derivative(t, &rho, convolver)?; - - // Calculate the grand potential density - for ((rho, dfdrho), &m) in rho - .outer_iter() - .zip(dfdrho.outer_iter()) - .zip(self.m().iter()) - { - f -= &((&dfdrho + m) * &rho); - } - - let bond_lengths = self.bond_lengths(t); - for segment in bond_lengths.node_indices() { - let n = bond_lengths.neighbors(segment).count(); - f += &(&rho.index_axis(Axis(0), segment.index()) * (0.5 * n as f64)); - } - - Ok(f * t * SIUnit::reference_pressure()) - } - - pub(crate) fn ideal_gas_contribution( - &self, - temperature: f64, - density: &Array, - ) -> Array - where - D: Dimension, - D::Larger: Dimension, - { - let n = self.components(); - let ig = self.ideal_gas(); - let lambda = ig.de_broglie_wavelength(temperature, n); - let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); - for (i, rhoi) in density.outer_iter().enumerate() { - phi += &rhoi.mapv(|rhoi| (rhoi.ln() + lambda[i] - 1.0) * rhoi); - } - phi * temperature - } - - fn ideal_gas_contribution_dual( - &self, - temperature: Dual64, - density: &Array, - ) -> Array - where - D: Dimension, - D::Larger: Dimension, - { - let n = self.components(); - let ig = self.ideal_gas(); - let lambda = ig.de_broglie_wavelength(temperature, n); - let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); - for (i, rhoi) in density.outer_iter().enumerate() { - phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); - } - phi * temperature - } - - fn intrinsic_helmholtz_energy_density( - &self, - temperature: N, - density: &Array, - convolver: &Arc>, - ) -> EosResult> - where - N: DualNum + Copy + ScalarOperand, - dyn FunctionalContribution: FunctionalContributionDual, - D: Dimension, - D::Larger: Dimension, - { - let density_dual = density.mapv(N::from); - let weighted_densities = convolver.weighted_densities(&density_dual); - let functional_contributions = self.contributions(); - let mut helmholtz_energy_density: Array = self - .ideal_chain_contribution() - .calculate_helmholtz_energy_density(&density.mapv(N::from))?; - for (c, wd) in functional_contributions.iter().zip(weighted_densities) { - let nwd = wd.shape()[0]; - let ngrid = wd.len() / nwd; - helmholtz_energy_density - .view_mut() - .into_shape(ngrid) - .unwrap() - .add_assign(&c.calculate_helmholtz_energy_density( - temperature, - wd.into_shape((nwd, ngrid)).unwrap().view(), - )?); - } - Ok(helmholtz_energy_density * temperature) - } - - /// Calculate the entropy density $s$. - /// - /// Untested with heterosegmented functionals. - pub fn entropy_density( - &self, - temperature: f64, - density: &Array, - convolver: &Arc>, - contributions: Contributions, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - let temperature_dual = Dual64::from(temperature).derivative(); - let mut helmholtz_energy_density = - self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?; - match contributions { - Contributions::Total => { - helmholtz_energy_density += &self.ideal_gas_contribution_dual::(temperature_dual, density); - }, - Contributions::ResidualNpt|Contributions::IdealGas => panic!("Entropy density can only be calculated for Contributions::Residual or Contributions::Total"), - Contributions::ResidualNvt => (), - } - Ok(helmholtz_energy_density.mapv(|f| -f.eps)) - } - - /// Calculate the individual contributions to the entropy density. - /// - /// Untested with heterosegmented functionals. - pub fn entropy_density_contributions( - &self, - temperature: f64, - density: &Array, - convolver: &Arc>, - ) -> EosResult>> - where - D: Dimension, - D::Larger: Dimension, - ::Larger: Dimension, - { - let density_dual = density.mapv(Dual64::from); - let temperature_dual = Dual64::from(temperature).derivative(); - let weighted_densities = convolver.weighted_densities(&density_dual); - let functional_contributions = self.contributions(); - let mut helmholtz_energy_density: Vec> = - Vec::with_capacity(functional_contributions.len() + 1); - helmholtz_energy_density.push( - self.ideal_chain_contribution() - .calculate_helmholtz_energy_density(&density.mapv(Dual64::from))?, - ); - - for (c, wd) in functional_contributions.iter().zip(weighted_densities) { - let nwd = wd.shape()[0]; - let ngrid = wd.len() / nwd; - helmholtz_energy_density.push( - c.calculate_helmholtz_energy_density( - temperature_dual, - wd.into_shape((nwd, ngrid)).unwrap().view(), - )? - .into_shape(density.raw_dim().remove_axis(Axis(0))) - .unwrap(), - ); - } - Ok(helmholtz_energy_density - .iter() - .map(|v| v.mapv(|f| -(f * temperature_dual).eps)) - .collect()) - } - - /// Calculate the internal energy density $u$. - /// - /// Untested with heterosegmented functionals. - pub fn internal_energy_density( - &self, - temperature: f64, - density: &Array, - external_potential: &Array, - convolver: &Arc>, - contributions: Contributions, - ) -> EosResult> - where - D: Dimension, - D::Larger: Dimension, - { - let temperature_dual = Dual64::from(temperature).derivative(); - let mut helmholtz_energy_density_dual = - self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?; - match contributions { - Contributions::Total => { - helmholtz_energy_density_dual += &self.ideal_gas_contribution_dual::(temperature_dual, density); - }, - Contributions::ResidualNpt|Contributions::IdealGas => panic!("Internal energy density can only be calculated for Contributions::Residual or Contributions::Total"), - Contributions::ResidualNvt => (), - } - let helmholtz_energy_density = helmholtz_energy_density_dual - .mapv(|f| f.re - f.eps * temperature) - + (external_potential * density).sum_axis(Axis(0)) * temperature; - Ok(helmholtz_energy_density) - } - /// Calculate the (residual) functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. + /// Calculate the (residual) intrinsic functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. #[allow(clippy::type_complexity)] - pub fn functional_derivative( + fn functional_derivative( &self, temperature: f64, density: &Array, @@ -442,7 +244,7 @@ impl DFT { } #[allow(clippy::type_complexity)] - pub(crate) fn functional_derivative_dual( + fn functional_derivative_dual( &self, temperature: f64, density: &Array, @@ -479,7 +281,7 @@ impl DFT { } /// Calculate the bond integrals $I_{\alpha\alpha'}(\mathbf{r})$ - pub fn bond_integrals( + fn bond_integrals( &self, temperature: f64, exponential: &Array, diff --git a/feos-dft/src/lib.rs b/feos-dft/src/lib.rs index 2962fbeea..4323b2303 100644 --- a/feos-dft/src/lib.rs +++ b/feos-dft/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::suspicious_operation_groupings)] #![allow(clippy::too_many_arguments)] #![allow(clippy::new_ret_no_self)] +#![allow(deprecated)] pub mod adsorption; mod convolver; diff --git a/feos-dft/src/pdgt.rs b/feos-dft/src/pdgt.rs index 44dc07a9d..d9c138660 100644 --- a/feos-dft/src/pdgt.rs +++ b/feos-dft/src/pdgt.rs @@ -1,7 +1,7 @@ use super::functional::{HelmholtzEnergyFunctional, DFT}; use super::functional_contribution::FunctionalContribution; use super::weight_functions::WeightFunctionInfo; -use feos_core::{Contributions, EosResult, EosUnit, EquationOfState, PhaseEquilibrium}; +use feos_core::{Components, Contributions, EosResult, EosUnit, PhaseEquilibrium}; use ndarray::*; use num_dual::Dual2_64; use quantity::si::{SIArray1, SIArray2, SINumber, SIUnit}; @@ -167,20 +167,15 @@ impl DFT { .ideal_chain_contribution() .helmholtz_energy_density::(vle.vapor().temperature, &density)?; - let t = vle - .vapor() - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let rho = density.to_reduced(SIUnit::reference_density())?; - delta_omega += - &(self.ideal_gas_contribution::(t, &rho) * SIUnit::reference_pressure()); - // calculate excess grand potential density - let mu = vle.vapor().chemical_potential(Contributions::Total); + let mu_res = vle.vapor().residual_chemical_potential(); for i in 0..self.components() { - let rhoi = density.index_axis(Axis(0), i); - let mui = mu.get(i); - delta_omega -= &(&rhoi * mui); + let rhoi = density.index_axis(Axis(0), i).to_owned(); + let rhoi_b = vle.vapor().partial_density.get(i); + let mui_res = mu_res.get(i); + let kt = SIUnit::gas_constant() * vle.vapor().temperature; + delta_omega += + &(&rhoi * (kt * (rhoi.to_reduced(rhoi_b)?.mapv(f64::ln) - 1.0) - mui_res)); } delta_omega += vle.vapor().pressure(Contributions::Total); diff --git a/feos-dft/src/profile.rs b/feos-dft/src/profile/mod.rs similarity index 61% rename from feos-dft/src/profile.rs rename to feos-dft/src/profile/mod.rs index 1c8ae407e..9886923f2 100644 --- a/feos-dft/src/profile.rs +++ b/feos-dft/src/profile/mod.rs @@ -1,18 +1,16 @@ -use crate::convolver::{BulkConvolver, Convolver, ConvolverFFT}; +use crate::convolver::{BulkConvolver, Convolver}; use crate::functional::{HelmholtzEnergyFunctional, DFT}; use crate::geometry::Grid; use crate::solver::{DFTSolver, DFTSolverLog}; -use crate::weight_functions::WeightFunctionInfo; -use feos_core::{Contributions, EosError, EosResult, EosUnit, EquationOfState, State, Verbosity}; -use ndarray::{ - Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3, RemoveAxis, -}; -use num_dual::Dual64; -use quantity::si::{SIArray, SIArray1, SIArray2, SINumber, SIUnit}; +use feos_core::{Components, EosError, EosResult, EosUnit, State}; +use ndarray::{Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3}; +use quantity::si::{SIArray, SIArray1, SINumber, SIUnit}; use quantity::Quantity; use std::ops::MulAssign; use std::sync::Arc; +mod properties; + pub(crate) const MAX_POTENTIAL: f64 = 50.0; #[cfg(feature = "rayon")] pub(crate) const CUTOFF_RADIUS: f64 = 14.0; @@ -290,11 +288,6 @@ where pub fn total_moles(&self) -> SINumber { self.moles().sum() } - - /// Return the chemical potential of the system - pub fn chemical_potential(&self) -> SIArray1 { - self.bulk.chemical_potential(Contributions::Total) - } } impl Clone for DFTProfile { @@ -326,16 +319,6 @@ where .weighted_densities(&self.density.to_reduced(SIUnit::reference_density())?)) } - pub fn functional_derivative(&self) -> EosResult> { - let (_, dfdrho) = self.dft.functional_derivative( - self.temperature - .to_reduced(SIUnit::reference_temperature())?, - &self.density.to_reduced(SIUnit::reference_density())?, - &self.convolver, - )?; - Ok(dfdrho) - } - #[allow(clippy::type_complexity)] pub fn residual(&self, log: bool) -> EosResult<(Array, Array1, f64)> { // Read from profile @@ -470,194 +453,3 @@ where Ok(()) } } - -impl DFTProfile -where - D::Larger: Dimension, - D::Smaller: Dimension, - ::Larger: Dimension, -{ - pub fn entropy_density(&self, contributions: Contributions) -> EosResult> { - // initialize convolver - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); - - Ok(self.dft.entropy_density( - t, - &self.density.to_reduced(SIUnit::reference_density())?, - &convolver, - contributions, - )? * (SIUnit::reference_entropy() / SIUnit::reference_volume())) - } - - pub fn entropy(&self, contributions: Contributions) -> EosResult { - Ok(self.integrate(&self.entropy_density(contributions)?)) - } - - pub fn grand_potential_density(&self) -> EosResult> { - self.dft - .grand_potential_density(self.temperature, &self.density, &self.convolver) - } - - pub fn grand_potential(&self) -> EosResult { - Ok(self.integrate(&self.grand_potential_density()?)) - } - - pub fn internal_energy(&self, contributions: Contributions) -> EosResult { - // initialize convolver - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); - - let internal_energy_density = self.dft.internal_energy_density( - t, - &self.density.to_reduced(SIUnit::reference_density())?, - &self.external_potential, - &convolver, - contributions, - )? * SIUnit::reference_pressure(); - Ok(self.integrate(&internal_energy_density)) - } - - fn density_derivative(&self, lhs: &Array) -> EosResult> { - let rho = self.density.to_reduced(SIUnit::reference_density())?; - let partial_density = self - .bulk - .partial_density - .to_reduced(SIUnit::reference_density())?; - let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); - - let second_partial_derivatives = self.second_partial_derivatives(&rho)?; - let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &rho_bulk, false)?; - - let rhs = |x: &_| { - let delta_functional_derivative = - self.delta_functional_derivative(x, &second_partial_derivatives); - let mut xm = x.clone(); - xm.outer_iter_mut() - .zip(self.dft.m().iter()) - .for_each(|(mut x, &m)| x *= m); - let delta_i = self.delta_bond_integrals(&exp_dfdrho, &delta_functional_derivative); - xm + (delta_functional_derivative - delta_i) * &rho - }; - let mut log = DFTSolverLog::new(Verbosity::None); - Self::gmres(rhs, lhs, 200, 1e-13, &mut log) - } - - /// Return the partial derivatives of the density profiles w.r.t. the chemical potentials $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial\mu_k}\right)_T$ - pub fn drho_dmu(&self) -> EosResult::Larger>> { - let shape = self.density.shape(); - let shape: Vec<_> = std::iter::once(&shape[0]).chain(shape).copied().collect(); - let mut drho_dmu = Array::zeros(shape).into_dimensionality().unwrap(); - for (k, mut d) in drho_dmu.outer_iter_mut().enumerate() { - let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; - for (i, mut l) in lhs.outer_iter_mut().enumerate() { - if i != k { - l.fill(0.0); - } - } - d.assign(&self.density_derivative(&lhs)?); - } - Ok(drho_dmu - * (SIUnit::reference_density() / SIUnit::reference_molar_entropy() / self.temperature)) - } - - /// Return the partial derivatives of the number of moles w.r.t. the chemical potentials $\left(\frac{\partial N_i}{\partial\mu_k}\right)_T$ - pub fn dn_dmu(&self) -> EosResult { - let drho_dmu = self.drho_dmu()?; - let n = drho_dmu.shape()[0]; - let dn_dmu = SIArray2::from_shape_fn([n; 2], |(i, j)| { - self.integrate(&drho_dmu.index_axis(Axis_nd(0), i).index_axis(Axis_nd(0), j)) - }); - Ok(dn_dmu) - } - - /// Return the partial derivatives of the density profiles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial p}\right)_{T,\mathbf{x}}$ - pub fn drho_dp(&self) -> EosResult> { - let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; - let v = self - .bulk - .partial_molar_volume(Contributions::Total) - .to_reduced(SIUnit::reference_volume() / SIUnit::reference_moles())?; - for (mut l, &c) in lhs.outer_iter_mut().zip(self.dft.component_index().iter()) { - l *= v[c]; - } - self.density_derivative(&lhs) - .map(|x| x / (SIUnit::reference_molar_entropy() * self.temperature)) - } - - /// Return the partial derivatives of the number of moles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial N_i}{\partial p}\right)_{T,\mathbf{x}}$ - pub fn dn_dp(&self) -> EosResult { - Ok(self.integrate_segments(&self.drho_dp()?)) - } - - /// Return the partial derivatives of the density profiles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial T}\right)_{p,\mathbf{x}}$ - /// - /// Not compatible with heterosegmented DFT. - pub fn drho_dt(&self) -> EosResult> { - let rho = self.density.to_reduced(SIUnit::reference_density())?; - let t = self - .temperature - .to_reduced(SIUnit::reference_temperature())?; - - // calculate temperature derivative of functional derivative - let functional_contributions = self.dft.contributions(); - let weight_functions: Vec> = functional_contributions - .iter() - .map(|c| c.weight_functions(Dual64::from(t).derivative())) - .collect(); - let convolver: Arc> = - ConvolverFFT::plan(&self.grid, &weight_functions, None); - let (_, dfdrhodt) = self.dft.functional_derivative_dual(t, &rho, &convolver)?; - - // calculate temperature derivative of bulk functional derivative - let partial_density = self - .bulk - .partial_density - .to_reduced(SIUnit::reference_density())?; - let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); - let bulk_convolver = BulkConvolver::new(weight_functions); - let (_, dfdrhodt_bulk) = - self.dft - .functional_derivative_dual(t, &rho_bulk, &bulk_convolver)?; - - // solve for drho_dt - let x = (self.bulk.partial_molar_volume(Contributions::Total) - * self.bulk.dp_dt(Contributions::Total)) - .to_reduced(SIUnit::reference_molar_entropy())?; - let mut lhs = dfdrhodt.mapv(|d| d.eps); - lhs.outer_iter_mut() - .zip(dfdrhodt_bulk.into_iter()) - .zip(x.into_iter()) - .for_each(|((mut lhs, d), x)| lhs -= d.eps - x); - lhs.outer_iter_mut() - .zip(rho.outer_iter()) - .zip(rho_bulk.into_iter()) - .zip(self.dft.m().iter()) - .for_each(|(((mut lhs, rho), rho_b), &m)| lhs += &((&rho / rho_b).mapv(f64::ln) * m)); - - lhs *= &(-&rho / t); - self.density_derivative(&lhs) - .map(|x| x * (SIUnit::reference_density() / SIUnit::reference_temperature())) - } - - /// Return the partial derivatives of the number of moles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial N_i}{\partial T}\right)_{p,\mathbf{x}}$ - /// - /// Not compatible with heterosegmented DFT. - pub fn dn_dt(&self) -> EosResult { - Ok(self.integrate_segments(&self.drho_dt()?)) - } -} diff --git a/feos-dft/src/profile/properties.rs b/feos-dft/src/profile/properties.rs new file mode 100644 index 000000000..2348fbd0f --- /dev/null +++ b/feos-dft/src/profile/properties.rs @@ -0,0 +1,397 @@ +use super::DFTProfile; +use crate::convolver::{BulkConvolver, Convolver}; +use crate::functional_contribution::{FunctionalContribution, FunctionalContributionDual}; +use crate::{ConvolverFFT, DFTSolverLog, HelmholtzEnergyFunctional, WeightFunctionInfo}; +use feos_core::{Contributions, EosResult, EosUnit, IdealGas, Verbosity}; +use ndarray::{Array, Axis, Dimension, RemoveAxis, ScalarOperand}; +use num_dual::{Dual64, DualNum}; +use quantity::si::{SIArray, SIArray1, SIArray2, SINumber, SIUnit}; +use std::ops::AddAssign; +use std::sync::Arc; + +impl DFTProfile +where + D::Larger: Dimension, +{ + /// Calculate the grand potential density $\omega$. + pub fn grand_potential_density(&self) -> EosResult> { + // Calculate residual Helmholtz energy density and functional derivative + let t = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let (mut f, dfdrho) = self.dft.functional_derivative(t, &rho, &self.convolver)?; + + // Calculate the grand potential density + for ((rho, dfdrho), &m) in rho + .outer_iter() + .zip(dfdrho.outer_iter()) + .zip(self.dft.m().iter()) + { + f -= &((&dfdrho + m) * &rho); + } + + let bond_lengths = self.dft.bond_lengths(t); + for segment in bond_lengths.node_indices() { + let n = bond_lengths.neighbors(segment).count(); + f += &(&rho.index_axis(Axis(0), segment.index()) * (0.5 * n as f64)); + } + + Ok(f * t * SIUnit::reference_pressure()) + } + + /// Calculate the grand potential $\Omega$. + pub fn grand_potential(&self) -> EosResult { + Ok(self.integrate(&self.grand_potential_density()?)) + } + + /// Calculate the (residual) intrinsic functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$. + pub fn functional_derivative(&self) -> EosResult> { + let (_, dfdrho) = self.dft.functional_derivative( + self.temperature + .to_reduced(SIUnit::reference_temperature())?, + &self.density.to_reduced(SIUnit::reference_density())?, + &self.convolver, + )?; + Ok(dfdrho) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn intrinsic_helmholtz_energy_density( + &self, + temperature: N, + density: &Array, + convolver: &Arc>, + ) -> EosResult> + where + N: DualNum + Copy + ScalarOperand, + dyn FunctionalContribution: FunctionalContributionDual, + { + let density_dual = density.mapv(N::from); + let weighted_densities = convolver.weighted_densities(&density_dual); + let functional_contributions = self.dft.contributions(); + let mut helmholtz_energy_density: Array = self + .dft + .ideal_chain_contribution() + .calculate_helmholtz_energy_density(&density.mapv(N::from))?; + for (c, wd) in functional_contributions.iter().zip(weighted_densities) { + let nwd = wd.shape()[0]; + let ngrid = wd.len() / nwd; + helmholtz_energy_density + .view_mut() + .into_shape(ngrid) + .unwrap() + .add_assign(&c.calculate_helmholtz_energy_density( + temperature, + wd.into_shape((nwd, ngrid)).unwrap().view(), + )?); + } + Ok(helmholtz_energy_density * temperature) + } + + /// Calculate the residual entropy density $s^\mathrm{res}(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn residual_entropy_density(&self) -> EosResult> { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let helmholtz_energy_density = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + Ok(helmholtz_energy_density.mapv(|f| -f.eps) + * (SIUnit::reference_entropy() / SIUnit::reference_volume())) + } + + /// Calculate the individual contributions to the entropy density. + /// + /// Untested with heterosegmented functionals. + pub fn entropy_density_contributions( + &self, + temperature: f64, + density: &Array, + convolver: &Arc>, + ) -> EosResult>> { + let density_dual = density.mapv(Dual64::from); + let temperature_dual = Dual64::from(temperature).derivative(); + let weighted_densities = convolver.weighted_densities(&density_dual); + let functional_contributions = self.dft.contributions(); + let mut helmholtz_energy_density: Vec> = + Vec::with_capacity(functional_contributions.len() + 1); + helmholtz_energy_density.push( + self.dft + .ideal_chain_contribution() + .calculate_helmholtz_energy_density(&density.mapv(Dual64::from))?, + ); + + for (c, wd) in functional_contributions.iter().zip(weighted_densities) { + let nwd = wd.shape()[0]; + let ngrid = wd.len() / nwd; + helmholtz_energy_density.push( + c.calculate_helmholtz_energy_density( + temperature_dual, + wd.into_shape((nwd, ngrid)).unwrap().view(), + )? + .into_shape(density.raw_dim().remove_axis(Axis(0))) + .unwrap(), + ); + } + Ok(helmholtz_energy_density + .iter() + .map(|v| v.mapv(|f| -(f * temperature_dual).eps)) + .collect()) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn ideal_gas_contribution_dual( + &self, + temperature: Dual64, + density: &Array, + ) -> Array { + let lambda = self.dft.ideal_gas_model().ln_lambda3(temperature); + let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); + for (i, rhoi) in density.outer_iter().enumerate() { + phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); + } + phi * temperature + } + + /// Calculate the entropy density $s(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn entropy_density(&self, contributions: Contributions) -> EosResult> { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let mut helmholtz_energy_density = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + match contributions { + Contributions::Total => { + helmholtz_energy_density += &self.ideal_gas_contribution_dual(temperature_dual, &density); + }, + Contributions::IdealGas => panic!("Entropy density can only be calculated for Contributions::Residual or Contributions::Total"), + Contributions::Residual => (), + } + Ok(helmholtz_energy_density.mapv(|f| -f.eps) + * (SIUnit::reference_entropy() / SIUnit::reference_volume())) + } + + /// Calculate the entropy $S$. + /// + /// Untested with heterosegmented functionals. + pub fn entropy(&self, contributions: Contributions) -> EosResult { + Ok(self.integrate(&self.entropy_density(contributions)?)) + } + + /// Calculate the internal energy density $u(\mathbf{r})$. + /// + /// Untested with heterosegmented functionals. + pub fn internal_energy_density(&self, contributions: Contributions) -> EosResult> + where + D: Dimension, + D::Larger: Dimension, + { + // initialize convolver + let temperature = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + let temperature_dual = Dual64::from(temperature).derivative(); + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(temperature_dual)) + .collect(); + let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, None); + + let density = self.density.to_reduced(SIUnit::reference_density())?; + + let mut helmholtz_energy_density_dual = + self.intrinsic_helmholtz_energy_density(temperature_dual, &density, &convolver)?; + match contributions { + Contributions::Total => { + helmholtz_energy_density_dual += &self.ideal_gas_contribution_dual(temperature_dual, &density); + }, + Contributions::IdealGas => panic!("Internal energy density can only be calculated for Contributions::Residual or Contributions::Total"), + Contributions::Residual => (), + } + let helmholtz_energy_density = helmholtz_energy_density_dual + .mapv(|f| f.re - f.eps * temperature) + + (&self.external_potential * density).sum_axis(Axis(0)) * temperature; + Ok(helmholtz_energy_density * (SIUnit::reference_energy() / SIUnit::reference_volume())) + } + + /// Calculate the internal energy $U$. + /// + /// Untested with heterosegmented functionals. + pub fn internal_energy(&self, contributions: Contributions) -> EosResult { + Ok(self.integrate(&self.internal_energy_density(contributions)?)) + } +} + +impl DFTProfile +where + D::Larger: Dimension, + D::Smaller: Dimension, + ::Larger: Dimension, +{ + fn density_derivative(&self, lhs: &Array) -> EosResult> { + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let partial_density = self + .bulk + .partial_density + .to_reduced(SIUnit::reference_density())?; + let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); + + let second_partial_derivatives = self.second_partial_derivatives(&rho)?; + let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &rho_bulk, false)?; + + let rhs = |x: &_| { + let delta_functional_derivative = + self.delta_functional_derivative(x, &second_partial_derivatives); + let mut xm = x.clone(); + xm.outer_iter_mut() + .zip(self.dft.m().iter()) + .for_each(|(mut x, &m)| x *= m); + let delta_i = self.delta_bond_integrals(&exp_dfdrho, &delta_functional_derivative); + xm + (delta_functional_derivative - delta_i) * &rho + }; + let mut log = DFTSolverLog::new(Verbosity::None); + Self::gmres(rhs, lhs, 200, 1e-13, &mut log) + } + + /// Return the partial derivatives of the density profiles w.r.t. the chemical potentials $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial\mu_k}\right)_T$ + pub fn drho_dmu(&self) -> EosResult::Larger>> { + let shape = self.density.shape(); + let shape: Vec<_> = std::iter::once(&shape[0]).chain(shape).copied().collect(); + let mut drho_dmu = Array::zeros(shape).into_dimensionality().unwrap(); + for (k, mut d) in drho_dmu.outer_iter_mut().enumerate() { + let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; + for (i, mut l) in lhs.outer_iter_mut().enumerate() { + if i != k { + l.fill(0.0); + } + } + d.assign(&self.density_derivative(&lhs)?); + } + Ok(drho_dmu + * (SIUnit::reference_density() / SIUnit::reference_molar_entropy() / self.temperature)) + } + + /// Return the partial derivatives of the number of moles w.r.t. the chemical potentials $\left(\frac{\partial N_i}{\partial\mu_k}\right)_T$ + pub fn dn_dmu(&self) -> EosResult { + let drho_dmu = self.drho_dmu()?; + let n = drho_dmu.shape()[0]; + let dn_dmu = SIArray2::from_shape_fn([n; 2], |(i, j)| { + self.integrate(&drho_dmu.index_axis(Axis(0), i).index_axis(Axis(0), j)) + }); + Ok(dn_dmu) + } + + /// Return the partial derivatives of the density profiles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial p}\right)_{T,\mathbf{x}}$ + pub fn drho_dp(&self) -> EosResult> { + let mut lhs = self.density.to_reduced(SIUnit::reference_density())?; + let v = self + .bulk + .partial_molar_volume() + .to_reduced(SIUnit::reference_volume() / SIUnit::reference_moles())?; + for (mut l, &c) in lhs.outer_iter_mut().zip(self.dft.component_index().iter()) { + l *= v[c]; + } + self.density_derivative(&lhs) + .map(|x| x / (SIUnit::reference_molar_entropy() * self.temperature)) + } + + /// Return the partial derivatives of the number of moles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial N_i}{\partial p}\right)_{T,\mathbf{x}}$ + pub fn dn_dp(&self) -> EosResult { + Ok(self.integrate_segments(&self.drho_dp()?)) + } + + /// Return the partial derivatives of the density profiles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial T}\right)_{p,\mathbf{x}}$ + /// + /// Not compatible with heterosegmented DFT. + pub fn drho_dt(&self) -> EosResult> { + let rho = self.density.to_reduced(SIUnit::reference_density())?; + let t = self + .temperature + .to_reduced(SIUnit::reference_temperature())?; + + // calculate temperature derivative of functional derivative + let functional_contributions = self.dft.contributions(); + let weight_functions: Vec> = functional_contributions + .iter() + .map(|c| c.weight_functions(Dual64::from(t).derivative())) + .collect(); + let convolver: Arc> = + ConvolverFFT::plan(&self.grid, &weight_functions, None); + let (_, dfdrhodt) = self.dft.functional_derivative_dual(t, &rho, &convolver)?; + + // calculate temperature derivative of bulk functional derivative + let partial_density = self + .bulk + .partial_density + .to_reduced(SIUnit::reference_density())?; + let rho_bulk = self.dft.component_index().mapv(|i| partial_density[i]); + let bulk_convolver = BulkConvolver::new(weight_functions); + let (_, dfdrhodt_bulk) = + self.dft + .functional_derivative_dual(t, &rho_bulk, &bulk_convolver)?; + + // solve for drho_dt + let x = (self.bulk.partial_molar_volume() * self.bulk.dp_dt(Contributions::Total)) + .to_reduced(SIUnit::reference_molar_entropy())?; + let mut lhs = dfdrhodt.mapv(|d| d.eps); + lhs.outer_iter_mut() + .zip(dfdrhodt_bulk.into_iter()) + .zip(x.into_iter()) + .for_each(|((mut lhs, d), x)| lhs -= d.eps - x); + lhs.outer_iter_mut() + .zip(rho.outer_iter()) + .zip(rho_bulk.into_iter()) + .zip(self.dft.m().iter()) + .for_each(|(((mut lhs, rho), rho_b), &m)| lhs += &((&rho / rho_b).mapv(f64::ln) * m)); + + lhs *= &(-&rho / t); + self.density_derivative(&lhs) + .map(|x| x * (SIUnit::reference_density() / SIUnit::reference_temperature())) + } + + /// Return the partial derivatives of the number of moles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial N_i}{\partial T}\right)_{p,\mathbf{x}}$ + /// + /// Not compatible with heterosegmented DFT. + pub fn dn_dt(&self) -> EosResult { + Ok(self.integrate_segments(&self.drho_dt()?)) + } +} diff --git a/feos-dft/src/python/adsorption/mod.rs b/feos-dft/src/python/adsorption/mod.rs index 99970cb55..688b9b0d3 100644 --- a/feos-dft/src/python/adsorption/mod.rs +++ b/feos-dft/src/python/adsorption/mod.rs @@ -229,11 +229,6 @@ macro_rules! impl_adsorption_isotherm { self.0.pressure().into() } - #[getter] - fn get_molar_gibbs_energy(&self) -> PySIArray1 { - self.0.molar_gibbs_energy().into() - } - #[getter] fn get_adsorption(&self) -> PySIArray2 { self.0.adsorption().into() diff --git a/feos-dft/src/python/profile.rs b/feos-dft/src/python/profile.rs index 4223f4dab..46c2f378f 100644 --- a/feos-dft/src/python/profile.rs +++ b/feos-dft/src/python/profile.rs @@ -77,11 +77,6 @@ macro_rules! impl_profile { self.0.profile.external_potential.view().to_pyarray(py) } - #[getter] - fn get_chemical_potential(&self) -> PySIArray1 { - PySIArray1::from(self.0.profile.chemical_potential()) - } - #[getter] fn get_bulk(&self) -> PyState { PyState(self.0.profile.bulk.clone()) diff --git a/src/dft.rs b/src/dft.rs index 352ce0c27..585681f4d 100644 --- a/src/dft.rs +++ b/src/dft.rs @@ -8,7 +8,7 @@ use crate::pets::PetsFunctional; #[cfg(feature = "saftvrqmie")] use crate::saftvrqmie::SaftVRQMieFunctional; use feos_core::*; -use feos_derive::HelmholtzEnergyFunctional; +use feos_derive::{Components, HelmholtzEnergyFunctional}; use feos_dft::adsorption::*; use feos_dft::solvation::*; use feos_dft::*; @@ -21,7 +21,7 @@ use quantity::si::*; /// /// Particularly relevant for situations in which generic types /// are undesirable (e.g. FFI). -#[derive(HelmholtzEnergyFunctional)] +#[derive(Components, HelmholtzEnergyFunctional)] pub enum FunctionalVariant { #[cfg(feature = "pcsaft")] #[implement(fluid_parameters, molar_weight, pair_potential)] diff --git a/src/eos.rs b/src/eos.rs index c93505db5..f5cd0d2de 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -9,10 +9,11 @@ use crate::saftvrqmie::SaftVRQMie; #[cfg(feature = "uvtheory")] use crate::uvtheory::UVTheory; use feos_core::cubic::PengRobinson; +use feos_core::joback::Joback; #[cfg(feature = "python")] -use feos_core::python::user_defined::PyEoSObj; +use feos_core::python::user_defined::{PyIdealGas, PyResidual}; use feos_core::*; -use feos_derive::EquationOfState; +use feos_derive::{Components, IdealGas, Residual}; use ndarray::Array1; use quantity::si::*; @@ -20,8 +21,8 @@ use quantity::si::*; /// /// Particularly relevant for situations in which generic types /// are undesirable (e.g. FFI). -#[derive(EquationOfState)] -pub enum EosVariant { +#[derive(Components, Residual)] +pub enum ResidualModel { #[cfg(feature = "pcsaft")] #[implement(entropy_scaling, molar_weight)] PcSaft(PcSaft), @@ -32,7 +33,7 @@ pub enum EosVariant { PengRobinson(PengRobinson), #[cfg(feature = "python")] #[implement(molar_weight)] - Python(PyEoSObj), + Python(PyResidual), #[cfg(feature = "saftvrqmie")] #[implement(molar_weight)] SaftVRQMie(SaftVRQMie), @@ -42,3 +43,11 @@ pub enum EosVariant { #[cfg(feature = "uvtheory")] UVTheory(UVTheory), } + +#[derive(Components, IdealGas)] +pub enum IdealGasModel { + NoModel(usize), + Joback(Joback), + #[cfg(feature = "python")] + Python(PyIdealGas), +} diff --git a/src/estimator/binary_vle.rs b/src/estimator/binary_vle.rs index 200925d68..63c668677 100644 --- a/src/estimator/binary_vle.rs +++ b/src/estimator/binary_vle.rs @@ -1,7 +1,6 @@ use super::{DataSet, EstimatorError}; use feos_core::{ - Contributions, DensityInitialization, EosUnit, EquationOfState, PhaseDiagram, PhaseEquilibrium, - State, + Contributions, DensityInitialization, EosUnit, PhaseDiagram, PhaseEquilibrium, Residual, State, }; use ndarray::{arr1, s, Array1, ArrayView1, Axis}; use quantity::si::{SIArray1, SINumber, SIUnit}; @@ -44,7 +43,7 @@ impl BinaryVleChemicalPotential { } } -impl DataSet for BinaryVleChemicalPotential { +impl DataSet for BinaryVleChemicalPotential { fn target(&self) -> &SIArray1 { &self.target } @@ -73,16 +72,22 @@ impl DataSet for BinaryVleChemicalPotential { { let liquid_moles = arr1(&[xi, 1.0 - xi]) * SIUnit::reference_moles(); let liquid = State::new_npt(eos, t, p, &liquid_moles, DensityInitialization::Liquid)?; - let mu_liquid = liquid.chemical_potential(Contributions::Total); + let mu_res_liquid = liquid.residual_chemical_potential(); let vapor_moles = arr1(&[yi, 1.0 - yi]) * SIUnit::reference_moles(); let vapor = State::new_npt(eos, t, p, &vapor_moles, DensityInitialization::Vapor)?; - let mu_vapor = vapor.chemical_potential(Contributions::Total); + let mu_res_vapor = vapor.residual_chemical_potential(); + let kt = SIUnit::gas_constant() * t; + let rho_frac = (&liquid.partial_density / &vapor.partial_density).into_value()?; prediction.push( - mu_liquid.get(0) - mu_vapor.get(0) + 500.0 * SIUnit::reference_molar_energy(), + mu_res_liquid.get(0) - mu_res_vapor.get(0) + + kt * rho_frac[0].ln() + + 500.0 * SIUnit::reference_molar_energy(), ); prediction.push( - mu_liquid.get(1) - mu_vapor.get(1) + 500.0 * SIUnit::reference_molar_energy(), + mu_res_liquid.get(1) - mu_res_vapor.get(1) + + kt * rho_frac[1].ln() + + 500.0 * SIUnit::reference_molar_energy(), ); } Ok(SIArray1::from_vec(prediction)) @@ -129,7 +134,7 @@ impl BinaryVlePressure { } } -impl DataSet for BinaryVlePressure { +impl DataSet for BinaryVlePressure { fn target(&self) -> &SIArray1 { &self.pressure } @@ -227,7 +232,7 @@ impl BinaryPhaseDiagram { } } -impl DataSet for BinaryPhaseDiagram { +impl DataSet for BinaryPhaseDiagram { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/dataset.rs b/src/estimator/dataset.rs index 4e8f78aa2..cf496b8c0 100644 --- a/src/estimator/dataset.rs +++ b/src/estimator/dataset.rs @@ -3,7 +3,7 @@ //! a `target` which can be values from experimental data or //! other models. use super::{EstimatorError, Loss}; -use feos_core::EquationOfState; +use feos_core::Residual; use ndarray::Array1; use quantity::si::SIArray1; use std::collections::HashMap; @@ -14,7 +14,7 @@ use std::sync::Arc; /// /// Functionalities in the context of optimizations of /// parameters of equations of state. -pub trait DataSet: Send + Sync { +pub trait DataSet: Send + Sync { /// Return target quantity. fn target(&self) -> &SIArray1; @@ -61,7 +61,7 @@ pub trait DataSet: Send + Sync { } } -impl fmt::Display for dyn DataSet { +impl fmt::Display for dyn DataSet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, diff --git a/src/estimator/diffusion.rs b/src/estimator/diffusion.rs index 25c9db749..d921ead42 100644 --- a/src/estimator/diffusion.rs +++ b/src/estimator/diffusion.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl Diffusion { } } -impl DataSet for Diffusion { +impl DataSet for Diffusion { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/estimator.rs b/src/estimator/estimator.rs index 300d26314..69299e5e3 100644 --- a/src/estimator/estimator.rs +++ b/src/estimator/estimator.rs @@ -1,7 +1,7 @@ //! The [`Estimator`] struct can be used to store multiple [`DataSet`]s for convenient parameter //! optimization. use super::{DataSet, EstimatorError, Loss}; -use feos_core::EquationOfState; +use feos_core::Residual; use ndarray::{arr1, concatenate, Array1, ArrayView1, Axis}; use quantity::si::SIArray1; use std::fmt; @@ -11,13 +11,13 @@ use std::sync::Arc; /// A collection of [`DataSet`]s and weights that can be used to /// evaluate an equation of state versus experimental data. -pub struct Estimator { +pub struct Estimator { data: Vec>>, weights: Vec, losses: Vec, } -impl Estimator { +impl Estimator { /// Create a new `Estimator` given `DataSet`s and weights. /// /// The weights are normalized and used as multiplicator when the @@ -99,7 +99,7 @@ impl Estimator { } } -impl Display for Estimator { +impl Display for Estimator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for d in self.data.iter() { writeln!(f, "{}", d)?; diff --git a/src/estimator/liquid_density.rs b/src/estimator/liquid_density.rs index 71a2e36eb..f1290b6a9 100644 --- a/src/estimator/liquid_density.rs +++ b/src/estimator/liquid_density.rs @@ -1,7 +1,6 @@ use super::{DataSet, EstimatorError}; use feos_core::{ - DensityInitialization, EosUnit, EquationOfState, MolarWeight, PhaseEquilibrium, SolverOptions, - State, + DensityInitialization, EosUnit, MolarWeight, PhaseEquilibrium, Residual, SolverOptions, State, }; use ndarray::arr1; use quantity::si::{SIArray1, SIUnit}; @@ -44,7 +43,7 @@ impl LiquidDensity { } } -impl DataSet for LiquidDensity { +impl DataSet for LiquidDensity { fn target(&self) -> &SIArray1 { &self.target } @@ -110,7 +109,7 @@ impl EquilibriumLiquidDensity { } } -impl DataSet for EquilibriumLiquidDensity { +impl DataSet for EquilibriumLiquidDensity { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/thermal_conductivity.rs b/src/estimator/thermal_conductivity.rs index 45b3e33f4..ec7003edd 100644 --- a/src/estimator/thermal_conductivity.rs +++ b/src/estimator/thermal_conductivity.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl ThermalConductivity { } } -impl DataSet for ThermalConductivity { +impl DataSet for ThermalConductivity { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/vapor_pressure.rs b/src/estimator/vapor_pressure.rs index 19d9e2fb7..0545c73da 100644 --- a/src/estimator/vapor_pressure.rs +++ b/src/estimator/vapor_pressure.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{Contributions, EosUnit, EquationOfState, PhaseEquilibrium, SolverOptions, State}; +use feos_core::{Contributions, EosUnit, PhaseEquilibrium, Residual, SolverOptions, State}; use ndarray::{arr1, Array1}; use quantity::si::{SIArray1, SINumber, SIUnit}; use std::collections::HashMap; @@ -57,7 +57,7 @@ impl VaporPressure { } } -impl DataSet for VaporPressure { +impl DataSet for VaporPressure { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/estimator/viscosity.rs b/src/estimator/viscosity.rs index 8e57c56d4..ad05b168f 100644 --- a/src/estimator/viscosity.rs +++ b/src/estimator/viscosity.rs @@ -1,5 +1,5 @@ use super::{DataSet, EstimatorError}; -use feos_core::{DensityInitialization, EntropyScaling, EosUnit, EquationOfState, State}; +use feos_core::{DensityInitialization, EntropyScaling, EosUnit, Residual, State}; use ndarray::arr1; use quantity::si::{SIArray1, SIUnit}; use std::collections::HashMap; @@ -38,7 +38,7 @@ impl Viscosity { } } -impl DataSet for Viscosity { +impl DataSet for Viscosity { fn target(&self) -> &SIArray1 { &self.target } diff --git a/src/gc_pcsaft/dft/mod.rs b/src/gc_pcsaft/dft/mod.rs index 2b0f43b38..1c66a6716 100644 --- a/src/gc_pcsaft/dft/mod.rs +++ b/src/gc_pcsaft/dft/mod.rs @@ -2,7 +2,7 @@ use super::eos::GcPcSaftOptions; use crate::association::Association; use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, MonomerShape}; use feos_core::parameter::ParameterHetero; -use feos_core::MolarWeight; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; use ndarray::Array1; @@ -66,27 +66,33 @@ impl GcPcSaftFunctional { contributions.push(Box::new(assoc)); } - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, }) - .into() } } -impl HelmholtzEnergyFunctional for GcPcSaftFunctional { - fn molecule_shape(&self) -> MoleculeShape { - MoleculeShape::Heterosegmented(&self.parameters.component_index) +impl Components for GcPcSaftFunctional { + fn components(&self) -> usize { + self.parameters.chemical_records.len() } - fn subset(&self, component_list: &[usize]) -> DFT { + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 + } +} + +impl HelmholtzEnergyFunctional for GcPcSaftFunctional { + fn molecule_shape(&self) -> MoleculeShape { + MoleculeShape::Heterosegmented(&self.parameters.component_index) } fn compute_max_density(&self, moles: &Array1) -> f64 { diff --git a/src/gc_pcsaft/dft/parameter.rs b/src/gc_pcsaft/dft/parameter.rs index d0931dfac..43fc9016e 100644 --- a/src/gc_pcsaft/dft/parameter.rs +++ b/src/gc_pcsaft/dft/parameter.rs @@ -1,6 +1,5 @@ use crate::association::AssociationParameters; use crate::gc_pcsaft::record::GcPcSaftRecord; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, ChemicalRecord, ParameterError, ParameterHetero, SegmentRecord, }; @@ -26,20 +25,19 @@ pub struct GcPcSaftFunctionalParameters { pub k_ij: Array2, pub sigma_ij: Array2, pub epsilon_k_ij: Array2, - chemical_records: Vec, - segment_records: Vec>, + pub chemical_records: Vec, + segment_records: Vec>, binary_segment_records: Option>>, } impl ParameterHetero for GcPcSaftFunctionalParameters { type Chemical = ChemicalRecord; type Pure = GcPcSaftRecord; - type IdealGas = JobackRecord; type Binary = f64; fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result { let chemical_records: Vec<_> = chemical_records.into_iter().map(|cr| cr.into()).collect(); @@ -155,7 +153,7 @@ impl ParameterHetero for GcPcSaftFunctionalParameters { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ) { ( diff --git a/src/gc_pcsaft/eos/mod.rs b/src/gc_pcsaft/eos/mod.rs index 67be9e25c..32b917dc9 100644 --- a/src/gc_pcsaft/eos/mod.rs +++ b/src/gc_pcsaft/eos/mod.rs @@ -1,8 +1,7 @@ use crate::association::Association; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::ParameterHetero; -use feos_core::{EquationOfState, HelmholtzEnergy, IdealGasContribution, MolarWeight}; +use feos_core::{Components, HelmholtzEnergy, MolarWeight, Residual}; use ndarray::Array1; use quantity::si::*; use std::f64::consts::FRAC_PI_6; @@ -43,7 +42,6 @@ pub struct GcPcSaft { pub parameters: Arc, options: GcPcSaftOptions, contributions: Vec>, - joback: Joback, } impl GcPcSaft { @@ -72,18 +70,14 @@ impl GcPcSaft { contributions.push(Box::new(Dipole::new(¶meters))) } Self { - parameters: parameters.clone(), + parameters, options, contributions, - joback: parameters.joback_records.clone().map_or_else( - || Joback::default(parameters.chemical_records.len()), - Joback::new, - ), } } } -impl EquationOfState for GcPcSaft { +impl Components for GcPcSaft { fn components(&self) -> usize { self.parameters.molarweight.len() } @@ -94,7 +88,9 @@ impl EquationOfState for GcPcSaft { self.options, ) } +} +impl Residual for GcPcSaft { fn compute_max_density(&self, moles: &Array1) -> f64 { let p = &self.parameters; let moles_segments: Array1 = p.component_index.iter().map(|&i| moles[i]).collect(); @@ -102,13 +98,9 @@ impl EquationOfState for GcPcSaft { / (FRAC_PI_6 * &p.m * p.sigma.mapv(|v| v.powi(3)) * moles_segments).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } } impl MolarWeight for GcPcSaft { diff --git a/src/gc_pcsaft/eos/parameter.rs b/src/gc_pcsaft/eos/parameter.rs index e71bca0b0..537b0ba64 100644 --- a/src/gc_pcsaft/eos/parameter.rs +++ b/src/gc_pcsaft/eos/parameter.rs @@ -1,10 +1,9 @@ use crate::association::AssociationParameters; use crate::gc_pcsaft::record::GcPcSaftRecord; use crate::hard_sphere::{HardSphereProperties, MonomerShape}; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ - BinaryRecord, ChemicalRecord, FromSegments, Identifier, ParameterError, ParameterHetero, - SegmentCount, SegmentRecord, + BinaryRecord, ChemicalRecord, Identifier, ParameterError, ParameterHetero, SegmentCount, + SegmentRecord, }; use indexmap::IndexMap; use ndarray::{Array1, Array2}; @@ -87,20 +86,18 @@ pub struct GcPcSaftEosParameters { pub epsilon_k_ij: Array2, pub chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, - pub joback_records: Option>, } impl ParameterHetero for GcPcSaftEosParameters { type Chemical = GcPcSaftChemicalRecord; type Pure = GcPcSaftRecord; - type IdealGas = JobackRecord; type Binary = f64; fn from_segments>( chemical_records: Vec, - segment_records: Vec>, + segment_records: Vec>, binary_segment_records: Option>>, ) -> Result { let chemical_records: Vec<_> = chemical_records.into_iter().map(|c| c.into()).collect(); @@ -123,8 +120,6 @@ impl ParameterHetero for GcPcSaftEosParameters { let mut phi = Vec::new(); - let mut joback_records = Vec::new(); - for (i, chemical_record) in chemical_records.iter().cloned().enumerate() { let mut segment_indices = IndexMap::with_capacity(segment_records.len()); let segment_map = chemical_record.segment_map(&segment_records)?; @@ -179,18 +174,6 @@ impl ParameterHetero for GcPcSaftEosParameters { *bond += count; } } - - let ideal_gas_segments: Option> = segment_map - .iter() - .map(|(s, &n)| s.ideal_gas_record.clone().map(|ig| (ig, n))) - .collect(); - - joback_records.push( - ideal_gas_segments - .as_ref() - .map(|s| JobackRecord::from_segments(s)) - .transpose()?, - ); } // Binary interaction parameter @@ -260,7 +243,6 @@ impl ParameterHetero for GcPcSaftEosParameters { chemical_records, segment_records, binary_segment_records, - joback_records: joback_records.into_iter().collect(), }) } @@ -268,7 +250,7 @@ impl ParameterHetero for GcPcSaftEosParameters { &self, ) -> ( &[Self::Chemical], - &[SegmentRecord], + &[SegmentRecord], &Option>>, ) { ( @@ -423,25 +405,23 @@ pub mod test { use crate::association::AssociationRecord; use feos_core::parameter::{ChemicalRecord, Identifier}; - fn ch3() -> SegmentRecord { + fn ch3() -> SegmentRecord { SegmentRecord::new( "CH3".into(), 15.0, GcPcSaftRecord::new(0.77247, 3.6937, 181.49, None, None, None), - None, ) } - fn ch2() -> SegmentRecord { + fn ch2() -> SegmentRecord { SegmentRecord::new( "CH2".into(), 14.0, GcPcSaftRecord::new(0.7912, 3.0207, 157.23, None, None, None), - None, ) } - fn oh() -> SegmentRecord { + fn oh() -> SegmentRecord { SegmentRecord::new( "OH".into(), 0.0, @@ -453,7 +433,6 @@ pub mod test { Some(AssociationRecord::new(0.009583, 2575.9, 1.0, 1.0, 0.0)), None, ), - None, ) } diff --git a/src/gc_pcsaft/python/mod.rs b/src/gc_pcsaft/python/mod.rs index eed44690d..86d3b3074 100644 --- a/src/gc_pcsaft/python/mod.rs +++ b/src/gc_pcsaft/python/mod.rs @@ -3,11 +3,9 @@ use super::dft::GcPcSaftFunctionalParameters; use super::eos::GcPcSaftEosParameters; use super::record::GcPcSaftRecord; use crate::association::PyAssociationRecord; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, IdentifierOption, ParameterError, ParameterHetero, SegmentRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::{PyBinarySegmentRecord, PyChemicalRecord, PyIdentifier}; use feos_core::{impl_json_handling, impl_parameter_from_segments, impl_segment_record}; #[cfg(feature = "dft")] @@ -77,12 +75,7 @@ impl PyGcPcSaftRecord { impl_json_handling!(PyGcPcSaftRecord); -impl_segment_record!( - GcPcSaftRecord, - PyGcPcSaftRecord, - JobackRecord, - PyJobackRecord -); +impl_segment_record!(GcPcSaftRecord, PyGcPcSaftRecord); #[pyclass(name = "GcPcSaftEosParameters")] #[pyo3( @@ -156,7 +149,6 @@ pub fn gc_pcsaft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/hard_sphere/dft.rs b/src/hard_sphere/dft.rs index c886e2c95..720da02a8 100644 --- a/src/hard_sphere/dft.rs +++ b/src/hard_sphere/dft.rs @@ -1,4 +1,4 @@ -use feos_core::EosResult; +use feos_core::{Components, EosResult}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{ @@ -322,26 +322,31 @@ impl FMTFunctional { }); let contributions: Vec> = vec![Box::new(FMTContribution::new(&properties, version))]; - (Self { + DFT(Self { properties, contributions, version, }) - .into() } } -impl HelmholtzEnergyFunctional for FMTFunctional { - fn contributions(&self) -> &[Box] { - &self.contributions +impl Components for FMTFunctional { + fn components(&self) -> usize { + self.properties.sigma.len() } - fn subset(&self, component_list: &[usize]) -> DFT { + fn subset(&self, component_list: &[usize]) -> Self { let sigma = component_list .iter() .map(|&c| self.properties.sigma[c]) .collect(); - Self::new(&sigma, self.version) + Self::new(&sigma, self.version).0 + } +} + +impl HelmholtzEnergyFunctional for FMTFunctional { + fn contributions(&self) -> &[Box] { + &self.contributions } fn compute_max_density(&self, moles: &Array1) -> f64 { diff --git a/src/lib.rs b/src/lib.rs index 24af2ff02..ddb0e7cba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,12 +33,14 @@ #![warn(clippy::all)] #![allow(clippy::too_many_arguments)] +#![allow(deprecated)] + #[cfg(feature = "dft")] mod dft; #[cfg(feature = "dft")] pub use dft::FunctionalVariant; mod eos; -pub use eos::EosVariant; +pub use eos::{IdealGasModel, ResidualModel}; #[cfg(feature = "estimator")] pub mod estimator; diff --git a/src/pcsaft/dft/mod.rs b/src/pcsaft/dft/mod.rs index deefc121d..f87d4b0c6 100644 --- a/src/pcsaft/dft/mod.rs +++ b/src/pcsaft/dft/mod.rs @@ -2,9 +2,8 @@ use super::PcSaftParameters; use crate::association::Association; use crate::hard_sphere::{FMTContribution, FMTVersion}; use crate::pcsaft::eos::PcSaftOptions; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -28,7 +27,6 @@ pub struct PcSaftFunctional { fmt_version: FMTVersion, options: PcSaftOptions, contributions: Vec>, - joback: Joback, } impl PcSaftFunctional { @@ -87,31 +85,31 @@ impl PcSaftFunctional { } } - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.m.len()), - }; - - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, - joback, }) - .into() } } -impl HelmholtzEnergyFunctional for PcSaftFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for PcSaftFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for PcSaftFunctional { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) @@ -122,10 +120,6 @@ impl HelmholtzEnergyFunctional for PcSaftFunctional { &self.contributions } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } - fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::NonSpherical(&self.parameters.m) } diff --git a/src/pcsaft/eos/mod.rs b/src/pcsaft/eos/mod.rs index c11a7714a..323563377 100644 --- a/src/pcsaft/eos/mod.rs +++ b/src/pcsaft/eos/mod.rs @@ -1,32 +1,23 @@ use super::parameters::PcSaftParameters; use crate::association::Association; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EquationOfState, HelmholtzEnergy, - IdealGasContribution, MolarWeight, State, + Components, EntropyScaling, EosError, EosResult, HelmholtzEnergy, MolarWeight, Residual, State, }; use ndarray::Array1; use quantity::si::*; use std::f64::consts::{FRAC_PI_6, PI}; +use std::fmt; use std::sync::Arc; pub(crate) mod dispersion; pub(crate) mod hard_chain; pub(crate) mod polar; -mod qspr; use dispersion::Dispersion; use hard_chain::HardChain; pub use polar::DQVariants; use polar::{Dipole, DipoleQuadrupole, Quadrupole}; -use qspr::QSPR; - -#[allow(clippy::upper_case_acronyms)] -enum IdealGasContributions { - QSPR(QSPR), - Joback(Joback), -} /// Customization options for the PC-SAFT equation of state and functional. #[derive(Copy, Clone)] @@ -53,7 +44,6 @@ pub struct PcSaft { parameters: Arc, options: PcSaftOptions, contributions: Vec>, - ideal_gas: IdealGasContributions, } impl PcSaft { @@ -95,21 +85,15 @@ impl PcSaft { ))); }; - let joback_records = parameters.joback_records.clone(); - Self { - parameters: parameters.clone(), + parameters, options, contributions, - ideal_gas: joback_records.map_or( - IdealGasContributions::QSPR(QSPR { parameters }), - |joback_records| IdealGasContributions::Joback(Joback::new(joback_records)), - ), } } } -impl EquationOfState for PcSaft { +impl Components for PcSaft { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -120,22 +104,23 @@ impl EquationOfState for PcSaft { self.options, ) } +} +impl Residual for PcSaft { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) .sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } +} - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match &self.ideal_gas { - IdealGasContributions::QSPR(qspr) => qspr, - IdealGasContributions::Joback(joback) => joback, - } +impl fmt::Display for PcSaft { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PC-SAFT") } } @@ -292,8 +277,7 @@ impl EntropyScaling for PcSaft { let tr = (temperature / p.epsilon_k[i] / KELVIN) .into_value() .unwrap(); - let s_res_reduced = state - .molar_entropy(Contributions::ResidualNvt) + let s_res_reduced = (state.residual_entropy() / state.total_moles) .to_reduced(RGAS) .unwrap() / p.m[i]; @@ -358,7 +342,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); @@ -374,7 +358,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); @@ -474,20 +458,6 @@ mod tests { } } - #[test] - fn speed_of_sound() { - let e = Arc::new(PcSaft::new(propane_parameters())); - let t = 300.0 * KELVIN; - let p = BAR; - let m = arr1(&[1.0]) * MOL; - let s = State::new_npt(&e, t, p, &m, DensityInitialization::None).unwrap(); - assert_relative_eq!( - s.speed_of_sound(), - 245.00185709137546 * METER / SECOND, - epsilon = 1e-4 - ) - } - #[test] fn mix_single() { let e1 = Arc::new(PcSaft::new(propane_parameters())); diff --git a/src/pcsaft/eos/qspr.rs b/src/pcsaft/eos/qspr.rs deleted file mode 100644 index 844c21ce5..000000000 --- a/src/pcsaft/eos/qspr.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::PcSaftParameters; -use feos_core::IdealGasContributionDual; -use ndarray::Array1; -use num_dual::*; -use std::fmt; -use std::sync::Arc; - -const RGAS: f64 = 6.022140857 * 1.38064852; -const KB: f64 = 1.38064852e-23; -const T300: f64 = 300.0; -const T400: f64 = 400.0; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; - -// Heat capacity parameters @ T = 300 K (col 1) and T = 400 K (col 2) -const NA_NP_300: [f64; 6] = [ - -5763.04893, - 1232.30607, - -239.3513996, - 0.0, - 0.0, - -15174.28321, -]; -const NA_NP_400: [f64; 6] = [ - -8171.26676935062, - 1498.01217504596, - -315.515836223387, - 0.0, - 0.0, - -19389.5468655708, -]; -const NA_P_300: [f64; 6] = [ - 5177.19095226181, - 919.565206504576, - -108.829105648889, - 0.0, - -3.93917830677682, - -13504.5671858292, -]; -const NA_P_400: [f64; 6] = [ - 10656.1018362315, - 1146.10782703748, - -131.023645998081, - 0.0, - -9.93789225413177, - -24430.12952497, -]; -const AP_300: [f64; 6] = [ - 3600.32322462175, - 1006.20461224949, - -151.688378113974, - 7.81876773647109e-07, - 8.01001754473385, - -8959.37140957179, -]; -const AP_400: [f64; 6] = [ - 7248.0697641199, - 1267.44346171358, - -208.738557800023, - 0.000170238690157906, - -6.7841792685616, - -12669.4196622924, -]; - -#[allow(clippy::upper_case_acronyms)] -pub struct QSPR { - pub parameters: Arc, -} - -impl + Copy> IdealGasContributionDual for QSPR { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { - let (c_300, c_400) = if self.parameters.association.is_empty() { - match self.parameters.ndipole + self.parameters.nquadpole { - 0 => (NA_NP_300, NA_NP_400), - _ => (NA_P_300, NA_P_400), - } - } else { - (AP_300, AP_400) - }; - - Array1::from_shape_fn(components, |i| { - let epsilon_kt = temperature.recip() * self.parameters.epsilon_k[i]; - let sigma3 = self.parameters.sigma[i].powi(3); - - let p1 = epsilon_kt * self.parameters.m[i]; - let p2 = sigma3 * self.parameters.m[i]; - let p3 = epsilon_kt * p2; - let p4 = self.parameters.pure_records[i] - .model_record - .association_record - .as_ref() - .map_or(D::zero(), |a| { - (temperature.recip() * a.epsilon_k_ab).exp_m1() * p2 * sigma3 * a.kappa_ab - }); - let p5 = p2 * self.parameters.q[i]; - let p6 = 1.0; - - let icpc300 = (p1 * c_300[0] / T300 - + p2 * c_300[1] - + p3 * c_300[2] / T300 - + p4 * c_300[3] / T300 - + p5 * c_300[4] - + p6 * c_300[5]) - * 0.001; - let icpc400 = (p1 * c_400[0] / T400 - + p2 * c_400[1] - + p3 * c_400[2] / T400 - + p4 * c_400[3] / T400 - + p5 * c_400[4] - + p6 * c_400[5]) - * 0.001; - - // linear approximation - let b = (icpc400 - icpc300) / (T400 - T300); - let a = icpc300 - b * T300; - - // integration - let k = a * (temperature - T0 - temperature * (temperature / T0).ln()) - - b * (temperature - T0).powi(2) * 0.5; - - // de Broglie wavelength - k / (temperature * RGAS) + (temperature * KB / (P0 * A3)).ln() - }) - } -} - -impl fmt::Display for QSPR { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (QSPR)") - } -} diff --git a/src/pcsaft/parameters.rs b/src/pcsaft/parameters.rs index 892cbee6b..097617a5a 100644 --- a/src/pcsaft/parameters.rs +++ b/src/pcsaft/parameters.rs @@ -1,7 +1,6 @@ use crate::association::{AssociationParameters, AssociationRecord}; use crate::hard_sphere::{HardSphereProperties, MonomerShape}; use conv::ValueInto; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ FromSegments, FromSegmentsBinary, Parameter, ParameterError, PureRecord, }; @@ -314,19 +313,17 @@ pub struct PcSaftParameters { pub viscosity: Option>, pub diffusion: Option>, pub thermal_conductivity: Option>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, - pub joback_records: Option>, } impl Parameter for PcSaftParameters { type Pure = PcSaftRecord; - type IdealGas = JobackRecord; type Binary = PcSaftBinaryRecord; fn from_records( - pure_records: Vec>, - binary_records: Array2, + pure_records: Vec>, + binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -422,11 +419,6 @@ impl Parameter for PcSaftParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, m, @@ -450,16 +442,10 @@ impl Parameter for PcSaftParameters { thermal_conductivity: thermal_conductivity_coefficients, pure_records, binary_records, - joback_records, } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } @@ -519,7 +505,6 @@ impl PcSaftParameters { #[cfg(test)] pub mod utils { use super::*; - use feos_core::joback::JobackRecord; use feos_core::parameter::{BinaryRecord, ChemicalRecord, SegmentRecord}; use std::sync::Arc; @@ -544,7 +529,7 @@ pub mod utils { }, "molarweight": 44.0962 }"#; - let propane_record: PureRecord = + let propane_record: PureRecord = serde_json::from_str(propane_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_pure(propane_record)) } @@ -568,7 +553,7 @@ pub mod utils { "q": 4.4 } }"#; - let co2_record: PureRecord = + let co2_record: PureRecord = serde_json::from_str(co2_json).expect("Unable to parse json."); PcSaftParameters::new_pure(co2_record) } @@ -591,7 +576,7 @@ pub mod utils { }, "molarweight": 58.123 }"#; - let butane_record: PureRecord = + let butane_record: PureRecord = serde_json::from_str(butane_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_pure(butane_record)) } @@ -615,7 +600,7 @@ pub mod utils { }, "molarweight": 46.0688 }"#; - let dme_record: PureRecord = + let dme_record: PureRecord = serde_json::from_str(dme_json).expect("Unable to parse json."); PcSaftParameters::new_pure(dme_record) } @@ -642,7 +627,7 @@ pub mod utils { }, "molarweight": 18.0152 }"#; - let water_record: PureRecord = + let water_record: PureRecord = serde_json::from_str(water_json).expect("Unable to parse json."); PcSaftParameters::new_pure(water_record) } @@ -684,7 +669,7 @@ pub mod utils { } } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); PcSaftParameters::new_binary(binary_record, None) } @@ -729,7 +714,7 @@ pub mod utils { "molarweight": 58.123 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(PcSaftParameters::new_binary(binary_record, None)) } diff --git a/src/pcsaft/python.rs b/src/pcsaft/python.rs index 077e4d7e6..b714962f8 100644 --- a/src/pcsaft/python.rs +++ b/src/pcsaft/python.rs @@ -1,11 +1,9 @@ use super::parameters::{PcSaftBinaryRecord, PcSaftParameters, PcSaftRecord}; use super::DQVariants; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, SegmentRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use feos_core::*; use ndarray::Array2; @@ -130,8 +128,8 @@ impl PyPcSaftRecord { impl_json_handling!(PyPcSaftRecord); -impl_pure_record!(PcSaftRecord, PyPcSaftRecord, JobackRecord, PyJobackRecord); -impl_segment_record!(PcSaftRecord, PyPcSaftRecord, JobackRecord, PyJobackRecord); +impl_pure_record!(PcSaftRecord, PyPcSaftRecord); +impl_segment_record!(PcSaftRecord, PyPcSaftRecord); #[pyclass(name = "PcSaftBinaryRecord")] #[pyo3( @@ -186,7 +184,6 @@ pub fn pcsaft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/pets/dft/mod.rs b/src/pets/dft/mod.rs index f14fd3aa5..abf76fa82 100644 --- a/src/pets/dft/mod.rs +++ b/src/pets/dft/mod.rs @@ -2,9 +2,8 @@ use super::eos::PetsOptions; use super::parameters::PetsParameters; use crate::hard_sphere::{FMTContribution, FMTVersion}; use dispersion::AttractiveFunctional; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -25,7 +24,6 @@ pub struct PetsFunctional { fmt_version: FMTVersion, options: PetsOptions, contributions: Vec>, - joback: Joback, } impl PetsFunctional { @@ -74,31 +72,31 @@ impl PetsFunctional { contributions.push(Box::new(att)); } - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.sigma.len()), - }; - - Self { + DFT(Self { parameters, fmt_version, options: pets_options, contributions, - joback, - } - .into() + }) } } -impl HelmholtzEnergyFunctional for PetsFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for PetsFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for PetsFunctional { fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::Spherical(self.parameters.sigma.len()) } @@ -111,10 +109,6 @@ impl HelmholtzEnergyFunctional for PetsFunctional { fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } } impl MolarWeight for PetsFunctional { diff --git a/src/pets/eos/mod.rs b/src/pets/eos/mod.rs index 93596d75d..4f806139b 100644 --- a/src/pets/eos/mod.rs +++ b/src/pets/eos/mod.rs @@ -1,26 +1,14 @@ use super::parameters::PetsParameters; use crate::hard_sphere::HardSphere; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EosUnit, EquationOfState, HelmholtzEnergy, - IdealGasContribution, MolarWeight, State, -}; +use feos_core::{Components, HelmholtzEnergy, MolarWeight, Residual}; use ndarray::Array1; use quantity::si::*; -use std::f64::consts::{FRAC_PI_6, PI}; +use std::f64::consts::FRAC_PI_6; use std::sync::Arc; pub(crate) mod dispersion; -mod qspr; use dispersion::Dispersion; -use qspr::QSPR; - -#[allow(clippy::upper_case_acronyms)] -enum IdealGasContributions { - QSPR(QSPR), - Joback(Joback), -} /// Configuration options for the PeTS equation of state and Helmholtz energy functional. /// @@ -43,7 +31,6 @@ pub struct Pets { parameters: Arc, options: PetsOptions, contributions: Vec>, - ideal_gas: IdealGasContributions, } impl Pets { @@ -60,22 +47,15 @@ impl Pets { parameters: parameters.clone(), }), ]; - - let joback_records = parameters.joback_records.clone(); - Self { - parameters: parameters.clone(), + parameters, options, contributions, - ideal_gas: joback_records.map_or( - IdealGasContributions::QSPR(QSPR { parameters }), - |joback_records| IdealGasContributions::Joback(Joback::new(joback_records)), - ), } } } -impl EquationOfState for Pets { +impl Components for Pets { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -86,22 +66,17 @@ impl EquationOfState for Pets { self.options, ) } +} +impl Residual for Pets { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * self.parameters.sigma.mapv(|v| v.powi(3)) * moles).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } - - fn ideal_gas(&self) -> &dyn IdealGasContribution { - match &self.ideal_gas { - IdealGasContributions::QSPR(qspr) => qspr, - IdealGasContributions::Joback(joback) => joback, - } - } } impl MolarWeight for Pets { @@ -110,217 +85,217 @@ impl MolarWeight for Pets { } } -fn omega11(t: f64) -> f64 { - 1.06036 * t.powf(-0.15610) - + 0.19300 * (-0.47635 * t).exp() - + 1.03587 * (-1.52996 * t).exp() - + 1.76474 * (-3.89411 * t).exp() -} - -fn omega22(t: f64) -> f64 { - 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() - - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() -} - -impl EntropyScaling for Pets { - fn viscosity_reference( - &self, - temperature: SINumber, - _: SINumber, - moles: &SIArray1, - ) -> EosResult { - let x = moles.to_reduced(moles.sum())?; - let p = &self.parameters; - let mw = &p.molarweight; - let ce: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 5.0 / 16.0 - * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) - .sqrt() - .unwrap() - / omega22(tr) - / (p.sigma[i] * ANGSTROM).powi(2) - }) - .collect(); - let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; - for i in 0..self.components() { - let denom: f64 = (0..self.components()) - .map(|j| { - x[j] * (1.0 - + (ce[i] / ce[j]).into_value().unwrap().sqrt() - * (mw[j] / mw[i]).powf(1.0 / 4.0)) - .powi(2) - / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() - }) - .sum(); - ce_mix += ce[i] * x[i] / denom - } - Ok(ce_mix) - } - - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - let coefficients = self - .parameters - .viscosity - .as_ref() - .expect("Missing viscosity coefficients."); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) - } - - fn diffusion_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let density = moles.sum() / volume; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) - * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) - .sqrt() - .unwrap() - }) - .collect(); - Ok(res[0]) - } - - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .diffusion - .as_ref() - .expect("Missing diffusion coefficients."); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - let e: f64 = (&coefficients.row(4) * x).sum(); - Ok(a + b * s_res - - c * (1.0 - s_res.exp()) * s_res.powi(2) - - d * s_res.powi(4) - - e * s_res.powi(8)) - } - - // fn thermal_conductivity_reference( - // &self, - // state: &State, - // ) -> EosResult { - // if self.components() != 1 { - // return Err(EosError::IncompatibleComponents(self.components(), 1)); - // } - // let p = &self.parameters; - // let res: Array1 = (0..self.components()) - // .map(|i| { - // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) - // .into_value() - // .unwrap(); - // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); - // let s_res_cp_reduced = cp - // .entropy(Contributions::Residual) - // .to_reduced(SIUnit::reference_entropy()) - // .unwrap(); - // let s_res_reduced = cp - // .entropy(Contributions::Residual) - // .to_reduced(SIUnit::reference_entropy()) - // .unwrap(); - // let ref_ce = 0.083235 - // * ((state.temperature / KELVIN).into_value().unwrap() - // / (p.molarweight[0])) - // .sqrt() - // / p.sigma[0] - // / p.sigma[0] - // / omega22(tr); - // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); - // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) - // * (p.sigma[i].powi(3) * p.epsilon_k[0]) - // / 100000.0; - // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN - // }) - // .collect(); - // Ok(res[0]) - // } - - // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 - fn thermal_conductivity_reference( - &self, - temperature: SINumber, - volume: SINumber, - moles: &SIArray1, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let state = State::new_nvt( - &Arc::new(Self::new(self.parameters.clone())), - temperature, - volume, - moles, - )?; - let res: Array1 = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN) - .into_value() - .unwrap(); - let ce = 83.235 - * f64::powf(10.0, -1.5) - * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() - / (p.sigma[0] * p.sigma[0]) - / omega22(tr); - ce * WATT / METER / KELVIN - + state.density - * self - .diffusion_reference(temperature, volume, moles) - .unwrap() - * self - .diffusion_correlation( - state - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(SIUnit::reference_molar_entropy()) - .unwrap(), - &state.molefracs, - ) - .unwrap() - * (state.c_v(Contributions::Total) - 1.5 * RGAS) - }) - .collect(); - Ok(res[0]) - } - - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .thermal_conductivity - .as_ref() - .expect("Missing thermal conductivity coefficients"); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * x).sum(); - let c: f64 = (&coefficients.row(2) * x).sum(); - let d: f64 = (&coefficients.row(3) * x).sum(); - Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) - } -} +// fn omega11(t: f64) -> f64 { +// 1.06036 * t.powf(-0.15610) +// + 0.19300 * (-0.47635 * t).exp() +// + 1.03587 * (-1.52996 * t).exp() +// + 1.76474 * (-3.89411 * t).exp() +// } + +// fn omega22(t: f64) -> f64 { +// 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() +// - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() +// } + +// impl EntropyScaling for Pets { +// fn viscosity_reference( +// &self, +// temperature: SINumber, +// _: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// let x = moles.to_reduced(moles.sum())?; +// let p = &self.parameters; +// let mw = &p.molarweight; +// let ce: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 5.0 / 16.0 +// * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI) +// .sqrt() +// .unwrap() +// / omega22(tr) +// / (p.sigma[i] * ANGSTROM).powi(2) +// }) +// .collect(); +// let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; +// for i in 0..self.components() { +// let denom: f64 = (0..self.components()) +// .map(|j| { +// x[j] * (1.0 +// + (ce[i] / ce[j]).into_value().unwrap().sqrt() +// * (mw[j] / mw[i]).powf(1.0 / 4.0)) +// .powi(2) +// / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() +// }) +// .sum(); +// ce_mix += ce[i] * x[i] / denom +// } +// Ok(ce_mix) +// } + +// fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// let coefficients = self +// .parameters +// .viscosity +// .as_ref() +// .expect("Missing viscosity coefficients."); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3)) +// } + +// fn diffusion_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let density = moles.sum() / volume; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV) +// * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL)) +// .sqrt() +// .unwrap() +// }) +// .collect(); +// Ok(res[0]) +// } + +// fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .diffusion +// .as_ref() +// .expect("Missing diffusion coefficients."); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// let e: f64 = (&coefficients.row(4) * x).sum(); +// Ok(a + b * s_res +// - c * (1.0 - s_res.exp()) * s_res.powi(2) +// - d * s_res.powi(4) +// - e * s_res.powi(8)) +// } + +// // fn thermal_conductivity_reference( +// // &self, +// // state: &State, +// // ) -> EosResult { +// // if self.components() != 1 { +// // return Err(EosError::IncompatibleComponents(self.components(), 1)); +// // } +// // let p = &self.parameters; +// // let res: Array1 = (0..self.components()) +// // .map(|i| { +// // let tr = (state.temperature / p.epsilon_k[i] / KELVIN) +// // .into_value() +// // .unwrap(); +// // let cp = State::critical_point_pure(&state.eos, Some(state.temperature)).unwrap(); +// // let s_res_cp_reduced = cp +// // .entropy(Contributions::Residual) +// // .to_reduced(SIUnit::reference_entropy()) +// // .unwrap(); +// // let s_res_reduced = cp +// // .entropy(Contributions::Residual) +// // .to_reduced(SIUnit::reference_entropy()) +// // .unwrap(); +// // let ref_ce = 0.083235 +// // * ((state.temperature / KELVIN).into_value().unwrap() +// // / (p.molarweight[0])) +// // .sqrt() +// // / p.sigma[0] +// // / p.sigma[0] +// // / omega22(tr); +// // let alpha_visc = (-s_res_reduced / s_res_cp_reduced).exp(); +// // let ref_ts = (-0.0167141 * tr + 0.0470581 * (tr).powi(2)) +// // * (p.sigma[i].powi(3) * p.epsilon_k[0]) +// // / 100000.0; +// // (ref_ce + ref_ts * alpha_visc) * WATT / METER / KELVIN +// // }) +// // .collect(); +// // Ok(res[0]) +// // } + +// // Equation 11 of DOI: 10.1021/acs.iecr.9b03998 +// fn thermal_conductivity_reference( +// &self, +// temperature: SINumber, +// volume: SINumber, +// moles: &SIArray1, +// ) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let p = &self.parameters; +// let state = State::new_nvt( +// &Arc::new(Self::new(self.parameters.clone())), +// temperature, +// volume, +// moles, +// )?; +// let res: Array1 = (0..self.components()) +// .map(|i| { +// let tr = (temperature / p.epsilon_k[i] / KELVIN) +// .into_value() +// .unwrap(); +// let ce = 83.235 +// * f64::powf(10.0, -1.5) +// * ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt() +// / (p.sigma[0] * p.sigma[0]) +// / omega22(tr); +// ce * WATT / METER / KELVIN +// + state.density +// * self +// .diffusion_reference(temperature, volume, moles) +// .unwrap() +// * self +// .diffusion_correlation( +// state +// .residual_entropy() +// .to_reduced(SIUnit::reference_molar_entropy() * state.total_moles) +// .unwrap(), +// &state.molefracs, +// ) +// .unwrap() +// * (state.c_v(Contributions::Total) - 1.5 * RGAS) +// }) +// .collect(); +// Ok(res[0]) +// } + +// fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { +// if self.components() != 1 { +// return Err(EosError::IncompatibleComponents(self.components(), 1)); +// } +// let coefficients = self +// .parameters +// .thermal_conductivity +// .as_ref() +// .expect("Missing thermal conductivity coefficients"); +// let a: f64 = (&coefficients.row(0) * x).sum(); +// let b: f64 = (&coefficients.row(1) * x).sum(); +// let c: f64 = (&coefficients.row(2) * x).sum(); +// let d: f64 = (&coefficients.row(3) * x).sum(); +// Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2)) +// } +// } #[cfg(test)] mod tests { @@ -345,23 +320,7 @@ mod tests { let p_ig = s.total_moles * RGAS * t / v; assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), - s.pressure(Contributions::Total), - epsilon = 1e-10 - ); - } - - #[test] - fn ideal_gas_heat_capacity_joback() { - let e = Arc::new(Pets::new(argon_parameters())); - let t = 200.0 * KELVIN; - let v = 1e-3 * METER.powi(3); - let n = arr1(&[1.0]) * MOL; - let s = State::new_nvt(&e, t, v, &n).unwrap(); - let p_ig = s.total_moles * RGAS * t / v; - assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); - assert_relative_eq!( - s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt), + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); diff --git a/src/pets/eos/qspr.rs b/src/pets/eos/qspr.rs deleted file mode 100644 index e4d5aa6e4..000000000 --- a/src/pets/eos/qspr.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::pets::parameters::PetsParameters; -use feos_core::IdealGasContributionDual; -use ndarray::Array1; -use num_dual::*; -use std::fmt; -use std::sync::Arc; - -const RGAS: f64 = 6.022140857 * 1.38064852; -const KB: f64 = 1.38064852e-23; -const T300: f64 = 300.0; -const T400: f64 = 400.0; -const T0: f64 = 298.15; -const P0: f64 = 1.0e5; -const A3: f64 = 1e-30; - -// Heat capacity parameters @ T = 300 K (col 1) and T = 400 K (col 2) -const NA_NP_300: [f64; 6] = [ - -5763.04893, - 1232.30607, - -239.3513996, - 0.0, - 0.0, - -15174.28321, -]; -const NA_NP_400: [f64; 6] = [ - -8171.26676935062, - 1498.01217504596, - -315.515836223387, - 0.0, - 0.0, - -19389.5468655708, -]; -// const NA_P_300: [f64; 6] = [ -// 5177.19095226181, -// 919.565206504576, -// -108.829105648889, -// 0.0, -// -3.93917830677682, -// -13504.5671858292, -// ]; -// const NA_P_400: [f64; 6] = [ -// 10656.1018362315, -// 1146.10782703748, -// -131.023645998081, -// 0.0, -// -9.93789225413177, -// -24430.12952497, -// ]; -// const AP_300: [f64; 6] = [ -// 3600.32322462175, -// 1006.20461224949, -// -151.688378113974, -// 7.81876773647109e-07, -// 8.01001754473385, -// -8959.37140957179, -// ]; -// const AP_400: [f64; 6] = [ -// 7248.0697641199, -// 1267.44346171358, -// -208.738557800023, -// 0.000170238690157906, -// -6.7841792685616, -// -12669.4196622924, -// ]; - -#[allow(clippy::upper_case_acronyms)] -pub struct QSPR { - pub parameters: Arc, -} - -impl + Copy> IdealGasContributionDual for QSPR { - fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1 { - let (c_300, c_400) = (NA_NP_300, NA_NP_400); - - Array1::from_shape_fn(components, |i| { - let epsilon_kt = temperature.recip() * self.parameters.epsilon_k[i]; - let sigma3 = self.parameters.sigma[i].powi(3); - - let p1 = epsilon_kt; - let p2 = sigma3; - let p3 = epsilon_kt * p2; - // let p4 = (temperature.recip() * self.parameters.epsilon_k_ab[i]).exp_m1() - // * p2 - // * sigma3 - // * self.parameters.kappa_ab[i]; - // let p5 = p2 * self.parameters.q[i]; - let p6 = 1.0; - - let icpc300 = (p1 * c_300[0] / T300 - + p2 * c_300[1] - + p3 * c_300[2] / T300 - // + p4 * c_300[3] / T300 - // + p5 * c_300[4] - + p6 * c_300[5]) - * 0.001; - let icpc400 = (p1 * c_400[0] / T400 - + p2 * c_400[1] - + p3 * c_400[2] / T400 - // + p4 * c_400[3] / T400 - // + p5 * c_400[4] - + p6 * c_400[5]) - * 0.001; - - // linear approximation - let b = (icpc400 - icpc300) / (T400 - T300); - let a = icpc300 - b * T300; - - // integration - let k = a * (temperature - T0 - temperature * (temperature / T0).ln()) - - b * (temperature - T0).powi(2) * 0.5; - - // de Broglie wavelength - k / (temperature * RGAS) + (temperature * KB / (P0 * A3)).ln() - }) - } -} - -impl fmt::Display for QSPR { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Ideal gas (QSPR)") - } -} diff --git a/src/pets/parameters.rs b/src/pets/parameters.rs index 436c9dc3a..36391a037 100644 --- a/src/pets/parameters.rs +++ b/src/pets/parameters.rs @@ -1,5 +1,4 @@ use crate::hard_sphere::{HardSphereProperties, MonomerShape}; -use feos_core::joback::JobackRecord; use feos_core::parameter::{Parameter, PureRecord}; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; @@ -119,20 +118,17 @@ pub struct PetsParameters { /// thermal conductivity parameters for entropy scaling pub thermal_conductivity: Option>, /// records of all pure substances of the system - pub pure_records: Vec>, - /// records of parameters for Joback method - pub joback_records: Option>, + pub pure_records: Vec>, /// records of all binary interaction parameters pub binary_records: Array2, } impl Parameter for PetsParameters { type Pure = PetsRecord; - type IdealGas = JobackRecord; type Binary = PetsBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -200,11 +196,6 @@ impl Parameter for PetsParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, sigma, @@ -217,17 +208,11 @@ impl Parameter for PetsParameters { diffusion: diffusion_coefficients, thermal_conductivity: thermal_conductivity_coefficients, pure_records, - joback_records, binary_records, } } - fn records( - &self, - ) -> ( - &[PureRecord], - &Array2, - ) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } @@ -285,7 +270,6 @@ impl std::fmt::Display for PetsParameters { #[cfg(test)] pub mod utils { use super::*; - use feos_core::joback::JobackRecord; use std::sync::Arc; pub fn argon_parameters() -> Arc { @@ -308,7 +292,7 @@ pub mod utils { }, "molarweight": 39.948 }"#; - let argon_record: PureRecord = + let argon_record: PureRecord = serde_json::from_str(argon_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_pure(argon_record)) } @@ -330,7 +314,7 @@ pub mod utils { }, "molarweight": 83.798 }"#; - let krypton_record: PureRecord = + let krypton_record: PureRecord = serde_json::from_str(krypton_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_pure(krypton_record)) } @@ -374,7 +358,7 @@ pub mod utils { "molarweight": 83.798 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(PetsParameters::new_binary(binary_record, None)) } diff --git a/src/pets/python.rs b/src/pets/python.rs index f0b365fa1..39a78a977 100644 --- a/src/pets/python.rs +++ b/src/pets/python.rs @@ -1,7 +1,5 @@ use super::parameters::*; -use feos_core::joback::JobackRecord; use feos_core::parameter::*; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use feos_core::{impl_binary_record, impl_json_handling, impl_parameter, impl_pure_record}; use ndarray::Array2; @@ -69,7 +67,7 @@ impl PyPetsRecord { } impl_json_handling!(PyPetsRecord); -impl_pure_record!(PetsRecord, PyPetsRecord, JobackRecord, PyJobackRecord); +impl_pure_record!(PetsRecord, PyPetsRecord); #[pyclass(name = "PetsBinaryRecord")] #[pyo3( @@ -180,7 +178,6 @@ impl PyPetsParameters { identifier, molarweight.as_ref().map_or(1.0, |v| v[i]), model_record, - None, ) // Hier Ideal Gas anstatt None??? }) @@ -239,7 +236,6 @@ impl PyPetsParameters { ), molarweight.map_or(1.0, |v| v), PetsRecord::new(sigma, epsilon_k, viscosity, diffusion, thermal_conductivity), - None, ); Self(Arc::new(PetsParameters::new_pure(pure_record))) } @@ -265,7 +261,6 @@ pub fn pets(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/cubic.rs b/src/python/cubic.rs index adcced3c3..e73ed374e 100644 --- a/src/python/cubic.rs +++ b/src/python/cubic.rs @@ -1,5 +1,4 @@ use feos_core::python::cubic::*; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::*; use pyo3::prelude::*; @@ -7,7 +6,6 @@ use pyo3::prelude::*; pub fn cubic(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/dft.rs b/src/python/dft.rs index 8be59bcbc..4a2b45ca8 100644 --- a/src/python/dft.rs +++ b/src/python/dft.rs @@ -21,6 +21,7 @@ use crate::saftvrqmie::python::PySaftVRQMieParameters; #[cfg(feature = "saftvrqmie")] use crate::saftvrqmie::{FeynmanHibbsOrder, SaftVRQMieFunctional, SaftVRQMieOptions}; +use crate::eos::IdealGasModel; use feos_core::*; use feos_dft::adsorption::*; use feos_dft::interface::*; @@ -33,14 +34,28 @@ use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; #[cfg(feature = "estimator")] use pyo3::wrap_pymodule; -use quantity::python::{PySINumber, PySIArray1, PySIArray2, PySIArray3, PySIArray4}; +use quantity::python::{PySIArray1, PySIArray2, PySIArray3, PySIArray4, PySINumber}; use quantity::si::*; use std::collections::HashMap; use std::sync::Arc; +type Functional = EquationOfState; + #[pyclass(name = "HelmholtzEnergyFunctional")] #[derive(Clone)] -pub struct PyFunctionalVariant(pub Arc>); +pub struct PyFunctionalVariant(pub Arc>); + +impl PyFunctionalVariant { + fn new(functional: DFT) -> Self + where + FunctionalVariant: From, + { + let functional: DFT = functional.into(); + let n = functional.components(); + let eos = functional.ideal_gas(IdealGasModel::NoModel(n)); + Self(Arc::new(eos)) + } +} #[pymethods] impl PyFunctionalVariant { @@ -84,9 +99,8 @@ impl PyFunctionalVariant { tol_cross_assoc, dq_variant, }; - Self(Arc::new( - PcSaftFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = PcSaftFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } /// (heterosegmented) group contribution PC-SAFT Helmholtz energy functional. @@ -125,9 +139,8 @@ impl PyFunctionalVariant { max_iter_cross_assoc, tol_cross_assoc, }; - Self(Arc::new( - GcPcSaftFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = GcPcSaftFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } /// PeTS Helmholtz energy functional without simplifications @@ -153,9 +166,8 @@ impl PyFunctionalVariant { )] fn pets(parameters: PyPetsParameters, fmt_version: FMTVersion, max_eta: f64) -> Self { let options = PetsOptions { max_eta }; - Self(Arc::new( - PetsFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = PetsFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } /// Helmholtz energy functional for hard sphere systems. @@ -172,9 +184,8 @@ impl PyFunctionalVariant { /// HelmholtzEnergyFunctional #[staticmethod] fn fmt(sigma: &PyArray1, fmt_version: FMTVersion) -> Self { - Self(Arc::new( - FMTFunctional::new(&sigma.to_owned_array(), fmt_version).into(), - )) + let func = FMTFunctional::new(&sigma.to_owned_array(), fmt_version); + Self::new(func) } /// SAFT-VRQ Mie Helmholtz energy functional. @@ -214,29 +225,28 @@ impl PyFunctionalVariant { fh_order, inc_nonadd_term, }; - Self(Arc::new( - SaftVRQMieFunctional::with_options(parameters.0, fmt_version, options).into(), - )) + let func = SaftVRQMieFunctional::with_options(parameters.0, fmt_version, options); + Self::new(func) } } impl_equation_of_state!(PyFunctionalVariant); -impl_state!(DFT, PyFunctionalVariant); -impl_state_molarweight!(DFT, PyFunctionalVariant); -impl_phase_equilibrium!(DFT, PyFunctionalVariant); +impl_state!(DFT, PyFunctionalVariant); +impl_state_molarweight!(DFT, PyFunctionalVariant); +impl_phase_equilibrium!(DFT, PyFunctionalVariant); -impl_planar_interface!(FunctionalVariant); -impl_surface_tension_diagram!(FunctionalVariant); +impl_planar_interface!(Functional); +impl_surface_tension_diagram!(Functional); -impl_pore!(FunctionalVariant, PyFunctionalVariant); -impl_adsorption!(FunctionalVariant, PyFunctionalVariant); +impl_pore!(Functional, PyFunctionalVariant); +impl_adsorption!(Functional, PyFunctionalVariant); -impl_pair_correlation!(FunctionalVariant); -impl_solvation_profile!(FunctionalVariant); +impl_pair_correlation!(Functional); +impl_solvation_profile!(Functional); #[cfg(feature = "estimator")] -impl_estimator!(DFT, PyFunctionalVariant); +impl_estimator!(DFT, PyFunctionalVariant); #[pymodule] pub fn dft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { diff --git a/src/python/eos.rs b/src/python/eos.rs index 94d1fc8a2..5aa19b504 100644 --- a/src/python/eos.rs +++ b/src/python/eos.rs @@ -1,4 +1,4 @@ -use crate::eos::EosVariant; +use crate::eos::{IdealGasModel, ResidualModel}; #[cfg(feature = "estimator")] use crate::estimator::*; #[cfg(feature = "gc_pcsaft")] @@ -27,8 +27,10 @@ use crate::uvtheory::python::PyUVParameters; use crate::uvtheory::{Perturbation, UVTheory, UVTheoryOptions, VirialOrder}; use feos_core::cubic::PengRobinson; +use feos_core::joback::Joback; use feos_core::python::cubic::PyPengRobinsonParameters; -use feos_core::python::user_defined::PyEoSObj; +use feos_core::python::joback::PyJobackParameters; +use feos_core::python::user_defined::{PyIdealGas, PyResidual}; use feos_core::*; use numpy::convert::ToPyArray; use numpy::{PyArray1, PyArray2}; @@ -36,7 +38,7 @@ use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; #[cfg(feature = "estimator")] use pyo3::wrap_pymodule; -use quantity::python::{PySINumber, PySIArray1, PySIArray2}; +use quantity::python::{PySIArray1, PySIArray2, PySINumber}; use quantity::si::*; use std::collections::HashMap; use std::sync::Arc; @@ -44,10 +46,10 @@ use std::sync::Arc; /// Collection of equations of state. #[pyclass(name = "EquationOfState")] #[derive(Clone)] -pub struct PyEosVariant(pub Arc); +pub struct PyEquationOfState(pub Arc>); #[pymethods] -impl PyEosVariant { +impl PyEquationOfState { /// PC-SAFT equation of state. /// /// Parameters @@ -87,10 +89,12 @@ impl PyEosVariant { tol_cross_assoc, dq_variant, }; - Self(Arc::new(EosVariant::PcSaft(PcSaft::with_options( + let residual = Arc::new(ResidualModel::PcSaft(PcSaft::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// (heterosegmented) group contribution PC-SAFT equation of state. @@ -128,10 +132,12 @@ impl PyEosVariant { max_iter_cross_assoc, tol_cross_assoc, }; - Self(Arc::new(EosVariant::GcPcSaft(GcPcSaft::with_options( + let residual = Arc::new(ResidualModel::GcPcSaft(GcPcSaft::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// Peng-Robinson equation of state. @@ -148,25 +154,27 @@ impl PyEosVariant { /// states. #[staticmethod] pub fn peng_robinson(parameters: PyPengRobinsonParameters) -> Self { - Self(Arc::new(EosVariant::PengRobinson(PengRobinson::new( - parameters.0, - )))) + let residual = Arc::new(ResidualModel::PengRobinson(PengRobinson::new(parameters.0))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } - /// Equation of state from a Python class. + /// Residual Helmholtz energy model from a Python class. /// /// Parameters /// ---------- - /// obj : Class + /// residual : Class /// A python class implementing the necessary methods - /// to be used as equation of state. + /// to be used as residual equation of state. /// /// Returns /// ------- /// EquationOfState #[staticmethod] - fn python(obj: Py) -> PyResult { - Ok(Self(Arc::new(EosVariant::Python(PyEoSObj::new(obj)?)))) + fn python_residual(residual: Py) -> PyResult { + let residual = Arc::new(ResidualModel::Python(PyResidual::new(residual)?)); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Ok(Self(Arc::new(EquationOfState::new(ideal_gas, residual)))) } /// PeTS equation of state. @@ -188,10 +196,12 @@ impl PyEosVariant { #[pyo3(signature = (parameters, max_eta=0.5), text_signature = "(parameters, max_eta=0.5)")] fn pets(parameters: PyPetsParameters, max_eta: f64) -> Self { let options = PetsOptions { max_eta }; - Self(Arc::new(EosVariant::Pets(Pets::with_options( + let residual = Arc::new(ResidualModel::Pets(Pets::with_options( parameters.0, options, - )))) + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } /// UV-Theory equation of state. @@ -230,9 +240,12 @@ impl PyEosVariant { perturbation, virial_order, }; - Ok(Self(Arc::new(EosVariant::UVTheory( - UVTheory::with_options(parameters.0, options)?, - )))) + let residual = Arc::new(ResidualModel::UVTheory(UVTheory::with_options( + parameters.0, + options, + )?)); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Ok(Self(Arc::new(EquationOfState::new(ideal_gas, residual)))) } /// SAFT-VRQ Mie equation of state. @@ -271,32 +284,70 @@ impl PyEosVariant { fh_order, inc_nonadd_term, }; - Self(Arc::new(EosVariant::SaftVRQMie(SaftVRQMie::with_options( + let residual = Arc::new(ResidualModel::SaftVRQMie(SaftVRQMie::with_options( parameters.0, options, + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) + } + + /// Ideal gas equation of state from a Python class. + /// + /// Parameters + /// ---------- + /// ideal_gas : Class + /// A python class implementing the necessary methods + /// to be used as an ideal gas model. + /// + /// Returns + /// ------- + /// EquationOfState + fn python_ideal_gas(&self, ideal_gas: Py) -> PyResult { + let ig = Arc::new(IdealGasModel::Python(PyIdealGas::new(ideal_gas)?)); + Ok(Self(Arc::new(EquationOfState::new( + ig, + self.0.residual.clone(), )))) } + + /// Ideal gas model of Joback and Reid. + /// + /// Parameters + /// ---------- + /// parameters : List[JobackRecord] + /// List containing + /// + /// Returns + /// ------- + /// EquationOfState + fn joback(&self, parameters: PyJobackParameters) -> Self { + let ideal_gas = Arc::new(IdealGasModel::Joback(Joback::new(parameters.0))); + Self(Arc::new(EquationOfState::new( + ideal_gas, + self.0.residual.clone(), + ))) + } } -impl_equation_of_state!(PyEosVariant); -impl_virial_coefficients!(PyEosVariant); -impl_state!(EosVariant, PyEosVariant); -impl_state_molarweight!(EosVariant, PyEosVariant); -#[cfg(feature = "pcsaft")] -impl_state_entropy_scaling!(EosVariant, PyEosVariant); -impl_phase_equilibrium!(EosVariant, PyEosVariant); +impl_equation_of_state!(PyEquationOfState); +impl_virial_coefficients!(PyEquationOfState); +impl_state!(EquationOfState, PyEquationOfState); +impl_state_molarweight!(EquationOfState, PyEquationOfState); +impl_state_entropy_scaling!(EquationOfState, PyEquationOfState); +impl_phase_equilibrium!(EquationOfState, PyEquationOfState); #[cfg(feature = "estimator")] -impl_estimator!(EosVariant, PyEosVariant); +impl_estimator!(EquationOfState, PyEquationOfState); #[cfg(all(feature = "estimator", feature = "pcsaft"))] -impl_estimator_entropy_scaling!(EosVariant, PyEosVariant); +impl_estimator_entropy_scaling!(EquationOfState, PyEquationOfState); #[pymodule] pub fn eos(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/python/ideal_gas.rs b/src/python/ideal_gas.rs new file mode 100644 index 000000000..04e0d177c --- /dev/null +++ b/src/python/ideal_gas.rs @@ -0,0 +1,8 @@ +use feos_core::python::joback::{PyJobackParameters, PyJobackRecord}; +use pyo3::prelude::*; + +#[pymodule] +pub fn ideal_gas(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::() +} diff --git a/src/python/mod.rs b/src/python/mod.rs index ec6e9adb0..e4a3e4f8a 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -15,8 +15,10 @@ use quantity::python::quantity as quantity_module; mod cubic; mod eos; +mod ideal_gas; use cubic::cubic as cubic_module; use eos::eos as eos_module; +use ideal_gas::ideal_gas as ideal_gas_module; #[cfg(feature = "dft")] mod dft; @@ -31,6 +33,7 @@ pub fn feos(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(eos_module))?; #[cfg(feature = "dft")] m.add_wrapped(wrap_pymodule!(dft_module))?; + m.add_wrapped(wrap_pymodule!(ideal_gas_module))?; m.add_wrapped(wrap_pymodule!(cubic_module))?; #[cfg(feature = "pcsaft")] m.add_wrapped(wrap_pymodule!(pcsaft_module))?; @@ -51,6 +54,7 @@ pub fn feos(py: Python<'_>, m: &PyModule) -> PyResult<()> { set_path(py, m, "feos.dft", "dft")?; #[cfg(all(feature = "dft", feature = "estimator"))] set_path(py, m, "feos.dft.estimator", "dft.estimator_dft")?; + set_path(py, m, "feos.ideal_gas", "ideal_gas")?; set_path(py, m, "feos.cubic", "cubic")?; #[cfg(feature = "pcsaft")] set_path(py, m, "feos.pcsaft", "pcsaft")?; diff --git a/src/saftvrqmie/dft/mod.rs b/src/saftvrqmie/dft/mod.rs index d728fe9db..92bfe3a28 100644 --- a/src/saftvrqmie/dft/mod.rs +++ b/src/saftvrqmie/dft/mod.rs @@ -2,9 +2,8 @@ use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, Mono use crate::saftvrqmie::eos::SaftVRQMieOptions; use crate::saftvrqmie::parameters::SaftVRQMieParameters; use dispersion::AttractiveFunctional; -use feos_core::joback::Joback; use feos_core::parameter::Parameter; -use feos_core::{IdealGasContribution, MolarWeight}; +use feos_core::{Components, MolarWeight}; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; use feos_dft::{FunctionalContribution, HelmholtzEnergyFunctional, MoleculeShape, DFT}; @@ -24,7 +23,6 @@ pub struct SaftVRQMieFunctional { fmt_version: FMTVersion, options: SaftVRQMieOptions, contributions: Vec>, - joback: Joback, } impl SaftVRQMieFunctional { @@ -61,31 +59,31 @@ impl SaftVRQMieFunctional { let att = AttractiveFunctional::new(parameters.clone()); contributions.push(Box::new(att)); - let joback = match ¶meters.joback_records { - Some(joback_records) => Joback::new(joback_records.clone()), - None => Joback::default(parameters.m.len()), - }; - - (Self { + DFT(Self { parameters, fmt_version, options: saft_options, contributions, - joback, }) - .into() } } -impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { - fn subset(&self, component_list: &[usize]) -> DFT { +impl Components for SaftVRQMieFunctional { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { Self::with_options( Arc::new(self.parameters.subset(component_list)), self.fmt_version, self.options, ) + .0 } +} +impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) @@ -96,10 +94,6 @@ impl HelmholtzEnergyFunctional for SaftVRQMieFunctional { &self.contributions } - fn ideal_gas(&self) -> &dyn IdealGasContribution { - &self.joback - } - fn molecule_shape(&self) -> MoleculeShape { MoleculeShape::NonSpherical(&self.parameters.m) } diff --git a/src/saftvrqmie/eos/mod.rs b/src/saftvrqmie/eos/mod.rs index 42330a9e3..a7b7100e1 100644 --- a/src/saftvrqmie/eos/mod.rs +++ b/src/saftvrqmie/eos/mod.rs @@ -1,8 +1,7 @@ use super::parameters::SaftVRQMieParameters; use feos_core::parameter::Parameter; use feos_core::{ - Contributions, EntropyScaling, EosError, EosResult, EquationOfState, HelmholtzEnergy, - MolarWeight, State, + Components, EntropyScaling, EosError, EosResult, HelmholtzEnergy, MolarWeight, Residual, State, }; use ndarray::Array1; use quantity::si::*; @@ -85,7 +84,7 @@ impl SaftVRQMie { } } -impl EquationOfState for SaftVRQMie { +impl Components for SaftVRQMie { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -96,14 +95,16 @@ impl EquationOfState for SaftVRQMie { self.options, ) } +} +impl Residual for SaftVRQMie { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) .sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -264,8 +265,8 @@ impl EntropyScaling for SaftVRQMie { .into_value() .unwrap(); let s_res_reduced = state - .molar_entropy(Contributions::ResidualNvt) - .to_reduced(RGAS) + .residual_entropy() + .to_reduced(RGAS * state.total_moles) .unwrap() / p.m[i]; let ref_ce = chapman_enskog_thermal_conductivity( diff --git a/src/saftvrqmie/parameters.rs b/src/saftvrqmie/parameters.rs index ed59cd9d5..13b49eb7e 100644 --- a/src/saftvrqmie/parameters.rs +++ b/src/saftvrqmie/parameters.rs @@ -1,4 +1,3 @@ -use feos_core::joback::JobackRecord; use feos_core::parameter::{Parameter, ParameterError, PureRecord}; use ndarray::{Array, Array1, Array2}; use num_traits::Zero; @@ -134,18 +133,16 @@ pub struct SaftVRQMieParameters { pub viscosity: Option>, pub diffusion: Option>, pub thermal_conductivity: Option>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, - pub joback_records: Option>, } impl Parameter for SaftVRQMieParameters { type Pure = SaftVRQMieRecord; - type IdealGas = JobackRecord; type Binary = SaftVRQMieBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -235,11 +232,6 @@ impl Parameter for SaftVRQMieParameters { Some(v) }; - let joback_records = pure_records - .iter() - .map(|r| r.ideal_gas_record.clone()) - .collect(); - Self { molarweight, m, @@ -261,14 +253,13 @@ impl Parameter for SaftVRQMieParameters { thermal_conductivity: thermal_conductivity_coefficients, pure_records, binary_records, - joback_records, } } fn records( &self, ) -> ( - &[PureRecord], + &[PureRecord], &Array2, ) { (&self.pure_records, &self.binary_records) @@ -431,7 +422,7 @@ pub mod utils { }, "molarweight": 2.0157309551872 }"#; - let hydrogen_record: PureRecord = + let hydrogen_record: PureRecord = serde_json::from_str(hydrogen_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(hydrogen_record)) } @@ -457,7 +448,7 @@ pub mod utils { }, "molarweight": 4.002601643881807 }"#; - let helium_record: PureRecord = + let helium_record: PureRecord = serde_json::from_str(helium_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(helium_record)) } @@ -483,7 +474,7 @@ pub mod utils { }, "molarweight": 20.17969806457545 }"#; - let neon_record: PureRecord = + let neon_record: PureRecord = serde_json::from_str(neon_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_pure(neon_record)) } @@ -527,7 +518,7 @@ pub mod utils { "molarweight": 20.17969806457545 } ]"#; - let binary_record: Vec> = + let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); Arc::new(SaftVRQMieParameters::new_binary( binary_record, diff --git a/src/saftvrqmie/python.rs b/src/saftvrqmie/python.rs index 699269d29..8063e4992 100644 --- a/src/saftvrqmie/python.rs +++ b/src/saftvrqmie/python.rs @@ -3,11 +3,9 @@ use crate::saftvrqmie::eos::FeynmanHibbsOrder; use crate::saftvrqmie::parameters::{ SaftVRQMieBinaryRecord, SaftVRQMieParameters, SaftVRQMieRecord, }; -use feos_core::joback::JobackRecord; use feos_core::parameter::{ BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, }; -use feos_core::python::joback::PyJobackRecord; use feos_core::python::parameter::PyIdentifier; use feos_core::*; use ndarray::Array2; @@ -147,12 +145,7 @@ impl PySaftVRQMieBinaryRecord { pub struct PySaftVRQMieParameters(pub Arc); impl_json_handling!(PySaftVRQMieRecord); -impl_pure_record!( - SaftVRQMieRecord, - PySaftVRQMieRecord, - JobackRecord, - PyJobackRecord -); +impl_pure_record!(SaftVRQMieRecord, PySaftVRQMieRecord); impl_binary_record!(SaftVRQMieBinaryRecord, PySaftVRQMieBinaryRecord); impl_parameter!(SaftVRQMieParameters, PySaftVRQMieParameters); @@ -229,7 +222,6 @@ impl PySaftVRQMieParameters { pub fn saftvrqmie(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/uvtheory/eos/mod.rs b/src/uvtheory/eos/mod.rs index 143b859c0..1be4f9674 100644 --- a/src/uvtheory/eos/mod.rs +++ b/src/uvtheory/eos/mod.rs @@ -2,7 +2,7 @@ #![allow(clippy::needless_range_loop)] use super::parameters::UVParameters; -use feos_core::{parameter::Parameter, EosError, EosResult, EquationOfState, HelmholtzEnergy}; +use feos_core::{parameter::Parameter, Components, EosError, EosResult, HelmholtzEnergy, Residual}; use ndarray::Array1; use std::f64::consts::FRAC_PI_6; use std::sync::Arc; @@ -144,7 +144,7 @@ impl UVTheory { } } -impl EquationOfState for UVTheory { +impl Components for UVTheory { fn components(&self) -> usize { self.parameters.pure_records.len() } @@ -156,13 +156,15 @@ impl EquationOfState for UVTheory { ) .expect("Not defined for mixture") } +} +impl Residual for UVTheory { fn compute_max_density(&self, moles: &Array1) -> f64 { self.options.max_eta * moles.sum() / (FRAC_PI_6 * self.parameters.sigma.mapv(|v| v.powi(3)) * moles).sum() } - fn residual(&self) -> &[Box] { + fn contributions(&self) -> &[Box] { &self.contributions } } @@ -175,7 +177,7 @@ mod test { use crate::uvtheory::parameters::*; use approx::assert_relative_eq; use feos_core::parameter::{Identifier, Parameter, PureRecord}; - use feos_core::{Contributions, State}; + use feos_core::State; use ndarray::arr1; use quantity::si::{ANGSTROM, KELVIN, MOL, NAV, RGAS}; @@ -192,8 +194,7 @@ mod test { let moles = arr1(&[2.0]) * MOL; let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); - let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) + let a = (s.residual_helmholtz_energy() / s.total_moles) .to_reduced(RGAS * temperature) .unwrap(); assert_relative_eq!(a, 2.972986567516, max_relative = 1e-12); //wca @@ -221,8 +222,7 @@ mod test { let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); - let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) + let a = (s.residual_helmholtz_energy() / s.total_moles) .to_reduced(RGAS * temperature) .unwrap(); @@ -251,8 +251,8 @@ mod test { let volume = (sig * ANGSTROM).powi(3) / reduced_density * NAV * 2.0 * MOL; let s = State::new_nvt(&eos, temperature, volume, &moles).unwrap(); let a = s - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * temperature) + .residual_helmholtz_energy() + .to_reduced(RGAS * temperature * s.total_moles) .unwrap(); dbg!(a); assert_relative_eq!(a, 0.37659379124271003, max_relative = 1e-12); @@ -276,8 +276,8 @@ mod test { let j = Identifier::new(None, None, None, None, None, None); ////////////// - let pr1 = PureRecord::new(i, 1.0, r1, None); - let pr2 = PureRecord::new(j, 1.0, r2, None); + let pr1 = PureRecord::new(i, 1.0, r1); + let pr2 = PureRecord::new(j, 1.0, r2); let pure_records = vec![pr1, pr2]; let uv_parameters = UVParameters::new_binary(pure_records, None); // state @@ -301,8 +301,8 @@ mod test { let state_bh = State::new_nvt(&eos_bh, t_x, volume, &moles).unwrap(); let a_bh = state_bh - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_bh.total_moles) .unwrap(); assert_relative_eq!(a_bh, 2.993577305779432, max_relative = 1e-12); @@ -330,8 +330,8 @@ mod test { let eos_wca = Arc::new(UVTheory::new(Arc::new(p))?); let state_wca = State::new_nvt(&eos_wca, t_x, volume, &moles).unwrap(); let a_wca = state_wca - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_wca.total_moles) .unwrap(); assert_relative_eq!(a_wca, -0.597791038364405, max_relative = 1e-5); @@ -360,8 +360,8 @@ mod test { let eos_wca = Arc::new(UVTheory::new(Arc::new(p))?); let state_wca = State::new_nvt(&eos_wca, t_x, volume, &moles).unwrap(); let a_wca = state_wca - .molar_helmholtz_energy(Contributions::ResidualNvt) - .to_reduced(RGAS * t_x) + .residual_helmholtz_energy() + .to_reduced(RGAS * t_x * state_wca.total_moles) .unwrap(); assert_relative_eq!(a_wca, -0.034206207363139396, max_relative = 1e-5); Ok(()) diff --git a/src/uvtheory/parameters.rs b/src/uvtheory/parameters.rs index f602084d6..e6dbd1fec 100644 --- a/src/uvtheory/parameters.rs +++ b/src/uvtheory/parameters.rs @@ -118,17 +118,16 @@ pub struct UVParameters { pub eps_k_ij: Array2, pub cd_bh_pure: Vec>, pub cd_bh_binary: Array2>, - pub pure_records: Vec>, + pub pure_records: Vec>, pub binary_records: Array2, } impl Parameter for UVParameters { type Pure = UVRecord; - type IdealGas = NoRecord; type Binary = UVBinaryRecord; fn from_records( - pure_records: Vec>, + pure_records: Vec>, binary_records: Array2, ) -> Self { let n = pure_records.len(); @@ -198,7 +197,7 @@ impl Parameter for UVParameters { } } - fn records(&self) -> (&[PureRecord], &Array2) { + fn records(&self) -> (&[PureRecord], &Array2) { (&self.pure_records, &self.binary_records) } } @@ -207,7 +206,7 @@ impl UVParameters { /// Parameters for a single substance with molar weight one and no (default) ideal gas contributions. pub fn new_simple(rep: f64, att: f64, sigma: f64, epsilon_k: f64) -> Self { let model_record = UVRecord::new(rep, att, sigma, epsilon_k); - let pure_record = PureRecord::new(Identifier::default(), 1.0, model_record, None); + let pure_record = PureRecord::new(Identifier::default(), 1.0, model_record); Self::new_pure(pure_record) } @@ -256,7 +255,7 @@ pub mod utils { pub fn test_parameters(rep: f64, att: f64, sigma: f64, epsilon: f64) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep, att, sigma, epsilon); - let pr = PureRecord::new(identifier, 1.0, model_record, None); + let pr = PureRecord::new(identifier, 1.0, model_record); UVParameters::new_pure(pr) } @@ -268,11 +267,11 @@ pub mod utils { ) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep[0], att[0], sigma[0], epsilon[0]); - let pr1 = PureRecord::new(identifier, 1.0, model_record, None); + let pr1 = PureRecord::new(identifier, 1.0, model_record); // let identifier2 = Identifier::new(Some("1"), None, None, None, None, None); let model_record2 = UVRecord::new(rep[1], att[1], sigma[1], epsilon[1]); - let pr2 = PureRecord::new(identifier2, 1.0, model_record2, None); + let pr2 = PureRecord::new(identifier2, 1.0, model_record2); let pure_records = vec![pr1, pr2]; UVParameters::new_binary(pure_records, None) } @@ -280,7 +279,7 @@ pub mod utils { pub fn methane_parameters(rep: f64, att: f64) -> UVParameters { let identifier = Identifier::new(Some("1"), None, None, None, None, None); let model_record = UVRecord::new(rep, att, 3.7039, 150.03); - let pr = PureRecord::new(identifier, 1.0, model_record, None); + let pr = PureRecord::new(identifier, 1.0, model_record); UVParameters::new_pure(pr) } } diff --git a/src/uvtheory/python.rs b/src/uvtheory/python.rs index fa235b6a5..04a4425e0 100644 --- a/src/uvtheory/python.rs +++ b/src/uvtheory/python.rs @@ -94,7 +94,7 @@ impl PyUVParameters { None, ); let model_record = UVRecord::new(rep[i], att[i], sigma[i], epsilon_k[i]); - PureRecord::new(identifier, 1.0, model_record, None) + PureRecord::new(identifier, 1.0, model_record) }) .collect(); let binary = Array2::from_shape_fn((n, n), |(_, _)| UVBinaryRecord { k_ij: 0.0 }); @@ -130,7 +130,7 @@ impl PyUVParameters { } } -impl_pure_record!(UVRecord, PyUVRecord, NoRecord, PyNoRecord); +impl_pure_record!(UVRecord, PyUVRecord); impl_parameter!(UVParameters, PyUVParameters); #[pymodule] diff --git a/tests/pcsaft/dft.rs b/tests/pcsaft/dft.rs index 066f874ff..d993011a0 100644 --- a/tests/pcsaft/dft.rs +++ b/tests/pcsaft/dft.rs @@ -3,6 +3,7 @@ use approx::assert_relative_eq; use feos::hard_sphere::FMTVersion; use feos::pcsaft::{PcSaft, PcSaftFunctional, PcSaftParameters}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; use feos_core::{Contributions, PhaseEquilibrium, State, Verbosity}; use feos_dft::interface::PlanarInterface; @@ -329,12 +330,17 @@ fn test_entropy_bulk_values() -> Result<(), Box> { None, IdentifierOption::Name, )?; - let func = Arc::new(PcSaftFunctional::new(Arc::new(params))); + let joback_params = JobackParameters::from_json( + vec!["water_np"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name + )?; + let joback = Joback::new(Arc::new(joback_params)); + let func = Arc::new(PcSaftFunctional::new(Arc::new(params)).ideal_gas(joback)); let vle = PhaseEquilibrium::pure(&func, 350.0 * KELVIN, None, Default::default())?; let profile = PlanarInterface::from_pdgt(&vle, 2048, false)?.solve(None)?; - let s_res = profile - .profile - .entropy_density(Contributions::ResidualNvt)?; + let s_res = profile.profile.entropy_density(Contributions::Residual)?; let s_tot = profile.profile.entropy_density(Contributions::Total)?; println!( "Density:\n{}", @@ -348,8 +354,8 @@ fn test_entropy_bulk_values() -> Result<(), Box> { println!("\nResidual:\n{}", s_res); println!( "liquid: {}, vapor: {}", - profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, - profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume + profile.vle.liquid().entropy(Contributions::Residual) / profile.vle.liquid().volume, + profile.vle.vapor().entropy(Contributions::Residual) / profile.vle.vapor().volume ); println!("\nTotal:\n{}", s_tot); println!( @@ -359,12 +365,12 @@ fn test_entropy_bulk_values() -> Result<(), Box> { ); assert_relative_eq!( s_res.get(0), - profile.vle.liquid().entropy(Contributions::ResidualNvt) / profile.vle.liquid().volume, + profile.vle.liquid().entropy(Contributions::Residual) / profile.vle.liquid().volume, max_relative = 1e-8, ); assert_relative_eq!( s_res.get(2047), - profile.vle.vapor().entropy(Contributions::ResidualNvt) / profile.vle.vapor().volume, + profile.vle.vapor().entropy(Contributions::Residual) / profile.vle.vapor().volume, max_relative = 1e-8, ); assert_relative_eq!( diff --git a/tests/pcsaft/properties.rs b/tests/pcsaft/properties.rs index f27c33712..70b527065 100644 --- a/tests/pcsaft/properties.rs +++ b/tests/pcsaft/properties.rs @@ -1,7 +1,7 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::{IdentifierOption, Parameter}; -use feos_core::{EquationOfState, StateBuilder}; +use feos_core::{Residual, StateBuilder}; use ndarray::*; use quantity::si::*; use std::error::Error; diff --git a/tests/pcsaft/state_creation_mixture.rs b/tests/pcsaft/state_creation_mixture.rs index 8b40d8cb6..5497d4b6b 100644 --- a/tests/pcsaft/state_creation_mixture.rs +++ b/tests/pcsaft/state_creation_mixture.rs @@ -1,35 +1,47 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; -use feos_core::{Contributions, StateBuilder}; +use feos_core::{Contributions, EquationOfState, StateBuilder}; use ndarray::prelude::*; use ndarray::Zip; use quantity::si::*; use std::error::Error; use std::sync::Arc; -fn propane_butane_parameters() -> Result, ParameterError> { - Ok(Arc::new(PcSaftParameters::from_json( +fn propane_butane_parameters( +) -> Result<(Arc, Arc), ParameterError> { + let saft = Arc::new(PcSaftParameters::from_json( vec!["propane", "butane"], "tests/pcsaft/test_parameters.json", None, IdentifierOption::Name, - )?)) + )?); + let joback = Arc::new(JobackParameters::from_json( + vec!["propane", "butane"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name, + )?); + Ok((saft, joback)) } #[test] fn pressure_entropy_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let (saft_params, joback_params) = propane_butane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = BAR; let temperature = 300.0 * KELVIN; let x = arr1(&[0.3, 0.7]); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .temperature(temperature) .pressure(pressure) .molefracs(&x) .build()?; let molar_entropy = state.molar_entropy(Contributions::Total); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .molar_entropy(molar_entropy) .molefracs(&x) @@ -50,7 +62,7 @@ fn pressure_entropy_molefracs() -> Result<(), Box> { #[test] fn volume_temperature_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let volume = 1.5e-3 * METER.powi(3); let moles = MOL; @@ -67,7 +79,7 @@ fn volume_temperature_molefracs() -> Result<(), Box> { #[test] fn temperature_partial_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let x = arr1(&[0.3, 0.7]); let partial_density = x.clone() * MOL / METER.powi(3); @@ -86,7 +98,7 @@ fn temperature_partial_density() -> Result<(), Box> { #[test] fn temperature_density_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let x = arr1(&[0.3, 0.7]); let density = MOL / METER.powi(3); @@ -104,7 +116,7 @@ fn temperature_density_molefracs() -> Result<(), Box> { #[test] fn temperature_pressure_molefracs() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_butane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_butane_parameters()?.0)); let temperature = 300.0 * KELVIN; let pressure = BAR; let x = arr1(&[0.3, 0.7]); diff --git a/tests/pcsaft/state_creation_pure.rs b/tests/pcsaft/state_creation_pure.rs index 2299d95cf..ea7dcafc7 100644 --- a/tests/pcsaft/state_creation_pure.rs +++ b/tests/pcsaft/state_creation_pure.rs @@ -1,25 +1,34 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; +use feos_core::joback::{Joback, JobackParameters}; use feos_core::parameter::{IdentifierOption, Parameter, ParameterError}; use feos_core::{ - Contributions, DensityInitialization, EquationOfState, PhaseEquilibrium, State, StateBuilder, + Contributions, DensityInitialization, EquationOfState, IdealGas, PhaseEquilibrium, Residual, + State, StateBuilder, }; use quantity::si::*; use std::error::Error; use std::sync::Arc; -fn propane_parameters() -> Result, ParameterError> { - Ok(Arc::new(PcSaftParameters::from_json( +fn propane_parameters() -> Result<(Arc, Arc), ParameterError> { + let saft = Arc::new(PcSaftParameters::from_json( vec!["propane"], "tests/pcsaft/test_parameters.json", None, IdentifierOption::Name, - )?)) + )?); + let joback = Arc::new(JobackParameters::from_json( + vec!["propane"], + "tests/pcsaft/test_parameters_joback.json", + None, + IdentifierOption::Name, + )?); + Ok((saft, joback)) } #[test] fn temperature_volume() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let volume = 1.5e-3 * METER.powi(3); let moles = MOL; @@ -34,7 +43,7 @@ fn temperature_volume() -> Result<(), Box> { #[test] fn temperature_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let density = MOL / METER.powi(3); let state = StateBuilder::new(&saft) @@ -47,7 +56,7 @@ fn temperature_density() -> Result<(), Box> { #[test] fn temperature_total_moles_volume() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let total_moles = MOL; let volume = METER.powi(3); @@ -63,7 +72,7 @@ fn temperature_total_moles_volume() -> Result<(), Box> { #[test] fn temperature_total_moles_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let temperature = 300.0 * KELVIN; let total_moles = MOL; let density = MOL / METER.powi(3); @@ -82,7 +91,7 @@ fn temperature_total_moles_density() -> Result<(), Box> { #[test] fn pressure_temperature() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -99,7 +108,7 @@ fn pressure_temperature() -> Result<(), Box> { #[test] fn pressure_temperature_phase() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -117,7 +126,7 @@ fn pressure_temperature_phase() -> Result<(), Box> { #[test] fn pressure_temperature_initial_density() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let saft = Arc::new(PcSaft::new(propane_parameters()?.0)); let pressure = BAR; let temperature = 300.0 * KELVIN; let state = StateBuilder::new(&saft) @@ -135,10 +144,13 @@ fn pressure_temperature_initial_density() -> Result<(), Box> { #[test] fn pressure_enthalpy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_enthalpy = 2000.0 * JOULE / MOL; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .molar_enthalpy(molar_enthalpy) .vapor() @@ -154,7 +166,7 @@ fn pressure_enthalpy_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .volume(state.volume) .temperature(state.temperature) .moles(&state.moles) @@ -174,17 +186,20 @@ fn pressure_enthalpy_vapor() -> Result<(), Box> { #[test] fn density_internal_energy() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 5.0 * BAR; let temperature = 315.0 * KELVIN; let total_moles = 2.5 * MOL; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .temperature(temperature) .total_moles(total_moles) .build()?; let molar_internal_energy = state.molar_internal_energy(Contributions::Total); - let state_nvu = StateBuilder::new(&saft) + let state_nvu = StateBuilder::new(&eos) .volume(state.volume) .molar_internal_energy(molar_internal_energy) .total_moles(total_moles) @@ -201,11 +216,14 @@ fn density_internal_energy() -> Result<(), Box> { #[test] fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_enthalpy = 2000.0 * JOULE / MOL; let total_moles = 2.5 * MOL; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .molar_enthalpy(molar_enthalpy) .total_moles(total_moles) @@ -222,7 +240,7 @@ fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .volume(state.volume) .temperature(state.temperature) .total_moles(state.total_moles) @@ -242,10 +260,13 @@ fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { #[test] fn pressure_entropy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 0.3 * BAR; let molar_entropy = -2.0 * JOULE / MOL / KELVIN; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .molar_entropy(molar_entropy) .vapor() @@ -261,7 +282,7 @@ fn pressure_entropy_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .volume(state.volume) .temperature(state.temperature) .moles(&state.moles) @@ -281,18 +302,21 @@ fn pressure_entropy_vapor() -> Result<(), Box> { #[test] fn temperature_entropy_vapor() -> Result<(), Box> { - let saft = Arc::new(PcSaft::new(propane_parameters()?)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let pressure = 3.0 * BAR; let temperature = 315.15 * KELVIN; let total_moles = 3.0 * MOL; - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .temperature(temperature) .pressure(pressure) .total_moles(total_moles) .build()?; let s = State::new_nts( - &saft, + &eos, temperature, state.molar_entropy(Contributions::Total), &state.moles, @@ -307,7 +331,7 @@ fn temperature_entropy_vapor() -> Result<(), Box> { Ok(()) } -fn assert_multiple_states( +fn assert_multiple_states( states: &[(&State, &str)], pressure: SINumber, enthalpy: SINumber, @@ -338,13 +362,15 @@ fn assert_multiple_states( #[test] fn test_consistency() -> Result<(), Box> { - let p = propane_parameters()?; - let saft = Arc::new(PcSaft::new(p)); + let (saft_params, joback_params) = propane_parameters()?; + let saft = Arc::new(PcSaft::new(saft_params)); + let joback = Joback::new(joback_params); + let eos = Arc::new(EquationOfState::new(Arc::new(joback), saft)); let temperatures = [350.0 * KELVIN, 400.0 * KELVIN, 450.0 * KELVIN]; let pressures = [1.0 * BAR, 2.0 * BAR, 3.0 * BAR]; for (&temperature, &pressure) in temperatures.iter().zip(pressures.iter()) { - let state = StateBuilder::new(&saft) + let state = StateBuilder::new(&eos) .pressure(pressure) .temperature(temperature) .build()?; @@ -361,21 +387,21 @@ fn test_consistency() -> Result<(), Box> { let molar_entropy = state.molar_entropy(Contributions::Total); let density = state.density; - let state_tv = StateBuilder::new(&saft) + let state_tv = StateBuilder::new(&eos) .temperature(temperature) .density(density) .build()?; - let vle = PhaseEquilibrium::pure(&saft, temperature, None, Default::default()); + let vle = PhaseEquilibrium::pure(&eos, temperature, None, Default::default()); let builder = if let Ok(ps) = vle { let p_sat = ps.liquid().pressure(Contributions::Total); if pressure > p_sat { - StateBuilder::new(&saft).liquid() + StateBuilder::new(&eos).liquid() } else { - StateBuilder::new(&saft).vapor() + StateBuilder::new(&eos).vapor() } } else { - StateBuilder::new(&saft).vapor() + StateBuilder::new(&eos).vapor() }; let state_ts = builder diff --git a/tests/pcsaft/test_parameters_joback.json b/tests/pcsaft/test_parameters_joback.json new file mode 100644 index 000000000..afb9c81f4 --- /dev/null +++ b/tests/pcsaft/test_parameters_joback.json @@ -0,0 +1,135 @@ +[ + { + "identifier": { + "cas": "74-98-6", + "name": "propane", + "iupac_name": "propane", + "smiles": "CCC", + "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3", + "formula": "C3H8" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 44.0962, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH3" + ] + } + }, + { + "identifier": { + "cas": "106-97-8", + "name": "butane", + "iupac_name": "butane", + "smiles": "CCCC", + "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3", + "formula": "C4H10" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 58.123, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH2", + "CH3" + ] + } + }, + { + "identifier": { + "cas": "74-82-8", + "name": "methane", + "iupac_name": "methane", + "smiles": "C", + "inchi": "InChI=1/CH4/h1H4", + "formula": "CH4" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 16.0426 + }, + { + "identifier": { + "cas": "124-38-9", + "name": "carbon-dioxide", + "iupac_name": "carbon dioxide", + "smiles": "O=C=O", + "inchi": "InChI=1/CO2/c2-1-3", + "formula": "CO2" + }, + "molarweight": 44.0098, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + } + }, + { + "identifier": { + "cas": "7732-18-5", + "name": "water_np", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "molarweight": 18.0152 + }, + { + "identifier": { + "cas": "110-54-3", + "name": "hexane", + "iupac_name": "hexane", + "smiles": "CCCCCC", + "inchi": "InChI=1/C6H14/c1-3-5-6-4-2/h3-6H2,1-2H3", + "formula": "C6H14" + }, + "model_record": { + "a": 1.0, + "b": 1e-2, + "c": 1e-4, + "d": 1e-6, + "e": 1e-8 + }, + "chemical_record": { + "segments": [ + "CH3", + "CH2", + "CH2", + "CH2", + "CH2", + "CH3" + ] + }, + "molarweight": 86.177 + } +] \ No newline at end of file